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,313 @@
|
|
|
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
|
+
* 0.8.0 slug-train car 4 addition (NV-BL-GATE-01 + NV-BL-ADM-03):
|
|
55
|
+
* - SCOPE-MISMATCH: an AC whose declared scope tag exceeds the scope
|
|
56
|
+
* of every bound TC. Verify emits this at REVIEW time so the pull-in
|
|
57
|
+
* from the finalise-time profile-vs-AC check fires per FBS rather
|
|
58
|
+
* than only at finalise.
|
|
59
|
+
*/
|
|
60
|
+
export const PER_AC_VERDICTS = Object.freeze([
|
|
61
|
+
'MOCK-ONLY-DECLARED',
|
|
62
|
+
'BLOCKED-BY-DECLARATION',
|
|
63
|
+
'UI-BASELINE-UNMET',
|
|
64
|
+
'BROWSER-VERIFICATION-MISSING',
|
|
65
|
+
'SCOPE-MISMATCH',
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Required fields on every finding (spec §5.2): the RCF payoff is that every
|
|
70
|
+
* defect maps to a contract line (acId / chain node), never a free-floating
|
|
71
|
+
* bug.
|
|
72
|
+
*
|
|
73
|
+
* @param {object} finding
|
|
74
|
+
* @returns {import('#core/errors').RcfError | null} error as data, or null if valid
|
|
75
|
+
*/
|
|
76
|
+
export function validateFinding(finding) {
|
|
77
|
+
if (!finding || typeof finding !== 'object') {
|
|
78
|
+
return rcfError({ kind: 'validation', message: 'finding must be an object' });
|
|
79
|
+
}
|
|
80
|
+
if (!FINDING_SEVERITIES.includes(finding.severity)) {
|
|
81
|
+
return rcfError({ kind: 'validation', message: `finding.severity must be one of ${FINDING_SEVERITIES.join('/')}`, field: 'severity' });
|
|
82
|
+
}
|
|
83
|
+
if (typeof finding.acId !== 'string' || finding.acId.length === 0) {
|
|
84
|
+
return rcfError({ kind: 'validation', message: 'finding.acId (chain-node reference) is required', field: 'acId' });
|
|
85
|
+
}
|
|
86
|
+
if (typeof finding.journey !== 'string' || finding.journey.length === 0) {
|
|
87
|
+
return rcfError({ kind: 'validation', message: 'finding.journey is required', field: 'journey' });
|
|
88
|
+
}
|
|
89
|
+
if (!Array.isArray(finding.reproSteps)) {
|
|
90
|
+
return rcfError({ kind: 'validation', message: 'finding.reproSteps must be an array', field: 'reproSteps' });
|
|
91
|
+
}
|
|
92
|
+
if (!finding.evidence || typeof finding.evidence !== 'object') {
|
|
93
|
+
return rcfError({ kind: 'validation', message: 'finding.evidence must be an object', field: 'evidence' });
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The worst (max) severity across findings. Empty → PASS. This is the
|
|
100
|
+
* split-not-averaged rule: the single worst finding drives the class.
|
|
101
|
+
*
|
|
102
|
+
* @param {Array<{severity: string}>} findings
|
|
103
|
+
* @returns {'PASS'|'COSMETIC'|'DEGRADED'|'BROKEN'}
|
|
104
|
+
*/
|
|
105
|
+
export function aggregateSeverity(findings = []) {
|
|
106
|
+
let worst = 'PASS';
|
|
107
|
+
for (const f of findings) {
|
|
108
|
+
if ((SEVERITY_ORDER[f.severity] ?? -1) > SEVERITY_ORDER[worst]) worst = f.severity;
|
|
109
|
+
}
|
|
110
|
+
return worst;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The overall run verdict (spec §5.1). NOT-DEPLOYED and a fully-blocked run
|
|
115
|
+
* are structural verdicts; otherwise the worst finding severity wins
|
|
116
|
+
* (split-not-averaged). A run with SOME findings and SOME blocked ACs is a
|
|
117
|
+
* partial verification: the verdict reflects what WAS exercised, and the
|
|
118
|
+
* blocked ACs are named separately in the report.
|
|
119
|
+
*
|
|
120
|
+
* @param {object} opts
|
|
121
|
+
* @param {Array<{severity: string}>} [opts.findings]
|
|
122
|
+
* @param {Array<object>} [opts.blockedAcs]
|
|
123
|
+
* @param {boolean} [opts.notDeployed]
|
|
124
|
+
* @returns {string}
|
|
125
|
+
*/
|
|
126
|
+
export function aggregateVerdict({ findings = [], blockedAcs = [], notDeployed = false } = {}) {
|
|
127
|
+
if (notDeployed) return 'NOT-DEPLOYED';
|
|
128
|
+
if (findings.length === 0 && blockedAcs.length > 0) return 'BLOCKED';
|
|
129
|
+
return aggregateSeverity(findings);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Whether the severity gate is tripped → the process exits non-zero
|
|
134
|
+
* (spec §3 rule 5, §8.2). NOT-DEPLOYED, BLOCKED and LAUNCH-FAILURE always trip
|
|
135
|
+
* (ship cannot be confirmed); otherwise the worst finding severity is compared
|
|
136
|
+
* against the gate. With no gate configured, nothing trips — the report is
|
|
137
|
+
* still written.
|
|
138
|
+
*
|
|
139
|
+
* @param {object} opts
|
|
140
|
+
* @param {string} opts.verdict
|
|
141
|
+
* @param {Array<{severity: string}>} [opts.findings]
|
|
142
|
+
* @param {string|null} [opts.gate] - one of FINDING_SEVERITIES, or null/undefined
|
|
143
|
+
* @returns {boolean}
|
|
144
|
+
*/
|
|
145
|
+
export function gateTripped({ verdict, findings = [], gate }) {
|
|
146
|
+
if (verdict === 'NOT-DEPLOYED' || verdict === 'BLOCKED' || verdict === 'LAUNCH-FAILURE') return true;
|
|
147
|
+
if (!gate) return false;
|
|
148
|
+
const worst = aggregateSeverity(findings);
|
|
149
|
+
return SEVERITY_ORDER[worst] >= SEVERITY_ORDER[gate];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Resolve the Track A per-AC verdict from an AC's aggregated service
|
|
154
|
+
* attestations. Priority: `declaredMockOnly` wins over `mocked`, because
|
|
155
|
+
* a chain that explicitly declared mock-only overrides an implicit mock.
|
|
156
|
+
* `live` / `sandboxed` / `notShipped` never emit a per-AC verdict here —
|
|
157
|
+
* they either mean a live path exists (verify's normal findings pipeline
|
|
158
|
+
* handles them) or the AC does not gate ship at all.
|
|
159
|
+
*
|
|
160
|
+
* @param {Array<{serviceId: string, attestationMode: string}>} attestations
|
|
161
|
+
* @returns {{ verdict: 'MOCK-ONLY-DECLARED'|'BLOCKED-BY-DECLARATION', reason: string } | null}
|
|
162
|
+
*/
|
|
163
|
+
export function attestationPerAcVerdict(attestations = []) {
|
|
164
|
+
if (!Array.isArray(attestations) || attestations.length === 0) return null;
|
|
165
|
+
const declared = attestations.find((a) => a && a.attestationMode === 'declaredMockOnly');
|
|
166
|
+
if (declared) {
|
|
167
|
+
return {
|
|
168
|
+
verdict: 'BLOCKED-BY-DECLARATION',
|
|
169
|
+
reason: `operator declared mock-only at pre-flight for service ${declared.serviceId}; verify refused to issue a live verdict rather than fabricate a PASS.`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const mocked = attestations.find((a) => a && a.attestationMode === 'mocked');
|
|
173
|
+
if (mocked) {
|
|
174
|
+
return {
|
|
175
|
+
verdict: 'MOCK-ONLY-DECLARED',
|
|
176
|
+
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.`,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Resolve the Track B per-AC UI verdict for an AC. UI verdicts fire only
|
|
184
|
+
* for `fbsUiBearing: true` ACs (the chain-derived flag from
|
|
185
|
+
* `packages/rcf-lite/src/verify/chain/index.js`). Priority:
|
|
186
|
+
* 1. Any bound FBS has NO browserVerification entry → BROWSER-VERIFICATION-MISSING.
|
|
187
|
+
* 2. Any bound FBS's browserVerification.verdict is `block` → UI-BASELINE-UNMET.
|
|
188
|
+
* 3. Otherwise no per-AC UI verdict is emitted.
|
|
189
|
+
*
|
|
190
|
+
* @param {object} ac - a flattened AC with `fbsUiBearing` and `fbsIds`
|
|
191
|
+
* @param {object[]} [browserVerification] - manifest.browserVerification[]
|
|
192
|
+
* @returns {{ verdict: 'UI-BASELINE-UNMET'|'BROWSER-VERIFICATION-MISSING', reason: string } | null}
|
|
193
|
+
*/
|
|
194
|
+
export function uiPerAcVerdict(ac, browserVerification = []) {
|
|
195
|
+
if (!ac || ac.fbsUiBearing !== true) return null;
|
|
196
|
+
const fbsIds = Array.isArray(ac.fbsIds) ? ac.fbsIds : [];
|
|
197
|
+
if (fbsIds.length === 0) return null;
|
|
198
|
+
const bv = Array.isArray(browserVerification) ? browserVerification : [];
|
|
199
|
+
const missing = [];
|
|
200
|
+
const blocked = [];
|
|
201
|
+
for (const fbsId of fbsIds) {
|
|
202
|
+
const records = bv.filter((r) => r && r.fbsId === fbsId);
|
|
203
|
+
if (records.length === 0) {
|
|
204
|
+
missing.push(fbsId);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
// Take the freshest record — records land per-run and the latest verdict
|
|
208
|
+
// is the one that gates ship. `createdAt` sorts lexicographically for ISO
|
|
209
|
+
// timestamps; when it is missing fall back to array order (the last
|
|
210
|
+
// written entry wins).
|
|
211
|
+
const latest = [...records].sort((a, b) => (a.createdAt ?? '').localeCompare(b.createdAt ?? '')).pop();
|
|
212
|
+
if (latest && latest.verdict === 'block') blocked.push({ fbsId, id: latest.id ?? null });
|
|
213
|
+
}
|
|
214
|
+
if (missing.length > 0) {
|
|
215
|
+
return {
|
|
216
|
+
verdict: 'BROWSER-VERIFICATION-MISSING',
|
|
217
|
+
reason: `UI-bearing FBS(es) ${missing.join(', ')} have no browserVerification record on the manifest; verify has nothing to read for the baseline check.`,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
if (blocked.length > 0) {
|
|
221
|
+
const detail = blocked.map((b) => `${b.fbsId}${b.id ? ` (${b.id})` : ''}`).join(', ');
|
|
222
|
+
return {
|
|
223
|
+
verdict: 'UI-BASELINE-UNMET',
|
|
224
|
+
reason: `browserVerification recorded verdict \`block\` for ${detail}; the deployed UI failed at least one baseline invariant.`,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Rank each scope tag so we can compare "TC scope >= AC scope"
|
|
232
|
+
* numerically. Mirrors `packages/rcf-lite/src/admissibility/scope-lint.js`
|
|
233
|
+
* (single source of truth on rcf-schemas 0.4.3's scopeTag values;
|
|
234
|
+
* this copy stays deliberately local so the verdict layer does not
|
|
235
|
+
* depend on the admissibility module -- both consume the same schemas
|
|
236
|
+
* vocabulary and the same values, and duplicating the ranking here
|
|
237
|
+
* keeps the verdict layer standalone).
|
|
238
|
+
*/
|
|
239
|
+
const SCOPE_RANK = Object.freeze({
|
|
240
|
+
library: 1,
|
|
241
|
+
runtime: 2,
|
|
242
|
+
deployed: 3,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 0.8.0 slug-train car 4 (NV-BL-GATE-01 + NV-BL-ADM-03): the profile-vs-AC
|
|
247
|
+
* scope-mismatch check, pulled from finalise into REVIEW so it fires
|
|
248
|
+
* per FBS. An AC declared runtime-scope bound only to library-scope TCs
|
|
249
|
+
* emits SCOPE-MISMATCH; a wider TC (deployed covering runtime) is fine;
|
|
250
|
+
* an AC with no scope declaration is silent here (NV-BL-ADM-02 catches
|
|
251
|
+
* it at admissibility); a bound TC with no declared scope is silent
|
|
252
|
+
* here (bootstrap window; NV-BL-ADM-03 in the admissibility lint
|
|
253
|
+
* catches it there).
|
|
254
|
+
*
|
|
255
|
+
* @param {object} ac - flattened AC with `scope` and `boundTcs`
|
|
256
|
+
* @returns {{ verdict: 'SCOPE-MISMATCH', reason: string } | null}
|
|
257
|
+
*/
|
|
258
|
+
export function scopePerAcVerdict(ac) {
|
|
259
|
+
if (!ac || typeof ac.scope !== 'string') return null;
|
|
260
|
+
if (!Object.prototype.hasOwnProperty.call(SCOPE_RANK, ac.scope)) return null;
|
|
261
|
+
const boundTcs = Array.isArray(ac.boundTcs) ? ac.boundTcs : [];
|
|
262
|
+
if (boundTcs.length === 0) return null; // coverage rule handles "no TC at all".
|
|
263
|
+
const acRank = SCOPE_RANK[ac.scope];
|
|
264
|
+
const narrower = [];
|
|
265
|
+
let anyAtLeastAsWide = false;
|
|
266
|
+
for (const tc of boundTcs) {
|
|
267
|
+
if (typeof tc?.scope !== 'string') continue;
|
|
268
|
+
if (!Object.prototype.hasOwnProperty.call(SCOPE_RANK, tc.scope)) continue;
|
|
269
|
+
const tcRank = SCOPE_RANK[tc.scope];
|
|
270
|
+
if (tcRank >= acRank) {
|
|
271
|
+
anyAtLeastAsWide = true;
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
narrower.push({ tsId: tc.tsId, tcId: tc.tcId, scope: tc.scope });
|
|
275
|
+
}
|
|
276
|
+
if (anyAtLeastAsWide) return null;
|
|
277
|
+
if (narrower.length === 0) return null;
|
|
278
|
+
const detail = narrower.map((n) => `${n.tcId} on ${n.tsId} (scope=${n.scope})`).join(', ');
|
|
279
|
+
return {
|
|
280
|
+
verdict: 'SCOPE-MISMATCH',
|
|
281
|
+
reason: `AC ${ac.acId} is scope=${ac.scope} but every bound TC is narrower: ${detail}. NV-BL-GATE-01 pulls this check into REVIEW; NV-BL-ADM-03 refuses at admissibility.`,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Emit per-AC verdicts across the whole chain. Combines Track A's service
|
|
287
|
+
* attestation verdicts with Track B's UI-baseline verdicts. An AC can carry
|
|
288
|
+
* BOTH a service-attestation verdict AND a UI verdict (a UI-bearing FBS
|
|
289
|
+
* that also depends on a mocked service produces two per-AC entries — one
|
|
290
|
+
* per class). This matches the finalise gate contract in
|
|
291
|
+
* `packages/rcf-lite/src/finalise/ingest.js:findMockOnlyDeclaredAcs`, which
|
|
292
|
+
* filters on verdict class and does not deduplicate by acId.
|
|
293
|
+
*
|
|
294
|
+
* 0.8.0 slug-train car 4: SCOPE-MISMATCH runs here too, alongside the
|
|
295
|
+
* attestation and UI verdicts.
|
|
296
|
+
*
|
|
297
|
+
* @param {object} opts
|
|
298
|
+
* @param {Array<object>} opts.acs - flattened ACs from `readChain`
|
|
299
|
+
* @param {object[]} [opts.browserVerification] - manifest.browserVerification[]
|
|
300
|
+
* @returns {Array<{ acId: string, verdict: string, reason: string }>}
|
|
301
|
+
*/
|
|
302
|
+
export function derivePerAcVerdicts({ acs = [], browserVerification = [] } = {}) {
|
|
303
|
+
const out = [];
|
|
304
|
+
for (const ac of acs) {
|
|
305
|
+
const attest = attestationPerAcVerdict(ac.serviceAttestations);
|
|
306
|
+
if (attest) out.push({ acId: ac.acId, verdict: attest.verdict, reason: attest.reason });
|
|
307
|
+
const ui = uiPerAcVerdict(ac, browserVerification);
|
|
308
|
+
if (ui) out.push({ acId: ac.acId, verdict: ui.verdict, reason: ui.reason });
|
|
309
|
+
const scopeMismatch = scopePerAcVerdict(ac);
|
|
310
|
+
if (scopeMismatch) out.push({ acId: ac.acId, verdict: scopeMismatch.verdict, reason: scopeMismatch.reason });
|
|
311
|
+
}
|
|
312
|
+
return out;
|
|
313
|
+
}
|
|
@@ -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
|
+
}
|