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.
- package/CHANGELOG.md +107 -1
- package/bin/rcf.js +4 -0
- package/fixtures/canary-manifest.json +9 -9
- package/guidance/define-draft-shapes.md +89 -0
- package/guidance/define-intake.md +97 -0
- package/guidance/define-intent.md +107 -0
- package/guidance/discover-prototype.md +67 -0
- package/guidance/discovery-prototypes.md +69 -0
- package/guidance/harness-template.md +34 -0
- package/guidance/managed/agent-instructions-block.hash +1 -1
- package/guidance/managed/agent-instructions-block.md +34 -0
- package/guidance/manifest.json +9 -2
- package/package.json +1 -1
- package/rcf/adrs/adr-4124-define-freeze-verb.json +30 -0
- package/rcf/adrs/adr-4126-persona-tagged-blockers-two-levels.json +40 -0
- package/rcf/adrs/adr-4127-readiness-tab-order-and-register.json +40 -0
- package/rcf/adrs/adr-4129-questions-harness-driven-loop.json +25 -0
- package/rcf/adrs/adr-4130-intake-harness-split-tool-scanned.json +20 -0
- package/rcf/adrs/adr-4132-prototypes-in-discovery-as-evidence.json +20 -0
- package/rcf/adrs/adr-4133-dag-dependency-depth-layout.json +40 -0
- package/rcf/adrs/adr-4134-index-json-route.json +35 -0
- package/rcf/adrs/adr-4135-readiness-po-register-and-adapter.json +35 -0
- package/rcf/adrs/adr-4136-embedding-host-fixture.json +35 -0
- package/rcf/code-nodes/cn-725.json +18 -2
- package/rcf/code-nodes/cn-726.json +10 -2
- package/rcf/code-nodes/cn-727.json +13 -4
- package/rcf/code-nodes/cn-728.json +18 -2
- package/rcf/code-nodes/cn-729.json +10 -2
- package/rcf/code-nodes/cn-730.json +13 -3
- package/rcf/code-nodes/cn-731.json +20 -0
- package/rcf/code-nodes/cn-732.json +14 -0
- package/rcf/code-nodes/cn-733.json +14 -0
- package/rcf/code-nodes/cn-734.json +14 -0
- package/rcf/code-nodes/cn-735.json +20 -0
- package/rcf/code-nodes/cn-736.json +14 -0
- package/rcf/code-nodes/cn-737.json +14 -0
- package/rcf/code-nodes/cn-738.json +14 -0
- package/rcf/code-nodes/cn-739.json +18 -0
- package/rcf/code-nodes/cn-740.json +19 -0
- package/rcf/code-nodes/cn-741.json +16 -0
- package/rcf/code-nodes/cn-742.json +16 -0
- package/rcf/code-nodes/cn-743.json +15 -0
- package/rcf/code-nodes/cn-744.json +22 -0
- package/rcf/code-nodes/cn-745.json +16 -0
- package/rcf/code-nodes/cn-746.json +14 -0
- package/rcf/code-nodes/cn-747.json +18 -0
- package/rcf/code-nodes/cn-748.json +16 -0
- package/rcf/code-nodes/cn-749.json +16 -0
- package/rcf/code-nodes/cn-750.json +15 -0
- package/rcf/code-nodes/cn-751.json +18 -0
- package/rcf/code-nodes/cn-752.json +14 -0
- package/rcf/fbs/fbs-196.json +13 -5
- package/rcf/fbs/fbs-198.json +42 -0
- package/rcf/fbs/fbs-199.json +59 -0
- package/rcf/fbs/fbs-200.json +46 -0
- package/rcf/fbs/fbs-201.json +49 -0
- package/rcf/fbs/fbs-202.json +110 -0
- package/rcf/fbs/fbs-203.json +100 -0
- package/rcf/requirements/req-002.json +2 -2
- package/rcf/requirements/req-173.json +2 -2
- package/rcf/requirements/req-174.json +10 -4
- package/rcf/requirements/req-175.json +10 -4
- package/rcf/requirements/req-176.json +25 -0
- package/rcf/requirements/req-180.json +31 -0
- package/rcf/requirements/req-181.json +31 -0
- package/rcf/requirements/req-182.json +25 -0
- package/rcf/requirements/req-186.json +25 -0
- package/rcf/requirements/req-187.json +31 -0
- package/rcf/requirements/req-188.json +25 -0
- package/rcf/tacs/tac-4121-define-ledgers.json +15 -3
- package/rcf/tacs/tac-4122-define-gates.json +40 -4
- package/rcf/tacs/tac-4123-define-readiness.json +39 -3
- package/rcf/tacs/tac-4124-define-freeze.json +28 -0
- package/rcf/tacs/tac-4127-readiness-view.json +49 -0
- package/rcf/tacs/tac-4128-define-guidance.json +53 -0
- package/rcf/tacs/tac-4129-define-questions.json +45 -0
- package/rcf/tacs/tac-4130-define-intake-brief.json +35 -0
- package/rcf/tacs/tac-4131-build-dag-view.json +39 -0
- package/rcf/tacs/tac-4132-id-lookup.json +42 -0
- package/rcf/tacs/tac-4133-readiness-po-layer.json +37 -0
- package/rcf/tacs/tac-4134-wespa-host-fixture.json +54 -0
- package/rcf/test-suites/ts-016.json +3 -3
- package/rcf/test-suites/ts-215.json +148 -4
- package/rcf/test-suites/ts-216.json +19 -3
- package/rcf/test-suites/ts-218.json +78 -0
- package/rcf/test-suites/ts-219.json +70 -0
- package/rcf/test-suites/ts-220.json +78 -0
- package/rcf/test-suites/ts-221.json +78 -0
- package/rcf/test-suites/ts-222.json +100 -0
- package/rcf/test-suites/ts-223.json +94 -0
- package/rcf/test-suites/ts-224.json +70 -0
- package/rcf/test-suites/ts-225.json +78 -0
- package/rcf/test-suites/ts-226.json +158 -0
- package/rcf/test-suites/ts-227.json +38 -0
- package/rcf/test-suites/ts-228.json +86 -0
- package/rcf/test-suites/ts-229.json +83 -0
- package/rcf/user-stories/us-17302.json +50 -0
- package/rcf/user-stories/us-17401.json +14 -2
- package/rcf/user-stories/us-17402.json +55 -0
- package/rcf/user-stories/us-17403.json +60 -0
- package/rcf/user-stories/us-17502.json +60 -0
- package/rcf/user-stories/us-17503.json +65 -0
- package/rcf/user-stories/us-17601.json +58 -0
- package/rcf/user-stories/us-18001.json +55 -0
- package/rcf/user-stories/us-18002.json +35 -0
- package/rcf/user-stories/us-18101.json +35 -0
- package/rcf/user-stories/us-18202.json +40 -0
- package/rcf/user-stories/us-18601.json +55 -0
- package/rcf/user-stories/us-18602.json +50 -0
- package/rcf/user-stories/us-18603.json +45 -0
- package/rcf/user-stories/us-18701.json +50 -0
- package/rcf/user-stories/us-18702.json +50 -0
- package/rcf/user-stories/us-18801.json +45 -0
- package/rcf/user-stories/us-204.json +59 -0
- package/rcf/user-stories/us-205.json +64 -0
- package/rcf/user-stories/us-206.json +54 -0
- package/releases/releases.yaml +11 -1
- package/src/cli/freeze.js +730 -0
- package/src/cli/help.js +12 -0
- package/src/cli/ledger.js +228 -12
- package/src/cli/questions.js +288 -0
- package/src/cli/readiness.js +267 -49
- package/src/cli/view.js +29 -0
- package/src/define/freeze-record.js +30 -15
- package/src/define/ledgers.js +210 -11
- package/src/intake/index.js +1 -1
- package/src/intake/orchestrator.js +117 -0
- package/src/mcp/tools.js +354 -0
- package/src/query/delta.js +6 -0
- package/src/query/gates.js +314 -39
- package/src/query/index.js +16 -2
- package/src/query/questions.js +438 -0
- package/src/query/readiness.js +248 -7
- package/src/server/index.js +24 -2
- package/src/server/routes.js +68 -1
- package/src/setup/identity-seed.js +17 -0
- package/src/view/build-dag.js +563 -0
- package/src/view/components/badge.js +41 -0
- package/src/view/components/diff.js +72 -0
- package/src/view/components/doc-row.js +62 -0
- package/src/view/components/empty-state.js +35 -0
- package/src/view/components/entity-selector.js +137 -0
- package/src/view/components/filter-bar.js +97 -0
- package/src/view/components/findings-list.js +49 -0
- package/src/view/components/index.js +12 -0
- package/src/view/components/pill.js +46 -0
- package/src/view/components/sub-tab-strip.js +43 -0
- package/src/view/components/toast.js +25 -0
- package/src/view/components-fixture.js +265 -0
- package/src/view/doc-renderers/architecture.js +304 -0
- package/src/view/doc-renderers/build.js +160 -0
- package/src/view/doc-renderers/index.js +2 -0
- package/src/view/doc-renderers/prd.js +35 -6
- package/src/view/html-page.js +700 -602
- package/src/view/index.js +96 -3
- package/src/view/live-client.js +39 -6
- package/src/view/needs-work.js +79 -0
- package/src/view/page-init.js +2193 -0
- package/src/view/readiness/phrasebook.js +115 -0
- package/src/view/readiness/po-layer.js +288 -0
- package/src/view/readiness/question-adapter.js +329 -0
- package/src/view/readiness/stage-legend.js +71 -0
- package/src/view/readiness.js +505 -0
- package/src/view/style.css +2017 -382
- package/src/view/test-host/embed-client.js +121 -0
- package/src/view/test-host/fixture.js +183 -0
- package/src/view/test-host/lints.js +220 -0
- package/src/view/view-index.js +323 -0
package/src/cli/help.js
CHANGED
|
@@ -47,6 +47,8 @@ import { HELP as BLUEPRINT_HELP } from './blueprint.js';
|
|
|
47
47
|
import { HELP as STANDARDS_HELP } from './standards.js';
|
|
48
48
|
import { HELP as LEDGER_HELP } from './ledger.js';
|
|
49
49
|
import { HELP as READINESS_HELP } from './readiness.js';
|
|
50
|
+
import { HELP as QUESTIONS_HELP } from './questions.js';
|
|
51
|
+
import { HELP as FREEZE_HELP } from './freeze.js';
|
|
50
52
|
|
|
51
53
|
// Verify group members (from the verify-suite subtree).
|
|
52
54
|
import { HELP as VERIFY_RUN_HELP } from '../verify/cli/run.js';
|
|
@@ -97,6 +99,7 @@ define Author the RCF tree.
|
|
|
97
99
|
standards <verb> Register standards packs against the project.
|
|
98
100
|
ledger <name> <verb> Sidecar-ledger CRUD under rcf/define/.
|
|
99
101
|
readiness DEFINE stage-gate readiness view.
|
|
102
|
+
questions Persona question set for the intent-complete loop.
|
|
100
103
|
|
|
101
104
|
build Drive the FBS queue and the five-stage build cycle.
|
|
102
105
|
queue Show the FBS build queue.
|
|
@@ -198,6 +201,13 @@ Verbs:
|
|
|
198
201
|
verbs: add | resolve | list).
|
|
199
202
|
readiness DEFINE stage-gate readiness view; twin of
|
|
200
203
|
the viewer's Readiness tab (slice 5).
|
|
204
|
+
questions Persona question set for the intent-complete
|
|
205
|
+
loop: one plain question per failing
|
|
206
|
+
product-owner item carrying its write-back.
|
|
207
|
+
freeze Close a change: run readiness, refuse on any
|
|
208
|
+
failing stage, write rcf/define/freeze.json
|
|
209
|
+
and print the section 2.5 summary. --status
|
|
210
|
+
reports the current record without writing.
|
|
201
211
|
|
|
202
212
|
Run 'rcf help define <verb>' for per-verb help.
|
|
203
213
|
`;
|
|
@@ -295,6 +305,8 @@ export const HELP_MAP = {
|
|
|
295
305
|
standards: STANDARDS_HELP,
|
|
296
306
|
ledger: LEDGER_HELP,
|
|
297
307
|
readiness: READINESS_HELP,
|
|
308
|
+
questions: QUESTIONS_HELP,
|
|
309
|
+
freeze: FREEZE_HELP,
|
|
298
310
|
},
|
|
299
311
|
build: {
|
|
300
312
|
queue: BUILD_HELP,
|
package/src/cli/ledger.js
CHANGED
|
@@ -16,11 +16,14 @@ import {
|
|
|
16
16
|
LEDGER_NAMES,
|
|
17
17
|
LedgerError,
|
|
18
18
|
addEntry,
|
|
19
|
+
isResolvedByGrammar,
|
|
19
20
|
loadLedger,
|
|
20
21
|
parseBriefFromFile,
|
|
21
22
|
resolveEntry,
|
|
22
23
|
saveLedger,
|
|
24
|
+
updateEntry,
|
|
23
25
|
} from '../define/ledgers.js';
|
|
26
|
+
import { runIntakeScansOnDelta } from '../intake/orchestrator.js';
|
|
24
27
|
import { findProjectRoot } from '../view/index.js';
|
|
25
28
|
|
|
26
29
|
const OPTION_SPEC = {
|
|
@@ -39,6 +42,11 @@ const OPTION_SPEC = {
|
|
|
39
42
|
disposition: { type: 'string' },
|
|
40
43
|
finding: { type: 'string' },
|
|
41
44
|
severity: { type: 'string' },
|
|
45
|
+
scan: { type: 'boolean' },
|
|
46
|
+
'no-scan': { type: 'boolean' },
|
|
47
|
+
findings: { type: 'string' },
|
|
48
|
+
'dry-run': { type: 'boolean' },
|
|
49
|
+
'resolved-by': { type: 'string' },
|
|
42
50
|
json: { type: 'boolean' },
|
|
43
51
|
help: { type: 'boolean' },
|
|
44
52
|
};
|
|
@@ -57,6 +65,7 @@ Ledger names:
|
|
|
57
65
|
|
|
58
66
|
Verbs:
|
|
59
67
|
add Append a new entry, next id
|
|
68
|
+
update <id> Patch named fields on an existing entry
|
|
60
69
|
resolve <id> Flip status to resolved, stamp resolvedAt
|
|
61
70
|
list Print every entry (JSON with --json)
|
|
62
71
|
|
|
@@ -68,8 +77,18 @@ brief add flags:
|
|
|
68
77
|
--kind <${BRIEF_KINDS.join('|')}>
|
|
69
78
|
Statement kind (default: capability)
|
|
70
79
|
--text <text> The statement text (mutually exclusive with --from)
|
|
71
|
-
--from <path> Read one statement per non-empty line from a file
|
|
72
|
-
|
|
80
|
+
--from <path> Read one statement per non-empty line from a file;
|
|
81
|
+
each line may carry [kind] and (source: ...) markers
|
|
82
|
+
--source <span> Default source span when a line has no marker
|
|
83
|
+
--scan Run the intake scans over new statements against
|
|
84
|
+
existing ones (default on with --from, off with --text)
|
|
85
|
+
--no-scan Skip scans
|
|
86
|
+
--findings <path> Harness-supplied findings JSON (same shape
|
|
87
|
+
rcf discover intake --input accepts). Each finding
|
|
88
|
+
becomes an openQuestion statement
|
|
89
|
+
--dry-run Print the statements and findings that would be
|
|
90
|
+
minted; write nothing
|
|
91
|
+
--json Emit { added[], findings[] } for scripting
|
|
73
92
|
|
|
74
93
|
decisions add flags:
|
|
75
94
|
--question <text> The decision question (required)
|
|
@@ -93,12 +112,25 @@ resolve flags:
|
|
|
93
112
|
--answer <text> On decisions: the answer chosen (a letter, or free text)
|
|
94
113
|
--reason <text> On probes / concerns: the resolution note
|
|
95
114
|
|
|
115
|
+
update flags (any subset; validation runs as on add):
|
|
116
|
+
brief: --kind --text --source --resolved-by --blocks
|
|
117
|
+
decisions: --question --option --default --blocks
|
|
118
|
+
concerns: --concern --req --disposition --reason
|
|
119
|
+
probes: --req --finding --severity --reason
|
|
120
|
+
--resolved-by must match the closed grammar:
|
|
121
|
+
REQ-nnn | TAD.entity:<name> | PRD.user:<name>
|
|
122
|
+
| TAD.system:<name> | TAC-nnnn | omitted:<reason>
|
|
123
|
+
|
|
96
124
|
Notes:
|
|
97
125
|
- Every ledger file lives under rcf/define/. The loader treats missing
|
|
98
126
|
files as empty ledgers; a malformed file is a hard error.
|
|
99
|
-
-
|
|
100
|
-
|
|
101
|
-
|
|
127
|
+
- 'brief add --from <path>' parses one statement per line, supports
|
|
128
|
+
the leading '[kind]' and trailing '(source: ...)' markers, mints
|
|
129
|
+
statements, and (unless --no-scan) runs the mechanical intake scans
|
|
130
|
+
over the new statements against existing ones, appending one
|
|
131
|
+
openQuestion statement per finding with source 'scan:<kind>:<ids>'.
|
|
132
|
+
See '--findings' for attested findings and '--dry-run' to preview
|
|
133
|
+
without writing.
|
|
102
134
|
- 'decisions list' prints the numbered format from #241: one decision
|
|
103
135
|
per item; options with letters; the default; what it blocks.
|
|
104
136
|
`;
|
|
@@ -124,8 +156,8 @@ export async function main(argv, deps = {}) {
|
|
|
124
156
|
stderr.write(`[error] usage ledger: unknown name '${name}' (expected: ${LEDGER_NAMES.join(', ')})\n`);
|
|
125
157
|
return 2;
|
|
126
158
|
}
|
|
127
|
-
if (!verb || !['add', 'resolve', 'list'].includes(verb)) {
|
|
128
|
-
stderr.write("[error] usage ledger: expected verb 'add', 'resolve' or 'list'\n");
|
|
159
|
+
if (!verb || !['add', 'update', 'resolve', 'list'].includes(verb)) {
|
|
160
|
+
stderr.write("[error] usage ledger: expected verb 'add', 'update', 'resolve' or 'list'\n");
|
|
129
161
|
return 2;
|
|
130
162
|
}
|
|
131
163
|
|
|
@@ -155,6 +187,9 @@ export async function main(argv, deps = {}) {
|
|
|
155
187
|
if (verb === 'add') {
|
|
156
188
|
return await runAdd({ projectRoot, name, flags, stdout, stderr });
|
|
157
189
|
}
|
|
190
|
+
if (verb === 'update') {
|
|
191
|
+
return await runUpdate({ projectRoot, name, positionals, flags, stdout, stderr });
|
|
192
|
+
}
|
|
158
193
|
return await runResolve({ projectRoot, name, positionals, flags, stdout, stderr });
|
|
159
194
|
} catch (err) {
|
|
160
195
|
if (err instanceof LedgerError) {
|
|
@@ -264,10 +299,30 @@ async function runAdd({ projectRoot, name, flags, stdout, stderr }) {
|
|
|
264
299
|
stderr.write('[error] usage ledger: brief add requires --text <text> or --from <path>\n');
|
|
265
300
|
return 2;
|
|
266
301
|
}
|
|
302
|
+
if (flags['no-scan'] && flags.findings) {
|
|
303
|
+
stderr.write('[error] usage ledger: brief add --no-scan and --findings cannot be combined\n');
|
|
304
|
+
return 2;
|
|
305
|
+
}
|
|
306
|
+
// Scans default on with --from and off with --text (spec 2.2).
|
|
307
|
+
const scansOn = flags['no-scan']
|
|
308
|
+
? false
|
|
309
|
+
: (flags.scan ? true : Boolean(flags.from));
|
|
310
|
+
|
|
267
311
|
let drafts;
|
|
268
312
|
if (flags.from) {
|
|
269
313
|
const raw = await readFile(String(flags.from), 'utf8');
|
|
270
|
-
|
|
314
|
+
try {
|
|
315
|
+
drafts = parseBriefFromFile(raw, {
|
|
316
|
+
kind,
|
|
317
|
+
source: flags.source ? String(flags.source) : String(flags.from),
|
|
318
|
+
});
|
|
319
|
+
} catch (err) {
|
|
320
|
+
if (err instanceof LedgerError && err.code === 'usage') {
|
|
321
|
+
stderr.write(`[error] usage ledger: ${err.message}\n`);
|
|
322
|
+
return 2;
|
|
323
|
+
}
|
|
324
|
+
throw err;
|
|
325
|
+
}
|
|
271
326
|
if (drafts.length === 0) {
|
|
272
327
|
stderr.write(`[error] usage ledger: brief --from ${flags.from} produced no non-empty lines\n`);
|
|
273
328
|
return 2;
|
|
@@ -277,9 +332,8 @@ async function runAdd({ projectRoot, name, flags, stdout, stderr }) {
|
|
|
277
332
|
if (flags.source) entry.source = String(flags.source);
|
|
278
333
|
drafts = [entry];
|
|
279
334
|
}
|
|
280
|
-
|
|
281
|
-
//
|
|
282
|
-
// here. This slice mints ids and writes.
|
|
335
|
+
|
|
336
|
+
// Compute what WOULD be minted (for both the real path and dry-run).
|
|
283
337
|
let cur = body;
|
|
284
338
|
const added = [];
|
|
285
339
|
for (const d of drafts) {
|
|
@@ -287,8 +341,74 @@ async function runAdd({ projectRoot, name, flags, stdout, stderr }) {
|
|
|
287
341
|
cur = step.body;
|
|
288
342
|
added.push(step.entry);
|
|
289
343
|
}
|
|
344
|
+
|
|
345
|
+
// Convert operator-supplied --findings into openQuestion statements.
|
|
346
|
+
/** @type {Array<{ kind: string, text: string, source: string }>} */
|
|
347
|
+
const findingStatements = [];
|
|
348
|
+
if (flags.findings) {
|
|
349
|
+
let findingsFile;
|
|
350
|
+
try {
|
|
351
|
+
findingsFile = JSON.parse(await readFile(String(flags.findings), 'utf8'));
|
|
352
|
+
} catch (err) {
|
|
353
|
+
stderr.write(`[error] usage ledger: brief add --findings: cannot read file: ${/** @type {Error} */ (err).message}\n`);
|
|
354
|
+
return 2;
|
|
355
|
+
}
|
|
356
|
+
const findings = Array.isArray(findingsFile?.validationFindings)
|
|
357
|
+
? findingsFile.validationFindings
|
|
358
|
+
: [];
|
|
359
|
+
for (const f of findings) {
|
|
360
|
+
if (typeof f?.kind !== 'string' || typeof f?.detail !== 'string') continue;
|
|
361
|
+
findingStatements.push({
|
|
362
|
+
kind: 'openQuestion',
|
|
363
|
+
text: f.detail,
|
|
364
|
+
source: `scan:${f.kind}`,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Run mechanical scans (new against existing).
|
|
370
|
+
if (scansOn) {
|
|
371
|
+
const existing = /** @type {any[]} */ (body.statements ?? []);
|
|
372
|
+
const scanResults = runIntakeScansOnDelta(added, existing);
|
|
373
|
+
for (const r of scanResults) {
|
|
374
|
+
findingStatements.push({
|
|
375
|
+
kind: 'openQuestion',
|
|
376
|
+
text: r.detail,
|
|
377
|
+
source: `scan:${r.scanName}:${r.ids.join(',')}`,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Mint the finding statements onto `cur` so they share the id space.
|
|
383
|
+
const findingEntries = [];
|
|
384
|
+
for (const fs of findingStatements) {
|
|
385
|
+
const step = addEntry({ name: 'brief', body: cur, entry: fs, now });
|
|
386
|
+
cur = step.body;
|
|
387
|
+
findingEntries.push(step.entry);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (flags['dry-run']) {
|
|
391
|
+
if (flags.json) {
|
|
392
|
+
stdout.write(`${JSON.stringify({ added, findings: findingEntries }, null, 2)}\n`);
|
|
393
|
+
} else {
|
|
394
|
+
stdout.write(`[dry-run] brief-ledger: would add ${added.length} statement(s), ids ${added.map((e) => e.id).join(', ') || '(none)'}\n`);
|
|
395
|
+
if (findingEntries.length > 0) {
|
|
396
|
+
stdout.write(`[dry-run] brief-ledger: would add ${findingEntries.length} openQuestion from findings, ids ${findingEntries.map((e) => e.id).join(', ')}\n`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return 0;
|
|
400
|
+
}
|
|
401
|
+
|
|
290
402
|
await saveLedger({ projectRoot, name: 'brief', body: cur });
|
|
291
|
-
|
|
403
|
+
if (flags.json) {
|
|
404
|
+
stdout.write(`${JSON.stringify({ added, findings: findingEntries }, null, 2)}\n`);
|
|
405
|
+
} else {
|
|
406
|
+
stdout.write(`brief-ledger: added ${added.length} statement(s), ids ${added.map((e) => e.id).join(', ')}`);
|
|
407
|
+
if (findingEntries.length > 0) {
|
|
408
|
+
stdout.write(`; added ${findingEntries.length} openQuestion from findings, ids ${findingEntries.map((e) => e.id).join(', ')}`);
|
|
409
|
+
}
|
|
410
|
+
stdout.write('\n');
|
|
411
|
+
}
|
|
292
412
|
return 0;
|
|
293
413
|
}
|
|
294
414
|
|
|
@@ -351,6 +471,102 @@ async function runAdd({ projectRoot, name, flags, stdout, stderr }) {
|
|
|
351
471
|
return 0;
|
|
352
472
|
}
|
|
353
473
|
|
|
474
|
+
/**
|
|
475
|
+
* `rcf define ledger <name> update <id>` subverb (REQ-173 amended,
|
|
476
|
+
* US-17302). Field patches honour the same `add`-side validation.
|
|
477
|
+
* `--resolved-by` on brief must match the closed pointer grammar; a
|
|
478
|
+
* pointer outside the grammar refuses the write (exit 2).
|
|
479
|
+
*
|
|
480
|
+
* @param {object} args
|
|
481
|
+
* @param {string} args.projectRoot
|
|
482
|
+
* @param {import('../define/ledgers.js').LedgerName} args.name
|
|
483
|
+
* @param {string[]} args.positionals
|
|
484
|
+
* @param {Record<string, unknown>} args.flags
|
|
485
|
+
* @param {NodeJS.WriteStream} args.stdout
|
|
486
|
+
* @param {NodeJS.WriteStream} args.stderr
|
|
487
|
+
* @returns {Promise<number>}
|
|
488
|
+
*/
|
|
489
|
+
async function runUpdate({ projectRoot, name, positionals, flags, stdout, stderr }) {
|
|
490
|
+
if (positionals.length !== 1) {
|
|
491
|
+
stderr.write('[error] usage ledger: update expects exactly one <id>\n');
|
|
492
|
+
return 2;
|
|
493
|
+
}
|
|
494
|
+
const id = Number(positionals[0]);
|
|
495
|
+
if (!Number.isInteger(id) || id < 1) {
|
|
496
|
+
stderr.write(`[error] usage ledger: update id must be a positive integer, got '${positionals[0]}'\n`);
|
|
497
|
+
return 2;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// --resolved-by only lands on brief statements; the pointer grammar
|
|
501
|
+
// is validated here (shape-only) BEFORE loading the ledger, so a
|
|
502
|
+
// bad pointer never touches the file.
|
|
503
|
+
if (flags['resolved-by'] !== undefined) {
|
|
504
|
+
if (name !== 'brief') {
|
|
505
|
+
stderr.write(`[error] usage ledger: --resolved-by applies to brief statements only\n`);
|
|
506
|
+
return 2;
|
|
507
|
+
}
|
|
508
|
+
if (!isResolvedByGrammar(String(flags['resolved-by']))) {
|
|
509
|
+
stderr.write(
|
|
510
|
+
'[error] usage ledger: --resolved-by outside grammar (REQ-nnn | TAD.entity:<name> | PRD.user:<name> | TAD.system:<name> | TAC-nnnn | omitted:<reason>)\n',
|
|
511
|
+
);
|
|
512
|
+
return 2;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const body = await loadLedger({ projectRoot, name });
|
|
517
|
+
|
|
518
|
+
/** @type {Record<string, unknown>} */
|
|
519
|
+
const patch = {};
|
|
520
|
+
|
|
521
|
+
if (name === 'brief') {
|
|
522
|
+
if (flags.kind !== undefined) {
|
|
523
|
+
const kind = String(flags.kind);
|
|
524
|
+
if (!BRIEF_KINDS.includes(/** @type {any} */ (kind))) {
|
|
525
|
+
stderr.write(`[error] usage ledger: brief --kind must be one of: ${BRIEF_KINDS.join(', ')}\n`);
|
|
526
|
+
return 2;
|
|
527
|
+
}
|
|
528
|
+
patch.kind = kind;
|
|
529
|
+
}
|
|
530
|
+
if (flags.text !== undefined) patch.text = String(flags.text);
|
|
531
|
+
if (flags.source !== undefined) patch.source = String(flags.source);
|
|
532
|
+
if (flags['resolved-by'] !== undefined) patch.resolvedBy = String(flags['resolved-by']);
|
|
533
|
+
if (flags.blocks !== undefined) patch.blocks = String(flags.blocks);
|
|
534
|
+
} else if (name === 'decisions') {
|
|
535
|
+
if (flags.question !== undefined) patch.question = String(flags.question);
|
|
536
|
+
if (flags.option !== undefined) {
|
|
537
|
+
const parsedOpts = parseOptionFlags(flags.option);
|
|
538
|
+
if (parsedOpts.error) {
|
|
539
|
+
stderr.write(`[error] usage ledger: ${parsedOpts.error}\n`);
|
|
540
|
+
return 2;
|
|
541
|
+
}
|
|
542
|
+
patch.options = parsedOpts.value;
|
|
543
|
+
}
|
|
544
|
+
if (flags.default !== undefined) patch.default = String(flags.default);
|
|
545
|
+
if (flags.blocks !== undefined) patch.blocks = String(flags.blocks);
|
|
546
|
+
} else if (name === 'concerns') {
|
|
547
|
+
if (flags.concern !== undefined) patch.concern = String(flags.concern);
|
|
548
|
+
if (flags.req !== undefined) patch.reqId = String(flags.req);
|
|
549
|
+
if (flags.disposition !== undefined) patch.disposition = String(flags.disposition);
|
|
550
|
+
if (flags.reason !== undefined) patch.reason = String(flags.reason);
|
|
551
|
+
} else {
|
|
552
|
+
// probes
|
|
553
|
+
if (flags.req !== undefined) patch.reqId = String(flags.req);
|
|
554
|
+
if (flags.finding !== undefined) patch.finding = String(flags.finding);
|
|
555
|
+
if (flags.severity !== undefined) patch.severity = String(flags.severity);
|
|
556
|
+
if (flags.reason !== undefined) patch.reason = String(flags.reason);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (Object.keys(patch).length === 0) {
|
|
560
|
+
stderr.write('[error] usage ledger: update needs at least one field flag to patch\n');
|
|
561
|
+
return 2;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const step = updateEntry({ name, body, id, patch });
|
|
565
|
+
await saveLedger({ projectRoot, name, body: step.body });
|
|
566
|
+
stdout.write(`${name}-ledger: updated ${id}\n`);
|
|
567
|
+
return 0;
|
|
568
|
+
}
|
|
569
|
+
|
|
354
570
|
async function runResolve({ projectRoot, name, positionals, flags, stdout, stderr }) {
|
|
355
571
|
if (positionals.length !== 1) {
|
|
356
572
|
stderr.write('[error] usage ledger: resolve expects exactly one <id>\n');
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// `rcf define questions` subcommand handler (REQ-186; spec 2026-10-01
|
|
2
|
+
// §1.2, §6).
|
|
3
|
+
//
|
|
4
|
+
// Computes and prints the persona question set from readiness and the
|
|
5
|
+
// four ledgers. Pure read: writes NOTHING under rcf/.
|
|
6
|
+
//
|
|
7
|
+
// rcf define questions [--json] [--persona productOwner|engineer]
|
|
8
|
+
// [--stage <D|short>] [--limit <n>]
|
|
9
|
+
//
|
|
10
|
+
// Persona defaults from the profile register (viewer / register
|
|
11
|
+
// marker scan in rcf/.identity/profile.md); when the profile is
|
|
12
|
+
// absent or unstated, the default is productOwner. Unknown persona
|
|
13
|
+
// exits 2 with the usage line naming productOwner and engineer.
|
|
14
|
+
// The verdict exit code lives on `rcf define readiness`; this verb
|
|
15
|
+
// is a diagnostic that always exits 0.
|
|
16
|
+
|
|
17
|
+
import { parseArgs } from 'node:util';
|
|
18
|
+
import { readFile } from 'node:fs/promises';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
|
|
21
|
+
import { resolveTestPointers, walkTree } from '#core/store';
|
|
22
|
+
import { checkCodeNodeResolution } from '#core/store';
|
|
23
|
+
import { findProjectRoot } from '../view/index.js';
|
|
24
|
+
import { loadFreezeRecord } from '../define/freeze-record.js';
|
|
25
|
+
import { loadAllLedgers } from '../define/ledgers.js';
|
|
26
|
+
import { STAGE_ALIASES } from '../query/gates.js';
|
|
27
|
+
import { computeReadiness } from '../query/readiness.js';
|
|
28
|
+
import { computeQuestions, PERSONAS } from '../query/questions.js';
|
|
29
|
+
|
|
30
|
+
const OPTION_SPEC = {
|
|
31
|
+
json: { type: 'boolean' },
|
|
32
|
+
persona: { type: 'string' },
|
|
33
|
+
stage: { type: 'string' },
|
|
34
|
+
limit: { type: 'string' },
|
|
35
|
+
help: { type: 'boolean' },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const HELP = `Usage: rcf define questions [options]
|
|
39
|
+
|
|
40
|
+
Print the persona question set (REQ-186). Each failing product-owner
|
|
41
|
+
check in readiness becomes one plain question per failing item, each
|
|
42
|
+
carrying the exact write-back that answers it.
|
|
43
|
+
|
|
44
|
+
The compute is pure: it never creates or modifies a file under rcf/.
|
|
45
|
+
Answers land through the write-back the question carries; the loop
|
|
46
|
+
ends when 'rcf define readiness --level intent' reports ok.
|
|
47
|
+
|
|
48
|
+
Options:
|
|
49
|
+
--json Emit the full questions object as JSON.
|
|
50
|
+
--persona <who> One of productOwner | engineer. Defaults
|
|
51
|
+
to the register marker in
|
|
52
|
+
rcf/.identity/profile.md, else
|
|
53
|
+
productOwner.
|
|
54
|
+
--stage <stage> Narrow the output to one stage. Accepts
|
|
55
|
+
a stage id (D1..D8) or its short name
|
|
56
|
+
(brief | skeleton | shapes | stories |
|
|
57
|
+
crosscut | consistency | decisions |
|
|
58
|
+
freeze).
|
|
59
|
+
--limit <n> Cap the questions[] array to the first n
|
|
60
|
+
entries. 'remaining' still counts every
|
|
61
|
+
matching item.
|
|
62
|
+
--help Print this help.
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Read `rcf/.identity/profile.md` when it exists; return null on
|
|
67
|
+
* ENOENT or on any read error (the question compute treats absence
|
|
68
|
+
* as "productOwner default" rather than a runtime error).
|
|
69
|
+
*
|
|
70
|
+
* @param {string} projectRoot
|
|
71
|
+
* @returns {Promise<string|null>}
|
|
72
|
+
*/
|
|
73
|
+
async function readProfileText(projectRoot) {
|
|
74
|
+
try {
|
|
75
|
+
return await readFile(join(projectRoot, 'rcf', '.identity', 'profile.md'), 'utf8');
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Pick the profile register from the text, returning one of
|
|
83
|
+
* `productOwner` | `engineer` | `unstated`.
|
|
84
|
+
*/
|
|
85
|
+
function pickRegister(text) {
|
|
86
|
+
if (typeof text !== 'string' || text.length === 0) return 'unstated';
|
|
87
|
+
// The canonical register template (src/setup/identity-seed.js) names
|
|
88
|
+
// all three markers in its explanation ("- `productOwner`: ..."),
|
|
89
|
+
// then places the chosen value as a bare word on a line of its own
|
|
90
|
+
// ("engineer\n", "unstated\n", etc.). Ad-hoc profiles also use
|
|
91
|
+
// "Register: engineer" on one line. In both shapes the ACTIVE value
|
|
92
|
+
// sits AFTER the explanation, so pick the LAST marker occurrence
|
|
93
|
+
// in the text; a line matching nothing but a marker word takes
|
|
94
|
+
// precedence over an in-prose occurrence at the same position.
|
|
95
|
+
const markers = ['productOwner', 'engineer', 'unstated'];
|
|
96
|
+
// Pass 1: prefer a bare-line marker (canonical template shape).
|
|
97
|
+
const lines = text.split(/\r?\n/);
|
|
98
|
+
let bareLine = null;
|
|
99
|
+
for (const raw of lines) {
|
|
100
|
+
const trimmed = raw.trim();
|
|
101
|
+
if (markers.includes(trimmed)) bareLine = trimmed;
|
|
102
|
+
}
|
|
103
|
+
if (bareLine) return /** @type {any} */ (bareLine);
|
|
104
|
+
// Pass 2: fallback - last occurrence anywhere (handles
|
|
105
|
+
// "Register: engineer" and other operator-prose shapes).
|
|
106
|
+
let bestIdx = -1;
|
|
107
|
+
let best = null;
|
|
108
|
+
for (const m of markers) {
|
|
109
|
+
const idx = text.lastIndexOf(m);
|
|
110
|
+
if (idx > bestIdx) { bestIdx = idx; best = m; }
|
|
111
|
+
}
|
|
112
|
+
return /** @type {any} */ (best) ?? 'unstated';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @param {string[]} argv
|
|
117
|
+
* @param {object} [deps]
|
|
118
|
+
* @returns {Promise<number>}
|
|
119
|
+
*/
|
|
120
|
+
export async function main(argv, deps = {}) {
|
|
121
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
122
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
123
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
124
|
+
|
|
125
|
+
let parsed;
|
|
126
|
+
try {
|
|
127
|
+
parsed = parseArgs({ args: argv, options: OPTION_SPEC, allowPositionals: false, strict: true });
|
|
128
|
+
} catch (err) {
|
|
129
|
+
stderr.write(`[error] usage ${err.message}\n`);
|
|
130
|
+
stderr.write(HELP);
|
|
131
|
+
return 2;
|
|
132
|
+
}
|
|
133
|
+
const flags = parsed.values;
|
|
134
|
+
if (flags.help) {
|
|
135
|
+
stdout.write(HELP);
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Validate --persona before any filesystem work.
|
|
140
|
+
let persona = flags.persona;
|
|
141
|
+
if (persona !== undefined && !PERSONAS.includes(persona)) {
|
|
142
|
+
stderr.write(`[error] usage questions: unknown persona '${persona}' (expected one of ${PERSONAS.join(' | ')})\n`);
|
|
143
|
+
return 2;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Validate --stage against the readiness aliases (same vocabulary).
|
|
147
|
+
let stageFilter = null;
|
|
148
|
+
if (typeof flags.stage === 'string') {
|
|
149
|
+
const canonical = STAGE_ALIASES[flags.stage];
|
|
150
|
+
if (!canonical) {
|
|
151
|
+
stderr.write(`[error] usage questions: unknown --stage '${flags.stage}' (expected D1..D8 or a short name)\n`);
|
|
152
|
+
return 2;
|
|
153
|
+
}
|
|
154
|
+
stageFilter = canonical;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let limit = null;
|
|
158
|
+
if (flags.limit !== undefined) {
|
|
159
|
+
const n = Number(flags.limit);
|
|
160
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
161
|
+
stderr.write(`[error] usage questions: --limit expects a non-negative integer, got '${flags.limit}'\n`);
|
|
162
|
+
return 2;
|
|
163
|
+
}
|
|
164
|
+
limit = n;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const projectRoot = await findProjectRoot(cwd);
|
|
168
|
+
if (!projectRoot) {
|
|
169
|
+
stderr.write('[error] usage no project root found (no rcf/manifest.json in this directory or any ancestor). Run `npx rcf init` to create and wire a project.\n');
|
|
170
|
+
return 2;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const { tree, errors } = await walkTree({ projectRoot });
|
|
174
|
+
const staleErrors = await checkCodeNodeResolution({ projectRoot, tree });
|
|
175
|
+
const validateErrors = [...errors, ...staleErrors];
|
|
176
|
+
|
|
177
|
+
const [freeze, ledgers, testPointers, profileText] = await Promise.all([
|
|
178
|
+
loadFreezeRecord({ projectRoot }),
|
|
179
|
+
loadAllLedgers({ projectRoot }),
|
|
180
|
+
resolveTestPointers({ projectRoot, tree }),
|
|
181
|
+
readProfileText(projectRoot),
|
|
182
|
+
]);
|
|
183
|
+
|
|
184
|
+
if (persona === undefined) {
|
|
185
|
+
const register = pickRegister(profileText);
|
|
186
|
+
persona = register === 'engineer' ? 'engineer' : 'productOwner';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const readiness = computeReadiness(tree, {
|
|
190
|
+
freeze, ledgers, profile: undefined, profileText, testPointers, validateErrors,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const result = computeQuestions(readiness, { tree, ledgers, profileText, persona });
|
|
194
|
+
|
|
195
|
+
// Apply --stage and --limit AFTER compute so `remaining` counts
|
|
196
|
+
// every matching item (spec §6).
|
|
197
|
+
let filtered = result.questions;
|
|
198
|
+
if (stageFilter) filtered = filtered.filter((q) => q.stage === stageFilter);
|
|
199
|
+
const remaining = filtered.length;
|
|
200
|
+
if (limit !== null) filtered = filtered.slice(0, limit);
|
|
201
|
+
|
|
202
|
+
// Rebuild groups[] from the filtered list, preserving source-span keys.
|
|
203
|
+
const groupMap = new Map();
|
|
204
|
+
for (const q of filtered) {
|
|
205
|
+
const key = q.context?.statement?.source ?? q.context?.sourceSpan ?? 'tree';
|
|
206
|
+
const existing = groupMap.get(key) ?? { key, label: result.groups.find((g) => g.key === key)?.label ?? key, questionIds: [] };
|
|
207
|
+
existing.questionIds.push(q.id);
|
|
208
|
+
groupMap.set(key, existing);
|
|
209
|
+
}
|
|
210
|
+
const envelope = {
|
|
211
|
+
...result,
|
|
212
|
+
questions: filtered,
|
|
213
|
+
groups: [...groupMap.values()],
|
|
214
|
+
remaining,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
if (flags.json) {
|
|
218
|
+
stdout.write(`${JSON.stringify(envelope, null, 2)}\n`);
|
|
219
|
+
return 0;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
renderText(stdout, envelope);
|
|
223
|
+
return 0;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Print the text summary (spec §1.5): one progress line, then each
|
|
228
|
+
* group with its asks in plain words. Ids, verbs and stage names are
|
|
229
|
+
* not shown unless the owner asks.
|
|
230
|
+
*
|
|
231
|
+
* @param {NodeJS.WritableStream} stdout
|
|
232
|
+
* @param {ReturnType<typeof computeQuestions>} result
|
|
233
|
+
*/
|
|
234
|
+
function renderText(stdout, result) {
|
|
235
|
+
const persona = result.persona;
|
|
236
|
+
// Termination is `result.ok` alone (spec §1.4): --stage filtering
|
|
237
|
+
// can leave remaining=0 for a stage while the whole tree is still
|
|
238
|
+
// failing, which must not print the complete verdict.
|
|
239
|
+
if (result.ok) {
|
|
240
|
+
stdout.write(persona === 'productOwner'
|
|
241
|
+
? 'Intent-complete: yes. The question set is empty.\n'
|
|
242
|
+
: 'Ready-to-build: no engineer blockers in this stage.\n');
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Progress line: "Intent-complete: not yet; N questions left, ..."
|
|
247
|
+
const n = result.remaining;
|
|
248
|
+
const partition = partitionByGroupLabel(result);
|
|
249
|
+
const parts = Object.entries(partition)
|
|
250
|
+
.filter(([, c]) => c > 0)
|
|
251
|
+
.map(([label, c]) => `${c} about ${label}`);
|
|
252
|
+
const progressLine = persona === 'productOwner'
|
|
253
|
+
? `Intent-complete: not yet; ${n} ${n === 1 ? 'question' : 'questions'} left${parts.length ? `, ${parts.join(', ')}` : ''}.`
|
|
254
|
+
: `Ready-to-build: not yet; ${n} ${n === 1 ? 'check' : 'checks'} blocking.`;
|
|
255
|
+
stdout.write(`${progressLine}\n`);
|
|
256
|
+
|
|
257
|
+
for (const group of result.groups) {
|
|
258
|
+
stdout.write(`\n(${group.label})\n`);
|
|
259
|
+
for (const qid of group.questionIds) {
|
|
260
|
+
const q = result.questions.find((x) => x.id === qid);
|
|
261
|
+
if (!q) continue;
|
|
262
|
+
stdout.write(` - ${q.ask}\n`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (persona === 'productOwner' && result.optional?.length) {
|
|
267
|
+
stdout.write('\nOpen decisions (optional, not blocking):\n');
|
|
268
|
+
for (const o of result.optional) {
|
|
269
|
+
stdout.write(` - ${o.ask}\n`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Count questions by group-label bucket for the progress line. For
|
|
276
|
+
* the PO register the two buckets are "your document" (brief:... items)
|
|
277
|
+
* and "requirements" (REQ-... items); anything else goes under "other".
|
|
278
|
+
*/
|
|
279
|
+
function partitionByGroupLabel(result) {
|
|
280
|
+
const buckets = { 'your document': 0, 'requirements': 0, 'other': 0 };
|
|
281
|
+
for (const q of result.questions) {
|
|
282
|
+
const src = q.context?.statement?.source ?? q.context?.sourceSpan ?? 'tree';
|
|
283
|
+
if (typeof src === 'string' && src !== 'tree' && !src.startsWith('scan:')) buckets['your document']++;
|
|
284
|
+
else if (q.context?.reqId) buckets['requirements']++;
|
|
285
|
+
else buckets['other']++;
|
|
286
|
+
}
|
|
287
|
+
return buckets;
|
|
288
|
+
}
|