rcf-lite 0.28.4 → 0.29.0

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.
Files changed (168) hide show
  1. package/CHANGELOG.md +107 -1
  2. package/bin/rcf.js +4 -0
  3. package/fixtures/canary-manifest.json +9 -9
  4. package/guidance/define-draft-shapes.md +89 -0
  5. package/guidance/define-intake.md +97 -0
  6. package/guidance/define-intent.md +107 -0
  7. package/guidance/discover-prototype.md +67 -0
  8. package/guidance/discovery-prototypes.md +69 -0
  9. package/guidance/harness-template.md +34 -0
  10. package/guidance/managed/agent-instructions-block.hash +1 -1
  11. package/guidance/managed/agent-instructions-block.md +34 -0
  12. package/guidance/manifest.json +9 -2
  13. package/package.json +1 -1
  14. package/rcf/adrs/adr-4124-define-freeze-verb.json +30 -0
  15. package/rcf/adrs/adr-4126-persona-tagged-blockers-two-levels.json +40 -0
  16. package/rcf/adrs/adr-4127-readiness-tab-order-and-register.json +40 -0
  17. package/rcf/adrs/adr-4129-questions-harness-driven-loop.json +25 -0
  18. package/rcf/adrs/adr-4130-intake-harness-split-tool-scanned.json +20 -0
  19. package/rcf/adrs/adr-4132-prototypes-in-discovery-as-evidence.json +20 -0
  20. package/rcf/adrs/adr-4133-dag-dependency-depth-layout.json +40 -0
  21. package/rcf/adrs/adr-4134-index-json-route.json +35 -0
  22. package/rcf/adrs/adr-4135-readiness-po-register-and-adapter.json +35 -0
  23. package/rcf/adrs/adr-4136-embedding-host-fixture.json +35 -0
  24. package/rcf/code-nodes/cn-725.json +18 -2
  25. package/rcf/code-nodes/cn-726.json +10 -2
  26. package/rcf/code-nodes/cn-727.json +13 -4
  27. package/rcf/code-nodes/cn-728.json +18 -2
  28. package/rcf/code-nodes/cn-729.json +10 -2
  29. package/rcf/code-nodes/cn-730.json +13 -3
  30. package/rcf/code-nodes/cn-731.json +20 -0
  31. package/rcf/code-nodes/cn-732.json +14 -0
  32. package/rcf/code-nodes/cn-733.json +14 -0
  33. package/rcf/code-nodes/cn-734.json +14 -0
  34. package/rcf/code-nodes/cn-735.json +20 -0
  35. package/rcf/code-nodes/cn-736.json +14 -0
  36. package/rcf/code-nodes/cn-737.json +14 -0
  37. package/rcf/code-nodes/cn-738.json +14 -0
  38. package/rcf/code-nodes/cn-739.json +18 -0
  39. package/rcf/code-nodes/cn-740.json +19 -0
  40. package/rcf/code-nodes/cn-741.json +16 -0
  41. package/rcf/code-nodes/cn-742.json +16 -0
  42. package/rcf/code-nodes/cn-743.json +15 -0
  43. package/rcf/code-nodes/cn-744.json +22 -0
  44. package/rcf/code-nodes/cn-745.json +16 -0
  45. package/rcf/code-nodes/cn-746.json +14 -0
  46. package/rcf/code-nodes/cn-747.json +18 -0
  47. package/rcf/code-nodes/cn-748.json +16 -0
  48. package/rcf/code-nodes/cn-749.json +16 -0
  49. package/rcf/code-nodes/cn-750.json +15 -0
  50. package/rcf/code-nodes/cn-751.json +18 -0
  51. package/rcf/code-nodes/cn-752.json +14 -0
  52. package/rcf/fbs/fbs-196.json +13 -5
  53. package/rcf/fbs/fbs-198.json +42 -0
  54. package/rcf/fbs/fbs-199.json +59 -0
  55. package/rcf/fbs/fbs-200.json +46 -0
  56. package/rcf/fbs/fbs-201.json +49 -0
  57. package/rcf/fbs/fbs-202.json +110 -0
  58. package/rcf/fbs/fbs-203.json +100 -0
  59. package/rcf/requirements/req-002.json +2 -2
  60. package/rcf/requirements/req-173.json +2 -2
  61. package/rcf/requirements/req-174.json +10 -4
  62. package/rcf/requirements/req-175.json +10 -4
  63. package/rcf/requirements/req-176.json +25 -0
  64. package/rcf/requirements/req-180.json +31 -0
  65. package/rcf/requirements/req-181.json +31 -0
  66. package/rcf/requirements/req-182.json +25 -0
  67. package/rcf/requirements/req-186.json +25 -0
  68. package/rcf/requirements/req-187.json +31 -0
  69. package/rcf/requirements/req-188.json +25 -0
  70. package/rcf/tacs/tac-4121-define-ledgers.json +15 -3
  71. package/rcf/tacs/tac-4122-define-gates.json +40 -4
  72. package/rcf/tacs/tac-4123-define-readiness.json +39 -3
  73. package/rcf/tacs/tac-4124-define-freeze.json +28 -0
  74. package/rcf/tacs/tac-4127-readiness-view.json +49 -0
  75. package/rcf/tacs/tac-4128-define-guidance.json +53 -0
  76. package/rcf/tacs/tac-4129-define-questions.json +45 -0
  77. package/rcf/tacs/tac-4130-define-intake-brief.json +35 -0
  78. package/rcf/tacs/tac-4131-build-dag-view.json +39 -0
  79. package/rcf/tacs/tac-4132-id-lookup.json +42 -0
  80. package/rcf/tacs/tac-4133-readiness-po-layer.json +37 -0
  81. package/rcf/tacs/tac-4134-wespa-host-fixture.json +54 -0
  82. package/rcf/test-suites/ts-016.json +3 -3
  83. package/rcf/test-suites/ts-215.json +148 -4
  84. package/rcf/test-suites/ts-216.json +19 -3
  85. package/rcf/test-suites/ts-218.json +78 -0
  86. package/rcf/test-suites/ts-219.json +70 -0
  87. package/rcf/test-suites/ts-220.json +78 -0
  88. package/rcf/test-suites/ts-221.json +78 -0
  89. package/rcf/test-suites/ts-222.json +100 -0
  90. package/rcf/test-suites/ts-223.json +94 -0
  91. package/rcf/test-suites/ts-224.json +70 -0
  92. package/rcf/test-suites/ts-225.json +78 -0
  93. package/rcf/test-suites/ts-226.json +158 -0
  94. package/rcf/test-suites/ts-227.json +38 -0
  95. package/rcf/test-suites/ts-228.json +86 -0
  96. package/rcf/test-suites/ts-229.json +83 -0
  97. package/rcf/user-stories/us-17302.json +50 -0
  98. package/rcf/user-stories/us-17401.json +14 -2
  99. package/rcf/user-stories/us-17402.json +55 -0
  100. package/rcf/user-stories/us-17403.json +60 -0
  101. package/rcf/user-stories/us-17502.json +60 -0
  102. package/rcf/user-stories/us-17503.json +65 -0
  103. package/rcf/user-stories/us-17601.json +58 -0
  104. package/rcf/user-stories/us-18001.json +55 -0
  105. package/rcf/user-stories/us-18002.json +35 -0
  106. package/rcf/user-stories/us-18101.json +35 -0
  107. package/rcf/user-stories/us-18202.json +40 -0
  108. package/rcf/user-stories/us-18601.json +55 -0
  109. package/rcf/user-stories/us-18602.json +50 -0
  110. package/rcf/user-stories/us-18603.json +45 -0
  111. package/rcf/user-stories/us-18701.json +50 -0
  112. package/rcf/user-stories/us-18702.json +50 -0
  113. package/rcf/user-stories/us-18801.json +45 -0
  114. package/rcf/user-stories/us-204.json +59 -0
  115. package/rcf/user-stories/us-205.json +64 -0
  116. package/rcf/user-stories/us-206.json +54 -0
  117. package/releases/releases.yaml +11 -1
  118. package/src/cli/freeze.js +730 -0
  119. package/src/cli/help.js +12 -0
  120. package/src/cli/ledger.js +228 -12
  121. package/src/cli/questions.js +288 -0
  122. package/src/cli/readiness.js +267 -49
  123. package/src/cli/view.js +29 -0
  124. package/src/define/freeze-record.js +30 -15
  125. package/src/define/ledgers.js +210 -11
  126. package/src/intake/index.js +1 -1
  127. package/src/intake/orchestrator.js +117 -0
  128. package/src/mcp/tools.js +354 -0
  129. package/src/query/delta.js +6 -0
  130. package/src/query/gates.js +314 -39
  131. package/src/query/index.js +16 -2
  132. package/src/query/questions.js +438 -0
  133. package/src/query/readiness.js +248 -7
  134. package/src/server/index.js +24 -2
  135. package/src/server/routes.js +68 -1
  136. package/src/setup/identity-seed.js +17 -0
  137. package/src/view/build-dag.js +563 -0
  138. package/src/view/components/badge.js +41 -0
  139. package/src/view/components/diff.js +72 -0
  140. package/src/view/components/doc-row.js +62 -0
  141. package/src/view/components/empty-state.js +35 -0
  142. package/src/view/components/entity-selector.js +137 -0
  143. package/src/view/components/filter-bar.js +97 -0
  144. package/src/view/components/findings-list.js +49 -0
  145. package/src/view/components/index.js +12 -0
  146. package/src/view/components/pill.js +46 -0
  147. package/src/view/components/sub-tab-strip.js +43 -0
  148. package/src/view/components/toast.js +25 -0
  149. package/src/view/components-fixture.js +265 -0
  150. package/src/view/doc-renderers/architecture.js +304 -0
  151. package/src/view/doc-renderers/build.js +160 -0
  152. package/src/view/doc-renderers/index.js +2 -0
  153. package/src/view/doc-renderers/prd.js +35 -6
  154. package/src/view/html-page.js +700 -602
  155. package/src/view/index.js +96 -3
  156. package/src/view/live-client.js +39 -6
  157. package/src/view/needs-work.js +79 -0
  158. package/src/view/page-init.js +2193 -0
  159. package/src/view/readiness/phrasebook.js +115 -0
  160. package/src/view/readiness/po-layer.js +288 -0
  161. package/src/view/readiness/question-adapter.js +329 -0
  162. package/src/view/readiness/stage-legend.js +71 -0
  163. package/src/view/readiness.js +505 -0
  164. package/src/view/style.css +2017 -382
  165. package/src/view/test-host/embed-client.js +121 -0
  166. package/src/view/test-host/fixture.js +183 -0
  167. package/src/view/test-host/lints.js +220 -0
  168. package/src/view/view-index.js +323 -0
