rcf-lite 0.28.1 → 0.28.4
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 +310 -0
- package/bin/rcf.js +4 -0
- package/fixtures/canary-manifest.json +9 -9
- package/package.json +1 -1
- package/rcf/adrs/adr-4120-canonical-json-hashing.json +30 -0
- package/rcf/adrs/adr-4121-define-sidecars.json +30 -0
- package/rcf/adrs/adr-4122-define-gates-blocking-and-optout.json +30 -0
- package/rcf/adrs/adr-4123-readiness-single-object.json +30 -0
- package/rcf/code-nodes/cn-602.json +3 -2
- package/rcf/code-nodes/cn-700.json +23 -0
- package/rcf/code-nodes/cn-701.json +14 -0
- package/rcf/code-nodes/cn-702.json +14 -0
- package/rcf/code-nodes/cn-703.json +16 -0
- package/rcf/code-nodes/cn-720.json +15 -0
- package/rcf/code-nodes/cn-721.json +15 -0
- package/rcf/code-nodes/cn-722.json +15 -0
- package/rcf/code-nodes/cn-723.json +18 -0
- package/rcf/code-nodes/cn-724.json +14 -0
- package/rcf/code-nodes/cn-725.json +21 -0
- package/rcf/code-nodes/cn-726.json +17 -0
- package/rcf/code-nodes/cn-727.json +14 -0
- package/rcf/code-nodes/cn-728.json +21 -0
- package/rcf/code-nodes/cn-729.json +18 -0
- package/rcf/code-nodes/cn-730.json +14 -0
- package/rcf/fbs/fbs-181.json +3 -2
- package/rcf/fbs/fbs-190.json +37 -0
- package/rcf/fbs/fbs-191.json +34 -0
- package/rcf/fbs/fbs-192.json +17 -0
- package/rcf/fbs/fbs-193.json +37 -0
- package/rcf/fbs/fbs-194.json +35 -0
- package/rcf/fbs/fbs-195.json +37 -0
- package/rcf/fbs/fbs-196.json +37 -0
- package/rcf/fbs/fbs-197.json +41 -0
- package/rcf/requirements/req-001.json +2 -1
- package/rcf/requirements/req-002.json +2 -1
- package/rcf/requirements/req-003.json +2 -1
- package/rcf/requirements/req-004.json +2 -1
- package/rcf/requirements/req-005.json +2 -1
- package/rcf/requirements/req-006.json +2 -1
- package/rcf/requirements/req-007.json +2 -1
- package/rcf/requirements/req-008.json +2 -1
- package/rcf/requirements/req-009.json +2 -1
- package/rcf/requirements/req-010.json +5 -2
- package/rcf/requirements/req-011.json +2 -1
- package/rcf/requirements/req-012.json +2 -1
- package/rcf/requirements/req-013.json +2 -1
- package/rcf/requirements/req-014.json +2 -1
- package/rcf/requirements/req-015.json +2 -1
- package/rcf/requirements/req-016.json +5 -2
- package/rcf/requirements/req-017.json +6 -2
- package/rcf/requirements/req-018.json +5 -2
- package/rcf/requirements/req-019.json +5 -2
- package/rcf/requirements/req-020.json +5 -2
- package/rcf/requirements/req-021.json +5 -2
- package/rcf/requirements/req-022.json +5 -2
- package/rcf/requirements/req-023.json +6 -4
- package/rcf/requirements/req-024.json +5 -2
- package/rcf/requirements/req-025.json +5 -2
- package/rcf/requirements/req-026.json +5 -2
- package/rcf/requirements/req-027.json +5 -2
- package/rcf/requirements/req-028.json +5 -2
- package/rcf/requirements/req-029.json +5 -2
- package/rcf/requirements/req-030.json +5 -2
- package/rcf/requirements/req-040.json +5 -2
- package/rcf/requirements/req-041.json +5 -2
- package/rcf/requirements/req-042.json +5 -2
- package/rcf/requirements/req-043.json +5 -2
- package/rcf/requirements/req-050.json +5 -2
- package/rcf/requirements/req-051.json +5 -2
- package/rcf/requirements/req-052.json +5 -2
- package/rcf/requirements/req-053.json +5 -2
- package/rcf/requirements/req-054.json +5 -2
- package/rcf/requirements/req-060.json +5 -2
- package/rcf/requirements/req-061.json +5 -2
- package/rcf/requirements/req-062.json +5 -2
- package/rcf/requirements/req-063.json +5 -2
- package/rcf/requirements/req-070.json +5 -2
- package/rcf/requirements/req-071.json +5 -2
- package/rcf/requirements/req-072.json +5 -2
- package/rcf/requirements/req-073.json +5 -2
- package/rcf/requirements/req-074.json +5 -2
- package/rcf/requirements/req-075.json +5 -2
- package/rcf/requirements/req-076.json +5 -2
- package/rcf/requirements/req-077.json +5 -2
- package/rcf/requirements/req-080.json +5 -2
- package/rcf/requirements/req-081.json +5 -2
- package/rcf/requirements/req-082.json +5 -2
- package/rcf/requirements/req-083.json +5 -2
- package/rcf/requirements/req-084.json +5 -2
- package/rcf/requirements/req-085.json +5 -2
- package/rcf/requirements/req-086.json +5 -2
- package/rcf/requirements/req-090.json +5 -2
- package/rcf/requirements/req-091.json +5 -2
- package/rcf/requirements/req-092.json +5 -2
- package/rcf/requirements/req-093.json +5 -2
- package/rcf/requirements/req-094.json +5 -2
- package/rcf/requirements/req-100.json +5 -2
- package/rcf/requirements/req-101.json +5 -2
- package/rcf/requirements/req-102.json +5 -2
- package/rcf/requirements/req-103.json +5 -2
- package/rcf/requirements/req-104.json +5 -2
- package/rcf/requirements/req-110.json +5 -2
- package/rcf/requirements/req-111.json +5 -2
- package/rcf/requirements/req-112.json +5 -2
- package/rcf/requirements/req-113.json +5 -2
- package/rcf/requirements/req-114.json +5 -2
- package/rcf/requirements/req-115.json +5 -2
- package/rcf/requirements/req-120.json +5 -2
- package/rcf/requirements/req-121.json +5 -2
- package/rcf/requirements/req-122.json +5 -2
- package/rcf/requirements/req-123.json +5 -2
- package/rcf/requirements/req-124.json +5 -2
- package/rcf/requirements/req-125.json +5 -2
- package/rcf/requirements/req-130.json +6 -2
- package/rcf/requirements/req-131.json +6 -2
- package/rcf/requirements/req-132.json +6 -2
- package/rcf/requirements/req-133.json +6 -2
- package/rcf/requirements/req-134.json +6 -2
- package/rcf/requirements/req-140.json +5 -2
- package/rcf/requirements/req-145.json +5 -2
- package/rcf/requirements/req-150.json +6 -2
- package/rcf/requirements/req-151.json +6 -2
- package/rcf/requirements/req-152.json +6 -2
- package/rcf/requirements/req-153.json +6 -2
- package/rcf/requirements/req-154.json +5 -2
- package/rcf/requirements/req-155.json +4 -1
- package/rcf/requirements/req-156.json +4 -1
- package/rcf/requirements/req-157.json +4 -1
- package/rcf/requirements/req-158.json +4 -1
- package/rcf/requirements/req-159.json +4 -1
- package/rcf/requirements/req-160.json +4 -1
- package/rcf/requirements/req-161.json +4 -1
- package/rcf/requirements/req-162.json +4 -1
- package/rcf/requirements/req-163.json +5 -2
- package/rcf/requirements/req-164.json +5 -2
- package/rcf/requirements/req-165.json +5 -2
- package/rcf/requirements/req-170.json +33 -0
- package/rcf/requirements/req-171.json +44 -0
- package/rcf/requirements/req-172.json +25 -0
- package/rcf/requirements/req-173.json +25 -0
- package/rcf/requirements/req-174.json +25 -0
- package/rcf/requirements/req-175.json +25 -0
- package/rcf/tacs/tac-4103-feedback-redact.json +2 -2
- package/rcf/tacs/tac-4120-define-delta-and-freeze.json +21 -0
- package/rcf/tacs/tac-4121-define-ledgers.json +23 -0
- package/rcf/tacs/tac-4122-define-gates.json +25 -0
- package/rcf/tacs/tac-4123-define-readiness.json +25 -0
- package/rcf/test-suites/ts-188.json +10 -2
- package/rcf/test-suites/ts-191.json +19 -5
- package/rcf/test-suites/ts-204.json +30 -0
- package/rcf/test-suites/ts-205.json +22 -0
- package/rcf/test-suites/ts-206.json +22 -0
- package/rcf/test-suites/ts-207.json +22 -0
- package/rcf/test-suites/ts-208.json +22 -0
- package/rcf/test-suites/ts-209.json +22 -0
- package/rcf/test-suites/ts-210.json +22 -0
- package/rcf/test-suites/ts-211.json +22 -0
- package/rcf/test-suites/ts-212.json +22 -0
- package/rcf/test-suites/ts-213.json +50 -0
- package/rcf/test-suites/ts-214.json +74 -0
- package/rcf/test-suites/ts-215.json +67 -0
- package/rcf/test-suites/ts-216.json +78 -0
- package/rcf/test-suites/ts-217.json +62 -0
- package/rcf/user-stories/us-15601.json +10 -1
- package/rcf/user-stories/us-15701.json +4 -4
- package/rcf/user-stories/us-17001.json +27 -0
- package/rcf/user-stories/us-17002.json +22 -0
- package/rcf/user-stories/us-17003.json +22 -0
- package/rcf/user-stories/us-17004.json +22 -0
- package/rcf/user-stories/us-17005.json +22 -0
- package/rcf/user-stories/us-17006.json +21 -0
- package/rcf/user-stories/us-17007.json +21 -0
- package/rcf/user-stories/us-17008.json +22 -0
- package/rcf/user-stories/us-17101.json +22 -0
- package/rcf/user-stories/us-17102.json +22 -0
- package/rcf/user-stories/us-17201.json +38 -0
- package/rcf/user-stories/us-17301.json +38 -0
- package/rcf/user-stories/us-17401.json +58 -0
- package/rcf/user-stories/us-17501.json +48 -0
- package/releases/releases.yaml +31 -1
- package/src/browser-verify/manifest-writer.js +9 -5
- package/src/cli/browser-verify.js +9 -1
- package/src/cli/feedback.js +316 -32
- package/src/cli/help.js +11 -0
- package/src/cli/intake.js +25 -4
- package/src/cli/ledger.js +399 -0
- package/src/cli/preflight.js +24 -3
- package/src/cli/readiness.js +280 -0
- package/src/cli/review.js +9 -1
- package/src/cli/ui-baseline.js +14 -1
- package/src/core/store/index.js +5 -0
- package/src/core/store/validate-composed-record.js +95 -0
- package/src/core/store/writer.js +86 -6
- package/src/define/freeze-record.js +232 -0
- package/src/define/ledgers.js +513 -0
- package/src/feedback/gh.js +27 -1
- package/src/feedback/redact-allowlist.json +7 -0
- package/src/feedback/redact.js +126 -11
- package/src/feedback/render.js +34 -12
- package/src/intake/manifest-writer.js +6 -3
- package/src/intake/orchestrator.js +44 -7
- package/src/preflight/manifest-writer.js +11 -11
- package/src/query/delta.js +276 -0
- package/src/query/gates.js +832 -0
- package/src/query/index.js +36 -0
- package/src/query/readiness.js +333 -0
- package/src/req-baseline/sweep.js +34 -5
- package/src/server/index.js +37 -0
- package/src/server/routes.js +65 -3
- package/src/ui-baseline/manifest-writer.js +11 -10
- package/src/view/doc-renderers/helpers.js +39 -2
- package/src/view/doc-renderers/req.js +10 -2
- package/src/view/doc-renderers/tad.js +67 -6
- package/src/view/doc-renderers/user-story.js +10 -5
- package/src/view/html-page.js +347 -28
- package/src/view/index.js +14 -1
- package/src/view/live-client.js +22 -2
- package/src/view/product-map.js +751 -0
- package/src/view/style.css +383 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
// `rcf define ledger <brief|decisions|concerns|probes> <add|resolve|list>`
|
|
2
|
+
// (REQ-173; proposal §8.2 v3).
|
|
3
|
+
//
|
|
4
|
+
// Sidecar-ledger CRUD. The intake-scan-against-frozen-statements part
|
|
5
|
+
// of `brief add --from <file>` is a seam here (slice 1 does not wire
|
|
6
|
+
// it): `rcf discover intake` is the intended caller in a later slice
|
|
7
|
+
// and the CLI exposes `--kind` and `--source` so a caller can feed
|
|
8
|
+
// them in. Exit codes match the rest of the CLI: 0 success, 2 usage
|
|
9
|
+
// error, 1 unexpected runtime failure.
|
|
10
|
+
|
|
11
|
+
import { readFile } from 'node:fs/promises';
|
|
12
|
+
import { parseArgs } from 'node:util';
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
BRIEF_KINDS,
|
|
16
|
+
LEDGER_NAMES,
|
|
17
|
+
LedgerError,
|
|
18
|
+
addEntry,
|
|
19
|
+
loadLedger,
|
|
20
|
+
parseBriefFromFile,
|
|
21
|
+
resolveEntry,
|
|
22
|
+
saveLedger,
|
|
23
|
+
} from '../define/ledgers.js';
|
|
24
|
+
import { findProjectRoot } from '../view/index.js';
|
|
25
|
+
|
|
26
|
+
const OPTION_SPEC = {
|
|
27
|
+
kind: { type: 'string' },
|
|
28
|
+
text: { type: 'string' },
|
|
29
|
+
from: { type: 'string' },
|
|
30
|
+
source: { type: 'string' },
|
|
31
|
+
reason: { type: 'string' },
|
|
32
|
+
answer: { type: 'string' },
|
|
33
|
+
question: { type: 'string' },
|
|
34
|
+
option: { type: 'string', multiple: true },
|
|
35
|
+
default: { type: 'string' },
|
|
36
|
+
blocks: { type: 'string' },
|
|
37
|
+
req: { type: 'string' },
|
|
38
|
+
concern: { type: 'string' },
|
|
39
|
+
disposition: { type: 'string' },
|
|
40
|
+
finding: { type: 'string' },
|
|
41
|
+
severity: { type: 'string' },
|
|
42
|
+
json: { type: 'boolean' },
|
|
43
|
+
help: { type: 'boolean' },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const HELP = `Usage: rcf define ledger <name> <verb> [options]
|
|
47
|
+
|
|
48
|
+
Ledgers (proposal 2026-09-22 §2.5, §3.2 v3) are sidecars under
|
|
49
|
+
rcf/define/. Four ledgers, each one JSON file, each with one numbered
|
|
50
|
+
sequence for the life of the project.
|
|
51
|
+
|
|
52
|
+
Ledger names:
|
|
53
|
+
brief D1 brief statements (kinded from a closed set)
|
|
54
|
+
decisions D7 open / answered decisions (#241 format)
|
|
55
|
+
concerns D5 (concern, REQ) pairs (applied | waived)
|
|
56
|
+
probes D6 probe findings (severity: low | medium | high)
|
|
57
|
+
|
|
58
|
+
Verbs:
|
|
59
|
+
add Append a new entry, next id
|
|
60
|
+
resolve <id> Flip status to resolved, stamp resolvedAt
|
|
61
|
+
list Print every entry (JSON with --json)
|
|
62
|
+
|
|
63
|
+
Common flags:
|
|
64
|
+
--json Emit machine-readable envelope on list
|
|
65
|
+
--help Print this help
|
|
66
|
+
|
|
67
|
+
brief add flags:
|
|
68
|
+
--kind <${BRIEF_KINDS.join('|')}>
|
|
69
|
+
Statement kind (default: capability)
|
|
70
|
+
--text <text> The statement text (mutually exclusive with --from)
|
|
71
|
+
--from <path> Read one statement per non-empty line from a file
|
|
72
|
+
--source <span> Optional source span label (file:line, URL, note)
|
|
73
|
+
|
|
74
|
+
decisions add flags:
|
|
75
|
+
--question <text> The decision question (required)
|
|
76
|
+
--option <letter:text> Repeatable option, format 'a:First option'
|
|
77
|
+
--default <letter> Default option letter
|
|
78
|
+
--blocks <text> What this decision blocks (a gate id, doc id)
|
|
79
|
+
|
|
80
|
+
concerns add flags:
|
|
81
|
+
--req <req-id> REQ the (concern, REQ) pair belongs to (required)
|
|
82
|
+
--concern <slug> Concern key (auth, retention, ...) (required)
|
|
83
|
+
--disposition applied|waived
|
|
84
|
+
Disposition (required)
|
|
85
|
+
--reason <text> Reason (mandatory when disposition=waived)
|
|
86
|
+
|
|
87
|
+
probes add flags:
|
|
88
|
+
--req <req-id> REQ the probe finding sits on (required)
|
|
89
|
+
--finding <text> The finding text (required)
|
|
90
|
+
--severity low|medium|high
|
|
91
|
+
|
|
92
|
+
resolve flags:
|
|
93
|
+
--answer <text> On decisions: the answer chosen (a letter, or free text)
|
|
94
|
+
--reason <text> On probes / concerns: the resolution note
|
|
95
|
+
|
|
96
|
+
Notes:
|
|
97
|
+
- Every ledger file lives under rcf/define/. The loader treats missing
|
|
98
|
+
files as empty ledgers; a malformed file is a hard error.
|
|
99
|
+
- The 'brief add --from <path>' seam for running rcf discover intake
|
|
100
|
+
against the frozen statements is wired in a later slice; this slice
|
|
101
|
+
only splits by non-empty lines and mints ids.
|
|
102
|
+
- 'decisions list' prints the numbered format from #241: one decision
|
|
103
|
+
per item; options with letters; the default; what it blocks.
|
|
104
|
+
`;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {string[]} argv - argv slice after `ledger`
|
|
108
|
+
* @param {object} [deps]
|
|
109
|
+
* @returns {Promise<number>}
|
|
110
|
+
*/
|
|
111
|
+
export async function main(argv, deps = {}) {
|
|
112
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
113
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
114
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
115
|
+
|
|
116
|
+
if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
|
|
117
|
+
stdout.write(HELP);
|
|
118
|
+
return argv.length === 0 ? 2 : 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const name = argv[0];
|
|
122
|
+
const verb = argv[1];
|
|
123
|
+
if (!LEDGER_NAMES.includes(/** @type {any} */ (name))) {
|
|
124
|
+
stderr.write(`[error] usage ledger: unknown name '${name}' (expected: ${LEDGER_NAMES.join(', ')})\n`);
|
|
125
|
+
return 2;
|
|
126
|
+
}
|
|
127
|
+
if (!verb || !['add', 'resolve', 'list'].includes(verb)) {
|
|
128
|
+
stderr.write("[error] usage ledger: expected verb 'add', 'resolve' or 'list'\n");
|
|
129
|
+
return 2;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let parsed;
|
|
133
|
+
try {
|
|
134
|
+
parsed = parseArgs({
|
|
135
|
+
args: argv.slice(2), options: OPTION_SPEC, allowPositionals: true, strict: true,
|
|
136
|
+
});
|
|
137
|
+
} catch (err) {
|
|
138
|
+
stderr.write(`[error] usage ${/** @type {Error} */ (err).message}\n`);
|
|
139
|
+
return 2;
|
|
140
|
+
}
|
|
141
|
+
const flags = parsed.values;
|
|
142
|
+
const positionals = parsed.positionals;
|
|
143
|
+
if (flags.help) { stdout.write(HELP); return 0; }
|
|
144
|
+
|
|
145
|
+
const projectRoot = await findProjectRoot(cwd);
|
|
146
|
+
if (!projectRoot) {
|
|
147
|
+
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');
|
|
148
|
+
return 2;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
if (verb === 'list') {
|
|
153
|
+
return await runList({ projectRoot, name, flags, stdout });
|
|
154
|
+
}
|
|
155
|
+
if (verb === 'add') {
|
|
156
|
+
return await runAdd({ projectRoot, name, flags, stdout, stderr });
|
|
157
|
+
}
|
|
158
|
+
return await runResolve({ projectRoot, name, positionals, flags, stdout, stderr });
|
|
159
|
+
} catch (err) {
|
|
160
|
+
if (err instanceof LedgerError) {
|
|
161
|
+
stderr.write(`[error] ${err.code === 'usage' ? 'usage ' : ''}ledger: ${err.message}\n`);
|
|
162
|
+
return err.code === 'usage' ? 2 : 1;
|
|
163
|
+
}
|
|
164
|
+
stderr.write(`[error] ledger: ${/** @type {Error} */ (err).message}\n`);
|
|
165
|
+
return 1;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* @param {object} args
|
|
171
|
+
* @param {string} args.projectRoot
|
|
172
|
+
* @param {import('../define/ledgers.js').LedgerName} args.name
|
|
173
|
+
* @param {Record<string, unknown>} args.flags
|
|
174
|
+
* @param {NodeJS.WriteStream} args.stdout
|
|
175
|
+
* @returns {Promise<number>}
|
|
176
|
+
*/
|
|
177
|
+
async function runList({ projectRoot, name, flags, stdout }) {
|
|
178
|
+
const body = await loadLedger({ projectRoot, name });
|
|
179
|
+
if (flags.json) {
|
|
180
|
+
stdout.write(`${JSON.stringify({ ledger: name, ...body }, null, 2)}\n`);
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
if (name === 'decisions') {
|
|
184
|
+
stdout.write(renderDecisionsList(body));
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
187
|
+
stdout.write(renderGenericList(name, body));
|
|
188
|
+
return 0;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Render decisions in the numbered #241 format:
|
|
193
|
+
* 1. <question> [status]
|
|
194
|
+
* a) <option a>
|
|
195
|
+
* b) <option b>
|
|
196
|
+
* default: <letter>
|
|
197
|
+
* blocks: <text>
|
|
198
|
+
* answer: <text> (when resolved)
|
|
199
|
+
*
|
|
200
|
+
* @param {Record<string, any>} body
|
|
201
|
+
* @returns {string}
|
|
202
|
+
*/
|
|
203
|
+
export function renderDecisionsList(body) {
|
|
204
|
+
const list = /** @type {any[]} */ (body.decisions ?? []);
|
|
205
|
+
if (list.length === 0) return '(no decisions)\n';
|
|
206
|
+
const lines = [];
|
|
207
|
+
for (const d of list) {
|
|
208
|
+
const status = d.status === 'resolved' ? 'resolved' : 'open';
|
|
209
|
+
lines.push(`${d.id}. ${d.question} [${status}]`);
|
|
210
|
+
for (const opt of d.options ?? []) {
|
|
211
|
+
lines.push(` ${opt.letter}) ${opt.text}`);
|
|
212
|
+
}
|
|
213
|
+
if (d.default != null) lines.push(` default: ${d.default}`);
|
|
214
|
+
if (d.blocks) lines.push(` blocks: ${d.blocks}`);
|
|
215
|
+
if (status === 'resolved' && d.answer != null) lines.push(` answer: ${d.answer}`);
|
|
216
|
+
lines.push('');
|
|
217
|
+
}
|
|
218
|
+
return `${lines.join('\n').trimEnd()}\n`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* @param {import('../define/ledgers.js').LedgerName} name
|
|
223
|
+
* @param {Record<string, any>} body
|
|
224
|
+
* @returns {string}
|
|
225
|
+
*/
|
|
226
|
+
function renderGenericList(name, body) {
|
|
227
|
+
const key = name === 'brief' ? 'statements' : name;
|
|
228
|
+
const list = /** @type {any[]} */ (body[key] ?? []);
|
|
229
|
+
if (list.length === 0) return `(no ${name} entries)\n`;
|
|
230
|
+
const lines = [];
|
|
231
|
+
for (const e of list) {
|
|
232
|
+
const status = e.status === 'resolved' ? 'resolved' : 'open';
|
|
233
|
+
if (name === 'brief') {
|
|
234
|
+
lines.push(`${e.id}. [${e.kind}] ${e.text} (${status})`);
|
|
235
|
+
if (e.source) lines.push(` source: ${e.source}`);
|
|
236
|
+
} else if (name === 'concerns') {
|
|
237
|
+
lines.push(`${e.id}. ${e.concern} on ${e.reqId}: ${e.disposition} (${status})`);
|
|
238
|
+
if (e.reason) lines.push(` reason: ${e.reason}`);
|
|
239
|
+
} else if (name === 'probes') {
|
|
240
|
+
lines.push(`${e.id}. [${e.severity}] ${e.reqId}: ${e.finding} (${status})`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return `${lines.join('\n')}\n`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* @param {object} args
|
|
248
|
+
*/
|
|
249
|
+
async function runAdd({ projectRoot, name, flags, stdout, stderr }) {
|
|
250
|
+
const body = await loadLedger({ projectRoot, name });
|
|
251
|
+
const now = new Date().toISOString();
|
|
252
|
+
|
|
253
|
+
if (name === 'brief') {
|
|
254
|
+
const kind = /** @type {string} */ (flags.kind ?? 'capability');
|
|
255
|
+
if (!BRIEF_KINDS.includes(/** @type {any} */ (kind))) {
|
|
256
|
+
stderr.write(`[error] usage ledger: brief --kind must be one of: ${BRIEF_KINDS.join(', ')}\n`);
|
|
257
|
+
return 2;
|
|
258
|
+
}
|
|
259
|
+
if (flags.from && flags.text) {
|
|
260
|
+
stderr.write('[error] usage ledger: brief add takes --text OR --from, not both\n');
|
|
261
|
+
return 2;
|
|
262
|
+
}
|
|
263
|
+
if (!flags.from && !flags.text) {
|
|
264
|
+
stderr.write('[error] usage ledger: brief add requires --text <text> or --from <path>\n');
|
|
265
|
+
return 2;
|
|
266
|
+
}
|
|
267
|
+
let drafts;
|
|
268
|
+
if (flags.from) {
|
|
269
|
+
const raw = await readFile(String(flags.from), 'utf8');
|
|
270
|
+
drafts = parseBriefFromFile(raw, { kind, source: flags.source ? String(flags.source) : String(flags.from) });
|
|
271
|
+
if (drafts.length === 0) {
|
|
272
|
+
stderr.write(`[error] usage ledger: brief --from ${flags.from} produced no non-empty lines\n`);
|
|
273
|
+
return 2;
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
const entry = { text: String(flags.text), kind };
|
|
277
|
+
if (flags.source) entry.source = String(flags.source);
|
|
278
|
+
drafts = [entry];
|
|
279
|
+
}
|
|
280
|
+
// The intake-scan-against-frozen-statements seam: a later slice
|
|
281
|
+
// calls the intake scans over drafts + the currently-frozen ledger
|
|
282
|
+
// here. This slice mints ids and writes.
|
|
283
|
+
let cur = body;
|
|
284
|
+
const added = [];
|
|
285
|
+
for (const d of drafts) {
|
|
286
|
+
const step = addEntry({ name: 'brief', body: cur, entry: d, now });
|
|
287
|
+
cur = step.body;
|
|
288
|
+
added.push(step.entry);
|
|
289
|
+
}
|
|
290
|
+
await saveLedger({ projectRoot, name: 'brief', body: cur });
|
|
291
|
+
stdout.write(`brief-ledger: added ${added.length} statement(s), ids ${added.map((e) => e.id).join(', ')}\n`);
|
|
292
|
+
return 0;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (name === 'decisions') {
|
|
296
|
+
if (!flags.question) {
|
|
297
|
+
stderr.write('[error] usage ledger: decisions add requires --question <text>\n');
|
|
298
|
+
return 2;
|
|
299
|
+
}
|
|
300
|
+
const options = parseOptionFlags(flags.option);
|
|
301
|
+
if (options.error) {
|
|
302
|
+
stderr.write(`[error] usage ledger: ${options.error}\n`);
|
|
303
|
+
return 2;
|
|
304
|
+
}
|
|
305
|
+
const entry = {
|
|
306
|
+
question: String(flags.question),
|
|
307
|
+
options: options.value,
|
|
308
|
+
default: flags.default ? String(flags.default) : null,
|
|
309
|
+
};
|
|
310
|
+
if (flags.blocks) entry.blocks = String(flags.blocks);
|
|
311
|
+
const step = addEntry({ name: 'decisions', body, entry, now });
|
|
312
|
+
await saveLedger({ projectRoot, name: 'decisions', body: step.body });
|
|
313
|
+
stdout.write(`decisions-ledger: added decision ${step.entry.id}\n`);
|
|
314
|
+
return 0;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (name === 'concerns') {
|
|
318
|
+
if (!flags.req || !flags.concern || !flags.disposition) {
|
|
319
|
+
stderr.write('[error] usage ledger: concerns add requires --req <req-id> --concern <slug> --disposition applied|waived\n');
|
|
320
|
+
return 2;
|
|
321
|
+
}
|
|
322
|
+
if (flags.disposition === 'waived' && !flags.reason) {
|
|
323
|
+
stderr.write('[error] usage ledger: concerns add --disposition waived requires --reason <text>\n');
|
|
324
|
+
return 2;
|
|
325
|
+
}
|
|
326
|
+
const entry = {
|
|
327
|
+
concern: String(flags.concern),
|
|
328
|
+
reqId: String(flags.req),
|
|
329
|
+
disposition: String(flags.disposition),
|
|
330
|
+
};
|
|
331
|
+
if (flags.reason) entry.reason = String(flags.reason);
|
|
332
|
+
const step = addEntry({ name: 'concerns', body, entry, now });
|
|
333
|
+
await saveLedger({ projectRoot, name: 'concerns', body: step.body });
|
|
334
|
+
stdout.write(`concern-ledger: added concern ${step.entry.id}\n`);
|
|
335
|
+
return 0;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// probes
|
|
339
|
+
if (!flags.req || !flags.finding) {
|
|
340
|
+
stderr.write('[error] usage ledger: probes add requires --req <req-id> --finding <text>\n');
|
|
341
|
+
return 2;
|
|
342
|
+
}
|
|
343
|
+
const entry = {
|
|
344
|
+
reqId: String(flags.req),
|
|
345
|
+
finding: String(flags.finding),
|
|
346
|
+
severity: flags.severity ? String(flags.severity) : 'medium',
|
|
347
|
+
};
|
|
348
|
+
const step = addEntry({ name: 'probes', body, entry, now });
|
|
349
|
+
await saveLedger({ projectRoot, name: 'probes', body: step.body });
|
|
350
|
+
stdout.write(`probe-ledger: added probe ${step.entry.id}\n`);
|
|
351
|
+
return 0;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function runResolve({ projectRoot, name, positionals, flags, stdout, stderr }) {
|
|
355
|
+
if (positionals.length !== 1) {
|
|
356
|
+
stderr.write('[error] usage ledger: resolve expects exactly one <id>\n');
|
|
357
|
+
return 2;
|
|
358
|
+
}
|
|
359
|
+
const id = Number(positionals[0]);
|
|
360
|
+
if (!Number.isInteger(id) || id < 1) {
|
|
361
|
+
stderr.write(`[error] usage ledger: resolve id must be a positive integer, got '${positionals[0]}'\n`);
|
|
362
|
+
return 2;
|
|
363
|
+
}
|
|
364
|
+
const body = await loadLedger({ projectRoot, name });
|
|
365
|
+
const now = new Date().toISOString();
|
|
366
|
+
/** @type {Record<string, unknown>} */
|
|
367
|
+
const patch = {};
|
|
368
|
+
if (flags.answer) patch.answer = String(flags.answer);
|
|
369
|
+
if (flags.reason) patch.reason = String(flags.reason);
|
|
370
|
+
if (flags.answer) patch.answeredAt = now;
|
|
371
|
+
const step = resolveEntry({ name, body, id, patch, now });
|
|
372
|
+
await saveLedger({ projectRoot, name, body: step.body });
|
|
373
|
+
stdout.write(`${name}-ledger: resolved ${id}\n`);
|
|
374
|
+
return 0;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Parse repeated `--option letter:text` into option records. Returns
|
|
379
|
+
* `{ value }` on success or `{ error }` on any malformed input.
|
|
380
|
+
*
|
|
381
|
+
* @param {unknown} raw
|
|
382
|
+
* @returns {{ value: Array<{ letter: string, text: string }> } | { error: string }}
|
|
383
|
+
*/
|
|
384
|
+
export function parseOptionFlags(raw) {
|
|
385
|
+
if (!raw || !Array.isArray(raw) || raw.length === 0) {
|
|
386
|
+
return { error: "decisions add requires at least one --option 'a:First option'" };
|
|
387
|
+
}
|
|
388
|
+
const value = [];
|
|
389
|
+
for (const s of raw) {
|
|
390
|
+
if (typeof s !== 'string') return { error: `--option value must be a string, got ${typeof s}` };
|
|
391
|
+
const sep = s.indexOf(':');
|
|
392
|
+
if (sep < 1) return { error: `--option must be 'letter:text', got '${s}'` };
|
|
393
|
+
const letter = s.slice(0, sep).trim();
|
|
394
|
+
const text = s.slice(sep + 1).trim();
|
|
395
|
+
if (!letter || !text) return { error: `--option must be 'letter:text', got '${s}'` };
|
|
396
|
+
value.push({ letter, text });
|
|
397
|
+
}
|
|
398
|
+
return { value };
|
|
399
|
+
}
|
package/src/cli/preflight.js
CHANGED
|
@@ -23,7 +23,7 @@ import { parseArgs } from 'node:util';
|
|
|
23
23
|
import { createInterface } from 'node:readline/promises';
|
|
24
24
|
import process from 'node:process';
|
|
25
25
|
|
|
26
|
-
import { walkTree } from '#core/store';
|
|
26
|
+
import { walkTree, validateComposedRecord } from '#core/store';
|
|
27
27
|
import { writeUnexpectedFailure, rcfError } from '#core/errors';
|
|
28
28
|
|
|
29
29
|
import { findProjectRoot } from '../view/index.js';
|
|
@@ -63,11 +63,18 @@ Options:
|
|
|
63
63
|
--input <path> Non-interactive: read pre-filled session
|
|
64
64
|
--non-interactive Force non-interactive mode (default when
|
|
65
65
|
not on a TTY or when piped)
|
|
66
|
-
--dry-run Print the composed record; do not write
|
|
66
|
+
--dry-run Print the composed record; do not write.
|
|
67
|
+
The composed record is still run through
|
|
68
|
+
the same manifest schema pass the write
|
|
69
|
+
path runs (0.28.2 fix for #232), so a
|
|
70
|
+
preview fails on the same schema misses.
|
|
67
71
|
--json Emit the composed record as JSON to stdout
|
|
68
72
|
--quiet Suppress non-error stdout
|
|
69
73
|
--help Print this help
|
|
70
74
|
|
|
75
|
+
Service id shape: ^[a-z][a-zA-Z0-9]*$ (camelCase, no hyphens; example:
|
|
76
|
+
"stripeCheckout" is valid, "stripe-checkout" is not).
|
|
77
|
+
|
|
71
78
|
Credentials NEVER enter the chain. The session prompts for env var
|
|
72
79
|
NAMES only; values are read from the shell at test / finalise time.
|
|
73
80
|
The name-metadata is written to .rcf/preflight-secrets.local.json,
|
|
@@ -77,7 +84,7 @@ Exit codes:
|
|
|
77
84
|
0 success
|
|
78
85
|
1 IO / unexpected runtime failure
|
|
79
86
|
2 usage error (bad flags, unresolvable PRD id)
|
|
80
|
-
3 schema validation on the composed record
|
|
87
|
+
3 schema validation on the composed record (including --dry-run)
|
|
81
88
|
4 operator cancelled the session at the confirm step
|
|
82
89
|
`;
|
|
83
90
|
|
|
@@ -189,6 +196,20 @@ export async function main(argv, deps = {}) {
|
|
|
189
196
|
});
|
|
190
197
|
|
|
191
198
|
if (flags['dry-run']) {
|
|
199
|
+
// 0.28.2 (issue #232): dry-run now runs the same schema pass the
|
|
200
|
+
// writer runs. The service-id pattern `^[a-z][a-zA-Z0-9]*$` is
|
|
201
|
+
// enforced by the manifest schema on the write path; the preview
|
|
202
|
+
// is no longer allowed to lie about a kebab-case id.
|
|
203
|
+
const dryValidation = validateComposedRecord({
|
|
204
|
+
tree,
|
|
205
|
+
record,
|
|
206
|
+
verb: 'preflight',
|
|
207
|
+
extraRecords: optOuts,
|
|
208
|
+
});
|
|
209
|
+
if (dryValidation) {
|
|
210
|
+
stderr.write(`[error] ${dryValidation.kind} ${dryValidation.message}\n`);
|
|
211
|
+
return 3;
|
|
212
|
+
}
|
|
192
213
|
if (flags.json) stdout.write(`${JSON.stringify({ record, optOuts }, null, 2)}\n`);
|
|
193
214
|
if (!flags.quiet) {
|
|
194
215
|
stdout.write(`[dry-run] preflight would write record ${record.id} (${record.servicesInScope.length} services, ${record.designShapeAnswers?.length ?? 0} design-shape answers, ${optOuts.length} opt-outs)\n`);
|