rcf-lite 0.0.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +288 -0
- package/LICENSE +202 -0
- package/README.md +92 -4
- package/bin/rcf-verify.js +122 -0
- package/bin/rcf.js +174 -0
- package/bin/view-supervisor-child.mjs +14 -0
- package/fixtures/canary-manifest.json +103 -0
- package/guidance/README.md +25 -0
- package/guidance/build-cycle-playbook.md +492 -0
- package/guidance/build-cycle.md +40 -0
- package/guidance/document-model.md +45 -0
- package/guidance/elicitation-playbook.md +442 -0
- package/guidance/harness-template.md +246 -0
- package/guidance/managed/README.md +63 -0
- package/guidance/managed/agent-instructions-block.hash +1 -0
- package/guidance/managed/agent-instructions-block.md +220 -0
- package/guidance/managed/legacy-fragment-hashes.json +22 -0
- package/guidance/manifest.json +21 -0
- package/guidance/overview.md +48 -0
- package/guidance/persona-programme.md +44 -0
- package/package.json +69 -8
- package/rcf/adrs/adr-001.json +25 -0
- package/rcf/adrs/adr-002.json +25 -0
- package/rcf/adrs/adr-003.json +25 -0
- package/rcf/adrs/adr-004.json +20 -0
- package/rcf/adrs/adr-005.json +20 -0
- package/rcf/adrs/adr-006.json +25 -0
- package/rcf/adrs/adr-007.json +25 -0
- package/rcf/adrs/adr-008.json +25 -0
- package/rcf/adrs/adr-009.json +25 -0
- package/rcf/build-sequence.json +11 -0
- package/rcf/code-nodes/cn-001.json +14 -0
- package/rcf/code-nodes/cn-002.json +14 -0
- package/rcf/code-nodes/cn-003.json +15 -0
- package/rcf/code-nodes/cn-004.json +17 -0
- package/rcf/code-nodes/cn-005.json +19 -0
- package/rcf/code-nodes/cn-006.json +17 -0
- package/rcf/code-nodes/cn-007.json +16 -0
- package/rcf/code-nodes/cn-008.json +19 -0
- package/rcf/code-nodes/cn-009.json +16 -0
- package/rcf/code-nodes/cn-010.json +18 -0
- package/rcf/code-nodes/cn-011.json +18 -0
- package/rcf/code-nodes/cn-012.json +18 -0
- package/rcf/code-nodes/cn-013.json +19 -0
- package/rcf/code-nodes/cn-014.json +16 -0
- package/rcf/code-nodes/cn-015.json +16 -0
- package/rcf/code-nodes/cn-016.json +16 -0
- package/rcf/code-nodes/cn-017.json +16 -0
- package/rcf/code-nodes/cn-018.json +15 -0
- package/rcf/code-nodes/cn-019.json +16 -0
- package/rcf/code-nodes/cn-020.json +16 -0
- package/rcf/code-nodes/cn-021.json +16 -0
- package/rcf/code-nodes/cn-022.json +16 -0
- package/rcf/code-nodes/cn-023.json +16 -0
- package/rcf/code-nodes/cn-024.json +16 -0
- package/rcf/code-nodes/cn-025.json +16 -0
- package/rcf/code-nodes/cn-026.json +16 -0
- package/rcf/code-nodes/cn-027.json +16 -0
- package/rcf/code-nodes/cn-028.json +16 -0
- package/rcf/code-nodes/cn-029.json +19 -0
- package/rcf/code-nodes/cn-030.json +14 -0
- package/rcf/code-nodes/cn-031.json +14 -0
- package/rcf/code-nodes/cn-032.json +14 -0
- package/rcf/code-nodes/cn-033.json +14 -0
- package/rcf/code-nodes/cn-034.json +14 -0
- package/rcf/code-nodes/cn-035.json +14 -0
- package/rcf/code-nodes/cn-036.json +14 -0
- package/rcf/code-nodes/cn-037.json +14 -0
- package/rcf/code-nodes/cn-038.json +14 -0
- package/rcf/code-nodes/cn-039.json +14 -0
- package/rcf/code-nodes/cn-040.json +14 -0
- package/rcf/code-nodes/cn-041.json +14 -0
- package/rcf/code-nodes/cn-042.json +14 -0
- package/rcf/code-nodes/cn-043.json +14 -0
- package/rcf/code-nodes/cn-044.json +14 -0
- package/rcf/code-nodes/cn-045.json +14 -0
- package/rcf/code-nodes/cn-046.json +14 -0
- package/rcf/code-nodes/cn-047.json +14 -0
- package/rcf/code-nodes/cn-048.json +14 -0
- package/rcf/code-nodes/cn-049.json +14 -0
- package/rcf/code-nodes/cn-050.json +14 -0
- package/rcf/code-nodes/cn-051.json +14 -0
- package/rcf/code-nodes/cn-052.json +14 -0
- package/rcf/code-nodes/cn-053.json +14 -0
- package/rcf/code-nodes/cn-054.json +14 -0
- package/rcf/code-nodes/cn-055.json +14 -0
- package/rcf/code-nodes/cn-056.json +14 -0
- package/rcf/code-nodes/cn-057.json +14 -0
- package/rcf/fbs/fbs-001.json +49 -0
- package/rcf/fbs/fbs-002.json +42 -0
- package/rcf/fbs/fbs-003.json +37 -0
- package/rcf/fbs/fbs-004.json +39 -0
- package/rcf/fbs/fbs-005.json +38 -0
- package/rcf/fbs/fbs-006.json +48 -0
- package/rcf/fbs/fbs-007.json +39 -0
- package/rcf/fbs/fbs-008.json +40 -0
- package/rcf/fbs/fbs-009.json +36 -0
- package/rcf/fbs/fbs-010.json +41 -0
- package/rcf/fbs/fbs-011.json +36 -0
- package/rcf/fbs/fbs-012.json +46 -0
- package/rcf/fbs/fbs-013.json +42 -0
- package/rcf/fbs/fbs-014.json +49 -0
- package/rcf/fbs/fbs-015.json +32 -0
- package/rcf/manifest.json +17 -0
- package/rcf/prd.json +47 -0
- package/rcf/requirements/req-001.json +19 -0
- package/rcf/requirements/req-002.json +19 -0
- package/rcf/requirements/req-003.json +19 -0
- package/rcf/requirements/req-004.json +19 -0
- package/rcf/requirements/req-005.json +19 -0
- package/rcf/requirements/req-006.json +19 -0
- package/rcf/requirements/req-007.json +19 -0
- package/rcf/requirements/req-008.json +19 -0
- package/rcf/requirements/req-009.json +19 -0
- package/rcf/tacs/tac-001.json +45 -0
- package/rcf/tacs/tac-002.json +109 -0
- package/rcf/tacs/tac-003.json +40 -0
- package/rcf/tacs/tac-004.json +51 -0
- package/rcf/tacs/tac-005.json +52 -0
- package/rcf/tacs/tac-006.json +104 -0
- package/rcf/tacs/tac-007.json +38 -0
- package/rcf/tacs/tac-008.json +51 -0
- package/rcf/tad.json +51 -0
- package/rcf/test-suites/PENDING.md +23 -0
- package/rcf/test-suites/ts-001.json +38 -0
- package/rcf/test-suites/ts-002.json +38 -0
- package/rcf/test-suites/ts-003.json +43 -0
- package/rcf/test-suites/ts-004.json +44 -0
- package/rcf/test-suites/ts-005.json +30 -0
- package/rcf/test-suites/ts-006.json +36 -0
- package/rcf/test-suites/ts-007.json +43 -0
- package/rcf/test-suites/ts-008.json +37 -0
- package/rcf/test-suites/ts-009.json +38 -0
- package/rcf/test-suites/ts-010.json +38 -0
- package/rcf/test-suites/ts-011.json +44 -0
- package/rcf/test-suites/ts-012.json +36 -0
- package/rcf/test-suites/ts-013.json +38 -0
- package/rcf/test-suites/ts-014.json +38 -0
- package/rcf/test-suites/ts-015.json +38 -0
- package/rcf/test-suites/ts-016.json +37 -0
- package/rcf/test-suites/ts-017.json +38 -0
- package/rcf/test-suites/ts-018.json +38 -0
- package/rcf/test-suites/ts-019.json +37 -0
- package/rcf/test-suites/ts-020.json +46 -0
- package/rcf/test-suites/ts-021.json +46 -0
- package/rcf/test-suites/ts-022.json +46 -0
- package/rcf/test-suites/ts-023.json +46 -0
- package/rcf/test-suites/ts-024.json +46 -0
- package/rcf/test-suites/ts-025.json +52 -0
- package/rcf/user-stories/us-101.json +40 -0
- package/rcf/user-stories/us-102.json +40 -0
- package/rcf/user-stories/us-201.json +40 -0
- package/rcf/user-stories/us-202.json +40 -0
- package/rcf/user-stories/us-203.json +32 -0
- package/rcf/user-stories/us-301.json +40 -0
- package/rcf/user-stories/us-302.json +40 -0
- package/rcf/user-stories/us-303.json +40 -0
- package/rcf/user-stories/us-304.json +40 -0
- package/rcf/user-stories/us-401.json +40 -0
- package/rcf/user-stories/us-402.json +40 -0
- package/rcf/user-stories/us-403.json +40 -0
- package/rcf/user-stories/us-501.json +40 -0
- package/rcf/user-stories/us-502.json +40 -0
- package/rcf/user-stories/us-503.json +40 -0
- package/rcf/user-stories/us-601.json +40 -0
- package/rcf/user-stories/us-602.json +40 -0
- package/rcf/user-stories/us-701.json +40 -0
- package/rcf/user-stories/us-702.json +40 -0
- package/rcf/user-stories/us-801.json +49 -0
- package/rcf/user-stories/us-802.json +49 -0
- package/rcf/user-stories/us-803.json +49 -0
- package/rcf/user-stories/us-804.json +49 -0
- package/rcf/user-stories/us-805.json +49 -0
- package/rcf/user-stories/us-901.json +40 -0
- package/src/.gitkeep +0 -0
- package/src/browser-verify/auth-smoke.js +109 -0
- package/src/browser-verify/index.js +29 -0
- package/src/browser-verify/invariants.js +336 -0
- package/src/browser-verify/manifest-writer.js +189 -0
- package/src/browser-verify/runner.js +171 -0
- package/src/build/bundle.js +198 -0
- package/src/build/formatters/json.js +18 -0
- package/src/build/formatters/markdown.js +406 -0
- package/src/build/index.js +14 -0
- package/src/build/mark.js +177 -0
- package/src/build/queue.js +285 -0
- package/src/cli/browser-verify.js +231 -0
- package/src/cli/build.js +584 -0
- package/src/cli/coverage.js +219 -0
- package/src/cli/create.js +355 -0
- package/src/cli/delete.js +127 -0
- package/src/cli/design.js +353 -0
- package/src/cli/doctor.js +548 -0
- package/src/cli/fbs.js +171 -0
- package/src/cli/finalise.js +377 -0
- package/src/cli/guidance.js +206 -0
- package/src/cli/help.js +156 -0
- package/src/cli/impact.js +119 -0
- package/src/cli/init.js +282 -0
- package/src/cli/intake.js +153 -0
- package/src/cli/link.js +128 -0
- package/src/cli/mcp.js +160 -0
- package/src/cli/preflight.js +220 -0
- package/src/cli/read.js +162 -0
- package/src/cli/req-baseline.js +269 -0
- package/src/cli/req-classify.js +135 -0
- package/src/cli/review.js +295 -0
- package/src/cli/test-suite.js +216 -0
- package/src/cli/trace.js +172 -0
- package/src/cli/ui-baseline.js +292 -0
- package/src/cli/ui-classify.js +108 -0
- package/src/cli/update.js +197 -0
- package/src/cli/validate.js +168 -0
- package/src/cli/view.js +452 -0
- package/src/core/baseline-catalog/data/auth.json +42 -0
- package/src/core/baseline-catalog/data/http-api.json +42 -0
- package/src/core/baseline-catalog/data/notifications.json +33 -0
- package/src/core/baseline-catalog/data/persistence.json +33 -0
- package/src/core/baseline-catalog/data/web-ui.json +60 -0
- package/src/core/baseline-catalog/index.js +121 -0
- package/src/core/errors/index.js +167 -0
- package/src/core/fixtures/register-canary/canary-prompt-01.json +18 -0
- package/src/core/fixtures/register-canary/canary-prompt-02.json +21 -0
- package/src/core/fixtures/register-canary/canary-prompt-03.json +17 -0
- package/src/core/isolation/index.js +60 -0
- package/src/core/mcp/framing.js +103 -0
- package/src/core/mcp/index.js +8 -0
- package/src/core/mcp/server.js +228 -0
- package/src/core/patterns/register-canary.js +209 -0
- package/src/core/patterns/req-shapes.js +158 -0
- package/src/core/patterns/services.js +358 -0
- package/src/core/patterns/ui-shapes.js +166 -0
- package/src/core/store/cn-resolve.js +134 -0
- package/src/core/store/derive-deps.js +93 -0
- package/src/core/store/ids.js +78 -0
- package/src/core/store/index.js +20 -0
- package/src/core/store/init.js +255 -0
- package/src/core/store/loader.js +211 -0
- package/src/core/store/tp-resolve.js +176 -0
- package/src/core/store/validator.js +191 -0
- package/src/core/store/walker.js +898 -0
- package/src/core/store/writer.js +1849 -0
- package/src/design/index.js +11 -0
- package/src/design/writer.js +271 -0
- package/src/finalise/detect.js +129 -0
- package/src/finalise/index.js +17 -0
- package/src/finalise/ingest.js +119 -0
- package/src/finalise/install.js +119 -0
- package/src/finalise/ship-without-verified.js +131 -0
- package/src/finalise/spawn.js +84 -0
- package/src/intake/fidelity.js +105 -0
- package/src/intake/index.js +6 -0
- package/src/intake/manifest-writer.js +100 -0
- package/src/intake/orchestrator.js +138 -0
- package/src/intake/validate.js +80 -0
- package/src/mcp/map-errors.js +131 -0
- package/src/mcp/prompts.js +49 -0
- package/src/mcp/resources.js +244 -0
- package/src/mcp/tools.js +1204 -0
- package/src/preflight/design-shapes.js +185 -0
- package/src/preflight/index.js +90 -0
- package/src/preflight/manifest-writer.js +264 -0
- package/src/preflight/scanner.js +206 -0
- package/src/preflight/secrets.js +134 -0
- package/src/preflight/session.js +246 -0
- package/src/query/attestation.js +285 -0
- package/src/query/coverage.js +308 -0
- package/src/query/formatters/json.js +21 -0
- package/src/query/formatters/mermaid.js +209 -0
- package/src/query/formatters/table.js +203 -0
- package/src/query/impact.js +173 -0
- package/src/query/index.js +9 -0
- package/src/query/trace.js +345 -0
- package/src/register-canary/fixture-loader.js +87 -0
- package/src/register-canary/index.js +10 -0
- package/src/register-canary/record-writer.js +132 -0
- package/src/register-canary/runner.js +156 -0
- package/src/req-baseline/gate.js +86 -0
- package/src/req-baseline/index.js +27 -0
- package/src/req-baseline/open-candidates.js +143 -0
- package/src/req-baseline/opt-out.js +195 -0
- package/src/req-baseline/sweep.js +230 -0
- package/src/req-detection/classifier.js +181 -0
- package/src/req-detection/index.js +9 -0
- package/src/req-detection/persist.js +55 -0
- package/src/review/index.js +325 -0
- package/src/review/mutation.js +117 -0
- package/src/review/ui-baseline-drift.js +138 -0
- package/src/server/index.js +178 -0
- package/src/server/routes.js +110 -0
- package/src/server/sse.js +118 -0
- package/src/setup/agent-setup.js +362 -0
- package/src/setup/identity-seed.js +104 -0
- package/src/setup/knowledge-seed.js +123 -0
- package/src/setup/managed-block.js +193 -0
- package/src/setup/managed-gitignore.js +166 -0
- package/src/setup/managed-markers.js +49 -0
- package/src/ui-baseline/defaults.js +119 -0
- package/src/ui-baseline/index.js +25 -0
- package/src/ui-baseline/manifest-writer.js +282 -0
- package/src/ui-baseline/session.js +178 -0
- package/src/ui-detection/classifier.js +192 -0
- package/src/verify/chain/index.js +190 -0
- package/src/verify/cli/cleanup.js +61 -0
- package/src/verify/cli/help.js +56 -0
- package/src/verify/cli/mcp.js +98 -0
- package/src/verify/cli/provision.js +71 -0
- package/src/verify/cli/report.js +71 -0
- package/src/verify/cli/run.js +155 -0
- package/src/verify/engine/brief.js +87 -0
- package/src/verify/engine/index.js +177 -0
- package/src/verify/engine/launcher.js +307 -0
- package/src/verify/mcp/tools.js +107 -0
- package/src/verify/profile/index.js +146 -0
- package/src/verify/provision/index.js +256 -0
- package/src/verify/report/index.js +139 -0
- package/src/verify/report/renderer.js +118 -0
- package/src/verify/verdict/index.js +246 -0
- package/src/view/doc-renderers/adr.js +44 -0
- package/src/view/doc-renderers/build-sequence.js +40 -0
- package/src/view/doc-renderers/fbs.js +128 -0
- package/src/view/doc-renderers/helpers.js +159 -0
- package/src/view/doc-renderers/index.js +12 -0
- package/src/view/doc-renderers/prd.js +45 -0
- package/src/view/doc-renderers/req.js +43 -0
- package/src/view/doc-renderers/tac.js +38 -0
- package/src/view/doc-renderers/tad.js +74 -0
- package/src/view/doc-renderers/test-suite.js +45 -0
- package/src/view/doc-renderers/user-story.js +63 -0
- package/src/view/html-page.js +462 -0
- package/src/view/index.js +63 -0
- package/src/view/live-client.js +338 -0
- package/src/view/mermaid-diagram.js +178 -0
- package/src/view/style.css +735 -0
- package/src/view/tree-model.js +152 -0
- package/src/view/vendored/mermaid.min.js +2607 -0
- package/src/view-supervisor/index.js +26 -0
- package/src/view-supervisor/logs.js +32 -0
- package/src/view-supervisor/manifest-writer.js +178 -0
- package/src/view-supervisor/persist-until.js +85 -0
- package/src/view-supervisor/supervisor.js +276 -0
- package/src/watch/index.js +152 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// Verdict taxonomy + aggregation (spec §5.1, §5.2). Mirrors the persona
|
|
2
|
+
// programme's PASS/BROKEN/DEGRADED/COSMETIC, plus the structural verdicts
|
|
3
|
+
// NOT-DEPLOYED (§4 refusal), BLOCKED (§6 unprovisionable), and LAUNCH-FAILURE
|
|
4
|
+
// (the verifier agent could not run or its output could not be ingested — a
|
|
5
|
+
// refusal to issue a verdict on the app, never a soft pass; see engine catch).
|
|
6
|
+
//
|
|
7
|
+
// Split verdicts are held split, NEVER averaged (§5.1): a run is BROKEN if
|
|
8
|
+
// ANY finding is BROKEN, regardless of how many ACs passed.
|
|
9
|
+
//
|
|
10
|
+
// 0.7.0 additions:
|
|
11
|
+
// - MOCK-ONLY-DECLARED (verification-integrity-cluster-spec §5.2): an AC
|
|
12
|
+
// whose aggregated service attestation resolves to `mocked` for at
|
|
13
|
+
// least one service. Verify has no live path to make it real-hit; the
|
|
14
|
+
// verdict is the honest alternative to a false PASS.
|
|
15
|
+
// - BLOCKED-BY-DECLARATION (verification-integrity-cluster-spec §5.2):
|
|
16
|
+
// an AC whose aggregated service attestation contains `declaredMockOnly`
|
|
17
|
+
// for at least one service. The operator declared mock-only at pre-flight;
|
|
18
|
+
// verify refuses to issue a live verdict and the chain records the
|
|
19
|
+
// decision.
|
|
20
|
+
// - UI-BASELINE-UNMET (ui-design-gate §8.7): a UI-bearing AC whose
|
|
21
|
+
// browser-verification record for a bound FBS came back `block`.
|
|
22
|
+
// - BROWSER-VERIFICATION-MISSING (ui-design-gate §8.7): a UI-bearing AC
|
|
23
|
+
// bound to an FBS with no browserVerification[] entry on the manifest.
|
|
24
|
+
//
|
|
25
|
+
// The four new classes are PER-AC verdicts emitted on the report's
|
|
26
|
+
// `perAcVerdicts[]` array. They do NOT replace the top-level `verdict`
|
|
27
|
+
// (which stays BROKEN/DEGRADED/… by finding severity + provisioning
|
|
28
|
+
// blocked); they run alongside so a finalise-gate consumer can refuse
|
|
29
|
+
// `verified` on an AC-level basis even when the run's aggregate is PASS.
|
|
30
|
+
|
|
31
|
+
import { rcfError } from '#core/errors';
|
|
32
|
+
|
|
33
|
+
/** Finding severities, low → high. */
|
|
34
|
+
export const FINDING_SEVERITIES = Object.freeze(['PASS', 'COSMETIC', 'DEGRADED', 'BROKEN']);
|
|
35
|
+
|
|
36
|
+
/** Severity rank for the split-not-averaged max and the severity gate. */
|
|
37
|
+
export const SEVERITY_ORDER = Object.freeze({ PASS: 0, COSMETIC: 1, DEGRADED: 2, BROKEN: 3 });
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* All overall-verdict classes: findings severities + the three structural
|
|
41
|
+
* verdicts. The 0.7.0 per-AC verdict classes are NOT in this set — they
|
|
42
|
+
* ride on `perAcVerdicts[]`, not the run-level `verdict` field. Keeping
|
|
43
|
+
* them out preserves backward compatibility on `validateReportShape`
|
|
44
|
+
* (§5.3) for existing consumers that only knew the pre-0.7.0 classes.
|
|
45
|
+
*/
|
|
46
|
+
export const VERDICTS = Object.freeze([...FINDING_SEVERITIES, 'NOT-DEPLOYED', 'BLOCKED', 'LAUNCH-FAILURE']);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Per-AC verdict classes emitted alongside the top-level verdict on
|
|
50
|
+
* `report.perAcVerdicts[]`. Consumed by `rcf finalise` to refuse promotion
|
|
51
|
+
* to `verified` on any of these AC-level verdicts (see
|
|
52
|
+
* `packages/rcf-lite/src/finalise/ingest.js:findMockOnlyDeclaredAcs`).
|
|
53
|
+
*/
|
|
54
|
+
export const PER_AC_VERDICTS = Object.freeze([
|
|
55
|
+
'MOCK-ONLY-DECLARED',
|
|
56
|
+
'BLOCKED-BY-DECLARATION',
|
|
57
|
+
'UI-BASELINE-UNMET',
|
|
58
|
+
'BROWSER-VERIFICATION-MISSING',
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Required fields on every finding (spec §5.2): the RCF payoff is that every
|
|
63
|
+
* defect maps to a contract line (acId / chain node), never a free-floating
|
|
64
|
+
* bug.
|
|
65
|
+
*
|
|
66
|
+
* @param {object} finding
|
|
67
|
+
* @returns {import('#core/errors').RcfError | null} error as data, or null if valid
|
|
68
|
+
*/
|
|
69
|
+
export function validateFinding(finding) {
|
|
70
|
+
if (!finding || typeof finding !== 'object') {
|
|
71
|
+
return rcfError({ kind: 'validation', message: 'finding must be an object' });
|
|
72
|
+
}
|
|
73
|
+
if (!FINDING_SEVERITIES.includes(finding.severity)) {
|
|
74
|
+
return rcfError({ kind: 'validation', message: `finding.severity must be one of ${FINDING_SEVERITIES.join('/')}`, field: 'severity' });
|
|
75
|
+
}
|
|
76
|
+
if (typeof finding.acId !== 'string' || finding.acId.length === 0) {
|
|
77
|
+
return rcfError({ kind: 'validation', message: 'finding.acId (chain-node reference) is required', field: 'acId' });
|
|
78
|
+
}
|
|
79
|
+
if (typeof finding.journey !== 'string' || finding.journey.length === 0) {
|
|
80
|
+
return rcfError({ kind: 'validation', message: 'finding.journey is required', field: 'journey' });
|
|
81
|
+
}
|
|
82
|
+
if (!Array.isArray(finding.reproSteps)) {
|
|
83
|
+
return rcfError({ kind: 'validation', message: 'finding.reproSteps must be an array', field: 'reproSteps' });
|
|
84
|
+
}
|
|
85
|
+
if (!finding.evidence || typeof finding.evidence !== 'object') {
|
|
86
|
+
return rcfError({ kind: 'validation', message: 'finding.evidence must be an object', field: 'evidence' });
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The worst (max) severity across findings. Empty → PASS. This is the
|
|
93
|
+
* split-not-averaged rule: the single worst finding drives the class.
|
|
94
|
+
*
|
|
95
|
+
* @param {Array<{severity: string}>} findings
|
|
96
|
+
* @returns {'PASS'|'COSMETIC'|'DEGRADED'|'BROKEN'}
|
|
97
|
+
*/
|
|
98
|
+
export function aggregateSeverity(findings = []) {
|
|
99
|
+
let worst = 'PASS';
|
|
100
|
+
for (const f of findings) {
|
|
101
|
+
if ((SEVERITY_ORDER[f.severity] ?? -1) > SEVERITY_ORDER[worst]) worst = f.severity;
|
|
102
|
+
}
|
|
103
|
+
return worst;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The overall run verdict (spec §5.1). NOT-DEPLOYED and a fully-blocked run
|
|
108
|
+
* are structural verdicts; otherwise the worst finding severity wins
|
|
109
|
+
* (split-not-averaged). A run with SOME findings and SOME blocked ACs is a
|
|
110
|
+
* partial verification: the verdict reflects what WAS exercised, and the
|
|
111
|
+
* blocked ACs are named separately in the report.
|
|
112
|
+
*
|
|
113
|
+
* @param {object} opts
|
|
114
|
+
* @param {Array<{severity: string}>} [opts.findings]
|
|
115
|
+
* @param {Array<object>} [opts.blockedAcs]
|
|
116
|
+
* @param {boolean} [opts.notDeployed]
|
|
117
|
+
* @returns {string}
|
|
118
|
+
*/
|
|
119
|
+
export function aggregateVerdict({ findings = [], blockedAcs = [], notDeployed = false } = {}) {
|
|
120
|
+
if (notDeployed) return 'NOT-DEPLOYED';
|
|
121
|
+
if (findings.length === 0 && blockedAcs.length > 0) return 'BLOCKED';
|
|
122
|
+
return aggregateSeverity(findings);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Whether the severity gate is tripped → the process exits non-zero
|
|
127
|
+
* (spec §3 rule 5, §8.2). NOT-DEPLOYED, BLOCKED and LAUNCH-FAILURE always trip
|
|
128
|
+
* (ship cannot be confirmed); otherwise the worst finding severity is compared
|
|
129
|
+
* against the gate. With no gate configured, nothing trips — the report is
|
|
130
|
+
* still written.
|
|
131
|
+
*
|
|
132
|
+
* @param {object} opts
|
|
133
|
+
* @param {string} opts.verdict
|
|
134
|
+
* @param {Array<{severity: string}>} [opts.findings]
|
|
135
|
+
* @param {string|null} [opts.gate] - one of FINDING_SEVERITIES, or null/undefined
|
|
136
|
+
* @returns {boolean}
|
|
137
|
+
*/
|
|
138
|
+
export function gateTripped({ verdict, findings = [], gate }) {
|
|
139
|
+
if (verdict === 'NOT-DEPLOYED' || verdict === 'BLOCKED' || verdict === 'LAUNCH-FAILURE') return true;
|
|
140
|
+
if (!gate) return false;
|
|
141
|
+
const worst = aggregateSeverity(findings);
|
|
142
|
+
return SEVERITY_ORDER[worst] >= SEVERITY_ORDER[gate];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Resolve the Track A per-AC verdict from an AC's aggregated service
|
|
147
|
+
* attestations. Priority: `declaredMockOnly` wins over `mocked`, because
|
|
148
|
+
* a chain that explicitly declared mock-only overrides an implicit mock.
|
|
149
|
+
* `live` / `sandboxed` / `notShipped` never emit a per-AC verdict here —
|
|
150
|
+
* they either mean a live path exists (verify's normal findings pipeline
|
|
151
|
+
* handles them) or the AC does not gate ship at all.
|
|
152
|
+
*
|
|
153
|
+
* @param {Array<{serviceId: string, attestationMode: string}>} attestations
|
|
154
|
+
* @returns {{ verdict: 'MOCK-ONLY-DECLARED'|'BLOCKED-BY-DECLARATION', reason: string } | null}
|
|
155
|
+
*/
|
|
156
|
+
export function attestationPerAcVerdict(attestations = []) {
|
|
157
|
+
if (!Array.isArray(attestations) || attestations.length === 0) return null;
|
|
158
|
+
const declared = attestations.find((a) => a && a.attestationMode === 'declaredMockOnly');
|
|
159
|
+
if (declared) {
|
|
160
|
+
return {
|
|
161
|
+
verdict: 'BLOCKED-BY-DECLARATION',
|
|
162
|
+
reason: `operator declared mock-only at pre-flight for service ${declared.serviceId}; verify refused to issue a live verdict rather than fabricate a PASS.`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const mocked = attestations.find((a) => a && a.attestationMode === 'mocked');
|
|
166
|
+
if (mocked) {
|
|
167
|
+
return {
|
|
168
|
+
verdict: 'MOCK-ONLY-DECLARED',
|
|
169
|
+
reason: `service ${mocked.serviceId} is chain-attested \`mocked\`; verify has no live path to the third party and no observable delivery record on the running app.`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Resolve the Track B per-AC UI verdict for an AC. UI verdicts fire only
|
|
177
|
+
* for `fbsUiBearing: true` ACs (the chain-derived flag from
|
|
178
|
+
* `packages/rcf-lite/src/verify/chain/index.js`). Priority:
|
|
179
|
+
* 1. Any bound FBS has NO browserVerification entry → BROWSER-VERIFICATION-MISSING.
|
|
180
|
+
* 2. Any bound FBS's browserVerification.verdict is `block` → UI-BASELINE-UNMET.
|
|
181
|
+
* 3. Otherwise no per-AC UI verdict is emitted.
|
|
182
|
+
*
|
|
183
|
+
* @param {object} ac - a flattened AC with `fbsUiBearing` and `fbsIds`
|
|
184
|
+
* @param {object[]} [browserVerification] - manifest.browserVerification[]
|
|
185
|
+
* @returns {{ verdict: 'UI-BASELINE-UNMET'|'BROWSER-VERIFICATION-MISSING', reason: string } | null}
|
|
186
|
+
*/
|
|
187
|
+
export function uiPerAcVerdict(ac, browserVerification = []) {
|
|
188
|
+
if (!ac || ac.fbsUiBearing !== true) return null;
|
|
189
|
+
const fbsIds = Array.isArray(ac.fbsIds) ? ac.fbsIds : [];
|
|
190
|
+
if (fbsIds.length === 0) return null;
|
|
191
|
+
const bv = Array.isArray(browserVerification) ? browserVerification : [];
|
|
192
|
+
const missing = [];
|
|
193
|
+
const blocked = [];
|
|
194
|
+
for (const fbsId of fbsIds) {
|
|
195
|
+
const records = bv.filter((r) => r && r.fbsId === fbsId);
|
|
196
|
+
if (records.length === 0) {
|
|
197
|
+
missing.push(fbsId);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
// Take the freshest record — records land per-run and the latest verdict
|
|
201
|
+
// is the one that gates ship. `createdAt` sorts lexicographically for ISO
|
|
202
|
+
// timestamps; when it is missing fall back to array order (the last
|
|
203
|
+
// written entry wins).
|
|
204
|
+
const latest = [...records].sort((a, b) => (a.createdAt ?? '').localeCompare(b.createdAt ?? '')).pop();
|
|
205
|
+
if (latest && latest.verdict === 'block') blocked.push({ fbsId, id: latest.id ?? null });
|
|
206
|
+
}
|
|
207
|
+
if (missing.length > 0) {
|
|
208
|
+
return {
|
|
209
|
+
verdict: 'BROWSER-VERIFICATION-MISSING',
|
|
210
|
+
reason: `UI-bearing FBS(es) ${missing.join(', ')} have no browserVerification record on the manifest; verify has nothing to read for the baseline check.`,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (blocked.length > 0) {
|
|
214
|
+
const detail = blocked.map((b) => `${b.fbsId}${b.id ? ` (${b.id})` : ''}`).join(', ');
|
|
215
|
+
return {
|
|
216
|
+
verdict: 'UI-BASELINE-UNMET',
|
|
217
|
+
reason: `browserVerification recorded verdict \`block\` for ${detail}; the deployed UI failed at least one baseline invariant.`,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Emit per-AC verdicts across the whole chain. Combines Track A's service
|
|
225
|
+
* attestation verdicts with Track B's UI-baseline verdicts. An AC can carry
|
|
226
|
+
* BOTH a service-attestation verdict AND a UI verdict (a UI-bearing FBS
|
|
227
|
+
* that also depends on a mocked service produces two per-AC entries — one
|
|
228
|
+
* per class). This matches the finalise gate contract in
|
|
229
|
+
* `packages/rcf-lite/src/finalise/ingest.js:findMockOnlyDeclaredAcs`, which
|
|
230
|
+
* filters on verdict class and does not deduplicate by acId.
|
|
231
|
+
*
|
|
232
|
+
* @param {object} opts
|
|
233
|
+
* @param {Array<object>} opts.acs - flattened ACs from `readChain`
|
|
234
|
+
* @param {object[]} [opts.browserVerification] - manifest.browserVerification[]
|
|
235
|
+
* @returns {Array<{ acId: string, verdict: string, reason: string }>}
|
|
236
|
+
*/
|
|
237
|
+
export function derivePerAcVerdicts({ acs = [], browserVerification = [] } = {}) {
|
|
238
|
+
const out = [];
|
|
239
|
+
for (const ac of acs) {
|
|
240
|
+
const attest = attestationPerAcVerdict(ac.serviceAttestations);
|
|
241
|
+
if (attest) out.push({ acId: ac.acId, verdict: attest.verdict, reason: attest.reason });
|
|
242
|
+
const ui = uiPerAcVerdict(ac, browserVerification);
|
|
243
|
+
if (ui) out.push({ acId: ac.acId, verdict: ui.verdict, reason: ui.reason });
|
|
244
|
+
}
|
|
245
|
+
return out;
|
|
246
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// ADR renderer.
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
anchorIdFor,
|
|
5
|
+
brokenBanner,
|
|
6
|
+
docLink,
|
|
7
|
+
docLinkList,
|
|
8
|
+
escapeHtml,
|
|
9
|
+
fieldPara,
|
|
10
|
+
rawJsonDisclosure,
|
|
11
|
+
} from './helpers.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {object} adr
|
|
15
|
+
* @param {object} ctx
|
|
16
|
+
* @param {string|undefined} ctx.raw
|
|
17
|
+
* @param {import('#core/errors').RcfError[]} [ctx.errors]
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
export function renderAdr(adr, ctx) {
|
|
21
|
+
if (!adr) return '';
|
|
22
|
+
const anchor = anchorIdFor(adr.adrId ?? 'ADR');
|
|
23
|
+
const broken = ctx.errors?.length ? brokenBanner(ctx.errors) : '';
|
|
24
|
+
const alts = (adr.alternativesConsidered ?? []).map((a) => `<li><strong>${escapeHtml(a.name ?? '')}</strong> - ${escapeHtml(a.summary ?? '')}<br/><em>Not chosen because:</em> ${escapeHtml(a.reasonNotChosen ?? '')}</li>`).join('');
|
|
25
|
+
const supersededBy = adr.supersededBy
|
|
26
|
+
? `<p><strong>Superseded by:</strong> ${docLink(adr.supersededBy)}</p>`
|
|
27
|
+
: '';
|
|
28
|
+
const related = Array.isArray(adr.relatedAdrs) && adr.relatedAdrs.length > 0
|
|
29
|
+
? `<p><strong>Related ADRs:</strong> ${docLinkList(adr.relatedAdrs)}</p>`
|
|
30
|
+
: '';
|
|
31
|
+
return `
|
|
32
|
+
<article id="${anchor}" class="doc doc-adr">
|
|
33
|
+
<h3>${escapeHtml(adr.adrId ?? 'ADR')} - ${escapeHtml(adr.title ?? '')}</h3>
|
|
34
|
+
${broken}
|
|
35
|
+
${fieldPara('Status', adr.status)}
|
|
36
|
+
${fieldPara('Context', adr.context)}
|
|
37
|
+
${fieldPara('Decision', adr.decision)}
|
|
38
|
+
${fieldPara('Consequences', adr.consequences)}
|
|
39
|
+
${alts ? `<section class="field-list"><h4>Alternatives considered</h4><ul>${alts}</ul></section>` : ''}
|
|
40
|
+
${supersededBy}
|
|
41
|
+
${related}
|
|
42
|
+
${rawJsonDisclosure(ctx.raw, adr, adr.adrId)}
|
|
43
|
+
</article>`.trim();
|
|
44
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Build Sequence renderer. Post-3.7 (D15) the ordered FBS slot list is not
|
|
2
|
+
// read from the removed `bs.fbs[]` array but computed by the caller and
|
|
3
|
+
// passed via `ctx.slots` -- a list of `{ fbsId, buildOrder, executionStatus,
|
|
4
|
+
// title? }` sorted by buildOrder ascending.
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
anchorIdFor,
|
|
8
|
+
brokenBanner,
|
|
9
|
+
docLink,
|
|
10
|
+
escapeHtml,
|
|
11
|
+
fieldPara,
|
|
12
|
+
rawJsonDisclosure,
|
|
13
|
+
} from './helpers.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {object} bs
|
|
17
|
+
* @param {object} ctx
|
|
18
|
+
* @param {string|undefined} ctx.raw
|
|
19
|
+
* @param {import('#core/errors').RcfError[]} [ctx.errors]
|
|
20
|
+
* @param {Array<{ fbsId: string, buildOrder: number, executionStatus?: string, title?: string }>} [ctx.slots]
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
export function renderBuildSequence(bs, ctx) {
|
|
24
|
+
if (!bs) return '';
|
|
25
|
+
const anchor = anchorIdFor(bs.bsId ?? 'BS');
|
|
26
|
+
const broken = ctx.errors?.length ? brokenBanner(ctx.errors) : '';
|
|
27
|
+
const slots = Array.isArray(ctx.slots)
|
|
28
|
+
? [...ctx.slots].sort((a, b) => (a.buildOrder ?? 0) - (b.buildOrder ?? 0))
|
|
29
|
+
: [];
|
|
30
|
+
const slotList = slots.map((s) => `<li><strong>${s.buildOrder ?? '?'}.</strong> ${docLink(s.fbsId)} - <span class="status ${escapeHtml(s.executionStatus ?? '')}">${escapeHtml(s.executionStatus ?? 'unknown')}</span>${s.title ? ` - ${escapeHtml(s.title)}` : ''}</li>`).join('');
|
|
31
|
+
return `
|
|
32
|
+
<article id="${anchor}" class="doc doc-bs">
|
|
33
|
+
<h3>${escapeHtml(bs.bsId ?? 'BS')} - ${escapeHtml(bs.title ?? 'Build sequence')}</h3>
|
|
34
|
+
${broken}
|
|
35
|
+
${fieldPara('Build philosophy', bs.buildPhilosophy)}
|
|
36
|
+
${fieldPara('Generation strategy', bs.generationStrategy)}
|
|
37
|
+
<section class="field-list"><h4>FBS slots</h4><ol>${slotList}</ol></section>
|
|
38
|
+
${rawJsonDisclosure(ctx.raw, bs, bs.bsId)}
|
|
39
|
+
</article>`.trim();
|
|
40
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// FBS renderer. Resolves AC ids into their Given/When/Then text rather
|
|
2
|
+
// than rendering just the id, so an owner reviewing an FBS section can read
|
|
3
|
+
// what each AC requires without jumping back to the User stories area.
|
|
4
|
+
// Phase 3.2: `acIds` are also rendered as clickable pills at the top so the
|
|
5
|
+
// operator can jump directly across into Requirements-tab context (D8).
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
anchorIdFor,
|
|
9
|
+
brokenBanner,
|
|
10
|
+
docLink,
|
|
11
|
+
docLinkList,
|
|
12
|
+
escapeHtml,
|
|
13
|
+
fieldList,
|
|
14
|
+
fieldPara,
|
|
15
|
+
rawJsonDisclosure,
|
|
16
|
+
} from './helpers.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {object} fbs
|
|
20
|
+
* @param {object} ctx
|
|
21
|
+
* @param {string|undefined} ctx.raw
|
|
22
|
+
* @param {import('#core/errors').RcfError[]} [ctx.errors]
|
|
23
|
+
* @param {Map<string, object>} [ctx.usByAcId]
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
export function renderFbs(fbs, ctx) {
|
|
27
|
+
if (!fbs) return '';
|
|
28
|
+
const anchor = anchorIdFor(fbs.fbsId ?? 'FBS');
|
|
29
|
+
const broken = ctx.errors?.length ? brokenBanner(ctx.errors) : '';
|
|
30
|
+
const acPills = renderAcPills(fbs.acIds ?? []);
|
|
31
|
+
const acBlocks = (fbs.acIds ?? []).map((acId) => renderResolvedAc(acId, ctx)).join('\n');
|
|
32
|
+
const ctxReq = fbs.contextRequirements ?? {};
|
|
33
|
+
const ctxBlocks = renderContextRequirements(ctxReq);
|
|
34
|
+
const designBlock = renderDesignBlock(fbs);
|
|
35
|
+
const designSlot = designBlock ? `\n ${designBlock}` : '';
|
|
36
|
+
return `
|
|
37
|
+
<article id="${anchor}" class="doc doc-fbs">
|
|
38
|
+
<h3>${escapeHtml(fbs.fbsId ?? 'FBS')} - ${escapeHtml(fbs.title ?? '')}</h3>
|
|
39
|
+
${broken}
|
|
40
|
+
${fieldPara('Summary', fbs.summary)}
|
|
41
|
+
${fieldPara('Approach', fbs.approach)}
|
|
42
|
+
${acPills}
|
|
43
|
+
<section class="field-list"><h4>Acceptance criteria delivered</h4>${acBlocks}</section>
|
|
44
|
+
${ctxBlocks}${designSlot}
|
|
45
|
+
${fbs.dependsOnFbsIds?.length ? `<section class="field-list"><h4>Depends on</h4><p>${docLinkList(fbs.dependsOnFbsIds)}</p></section>` : ''}
|
|
46
|
+
${fieldPara('Estimated size', fbs.estimatedSize)}
|
|
47
|
+
${fieldPara('Estimated hours', fbs.estimatedHours)}
|
|
48
|
+
${fieldList('Deliverables', fbs.deliverables)}
|
|
49
|
+
${fieldPara('Risk level', fbs.riskLevel)}
|
|
50
|
+
${fieldPara('Build order', fbs.buildOrder)}
|
|
51
|
+
${fieldPara('Execution status', fbs.executionStatus)}
|
|
52
|
+
${fieldPara('Notes', fbs.notes)}
|
|
53
|
+
${rawJsonDisclosure(ctx.raw, fbs, fbs.fbsId)}
|
|
54
|
+
</article>`.trim();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Design substage block (Track B, ui-design-gate-0.7.0-spec §5.5
|
|
59
|
+
* "Honest cost" list: FBS view renderer surfaces designStageComplete +
|
|
60
|
+
* the designStage artefacts). Renders nothing for non-UI FBS.
|
|
61
|
+
*
|
|
62
|
+
* @param {object} fbs
|
|
63
|
+
* @returns {string}
|
|
64
|
+
*/
|
|
65
|
+
function renderDesignBlock(fbs) {
|
|
66
|
+
if (fbs?.uiBearing !== true && !fbs?.designStage && fbs?.designStageComplete !== true) return '';
|
|
67
|
+
const stage = fbs.designStage ?? {};
|
|
68
|
+
const journeyCount = Array.isArray(stage.journeys) ? stage.journeys.length : 0;
|
|
69
|
+
const navShape = stage.navModel?.shape ?? '(none)';
|
|
70
|
+
const routeCount = Array.isArray(stage.navModel?.routes) ? stage.navModel.routes.length : 0;
|
|
71
|
+
const themeMode = stage.themeAndA11y?.themeMode ?? '(none)';
|
|
72
|
+
const complete = fbs?.designStageComplete === true;
|
|
73
|
+
return `<section class="field-list"><h4>Design substage</h4>
|
|
74
|
+
<p><strong>UI-bearing:</strong> ${escapeHtml(String(fbs?.uiBearing ?? false))}</p>
|
|
75
|
+
<p><strong>Design stage complete:</strong> ${escapeHtml(String(complete))}</p>
|
|
76
|
+
<p><strong>Journeys:</strong> ${escapeHtml(String(journeyCount))}</p>
|
|
77
|
+
<p><strong>Nav model:</strong> ${escapeHtml(String(navShape))} (${escapeHtml(String(routeCount))} route(s))</p>
|
|
78
|
+
<p><strong>Theme and a11y:</strong> ${escapeHtml(String(themeMode))}</p>
|
|
79
|
+
</section>`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function renderAcPills(acIds) {
|
|
83
|
+
if (!Array.isArray(acIds) || acIds.length === 0) return '';
|
|
84
|
+
const pills = acIds.map((id) => `<a class="ac-pill" href="#${escapeHtml(id)}">${escapeHtml(id)}</a>`).join('');
|
|
85
|
+
return `<div class="ac-pills">${pills}</div>`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function renderResolvedAc(acId, ctx) {
|
|
89
|
+
const us = ctx.usByAcId?.get(acId);
|
|
90
|
+
const ac = us?.acceptanceCriteria?.find((a) => a.id === acId);
|
|
91
|
+
if (!ac) {
|
|
92
|
+
return `<div class="ac-resolved broken"><p><strong>${escapeHtml(acId)}</strong> (unresolved)</p></div>`;
|
|
93
|
+
}
|
|
94
|
+
return `
|
|
95
|
+
<div class="ac-resolved">
|
|
96
|
+
<p><strong>${docLink(acId)}</strong> - ${escapeHtml(ac.description ?? '')}</p>
|
|
97
|
+
${ac.given ? `<p><em>Given</em> ${escapeHtml(ac.given)}</p>` : ''}
|
|
98
|
+
${ac.when ? `<p><em>When</em> ${escapeHtml(ac.when)}</p>` : ''}
|
|
99
|
+
${ac.then ? `<p><em>Then</em> ${escapeHtml(ac.then)}</p>` : ''}
|
|
100
|
+
</div>`.trim();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function renderContextRequirements(ctx) {
|
|
104
|
+
const parts = [];
|
|
105
|
+
if (Array.isArray(ctx.tadSections) && ctx.tadSections.length > 0) {
|
|
106
|
+
parts.push(`<p><strong>TAD sections:</strong> ${ctx.tadSections.map((s) => escapeHtml(s)).join(', ')}</p>`);
|
|
107
|
+
}
|
|
108
|
+
if (Array.isArray(ctx.tacIds) && ctx.tacIds.length > 0) {
|
|
109
|
+
parts.push(`<p><strong>TACs:</strong> ${docLinkList(ctx.tacIds)}</p>`);
|
|
110
|
+
}
|
|
111
|
+
if (Array.isArray(ctx.adrIds) && ctx.adrIds.length > 0) {
|
|
112
|
+
parts.push(`<p><strong>ADRs:</strong> ${docLinkList(ctx.adrIds)}</p>`);
|
|
113
|
+
}
|
|
114
|
+
if (Array.isArray(ctx.schemas) && ctx.schemas.length > 0) {
|
|
115
|
+
parts.push(`<p><strong>Schemas:</strong> ${ctx.schemas.map((s) => `<code>${escapeHtml(s)}</code>`).join(', ')}</p>`);
|
|
116
|
+
}
|
|
117
|
+
if (Array.isArray(ctx.externalDocs) && ctx.externalDocs.length > 0) {
|
|
118
|
+
parts.push(`<p><strong>External docs:</strong> ${ctx.externalDocs.map((s) => `<code>${escapeHtml(s)}</code>`).join(', ')}</p>`);
|
|
119
|
+
}
|
|
120
|
+
if (Array.isArray(ctx.existingModules) && ctx.existingModules.length > 0) {
|
|
121
|
+
parts.push(`<p><strong>Existing modules:</strong> ${ctx.existingModules.map((s) => `<code>${escapeHtml(s)}</code>`).join(', ')}</p>`);
|
|
122
|
+
}
|
|
123
|
+
if (Array.isArray(ctx.other) && ctx.other.length > 0) {
|
|
124
|
+
parts.push(`<p><strong>Other:</strong> ${ctx.other.map((s) => escapeHtml(s)).join(', ')}</p>`);
|
|
125
|
+
}
|
|
126
|
+
if (parts.length === 0) return '';
|
|
127
|
+
return `<section class="field-list"><h4>Context requirements</h4>${parts.join('\n')}</section>`;
|
|
128
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Small helpers shared across the per-document renderers. Vanilla template
|
|
2
|
+
// strings; no template engine.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* HTML-escape a string for safe injection between tags.
|
|
6
|
+
*
|
|
7
|
+
* @param {unknown} value
|
|
8
|
+
* @returns {string}
|
|
9
|
+
*/
|
|
10
|
+
export function escapeHtml(value) {
|
|
11
|
+
if (value === null || value === undefined) return '';
|
|
12
|
+
const s = String(value);
|
|
13
|
+
return s
|
|
14
|
+
.replace(/&/g, '&')
|
|
15
|
+
.replace(/</g, '<')
|
|
16
|
+
.replace(/>/g, '>')
|
|
17
|
+
.replace(/"/g, '"')
|
|
18
|
+
.replace(/'/g, ''');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Return the raw document id as an anchor id. Phase 3.2 unified the anchor
|
|
23
|
+
* convention around the display id (e.g. "REQ-002") so Mermaid click targets
|
|
24
|
+
* and internal doc links resolve to the same DOM node.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} id
|
|
27
|
+
* @returns {string}
|
|
28
|
+
*/
|
|
29
|
+
export function anchorIdFor(id) {
|
|
30
|
+
return String(id);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Render an `<a>` link to the section of another document.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} id
|
|
37
|
+
* @param {string} [label]
|
|
38
|
+
* @returns {string}
|
|
39
|
+
*/
|
|
40
|
+
export function docLink(id, label) {
|
|
41
|
+
return `<a href="#${anchorIdFor(id)}">${escapeHtml(label ?? id)}</a>`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Render a list of ids as comma-separated doc links.
|
|
46
|
+
*
|
|
47
|
+
* @param {string[] | undefined} ids
|
|
48
|
+
* @returns {string}
|
|
49
|
+
*/
|
|
50
|
+
export function docLinkList(ids) {
|
|
51
|
+
if (!Array.isArray(ids) || ids.length === 0) return '<em>none</em>';
|
|
52
|
+
return ids.map((id) => docLink(id)).join(', ');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Render a paragraph if value is present.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} label
|
|
59
|
+
* @param {unknown} value
|
|
60
|
+
* @returns {string}
|
|
61
|
+
*/
|
|
62
|
+
export function fieldPara(label, value) {
|
|
63
|
+
if (value === undefined || value === null || value === '') return '';
|
|
64
|
+
return `<p><strong>${escapeHtml(label)}:</strong> ${escapeHtml(value)}</p>`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Render an unordered list if items are present.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} label
|
|
71
|
+
* @param {string[] | undefined} items
|
|
72
|
+
* @returns {string}
|
|
73
|
+
*/
|
|
74
|
+
export function fieldList(label, items) {
|
|
75
|
+
if (!Array.isArray(items) || items.length === 0) return '';
|
|
76
|
+
const li = items.map((s) => `<li>${escapeHtml(s)}</li>`).join('');
|
|
77
|
+
return `<section class="field-list"><h4>${escapeHtml(label)}</h4><ul>${li}</ul></section>`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Render the "Show raw JSON" disclosure block for a document.
|
|
82
|
+
*
|
|
83
|
+
* Phase 3.8 D13b: the raw-JSON disclosure now carries a stable
|
|
84
|
+
* `data-doc-id="{parentDocId}::raw"` attribute so the live-client can
|
|
85
|
+
* persist its open state across SSE swaps and page reloads. The main
|
|
86
|
+
* doc-details already get a `data-doc-id` via `detailsWrap`; this closes
|
|
87
|
+
* the last state-persistence gap.
|
|
88
|
+
*
|
|
89
|
+
* @param {string|undefined} raw
|
|
90
|
+
* @param {object} doc
|
|
91
|
+
* @param {string} parentDocId - the enclosing doc's display id (e.g. "REQ-002")
|
|
92
|
+
* @returns {string}
|
|
93
|
+
*/
|
|
94
|
+
export function rawJsonDisclosure(raw, doc, parentDocId) {
|
|
95
|
+
const json = raw ?? JSON.stringify(doc, null, 2);
|
|
96
|
+
const rawId = `${parentDocId ?? 'doc'}::raw`;
|
|
97
|
+
return `<details class="raw-json" data-doc-id="${escapeHtml(rawId)}"><summary>Show raw JSON</summary><pre>${escapeHtml(json)}</pre></details>`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Render a broken-document banner above the rest of a document section.
|
|
102
|
+
*
|
|
103
|
+
* @param {import('#core/errors').RcfError[]} errors
|
|
104
|
+
* @returns {string}
|
|
105
|
+
*/
|
|
106
|
+
export function brokenBanner(errors) {
|
|
107
|
+
if (!errors || errors.length === 0) return '';
|
|
108
|
+
const items = errors
|
|
109
|
+
.map((e) => `<li><code>${escapeHtml(e.kind)}</code> ${escapeHtml(e.message)}</li>`)
|
|
110
|
+
.join('');
|
|
111
|
+
return `
|
|
112
|
+
<aside class="broken" role="alert">
|
|
113
|
+
<p><strong>Broken document</strong> - schema validation or reference failure.</p>
|
|
114
|
+
<ul>${items}</ul>
|
|
115
|
+
</aside>`.trim();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Render a broken-reference placeholder when a referenced id has no file.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} id
|
|
122
|
+
* @returns {string}
|
|
123
|
+
*/
|
|
124
|
+
export function brokenReferenceSection(id) {
|
|
125
|
+
const anchor = anchorIdFor(id);
|
|
126
|
+
return `
|
|
127
|
+
<article id="${anchor}" class="doc broken-doc">
|
|
128
|
+
<h3>${escapeHtml(id)} - broken reference</h3>
|
|
129
|
+
<aside class="broken" role="alert">
|
|
130
|
+
<p>Referenced by a parent document but no file was found at the expected path.</p>
|
|
131
|
+
</aside>
|
|
132
|
+
</article>`.trim();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Wrap a doc's rendered body in a `<details data-doc-id>` so it can be
|
|
137
|
+
* drilled-in from the tab tree. Closed by default. The `data-doc-id`
|
|
138
|
+
* attribute is what the hash-routing script targets.
|
|
139
|
+
*
|
|
140
|
+
* @param {object} args
|
|
141
|
+
* @param {string} args.id - display doc id (e.g. "REQ-001")
|
|
142
|
+
* @param {string} args.summary - short label shown in the summary line
|
|
143
|
+
* @param {string} args.className - class applied to the details wrapper
|
|
144
|
+
* @param {string} args.body - the rendered body HTML
|
|
145
|
+
* @param {string} [args.status] - optional doc status, rendered as a pill
|
|
146
|
+
* @returns {string}
|
|
147
|
+
*/
|
|
148
|
+
export function detailsWrap({
|
|
149
|
+
id, summary, className, body, status,
|
|
150
|
+
}) {
|
|
151
|
+
const statusPill = status
|
|
152
|
+
? `<span class="status ${escapeHtml(status)}">${escapeHtml(status)}</span>`
|
|
153
|
+
: '';
|
|
154
|
+
return `
|
|
155
|
+
<details class="doc-details ${className}" data-doc-id="${escapeHtml(id)}">
|
|
156
|
+
<summary><span class="summary-label">${escapeHtml(summary)}</span>${statusPill}</summary>
|
|
157
|
+
${body}
|
|
158
|
+
</details>`.trim();
|
|
159
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Doc renderer index. Re-exports each per-type renderer so html-page.js can
|
|
2
|
+
// import them through a single module.
|
|
3
|
+
|
|
4
|
+
export { renderPrd } from './prd.js';
|
|
5
|
+
export { renderReq } from './req.js';
|
|
6
|
+
export { renderUserStory } from './user-story.js';
|
|
7
|
+
export { renderTad } from './tad.js';
|
|
8
|
+
export { renderTac } from './tac.js';
|
|
9
|
+
export { renderAdr } from './adr.js';
|
|
10
|
+
export { renderBuildSequence } from './build-sequence.js';
|
|
11
|
+
export { renderFbs } from './fbs.js';
|
|
12
|
+
export { renderTestSuite } from './test-suite.js';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// PRD renderer. Curated fields per spec D11. Post-3.7 (D15) the child REQ
|
|
2
|
+
// list is not read from `prd.requirementIds` (removed in 0.2.0) but from
|
|
3
|
+
// the computed `childrenByParent` map on the tree model.
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
anchorIdFor,
|
|
7
|
+
brokenBanner,
|
|
8
|
+
docLinkList,
|
|
9
|
+
escapeHtml,
|
|
10
|
+
fieldList,
|
|
11
|
+
fieldPara,
|
|
12
|
+
rawJsonDisclosure,
|
|
13
|
+
} from './helpers.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {object} prd
|
|
17
|
+
* @param {object} ctx
|
|
18
|
+
* @param {string|undefined} ctx.raw
|
|
19
|
+
* @param {import('#core/errors').RcfError[]} [ctx.errors]
|
|
20
|
+
* @param {string[]} [ctx.requirementIds] - computed REQ children for this PRD
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
export function renderPrd(prd, ctx) {
|
|
24
|
+
if (!prd) return '';
|
|
25
|
+
const anchor = anchorIdFor(prd.prdId ?? 'PRD');
|
|
26
|
+
const broken = ctx.errors?.length ? brokenBanner(ctx.errors) : '';
|
|
27
|
+
const requirementIds = ctx.requirementIds ?? [];
|
|
28
|
+
const sections = [
|
|
29
|
+
fieldPara('Executive summary', prd.executiveSummary),
|
|
30
|
+
fieldPara('Problem statement', prd.problemStatement),
|
|
31
|
+
fieldList('Target users', prd.targetUsers),
|
|
32
|
+
fieldList('In scope', prd.inScope),
|
|
33
|
+
fieldList('Out of scope', prd.outOfScope),
|
|
34
|
+
fieldList('Objectives', prd.objectives),
|
|
35
|
+
fieldList('Constraints', prd.constraints),
|
|
36
|
+
`<section class="field-list"><h4>Requirements</h4><p>${docLinkList(requirementIds)}</p></section>`,
|
|
37
|
+
].filter(Boolean).join('\n');
|
|
38
|
+
return `
|
|
39
|
+
<article id="${anchor}" class="doc doc-prd">
|
|
40
|
+
<h3>${escapeHtml(prd.prdId ?? 'PRD')} - ${escapeHtml(prd.productName ?? '')}</h3>
|
|
41
|
+
${broken}
|
|
42
|
+
${sections}
|
|
43
|
+
${rawJsonDisclosure(ctx.raw, prd, prd.prdId)}
|
|
44
|
+
</article>`.trim();
|
|
45
|
+
}
|