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,11 @@
|
|
|
1
|
+
// Public surface for the design substage module
|
|
2
|
+
// (ui-design-gate-0.7.0-spec §5). CLI handler at `src/cli/design.js`.
|
|
3
|
+
|
|
4
|
+
export {
|
|
5
|
+
firstBaselineDisagreement,
|
|
6
|
+
missingDesignStageArtefacts,
|
|
7
|
+
writeJourneyAdd,
|
|
8
|
+
writeMarkComplete,
|
|
9
|
+
writeNavSet,
|
|
10
|
+
writeThemeA11ySet,
|
|
11
|
+
} from './writer.js';
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// Design substage writer (ui-design-gate-0.7.0-spec §5, §6.2).
|
|
2
|
+
//
|
|
3
|
+
// Persistence primitives for the three artefacts (journeys, navModel,
|
|
4
|
+
// themeAndA11y) plus the `designStageComplete` boolean. Each verb
|
|
5
|
+
// writes via the core store's `updateDocument` path so the writer
|
|
6
|
+
// bumps `updatedAt` and schema-validates the FBS document.
|
|
7
|
+
//
|
|
8
|
+
// Grammar constraint (spec §5.5): the sub-verb parser lives in the CLI
|
|
9
|
+
// handler (`src/cli/design.js`); this module accepts already-parsed
|
|
10
|
+
// arguments and does not need to defend against string positional
|
|
11
|
+
// confusion at this layer.
|
|
12
|
+
|
|
13
|
+
import { rcfError } from '#core/errors';
|
|
14
|
+
import { updateDocument } from '#core/store';
|
|
15
|
+
|
|
16
|
+
import { baselineDesignDisagreement } from '../ui-baseline/manifest-writer.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {object} DesignJourney
|
|
20
|
+
* @property {string} id
|
|
21
|
+
* @property {string} actor
|
|
22
|
+
* @property {string} goal
|
|
23
|
+
* @property {string[]} steps
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {object} DesignNavRoute
|
|
28
|
+
* @property {string} path
|
|
29
|
+
* @property {string} label
|
|
30
|
+
* @property {boolean} authRequired
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const NAV_SHAPES = new Set([
|
|
34
|
+
'shared-persistent',
|
|
35
|
+
'shared-per-section',
|
|
36
|
+
'none-single-page',
|
|
37
|
+
'operator-declared-other',
|
|
38
|
+
]);
|
|
39
|
+
const THEME_MODES = new Set([
|
|
40
|
+
'light-default-with-toggle',
|
|
41
|
+
'dark-default-with-toggle',
|
|
42
|
+
'single-theme-declared',
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Append a journey to `fbs.designStage.journeys[]`. Writes via
|
|
47
|
+
* `updateDocument`.
|
|
48
|
+
*
|
|
49
|
+
* @param {object} args
|
|
50
|
+
* @param {string} args.projectRoot
|
|
51
|
+
* @param {import('#core/store/walker.js').TreeModel} args.tree
|
|
52
|
+
* @param {string} args.fbsId
|
|
53
|
+
* @param {DesignJourney} args.journey
|
|
54
|
+
* @param {string} [args.authoredBy]
|
|
55
|
+
* @param {Date} [args.now]
|
|
56
|
+
*/
|
|
57
|
+
export async function writeJourneyAdd({ projectRoot, tree, fbsId, journey, authoredBy = 'operator', now = new Date() }) {
|
|
58
|
+
const fbs = tree.byId?.get(fbsId);
|
|
59
|
+
if (!fbs || tree.kindById?.get(fbsId) !== 'fbs') {
|
|
60
|
+
return rcfError({ kind: 'usage', message: `design journeys add: ${fbsId} not found or not an FBS`, documentId: fbsId });
|
|
61
|
+
}
|
|
62
|
+
const validation = validateJourneyShape(journey);
|
|
63
|
+
if (validation) return validation;
|
|
64
|
+
const existing = fbs.designStage?.journeys ?? [];
|
|
65
|
+
if (existing.some((j) => j.id === journey.id)) {
|
|
66
|
+
return rcfError({ kind: 'usage', message: `design journeys add: journey id '${journey.id}' already exists on ${fbsId}`, documentId: fbsId });
|
|
67
|
+
}
|
|
68
|
+
const nextStage = {
|
|
69
|
+
...(fbs.designStage ?? {}),
|
|
70
|
+
journeys: [...existing, { ...journey }],
|
|
71
|
+
authoredAt: now.toISOString(),
|
|
72
|
+
authoredBy,
|
|
73
|
+
};
|
|
74
|
+
return await updateDocument({
|
|
75
|
+
projectRoot, tree, id: fbsId,
|
|
76
|
+
sets: [{ path: 'designStage', value: nextStage }],
|
|
77
|
+
options: {},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Overwrite `fbs.designStage.navModel`. Writes via `updateDocument`.
|
|
83
|
+
*
|
|
84
|
+
* @param {object} args
|
|
85
|
+
*/
|
|
86
|
+
export async function writeNavSet({ projectRoot, tree, fbsId, shape, routes, signedInAsAffordance, notes, authoredBy = 'operator', now = new Date() }) {
|
|
87
|
+
const fbs = tree.byId?.get(fbsId);
|
|
88
|
+
if (!fbs || tree.kindById?.get(fbsId) !== 'fbs') {
|
|
89
|
+
return rcfError({ kind: 'usage', message: `design nav set: ${fbsId} not found or not an FBS`, documentId: fbsId });
|
|
90
|
+
}
|
|
91
|
+
if (!NAV_SHAPES.has(shape)) {
|
|
92
|
+
return rcfError({ kind: 'usage', message: `design nav set: unknown --shape '${shape}' (expected ${[...NAV_SHAPES].join(' | ')})` });
|
|
93
|
+
}
|
|
94
|
+
if (!Array.isArray(routes) || routes.length === 0) {
|
|
95
|
+
return rcfError({ kind: 'usage', message: 'design nav set: at least one --route is required' });
|
|
96
|
+
}
|
|
97
|
+
for (const r of routes) {
|
|
98
|
+
if (!r || typeof r.path !== 'string' || typeof r.label !== 'string' || typeof r.authRequired !== 'boolean') {
|
|
99
|
+
return rcfError({ kind: 'usage', message: `design nav set: bad route '${JSON.stringify(r)}'; expected path=label:authRequired` });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const navModel = { shape, routes: routes.map((r) => ({ path: r.path, label: r.label, authRequired: r.authRequired })) };
|
|
103
|
+
if (typeof signedInAsAffordance === 'boolean') navModel.signedInAsAffordance = signedInAsAffordance;
|
|
104
|
+
if (typeof notes === 'string' && notes.length > 0) navModel.notes = notes;
|
|
105
|
+
const nextStage = {
|
|
106
|
+
...(fbs.designStage ?? {}),
|
|
107
|
+
navModel,
|
|
108
|
+
authoredAt: now.toISOString(),
|
|
109
|
+
authoredBy,
|
|
110
|
+
};
|
|
111
|
+
return await updateDocument({
|
|
112
|
+
projectRoot, tree, id: fbsId,
|
|
113
|
+
sets: [{ path: 'designStage', value: nextStage }],
|
|
114
|
+
options: {},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Overwrite `fbs.designStage.themeAndA11y`. Writes via `updateDocument`.
|
|
120
|
+
*
|
|
121
|
+
* @param {object} args
|
|
122
|
+
*/
|
|
123
|
+
export async function writeThemeA11ySet({
|
|
124
|
+
projectRoot, tree, fbsId, themeMode, themeTokensModule, contrastTargets,
|
|
125
|
+
contrastTestPath, contrastTestAuthoredBeforePalette, authoredBy = 'operator', now = new Date(),
|
|
126
|
+
}) {
|
|
127
|
+
const fbs = tree.byId?.get(fbsId);
|
|
128
|
+
if (!fbs || tree.kindById?.get(fbsId) !== 'fbs') {
|
|
129
|
+
return rcfError({ kind: 'usage', message: `design theme-a11y set: ${fbsId} not found or not an FBS`, documentId: fbsId });
|
|
130
|
+
}
|
|
131
|
+
if (!THEME_MODES.has(themeMode)) {
|
|
132
|
+
return rcfError({ kind: 'usage', message: `design theme-a11y set: unknown --mode '${themeMode}' (expected ${[...THEME_MODES].join(' | ')})` });
|
|
133
|
+
}
|
|
134
|
+
if (typeof themeTokensModule !== 'string' || themeTokensModule.length === 0) {
|
|
135
|
+
return rcfError({ kind: 'usage', message: 'design theme-a11y set: --tokens is required' });
|
|
136
|
+
}
|
|
137
|
+
if (typeof contrastTestPath !== 'string' || contrastTestPath.length === 0) {
|
|
138
|
+
return rcfError({ kind: 'usage', message: 'design theme-a11y set: --contrast-test is required' });
|
|
139
|
+
}
|
|
140
|
+
if (typeof contrastTestAuthoredBeforePalette !== 'boolean') {
|
|
141
|
+
return rcfError({ kind: 'usage', message: 'design theme-a11y set: --contrast-before-palette (true|false) is required' });
|
|
142
|
+
}
|
|
143
|
+
const themeAndA11y = {
|
|
144
|
+
themeMode,
|
|
145
|
+
themeTokensModule,
|
|
146
|
+
contrastTargets: contrastTargets || 'WCAG AA',
|
|
147
|
+
contrastTestPath,
|
|
148
|
+
contrastTestAuthoredBeforePalette,
|
|
149
|
+
};
|
|
150
|
+
const nextStage = {
|
|
151
|
+
...(fbs.designStage ?? {}),
|
|
152
|
+
themeAndA11y,
|
|
153
|
+
authoredAt: now.toISOString(),
|
|
154
|
+
authoredBy,
|
|
155
|
+
};
|
|
156
|
+
return await updateDocument({
|
|
157
|
+
projectRoot, tree, id: fbsId,
|
|
158
|
+
sets: [{ path: 'designStage', value: nextStage }],
|
|
159
|
+
options: {},
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Set `designStageComplete: true`. Refuses when any of the three
|
|
165
|
+
* artefacts is absent or empty, and when the baseline disagrees with
|
|
166
|
+
* the designStage without an opt-out (spec §6.2 second refusal shape).
|
|
167
|
+
*
|
|
168
|
+
* @param {object} args
|
|
169
|
+
* @returns {Promise<{ ok: true } | import('#core/errors').RcfError>}
|
|
170
|
+
*/
|
|
171
|
+
export async function writeMarkComplete({
|
|
172
|
+
projectRoot, tree, fbsId, authoredBy = 'operator', now = new Date(),
|
|
173
|
+
}) {
|
|
174
|
+
const fbs = tree.byId?.get(fbsId);
|
|
175
|
+
if (!fbs || tree.kindById?.get(fbsId) !== 'fbs') {
|
|
176
|
+
return rcfError({ kind: 'usage', message: `design --mark-complete: ${fbsId} not found or not an FBS`, documentId: fbsId });
|
|
177
|
+
}
|
|
178
|
+
const missing = missingDesignStageArtefacts(fbs);
|
|
179
|
+
if (missing.length > 0) {
|
|
180
|
+
return rcfError({
|
|
181
|
+
kind: 'usage',
|
|
182
|
+
message: `design --mark-complete: refused - ${fbsId} designStage is missing: ${missing.join(', ')}. `
|
|
183
|
+
+ 'Author the missing artefacts first via `rcf design ' + fbsId + ' journeys add ...`, `nav set ...`, or `theme-a11y set ...`.',
|
|
184
|
+
documentId: fbsId,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
const disagreement = firstBaselineDisagreement(tree, fbs);
|
|
188
|
+
if (disagreement) {
|
|
189
|
+
return rcfError({
|
|
190
|
+
kind: 'usage',
|
|
191
|
+
message: `design --mark-complete: refused - ${fbsId} designStage.${disagreement.designStagePath} = ${JSON.stringify(disagreement.designValue)} conflicts with uiBaseline.defaults.${disagreement.path} = ${JSON.stringify(disagreement.baselineValue)} and there is no operatorOptOuts entry. `
|
|
192
|
+
+ 'Options: (1) change designStage to match, (2) rcf ui-baseline opt-out --field '
|
|
193
|
+
+ disagreement.path + ' --reason "..." (project override), (3) rcf update '
|
|
194
|
+
+ fbsId + ' --set designStage.' + disagreement.designStagePath + '=... (per-FBS override).',
|
|
195
|
+
documentId: fbsId,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
const nextStage = {
|
|
199
|
+
...(fbs.designStage ?? {}),
|
|
200
|
+
authoredAt: now.toISOString(),
|
|
201
|
+
authoredBy,
|
|
202
|
+
};
|
|
203
|
+
const result = await updateDocument({
|
|
204
|
+
projectRoot, tree, id: fbsId,
|
|
205
|
+
sets: [
|
|
206
|
+
{ path: 'designStage', value: nextStage },
|
|
207
|
+
{ path: 'designStageComplete', value: true },
|
|
208
|
+
],
|
|
209
|
+
options: {},
|
|
210
|
+
});
|
|
211
|
+
if (result && 'kind' in result && 'message' in result) return result;
|
|
212
|
+
return { ok: true };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Which of the three required artefacts is missing or empty on the FBS.
|
|
217
|
+
*
|
|
218
|
+
* @param {object} fbs
|
|
219
|
+
* @returns {string[]}
|
|
220
|
+
*/
|
|
221
|
+
export function missingDesignStageArtefacts(fbs) {
|
|
222
|
+
const missing = [];
|
|
223
|
+
const stage = fbs?.designStage ?? {};
|
|
224
|
+
if (!Array.isArray(stage.journeys) || stage.journeys.length === 0) missing.push('journeys');
|
|
225
|
+
if (!stage.navModel || !Array.isArray(stage.navModel.routes) || stage.navModel.routes.length === 0) missing.push('navModel');
|
|
226
|
+
if (!stage.themeAndA11y || typeof stage.themeAndA11y.themeMode !== 'string') missing.push('themeAndA11y');
|
|
227
|
+
return missing;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* First baseline-vs-designStage disagreement (per spec §6.2). Returns
|
|
232
|
+
* null on agreement or when no baseline / designStage exists.
|
|
233
|
+
*
|
|
234
|
+
* @param {import('#core/store/walker.js').TreeModel} tree
|
|
235
|
+
* @param {object} fbs
|
|
236
|
+
* @returns {{ path: string, designStagePath: string, baselineValue: *, designValue: * }|null}
|
|
237
|
+
*/
|
|
238
|
+
export function firstBaselineDisagreement(tree, fbs) {
|
|
239
|
+
const baseline = tree?.manifest?.uiBaseline;
|
|
240
|
+
const stage = fbs?.designStage;
|
|
241
|
+
if (!baseline || !stage) return null;
|
|
242
|
+
// Fields that pair between baseline defaults and designStage.
|
|
243
|
+
const pairs = [
|
|
244
|
+
{ defaults: 'themeMode', designStage: 'themeAndA11y.themeMode' },
|
|
245
|
+
{ defaults: 'designTokensModule', designStage: 'themeAndA11y.themeTokensModule' },
|
|
246
|
+
{ defaults: 'contrastTestBeforePalette', designStage: 'themeAndA11y.contrastTestAuthoredBeforePalette' },
|
|
247
|
+
];
|
|
248
|
+
for (const p of pairs) {
|
|
249
|
+
const disagreement = baselineDesignDisagreement(baseline, stage, p.defaults, p.designStage);
|
|
250
|
+
if (disagreement) {
|
|
251
|
+
return { ...disagreement, designStagePath: p.designStage };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function validateJourneyShape(j) {
|
|
258
|
+
if (!j || typeof j !== 'object') return rcfError({ kind: 'usage', message: 'design journeys add: journey object required' });
|
|
259
|
+
if (typeof j.id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(j.id)) {
|
|
260
|
+
return rcfError({ kind: 'usage', message: `design journeys add: --id must be a lowercase slug matching /^[a-z][a-z0-9-]*$/, got '${j.id}'` });
|
|
261
|
+
}
|
|
262
|
+
if (typeof j.actor !== 'string' || j.actor.length === 0) return rcfError({ kind: 'usage', message: 'design journeys add: --actor is required' });
|
|
263
|
+
if (typeof j.goal !== 'string' || j.goal.length === 0) return rcfError({ kind: 'usage', message: 'design journeys add: --goal is required' });
|
|
264
|
+
if (!Array.isArray(j.steps) || j.steps.length < 2 || j.steps.length > 8) {
|
|
265
|
+
return rcfError({ kind: 'usage', message: `design journeys add: --step required 2 to 8 times, got ${Array.isArray(j.steps) ? j.steps.length : 0}` });
|
|
266
|
+
}
|
|
267
|
+
for (const s of j.steps) if (typeof s !== 'string' || s.length === 0) {
|
|
268
|
+
return rcfError({ kind: 'usage', message: 'design journeys add: each --step must be a non-empty string' });
|
|
269
|
+
}
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// rcf-verify install detection (spec §8.3, amendment 5 - install-together
|
|
2
|
+
// posture). The finalise gate MUST detect whether `rcf-verify` is resolvable
|
|
3
|
+
// and, when it is absent, prompt to install it - NEVER silently skip the ship
|
|
4
|
+
// gate (the one behaviour §8.3 explicitly forbids).
|
|
5
|
+
//
|
|
6
|
+
// Two detection routes, in order, matching how the CLI is actually installed:
|
|
7
|
+
// 1. The `rcf-verify` bin on PATH - the umbrella install exposes both `rcf`
|
|
8
|
+
// and `rcf-verify` as global bins (`npm i -g rcf-lite`).
|
|
9
|
+
// 2. Package resolution from the project dir - the local-project install
|
|
10
|
+
// (`npm i rcf-lite` in a repo's node_modules). Two package specifiers
|
|
11
|
+
// are tried in order: the current umbrella `rcf-lite`, then the legacy
|
|
12
|
+
// `@stravica-ai/rcf-verify-lite` name (deprecated at 0.7.1 but still
|
|
13
|
+
// resolvable in lockfiles that pinned it). Either resolves to the same
|
|
14
|
+
// `bin/rcf-verify.js` entry point.
|
|
15
|
+
// Either route yields a concrete invocation the finalise spawn (spawn.js)
|
|
16
|
+
// uses verbatim. A miss returns { installed:false } and the caller enters
|
|
17
|
+
// the prompt-or-explicit-flag path.
|
|
18
|
+
|
|
19
|
+
import { access, constants } from 'node:fs/promises';
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
21
|
+
import { delimiter, dirname, join, resolve } from 'node:path';
|
|
22
|
+
|
|
23
|
+
// VERIFY_PACKAGE is the current install target and the name shown to
|
|
24
|
+
// operators in absent-verify messaging. VERIFY_PACKAGE_LEGACY is only used
|
|
25
|
+
// as a resolution fallback for lockfiles still pinning the pre-0.7.1 scoped
|
|
26
|
+
// name; it is never displayed as an install target.
|
|
27
|
+
const VERIFY_PACKAGE = 'rcf-lite';
|
|
28
|
+
const VERIFY_PACKAGE_LEGACY = '@stravica-ai/rcf-verify-lite';
|
|
29
|
+
const VERIFY_PACKAGE_CANDIDATES = [VERIFY_PACKAGE, VERIFY_PACKAGE_LEGACY];
|
|
30
|
+
const VERIFY_BIN = 'rcf-verify';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The concrete way to launch rcf-verify as a fresh subprocess.
|
|
34
|
+
* - `{ command: '<abs-bin>', prefixArgs: [] }` for a PATH / shim bin
|
|
35
|
+
* (its own shebang runs it).
|
|
36
|
+
* - `{ command: process.execPath, prefixArgs: ['<abs-entry.js>'] }` for a
|
|
37
|
+
* package-resolved entry (run it under the current node).
|
|
38
|
+
* @typedef {{ command: string, prefixArgs: string[], source: 'path'|'package' }} VerifyInvocation
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @typedef {{ installed: boolean, invocation: VerifyInvocation | null }} VerifyDetection
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Scan PATH for an executable named `rcf-verify` (plus the Windows
|
|
47
|
+
* `.cmd`/`.exe` shim variants). Returns the first absolute path that exists
|
|
48
|
+
* and is executable, or null.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} name
|
|
51
|
+
* @param {object} [io]
|
|
52
|
+
* @param {NodeJS.ProcessEnv} [io.env]
|
|
53
|
+
* @returns {Promise<string|null>}
|
|
54
|
+
*/
|
|
55
|
+
export async function findOnPath(name, { env = process.env } = {}) {
|
|
56
|
+
const rawPath = env.PATH ?? env.Path ?? '';
|
|
57
|
+
if (!rawPath) return null;
|
|
58
|
+
const dirs = rawPath.split(delimiter).filter(Boolean);
|
|
59
|
+
// On Windows a bare name resolves via PATHEXT-style shims; probe the common
|
|
60
|
+
// npm shim names. On POSIX only the bare name matters.
|
|
61
|
+
const candidates = process.platform === 'win32'
|
|
62
|
+
? [`${name}.cmd`, `${name}.exe`, name]
|
|
63
|
+
: [name];
|
|
64
|
+
for (const dir of dirs) {
|
|
65
|
+
for (const candidate of candidates) {
|
|
66
|
+
const full = resolve(dir, candidate);
|
|
67
|
+
try {
|
|
68
|
+
// X_OK is meaningless for the .cmd/.exe shims on Windows; existence is
|
|
69
|
+
// enough there. On POSIX require the execute bit.
|
|
70
|
+
await access(full, process.platform === 'win32' ? constants.F_OK : constants.X_OK);
|
|
71
|
+
return full;
|
|
72
|
+
} catch {
|
|
73
|
+
// not here; keep scanning
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Resolve the rcf-verify bin entry point from a starting directory, following
|
|
82
|
+
* the normal node_modules resolution the caller's project sees. Returns the
|
|
83
|
+
* absolute path to `bin/rcf-verify.js`, or null if no candidate package is
|
|
84
|
+
* installed / resolvable from there.
|
|
85
|
+
*
|
|
86
|
+
* Candidates are tried in the order defined by VERIFY_PACKAGE_CANDIDATES: the
|
|
87
|
+
* current umbrella (`rcf-lite`) first, then the deprecated scoped name
|
|
88
|
+
* (`@stravica-ai/rcf-verify-lite`) as a fallback for lockfiles pinned to it.
|
|
89
|
+
* The first candidate whose bin resolves and exists on disk wins.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} fromDir - directory to resolve from (the project root / cwd)
|
|
92
|
+
* @returns {Promise<string|null>}
|
|
93
|
+
*/
|
|
94
|
+
export async function resolvePackageBin(fromDir) {
|
|
95
|
+
// Resolve from a synthetic module living in fromDir so node walks that
|
|
96
|
+
// project's node_modules chain, not this package's own.
|
|
97
|
+
let req;
|
|
98
|
+
try {
|
|
99
|
+
req = createRequire(join(fromDir, 'noop.js'));
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
for (const candidate of VERIFY_PACKAGE_CANDIDATES) {
|
|
104
|
+
try {
|
|
105
|
+
const pkgJsonPath = req.resolve(`${candidate}/package.json`);
|
|
106
|
+
const req2 = createRequire(pkgJsonPath);
|
|
107
|
+
const pkg = req2(`${candidate}/package.json`);
|
|
108
|
+
const binField = pkg.bin;
|
|
109
|
+
const rel = typeof binField === 'string' ? binField : binField?.[VERIFY_BIN];
|
|
110
|
+
if (!rel) continue;
|
|
111
|
+
const abs = resolve(dirname(pkgJsonPath), rel);
|
|
112
|
+
await access(abs, constants.F_OK);
|
|
113
|
+
return abs;
|
|
114
|
+
} catch {
|
|
115
|
+
// Candidate not resolvable from here; try the next one.
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Detect whether rcf-verify is resolvable and, if so, how to launch it.
|
|
123
|
+
* Deps are injectable so the finalise gate can be exercised without a real
|
|
124
|
+
* rcf-verify on the test machine.
|
|
125
|
+
*
|
|
126
|
+
* @param {object} [deps]
|
|
127
|
+
* @param {string} [deps.cwd] - project dir to resolve a local install from
|
|
128
|
+
* @param {typeof findOnPath} [deps.findOnPath]
|
|
129
|
+
* @param {typeof resolvePackageBin} [deps.resolvePackageBin]
|
|
130
|
+
* @param {NodeJS.ProcessEnv} [deps.env]
|
|
131
|
+
* @returns {Promise<VerifyDetection>}
|
|
132
|
+
*/
|
|
133
|
+
export async function detectVerify(deps = {}) {
|
|
134
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
135
|
+
const onPath = deps.findOnPath ? await deps.findOnPath(VERIFY_BIN, { env: deps.env })
|
|
136
|
+
: await findOnPath(VERIFY_BIN, { env: deps.env });
|
|
137
|
+
if (onPath) {
|
|
138
|
+
return { installed: true, invocation: { command: onPath, prefixArgs: [], source: 'path' } };
|
|
139
|
+
}
|
|
140
|
+
const resolved = deps.resolvePackageBin ? await deps.resolvePackageBin(cwd)
|
|
141
|
+
: await resolvePackageBin(cwd);
|
|
142
|
+
if (resolved) {
|
|
143
|
+
return {
|
|
144
|
+
installed: true,
|
|
145
|
+
invocation: { command: process.execPath, prefixArgs: [resolved], source: 'package' },
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return { installed: false, invocation: null };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export { VERIFY_PACKAGE, VERIFY_PACKAGE_LEGACY, VERIFY_PACKAGE_CANDIDATES, VERIFY_BIN };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Finalise-gate barrel (spec §8). build-lite's finalise step invokes rcf-verify
|
|
2
|
+
// as a fresh subprocess with the isolation env (spawn.js), gates the
|
|
3
|
+
// complete -> verified transition on the subprocess exit code AND the report's
|
|
4
|
+
// ship authority (spec §4 - a correctness-only pass holds without promoting),
|
|
5
|
+
// ingests findings from the --out report (ingest.js), and - when rcf-verify is
|
|
6
|
+
// absent - prompts to install rather than silently skipping the gate (detect.js
|
|
7
|
+
// + install.js).
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
detectVerify,
|
|
11
|
+
findOnPath,
|
|
12
|
+
resolvePackageBin,
|
|
13
|
+
VERIFY_PACKAGE,
|
|
14
|
+
VERIFY_PACKAGE_LEGACY,
|
|
15
|
+
VERIFY_PACKAGE_CANDIDATES,
|
|
16
|
+
VERIFY_BIN,
|
|
17
|
+
} from './detect.js';
|
|
18
|
+
export { buildVerifyArgs, spawnVerify } from './spawn.js';
|
|
19
|
+
export { promptYesNo, installVerify, resolveAbsentVerify } from './install.js';
|
|
20
|
+
export {
|
|
21
|
+
loadReport, summariseReport,
|
|
22
|
+
findMockOnlyDeclaredAcs, reportHasMockOnlyDeclared,
|
|
23
|
+
// 0.8.0 slug-train car 4: NV-BL-GATE-01 pull-in of the profile-vs-AC
|
|
24
|
+
// scope-mismatch check into REVIEW.
|
|
25
|
+
findScopeMismatchAcs, reportHasScopeMismatch,
|
|
26
|
+
} from './ingest.js';
|
|
27
|
+
export {
|
|
28
|
+
composeShipWithoutVerifiedRecord,
|
|
29
|
+
nextShipWithoutVerifiedId,
|
|
30
|
+
writeShipWithoutVerifiedRecord,
|
|
31
|
+
} from './ship-without-verified.js';
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Report ingestion (spec §8.2 + §5.4). Findings flow from verify to build-lite
|
|
2
|
+
// via the --out report file (chain-node-addressed camelCase JSON), NEVER via
|
|
3
|
+
// stdout scraping. build-lite reads the artifact and, on a blocked gate,
|
|
4
|
+
// surfaces the findings mapped to their contract lines (acId / chain node) so
|
|
5
|
+
// the operator can drive the §5.4 verify -> fix -> re-verify loop.
|
|
6
|
+
//
|
|
7
|
+
// This is deliberately a READ of verify's artifact - build never re-derives a
|
|
8
|
+
// verdict, it consumes the one verify stamped.
|
|
9
|
+
|
|
10
|
+
import { readFile } from 'node:fs/promises';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Load and parse a verify report artifact. Returns the parsed report, or a
|
|
14
|
+
* shape describing why it could not be read (missing / unparseable) so the
|
|
15
|
+
* caller can degrade gracefully - a gate failure with an unreadable report is
|
|
16
|
+
* still a gate failure, never a pass.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} reportPath
|
|
19
|
+
* @param {object} [deps]
|
|
20
|
+
* @param {typeof readFile} [deps.readFile]
|
|
21
|
+
* @returns {Promise<{ ok: true, report: object } | { ok: false, reason: string }>}
|
|
22
|
+
*/
|
|
23
|
+
export async function loadReport(reportPath, deps = {}) {
|
|
24
|
+
const read = deps.readFile ?? readFile;
|
|
25
|
+
let raw;
|
|
26
|
+
try {
|
|
27
|
+
raw = await read(reportPath, 'utf8');
|
|
28
|
+
} catch (err) {
|
|
29
|
+
return { ok: false, reason: `report not found at ${reportPath}: ${err.message}` };
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
return { ok: true, report: JSON.parse(raw) };
|
|
33
|
+
} catch (err) {
|
|
34
|
+
return { ok: false, reason: `report at ${reportPath} is not valid JSON: ${err.message}` };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Render a compact, human-readable finalise summary of a verify report. Maps
|
|
40
|
+
* each finding to its contract line (acId) - the RCF payoff. Pure so it is
|
|
41
|
+
* directly testable; the caller writes the returned string to its sink.
|
|
42
|
+
*
|
|
43
|
+
* The verification-integrity 0.7.0 additions (spec §5.2 / §4.5 finalise
|
|
44
|
+
* text): if the report carries per-AC verdicts in
|
|
45
|
+
* {MOCK-ONLY-DECLARED, BLOCKED-BY-DECLARATION}, they are surfaced as a
|
|
46
|
+
* dedicated section so the operator sees an honest picture of what
|
|
47
|
+
* shipped mock-only. The section renders even on a passing verdict
|
|
48
|
+
* (it is disclosure, not a refusal).
|
|
49
|
+
*
|
|
50
|
+
* @param {object} report - a parsed verify report (§5.3 schema)
|
|
51
|
+
* @returns {string}
|
|
52
|
+
*/
|
|
53
|
+
export function summariseReport(report) {
|
|
54
|
+
const lines = [];
|
|
55
|
+
const authority = report.verdictAuthority ? ` [${report.verdictAuthority}]` : '';
|
|
56
|
+
lines.push(`verdict: ${report.verdict}${authority}`);
|
|
57
|
+
const run = report.run ?? {};
|
|
58
|
+
if (run.profile || run.url) {
|
|
59
|
+
lines.push(`runtime: profile=${run.profile ?? '?'} url=${run.url ?? '?'}`
|
|
60
|
+
+ (run.parityEnv ? ' parity-env' : ''));
|
|
61
|
+
}
|
|
62
|
+
const findings = Array.isArray(report.findings) ? report.findings : [];
|
|
63
|
+
if (findings.length > 0) {
|
|
64
|
+
lines.push(`findings (${findings.length}):`);
|
|
65
|
+
for (const f of findings) {
|
|
66
|
+
const ac = f.acId ? ` ${f.acId}` : '';
|
|
67
|
+
const journey = f.journey ? ` (${f.journey})` : '';
|
|
68
|
+
lines.push(` - ${f.severity ?? '?'}${ac}${journey}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const blocked = Array.isArray(report.blockedAcs) ? report.blockedAcs : [];
|
|
72
|
+
if (blocked.length > 0) {
|
|
73
|
+
lines.push(`blocked ACs (${blocked.length}):`);
|
|
74
|
+
for (const b of blocked) {
|
|
75
|
+
lines.push(` - ${b.acId ?? '?'}: ${b.reason ?? 'unprovisionable'}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const declared = findMockOnlyDeclaredAcs(report);
|
|
79
|
+
if (declared.length > 0) {
|
|
80
|
+
lines.push(`mock-only declared (${declared.length}):`);
|
|
81
|
+
for (const d of declared) {
|
|
82
|
+
lines.push(` - ${d.acId ?? '?'} (${d.verdict}): ${d.reason ?? 'declaredMockOnly at pre-flight; verify emitted the honest verdict rather than a false PASS.'}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// 0.8.0 slug-train car 4: NV-BL-GATE-01 pulls verify's profile-vs-AC
|
|
86
|
+
// scope-mismatch check into REVIEW. When verify emits SCOPE-MISMATCH
|
|
87
|
+
// on perAcVerdicts[] the summary surfaces it so REVIEW / finalise see
|
|
88
|
+
// it. Zero-mismatch reports render nothing (no false-flag noise).
|
|
89
|
+
const scopeMismatches = findScopeMismatchAcs(report);
|
|
90
|
+
if (scopeMismatches.length > 0) {
|
|
91
|
+
lines.push(`scope mismatches (${scopeMismatches.length}):`);
|
|
92
|
+
for (const s of scopeMismatches) {
|
|
93
|
+
lines.push(` - ${s.acId ?? '?'} (${s.verdict}): ${s.reason ?? 'a bound TC is narrower than the AC scope; NV-BL-ADM-03 / NV-BL-GATE-01.'}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (report.launchFailure?.message) {
|
|
97
|
+
lines.push(`launch failure: ${report.launchFailure.message}`);
|
|
98
|
+
}
|
|
99
|
+
return `${lines.join('\n')}\n`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Extract per-AC verdicts in {MOCK-ONLY-DECLARED, BLOCKED-BY-DECLARATION}
|
|
104
|
+
* from a verify report. Verify authors these under `perAcVerdicts[]` in
|
|
105
|
+
* the 0.7.0 extension; earlier reports carry no field, in which case
|
|
106
|
+
* this returns an empty array (graceful when no verify-side extension
|
|
107
|
+
* has landed — verify's train car is later).
|
|
108
|
+
*
|
|
109
|
+
* @param {object} report
|
|
110
|
+
* @returns {Array<{ acId: string, verdict: string, reason?: string }>}
|
|
111
|
+
*/
|
|
112
|
+
export function findMockOnlyDeclaredAcs(report) {
|
|
113
|
+
const perAc = Array.isArray(report?.perAcVerdicts) ? report.perAcVerdicts : [];
|
|
114
|
+
return perAc
|
|
115
|
+
.filter((e) => e && (e.verdict === 'MOCK-ONLY-DECLARED' || e.verdict === 'BLOCKED-BY-DECLARATION'))
|
|
116
|
+
.map((e) => ({ acId: e.acId, verdict: e.verdict, reason: e.reason }));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* True when a verify report carries at least one MOCK-ONLY-DECLARED or
|
|
121
|
+
* BLOCKED-BY-DECLARATION verdict. Used by the finalise gate to refuse
|
|
122
|
+
* promotion to `verified` on such reports unless the operator has
|
|
123
|
+
* explicitly shipped-without-verified (spec §5.2 finalise gate rule).
|
|
124
|
+
*
|
|
125
|
+
* @param {object} report
|
|
126
|
+
* @returns {boolean}
|
|
127
|
+
*/
|
|
128
|
+
export function reportHasMockOnlyDeclared(report) {
|
|
129
|
+
return findMockOnlyDeclaredAcs(report).length > 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Extract SCOPE-MISMATCH per-AC verdicts from a verify report. Introduced
|
|
134
|
+
* in the 0.8.0 slug-train (car 4) so REVIEW consumes the same shape via
|
|
135
|
+
* NV-BL-GATE-01. Earlier reports carry no such entries; this returns an
|
|
136
|
+
* empty array on those.
|
|
137
|
+
*
|
|
138
|
+
* @param {object} report
|
|
139
|
+
* @returns {Array<{ acId: string, verdict: string, reason?: string }>}
|
|
140
|
+
*/
|
|
141
|
+
export function findScopeMismatchAcs(report) {
|
|
142
|
+
const perAc = Array.isArray(report?.perAcVerdicts) ? report.perAcVerdicts : [];
|
|
143
|
+
return perAc
|
|
144
|
+
.filter((e) => e && e.verdict === 'SCOPE-MISMATCH')
|
|
145
|
+
.map((e) => ({ acId: e.acId, verdict: e.verdict, reason: e.reason }));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* True when a verify report carries at least one SCOPE-MISMATCH per-AC
|
|
150
|
+
* verdict. NV-BL-GATE-01 (0.8.0 slug-train car 4): the REVIEW gate
|
|
151
|
+
* consumes this so a scope mismatch caught at REVIEW-time fails the
|
|
152
|
+
* gate and returns the FBS to BUILD; the finalise gate reads the same
|
|
153
|
+
* shape as a last-mile refusal.
|
|
154
|
+
*
|
|
155
|
+
* @param {object} report
|
|
156
|
+
* @returns {boolean}
|
|
157
|
+
*/
|
|
158
|
+
export function reportHasScopeMismatch(report) {
|
|
159
|
+
return findScopeMismatchAcs(report).length > 0;
|
|
160
|
+
}
|