@dforge-core/dforge-mcp 0.1.13 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -464,14 +464,49 @@ In `ui/actions.json`:
464
464
  | `entityCode` | Yes | Which entity this action applies to |
465
465
  | `executionMode` | Yes | `"single"` (default) or `"each"` — runs per record, DSL uses `[field]`; or `"batch"` — runs once, DSL uses `for x in records { ... }` with `__records` |
466
466
  | `script` | Yes | DSL script name (without path or extension — matches filename in `logic/actions/`) |
467
- | `isTransacted` | Recommended | `true` = all changes roll back on error; `false` = partial commits possible |
467
+ | `isTransacted` | Recommended | `true` = the first record that fails aborts the run; `false` = the failure is reported and the loop moves to the next record. It does **not** open a transaction — see *Don't assume your action is atomic* below |
468
468
  | `orderNum` | Recommended | Display order in the action menu |
469
469
  | `isAsync` | Optional | `true` = runs in background via Hangfire, user gets completion notification |
470
470
 
471
+ ## Don't assume your action is atomic
472
+
473
+ An action is **not** reliably one transaction, and `isTransacted: true` does not
474
+ make it one — it only decides whether the first failing record aborts the run.
475
+ Whether the writes made before a failure survive depends on the execution path:
476
+
477
+ | path | mode | writes before the failure |
478
+ |---|---|---|
479
+ | Sync execute, entity **with** `auditHistory` | `single` | rolled back |
480
+ | Sync execute, entity **with** `auditHistory` | `batch` | **stay committed** |
481
+ | Sync execute, entity without it | either | **stay committed** |
482
+ | Scheduled job | — | rolled back |
483
+ | Background (`isAsync: true`) action | either | **stay committed** |
484
+
485
+ Both "rolled back" rows are a side effect of the audit machinery opening a
486
+ transaction the action then inherits — and in `batch` mode it opens only after the
487
+ script has finished, so it doesn't cover the script at all. Adding or removing
488
+ `auditHistory`, or switching between `single` and `batch`, changes whether a failed
489
+ action leaves partial writes behind, with no change to the action itself.
490
+
491
+ **This is a platform defect, not a contract** — actions are intended to be atomic,
492
+ and this table is what happens until that lands. So don't design around either
493
+ outcome, and don't reach for `auditHistory` as an atomicity switch.
494
+
495
+ **Write actions that don't depend on it:** do all validation up front and call
496
+ `error()` before any `insert()` / `update()`, so there is nothing to unwind either
497
+ way.
498
+
471
499
  ## SQL in query() — important notes
472
500
 
473
501
  `query()` is the **escape hatch** — reach for it only when the structured built-ins can't express the read/write (aggregates, multi-hop joins, CTEs, `FOR UPDATE`). For row-shaped work prefer `select()` / `insert()` / `update()` / `delete()`: they validate entity codes against metadata, coerce values per column, and (for `insert`/`update`) maintain auto-fill and audit columns.
474
502
 
503
+ > **`query()` runs on a pooled, shared connection.** Once your statement — or the
504
+ > surrounding transaction, where there is one — finishes, that connection is
505
+ > handed to another request. So never leave state behind on it: use `SET LOCAL`
506
+ > rather than `SET`, `pg_advisory_xact_lock()` rather than `pg_advisory_lock()`,
507
+ > and `ON COMMIT DROP` on any temp table. Only `query()` can do any of this — the
508
+ > structured built-ins emit plain DML — and nothing checks it statically.
509
+
475
510
  1. **Table names are schema-qualified** within the module: `crm.contact`, `wms.stock_movement`, `fin.invoice`. Use the module code as the schema prefix. (Module codes with a hyphen, e.g. `crm-fin`, cannot be schema-qualified in raw SQL — another reason to use the structured built-ins, which accept the qualified `module.entity` code.)
476
511
  2. **Use `@param` placeholders** for values — NEVER concatenate user input into SQL strings. The engine auto-parameterizes.
477
512
  3. **SELECT, INSERT, UPDATE, DELETE all work** via `query()`. For reads, it returns row arrays. For writes, it executes the statement.
