rcf-lite 0.28.2 → 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 +317 -0
- package/bin/rcf.js +8 -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-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/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-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 +37 -0
- package/rcf/code-nodes/cn-726.json +25 -0
- package/rcf/code-nodes/cn-727.json +23 -0
- package/rcf/code-nodes/cn-728.json +37 -0
- package/rcf/code-nodes/cn-729.json +26 -0
- package/rcf/code-nodes/cn-730.json +24 -0
- 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-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 +45 -0
- package/rcf/fbs/fbs-197.json +41 -0
- 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-001.json +2 -1
- package/rcf/requirements/req-002.json +3 -2
- 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 +31 -0
- package/rcf/requirements/req-175.json +31 -0
- 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-4120-define-delta-and-freeze.json +21 -0
- package/rcf/tacs/tac-4121-define-ledgers.json +35 -0
- package/rcf/tacs/tac-4122-define-gates.json +61 -0
- package/rcf/tacs/tac-4123-define-readiness.json +61 -0
- 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-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 +211 -0
- package/rcf/test-suites/ts-216.json +94 -0
- package/rcf/test-suites/ts-217.json +62 -0
- 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-15601.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-17302.json +50 -0
- package/rcf/user-stories/us-17401.json +70 -0
- package/rcf/user-stories/us-17402.json +55 -0
- package/rcf/user-stories/us-17403.json +60 -0
- package/rcf/user-stories/us-17501.json +48 -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 +31 -1
- package/src/cli/feedback.js +126 -10
- package/src/cli/freeze.js +730 -0
- package/src/cli/help.js +23 -0
- package/src/cli/ledger.js +615 -0
- package/src/cli/questions.js +288 -0
- package/src/cli/readiness.js +498 -0
- package/src/cli/view.js +29 -0
- package/src/core/store/writer.js +86 -6
- package/src/define/freeze-record.js +247 -0
- package/src/define/ledgers.js +712 -0
- package/src/feedback/redact.js +105 -19
- 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 +282 -0
- package/src/query/gates.js +1107 -0
- package/src/query/index.js +50 -0
- package/src/query/questions.js +438 -0
- package/src/query/readiness.js +574 -0
- package/src/server/index.js +60 -1
- package/src/server/routes.js +133 -4
- 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/helpers.js +2 -2
- package/src/view/doc-renderers/index.js +2 -0
- package/src/view/doc-renderers/prd.js +35 -6
- package/src/view/doc-renderers/req.js +10 -2
- package/src/view/doc-renderers/user-story.js +10 -5
- package/src/view/html-page.js +717 -300
- package/src/view/index.js +109 -3
- package/src/view/live-client.js +61 -8
- package/src/view/needs-work.js +79 -0
- package/src/view/page-init.js +2193 -0
- package/src/view/product-map.js +751 -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 +2279 -266
- 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
|
@@ -45,6 +45,10 @@ import { HELP as REQ_BASELINE_HELP } from './req-baseline.js';
|
|
|
45
45
|
import { HELP as INTAKE_HELP } from './intake.js';
|
|
46
46
|
import { HELP as BLUEPRINT_HELP } from './blueprint.js';
|
|
47
47
|
import { HELP as STANDARDS_HELP } from './standards.js';
|
|
48
|
+
import { HELP as LEDGER_HELP } from './ledger.js';
|
|
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';
|
|
48
52
|
|
|
49
53
|
// Verify group members (from the verify-suite subtree).
|
|
50
54
|
import { HELP as VERIFY_RUN_HELP } from '../verify/cli/run.js';
|
|
@@ -93,6 +97,9 @@ define Author the RCF tree.
|
|
|
93
97
|
design <fbs-id> <verb> Design substage for a UI-bearing FBS.
|
|
94
98
|
blueprint <verb> Compose blueprints onto the project.
|
|
95
99
|
standards <verb> Register standards packs against the project.
|
|
100
|
+
ledger <name> <verb> Sidecar-ledger CRUD under rcf/define/.
|
|
101
|
+
readiness DEFINE stage-gate readiness view.
|
|
102
|
+
questions Persona question set for the intent-complete loop.
|
|
96
103
|
|
|
97
104
|
build Drive the FBS queue and the five-stage build cycle.
|
|
98
105
|
queue Show the FBS build queue.
|
|
@@ -189,6 +196,18 @@ Verbs:
|
|
|
189
196
|
design <fbs-id> <verb> Design substage for a UI-bearing FBS.
|
|
190
197
|
blueprint <verb> Compose blueprints onto the project.
|
|
191
198
|
standards <verb> Register standards packs against the project.
|
|
199
|
+
ledger <name> <verb> Sidecar-ledger CRUD under rcf/define/
|
|
200
|
+
(brief | decisions | concerns | probes;
|
|
201
|
+
verbs: add | resolve | list).
|
|
202
|
+
readiness DEFINE stage-gate readiness view; twin of
|
|
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.
|
|
192
211
|
|
|
193
212
|
Run 'rcf help define <verb>' for per-verb help.
|
|
194
213
|
`;
|
|
@@ -284,6 +303,10 @@ export const HELP_MAP = {
|
|
|
284
303
|
design: DESIGN_HELP,
|
|
285
304
|
blueprint: BLUEPRINT_HELP,
|
|
286
305
|
standards: STANDARDS_HELP,
|
|
306
|
+
ledger: LEDGER_HELP,
|
|
307
|
+
readiness: READINESS_HELP,
|
|
308
|
+
questions: QUESTIONS_HELP,
|
|
309
|
+
freeze: FREEZE_HELP,
|
|
287
310
|
},
|
|
288
311
|
build: {
|
|
289
312
|
queue: BUILD_HELP,
|
|
@@ -0,0 +1,615 @@
|
|
|
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
|
+
isResolvedByGrammar,
|
|
20
|
+
loadLedger,
|
|
21
|
+
parseBriefFromFile,
|
|
22
|
+
resolveEntry,
|
|
23
|
+
saveLedger,
|
|
24
|
+
updateEntry,
|
|
25
|
+
} from '../define/ledgers.js';
|
|
26
|
+
import { runIntakeScansOnDelta } from '../intake/orchestrator.js';
|
|
27
|
+
import { findProjectRoot } from '../view/index.js';
|
|
28
|
+
|
|
29
|
+
const OPTION_SPEC = {
|
|
30
|
+
kind: { type: 'string' },
|
|
31
|
+
text: { type: 'string' },
|
|
32
|
+
from: { type: 'string' },
|
|
33
|
+
source: { type: 'string' },
|
|
34
|
+
reason: { type: 'string' },
|
|
35
|
+
answer: { type: 'string' },
|
|
36
|
+
question: { type: 'string' },
|
|
37
|
+
option: { type: 'string', multiple: true },
|
|
38
|
+
default: { type: 'string' },
|
|
39
|
+
blocks: { type: 'string' },
|
|
40
|
+
req: { type: 'string' },
|
|
41
|
+
concern: { type: 'string' },
|
|
42
|
+
disposition: { type: 'string' },
|
|
43
|
+
finding: { type: 'string' },
|
|
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' },
|
|
50
|
+
json: { type: 'boolean' },
|
|
51
|
+
help: { type: 'boolean' },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const HELP = `Usage: rcf define ledger <name> <verb> [options]
|
|
55
|
+
|
|
56
|
+
Ledgers (proposal 2026-09-22 §2.5, §3.2 v3) are sidecars under
|
|
57
|
+
rcf/define/. Four ledgers, each one JSON file, each with one numbered
|
|
58
|
+
sequence for the life of the project.
|
|
59
|
+
|
|
60
|
+
Ledger names:
|
|
61
|
+
brief D1 brief statements (kinded from a closed set)
|
|
62
|
+
decisions D7 open / answered decisions (#241 format)
|
|
63
|
+
concerns D5 (concern, REQ) pairs (applied | waived)
|
|
64
|
+
probes D6 probe findings (severity: low | medium | high)
|
|
65
|
+
|
|
66
|
+
Verbs:
|
|
67
|
+
add Append a new entry, next id
|
|
68
|
+
update <id> Patch named fields on an existing entry
|
|
69
|
+
resolve <id> Flip status to resolved, stamp resolvedAt
|
|
70
|
+
list Print every entry (JSON with --json)
|
|
71
|
+
|
|
72
|
+
Common flags:
|
|
73
|
+
--json Emit machine-readable envelope on list
|
|
74
|
+
--help Print this help
|
|
75
|
+
|
|
76
|
+
brief add flags:
|
|
77
|
+
--kind <${BRIEF_KINDS.join('|')}>
|
|
78
|
+
Statement kind (default: capability)
|
|
79
|
+
--text <text> The statement text (mutually exclusive with --from)
|
|
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
|
|
92
|
+
|
|
93
|
+
decisions add flags:
|
|
94
|
+
--question <text> The decision question (required)
|
|
95
|
+
--option <letter:text> Repeatable option, format 'a:First option'
|
|
96
|
+
--default <letter> Default option letter
|
|
97
|
+
--blocks <text> What this decision blocks (a gate id, doc id)
|
|
98
|
+
|
|
99
|
+
concerns add flags:
|
|
100
|
+
--req <req-id> REQ the (concern, REQ) pair belongs to (required)
|
|
101
|
+
--concern <slug> Concern key (auth, retention, ...) (required)
|
|
102
|
+
--disposition applied|waived
|
|
103
|
+
Disposition (required)
|
|
104
|
+
--reason <text> Reason (mandatory when disposition=waived)
|
|
105
|
+
|
|
106
|
+
probes add flags:
|
|
107
|
+
--req <req-id> REQ the probe finding sits on (required)
|
|
108
|
+
--finding <text> The finding text (required)
|
|
109
|
+
--severity low|medium|high
|
|
110
|
+
|
|
111
|
+
resolve flags:
|
|
112
|
+
--answer <text> On decisions: the answer chosen (a letter, or free text)
|
|
113
|
+
--reason <text> On probes / concerns: the resolution note
|
|
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
|
+
|
|
124
|
+
Notes:
|
|
125
|
+
- Every ledger file lives under rcf/define/. The loader treats missing
|
|
126
|
+
files as empty ledgers; a malformed file is a hard error.
|
|
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.
|
|
134
|
+
- 'decisions list' prints the numbered format from #241: one decision
|
|
135
|
+
per item; options with letters; the default; what it blocks.
|
|
136
|
+
`;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @param {string[]} argv - argv slice after `ledger`
|
|
140
|
+
* @param {object} [deps]
|
|
141
|
+
* @returns {Promise<number>}
|
|
142
|
+
*/
|
|
143
|
+
export async function main(argv, deps = {}) {
|
|
144
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
145
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
146
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
147
|
+
|
|
148
|
+
if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
|
|
149
|
+
stdout.write(HELP);
|
|
150
|
+
return argv.length === 0 ? 2 : 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const name = argv[0];
|
|
154
|
+
const verb = argv[1];
|
|
155
|
+
if (!LEDGER_NAMES.includes(/** @type {any} */ (name))) {
|
|
156
|
+
stderr.write(`[error] usage ledger: unknown name '${name}' (expected: ${LEDGER_NAMES.join(', ')})\n`);
|
|
157
|
+
return 2;
|
|
158
|
+
}
|
|
159
|
+
if (!verb || !['add', 'update', 'resolve', 'list'].includes(verb)) {
|
|
160
|
+
stderr.write("[error] usage ledger: expected verb 'add', 'update', 'resolve' or 'list'\n");
|
|
161
|
+
return 2;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let parsed;
|
|
165
|
+
try {
|
|
166
|
+
parsed = parseArgs({
|
|
167
|
+
args: argv.slice(2), options: OPTION_SPEC, allowPositionals: true, strict: true,
|
|
168
|
+
});
|
|
169
|
+
} catch (err) {
|
|
170
|
+
stderr.write(`[error] usage ${/** @type {Error} */ (err).message}\n`);
|
|
171
|
+
return 2;
|
|
172
|
+
}
|
|
173
|
+
const flags = parsed.values;
|
|
174
|
+
const positionals = parsed.positionals;
|
|
175
|
+
if (flags.help) { stdout.write(HELP); return 0; }
|
|
176
|
+
|
|
177
|
+
const projectRoot = await findProjectRoot(cwd);
|
|
178
|
+
if (!projectRoot) {
|
|
179
|
+
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');
|
|
180
|
+
return 2;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
if (verb === 'list') {
|
|
185
|
+
return await runList({ projectRoot, name, flags, stdout });
|
|
186
|
+
}
|
|
187
|
+
if (verb === 'add') {
|
|
188
|
+
return await runAdd({ projectRoot, name, flags, stdout, stderr });
|
|
189
|
+
}
|
|
190
|
+
if (verb === 'update') {
|
|
191
|
+
return await runUpdate({ projectRoot, name, positionals, flags, stdout, stderr });
|
|
192
|
+
}
|
|
193
|
+
return await runResolve({ projectRoot, name, positionals, flags, stdout, stderr });
|
|
194
|
+
} catch (err) {
|
|
195
|
+
if (err instanceof LedgerError) {
|
|
196
|
+
stderr.write(`[error] ${err.code === 'usage' ? 'usage ' : ''}ledger: ${err.message}\n`);
|
|
197
|
+
return err.code === 'usage' ? 2 : 1;
|
|
198
|
+
}
|
|
199
|
+
stderr.write(`[error] ledger: ${/** @type {Error} */ (err).message}\n`);
|
|
200
|
+
return 1;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* @param {object} args
|
|
206
|
+
* @param {string} args.projectRoot
|
|
207
|
+
* @param {import('../define/ledgers.js').LedgerName} args.name
|
|
208
|
+
* @param {Record<string, unknown>} args.flags
|
|
209
|
+
* @param {NodeJS.WriteStream} args.stdout
|
|
210
|
+
* @returns {Promise<number>}
|
|
211
|
+
*/
|
|
212
|
+
async function runList({ projectRoot, name, flags, stdout }) {
|
|
213
|
+
const body = await loadLedger({ projectRoot, name });
|
|
214
|
+
if (flags.json) {
|
|
215
|
+
stdout.write(`${JSON.stringify({ ledger: name, ...body }, null, 2)}\n`);
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
218
|
+
if (name === 'decisions') {
|
|
219
|
+
stdout.write(renderDecisionsList(body));
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
stdout.write(renderGenericList(name, body));
|
|
223
|
+
return 0;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Render decisions in the numbered #241 format:
|
|
228
|
+
* 1. <question> [status]
|
|
229
|
+
* a) <option a>
|
|
230
|
+
* b) <option b>
|
|
231
|
+
* default: <letter>
|
|
232
|
+
* blocks: <text>
|
|
233
|
+
* answer: <text> (when resolved)
|
|
234
|
+
*
|
|
235
|
+
* @param {Record<string, any>} body
|
|
236
|
+
* @returns {string}
|
|
237
|
+
*/
|
|
238
|
+
export function renderDecisionsList(body) {
|
|
239
|
+
const list = /** @type {any[]} */ (body.decisions ?? []);
|
|
240
|
+
if (list.length === 0) return '(no decisions)\n';
|
|
241
|
+
const lines = [];
|
|
242
|
+
for (const d of list) {
|
|
243
|
+
const status = d.status === 'resolved' ? 'resolved' : 'open';
|
|
244
|
+
lines.push(`${d.id}. ${d.question} [${status}]`);
|
|
245
|
+
for (const opt of d.options ?? []) {
|
|
246
|
+
lines.push(` ${opt.letter}) ${opt.text}`);
|
|
247
|
+
}
|
|
248
|
+
if (d.default != null) lines.push(` default: ${d.default}`);
|
|
249
|
+
if (d.blocks) lines.push(` blocks: ${d.blocks}`);
|
|
250
|
+
if (status === 'resolved' && d.answer != null) lines.push(` answer: ${d.answer}`);
|
|
251
|
+
lines.push('');
|
|
252
|
+
}
|
|
253
|
+
return `${lines.join('\n').trimEnd()}\n`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* @param {import('../define/ledgers.js').LedgerName} name
|
|
258
|
+
* @param {Record<string, any>} body
|
|
259
|
+
* @returns {string}
|
|
260
|
+
*/
|
|
261
|
+
function renderGenericList(name, body) {
|
|
262
|
+
const key = name === 'brief' ? 'statements' : name;
|
|
263
|
+
const list = /** @type {any[]} */ (body[key] ?? []);
|
|
264
|
+
if (list.length === 0) return `(no ${name} entries)\n`;
|
|
265
|
+
const lines = [];
|
|
266
|
+
for (const e of list) {
|
|
267
|
+
const status = e.status === 'resolved' ? 'resolved' : 'open';
|
|
268
|
+
if (name === 'brief') {
|
|
269
|
+
lines.push(`${e.id}. [${e.kind}] ${e.text} (${status})`);
|
|
270
|
+
if (e.source) lines.push(` source: ${e.source}`);
|
|
271
|
+
} else if (name === 'concerns') {
|
|
272
|
+
lines.push(`${e.id}. ${e.concern} on ${e.reqId}: ${e.disposition} (${status})`);
|
|
273
|
+
if (e.reason) lines.push(` reason: ${e.reason}`);
|
|
274
|
+
} else if (name === 'probes') {
|
|
275
|
+
lines.push(`${e.id}. [${e.severity}] ${e.reqId}: ${e.finding} (${status})`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return `${lines.join('\n')}\n`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* @param {object} args
|
|
283
|
+
*/
|
|
284
|
+
async function runAdd({ projectRoot, name, flags, stdout, stderr }) {
|
|
285
|
+
const body = await loadLedger({ projectRoot, name });
|
|
286
|
+
const now = new Date().toISOString();
|
|
287
|
+
|
|
288
|
+
if (name === 'brief') {
|
|
289
|
+
const kind = /** @type {string} */ (flags.kind ?? 'capability');
|
|
290
|
+
if (!BRIEF_KINDS.includes(/** @type {any} */ (kind))) {
|
|
291
|
+
stderr.write(`[error] usage ledger: brief --kind must be one of: ${BRIEF_KINDS.join(', ')}\n`);
|
|
292
|
+
return 2;
|
|
293
|
+
}
|
|
294
|
+
if (flags.from && flags.text) {
|
|
295
|
+
stderr.write('[error] usage ledger: brief add takes --text OR --from, not both\n');
|
|
296
|
+
return 2;
|
|
297
|
+
}
|
|
298
|
+
if (!flags.from && !flags.text) {
|
|
299
|
+
stderr.write('[error] usage ledger: brief add requires --text <text> or --from <path>\n');
|
|
300
|
+
return 2;
|
|
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
|
+
|
|
311
|
+
let drafts;
|
|
312
|
+
if (flags.from) {
|
|
313
|
+
const raw = await readFile(String(flags.from), 'utf8');
|
|
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
|
+
}
|
|
326
|
+
if (drafts.length === 0) {
|
|
327
|
+
stderr.write(`[error] usage ledger: brief --from ${flags.from} produced no non-empty lines\n`);
|
|
328
|
+
return 2;
|
|
329
|
+
}
|
|
330
|
+
} else {
|
|
331
|
+
const entry = { text: String(flags.text), kind };
|
|
332
|
+
if (flags.source) entry.source = String(flags.source);
|
|
333
|
+
drafts = [entry];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Compute what WOULD be minted (for both the real path and dry-run).
|
|
337
|
+
let cur = body;
|
|
338
|
+
const added = [];
|
|
339
|
+
for (const d of drafts) {
|
|
340
|
+
const step = addEntry({ name: 'brief', body: cur, entry: d, now });
|
|
341
|
+
cur = step.body;
|
|
342
|
+
added.push(step.entry);
|
|
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
|
+
|
|
402
|
+
await saveLedger({ projectRoot, name: 'brief', body: cur });
|
|
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
|
+
}
|
|
412
|
+
return 0;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (name === 'decisions') {
|
|
416
|
+
if (!flags.question) {
|
|
417
|
+
stderr.write('[error] usage ledger: decisions add requires --question <text>\n');
|
|
418
|
+
return 2;
|
|
419
|
+
}
|
|
420
|
+
const options = parseOptionFlags(flags.option);
|
|
421
|
+
if (options.error) {
|
|
422
|
+
stderr.write(`[error] usage ledger: ${options.error}\n`);
|
|
423
|
+
return 2;
|
|
424
|
+
}
|
|
425
|
+
const entry = {
|
|
426
|
+
question: String(flags.question),
|
|
427
|
+
options: options.value,
|
|
428
|
+
default: flags.default ? String(flags.default) : null,
|
|
429
|
+
};
|
|
430
|
+
if (flags.blocks) entry.blocks = String(flags.blocks);
|
|
431
|
+
const step = addEntry({ name: 'decisions', body, entry, now });
|
|
432
|
+
await saveLedger({ projectRoot, name: 'decisions', body: step.body });
|
|
433
|
+
stdout.write(`decisions-ledger: added decision ${step.entry.id}\n`);
|
|
434
|
+
return 0;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (name === 'concerns') {
|
|
438
|
+
if (!flags.req || !flags.concern || !flags.disposition) {
|
|
439
|
+
stderr.write('[error] usage ledger: concerns add requires --req <req-id> --concern <slug> --disposition applied|waived\n');
|
|
440
|
+
return 2;
|
|
441
|
+
}
|
|
442
|
+
if (flags.disposition === 'waived' && !flags.reason) {
|
|
443
|
+
stderr.write('[error] usage ledger: concerns add --disposition waived requires --reason <text>\n');
|
|
444
|
+
return 2;
|
|
445
|
+
}
|
|
446
|
+
const entry = {
|
|
447
|
+
concern: String(flags.concern),
|
|
448
|
+
reqId: String(flags.req),
|
|
449
|
+
disposition: String(flags.disposition),
|
|
450
|
+
};
|
|
451
|
+
if (flags.reason) entry.reason = String(flags.reason);
|
|
452
|
+
const step = addEntry({ name: 'concerns', body, entry, now });
|
|
453
|
+
await saveLedger({ projectRoot, name: 'concerns', body: step.body });
|
|
454
|
+
stdout.write(`concern-ledger: added concern ${step.entry.id}\n`);
|
|
455
|
+
return 0;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// probes
|
|
459
|
+
if (!flags.req || !flags.finding) {
|
|
460
|
+
stderr.write('[error] usage ledger: probes add requires --req <req-id> --finding <text>\n');
|
|
461
|
+
return 2;
|
|
462
|
+
}
|
|
463
|
+
const entry = {
|
|
464
|
+
reqId: String(flags.req),
|
|
465
|
+
finding: String(flags.finding),
|
|
466
|
+
severity: flags.severity ? String(flags.severity) : 'medium',
|
|
467
|
+
};
|
|
468
|
+
const step = addEntry({ name: 'probes', body, entry, now });
|
|
469
|
+
await saveLedger({ projectRoot, name: 'probes', body: step.body });
|
|
470
|
+
stdout.write(`probe-ledger: added probe ${step.entry.id}\n`);
|
|
471
|
+
return 0;
|
|
472
|
+
}
|
|
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
|
+
|
|
570
|
+
async function runResolve({ projectRoot, name, positionals, flags, stdout, stderr }) {
|
|
571
|
+
if (positionals.length !== 1) {
|
|
572
|
+
stderr.write('[error] usage ledger: resolve expects exactly one <id>\n');
|
|
573
|
+
return 2;
|
|
574
|
+
}
|
|
575
|
+
const id = Number(positionals[0]);
|
|
576
|
+
if (!Number.isInteger(id) || id < 1) {
|
|
577
|
+
stderr.write(`[error] usage ledger: resolve id must be a positive integer, got '${positionals[0]}'\n`);
|
|
578
|
+
return 2;
|
|
579
|
+
}
|
|
580
|
+
const body = await loadLedger({ projectRoot, name });
|
|
581
|
+
const now = new Date().toISOString();
|
|
582
|
+
/** @type {Record<string, unknown>} */
|
|
583
|
+
const patch = {};
|
|
584
|
+
if (flags.answer) patch.answer = String(flags.answer);
|
|
585
|
+
if (flags.reason) patch.reason = String(flags.reason);
|
|
586
|
+
if (flags.answer) patch.answeredAt = now;
|
|
587
|
+
const step = resolveEntry({ name, body, id, patch, now });
|
|
588
|
+
await saveLedger({ projectRoot, name, body: step.body });
|
|
589
|
+
stdout.write(`${name}-ledger: resolved ${id}\n`);
|
|
590
|
+
return 0;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Parse repeated `--option letter:text` into option records. Returns
|
|
595
|
+
* `{ value }` on success or `{ error }` on any malformed input.
|
|
596
|
+
*
|
|
597
|
+
* @param {unknown} raw
|
|
598
|
+
* @returns {{ value: Array<{ letter: string, text: string }> } | { error: string }}
|
|
599
|
+
*/
|
|
600
|
+
export function parseOptionFlags(raw) {
|
|
601
|
+
if (!raw || !Array.isArray(raw) || raw.length === 0) {
|
|
602
|
+
return { error: "decisions add requires at least one --option 'a:First option'" };
|
|
603
|
+
}
|
|
604
|
+
const value = [];
|
|
605
|
+
for (const s of raw) {
|
|
606
|
+
if (typeof s !== 'string') return { error: `--option value must be a string, got ${typeof s}` };
|
|
607
|
+
const sep = s.indexOf(':');
|
|
608
|
+
if (sep < 1) return { error: `--option must be 'letter:text', got '${s}'` };
|
|
609
|
+
const letter = s.slice(0, sep).trim();
|
|
610
|
+
const text = s.slice(sep + 1).trim();
|
|
611
|
+
if (!letter || !text) return { error: `--option must be 'letter:text', got '${s}'` };
|
|
612
|
+
value.push({ letter, text });
|
|
613
|
+
}
|
|
614
|
+
return { value };
|
|
615
|
+
}
|