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,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,129 @@
|
|
|
1
|
+
// rcf-verify install detection (spec §8.3, amendment 5 - install-together
|
|
2
|
+
// posture). build-lite's finalise gate MUST detect whether `rcf-verify` is
|
|
3
|
+
// resolvable and, when it is absent, prompt to install it - NEVER silently
|
|
4
|
+
// skip the ship gate (the one behaviour §8.3 explicitly forbids).
|
|
5
|
+
//
|
|
6
|
+
// Two detection routes, in order, matching how the two packages are actually
|
|
7
|
+
// installed:
|
|
8
|
+
// 1. The `rcf-verify` bin on PATH - the install-together default is two
|
|
9
|
+
// global bins (`npm i -g @stravica-ai/rcf-build-lite @stravica-ai/rcf-verify-lite`).
|
|
10
|
+
// 2. Package resolution from the project dir - the local-project install
|
|
11
|
+
// (`npm i @stravica-ai/rcf-verify-lite` in a repo's node_modules).
|
|
12
|
+
// Either hit yields a concrete invocation the finalise spawn (spawn.js) uses
|
|
13
|
+
// verbatim. A miss returns { installed:false } and the caller enters the
|
|
14
|
+
// prompt-or-explicit-flag path.
|
|
15
|
+
|
|
16
|
+
import { access, constants } from 'node:fs/promises';
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
import { delimiter, dirname, join, resolve } from 'node:path';
|
|
19
|
+
|
|
20
|
+
const VERIFY_PACKAGE = '@stravica-ai/rcf-verify-lite';
|
|
21
|
+
const VERIFY_BIN = 'rcf-verify';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The concrete way to launch rcf-verify as a fresh subprocess.
|
|
25
|
+
* - `{ command: '<abs-bin>', prefixArgs: [] }` for a PATH / shim bin
|
|
26
|
+
* (its own shebang runs it).
|
|
27
|
+
* - `{ command: process.execPath, prefixArgs: ['<abs-entry.js>'] }` for a
|
|
28
|
+
* package-resolved entry (run it under the current node).
|
|
29
|
+
* @typedef {{ command: string, prefixArgs: string[], source: 'path'|'package' }} VerifyInvocation
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {{ installed: boolean, invocation: VerifyInvocation | null }} VerifyDetection
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Scan PATH for an executable named `rcf-verify` (plus the Windows
|
|
38
|
+
* `.cmd`/`.exe` shim variants). Returns the first absolute path that exists
|
|
39
|
+
* and is executable, or null.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} name
|
|
42
|
+
* @param {object} [io]
|
|
43
|
+
* @param {NodeJS.ProcessEnv} [io.env]
|
|
44
|
+
* @returns {Promise<string|null>}
|
|
45
|
+
*/
|
|
46
|
+
export async function findOnPath(name, { env = process.env } = {}) {
|
|
47
|
+
const rawPath = env.PATH ?? env.Path ?? '';
|
|
48
|
+
if (!rawPath) return null;
|
|
49
|
+
const dirs = rawPath.split(delimiter).filter(Boolean);
|
|
50
|
+
// On Windows a bare name resolves via PATHEXT-style shims; probe the common
|
|
51
|
+
// npm shim names. On POSIX only the bare name matters.
|
|
52
|
+
const candidates = process.platform === 'win32'
|
|
53
|
+
? [`${name}.cmd`, `${name}.exe`, name]
|
|
54
|
+
: [name];
|
|
55
|
+
for (const dir of dirs) {
|
|
56
|
+
for (const candidate of candidates) {
|
|
57
|
+
const full = resolve(dir, candidate);
|
|
58
|
+
try {
|
|
59
|
+
// X_OK is meaningless for the .cmd/.exe shims on Windows; existence is
|
|
60
|
+
// enough there. On POSIX require the execute bit.
|
|
61
|
+
await access(full, process.platform === 'win32' ? constants.F_OK : constants.X_OK);
|
|
62
|
+
return full;
|
|
63
|
+
} catch {
|
|
64
|
+
// not here; keep scanning
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Resolve the rcf-verify package's bin entry point from a starting directory,
|
|
73
|
+
* following the normal node_modules resolution the caller's project sees.
|
|
74
|
+
* Returns the absolute path to `bin/rcf-verify.js`, or null if the package is
|
|
75
|
+
* not installed / not resolvable from there.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} fromDir - directory to resolve from (the project root / cwd)
|
|
78
|
+
* @returns {Promise<string|null>}
|
|
79
|
+
*/
|
|
80
|
+
export async function resolvePackageBin(fromDir) {
|
|
81
|
+
try {
|
|
82
|
+
// Resolve from a synthetic module living in fromDir so node walks that
|
|
83
|
+
// project's node_modules chain, not build-lite's own.
|
|
84
|
+
const req = createRequire(join(fromDir, 'noop.js'));
|
|
85
|
+
const pkgJsonPath = req.resolve(`${VERIFY_PACKAGE}/package.json`);
|
|
86
|
+
const req2 = createRequire(pkgJsonPath);
|
|
87
|
+
const pkg = req2(`${VERIFY_PACKAGE}/package.json`);
|
|
88
|
+
const binField = pkg.bin;
|
|
89
|
+
const rel = typeof binField === 'string' ? binField : binField?.[VERIFY_BIN];
|
|
90
|
+
if (!rel) return null;
|
|
91
|
+
const abs = resolve(dirname(pkgJsonPath), rel);
|
|
92
|
+
await access(abs, constants.F_OK);
|
|
93
|
+
return abs;
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Detect whether rcf-verify is resolvable and, if so, how to launch it.
|
|
101
|
+
* Deps are injectable so the finalise gate can be exercised without a real
|
|
102
|
+
* rcf-verify on the test machine.
|
|
103
|
+
*
|
|
104
|
+
* @param {object} [deps]
|
|
105
|
+
* @param {string} [deps.cwd] - project dir to resolve a local install from
|
|
106
|
+
* @param {typeof findOnPath} [deps.findOnPath]
|
|
107
|
+
* @param {typeof resolvePackageBin} [deps.resolvePackageBin]
|
|
108
|
+
* @param {NodeJS.ProcessEnv} [deps.env]
|
|
109
|
+
* @returns {Promise<VerifyDetection>}
|
|
110
|
+
*/
|
|
111
|
+
export async function detectVerify(deps = {}) {
|
|
112
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
113
|
+
const onPath = deps.findOnPath ? await deps.findOnPath(VERIFY_BIN, { env: deps.env })
|
|
114
|
+
: await findOnPath(VERIFY_BIN, { env: deps.env });
|
|
115
|
+
if (onPath) {
|
|
116
|
+
return { installed: true, invocation: { command: onPath, prefixArgs: [], source: 'path' } };
|
|
117
|
+
}
|
|
118
|
+
const resolved = deps.resolvePackageBin ? await deps.resolvePackageBin(cwd)
|
|
119
|
+
: await resolvePackageBin(cwd);
|
|
120
|
+
if (resolved) {
|
|
121
|
+
return {
|
|
122
|
+
installed: true,
|
|
123
|
+
invocation: { command: process.execPath, prefixArgs: [resolved], source: 'package' },
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return { installed: false, invocation: null };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export { VERIFY_PACKAGE, VERIFY_BIN };
|
|
@@ -0,0 +1,17 @@
|
|
|
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 { detectVerify, findOnPath, resolvePackageBin, VERIFY_PACKAGE, VERIFY_BIN } from './detect.js';
|
|
10
|
+
export { buildVerifyArgs, spawnVerify } from './spawn.js';
|
|
11
|
+
export { promptYesNo, installVerify, resolveAbsentVerify } from './install.js';
|
|
12
|
+
export { loadReport, summariseReport, findMockOnlyDeclaredAcs, reportHasMockOnlyDeclared } from './ingest.js';
|
|
13
|
+
export {
|
|
14
|
+
composeShipWithoutVerifiedRecord,
|
|
15
|
+
nextShipWithoutVerifiedId,
|
|
16
|
+
writeShipWithoutVerifiedRecord,
|
|
17
|
+
} from './ship-without-verified.js';
|
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
if (report.launchFailure?.message) {
|
|
86
|
+
lines.push(`launch failure: ${report.launchFailure.message}`);
|
|
87
|
+
}
|
|
88
|
+
return `${lines.join('\n')}\n`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Extract per-AC verdicts in {MOCK-ONLY-DECLARED, BLOCKED-BY-DECLARATION}
|
|
93
|
+
* from a verify report. Verify authors these under `perAcVerdicts[]` in
|
|
94
|
+
* the 0.7.0 extension; earlier reports carry no field, in which case
|
|
95
|
+
* this returns an empty array (graceful when no verify-side extension
|
|
96
|
+
* has landed — verify's train car is later).
|
|
97
|
+
*
|
|
98
|
+
* @param {object} report
|
|
99
|
+
* @returns {Array<{ acId: string, verdict: string, reason?: string }>}
|
|
100
|
+
*/
|
|
101
|
+
export function findMockOnlyDeclaredAcs(report) {
|
|
102
|
+
const perAc = Array.isArray(report?.perAcVerdicts) ? report.perAcVerdicts : [];
|
|
103
|
+
return perAc
|
|
104
|
+
.filter((e) => e && (e.verdict === 'MOCK-ONLY-DECLARED' || e.verdict === 'BLOCKED-BY-DECLARATION'))
|
|
105
|
+
.map((e) => ({ acId: e.acId, verdict: e.verdict, reason: e.reason }));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* True when a verify report carries at least one MOCK-ONLY-DECLARED or
|
|
110
|
+
* BLOCKED-BY-DECLARATION verdict. Used by the finalise gate to refuse
|
|
111
|
+
* promotion to `verified` on such reports unless the operator has
|
|
112
|
+
* explicitly shipped-without-verified (spec §5.2 finalise gate rule).
|
|
113
|
+
*
|
|
114
|
+
* @param {object} report
|
|
115
|
+
* @returns {boolean}
|
|
116
|
+
*/
|
|
117
|
+
export function reportHasMockOnlyDeclared(report) {
|
|
118
|
+
return findMockOnlyDeclaredAcs(report).length > 0;
|
|
119
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Absent-verify handling (spec §8.3, amendment 5). When `rcf-verify` is not
|
|
2
|
+
// resolvable, the finalise gate MUST NOT silently skip - that recreates the
|
|
3
|
+
// false-confidence failure the whole programme exists to prevent. The
|
|
4
|
+
// sanctioned paths are exactly two, and nothing else:
|
|
5
|
+
// - interactive TTY -> prompt the operator to install now (y/N);
|
|
6
|
+
// - non-interactive -> require an explicit --install-verify flag.
|
|
7
|
+
// A silent skip and a silent auto-install are BOTH violations. Declining the
|
|
8
|
+
// prompt (or omitting the flag off a TTY) aborts finalise - it does not pass
|
|
9
|
+
// the gate.
|
|
10
|
+
|
|
11
|
+
import { spawn } from 'node:child_process';
|
|
12
|
+
import { createInterface } from 'node:readline';
|
|
13
|
+
|
|
14
|
+
import { VERIFY_PACKAGE } from './detect.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Interactive yes/no prompt over stdin/stdout. Resolves true only on an
|
|
18
|
+
* explicit affirmative; EOF / anything else is a decline (safe default: do
|
|
19
|
+
* not install, do not skip the gate).
|
|
20
|
+
*
|
|
21
|
+
* @param {string} question
|
|
22
|
+
* @param {object} [io]
|
|
23
|
+
* @param {NodeJS.ReadableStream} [io.input]
|
|
24
|
+
* @param {NodeJS.WritableStream} [io.output]
|
|
25
|
+
* @returns {Promise<boolean>}
|
|
26
|
+
*/
|
|
27
|
+
export function promptYesNo(question, { input = process.stdin, output = process.stdout } = {}) {
|
|
28
|
+
return new Promise((resolvePromise) => {
|
|
29
|
+
const rl = createInterface({ input, output });
|
|
30
|
+
rl.question(`${question} `, (answer) => {
|
|
31
|
+
rl.close();
|
|
32
|
+
resolvePromise(/^y(es)?$/i.test((answer ?? '').trim()));
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Install rcf-verify globally (matches the install-together default of two
|
|
39
|
+
* global bins). Streams npm's output to the operator. Returns the install
|
|
40
|
+
* process exit code.
|
|
41
|
+
*
|
|
42
|
+
* @param {object} [deps]
|
|
43
|
+
* @param {typeof spawn} [deps.spawn]
|
|
44
|
+
* @param {NodeJS.WritableStream} [deps.stdout]
|
|
45
|
+
* @param {NodeJS.WritableStream} [deps.stderr]
|
|
46
|
+
* @param {string} [deps.packageSpec]
|
|
47
|
+
* @returns {Promise<number>}
|
|
48
|
+
*/
|
|
49
|
+
export function installVerify(deps = {}) {
|
|
50
|
+
const spawnFn = deps.spawn ?? spawn;
|
|
51
|
+
const spec = deps.packageSpec ?? VERIFY_PACKAGE;
|
|
52
|
+
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
53
|
+
return new Promise((resolvePromise, reject) => {
|
|
54
|
+
const child = spawnFn(npm, ['install', '-g', spec], { stdio: 'inherit' });
|
|
55
|
+
child.on('error', reject);
|
|
56
|
+
child.on('close', (code, signal) => resolvePromise(code ?? (signal ? 1 : 0)));
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The absent-verify decision (§8.3). Given the runtime shape (TTY? explicit
|
|
62
|
+
* flag?) decides among: install-then-proceed, or abort. NEVER skip.
|
|
63
|
+
*
|
|
64
|
+
* @param {object} args
|
|
65
|
+
* @param {boolean} args.installFlag - --install-verify was passed
|
|
66
|
+
* @param {boolean} args.isTty - stdin+stdout are a TTY
|
|
67
|
+
* @param {object} io - { stdout, stderr, input, output }
|
|
68
|
+
* @param {object} [deps]
|
|
69
|
+
* @param {typeof promptYesNo} [deps.promptYesNo]
|
|
70
|
+
* @param {typeof installVerify} [deps.installVerify]
|
|
71
|
+
* @returns {Promise<{ action: 'installed' } | { action: 'abort', reason: string, code: number }>}
|
|
72
|
+
*/
|
|
73
|
+
export async function resolveAbsentVerify({ installFlag, isTty }, io, deps = {}) {
|
|
74
|
+
const prompt = deps.promptYesNo ?? promptYesNo;
|
|
75
|
+
const install = deps.installVerify ?? installVerify;
|
|
76
|
+
|
|
77
|
+
let wantsInstall = false;
|
|
78
|
+
if (installFlag) {
|
|
79
|
+
// Explicit flag: the sanctioned non-interactive install path.
|
|
80
|
+
io.stdout.write(`[finalise] rcf-verify not found; installing ${VERIFY_PACKAGE} (--install-verify)...\n`);
|
|
81
|
+
wantsInstall = true;
|
|
82
|
+
} else if (isTty) {
|
|
83
|
+
// Interactive: prompt. Declining aborts (never a silent skip).
|
|
84
|
+
io.stderr.write(`[finalise] rcf-verify is not installed. The ship gate cannot run without it.\n`);
|
|
85
|
+
wantsInstall = await prompt(`Install ${VERIFY_PACKAGE} now? [y/N]`, {
|
|
86
|
+
input: io.input, output: io.output,
|
|
87
|
+
});
|
|
88
|
+
if (!wantsInstall) {
|
|
89
|
+
return {
|
|
90
|
+
action: 'abort',
|
|
91
|
+
code: 4,
|
|
92
|
+
reason: `finalise refused: rcf-verify is required and install was declined. `
|
|
93
|
+
+ `Install it (npm i -g ${VERIFY_PACKAGE}) or re-run with --install-verify. `
|
|
94
|
+
+ `The ship gate is never skipped.`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
// Non-interactive without the flag: cannot prompt, must not auto-install
|
|
99
|
+
// silently, must not skip. Abort with the exact remedy.
|
|
100
|
+
return {
|
|
101
|
+
action: 'abort',
|
|
102
|
+
code: 4,
|
|
103
|
+
reason: `finalise refused: rcf-verify is not installed and no TTY is available to prompt. `
|
|
104
|
+
+ `Install it (npm i -g ${VERIFY_PACKAGE}) or re-run with --install-verify. `
|
|
105
|
+
+ `The ship gate is never silently skipped.`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const installCode = await install({ stdout: io.stdout, stderr: io.stderr });
|
|
110
|
+
if (installCode !== 0) {
|
|
111
|
+
return {
|
|
112
|
+
action: 'abort',
|
|
113
|
+
code: 1,
|
|
114
|
+
reason: `finalise aborted: installing ${VERIFY_PACKAGE} failed (npm exit ${installCode}). `
|
|
115
|
+
+ `Install it manually and re-run finalise.`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return { action: 'installed' };
|
|
119
|
+
}
|