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,325 @@
|
|
|
1
|
+
// REVIEW-stage test-theatre audit + mutation-sampling
|
|
2
|
+
// (verification-integrity-cluster-spec §5.5, §6).
|
|
3
|
+
//
|
|
4
|
+
// Two surfaces composed into ONE Review pass, emitting ONE
|
|
5
|
+
// reviewAudit record per FBS:
|
|
6
|
+
//
|
|
7
|
+
// 1. test-theatre audit (deterministic): five finding categories from
|
|
8
|
+
// §5.5 - mockOnlyIntegrationClaim, testPointerBroken,
|
|
9
|
+
// assertionStrengthWeak, acIdsCoverageDrift, otherDeclared. This
|
|
10
|
+
// module implements the four deterministic ones; assertion strength
|
|
11
|
+
// needs a code-reading agent, so v1 raises it only via the
|
|
12
|
+
// mutation-sampling side, not the deterministic sweep (the sweep is
|
|
13
|
+
// the belt, the mutation-sampling agent is the braces).
|
|
14
|
+
//
|
|
15
|
+
// 2. mutation-sampling (agent-driven, injectable): the runner accepts
|
|
16
|
+
// a `mutationRunner` dependency conforming to the spec §6 shape.
|
|
17
|
+
// The runner takes the FBS diff and TS/TC list and returns the
|
|
18
|
+
// mutation-sampling record. When no runner is wired, the audit
|
|
19
|
+
// emits a `notes` entry explaining that the sampler was not run;
|
|
20
|
+
// the reviewAudit record is still valid schema.
|
|
21
|
+
|
|
22
|
+
import { classifyAttestationProfile, aggregateAttestationsByAc } from '../query/attestation.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {import('#core/store/walker.js').TreeModel} TreeModel
|
|
26
|
+
* @typedef {'mockOnlyIntegrationClaim'|'testPointerBroken'|'assertionStrengthWeak'|'acIdsCoverageDrift'|'uiBaselineDrift'|'otherDeclared'} TestTheatreFindingKind
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {object} TestTheatreFinding
|
|
31
|
+
* @property {string} [tsId] required for test-theatre kinds; omitted on kind=uiBaselineDrift (rcf-schemas 0.4.2)
|
|
32
|
+
* @property {string} [tcId]
|
|
33
|
+
* @property {string} [anchorId] free-form anchor for kinds without a test suite (for example, uiBaselineDrift, anchored on an FBS id or file path)
|
|
34
|
+
* @property {TestTheatreFindingKind} kind
|
|
35
|
+
* @property {string} detail
|
|
36
|
+
* @property {'advisory'|'warn'|'block'} severity
|
|
37
|
+
* @property {string} [kindDescription]
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {object} MutationSurvivor
|
|
42
|
+
* @property {string} mutationId
|
|
43
|
+
* @property {string} targetFile
|
|
44
|
+
* @property {string} [targetSymbol]
|
|
45
|
+
* @property {string} mutationSummary
|
|
46
|
+
* @property {string[]} acIds
|
|
47
|
+
* @property {string[]} [tsIdsShouldHaveCaught]
|
|
48
|
+
* @property {string[]} [tcIdsShouldHaveCaught]
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @typedef {object} MutationSamplingRecord
|
|
53
|
+
* @property {string} mode
|
|
54
|
+
* @property {number} mutantsGenerated
|
|
55
|
+
* @property {number} mutantsRun
|
|
56
|
+
* @property {number} killed
|
|
57
|
+
* @property {number} survived
|
|
58
|
+
* @property {number} [durationMs]
|
|
59
|
+
* @property {MutationSurvivor[]} [survivors]
|
|
60
|
+
* @property {string} [notes]
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
const INTEGRATION_LEVELS = new Set(['integration', 'e2e', 'contract']);
|
|
64
|
+
const MOCK_PROFILES = new Set(['mock', 'stub', 'fixture']);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Detect `mockOnlyIntegrationClaim`: a TS whose testLevel is
|
|
68
|
+
* integration-or-higher and every TC records `runtimeProvenance.profile`
|
|
69
|
+
* in {mock, stub, fixture} AND at least one bound AC's aggregated
|
|
70
|
+
* attestation is live or sandboxed. This is the exact d-142 failure
|
|
71
|
+
* mode.
|
|
72
|
+
*
|
|
73
|
+
* @param {TreeModel} tree
|
|
74
|
+
* @param {object} fbs
|
|
75
|
+
* @returns {TestTheatreFinding[]}
|
|
76
|
+
*/
|
|
77
|
+
function detectMockOnlyIntegrationClaim(tree, fbs) {
|
|
78
|
+
const findings = [];
|
|
79
|
+
const attByAc = aggregateAttestationsByAc(tree);
|
|
80
|
+
const fbsAcIds = new Set(fbs.acIds ?? []);
|
|
81
|
+
// Every TS that covers one of the FBS's ACs.
|
|
82
|
+
const relevantTs = (tree.testSuites ?? []).filter((ts) => (ts.acIds ?? []).some((a) => fbsAcIds.has(a)));
|
|
83
|
+
for (const ts of relevantTs) {
|
|
84
|
+
if (!INTEGRATION_LEVELS.has(ts.testLevel)) continue;
|
|
85
|
+
const tcs = ts.testCases ?? [];
|
|
86
|
+
if (tcs.length === 0) continue;
|
|
87
|
+
const allMockShaped = tcs.every((tc) => MOCK_PROFILES.has(tc?.runtimeProvenance?.profile ?? ''));
|
|
88
|
+
if (!allMockShaped) continue;
|
|
89
|
+
const bindingsOnFbsAcs = (ts.acIds ?? [])
|
|
90
|
+
.filter((a) => fbsAcIds.has(a))
|
|
91
|
+
.flatMap((a) => attByAc.get(a) ?? []);
|
|
92
|
+
const liveish = bindingsOnFbsAcs.find((b) => b.attestationMode === 'live' || b.attestationMode === 'sandboxed');
|
|
93
|
+
if (liveish) {
|
|
94
|
+
findings.push({
|
|
95
|
+
tsId: ts.id,
|
|
96
|
+
kind: 'mockOnlyIntegrationClaim',
|
|
97
|
+
detail: `${ts.id} testLevel=${ts.testLevel} but every TC's runtimeProvenance.profile is in {mock,stub,fixture}, while AC bindings include service ${liveish.serviceId} attested ${liveish.attestationMode}.`,
|
|
98
|
+
severity: 'block',
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return findings;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Detect `testPointerBroken`: any TC whose `testPointer` is missing or
|
|
107
|
+
* fails resolution. Uses the pre-computed testPointers map (a resolved
|
|
108
|
+
* pointer's `.resolved === true`). When no map is supplied, we skip
|
|
109
|
+
* this check (the caller must pass one for the audit to police it).
|
|
110
|
+
*
|
|
111
|
+
* @param {TreeModel} tree
|
|
112
|
+
* @param {object} fbs
|
|
113
|
+
* @param {Map<string, { resolved?: boolean, testPointer?: string|null, reason?: string }>} [testPointers]
|
|
114
|
+
* @returns {TestTheatreFinding[]}
|
|
115
|
+
*/
|
|
116
|
+
function detectTestPointerBroken(tree, fbs, testPointers) {
|
|
117
|
+
if (!testPointers || testPointers.size === 0) return [];
|
|
118
|
+
const findings = [];
|
|
119
|
+
const fbsAcIds = new Set(fbs.acIds ?? []);
|
|
120
|
+
for (const ts of tree.testSuites ?? []) {
|
|
121
|
+
for (const tc of ts.testCases ?? []) {
|
|
122
|
+
if (!fbsAcIds.has(tc.acId)) continue;
|
|
123
|
+
const key = `${ts.id}::${tc.id}`;
|
|
124
|
+
const resolution = testPointers.get(key);
|
|
125
|
+
if (!resolution || resolution.resolved !== true) {
|
|
126
|
+
findings.push({
|
|
127
|
+
tsId: ts.id,
|
|
128
|
+
tcId: tc.id,
|
|
129
|
+
kind: 'testPointerBroken',
|
|
130
|
+
detail: `TC ${tc.id} testPointer '${resolution?.testPointer ?? tc.testPointer ?? '(missing)'}' does not resolve: ${resolution?.reason ?? 'missing-pointer'}.`,
|
|
131
|
+
severity: 'block',
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return findings;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Detect `acIdsCoverageDrift`: an FBS acId that no TS covers, OR a
|
|
141
|
+
* covering TS acId that this FBS does not claim.
|
|
142
|
+
*
|
|
143
|
+
* @param {TreeModel} tree
|
|
144
|
+
* @param {object} fbs
|
|
145
|
+
* @returns {TestTheatreFinding[]}
|
|
146
|
+
*/
|
|
147
|
+
function detectAcIdsCoverageDrift(tree, fbs) {
|
|
148
|
+
const findings = [];
|
|
149
|
+
const fbsAcIds = new Set(fbs.acIds ?? []);
|
|
150
|
+
for (const ts of tree.testSuites ?? []) {
|
|
151
|
+
const covers = (ts.acIds ?? []).some((a) => fbsAcIds.has(a));
|
|
152
|
+
if (!covers) continue;
|
|
153
|
+
for (const a of ts.acIds ?? []) {
|
|
154
|
+
if (!fbsAcIds.has(a)) {
|
|
155
|
+
findings.push({
|
|
156
|
+
tsId: ts.id,
|
|
157
|
+
kind: 'acIdsCoverageDrift',
|
|
158
|
+
detail: `${ts.id} covers ${a}, but ${fbs.fbsId} does not claim ${a} in acIds[].`,
|
|
159
|
+
severity: 'warn',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// The reverse ("FBS claims an AC no TS covers") is intentionally
|
|
165
|
+
// deferred to `coverage --strict`, which already raises exit 4 on
|
|
166
|
+
// uncovered ACs and needs no tsId to do so (the reviewAudit schema
|
|
167
|
+
// requires a valid tsId on every testTheatreFinding, and the "no TS
|
|
168
|
+
// covers" case has none to name). Belt and braces: coverage is the
|
|
169
|
+
// belt, this audit is the braces.
|
|
170
|
+
return findings;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Detect `attestationDrift` findings as test-theatre entries. This is
|
|
175
|
+
* the passWithWarn class from §3.5 (declaredMockOnly × live) plus the
|
|
176
|
+
* refuse class (already blocking coverage --strict, but re-raised here
|
|
177
|
+
* so the Review record surfaces it too).
|
|
178
|
+
*
|
|
179
|
+
* The kind used is `mockOnlyIntegrationClaim` when the drift refuses
|
|
180
|
+
* live-attested ACs on mock-profile TCs; the standalone `attestationDrift`
|
|
181
|
+
* kind is not in the schema enum (spec §3.4 fixed enum), so v1 folds
|
|
182
|
+
* these into the closest matching kind (`mockOnlyIntegrationClaim` for
|
|
183
|
+
* live-attested drift, `otherDeclared` with a kindDescription for the
|
|
184
|
+
* passWithWarn drift). This is spec-faithful — §5.5 defines only the
|
|
185
|
+
* enumerated `kind` set, and downstream consumers key on that set.
|
|
186
|
+
*
|
|
187
|
+
* @param {TreeModel} tree
|
|
188
|
+
* @param {object} fbs
|
|
189
|
+
* @returns {TestTheatreFinding[]}
|
|
190
|
+
*/
|
|
191
|
+
function detectAttestationDrift(tree, fbs) {
|
|
192
|
+
const findings = [];
|
|
193
|
+
const attByAc = aggregateAttestationsByAc(tree);
|
|
194
|
+
const fbsAcIds = new Set(fbs.acIds ?? []);
|
|
195
|
+
for (const ts of tree.testSuites ?? []) {
|
|
196
|
+
for (const tc of ts.testCases ?? []) {
|
|
197
|
+
if (!fbsAcIds.has(tc.acId)) continue;
|
|
198
|
+
const profile = tc?.runtimeProvenance?.profile;
|
|
199
|
+
if (typeof profile !== 'string') continue;
|
|
200
|
+
const bindings = attByAc.get(tc.acId) ?? [];
|
|
201
|
+
for (const { serviceId, attestationMode } of bindings) {
|
|
202
|
+
const cell = classifyAttestationProfile(attestationMode, profile);
|
|
203
|
+
if (cell.verdict === 'passWithWarn') {
|
|
204
|
+
findings.push({
|
|
205
|
+
tsId: ts.id,
|
|
206
|
+
tcId: tc.id,
|
|
207
|
+
kind: 'otherDeclared',
|
|
208
|
+
kindDescription: 'attestationDrift',
|
|
209
|
+
detail: `${ts.id}/${tc.id} on ${tc.acId} (service ${serviceId}): ${cell.reason}`,
|
|
210
|
+
severity: 'warn',
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return findings;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Run the deterministic test-theatre audit on one FBS. Returns the
|
|
221
|
+
* findings list (aggregated across the four detectors).
|
|
222
|
+
*
|
|
223
|
+
* @param {object} args
|
|
224
|
+
* @param {TreeModel} args.tree
|
|
225
|
+
* @param {object} args.fbs
|
|
226
|
+
* @param {Map<string, object>} [args.testPointers]
|
|
227
|
+
* @returns {TestTheatreFinding[]}
|
|
228
|
+
*/
|
|
229
|
+
export function auditTestTheatre({ tree, fbs, testPointers }) {
|
|
230
|
+
return [
|
|
231
|
+
...detectMockOnlyIntegrationClaim(tree, fbs),
|
|
232
|
+
...detectTestPointerBroken(tree, fbs, testPointers),
|
|
233
|
+
...detectAcIdsCoverageDrift(tree, fbs),
|
|
234
|
+
...detectAttestationDrift(tree, fbs),
|
|
235
|
+
];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* @typedef {object} ReviewAuditRecord
|
|
240
|
+
* @property {string} id `ra-<fbsId>-<n>`
|
|
241
|
+
* @property {string} fbsId
|
|
242
|
+
* @property {string} createdAt
|
|
243
|
+
* @property {TestTheatreFinding[]} testTheatreFindings
|
|
244
|
+
* @property {MutationSamplingRecord} [mutationSampling]
|
|
245
|
+
* @property {'pass'|'warn'|'block'} verdict
|
|
246
|
+
*/
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Aggregate a verdict from the findings + mutation sampling per spec
|
|
250
|
+
* §5.5: block if any finding severity is block or any mutation
|
|
251
|
+
* survivor traces to an in-scope AC; warn if any warn and no block;
|
|
252
|
+
* pass otherwise.
|
|
253
|
+
*
|
|
254
|
+
* Review N-2 (non-blocking): an unwired mutation runner
|
|
255
|
+
* (`mode: 'agent-v1-not-wired'`) is promoted to `warn` even on an
|
|
256
|
+
* otherwise-clean audit. Without this, an unwired runner is
|
|
257
|
+
* indistinguishable from a wired runner that killed every mutant:
|
|
258
|
+
* both emit `verdict: pass`, so the exit-code layer conflates "audit
|
|
259
|
+
* clean" with "audit did not run". Warn forces the operator to wire a
|
|
260
|
+
* runner or pass `--skip-mutation` (mode: 'skipped'), which remains
|
|
261
|
+
* pass as an explicit operator choice.
|
|
262
|
+
*
|
|
263
|
+
* @param {TestTheatreFinding[]} findings
|
|
264
|
+
* @param {MutationSamplingRecord} [mutationSampling]
|
|
265
|
+
* @returns {'pass'|'warn'|'block'}
|
|
266
|
+
*/
|
|
267
|
+
export function aggregateVerdict(findings, mutationSampling) {
|
|
268
|
+
const severities = findings.map((f) => f.severity);
|
|
269
|
+
if (severities.includes('block')) return 'block';
|
|
270
|
+
if ((mutationSampling?.survivors?.length ?? 0) > 0) return 'block';
|
|
271
|
+
if (severities.includes('warn')) return 'warn';
|
|
272
|
+
if (mutationSampling?.mode === 'agent-v1-not-wired') return 'warn';
|
|
273
|
+
return 'pass';
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Monotonic id allocator for the reviewAudit array.
|
|
278
|
+
*
|
|
279
|
+
* @param {object|null} manifest
|
|
280
|
+
* @param {string} fbsId
|
|
281
|
+
* @returns {string}
|
|
282
|
+
*/
|
|
283
|
+
export function nextReviewAuditId(manifest, fbsId) {
|
|
284
|
+
const prefix = `ra-${fbsId}-`;
|
|
285
|
+
const existing = Array.isArray(manifest?.reviewAudit) ? manifest.reviewAudit : [];
|
|
286
|
+
let maxN = 0;
|
|
287
|
+
for (const rec of existing) {
|
|
288
|
+
if (typeof rec?.id !== 'string' || !rec.id.startsWith(prefix)) continue;
|
|
289
|
+
const n = Number.parseInt(rec.id.slice(prefix.length), 10);
|
|
290
|
+
if (Number.isFinite(n) && n > maxN) maxN = n;
|
|
291
|
+
}
|
|
292
|
+
return `${prefix}${maxN + 1}`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Compose the record. Timestamps at UTC ISO.
|
|
297
|
+
*
|
|
298
|
+
* @param {object} args
|
|
299
|
+
* @param {TreeModel} args.tree
|
|
300
|
+
* @param {object} args.fbs
|
|
301
|
+
* @param {TestTheatreFinding[]} args.findings
|
|
302
|
+
* @param {MutationSamplingRecord} [args.mutationSampling]
|
|
303
|
+
* @param {Date} [args.now]
|
|
304
|
+
* @returns {ReviewAuditRecord}
|
|
305
|
+
*/
|
|
306
|
+
export function composeReviewAuditRecord({ tree, fbs, findings, mutationSampling, now = new Date() }) {
|
|
307
|
+
const id = nextReviewAuditId(tree.manifest, fbs.fbsId);
|
|
308
|
+
const verdict = aggregateVerdict(findings, mutationSampling);
|
|
309
|
+
const record = {
|
|
310
|
+
id,
|
|
311
|
+
fbsId: fbs.fbsId,
|
|
312
|
+
createdAt: now.toISOString(),
|
|
313
|
+
testTheatreFindings: findings.map((f) => normaliseFinding(f)),
|
|
314
|
+
verdict,
|
|
315
|
+
};
|
|
316
|
+
if (mutationSampling) record.mutationSampling = mutationSampling;
|
|
317
|
+
return record;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function normaliseFinding(f) {
|
|
321
|
+
const out = { tsId: f.tsId, kind: f.kind, detail: f.detail, severity: f.severity };
|
|
322
|
+
if (typeof f.tcId === 'string' && f.tcId.length > 0) out.tcId = f.tcId;
|
|
323
|
+
if (typeof f.kindDescription === 'string' && f.kindDescription.length > 0) out.kindDescription = f.kindDescription;
|
|
324
|
+
return out;
|
|
325
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Agent-driven mutation-sampling coordinator
|
|
2
|
+
// (verification-integrity-cluster-spec §6).
|
|
3
|
+
//
|
|
4
|
+
// This module owns the ORCHESTRATION contract, not the agent dispatch.
|
|
5
|
+
// The actual mutation-sampling is done by a Review-stage subagent (§6.5,
|
|
6
|
+
// §6.6) that runs in a worktree, applies each mutation, runs the test
|
|
7
|
+
// command, and records kill / survive. The subagent lives outside the
|
|
8
|
+
// build package (it is a Claude Code dispatch orchestrated by the
|
|
9
|
+
// harness, spec §6.7 note that native tool adapters are v2). Build
|
|
10
|
+
// provides:
|
|
11
|
+
// - the CONTRACT the runner must satisfy (function shape, input, output);
|
|
12
|
+
// - a DEFAULT runner that no-ops with a clear note when no wiring is
|
|
13
|
+
// supplied (the record still validates against the schema); this
|
|
14
|
+
// lets `rcf review` return a valid reviewAudit record on any repo,
|
|
15
|
+
// even before the harness is set up;
|
|
16
|
+
// - the SIZING policy (§6.3) and TEST-COMMAND resolver (§6.4).
|
|
17
|
+
//
|
|
18
|
+
// The wire-up: `rcf review` accepts `--mutation-runner <name>` (in a
|
|
19
|
+
// future iteration) or a dep-injected runner (this iteration). In tests,
|
|
20
|
+
// injection is direct. In production, the harness passes a runner that
|
|
21
|
+
// dispatches an Opus 4.7 subagent per the estate ladder.
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {object} MutationRunnerInput
|
|
25
|
+
* @property {string} fbsId
|
|
26
|
+
* @property {string[]} acIds the FBS's in-scope ACs
|
|
27
|
+
* @property {object[]} testSuites the TSes that cover any of the ACs
|
|
28
|
+
* @property {object} sizing min/max/timeBudgetMs
|
|
29
|
+
* @property {string} [testCommand] resolved test command (§6.4)
|
|
30
|
+
* @property {string} projectRoot
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {object} MutationRunnerOutput
|
|
35
|
+
* @property {import('./index.js').MutationSamplingRecord} record
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @typedef {(input: MutationRunnerInput) => Promise<MutationRunnerOutput>} MutationRunner
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve the sizing policy for a mutation-sampling pass. Spec §6.3:
|
|
44
|
+
* 10 min, 30 max, ~3 mutants per 50 LOC of changed function bodies,
|
|
45
|
+
* default 10-minute time budget. LOC counting is the runner's job;
|
|
46
|
+
* this returns the plain envelope.
|
|
47
|
+
*
|
|
48
|
+
* @param {object} [opts]
|
|
49
|
+
* @param {number} [opts.timeBudgetMs]
|
|
50
|
+
* @returns {{ min: number, max: number, timeBudgetMs: number }}
|
|
51
|
+
*/
|
|
52
|
+
export function defaultSizing({ timeBudgetMs = 10 * 60 * 1000 } = {}) {
|
|
53
|
+
return { min: 10, max: 30, timeBudgetMs };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Test command resolution (§6.4): manifest.testCommand -> flag ->
|
|
58
|
+
* fallback. Returns the resolved command string OR the reason a
|
|
59
|
+
* fallback was reached. Never throws — the caller records the reason
|
|
60
|
+
* on the mutation-sampling record when applicable.
|
|
61
|
+
*
|
|
62
|
+
* @param {object} args
|
|
63
|
+
* @param {object|null} args.manifest
|
|
64
|
+
* @param {string} [args.cliFlag]
|
|
65
|
+
* @param {boolean} args.hasPackageJson
|
|
66
|
+
* @returns {{ command: string|null, source: 'manifest'|'flag'|'fallback'|'unresolved' }}
|
|
67
|
+
*/
|
|
68
|
+
export function resolveTestCommand({ manifest, cliFlag, hasPackageJson }) {
|
|
69
|
+
if (typeof cliFlag === 'string' && cliFlag.length > 0) {
|
|
70
|
+
return { command: cliFlag, source: 'flag' };
|
|
71
|
+
}
|
|
72
|
+
if (typeof manifest?.testCommand === 'string' && manifest.testCommand.length > 0) {
|
|
73
|
+
return { command: manifest.testCommand, source: 'manifest' };
|
|
74
|
+
}
|
|
75
|
+
if (hasPackageJson) return { command: 'npm test', source: 'fallback' };
|
|
76
|
+
return { command: null, source: 'unresolved' };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Default runner: emits a valid mutation-sampling record with zero
|
|
81
|
+
* mutants and a `notes` explaining that no runner was wired. This is
|
|
82
|
+
* the "no-op with honesty" path — the reviewAudit record stays valid,
|
|
83
|
+
* the audit's verdict is not spoofed, and the operator sees an
|
|
84
|
+
* explicit message rather than a silent green.
|
|
85
|
+
*
|
|
86
|
+
* @type {MutationRunner}
|
|
87
|
+
*/
|
|
88
|
+
export async function defaultMutationRunner() {
|
|
89
|
+
return {
|
|
90
|
+
record: {
|
|
91
|
+
mode: 'agent-v1-not-wired',
|
|
92
|
+
mutantsGenerated: 0,
|
|
93
|
+
mutantsRun: 0,
|
|
94
|
+
killed: 0,
|
|
95
|
+
survived: 0,
|
|
96
|
+
notes: 'No mutation-sampling runner supplied. Wire one via the harness (spec §6.5-§6.7) or run --skip-mutation to acknowledge the omission on the record.',
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Explicit skip: same shape as the default runner but says so.
|
|
103
|
+
*
|
|
104
|
+
* @type {MutationRunner}
|
|
105
|
+
*/
|
|
106
|
+
export async function skippedMutationRunner() {
|
|
107
|
+
return {
|
|
108
|
+
record: {
|
|
109
|
+
mode: 'skipped',
|
|
110
|
+
mutantsGenerated: 0,
|
|
111
|
+
mutantsRun: 0,
|
|
112
|
+
killed: 0,
|
|
113
|
+
survived: 0,
|
|
114
|
+
notes: 'Mutation sampling skipped by --skip-mutation.',
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Stage-3 UI baseline drift audit
|
|
2
|
+
// (ui-design-gate-0.7.0-spec §3.4, §7 mandate 1, mandate 3).
|
|
3
|
+
//
|
|
4
|
+
// Runs alongside the existing Track A test-theatre audit and emits
|
|
5
|
+
// findings on the same reviewAudit record under the `uiBaselineDrift`
|
|
6
|
+
// kind (schema-registered on `reviewAudit.testTheatreFindings[].kind`
|
|
7
|
+
// per rcf-schemas@0.4.0).
|
|
8
|
+
//
|
|
9
|
+
// Two deterministic checks in v1:
|
|
10
|
+
// 1. `noHexInViewFiles` (§7 mandate 1): any view file matches
|
|
11
|
+
// /#[0-9a-fA-F]{3,8}\b/ while the baseline sets
|
|
12
|
+
// `defaults.noHexInViewFiles: true`. Severity: block. Scope is
|
|
13
|
+
// configurable via `uiBaseline.defaults.viewFileGlobs` (default
|
|
14
|
+
// ["src/ui/**"], minus the themeTokensModule path).
|
|
15
|
+
// 2. `sharedLayoutImport` (§7 mandate 3): route files (default heuristic
|
|
16
|
+
// src/routes/**) do NOT import the sharedLayoutModule declared on
|
|
17
|
+
// the baseline. Severity: block.
|
|
18
|
+
//
|
|
19
|
+
// Both checks are opt-out-aware via `uiBaseline.operatorOptOuts[]`; the
|
|
20
|
+
// finding is still recorded (so the operator can see the drift), but
|
|
21
|
+
// severity demotes to `advisory` when the opt-out matches.
|
|
22
|
+
//
|
|
23
|
+
// The audit is a pure function of (fbs, uiBaseline, file listing +
|
|
24
|
+
// contents). Callers supply the file listing so tests can stub without
|
|
25
|
+
// touching disk.
|
|
26
|
+
|
|
27
|
+
import { readFile } from 'node:fs/promises';
|
|
28
|
+
import { join } from 'node:path';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {import('./index.js').TestTheatreFinding} TestTheatreFinding
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const HEX_RE = /#[0-9a-fA-F]{3,8}\b/;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Run the UI-baseline drift audit on one FBS. Requires `uiBaseline`
|
|
38
|
+
* present on the manifest; when absent, returns an empty list (the
|
|
39
|
+
* baseline is the ruling, and no baseline means nothing to drift from
|
|
40
|
+
* for this audit - coverage of that state lives elsewhere).
|
|
41
|
+
*
|
|
42
|
+
* @param {object} args
|
|
43
|
+
* @param {string} args.projectRoot
|
|
44
|
+
* @param {object} args.fbs
|
|
45
|
+
* @param {object|null} args.uiBaseline
|
|
46
|
+
* @param {(patterns: string[]) => Promise<string[]>} args.listFiles glob helper (injectable; project-root-relative paths returned)
|
|
47
|
+
* @returns {Promise<TestTheatreFinding[]>}
|
|
48
|
+
*/
|
|
49
|
+
export async function auditUiBaselineDrift({ projectRoot, fbs, uiBaseline, listFiles }) {
|
|
50
|
+
/** @type {TestTheatreFinding[]} */
|
|
51
|
+
const findings = [];
|
|
52
|
+
if (!uiBaseline || fbs?.uiBearing !== true) return findings;
|
|
53
|
+
|
|
54
|
+
const defaults = uiBaseline.defaults ?? {};
|
|
55
|
+
const optOuts = new Set((uiBaseline.operatorOptOuts ?? []).map((o) => o.field));
|
|
56
|
+
|
|
57
|
+
// 1. noHexInViewFiles
|
|
58
|
+
if (defaults.noHexInViewFiles === true) {
|
|
59
|
+
const globs = Array.isArray(defaults.viewFileGlobs) && defaults.viewFileGlobs.length > 0
|
|
60
|
+
? defaults.viewFileGlobs
|
|
61
|
+
: ['src/ui/**'];
|
|
62
|
+
const tokensModule = typeof defaults.designTokensModule === 'string' ? defaults.designTokensModule : null;
|
|
63
|
+
const files = await safeListFiles(listFiles, globs);
|
|
64
|
+
for (const rel of files) {
|
|
65
|
+
if (tokensModule && rel === tokensModule) continue;
|
|
66
|
+
const abs = join(projectRoot, rel);
|
|
67
|
+
let contents;
|
|
68
|
+
try { contents = await readFile(abs, 'utf8'); } catch { continue; }
|
|
69
|
+
const match = HEX_RE.exec(contents);
|
|
70
|
+
if (match) {
|
|
71
|
+
const severity = optOuts.has('noHexInViewFiles') ? 'advisory' : 'block';
|
|
72
|
+
findings.push({
|
|
73
|
+
// Track B review N-5 (@stravica-ai/rcf-schemas 0.4.2): the
|
|
74
|
+
// finding anchors on the FBS id via the dedicated anchorId
|
|
75
|
+
// field. Prior versions smuggled fbs.fbsId through the tsId
|
|
76
|
+
// slot to satisfy the pre-0.4.2 blanket required rule; the
|
|
77
|
+
// slot mismatch was a real category-vs-slot defect (a
|
|
78
|
+
// downstream reader assuming tsId names a TS would follow a
|
|
79
|
+
// broken pointer). 0.4.2 keeps tsId required for the
|
|
80
|
+
// test-theatre kinds only.
|
|
81
|
+
anchorId: fbs.fbsId,
|
|
82
|
+
kind: 'uiBaselineDrift',
|
|
83
|
+
detail: `hex literal ${match[0]} detected in ${rel} (baseline defaults.noHexInViewFiles: true). Move colours into ${tokensModule ?? 'the design tokens module'}.`,
|
|
84
|
+
severity,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 2. sharedLayoutImport
|
|
91
|
+
if (typeof defaults.sharedLayoutModule === 'string' && defaults.sharedLayoutModule.length > 0) {
|
|
92
|
+
const routeGlobs = Array.isArray(defaults.routeFileGlobs) && defaults.routeFileGlobs.length > 0
|
|
93
|
+
? defaults.routeFileGlobs
|
|
94
|
+
: ['src/routes/**'];
|
|
95
|
+
const layoutModule = defaults.sharedLayoutModule;
|
|
96
|
+
const layoutBase = basename(layoutModule).replace(/\.[jt]sx?$/, '');
|
|
97
|
+
const files = await safeListFiles(listFiles, routeGlobs);
|
|
98
|
+
for (const rel of files) {
|
|
99
|
+
const abs = join(projectRoot, rel);
|
|
100
|
+
let contents;
|
|
101
|
+
try { contents = await readFile(abs, 'utf8'); } catch { continue; }
|
|
102
|
+
// A route file is expected to import the shared layout module,
|
|
103
|
+
// either by the module path or by the module basename. Both are
|
|
104
|
+
// recognised; the check is a coarse heuristic, not a formal AST.
|
|
105
|
+
const hasImport = contents.includes(layoutModule)
|
|
106
|
+
|| new RegExp(`\\b${escapeForRegExp(layoutBase)}\\b`).test(contents);
|
|
107
|
+
if (!hasImport) {
|
|
108
|
+
const severity = optOuts.has('sharedLayoutModule') ? 'advisory' : 'block';
|
|
109
|
+
findings.push({
|
|
110
|
+
// See the noHexInViewFiles comment above for the anchorId
|
|
111
|
+
// rationale (rcf-schemas 0.4.2, Track B review N-5).
|
|
112
|
+
anchorId: fbs.fbsId,
|
|
113
|
+
kind: 'uiBaselineDrift',
|
|
114
|
+
detail: `${rel} does not import the shared layout module (${layoutModule}); baseline mandate 3 says every route uses one layout.`,
|
|
115
|
+
severity,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return findings;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function safeListFiles(listFiles, patterns) {
|
|
124
|
+
if (typeof listFiles !== 'function') return [];
|
|
125
|
+
try {
|
|
126
|
+
const raw = await listFiles(patterns);
|
|
127
|
+
return Array.isArray(raw) ? raw : [];
|
|
128
|
+
} catch { return []; }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function basename(path) {
|
|
132
|
+
const idx = String(path).lastIndexOf('/');
|
|
133
|
+
return idx >= 0 ? String(path).slice(idx + 1) : String(path);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function escapeForRegExp(s) {
|
|
137
|
+
return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
138
|
+
}
|