@@ -45,7 +45,7 @@
45
45
 
46
46
  import { computeQueue } from '../build/queue.js';
47
47
  import { isOptedOut } from '../req-baseline/opt-out.js';
48
- import { BRIEF_KINDS } from '../define/ledgers.js';
48
+ import { BRIEF_KINDS, parseResolvedBy } from '../define/ledgers.js';
49
49
 
50
50
  /** Canonical stage order (proposal §3.2). */
51
51
  export const STAGE_ORDER = /** @type {const} */ (['D1', 'D2', 'D3', 'D4', 'D5', 'D6', 'D7', 'D8']);
@@ -91,6 +91,195 @@ export const INTERFACE_KINDS = /** @type {const} */ ([
91
91
  'port', 'fileFormat', 'fixture', 'other',
92
92
  ]);
93
93
 
94
+ /**
95
+ * Owning persona for every stage-gate check in 0.29.0 (ADR-4126).
96
+ *
97
+ * Two values: `productOwner` (the person who owns what the product
98
+ * must do) and `engineer` (the person who owns how it is built).
99
+ * There is no `either` / `shared` / `both`; a check that would need
100
+ * both is split, which is what happens to `skeleton:reqFields`
101
+ * (becomes `skeleton:reqIntent` + `skeleton:reqShape`) and
102
+ * `decisions:allAnswered` (becomes `decisions:wellFormed` +
103
+ * `decisions:allAnswered`). A failing item inherits its check's
104
+ * persona; there is no per-item override.
105
+ *
106
+ * The `notApplicable` placeholder (`stage:<D>:scope`) carries the
107
+ * per-stage fallback persona from `STAGE_FALLBACK_PERSONA` below.
108
+ * It is always ok so it never affects a level verdict.
109
+ */
110
+ export const CHECK_PERSONA = /** @type {const} */ ({
111
+ // D1 Brief: every check is PO (the brief is the PO's statement of intent).
112
+ 'brief:sinceFreeze': 'productOwner',
113
+ 'brief:kinds': 'productOwner',
114
+ 'brief:openQuestions': 'productOwner',
115
+ 'brief:profile': 'productOwner',
116
+ // D2 Skeleton: resolvedBy + reqIntent are PO; reqShape, tadPersistence, deployAdr are engineer.
117
+ 'skeleton:resolvedBy': 'productOwner',
118
+ 'skeleton:reqIntent': 'productOwner',
119
+ 'skeleton:reqShape': 'engineer',
120
+ 'skeleton:tadPersistence': 'engineer',
121
+ 'skeleton:deployAdr': 'engineer',
122
+ // D3 Shapes: every check is engineer (shapes are the engineer's first act after hand-over).
123
+ 'shapes:tacHasInterface': 'engineer',
124
+ 'shapes:kindVocabulary': 'engineer',
125
+ 'shapes:draftSettled': 'engineer',
126
+ // D4 Stories: reqHasUs is PO ("what does someone do with this?"); the floors are engineer.
127
+ 'stories:reqHasUs': 'productOwner',
128
+ 'stories:usFloors': 'engineer',
129
+ // D5 Crosscut: TAD + concern-ledger content is engineering.
130
+ 'crosscut:securityArchitecture': 'engineer',
131
+ 'crosscut:operationalConcerns': 'engineer',
132
+ 'crosscut:concernsResolved': 'engineer',
133
+ // D6 Consistency: schema validity and probes are engineer.
134
+ 'consistency:validateClean': 'engineer',
135
+ 'consistency:probeCount': 'engineer',
136
+ // D7 Decisions: enumeration is PO (the PO answers or lets the default stand);
137
+ // zero-open is the engineer's hand-over condition.
138
+ 'decisions:wellFormed': 'productOwner',
139
+ 'decisions:allAnswered': 'engineer',
140
+ // D8 Freeze: every check is mechanical / engineering.
141
+ 'freeze:priorGates': 'engineer',
142
+ 'freeze:acFbsOwnership': 'engineer',
143
+ 'freeze:queueHead': 'engineer',
144
+ 'freeze:validateClean': 'engineer',
145
+ });
146
+
147
+ /**
148
+ * Per-check heading question in the owning persona's register
149
+ * (ADR-4126, proposal section 1.1). The text report and the
150
+ * Readiness tab print the question as the heading of each check's
151
+ * findings list. Static per check in 0.29.0; per-item plain-English
152
+ * wording is step 3's guided elicitation.
153
+ */
154
+ export const CHECK_QUESTION = /** @type {const} */ ({
155
+ 'brief:sinceFreeze': 'What has changed, or what is this project for?',
156
+ 'brief:kinds': 'Is this a capability, a constraint, an actor, or something else?',
157
+ 'brief:openQuestions': 'Any open questions in the brief, resolved or promoted to a decision?',
158
+ 'brief:profile': 'How do you want to review, and who are you writing for?',
159
+ 'skeleton:resolvedBy': 'Does this statement become a requirement, an entity, or an omission?',
160
+ 'skeleton:reqIntent': 'What does this requirement mean and where does it sit?',
161
+ 'skeleton:reqShape': 'Every requirement in scope carries a shape classification.',
162
+ 'skeleton:tadPersistence': 'TAD data architecture lists dataStores and coreEntities for persistence REQs.',
163
+ 'skeleton:deployAdr': 'Exactly one Deploy target or Deploy deferral ADR exists.',
164
+ 'shapes:tacHasInterface': 'Every TAC in scope has at least one interface.',
165
+ 'shapes:kindVocabulary': 'Every interface kind is in the closed vocabulary.',
166
+ 'shapes:draftSettled': 'Every draft interface has been settled (the [draft] marker is removed).',
167
+ 'stories:reqHasUs': 'What does someone do with this requirement?',
168
+ 'stories:usFloors': 'Every story in scope meets the class, tacIds and testability floors.',
169
+ 'crosscut:securityArchitecture': 'TAD.securityArchitecture is present when an auth or httpApi REQ exists.',
170
+ 'crosscut:operationalConcerns': 'TAD.operationalConcerns is present when a deployed-scope AC exists.',
171
+ 'crosscut:concernsResolved': 'Every concern-ledger entry on a REQ in scope is applied or waived.',
172
+ 'consistency:validateClean': 'Validate is clean tree-wide.',
173
+ 'consistency:probeCount': 'Zero open probe-ledger entries.',
174
+ 'decisions:wellFormed': 'Every decision is enumerated (question, two or more options, a default).',
175
+ 'decisions:allAnswered': 'Zero decisions are open.',
176
+ 'freeze:priorGates': 'Every prior stage passed or is acknowledged at the current hash.',
177
+ 'freeze:acFbsOwnership': 'Every AC is owned by exactly one FBS.',
178
+ 'freeze:queueHead': 'Queue head is actionable and not a placeholder.',
179
+ 'freeze:validateClean': 'Validate is clean tree-wide.',
180
+ });
181
+
182
+ /**
183
+ * Per-stage fallback persona for the `stage:<D>:scope` placeholder
184
+ * check that `notApplicable` envelopes carry. The placeholder is
185
+ * always ok so this never affects a level, but keeps "every check
186
+ * has a persona" true for consumers and the table test (ADR-4126
187
+ * decision 10).
188
+ */
189
+ export const STAGE_FALLBACK_PERSONA = /** @type {const} */ ({
190
+ D1: 'productOwner',
191
+ D2: 'productOwner',
192
+ D3: 'engineer',
193
+ D4: 'productOwner',
194
+ D5: 'engineer',
195
+ D6: 'engineer',
196
+ D7: 'productOwner',
197
+ D8: 'engineer',
198
+ });
199
+
200
+ /**
201
+ * Look up the question heading for a check name.
202
+ * @param {string} name
203
+ * @returns {string}
204
+ */
205
+ export function checkQuestion(name) {
206
+ return /** @type {any} */ (CHECK_QUESTION)[name] ?? '';
207
+ }
208
+
209
+ /**
210
+ * Look up the owning persona for a check name.
211
+ * @param {string} name
212
+ * @returns {'productOwner' | 'engineer' | null}
213
+ */
214
+ export function checkPersona(name) {
215
+ const v = /** @type {any} */ (CHECK_PERSONA)[name];
216
+ return v === 'productOwner' || v === 'engineer' ? v : null;
217
+ }
218
+
219
+ /**
220
+ * Draft marker (ADR-4126 for `TAC.interfaces[].description` in 0.29.0;
221
+ * REQ-188 extends it to `TAC.purpose` and `coreEntities[].description`
222
+ * in 0.30.0). A description that starts with `[draft]` after optional
223
+ * leading whitespace is pre-populated engineer entry material; the
224
+ * engineer removes the marker when the shape is settled. Decision 6
225
+ * uses the same bracket-prefix convention for AC class.
226
+ */
227
+ const INTERFACE_DRAFT_RE = /^\[draft\]/;
228
+
229
+ /**
230
+ * True when a string carries the leading `[draft]` marker
231
+ * (whitespace tolerated). Pure string test used by every draft-home
232
+ * reader below.
233
+ *
234
+ * @param {unknown} value
235
+ * @returns {boolean}
236
+ */
237
+ export function hasDraftMarker(value) {
238
+ if (typeof value !== 'string') return false;
239
+ return INTERFACE_DRAFT_RE.test(value.trimStart());
240
+ }
241
+
242
+ /**
243
+ * True when an interface's `description` carries the leading
244
+ * `[draft]` marker (whitespace tolerated). Thin wrapper over
245
+ * `hasDraftMarker` kept for ADR-4126 call sites; REQ-188 adds
246
+ * `parseTacPurposeDraft` and `parseCoreEntityDraft` for the two new
247
+ * homes.
248
+ *
249
+ * @param {unknown} iface
250
+ * @returns {boolean}
251
+ */
252
+ export function parseInterfaceDraft(iface) {
253
+ if (!iface || typeof iface !== 'object') return false;
254
+ return hasDraftMarker(/** @type {any} */ (iface).description);
255
+ }
256
+
257
+ /**
258
+ * True when a TAC's `purpose` carries the leading `[draft]` marker
259
+ * (whitespace tolerated). REQ-188 draft home; the engineer removes
260
+ * the marker when the TAC's purpose is owned.
261
+ *
262
+ * @param {unknown} tac
263
+ * @returns {boolean}
264
+ */
265
+ export function parseTacPurposeDraft(tac) {
266
+ if (!tac || typeof tac !== 'object') return false;
267
+ return hasDraftMarker(/** @type {any} */ (tac).purpose);
268
+ }
269
+
270
+ /**
271
+ * True when a coreEntities entry's `description` carries the
272
+ * leading `[draft]` marker (whitespace tolerated). REQ-188 draft
273
+ * home; the engineer removes the marker when the entity is owned.
274
+ *
275
+ * @param {unknown} entity
276
+ * @returns {boolean}
277
+ */
278
+ export function parseCoreEntityDraft(entity) {
279
+ if (!entity || typeof entity !== 'object') return false;
280
+ return hasDraftMarker(/** @type {any} */ (entity).description);
281
+ }
282
+
94
283
  /** Warn-with-ack stages in 0.29.0 (ADR-4122). */
