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/build.js
ADDED
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
// `rcf build` subcommand handler (Phase 6 §D1). One verb, four modes:
|
|
2
|
+
//
|
|
3
|
+
// rcf build queue overview
|
|
4
|
+
// rcf build <fbs-id> spec bundle for one FBS item
|
|
5
|
+
// rcf build --next bundle for the next actionable item
|
|
6
|
+
// rcf build <fbs-id> --mark <status> record a lifecycle transition
|
|
7
|
+
//
|
|
8
|
+
// Positional is an FBS id ONLY: the FBS is the queue unit (one US's
|
|
9
|
+
// ACs can span multiple FBS items, so US addressing is ambiguous by
|
|
10
|
+
// construction). US ids exit 2 with a pointer at `rcf trace`.
|
|
11
|
+
//
|
|
12
|
+
// Deterministic-only boundary (§D13): this verb assembles what the
|
|
13
|
+
// tree says. It does not score bundle quality, detect under-specified
|
|
14
|
+
// FBS items, or generate spec prose - that non-deterministic judgement
|
|
15
|
+
// belongs to the Phase 7+ prompting + MCP resources surface.
|
|
16
|
+
|
|
17
|
+
import { writeFile } from 'node:fs/promises';
|
|
18
|
+
import { parseArgs } from 'node:util';
|
|
19
|
+
|
|
20
|
+
import { formatErrors, isRcfError, rcfError, writeUnexpectedFailure } from '#core/errors';
|
|
21
|
+
import { updateDocument, walkTree } from '#core/store';
|
|
22
|
+
import { findProjectRoot } from '../view/index.js';
|
|
23
|
+
import { kindOf } from '../query/index.js';
|
|
24
|
+
import { scanUnbackedServices } from '../query/attestation.js';
|
|
25
|
+
import {
|
|
26
|
+
assembleBundle,
|
|
27
|
+
checkCodeNodeGate,
|
|
28
|
+
checkContrastBeforePaletteGate,
|
|
29
|
+
checkDesignGate,
|
|
30
|
+
computeQueue,
|
|
31
|
+
formatJson,
|
|
32
|
+
formatMarkdown,
|
|
33
|
+
hasNoCodeNodesDeclaration,
|
|
34
|
+
planMark,
|
|
35
|
+
selectNext,
|
|
36
|
+
} from '../build/index.js';
|
|
37
|
+
import { classifyFbs } from '../ui-detection/classifier.js';
|
|
38
|
+
import { firstBaselineDisagreement } from '../design/index.js';
|
|
39
|
+
import { writeBrowserVerificationAck } from '../browser-verify/index.js';
|
|
40
|
+
// Track C+D §5.4 Stage-1 refusal gate.
|
|
41
|
+
import { fbsRefusalForOpenSweep } from '../req-baseline/gate.js';
|
|
42
|
+
|
|
43
|
+
const OPTION_SPEC = {
|
|
44
|
+
next: { type: 'boolean' },
|
|
45
|
+
mark: { type: 'string' },
|
|
46
|
+
format: { type: 'string' },
|
|
47
|
+
out: { type: 'string' },
|
|
48
|
+
strict: { type: 'boolean' },
|
|
49
|
+
quiet: { type: 'boolean' },
|
|
50
|
+
help: { type: 'boolean' },
|
|
51
|
+
// Phase 10 (X2 CodeNode bridge, D17): declare a build spec as genuinely
|
|
52
|
+
// producing no traceable code (docs-only, config-only), exempting it
|
|
53
|
+
// from the mark-complete CN gate. Combines only with `--mark complete`.
|
|
54
|
+
'no-code-nodes': { type: 'boolean' },
|
|
55
|
+
// Track B (ui-design-gate-0.7.0-spec §8.6): ship-without-verified
|
|
56
|
+
// escape hatch on --mark complete for a browserVerification record
|
|
57
|
+
// whose verdict is `block`. Records the operator's reason on the
|
|
58
|
+
// browserVerification record.
|
|
59
|
+
'accept-block': { type: 'boolean' },
|
|
60
|
+
reason: { type: 'string' },
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const HELP = `Usage: rcf build [fbs-id] [options]
|
|
64
|
+
|
|
65
|
+
Assemble FBS spec bundles and drive the build queue (the SDD adapter).
|
|
66
|
+
Four modes:
|
|
67
|
+
|
|
68
|
+
rcf build Queue overview (the FBS queue as a table,
|
|
69
|
+
with parallel-safe tier groups)
|
|
70
|
+
rcf build <fbs-id> Spec bundle for one FBS item
|
|
71
|
+
rcf build --next Bundle for the next actionable item
|
|
72
|
+
rcf build <fbs-id> --mark <status> Record a lifecycle transition
|
|
73
|
+
|
|
74
|
+
The positional is an FBS id only. For 'which FBS items implement this
|
|
75
|
+
story', use: rcf trace <us-id> --forward --format json
|
|
76
|
+
|
|
77
|
+
Lifecycle (forward-only): notStarted -> inProgress -> complete -> verified.
|
|
78
|
+
The --mark ladder caps at 'complete': --mark verified is refused (exit 4)
|
|
79
|
+
because 'verified' is written only by the finalise gate (rcf finalise) after
|
|
80
|
+
an independent verify run. Backward transitions are also refused (exit 4);
|
|
81
|
+
the deliberate-correction / manual-override escape hatch is:
|
|
82
|
+
rcf update <fbs-id> --set executionStatus=<status>
|
|
83
|
+
|
|
84
|
+
Bundle assembly is mechanical and deterministic: it projects what the
|
|
85
|
+
tree says. It does NOT judge whether the FBS is well-specified or the
|
|
86
|
+
bundle sufficient - that belongs to a later prompting + MCP phase.
|
|
87
|
+
|
|
88
|
+
Options:
|
|
89
|
+
--next Select the next actionable FBS item (lowest
|
|
90
|
+
buildOrder, notStarted, all dependencies
|
|
91
|
+
satisfied) and emit its bundle
|
|
92
|
+
--mark <status> Record a lifecycle transition (combines only
|
|
93
|
+
with the positional and --quiet)
|
|
94
|
+
--format <format> md (default) | json (queue + bundle modes)
|
|
95
|
+
--out <path> Write the bundle to a file (bundle modes)
|
|
96
|
+
--strict Refuse (exit 4) a bundle for a blocked item;
|
|
97
|
+
no effect with --next (it never selects
|
|
98
|
+
blocked items)
|
|
99
|
+
--no-code-nodes With --mark complete: declare this FBS as
|
|
100
|
+
genuinely producing no traceable code
|
|
101
|
+
(docs-only, config-only), recorded on the FBS
|
|
102
|
+
and exempting it from the mark-complete CN gate
|
|
103
|
+
--accept-block With --mark complete on a uiBearing FBS: accept
|
|
104
|
+
a browserVerification block (or a missing record)
|
|
105
|
+
and ship the FBS at complete. Requires --reason
|
|
106
|
+
"..." (>= 20 chars). Records the reason on the
|
|
107
|
+
browserVerification.operatorShipDespiteBlockReason
|
|
108
|
+
field. Track B ship-without-verified (spec §8.6).
|
|
109
|
+
--reason "..." Reason string for --accept-block.
|
|
110
|
+
--quiet Suppress non-error confirmations
|
|
111
|
+
--help Print this help
|
|
112
|
+
|
|
113
|
+
Mark-complete CN gate (D17): marking an FBS complete refuses (exit 3,
|
|
114
|
+
missingCodeNodes) when any of its ACs carries no Code Node. Author CN
|
|
115
|
+
coverage first, or pass --no-code-nodes for a genuinely no-code spec.
|
|
116
|
+
`;
|
|
117
|
+
|
|
118
|
+
const VALID_FORMATS = new Set(['md', 'json']);
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @param {string[]} argv - argv slice after `build`
|
|
122
|
+
* @param {object} [deps]
|
|
123
|
+
* @returns {Promise<number>}
|
|
124
|
+
*/
|
|
125
|
+
export async function main(argv, deps = {}) {
|
|
126
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
127
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
128
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
129
|
+
|
|
130
|
+
let parsed;
|
|
131
|
+
try {
|
|
132
|
+
parsed = parseArgs({ args: argv, options: OPTION_SPEC, allowPositionals: true, strict: true });
|
|
133
|
+
} catch (err) {
|
|
134
|
+
stderr.write(`[error] usage ${err.message}\n`);
|
|
135
|
+
stderr.write(HELP);
|
|
136
|
+
return 2;
|
|
137
|
+
}
|
|
138
|
+
const flags = parsed.values;
|
|
139
|
+
const positionals = parsed.positionals;
|
|
140
|
+
if (flags.help) { stdout.write(HELP); return 0; }
|
|
141
|
+
|
|
142
|
+
// Positional discipline (§D1): at most one, no globs.
|
|
143
|
+
if (positionals.length > 1) {
|
|
144
|
+
stderr.write('[error] usage build: multiple positional ids are not supported\n');
|
|
145
|
+
return 2;
|
|
146
|
+
}
|
|
147
|
+
const positional = positionals[0] ?? null;
|
|
148
|
+
if (positional && (positional.includes('*') || positional.includes('?'))) {
|
|
149
|
+
stderr.write('[error] usage build: wildcard / glob positional not supported\n');
|
|
150
|
+
return 2;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Mode detection + flag-conflict rules (§D1 / §D9).
|
|
154
|
+
const usage = (message) => {
|
|
155
|
+
stderr.write(`[error] usage build: ${message}\n`);
|
|
156
|
+
return 2;
|
|
157
|
+
};
|
|
158
|
+
if (flags.next && positional) return usage('--next takes no positional');
|
|
159
|
+
if (flags.mark !== undefined) {
|
|
160
|
+
if (!positional) return usage('--mark requires an <fbs-id> positional');
|
|
161
|
+
if (flags.next) return usage('--mark cannot combine with --next');
|
|
162
|
+
if (flags.format !== undefined) return usage('--mark cannot combine with --format');
|
|
163
|
+
if (flags.out !== undefined) return usage('--out is invalid in mark mode');
|
|
164
|
+
if (flags.strict) return usage('--mark cannot combine with --strict');
|
|
165
|
+
}
|
|
166
|
+
// Phase 10 (X2 CodeNode bridge, D17): --no-code-nodes only makes sense
|
|
167
|
+
// declaring a spec complete with no traceable code.
|
|
168
|
+
if (flags['no-code-nodes'] && flags.mark !== 'complete') {
|
|
169
|
+
return usage('--no-code-nodes only combines with --mark complete');
|
|
170
|
+
}
|
|
171
|
+
// Track B (§8.6): --accept-block only makes sense with --mark complete
|
|
172
|
+
// and requires --reason "...".
|
|
173
|
+
if (flags['accept-block'] && flags.mark !== 'complete') {
|
|
174
|
+
return usage('--accept-block only combines with --mark complete');
|
|
175
|
+
}
|
|
176
|
+
if (flags['accept-block'] && (typeof flags.reason !== 'string' || flags.reason.length < 20)) {
|
|
177
|
+
return usage('--accept-block requires --reason "..." with at least 20 characters');
|
|
178
|
+
}
|
|
179
|
+
const mode = flags.mark !== undefined
|
|
180
|
+
? 'mark'
|
|
181
|
+
: flags.next
|
|
182
|
+
? 'next'
|
|
183
|
+
: positional
|
|
184
|
+
? 'bundle'
|
|
185
|
+
: 'queue';
|
|
186
|
+
if (mode === 'queue') {
|
|
187
|
+
if (flags.out !== undefined) return usage('--out is invalid in queue mode');
|
|
188
|
+
if (flags.strict) return usage('--strict applies to bundle modes only');
|
|
189
|
+
}
|
|
190
|
+
const format = flags.format ?? 'md';
|
|
191
|
+
if (!VALID_FORMATS.has(format)) {
|
|
192
|
+
return usage(`unknown --format ${format} (expected md | json)`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const projectRoot = await findProjectRoot(cwd);
|
|
196
|
+
if (!projectRoot) {
|
|
197
|
+
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');
|
|
198
|
+
return 2;
|
|
199
|
+
}
|
|
200
|
+
// Single walkTree call; walker errors block every mode, including
|
|
201
|
+
// --mark - no status write lands on a tree that fails validation (§D6).
|
|
202
|
+
const { tree, errors } = await walkTree({ projectRoot });
|
|
203
|
+
if (errors.length > 0) {
|
|
204
|
+
stderr.write(`${formatErrors(errors, { verbose: false, strict: false })}\n`);
|
|
205
|
+
return 3;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const io = { stdout, stderr, quiet: Boolean(flags.quiet), out: flags.out ?? null };
|
|
209
|
+
|
|
210
|
+
if (mode === 'mark') {
|
|
211
|
+
return await runMark({
|
|
212
|
+
tree, projectRoot, fbsId: positional, status: flags.mark, io,
|
|
213
|
+
noCodeNodes: Boolean(flags['no-code-nodes']),
|
|
214
|
+
acceptBlock: Boolean(flags['accept-block']),
|
|
215
|
+
acceptReason: typeof flags.reason === 'string' ? flags.reason : null,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
if (mode === 'queue') {
|
|
219
|
+
const queue = computeQueue(tree);
|
|
220
|
+
const output = format === 'json' ? formatJson(queue, 'queue') : formatMarkdown(queue, 'queue');
|
|
221
|
+
stdout.write(output);
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
224
|
+
if (mode === 'next') {
|
|
225
|
+
return await emitNext({ tree, format, io });
|
|
226
|
+
}
|
|
227
|
+
return await emitBundle({
|
|
228
|
+
tree, fbsId: positional, format, strict: Boolean(flags.strict), io,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Classify a positional id (§D1): FBS ids proceed; US ids exit 2 with
|
|
234
|
+
* the trace pointer; other ids exit 2.
|
|
235
|
+
*/
|
|
236
|
+
function classifyPositional(tree, id) {
|
|
237
|
+
const kind = kindOf(tree, id);
|
|
238
|
+
if (kind === 'fbs') return null;
|
|
239
|
+
if (kind === 'userStory') {
|
|
240
|
+
return rcfError({
|
|
241
|
+
kind: 'usage',
|
|
242
|
+
message: `build: ${id} is a user story, not an FBS id; the FBS is the queue unit. `
|
|
243
|
+
+ `To list the FBS items linked to this story: rcf trace ${id} --forward --format json`,
|
|
244
|
+
documentId: id,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
if (kind) {
|
|
248
|
+
return rcfError({
|
|
249
|
+
kind: 'usage',
|
|
250
|
+
message: `build: ${id} is a ${kind} id; rcf build addresses FBS items only`,
|
|
251
|
+
documentId: id,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
return rcfError({ kind: 'usage', message: `build: id ${id} not found`, documentId: id });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function emitBundle({ tree, fbsId, format, strict, io }) {
|
|
258
|
+
const classification = classifyPositional(tree, fbsId);
|
|
259
|
+
if (classification) {
|
|
260
|
+
io.stderr.write(`[error] usage ${classification.message}\n`);
|
|
261
|
+
return 2;
|
|
262
|
+
}
|
|
263
|
+
// Track C+D §5.4: single-FBS bundle emission also refuses when the
|
|
264
|
+
// bound US has open baseline sweep candidates. Same shape as --next
|
|
265
|
+
// so a caller can rely on one refusal semantics regardless of entry.
|
|
266
|
+
const openSweep = fbsRefusalForOpenSweep(tree, fbsId);
|
|
267
|
+
if (openSweep) {
|
|
268
|
+
io.stderr.write(`[error] ${openSweep.message}\n`);
|
|
269
|
+
return 4;
|
|
270
|
+
}
|
|
271
|
+
// Track B (spec §4.4): classifier signal alongside the single-FBS
|
|
272
|
+
// bundle. Same transparency rule as --next.
|
|
273
|
+
emitClassifierSignal(io, tree, fbsId);
|
|
274
|
+
const bundle = assembleBundle(tree, { fbsId });
|
|
275
|
+
// Blocked-dependency gate (§D12): warn by default (the BLOCKED block
|
|
276
|
+
// in section 2), refuse under --strict.
|
|
277
|
+
if (strict && bundle.blockedBy.length > 0) {
|
|
278
|
+
const blocking = bundle.dependencies
|
|
279
|
+
.filter((d) => bundle.blockedBy.includes(d.fbsId))
|
|
280
|
+
.map((d) => `${d.fbsId} (${d.executionStatus ?? 'unknown'})`)
|
|
281
|
+
.join(', ');
|
|
282
|
+
io.stderr.write(`[error] refused build: ${fbsId} is blocked by ${blocking}\n`);
|
|
283
|
+
return 4;
|
|
284
|
+
}
|
|
285
|
+
const output = format === 'json' ? formatJson(bundle, 'bundle') : formatMarkdown(bundle, 'bundle');
|
|
286
|
+
return await emitToSink(output, io);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function emitNext({ tree, format, io }) {
|
|
290
|
+
const queue = computeQueue(tree);
|
|
291
|
+
const next = selectNext(queue);
|
|
292
|
+
if (next) {
|
|
293
|
+
// Track C+D §5.4: Stage-1 (Define) refuses to enter Build for any
|
|
294
|
+
// FBS binding an AC on a US that still has open baseline sweep
|
|
295
|
+
// candidates. Fires before any classifier signal so the operator
|
|
296
|
+
// sees the refusal ahead of noise.
|
|
297
|
+
const openSweep = fbsRefusalForOpenSweep(tree, next.fbsId);
|
|
298
|
+
if (openSweep) {
|
|
299
|
+
io.stderr.write(`[error] ${openSweep.message}\n`);
|
|
300
|
+
return 4;
|
|
301
|
+
}
|
|
302
|
+
// Spec section 4.2 / review N-1: warn when the selected FBS names
|
|
303
|
+
// dependsOnServices that no preFlightConfig record covers, so the
|
|
304
|
+
// operator sees the same signal the elicitation and build-cycle
|
|
305
|
+
// playbooks already advertise for `rcf build --next`. Warn-only
|
|
306
|
+
// (not exit-4) matches the spec's Stage 1 warn-only ruling; the
|
|
307
|
+
// hard refuse lives at Stage 4 via `coverage --strict`.
|
|
308
|
+
const unbacked = scanUnbackedServices(tree, next.fbsId);
|
|
309
|
+
if (unbacked.length > 0) {
|
|
310
|
+
const names = unbacked.map((u) => u.serviceId).join(', ');
|
|
311
|
+
io.stderr.write(`[warn] build --next: ${next.fbsId} touches services not covered by any preFlightConfig (${names}); run 'rcf preflight' before Stage 4.\n`);
|
|
312
|
+
}
|
|
313
|
+
// Track B (spec §4.4): always print the classifier verdict + signals
|
|
314
|
+
// for the selected FBS. Transparent-by-default so a `notUi` verdict
|
|
315
|
+
// on prose that visibly contains UI keywords is still visible to the
|
|
316
|
+
// operator; missing the transparency would re-enable the silent
|
|
317
|
+
// shortcut pattern the cold-run "Skip RCF" wobble called out.
|
|
318
|
+
emitClassifierSignal(io, tree, next.fbsId);
|
|
319
|
+
const bundle = assembleBundle(tree, { fbsId: next.fbsId });
|
|
320
|
+
const output = format === 'json' ? formatJson(bundle, 'next') : formatMarkdown(bundle, 'next');
|
|
321
|
+
return await emitToSink(output, io);
|
|
322
|
+
}
|
|
323
|
+
// Nothing actionable is a valid answer, exit 0 (§D2 / OQ-P6-2): the
|
|
324
|
+
// envelope distinguishes "done" (queueEmpty) from "stuck".
|
|
325
|
+
const envelope = {
|
|
326
|
+
queueEmpty: queue.totals.notStarted === 0 && queue.totals.inProgress === 0,
|
|
327
|
+
totals: queue.totals,
|
|
328
|
+
blocked: queue.items.filter((i) => i.state === 'blocked').map((i) => i.fbsId),
|
|
329
|
+
inProgress: queue.items.filter((i) => i.state === 'inProgress').map((i) => i.fbsId),
|
|
330
|
+
};
|
|
331
|
+
const output = format === 'json' ? formatJson(envelope, 'next') : formatMarkdown(envelope, 'next');
|
|
332
|
+
return await emitToSink(output, io);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Default sink is stdout (pipe-friendly for the harness loop); `--out`
|
|
337
|
+
* writes to a file instead - parent directory must exist, plain
|
|
338
|
+
* overwrite, single writeFile (§D4). Write failures exit 1.
|
|
339
|
+
*/
|
|
340
|
+
async function emitToSink(output, io) {
|
|
341
|
+
if (!io.out) {
|
|
342
|
+
io.stdout.write(output);
|
|
343
|
+
return 0;
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
await writeFile(io.out, output, 'utf8');
|
|
347
|
+
} catch (err) {
|
|
348
|
+
writeUnexpectedFailure(
|
|
349
|
+
rcfError({ kind: 'ioFailure', message: `build: --out write failed: ${err.message}`, stack: err.stack }),
|
|
350
|
+
io.stderr,
|
|
351
|
+
);
|
|
352
|
+
return 1;
|
|
353
|
+
}
|
|
354
|
+
if (!io.quiet) io.stdout.write(`bundle written to ${io.out}\n`);
|
|
355
|
+
return 0;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Mark mode (§D5): plan via the pure transition table, execute via the
|
|
360
|
+
* Phase 4 `updateDocument` path (single executionStatus set; the
|
|
361
|
+
* writer schema-validates and bumps updatedAt). Output is a fixed
|
|
362
|
+
* one-line confirmation - exit codes carry the outcome (OQ-P6-4).
|
|
363
|
+
*/
|
|
364
|
+
async function runMark({ tree, projectRoot, fbsId, status, io, noCodeNodes = false, acceptBlock = false, acceptReason = null }) {
|
|
365
|
+
const plan = planMark(tree, { fbsId, status });
|
|
366
|
+
if (isRcfError(plan)) {
|
|
367
|
+
io.stderr.write(`[error] usage ${plan.message}\n`);
|
|
368
|
+
return 2;
|
|
369
|
+
}
|
|
370
|
+
if (plan.refused) {
|
|
371
|
+
io.stderr.write(`[error] refused ${plan.message}\n`);
|
|
372
|
+
return 4;
|
|
373
|
+
}
|
|
374
|
+
if (plan.noOp) {
|
|
375
|
+
// Idempotent no-op: a retried harness step does not fail the loop.
|
|
376
|
+
if (!io.quiet) io.stdout.write(`${plan.fbsId} already ${plan.to}\n`);
|
|
377
|
+
return 0;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const sets = [{ path: 'executionStatus', value: plan.to }];
|
|
381
|
+
|
|
382
|
+
// Track B (§5.2): soft warning on --mark inProgress for uiBearing
|
|
383
|
+
// FBS that has no designStage authored yet. Warn-only per spec.
|
|
384
|
+
if (plan.to === 'inProgress') {
|
|
385
|
+
const fbs = tree.byId.get(plan.fbsId);
|
|
386
|
+
if (fbs?.uiBearing === true && !fbs?.designStage) {
|
|
387
|
+
io.stderr.write(`[warn] build --mark inProgress: ${plan.fbsId} is uiBearing but has no designStage yet. Consider running 'rcf design ${plan.fbsId}' before Stage 2 build begins. Not a refusal; the hard gate fires at --mark complete.\n`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Phase 10 (X2 CodeNode bridge, D17, operator ruling): the mark-complete
|
|
392
|
+
// CN gate. Refuses (exit 3, structured missingCodeNodes) when any AC of
|
|
393
|
+
// the completed spec carries no Code Node, unless the FBS already
|
|
394
|
+
// carries the no-code-nodes declaration or this invocation supplies it.
|
|
395
|
+
if (plan.to === 'complete') {
|
|
396
|
+
const fbs = tree.byId.get(plan.fbsId);
|
|
397
|
+
|
|
398
|
+
// Track B (§5.5): the Design gate. Refuses --mark complete when
|
|
399
|
+
// the FBS is uiBearing=true and designStageComplete is not true.
|
|
400
|
+
// Fires before the CN gate so operators do not have to fix CN
|
|
401
|
+
// coverage on an FBS that also needs a design pass.
|
|
402
|
+
const designGate = checkDesignGate(fbs);
|
|
403
|
+
if (!designGate.ok) {
|
|
404
|
+
io.stderr.write(`[error] refused build: ${plan.fbsId} is uiBearing but designStageComplete is not true on the FBS record.\n`
|
|
405
|
+
+ ' Complete the Design substage first:\n'
|
|
406
|
+
+ ` rcf design ${plan.fbsId} (dispatches the Design worker)\n`
|
|
407
|
+
+ ` rcf design ${plan.fbsId} --mark-complete (hand-authored equivalent, once all three artefacts are present)\n`);
|
|
408
|
+
return 4;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Track B (§7 mandate 10, §6.2): contrast-before-palette gate.
|
|
412
|
+
// The boolean is the primary attestation (§12 O-5); git history
|
|
413
|
+
// corroboration is deferred to a future spec release (§7 mandate
|
|
414
|
+
// 10 permits this when history is inconclusive, and a fresh clone
|
|
415
|
+
// is the default posture for the sub-process runner). The pure
|
|
416
|
+
// gate refuses on the boolean alone.
|
|
417
|
+
const cbpGate = checkContrastBeforePaletteGate(fbs);
|
|
418
|
+
if (!cbpGate.ok) {
|
|
419
|
+
io.stderr.write(`[error] refused ${cbpGate.message}\n`);
|
|
420
|
+
return 4;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Track B (§6.2): belt-and-braces baseline-vs-designStage
|
|
424
|
+
// disagreement refusal, in case the design --mark-complete path
|
|
425
|
+
// was bypassed (e.g. rcf update wrote the disagreement after
|
|
426
|
+
// design --mark-complete succeeded).
|
|
427
|
+
if (fbs?.uiBearing === true) {
|
|
428
|
+
const disagreement = firstBaselineDisagreement(tree, fbs);
|
|
429
|
+
if (disagreement) {
|
|
430
|
+
io.stderr.write(`[error] refused build: ${plan.fbsId} designStage.${disagreement.designStagePath} = ${JSON.stringify(disagreement.designValue)} conflicts with uiBaseline.defaults.${disagreement.path} = ${JSON.stringify(disagreement.baselineValue)} and there is no operatorOptOuts entry.\n`
|
|
431
|
+
+ ' Options:\n'
|
|
432
|
+
+ ` 1) Change designStage.${disagreement.designStagePath} to match the baseline.\n`
|
|
433
|
+
+ ` 2) rcf ui-baseline opt-out --field ${disagreement.path} --reason "..." (project-wide override).\n`
|
|
434
|
+
+ ` 3) rcf update ${plan.fbsId} --set designStage.${disagreement.designStagePath}=... (per-FBS override; records ack).\n`);
|
|
435
|
+
return 4;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Track B (§8.5, §8.6): browser-verification verdict gate.
|
|
440
|
+
// Reads the latest browserVerification record for the FBS and
|
|
441
|
+
// gates by verdict:
|
|
442
|
+
// pass -> permitted
|
|
443
|
+
// warn -> permitted only when operatorAckAt is populated
|
|
444
|
+
// block -> refused unless --accept-block --reason "..."
|
|
445
|
+
// Absent record -> refused (finalise still refuses via the verify
|
|
446
|
+
// `BROWSER-VERIFICATION-MISSING` verdict class, which lives in
|
|
447
|
+
// the verify car; the build-side refusal here catches the same
|
|
448
|
+
// defect earlier so the operator does not push the FBS to
|
|
449
|
+
// complete without any evidence).
|
|
450
|
+
if (fbs?.uiBearing === true) {
|
|
451
|
+
const bvRecord = latestBrowserVerification(tree.manifest, plan.fbsId);
|
|
452
|
+
if (!bvRecord) {
|
|
453
|
+
// Track B review B-1 (2026-07-31): refuse cleanly when no
|
|
454
|
+
// browserVerification record exists, even with --accept-block.
|
|
455
|
+
// Previously acceptBlock let execution fall through past the
|
|
456
|
+
// ack-writer (which is guarded on bvRecord being truthy),
|
|
457
|
+
// marking the FBS complete with only an ephemeral stderr line
|
|
458
|
+
// as evidence of the operator's reason. That is the exact
|
|
459
|
+
// "durable record or it did not happen" defect Track A's B-1
|
|
460
|
+
// closed for --ship-without-verified. Composing a synthetic
|
|
461
|
+
// browserVerification record here would invent evidence for a
|
|
462
|
+
// gate the operator never actually ran; the honest posture is
|
|
463
|
+
// to make the operator run the gate (the stub driver produces
|
|
464
|
+
// a warn record, which then legitimately anchors the ack) or
|
|
465
|
+
// ratify a real record. Two-command dance: acknowledged and
|
|
466
|
+
// preferred.
|
|
467
|
+
io.stderr.write(`[error] refused build: ${plan.fbsId} is uiBearing but no browserVerification record exists on the manifest.\n`
|
|
468
|
+
+ ` Run: rcf browser-verify ${plan.fbsId}\n`
|
|
469
|
+
+ ` Then, if the resulting record is block or warn and you have decided to ship anyway, rerun with --accept-block --reason "..." on that record.\n`
|
|
470
|
+
+ ' (--accept-block requires an existing browserVerification record; it acknowledges a real verdict rather than skipping the gate.)\n');
|
|
471
|
+
return 4;
|
|
472
|
+
} else if (bvRecord.verdict === 'block' && !acceptBlock) {
|
|
473
|
+
io.stderr.write(`[error] refused build: ${plan.fbsId} browserVerification ${bvRecord.id} verdict is 'block'.\n`
|
|
474
|
+
+ ' Re-run browser-verify after fixing the failures, or acknowledge with:\n'
|
|
475
|
+
+ ` rcf build ${plan.fbsId} --mark complete --accept-block --reason "..."\n`);
|
|
476
|
+
return 4;
|
|
477
|
+
} else if (bvRecord.verdict === 'warn' && !bvRecord.operatorAckAt) {
|
|
478
|
+
io.stderr.write(`[error] refused build: ${plan.fbsId} browserVerification ${bvRecord.id} verdict is 'warn' but operatorAckAt is not set.\n`
|
|
479
|
+
+ ' Ack the warn verdict with:\n'
|
|
480
|
+
+ ` rcf browser-verify ${plan.fbsId} --ack\n`);
|
|
481
|
+
return 4;
|
|
482
|
+
}
|
|
483
|
+
// Ship-without-verified: record the operator's reason on the bv
|
|
484
|
+
// record when they used --accept-block. Only fires on `block`
|
|
485
|
+
// because the earlier `warn` guard already returned 4 when the
|
|
486
|
+
// record was warn-without-ack; the sanctioned warn clear is
|
|
487
|
+
// `rcf browser-verify <id> --ack`, not `--accept-block`.
|
|
488
|
+
if (acceptBlock && bvRecord && bvRecord.verdict === 'block') {
|
|
489
|
+
const ackResult = await writeBrowserVerificationAck({
|
|
490
|
+
projectRoot, tree, fbsId: plan.fbsId,
|
|
491
|
+
operatorAckAt: true,
|
|
492
|
+
operatorShipDespiteBlockReason: acceptReason,
|
|
493
|
+
});
|
|
494
|
+
if (ackResult && 'kind' in ackResult && 'message' in ackResult) {
|
|
495
|
+
if (ackResult.kind === 'ioFailure') { writeUnexpectedFailure(ackResult, io.stderr); return 1; }
|
|
496
|
+
io.stderr.write(`[error] ${ackResult.kind} ${ackResult.message}\n`);
|
|
497
|
+
return 3;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const alreadyDeclared = hasNoCodeNodesDeclaration(fbs);
|
|
503
|
+
if (noCodeNodes && !alreadyDeclared) {
|
|
504
|
+
sets.push({ path: 'noCodeNodes', value: true });
|
|
505
|
+
} else if (!alreadyDeclared) {
|
|
506
|
+
const gate = checkCodeNodeGate(tree, fbs);
|
|
507
|
+
if (!gate.ok) {
|
|
508
|
+
const err = rcfError({
|
|
509
|
+
kind: 'missingCodeNodes',
|
|
510
|
+
message: `build --mark complete: refused - ${plan.fbsId} has AC(s) with no Code Node: ${gate.missingAcIds.join(', ')}. `
|
|
511
|
+
+ 'Author CN coverage for these ACs, or pass --no-code-nodes for a genuinely no-code (docs-only, config-only) spec.',
|
|
512
|
+
documentId: plan.fbsId,
|
|
513
|
+
field: 'acIds',
|
|
514
|
+
rule: 'missingCodeNodes',
|
|
515
|
+
});
|
|
516
|
+
io.stderr.write(`[error] ${err.kind} ${err.message}\n`);
|
|
517
|
+
return 3;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const result = await updateDocument({
|
|
523
|
+
projectRoot,
|
|
524
|
+
tree,
|
|
525
|
+
id: plan.fbsId,
|
|
526
|
+
sets,
|
|
527
|
+
options: {},
|
|
528
|
+
});
|
|
529
|
+
if (isRcfError(result)) {
|
|
530
|
+
if (result.kind === 'ioFailure') {
|
|
531
|
+
writeUnexpectedFailure(result, io.stderr);
|
|
532
|
+
return 1;
|
|
533
|
+
}
|
|
534
|
+
io.stderr.write(`[error] ${result.kind} ${result.message}\n`);
|
|
535
|
+
if (result.kind === 'usage') return 2;
|
|
536
|
+
if (result.kind === 'validation' || result.kind === 'brokenReference') return 3;
|
|
537
|
+
return 1;
|
|
538
|
+
}
|
|
539
|
+
if (!io.quiet) io.stdout.write(`marked ${plan.fbsId} ${plan.from} -> ${plan.to}\n`);
|
|
540
|
+
return 0;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Print the UI-bearing classifier verdict for an FBS on the bundle
|
|
545
|
+
* runbook (Track B, ui-design-gate-0.7.0-spec §4.4). Emits nothing when
|
|
546
|
+
* the FBS document is missing (defensive; the caller has already
|
|
547
|
+
* classified). Not enough to gate on its own; the operator ratifies
|
|
548
|
+
* via `rcf update <fbs-id> --set uiBearing=true|false`.
|
|
549
|
+
*
|
|
550
|
+
* @param {object} io
|
|
551
|
+
* @param {object} tree
|
|
552
|
+
* @param {string} fbsId
|
|
553
|
+
*/
|
|
554
|
+
function emitClassifierSignal(io, tree, fbsId) {
|
|
555
|
+
const fbs = tree.byId.get(fbsId);
|
|
556
|
+
if (!fbs) return;
|
|
557
|
+
const block = classifyFbs(tree, fbsId);
|
|
558
|
+
if (!block) return;
|
|
559
|
+
const signalCount = Array.isArray(block.signals) ? block.signals.length : 0;
|
|
560
|
+
const suffix = fbs.uiBearing === true ? ' [ratified: uiBearing=true]'
|
|
561
|
+
: fbs.uiBearing === false ? ' [ratified: uiBearing=false]'
|
|
562
|
+
: ' [not yet ratified]';
|
|
563
|
+
io.stderr.write(`[info] build: ui-classifier verdict=${block.verdict} reason=${block.reason} (${signalCount} signal(s))${suffix}\n`);
|
|
564
|
+
if (block.verdict === 'ui' && fbs.uiBearing !== true) {
|
|
565
|
+
io.stderr.write(` Classified as UI-bearing. Ratify with: rcf update ${fbsId} --set uiBearing=true\n`);
|
|
566
|
+
io.stderr.write(` Override with: rcf update ${fbsId} --set uiBearing=false (if the classifier is wrong).\n`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Latest browserVerification record for an FBS. Returns null when
|
|
572
|
+
* none exists.
|
|
573
|
+
*
|
|
574
|
+
* @param {object|null} manifest
|
|
575
|
+
* @param {string} fbsId
|
|
576
|
+
* @returns {object|null}
|
|
577
|
+
*/
|
|
578
|
+
function latestBrowserVerification(manifest, fbsId) {
|
|
579
|
+
const records = Array.isArray(manifest?.browserVerification) ? manifest.browserVerification : [];
|
|
580
|
+
for (let i = records.length - 1; i >= 0; i -= 1) {
|
|
581
|
+
if (records[i]?.fbsId === fbsId) return records[i];
|
|
582
|
+
}
|
|
583
|
+
return null;
|
|
584
|
+
}
|