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,156 @@
|
|
|
1
|
+
// Register-canary runner (spec §7.4).
|
|
2
|
+
//
|
|
3
|
+
// Runs one fixture (or the whole pack) against a subagent driver,
|
|
4
|
+
// grades the response with core's REGISTER_CANARY_DIMENSIONS_V1, and
|
|
5
|
+
// composes the record shape for the fixture manifest.
|
|
6
|
+
//
|
|
7
|
+
// Subagent driver: injected. The default is a mock that returns a
|
|
8
|
+
// clean "in hand, next step is X" message, but the runner FORCES a
|
|
9
|
+
// mock run to be distinguishable from a real subagent run in the
|
|
10
|
+
// durable record — a mock never produces a pass verdict indistinguishable
|
|
11
|
+
// from a live one. Concretely:
|
|
12
|
+
// - the runner takes a `driverMode` argument ('mock' | 'live');
|
|
13
|
+
// - a mock run suffixes `buildVersion` with `-mockdriver` so the
|
|
14
|
+
// mode marker travels with the record into any chain manifest a
|
|
15
|
+
// downstream consumer could copy the record into (§7.4 fixture
|
|
16
|
+
// manifest is not chain-validated, but the record shape must stay
|
|
17
|
+
// honest across surfaces);
|
|
18
|
+
// - a mock run OVERRIDES the top-level verdict to `fail` regardless
|
|
19
|
+
// of grade, and populates `shipDespiteFailReason` with a plain
|
|
20
|
+
// "mock canary driver — infrastructure test only, no real subagent
|
|
21
|
+
// dispatched" line, so the record is durably marked as machinery
|
|
22
|
+
// verification rather than a release gate.
|
|
23
|
+
// This mirrors Track A N-2 (unwired mutation runner returning PASS is
|
|
24
|
+
// theatre) and Track B flagged-call-2 (stub browser driver forced to
|
|
25
|
+
// warn-never-pass): unwired drivers must not produce clean-pass records.
|
|
26
|
+
// The production driver (Anthropic Agent SDK, Opus 4.7 per estate
|
|
27
|
+
// ladder) invokes with `driverMode: 'live'` and the overlay is skipped
|
|
28
|
+
// entirely — the record is a genuine grader output.
|
|
29
|
+
|
|
30
|
+
import { gradeResponse } from '#core/patterns/register-canary';
|
|
31
|
+
|
|
32
|
+
import { composeCanaryRecord } from './record-writer.js';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Marker text applied to a mock-driver record's `shipDespiteFailReason`.
|
|
36
|
+
* Deliberately long enough to satisfy the schema `minLength: 1` and
|
|
37
|
+
* explicit enough that a reviewer greps the record and sees the mode.
|
|
38
|
+
*/
|
|
39
|
+
export const MOCK_DRIVER_MARKER = 'mock canary driver used; no real subagent was dispatched. This record verifies canary infrastructure only, not the register itself.';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Suffix appended to `buildVersion` when the driver mode is mock. Ports
|
|
43
|
+
* across surfaces (the fixture manifest is a private greppable log
|
|
44
|
+
* today; a future ship into a chain manifest would carry the marker
|
|
45
|
+
* intact through the schema-legal buildVersion string).
|
|
46
|
+
*/
|
|
47
|
+
export const MOCK_DRIVER_BUILD_VERSION_SUFFIX = '-mockdriver';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {object} CanaryDriverInput
|
|
51
|
+
* @property {string} operatorPrompt
|
|
52
|
+
* @property {Array<{ path: string, content: string }>} supportingArtefacts
|
|
53
|
+
* @property {string[]} grantedPermissions
|
|
54
|
+
* @property {string} guidance the shipping playbook content injected as system context
|
|
55
|
+
* @property {string} fixtureId
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @typedef {object} CanaryDriverOutput
|
|
60
|
+
* @property {string} responseBody the first-response body text (system prompts / tool-use excluded)
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @typedef {(input: CanaryDriverInput) => Promise<CanaryDriverOutput>} CanaryDriver
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A clean-register mock driver. Produces a short, on-register reply
|
|
69
|
+
* that passes every graded dimension. Used by tests and by the CLI
|
|
70
|
+
* when no live driver is wired.
|
|
71
|
+
*
|
|
72
|
+
* @type {CanaryDriver}
|
|
73
|
+
*/
|
|
74
|
+
export async function MOCK_SUBAGENT() {
|
|
75
|
+
return {
|
|
76
|
+
responseBody: "Have it in hand. The brief points at a single-operator uptime tool with an admin surface and email recovery pings. Two open decisions before any code: which host you want to deploy against, and the credential store for Resend. Once you name those I will scaffold the tree and hand you the first build item.",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Run one fixture against a driver and produce a record.
|
|
82
|
+
*
|
|
83
|
+
* @param {object} args
|
|
84
|
+
* @param {object} args.fixture
|
|
85
|
+
* @param {CanaryDriver} args.driver
|
|
86
|
+
* @param {'mock'|'live'} args.driverMode mock runs are overridden to fail; see file header
|
|
87
|
+
* @param {string} args.buildVersion
|
|
88
|
+
* @param {string} args.guidance
|
|
89
|
+
* @param {object[]} args.existingRecords
|
|
90
|
+
* @param {Date} [args.now]
|
|
91
|
+
* @returns {Promise<{ record: object, responseBody: string }>}
|
|
92
|
+
*/
|
|
93
|
+
export async function runCanaryAgainstFixture({ fixture, driver, driverMode, buildVersion, guidance, existingRecords, now }) {
|
|
94
|
+
if (driverMode !== 'mock' && driverMode !== 'live') {
|
|
95
|
+
throw new Error(`runCanaryAgainstFixture: driverMode must be 'mock' or 'live' (got ${JSON.stringify(driverMode)})`);
|
|
96
|
+
}
|
|
97
|
+
const output = await driver({
|
|
98
|
+
operatorPrompt: fixture.operatorPrompt,
|
|
99
|
+
supportingArtefacts: fixture.supportingArtefacts ?? [],
|
|
100
|
+
grantedPermissions: fixture.grantedPermissions ?? [],
|
|
101
|
+
guidance,
|
|
102
|
+
fixtureId: fixture.id,
|
|
103
|
+
});
|
|
104
|
+
const grade = gradeResponse({
|
|
105
|
+
responseBody: output.responseBody,
|
|
106
|
+
grantedPermissions: fixture.grantedPermissions,
|
|
107
|
+
wordCountBudget: fixture.wordCountBudget,
|
|
108
|
+
});
|
|
109
|
+
// A mock driver never produces a clean-pass record: suffix the
|
|
110
|
+
// buildVersion, force the top-level verdict to `fail`, and stamp
|
|
111
|
+
// shipDespiteFailReason with the mock-marker string. The `grades`
|
|
112
|
+
// block still reflects what the mock actually said, so a reader can
|
|
113
|
+
// see the pattern-level detail — the aggregate is what is forced.
|
|
114
|
+
const effectiveBuildVersion = driverMode === 'mock' ? `${buildVersion}${MOCK_DRIVER_BUILD_VERSION_SUFFIX}` : buildVersion;
|
|
115
|
+
const effectiveGrade = driverMode === 'mock' ? { verdict: 'fail', grades: grade.grades } : grade;
|
|
116
|
+
const record = composeCanaryRecord({
|
|
117
|
+
existingRecords,
|
|
118
|
+
buildVersion: effectiveBuildVersion,
|
|
119
|
+
fixturePromptId: fixture.id,
|
|
120
|
+
responseBody: output.responseBody,
|
|
121
|
+
grade: effectiveGrade,
|
|
122
|
+
shipDespiteFailReason: driverMode === 'mock' ? MOCK_DRIVER_MARKER : undefined,
|
|
123
|
+
now,
|
|
124
|
+
});
|
|
125
|
+
return { record, responseBody: output.responseBody };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Run every fixture in the pack. Returns the records in run order plus
|
|
130
|
+
* an aggregate verdict (any fail forces aggregate fail).
|
|
131
|
+
*
|
|
132
|
+
* @param {object} args
|
|
133
|
+
* @param {object[]} args.fixtures
|
|
134
|
+
* @param {CanaryDriver} args.driver
|
|
135
|
+
* @param {'mock'|'live'} args.driverMode
|
|
136
|
+
* @param {string} args.buildVersion
|
|
137
|
+
* @param {string} args.guidance
|
|
138
|
+
* @param {object[]} args.existingRecords
|
|
139
|
+
* @param {Date} [args.now]
|
|
140
|
+
* @returns {Promise<{ records: object[], verdict: 'pass'|'fail', driverMode: 'mock'|'live' }>}
|
|
141
|
+
*/
|
|
142
|
+
export async function runCanaryPack({ fixtures, driver, driverMode, buildVersion, guidance, existingRecords, now }) {
|
|
143
|
+
const records = [];
|
|
144
|
+
let aggregate = 'pass';
|
|
145
|
+
let existing = [...existingRecords];
|
|
146
|
+
for (const fixture of fixtures) {
|
|
147
|
+
// eslint-disable-next-line no-await-in-loop
|
|
148
|
+
const { record } = await runCanaryAgainstFixture({
|
|
149
|
+
fixture, driver, driverMode, buildVersion, guidance, existingRecords: existing, now,
|
|
150
|
+
});
|
|
151
|
+
records.push(record);
|
|
152
|
+
existing = [...existing, record];
|
|
153
|
+
if (record.verdict === 'fail') aggregate = 'fail';
|
|
154
|
+
}
|
|
155
|
+
return { records, verdict: aggregate, driverMode };
|
|
156
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Stage-1 refusal gate (spec §5.4).
|
|
2
|
+
//
|
|
3
|
+
// `rcf build --next` refuses to enter Build for any FBS binding an AC
|
|
4
|
+
// on a US that still has open baseline sweep candidates. The refusal
|
|
5
|
+
// message follows the canonical shape in spec §5.4.
|
|
6
|
+
//
|
|
7
|
+
// The gate itself is a pure function against the tree; the CLI layer
|
|
8
|
+
// calls it after selecting the next actionable FBS (see build.js).
|
|
9
|
+
|
|
10
|
+
import { openCandidatesForUs } from './open-candidates.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {object} Stage1Refusal
|
|
14
|
+
* @property {string} fbsId
|
|
15
|
+
* @property {string} usId
|
|
16
|
+
* @property {Array<{ baselineKey: string, canonicalText: string, reqShape: string }>} openCandidates
|
|
17
|
+
* @property {string} reqId
|
|
18
|
+
* @property {string} message pre-formatted refusal, spec §5.4 shape
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Inspect an FBS for a refusal condition. Walks the FBS's bound ACs to
|
|
23
|
+
* their owning USes; any US with open baseline candidates fails the
|
|
24
|
+
* gate. Returns null when the FBS is clear.
|
|
25
|
+
*
|
|
26
|
+
* @param {import('#core/store/walker.js').TreeModel} tree
|
|
27
|
+
* @param {string} fbsId
|
|
28
|
+
* @returns {Stage1Refusal|null}
|
|
29
|
+
*/
|
|
30
|
+
export function fbsRefusalForOpenSweep(tree, fbsId) {
|
|
31
|
+
const fbs = tree?.byId?.get(fbsId);
|
|
32
|
+
if (!fbs) return null;
|
|
33
|
+
const acIds = Array.isArray(fbs.acIds) ? fbs.acIds : [];
|
|
34
|
+
const seenUsIds = new Set();
|
|
35
|
+
for (const acId of acIds) {
|
|
36
|
+
// AC ids are inline on the parent US; `walkTree` records the
|
|
37
|
+
// AC-to-US mapping under `parentByChild` for exactly this lookup
|
|
38
|
+
// (the `usByAcId` map on the render-side tree model is derived
|
|
39
|
+
// from the same source).
|
|
40
|
+
const usId = tree.parentByChild?.get(acId);
|
|
41
|
+
if (!usId || seenUsIds.has(usId)) continue;
|
|
42
|
+
seenUsIds.add(usId);
|
|
43
|
+
const us = tree.byId.get(usId);
|
|
44
|
+
if (!us) continue;
|
|
45
|
+
const open = openCandidatesForUs(tree, us);
|
|
46
|
+
if (open.length === 0) continue;
|
|
47
|
+
return {
|
|
48
|
+
fbsId,
|
|
49
|
+
usId,
|
|
50
|
+
reqId: us.reqId,
|
|
51
|
+
openCandidates: open,
|
|
52
|
+
message: formatStage1RefusalMessage({ fbsId, usId, reqId: us.reqId, openCandidates: open }),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Format the spec §5.4 refusal line for an operator.
|
|
60
|
+
*
|
|
61
|
+
* @param {object} args
|
|
62
|
+
* @param {string} args.fbsId
|
|
63
|
+
* @param {string} args.usId
|
|
64
|
+
* @param {string} args.reqId
|
|
65
|
+
* @param {Array<{ baselineKey: string, canonicalText: string }>} args.openCandidates
|
|
66
|
+
* @returns {string}
|
|
67
|
+
*/
|
|
68
|
+
export function formatStage1RefusalMessage({ fbsId, usId, reqId, openCandidates }) {
|
|
69
|
+
const lines = [];
|
|
70
|
+
lines.push(`refused build: ${fbsId} binds ACs on ${usId}, but ${usId} has ${openCandidates.length} open baseline`);
|
|
71
|
+
lines.push(` candidate${openCandidates.length === 1 ? '' : 's'} awaiting a decision:`);
|
|
72
|
+
for (const c of openCandidates) {
|
|
73
|
+
const short = shortDescription(c.canonicalText);
|
|
74
|
+
lines.push(` ${c.baselineKey}: ${short}`);
|
|
75
|
+
}
|
|
76
|
+
lines.push(` Resolve: rcf req-baseline sweep --req ${reqId}`);
|
|
77
|
+
return lines.join('\n');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function shortDescription(text) {
|
|
81
|
+
// Trim to a leading "given ... then <clause>" summary; cap at 100
|
|
82
|
+
// chars with an ellipsis. Never rewrites the sentence.
|
|
83
|
+
const trimmed = typeof text === 'string' ? text : '';
|
|
84
|
+
if (trimmed.length <= 100) return trimmed;
|
|
85
|
+
return `${trimmed.slice(0, 97)}...`;
|
|
86
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Public surface for the baseline-AC injection module
|
|
2
|
+
// (elicitation-and-playbook-hardening-0.7.0-spec §5).
|
|
3
|
+
|
|
4
|
+
export {
|
|
5
|
+
planSweep,
|
|
6
|
+
applySweepDecisions,
|
|
7
|
+
applyPendingBaselinesForUs,
|
|
8
|
+
} from './sweep.js';
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
writeOptOut,
|
|
12
|
+
removeOptOut,
|
|
13
|
+
nextOptOutId,
|
|
14
|
+
isOptedOut,
|
|
15
|
+
optOutMap,
|
|
16
|
+
} from './opt-out.js';
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
listOpenCandidates,
|
|
20
|
+
usHasOpenCandidates,
|
|
21
|
+
openCandidatesForUs,
|
|
22
|
+
} from './open-candidates.js';
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
fbsRefusalForOpenSweep,
|
|
26
|
+
formatStage1RefusalMessage,
|
|
27
|
+
} from './gate.js';
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Open-sweep candidate tracker (spec §5.3 moment 4, §5.4 gate).
|
|
2
|
+
//
|
|
3
|
+
// The moment-4 posture: a non-interactive `rcf create us` under a
|
|
4
|
+
// classified REQ surfaces baseline keys that are neither present as
|
|
5
|
+
// ACs nor covered by an opt-out. This module answers "which USes have
|
|
6
|
+
// baselineKeys that are still OPEN?" without ever writing to disk.
|
|
7
|
+
//
|
|
8
|
+
// There is NO persisted sweep queue: `@stravica-ai/rcf-schemas`
|
|
9
|
+
// user-story.schema.json is `additionalProperties: false` and carries
|
|
10
|
+
// no `baselineSweepQueue` property, so writing one would fail schema
|
|
11
|
+
// validation. Openness is DERIVED from three facts read live per call:
|
|
12
|
+
// the parent REQ's `shapeClassification`, the US's own AC keys, and
|
|
13
|
+
// the manifest's `baselineAcOptOuts[]`. Spec §18 2026-07-31 fold
|
|
14
|
+
// ratifies the derivation as normative (replaces the earlier
|
|
15
|
+
// "auto-enqueue" wording).
|
|
16
|
+
|
|
17
|
+
import { getBaselineSet, getBaselineEntry, BASELINE_SHAPE_KEYS } from '#core/baseline-catalog';
|
|
18
|
+
import { optOutMap } from './opt-out.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {object} OpenCandidate
|
|
22
|
+
* @property {string} usId
|
|
23
|
+
* @property {string} reqId
|
|
24
|
+
* @property {string} baselineKey
|
|
25
|
+
* @property {string} reqShape
|
|
26
|
+
* @property {string} canonicalText
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Enumerate open sweep candidates across the tree.
|
|
31
|
+
*
|
|
32
|
+
* A candidate is open when:
|
|
33
|
+
* - the parent REQ carries a `shapeClassification` with a non-empty
|
|
34
|
+
* shape list (excluding `none`);
|
|
35
|
+
* - a baseline key from any of those shapes is not present as an AC
|
|
36
|
+
* on the US (neither authoredBy: baseline nor operatorEdited with
|
|
37
|
+
* the same key);
|
|
38
|
+
* - no baselineAcOptOuts entry covers the key (req-scoped for this
|
|
39
|
+
* REQ, or project-scoped).
|
|
40
|
+
*
|
|
41
|
+
* Openness is derived per call from these three signals; there is no
|
|
42
|
+
* persisted queue by ruling (spec §18 2026-07-31 fold; schema fence at
|
|
43
|
+
* user-story additionalProperties:false).
|
|
44
|
+
*
|
|
45
|
+
* Note the caller may pass reqId to narrow to one REQ.
|
|
46
|
+
*
|
|
47
|
+
* @param {import('#core/store/walker.js').TreeModel} tree
|
|
48
|
+
* @param {object} [opts]
|
|
49
|
+
* @param {string|null} [opts.reqId]
|
|
50
|
+
* @returns {Array<{ usId: string, reqId: string, baselineKeys: string[] }>}
|
|
51
|
+
*/
|
|
52
|
+
export function listOpenCandidates(tree, opts = {}) {
|
|
53
|
+
const filterReq = opts.reqId ?? null;
|
|
54
|
+
const results = [];
|
|
55
|
+
for (const [id, kind] of tree.kindById) {
|
|
56
|
+
if (kind !== 'userStory') continue;
|
|
57
|
+
const us = tree.byId.get(id);
|
|
58
|
+
if (!us) continue;
|
|
59
|
+
if (filterReq && us.reqId !== filterReq) continue;
|
|
60
|
+
const open = openCandidatesForUs(tree, us);
|
|
61
|
+
if (open.length > 0) {
|
|
62
|
+
results.push({ usId: id, reqId: us.reqId, baselineKeys: open.map((c) => c.baselineKey) });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
results.sort((a, b) => a.usId.localeCompare(b.usId));
|
|
66
|
+
return results;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Enumerate open candidates for one loaded US.
|
|
71
|
+
*
|
|
72
|
+
* @param {import('#core/store/walker.js').TreeModel} tree
|
|
73
|
+
* @param {object} usDoc
|
|
74
|
+
* @returns {OpenCandidate[]}
|
|
75
|
+
*/
|
|
76
|
+
export function openCandidatesForUs(tree, usDoc) {
|
|
77
|
+
const reqDoc = tree?.byId?.get(usDoc?.reqId);
|
|
78
|
+
const shapes = shapesOfReq(reqDoc);
|
|
79
|
+
if (shapes.length === 0) return [];
|
|
80
|
+
|
|
81
|
+
const acKeys = new Set();
|
|
82
|
+
for (const ac of usDoc.acceptanceCriteria ?? []) {
|
|
83
|
+
if (ac?.provenance?.baselineKey) acKeys.add(ac.provenance.baselineKey);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const optedOut = optOutMap(tree?.manifest ?? null, usDoc.reqId);
|
|
87
|
+
|
|
88
|
+
const proposeAll = new Set();
|
|
89
|
+
for (const shape of shapes) {
|
|
90
|
+
const set = getBaselineSet(shape);
|
|
91
|
+
if (!set) continue;
|
|
92
|
+
for (const entry of set.entries) proposeAll.add(entry.baselineKey);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const results = [];
|
|
96
|
+
for (const key of proposeAll) {
|
|
97
|
+
if (acKeys.has(key)) continue;
|
|
98
|
+
if (optedOut.has(key)) continue;
|
|
99
|
+
const entry = getBaselineEntry(key);
|
|
100
|
+
if (!entry) continue;
|
|
101
|
+
// shape derived from entry's set (spec order: webUi > httpApi > auth
|
|
102
|
+
// > persistence > notifications).
|
|
103
|
+
const reqShape = shapeOfEntry(key);
|
|
104
|
+
results.push({
|
|
105
|
+
usId: usDoc.usId,
|
|
106
|
+
reqId: usDoc.reqId,
|
|
107
|
+
baselineKey: key,
|
|
108
|
+
reqShape,
|
|
109
|
+
canonicalText: entry.canonicalText,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
// Stable order by shape then by canonical order within the set.
|
|
113
|
+
results.sort((a, b) => a.baselineKey.localeCompare(b.baselineKey));
|
|
114
|
+
return results;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* True when the US has one or more open sweep candidates.
|
|
119
|
+
*
|
|
120
|
+
* @param {import('#core/store/walker.js').TreeModel} tree
|
|
121
|
+
* @param {object} usDoc
|
|
122
|
+
* @returns {boolean}
|
|
123
|
+
*/
|
|
124
|
+
export function usHasOpenCandidates(tree, usDoc) {
|
|
125
|
+
return openCandidatesForUs(tree, usDoc).length > 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function shapesOfReq(reqDoc) {
|
|
129
|
+
const shapes = reqDoc?.shapeClassification?.shapes;
|
|
130
|
+
if (!Array.isArray(shapes)) return [];
|
|
131
|
+
return shapes.filter((s) => s !== 'none');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function shapeOfEntry(baselineKey) {
|
|
135
|
+
for (const shape of BASELINE_SHAPE_KEYS) {
|
|
136
|
+
const set = getBaselineSet(shape);
|
|
137
|
+
if (!set) continue;
|
|
138
|
+
for (const entry of set.entries) {
|
|
139
|
+
if (entry.baselineKey === baselineKey) return shape;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return 'unknown';
|
|
143
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// Baseline-AC opt-out ledger (spec §5.4).
|
|
2
|
+
//
|
|
3
|
+
// Writes and removes entries on manifest.baselineAcOptOuts[]. Composes
|
|
4
|
+
// with the design-shape answer opt-out surface that Track A owns
|
|
5
|
+
// (preflight/manifest-writer.js already carries composeOptOutRecord for
|
|
6
|
+
// the preflight-driven case); this module is the C+D-native writer for
|
|
7
|
+
// operator rulings recorded outside a preflight session.
|
|
8
|
+
//
|
|
9
|
+
// Both surfaces write to the same array; both use the same
|
|
10
|
+
// boo-YYYY-MM-DD-NNN monotonic id scheme so id collisions across
|
|
11
|
+
// surfaces are impossible.
|
|
12
|
+
|
|
13
|
+
import { mkdir, rename, unlink, writeFile } from 'node:fs/promises';
|
|
14
|
+
import { dirname, join } from 'node:path';
|
|
15
|
+
|
|
16
|
+
import { rcfError, isRcfError } from '#core/errors';
|
|
17
|
+
import { validateDocument } from '#core/store';
|
|
18
|
+
|
|
19
|
+
const OPT_OUT_ID_RE = /^boo-\d{4}-\d{2}-\d{2}-(\d{3})$/;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Compose the next monotonic opt-out id for the current date. Reads the
|
|
23
|
+
* existing manifest to find the max NNN suffix for the same YYYY-MM-DD.
|
|
24
|
+
*
|
|
25
|
+
* @param {object|null} manifest
|
|
26
|
+
* @param {Date} [now]
|
|
27
|
+
* @returns {string}
|
|
28
|
+
*/
|
|
29
|
+
export function nextOptOutId(manifest, now = new Date()) {
|
|
30
|
+
const yyyy = now.getUTCFullYear().toString().padStart(4, '0');
|
|
31
|
+
const mm = (now.getUTCMonth() + 1).toString().padStart(2, '0');
|
|
32
|
+
const dd = now.getUTCDate().toString().padStart(2, '0');
|
|
33
|
+
const prefix = `boo-${yyyy}-${mm}-${dd}-`;
|
|
34
|
+
const existing = Array.isArray(manifest?.baselineAcOptOuts) ? manifest.baselineAcOptOuts : [];
|
|
35
|
+
let maxN = 0;
|
|
36
|
+
for (const rec of existing) {
|
|
37
|
+
if (typeof rec?.id !== 'string') continue;
|
|
38
|
+
const m = rec.id.match(OPT_OUT_ID_RE);
|
|
39
|
+
if (!m) continue;
|
|
40
|
+
if (!rec.id.startsWith(prefix)) continue;
|
|
41
|
+
const n = Number.parseInt(m[1], 10);
|
|
42
|
+
if (Number.isFinite(n) && n > maxN) maxN = n;
|
|
43
|
+
}
|
|
44
|
+
return `${prefix}${(maxN + 1).toString().padStart(3, '0')}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Compose a C+D-native opt-out record (no preflight linkage).
|
|
49
|
+
*
|
|
50
|
+
* @param {object} args
|
|
51
|
+
* @param {object|null} args.manifest
|
|
52
|
+
* @param {string} args.reqId
|
|
53
|
+
* @param {string} args.baselineKey
|
|
54
|
+
* @param {string} args.reason minLength 20 (schema-enforced)
|
|
55
|
+
* @param {'req'|'project'} [args.scope] defaults to req
|
|
56
|
+
* @param {Date} [args.now]
|
|
57
|
+
* @returns {object}
|
|
58
|
+
*/
|
|
59
|
+
export function composeOptOutRecord({ manifest, reqId, baselineKey, reason, scope = 'req', now = new Date() }) {
|
|
60
|
+
const isoNow = now.toISOString();
|
|
61
|
+
const id = nextOptOutId(manifest, now);
|
|
62
|
+
const record = {
|
|
63
|
+
id,
|
|
64
|
+
createdAt: isoNow,
|
|
65
|
+
baselineKey,
|
|
66
|
+
scope,
|
|
67
|
+
reason,
|
|
68
|
+
operatorAckAt: isoNow,
|
|
69
|
+
};
|
|
70
|
+
if (scope === 'req') record.reqId = reqId;
|
|
71
|
+
return record;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Write a new opt-out record atomically.
|
|
76
|
+
*
|
|
77
|
+
* @param {object} args
|
|
78
|
+
* @param {string} args.projectRoot
|
|
79
|
+
* @param {import('#core/store/walker.js').TreeModel} args.tree
|
|
80
|
+
* @param {string} args.reqId
|
|
81
|
+
* @param {string} args.baselineKey
|
|
82
|
+
* @param {string} args.reason
|
|
83
|
+
* @param {'req'|'project'} [args.scope]
|
|
84
|
+
* @param {Date} [args.now]
|
|
85
|
+
* @returns {Promise<{ id: string, record: object } | import('#core/errors').RcfError>}
|
|
86
|
+
*/
|
|
87
|
+
export async function writeOptOut({ projectRoot, tree, reqId, baselineKey, reason, scope = 'req', now = new Date() }) {
|
|
88
|
+
const manifest = tree?.manifest ?? {};
|
|
89
|
+
const record = composeOptOutRecord({ manifest, reqId, baselineKey, reason, scope, now });
|
|
90
|
+
const nextManifest = {
|
|
91
|
+
...manifest,
|
|
92
|
+
baselineAcOptOuts: [
|
|
93
|
+
...(Array.isArray(manifest.baselineAcOptOuts) ? manifest.baselineAcOptOuts : []),
|
|
94
|
+
record,
|
|
95
|
+
],
|
|
96
|
+
};
|
|
97
|
+
const validation = validateDocument({ doc: nextManifest, kind: 'manifest', filePath: 'rcf/manifest.json' });
|
|
98
|
+
if (validation) return validation;
|
|
99
|
+
const abs = join(projectRoot, 'rcf', 'manifest.json');
|
|
100
|
+
try {
|
|
101
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
102
|
+
const tmp = `${abs}.tmp`;
|
|
103
|
+
await writeFile(tmp, `${JSON.stringify(nextManifest, null, 2)}\n`, 'utf8');
|
|
104
|
+
try {
|
|
105
|
+
await rename(tmp, abs);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
try { await unlink(tmp); } catch { /* ignore */ }
|
|
108
|
+
throw err;
|
|
109
|
+
}
|
|
110
|
+
} catch (err) {
|
|
111
|
+
return rcfError({
|
|
112
|
+
kind: 'ioFailure',
|
|
113
|
+
message: `req-baseline opt-out: manifest write failed: ${err.message}`,
|
|
114
|
+
filePath: 'rcf/manifest.json',
|
|
115
|
+
stack: err.stack,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return { id: record.id, record };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Remove any existing opt-out matching the reqId/baselineKey. Deletes
|
|
123
|
+
* both req-scoped and project-scoped matches for the same baselineKey
|
|
124
|
+
* so a stale opt-out cannot linger.
|
|
125
|
+
*
|
|
126
|
+
* @param {object} args
|
|
127
|
+
* @param {string} args.projectRoot
|
|
128
|
+
* @param {import('#core/store/walker.js').TreeModel} args.tree
|
|
129
|
+
* @param {string} args.reqId
|
|
130
|
+
* @param {string} args.baselineKey
|
|
131
|
+
* @returns {Promise<{ removed: number } | import('#core/errors').RcfError>}
|
|
132
|
+
*/
|
|
133
|
+
export async function removeOptOut({ projectRoot, tree, reqId, baselineKey }) {
|
|
134
|
+
const manifest = tree?.manifest ?? {};
|
|
135
|
+
const existing = Array.isArray(manifest.baselineAcOptOuts) ? manifest.baselineAcOptOuts : [];
|
|
136
|
+
const remaining = existing.filter((r) => !(r?.baselineKey === baselineKey && (r?.scope === 'project' || r?.reqId === reqId)));
|
|
137
|
+
const removed = existing.length - remaining.length;
|
|
138
|
+
if (removed === 0) return { removed: 0 };
|
|
139
|
+
const nextManifest = { ...manifest, baselineAcOptOuts: remaining };
|
|
140
|
+
const validation = validateDocument({ doc: nextManifest, kind: 'manifest', filePath: 'rcf/manifest.json' });
|
|
141
|
+
if (validation) return validation;
|
|
142
|
+
const abs = join(projectRoot, 'rcf', 'manifest.json');
|
|
143
|
+
try {
|
|
144
|
+
const tmp = `${abs}.tmp`;
|
|
145
|
+
await writeFile(tmp, `${JSON.stringify(nextManifest, null, 2)}\n`, 'utf8');
|
|
146
|
+
try {
|
|
147
|
+
await rename(tmp, abs);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
try { await unlink(tmp); } catch { /* ignore */ }
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
152
|
+
} catch (err) {
|
|
153
|
+
return rcfError({
|
|
154
|
+
kind: 'ioFailure',
|
|
155
|
+
message: `req-baseline opt-out --remove: manifest write failed: ${err.message}`,
|
|
156
|
+
filePath: 'rcf/manifest.json',
|
|
157
|
+
stack: err.stack,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return { removed };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Build a Set of baselineKeys opted out for a given REQ.
|
|
165
|
+
*
|
|
166
|
+
* @param {object|null} manifest
|
|
167
|
+
* @param {string} reqId
|
|
168
|
+
* @returns {Set<string>}
|
|
169
|
+
*/
|
|
170
|
+
export function optOutMap(manifest, reqId) {
|
|
171
|
+
const entries = Array.isArray(manifest?.baselineAcOptOuts) ? manifest.baselineAcOptOuts : [];
|
|
172
|
+
const out = new Set();
|
|
173
|
+
for (const r of entries) {
|
|
174
|
+
if (!r || typeof r.baselineKey !== 'string') continue;
|
|
175
|
+
if (r.scope === 'project') out.add(r.baselineKey);
|
|
176
|
+
else if (r.scope === 'req' && r.reqId === reqId) out.add(r.baselineKey);
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Convenience predicate.
|
|
183
|
+
*
|
|
184
|
+
* @param {object|null} manifest
|
|
185
|
+
* @param {string} reqId
|
|
186
|
+
* @param {string} baselineKey
|
|
187
|
+
* @returns {boolean}
|
|
188
|
+
*/
|
|
189
|
+
export function isOptedOut(manifest, reqId, baselineKey) {
|
|
190
|
+
return optOutMap(manifest, reqId).has(baselineKey);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Kept exported for consumers that need the RcfError predicate alongside
|
|
194
|
+
// the writer helpers.
|
|
195
|
+
void isRcfError;
|