rcf-lite 0.0.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +344 -0
- package/LICENSE +202 -0
- package/README.md +92 -4
- package/bin/rcf-verify.js +122 -0
- package/bin/rcf.js +174 -0
- package/bin/view-supervisor-child.mjs +14 -0
- package/fixtures/canary-manifest.json +103 -0
- package/guidance/README.md +25 -0
- package/guidance/build-cycle-playbook.md +492 -0
- package/guidance/build-cycle.md +40 -0
- package/guidance/document-model.md +45 -0
- package/guidance/elicitation-playbook.md +442 -0
- package/guidance/harness-template.md +246 -0
- package/guidance/managed/README.md +63 -0
- package/guidance/managed/agent-instructions-block.hash +1 -0
- package/guidance/managed/agent-instructions-block.md +220 -0
- package/guidance/managed/legacy-fragment-hashes.json +22 -0
- package/guidance/manifest.json +21 -0
- package/guidance/overview.md +48 -0
- package/guidance/persona-programme.md +44 -0
- package/package.json +71 -8
- package/rcf/adrs/adr-001.json +25 -0
- package/rcf/adrs/adr-002.json +25 -0
- package/rcf/adrs/adr-003.json +25 -0
- package/rcf/adrs/adr-004.json +20 -0
- package/rcf/adrs/adr-005.json +20 -0
- package/rcf/adrs/adr-006.json +25 -0
- package/rcf/adrs/adr-007.json +25 -0
- package/rcf/adrs/adr-008.json +25 -0
- package/rcf/adrs/adr-009.json +25 -0
- package/rcf/build-sequence.json +11 -0
- package/rcf/code-nodes/cn-001.json +14 -0
- package/rcf/code-nodes/cn-002.json +14 -0
- package/rcf/code-nodes/cn-003.json +15 -0
- package/rcf/code-nodes/cn-004.json +17 -0
- package/rcf/code-nodes/cn-005.json +19 -0
- package/rcf/code-nodes/cn-006.json +17 -0
- package/rcf/code-nodes/cn-007.json +16 -0
- package/rcf/code-nodes/cn-008.json +19 -0
- package/rcf/code-nodes/cn-009.json +16 -0
- package/rcf/code-nodes/cn-010.json +18 -0
- package/rcf/code-nodes/cn-011.json +18 -0
- package/rcf/code-nodes/cn-012.json +18 -0
- package/rcf/code-nodes/cn-013.json +19 -0
- package/rcf/code-nodes/cn-014.json +16 -0
- package/rcf/code-nodes/cn-015.json +16 -0
- package/rcf/code-nodes/cn-016.json +16 -0
- package/rcf/code-nodes/cn-017.json +16 -0
- package/rcf/code-nodes/cn-018.json +15 -0
- package/rcf/code-nodes/cn-019.json +16 -0
- package/rcf/code-nodes/cn-020.json +16 -0
- package/rcf/code-nodes/cn-021.json +16 -0
- package/rcf/code-nodes/cn-022.json +16 -0
- package/rcf/code-nodes/cn-023.json +16 -0
- package/rcf/code-nodes/cn-024.json +16 -0
- package/rcf/code-nodes/cn-025.json +16 -0
- package/rcf/code-nodes/cn-026.json +16 -0
- package/rcf/code-nodes/cn-027.json +16 -0
- package/rcf/code-nodes/cn-028.json +16 -0
- package/rcf/code-nodes/cn-029.json +19 -0
- package/rcf/code-nodes/cn-030.json +14 -0
- package/rcf/code-nodes/cn-031.json +14 -0
- package/rcf/code-nodes/cn-032.json +14 -0
- package/rcf/code-nodes/cn-033.json +14 -0
- package/rcf/code-nodes/cn-034.json +14 -0
- package/rcf/code-nodes/cn-035.json +14 -0
- package/rcf/code-nodes/cn-036.json +14 -0
- package/rcf/code-nodes/cn-037.json +14 -0
- package/rcf/code-nodes/cn-038.json +14 -0
- package/rcf/code-nodes/cn-039.json +14 -0
- package/rcf/code-nodes/cn-040.json +14 -0
- package/rcf/code-nodes/cn-041.json +14 -0
- package/rcf/code-nodes/cn-042.json +14 -0
- package/rcf/code-nodes/cn-043.json +14 -0
- package/rcf/code-nodes/cn-044.json +14 -0
- package/rcf/code-nodes/cn-045.json +14 -0
- package/rcf/code-nodes/cn-046.json +14 -0
- package/rcf/code-nodes/cn-047.json +14 -0
- package/rcf/code-nodes/cn-048.json +14 -0
- package/rcf/code-nodes/cn-049.json +14 -0
- package/rcf/code-nodes/cn-050.json +14 -0
- package/rcf/code-nodes/cn-051.json +14 -0
- package/rcf/code-nodes/cn-052.json +14 -0
- package/rcf/code-nodes/cn-053.json +14 -0
- package/rcf/code-nodes/cn-054.json +14 -0
- package/rcf/code-nodes/cn-055.json +14 -0
- package/rcf/code-nodes/cn-056.json +14 -0
- package/rcf/code-nodes/cn-057.json +14 -0
- package/rcf/fbs/fbs-001.json +49 -0
- package/rcf/fbs/fbs-002.json +42 -0
- package/rcf/fbs/fbs-003.json +37 -0
- package/rcf/fbs/fbs-004.json +39 -0
- package/rcf/fbs/fbs-005.json +38 -0
- package/rcf/fbs/fbs-006.json +48 -0
- package/rcf/fbs/fbs-007.json +39 -0
- package/rcf/fbs/fbs-008.json +40 -0
- package/rcf/fbs/fbs-009.json +36 -0
- package/rcf/fbs/fbs-010.json +41 -0
- package/rcf/fbs/fbs-011.json +36 -0
- package/rcf/fbs/fbs-012.json +46 -0
- package/rcf/fbs/fbs-013.json +42 -0
- package/rcf/fbs/fbs-014.json +49 -0
- package/rcf/fbs/fbs-015.json +32 -0
- package/rcf/manifest.json +17 -0
- package/rcf/prd.json +47 -0
- package/rcf/requirements/req-001.json +19 -0
- package/rcf/requirements/req-002.json +19 -0
- package/rcf/requirements/req-003.json +19 -0
- package/rcf/requirements/req-004.json +19 -0
- package/rcf/requirements/req-005.json +19 -0
- package/rcf/requirements/req-006.json +19 -0
- package/rcf/requirements/req-007.json +19 -0
- package/rcf/requirements/req-008.json +19 -0
- package/rcf/requirements/req-009.json +19 -0
- package/rcf/tacs/tac-001.json +45 -0
- package/rcf/tacs/tac-002.json +109 -0
- package/rcf/tacs/tac-003.json +40 -0
- package/rcf/tacs/tac-004.json +51 -0
- package/rcf/tacs/tac-005.json +52 -0
- package/rcf/tacs/tac-006.json +104 -0
- package/rcf/tacs/tac-007.json +38 -0
- package/rcf/tacs/tac-008.json +51 -0
- package/rcf/tad.json +51 -0
- package/rcf/test-suites/PENDING.md +23 -0
- package/rcf/test-suites/ts-001.json +38 -0
- package/rcf/test-suites/ts-002.json +38 -0
- package/rcf/test-suites/ts-003.json +43 -0
- package/rcf/test-suites/ts-004.json +44 -0
- package/rcf/test-suites/ts-005.json +30 -0
- package/rcf/test-suites/ts-006.json +36 -0
- package/rcf/test-suites/ts-007.json +43 -0
- package/rcf/test-suites/ts-008.json +37 -0
- package/rcf/test-suites/ts-009.json +38 -0
- package/rcf/test-suites/ts-010.json +38 -0
- package/rcf/test-suites/ts-011.json +44 -0
- package/rcf/test-suites/ts-012.json +36 -0
- package/rcf/test-suites/ts-013.json +38 -0
- package/rcf/test-suites/ts-014.json +38 -0
- package/rcf/test-suites/ts-015.json +38 -0
- package/rcf/test-suites/ts-016.json +37 -0
- package/rcf/test-suites/ts-017.json +38 -0
- package/rcf/test-suites/ts-018.json +38 -0
- package/rcf/test-suites/ts-019.json +37 -0
- package/rcf/test-suites/ts-020.json +46 -0
- package/rcf/test-suites/ts-021.json +46 -0
- package/rcf/test-suites/ts-022.json +46 -0
- package/rcf/test-suites/ts-023.json +46 -0
- package/rcf/test-suites/ts-024.json +46 -0
- package/rcf/test-suites/ts-025.json +52 -0
- package/rcf/user-stories/us-101.json +40 -0
- package/rcf/user-stories/us-102.json +40 -0
- package/rcf/user-stories/us-201.json +40 -0
- package/rcf/user-stories/us-202.json +40 -0
- package/rcf/user-stories/us-203.json +32 -0
- package/rcf/user-stories/us-301.json +40 -0
- package/rcf/user-stories/us-302.json +40 -0
- package/rcf/user-stories/us-303.json +40 -0
- package/rcf/user-stories/us-304.json +40 -0
- package/rcf/user-stories/us-401.json +40 -0
- package/rcf/user-stories/us-402.json +40 -0
- package/rcf/user-stories/us-403.json +40 -0
- package/rcf/user-stories/us-501.json +40 -0
- package/rcf/user-stories/us-502.json +40 -0
- package/rcf/user-stories/us-503.json +40 -0
- package/rcf/user-stories/us-601.json +40 -0
- package/rcf/user-stories/us-602.json +40 -0
- package/rcf/user-stories/us-701.json +40 -0
- package/rcf/user-stories/us-702.json +40 -0
- package/rcf/user-stories/us-801.json +49 -0
- package/rcf/user-stories/us-802.json +49 -0
- package/rcf/user-stories/us-803.json +49 -0
- package/rcf/user-stories/us-804.json +49 -0
- package/rcf/user-stories/us-805.json +49 -0
- package/rcf/user-stories/us-901.json +40 -0
- package/src/.gitkeep +0 -0
- package/src/admissibility/enforce.js +142 -0
- package/src/admissibility/index.js +8 -0
- package/src/admissibility/markers.js +104 -0
- package/src/admissibility/scope-lint.js +163 -0
- package/src/browser-verify/auth-smoke.js +109 -0
- package/src/browser-verify/index.js +29 -0
- package/src/browser-verify/invariants.js +336 -0
- package/src/browser-verify/manifest-writer.js +189 -0
- package/src/browser-verify/runner.js +171 -0
- package/src/build/bundle.js +198 -0
- package/src/build/formatters/json.js +18 -0
- package/src/build/formatters/markdown.js +406 -0
- package/src/build/index.js +14 -0
- package/src/build/mark.js +177 -0
- package/src/build/queue.js +285 -0
- package/src/cli/browser-verify.js +231 -0
- package/src/cli/build.js +584 -0
- package/src/cli/coverage.js +219 -0
- package/src/cli/create.js +358 -0
- package/src/cli/delete.js +127 -0
- package/src/cli/design.js +353 -0
- package/src/cli/doctor.js +548 -0
- package/src/cli/fbs.js +171 -0
- package/src/cli/finalise.js +377 -0
- package/src/cli/guidance.js +206 -0
- package/src/cli/help.js +156 -0
- package/src/cli/impact.js +119 -0
- package/src/cli/init.js +282 -0
- package/src/cli/intake.js +153 -0
- package/src/cli/link.js +128 -0
- package/src/cli/mcp.js +160 -0
- package/src/cli/preflight.js +220 -0
- package/src/cli/read.js +168 -0
- package/src/cli/req-baseline.js +269 -0
- package/src/cli/req-classify.js +135 -0
- package/src/cli/review.js +295 -0
- package/src/cli/test-suite.js +221 -0
- package/src/cli/trace.js +172 -0
- package/src/cli/ui-baseline.js +292 -0
- package/src/cli/ui-classify.js +108 -0
- package/src/cli/update.js +197 -0
- package/src/cli/validate.js +168 -0
- package/src/cli/view.js +452 -0
- package/src/core/baseline-catalog/data/auth.json +42 -0
- package/src/core/baseline-catalog/data/http-api.json +42 -0
- package/src/core/baseline-catalog/data/notifications.json +33 -0
- package/src/core/baseline-catalog/data/persistence.json +33 -0
- package/src/core/baseline-catalog/data/web-ui.json +60 -0
- package/src/core/baseline-catalog/index.js +121 -0
- package/src/core/errors/index.js +167 -0
- package/src/core/fixtures/register-canary/canary-prompt-01.json +18 -0
- package/src/core/fixtures/register-canary/canary-prompt-02.json +21 -0
- package/src/core/fixtures/register-canary/canary-prompt-03.json +17 -0
- package/src/core/isolation/index.js +60 -0
- package/src/core/mcp/framing.js +103 -0
- package/src/core/mcp/index.js +8 -0
- package/src/core/mcp/server.js +228 -0
- package/src/core/patterns/register-canary.js +209 -0
- package/src/core/patterns/req-shapes.js +158 -0
- package/src/core/patterns/services.js +358 -0
- package/src/core/patterns/ui-shapes.js +166 -0
- package/src/core/store/cn-resolve.js +134 -0
- package/src/core/store/derive-deps.js +93 -0
- package/src/core/store/ids.js +78 -0
- package/src/core/store/index.js +20 -0
- package/src/core/store/init.js +255 -0
- package/src/core/store/loader.js +214 -0
- package/src/core/store/tp-resolve.js +176 -0
- package/src/core/store/validator.js +191 -0
- package/src/core/store/walker.js +944 -0
- package/src/core/store/writer.js +1879 -0
- package/src/design/index.js +11 -0
- package/src/design/writer.js +271 -0
- package/src/finalise/detect.js +151 -0
- package/src/finalise/index.js +31 -0
- package/src/finalise/ingest.js +160 -0
- package/src/finalise/install.js +119 -0
- package/src/finalise/ship-without-verified.js +131 -0
- package/src/finalise/spawn.js +84 -0
- package/src/intake/fidelity.js +105 -0
- package/src/intake/index.js +6 -0
- package/src/intake/manifest-writer.js +100 -0
- package/src/intake/orchestrator.js +138 -0
- package/src/intake/validate.js +80 -0
- package/src/mcp/map-errors.js +131 -0
- package/src/mcp/prompts.js +49 -0
- package/src/mcp/resources.js +244 -0
- package/src/mcp/tools.js +1212 -0
- package/src/preflight/design-shapes.js +185 -0
- package/src/preflight/index.js +90 -0
- package/src/preflight/manifest-writer.js +264 -0
- package/src/preflight/scanner.js +206 -0
- package/src/preflight/secrets.js +134 -0
- package/src/preflight/session.js +246 -0
- package/src/query/attestation.js +285 -0
- package/src/query/coverage.js +308 -0
- package/src/query/formatters/json.js +21 -0
- package/src/query/formatters/mermaid.js +209 -0
- package/src/query/formatters/table.js +203 -0
- package/src/query/impact.js +173 -0
- package/src/query/index.js +13 -0
- package/src/query/refuse-on-admissibility.js +73 -0
- package/src/query/trace.js +345 -0
- package/src/register-canary/fixture-loader.js +87 -0
- package/src/register-canary/index.js +10 -0
- package/src/register-canary/record-writer.js +132 -0
- package/src/register-canary/runner.js +156 -0
- package/src/req-baseline/gate.js +86 -0
- package/src/req-baseline/index.js +27 -0
- package/src/req-baseline/open-candidates.js +143 -0
- package/src/req-baseline/opt-out.js +195 -0
- package/src/req-baseline/sweep.js +230 -0
- package/src/req-detection/classifier.js +181 -0
- package/src/req-detection/index.js +9 -0
- package/src/req-detection/persist.js +55 -0
- package/src/review/index.js +325 -0
- package/src/review/mutation.js +117 -0
- package/src/review/ui-baseline-drift.js +138 -0
- package/src/ruleset/index.js +140 -0
- package/src/ruleset/ruleset.json +146 -0
- package/src/server/index.js +178 -0
- package/src/server/routes.js +110 -0
- package/src/server/sse.js +118 -0
- package/src/setup/agent-setup.js +362 -0
- package/src/setup/identity-seed.js +104 -0
- package/src/setup/knowledge-seed.js +123 -0
- package/src/setup/managed-block.js +193 -0
- package/src/setup/managed-gitignore.js +166 -0
- package/src/setup/managed-markers.js +49 -0
- package/src/ui-baseline/defaults.js +119 -0
- package/src/ui-baseline/index.js +25 -0
- package/src/ui-baseline/manifest-writer.js +282 -0
- package/src/ui-baseline/session.js +178 -0
- package/src/ui-detection/classifier.js +192 -0
- package/src/verify/chain/index.js +221 -0
- package/src/verify/cli/cleanup.js +61 -0
- package/src/verify/cli/help.js +56 -0
- package/src/verify/cli/mcp.js +98 -0
- package/src/verify/cli/provision.js +71 -0
- package/src/verify/cli/report.js +71 -0
- package/src/verify/cli/run.js +155 -0
- package/src/verify/engine/brief.js +87 -0
- package/src/verify/engine/index.js +177 -0
- package/src/verify/engine/launcher.js +307 -0
- package/src/verify/mcp/tools.js +107 -0
- package/src/verify/profile/index.js +146 -0
- package/src/verify/provision/index.js +256 -0
- package/src/verify/report/index.js +139 -0
- package/src/verify/report/renderer.js +118 -0
- package/src/verify/verdict/index.js +313 -0
- package/src/view/doc-renderers/adr.js +44 -0
- package/src/view/doc-renderers/build-sequence.js +40 -0
- package/src/view/doc-renderers/fbs.js +128 -0
- package/src/view/doc-renderers/helpers.js +159 -0
- package/src/view/doc-renderers/index.js +12 -0
- package/src/view/doc-renderers/prd.js +45 -0
- package/src/view/doc-renderers/req.js +43 -0
- package/src/view/doc-renderers/tac.js +38 -0
- package/src/view/doc-renderers/tad.js +74 -0
- package/src/view/doc-renderers/test-suite.js +45 -0
- package/src/view/doc-renderers/user-story.js +63 -0
- package/src/view/html-page.js +462 -0
- package/src/view/index.js +63 -0
- package/src/view/live-client.js +338 -0
- package/src/view/mermaid-diagram.js +178 -0
- package/src/view/style.css +735 -0
- package/src/view/tree-model.js +152 -0
- package/src/view/vendored/mermaid.min.js +2607 -0
- package/src/view-supervisor/index.js +26 -0
- package/src/view-supervisor/logs.js +32 -0
- package/src/view-supervisor/manifest-writer.js +178 -0
- package/src/view-supervisor/persist-until.js +85 -0
- package/src/view-supervisor/supervisor.js +276 -0
- package/src/watch/index.js +152 -0
|
@@ -0,0 +1,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
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Ship-without-verified acknowledgement writer
|
|
2
|
+
// (verification-integrity-cluster-spec section 5.2, review finding B-1).
|
|
3
|
+
//
|
|
4
|
+
// When `rcf finalise --ship-without-verified` acknowledges MOCK-ONLY-DECLARED
|
|
5
|
+
// or BLOCKED-BY-DECLARATION per-AC verdicts on a verify report and leaves
|
|
6
|
+
// the FBS at 'complete' (spec section 5.2), the ack lands on the manifest
|
|
7
|
+
// under the optional `shipWithoutVerified[]` array added in
|
|
8
|
+
// @stravica-ai/rcf-schemas 0.4.1. Stdout is not durable and not greppable
|
|
9
|
+
// at ship time; the manifest record is both. Schema shape:
|
|
10
|
+
//
|
|
11
|
+
// { id: swv-<fbsId>-<n>, fbsId, ackedAt,
|
|
12
|
+
// declaredAcs[{acId, verdict, reason?}], reportPath }
|
|
13
|
+
//
|
|
14
|
+
// Id allocation mirrors reviewAudit's `ra-<fbsId>-<n>` monotonic pattern
|
|
15
|
+
// (packages/rcf-lite/src/review/index.js:nextReviewAuditId) so a re-ack of
|
|
16
|
+
// the same FBS increments deterministically.
|
|
17
|
+
|
|
18
|
+
import { mkdir, rename, unlink, writeFile } from 'node:fs/promises';
|
|
19
|
+
import { dirname, join } from 'node:path';
|
|
20
|
+
|
|
21
|
+
import { rcfError } from '#core/errors';
|
|
22
|
+
import { validateDocument } from '#core/store';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {object} ShipWithoutVerifiedDeclaredAc
|
|
26
|
+
* @property {string} acId
|
|
27
|
+
* @property {'MOCK-ONLY-DECLARED'|'BLOCKED-BY-DECLARATION'} verdict
|
|
28
|
+
* @property {string} [reason]
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {object} ShipWithoutVerifiedRecord
|
|
33
|
+
* @property {string} id `swv-<fbsId>-<n>` monotonic per FBS.
|
|
34
|
+
* @property {string} fbsId
|
|
35
|
+
* @property {string} ackedAt ISO timestamp.
|
|
36
|
+
* @property {ShipWithoutVerifiedDeclaredAc[]} declaredAcs minItems 1.
|
|
37
|
+
* @property {string} reportPath
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Monotonic id allocator for the shipWithoutVerified array. Mirrors
|
|
42
|
+
* `nextReviewAuditId` (review/index.js).
|
|
43
|
+
*
|
|
44
|
+
* @param {object|null} manifest
|
|
45
|
+
* @param {string} fbsId
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
export function nextShipWithoutVerifiedId(manifest, fbsId) {
|
|
49
|
+
const prefix = `swv-${fbsId}-`;
|
|
50
|
+
const existing = Array.isArray(manifest?.shipWithoutVerified) ? manifest.shipWithoutVerified : [];
|
|
51
|
+
let maxN = 0;
|
|
52
|
+
for (const rec of existing) {
|
|
53
|
+
if (typeof rec?.id !== 'string' || !rec.id.startsWith(prefix)) continue;
|
|
54
|
+
const n = Number.parseInt(rec.id.slice(prefix.length), 10);
|
|
55
|
+
if (Number.isFinite(n) && n > maxN) maxN = n;
|
|
56
|
+
}
|
|
57
|
+
return `${prefix}${maxN + 1}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Compose the record. Timestamps at UTC ISO.
|
|
62
|
+
*
|
|
63
|
+
* @param {object} args
|
|
64
|
+
* @param {object|null} args.manifest
|
|
65
|
+
* @param {string} args.fbsId
|
|
66
|
+
* @param {ShipWithoutVerifiedDeclaredAc[]} args.declaredAcs
|
|
67
|
+
* @param {string} args.reportPath
|
|
68
|
+
* @param {Date} [args.now]
|
|
69
|
+
* @returns {ShipWithoutVerifiedRecord}
|
|
70
|
+
*/
|
|
71
|
+
export function composeShipWithoutVerifiedRecord({
|
|
72
|
+
manifest, fbsId, declaredAcs, reportPath, now = new Date(),
|
|
73
|
+
}) {
|
|
74
|
+
return {
|
|
75
|
+
id: nextShipWithoutVerifiedId(manifest, fbsId),
|
|
76
|
+
fbsId,
|
|
77
|
+
ackedAt: now.toISOString(),
|
|
78
|
+
declaredAcs: declaredAcs.map((a) => {
|
|
79
|
+
const out = { acId: a.acId, verdict: a.verdict };
|
|
80
|
+
if (typeof a.reason === 'string' && a.reason.length > 0) out.reason = a.reason;
|
|
81
|
+
return out;
|
|
82
|
+
}),
|
|
83
|
+
reportPath,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Persist a ship-without-verified acknowledgement onto the manifest.
|
|
89
|
+
* Reads `tree.manifest` for the current state, appends the composed
|
|
90
|
+
* record to `manifest.shipWithoutVerified[]`, validates the composed
|
|
91
|
+
* manifest against the 0.4.1 schema, and writes it atomically. On
|
|
92
|
+
* validation failure the on-disk manifest is untouched (mirrors the
|
|
93
|
+
* preflight manifest-writer discipline).
|
|
94
|
+
*
|
|
95
|
+
* @param {object} args
|
|
96
|
+
* @param {string} args.projectRoot
|
|
97
|
+
* @param {object} args.tree
|
|
98
|
+
* @param {ShipWithoutVerifiedRecord} args.record
|
|
99
|
+
* @returns {Promise<{ record: ShipWithoutVerifiedRecord } | import('#core/errors').RcfError>}
|
|
100
|
+
*/
|
|
101
|
+
export async function writeShipWithoutVerifiedRecord({ projectRoot, tree, record }) {
|
|
102
|
+
const manifest = tree.manifest ?? {};
|
|
103
|
+
const nextManifest = { ...manifest };
|
|
104
|
+
const existing = Array.isArray(nextManifest.shipWithoutVerified) ? nextManifest.shipWithoutVerified : [];
|
|
105
|
+
nextManifest.shipWithoutVerified = [...existing, record];
|
|
106
|
+
|
|
107
|
+
const relPath = 'rcf/manifest.json';
|
|
108
|
+
const validation = validateDocument({ doc: nextManifest, kind: 'manifest', filePath: relPath });
|
|
109
|
+
if (validation) return validation;
|
|
110
|
+
|
|
111
|
+
const absPath = join(projectRoot, 'rcf', 'manifest.json');
|
|
112
|
+
try {
|
|
113
|
+
await mkdir(dirname(absPath), { recursive: true });
|
|
114
|
+
const tmp = `${absPath}.tmp`;
|
|
115
|
+
await writeFile(tmp, `${JSON.stringify(nextManifest, null, 2)}\n`, 'utf8');
|
|
116
|
+
try {
|
|
117
|
+
await rename(tmp, absPath);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
try { await unlink(tmp); } catch { /* ignore */ }
|
|
120
|
+
throw err;
|
|
121
|
+
}
|
|
122
|
+
} catch (err) {
|
|
123
|
+
return rcfError({
|
|
124
|
+
kind: 'ioFailure',
|
|
125
|
+
message: `finalise: ship-without-verified manifest write failed: ${err.message}`,
|
|
126
|
+
filePath: relPath,
|
|
127
|
+
stack: err.stack,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return { record };
|
|
131
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Fresh-subprocess invocation of `rcf-verify run` (spec §8.2). This is the
|
|
2
|
+
// load-bearing independence guarantee: build-lite's finalise step launches
|
|
3
|
+
// verify as a SEPARATE OS PROCESS with the isolation env - it MUST NOT import
|
|
4
|
+
// verify's engine in-process, because the verifier agent must start from a
|
|
5
|
+
// cold session with zero build context (§9). The subprocess exit code is the
|
|
6
|
+
// gate; findings flow back via the --out report file, never stdout scraping.
|
|
7
|
+
//
|
|
8
|
+
// The isolation env (core's ISOLATION_RECIPE, §7.3) is layered onto the child
|
|
9
|
+
// env here so the recipe travels with the process boundary exactly as §8.2
|
|
10
|
+
// specifies ("spawn ... with the isolation env").
|
|
11
|
+
|
|
12
|
+
import { spawn } from 'node:child_process';
|
|
13
|
+
|
|
14
|
+
import { isolationEnv } from '#core/isolation';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Assemble the `rcf-verify run` argument vector from finalise options.
|
|
18
|
+
* Deterministic and pure so it is directly unit-testable; the exact shape is
|
|
19
|
+
* the §8.2 invocation contract.
|
|
20
|
+
*
|
|
21
|
+
* @param {object} opts
|
|
22
|
+
* @param {string} opts.repo - RCF chain source (the project root)
|
|
23
|
+
* @param {string} opts.url - the running deployed app
|
|
24
|
+
* @param {string} opts.profile - deployed | ci | local-dev
|
|
25
|
+
* @param {string} opts.out - report artifact path (always written by verify)
|
|
26
|
+
* @param {string} opts.severityGate - PASS|COSMETIC|DEGRADED|BROKEN
|
|
27
|
+
* @param {boolean} [opts.parityEnv]
|
|
28
|
+
* @param {string} [opts.provision] - provisioning FILE path
|
|
29
|
+
* @param {string} [opts.chain] - PRD/chain ref
|
|
30
|
+
* @param {string} [opts.persona]
|
|
31
|
+
* @returns {string[]}
|
|
32
|
+
*/
|
|
33
|
+
export function buildVerifyArgs(opts) {
|
|
34
|
+
const args = [
|
|
35
|
+
'run',
|
|
36
|
+
'--repo', opts.repo,
|
|
37
|
+
'--profile', opts.profile,
|
|
38
|
+
'--url', opts.url,
|
|
39
|
+
'--severity-gate', opts.severityGate,
|
|
40
|
+
'--out', opts.out,
|
|
41
|
+
];
|
|
42
|
+
if (opts.chain) args.push('--chain', opts.chain);
|
|
43
|
+
if (opts.parityEnv) args.push('--parity-env');
|
|
44
|
+
if (opts.provision) args.push('--provision', opts.provision);
|
|
45
|
+
if (opts.persona) args.push('--persona', opts.persona);
|
|
46
|
+
return args;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Spawn rcf-verify as a fresh subprocess and resolve with its exit code.
|
|
51
|
+
* stdio is inherited by default so the operator sees verify's live progress
|
|
52
|
+
* and its verdict line; the report artifact is read separately by the caller.
|
|
53
|
+
*
|
|
54
|
+
* The child env is the isolation recipe layered over the current env (§8.2 +
|
|
55
|
+
* §7.3): the recipe wins on conflict so a leaked parent value cannot re-enable
|
|
56
|
+
* auto-memory or non-essential traffic in the verifier session.
|
|
57
|
+
*
|
|
58
|
+
* @param {object} args
|
|
59
|
+
* @param {import('./detect.js').VerifyInvocation} args.invocation
|
|
60
|
+
* @param {string[]} args.verifyArgs - from buildVerifyArgs()
|
|
61
|
+
* @param {string} args.cwd
|
|
62
|
+
* @param {object} [deps]
|
|
63
|
+
* @param {typeof spawn} [deps.spawn]
|
|
64
|
+
* @param {NodeJS.ProcessEnv} [deps.baseEnv]
|
|
65
|
+
* @param {'inherit'|'pipe'|'ignore'} [deps.stdio]
|
|
66
|
+
* @returns {Promise<{ code: number, signal: NodeJS.Signals | null, env: NodeJS.ProcessEnv }>}
|
|
67
|
+
*/
|
|
68
|
+
export function spawnVerify({ invocation, verifyArgs, cwd }, deps = {}) {
|
|
69
|
+
const spawnFn = deps.spawn ?? spawn;
|
|
70
|
+
const childEnv = isolationEnv(deps.baseEnv ?? process.env);
|
|
71
|
+
const argv = [...invocation.prefixArgs, ...verifyArgs];
|
|
72
|
+
return new Promise((resolvePromise, reject) => {
|
|
73
|
+
const child = spawnFn(invocation.command, argv, {
|
|
74
|
+
cwd,
|
|
75
|
+
env: childEnv,
|
|
76
|
+
stdio: deps.stdio ?? 'inherit',
|
|
77
|
+
});
|
|
78
|
+
child.on('error', reject);
|
|
79
|
+
child.on('close', (code, signal) => {
|
|
80
|
+
// A signalled child (no numeric code) is a failure, not a pass.
|
|
81
|
+
resolvePromise({ code: code ?? (signal ? 1 : 0), signal, env: childEnv });
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Intake-fidelity classifier (spec §3.7, §6.4 phase 1).
|
|
2
|
+
//
|
|
3
|
+
// Deterministic word-count + signal-count classifier over the raw text of
|
|
4
|
+
// a supplied artefact. The bands are the ones the spec ratified:
|
|
5
|
+
// none → nothing supplied (caller passes an empty artefact list)
|
|
6
|
+
// napkin → very short: one paragraph or a sketchy note
|
|
7
|
+
// briefLight → short brief: names problem and capabilities, unstated NFRs
|
|
8
|
+
// briefStrong → full product brief with capabilities enumerated, NFRs named,
|
|
9
|
+
// out-of-scope called out. The watchpost brief lives here.
|
|
10
|
+
// prd → a full PRD or PRD-adjacent doc (front-matter, requirement
|
|
11
|
+
// sections, acceptance-shaped bullets)
|
|
12
|
+
// prdPlusTad → a PRD plus a TAD (architecture, ADR, TAC signals)
|
|
13
|
+
//
|
|
14
|
+
// The classifier is not a scoring engine; it uses cheap keyword signals
|
|
15
|
+
// (headings, capability lists, NFR keywords, out-of-scope markers,
|
|
16
|
+
// architecture markers) plus word count to assign a band. The operator
|
|
17
|
+
// always confirms the verdict at phase-1 handoff.
|
|
18
|
+
|
|
19
|
+
export const FIDELITY_LEVELS = Object.freeze([
|
|
20
|
+
'none',
|
|
21
|
+
'napkin',
|
|
22
|
+
'briefLight',
|
|
23
|
+
'briefStrong',
|
|
24
|
+
'prd',
|
|
25
|
+
'prdPlusTad',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
const CAP_HEADINGS = /^(?:\s*#+\s*)(?:capabilit(?:y|ies)|must-have|should-have|features?|user story|user stories|acceptance criteria)\b/im;
|
|
29
|
+
const NFR_HEADINGS = /^(?:\s*#+\s*)(?:non[-\s]?functional|nfr|constraints?|performance|reliability|security|scalability|budgets?)\b/im;
|
|
30
|
+
const OUT_OF_SCOPE = /^(?:\s*#+\s*)?(?:out[-\s]?of[-\s]?scope|non[-\s]?goals?|will not do|not (?:in|for) v[0-9])\b/im;
|
|
31
|
+
const REQ_MARKERS = /^\s*(?:REQ|US|AC)-?[0-9]+/im;
|
|
32
|
+
const PRD_MARKERS = /^(?:\s*#+\s*)?(?:product\s+requirements\s+document|prd|problem statement|users?\s*&?\s*personas?)\b/im;
|
|
33
|
+
const TAD_MARKERS = /^(?:\s*#+\s*)?(?:technical\s+architecture|architecture decision record|adr[-\s]?[0-9]+|tac[-\s]?[0-9]+|system architecture|component diagram|api boundaries)\b/im;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @typedef {'none'|'napkin'|'briefLight'|'briefStrong'|'prd'|'prdPlusTad'} FidelityBand
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @typedef {object} FidelitySignals
|
|
41
|
+
* @property {number} wordCount
|
|
42
|
+
* @property {boolean} capabilitiesEnumerated
|
|
43
|
+
* @property {boolean} nfrsNamed
|
|
44
|
+
* @property {boolean} outOfScopeListed
|
|
45
|
+
* @property {boolean} requirementMarkers
|
|
46
|
+
* @property {boolean} prdShape
|
|
47
|
+
* @property {boolean} tadShape
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Classify one artefact into a fidelity band. Accepts null/empty text
|
|
52
|
+
* ("nothing supplied").
|
|
53
|
+
*
|
|
54
|
+
* @param {string|null} text
|
|
55
|
+
* @param {object} [opts]
|
|
56
|
+
* @param {string|null} [opts.kindHint] operator hint: napkin|productBrief|prd|prdPlusTad|other
|
|
57
|
+
* @returns {{ fidelity: FidelityBand, signals: FidelitySignals }}
|
|
58
|
+
*/
|
|
59
|
+
export function classifyFidelity(text, opts = {}) {
|
|
60
|
+
const hint = opts.kindHint ?? null;
|
|
61
|
+
const t = typeof text === 'string' ? text : '';
|
|
62
|
+
const wordCount = t.trim().length === 0 ? 0 : t.trim().split(/\s+/).length;
|
|
63
|
+
const signals = {
|
|
64
|
+
wordCount,
|
|
65
|
+
capabilitiesEnumerated: CAP_HEADINGS.test(t),
|
|
66
|
+
nfrsNamed: NFR_HEADINGS.test(t),
|
|
67
|
+
outOfScopeListed: OUT_OF_SCOPE.test(t),
|
|
68
|
+
requirementMarkers: REQ_MARKERS.test(t),
|
|
69
|
+
prdShape: PRD_MARKERS.test(t),
|
|
70
|
+
tadShape: TAD_MARKERS.test(t),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Operator hint takes precedence when it is explicit and unambiguous.
|
|
74
|
+
if (hint === 'napkin') return { fidelity: 'napkin', signals };
|
|
75
|
+
if (hint === 'prd') return { fidelity: 'prd', signals };
|
|
76
|
+
if (hint === 'prdPlusTad') return { fidelity: 'prdPlusTad', signals };
|
|
77
|
+
|
|
78
|
+
if (wordCount === 0) return { fidelity: 'none', signals };
|
|
79
|
+
|
|
80
|
+
// Structural markers (PRD / TAD / requirement ids) win over the word
|
|
81
|
+
// count band: an operator supplying an already-structured short doc
|
|
82
|
+
// (a partial PRD, an ADR, a REQ-ids list) is at the prd/prdPlusTad
|
|
83
|
+
// band regardless of length.
|
|
84
|
+
if (signals.tadShape && (signals.prdShape || signals.requirementMarkers)) {
|
|
85
|
+
return { fidelity: 'prdPlusTad', signals };
|
|
86
|
+
}
|
|
87
|
+
if (signals.tadShape) return { fidelity: 'prdPlusTad', signals };
|
|
88
|
+
if (signals.requirementMarkers || (signals.prdShape && signals.capabilitiesEnumerated)) {
|
|
89
|
+
return { fidelity: 'prd', signals };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (wordCount < 100) return { fidelity: 'napkin', signals };
|
|
93
|
+
|
|
94
|
+
// Brief band: capabilities enumerated + NFRs named + out-of-scope
|
|
95
|
+
// listed → briefStrong; otherwise briefLight.
|
|
96
|
+
if (signals.capabilitiesEnumerated && signals.nfrsNamed && signals.outOfScopeListed) {
|
|
97
|
+
return { fidelity: 'briefStrong', signals };
|
|
98
|
+
}
|
|
99
|
+
if (signals.capabilitiesEnumerated || signals.nfrsNamed || signals.outOfScopeListed) {
|
|
100
|
+
return { fidelity: 'briefLight', signals };
|
|
101
|
+
}
|
|
102
|
+
// Default: prose of some length but no signals — treat as napkin
|
|
103
|
+
// upgraded to briefLight over 400 words.
|
|
104
|
+
return { fidelity: wordCount >= 400 ? 'briefLight' : 'napkin', signals };
|
|
105
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Public surface for the intake module (spec §6).
|
|
2
|
+
|
|
3
|
+
export { classifyFidelity, FIDELITY_LEVELS } from './fidelity.js';
|
|
4
|
+
export { scanArtefactForFindings } from './validate.js';
|
|
5
|
+
export { runIntakePhases } from './orchestrator.js';
|
|
6
|
+
export { writeIntakeRecord, composeIntakeRecord, nextIntakeId } from './manifest-writer.js';
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Intake record composer + writer (spec §3.4).
|
|
2
|
+
//
|
|
3
|
+
// Composes the `intakeClassification` block and appends/replaces it on
|
|
4
|
+
// the manifest, then writes the manifest atomically after schema
|
|
5
|
+
// validation. Uses the same `.tmp → rename` pattern as the preflight
|
|
6
|
+
// writer.
|
|
7
|
+
|
|
8
|
+
import { mkdir, rename, unlink, writeFile } from 'node:fs/promises';
|
|
9
|
+
import { dirname, join } from 'node:path';
|
|
10
|
+
|
|
11
|
+
import { rcfError } from '#core/errors';
|
|
12
|
+
import { validateDocument } from '#core/store';
|
|
13
|
+
|
|
14
|
+
const ID_RE = /^ic-\d{4}-\d{2}-\d{2}-(\d{3})$/;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Compose the next monotonic intake id for the current date.
|
|
18
|
+
*
|
|
19
|
+
* @param {object|null} manifest
|
|
20
|
+
* @param {Date} [now]
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
export function nextIntakeId(manifest, now = new Date()) {
|
|
24
|
+
const yyyy = now.getUTCFullYear().toString().padStart(4, '0');
|
|
25
|
+
const mm = (now.getUTCMonth() + 1).toString().padStart(2, '0');
|
|
26
|
+
const dd = now.getUTCDate().toString().padStart(2, '0');
|
|
27
|
+
const prefix = `ic-${yyyy}-${mm}-${dd}-`;
|
|
28
|
+
const existingRec = manifest?.intakeClassification ?? null;
|
|
29
|
+
let maxN = 0;
|
|
30
|
+
if (existingRec && typeof existingRec.id === 'string' && existingRec.id.startsWith(prefix)) {
|
|
31
|
+
const m = existingRec.id.match(ID_RE);
|
|
32
|
+
if (m) {
|
|
33
|
+
const n = Number.parseInt(m[1], 10);
|
|
34
|
+
if (Number.isFinite(n) && n > maxN) maxN = n;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return `${prefix}${(maxN + 1).toString().padStart(3, '0')}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Compose an intakeClassification block.
|
|
42
|
+
*
|
|
43
|
+
* @param {object} args
|
|
44
|
+
* @param {object|null} args.manifest
|
|
45
|
+
* @param {string} args.fidelity
|
|
46
|
+
* @param {object[]} args.artefacts
|
|
47
|
+
* @param {object[]} args.validationFindings
|
|
48
|
+
* @param {object} args.elicitationScope
|
|
49
|
+
* @param {Date} [args.now]
|
|
50
|
+
* @returns {object}
|
|
51
|
+
*/
|
|
52
|
+
export function composeIntakeRecord({ manifest, fidelity, artefacts, validationFindings, elicitationScope, now = new Date() }) {
|
|
53
|
+
const isoNow = now.toISOString();
|
|
54
|
+
const record = {
|
|
55
|
+
id: nextIntakeId(manifest, now),
|
|
56
|
+
createdAt: isoNow,
|
|
57
|
+
fidelity,
|
|
58
|
+
artefacts,
|
|
59
|
+
validationFindings,
|
|
60
|
+
elicitationScope,
|
|
61
|
+
operatorAckAt: isoNow,
|
|
62
|
+
};
|
|
63
|
+
return record;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Write the composed intakeClassification onto the manifest.
|
|
68
|
+
*
|
|
69
|
+
* @param {object} args
|
|
70
|
+
* @param {string} args.projectRoot
|
|
71
|
+
* @param {import('#core/store/walker.js').TreeModel} args.tree
|
|
72
|
+
* @param {object} args.record
|
|
73
|
+
* @returns {Promise<{ record: object } | import('#core/errors').RcfError>}
|
|
74
|
+
*/
|
|
75
|
+
export async function writeIntakeRecord({ projectRoot, tree, record }) {
|
|
76
|
+
const manifest = tree?.manifest ?? {};
|
|
77
|
+
const nextManifest = { ...manifest, intakeClassification: record };
|
|
78
|
+
const validation = validateDocument({ doc: nextManifest, kind: 'manifest', filePath: 'rcf/manifest.json' });
|
|
79
|
+
if (validation) return validation;
|
|
80
|
+
const abs = join(projectRoot, 'rcf', 'manifest.json');
|
|
81
|
+
try {
|
|
82
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
83
|
+
const tmp = `${abs}.tmp`;
|
|
84
|
+
await writeFile(tmp, `${JSON.stringify(nextManifest, null, 2)}\n`, 'utf8');
|
|
85
|
+
try {
|
|
86
|
+
await rename(tmp, abs);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
try { await unlink(tmp); } catch { /* ignore */ }
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
} catch (err) {
|
|
92
|
+
return rcfError({
|
|
93
|
+
kind: 'ioFailure',
|
|
94
|
+
message: `intake: manifest write failed: ${err.message}`,
|
|
95
|
+
filePath: 'rcf/manifest.json',
|
|
96
|
+
stack: err.stack,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return { record };
|
|
100
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Intake orchestrator (spec §6.4 phases 1-3 combined).
|
|
2
|
+
//
|
|
3
|
+
// Reads the supplied artefacts, runs the fidelity classifier, applies
|
|
4
|
+
// the validation scans, folds in any operator-supplied non-interactive
|
|
5
|
+
// input file, and composes an intakeClassification record ready for the
|
|
6
|
+
// manifest writer. Never talks to a subagent in v1 (the intake-worker
|
|
7
|
+
// dispatch in spec §6.6 is out of scope for this build ship — the
|
|
8
|
+
// deterministic scans plus operator-declared "otherDeclared" findings
|
|
9
|
+
// via --input cover phase 2 without the reader dispatch).
|
|
10
|
+
|
|
11
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
12
|
+
import { basename, isAbsolute, join } from 'node:path';
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
|
|
15
|
+
import { rcfError } from '#core/errors';
|
|
16
|
+
|
|
17
|
+
import { classifyFidelity } from './fidelity.js';
|
|
18
|
+
import { scanArtefactForFindings } from './validate.js';
|
|
19
|
+
import { composeIntakeRecord } from './manifest-writer.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {'napkin'|'productBrief'|'prd'|'tad'|'other'} ArtefactKind
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const KIND_MAP = new Map([
|
|
26
|
+
['napkin', 'napkin'],
|
|
27
|
+
['productbrief', 'productBrief'],
|
|
28
|
+
['prd', 'prd'],
|
|
29
|
+
['prdplustad', 'prd'],
|
|
30
|
+
['tad', 'tad'],
|
|
31
|
+
['other', 'other'],
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Read + classify + validate every artefact and compose a full
|
|
36
|
+
* intakeClassification record. Returns an RcfError on IO failure.
|
|
37
|
+
*
|
|
38
|
+
* @param {object} args
|
|
39
|
+
* @param {string} args.projectRoot
|
|
40
|
+
* @param {string[]} args.artefactPaths
|
|
41
|
+
* @param {string|null} [args.kindHint]
|
|
42
|
+
* @param {object|null} [args.input] optional non-interactive input file
|
|
43
|
+
* @param {Date} [args.now]
|
|
44
|
+
* @returns {Promise<{ record: object } | import('#core/errors').RcfError>}
|
|
45
|
+
*/
|
|
46
|
+
export async function runIntakePhases({ projectRoot, artefactPaths, kindHint = null, input = null, now = new Date() }) {
|
|
47
|
+
const artefactMeta = [];
|
|
48
|
+
const allFindings = [];
|
|
49
|
+
let text = '';
|
|
50
|
+
const paths = [...new Set(artefactPaths)];
|
|
51
|
+
for (const raw of paths) {
|
|
52
|
+
const abs = isAbsolute(raw) ? raw : join(projectRoot, raw);
|
|
53
|
+
let body = '';
|
|
54
|
+
let hash = null;
|
|
55
|
+
let wordCount = 0;
|
|
56
|
+
try {
|
|
57
|
+
body = await readFile(abs, 'utf8');
|
|
58
|
+
const info = await stat(abs);
|
|
59
|
+
void info;
|
|
60
|
+
hash = `sha256:${createHash('sha256').update(body).digest('hex')}`;
|
|
61
|
+
wordCount = body.trim().length === 0 ? 0 : body.trim().split(/\s+/).length;
|
|
62
|
+
} catch (err) {
|
|
63
|
+
return rcfError({
|
|
64
|
+
kind: 'usage',
|
|
65
|
+
message: `intake: cannot read artefact ${raw}: ${err.message}`,
|
|
66
|
+
filePath: raw,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
text += `\n${body}`;
|
|
70
|
+
const kind = resolveKind({ path: raw, kindHint });
|
|
71
|
+
artefactMeta.push({
|
|
72
|
+
path: raw,
|
|
73
|
+
kind,
|
|
74
|
+
wordCount,
|
|
75
|
+
hash,
|
|
76
|
+
operatorSourced: true,
|
|
77
|
+
});
|
|
78
|
+
for (const f of scanArtefactForFindings(body)) allFindings.push(f);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const { fidelity, signals } = classifyFidelity(text, { kindHint });
|
|
82
|
+
|
|
83
|
+
// Fold non-interactive input: extra findings, operator responses,
|
|
84
|
+
// and elicitationScope hints all land here.
|
|
85
|
+
const inputFindings = Array.isArray(input?.validationFindings) ? input.validationFindings : [];
|
|
86
|
+
const inputResponses = new Map();
|
|
87
|
+
for (const f of inputFindings) {
|
|
88
|
+
if (typeof f?.detail === 'string' && typeof f?.operatorResponse === 'string') {
|
|
89
|
+
inputResponses.set(f.detail, f.operatorResponse);
|
|
90
|
+
}
|
|
91
|
+
if (f?.kind === 'otherDeclared') allFindings.push(f);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const isoNow = now.toISOString();
|
|
95
|
+
const findingsWithTimestamps = allFindings.map((f) => {
|
|
96
|
+
const entry = { kind: f.kind, detail: f.detail, raisedAt: isoNow };
|
|
97
|
+
if (f.kindDescription) entry.kindDescription = f.kindDescription;
|
|
98
|
+
const response = inputResponses.get(f.detail) ?? input?.operatorResponse;
|
|
99
|
+
if (typeof response === 'string' && response.length > 0) {
|
|
100
|
+
entry.operatorResponse = response;
|
|
101
|
+
entry.resolvedAt = isoNow;
|
|
102
|
+
}
|
|
103
|
+
return entry;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const elicitationScope = input?.elicitationScope ?? {
|
|
107
|
+
prdDrafted: fidelity === 'prd' || fidelity === 'prdPlusTad' ? 'supplied' : 'drafted',
|
|
108
|
+
reqDraftedFromArtefact: [],
|
|
109
|
+
reqRequiringElicitation: [],
|
|
110
|
+
acsFromArtefact: 0,
|
|
111
|
+
acsFromElicitation: 'all',
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const record = composeIntakeRecord({
|
|
115
|
+
manifest: null,
|
|
116
|
+
fidelity: input?.fidelity ?? fidelity,
|
|
117
|
+
artefacts: artefactMeta,
|
|
118
|
+
validationFindings: findingsWithTimestamps,
|
|
119
|
+
elicitationScope,
|
|
120
|
+
now,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
void signals; // exposed on classifier return for future observability
|
|
124
|
+
|
|
125
|
+
return { record };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function resolveKind({ path, kindHint }) {
|
|
129
|
+
const hint = KIND_MAP.get(String(kindHint ?? '').toLowerCase());
|
|
130
|
+
if (hint) return hint;
|
|
131
|
+
const base = basename(path).toLowerCase();
|
|
132
|
+
if (base.includes('napkin')) return 'napkin';
|
|
133
|
+
if (base.includes('prd') && base.includes('tad')) return 'prd';
|
|
134
|
+
if (base.includes('prd')) return 'prd';
|
|
135
|
+
if (base.includes('tad')) return 'tad';
|
|
136
|
+
if (base.includes('brief')) return 'productBrief';
|
|
137
|
+
return 'other';
|
|
138
|
+
}
|