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
package/src/cli/fbs.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// `rcf fbs <fbs-id> <verb> [options]` subcommand handler
|
|
2
|
+
// (verification-integrity-cluster-spec §3.1, §5.2).
|
|
3
|
+
//
|
|
4
|
+
// One verb in Track A:
|
|
5
|
+
// depends-on append or replace a dependsOnServices[] entry
|
|
6
|
+
//
|
|
7
|
+
// Written at Define stage (build-cycle §3, spec §5.1) with the
|
|
8
|
+
// attestation mode drawn from the pre-flight config record. Never
|
|
9
|
+
// invented mid-Test to justify a green suite. The verb refuses to
|
|
10
|
+
// write an entry whose acIds are not on the FBS.
|
|
11
|
+
|
|
12
|
+
import { parseArgs } from 'node:util';
|
|
13
|
+
|
|
14
|
+
import { isRcfError, writeUnexpectedFailure } from '#core/errors';
|
|
15
|
+
import { updateDocument, walkTree } from '#core/store';
|
|
16
|
+
|
|
17
|
+
import { findProjectRoot } from '../view/index.js';
|
|
18
|
+
|
|
19
|
+
const VALID_MODES = new Set(['live', 'sandboxed', 'mocked', 'declaredMockOnly', 'notShipped']);
|
|
20
|
+
|
|
21
|
+
const DEPENDS_ON_OPTION_SPEC = {
|
|
22
|
+
service: { type: 'string' },
|
|
23
|
+
mode: { type: 'string' },
|
|
24
|
+
acs: { type: 'string' },
|
|
25
|
+
display: { type: 'string' },
|
|
26
|
+
purpose: { type: 'string' },
|
|
27
|
+
preflight: { type: 'string' },
|
|
28
|
+
force: { type: 'boolean' },
|
|
29
|
+
quiet: { type: 'boolean' },
|
|
30
|
+
help: { type: 'boolean' },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const HELP = `Usage: rcf fbs <fbs-id> <verb> [options]
|
|
34
|
+
|
|
35
|
+
Verbs:
|
|
36
|
+
depends-on Record a third-party service dependency on this FBS
|
|
37
|
+
|
|
38
|
+
rcf fbs <fbs-id> depends-on --service <id> --mode <live|sandboxed|mocked|declaredMockOnly|notShipped>
|
|
39
|
+
--acs <acId>[,<acId>...] [--display "..."] [--purpose "..."] [--preflight <pfc-id>]
|
|
40
|
+
|
|
41
|
+
Writes a dependsOnServices[] entry keyed by --service. Idempotent by
|
|
42
|
+
service id: an existing entry with the same id is replaced (silent
|
|
43
|
+
overwrite is intentional here — the entry is derived from the same
|
|
44
|
+
pre-flight ruling every time). --acs must reference ACs the FBS
|
|
45
|
+
already binds via its acIds.
|
|
46
|
+
|
|
47
|
+
Options common:
|
|
48
|
+
--force Bypass the AC-in-FBS check (for repair only)
|
|
49
|
+
--quiet Suppress non-error stdout
|
|
50
|
+
--help Print this help
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {string[]} argv - argv slice after `fbs`
|
|
55
|
+
* @param {object} [deps]
|
|
56
|
+
* @returns {Promise<number>}
|
|
57
|
+
*/
|
|
58
|
+
export async function main(argv, deps = {}) {
|
|
59
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
60
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
61
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
62
|
+
|
|
63
|
+
if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
|
|
64
|
+
stdout.write(HELP);
|
|
65
|
+
return argv[0] ? 0 : 2;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const fbsId = argv[0];
|
|
69
|
+
const verb = argv[1];
|
|
70
|
+
const rest = argv.slice(2);
|
|
71
|
+
if (!verb) {
|
|
72
|
+
stderr.write('[error] usage fbs: verb required (depends-on)\n');
|
|
73
|
+
return 2;
|
|
74
|
+
}
|
|
75
|
+
if (!/^FBS-\d{3,}$/.test(fbsId)) {
|
|
76
|
+
stderr.write(`[error] usage fbs: expected an FBS id like FBS-011, got '${fbsId}'\n`);
|
|
77
|
+
return 2;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const projectRoot = await findProjectRoot(cwd);
|
|
81
|
+
if (!projectRoot) {
|
|
82
|
+
stderr.write('[error] usage no project root found (no rcf/manifest.json in this directory or any ancestor).\n');
|
|
83
|
+
return 2;
|
|
84
|
+
}
|
|
85
|
+
const walkResult = await walkTree({ projectRoot });
|
|
86
|
+
const tree = walkResult.tree;
|
|
87
|
+
const fbs = tree.byId.get(fbsId);
|
|
88
|
+
if (!fbs || tree.kindById.get(fbsId) !== 'fbs') {
|
|
89
|
+
stderr.write(`[error] usage fbs: ${fbsId} not found or not an FBS\n`);
|
|
90
|
+
return 2;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (verb === 'depends-on') return runDependsOn({ fbsId, fbs, rest, projectRoot, tree, walkResult, stdout, stderr });
|
|
94
|
+
stderr.write(`[error] usage fbs: unknown verb '${verb}' (expected depends-on)\n`);
|
|
95
|
+
return 2;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function runDependsOn({ fbsId, fbs, rest, projectRoot, tree, walkResult, stdout, stderr }) {
|
|
99
|
+
let parsed;
|
|
100
|
+
try {
|
|
101
|
+
parsed = parseArgs({ args: rest, options: DEPENDS_ON_OPTION_SPEC, allowPositionals: false, strict: true });
|
|
102
|
+
} catch (err) {
|
|
103
|
+
stderr.write(`[error] usage ${err.message}\n`);
|
|
104
|
+
return 2;
|
|
105
|
+
}
|
|
106
|
+
const flags = parsed.values;
|
|
107
|
+
if (flags.help) { stdout.write(HELP); return 0; }
|
|
108
|
+
|
|
109
|
+
if (!flags.service) return usageErr(stderr, 'fbs depends-on: --service required');
|
|
110
|
+
if (!/^[a-z][a-zA-Z0-9]*$/.test(flags.service)) {
|
|
111
|
+
return usageErr(stderr, `fbs depends-on: --service must be camelCase (got '${flags.service}')`);
|
|
112
|
+
}
|
|
113
|
+
if (!flags.mode) return usageErr(stderr, 'fbs depends-on: --mode required');
|
|
114
|
+
if (!VALID_MODES.has(flags.mode)) {
|
|
115
|
+
return usageErr(stderr, `fbs depends-on: unknown --mode '${flags.mode}' (expected ${[...VALID_MODES].join(' | ')})`);
|
|
116
|
+
}
|
|
117
|
+
if (!flags.acs) return usageErr(stderr, 'fbs depends-on: --acs required (comma-separated AC ids)');
|
|
118
|
+
const acIds = flags.acs.split(',').map((s) => s.trim()).filter(Boolean);
|
|
119
|
+
if (acIds.length === 0) return usageErr(stderr, 'fbs depends-on: --acs is empty');
|
|
120
|
+
|
|
121
|
+
if (!flags.force) {
|
|
122
|
+
const fbsAcIds = new Set(fbs.acIds ?? []);
|
|
123
|
+
const outside = acIds.filter((ac) => !fbsAcIds.has(ac));
|
|
124
|
+
if (outside.length > 0) {
|
|
125
|
+
stderr.write(`[error] refused fbs depends-on: ${fbsId} does not bind AC(s): ${outside.join(', ')}; add them to the FBS first (or pass --force for a repair path).\n`);
|
|
126
|
+
return 4;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const entry = {
|
|
131
|
+
id: flags.service,
|
|
132
|
+
displayName: flags.display ?? flags.service,
|
|
133
|
+
purpose: flags.purpose ?? `${flags.service} dependency`,
|
|
134
|
+
attestationMode: flags.mode,
|
|
135
|
+
acIds,
|
|
136
|
+
};
|
|
137
|
+
if (typeof flags.preflight === 'string' && flags.preflight.length > 0) {
|
|
138
|
+
// If the operator passed a bare pfc id, expand to the full ref; if
|
|
139
|
+
// they passed the composite (`pfc-...#services.<id>`) keep it.
|
|
140
|
+
entry.preFlightRef = flags.preflight.includes('#')
|
|
141
|
+
? flags.preflight
|
|
142
|
+
: `${flags.preflight}#services.${flags.service}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const existing = Array.isArray(fbs.dependsOnServices) ? fbs.dependsOnServices : [];
|
|
146
|
+
const nextEntries = existing.filter((e) => e.id !== flags.service);
|
|
147
|
+
nextEntries.push(entry);
|
|
148
|
+
|
|
149
|
+
const sets = [{ path: 'dependsOnServices', value: nextEntries }];
|
|
150
|
+
const result = await updateDocument({
|
|
151
|
+
projectRoot, tree, id: fbsId, patch: null, sets, options: {}, walkErrors: walkResult.errors,
|
|
152
|
+
});
|
|
153
|
+
if (isRcfError(result)) return handleWriterError(result, stderr);
|
|
154
|
+
if (!flags.quiet) {
|
|
155
|
+
stdout.write(`fbs ${fbsId} depends-on: ${flags.service} (${flags.mode}) covers ${acIds.length} AC(s).\n`);
|
|
156
|
+
}
|
|
157
|
+
return 0;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function usageErr(stderr, message) {
|
|
161
|
+
stderr.write(`[error] usage ${message}\n`);
|
|
162
|
+
return 2;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function handleWriterError(err, stderr) {
|
|
166
|
+
if (err.kind === 'ioFailure') { writeUnexpectedFailure(err, stderr); return 1; }
|
|
167
|
+
stderr.write(`[error] ${err.kind} ${err.message}\n`);
|
|
168
|
+
if (err.kind === 'usage') return 2;
|
|
169
|
+
if (err.kind === 'validation' || err.kind === 'brokenReference') return 3;
|
|
170
|
+
return 1;
|
|
171
|
+
}
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
// `rcf finalise` subcommand handler (spec §8, amendments 4 + 5). The build-side
|
|
2
|
+
// home of the finalise-gate integration contract: the ship gate that promotes
|
|
3
|
+
// an FBS from `complete` to `verified` ONLY when an independent rcf-verify run
|
|
4
|
+
// against the deployed app passes.
|
|
5
|
+
//
|
|
6
|
+
// rcf finalise <fbs-id> --url <deploy-url> [options]
|
|
7
|
+
//
|
|
8
|
+
// The spec (§8.2) offers two homes for this: overloading `rcf build --mark
|
|
9
|
+
// complete`, or "a dedicated finalise verb". This is the dedicated verb.
|
|
10
|
+
// `rcf build --mark` is a deterministic-only, pure transition path that runs
|
|
11
|
+
// PRE-deploy; the finalise gate spawns a subprocess, hits the network, and may
|
|
12
|
+
// prompt - it does not belong on the mark path. The `complete -> verified`
|
|
13
|
+
// transition is the exact semantic home: `verified` means "independently
|
|
14
|
+
// verified against the running deploy", which is precisely what rcf-verify
|
|
15
|
+
// stamps.
|
|
16
|
+
//
|
|
17
|
+
// Hard contract points (spec §8.2 / §9):
|
|
18
|
+
// - verify is a FRESH SUBPROCESS with the isolation env, never an in-process
|
|
19
|
+
// import (§8.2 / §9: the verifier agent must start cold, zero build context);
|
|
20
|
+
// - the gate is the subprocess exit code AND the report's ship authority
|
|
21
|
+
// (spec §4): exit 0 with verdictAuthority 'ship' -> promote to verified;
|
|
22
|
+
// exit 0 with correctness-only authority -> HOLD (stay put, no promotion);
|
|
23
|
+
// non-zero -> stay put, surface findings;
|
|
24
|
+
// - findings flow via the --out report file, not stdout scraping;
|
|
25
|
+
// - if rcf-verify is absent, PROMPT to install (or accept an explicit
|
|
26
|
+
// --install-verify flag) - NEVER silently skip the gate (§8.3).
|
|
27
|
+
|
|
28
|
+
import { resolve as resolvePath } from 'node:path';
|
|
29
|
+
import process from 'node:process';
|
|
30
|
+
import { parseArgs } from 'node:util';
|
|
31
|
+
|
|
32
|
+
import { isRcfError, rcfError, writeUnexpectedFailure } from '#core/errors';
|
|
33
|
+
import { updateDocument, walkTree } from '#core/store';
|
|
34
|
+
|
|
35
|
+
import { findProjectRoot } from '../view/index.js';
|
|
36
|
+
import { kindOf } from '../query/index.js';
|
|
37
|
+
import {
|
|
38
|
+
buildVerifyArgs,
|
|
39
|
+
composeShipWithoutVerifiedRecord,
|
|
40
|
+
detectVerify,
|
|
41
|
+
findMockOnlyDeclaredAcs,
|
|
42
|
+
loadReport,
|
|
43
|
+
reportHasMockOnlyDeclared,
|
|
44
|
+
resolveAbsentVerify,
|
|
45
|
+
spawnVerify,
|
|
46
|
+
summariseReport,
|
|
47
|
+
writeShipWithoutVerifiedRecord,
|
|
48
|
+
} from '../finalise/index.js';
|
|
49
|
+
|
|
50
|
+
// Kept in sync with verify's own sets by contract (build never imports verify -
|
|
51
|
+
// that would break the §9 independence guarantee). Profiles: spec §4.
|
|
52
|
+
// Severities: spec §5.1 finding taxonomy.
|
|
53
|
+
const PROFILES = new Set(['deployed', 'ci', 'local-dev']);
|
|
54
|
+
const SEVERITIES = new Set(['PASS', 'COSMETIC', 'DEGRADED', 'BROKEN']);
|
|
55
|
+
const DEFAULT_PROFILE = 'deployed';
|
|
56
|
+
const DEFAULT_GATE = 'BROKEN';
|
|
57
|
+
const DEFAULT_REPORT = '.rcf-verify-report.json';
|
|
58
|
+
// The finalise gate only makes sense on a built item; verified re-verify is
|
|
59
|
+
// allowed (the §5.4 loop re-runs the gate on a previously-verified item).
|
|
60
|
+
const FINALISABLE_STATUSES = new Set(['complete', 'verified']);
|
|
61
|
+
|
|
62
|
+
const OPTION_SPEC = {
|
|
63
|
+
url: { type: 'string' },
|
|
64
|
+
profile: { type: 'string' },
|
|
65
|
+
'parity-env': { type: 'boolean' },
|
|
66
|
+
provision: { type: 'string' },
|
|
67
|
+
'severity-gate': { type: 'string' },
|
|
68
|
+
out: { type: 'string' },
|
|
69
|
+
chain: { type: 'string' },
|
|
70
|
+
persona: { type: 'string' },
|
|
71
|
+
'install-verify': { type: 'boolean' },
|
|
72
|
+
// 0.7.0 verification-integrity: acknowledge MOCK-ONLY-DECLARED /
|
|
73
|
+
// BLOCKED-BY-DECLARATION per-AC verdicts on the verify report and
|
|
74
|
+
// ship the FBS 'complete' without promoting to 'verified'. Records
|
|
75
|
+
// the operator's ack on the manifest.
|
|
76
|
+
'ship-without-verified': { type: 'boolean' },
|
|
77
|
+
quiet: { type: 'boolean' },
|
|
78
|
+
help: { type: 'boolean' },
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const HELP = `Usage: rcf finalise <fbs-id> --url <deploy-url> [options]
|
|
82
|
+
|
|
83
|
+
The ship gate. Promotes an FBS at status complete to verified ONLY when an
|
|
84
|
+
independent rcf-verify run passes AND carries ship authority.
|
|
85
|
+
|
|
86
|
+
rcf-verify is launched as a FRESH SUBPROCESS (never in-process) so the
|
|
87
|
+
verifier agent starts cold, with zero build context - its only inputs are the
|
|
88
|
+
RCF chain (the acceptance contract) and the live URL. The gate is the
|
|
89
|
+
subprocess exit code AND the report's ship authority: a run must both pass and
|
|
90
|
+
carry SHIP authority (a 'deployed'-profile run, or a 'ci'/'local-dev' run with
|
|
91
|
+
--parity-env) to promote. A correctness-only PASS holds without promoting.
|
|
92
|
+
Findings flow back via the --out report file.
|
|
93
|
+
|
|
94
|
+
If rcf-verify is not installed, finalise PROMPTS to install it (install-together
|
|
95
|
+
posture) - it never silently skips the gate. Off a TTY, pass --install-verify.
|
|
96
|
+
|
|
97
|
+
Required:
|
|
98
|
+
<fbs-id> The FBS item to finalise (must be 'complete')
|
|
99
|
+
--url <deploy-url> The running deployed app to verify against
|
|
100
|
+
|
|
101
|
+
Options:
|
|
102
|
+
--profile <p> deployed | ci | local-dev (default: deployed)
|
|
103
|
+
--parity-env Assert a ci/local-dev runtime is edge-identical to
|
|
104
|
+
prod - the only path to a SHIP verdict off 'deployed'
|
|
105
|
+
--provision <file> Provisioning / prerequisite-credentials FILE
|
|
106
|
+
(never inline; secrets never echoed)
|
|
107
|
+
--severity-gate <sev> Block at/above this severity:
|
|
108
|
+
PASS | COSMETIC | DEGRADED | BROKEN (default: BROKEN)
|
|
109
|
+
--out <path> Where verify writes its report artifact
|
|
110
|
+
(default: ${DEFAULT_REPORT})
|
|
111
|
+
--chain <PRD/ref> Which PRD/chain to verify against (default: repo's)
|
|
112
|
+
--persona <name> Adversarial persona flavour
|
|
113
|
+
--install-verify If rcf-verify is absent, install it without
|
|
114
|
+
prompting (the sanctioned non-interactive path)
|
|
115
|
+
--ship-without-verified Acknowledge MOCK-ONLY-DECLARED /
|
|
116
|
+
BLOCKED-BY-DECLARATION per-AC verdicts on
|
|
117
|
+
the verify report and ship the FBS
|
|
118
|
+
'complete' without promoting to 'verified'
|
|
119
|
+
(verification-integrity 0.7.0 §5.2). Reads
|
|
120
|
+
the disclosed verdicts and leaves the FBS at
|
|
121
|
+
'complete'. Without this flag, the gate
|
|
122
|
+
refuses to promote when any AC lands in
|
|
123
|
+
those verdicts.
|
|
124
|
+
--quiet Suppress non-error confirmations
|
|
125
|
+
--help Print this help
|
|
126
|
+
|
|
127
|
+
Exit codes:
|
|
128
|
+
0 gate passed - FBS marked verified
|
|
129
|
+
1 IO / unexpected runtime failure
|
|
130
|
+
2 usage error (bad flags, unknown / non-FBS id)
|
|
131
|
+
3 schema validation or broken references (tree unreadable, or write refused)
|
|
132
|
+
4 gate NOT passed - verify blocked ship, OR verify passed but the run lacks
|
|
133
|
+
ship authority (a correctness-only HOLD), OR rcf-verify absent and install
|
|
134
|
+
declined/unavailable - the FBS is left unchanged, findings surfaced
|
|
135
|
+
`;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* @param {string[]} argv - argv slice after `finalise`
|
|
139
|
+
* @param {object} [deps] - injectable seams (detect / spawn / prompt / install /
|
|
140
|
+
* loadReport) for testing the gate without a real rcf-verify + live app.
|
|
141
|
+
* @returns {Promise<number>}
|
|
142
|
+
*/
|
|
143
|
+
export async function main(argv, deps = {}) {
|
|
144
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
145
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
146
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
147
|
+
|
|
148
|
+
let parsed;
|
|
149
|
+
try {
|
|
150
|
+
parsed = parseArgs({ args: argv, options: OPTION_SPEC, allowPositionals: true, strict: true });
|
|
151
|
+
} catch (err) {
|
|
152
|
+
stderr.write(`[error] usage ${err.message}\n`);
|
|
153
|
+
stderr.write(HELP);
|
|
154
|
+
return 2;
|
|
155
|
+
}
|
|
156
|
+
const flags = parsed.values;
|
|
157
|
+
const positionals = parsed.positionals;
|
|
158
|
+
if (flags.help) { stdout.write(HELP); return 0; }
|
|
159
|
+
|
|
160
|
+
const usage = (message) => {
|
|
161
|
+
stderr.write(`[error] usage finalise: ${message}\n`);
|
|
162
|
+
return 2;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
if (positionals.length === 0) return usage('an <fbs-id> positional is required');
|
|
166
|
+
if (positionals.length > 1) return usage('multiple positional ids are not supported');
|
|
167
|
+
const fbsId = positionals[0];
|
|
168
|
+
if (fbsId.includes('*') || fbsId.includes('?')) {
|
|
169
|
+
return usage('wildcard / glob positional not supported');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!flags.url) return usage('--url <deploy-url> is required');
|
|
173
|
+
|
|
174
|
+
const profile = flags.profile ?? DEFAULT_PROFILE;
|
|
175
|
+
if (!PROFILES.has(profile)) {
|
|
176
|
+
return usage(`unknown --profile ${profile} (expected deployed | ci | local-dev)`);
|
|
177
|
+
}
|
|
178
|
+
const gate = flags['severity-gate'] ?? DEFAULT_GATE;
|
|
179
|
+
if (!SEVERITIES.has(gate)) {
|
|
180
|
+
return usage(`unknown --severity-gate ${gate} (expected PASS | COSMETIC | DEGRADED | BROKEN)`);
|
|
181
|
+
}
|
|
182
|
+
// Dash-footgun (mirrors build's --body-file / verify's --provision discipline):
|
|
183
|
+
// a --provision that looks like a flag is a swallowed next option or an inline
|
|
184
|
+
// credential. Credentials are FILE-only, never inline, never echoed.
|
|
185
|
+
if (typeof flags.provision === 'string' && flags.provision.startsWith('-')) {
|
|
186
|
+
return usage('--provision takes a file path, not a flag or inline value (credentials are never accepted inline)');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const projectRoot = await findProjectRoot(cwd);
|
|
190
|
+
if (!projectRoot) {
|
|
191
|
+
stderr.write('[error] usage no project root found (no rcf/manifest.json in this directory or any ancestor). Run `npx rcf init` to create and wire a project.\n');
|
|
192
|
+
return 2;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Walker errors block finalise entirely - no gate runs against a tree that
|
|
196
|
+
// fails validation, and no status write lands (mirrors build --mark, §D6).
|
|
197
|
+
const { tree, errors } = await walkTree({ projectRoot });
|
|
198
|
+
if (errors.length > 0) {
|
|
199
|
+
stderr.write('[error] validation the RCF tree does not validate; fix it before finalising.\n');
|
|
200
|
+
return 3;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// The positional must be an FBS at a finalisable status.
|
|
204
|
+
const kind = kindOf(tree, fbsId);
|
|
205
|
+
if (kind !== 'fbs') {
|
|
206
|
+
if (!kind) return usage(`id ${fbsId} not found`);
|
|
207
|
+
return usage(`${fbsId} is a ${kind} id; finalise addresses FBS items only`);
|
|
208
|
+
}
|
|
209
|
+
const fbs = tree.byId.get(fbsId);
|
|
210
|
+
const currentStatus = fbs.executionStatus;
|
|
211
|
+
if (!FINALISABLE_STATUSES.has(currentStatus)) {
|
|
212
|
+
stderr.write(`[error] refused finalise: ${fbsId} is '${currentStatus}'; the finalise gate promotes 'complete' -> 'verified'. `
|
|
213
|
+
+ `Mark it complete first: rcf build ${fbsId} --mark complete\n`);
|
|
214
|
+
return 4;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const quiet = Boolean(flags.quiet);
|
|
218
|
+
const io = { stdout, stderr, input: deps.input ?? process.stdin, output: deps.output ?? process.stdout };
|
|
219
|
+
|
|
220
|
+
// --- rcf-verify install detection (§8.3) --------------------------------
|
|
221
|
+
let detection = deps.detectVerify
|
|
222
|
+
? await deps.detectVerify({ cwd: projectRoot })
|
|
223
|
+
: await detectVerify({ cwd: projectRoot });
|
|
224
|
+
if (!detection.installed) {
|
|
225
|
+
const isTty = deps.isTty ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
226
|
+
const outcome = await resolveAbsentVerify(
|
|
227
|
+
{ installFlag: Boolean(flags['install-verify']), isTty },
|
|
228
|
+
io,
|
|
229
|
+
deps,
|
|
230
|
+
);
|
|
231
|
+
if (outcome.action === 'abort') {
|
|
232
|
+
stderr.write(`[error] refused ${outcome.reason}\n`);
|
|
233
|
+
return outcome.code;
|
|
234
|
+
}
|
|
235
|
+
// Installed - re-detect to get a concrete invocation.
|
|
236
|
+
detection = deps.detectVerify
|
|
237
|
+
? await deps.detectVerify({ cwd: projectRoot })
|
|
238
|
+
: await detectVerify({ cwd: projectRoot });
|
|
239
|
+
if (!detection.installed) {
|
|
240
|
+
stderr.write('[error] unexpected rcf-verify still not resolvable after install; install it manually and re-run.\n');
|
|
241
|
+
return 1;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// --- spawn the fresh verify subprocess (§8.2) ---------------------------
|
|
246
|
+
const outPath = resolvePath(projectRoot, flags.out ?? DEFAULT_REPORT);
|
|
247
|
+
const verifyArgs = buildVerifyArgs({
|
|
248
|
+
repo: projectRoot,
|
|
249
|
+
url: flags.url,
|
|
250
|
+
profile,
|
|
251
|
+
out: outPath,
|
|
252
|
+
severityGate: gate,
|
|
253
|
+
parityEnv: Boolean(flags['parity-env']),
|
|
254
|
+
provision: flags.provision,
|
|
255
|
+
chain: flags.chain,
|
|
256
|
+
persona: flags.persona,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
if (!quiet) {
|
|
260
|
+
stdout.write(`[finalise] launching rcf-verify (fresh subprocess) against ${flags.url} [profile=${profile}]...\n`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let spawnResult;
|
|
264
|
+
try {
|
|
265
|
+
spawnResult = deps.spawnVerify
|
|
266
|
+
? await deps.spawnVerify({ invocation: detection.invocation, verifyArgs, cwd: projectRoot }, deps)
|
|
267
|
+
: await spawnVerify({ invocation: detection.invocation, verifyArgs, cwd: projectRoot }, deps);
|
|
268
|
+
} catch (err) {
|
|
269
|
+
writeUnexpectedFailure(
|
|
270
|
+
rcfError({ kind: 'ioFailure', message: `finalise: could not launch rcf-verify: ${err.message}`, stack: err.stack }),
|
|
271
|
+
stderr,
|
|
272
|
+
);
|
|
273
|
+
return 1;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// --- gate on the subprocess exit code (§8.2) + ship authority (§4) ------
|
|
277
|
+
if (spawnResult.code === 0) {
|
|
278
|
+
// Gate passed on exit code. Promote to verified (idempotent if a re-verify
|
|
279
|
+
// of an already-verified item).
|
|
280
|
+
if (currentStatus === 'verified') {
|
|
281
|
+
if (!quiet) stdout.write(`[finalise] gate passed; ${fbsId} already verified (re-verify) -> ${outPath}\n`);
|
|
282
|
+
return 0;
|
|
283
|
+
}
|
|
284
|
+
// Ship-authority gate (spec §4, w-2026-07-22-004): exit 0 is necessary but
|
|
285
|
+
// not sufficient to write `verified`. Only a run that carries SHIP authority
|
|
286
|
+
// may promote - a `deployed`-profile run, or a `ci`/`local-dev` run with an
|
|
287
|
+
// explicit --parity-env parity assertion. A correctness-only PASS (e.g. a
|
|
288
|
+
// bare `--profile ci` run) is a real regression pass but carries no ship
|
|
289
|
+
// authority, so it must HOLD rather than promote. We read the authority from
|
|
290
|
+
// the report verify stamped (never re-derive it here - that would break the
|
|
291
|
+
// §9 independence guarantee). An unreadable report on a pass is treated as a
|
|
292
|
+
// HOLD, never a silent promotion (mirrors ingest's "unreadable is not a pass").
|
|
293
|
+
const passLoaded = await (deps.loadReport ? deps.loadReport(outPath, deps) : loadReport(outPath, deps));
|
|
294
|
+
const authority = passLoaded.ok ? passLoaded.report.verdictAuthority : undefined;
|
|
295
|
+
if (authority !== 'ship') {
|
|
296
|
+
stderr.write(`[finalise] HOLD: rcf-verify passed but this run carries '${authority ?? 'unknown'}' authority, `
|
|
297
|
+
+ `not 'ship'; ${fbsId} left '${currentStatus}'.\n`);
|
|
298
|
+
stderr.write('Only a \'deployed\'-profile run - or a \'ci\'/\'local-dev\' run with --parity-env asserting the '
|
|
299
|
+
+ 'runtime is edge-identical to prod - carries ship authority and can promote to verified. '
|
|
300
|
+
+ `Report: ${outPath}\n`);
|
|
301
|
+
if (!passLoaded.ok) stderr.write(`[finalise] (the verify report could not be read: ${passLoaded.reason})\n`);
|
|
302
|
+
return 4;
|
|
303
|
+
}
|
|
304
|
+
// 0.7.0 verification-integrity §5.2: the finalise gate reads the
|
|
305
|
+
// verify report's per-AC verdicts and refuses to promote to
|
|
306
|
+
// 'verified' on MOCK-ONLY-DECLARED / BLOCKED-BY-DECLARATION unless
|
|
307
|
+
// the operator has explicitly shipped-without-verified. Graceful
|
|
308
|
+
// when the report carries no perAcVerdicts field (verify's train
|
|
309
|
+
// car is later; older reports are silent on this axis).
|
|
310
|
+
if (passLoaded.ok && reportHasMockOnlyDeclared(passLoaded.report)) {
|
|
311
|
+
const declared = findMockOnlyDeclaredAcs(passLoaded.report);
|
|
312
|
+
if (flags['ship-without-verified']) {
|
|
313
|
+
// B-1 (spec section 5.2, review 2026-07-31): the ack is
|
|
314
|
+
// recorded on the manifest so it is durable and greppable at
|
|
315
|
+
// ship time. Stdout alone is neither. Written BEFORE the
|
|
316
|
+
// success confirmation so a manifest-write failure surfaces
|
|
317
|
+
// to the operator as a hard failure rather than a silent
|
|
318
|
+
// stdout-only ack.
|
|
319
|
+
const ackRecord = composeShipWithoutVerifiedRecord({
|
|
320
|
+
manifest: tree.manifest,
|
|
321
|
+
fbsId,
|
|
322
|
+
declaredAcs: declared,
|
|
323
|
+
reportPath: outPath,
|
|
324
|
+
});
|
|
325
|
+
const ackResult = await writeShipWithoutVerifiedRecord({
|
|
326
|
+
projectRoot, tree, record: ackRecord,
|
|
327
|
+
});
|
|
328
|
+
if (isRcfError(ackResult)) {
|
|
329
|
+
if (ackResult.kind === 'ioFailure') { writeUnexpectedFailure(ackResult, stderr); return 1; }
|
|
330
|
+
stderr.write(`[error] ${ackResult.kind} ${ackResult.message}\n`);
|
|
331
|
+
if (ackResult.kind === 'validation' || ackResult.kind === 'brokenReference') return 3;
|
|
332
|
+
return 1;
|
|
333
|
+
}
|
|
334
|
+
if (!quiet) {
|
|
335
|
+
stdout.write(`[finalise] gate passed and ${declared.length} AC(s) came back with declared mock-only verdicts; --ship-without-verified acknowledged (${ackRecord.id} on rcf/manifest.json); ${fbsId} left '${currentStatus}'. Report: ${outPath}\n`);
|
|
336
|
+
stdout.write(summariseReport(passLoaded.report));
|
|
337
|
+
}
|
|
338
|
+
return 0;
|
|
339
|
+
}
|
|
340
|
+
stderr.write(`[finalise] gate NOT promoted: ${declared.length} AC(s) came back MOCK-ONLY-DECLARED / BLOCKED-BY-DECLARATION on the report; ${fbsId} left '${currentStatus}'.\n`);
|
|
341
|
+
stderr.write('Only a run where every service-bound AC verifies with a live-attested profile promotes to verified. To ship the FBS at complete anyway, re-run with --ship-without-verified (the ack lands on the record).\n');
|
|
342
|
+
stderr.write(summariseReport(passLoaded.report));
|
|
343
|
+
stderr.write(`Report: ${outPath}\n`);
|
|
344
|
+
return 4;
|
|
345
|
+
}
|
|
346
|
+
const result = await updateDocument({
|
|
347
|
+
projectRoot, tree, id: fbsId, sets: [{ path: 'executionStatus', value: 'verified' }], options: {},
|
|
348
|
+
});
|
|
349
|
+
if (isRcfError(result)) {
|
|
350
|
+
if (result.kind === 'ioFailure') { writeUnexpectedFailure(result, stderr); return 1; }
|
|
351
|
+
stderr.write(`[error] ${result.kind} ${result.message}\n`);
|
|
352
|
+
if (result.kind === 'usage') return 2;
|
|
353
|
+
if (result.kind === 'validation' || result.kind === 'brokenReference') return 3;
|
|
354
|
+
return 1;
|
|
355
|
+
}
|
|
356
|
+
if (!quiet) stdout.write(`[finalise] gate passed; marked ${fbsId} complete -> verified. Report: ${outPath}\n`);
|
|
357
|
+
return 0;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Gate NOT passed. The FBS stays put; surface the findings from the report
|
|
361
|
+
// (the §5.4 verify -> fix -> re-verify seam), then fail non-zero.
|
|
362
|
+
stderr.write(`[finalise] gate NOT passed (rcf-verify exit ${spawnResult.code}); ${fbsId} left '${currentStatus}'.\n`);
|
|
363
|
+
const loaded = await (deps.loadReport ? deps.loadReport(outPath, deps) : loadReport(outPath, deps));
|
|
364
|
+
if (loaded.ok) {
|
|
365
|
+
stderr.write(summariseReport(loaded.report));
|
|
366
|
+
stderr.write(`Full report (build-lite ingestible, chain-node-addressed): ${outPath}\n`);
|
|
367
|
+
} else {
|
|
368
|
+
stderr.write(`[finalise] could not read the verify report: ${loaded.reason}\n`);
|
|
369
|
+
}
|
|
370
|
+
// Map verify's exit-code convention to finalise's. Verify: 2 usage, 3 chain
|
|
371
|
+
// load, 5 gate/NOT-DEPLOYED/BLOCKED/LAUNCH-FAILURE, 1 io. A tripped ship gate
|
|
372
|
+
// is a refusal to finalise -> 4; a verify usage/chain error surfaces as-is.
|
|
373
|
+
if (spawnResult.code === 2) return 2;
|
|
374
|
+
if (spawnResult.code === 3) return 3;
|
|
375
|
+
if (spawnResult.code === 5) return 4;
|
|
376
|
+
return 1;
|
|
377
|
+
}
|