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,209 @@
|
|
|
1
|
+
// Mermaid formatter for coverage / trace / impact result envelopes.
|
|
2
|
+
// Phase 5 §D14: `flowchart LR` orientation, class palette matches
|
|
3
|
+
// `src/view/mermaid-diagram.js`. Text-only output; no HTML wrapper.
|
|
4
|
+
//
|
|
5
|
+
// The class-defs block, `classForId`, and label helpers are
|
|
6
|
+
// duplicated here rather than extracted from `src/view/mermaid-diagram.js`.
|
|
7
|
+
// Reason (per Phase 5 brief §5, spec §D16): view-side helpers take
|
|
8
|
+
// the view's `BuiltTreeModel` (title lookup via `model.byId`), while
|
|
9
|
+
// this formatter operates over a pure `computeTrace` result envelope
|
|
10
|
+
// that carries only node ids and kinds. Bridging the two contract
|
|
11
|
+
// shapes would need a wrapper in the view module - the "invasive"
|
|
12
|
+
// extraction the spec calls out. Duplicating ~30 LoC of trivial
|
|
13
|
+
// static defs is cheaper and keeps the view module unchanged.
|
|
14
|
+
|
|
15
|
+
const CLASS_DEFS = `
|
|
16
|
+
classDef prd fill:#fde68a,stroke:#92400e,color:#1f2937;
|
|
17
|
+
classDef req fill:#bbf7d0,stroke:#065f46,color:#1f2937;
|
|
18
|
+
classDef us fill:#bae6fd,stroke:#0c4a6e,color:#1f2937;
|
|
19
|
+
classDef ac fill:#e0e7ff,stroke:#3730a3,color:#1f2937;
|
|
20
|
+
classDef tad fill:#fecaca,stroke:#991b1b,color:#1f2937;
|
|
21
|
+
classDef tac fill:#fed7aa,stroke:#9a3412,color:#1f2937;
|
|
22
|
+
classDef adr fill:#fbcfe8,stroke:#9d174d,color:#1f2937;
|
|
23
|
+
classDef bs fill:#ddd6fe,stroke:#5b21b6,color:#1f2937;
|
|
24
|
+
classDef fbs fill:#c7d2fe,stroke:#3730a3,color:#1f2937;
|
|
25
|
+
classDef ts fill:#99f6e4,stroke:#115e59,color:#1f2937;
|
|
26
|
+
classDef tc fill:#d9f99d,stroke:#3f6212,color:#1f2937;
|
|
27
|
+
classDef cn fill:#a5b4fc,stroke:#312e81,color:#1f2937;
|
|
28
|
+
classDef broken stroke:#dc2626,stroke-width:2px,stroke-dasharray:5 5,color:#7f1d1d;
|
|
29
|
+
`.trim();
|
|
30
|
+
|
|
31
|
+
function classForId(id) {
|
|
32
|
+
if (id.startsWith('PRD-')) return 'prd';
|
|
33
|
+
if (id.startsWith('REQ-')) return 'req';
|
|
34
|
+
if (id.startsWith('US-')) return 'us';
|
|
35
|
+
if (id.startsWith('AC-')) return 'ac';
|
|
36
|
+
if (id.startsWith('TAD-')) return 'tad';
|
|
37
|
+
if (id.startsWith('TAC-')) return 'tac';
|
|
38
|
+
if (id.startsWith('ADR-')) return 'adr';
|
|
39
|
+
if (id.startsWith('BS-')) return 'bs';
|
|
40
|
+
if (id.startsWith('FBS-')) return 'fbs';
|
|
41
|
+
if (id.startsWith('TS-')) return 'ts';
|
|
42
|
+
if (id.startsWith('TC-')) return 'tc';
|
|
43
|
+
// Phase 10 (X2 CodeNode bridge, spec D12): CN cosmetic class.
|
|
44
|
+
if (id.startsWith('CN-')) return 'cn';
|
|
45
|
+
return 'unknown';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function nodeLabel(id) {
|
|
49
|
+
return `"${id}"`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Emit `flowchart LR` mermaid text for a query result.
|
|
54
|
+
*
|
|
55
|
+
* @param {object} result
|
|
56
|
+
* @param {'coverage' | 'trace' | 'impact'} verb
|
|
57
|
+
* @returns {string}
|
|
58
|
+
*/
|
|
59
|
+
export function formatMermaid(result, verb) {
|
|
60
|
+
if (verb === 'coverage') return renderCoverageMermaid(result);
|
|
61
|
+
if (verb === 'trace') return renderTraceMermaid(result);
|
|
62
|
+
if (verb === 'impact') return renderImpactMermaid(result);
|
|
63
|
+
return '';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function renderCoverageMermaid(result) {
|
|
67
|
+
const lines = ['flowchart LR'];
|
|
68
|
+
const seenIds = new Set();
|
|
69
|
+
// w-2026-07-28-005: TCs whose pointer does not resolve carry the
|
|
70
|
+
// `broken` class on top of `tc` - dashed red stroke, so a stub TC is
|
|
71
|
+
// visibly not the same thing as a resolving one.
|
|
72
|
+
const unresolvedTcIds = new Set();
|
|
73
|
+
const declare = (id) => {
|
|
74
|
+
if (seenIds.has(id)) return;
|
|
75
|
+
seenIds.add(id);
|
|
76
|
+
lines.push(` ${id}[${nodeLabel(id)}]`);
|
|
77
|
+
};
|
|
78
|
+
for (const req of result.requirements) {
|
|
79
|
+
declare(req.id);
|
|
80
|
+
for (const ac of req.acs) {
|
|
81
|
+
declare(ac.id);
|
|
82
|
+
lines.push(` ${req.id} --> ${ac.id}`);
|
|
83
|
+
const unresolved = new Set(ac.unresolvedTestCases ?? []);
|
|
84
|
+
for (const tc of ac.testCases) {
|
|
85
|
+
declare(tc);
|
|
86
|
+
lines.push(` ${ac.id} -.-> ${tc}`);
|
|
87
|
+
if (unresolved.has(tc)) unresolvedTcIds.add(tc);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
for (const tc of unresolvedTcIds) {
|
|
92
|
+
lines.push(` class ${tc} broken;`);
|
|
93
|
+
}
|
|
94
|
+
return `${appendClassBlock(lines, seenIds)}\n`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function renderTraceMermaid(result) {
|
|
98
|
+
if (!result.found) return `%% trace: id ${result.pivot} not found\n`;
|
|
99
|
+
if (result.direction === 'both') {
|
|
100
|
+
// Two adjacent flowchart LR blocks with the pivot appearing in both,
|
|
101
|
+
// so the pivot visually anchors between ancestors and descendants
|
|
102
|
+
// (spec §D9).
|
|
103
|
+
const pivotId = result.pivot;
|
|
104
|
+
const ancestors = result.ancestors ?? [];
|
|
105
|
+
const descendants = result.descendants ?? [];
|
|
106
|
+
const ancBlock = buildTraceBlock({
|
|
107
|
+
title: 'Ancestors',
|
|
108
|
+
pivot: pivotId,
|
|
109
|
+
nodes: [{ id: pivotId, kind: 'pivot', depth: 0 }, ...ancestors],
|
|
110
|
+
edges: pairToEdges(ancestors, pivotId, /*isAncestor*/ true),
|
|
111
|
+
});
|
|
112
|
+
const descBlock = buildTraceBlock({
|
|
113
|
+
title: 'Descendants',
|
|
114
|
+
pivot: pivotId,
|
|
115
|
+
nodes: [{ id: pivotId, kind: 'pivot', depth: 0 }, ...descendants],
|
|
116
|
+
edges: pairToEdges(descendants, pivotId, /*isAncestor*/ false),
|
|
117
|
+
});
|
|
118
|
+
return `${ancBlock}\n\n${descBlock}\n`;
|
|
119
|
+
}
|
|
120
|
+
const lines = ['flowchart LR'];
|
|
121
|
+
const seenIds = new Set();
|
|
122
|
+
const declare = (id) => {
|
|
123
|
+
if (seenIds.has(id)) return;
|
|
124
|
+
seenIds.add(id);
|
|
125
|
+
lines.push(` ${id}[${nodeLabel(id)}]`);
|
|
126
|
+
};
|
|
127
|
+
for (const n of result.nodes ?? []) declare(n.id);
|
|
128
|
+
for (const e of result.edges ?? []) {
|
|
129
|
+
const arrow = e.kind === 'crossLink' ? '-.->' : '-->';
|
|
130
|
+
lines.push(` ${e.from} ${arrow} ${e.to}`);
|
|
131
|
+
}
|
|
132
|
+
return `${appendClassBlock(lines, seenIds)}\n`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function renderImpactMermaid(result) {
|
|
136
|
+
if (!result.found) return `%% impact: id ${result.pivot} not found\n`;
|
|
137
|
+
const lines = ['flowchart LR'];
|
|
138
|
+
const seenIds = new Set();
|
|
139
|
+
const declare = (id) => {
|
|
140
|
+
if (seenIds.has(id)) return;
|
|
141
|
+
seenIds.add(id);
|
|
142
|
+
lines.push(` ${id}[${nodeLabel(id)}]`);
|
|
143
|
+
};
|
|
144
|
+
for (const n of result.nodes ?? []) declare(n.id);
|
|
145
|
+
for (const e of result.edges ?? []) {
|
|
146
|
+
const arrow = e.kind === 'crossLink' ? '-.->' : '-->';
|
|
147
|
+
lines.push(` ${e.from} ${arrow} ${e.to}`);
|
|
148
|
+
}
|
|
149
|
+
return `${appendClassBlock(lines, seenIds)}\n`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// -- helpers --
|
|
153
|
+
|
|
154
|
+
function buildTraceBlock({ nodes, edges }) {
|
|
155
|
+
const lines = ['flowchart LR'];
|
|
156
|
+
const seen = new Set();
|
|
157
|
+
const declare = (id) => {
|
|
158
|
+
if (seen.has(id)) return;
|
|
159
|
+
seen.add(id);
|
|
160
|
+
lines.push(` ${id}[${nodeLabel(id)}]`);
|
|
161
|
+
};
|
|
162
|
+
for (const n of nodes) declare(n.id);
|
|
163
|
+
for (const e of edges) {
|
|
164
|
+
const arrow = e.kind === 'crossLink' ? '-.->' : '-->';
|
|
165
|
+
lines.push(` ${e.from} ${arrow} ${e.to}`);
|
|
166
|
+
}
|
|
167
|
+
return appendClassBlock(lines, seen);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// The both-direction trace result does not preserve raw edges; synthesise
|
|
171
|
+
// a linear chain from the pivot through ancestors / descendants in
|
|
172
|
+
// depth order. Edge kind defaults to parentChild because we don't know
|
|
173
|
+
// the arrival relationship at this layer - the caller uses the raw
|
|
174
|
+
// forward / back envelopes when precise edge kinds matter.
|
|
175
|
+
function pairToEdges(list, pivot, isAncestor) {
|
|
176
|
+
const edges = [];
|
|
177
|
+
if (list.length === 0) return edges;
|
|
178
|
+
if (isAncestor) {
|
|
179
|
+
// Ancestors sorted by depth descending (closest first). Chain the
|
|
180
|
+
// pivot -> first ancestor -> next ancestor. Depth on the trace
|
|
181
|
+
// result is negative, closest ancestor has depth -1.
|
|
182
|
+
let prev = pivot;
|
|
183
|
+
const sorted = [...list].sort((a, b) => (b.depth ?? 0) - (a.depth ?? 0));
|
|
184
|
+
for (const n of sorted) {
|
|
185
|
+
edges.push({ from: n.id, to: prev, kind: 'parentChild' });
|
|
186
|
+
prev = n.id;
|
|
187
|
+
}
|
|
188
|
+
return edges;
|
|
189
|
+
}
|
|
190
|
+
// Descendants: pivot -> depth 1 -> depth 2 -> ...
|
|
191
|
+
const sorted = [...list].sort((a, b) => (a.depth ?? 0) - (b.depth ?? 0));
|
|
192
|
+
let prev = pivot;
|
|
193
|
+
for (const n of sorted) {
|
|
194
|
+
edges.push({ from: prev, to: n.id, kind: 'parentChild' });
|
|
195
|
+
prev = n.id;
|
|
196
|
+
}
|
|
197
|
+
return edges;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function appendClassBlock(lines, ids) {
|
|
201
|
+
for (const id of ids) {
|
|
202
|
+
const cls = classForId(id);
|
|
203
|
+
if (cls !== 'unknown') lines.push(` class ${id} ${cls};`);
|
|
204
|
+
}
|
|
205
|
+
lines.push(CLASS_DEFS);
|
|
206
|
+
return lines.join('\n');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export { classForId, CLASS_DEFS };
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// ASCII table formatter for coverage / trace / impact result envelopes.
|
|
2
|
+
// Zero-dep, uses Node built-in string padding. Column widths adapt to
|
|
3
|
+
// the widest cell per column, capped at a sane per-column maximum so a
|
|
4
|
+
// single very long title doesn't force the whole table off-screen.
|
|
5
|
+
//
|
|
6
|
+
// Phase 5 §D3: default output format. British English column headers,
|
|
7
|
+
// ASCII hyphens only (§D19).
|
|
8
|
+
|
|
9
|
+
const COLUMN_CAP = 60;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Format a query result as an ASCII table (or two labelled blocks
|
|
13
|
+
* around a pivot for trace --both). Trailing newline included.
|
|
14
|
+
*
|
|
15
|
+
* @param {object} result
|
|
16
|
+
* @param {'coverage' | 'trace' | 'impact'} verb
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
export function formatTable(result, verb) {
|
|
20
|
+
if (verb === 'coverage') return formatCoverageTable(result);
|
|
21
|
+
if (verb === 'trace') return formatTraceTable(result);
|
|
22
|
+
if (verb === 'impact') return formatImpactTable(result);
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// w-2026-07-28-005: three-state cell vocabulary. 'unresolved' = TC rows
|
|
27
|
+
// exist but none of their pointers resolves to a real test - the stub-TC
|
|
28
|
+
// state, shown as its own word rather than folded into yes/no.
|
|
29
|
+
function reqCoveredCell(req) {
|
|
30
|
+
if (req.covered) return 'yes';
|
|
31
|
+
return req.coverageClass === 'covered-unresolved' ? 'unresolved' : 'no';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function acCoveredCell(ac) {
|
|
35
|
+
if (ac.covered) return 'yes';
|
|
36
|
+
return ac.testCases.length > 0 ? 'unresolved' : 'no';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function testCasesCell(ac) {
|
|
40
|
+
if (ac.testCases.length === 0) return '-';
|
|
41
|
+
const unresolved = new Set(ac.unresolvedTestCases ?? []);
|
|
42
|
+
return ac.testCases
|
|
43
|
+
.map((tc) => (unresolved.has(tc) ? `${tc}[unresolved]` : tc))
|
|
44
|
+
.join(', ');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function formatCoverageTable(result) {
|
|
48
|
+
const lines = [];
|
|
49
|
+
const modeLabel = result.strict ? 'strict (per-AC)' : 'shallow-any';
|
|
50
|
+
lines.push(`Coverage mode: ${modeLabel}`);
|
|
51
|
+
lines.push(
|
|
52
|
+
`Requirements: ${result.totals.requirements} ` +
|
|
53
|
+
`covered: ${result.totals.covered} ` +
|
|
54
|
+
`covered-unresolved: ${result.totals.coveredUnresolved} ` +
|
|
55
|
+
`uncovered: ${result.totals.uncovered}`,
|
|
56
|
+
);
|
|
57
|
+
lines.push('');
|
|
58
|
+
const rows = [];
|
|
59
|
+
// Phase 10 (X2 CodeNode bridge, D11): --with-code appends a Code column.
|
|
60
|
+
const withCode = Boolean(result.withCode);
|
|
61
|
+
const header = ['Requirement', 'Covered', 'AC', 'AC covered', 'Test cases'];
|
|
62
|
+
if (withCode) header.push('Code');
|
|
63
|
+
rows.push(header);
|
|
64
|
+
for (const req of result.requirements) {
|
|
65
|
+
if (req.acs.length === 0) {
|
|
66
|
+
const row = [req.id, reqCoveredCell(req), '(no AC)', '-', '-'];
|
|
67
|
+
if (withCode) row.push('-');
|
|
68
|
+
rows.push(row);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
for (const [i, ac] of req.acs.entries()) {
|
|
72
|
+
const row = [
|
|
73
|
+
i === 0 ? req.id : '',
|
|
74
|
+
i === 0 ? reqCoveredCell(req) : '',
|
|
75
|
+
ac.id,
|
|
76
|
+
acCoveredCell(ac),
|
|
77
|
+
testCasesCell(ac),
|
|
78
|
+
];
|
|
79
|
+
if (withCode) row.push(ac.codeClass ?? '-');
|
|
80
|
+
rows.push(row);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
lines.push(renderTable(rows));
|
|
84
|
+
const unresolvedPointers = result.unresolvedTestPointers ?? [];
|
|
85
|
+
if (unresolvedPointers.length > 0) {
|
|
86
|
+
lines.push('');
|
|
87
|
+
lines.push('Unresolved test pointers (never counted as coverage):');
|
|
88
|
+
for (const u of unresolvedPointers) {
|
|
89
|
+
const pointer = u.testPointer ?? '(no pointer)';
|
|
90
|
+
lines.push(` ${u.tcId} (${u.tsId}): ${pointer} - ${u.reason}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (withCode) {
|
|
94
|
+
lines.push('');
|
|
95
|
+
lines.push(
|
|
96
|
+
`Code totals: implemented-and-covered=${result.codeTotals.implementedAndCovered} ` +
|
|
97
|
+
`implemented-uncovered=${result.codeTotals.implementedUncovered} ` +
|
|
98
|
+
`unimplemented=${result.codeTotals.unimplemented}`,
|
|
99
|
+
);
|
|
100
|
+
lines.push(
|
|
101
|
+
result.codeNodeOrphans.length > 0
|
|
102
|
+
? `CN-orphaned (no implementsAcIds): ${result.codeNodeOrphans.join(', ')}`
|
|
103
|
+
: 'CN-orphaned (no implementsAcIds): (none)',
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
return `${lines.join('\n')}\n`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function formatTraceTable(result) {
|
|
110
|
+
if (!result.found) return `trace: id ${result.pivot} not found\n`;
|
|
111
|
+
if (result.direction === 'both') {
|
|
112
|
+
return formatBothTraceTable(result);
|
|
113
|
+
}
|
|
114
|
+
const lines = [];
|
|
115
|
+
const directionLabel = result.direction === 'forward' ? 'Descendants' : 'Ancestors';
|
|
116
|
+
lines.push(`Trace pivot: ${result.pivot} direction: ${result.direction}`);
|
|
117
|
+
lines.push('');
|
|
118
|
+
const rows = [];
|
|
119
|
+
rows.push(['Depth', 'Id', 'Kind', 'Title']);
|
|
120
|
+
const showList = (result.nodes ?? []).slice();
|
|
121
|
+
if (showList.length <= 1) {
|
|
122
|
+
lines.push(`${directionLabel}: (none)`);
|
|
123
|
+
} else {
|
|
124
|
+
for (const n of showList) {
|
|
125
|
+
rows.push([String(n.depth), n.id, n.kind, cellTitle(n.id)]);
|
|
126
|
+
}
|
|
127
|
+
lines.push(renderTable(rows));
|
|
128
|
+
}
|
|
129
|
+
return `${lines.join('\n')}\n`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function formatBothTraceTable(result) {
|
|
133
|
+
const lines = [];
|
|
134
|
+
lines.push(`Trace pivot: ${result.pivot} direction: both`);
|
|
135
|
+
lines.push('');
|
|
136
|
+
lines.push('Ancestors:');
|
|
137
|
+
const ancestors = result.ancestors ?? [];
|
|
138
|
+
if (ancestors.length === 0) {
|
|
139
|
+
lines.push(' (none)');
|
|
140
|
+
} else {
|
|
141
|
+
const rows = [['Depth', 'Id', 'Kind', 'Title']];
|
|
142
|
+
for (const n of ancestors) rows.push([String(n.depth), n.id, n.kind, '']);
|
|
143
|
+
lines.push(renderTable(rows));
|
|
144
|
+
}
|
|
145
|
+
lines.push('');
|
|
146
|
+
lines.push(`Pivot: ${result.pivot}`);
|
|
147
|
+
lines.push('');
|
|
148
|
+
lines.push('Descendants:');
|
|
149
|
+
const descendants = result.descendants ?? [];
|
|
150
|
+
if (descendants.length === 0) {
|
|
151
|
+
lines.push(' (none)');
|
|
152
|
+
} else {
|
|
153
|
+
const rows = [['Depth', 'Id', 'Kind', 'Title']];
|
|
154
|
+
for (const n of descendants) rows.push([String(n.depth), n.id, n.kind, '']);
|
|
155
|
+
lines.push(renderTable(rows));
|
|
156
|
+
}
|
|
157
|
+
return `${lines.join('\n')}\n`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function formatImpactTable(result) {
|
|
161
|
+
if (!result.found) return `impact: id ${result.pivot} not found\n`;
|
|
162
|
+
const lines = [];
|
|
163
|
+
lines.push(`Impact pivot: ${result.pivot}`);
|
|
164
|
+
lines.push('');
|
|
165
|
+
const rows = [['Id', 'Kind', 'Role', 'Action needed']];
|
|
166
|
+
for (const n of result.nodes ?? []) {
|
|
167
|
+
rows.push([n.id, n.kind, n.role, n.actionNeeded ?? '-']);
|
|
168
|
+
}
|
|
169
|
+
lines.push(renderTable(rows));
|
|
170
|
+
return `${lines.join('\n')}\n`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Table renderer. Every cell is stringified, truncated at COLUMN_CAP,
|
|
174
|
+
// then padded to the column's widest observed cell. Deterministic
|
|
175
|
+
// spacing; no ANSI colours; two spaces between columns.
|
|
176
|
+
|
|
177
|
+
function renderTable(rows) {
|
|
178
|
+
const widths = new Array(rows[0].length).fill(0);
|
|
179
|
+
const cells = rows.map((r) => r.map((c) => {
|
|
180
|
+
let s = c == null ? '' : String(c);
|
|
181
|
+
if (s.length > COLUMN_CAP) s = `${s.slice(0, COLUMN_CAP - 3)}...`;
|
|
182
|
+
return s;
|
|
183
|
+
}));
|
|
184
|
+
for (const r of cells) {
|
|
185
|
+
for (let i = 0; i < r.length; i += 1) {
|
|
186
|
+
if (r[i].length > widths[i]) widths[i] = r[i].length;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const out = cells.map(
|
|
190
|
+
(r) => r.map((c, i) => c.padEnd(widths[i])).join(' ').replace(/ +$/, ''),
|
|
191
|
+
);
|
|
192
|
+
// Header separator: `-` under each column.
|
|
193
|
+
const sep = widths.map((w) => '-'.repeat(w)).join(' ').replace(/ +$/, '');
|
|
194
|
+
return [out[0], sep, ...out.slice(1)].join('\n');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Placeholder for future title lookup - the pure trace result does not
|
|
198
|
+
// carry doc bodies, so title columns render blank unless the caller
|
|
199
|
+
// passes a title source. Left here for the trace `Title` column so the
|
|
200
|
+
// column stays if we ever wire in a title map without a schema change.
|
|
201
|
+
function cellTitle(_id) {
|
|
202
|
+
return '';
|
|
203
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// Pure impact compute. Impact = trace-forward + trace-back + a
|
|
2
|
+
// labelled `actionNeeded` column per node kind + role (Phase 5 §D7).
|
|
3
|
+
//
|
|
4
|
+
// The delta over `computeTrace(..., 'both')` is the labelled
|
|
5
|
+
// `actionNeeded` column: it turns a raw traversal into an actionable
|
|
6
|
+
// re-verify / re-approve fan-out that the operator can walk through
|
|
7
|
+
// end-to-end.
|
|
8
|
+
//
|
|
9
|
+
// Action label rules (§D7):
|
|
10
|
+
// - PRD ancestor: "re-approve"
|
|
11
|
+
// - TAD ancestor: "review-arch"
|
|
12
|
+
// - BS ancestor: "review-plan"
|
|
13
|
+
// - REQ / US descendant: "review-scope"
|
|
14
|
+
// - AC descendant: "re-approve"
|
|
15
|
+
// - TS descendant: "re-verify"
|
|
16
|
+
// - TC descendant: "re-run"
|
|
17
|
+
// - FBS descendant: "re-execute" (cross-linked to an affected AC,
|
|
18
|
+
// or a dependent of an affected FBS)
|
|
19
|
+
// - TAC / ADR descendant: "review-context" (reached via a
|
|
20
|
+
// contextRequirements cross-link, which is what
|
|
21
|
+
// the current impact fan-out surfaces)
|
|
22
|
+
// - Pivot: null
|
|
23
|
+
|
|
24
|
+
import { computeTrace, kindOf } from './trace.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {import('#core/store/walker.js').TreeModel} TreeModel
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {'pivot' | 'ancestor' | 'descendant'} ImpactRole
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {object} ImpactNode
|
|
36
|
+
* @property {string} id
|
|
37
|
+
* @property {string} kind
|
|
38
|
+
* @property {ImpactRole} role
|
|
39
|
+
* @property {string | null} actionNeeded
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @typedef {object} ImpactEdge
|
|
44
|
+
* @property {string} from
|
|
45
|
+
* @property {string} to
|
|
46
|
+
* @property {'parentChild' | 'crossLink'} kind
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {object} ImpactResult
|
|
51
|
+
* @property {string} pivot
|
|
52
|
+
* @property {boolean} found - false when the pivot id is unknown
|
|
53
|
+
* @property {ImpactNode[]} [nodes]
|
|
54
|
+
* @property {ImpactEdge[]} [edges]
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Compute impact for a pivot. Composes computeTrace('both') and layers
|
|
59
|
+
* the D7 actionNeeded column per (kind, role).
|
|
60
|
+
*
|
|
61
|
+
* @param {TreeModel} tree
|
|
62
|
+
* @param {object} opts
|
|
63
|
+
* @param {string} opts.id
|
|
64
|
+
* @param {boolean} [opts.includeCode] - Phase 10: extend the forward fan-out
|
|
65
|
+
* into Code Nodes.
|
|
66
|
+
* @returns {ImpactResult}
|
|
67
|
+
*/
|
|
68
|
+
export function computeImpact(tree, { id, includeCode = false }) {
|
|
69
|
+
const pivotKind = kindOf(tree, id);
|
|
70
|
+
if (!pivotKind) return { pivot: id, found: false };
|
|
71
|
+
|
|
72
|
+
const back = computeTrace(tree, { id, direction: 'back' });
|
|
73
|
+
// expandFbsDependents: impact is the blast-radius question, so the FBS
|
|
74
|
+
// dependency fan-out is in scope here (D7: a dependent of an affected
|
|
75
|
+
// FBS is labelled re-execute). Plain trace keeps FBS as a cross-link
|
|
76
|
+
// leaf (AC-402-3).
|
|
77
|
+
const fwd = computeTrace(tree, {
|
|
78
|
+
id, direction: 'forward', includeCode, expandFbsDependents: true,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
/** @type {ImpactNode[]} */
|
|
82
|
+
const nodes = [];
|
|
83
|
+
/** @type {ImpactEdge[]} */
|
|
84
|
+
const edges = [];
|
|
85
|
+
const seen = new Set();
|
|
86
|
+
|
|
87
|
+
const push = (n) => {
|
|
88
|
+
if (seen.has(n.id)) return;
|
|
89
|
+
seen.add(n.id);
|
|
90
|
+
nodes.push(n);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
push({ id, kind: pivotKind, role: 'pivot', actionNeeded: null });
|
|
94
|
+
|
|
95
|
+
const backNodes = (back.nodes ?? []).filter((n) => n.id !== id);
|
|
96
|
+
for (const n of backNodes) {
|
|
97
|
+
push({
|
|
98
|
+
id: n.id,
|
|
99
|
+
kind: n.kind,
|
|
100
|
+
role: 'ancestor',
|
|
101
|
+
actionNeeded: labelFor(n.kind, 'ancestor'),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const fwdNodes = (fwd.nodes ?? []).filter((n) => n.id !== id);
|
|
105
|
+
for (const n of fwdNodes) {
|
|
106
|
+
push({
|
|
107
|
+
id: n.id,
|
|
108
|
+
kind: n.kind,
|
|
109
|
+
role: 'descendant',
|
|
110
|
+
actionNeeded: labelFor(n.kind, 'descendant'),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Edges: combine both directions. De-dupe on (from, to) so the same
|
|
115
|
+
// edge doesn't appear twice when the forward and back traces share a
|
|
116
|
+
// segment (they don't, structurally, but keep the guard for safety).
|
|
117
|
+
const edgeKey = (e) => `${e.from}->${e.to}`;
|
|
118
|
+
const edgeSeen = new Set();
|
|
119
|
+
for (const e of (back.edges ?? []).concat(fwd.edges ?? [])) {
|
|
120
|
+
const k = edgeKey(e);
|
|
121
|
+
if (edgeSeen.has(k)) continue;
|
|
122
|
+
edgeSeen.add(k);
|
|
123
|
+
edges.push(e);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { pivot: id, found: true, nodes, edges };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Map (kind, role) to a D7 action label. Returns null when no rule
|
|
131
|
+
* applies (either because it's the pivot or because the kind isn't
|
|
132
|
+
* in the D7 table for that role - a design defensive default).
|
|
133
|
+
*
|
|
134
|
+
* @param {string} kind
|
|
135
|
+
* @param {ImpactRole} role
|
|
136
|
+
* @returns {string | null}
|
|
137
|
+
*/
|
|
138
|
+
export function labelFor(kind, role) {
|
|
139
|
+
if (role === 'pivot') return null;
|
|
140
|
+
if (role === 'ancestor') {
|
|
141
|
+
switch (kind) {
|
|
142
|
+
case 'prd': return 're-approve';
|
|
143
|
+
case 'tad': return 'review-arch';
|
|
144
|
+
case 'buildSequence': return 'review-plan';
|
|
145
|
+
case 'req': return 'review-scope';
|
|
146
|
+
case 'userStory': return 'review-scope';
|
|
147
|
+
case 'ac': return 're-approve';
|
|
148
|
+
case 'testSuite': return 're-verify';
|
|
149
|
+
case 'tac': return 'review-arch';
|
|
150
|
+
case 'adr': return 'review-arch';
|
|
151
|
+
case 'fbs': return 're-execute';
|
|
152
|
+
default: return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// descendant
|
|
156
|
+
switch (kind) {
|
|
157
|
+
case 'prd': return 're-approve';
|
|
158
|
+
case 'tad': return 'review-arch';
|
|
159
|
+
case 'buildSequence': return 'review-plan';
|
|
160
|
+
case 'req': return 'review-scope';
|
|
161
|
+
case 'userStory': return 'review-scope';
|
|
162
|
+
case 'ac': return 're-approve';
|
|
163
|
+
case 'testSuite': return 're-verify';
|
|
164
|
+
case 'tc': return 're-run';
|
|
165
|
+
case 'fbs': return 're-execute';
|
|
166
|
+
case 'tac': return 'review-context';
|
|
167
|
+
case 'adr': return 'review-context';
|
|
168
|
+
// Phase 10 (X2 CodeNode bridge): a code node reached forward from a
|
|
169
|
+
// spec change is source that may need re-implementation + re-test.
|
|
170
|
+
case 'codeNode': return 're-verify-code';
|
|
171
|
+
default: return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Public surface for the query layer (Phase 5 §D16). Pure logic +
|
|
2
|
+
// formatters; handlers live under `src/cli/`.
|
|
3
|
+
|
|
4
|
+
export { computeCoverage, classifyCoverageScope } from './coverage.js';
|
|
5
|
+
export { computeTrace, kindOf } from './trace.js';
|
|
6
|
+
export { computeImpact, labelFor } from './impact.js';
|
|
7
|
+
export { formatTable } from './formatters/table.js';
|
|
8
|
+
export { formatJson } from './formatters/json.js';
|
|
9
|
+
export { formatMermaid } from './formatters/mermaid.js';
|
|
10
|
+
// 0.8.0 slug-train car 3: NV-BL-SR-03 addendum (ruling-sheet item 1)
|
|
11
|
+
// -- traceability / query tools share the refuse-first posture that
|
|
12
|
+
// gates rcf build. Callers wrap their query producer with this.
|
|
13
|
+
export { runWithAdmissibilityGate } from './refuse-on-admissibility.js';
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Traceability / query tool refuse-first wrapper (NV-BL-SR-03
|
|
2
|
+
// addendum on ruling-sheet item 1, ratified 2026-08-11).
|
|
3
|
+
//
|
|
4
|
+
// The ruleset's `toolScope` block declares:
|
|
5
|
+
// { chainAdmissibility: true, traceabilityAndQueryTools: true }
|
|
6
|
+
//
|
|
7
|
+
// meaning the same refusal semantics that gate `rcf build` also gate
|
|
8
|
+
// the traceability and query tools. A tool that hides an admissibility
|
|
9
|
+
// failure is the same class of defect as a build that hides one.
|
|
10
|
+
//
|
|
11
|
+
// This module wraps a query result so a REFUSE verdict from
|
|
12
|
+
// `enforceAdmissibility` short-circuits the tool's output. Callers
|
|
13
|
+
// pass the walker tree and the chain's declared ruleset version; on
|
|
14
|
+
// REFUSE the wrapper returns a refusal envelope naming the unresolved
|
|
15
|
+
// findings. On PASS or PASS-WITH-OVERRIDES the query's own result flows
|
|
16
|
+
// through unchanged.
|
|
17
|
+
|
|
18
|
+
import { enforceAdmissibility, getRulesetToolScope } from '#admissibility';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {import('../admissibility/enforce.js').AdmissibilityVerdict} AdmissibilityVerdict
|
|
22
|
+
* @typedef {import('../admissibility/enforce.js').AdmissibilityOverride} AdmissibilityOverride
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {object} QueryResult
|
|
27
|
+
* @property {'ok' | 'refused-admissibility'} status
|
|
28
|
+
* @property {AdmissibilityVerdict} [admissibility] - always present so callers can log.
|
|
29
|
+
* @property {*} [payload] - the underlying query result on status 'ok'.
|
|
30
|
+
* @property {string} [refusal] - human-readable summary on 'refused-admissibility'.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Wrap a query producer with the refuse-first posture. The producer is
|
|
35
|
+
* only called when admissibility passes (or passes-with-overrides);
|
|
36
|
+
* on refusal, its produce function does NOT run and the wrapper
|
|
37
|
+
* returns a refusal envelope naming the unresolved rules.
|
|
38
|
+
*
|
|
39
|
+
* @param {object} args
|
|
40
|
+
* @param {object} args.tree
|
|
41
|
+
* @param {string|null} [args.chainRulesetVersion]
|
|
42
|
+
* @param {AdmissibilityOverride[]} [args.overrides]
|
|
43
|
+
* @param {() => (Promise<*> | *)} args.produce - the underlying query
|
|
44
|
+
* @param {object} [args.opts] - passed through to enforceAdmissibility
|
|
45
|
+
* @returns {Promise<QueryResult>}
|
|
46
|
+
*/
|
|
47
|
+
export async function runWithAdmissibilityGate({
|
|
48
|
+
tree,
|
|
49
|
+
chainRulesetVersion = null,
|
|
50
|
+
overrides = [],
|
|
51
|
+
produce,
|
|
52
|
+
opts = {},
|
|
53
|
+
} = {}) {
|
|
54
|
+
const toolScope = await getRulesetToolScope();
|
|
55
|
+
if (!toolScope.traceabilityAndQueryTools) {
|
|
56
|
+
// Ruleset opted out of tool-scope gating (currently the artefact
|
|
57
|
+
// ships with this on -- item 1 addendum -- but the switch is
|
|
58
|
+
// read at runtime so a future ruleset revision can amend it).
|
|
59
|
+
const payload = await Promise.resolve(produce());
|
|
60
|
+
return { status: 'ok', payload };
|
|
61
|
+
}
|
|
62
|
+
const verdict = await enforceAdmissibility({ tree, chainRulesetVersion, overrides, opts });
|
|
63
|
+
if (verdict.verdict === 'refuse') {
|
|
64
|
+
const ruleIds = [...new Set(verdict.unresolved.map((f) => f.rule).filter(Boolean))].sort();
|
|
65
|
+
return {
|
|
66
|
+
status: 'refused-admissibility',
|
|
67
|
+
admissibility: verdict,
|
|
68
|
+
refusal: `traceability/query tool refused (NV-BL-SR-03 addendum): unresolved admissibility rules [${ruleIds.join(', ')}]. Fix or record a NV-BL-ADM-05 override before re-querying.`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const payload = await Promise.resolve(produce());
|
|
72
|
+
return { status: 'ok', admissibility: verdict, payload };
|
|
73
|
+
}
|