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,185 @@
|
|
|
1
|
+
// Design-shape questions catalogue for `rcf preflight`
|
|
2
|
+
// (verification-integrity-cluster-spec ADDENDUM §A).
|
|
3
|
+
//
|
|
4
|
+
// The v1 catalogue carries exactly ONE question — `auth.htmlLoginPage` —
|
|
5
|
+
// per the plan-committed Baz-approved scope. Additional questions land
|
|
6
|
+
// in v2 only on the evidence-gated triggers named in §A.3 (three
|
|
7
|
+
// separate projects that would have benefited, OR a case-study defect
|
|
8
|
+
// the existing surface cannot express). Broadening speculatively
|
|
9
|
+
// re-introduces the elicitation surface bloat this addendum exists to
|
|
10
|
+
// avoid.
|
|
11
|
+
//
|
|
12
|
+
// Two exports:
|
|
13
|
+
// - CATALOGUE_V1: the versioned question set, one entry per question.
|
|
14
|
+
// - selectApplicableQuestions(reqs): given a list of REQs, decide which
|
|
15
|
+
// questions to pose. Deterministic; the interactive session drives it,
|
|
16
|
+
// the non-interactive path reads the same catalogue when validating a
|
|
17
|
+
// pre-filled input file.
|
|
18
|
+
//
|
|
19
|
+
// The catalogue is data, not logic. Downstream consumers own the
|
|
20
|
+
// side-effects (writing designShapeAnswers into the preFlightConfig
|
|
21
|
+
// record; the Track B / Track C+D field composition documented in
|
|
22
|
+
// ADDENDUM §A.2 and §A.5).
|
|
23
|
+
|
|
24
|
+
import { matchReqShapeSignals } from '#core/patterns/req-shapes';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {'reqScoped'} DesignShapeQuestionScope
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {object} DesignShapeAnswerChoice
|
|
32
|
+
* @property {string} value the answer id written into designShapeAnswers[]
|
|
33
|
+
* @property {string} display operator-facing label
|
|
34
|
+
* @property {boolean} triggersOptOut whether choosing this writes into baselineAcOptOuts[]
|
|
35
|
+
* @property {string} [uiBaselineWritePath] dot path into uiBaseline.defaults when known
|
|
36
|
+
* @property {boolean|string} [uiBaselineWriteValue] value to write at that path
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @typedef {object} DesignShapeQuestion
|
|
41
|
+
* @property {string} id catalogue id (for example, auth.htmlLoginPage)
|
|
42
|
+
* @property {DesignShapeQuestionScope} scope
|
|
43
|
+
* @property {(reqDoc: object) => boolean} trigger returns true when the REQ should be asked this question
|
|
44
|
+
* @property {string} prompt canonical prompt text
|
|
45
|
+
* @property {DesignShapeAnswerChoice[]} choices
|
|
46
|
+
* @property {number} reasonMinLength minimum reason length when the answer triggers a ledger write; matches Track C+D floor
|
|
47
|
+
* @property {string} baselineKey Track C+D baseline key this question backs, for opt-out ledger writes
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* True when a REQ carries the `auth` shape. Reads
|
|
52
|
+
* `req.shapeClassification.shapes[]` when the classifier has run (Track
|
|
53
|
+
* C+D populates this); falls back to a live `matchReqShapeSignals` pass
|
|
54
|
+
* over the REQ's title, description and rationale when the field is
|
|
55
|
+
* absent (Track C+D classifier not yet run on this repo).
|
|
56
|
+
*
|
|
57
|
+
* @param {object} reqDoc
|
|
58
|
+
* @returns {boolean}
|
|
59
|
+
*/
|
|
60
|
+
function reqCarriesAuthShape(reqDoc) {
|
|
61
|
+
const classified = reqDoc?.shapeClassification?.shapes;
|
|
62
|
+
if (Array.isArray(classified) && classified.length > 0) {
|
|
63
|
+
return classified.includes('auth');
|
|
64
|
+
}
|
|
65
|
+
const parts = [reqDoc?.title, reqDoc?.description, reqDoc?.rationale]
|
|
66
|
+
.filter((s) => typeof s === 'string' && s.length > 0);
|
|
67
|
+
for (const text of parts) {
|
|
68
|
+
for (const m of matchReqShapeSignals(text)) {
|
|
69
|
+
if (m.shape === 'auth') return true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Deep-freeze helper. Object.freeze is shallow, so a caller could mutate
|
|
77
|
+
* a nested field even on an outwardly frozen object. Review N-6
|
|
78
|
+
* (non-blocking): the catalogue is a spec-committed data surface and
|
|
79
|
+
* must not drift in memory. This helper recurses through plain objects
|
|
80
|
+
* and arrays, freezing every reachable value, and returns the input
|
|
81
|
+
* for chaining. Functions (used as `trigger`) are frozen at the field
|
|
82
|
+
* level (Object.freeze on the containing object) but not descended
|
|
83
|
+
* into.
|
|
84
|
+
*
|
|
85
|
+
* @template T
|
|
86
|
+
* @param {T} value
|
|
87
|
+
* @returns {Readonly<T>}
|
|
88
|
+
*/
|
|
89
|
+
function deepFreeze(value) {
|
|
90
|
+
if (value === null || typeof value !== 'object') return value;
|
|
91
|
+
if (Object.isFrozen(value)) return value;
|
|
92
|
+
Object.freeze(value);
|
|
93
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
94
|
+
const nested = /** @type {any} */ (value)[key];
|
|
95
|
+
if (nested !== null && typeof nested === 'object' && !Object.isFrozen(nested)) {
|
|
96
|
+
deepFreeze(nested);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Ratified catalogue. Editing this array in-place is a spec change;
|
|
104
|
+
* adding a question requires evidence per §A.3. Deep-frozen so the
|
|
105
|
+
* shape cannot drift in memory even at nested field level (review
|
|
106
|
+
* N-6).
|
|
107
|
+
*
|
|
108
|
+
* @type {Readonly<DesignShapeQuestion[]>}
|
|
109
|
+
*/
|
|
110
|
+
export const CATALOGUE_V1 = deepFreeze([
|
|
111
|
+
{
|
|
112
|
+
id: 'auth.htmlLoginPage',
|
|
113
|
+
scope: 'reqScoped',
|
|
114
|
+
trigger: reqCarriesAuthShape,
|
|
115
|
+
prompt: 'HTML login page or API-only?',
|
|
116
|
+
choices: [
|
|
117
|
+
{
|
|
118
|
+
value: 'htmlLoginPage',
|
|
119
|
+
display: 'HTML login page',
|
|
120
|
+
triggersOptOut: false,
|
|
121
|
+
uiBaselineWritePath: 'defaults.authFlow.htmlLoginPageRequired',
|
|
122
|
+
uiBaselineWriteValue: true,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
value: 'apiOnly',
|
|
126
|
+
display: 'API-only',
|
|
127
|
+
triggersOptOut: true,
|
|
128
|
+
uiBaselineWritePath: 'defaults.authFlow.htmlLoginPageRequired',
|
|
129
|
+
uiBaselineWriteValue: false,
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
reasonMinLength: 20,
|
|
133
|
+
baselineKey: 'auth.htmlLoginPage',
|
|
134
|
+
},
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Given the REQs in scope, decide which questions to pose against each
|
|
139
|
+
* REQ. Returns one entry per (question, req) pair whose trigger fires;
|
|
140
|
+
* the interactive session iterates in the returned order.
|
|
141
|
+
*
|
|
142
|
+
* @param {object[]} reqs
|
|
143
|
+
* @param {Readonly<DesignShapeQuestion[]>} [catalogue]
|
|
144
|
+
* @returns {Array<{ question: DesignShapeQuestion, reqId: string }>}
|
|
145
|
+
*/
|
|
146
|
+
export function selectApplicableQuestions(reqs, catalogue = CATALOGUE_V1) {
|
|
147
|
+
/** @type {Array<{ question: DesignShapeQuestion, reqId: string }>} */
|
|
148
|
+
const out = [];
|
|
149
|
+
for (const req of reqs ?? []) {
|
|
150
|
+
if (!req?.reqId) continue;
|
|
151
|
+
for (const question of catalogue) {
|
|
152
|
+
if (question.scope !== 'reqScoped') continue;
|
|
153
|
+
if (question.trigger(req)) {
|
|
154
|
+
out.push({ question, reqId: req.reqId });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Validate a design-shape answer against the catalogue. Returns null on
|
|
163
|
+
* a valid answer, or a string message describing the problem.
|
|
164
|
+
*
|
|
165
|
+
* @param {object} args
|
|
166
|
+
* @param {string} args.questionId
|
|
167
|
+
* @param {string} args.answer
|
|
168
|
+
* @param {string} [args.reason]
|
|
169
|
+
* @returns {string|null}
|
|
170
|
+
*/
|
|
171
|
+
export function validateDesignShapeAnswer({ questionId, answer, reason }) {
|
|
172
|
+
const question = CATALOGUE_V1.find((q) => q.id === questionId);
|
|
173
|
+
if (!question) return `unknown design-shape question '${questionId}'`;
|
|
174
|
+
const choice = question.choices.find((c) => c.value === answer);
|
|
175
|
+
if (!choice) {
|
|
176
|
+
const values = question.choices.map((c) => c.value).join(' | ');
|
|
177
|
+
return `unknown answer '${answer}' for question '${questionId}' (expected ${values})`;
|
|
178
|
+
}
|
|
179
|
+
if (choice.triggersOptOut) {
|
|
180
|
+
if (typeof reason !== 'string' || reason.length < question.reasonMinLength) {
|
|
181
|
+
return `answer '${answer}' for question '${questionId}' writes a baseline opt-out; reason must be at least ${question.reasonMinLength} characters`;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Barrel: pre-flight surface for verification-integrity-cluster-spec §4.
|
|
2
|
+
//
|
|
3
|
+
// Consumers reach the pre-flight surface through this barrel so the
|
|
4
|
+
// internal file layout (secrets side-file, scanner, session, design
|
|
5
|
+
// shapes, manifest writer) can evolve without churning callers. The
|
|
6
|
+
// gitignore aggregator seam is the sole exception: `managed-gitignore.js`
|
|
7
|
+
// imports `preflightEntry` directly from `./secrets.js` to keep the
|
|
8
|
+
// aggregator's own import list flat, one entry per module (0.6.0 spec
|
|
9
|
+
// §4.1 D-4 pattern).
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
CATALOGUE_V1,
|
|
13
|
+
} from './design-shapes.js';
|
|
14
|
+
import {
|
|
15
|
+
composeOptOutRecord,
|
|
16
|
+
} from './manifest-writer.js';
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
preflightEntry,
|
|
20
|
+
preflightSecretsPath,
|
|
21
|
+
loadPreflightSecretsFile,
|
|
22
|
+
recordPreflightSecret,
|
|
23
|
+
} from './secrets.js';
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
scanForServiceCandidates,
|
|
27
|
+
CATEGORY_DISPLAY_NAMES,
|
|
28
|
+
} from './scanner.js';
|
|
29
|
+
|
|
30
|
+
export {
|
|
31
|
+
CATALOGUE_V1,
|
|
32
|
+
selectApplicableQuestions,
|
|
33
|
+
validateDesignShapeAnswer,
|
|
34
|
+
} from './design-shapes.js';
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
composePreflightRecord,
|
|
38
|
+
composeOptOutRecord,
|
|
39
|
+
nextPreflightId,
|
|
40
|
+
nextOptOutId,
|
|
41
|
+
writePreflightRecord,
|
|
42
|
+
} from './manifest-writer.js';
|
|
43
|
+
|
|
44
|
+
export {
|
|
45
|
+
runInteractiveSession,
|
|
46
|
+
normaliseNonInteractiveInput,
|
|
47
|
+
} from './session.js';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Compose the baselineAcOptOuts entries triggered by design-shape
|
|
51
|
+
* answers on a preflight record. Called by the CLI handler after
|
|
52
|
+
* `composePreflightRecord` so the `linkedPreFlightConfigRef` can point
|
|
53
|
+
* at the composed record's id.
|
|
54
|
+
*
|
|
55
|
+
* @param {object} args
|
|
56
|
+
* @param {object|null} args.manifest
|
|
57
|
+
* @param {object} args.preflightRecord the composed record (id already allocated)
|
|
58
|
+
* @param {import('./manifest-writer.js').PreflightDesignShapeAnswer[]} args.designShapeAnswers
|
|
59
|
+
* @param {Date} [args.now]
|
|
60
|
+
* @returns {object[]}
|
|
61
|
+
*/
|
|
62
|
+
export function composeDesignShapeOptOuts({ manifest, preflightRecord, designShapeAnswers, now = new Date() }) {
|
|
63
|
+
const optOuts = [];
|
|
64
|
+
let dateCursor = now;
|
|
65
|
+
for (const answer of designShapeAnswers) {
|
|
66
|
+
const question = CATALOGUE_V1.find((q) => q.id === answer.questionId);
|
|
67
|
+
if (!question) continue;
|
|
68
|
+
const choice = question.choices.find((c) => c.value === answer.answer);
|
|
69
|
+
if (!choice || !choice.triggersOptOut) continue;
|
|
70
|
+
// Compose one opt-out at a time so the id monotonic counter walks
|
|
71
|
+
// forward correctly on a single-session batch. We synthesise a
|
|
72
|
+
// provisional manifest snapshot per compose so `nextOptOutId` sees
|
|
73
|
+
// any records the previous compose calls in this loop added.
|
|
74
|
+
const provisional = {
|
|
75
|
+
...(manifest ?? {}),
|
|
76
|
+
baselineAcOptOuts: [...(manifest?.baselineAcOptOuts ?? []), ...optOuts],
|
|
77
|
+
};
|
|
78
|
+
const linkedPreFlightConfigRef = `${preflightRecord.id}#designShapeAnswers.${answer.questionId}`;
|
|
79
|
+
const record = composeOptOutRecord({
|
|
80
|
+
manifest: provisional,
|
|
81
|
+
baselineKey: question.baselineKey,
|
|
82
|
+
reason: answer.reason ?? '',
|
|
83
|
+
reqId: answer.reqId,
|
|
84
|
+
linkedPreFlightConfigRef,
|
|
85
|
+
now: dateCursor,
|
|
86
|
+
});
|
|
87
|
+
optOuts.push(record);
|
|
88
|
+
}
|
|
89
|
+
return optOuts;
|
|
90
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// Pre-flight manifest writer
|
|
2
|
+
// (verification-integrity-cluster-spec §3.3, ADDENDUM §A.2).
|
|
3
|
+
//
|
|
4
|
+
// Composes and persists `preFlightConfig[]` records on the project
|
|
5
|
+
// manifest. The record shape is defined by the 0.4.0 manifest schema
|
|
6
|
+
// (`$defs/preFlightConfigRecord`); this module owns id allocation,
|
|
7
|
+
// timestamping, per-service entry composition, design-shape answer
|
|
8
|
+
// composition, and the `baselineAcOptOuts[]` fold that happens when a
|
|
9
|
+
// design-shape answer triggers a ledger write per §A.2.
|
|
10
|
+
//
|
|
11
|
+
// The writer takes an already-loaded walker tree and a session summary,
|
|
12
|
+
// runs the composition, and calls the core store's `updateDocument`
|
|
13
|
+
// against the manifest. All secrets discipline is upstream: this module
|
|
14
|
+
// never sees a credential value; the session's per-service records carry
|
|
15
|
+
// only booleans (`credentialSupplied`, `sandboxProvisioned`) and the
|
|
16
|
+
// env-var NAME (side-file, not chain).
|
|
17
|
+
|
|
18
|
+
import { mkdir, rename, unlink, writeFile } from 'node:fs/promises';
|
|
19
|
+
import { dirname, join } from 'node:path';
|
|
20
|
+
|
|
21
|
+
import { rcfError } from '#core/errors';
|
|
22
|
+
import { validateDocument } from '#core/store';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {object} PreflightServiceRuling
|
|
26
|
+
* @property {string} id
|
|
27
|
+
* @property {string} displayName
|
|
28
|
+
* @property {string[]} sourceRefs
|
|
29
|
+
* @property {'live'|'sandboxed'|'mocked'|'declaredMockOnly'|'notShipped'} attestationMode
|
|
30
|
+
* @property {boolean} credentialSupplied
|
|
31
|
+
* @property {boolean} sandboxProvisioned
|
|
32
|
+
* @property {string} [operatorReason]
|
|
33
|
+
* @property {string[]} [affectedFbsIds]
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {object} PreflightDesignShapeAnswer
|
|
38
|
+
* @property {string} questionId
|
|
39
|
+
* @property {string} [reqId]
|
|
40
|
+
* @property {string} answer
|
|
41
|
+
* @property {string} [reason]
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {object} BaselineOptOutWrite
|
|
46
|
+
* @property {string} baselineKey
|
|
47
|
+
* @property {'req'|'project'} scope
|
|
48
|
+
* @property {string} reason
|
|
49
|
+
* @property {string} [reqId]
|
|
50
|
+
* @property {string} linkedPreFlightConfigRef
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Compute the next preFlightConfig id for today: monotonic
|
|
55
|
+
* `pfc-YYYY-MM-DD-NNN`. Reads the manifest's existing preFlightConfig
|
|
56
|
+
* to find the highest suffix for the given date.
|
|
57
|
+
*
|
|
58
|
+
* @param {object|null} manifest
|
|
59
|
+
* @param {Date} now
|
|
60
|
+
* @returns {string}
|
|
61
|
+
*/
|
|
62
|
+
export function nextPreflightId(manifest, now) {
|
|
63
|
+
const y = now.getUTCFullYear().toString().padStart(4, '0');
|
|
64
|
+
const m = (now.getUTCMonth() + 1).toString().padStart(2, '0');
|
|
65
|
+
const d = now.getUTCDate().toString().padStart(2, '0');
|
|
66
|
+
const prefix = `pfc-${y}-${m}-${d}-`;
|
|
67
|
+
const existing = Array.isArray(manifest?.preFlightConfig) ? manifest.preFlightConfig : [];
|
|
68
|
+
let maxN = 0;
|
|
69
|
+
for (const rec of existing) {
|
|
70
|
+
if (typeof rec?.id !== 'string' || !rec.id.startsWith(prefix)) continue;
|
|
71
|
+
const suffix = rec.id.slice(prefix.length);
|
|
72
|
+
const n = Number.parseInt(suffix, 10);
|
|
73
|
+
if (Number.isFinite(n) && n > maxN) maxN = n;
|
|
74
|
+
}
|
|
75
|
+
const next = (maxN + 1).toString().padStart(3, '0');
|
|
76
|
+
return `${prefix}${next}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Compute the next baselineAcOptOuts id: monotonic `boo-YYYY-MM-DD-NNN`.
|
|
81
|
+
*
|
|
82
|
+
* @param {object|null} manifest
|
|
83
|
+
* @param {Date} now
|
|
84
|
+
* @returns {string}
|
|
85
|
+
*/
|
|
86
|
+
export function nextOptOutId(manifest, now) {
|
|
87
|
+
const y = now.getUTCFullYear().toString().padStart(4, '0');
|
|
88
|
+
const m = (now.getUTCMonth() + 1).toString().padStart(2, '0');
|
|
89
|
+
const d = now.getUTCDate().toString().padStart(2, '0');
|
|
90
|
+
const prefix = `boo-${y}-${m}-${d}-`;
|
|
91
|
+
const existing = Array.isArray(manifest?.baselineAcOptOuts) ? manifest.baselineAcOptOuts : [];
|
|
92
|
+
let maxN = 0;
|
|
93
|
+
for (const rec of existing) {
|
|
94
|
+
if (typeof rec?.id !== 'string' || !rec.id.startsWith(prefix)) continue;
|
|
95
|
+
const n = Number.parseInt(rec.id.slice(prefix.length), 10);
|
|
96
|
+
if (Number.isFinite(n) && n > maxN) maxN = n;
|
|
97
|
+
}
|
|
98
|
+
return `${prefix}${(maxN + 1).toString().padStart(3, '0')}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Compose one preFlightConfig record from a session. The record's
|
|
103
|
+
* `id` is monotonic per date; `createdAt` and `operatorAckAt` are the
|
|
104
|
+
* same instant for a completed session.
|
|
105
|
+
*
|
|
106
|
+
* @param {object} args
|
|
107
|
+
* @param {object|null} args.manifest
|
|
108
|
+
* @param {string} args.prdId
|
|
109
|
+
* @param {PreflightServiceRuling[]} args.services
|
|
110
|
+
* @param {PreflightDesignShapeAnswer[]} [args.designShapeAnswers]
|
|
111
|
+
* @param {Date} [args.now]
|
|
112
|
+
* @returns {object}
|
|
113
|
+
*/
|
|
114
|
+
export function composePreflightRecord({
|
|
115
|
+
manifest, prdId, services, designShapeAnswers = [], now = new Date(),
|
|
116
|
+
}) {
|
|
117
|
+
const isoNow = now.toISOString();
|
|
118
|
+
const id = nextPreflightId(manifest, now);
|
|
119
|
+
const servicesInScope = services.map((s) => {
|
|
120
|
+
const entry = {
|
|
121
|
+
id: s.id,
|
|
122
|
+
displayName: s.displayName,
|
|
123
|
+
sourceRefs: Array.isArray(s.sourceRefs) ? [...s.sourceRefs] : [],
|
|
124
|
+
attestationMode: s.attestationMode,
|
|
125
|
+
credentialSupplied: Boolean(s.credentialSupplied),
|
|
126
|
+
sandboxProvisioned: Boolean(s.sandboxProvisioned),
|
|
127
|
+
};
|
|
128
|
+
if (typeof s.operatorReason === 'string' && s.operatorReason.length > 0) {
|
|
129
|
+
entry.operatorReason = s.operatorReason;
|
|
130
|
+
}
|
|
131
|
+
if (Array.isArray(s.affectedFbsIds) && s.affectedFbsIds.length > 0) {
|
|
132
|
+
entry.affectedFbsIds = [...s.affectedFbsIds];
|
|
133
|
+
}
|
|
134
|
+
return entry;
|
|
135
|
+
});
|
|
136
|
+
const record = {
|
|
137
|
+
id,
|
|
138
|
+
createdAt: isoNow,
|
|
139
|
+
prdId,
|
|
140
|
+
servicesInScope,
|
|
141
|
+
operatorAckAt: isoNow,
|
|
142
|
+
};
|
|
143
|
+
if (Array.isArray(designShapeAnswers) && designShapeAnswers.length > 0) {
|
|
144
|
+
record.designShapeAnswers = designShapeAnswers.map((a) => {
|
|
145
|
+
const entry = { questionId: a.questionId, answer: a.answer, answeredAt: isoNow };
|
|
146
|
+
if (typeof a.reqId === 'string' && a.reqId.length > 0) entry.reqId = a.reqId;
|
|
147
|
+
if (typeof a.reason === 'string' && a.reason.length > 0) entry.reason = a.reason;
|
|
148
|
+
return entry;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return record;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Compose a baselineAcOptOuts entry for a design-shape answer that
|
|
156
|
+
* triggers a ledger write (ADDENDUM §A.2). The record is REQ-scoped
|
|
157
|
+
* when the answer is REQ-scoped; project scope requires an explicit
|
|
158
|
+
* upstream ruling.
|
|
159
|
+
*
|
|
160
|
+
* @param {object} args
|
|
161
|
+
* @param {object|null} args.manifest
|
|
162
|
+
* @param {string} args.baselineKey
|
|
163
|
+
* @param {string} args.reason
|
|
164
|
+
* @param {string} [args.reqId]
|
|
165
|
+
* @param {string} args.linkedPreFlightConfigRef
|
|
166
|
+
* @param {Date} [args.now]
|
|
167
|
+
* @returns {object}
|
|
168
|
+
*/
|
|
169
|
+
export function composeOptOutRecord({
|
|
170
|
+
manifest, baselineKey, reason, reqId, linkedPreFlightConfigRef, now = new Date(),
|
|
171
|
+
}) {
|
|
172
|
+
const isoNow = now.toISOString();
|
|
173
|
+
const id = nextOptOutId(manifest, now);
|
|
174
|
+
const record = {
|
|
175
|
+
id,
|
|
176
|
+
createdAt: isoNow,
|
|
177
|
+
baselineKey,
|
|
178
|
+
scope: reqId ? 'req' : 'project',
|
|
179
|
+
reason,
|
|
180
|
+
operatorAckAt: isoNow,
|
|
181
|
+
linkedPreFlightConfigRef,
|
|
182
|
+
};
|
|
183
|
+
if (reqId) record.reqId = reqId;
|
|
184
|
+
return record;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Persist a preflight session: append the composed record onto the
|
|
189
|
+
* manifest's `preFlightConfig[]` and any triggered opt-out records onto
|
|
190
|
+
* `baselineAcOptOuts[]`. Validates the composed manifest against the
|
|
191
|
+
* schema BEFORE writing; on validation failure the on-disk manifest is
|
|
192
|
+
* untouched. Atomic write via a `.tmp` rename so a partial write cannot
|
|
193
|
+
* leave the manifest half-composed.
|
|
194
|
+
*
|
|
195
|
+
* The store's `updateDocument` verb accepts child-doc and root-doc ids
|
|
196
|
+
* (PRD / TAD / BS) via `pathForId`, but the manifest sits outside that
|
|
197
|
+
* id space; every other manifest mutation the codebase performs also
|
|
198
|
+
* goes direct-to-disk (see `initProject` in `core/store/init.js`), which
|
|
199
|
+
* this helper mirrors.
|
|
200
|
+
*
|
|
201
|
+
* @param {object} args
|
|
202
|
+
* @param {string} args.projectRoot
|
|
203
|
+
* @param {import('#core/store/walker.js').TreeModel} args.tree
|
|
204
|
+
* @param {object} args.record the composed preFlightConfig record
|
|
205
|
+
* @param {object[]} [args.optOuts] composed baselineAcOptOuts entries
|
|
206
|
+
* @param {object} [args.uiBaselineWrites] optional deep-set writes for uiBaseline.defaults (Track B fenced-TODO)
|
|
207
|
+
* @param {object} [args.options]
|
|
208
|
+
* @returns {Promise<{ record: object, optOutIds: string[], skippedUiBaseline: boolean } | import('#core/errors').RcfError>}
|
|
209
|
+
*/
|
|
210
|
+
export async function writePreflightRecord({
|
|
211
|
+
projectRoot, tree, record, optOuts = [], options = {},
|
|
212
|
+
}) {
|
|
213
|
+
const manifest = tree.manifest ?? {};
|
|
214
|
+
const nextManifest = { ...manifest };
|
|
215
|
+
|
|
216
|
+
const existingPfc = Array.isArray(nextManifest.preFlightConfig) ? nextManifest.preFlightConfig : [];
|
|
217
|
+
nextManifest.preFlightConfig = [...existingPfc, record];
|
|
218
|
+
|
|
219
|
+
if (optOuts.length > 0) {
|
|
220
|
+
const existingOptOuts = Array.isArray(nextManifest.baselineAcOptOuts) ? nextManifest.baselineAcOptOuts : [];
|
|
221
|
+
nextManifest.baselineAcOptOuts = [...existingOptOuts, ...optOuts];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const relPath = 'rcf/manifest.json';
|
|
225
|
+
const validation = validateDocument({ doc: nextManifest, kind: 'manifest', filePath: relPath });
|
|
226
|
+
if (validation) return validation;
|
|
227
|
+
|
|
228
|
+
const absPath = join(projectRoot, 'rcf', 'manifest.json');
|
|
229
|
+
if (options.dryRun) {
|
|
230
|
+
return { record, optOutIds: optOuts.map((o) => o.id), skippedUiBaseline: true, dryRun: true };
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
await mkdir(dirname(absPath), { recursive: true });
|
|
234
|
+
const tmp = `${absPath}.tmp`;
|
|
235
|
+
await writeFile(tmp, `${JSON.stringify(nextManifest, null, 2)}\n`, 'utf8');
|
|
236
|
+
try {
|
|
237
|
+
await rename(tmp, absPath);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
try { await unlink(tmp); } catch { /* ignore */ }
|
|
240
|
+
throw err;
|
|
241
|
+
}
|
|
242
|
+
} catch (err) {
|
|
243
|
+
return rcfError({
|
|
244
|
+
kind: 'ioFailure',
|
|
245
|
+
message: `preflight: manifest write failed: ${err.message}`,
|
|
246
|
+
filePath: relPath,
|
|
247
|
+
stack: err.stack,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
record,
|
|
252
|
+
optOutIds: optOuts.map((o) => o.id),
|
|
253
|
+
// Track A does NOT build the uiBaseline init machinery (that's
|
|
254
|
+
// Track B). When a design-shape answer would write into
|
|
255
|
+
// `uiBaseline.defaults.authFlow.htmlLoginPageRequired` and no
|
|
256
|
+
// `uiBaseline` record exists yet, the answer stays in
|
|
257
|
+
// designShapeAnswers[] and (when the answer is API-only) in the
|
|
258
|
+
// baselineAcOptOuts[] ledger, exactly as ADDENDUM §A.2 specifies.
|
|
259
|
+
// The uiBaseline write itself is fenced pending Track B — the
|
|
260
|
+
// linkedPreFlightConfigRef on the opt-out record is the seam
|
|
261
|
+
// Track B reads.
|
|
262
|
+
skippedUiBaseline: true,
|
|
263
|
+
};
|
|
264
|
+
}
|