@@ -0,0 +1,309 @@
1
+ ---
2
+ name: dforge-module-build
3
+ description: Phases 1-5 of dForge module authoring — scaffold the module, then build entities and fields, relations, roll-ups and status columns, actions/triggers/jobs/webhooks, data views and menus, reports, settings, translations, seed data, and the security roles and rights matrix. Use when dforge_module_plan reports a currentPhase of 1-5, or when adding to / fixing an existing module's entities, views, or security. Not for the design documents (dforge-module-design) or for packing and installing (dforge-module-ship).
4
+ ---
5
+
6
+ # dForge module — Build (Phases 1–5)
7
+
8
+ Phase 0 is done and `readyToScaffold: true`. You're now producing the actual
9
+ module artifacts.
10
+
11
+ **Before each phase:** run `dforge_module_inspect` and parse
12
+ `files["_inspect.json"]` — never plan a patch against what you assume is there.
13
+ **After each phase:** record it with
14
+ `dforge_module_plan({ action: "complete_phase", moduleDir, phase: "N", note: "..." })`.
15
+
16
+ ## Loading policy — one reference per step, at the step
17
+
18
+ **Load nothing up front.** Pull the row you need when you need it, by URI.
19
+ Re-load on every backtrack into that element type. The example files are
20
+ mandatory structure validators — never work from memory for schema shapes,
21
+ flags, or column patterns.
22
+
23
+ | When you need to… | Load (primary reference + example) | Also load |
24
+ |---|---|---|
25
+ | Add any field | `dforge://reference/field-types`, `dforge://reference/flags`, `dforge://example/entities/todo_item.json` | — |
26
+ | Add a relation / Set column | `dforge://reference/column-types`, `dforge://example/entities/todo_item.json` | — |
27
+ | Reuse a type/enum across columns (a `domain`) | `dforge://reference/column-domains` | `dforge://schema/domains` |
28
+ | Add a formula column | `dforge://reference/formulas` | — |
29
+ | Add a trait | `dforge://reference/traits` | — |
30
+ | Add a number sequence | `dforge://reference/number-sequences` | — |
31
+ | Add a grid / list view | `dforge://reference/data-views`, `dforge://example/ui/data_views.json` | — |
32
+ | Add a matrix (pivot) view | `dforge://reference/data-views` (§Matrix), `dforge://example/matrix-budget/ui/data_views.json`, `dforge://example/matrix-budget/entities/budget_line.json` | — |
33
+ | Add kanban / calendar / tree-grid / master-detail | `dforge://reference/data-views` | `dforge://schema/data-views` |
34
+ | Add a menu | `dforge://reference/menus`, `dforge://example/ui/menus.json` | — |
35
+ | Add an action | `dforge://reference/action-dsl`, `dforge://example/logic/actions/mark_done.dsl`, `dforge://example/ui/actions.json` | `dforge://docs/dsl` |
36
+ | Add a trigger | — | `dforge://schema/triggers`; condition syntax in `dforge://docs/dsl` |
37
+ | Add a scheduled job | `dforge://reference/jobs` | `dforge://schema/jobs` |
38
+ | Add a webhook | — | `dforge://schema/webhooks` |
39
+ | Add filters (views, folders, reports) | `dforge://reference/filters` | — |
40
+ | Add roles or security folders | `dforge://reference/security`, `dforge://reference/filters`, `dforge://example/security/roles.json` | — |
41
+ | Add a report | `dforge://reference/reports` | `dforge://schema/reports` |
42
+ | Add a print template | `dforge://reference/print-templates` | — |
43
+ | Wire OCR / document extraction | `dforge://reference/document-extraction` | `dforge://docs/dsl` |
44
+ | Add / localize translations | `dforge://reference/translations` | — |
45
+ | Add module settings | `dforge://reference/settings` | — |
46
+ | Add saved queries | `dforge://reference/queries` | — |
47
+ | Import from DBML/SQL | `dforge://reference/schema-import` | — |
48
+ | Import from a spreadsheet | `dforge://reference/excel-import` | — |
49
+ | Migrate a legacy database | `dforge://reference/data-migration` | — |
50
+
51
+ ## Core rules — the always-on cheat sheet
52
+
53
+ Enough to author inline; load the linked reference for the full detail.
54
+
55
+ - **Naming.** `code`, entity `dbObject`, column keys: all `snake_case`,
56
+ case-sensitive, entities singular (`opportunity_line`). `code` = DB schema name.
57
+ - **A relation is TWO columns + a references entry.** Use
58
+ `dforge_entity_reference_add` — it emits all three parts. → `column-types`
59
+ - **Flags** are letters from `V I E M H` only (never `U`/`S`/`P`): `VEM`
60
+ required+visible, `VE` optional+visible, `V` read-only, `EM` hidden FK, `I`
61
+ trait-provided. → `flags`
62
+ - **`fieldTypeCd` = UI control; `dbDatatype` = SQL type.** Omit `dbDatatype` on
63
+ plain data columns — it's derived. Set it only for a hidden FK or to override
64
+ size/precision. Common fixes: `number` not `integer`/`float`; `phone` not
65
+ `phoneNumber`; `date` not `datePicker`; `timestamptz` not `datetime`; `bool`
66
+ not `boolean`; `varchar`/`text` not `string`. → `field-types`
67
+ - **Formula columns** (`columnType: "F"`): `baseDatatypeCd` required, no
68
+ `dbDatatype`, `flags: "V"`. → `formulas`
69
+ - **Roll-up totals** over a child set are Generated (`G`) columns with
70
+ `SUM([set].[field])` — **never** Formula (`F`), whose set-aggregates silently
71
+ render empty — and they aggregate only a **physical** child column. Use
72
+ `dforge_entity_rollup_add`. → `column-types`
73
+ - **Column defaults** are set via `formula` (`"TODAY()"`, `"'draft'"`), a
74
+ `numberSequence`, or DSL. Entity fields have **no** `defaultValue` key —
75
+ that's settings-only. → `field-types`
76
+ - **Traits:** default `["identity", "audit"]`. `audit-full` adds required
77
+ `created_by`/`last_updated_by` with no default — a seeded `audit-full` entity
78
+ must set both to `0` in every record, or use plain `audit`. → `traits`
79
+ - **`toString`** on every entity, `{column}` braces: `"{first_name} {last_name}"`.
80
+ - **Data views:** `dataSources` array at root — never a root-level `entityCode` +
81
+ `columns`. Sort is `"order": ["-col", "col"]`. → `data-views`
82
+ - **Menus:** leaf items use `dataViewCode` (not `viewCode`); section nodes omit
83
+ `itemType`; icons are Bootstrap names **without** `bi-`. Use
84
+ `dforge_menu_add`. → `menus`
85
+ - **Rights keys:** same-module entity bare (`product`); cross-module entity
86
+ dotted (`fin.invoice`); actions/reports/folders take a **colon** —
87
+ `action:approve`, `report:summary`, `folder:east` (never a dot). Deny by
88
+ omitting the key, never `""`. `roles.json` carries `description` + `rights`
89
+ only — no `label`. → `security`
90
+ - **Action `script`** in `ui/actions.json` is a bare filename (no path, no
91
+ `.dsl`).
92
+ - **Action DSL dates:** inside `execute:` use lowercase `now()`. `TODAY()`/`NOW()`
93
+ are formula-only and undefined in `execute:`. → `action-dsl`
94
+ - **SQL placeholders** are `@paramName`, not `:paramName`.
95
+ - **Manifest:** non-English locales go in `supportedLocales` (`ll-CC`, never
96
+ `en`/`en-US`); there is no `translations` manifest key. `security` has both
97
+ `roles` and `folders`. → `manifest`
98
+ - **Seed data:** explicit numeric PKs under `{entity}_id`; parents before
99
+ children via the `NN-` prefix. Use `dforge_seed_add`.
100
+
101
+ ## Phase 1 — Domain (required)
102
+
103
+ > **Fast on-ramp.** If the user already has the model in **DBML** → use
104
+ > `dforge_dbml_import`. In a **spreadsheet** → load
105
+ > `dforge://reference/excel-import`; a binary `.xlsx` must be decoded first with
106
+ > `dforge://script/xlsx-to-model` (`python3 tmp.py file.xlsx` → `{sheets:[…]}`),
107
+ > then turned into a table-spec for `dforge_module_import`. A `.csv` is plain
108
+ > text — read it directly. Both paths ADD entities; show the proposed table-spec
109
+ > for sign-off first, then run `dforge_module_validate` and refine the generated
110
+ > grids.
111
+
112
+ **This phase's first deliverable, before any tool call, is the proposed entity
113
+ inventory.** The user needs to see "the module will have these N things in it"
114
+ before files exist — entities are the spine everything else references.
115
+
116
+ ```
117
+ Proposed entities (N):
118
+ - <entity_code> — <one-line purpose, tied to a verb from intake>
119
+ ```
120
+
121
+ Loop with the user until they explicitly approve.
122
+
123
+ **Pre-scaffold consistency checks** against `docs/DESIGN.md` — if any fail,
124
+ surface it and go back to Phase 0c; don't silently adjust the design:
125
+
126
+ 1. Every FK in the relationship map has a corresponding field on the child.
127
+ 2. Every action's `canExecute` guard references a status value that exists.
128
+ 3. Every seed record's FK points at a parent that is also seeded.
129
+ 4. Every formula uses only fields on the same entity or exactly one FK hop away.
130
+ 5. Every set aggregate is a Generated column over a physical child column.
131
+ 6. Every seeded `audit-full` entity sets `created_by`/`last_updated_by: 0`.
132
+
133
+ Then: `dforge_module_create` → per-entity loop.
134
+
135
+ **Per-entity loop.** For each entity, propose fields + traits + relations, get
136
+ approval, then patch. Reach for the **composite tools first**:
137
+
138
+ - relation → `dforge_entity_reference_add`
139
+ - roll-up total → `dforge_entity_rollup_add`
140
+ - status column → `dforge_entity_status_add`
141
+ - plain scalar → `dforge_entity_field_add`
142
+
143
+ **Field batching rule** — the only Phase 1 exception to one-at-a-time. A field
144
+ is batchable only if ALL of: scalar primitive (string/integer/decimal/boolean/
145
+ date), no FK or reference, no formula, and unambiguous nullability. Everything
146
+ else goes one at a time. This never justifies batching entities, views, roles,
147
+ actions, or reports.
148
+
149
+ **Extension entities last.** `extends: "module.entity"`, `toString: null`,
150
+ dotted manifest key. Snapshot the base entity's fields with
151
+ `dforge_module_inspect` on the dependency dir when it's available locally;
152
+ otherwise document the known base fields and note that drift tracking is the
153
+ user's responsibility.
154
+
155
+ **Exit:** every entity has PK + audit traits + at least one user-visible field;
156
+ FK references resolve; the manifest's `entities` map reflects reality.
157
+
158
+ ## Phase 2 — Behavior (all optional)
159
+
160
+ Four kinds of behavior, all individually skippable. **Do not fabricate behavior
161
+ to fill a sub-step** — a pure CRUD module skips Phase 2 entirely (record it with
162
+ `skipped: true`).
163
+
164
+ | Sub-step | Fires when | Tool |
165
+ |---|---|---|
166
+ | 2a Actions | user clicks a button | `dforge_action_add` |
167
+ | 2b Triggers | a DB event happens | `dforge_trigger_add` |
168
+ | 2c Jobs | a cron timer fires | `dforge_job_add` |
169
+ | 2d Webhooks | a DB event → outbound POST | `dforge_webhook_add` |
170
+
171
+ **2a Actions.** Load all four resources in the action row — the wrong-field-
172
+ access / wrong-batch-flag / wrong-`ui/actions.json`-property mistakes only
173
+ surface when you cross-check them. Draft the DSL, run
174
+ **`dforge_action_check({ dslBody, executionMode })`** to catch install failures
175
+ before committing, then `dforge_action_add`. One action per turn.
176
+
177
+ **2b Triggers** reference actions that must already exist. `async: true` runs
178
+ after the transaction commits (use for anything slow); `async: false` runs
179
+ in-transaction, so a failure rolls back the original change.
180
+
181
+ **2c Jobs** run as the system user with **no current record** — the action must
182
+ not use `[field]` syntax. Wrap a record-context action in a thin job action that
183
+ uses `select()`/`query()` first. `timeout` is required (≤ 3600s); over 300s you
184
+ must set `jobClass: 'long_running'`. `dforge_module_validate` flags violations.
185
+
186
+ **2d Webhooks.** For authenticated endpoints, put credentials behind
187
+ `getSecret()` and reference them as `"Authorization": "$secret:<secret_cd>"`.
188
+
189
+ ## Phase 3 — Views + menus (3a required), reports (optional)
190
+
191
+ **3a. Default grids first.** Every entity gets a `viewType: "grid"` view with
192
+ `dataSources: [{ entityCode, columns: [...] }]`. The scaffolder already wrote one
193
+ per entity, so you'll often `view_modify` rather than `view_add`.
194
+
195
+ View codes are semantic — the entity name (`feedback_item`), a plural
196
+ (`invoices`), or descriptive (`invoices_kanban`). **Never use the literal code
197
+ `default`**; `ui/folders.json`'s `viewName: "default"` is a fallback alias the
198
+ platform resolves to the entity's first declared view.
199
+
200
+ Then wire menus with `dforge_menu_add` — a section node per group, leaves
201
+ pointing at the views.
202
+
203
+ **Do not propose any specialized view until every entity has its grid.**
204
+
205
+ **3b. Specialized views** only when an objective trigger fires:
206
+
207
+ - the user explicitly asked for the visualization;
208
+ - a status/stage/kind field with **3+ discrete values** → kanban;
209
+ - a required date/time field for scheduling → calendar;
210
+ - a self-referencing FK → tree-grid;
211
+ - a 1:N child with `parentSetField` → list-with-levels or master-detail.
212
+
213
+ If none fire, skip specialized views for that entity.
214
+
215
+ **3c. Reports** only when aggregation/grouping isn't covered by views.
216
+
217
+ **Exit:** every entity has a grid and a menu entry; every specialized view has a
218
+ stated trigger.
219
+
220
+ ## Phase 4 — Polish (mostly optional)
221
+
222
+ - **Settings** — `dforge_setting_add` per configurable value the user asked for.
223
+ - **Translations** — run **`dforge_translation_sync`**. It generates every
224
+ required key from the module's own contents, never overwrites existing
225
+ translated text, and fills the **role labels that are completeness-enforced at
226
+ install**. Run it after any entity/view/role change, and always before Phase 6.
227
+ For a non-English locale the seeded values are English placeholders — tell the
228
+ user they still need real translation.
229
+ - **Seed data** — `dforge_seed_add`, only when the module needs reference data on
230
+ install. Parents get a lower `order` than children.
231
+
232
+ ## Phase 5 — Security (5a required)
233
+
234
+ 1. **Inspect first.** The scaffolder pre-created `<code>.admin` with `SIUDC` on
235
+ every entity declared at scaffold time. It exists — don't re-create it
236
+ (`role_add` fails on an existing code; use `role_right_set` to amend).
237
+ 2. **Derive roles from the intake's user types and verbs.** Re-read the user-type
238
+ sentences in `docs/REQUIREMENTS.md`. One role per distinct user type, named
239
+ `<code>.<user-type>`. **Forbidden:** a generic admin/contributor/viewer matrix
240
+ the user didn't ask for.
241
+ 3. **Reflect the role list back** before computing rights. Get confirmation.
242
+ 4. **Show the rights matrix as a table** (rows = entities/actions/reports,
243
+ columns = roles, cells = rights strings), each cell explained by the verb it
244
+ maps to. Get sign-off.
245
+ 5. `dforge_role_add` for new roles; `dforge_role_right_set` per grant when
246
+ amending an existing one.
247
+
248
+ Rights are **additive** — multiple roles UNION, never revoke. Entities take any
249
+ subset of `SIUDC`; actions/reports/folders take `E`.
250
+
251
+ **5b. Security folders** only if intake said data must be partitioned
252
+ (multi-warehouse, multi-region). Default: root only.
253
+
254
+ **Folder codes must be unique across the whole tree**, not just among siblings.
255
+ A folder is addressed flat and path-less everywhere outside `ui/folders.json` —
256
+ role rights say `folder:<code>`, translations key on `folders.<code>.label` — so
257
+ two folders both coded `admin` under different parents are ambiguous in the
258
+ rights matrix and collide in the translation files. `dforge_folder_add` refuses
259
+ a duplicate and `dforge_module_validate` errors on one; name them distinctly
260
+ (`north_admin`, `south_admin`).
261
+
262
+ > ⛔ **`dforge_module_pack` refuses to build** if any entity has no role granting
263
+ > `S`. Don't lean on the gate — derive real persona roles here, and grant `E` on
264
+ > the actions/reports each persona uses.
265
+
266
+ **Exit:** every entity appears in at least one role's rights with at least `S`;
267
+ every folder declared has security mapped.
268
+
269
+ ## Backtrack protocol
270
+
271
+ When a later phase exposes a problem in an earlier one, in order:
272
+
273
+ 1. **Stop.** Don't paper over it.
274
+ 2. **Name it precisely.** "Phase 3 wants a kanban grouped by `lead_status`, but
275
+ Phase 1 didn't define `lead_status` on `lead`."
276
+ 3. **Identify the target phase + decision.**
277
+ 4. **Get user sign-off**, including cascading impacts.
278
+ 5. **Apply the smallest tool that fits** — this overrides the phase labels:
279
+ `entity_field_add` over `entity_add`; `entity_field_rename` over remove+add;
280
+ `role_right_set` over `role_add`; `view_modify` over remove+add.
281
+ 6. **Re-run `dforge_module_inspect`**, fix knock-on impacts, resume.
282
+
283
+ **Multi-trigger rule:** if several phases expose gaps at once, resolve the
284
+ **earliest-phase** gap first, complete its backtrack, re-inspect, then
285
+ re-evaluate.
286
+
287
+ **Rename and delete are handled for you** — `entity_field_rename`,
288
+ `entity_field_remove`, `entity_rename`, `entity_delete` each propagate the
289
+ cascade in one call. **Always apply the response's `deletes` as well as
290
+ `files`** (or just pass `apply: true`, which guarantees it). Each tool surfaces
291
+ what it could NOT auto-fix — reports, translations, menu labels, DSL bodies,
292
+ dangling cross-entity FKs — in `warning`. **Run `dforge_module_validate` after
293
+ any rename/delete.**
294
+
295
+ After every backtrack, append to `_brief/changelog.md`:
296
+
297
+ ```markdown
298
+ ## <YYYY-MM-DD> — Phase N → Phase M backtrack
299
+ - Trigger: <what the later phase tried to do>
300
+ - Change: <what was patched>
301
+ - Affected files: <list>
302
+ ```
303
+
304
+ ## Exit
305
+
306
+ When Phases 1, 3a and 5a are done (and 2/4 done or recorded as skipped):
307
+
308
+ 1. Record each with `dforge_module_plan({ action: "complete_phase", ... })`.
309
+ 2. **Hand off:** invoke the **`dforge-module-ship`** skill for Phase 6.
@@ -0,0 +1,125 @@
1
+ ---
2
+ name: dforge-module-design
3
+ description: Phase 0 of dForge module authoring — module identity, requirements intake, the design document, and pre-scaffold validation. Use when starting a NEW dForge module, or when dforge_module_plan reports a currentPhase of 0a/0b/0c/0d. Produces CLAUDE.md, docs/REQUIREMENTS.md, docs/DESIGN.md and docs/VALIDATION.md, and ends by unlocking dforge_module_create. Do not use for building entities or views — that is dforge-module-build.
4
+ ---
5
+
6
+ # dForge module — Design (Phase 0)
7
+
8
+ You are designing a module **before any code exists**. The output is four
9
+ documents. Nothing here creates entities, views, or JSON artifacts.
10
+
11
+ Phase 0 is driven by the **`dforge_module_plan`** tool: call it, ask what it
12
+ returns, write what it tells you to write. It is the source of truth for the
13
+ question lists, the design template, the gap checks, and the gate — don't
14
+ re-derive them from memory.
15
+
16
+ > **Prerequisite:** you should have arrived here from `dforge-mcp-author` after
17
+ > `dforge_module_plan({ action: "check", moduleDir })` reported a Phase 0
18
+ > `currentPhase`. If you haven't made that call yet, make it now.
19
+
20
+ ## The loop
21
+
22
+ | Sub-phase | You do | Then call |
23
+ |---|---|---|
24
+ | **0a** Identity | Ask the 5 returned questions, ONE at a time | `write_identity` → write the returned `CLAUDE.md` to disk |
25
+ | **0b** Intake | Ask the returned questions in free-form prose; write `docs/REQUIREMENTS.md` to disk; get an explicit YES | `write_requirements { userConfirmed: true }` |
26
+ | **0c** Design | Draft `docs/DESIGN.md` from the returned `designTemplate` covering all 8 `designItems`; run the gap scan; write to disk; get an explicit YES | `write_design { userConfirmed: true }` |
27
+ | **0d** Validate | `validate` (structural) → evaluate the returned `semanticChecks` against the docs → `validate` again with `checkResults` | unlocks `readyToScaffold: true` |
28
+
29
+ After every user answer: **restate what you understood and confirm** before
30
+ moving on.
31
+
32
+ ## Document-write ordering (the one exception to preview-first)
33
+
34
+ For `REQUIREMENTS.md` and `DESIGN.md` you **write the file to disk first**, then
35
+ ask the user to review the file and reply YES — do not paste the full document
36
+ into chat. Give them a short orientation instead: section headings, counts, and
37
+ gap status ("No gaps" or a one-line list).
38
+
39
+ On a change request, **edit the file directly** with targeted edits, summarize
40
+ the change in one line, and ask again. Loop until they confirm.
41
+
42
+ `userConfirmed: true` means the user actually said yes to the written file. The
43
+ tool has no way to check that — it is on you.
44
+
45
+ ## 0b intake — guardrails the tool can't enforce
46
+
47
+ **Free-form prose only.** Ask every 0b question as a plain sentence. Do **NOT**
48
+ use `AskUserQuestion`, picker UIs, multiple-choice tabs, or any predefined-option
49
+ tool. Predefined buckets bias the answer into your taxonomy and destroy the verbs
50
+ Phase 5 needs to derive roles. Resume normal tool use in Phase 1.
51
+
52
+ Forbidden picker variants that have leaked before: _"Single role / Two roles /
53
+ Three+ roles"_; _"admin / manager / user / viewer"_.
54
+
55
+ **Capture user types as verb-form sentences, never role labels.** Write each as
56
+ `<descriptor of the person> <verb phrase>`:
57
+
58
+ ✅ Good:
59
+ ```
60
+ - Anyone in the company submits purchase requests and tracks their own.
61
+ - Department managers approve or reject pending requests for their team.
62
+ - Buyers in procurement manage suppliers, collect quotes, and issue purchase orders.
63
+ ```
64
+
65
+ ❌ Bad (role-noun headings pre-commit Phase 5 to exactly those roles):
66
+ ```
67
+ - **Requester** — submits purchase requests
68
+ - **Approver** — approves pending requests
69
+ ```
70
+
71
+ Push back on verb-less answers: "admins and users" → _"What does an admin do
72
+ that a user can't?"_
73
+
74
+ **Hard forbidden in 0b:** role codes (`<code>.admin`), role-noun bullet heads, a
75
+ rights matrix, or a "Target user roles" section. Roles are derived from entities
76
+ plus verbs in Phase 5 — and entities don't exist yet.
77
+
78
+ ### Requirements gap scan
79
+
80
+ Run before writing `REQUIREMENTS.md`. Surface each finding inline as
81
+ **"Gap:** … **Proposal:** … Confirm or change?"**
82
+
83
+ - **Approval recovery** — is a reject terminal, or can it be re-submitted?
84
+ - **Audit depth** — does the user need to record *who* changed each row, not just
85
+ when? Only then `audit-full`. And if such an entity is also seeded, every seed
86
+ record must set `created_by`/`last_updated_by: 0` (the System user) — otherwise
87
+ use plain `audit`.
88
+ - **Integration codes** — the exact `module.entity` codes for anything external.
89
+ - **Implied-but-unnamed entities.**
90
+ - **Reference-number scale** → does it need a number sequence?
91
+
92
+ ## 0c design — the gap detection pass
93
+
94
+ After drafting, run the gap detection the tool lists, and add a **Gaps &
95
+ Proposals** section for each finding. **All gaps must be resolved before 0d.**
96
+ The scan covers: FK optionality, status recovery paths, boolean-that-should-be-a-
97
+ status, set aggregation risk, deep navigation (2+ FK hops), self-referential FKs,
98
+ security coverage, and seed-data circular references.
99
+
100
+ Two design decisions to get right here, because fixing them in Phase 1 is a
101
+ backtrack:
102
+
103
+ - **A total over child rows is a Generated column**, not a Formula. Note it as
104
+ such in the design. (`dforge_entity_rollup_add` enforces it later.)
105
+ - **A field with 3+ discrete states is a status**, not a boolean — and a 3+ value
106
+ status is the objective trigger for a kanban view in Phase 3.
107
+
108
+ ## 0d validation — what it does and doesn't certify
109
+
110
+ `docs/VALIDATION.md` / `readyToScaffold: true` certifies only that the **design
111
+ documents are internally consistent**. It runs *before* scaffolding and inspects
112
+ no entity / UI / security / DSL file.
113
+
114
+ Artifact correctness is enforced by `dforge_module_validate` and by the platform
115
+ at install. **A green VALIDATION.md is not a signal that the module will
116
+ install.** Say so if the user reads it that way.
117
+
118
+ ## Exit
119
+
120
+ When `validate` returns `readyToScaffold: true`:
121
+
122
+ 1. Write the returned `CLAUDE.md`, `docs/VALIDATION.md` and `docs/phase.json`.
123
+ 2. Tell the user Phase 0 is complete and summarize the design in two lines.
124
+ 3. **Hand off:** invoke the **`dforge-module-build`** skill to scaffold and build
125
+ Phases 1–5. Do not call `dforge_module_create` from this skill.
@@ -0,0 +1,163 @@
1
+ ---
2
+ name: dforge-module-ship
3
+ description: Phase 6 of dForge module authoring — the verify-and-release stage. Runs dforge_module_validate, the pre-pack self-review checklist, the version audit, then dforge_module_pack and dforge_module_install, and drives the install-fix loop until the module installs against a real tenant. Use when dforge_module_plan reports currentPhase 6, or when the user asks to validate, pack, install, deploy, or fix a failing install of an existing dForge module.
4
+ ---
5
+
6
+ # dForge module — Ship (Phase 6)
7
+
8
+ Phase 6 is **required and non-skippable**. Its job is to find every defect
9
+ offline, then get a clean install against a real tenant.
10
+
11
+ **Preconditions:** Phases 0a–0d, 1, 3a and 5a complete. Optional phases (2, 4,
12
+ 3b/3c, 5b) don't block — explicitly skipped ones are fine.
13
+
14
+ > **The platform validator at install is the only complete validator.** Every
15
+ > step before it exists to make that round trip succeed on the first or second
16
+ > try, because it is slow and tenant-bound.
17
+
18
+ ## Step 1 — Automated validation (blocking gate)
19
+
20
+ Call **`dforge_module_validate`** on the module dir. It runs offline what
21
+ otherwise only surfaces at install:
22
+
23
+ - dangling FK / reference targets; a missing hidden-FK column
24
+ - view `dataSources`/columns pointing at unknown entities/fields; a grid over an
25
+ entity with no visible column
26
+ - menu `dataViewCode` → missing view; role rights on unknown objects
27
+ - entities with no Select grant
28
+ - **field-spec rules re-run across every field** — catching anything that entered
29
+ via `module_import` / `dbml_import` / a hand edit and never passed the
30
+ authoring schema
31
+ - `toString` templates whose braces don't resolve
32
+ - **Formula-vs-Generated set aggregates**, and Generated aggregates over a
33
+ virtual child column
34
+ - action `script` files missing from disk; triggers/jobs firing actions that
35
+ don't exist; `[field]` record-context in a job-invoked action
36
+ - **DSL static checks** on every action body
37
+ - **translation completeness** — a missing `roles.<code>.label` fails install
38
+
39
+ Read `files["_validate.json"]`. **Every `error` must be fixed** (apply the
40
+ backtrack protocol in `dforge-module-build`) before continuing. Review the
41
+ `warning`s with the user. Re-run `dforge_module_validate` until `ok: true`.
42
+
43
+ If `dforge_module_validate` returns a tool error or is unavailable, halt and
44
+ tell the user: the automated gate cannot be bypassed; resolve the tooling issue
45
+ before continuing.
46
+
47
+ Structural only — it doesn't judge intent, so still do Step 2.
48
+
49
+ ## Step 2 — Pre-pack self-review (blocking gate)
50
+
51
+ Load `dforge://reference/validation-checklist` and work every section.
52
+
53
+ **Top install-blockers — scan these first.** Each is a documented real failure
54
+ the platform validator rejects:
55
+
56
+ 1. **DSL dates** — `execute:` uses lowercase `now()`; never `TODAY()`/`NOW()`
57
+ (formula-only → `'TODAY' is not defined`). Verify with
58
+ `dforge_action_check({ moduleDir, actionCode })` per action.
59
+ 2. **Roll-ups** — a total over a child set is a Generated (`G`) column with
60
+ `SUM([set].[field])`, never a Formula (`F`), and it aggregates a **physical**
61
+ child column (`D` or same-row `G`). A virtual `F`/`R`/`S` child fails with
62
+ `column old.<field> does not exist`.
63
+ 3. **Rights keys** — actions/reports/folders take a **colon** (`action:x`);
64
+ entities are bare or cross-module-dotted; deny by omitting, never `""`.
65
+ 4. **Manifest** — no `translations` key; non-English locales in
66
+ `supportedLocales` (never `en`/`en-US`); `moduleId` a valid UUID; `version`
67
+ and `dbSchemaVersion` set; `security` has both `roles` and `folders`.
68
+ 5. **Column defaults** — via `formula` / `numberSequence` / DSL, never a
69
+ `defaultValue` key on an entity field.
70
+ 6. **Seed + traits** — a seeded `audit-full` entity sets
71
+ `created_by`/`last_updated_by: 0` on every record, or uses plain `audit`.
72
+
73
+ Remaining checklist areas: entity traits + `toString` + the FK+Reference pattern;
74
+ formula column shape; flags letters (`V I E M H` only); every entity has a
75
+ default grid and `dataSources` at root with `"order"` as a string array; menu
76
+ leaves use `dataViewCode` and bare icon names; every entity in at least one
77
+ role's rights; action `script` values are bare filenames; seed PKs numeric with
78
+ parents first; a `translations/<locale>.json` for every `supportedLocales` entry
79
+ **plus** the `en-US` base, each carrying a label for every trait-provided field
80
+ and every role.
81
+
82
+ If translations are thin, run **`dforge_translation_sync`** now rather than
83
+ hand-patching — it fills exactly the keys install requires.
84
+
85
+ ## Step 3 — Translation deferral check
86
+
87
+ Read `_brief/changelog.md`. If a translation deferral warning is present
88
+ ("Translation files for [locales] are incomplete"), **halt**: install will fail
89
+ completeness validation. Run `dforge_translation_sync`, confirm the non-English
90
+ values are really translated (the tool seeds English placeholders), and clear the
91
+ warning before proceeding.
92
+
93
+ ## Step 4 — Final inspect + version audit
94
+
95
+ Run `dforge_module_inspect`. Show a one-line summary — entity / view / action /
96
+ role / trigger / job counts. Then confirm both version strings with the user:
97
+
98
+ - **`version`** — always bump (semver) before packing.
99
+ - **`dbSchemaVersion`** — bump only if entity fields were added, removed, or
100
+ type-changed since the last install.
101
+
102
+ Get explicit confirmation on both before packing. This version confirmation
103
+ applies only to the initial pack. During the install-fix loop, re-pack without
104
+ re-asking unless the fix changes the version strings.
105
+
106
+ ## Step 5 — Pack + install
107
+
108
+ 1. `dforge_module_pack` → the `.dforge` tarball. Blocked if any entity lacks a
109
+ role granting Select; fix security coverage and re-run.
110
+ 2. `dforge_module_install` with `DFORGE_URL` / `DFORGE_TOKEN`.
111
+
112
+ ### Install-fix loop (mandatory)
113
+
114
+ 1. **Call `dforge_module_install` yourself.** Never ask the user to run the
115
+ install command for you.
116
+ 2. If `ok: false`, read the returned `output` **in full** — it's the raw
117
+ CLI/server validator output and it is the input to the next fix.
118
+ 3. If it's a module defect, **fix it yourself.** Use the table below to pick the
119
+ phase, then apply the backtrack protocol with the smallest suitable tool.
120
+ 4. Re-run Step 1 (`validate`) until `ok: true`, then re-run only the Step 2
121
+ checks for the area you touched, then pack and install again.
122
+ 5. Repeat until install succeeds, or the remaining failure is clearly
123
+ environment/tooling.
124
+
125
+ Environment/tooling failures include: 401/403 auth errors, network connectivity
126
+ refused, server-side feature not enabled for the tenant, and CLI version
127
+ incompatibility. All other errors are module defects.
128
+
129
+ After 4 failed install attempts without progress, pause the loop, summarize all
130
+ outstanding errors to the user, and ask how to proceed rather than continuing
131
+ autonomously.
132
+
133
+ | Install error pattern | Backtrack to |
134
+ |---|---|
135
+ | "unknown entity code" / "unknown view code" | Phase 1 or 3 |
136
+ | "missing translation key" | Phase 4 (`dforge_translation_sync`) |
137
+ | "FK constraint violation in seed data" | Phase 1 (seed load order) |
138
+ | "role right granted on unknown object" | Phase 5 |
139
+ | "action script not found" | Phase 2a |
140
+ | "formula compile error" | Phase 1 (field def) or Phase 2a (DSL) |
141
+ | "duplicate code" | wherever the duplicate was introduced |
142
+
143
+ **Phase 6 sign-off exception:** when install fails with a clear module defect
144
+ (schema validation, missing file/key, translation completeness, DSL compile
145
+ error, dependency contract, FK/seed/import error), do **not** ask the user for
146
+ sign-off before the corrective patch. The server has already rejected the
147
+ package — fix the referenced files, report what changed, and immediately re-run
148
+ validate → pack → install. Keep sign-off for product/design choices or genuinely
149
+ ambiguous fixes.
150
+
151
+ **If install fails on auth (401/403) or connectivity (refused):** that is
152
+ credentials/connectivity, NOT a module defect. Tell the user to verify
153
+ `DFORGE_URL` and `DFORGE_TOKEN`. **Do not backtrack** to earlier phases.
154
+
155
+ **Exit criterion:** install exits 0 against a real tenant.
156
+
157
+ ## Step 6 — Record and close out
158
+
159
+ 1. `dforge_module_plan({ action: "complete_phase", moduleDir, phase: "6", note: "installed v<x.y.z>" })`.
160
+ 2. **Ask the user**: "Delete `_brief/` (session scratch), or move it to `docs/`
161
+ as committed design rationale?" Wait for the answer — don't act unilaterally.
162
+ 3. Suggest a `git commit` summarizing the module. **Do not commit unless the user
163
+ asks.**