rcf-lite 0.0.1 → 0.8.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 +344 -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 +71 -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/admissibility/enforce.js +142 -0
- package/src/admissibility/index.js +8 -0
- package/src/admissibility/markers.js +104 -0
- package/src/admissibility/scope-lint.js +163 -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 +358 -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 +168 -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 +221 -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 +214 -0
- package/src/core/store/tp-resolve.js +176 -0
- package/src/core/store/validator.js +191 -0
- package/src/core/store/walker.js +944 -0
- package/src/core/store/writer.js +1879 -0
- package/src/design/index.js +11 -0
- package/src/design/writer.js +271 -0
- package/src/finalise/detect.js +151 -0
- package/src/finalise/index.js +31 -0
- package/src/finalise/ingest.js +160 -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 +1212 -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 +13 -0
- package/src/query/refuse-on-admissibility.js +73 -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/ruleset/index.js +140 -0
- package/src/ruleset/ruleset.json +146 -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 +221 -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 +313 -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,269 @@
|
|
|
1
|
+
// `rcf req-baseline <verb>` subcommand handler
|
|
2
|
+
// (elicitation-and-playbook-hardening-0.7.0-spec §5).
|
|
3
|
+
//
|
|
4
|
+
// Verbs (spec §5.4 / §13.3):
|
|
5
|
+
// rcf req-baseline sweep [--req <id> | --all] [--dry-run] [--json] [--yes]
|
|
6
|
+
// rcf req-baseline sweep --status [--json]
|
|
7
|
+
// rcf req-baseline opt-out --req <id> --key <baselineKey> --reason "..." [--scope req|project]
|
|
8
|
+
// rcf req-baseline opt-out --remove --req <id> --key <baselineKey>
|
|
9
|
+
//
|
|
10
|
+
// The sweep proposes any baseline AC not yet present on a US under the
|
|
11
|
+
// target REQ; the operator answers accept / opt-out / skip per candidate.
|
|
12
|
+
// Silence is refused: candidates left undecided stay OPEN, and Stage 1
|
|
13
|
+
// (rcf build --next) refuses any FBS binding an AC on a US that has
|
|
14
|
+
// open sweeps (spec §5.4 + §5.5 canonical text).
|
|
15
|
+
//
|
|
16
|
+
// Non-interactive callers (--yes accept-all, --dry-run preview) are
|
|
17
|
+
// legitimate; the moment-4 auto-enqueue path (spec §5.3) also produces
|
|
18
|
+
// OPEN candidates that this verb resolves.
|
|
19
|
+
|
|
20
|
+
import { parseArgs } from 'node:util';
|
|
21
|
+
import { createInterface } from 'node:readline/promises';
|
|
22
|
+
import process from 'node:process';
|
|
23
|
+
|
|
24
|
+
import { walkTree } from '#core/store';
|
|
25
|
+
|
|
26
|
+
import { findProjectRoot } from '../view/index.js';
|
|
27
|
+
import {
|
|
28
|
+
planSweep,
|
|
29
|
+
applySweepDecisions,
|
|
30
|
+
writeOptOut,
|
|
31
|
+
removeOptOut,
|
|
32
|
+
listOpenCandidates,
|
|
33
|
+
} from '../req-baseline/index.js';
|
|
34
|
+
|
|
35
|
+
const OPTION_SPEC = {
|
|
36
|
+
req: { type: 'string' },
|
|
37
|
+
all: { type: 'boolean' },
|
|
38
|
+
key: { type: 'string' },
|
|
39
|
+
reason: { type: 'string' },
|
|
40
|
+
scope: { type: 'string' },
|
|
41
|
+
remove: { type: 'boolean' },
|
|
42
|
+
status: { type: 'boolean' },
|
|
43
|
+
'dry-run': { type: 'boolean' },
|
|
44
|
+
yes: { type: 'boolean' },
|
|
45
|
+
json: { type: 'boolean' },
|
|
46
|
+
quiet: { type: 'boolean' },
|
|
47
|
+
help: { type: 'boolean' },
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const HELP = `Usage: rcf req-baseline <verb> [options]
|
|
51
|
+
|
|
52
|
+
Verbs:
|
|
53
|
+
sweep [--req <id> | --all] [--status] [--dry-run] [--yes] [--json]
|
|
54
|
+
Walk every US under the target REQ (or all
|
|
55
|
+
classified REQs) and propose any baseline
|
|
56
|
+
ACs not yet present. Interactive by default
|
|
57
|
+
on a TTY; --yes accepts every candidate;
|
|
58
|
+
--dry-run prints the plan without writing.
|
|
59
|
+
--status prints the open-candidate list
|
|
60
|
+
(silence-refuses-build queue) without
|
|
61
|
+
prompting.
|
|
62
|
+
|
|
63
|
+
opt-out --req <id> --key <baselineKey> --reason "..." [--scope req|project]
|
|
64
|
+
Record an operator ruling that removes a
|
|
65
|
+
baseline AC from the REQ (or the whole
|
|
66
|
+
project) and inherit the ruling on future
|
|
67
|
+
sweeps. Reason is at least 20 characters
|
|
68
|
+
(schema-enforced).
|
|
69
|
+
|
|
70
|
+
opt-out --remove --req <id> --key <baselineKey>
|
|
71
|
+
Delete an existing opt-out; the next sweep
|
|
72
|
+
will re-propose the AC.
|
|
73
|
+
|
|
74
|
+
Options:
|
|
75
|
+
--req <id> REQ to target
|
|
76
|
+
--all Target every classified REQ in the tree
|
|
77
|
+
--key <baselineKey> Catalogue key (for example, auth.htmlLoginPage)
|
|
78
|
+
--reason "..." Operator ruling (at least 20 characters)
|
|
79
|
+
--scope req|project Opt-out scope; defaults to req
|
|
80
|
+
--dry-run Preview only, do not write
|
|
81
|
+
--yes Accept every proposed candidate (non-interactive)
|
|
82
|
+
--status Show open-sweep candidates without prompting
|
|
83
|
+
--json Emit machine-readable output
|
|
84
|
+
--quiet Suppress non-error confirmations
|
|
85
|
+
--help Print this help
|
|
86
|
+
|
|
87
|
+
Exit codes:
|
|
88
|
+
0 success
|
|
89
|
+
2 usage error (unknown flag, missing --req, no project root)
|
|
90
|
+
3 validation failure on the written manifest
|
|
91
|
+
4 refused (opt-out reason below the 20-character floor)
|
|
92
|
+
`;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {string[]} argv
|
|
96
|
+
* @param {object} [deps]
|
|
97
|
+
* @returns {Promise<number>}
|
|
98
|
+
*/
|
|
99
|
+
export async function main(argv, deps = {}) {
|
|
100
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
101
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
102
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
103
|
+
|
|
104
|
+
let parsed;
|
|
105
|
+
try {
|
|
106
|
+
parsed = parseArgs({ args: argv, options: OPTION_SPEC, allowPositionals: true, strict: true });
|
|
107
|
+
} catch (err) {
|
|
108
|
+
stderr.write(`[error] usage ${err.message}\n`);
|
|
109
|
+
stderr.write(HELP);
|
|
110
|
+
return 2;
|
|
111
|
+
}
|
|
112
|
+
const flags = parsed.values;
|
|
113
|
+
const positionals = parsed.positionals;
|
|
114
|
+
if (flags.help) { stdout.write(HELP); return 0; }
|
|
115
|
+
if (positionals.length === 0) {
|
|
116
|
+
stderr.write('[error] usage req-baseline: missing verb (sweep|opt-out)\n');
|
|
117
|
+
stderr.write(HELP);
|
|
118
|
+
return 2;
|
|
119
|
+
}
|
|
120
|
+
const verb = positionals[0];
|
|
121
|
+
if (!['sweep', 'opt-out'].includes(verb)) {
|
|
122
|
+
stderr.write(`[error] usage req-baseline: unknown verb ${verb} (expected sweep|opt-out)\n`);
|
|
123
|
+
return 2;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const projectRoot = await findProjectRoot(cwd);
|
|
127
|
+
if (!projectRoot) {
|
|
128
|
+
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');
|
|
129
|
+
return 2;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const walkResult = await walkTree({ projectRoot });
|
|
133
|
+
if (walkResult.errors.length > 0) {
|
|
134
|
+
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`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (verb === 'sweep') {
|
|
138
|
+
if (flags.status) {
|
|
139
|
+
const open = listOpenCandidates(walkResult.tree, { reqId: flags.req ?? null });
|
|
140
|
+
if (flags.json) {
|
|
141
|
+
stdout.write(`${JSON.stringify(open, null, 2)}\n`);
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
if (open.length === 0) {
|
|
145
|
+
stdout.write('no open baseline sweep candidates\n');
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
for (const item of open) {
|
|
149
|
+
stdout.write(`${item.usId} (${item.reqId}) open: ${item.baselineKeys.join(', ')}\n`);
|
|
150
|
+
}
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!flags.req && !flags.all) {
|
|
155
|
+
stderr.write('[error] usage req-baseline sweep: --req <id> or --all is required\n');
|
|
156
|
+
return 2;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const plan = planSweep(walkResult.tree, { reqId: flags.req ?? null, all: Boolean(flags.all) });
|
|
160
|
+
if (plan.candidates.length === 0) {
|
|
161
|
+
if (!flags.quiet) stdout.write('no baseline candidates to propose\n');
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (flags['dry-run']) {
|
|
166
|
+
if (flags.json) {
|
|
167
|
+
stdout.write(`${JSON.stringify(plan, null, 2)}\n`);
|
|
168
|
+
} else {
|
|
169
|
+
for (const c of plan.candidates) {
|
|
170
|
+
stdout.write(`[dry-run] would propose ${c.baselineKey} on ${c.usId} (${c.reqId})\n`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Resolve decisions.
|
|
177
|
+
const decisions = [];
|
|
178
|
+
if (flags.yes) {
|
|
179
|
+
for (const c of plan.candidates) decisions.push({ candidate: c, action: 'accept' });
|
|
180
|
+
} else {
|
|
181
|
+
const isTty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
182
|
+
if (!isTty) {
|
|
183
|
+
// Non-interactive with no --yes: leave every candidate OPEN. This
|
|
184
|
+
// matches the moment-4 auto-enqueue posture (spec §5.3): a
|
|
185
|
+
// baseline-blind US never silently ships; the operator must
|
|
186
|
+
// resolve open sweeps before Stage 1 accepts an FBS binding it.
|
|
187
|
+
if (!flags.quiet) stdout.write(`sweep left ${plan.candidates.length} candidate(s) OPEN (non-interactive; rerun with --yes to accept every candidate)\n`);
|
|
188
|
+
return 0;
|
|
189
|
+
}
|
|
190
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
191
|
+
for (const c of plan.candidates) {
|
|
192
|
+
stdout.write(`\nREQ ${c.reqId} (${c.reqShape}) baseline for US ${c.usId}:\n`);
|
|
193
|
+
stdout.write(` ${c.baselineKey}: ${c.canonicalText}\n`);
|
|
194
|
+
// eslint-disable-next-line no-await-in-loop
|
|
195
|
+
const ans = (await rl.question('[a]ccept / [o]pt-out / [s]kip? ')).trim().toLowerCase();
|
|
196
|
+
if (ans === 'a' || ans === 'accept') {
|
|
197
|
+
decisions.push({ candidate: c, action: 'accept' });
|
|
198
|
+
} else if (ans === 'o' || ans === 'opt-out') {
|
|
199
|
+
// eslint-disable-next-line no-await-in-loop
|
|
200
|
+
const reason = (await rl.question('reason (>= 20 chars): ')).trim();
|
|
201
|
+
if (reason.length < 20) {
|
|
202
|
+
stderr.write(`[error] refused opt-out: reason below 20-character floor for ${c.baselineKey}; leaving candidate OPEN\n`);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
decisions.push({ candidate: c, action: 'opt-out', reason });
|
|
206
|
+
} else {
|
|
207
|
+
// skip: leaves the candidate OPEN.
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
rl.close();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const result = await applySweepDecisions({ projectRoot, tree: walkResult.tree, decisions });
|
|
214
|
+
if (result && result.kind && typeof result.message === 'string') {
|
|
215
|
+
stderr.write(`[error] ${result.kind} ${result.message}\n`);
|
|
216
|
+
return 3;
|
|
217
|
+
}
|
|
218
|
+
if (flags.json) {
|
|
219
|
+
stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
if (!flags.quiet) {
|
|
223
|
+
stdout.write(`sweep: ${result.accepted} accepted, ${result.optedOut} opted out, ${result.left} left OPEN\n`);
|
|
224
|
+
}
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// verb === 'opt-out'
|
|
229
|
+
if (flags.remove) {
|
|
230
|
+
if (!flags.req || !flags.key) {
|
|
231
|
+
stderr.write('[error] usage req-baseline opt-out --remove: --req and --key are required\n');
|
|
232
|
+
return 2;
|
|
233
|
+
}
|
|
234
|
+
const result = await removeOptOut({ projectRoot, tree: walkResult.tree, reqId: flags.req, baselineKey: flags.key });
|
|
235
|
+
if (result && result.kind && typeof result.message === 'string') {
|
|
236
|
+
stderr.write(`[error] ${result.kind} ${result.message}\n`);
|
|
237
|
+
return 3;
|
|
238
|
+
}
|
|
239
|
+
if (!flags.quiet) stdout.write(`opt-out removed for ${flags.req} ${flags.key}${result.removed ? '' : ' (no matching record found)'}\n`);
|
|
240
|
+
return 0;
|
|
241
|
+
}
|
|
242
|
+
if (!flags.req || !flags.key || !flags.reason) {
|
|
243
|
+
stderr.write('[error] usage req-baseline opt-out: --req, --key and --reason are required\n');
|
|
244
|
+
return 2;
|
|
245
|
+
}
|
|
246
|
+
if (flags.reason.length < 20) {
|
|
247
|
+
stderr.write(`[error] refused opt-out: --reason below 20-character floor (got ${flags.reason.length} chars); a one-word answer reads as silence, which is exactly what this ledger exists to prevent\n`);
|
|
248
|
+
return 4;
|
|
249
|
+
}
|
|
250
|
+
const scope = flags.scope ?? 'req';
|
|
251
|
+
if (!['req', 'project'].includes(scope)) {
|
|
252
|
+
stderr.write(`[error] usage req-baseline opt-out: --scope must be req or project (got ${scope})\n`);
|
|
253
|
+
return 2;
|
|
254
|
+
}
|
|
255
|
+
const result = await writeOptOut({
|
|
256
|
+
projectRoot,
|
|
257
|
+
tree: walkResult.tree,
|
|
258
|
+
reqId: flags.req,
|
|
259
|
+
baselineKey: flags.key,
|
|
260
|
+
reason: flags.reason,
|
|
261
|
+
scope,
|
|
262
|
+
});
|
|
263
|
+
if (result && result.kind && typeof result.message === 'string') {
|
|
264
|
+
stderr.write(`[error] ${result.kind} ${result.message}\n`);
|
|
265
|
+
return 3;
|
|
266
|
+
}
|
|
267
|
+
if (!flags.quiet) stdout.write(`opt-out ${result.id} recorded for ${flags.req} ${flags.key} (scope=${scope})\n`);
|
|
268
|
+
return 0;
|
|
269
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// `rcf req-classify <req-id>` subcommand handler
|
|
2
|
+
// (elicitation-and-playbook-hardening-0.7.0-spec §4.4).
|
|
3
|
+
//
|
|
4
|
+
// Reruns the REQ-shape classifier for one REQ (or all REQs when
|
|
5
|
+
// --all is passed) and writes the resulting shapeClassification block
|
|
6
|
+
// back onto the REQ. Useful after the pattern-set version bumps or when
|
|
7
|
+
// the operator has updated a description and wants to re-check.
|
|
8
|
+
|
|
9
|
+
import { parseArgs } from 'node:util';
|
|
10
|
+
|
|
11
|
+
import { formatErrors, writeUnexpectedFailure, rcfError } from '#core/errors';
|
|
12
|
+
import { walkTree } from '#core/store';
|
|
13
|
+
|
|
14
|
+
import { findProjectRoot } from '../view/index.js';
|
|
15
|
+
import { classifyAndPersistReq } from '../req-detection/index.js';
|
|
16
|
+
|
|
17
|
+
const OPTION_SPEC = {
|
|
18
|
+
all: { type: 'boolean' },
|
|
19
|
+
json: { type: 'boolean' },
|
|
20
|
+
quiet: { type: 'boolean' },
|
|
21
|
+
help: { type: 'boolean' },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const HELP = `Usage: rcf req-classify <req-id> [options]
|
|
25
|
+
rcf req-classify --all [options]
|
|
26
|
+
|
|
27
|
+
Rerun the REQ-shape classifier for one requirement (or every requirement
|
|
28
|
+
in the tree with --all) and write the resulting shapeClassification
|
|
29
|
+
block back onto the REQ.
|
|
30
|
+
|
|
31
|
+
Reads REQ title, description and rationale plus the parent PRD's intent
|
|
32
|
+
and problem as fallback context; matches against the versioned pattern
|
|
33
|
+
set in #core/patterns/req-shapes and writes shapes,
|
|
34
|
+
signals and classifiedAt. Preserves any prior operatorOverride block.
|
|
35
|
+
|
|
36
|
+
Options:
|
|
37
|
+
--all Classify every REQ in the tree
|
|
38
|
+
--json Emit the classification block as JSON on stdout
|
|
39
|
+
--quiet Suppress non-error confirmations
|
|
40
|
+
--help Print this help
|
|
41
|
+
|
|
42
|
+
Exit codes:
|
|
43
|
+
0 success (or nothing to classify)
|
|
44
|
+
2 usage error (unknown id, no project root, bad flags)
|
|
45
|
+
3 validation failure on the merged REQ document
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {string[]} argv
|
|
50
|
+
* @param {object} [deps]
|
|
51
|
+
* @returns {Promise<number>}
|
|
52
|
+
*/
|
|
53
|
+
export async function main(argv, deps = {}) {
|
|
54
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
55
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
56
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
57
|
+
|
|
58
|
+
let parsed;
|
|
59
|
+
try {
|
|
60
|
+
parsed = parseArgs({ args: argv, options: OPTION_SPEC, allowPositionals: true, strict: true });
|
|
61
|
+
} catch (err) {
|
|
62
|
+
stderr.write(`[error] usage ${err.message}\n`);
|
|
63
|
+
stderr.write(HELP);
|
|
64
|
+
return 2;
|
|
65
|
+
}
|
|
66
|
+
const flags = parsed.values;
|
|
67
|
+
const positionals = parsed.positionals;
|
|
68
|
+
if (flags.help) { stdout.write(HELP); return 0; }
|
|
69
|
+
|
|
70
|
+
const useAll = Boolean(flags.all);
|
|
71
|
+
if (useAll && positionals.length > 0) {
|
|
72
|
+
stderr.write('[error] usage req-classify: --all takes no positional\n');
|
|
73
|
+
return 2;
|
|
74
|
+
}
|
|
75
|
+
if (!useAll && positionals.length !== 1) {
|
|
76
|
+
stderr.write('[error] usage req-classify: expected exactly one <req-id> (or --all)\n');
|
|
77
|
+
stderr.write(HELP);
|
|
78
|
+
return 2;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const projectRoot = await findProjectRoot(cwd);
|
|
82
|
+
if (!projectRoot) {
|
|
83
|
+
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');
|
|
84
|
+
return 2;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const walkResult = await walkTree({ projectRoot });
|
|
88
|
+
if (walkResult.errors.length > 0) {
|
|
89
|
+
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`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const reqIds = [];
|
|
93
|
+
if (useAll) {
|
|
94
|
+
for (const [id, kind] of walkResult.tree.kindById) {
|
|
95
|
+
if (kind === 'requirement') reqIds.push(id);
|
|
96
|
+
}
|
|
97
|
+
reqIds.sort();
|
|
98
|
+
} else {
|
|
99
|
+
reqIds.push(positionals[0]);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** @type {Array<{ reqId: string, block: object|null, changed: boolean }>} */
|
|
103
|
+
const results = [];
|
|
104
|
+
for (const reqId of reqIds) {
|
|
105
|
+
const outcome = await classifyAndPersistReq({
|
|
106
|
+
projectRoot,
|
|
107
|
+
tree: walkResult.tree,
|
|
108
|
+
reqId,
|
|
109
|
+
});
|
|
110
|
+
if (!outcome.ok) {
|
|
111
|
+
stderr.write(`[error] ${outcome.message}\n`);
|
|
112
|
+
return 3;
|
|
113
|
+
}
|
|
114
|
+
results.push({ reqId, block: outcome.block, changed: outcome.changed });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (flags.json) {
|
|
118
|
+
stdout.write(`${JSON.stringify(results, null, 2)}\n`);
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
if (!flags.quiet) {
|
|
122
|
+
for (const r of results) {
|
|
123
|
+
const verdict = r.block?.shapes?.join(', ') || 'none';
|
|
124
|
+
const reason = r.block?.reason ?? 'unknown';
|
|
125
|
+
const state = r.changed ? 'classified' : 'unchanged';
|
|
126
|
+
stdout.write(`${r.reqId}: shapes=[${verdict}] reason=${reason} (${state})\n`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Silence linter on unused imports where the CLI helper does not need
|
|
133
|
+
// the RcfError formatters directly today; they are the canonical import
|
|
134
|
+
// surface for future error-path expansion.
|
|
135
|
+
void formatErrors; void writeUnexpectedFailure; void rcfError;
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
// `rcf review <fbs-id>` subcommand handler
|
|
2
|
+
// (verification-integrity-cluster-spec §5.5, §6).
|
|
3
|
+
//
|
|
4
|
+
// Runs the REVIEW-stage audit (test-theatre + mutation-sampling),
|
|
5
|
+
// writes the reviewAudit record to the manifest, and exits with:
|
|
6
|
+
// 0 on pass
|
|
7
|
+
// 4 on warn or block
|
|
8
|
+
// 3 on schema/tree validation failure
|
|
9
|
+
// 1 on IO / unexpected runtime failure
|
|
10
|
+
// 2 on usage error
|
|
11
|
+
//
|
|
12
|
+
// The mutation-sampling agent is injected via `deps.mutationRunner`;
|
|
13
|
+
// v1 default is a no-op runner that emits a valid schema record with
|
|
14
|
+
// a `notes` explaining that no runner was wired. `--skip-mutation`
|
|
15
|
+
// records the skip explicitly. `--dry-run` runs the audit without
|
|
16
|
+
// writing the record.
|
|
17
|
+
|
|
18
|
+
import { access, mkdir, readdir, rename, unlink, writeFile } from 'node:fs/promises';
|
|
19
|
+
import { join, dirname, relative } from 'node:path';
|
|
20
|
+
import { parseArgs } from 'node:util';
|
|
21
|
+
import process from 'node:process';
|
|
22
|
+
|
|
23
|
+
import { rcfError, writeUnexpectedFailure } from '#core/errors';
|
|
24
|
+
import { resolveTestPointers, validateDocument, walkTree } from '#core/store';
|
|
25
|
+
|
|
26
|
+
import { findProjectRoot } from '../view/index.js';
|
|
27
|
+
import {
|
|
28
|
+
auditTestTheatre,
|
|
29
|
+
composeReviewAuditRecord,
|
|
30
|
+
} from '../review/index.js';
|
|
31
|
+
import { auditUiBaselineDrift } from '../review/ui-baseline-drift.js';
|
|
32
|
+
import {
|
|
33
|
+
defaultMutationRunner,
|
|
34
|
+
defaultSizing,
|
|
35
|
+
resolveTestCommand,
|
|
36
|
+
skippedMutationRunner,
|
|
37
|
+
} from '../review/mutation.js';
|
|
38
|
+
|
|
39
|
+
const OPTION_SPEC = {
|
|
40
|
+
'dry-run': { type: 'boolean' },
|
|
41
|
+
json: { type: 'boolean' },
|
|
42
|
+
'skip-mutation': { type: 'boolean' },
|
|
43
|
+
'test-cmd': { type: 'string' },
|
|
44
|
+
quiet: { type: 'boolean' },
|
|
45
|
+
help: { type: 'boolean' },
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const HELP = `Usage: rcf review <fbs-id> [options]
|
|
49
|
+
|
|
50
|
+
Run the REVIEW-stage audit (test-theatre + mutation-sampling) and write
|
|
51
|
+
the reviewAudit record to the manifest. Exit 4 on any warn or block
|
|
52
|
+
verdict, so the audit is a Stage 3 -> Stage 4 gate.
|
|
53
|
+
|
|
54
|
+
Options:
|
|
55
|
+
--dry-run Run the audit and print findings without
|
|
56
|
+
writing the reviewAudit record.
|
|
57
|
+
--json Emit the composed record to stdout as JSON.
|
|
58
|
+
--skip-mutation Record that mutation-sampling was skipped
|
|
59
|
+
without running it; useful when the harness
|
|
60
|
+
has no runner wired yet. The reviewAudit
|
|
61
|
+
record still validates.
|
|
62
|
+
--test-cmd <command> Test command for the mutation-sampling agent
|
|
63
|
+
(default: manifest.testCommand, else
|
|
64
|
+
'npm test' when package.json exists).
|
|
65
|
+
--quiet Suppress non-error stdout
|
|
66
|
+
--help Print this help
|
|
67
|
+
|
|
68
|
+
Exit codes:
|
|
69
|
+
0 audit passed
|
|
70
|
+
1 IO / unexpected runtime failure
|
|
71
|
+
2 usage error
|
|
72
|
+
3 schema or tree validation refused the write
|
|
73
|
+
4 audit warn or block verdict (Stage 4 refused)
|
|
74
|
+
`;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {string[]} argv
|
|
78
|
+
* @param {object} [deps]
|
|
79
|
+
* @returns {Promise<number>}
|
|
80
|
+
*/
|
|
81
|
+
export async function main(argv, deps = {}) {
|
|
82
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
83
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
84
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
85
|
+
|
|
86
|
+
let parsed;
|
|
87
|
+
try {
|
|
88
|
+
parsed = parseArgs({ args: argv, options: OPTION_SPEC, allowPositionals: true, strict: true });
|
|
89
|
+
} catch (err) {
|
|
90
|
+
stderr.write(`[error] usage ${err.message}\n`);
|
|
91
|
+
stderr.write(HELP);
|
|
92
|
+
return 2;
|
|
93
|
+
}
|
|
94
|
+
const flags = parsed.values;
|
|
95
|
+
if (flags.help) { stdout.write(HELP); return 0; }
|
|
96
|
+
if (parsed.positionals.length !== 1) {
|
|
97
|
+
stderr.write('[error] usage review: expected exactly one <fbs-id> positional\n');
|
|
98
|
+
return 2;
|
|
99
|
+
}
|
|
100
|
+
const fbsId = parsed.positionals[0];
|
|
101
|
+
if (!/^FBS-\d{3,}$/.test(fbsId)) {
|
|
102
|
+
stderr.write(`[error] usage review: expected an FBS id like FBS-011, got '${fbsId}'\n`);
|
|
103
|
+
return 2;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const projectRoot = await findProjectRoot(cwd);
|
|
107
|
+
if (!projectRoot) {
|
|
108
|
+
stderr.write('[error] usage no project root found (no rcf/manifest.json in this directory or any ancestor).\n');
|
|
109
|
+
return 2;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const walkResult = await walkTree({ projectRoot });
|
|
113
|
+
if (walkResult.errors.length > 0) {
|
|
114
|
+
stderr.write(`[warn] tree has ${walkResult.errors.length} pre-existing issue(s); the audit proceeds but coverage-drift findings may be noisy\n`);
|
|
115
|
+
}
|
|
116
|
+
const tree = walkResult.tree;
|
|
117
|
+
const fbs = tree.byId.get(fbsId);
|
|
118
|
+
if (!fbs || tree.kindById.get(fbsId) !== 'fbs') {
|
|
119
|
+
stderr.write(`[error] usage review: ${fbsId} not found or not an FBS\n`);
|
|
120
|
+
return 2;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Test-pointer resolution feeds the testPointerBroken detector.
|
|
124
|
+
const testPointers = await resolveTestPointers({ projectRoot, tree });
|
|
125
|
+
const findings = auditTestTheatre({ tree, fbs, testPointers });
|
|
126
|
+
|
|
127
|
+
// Track B (ui-design-gate-0.7.0-spec §3.4): run the UI-baseline drift
|
|
128
|
+
// audit alongside the Track A test-theatre audit for uiBearing FBS.
|
|
129
|
+
// One brief, one worker, one record per FBS: the same reviewAudit
|
|
130
|
+
// record carries both Track A and Track B kinds (spec §12 O-12).
|
|
131
|
+
const listFiles = deps.listFiles ?? defaultListFiles(projectRoot);
|
|
132
|
+
const uiFindings = await auditUiBaselineDrift({
|
|
133
|
+
projectRoot,
|
|
134
|
+
fbs,
|
|
135
|
+
uiBaseline: tree.manifest?.uiBaseline ?? null,
|
|
136
|
+
listFiles,
|
|
137
|
+
});
|
|
138
|
+
for (const f of uiFindings) findings.push(f);
|
|
139
|
+
|
|
140
|
+
// Mutation-sampling.
|
|
141
|
+
let mutationRunner = deps.mutationRunner;
|
|
142
|
+
if (!mutationRunner) mutationRunner = flags['skip-mutation'] ? skippedMutationRunner : defaultMutationRunner;
|
|
143
|
+
const hasPackageJson = await fileExists(join(projectRoot, 'package.json'));
|
|
144
|
+
const { command: testCommand } = resolveTestCommand({
|
|
145
|
+
manifest: tree.manifest, cliFlag: flags['test-cmd'], hasPackageJson,
|
|
146
|
+
});
|
|
147
|
+
const fbsAcIds = new Set(fbs.acIds ?? []);
|
|
148
|
+
const coveringTs = (tree.testSuites ?? []).filter((ts) => (ts.acIds ?? []).some((a) => fbsAcIds.has(a)));
|
|
149
|
+
const runnerInput = {
|
|
150
|
+
fbsId, acIds: fbs.acIds ?? [], testSuites: coveringTs,
|
|
151
|
+
sizing: defaultSizing(), testCommand: testCommand ?? undefined, projectRoot,
|
|
152
|
+
};
|
|
153
|
+
let mutationSampling;
|
|
154
|
+
try {
|
|
155
|
+
const runnerOut = await mutationRunner(runnerInput);
|
|
156
|
+
mutationSampling = runnerOut?.record;
|
|
157
|
+
} catch (err) {
|
|
158
|
+
writeUnexpectedFailure(
|
|
159
|
+
rcfError({ kind: 'ioFailure', message: `review: mutation runner failed: ${err.message}`, stack: err.stack }),
|
|
160
|
+
stderr,
|
|
161
|
+
);
|
|
162
|
+
return 1;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const record = composeReviewAuditRecord({
|
|
166
|
+
tree, fbs, findings, mutationSampling,
|
|
167
|
+
now: deps.now ? new Date(deps.now()) : new Date(),
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (flags.json) stdout.write(`${JSON.stringify(record, null, 2)}\n`);
|
|
171
|
+
|
|
172
|
+
if (flags['dry-run']) {
|
|
173
|
+
if (!flags.quiet) {
|
|
174
|
+
stdout.write(`[dry-run] review ${fbsId}: ${findings.length} finding(s); verdict=${record.verdict}\n`);
|
|
175
|
+
for (const f of findings) {
|
|
176
|
+
stdout.write(` [${f.severity}] ${f.kind} ${f.tsId}${f.tcId ? '/' + f.tcId : ''}: ${f.detail}\n`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return record.verdict === 'pass' ? 0 : 4;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Write the record to the manifest.
|
|
183
|
+
const nextManifest = { ...(tree.manifest ?? {}) };
|
|
184
|
+
const existing = Array.isArray(nextManifest.reviewAudit) ? nextManifest.reviewAudit : [];
|
|
185
|
+
nextManifest.reviewAudit = [...existing, record];
|
|
186
|
+
if (testCommand && nextManifest.testCommand !== testCommand && !nextManifest.testCommand) {
|
|
187
|
+
// Optionally record the resolved testCommand when not already set,
|
|
188
|
+
// so a later re-audit inherits the same resolution without a flag.
|
|
189
|
+
nextManifest.testCommand = testCommand;
|
|
190
|
+
}
|
|
191
|
+
const relPath = 'rcf/manifest.json';
|
|
192
|
+
const validation = validateDocument({ doc: nextManifest, kind: 'manifest', filePath: relPath });
|
|
193
|
+
if (validation) {
|
|
194
|
+
stderr.write(`[error] validation ${validation.message}\n`);
|
|
195
|
+
return 3;
|
|
196
|
+
}
|
|
197
|
+
const absPath = join(projectRoot, 'rcf', 'manifest.json');
|
|
198
|
+
try {
|
|
199
|
+
await mkdir(dirname(absPath), { recursive: true });
|
|
200
|
+
const tmp = `${absPath}.tmp`;
|
|
201
|
+
await writeFile(tmp, `${JSON.stringify(nextManifest, null, 2)}\n`, 'utf8');
|
|
202
|
+
try {
|
|
203
|
+
await rename(tmp, absPath);
|
|
204
|
+
} catch (err) {
|
|
205
|
+
try { await unlink(tmp); } catch { /* ignore */ }
|
|
206
|
+
throw err;
|
|
207
|
+
}
|
|
208
|
+
} catch (err) {
|
|
209
|
+
writeUnexpectedFailure(
|
|
210
|
+
rcfError({ kind: 'ioFailure', message: `review: manifest write failed: ${err.message}`, filePath: relPath, stack: err.stack }),
|
|
211
|
+
stderr,
|
|
212
|
+
);
|
|
213
|
+
return 1;
|
|
214
|
+
}
|
|
215
|
+
if (!flags.quiet) {
|
|
216
|
+
stdout.write(`review ${fbsId}: wrote ${record.id} to rcf/manifest.json (${findings.length} findings, verdict=${record.verdict}).\n`);
|
|
217
|
+
}
|
|
218
|
+
return record.verdict === 'pass' ? 0 : 4;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function fileExists(path) {
|
|
222
|
+
try {
|
|
223
|
+
await access(path);
|
|
224
|
+
return true;
|
|
225
|
+
} catch {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Default file lister used by the UI-baseline drift audit. Walks the
|
|
232
|
+
* project tree once and matches each path against the supplied glob
|
|
233
|
+
* patterns. Supports the `**` suffix (recursive) and `*` mid-path
|
|
234
|
+
* wildcards. Deliberately minimal: the audit's callers pass either
|
|
235
|
+
* `src/ui/**` or `src/routes/**` in v1, so a formal glob library
|
|
236
|
+
* would be scope-creep. Returns project-root-relative paths.
|
|
237
|
+
*
|
|
238
|
+
* @param {string} projectRoot
|
|
239
|
+
* @returns {(patterns: string[]) => Promise<string[]>}
|
|
240
|
+
*/
|
|
241
|
+
function defaultListFiles(projectRoot) {
|
|
242
|
+
return async (patterns) => {
|
|
243
|
+
const out = new Set();
|
|
244
|
+
for (const raw of Array.isArray(patterns) ? patterns : []) {
|
|
245
|
+
const pattern = String(raw);
|
|
246
|
+
const idx = pattern.indexOf('**');
|
|
247
|
+
let root;
|
|
248
|
+
let recursive;
|
|
249
|
+
let suffix;
|
|
250
|
+
if (idx >= 0) {
|
|
251
|
+
root = pattern.slice(0, Math.max(0, idx - 1));
|
|
252
|
+
recursive = true;
|
|
253
|
+
suffix = pattern.slice(idx + 2).replace(/^\//, '');
|
|
254
|
+
} else if (pattern.includes('*')) {
|
|
255
|
+
const slash = pattern.lastIndexOf('/');
|
|
256
|
+
root = slash >= 0 ? pattern.slice(0, slash) : '.';
|
|
257
|
+
recursive = false;
|
|
258
|
+
suffix = pattern.slice(slash + 1);
|
|
259
|
+
} else {
|
|
260
|
+
// Literal path; include as-is.
|
|
261
|
+
root = pattern;
|
|
262
|
+
recursive = false;
|
|
263
|
+
suffix = '';
|
|
264
|
+
}
|
|
265
|
+
const absRoot = join(projectRoot, root);
|
|
266
|
+
const collected = [];
|
|
267
|
+
await walkDir(absRoot, recursive, collected);
|
|
268
|
+
for (const abs of collected) {
|
|
269
|
+
const rel = relative(projectRoot, abs);
|
|
270
|
+
if (matchesSuffix(rel, suffix)) out.add(rel);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return [...out];
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function walkDir(root, recursive, out) {
|
|
278
|
+
let entries;
|
|
279
|
+
try { entries = await readdir(root, { withFileTypes: true }); } catch { return; }
|
|
280
|
+
for (const entry of entries) {
|
|
281
|
+
const abs = join(root, entry.name);
|
|
282
|
+
if (entry.isDirectory()) {
|
|
283
|
+
if (recursive) await walkDir(abs, true, out);
|
|
284
|
+
} else if (entry.isFile()) {
|
|
285
|
+
out.push(abs);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function matchesSuffix(rel, suffix) {
|
|
291
|
+
if (!suffix || suffix.length === 0) return true;
|
|
292
|
+
// Support a single `*` wildcard on the suffix segment.
|
|
293
|
+
const escaped = suffix.split('*').map((p) => p.replace(/[.+?^${}()|[\]\\]/g, '\\$&')).join('.*');
|
|
294
|
+
return new RegExp(`(?:^|/)${escaped}$`).test(rel);
|
|
295
|
+
}
|