rcf-lite 0.0.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +288 -0
- package/LICENSE +202 -0
- package/README.md +92 -4
- package/bin/rcf-verify.js +122 -0
- package/bin/rcf.js +174 -0
- package/bin/view-supervisor-child.mjs +14 -0
- package/fixtures/canary-manifest.json +103 -0
- package/guidance/README.md +25 -0
- package/guidance/build-cycle-playbook.md +492 -0
- package/guidance/build-cycle.md +40 -0
- package/guidance/document-model.md +45 -0
- package/guidance/elicitation-playbook.md +442 -0
- package/guidance/harness-template.md +246 -0
- package/guidance/managed/README.md +63 -0
- package/guidance/managed/agent-instructions-block.hash +1 -0
- package/guidance/managed/agent-instructions-block.md +220 -0
- package/guidance/managed/legacy-fragment-hashes.json +22 -0
- package/guidance/manifest.json +21 -0
- package/guidance/overview.md +48 -0
- package/guidance/persona-programme.md +44 -0
- package/package.json +69 -8
- package/rcf/adrs/adr-001.json +25 -0
- package/rcf/adrs/adr-002.json +25 -0
- package/rcf/adrs/adr-003.json +25 -0
- package/rcf/adrs/adr-004.json +20 -0
- package/rcf/adrs/adr-005.json +20 -0
- package/rcf/adrs/adr-006.json +25 -0
- package/rcf/adrs/adr-007.json +25 -0
- package/rcf/adrs/adr-008.json +25 -0
- package/rcf/adrs/adr-009.json +25 -0
- package/rcf/build-sequence.json +11 -0
- package/rcf/code-nodes/cn-001.json +14 -0
- package/rcf/code-nodes/cn-002.json +14 -0
- package/rcf/code-nodes/cn-003.json +15 -0
- package/rcf/code-nodes/cn-004.json +17 -0
- package/rcf/code-nodes/cn-005.json +19 -0
- package/rcf/code-nodes/cn-006.json +17 -0
- package/rcf/code-nodes/cn-007.json +16 -0
- package/rcf/code-nodes/cn-008.json +19 -0
- package/rcf/code-nodes/cn-009.json +16 -0
- package/rcf/code-nodes/cn-010.json +18 -0
- package/rcf/code-nodes/cn-011.json +18 -0
- package/rcf/code-nodes/cn-012.json +18 -0
- package/rcf/code-nodes/cn-013.json +19 -0
- package/rcf/code-nodes/cn-014.json +16 -0
- package/rcf/code-nodes/cn-015.json +16 -0
- package/rcf/code-nodes/cn-016.json +16 -0
- package/rcf/code-nodes/cn-017.json +16 -0
- package/rcf/code-nodes/cn-018.json +15 -0
- package/rcf/code-nodes/cn-019.json +16 -0
- package/rcf/code-nodes/cn-020.json +16 -0
- package/rcf/code-nodes/cn-021.json +16 -0
- package/rcf/code-nodes/cn-022.json +16 -0
- package/rcf/code-nodes/cn-023.json +16 -0
- package/rcf/code-nodes/cn-024.json +16 -0
- package/rcf/code-nodes/cn-025.json +16 -0
- package/rcf/code-nodes/cn-026.json +16 -0
- package/rcf/code-nodes/cn-027.json +16 -0
- package/rcf/code-nodes/cn-028.json +16 -0
- package/rcf/code-nodes/cn-029.json +19 -0
- package/rcf/code-nodes/cn-030.json +14 -0
- package/rcf/code-nodes/cn-031.json +14 -0
- package/rcf/code-nodes/cn-032.json +14 -0
- package/rcf/code-nodes/cn-033.json +14 -0
- package/rcf/code-nodes/cn-034.json +14 -0
- package/rcf/code-nodes/cn-035.json +14 -0
- package/rcf/code-nodes/cn-036.json +14 -0
- package/rcf/code-nodes/cn-037.json +14 -0
- package/rcf/code-nodes/cn-038.json +14 -0
- package/rcf/code-nodes/cn-039.json +14 -0
- package/rcf/code-nodes/cn-040.json +14 -0
- package/rcf/code-nodes/cn-041.json +14 -0
- package/rcf/code-nodes/cn-042.json +14 -0
- package/rcf/code-nodes/cn-043.json +14 -0
- package/rcf/code-nodes/cn-044.json +14 -0
- package/rcf/code-nodes/cn-045.json +14 -0
- package/rcf/code-nodes/cn-046.json +14 -0
- package/rcf/code-nodes/cn-047.json +14 -0
- package/rcf/code-nodes/cn-048.json +14 -0
- package/rcf/code-nodes/cn-049.json +14 -0
- package/rcf/code-nodes/cn-050.json +14 -0
- package/rcf/code-nodes/cn-051.json +14 -0
- package/rcf/code-nodes/cn-052.json +14 -0
- package/rcf/code-nodes/cn-053.json +14 -0
- package/rcf/code-nodes/cn-054.json +14 -0
- package/rcf/code-nodes/cn-055.json +14 -0
- package/rcf/code-nodes/cn-056.json +14 -0
- package/rcf/code-nodes/cn-057.json +14 -0
- package/rcf/fbs/fbs-001.json +49 -0
- package/rcf/fbs/fbs-002.json +42 -0
- package/rcf/fbs/fbs-003.json +37 -0
- package/rcf/fbs/fbs-004.json +39 -0
- package/rcf/fbs/fbs-005.json +38 -0
- package/rcf/fbs/fbs-006.json +48 -0
- package/rcf/fbs/fbs-007.json +39 -0
- package/rcf/fbs/fbs-008.json +40 -0
- package/rcf/fbs/fbs-009.json +36 -0
- package/rcf/fbs/fbs-010.json +41 -0
- package/rcf/fbs/fbs-011.json +36 -0
- package/rcf/fbs/fbs-012.json +46 -0
- package/rcf/fbs/fbs-013.json +42 -0
- package/rcf/fbs/fbs-014.json +49 -0
- package/rcf/fbs/fbs-015.json +32 -0
- package/rcf/manifest.json +17 -0
- package/rcf/prd.json +47 -0
- package/rcf/requirements/req-001.json +19 -0
- package/rcf/requirements/req-002.json +19 -0
- package/rcf/requirements/req-003.json +19 -0
- package/rcf/requirements/req-004.json +19 -0
- package/rcf/requirements/req-005.json +19 -0
- package/rcf/requirements/req-006.json +19 -0
- package/rcf/requirements/req-007.json +19 -0
- package/rcf/requirements/req-008.json +19 -0
- package/rcf/requirements/req-009.json +19 -0
- package/rcf/tacs/tac-001.json +45 -0
- package/rcf/tacs/tac-002.json +109 -0
- package/rcf/tacs/tac-003.json +40 -0
- package/rcf/tacs/tac-004.json +51 -0
- package/rcf/tacs/tac-005.json +52 -0
- package/rcf/tacs/tac-006.json +104 -0
- package/rcf/tacs/tac-007.json +38 -0
- package/rcf/tacs/tac-008.json +51 -0
- package/rcf/tad.json +51 -0
- package/rcf/test-suites/PENDING.md +23 -0
- package/rcf/test-suites/ts-001.json +38 -0
- package/rcf/test-suites/ts-002.json +38 -0
- package/rcf/test-suites/ts-003.json +43 -0
- package/rcf/test-suites/ts-004.json +44 -0
- package/rcf/test-suites/ts-005.json +30 -0
- package/rcf/test-suites/ts-006.json +36 -0
- package/rcf/test-suites/ts-007.json +43 -0
- package/rcf/test-suites/ts-008.json +37 -0
- package/rcf/test-suites/ts-009.json +38 -0
- package/rcf/test-suites/ts-010.json +38 -0
- package/rcf/test-suites/ts-011.json +44 -0
- package/rcf/test-suites/ts-012.json +36 -0
- package/rcf/test-suites/ts-013.json +38 -0
- package/rcf/test-suites/ts-014.json +38 -0
- package/rcf/test-suites/ts-015.json +38 -0
- package/rcf/test-suites/ts-016.json +37 -0
- package/rcf/test-suites/ts-017.json +38 -0
- package/rcf/test-suites/ts-018.json +38 -0
- package/rcf/test-suites/ts-019.json +37 -0
- package/rcf/test-suites/ts-020.json +46 -0
- package/rcf/test-suites/ts-021.json +46 -0
- package/rcf/test-suites/ts-022.json +46 -0
- package/rcf/test-suites/ts-023.json +46 -0
- package/rcf/test-suites/ts-024.json +46 -0
- package/rcf/test-suites/ts-025.json +52 -0
- package/rcf/user-stories/us-101.json +40 -0
- package/rcf/user-stories/us-102.json +40 -0
- package/rcf/user-stories/us-201.json +40 -0
- package/rcf/user-stories/us-202.json +40 -0
- package/rcf/user-stories/us-203.json +32 -0
- package/rcf/user-stories/us-301.json +40 -0
- package/rcf/user-stories/us-302.json +40 -0
- package/rcf/user-stories/us-303.json +40 -0
- package/rcf/user-stories/us-304.json +40 -0
- package/rcf/user-stories/us-401.json +40 -0
- package/rcf/user-stories/us-402.json +40 -0
- package/rcf/user-stories/us-403.json +40 -0
- package/rcf/user-stories/us-501.json +40 -0
- package/rcf/user-stories/us-502.json +40 -0
- package/rcf/user-stories/us-503.json +40 -0
- package/rcf/user-stories/us-601.json +40 -0
- package/rcf/user-stories/us-602.json +40 -0
- package/rcf/user-stories/us-701.json +40 -0
- package/rcf/user-stories/us-702.json +40 -0
- package/rcf/user-stories/us-801.json +49 -0
- package/rcf/user-stories/us-802.json +49 -0
- package/rcf/user-stories/us-803.json +49 -0
- package/rcf/user-stories/us-804.json +49 -0
- package/rcf/user-stories/us-805.json +49 -0
- package/rcf/user-stories/us-901.json +40 -0
- package/src/.gitkeep +0 -0
- package/src/browser-verify/auth-smoke.js +109 -0
- package/src/browser-verify/index.js +29 -0
- package/src/browser-verify/invariants.js +336 -0
- package/src/browser-verify/manifest-writer.js +189 -0
- package/src/browser-verify/runner.js +171 -0
- package/src/build/bundle.js +198 -0
- package/src/build/formatters/json.js +18 -0
- package/src/build/formatters/markdown.js +406 -0
- package/src/build/index.js +14 -0
- package/src/build/mark.js +177 -0
- package/src/build/queue.js +285 -0
- package/src/cli/browser-verify.js +231 -0
- package/src/cli/build.js +584 -0
- package/src/cli/coverage.js +219 -0
- package/src/cli/create.js +355 -0
- package/src/cli/delete.js +127 -0
- package/src/cli/design.js +353 -0
- package/src/cli/doctor.js +548 -0
- package/src/cli/fbs.js +171 -0
- package/src/cli/finalise.js +377 -0
- package/src/cli/guidance.js +206 -0
- package/src/cli/help.js +156 -0
- package/src/cli/impact.js +119 -0
- package/src/cli/init.js +282 -0
- package/src/cli/intake.js +153 -0
- package/src/cli/link.js +128 -0
- package/src/cli/mcp.js +160 -0
- package/src/cli/preflight.js +220 -0
- package/src/cli/read.js +162 -0
- package/src/cli/req-baseline.js +269 -0
- package/src/cli/req-classify.js +135 -0
- package/src/cli/review.js +295 -0
- package/src/cli/test-suite.js +216 -0
- package/src/cli/trace.js +172 -0
- package/src/cli/ui-baseline.js +292 -0
- package/src/cli/ui-classify.js +108 -0
- package/src/cli/update.js +197 -0
- package/src/cli/validate.js +168 -0
- package/src/cli/view.js +452 -0
- package/src/core/baseline-catalog/data/auth.json +42 -0
- package/src/core/baseline-catalog/data/http-api.json +42 -0
- package/src/core/baseline-catalog/data/notifications.json +33 -0
- package/src/core/baseline-catalog/data/persistence.json +33 -0
- package/src/core/baseline-catalog/data/web-ui.json +60 -0
- package/src/core/baseline-catalog/index.js +121 -0
- package/src/core/errors/index.js +167 -0
- package/src/core/fixtures/register-canary/canary-prompt-01.json +18 -0
- package/src/core/fixtures/register-canary/canary-prompt-02.json +21 -0
- package/src/core/fixtures/register-canary/canary-prompt-03.json +17 -0
- package/src/core/isolation/index.js +60 -0
- package/src/core/mcp/framing.js +103 -0
- package/src/core/mcp/index.js +8 -0
- package/src/core/mcp/server.js +228 -0
- package/src/core/patterns/register-canary.js +209 -0
- package/src/core/patterns/req-shapes.js +158 -0
- package/src/core/patterns/services.js +358 -0
- package/src/core/patterns/ui-shapes.js +166 -0
- package/src/core/store/cn-resolve.js +134 -0
- package/src/core/store/derive-deps.js +93 -0
- package/src/core/store/ids.js +78 -0
- package/src/core/store/index.js +20 -0
- package/src/core/store/init.js +255 -0
- package/src/core/store/loader.js +211 -0
- package/src/core/store/tp-resolve.js +176 -0
- package/src/core/store/validator.js +191 -0
- package/src/core/store/walker.js +898 -0
- package/src/core/store/writer.js +1849 -0
- package/src/design/index.js +11 -0
- package/src/design/writer.js +271 -0
- package/src/finalise/detect.js +129 -0
- package/src/finalise/index.js +17 -0
- package/src/finalise/ingest.js +119 -0
- package/src/finalise/install.js +119 -0
- package/src/finalise/ship-without-verified.js +131 -0
- package/src/finalise/spawn.js +84 -0
- package/src/intake/fidelity.js +105 -0
- package/src/intake/index.js +6 -0
- package/src/intake/manifest-writer.js +100 -0
- package/src/intake/orchestrator.js +138 -0
- package/src/intake/validate.js +80 -0
- package/src/mcp/map-errors.js +131 -0
- package/src/mcp/prompts.js +49 -0
- package/src/mcp/resources.js +244 -0
- package/src/mcp/tools.js +1204 -0
- package/src/preflight/design-shapes.js +185 -0
- package/src/preflight/index.js +90 -0
- package/src/preflight/manifest-writer.js +264 -0
- package/src/preflight/scanner.js +206 -0
- package/src/preflight/secrets.js +134 -0
- package/src/preflight/session.js +246 -0
- package/src/query/attestation.js +285 -0
- package/src/query/coverage.js +308 -0
- package/src/query/formatters/json.js +21 -0
- package/src/query/formatters/mermaid.js +209 -0
- package/src/query/formatters/table.js +203 -0
- package/src/query/impact.js +173 -0
- package/src/query/index.js +9 -0
- package/src/query/trace.js +345 -0
- package/src/register-canary/fixture-loader.js +87 -0
- package/src/register-canary/index.js +10 -0
- package/src/register-canary/record-writer.js +132 -0
- package/src/register-canary/runner.js +156 -0
- package/src/req-baseline/gate.js +86 -0
- package/src/req-baseline/index.js +27 -0
- package/src/req-baseline/open-candidates.js +143 -0
- package/src/req-baseline/opt-out.js +195 -0
- package/src/req-baseline/sweep.js +230 -0
- package/src/req-detection/classifier.js +181 -0
- package/src/req-detection/index.js +9 -0
- package/src/req-detection/persist.js +55 -0
- package/src/review/index.js +325 -0
- package/src/review/mutation.js +117 -0
- package/src/review/ui-baseline-drift.js +138 -0
- package/src/server/index.js +178 -0
- package/src/server/routes.js +110 -0
- package/src/server/sse.js +118 -0
- package/src/setup/agent-setup.js +362 -0
- package/src/setup/identity-seed.js +104 -0
- package/src/setup/knowledge-seed.js +123 -0
- package/src/setup/managed-block.js +193 -0
- package/src/setup/managed-gitignore.js +166 -0
- package/src/setup/managed-markers.js +49 -0
- package/src/ui-baseline/defaults.js +119 -0
- package/src/ui-baseline/index.js +25 -0
- package/src/ui-baseline/manifest-writer.js +282 -0
- package/src/ui-baseline/session.js +178 -0
- package/src/ui-detection/classifier.js +192 -0
- package/src/verify/chain/index.js +190 -0
- package/src/verify/cli/cleanup.js +61 -0
- package/src/verify/cli/help.js +56 -0
- package/src/verify/cli/mcp.js +98 -0
- package/src/verify/cli/provision.js +71 -0
- package/src/verify/cli/report.js +71 -0
- package/src/verify/cli/run.js +155 -0
- package/src/verify/engine/brief.js +87 -0
- package/src/verify/engine/index.js +177 -0
- package/src/verify/engine/launcher.js +307 -0
- package/src/verify/mcp/tools.js +107 -0
- package/src/verify/profile/index.js +146 -0
- package/src/verify/provision/index.js +256 -0
- package/src/verify/report/index.js +139 -0
- package/src/verify/report/renderer.js +118 -0
- package/src/verify/verdict/index.js +246 -0
- package/src/view/doc-renderers/adr.js +44 -0
- package/src/view/doc-renderers/build-sequence.js +40 -0
- package/src/view/doc-renderers/fbs.js +128 -0
- package/src/view/doc-renderers/helpers.js +159 -0
- package/src/view/doc-renderers/index.js +12 -0
- package/src/view/doc-renderers/prd.js +45 -0
- package/src/view/doc-renderers/req.js +43 -0
- package/src/view/doc-renderers/tac.js +38 -0
- package/src/view/doc-renderers/tad.js +74 -0
- package/src/view/doc-renderers/test-suite.js +45 -0
- package/src/view/doc-renderers/user-story.js +63 -0
- package/src/view/html-page.js +462 -0
- package/src/view/index.js +63 -0
- package/src/view/live-client.js +338 -0
- package/src/view/mermaid-diagram.js +178 -0
- package/src/view/style.css +735 -0
- package/src/view/tree-model.js +152 -0
- package/src/view/vendored/mermaid.min.js +2607 -0
- package/src/view-supervisor/index.js +26 -0
- package/src/view-supervisor/logs.js +32 -0
- package/src/view-supervisor/manifest-writer.js +178 -0
- package/src/view-supervisor/persist-until.js +85 -0
- package/src/view-supervisor/supervisor.js +276 -0
- package/src/watch/index.js +152 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// `rcf ui-classify <fbs-id>` subcommand handler
|
|
2
|
+
// (ui-design-gate-0.7.0-spec §4.2).
|
|
3
|
+
//
|
|
4
|
+
// Runs the UI-bearing classifier on demand and prints the verdict. No
|
|
5
|
+
// side effects: the classifier does not write to the FBS document; the
|
|
6
|
+
// operator ratifies via `rcf update <fbs-id> --set uiBearing=true|false`
|
|
7
|
+
// when they want the verdict recorded (§4.4).
|
|
8
|
+
|
|
9
|
+
import { parseArgs } from 'node:util';
|
|
10
|
+
import process from 'node:process';
|
|
11
|
+
|
|
12
|
+
import { walkTree } from '#core/store';
|
|
13
|
+
|
|
14
|
+
import { findProjectRoot } from '../view/index.js';
|
|
15
|
+
import { classifyFbs } from '../ui-detection/classifier.js';
|
|
16
|
+
|
|
17
|
+
const OPTION_SPEC = {
|
|
18
|
+
json: { type: 'boolean' },
|
|
19
|
+
help: { type: 'boolean' },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const HELP = `Usage: rcf ui-classify <fbs-id> [options]
|
|
23
|
+
|
|
24
|
+
Run the UI-bearing classifier on one FBS and print the verdict. Does
|
|
25
|
+
not write to the FBS document; ratify with:
|
|
26
|
+
rcf update <fbs-id> --set uiBearing=true
|
|
27
|
+
|
|
28
|
+
Options:
|
|
29
|
+
--json Emit the uiClassification block as JSON.
|
|
30
|
+
--help Print this help.
|
|
31
|
+
|
|
32
|
+
Exit codes:
|
|
33
|
+
0 classified (verdict printed)
|
|
34
|
+
1 IO / unexpected runtime failure
|
|
35
|
+
2 usage error
|
|
36
|
+
3 tree validation failure
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {string[]} argv
|
|
41
|
+
* @param {object} [deps]
|
|
42
|
+
* @returns {Promise<number>}
|
|
43
|
+
*/
|
|
44
|
+
export async function main(argv, deps = {}) {
|
|
45
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
46
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
47
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
48
|
+
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = parseArgs({ args: argv, options: OPTION_SPEC, allowPositionals: true, strict: true });
|
|
52
|
+
} catch (err) {
|
|
53
|
+
stderr.write(`[error] usage ${err.message}\n`);
|
|
54
|
+
stderr.write(HELP);
|
|
55
|
+
return 2;
|
|
56
|
+
}
|
|
57
|
+
const flags = parsed.values;
|
|
58
|
+
if (flags.help) { stdout.write(HELP); return 0; }
|
|
59
|
+
if (parsed.positionals.length !== 1) {
|
|
60
|
+
stderr.write('[error] usage ui-classify: expected exactly one <fbs-id> positional\n');
|
|
61
|
+
return 2;
|
|
62
|
+
}
|
|
63
|
+
const fbsId = parsed.positionals[0];
|
|
64
|
+
if (!/^FBS-\d{3,}$/.test(fbsId)) {
|
|
65
|
+
stderr.write(`[error] usage ui-classify: expected an FBS id like FBS-011, got '${fbsId}'\n`);
|
|
66
|
+
return 2;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const projectRoot = await findProjectRoot(cwd);
|
|
70
|
+
if (!projectRoot) {
|
|
71
|
+
stderr.write('[error] usage no project root found (no rcf/manifest.json in this directory or any ancestor). Run `rcf init` first.\n');
|
|
72
|
+
return 2;
|
|
73
|
+
}
|
|
74
|
+
const { tree, errors } = await walkTree({ projectRoot });
|
|
75
|
+
if (errors.length > 0) {
|
|
76
|
+
stderr.write(`[warn] tree has ${errors.length} pre-existing issue(s); classification proceeds against what could be loaded\n`);
|
|
77
|
+
}
|
|
78
|
+
if (!tree.byId.has(fbsId) || tree.kindById.get(fbsId) !== 'fbs') {
|
|
79
|
+
stderr.write(`[error] usage ui-classify: ${fbsId} not found or not an FBS\n`);
|
|
80
|
+
return 2;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const now = deps.now ? new Date(deps.now()) : new Date();
|
|
84
|
+
const block = classifyFbs(tree, fbsId, { now });
|
|
85
|
+
|
|
86
|
+
if (flags.json) {
|
|
87
|
+
stdout.write(`${JSON.stringify(block, null, 2)}\n`);
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
stdout.write(`ui-classify ${fbsId}: verdict=${block.verdict} reason=${block.reason}\n`);
|
|
92
|
+
const signals = Array.isArray(block.signals) ? block.signals : [];
|
|
93
|
+
if (signals.length > 0) {
|
|
94
|
+
stdout.write(` signals (${signals.length}):\n`);
|
|
95
|
+
for (const s of signals) {
|
|
96
|
+
const anchor = s.acId ? `${s.source}[${s.acId}]` : s.source;
|
|
97
|
+
stdout.write(` - ${anchor}: ${s.match}\n`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (block.verdict === 'ui') {
|
|
101
|
+
stdout.write(' Ratify with: rcf update ' + fbsId + ' --set uiBearing=true\n');
|
|
102
|
+
} else if (block.verdict === 'notUi' && signals.length === 0) {
|
|
103
|
+
stdout.write(' No UI signals detected. Override with: rcf update ' + fbsId + ' --set uiBearing=true\n');
|
|
104
|
+
} else if (block.verdict === 'operatorOverride') {
|
|
105
|
+
stdout.write(' Operator ruling recorded on FBS.uiBearing wins over the classifier.\n');
|
|
106
|
+
}
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// `rcf update <id>` subcommand handler. Parses --set dot-path lists into
|
|
2
|
+
// a patch array, optionally deep-merges a --from-file body, and calls
|
|
3
|
+
// writer.updateDocument. Phase 4 §D8 (with inline-id + root-singleton
|
|
4
|
+
// support).
|
|
5
|
+
|
|
6
|
+
import { readFile } from 'node:fs/promises';
|
|
7
|
+
import { parseArgs } from 'node:util';
|
|
8
|
+
|
|
9
|
+
import { isRcfError, writeUnexpectedFailure } from '#core/errors';
|
|
10
|
+
import { splitCnPath, updateDocument, walkTree } from '#core/store';
|
|
11
|
+
import { deriveFileDeps, mapDerivedDepsToCnIds } from '#core/store/derive-deps.js';
|
|
12
|
+
import { findProjectRoot } from '../view/index.js';
|
|
13
|
+
// Track C+D §4.4: reclassify a REQ when its description-carrying fields
|
|
14
|
+
// change so downstream tooling sees a fresh shapeClassification.
|
|
15
|
+
import { classifyAndPersistReq } from '../req-detection/index.js';
|
|
16
|
+
|
|
17
|
+
const OPTION_SPEC = {
|
|
18
|
+
set: { type: 'string', multiple: true },
|
|
19
|
+
'from-file': { type: 'string' },
|
|
20
|
+
json: { type: 'boolean' },
|
|
21
|
+
'dry-run': { type: 'boolean' },
|
|
22
|
+
quiet: { type: 'boolean' },
|
|
23
|
+
help: { type: 'boolean' },
|
|
24
|
+
// Phase 10 (X2 CodeNode bridge, D5): re-derive a CN's file-level
|
|
25
|
+
// dependencies via dependency-cruiser, merged into dependencies[].
|
|
26
|
+
'derive-deps': { type: 'boolean' },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const HELP = `Usage: rcf update <id> [options]
|
|
30
|
+
|
|
31
|
+
Options:
|
|
32
|
+
--set <dotPath>=<value> Set a field; repeatable
|
|
33
|
+
--from-file <path> Merge body fields from a JSON file
|
|
34
|
+
(deep merge; arrays replace)
|
|
35
|
+
--json Parse --set values as JSON (default: string)
|
|
36
|
+
--dry-run Print intended writes without executing
|
|
37
|
+
--quiet Suppress non-error stdout
|
|
38
|
+
--derive-deps CN only: re-derive file-level dependencies via
|
|
39
|
+
dependency-cruiser and merge into dependencies[]
|
|
40
|
+
(dev-time assist; never a runtime dependency)
|
|
41
|
+
--help Print this help
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {string[]} argv - argv slice after `update`
|
|
46
|
+
* @param {object} [deps]
|
|
47
|
+
* @returns {Promise<number>}
|
|
48
|
+
*/
|
|
49
|
+
export async function main(argv, deps = {}) {
|
|
50
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
51
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
52
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
53
|
+
|
|
54
|
+
let parsed;
|
|
55
|
+
try {
|
|
56
|
+
parsed = parseArgs({ args: argv, options: OPTION_SPEC, allowPositionals: true, strict: true });
|
|
57
|
+
} catch (err) {
|
|
58
|
+
stderr.write(`[error] usage ${err.message}\n`);
|
|
59
|
+
stderr.write(HELP);
|
|
60
|
+
return 2;
|
|
61
|
+
}
|
|
62
|
+
const flags = parsed.values;
|
|
63
|
+
const positionals = parsed.positionals;
|
|
64
|
+
if (flags.help) { stdout.write(HELP); return 0; }
|
|
65
|
+
if (positionals.length !== 1) {
|
|
66
|
+
stderr.write('[error] usage update: expected exactly one <id>\n');
|
|
67
|
+
stderr.write(HELP);
|
|
68
|
+
return 2;
|
|
69
|
+
}
|
|
70
|
+
const id = positionals[0];
|
|
71
|
+
|
|
72
|
+
const projectRoot = await findProjectRoot(cwd);
|
|
73
|
+
if (!projectRoot) {
|
|
74
|
+
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');
|
|
75
|
+
return 2;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const walkResult = await walkTree({ projectRoot });
|
|
79
|
+
// B5: pre-existing tree breakage no longer blocks write verbs - the
|
|
80
|
+
// update is gated on the POST-write tree state inside the writer, so
|
|
81
|
+
// repairing a broken doc is exactly what this verb is now for.
|
|
82
|
+
if (walkResult.errors.length > 0) {
|
|
83
|
+
stderr.write(`[warn] tree has ${walkResult.errors.length} pre-existing issue(s); proceeding - writes are validated against the post-write state (run 'rcf validate' for details)\n`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const sets = [];
|
|
87
|
+
const useJson = Boolean(flags.json);
|
|
88
|
+
for (const rawSet of flags.set ?? []) {
|
|
89
|
+
const eq = rawSet.indexOf('=');
|
|
90
|
+
if (eq < 0) {
|
|
91
|
+
stderr.write(`[error] usage update: bad --set '${rawSet}' (expected <path>=<value>)\n`);
|
|
92
|
+
return 2;
|
|
93
|
+
}
|
|
94
|
+
const path = rawSet.slice(0, eq);
|
|
95
|
+
const raw = rawSet.slice(eq + 1);
|
|
96
|
+
let value = raw;
|
|
97
|
+
if (useJson) {
|
|
98
|
+
try { value = JSON.parse(raw); } catch (err) {
|
|
99
|
+
stderr.write(`[error] usage update: --json parse failed on '${raw}': ${err.message}\n`);
|
|
100
|
+
return 2;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
sets.push({ path, value });
|
|
104
|
+
}
|
|
105
|
+
let patch = null;
|
|
106
|
+
if (flags['from-file']) {
|
|
107
|
+
try {
|
|
108
|
+
patch = JSON.parse(await readFile(flags['from-file'], 'utf8'));
|
|
109
|
+
} catch (err) {
|
|
110
|
+
stderr.write(`[error] usage update: cannot read --from-file: ${err.message}\n`);
|
|
111
|
+
return 2;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (flags['derive-deps']) {
|
|
115
|
+
if (walkResult.tree.kindById.get(id) !== 'codeNode') {
|
|
116
|
+
stderr.write('[error] usage update: --derive-deps only applies to cn ids\n');
|
|
117
|
+
return 2;
|
|
118
|
+
}
|
|
119
|
+
const setPath = sets.find((s) => s.path === 'path')?.value;
|
|
120
|
+
const currentPath = typeof setPath === 'string' ? setPath : walkResult.tree.byId.get(id)?.path;
|
|
121
|
+
const { file } = splitCnPath(currentPath ?? '');
|
|
122
|
+
const derived = await deriveFileDeps({ projectRoot, filePath: file });
|
|
123
|
+
if (!derived.ok) {
|
|
124
|
+
stderr.write(`[error] usage update: --derive-deps: ${derived.message}\n`);
|
|
125
|
+
return 2;
|
|
126
|
+
}
|
|
127
|
+
const { cnIds, unmatched } = mapDerivedDepsToCnIds(walkResult.tree, derived.deps);
|
|
128
|
+
const existing = walkResult.tree.byId.get(id)?.dependencies ?? [];
|
|
129
|
+
const merged = [...new Set([...existing, ...cnIds])].filter((d) => d !== id).sort();
|
|
130
|
+
sets.push({ path: 'dependencies', value: merged });
|
|
131
|
+
if (unmatched.length > 0 && !flags.quiet) {
|
|
132
|
+
stdout.write(`[info] --derive-deps: ${unmatched.length} file-level import(s) have no matching CN yet, skipped: ${unmatched.join(', ')}\n`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (sets.length === 0 && !patch) {
|
|
136
|
+
stderr.write('[error] usage update: at least one --set or --from-file is required\n');
|
|
137
|
+
return 2;
|
|
138
|
+
}
|
|
139
|
+
const result = await updateDocument({
|
|
140
|
+
projectRoot, tree: walkResult.tree, id, patch, sets,
|
|
141
|
+
options: { dryRun: Boolean(flags['dry-run']) },
|
|
142
|
+
walkErrors: walkResult.errors,
|
|
143
|
+
});
|
|
144
|
+
if (isRcfError(result)) return handleWriterError(result, stderr);
|
|
145
|
+
if (flags['dry-run']) {
|
|
146
|
+
if (!flags.quiet) stdout.write(`[dry-run] would update ${result.id} at ${result.filePath}\n`);
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
if (!flags.quiet) stdout.write(`${result.id} updated at ${result.filePath}\n`);
|
|
150
|
+
|
|
151
|
+
// Track C+D §4.4: reclassify a REQ when the update touched a
|
|
152
|
+
// shape-signal-carrying field (title / description / rationale).
|
|
153
|
+
const touched = new Set();
|
|
154
|
+
for (const s of sets) {
|
|
155
|
+
const head = String(s.path ?? '').split('.')[0];
|
|
156
|
+
if (head) touched.add(head);
|
|
157
|
+
}
|
|
158
|
+
if (patch && typeof patch === 'object') {
|
|
159
|
+
for (const key of Object.keys(patch)) touched.add(key);
|
|
160
|
+
}
|
|
161
|
+
const kindOfDoc = walkResult.tree.kindById.get(id);
|
|
162
|
+
const shouldReclassify = kindOfDoc === 'requirement'
|
|
163
|
+
&& (touched.has('title') || touched.has('description') || touched.has('rationale'));
|
|
164
|
+
if (shouldReclassify) {
|
|
165
|
+
try {
|
|
166
|
+
const postWalk = await walkTree({ projectRoot });
|
|
167
|
+
const classifyOutcome = await classifyAndPersistReq({
|
|
168
|
+
projectRoot,
|
|
169
|
+
tree: postWalk.tree,
|
|
170
|
+
reqId: id,
|
|
171
|
+
});
|
|
172
|
+
if (classifyOutcome && classifyOutcome.ok === false) {
|
|
173
|
+
stderr.write(`[warn] ${classifyOutcome.message}\n`);
|
|
174
|
+
} else if (!flags.quiet && classifyOutcome?.changed && classifyOutcome.block?.shapes?.length > 0) {
|
|
175
|
+
const shapes = classifyOutcome.block.shapes.join(', ');
|
|
176
|
+
stdout.write(`${id} shapeClassification: [${shapes}] (${classifyOutcome.block.reason})\n`);
|
|
177
|
+
}
|
|
178
|
+
} catch (err) {
|
|
179
|
+
stderr.write(`[warn] req-classify (post-update): ${err.message}\n`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function handleWriterError(err, stderr) {
|
|
186
|
+
const kind = err.kind;
|
|
187
|
+
// BUG-007 fix: spec §D15 mandates exit-1 emit
|
|
188
|
+
// `[rcf] unexpected failure: <msg>\n<stack>` — even under --quiet.
|
|
189
|
+
if (kind === 'ioFailure') {
|
|
190
|
+
writeUnexpectedFailure(err, stderr);
|
|
191
|
+
return 1;
|
|
192
|
+
}
|
|
193
|
+
stderr.write(`[error] ${kind} ${err.message}\n`);
|
|
194
|
+
if (kind === 'usage') return 2;
|
|
195
|
+
if (kind === 'validation' || kind === 'brokenReference') return 3;
|
|
196
|
+
return 1;
|
|
197
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// `rcf validate` subcommand handler. Wraps walker + validator + broken-
|
|
2
|
+
// reference machinery. Exits 0 clean, 2 on usage failure, 3 on validation
|
|
3
|
+
// or broken-references. Phase 4 §D3.
|
|
4
|
+
|
|
5
|
+
import { parseArgs } from 'node:util';
|
|
6
|
+
|
|
7
|
+
import { formatErrors } from '#core/errors';
|
|
8
|
+
import { checkCodeNodeResolution, walkTree } from '#core/store';
|
|
9
|
+
import { findProjectRoot } from '../view/index.js';
|
|
10
|
+
|
|
11
|
+
const OPTION_SPEC = {
|
|
12
|
+
quiet: { type: 'boolean' },
|
|
13
|
+
json: { type: 'boolean' },
|
|
14
|
+
help: { type: 'boolean' },
|
|
15
|
+
// Phase 10 (X2 CodeNode bridge, spec D8): skip the Code Node staleness
|
|
16
|
+
// pass (spec-graph checks only). The default remains full validation -
|
|
17
|
+
// the staleCode floor is only a floor if it runs by default.
|
|
18
|
+
'no-code': { type: 'boolean' },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const HELP = `Usage: rcf validate [options]
|
|
22
|
+
|
|
23
|
+
Walk the rcf/ tree and report schema-validation and broken-reference
|
|
24
|
+
issues. Exits 0 when clean, 3 on issues.
|
|
25
|
+
|
|
26
|
+
Options:
|
|
27
|
+
--quiet Only summary line + first 3 issues
|
|
28
|
+
--json Emit machine-readable envelope
|
|
29
|
+
--no-code Skip the Code Node staleness pass (spec-graph
|
|
30
|
+
checks only; the default runs full validation)
|
|
31
|
+
--help Print this help
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
// Case-insensitive whole-word TODO match. Word-bounded so incidental
|
|
35
|
+
// substrings (e.g. "autodoc") do not trip the scan.
|
|
36
|
+
const TODO_RE = /\btodo\b/i;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Recursively collect dot-paths of string fields containing TODO
|
|
40
|
+
* placeholder text.
|
|
41
|
+
* @param {unknown} value
|
|
42
|
+
* @param {string} path
|
|
43
|
+
* @param {string[]} out
|
|
44
|
+
*/
|
|
45
|
+
function findTodoFields(value, path, out) {
|
|
46
|
+
if (typeof value === 'string') {
|
|
47
|
+
if (TODO_RE.test(value)) out.push(path);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (Array.isArray(value)) {
|
|
51
|
+
value.forEach((v, i) => findTodoFields(v, `${path}[${i}]`, out));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (value && typeof value === 'object') {
|
|
55
|
+
for (const [k, v] of Object.entries(value)) {
|
|
56
|
+
findTodoFields(v, path ? `${path}.${k}` : k, out);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Scan every loaded document (roots + children; inline ACs / TCs ride
|
|
63
|
+
* inside their parent doc's fields) for TODO placeholder text left over
|
|
64
|
+
* from `rcf init` scaffolding. Cheap by design (B4): one string sweep,
|
|
65
|
+
* one entry per affected doc.
|
|
66
|
+
* @param {object} tree - walker TreeModel
|
|
67
|
+
* @returns {Array<{ id: string, fields: string[] }>}
|
|
68
|
+
*/
|
|
69
|
+
function collectTodoNotices(tree) {
|
|
70
|
+
const docs = [
|
|
71
|
+
...(tree.prd ? [{ id: tree.prd.prdId, doc: tree.prd }] : []),
|
|
72
|
+
...(tree.tad ? [{ id: tree.tad.tadId, doc: tree.tad }] : []),
|
|
73
|
+
...(tree.bs ? [{ id: tree.bs.bsId, doc: tree.bs }] : []),
|
|
74
|
+
...tree.requirements.map((d) => ({ id: d.reqId, doc: d })),
|
|
75
|
+
...tree.userStories.map((d) => ({ id: d.usId, doc: d })),
|
|
76
|
+
...tree.tacs.map((d) => ({ id: d.tacId, doc: d })),
|
|
77
|
+
...tree.adrs.map((d) => ({ id: d.adrId, doc: d })),
|
|
78
|
+
...tree.fbsItems.map((d) => ({ id: d.fbsId, doc: d })),
|
|
79
|
+
...tree.testSuites.map((d) => ({ id: d.id, doc: d })),
|
|
80
|
+
];
|
|
81
|
+
const notices = [];
|
|
82
|
+
for (const { id, doc } of docs) {
|
|
83
|
+
/** @type {string[]} */
|
|
84
|
+
const fields = [];
|
|
85
|
+
findTodoFields(doc, '', fields);
|
|
86
|
+
if (fields.length > 0) notices.push({ id: id ?? '(unknown id)', fields });
|
|
87
|
+
}
|
|
88
|
+
return notices;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param {string[]} argv - argv slice after `validate`
|
|
93
|
+
* @param {object} [deps]
|
|
94
|
+
* @returns {Promise<number>}
|
|
95
|
+
*/
|
|
96
|
+
export async function main(argv, deps = {}) {
|
|
97
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
98
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
99
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
100
|
+
|
|
101
|
+
let parsed;
|
|
102
|
+
try {
|
|
103
|
+
parsed = parseArgs({ args: argv, options: OPTION_SPEC, allowPositionals: true, strict: true });
|
|
104
|
+
} catch (err) {
|
|
105
|
+
stderr.write(`[error] usage ${err.message}\n`);
|
|
106
|
+
stderr.write(HELP);
|
|
107
|
+
return 2;
|
|
108
|
+
}
|
|
109
|
+
const flags = parsed.values;
|
|
110
|
+
if (flags.help) {
|
|
111
|
+
stdout.write(HELP);
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
const projectRoot = await findProjectRoot(cwd);
|
|
115
|
+
if (!projectRoot) {
|
|
116
|
+
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');
|
|
117
|
+
return 2;
|
|
118
|
+
}
|
|
119
|
+
const { tree, errors } = await walkTree({ projectRoot });
|
|
120
|
+
// Phase 10 (X2 CodeNode bridge, spec D6): staleness pass. Every Code
|
|
121
|
+
// Node's declared path/symbol is checked against the working tree;
|
|
122
|
+
// unresolved ones surface as `staleCode` errors folded into the same
|
|
123
|
+
// exit path as schema / referential-integrity errors. This is the X2
|
|
124
|
+
// detection claim: links that break visibly.
|
|
125
|
+
if (!flags['no-code']) {
|
|
126
|
+
const staleErrors = await checkCodeNodeResolution({ projectRoot, tree });
|
|
127
|
+
errors.push(...staleErrors);
|
|
128
|
+
}
|
|
129
|
+
if (flags.json) {
|
|
130
|
+
const issues = errors.map((e) => ({
|
|
131
|
+
id: e.documentId ?? null,
|
|
132
|
+
kind: e.kind,
|
|
133
|
+
rule: e.rule ?? null,
|
|
134
|
+
filePath: e.filePath ?? null,
|
|
135
|
+
field: e.field ?? null,
|
|
136
|
+
message: e.message,
|
|
137
|
+
}));
|
|
138
|
+
stdout.write(`${JSON.stringify({ ok: errors.length === 0, issues }, null, 2)}\n`);
|
|
139
|
+
return errors.length === 0 ? 0 : 3;
|
|
140
|
+
}
|
|
141
|
+
if (errors.length === 0) {
|
|
142
|
+
if (!flags.quiet) {
|
|
143
|
+
stdout.write('rcf validate: tree is clean.\n');
|
|
144
|
+
// B4 (E2E matrix 2026-07-06-003): scaffold TODO placeholders were
|
|
145
|
+
// surviving to otherwise-valid trees unflagged. Non-blocking notice
|
|
146
|
+
// only - the exit code is unchanged.
|
|
147
|
+
const notices = collectTodoNotices(tree);
|
|
148
|
+
if (notices.length > 0) {
|
|
149
|
+
stdout.write(`notice: ${notices.length} document(s) still carry scaffold TODO placeholder text (informational; exit code unaffected):\n`);
|
|
150
|
+
for (const n of notices) {
|
|
151
|
+
stdout.write(` ${n.id}: ${n.fields.join(', ')}\n`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
if (flags.quiet) {
|
|
158
|
+
const first = errors.slice(0, 3);
|
|
159
|
+
// BUG-004 fix: `--quiet` used to render `errors.length` (== 3) as the
|
|
160
|
+
// summary count. Pass the true total through so the summary reads
|
|
161
|
+
// "N errors found" for the whole tree, not just the shown subset.
|
|
162
|
+
stderr.write(`${formatErrors(first, { verbose: false, strict: false, total: errors.length })}\n`);
|
|
163
|
+
if (errors.length > 3) stderr.write(`... ${errors.length - 3} more issue(s) suppressed by --quiet\n`);
|
|
164
|
+
} else {
|
|
165
|
+
stderr.write(`${formatErrors(errors, { verbose: false, strict: false })}\n`);
|
|
166
|
+
}
|
|
167
|
+
return 3;
|
|
168
|
+
}
|