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,285 @@
|
|
|
1
|
+
// Attestation × Profile enforcement matrix
|
|
2
|
+
// (verification-integrity-cluster-spec §3.5, §5.2).
|
|
3
|
+
//
|
|
4
|
+
// Pure data + pure classifiers. Consumers:
|
|
5
|
+
// - `coverage --strict` gates on the matrix at build time (§5.1, §5.2);
|
|
6
|
+
// - the Review-stage test-theatre audit re-runs the matrix at
|
|
7
|
+
// Stage 3 as one of its finding categories (§5.5, `attestationDrift`);
|
|
8
|
+
// - verify's deployed-verdict gate reads the same matrix inputs
|
|
9
|
+
// (aggregated attestations per AC) at chain-read time (§9.2 seam).
|
|
10
|
+
//
|
|
11
|
+
// The matrix returns a verdict + a per-cell reason string that
|
|
12
|
+
// downstream consumers can render in table / JSON / mermaid without
|
|
13
|
+
// re-deriving the semantics.
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {'live'|'sandboxed'|'mocked'|'declaredMockOnly'|'notShipped'} AttestationMode
|
|
17
|
+
* @typedef {'mock'|'stub'|'fixture'|'live'|'mixed'} ProvenanceProfile
|
|
18
|
+
* @typedef {'pass'|'passWithWarn'|'refuse'} MatrixVerdict
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The matrix cell.
|
|
23
|
+
* @typedef {object} MatrixCell
|
|
24
|
+
* @property {MatrixVerdict} verdict
|
|
25
|
+
* @property {string} reason
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Classify one (attestationMode, profile) pair.
|
|
30
|
+
*
|
|
31
|
+
* @param {AttestationMode} attestation
|
|
32
|
+
* @param {ProvenanceProfile} profile
|
|
33
|
+
* @returns {MatrixCell}
|
|
34
|
+
*/
|
|
35
|
+
export function classifyAttestationProfile(attestation, profile) {
|
|
36
|
+
// `mixed` in a TC is an anti-pattern flag by design: it should be
|
|
37
|
+
// expanded into finer TCs. coverage --strict refuses it here on the
|
|
38
|
+
// build side (§3.5 note on mixed) regardless of the covering AC's
|
|
39
|
+
// attestation.
|
|
40
|
+
if (profile === 'mixed') {
|
|
41
|
+
return {
|
|
42
|
+
verdict: 'refuse',
|
|
43
|
+
reason: 'TC profile is `mixed`; expand into finer TCs before coverage --strict will accept it.',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (attestation === 'notShipped') {
|
|
47
|
+
return { verdict: 'pass', reason: 'AC does not gate ship; attestation notShipped.' };
|
|
48
|
+
}
|
|
49
|
+
if (attestation === 'mocked') {
|
|
50
|
+
return { verdict: 'pass', reason: 'AC attests mocked; any TC profile passes.' };
|
|
51
|
+
}
|
|
52
|
+
if (attestation === 'declaredMockOnly') {
|
|
53
|
+
if (profile === 'live') {
|
|
54
|
+
return {
|
|
55
|
+
verdict: 'passWithWarn',
|
|
56
|
+
reason: 'AC attests declaredMockOnly but TC went live; recorded as attestationDrift.',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return { verdict: 'pass', reason: 'AC attests declaredMockOnly; mock / stub / fixture is expected.' };
|
|
60
|
+
}
|
|
61
|
+
if (attestation === 'live') {
|
|
62
|
+
if (profile === 'live') return { verdict: 'pass', reason: 'live × live.' };
|
|
63
|
+
return {
|
|
64
|
+
verdict: 'refuse',
|
|
65
|
+
reason: `AC attests live; TC provenance is \`${profile}\`. Elevate the TC to live OR downgrade the AC attestation OR add a live-profile TC alongside the mock one.`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (attestation === 'sandboxed') {
|
|
69
|
+
if (profile === 'live' || profile === 'stub') return { verdict: 'pass', reason: 'sandboxed × live/stub (points at sandbox).' };
|
|
70
|
+
return {
|
|
71
|
+
verdict: 'refuse',
|
|
72
|
+
reason: `AC attests sandboxed; TC provenance is \`${profile}\`. Elevate the TC to live/stub against the sandbox OR downgrade the AC attestation.`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return { verdict: 'refuse', reason: `Unknown attestation mode \`${attestation}\`.` };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Aggregate the `dependsOnServices` entries across every FBS that binds
|
|
80
|
+
* each AC. Returns a map `acId -> Array<{ serviceId, attestationMode,
|
|
81
|
+
* fbsId }>`. This is the shape verify's chain reader will surface via
|
|
82
|
+
* core's walker (spec §5.2 / §9.2); build re-computes it locally for
|
|
83
|
+
* `coverage --strict`.
|
|
84
|
+
*
|
|
85
|
+
* @param {import('#core/store/walker.js').TreeModel} tree
|
|
86
|
+
* @returns {Map<string, Array<{ serviceId: string, attestationMode: AttestationMode, fbsId: string }>>}
|
|
87
|
+
*/
|
|
88
|
+
export function aggregateAttestationsByAc(tree) {
|
|
89
|
+
/** @type {Map<string, Array<{ serviceId: string, attestationMode: AttestationMode, fbsId: string }>>} */
|
|
90
|
+
const out = new Map();
|
|
91
|
+
for (const fbs of tree.fbsItems ?? []) {
|
|
92
|
+
const services = Array.isArray(fbs.dependsOnServices) ? fbs.dependsOnServices : [];
|
|
93
|
+
for (const svc of services) {
|
|
94
|
+
if (!svc?.id || !svc?.attestationMode || !Array.isArray(svc.acIds)) continue;
|
|
95
|
+
for (const acId of svc.acIds) {
|
|
96
|
+
if (!out.has(acId)) out.set(acId, []);
|
|
97
|
+
out.get(acId).push({
|
|
98
|
+
serviceId: svc.id,
|
|
99
|
+
attestationMode: svc.attestationMode,
|
|
100
|
+
fbsId: fbs.fbsId,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Given a walker tree, produce the pre-flight service coverage: for
|
|
110
|
+
* every service the preFlightConfig[] records name (except notShipped)
|
|
111
|
+
* whose entry carries a non-empty `affectedFbsIds`, cross-check that
|
|
112
|
+
* every affected FBS carries a matching `dependsOnServices[]` entry.
|
|
113
|
+
* The `affectedFbsIds` back-reference is the load-bearing link between
|
|
114
|
+
* a preflight ruling and the FBSes that inherit it (spec §3.3); when
|
|
115
|
+
* empty we skip the check for that service rather than raise on every
|
|
116
|
+
* FBS — v1 stays honest about what it can and cannot infer.
|
|
117
|
+
*
|
|
118
|
+
* Used by `coverage --strict` to detect "attestation missing" (spec
|
|
119
|
+
* §5.2) — an FBS listed in `affectedFbsIds` for a pre-flight-declared
|
|
120
|
+
* service must itself carry the `dependsOnServices` entry naming that
|
|
121
|
+
* service.
|
|
122
|
+
*
|
|
123
|
+
* @param {import('#core/store/walker.js').TreeModel} tree
|
|
124
|
+
* @returns {Array<{ serviceId: string, fbsId: string, attestationMode: AttestationMode }>}
|
|
125
|
+
*/
|
|
126
|
+
export function findAttestationMissing(tree) {
|
|
127
|
+
/** @type {Array<{ serviceId: string, fbsId: string, attestationMode: AttestationMode }>} */
|
|
128
|
+
const missing = [];
|
|
129
|
+
const preflight = Array.isArray(tree.manifest?.preFlightConfig) ? tree.manifest.preFlightConfig : [];
|
|
130
|
+
if (preflight.length === 0) return missing;
|
|
131
|
+
|
|
132
|
+
for (const pfc of preflight) {
|
|
133
|
+
for (const s of pfc.servicesInScope ?? []) {
|
|
134
|
+
if (!s?.id) continue;
|
|
135
|
+
if (s.attestationMode === 'notShipped') continue;
|
|
136
|
+
const affected = Array.isArray(s.affectedFbsIds) ? s.affectedFbsIds : [];
|
|
137
|
+
if (affected.length === 0) continue;
|
|
138
|
+
for (const fbsId of affected) {
|
|
139
|
+
const fbs = tree.byId.get(fbsId);
|
|
140
|
+
if (!fbs || tree.kindById.get(fbsId) !== 'fbs') continue;
|
|
141
|
+
const hasEntry = (fbs.dependsOnServices ?? []).some((e) => e.id === s.id);
|
|
142
|
+
if (!hasEntry) {
|
|
143
|
+
missing.push({ serviceId: s.id, fbsId, attestationMode: s.attestationMode });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return missing;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Given a walker tree, list every preFlightConfig service whose
|
|
153
|
+
* `affectedFbsIds` back-reference is empty. The `attestation-missing`
|
|
154
|
+
* detector (findAttestationMissing) skips those services on purpose:
|
|
155
|
+
* without the back-reference there is nothing to cross-check against,
|
|
156
|
+
* but the honest posture is to surface the skip so the operator can
|
|
157
|
+
* back-fill the field (review N-3 non-blocking finding).
|
|
158
|
+
*
|
|
159
|
+
* notShipped services are excluded: they never gate ship and never
|
|
160
|
+
* contribute an FBS-level attestation, so an empty back-reference on
|
|
161
|
+
* them is fine.
|
|
162
|
+
*
|
|
163
|
+
* @param {import('#core/store/walker.js').TreeModel} tree
|
|
164
|
+
* @returns {Array<{ serviceId: string, preFlightConfigId: string, attestationMode: AttestationMode }>}
|
|
165
|
+
*/
|
|
166
|
+
export function findServicesWithEmptyAffectedFbsIds(tree) {
|
|
167
|
+
/** @type {Array<{ serviceId: string, preFlightConfigId: string, attestationMode: AttestationMode }>} */
|
|
168
|
+
const out = [];
|
|
169
|
+
const preflight = Array.isArray(tree.manifest?.preFlightConfig) ? tree.manifest.preFlightConfig : [];
|
|
170
|
+
for (const pfc of preflight) {
|
|
171
|
+
for (const s of pfc.servicesInScope ?? []) {
|
|
172
|
+
if (!s?.id) continue;
|
|
173
|
+
if (s.attestationMode === 'notShipped') continue;
|
|
174
|
+
const affected = Array.isArray(s.affectedFbsIds) ? s.affectedFbsIds : [];
|
|
175
|
+
if (affected.length === 0) {
|
|
176
|
+
out.push({ serviceId: s.id, preFlightConfigId: pfc.id, attestationMode: s.attestationMode });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Given a walker tree and a target FBS id, return the FBS's
|
|
185
|
+
* `dependsOnServices[]` entries whose service `id` is not named in
|
|
186
|
+
* ANY `preFlightConfig[].servicesInScope[].id`. Powers the `rcf build
|
|
187
|
+
* --next` preflight-warning that the elicitation and build-cycle
|
|
188
|
+
* playbooks already advertise (spec section 4.2, review N-1
|
|
189
|
+
* non-blocking finding); the warning nudges the operator to run `rcf
|
|
190
|
+
* preflight` before the coverage-strict gate refuses at Stage 4.
|
|
191
|
+
*
|
|
192
|
+
* Returns an empty array when the FBS has no dependsOnServices,
|
|
193
|
+
* when the FBS is not found, or when every service is preflight-backed.
|
|
194
|
+
*
|
|
195
|
+
* @param {import('#core/store/walker.js').TreeModel} tree
|
|
196
|
+
* @param {string} fbsId
|
|
197
|
+
* @returns {Array<{ serviceId: string, displayName?: string, attestationMode?: AttestationMode }>}
|
|
198
|
+
*/
|
|
199
|
+
export function scanUnbackedServices(tree, fbsId) {
|
|
200
|
+
const fbs = tree?.byId?.get?.(fbsId);
|
|
201
|
+
if (!fbs || tree?.kindById?.get?.(fbsId) !== 'fbs') return [];
|
|
202
|
+
const services = Array.isArray(fbs.dependsOnServices) ? fbs.dependsOnServices : [];
|
|
203
|
+
if (services.length === 0) return [];
|
|
204
|
+
const preflight = Array.isArray(tree.manifest?.preFlightConfig) ? tree.manifest.preFlightConfig : [];
|
|
205
|
+
const covered = new Set();
|
|
206
|
+
for (const pfc of preflight) {
|
|
207
|
+
for (const s of pfc.servicesInScope ?? []) {
|
|
208
|
+
if (s?.id) covered.add(s.id);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/** @type {Array<{ serviceId: string, displayName?: string, attestationMode?: AttestationMode }>} */
|
|
212
|
+
const unbacked = [];
|
|
213
|
+
for (const s of services) {
|
|
214
|
+
if (!s?.id || covered.has(s.id)) continue;
|
|
215
|
+
const entry = { serviceId: s.id };
|
|
216
|
+
if (typeof s.displayName === 'string') entry.displayName = s.displayName;
|
|
217
|
+
if (typeof s.attestationMode === 'string') entry.attestationMode = s.attestationMode;
|
|
218
|
+
unbacked.push(entry);
|
|
219
|
+
}
|
|
220
|
+
return unbacked;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Given a walker tree, list every TC that lacks `runtimeProvenance`
|
|
225
|
+
* where the covering AC binds a dependsOnServices entry. This is the
|
|
226
|
+
* §5.1 gate: provenance is authored, not remembered, on any AC that
|
|
227
|
+
* binds a service — coverage --strict refuses without it.
|
|
228
|
+
*
|
|
229
|
+
* @param {import('#core/store/walker.js').TreeModel} tree
|
|
230
|
+
* @returns {Array<{ tsId: string, tcId: string, acId: string }>}
|
|
231
|
+
*/
|
|
232
|
+
export function findProvenanceMissing(tree) {
|
|
233
|
+
const acsWithServices = new Set(aggregateAttestationsByAc(tree).keys());
|
|
234
|
+
/** @type {Array<{ tsId: string, tcId: string, acId: string }>} */
|
|
235
|
+
const missing = [];
|
|
236
|
+
for (const ts of tree.testSuites ?? []) {
|
|
237
|
+
for (const tc of ts.testCases ?? []) {
|
|
238
|
+
if (!tc?.acId) continue;
|
|
239
|
+
if (!acsWithServices.has(tc.acId)) continue;
|
|
240
|
+
if (!tc.runtimeProvenance || typeof tc.runtimeProvenance.profile !== 'string') {
|
|
241
|
+
missing.push({ tsId: ts.id, tcId: tc.id, acId: tc.acId });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return missing;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Given a walker tree, find every TC whose runtimeProvenance profile
|
|
250
|
+
* contradicts the covering AC's aggregated attestation, per the §3.5
|
|
251
|
+
* matrix. Returns one entry per offending TC × service pair, so a TC
|
|
252
|
+
* covering an AC governed by two services can raise twice (both must
|
|
253
|
+
* be resolved before ship).
|
|
254
|
+
*
|
|
255
|
+
* @param {import('#core/store/walker.js').TreeModel} tree
|
|
256
|
+
* @returns {Array<{ tsId: string, tcId: string, acId: string, serviceId: string, attestationMode: AttestationMode, profile: ProvenanceProfile, verdict: MatrixVerdict, reason: string }>}
|
|
257
|
+
*/
|
|
258
|
+
export function findAttestationDrift(tree) {
|
|
259
|
+
const attByAc = aggregateAttestationsByAc(tree);
|
|
260
|
+
/** @type {Array<{ tsId: string, tcId: string, acId: string, serviceId: string, attestationMode: AttestationMode, profile: ProvenanceProfile, verdict: MatrixVerdict, reason: string }>} */
|
|
261
|
+
const drift = [];
|
|
262
|
+
for (const ts of tree.testSuites ?? []) {
|
|
263
|
+
for (const tc of ts.testCases ?? []) {
|
|
264
|
+
const profile = tc?.runtimeProvenance?.profile;
|
|
265
|
+
if (typeof profile !== 'string') continue;
|
|
266
|
+
const bindings = attByAc.get(tc.acId) ?? [];
|
|
267
|
+
for (const { serviceId, attestationMode } of bindings) {
|
|
268
|
+
const cell = classifyAttestationProfile(attestationMode, profile);
|
|
269
|
+
if (cell.verdict === 'refuse' || cell.verdict === 'passWithWarn') {
|
|
270
|
+
drift.push({
|
|
271
|
+
tsId: ts.id,
|
|
272
|
+
tcId: tc.id,
|
|
273
|
+
acId: tc.acId,
|
|
274
|
+
serviceId,
|
|
275
|
+
attestationMode,
|
|
276
|
+
profile,
|
|
277
|
+
verdict: cell.verdict,
|
|
278
|
+
reason: cell.reason,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return drift;
|
|
285
|
+
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
// Pure coverage compute. Given a walker-produced TreeModel, walk the
|
|
2
|
+
// REQ chain (PRD -> REQ -> US -> AC -> TS -> TC) and report whether at
|
|
3
|
+
// least one full chain reaches a TC leaf per REQ.
|
|
4
|
+
//
|
|
5
|
+
// w-2026-07-28-005: the TC leaf test is RESOLUTION-GATED. A TC counts as
|
|
6
|
+
// covering its AC only when its `testPointer` resolves to a real test in
|
|
7
|
+
// the working tree (see core's tp-resolve.js). Callers resolve pointers
|
|
8
|
+
// first (async, fs-touching) and pass the result in via
|
|
9
|
+
// `opts.testPointers`; this function stays pure. When no resolution map
|
|
10
|
+
// is supplied the compute FAILS CLOSED - every TC is treated as
|
|
11
|
+
// unresolved - because an unverified pointer must never count as
|
|
12
|
+
// coverage. A TC row whose pointer does not resolve is reported as its
|
|
13
|
+
// own class (`covered-unresolved`), never silently folded into either
|
|
14
|
+
// covered or uncovered.
|
|
15
|
+
//
|
|
16
|
+
// Phase 5 §D2: shallow-any default (any AC covered by any TC = REQ
|
|
17
|
+
// covered); `--strict` flips to per-AC-strict (every AC has TC coverage).
|
|
18
|
+
// Phase-boundary note (§1.4, §D2): this is a MECHANICAL / DETERMINISTIC
|
|
19
|
+
// structural check. It does NOT answer "does the AC set adequately
|
|
20
|
+
// capture the REQ's intent?" - that non-deterministic question belongs
|
|
21
|
+
// to a later prompting + MCP resources phase (Phase 7+).
|
|
22
|
+
//
|
|
23
|
+
// Phase 5 §D10 scoping: no positional -> tree-wide; positional PRD id
|
|
24
|
+
// -> scope to REQs whose prdId matches; positional REQ id -> scope to
|
|
25
|
+
// that REQ; positional US id -> scope to the REQ that owns that US.
|
|
26
|
+
// Below-AC / cross-chain positionals (AC / TS / TC / FBS / TAC / ADR /
|
|
27
|
+
// BS / TAD) are refused at the handler layer (exit 2).
|
|
28
|
+
|
|
29
|
+
import { testCaseKey } from '#core/store';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {import('#core/store/walker.js').TreeModel} TreeModel
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @typedef {object} AcCoverage
|
|
37
|
+
* @property {string} id
|
|
38
|
+
* @property {boolean} covered - at least one TC whose pointer RESOLVES
|
|
39
|
+
* @property {string[]} testCases - all TC ids that cross-reference this AC
|
|
40
|
+
* @property {string[]} unresolvedTestCases - the subset whose pointer does not resolve
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @typedef {object} ReqCoverage
|
|
45
|
+
* @property {string} id
|
|
46
|
+
* @property {boolean} covered - true only for coverageClass 'covered'
|
|
47
|
+
* @property {'covered'|'covered-unresolved'|'uncovered'} coverageClass -
|
|
48
|
+
* 'covered-unresolved' = the TC rows exist to claim coverage, but the
|
|
49
|
+
* claim rests on at least one pointer that does not resolve
|
|
50
|
+
* @property {AcCoverage[]} acs
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @typedef {object} CoverageResult
|
|
55
|
+
* @property {boolean} ok - true when every requirement in scope is covered
|
|
56
|
+
* by RESOLVING test cases (covered-unresolved is a gap, not a pass)
|
|
57
|
+
* @property {boolean} strict - the --strict flag echoed in the envelope
|
|
58
|
+
* @property {{requirements: number, covered: number, coveredUnresolved: number, uncovered: number}} totals
|
|
59
|
+
* @property {ReqCoverage[]} requirements
|
|
60
|
+
* @property {Array<{tsId: string, tcId: string, testPointer: string|null, reason: string}>} unresolvedTestPointers -
|
|
61
|
+
* every in-scope TC whose pointer failed to resolve, with why
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Compute coverage over the tree. Returns a stable CoverageResult
|
|
66
|
+
* envelope suitable for the JSON / table / mermaid emitters.
|
|
67
|
+
*
|
|
68
|
+
* Phase 10 (X2 CodeNode bridge, D11): `opts.withCode` layers an
|
|
69
|
+
* INFORMATIONAL code axis onto every AC - `codeClass` is one of
|
|
70
|
+
* `implemented-and-covered` / `implemented-uncovered` / `unimplemented`
|
|
71
|
+
* (deterministic edge counting via `tree.cnByAcId` / `tree.tcsByAcId`,
|
|
72
|
+
* with the covered leg gated on pointer resolution per w-2026-07-28-005),
|
|
73
|
+
* plus a tree-wide `codeNodeOrphans` list (CN docs with empty
|
|
74
|
+
* `implementsAcIds`). None of this blocks - `ok` / exit code are
|
|
75
|
+
* unaffected by the code axis (spec D11: the mark-complete gate, not
|
|
76
|
+
* coverage, is where CN completeness is enforced).
|
|
77
|
+
*
|
|
78
|
+
* @param {TreeModel} tree
|
|
79
|
+
* @param {object} [opts]
|
|
80
|
+
* @param {boolean} [opts.strict] - per-AC-strict mode
|
|
81
|
+
* @param {string} [opts.scopeId] - optional PRD / REQ / US id to scope
|
|
82
|
+
* @param {boolean} [opts.withCode] - layer the code-axis classification
|
|
83
|
+
* @param {Map<string, import('#core/store/tp-resolve.js').TestPointerResolution>} [opts.testPointers] -
|
|
84
|
+
* per-TC pointer resolution keyed by `testCaseKey(tsId, tcId)` (from
|
|
85
|
+
* core's `resolveTestPointers`). Absent entries / an absent map fail
|
|
86
|
+
* closed: the TC counts as unresolved.
|
|
87
|
+
* @returns {CoverageResult}
|
|
88
|
+
*/
|
|
89
|
+
export function computeCoverage(tree, opts = {}) {
|
|
90
|
+
const strict = Boolean(opts.strict);
|
|
91
|
+
const scopeId = opts.scopeId ?? null;
|
|
92
|
+
const withCode = Boolean(opts.withCode);
|
|
93
|
+
const testPointers = opts.testPointers ?? new Map();
|
|
94
|
+
const reqs = selectRequirements(tree, scopeId);
|
|
95
|
+
|
|
96
|
+
/** @type {ReqCoverage[]} */
|
|
97
|
+
const requirements = [];
|
|
98
|
+
/** @type {Map<string, {tsId: string, tcId: string, testPointer: string|null, reason: string}>} */
|
|
99
|
+
const unresolvedByKey = new Map();
|
|
100
|
+
let covered = 0;
|
|
101
|
+
let coveredUnresolved = 0;
|
|
102
|
+
let uncovered = 0;
|
|
103
|
+
|
|
104
|
+
for (const req of reqs) {
|
|
105
|
+
const acs = collectAcs(tree, req.reqId, { withCode, testPointers, unresolvedByKey });
|
|
106
|
+
// The real verdict counts only resolving TCs; the structural verdict
|
|
107
|
+
// is the pre-w-2026-07-28-005 rule (a TC row exists). A REQ that
|
|
108
|
+
// passes structurally but not really is 'covered-unresolved' - the
|
|
109
|
+
// stub-TC state, made visible instead of counted.
|
|
110
|
+
const isCovered = decideReqCoverage(acs, strict, (ac) => ac.covered);
|
|
111
|
+
const structurallyCovered = decideReqCoverage(acs, strict, (ac) => ac.testCases.length > 0);
|
|
112
|
+
const coverageClass = isCovered ? 'covered' : structurallyCovered ? 'covered-unresolved' : 'uncovered';
|
|
113
|
+
if (coverageClass === 'covered') covered += 1;
|
|
114
|
+
else if (coverageClass === 'covered-unresolved') coveredUnresolved += 1;
|
|
115
|
+
else uncovered += 1;
|
|
116
|
+
requirements.push({ id: req.reqId, covered: isCovered, coverageClass, acs });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const result = {
|
|
120
|
+
ok: coveredUnresolved === 0 && uncovered === 0,
|
|
121
|
+
strict,
|
|
122
|
+
totals: {
|
|
123
|
+
requirements: requirements.length,
|
|
124
|
+
covered,
|
|
125
|
+
coveredUnresolved,
|
|
126
|
+
uncovered,
|
|
127
|
+
},
|
|
128
|
+
requirements,
|
|
129
|
+
unresolvedTestPointers: [...unresolvedByKey.values()]
|
|
130
|
+
.sort((a, b) => (a.tsId + a.tcId).localeCompare(b.tsId + b.tcId)),
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
if (withCode) {
|
|
134
|
+
result.withCode = true;
|
|
135
|
+
result.codeNodeOrphans = collectCodeNodeOrphans(tree);
|
|
136
|
+
result.codeTotals = summariseCodeClasses(requirements);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* CN docs whose `implementsAcIds` is empty - a legitimate, common state
|
|
144
|
+
* (utilities, glue, wiring), reported informationally (D3/D11).
|
|
145
|
+
* @param {TreeModel} tree
|
|
146
|
+
* @returns {string[]}
|
|
147
|
+
*/
|
|
148
|
+
function collectCodeNodeOrphans(tree) {
|
|
149
|
+
return (tree.codeNodes ?? [])
|
|
150
|
+
.filter((cn) => (cn.implementsAcIds ?? []).length === 0)
|
|
151
|
+
.map((cn) => cn.cnId)
|
|
152
|
+
.sort();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @param {ReqCoverage[]} requirements
|
|
157
|
+
* @returns {{ implementedAndCovered: number, implementedUncovered: number, unimplemented: number }}
|
|
158
|
+
*/
|
|
159
|
+
function summariseCodeClasses(requirements) {
|
|
160
|
+
const totals = { implementedAndCovered: 0, implementedUncovered: 0, unimplemented: 0 };
|
|
161
|
+
for (const req of requirements) {
|
|
162
|
+
for (const ac of req.acs) {
|
|
163
|
+
if (ac.codeClass === 'implemented-and-covered') totals.implementedAndCovered += 1;
|
|
164
|
+
else if (ac.codeClass === 'implemented-uncovered') totals.implementedUncovered += 1;
|
|
165
|
+
else if (ac.codeClass === 'unimplemented') totals.unimplemented += 1;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return totals;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Select the requirements in scope for coverage. No scope = every REQ
|
|
173
|
+
* in the tree. PRD scope = REQs whose prdId matches. REQ scope = just
|
|
174
|
+
* that REQ. US scope = the REQ that owns that US.
|
|
175
|
+
*
|
|
176
|
+
* @param {TreeModel} tree
|
|
177
|
+
* @param {string | null} scopeId
|
|
178
|
+
* @returns {object[]}
|
|
179
|
+
*/
|
|
180
|
+
function selectRequirements(tree, scopeId) {
|
|
181
|
+
if (!scopeId) return [...tree.requirements];
|
|
182
|
+
const kind = tree.kindById.get(scopeId);
|
|
183
|
+
if (kind === 'prd') {
|
|
184
|
+
return tree.requirements.filter((r) => r.prdId === scopeId);
|
|
185
|
+
}
|
|
186
|
+
if (kind === 'req') {
|
|
187
|
+
const req = tree.requirements.find((r) => r.reqId === scopeId);
|
|
188
|
+
return req ? [req] : [];
|
|
189
|
+
}
|
|
190
|
+
if (kind === 'userStory') {
|
|
191
|
+
const us = tree.userStories.find((u) => u.usId === scopeId);
|
|
192
|
+
if (!us) return [];
|
|
193
|
+
const req = tree.requirements.find((r) => r.reqId === us.reqId);
|
|
194
|
+
return req ? [req] : [];
|
|
195
|
+
}
|
|
196
|
+
// Unknown / unscopeable id: caller (handler) refuses with exit 2. The
|
|
197
|
+
// pure function returns an empty scope so callers that pass a bad id
|
|
198
|
+
// without pre-flighting still get an empty envelope back.
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Collect every AC under a REQ (across all its USs) with per-AC coverage
|
|
204
|
+
* signal + the list of TC ids referencing that AC. A TC only counts
|
|
205
|
+
* towards `covered` when its pointer resolves (w-2026-07-28-005); the
|
|
206
|
+
* unresolved subset is carried per AC and accumulated into the caller's
|
|
207
|
+
* `unresolvedByKey` detail map. Phase 10: when `withCode`, also attaches
|
|
208
|
+
* `cnIds` and the D11 `codeClass` (the covered leg is resolution-gated
|
|
209
|
+
* too - a stub TC never manufactures 'implemented-and-covered').
|
|
210
|
+
*
|
|
211
|
+
* @param {TreeModel} tree
|
|
212
|
+
* @param {string} reqId
|
|
213
|
+
* @param {{ withCode?: boolean, testPointers?: Map<string, object>, unresolvedByKey?: Map<string, object> }} [opts]
|
|
214
|
+
* @returns {AcCoverage[]}
|
|
215
|
+
*/
|
|
216
|
+
function collectAcs(tree, reqId, opts = {}) {
|
|
217
|
+
const withCode = Boolean(opts.withCode);
|
|
218
|
+
const testPointers = opts.testPointers ?? new Map();
|
|
219
|
+
const unresolvedByKey = opts.unresolvedByKey ?? new Map();
|
|
220
|
+
/** @type {AcCoverage[]} */
|
|
221
|
+
const acs = [];
|
|
222
|
+
const usIds = tree.childrenByParent.get(reqId) ?? [];
|
|
223
|
+
for (const usId of usIds) {
|
|
224
|
+
const us = tree.byId.get(usId);
|
|
225
|
+
if (!us) continue;
|
|
226
|
+
for (const ac of us.acceptanceCriteria ?? []) {
|
|
227
|
+
if (!ac?.id) continue;
|
|
228
|
+
const tcEntries = tree.tcsByAcId.get(ac.id) ?? [];
|
|
229
|
+
const resolvedTcs = [];
|
|
230
|
+
const unresolvedTcs = [];
|
|
231
|
+
for (const { tsId, tcId } of tcEntries) {
|
|
232
|
+
const key = testCaseKey(tsId, tcId);
|
|
233
|
+
const resolution = testPointers.get(key);
|
|
234
|
+
if (resolution?.resolved === true) {
|
|
235
|
+
resolvedTcs.push(tcId);
|
|
236
|
+
} else {
|
|
237
|
+
unresolvedTcs.push(tcId);
|
|
238
|
+
if (!unresolvedByKey.has(key)) {
|
|
239
|
+
unresolvedByKey.set(key, {
|
|
240
|
+
tsId,
|
|
241
|
+
tcId,
|
|
242
|
+
testPointer: resolution?.testPointer ?? null,
|
|
243
|
+
reason: resolution?.reason ?? 'missing-pointer',
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const entry = {
|
|
249
|
+
id: ac.id,
|
|
250
|
+
covered: resolvedTcs.length > 0,
|
|
251
|
+
testCases: tcEntries.map((e) => e.tcId).sort(),
|
|
252
|
+
unresolvedTestCases: [...unresolvedTcs].sort(),
|
|
253
|
+
};
|
|
254
|
+
if (withCode) {
|
|
255
|
+
const cnIds = [...(tree.cnByAcId?.get(ac.id) ?? [])].sort();
|
|
256
|
+
entry.cnIds = cnIds;
|
|
257
|
+
entry.codeClass = cnIds.length === 0
|
|
258
|
+
? 'unimplemented'
|
|
259
|
+
: resolvedTcs.length > 0
|
|
260
|
+
? 'implemented-and-covered'
|
|
261
|
+
: 'implemented-uncovered';
|
|
262
|
+
}
|
|
263
|
+
acs.push(entry);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return acs;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* shallow-any: any one AC passing = REQ passing.
|
|
271
|
+
* strict: every AC passing = REQ passing. REQ with zero ACs is uncovered
|
|
272
|
+
* under either mode (no chain to walk). The per-AC predicate is supplied
|
|
273
|
+
* by the caller so the same rule serves both the resolution-gated verdict
|
|
274
|
+
* and the structural (TC-row-exists) verdict.
|
|
275
|
+
*
|
|
276
|
+
* @param {AcCoverage[]} acs
|
|
277
|
+
* @param {boolean} strict
|
|
278
|
+
* @param {(ac: AcCoverage) => boolean} acPasses
|
|
279
|
+
* @returns {boolean}
|
|
280
|
+
*/
|
|
281
|
+
function decideReqCoverage(acs, strict, acPasses) {
|
|
282
|
+
if (acs.length === 0) return false;
|
|
283
|
+
if (strict) return acs.every(acPasses);
|
|
284
|
+
return acs.some(acPasses);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Detect whether the positional is a scopeable id (PRD / REQ / US) or
|
|
289
|
+
* a below-AC id that the handler must refuse with exit 2 per D10.
|
|
290
|
+
* Returns 'valid' | 'below-ac' | 'unknown-kind' | 'not-found'.
|
|
291
|
+
*
|
|
292
|
+
* @param {TreeModel} tree
|
|
293
|
+
* @param {string} id
|
|
294
|
+
* @returns {'valid' | 'below-ac' | 'unknown-kind' | 'not-found'}
|
|
295
|
+
*/
|
|
296
|
+
export function classifyCoverageScope(tree, id) {
|
|
297
|
+
const kind = tree.kindById.get(id);
|
|
298
|
+
if (!kind) {
|
|
299
|
+
// Inline AC / TC ids are below-AC by definition.
|
|
300
|
+
if (/^AC-/.test(id) || /^TC-/.test(id)) return 'below-ac';
|
|
301
|
+
return 'not-found';
|
|
302
|
+
}
|
|
303
|
+
if (kind === 'prd' || kind === 'req' || kind === 'userStory') return 'valid';
|
|
304
|
+
// Below-AC or off-chain kinds. TAC / ADR / FBS / BS / TAD / TS all
|
|
305
|
+
// refuse: coverage backbone is PRD -> REQ -> US -> AC -> TS -> TC,
|
|
306
|
+
// scoping below AC has no meaningful reduction.
|
|
307
|
+
return 'below-ac';
|
|
308
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// JSON formatter for coverage / trace / impact result envelopes.
|
|
2
|
+
// Phase 5 §D15: shape is stable-by-convention. Downstream consumers
|
|
3
|
+
// (Phase 7 MCP layer) will consume these envelopes; no in-place
|
|
4
|
+
// reshape without a version bump.
|
|
5
|
+
//
|
|
6
|
+
// The `verb` tag is currently unused - every envelope is already
|
|
7
|
+
// self-describing (`ok`, `pivot`, `direction`, etc.) - but it's
|
|
8
|
+
// retained on the API so future formatter dispatch stays uniform
|
|
9
|
+
// across the three verbs.
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Format a query result as a JSON envelope.
|
|
13
|
+
*
|
|
14
|
+
* @param {object} result - CoverageResult | TraceResult | ImpactResult
|
|
15
|
+
* @param {string} verb - 'coverage' | 'trace' | 'impact'
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
export function formatJson(result, verb) {
|
|
19
|
+
void verb;
|
|
20
|
+
return `${JSON.stringify(result, null, 2)}\n`;
|
|
21
|
+
}
|