95
284
  const WARN_WITH_ACK = new Set(['D3', 'D5', 'D6']);
96
285
 
@@ -156,12 +345,26 @@ export function parseAcClass(ac) {
156
345
  * twentieth failure. `ok` is likewise derived from the untruncated
157
346
  * count. The section 2.4 shape stays intact -- `failing` remains the
158
347
  * display array; no new field is exported.
159
- * @returns {{ name: string, ok: boolean, over: 'delta'|'tree', pass: number, total: number, failing: Array<{ id: string, why: string }> }}
348
+ * @returns {{ name: string, ok: boolean, over: 'delta'|'tree', pass: number, total: number, failing: Array<{ id: string, why: string }>, persona: 'productOwner'|'engineer', question: string }}
160
349
  */
161
350
  function makeCheck(name, over, total, failing, totalFailingCount) {
162
351
  const nFailing = typeof totalFailingCount === 'number' ? totalFailingCount : failing.length;
163
352
  const pass = Math.max(0, total - nFailing);
164
- return { name, ok: nFailing === 0, over, pass, total, failing: [...failing] };
353
+ // Resolve persona + question for the check. For stage placeholders
354
+ // (`stage:<D>:scope`) the persona comes from STAGE_FALLBACK_PERSONA.
355
+ // Unknown names fall back to engineer with an empty question so
356
+ // consumers never see null; the table test catches any new check
357
+ // that forgets to register in CHECK_PERSONA / CHECK_QUESTION.
358
+ let persona = /** @type {any} */ (CHECK_PERSONA)[name];
359
+ if (!persona) {
360
+ const stageMatch = /^stage:(D[1-8]):scope$/.exec(name);
361
+ persona = stageMatch
362
+ ? /** @type {any} */ (STAGE_FALLBACK_PERSONA)[stageMatch[1]]
363
+ : 'engineer';
364
+ }
365
+ const question = /** @type {any} */ (CHECK_QUESTION)[name]
366
+ ?? (name.startsWith('stage:') ? 'No documents of this stage’s type are in scope.' : '');
367
+ return { name, ok: nFailing === 0, over, pass, total, failing: [...failing], persona, question };
165
368
  }
166
369
 
167
370
  /**
@@ -342,57 +545,68 @@ export function checkD2Skeleton(ctx) {
342
545
 
343
546
  const checks = [];
344
547
 
345
- // Check 1: every capability/constraint/entity/... statement resolves
346
- // (the statement carries `resolvedBy` naming a REQ / TAD entity / TAC
347
- // / recorded omission id, OR its text matches an existing REQ title).
348
- // Cheap check: a `resolvedBy` string or a name-hit on titles.
349
- const resolvedByStmt = (s) => {
548
+ // Check 1 (0.30.0): every capability/constraint/entity/... statement
549
+ // resolves to a closed-grammar pointer in `resolvedBy`:
550
+ // REQ-nnn | TAD.entity:<name> | PRD.user:<name>
551
+ // | TAD.system:<name> | TAC-nnnn | omitted:<reason>
552
+ // The 0.29.0 REQ-title fallback is dropped (decision 3; a title hit
553
+ // becomes a suggestion the PR 2 computeQuestions surface returns).
554
+ // A statement with no `resolvedBy` fails; a statement with a
555
+ // `resolvedBy` outside the grammar fails with 'pointer does not
556
+ // resolve'.
557
+ /** @type {Array<{ id: string, why: string }>} */
558
+ const unresolvedFailing = [];
559
+ for (const s of resolvingStmts) {
350
560
  const rb = s?.resolvedBy;
351
- if (typeof rb === 'string' && rb.length > 0) return true;
352
- if (rb && typeof rb === 'object' && Object.keys(rb).length > 0) return true;
353
- const text = typeof s?.text === 'string' ? s.text.trim().toLowerCase() : '';
354
- if (!text) return false;
355
- for (const req of tree.requirements ?? []) {
356
- const title = typeof req?.title === 'string' ? req.title.toLowerCase() : '';
357
- if (title && text.includes(title)) return true;
561
+ if (typeof rb !== 'string' || rb.length === 0) {
562
+ unresolvedFailing.push({ id: `brief:${s.id}`, why: `${s.kind} statement has no resolvedBy` });
563
+ continue;
358
564
  }
359
- return false;
360
- };
361
- const unresolvedStmts = resolvingStmts.filter((s) => !resolvedByStmt(s));
565
+ const parsed = parseResolvedBy(rb, tree);
566
+ if (!parsed.ok) {
567
+ unresolvedFailing.push({ id: `brief:${s.id}`, why: 'pointer does not resolve' });
568
+ }
569
+ }
362
570
  checks.push(makeCheck(
363
571
  'skeleton:resolvedBy',
364
572
  'delta',
365
573
  resolvingStmts.length,
366
- unresolvedStmts.map((s) => ({ id: `brief:${s.id}`, why: `${s.kind} statement has no resolvedBy and no REQ-title hit` })),
574
+ unresolvedFailing,
367
575
  ));
368
576
 
369
- // Check 2: every REQ in scope has description, shapeClassification, domain.
577
+ // Checks 2a / 2b: the former `skeleton:reqFields` split into two
578
+ // homogeneous checks (ADR-4126, proposal section 1.3). Each
579
+ // evaluates every REQ in scope independently so a REQ with a TODO
580
+ // description still has its shapes reported, and vice versa.
581
+ const reqIntentFail = [];
370
582
  const reqShapeFail = [];
371
583
  for (const req of reqInScope) {
584
+ // reqIntent (productOwner): description without a TODO + domain
585
+ // (what the requirement means and where it sits).
372
586
  if (typeof req.description !== 'string' || req.description.length === 0 || TODO_RE.test(req.description)) {
373
- reqShapeFail.push({ id: req.reqId, why: 'description missing or contains TODO placeholder' });
374
- continue;
587
+ reqIntentFail.push({ id: req.reqId, why: 'description missing or contains TODO placeholder' });
588
+ } else if (typeof req.domain !== 'string' || req.domain.length === 0) {
589
+ reqIntentFail.push({ id: req.reqId, why: 'domain missing' });
375
590
  }
591
+ // reqShape (engineer): shapeClassification.shapes classification.
376
592
  if (!req.shapeClassification || !Array.isArray(req.shapeClassification.shapes)) {
377
593
  reqShapeFail.push({ id: req.reqId, why: 'shapeClassification.shapes missing' });
378
- continue;
379
- }
380
- if (typeof req.domain !== 'string' || req.domain.length === 0) {
381
- reqShapeFail.push({ id: req.reqId, why: 'domain missing' });
382
594
  }
383
595
  }
384
- checks.push(makeCheck('skeleton:reqFields', 'delta', reqInScope.length, reqShapeFail));
596
+ checks.push(makeCheck('skeleton:reqIntent', 'delta', reqInScope.length, reqIntentFail));
597
+ checks.push(makeCheck('skeleton:reqShape', 'delta', reqInScope.length, reqShapeFail));
385
598
 
386
- // Check 3: TAD.dataStores + coreEntities present when any persistence
599
+ // Check 3: TAD.dataArchitecture.dataStores + coreEntities present when any
387
600
  // REQ exists tree-wide.
388
601
  const persistenceReq = (tree.requirements ?? []).some((r) => Array.isArray(r?.shapeClassification?.shapes) && r.shapeClassification.shapes.includes('persistence'));
389
602
  const tadFail = [];
390
603
  if (persistenceReq) {
391
604
  const tad = tree.tad ?? {};
392
- const dataStores = Array.isArray(tad.dataStores) ? tad.dataStores : [];
393
- const coreEntities = Array.isArray(tad.coreEntities) ? tad.coreEntities : [];
394
- if (dataStores.length === 0) tadFail.push({ id: 'TAD.dataStores', why: 'TAD.dataStores empty while persistence REQ exists' });
395
- if (coreEntities.length === 0) tadFail.push({ id: 'TAD.coreEntities', why: 'TAD.coreEntities empty while persistence REQ exists' });
605
+ const dataArch = tad?.dataArchitecture ?? {};
606
+ const dataStores = Array.isArray(dataArch.dataStores) ? dataArch.dataStores : [];
607
+ const coreEntities = Array.isArray(dataArch.coreEntities) ? dataArch.coreEntities : [];
608
+ if (dataStores.length === 0) tadFail.push({ id: 'TAD.dataArchitecture.dataStores', why: 'TAD.dataArchitecture.dataStores empty while persistence REQ exists' });
609
+ if (coreEntities.length === 0) tadFail.push({ id: 'TAD.dataArchitecture.coreEntities', why: 'TAD.dataArchitecture.coreEntities empty while persistence REQ exists' });
396
610
  }
397
611
  checks.push(makeCheck('skeleton:tadPersistence', 'tree', persistenceReq ? 2 : 0, tadFail));
398
612
 
@@ -421,14 +635,20 @@ export function checkD2Skeleton(ctx) {
421
635
  const INTERFACE_KINDS_SET = new Set(INTERFACE_KINDS);
422
636
 
423
637
  /**
424
- * D3 -- Interface contracts and shapes. 0.29.0 runs only the cheap
425
- * presence + closed-vocabulary checks.
638
+ * D3 -- Interface contracts and shapes. 0.29.0 runs the cheap
639
+ * presence, closed-vocabulary and draft-marker checks
640
+ * (`shapes:draftSettled` per ADR-4126 scans the `[draft]` prefix).
641
+ * REQ-188 (0.30.0): the draft marker extends to `TAC.purpose` and
642
+ * `TAD.dataArchitecture.coreEntities[].description`; `shapes:draftSettled`
643
+ * enumerates all three homes with ids `<tacId>:<name>`, `<tacId>`
644
+ * and `TAD.entity:<name>` respectively. `tacHasInterface` and
645
+ * `kindVocabulary` apply to drafts unchanged.
426
646
  *
427
647
  * SEAM 0.30: template markers per kind (recordShape.fields:, httpRoute
428
648
  * method/path/request/response/errors, fixture.instances:); entity-name
429
- * join across TAD.coreEntities and TAC.interfaces[]; ownerRef and
430
- * `authoredAt` path resolution. Those add checks to this stage; the
431
- * fold shape stays the same.
649
+ * join across TAD.dataArchitecture.coreEntities and TAC.interfaces[];
650
+ * ownerRef and `authoredAt` path resolution. Those add checks to this
651
+ * stage; the fold shape stays the same.
432
652
  *
433
653
  * @param {StageContext} ctx
434
654
  */
@@ -443,8 +663,17 @@ export function checkD3Shapes(ctx) {
443
663
  return shapes.includes('httpApi') || shapes.includes('persistence') || shapes.includes('auth');
444
664
  });
445
665
 
446
- if (tacsInScope.length === 0 && shapedReqInScope.length === 0) {
447
- return notApplicable('D3', gate, 'no TAC and no shaped REQ (httpApi/persistence/auth) in scope');
666
+ // Core entities on TAD.dataArchitecture come from the whole tree,
667
+ // not the scope: an entity's draft marker is a tree-wide draft home
668
+ // (the engineer owns every entity in one place) and reporting it
669
+ // under D3 does not depend on which TAC a change touched.
670
+ const coreEntities = Array.isArray(tree.tad?.dataArchitecture?.coreEntities)
671
+ ? /** @type {any[]} */ (tree.tad.dataArchitecture.coreEntities)
672
+ : [];
673
+ const draftEntities = coreEntities.filter(parseCoreEntityDraft);
674
+
675
+ if (tacsInScope.length === 0 && shapedReqInScope.length === 0 && draftEntities.length === 0) {
676
+ return notApplicable('D3', gate, 'no TAC, no shaped REQ (httpApi/persistence/auth) and no [draft] core entity in scope');
448
677
  }
449
678
 
450
679
  const checks = [];
@@ -459,18 +688,39 @@ export function checkD3Shapes(ctx) {
459
688
  ));
460
689
 
461
690
  // Check 2: every interface kind is in the closed vocabulary.
691
+ // Check 3: no description across the three draft homes still
692
+ // carries the `[draft]` marker. A draft shape is entry material
693
+ // the agent pre-populates after L1; the engineer removes the
694
+ // marker when the shape is settled. `tacHasInterface` and
695
+ // `kindVocabulary` apply equally to drafts.
462
696
  let totalInterfaces = 0;
463
697
  const badKinds = [];
698
+ const draftFindings = [];
464
699
  for (const tac of tacsInScope) {
700
+ if (parseTacPurposeDraft(tac)) {
701
+ draftFindings.push({ id: tac.tacId, why: 'draft TAC purpose pre-populated at L1, not yet settled' });
702
+ }
465
703
  for (const iface of tac.interfaces ?? []) {
466
704
  totalInterfaces += 1;
467
705
  const kind = iface?.kind;
468
706
  if (typeof kind !== 'string' || !INTERFACE_KINDS_SET.has(kind)) {
469
707
  badKinds.push({ id: `${tac.tacId}:${iface?.name ?? '(unnamed)'}`, why: `unknown interface kind ${JSON.stringify(kind)}` });
470
708
  }
709
+ if (parseInterfaceDraft(iface)) {
710
+ draftFindings.push({ id: `${tac.tacId}:${iface?.name ?? '(unnamed)'}`, why: 'draft interface pre-populated at L1, not yet settled' });
711
+ }
471
712
  }
472
713
  }
714
+ for (const entity of draftEntities) {
715
+ draftFindings.push({ id: `TAD.entity:${entity?.name ?? '(unnamed)'}`, why: 'draft core entity pre-populated at L1, not yet settled' });
716
+ }
717
+ // Total for the draft check is every draft home that could carry
718
+ // the marker on an in-scope TAC plus every core entity (tree-wide):
719
+ // the TAC itself (its purpose) + every interface on it, plus the
720
+ // coreEntities list.
721
+ const totalDraftHomes = tacsInScope.length + totalInterfaces + coreEntities.length;
473
722
  checks.push(makeCheck('shapes:kindVocabulary', 'delta', totalInterfaces, badKinds));
723
+ checks.push(makeCheck('shapes:draftSettled', 'delta', totalDraftHomes, draftFindings));
474
724
 
475
725
  return foldState('D3', gate, checks, {
476
726
  currentTreeHash: ctx.currentTreeHash ?? null,
@@ -713,6 +963,31 @@ export function checkD7Decisions(ctx) {
713
963
  }
714
964
 
715
965
  const checks = [];
966
+ // Check 1: `decisions:wellFormed` (productOwner, over tree) --
967
+ // every decision is enumerated (ADR-4126, proposal section 1.3):
968
+ // a non-empty question, at least two options, a default that
969
+ // names one of the option letters.
970
+ const wellFormedFail = [];
971
+ for (const d of decisionEntries) {
972
+ const why = 'decision is not enumerated (question, two or more options, a default)';
973
+ const id = `decision:${d?.id ?? '?'}`;
974
+ if (typeof d?.question !== 'string' || d.question.length === 0) {
975
+ wellFormedFail.push({ id, why });
976
+ continue;
977
+ }
978
+ if (!Array.isArray(d.options) || d.options.length < 2) {
979
+ wellFormedFail.push({ id, why });
980
+ continue;
981
+ }
982
+ const letters = new Set(d.options.map((o) => (o && typeof o === 'object') ? /** @type {any} */ (o).letter : undefined));
983
+ if (typeof d.default !== 'string' || !letters.has(d.default)) {
984
+ wellFormedFail.push({ id, why });
985
+ }
986
+ }
987
+ checks.push(makeCheck('decisions:wellFormed', 'tree', decisionEntries.length, wellFormedFail));
988
+
989
+ // Check 2: `decisions:allAnswered` (engineer, over tree) -- zero
990
+ // open decisions. The hand-over condition for build.
716
991
  checks.push(makeCheck(
717
992
  'decisions:allAnswered',
718
993
  'tree',
@@ -13,15 +13,25 @@ export {
13
13
  computeDelta,
14
14
  } from './delta.js';
15
15
  // DEFINE stage gates (REQ-174; proposal §2.4, §3.2 v3). One check
16
- // function per stage; foldState + parseAcClass helpers.
16
+ // function per stage; foldState + parseAcClass helpers. Persona /
17
+ // question / draft-marker helpers added for ADR-4126 (0.29.0).
17
18
  export {
18
19
  STAGE_ORDER,
19
20
  STAGE_GATES,
20
21
  STAGE_SHORT_NAMES,
21
22
  STAGE_ALIASES,
22
23
  INTERFACE_KINDS,
24
+ CHECK_PERSONA,
25
+ CHECK_QUESTION,
26
+ STAGE_FALLBACK_PERSONA,
23
27
  stagePolicy,
24
28
  parseAcClass,
29
+ parseInterfaceDraft,
30
+ parseTacPurposeDraft,
31
+ parseCoreEntityDraft,
32
+ hasDraftMarker,
33
+ checkPersona,
34
+ checkQuestion,
25
35
  foldState,
26
36
  runStage,
27
37
  checkD1Brief,
@@ -33,10 +43,14 @@ export {
33
43
  checkD7Decisions,
34
44
  checkD8Freeze,
35
45
  } from './gates.js';
36
- // Readiness composer (REQ-175; proposal §2.4, §6 v3).
46
+ // Readiness composer (REQ-175; proposal §2.4, §6 v3). Levels /
47
+ // personas / verdict-line helpers added for ADR-4126 (0.29.0).
37
48
  export {
38
49
  computeReadiness,
39
50
  deriveNextAction,
51
+ deriveLevels,
52
+ derivePersonas,
53
+ formatVerdictLines,
40
54
  shortHash,
41
55
  formatTreeLine,
42
56
  } from './readiness.js';