Route business teams and agents to a flat package helpers layout with
thin {product}.go wiring and {product}_{resource}.go files, and forbid
growing megafiles like chat.go. Keep the harness check local and opt-in.
Co-authored-by: Cursor <cursoragent@cursor.com>
215 lines
8.8 KiB
Markdown
215 lines
8.8 KiB
Markdown
# Helpers Package Structure Guide
|
||
|
||
This is the coding-structure contract for product commands under
|
||
`internal/helpers/`. Business teams and coding agents must follow it when
|
||
adding or moving CLI leaves. Behavioral contracts (stdout, errors, confirmation,
|
||
Schema) stay in [`internal/helpers/AGENTS.md`](../internal/helpers/AGENTS.md);
|
||
this document only owns file layout and split rules.
|
||
|
||
Reference implementations already in-tree:
|
||
|
||
| Pattern | Use as |
|
||
|---|---|
|
||
| [`sheet.go`](../internal/helpers/sheet.go) + `sheet_*.go` | Preferred product layout: thin root wiring, resource files |
|
||
| [`chat_media_upload.go`](../internal/helpers/chat_media_upload.go) | Incremental extract from a megafile without behavior change |
|
||
| `connect_*.go` | Concern-based split inside the same `helpers` package |
|
||
|
||
Anti-pattern to stop growing: single megafiles such as `chat.go` / `aitable.go`
|
||
(thousands of lines). New work must not enlarge them.
|
||
|
||
## 1. Package boundary
|
||
|
||
Keep product handlers in the flat package `helpers`:
|
||
|
||
```text
|
||
internal/helpers/ # package helpers (default)
|
||
register_products.go # public product registration table
|
||
{product}.go # product root: new{Product}Command()
|
||
{product}_{resource}.go # one resource / cohesive concern
|
||
{product}_{resource}_test.go # focused tests beside the file
|
||
helpers.go / interfaces.go … # cross-product shared machinery
|
||
```
|
||
|
||
Do **not** create `internal/helpers/{product}/` subpackages by default. The flat
|
||
package exists so leaves can share unexported helpers (`callMCPToolOnServer`,
|
||
flag validators, confirmation wrappers, transport adapters) without exporting a
|
||
public API surface. Split into a subpackage only when the concern is a real
|
||
library boundary with its own tests and almost no need for helpers-private
|
||
symbols—and get an explicit review for that exception.
|
||
|
||
Repository layers outside helpers stay unchanged:
|
||
|
||
| Layer | Owns |
|
||
|---|---|
|
||
| `internal/app` | Root/static wiring, plugin load |
|
||
| `internal/helpers` | Product Cobra trees and handler behavior |
|
||
| `internal/cobracmd` | Shared Cobra construction primitives |
|
||
| `internal/executor`, `internal/transport` | Invocation and transport |
|
||
| `internal/output`, `internal/errors`, `internal/safety` | Projection, failures, confirmation |
|
||
| `internal/cli` | Schema identity / Agent metadata |
|
||
|
||
Ordinary product leaves belong in helpers. Do not push product business mapping
|
||
into app, transport, or Schema generators.
|
||
|
||
## 2. File roles inside a product
|
||
|
||
### 2.1 Product root — `{product}.go`
|
||
|
||
Owns exactly one entry constructor, registered from `register_products.go`:
|
||
|
||
```go
|
||
func newChatCommand() *cobra.Command { /* wire subgroups only */ }
|
||
```
|
||
|
||
The root file should:
|
||
|
||
1. Define the product `cobra.Command` (`Use` / `Short` / `Long` / aliases).
|
||
2. Call resource factories and `AddCommand` them.
|
||
3. Register product-level aliases or hint stubs when needed.
|
||
4. Avoid large inline `RunE` bodies for leaves.
|
||
|
||
Target size: wiring-only, roughly under **400 lines** (see `sheet.go` ≈ 270).
|
||
If the root grows past that because leaves are inlined, extract a resource file.
|
||
|
||
### 2.2 Resource / concern file — `{product}_{resource}.go`
|
||
|
||
Split on the **CLI path segment or cohesive concern**, not on “one function per
|
||
file”:
|
||
|
||
| CLI path | File |
|
||
|---|---|
|
||
| `dws chat group …` | `chat_group.go` |
|
||
| `dws chat message …` | `chat_message.go` |
|
||
| `dws chat media …` | `chat_media_upload.go` (or `chat_media.go`) |
|
||
| `dws sheet filter-view …` | `sheet_filter_view.go` |
|
||
| `dws sheet dimension …` | `sheet_dimension.go` |
|
||
|
||
Each file exposes one or more unexported factories, for example:
|
||
|
||
```go
|
||
func newChatGroupCmd() *cobra.Command { … }
|
||
func newChatMessageCmd() *cobra.Command { … }
|
||
func newWorkbookCmds() []*cobra.Command { … }
|
||
```
|
||
|
||
The product root only wires those factories. Prefer keeping a leaf’s flags,
|
||
`RunE`, and nearby request-mapping helpers in the same resource file until a
|
||
helper is reused by multiple resources.
|
||
|
||
Soft size guide per resource file:
|
||
|
||
| Lines | Action |
|
||
|---|---|
|
||
| < 600 | Normal |
|
||
| 600–1000 | Prefer splitting the next cohesive subgroup before adding more |
|
||
| > 1000 | Required split before landing substantial new leaves |
|
||
|
||
“One command per file” is **not** required. Tiny sibling leaves that share
|
||
flags and mapping belong together. Split when the file holds multiple unrelated
|
||
resources or becomes hard to review.
|
||
|
||
### 2.3 Shared product helpers — `{product}_{concern}.go`
|
||
|
||
Use a concern suffix when code is shared across resources and is not itself a
|
||
command tree:
|
||
|
||
- `sheet_validate.go` — shared validation
|
||
- `chat_args.go` / similar — grant/arg builders used by several leaves
|
||
- `connect_command.go` — slash-command parsing used by the daemon
|
||
|
||
Do not park unrelated products’ utilities in these files. Cross-product
|
||
machinery belongs in non-prefixed shared files (`helpers.go`, `interfaces.go`,
|
||
output/error helpers), never copied per product.
|
||
|
||
### 2.4 Tests
|
||
|
||
- Name tests after the file or scenario: `chat_message_search_test.go`,
|
||
`sheet_filter_view_test.go`.
|
||
- Prefer exercising public product construction (`newChatCommand().Find(…)`)
|
||
for command-surface regressions.
|
||
- Pure helpers may be unit-tested directly in the same package.
|
||
- A mechanical file split without behavior change should keep existing tests
|
||
green with no assertion edits; if tests must change, the split leaked a
|
||
behavior or visibility change and needs review.
|
||
|
||
## 3. Registration and naming
|
||
|
||
1. Public products enter the CLI only through the table in
|
||
`register_products.go` (`new{Product}Command` factories). Do not invent a
|
||
second registration path for ordinary product leaves.
|
||
2. Constructor names stay unexported (`new…`) unless a deliberate test helper
|
||
requires otherwise.
|
||
3. File names are lowercase snake_case. Match the CLI resource token when
|
||
practical (`filter-view` → `filter_view`).
|
||
4. Do not hand-edit generated sync markers in `register_products.go` outside
|
||
the product-registration workflow that owns that file.
|
||
|
||
## 4. How to add a new command (business-team checklist)
|
||
|
||
1. Confirm the owning product and CLI path with `dws <product> --help`.
|
||
2. Open `{product}.go` only to wire `AddCommand`; put the leaf in
|
||
`{product}_{resource}.go`.
|
||
3. If the product is still a megafile (`chat.go`, `aitable.go`, …) and your
|
||
resource file does not exist yet, **create the resource file and move only
|
||
the subgroup you need** (or add the new leaf there and wire it from the
|
||
root). Do not append another large leaf into the megafile.
|
||
4. Keep stdout / stderr / error / confirmation contracts from
|
||
`internal/helpers/AGENTS.md`.
|
||
5. If path, flags, safety, or Agent selection change, follow
|
||
[`schema-contributor-guide.md`](schema-contributor-guide.md).
|
||
6. Add or extend the closest test; run
|
||
`go test ./internal/helpers -count=1` (or a tighter `-run`) plus the checks
|
||
selected by [`coding-agent-guide.md`](coding-agent-guide.md).
|
||
|
||
## 5. Migrating an existing megafile
|
||
|
||
Mechanical splits are welcome and preferred over “big-bang” rewrites.
|
||
|
||
Rules for a split PR:
|
||
|
||
1. **Behavior-neutral**: same command paths, flags, help text, request mapping,
|
||
confirmation, and output.
|
||
2. **Stable entrypoint**: keep `new{Product}Command()` as the registration
|
||
symbol; only its body becomes wiring.
|
||
3. **Move by resource**: extract one CLI subgroup per commit/PR when possible
|
||
(`group`, `message`, `category`, …).
|
||
4. **No drive-by cleanups** in the same PR (renames, flag redesign, Schema
|
||
edits) unless the task explicitly includes them.
|
||
5. **Stop growing the megafile**: after the first extract, new leaves for that
|
||
resource go to the new file only.
|
||
|
||
Suggested first-wave split for `chat` (illustrative, not mandatory order):
|
||
|
||
| Extract to | Contents |
|
||
|---|---|
|
||
| `chat.go` | `newChatCommand()` wiring only |
|
||
| `chat_permission.go` | `chmod`, `data-auth` |
|
||
| `chat_group.go` | `group` tree |
|
||
| `chat_message.go` | `message` tree |
|
||
| `chat_category.go` | `category` tree |
|
||
| `chat_bot.go` | `bot` tree |
|
||
| `chat_conversation.go` | top-level conversation ops (`set-top`, mute, red-point, …) |
|
||
| existing `chat_media_upload.go` | keep; optionally rename to `chat_media.go` only in a dedicated rename PR |
|
||
|
||
Apply the same pattern to `aitable`, `attendance`, `mail`, and `doc` when those
|
||
products take new work.
|
||
|
||
## 6. What this guide does not change
|
||
|
||
- Wire format, MCP method names, or Schema identity rules.
|
||
- The choice to keep `package helpers` flat.
|
||
- Runtime confirmation / dry-run policy (still owned by safety + Schema metadata).
|
||
- Permission to skip tests because a change was “only a move”—moves still need
|
||
the product’s focused tests green.
|
||
|
||
## 7. Harness check
|
||
|
||
After editing this guide or its links from `AGENTS.md` /
|
||
`internal/helpers/AGENTS.md`, run:
|
||
|
||
```bash
|
||
make coding-agent-harness
|
||
```
|
||
|
||
This check is a local, opt-in agent aid; it is not part of `make policy` or CI.
|