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,206 @@
|
|
|
1
|
+
// Pre-flight service candidate scanner
|
|
2
|
+
// (verification-integrity-cluster-spec §4.3 / §8.3).
|
|
3
|
+
//
|
|
4
|
+
// Deterministic candidate GENERATOR (not a decider). Reads the named
|
|
5
|
+
// PRD and optionally a TAD from the RCF chain, walks every string-shaped
|
|
6
|
+
// prose field on those documents with core's `matchServiceSignals`
|
|
7
|
+
// (imported, never re-implemented — the seed pattern set is the shared
|
|
8
|
+
// source of truth per §5.3), and returns per-candidate provenance
|
|
9
|
+
// records the interactive session then walks with the operator.
|
|
10
|
+
//
|
|
11
|
+
// The scanner intentionally over-collects (spec §4.3 / §4.4): false
|
|
12
|
+
// positives are cheap (the operator dismisses in-session), false
|
|
13
|
+
// negatives are the failure mode that motivated the whole cluster (the
|
|
14
|
+
// d-142 "email channel" miss). Every match records the source doc, a
|
|
15
|
+
// section-anchor guess (heading nearest the match), the matched
|
|
16
|
+
// substring, and the category the seed set fired under.
|
|
17
|
+
//
|
|
18
|
+
// This module is pure: it takes a walker tree (already loaded from
|
|
19
|
+
// disk) plus a target PRD id and optional TAD id, and returns a
|
|
20
|
+
// candidate list. Every side-effect (filesystem, prompting) belongs to
|
|
21
|
+
// the CLI handler in `../cli/preflight.js`.
|
|
22
|
+
|
|
23
|
+
import { matchServiceSignals, SERVICE_CATEGORY_KEYS } from '#core/patterns/services';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {object} ScannerSourceRef
|
|
27
|
+
* @property {string} docId the PRD / TAD id
|
|
28
|
+
* @property {string} anchor the section-anchor guess (heading id / index)
|
|
29
|
+
* @property {string} phrase the matched substring
|
|
30
|
+
* @property {string} category the seed-set category the match fired under
|
|
31
|
+
* @property {string} role 'token' | 'verb' | 'vendor'
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {object} ServiceCandidate
|
|
36
|
+
* @property {string} id suggested camelCase service id (best-guess vendor if present, else category)
|
|
37
|
+
* @property {string} displayName human-readable label
|
|
38
|
+
* @property {string} category the dominant category (first hit)
|
|
39
|
+
* @property {ScannerSourceRef[]} sourceRefs
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @typedef {object} ScannerResult
|
|
44
|
+
* @property {ServiceCandidate[]} candidates
|
|
45
|
+
* @property {string[]} scannedDocIds
|
|
46
|
+
* @property {string[]} skippedDocIds docs the caller asked for that were not found
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Extract every string-typed value under `doc` recursively, tagged with
|
|
51
|
+
* a synthetic anchor derived from the JSON pointer. The anchor lets the
|
|
52
|
+
* operator locate the match in the source without asking the scanner
|
|
53
|
+
* to understand every RCF document shape.
|
|
54
|
+
*
|
|
55
|
+
* @param {object} doc
|
|
56
|
+
* @returns {Array<{ anchor: string, text: string }>}
|
|
57
|
+
*/
|
|
58
|
+
function collectProseFields(doc) {
|
|
59
|
+
/** @type {Array<{ anchor: string, text: string }>} */
|
|
60
|
+
const out = [];
|
|
61
|
+
const stack = [{ node: doc, path: '' }];
|
|
62
|
+
while (stack.length > 0) {
|
|
63
|
+
const { node, path } = stack.pop();
|
|
64
|
+
if (node == null) continue;
|
|
65
|
+
if (typeof node === 'string') {
|
|
66
|
+
// Ignore very short strings (ids / statuses) unless they carry a
|
|
67
|
+
// space (a phrase); anything ≥ 3 chars with a space is prose-ish.
|
|
68
|
+
if (node.length >= 6 && node.includes(' ')) {
|
|
69
|
+
out.push({ anchor: path || '/', text: node });
|
|
70
|
+
}
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (Array.isArray(node)) {
|
|
74
|
+
for (let i = node.length - 1; i >= 0; i -= 1) {
|
|
75
|
+
stack.push({ node: node[i], path: `${path}/${i}` });
|
|
76
|
+
}
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (typeof node === 'object') {
|
|
80
|
+
for (const [k, v] of Object.entries(node)) {
|
|
81
|
+
stack.push({ node: v, path: `${path}/${k}` });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const CATEGORY_DISPLAY = {
|
|
89
|
+
emailDelivery: 'Email delivery service',
|
|
90
|
+
payment: 'Payment provider',
|
|
91
|
+
smsVoice: 'SMS or voice provider',
|
|
92
|
+
auth: 'Identity / auth provider',
|
|
93
|
+
storageCdn: 'Storage or CDN provider',
|
|
94
|
+
llmAi: 'LLM / AI provider',
|
|
95
|
+
analyticsTelemetry: 'Analytics or telemetry provider',
|
|
96
|
+
featureFlags: 'Feature-flag provider',
|
|
97
|
+
search: 'Search provider',
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Best-guess camelCase id for a candidate: vendor name (if any hit was
|
|
102
|
+
* a vendor role) wins; otherwise the category id. Vendor names are
|
|
103
|
+
* lower-cased and non-alphanumerics stripped.
|
|
104
|
+
*
|
|
105
|
+
* @param {ScannerSourceRef[]} refs
|
|
106
|
+
* @param {string} fallbackCategory
|
|
107
|
+
* @returns {{ id: string, displayName: string }}
|
|
108
|
+
*/
|
|
109
|
+
function chooseCandidateName(refs, fallbackCategory) {
|
|
110
|
+
const vendor = refs.find((r) => r.role === 'vendor');
|
|
111
|
+
if (vendor) {
|
|
112
|
+
const cleaned = vendor.phrase.toLowerCase().replace(/[^a-z0-9]+/g, '');
|
|
113
|
+
if (cleaned.length > 0) {
|
|
114
|
+
const displayName = vendor.phrase.trim();
|
|
115
|
+
return { id: cleaned, displayName };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
id: fallbackCategory,
|
|
120
|
+
displayName: CATEGORY_DISPLAY[fallbackCategory] ?? fallbackCategory,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Group signal matches into service candidates. Groups by category
|
|
126
|
+
* (§4.4's nine categories); one candidate per category that fired at
|
|
127
|
+
* least once. Vendor hits within a category promote the candidate's
|
|
128
|
+
* suggested id + displayName. The operator can split (add a new
|
|
129
|
+
* candidate) or merge in the interactive session — the scanner does
|
|
130
|
+
* NOT try to be a classifier (§4.3 candidate generator, not decider).
|
|
131
|
+
*
|
|
132
|
+
* @param {ScannerSourceRef[]} refsInDocOrder
|
|
133
|
+
* @returns {ServiceCandidate[]}
|
|
134
|
+
*/
|
|
135
|
+
function groupIntoCandidates(refsInDocOrder) {
|
|
136
|
+
/** @type {Map<string, ScannerSourceRef[]>} */
|
|
137
|
+
const byCategory = new Map();
|
|
138
|
+
for (const ref of refsInDocOrder) {
|
|
139
|
+
if (!byCategory.has(ref.category)) byCategory.set(ref.category, []);
|
|
140
|
+
byCategory.get(ref.category).push(ref);
|
|
141
|
+
}
|
|
142
|
+
/** @type {ServiceCandidate[]} */
|
|
143
|
+
const candidates = [];
|
|
144
|
+
// Preserve seed-set category order for a stable presentation.
|
|
145
|
+
for (const category of SERVICE_CATEGORY_KEYS) {
|
|
146
|
+
const refs = byCategory.get(category);
|
|
147
|
+
if (!refs || refs.length === 0) continue;
|
|
148
|
+
const { id, displayName } = chooseCandidateName(refs, category);
|
|
149
|
+
candidates.push({ id, displayName, category, sourceRefs: refs });
|
|
150
|
+
}
|
|
151
|
+
return candidates;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Scan the named PRD (and optional TAD) for third-party service
|
|
156
|
+
* candidates. The tree is already loaded; the scanner reads only the
|
|
157
|
+
* documents named by id.
|
|
158
|
+
*
|
|
159
|
+
* @param {object} args
|
|
160
|
+
* @param {import('#core/store/walker.js').TreeModel} args.tree
|
|
161
|
+
* @param {string} args.prdId
|
|
162
|
+
* @param {string} [args.tadId]
|
|
163
|
+
* @returns {ScannerResult}
|
|
164
|
+
*/
|
|
165
|
+
export function scanForServiceCandidates({ tree, prdId, tadId }) {
|
|
166
|
+
/** @type {ScannerSourceRef[]} */
|
|
167
|
+
const refs = [];
|
|
168
|
+
const scannedDocIds = [];
|
|
169
|
+
const skippedDocIds = [];
|
|
170
|
+
|
|
171
|
+
const targets = [{ id: prdId, kind: 'prd' }];
|
|
172
|
+
if (tadId) targets.push({ id: tadId, kind: 'tad' });
|
|
173
|
+
|
|
174
|
+
for (const { id, kind } of targets) {
|
|
175
|
+
const doc = tree.byId.get(id) ?? (kind === 'prd' ? tree.prd : kind === 'tad' ? tree.tad : null);
|
|
176
|
+
if (!doc) {
|
|
177
|
+
skippedDocIds.push(id);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
// Sanity-check the id matches the loaded doc (walker binds ids into
|
|
181
|
+
// byId by their own id field).
|
|
182
|
+
scannedDocIds.push(id);
|
|
183
|
+
const proseFields = collectProseFields(doc);
|
|
184
|
+
for (const { anchor, text } of proseFields) {
|
|
185
|
+
const matches = matchServiceSignals(text);
|
|
186
|
+
for (const m of matches) {
|
|
187
|
+
refs.push({
|
|
188
|
+
docId: id,
|
|
189
|
+
anchor,
|
|
190
|
+
phrase: m.match,
|
|
191
|
+
category: m.category,
|
|
192
|
+
role: m.role,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const candidates = groupIntoCandidates(refs);
|
|
199
|
+
return { candidates, scannedDocIds, skippedDocIds };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Public accessor for the display-name lookup so the interactive
|
|
204
|
+
* session's presentation can stay in one place.
|
|
205
|
+
*/
|
|
206
|
+
export const CATEGORY_DISPLAY_NAMES = Object.freeze({ ...CATEGORY_DISPLAY });
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// Pre-flight credentials side-file (verification-integrity-cluster-spec §4.6).
|
|
2
|
+
//
|
|
3
|
+
// Credentials NEVER enter the RCF chain: the preflight session captures
|
|
4
|
+
// only the env-var NAME the credential lives under, plus a boolean
|
|
5
|
+
// snapshot of whether that env var was present at session time. Values
|
|
6
|
+
// are read from the shell at test/finalise time; the chain is
|
|
7
|
+
// grep-friendly for names and audit-friendly for provenance without
|
|
8
|
+
// ever holding a token.
|
|
9
|
+
//
|
|
10
|
+
// This module owns:
|
|
11
|
+
// - the gitignore aggregator entry (`preflightEntry`) the shared 0.6.0
|
|
12
|
+
// `managedGitignoreEntries()` seam consumes to keep the side-file out
|
|
13
|
+
// of the shared repo by default; adding it is exactly the two-line
|
|
14
|
+
// extension the 0.6.0 spec §4.1 D-4 pattern promises;
|
|
15
|
+
// - the side-file path helper;
|
|
16
|
+
// - the loader/writer/merge helpers, all of which read the shell env
|
|
17
|
+
// var by NAME only and never accept, log, or persist a value.
|
|
18
|
+
//
|
|
19
|
+
// Redaction discipline (§4.6): every path through this module obeys
|
|
20
|
+
// three rules: values are never read into the record (only presence),
|
|
21
|
+
// values are never returned to callers (only names + presence booleans),
|
|
22
|
+
// and error messages never quote env-var contents.
|
|
23
|
+
|
|
24
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
25
|
+
import { dirname, join } from 'node:path';
|
|
26
|
+
import process from 'node:process';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The gitignore entry the aggregator consumes. Shape mirrors
|
|
30
|
+
* `identityEntry` (0.6.0 aggregator seam contract). Adding this to
|
|
31
|
+
* `managedGitignoreEntries()` in `../setup/managed-gitignore.js` is the
|
|
32
|
+
* whole extension: one import line, one array entry.
|
|
33
|
+
*/
|
|
34
|
+
export const preflightEntry = Object.freeze({
|
|
35
|
+
path: '.rcf/preflight-secrets.local.json',
|
|
36
|
+
owner: 'rcf preflight: credential name-metadata (values never enter the chain)',
|
|
37
|
+
since: '0.7.0',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const SECRETS_FILE_REL = '.rcf/preflight-secrets.local.json';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Absolute path of the side-file for a given project root.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} projectRoot
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
export function preflightSecretsPath(projectRoot) {
|
|
49
|
+
return join(projectRoot, '.rcf', 'preflight-secrets.local.json');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @typedef {object} PreflightSecretEntry
|
|
54
|
+
* @property {string} serviceId camelCase service id (matches manifest)
|
|
55
|
+
* @property {string} envVarName name only; never the value
|
|
56
|
+
* @property {boolean} envVarPresentAtSessionTime
|
|
57
|
+
* @property {string} recordedAt ISO timestamp
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @typedef {object} PreflightSecretsFile
|
|
62
|
+
* @property {string} note human-readable header
|
|
63
|
+
* @property {PreflightSecretEntry[]} entries
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
const FILE_HEADER_NOTE = 'Credential name-metadata only. Values are never written here or into the RCF chain; env vars are read from the shell at runtime.';
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Load the side-file. Returns an empty file object when the file does
|
|
70
|
+
* not exist. Never throws on missing file; propagates any other read
|
|
71
|
+
* error to the caller.
|
|
72
|
+
*
|
|
73
|
+
* @param {object} args
|
|
74
|
+
* @param {string} args.projectRoot
|
|
75
|
+
* @returns {Promise<PreflightSecretsFile>}
|
|
76
|
+
*/
|
|
77
|
+
export async function loadPreflightSecretsFile({ projectRoot }) {
|
|
78
|
+
const path = preflightSecretsPath(projectRoot);
|
|
79
|
+
let raw;
|
|
80
|
+
try {
|
|
81
|
+
raw = await readFile(path, 'utf8');
|
|
82
|
+
} catch (err) {
|
|
83
|
+
if (/** @type {NodeJS.ErrnoException} */ (err).code === 'ENOENT') {
|
|
84
|
+
return { note: FILE_HEADER_NOTE, entries: [] };
|
|
85
|
+
}
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
const parsed = JSON.parse(raw);
|
|
89
|
+
const entries = Array.isArray(parsed?.entries) ? parsed.entries : [];
|
|
90
|
+
return { note: FILE_HEADER_NOTE, entries };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Record credential name-metadata for one service. The env var VALUE is
|
|
95
|
+
* never accepted as input; the loader reads the shell to observe
|
|
96
|
+
* presence and stores the boolean only.
|
|
97
|
+
*
|
|
98
|
+
* @param {object} args
|
|
99
|
+
* @param {string} args.projectRoot
|
|
100
|
+
* @param {string} args.serviceId
|
|
101
|
+
* @param {string} args.envVarName
|
|
102
|
+
* @param {NodeJS.ProcessEnv} [args.env] defaults to process.env
|
|
103
|
+
* @param {() => string} [args.now] defaults to () => new Date().toISOString()
|
|
104
|
+
* @returns {Promise<PreflightSecretEntry>}
|
|
105
|
+
*/
|
|
106
|
+
export async function recordPreflightSecret({
|
|
107
|
+
projectRoot, serviceId, envVarName, env = process.env, now = () => new Date().toISOString(),
|
|
108
|
+
}) {
|
|
109
|
+
if (typeof serviceId !== 'string' || serviceId.length === 0) {
|
|
110
|
+
throw new Error('recordPreflightSecret: serviceId required');
|
|
111
|
+
}
|
|
112
|
+
if (typeof envVarName !== 'string' || envVarName.length === 0) {
|
|
113
|
+
throw new Error('recordPreflightSecret: envVarName required');
|
|
114
|
+
}
|
|
115
|
+
const present = Object.prototype.hasOwnProperty.call(env, envVarName)
|
|
116
|
+
&& typeof env[envVarName] === 'string'
|
|
117
|
+
&& env[envVarName].length > 0;
|
|
118
|
+
const entry = /** @type {PreflightSecretEntry} */ ({
|
|
119
|
+
serviceId,
|
|
120
|
+
envVarName,
|
|
121
|
+
envVarPresentAtSessionTime: present,
|
|
122
|
+
recordedAt: now(),
|
|
123
|
+
});
|
|
124
|
+
const file = await loadPreflightSecretsFile({ projectRoot });
|
|
125
|
+
const nextEntries = file.entries.filter((e) => e.serviceId !== serviceId);
|
|
126
|
+
nextEntries.push(entry);
|
|
127
|
+
nextEntries.sort((a, b) => a.serviceId.localeCompare(b.serviceId));
|
|
128
|
+
const outFile = { note: FILE_HEADER_NOTE, entries: nextEntries };
|
|
129
|
+
const path = preflightSecretsPath(projectRoot);
|
|
130
|
+
await mkdir(dirname(path), { recursive: true });
|
|
131
|
+
// Deterministic 2-space JSON with trailing newline for editor-friendliness.
|
|
132
|
+
await writeFile(path, `${JSON.stringify(outFile, null, 2)}\n`, 'utf8');
|
|
133
|
+
return entry;
|
|
134
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// Pre-flight interactive session
|
|
2
|
+
// (verification-integrity-cluster-spec §4.5, ADDENDUM §A.1).
|
|
3
|
+
//
|
|
4
|
+
// Walks the scanner's candidates with the operator, then poses any
|
|
5
|
+
// applicable design-shape questions. Emits a session record ready for
|
|
6
|
+
// the manifest writer. Every path in this module operates through an
|
|
7
|
+
// injected prompt/write pair so the CLI handler owns the readline
|
|
8
|
+
// wiring and the tests can drive the session without a TTY.
|
|
9
|
+
//
|
|
10
|
+
// Register (per the 0.5.2 operator-communication register — plain
|
|
11
|
+
// prose, no jargon citations, one leading question, tone of "it's in
|
|
12
|
+
// hand"): the interactive text below reads as a colleague walking the
|
|
13
|
+
// operator through decisions, not as a schema questionnaire.
|
|
14
|
+
|
|
15
|
+
import { recordPreflightSecret } from './secrets.js';
|
|
16
|
+
import { validateDesignShapeAnswer } from './design-shapes.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {(question: string) => Promise<string>} PromptFn
|
|
20
|
+
* @typedef {(line: string) => void} WriteFn
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const ATTESTATION_CHOICES = [
|
|
24
|
+
{ key: '1', mode: 'live', label: 'live — tests hit the live service with a real key you supply' },
|
|
25
|
+
{ key: '2', mode: 'sandboxed', label: 'sandboxed — provider sandbox mode with a real key (no delivery)' },
|
|
26
|
+
{ key: '3', mode: 'mocked', label: 'mocked — local fixtures or stubs; not ship-authoritative on its own' },
|
|
27
|
+
{ key: '4', mode: 'declaredMockOnly', label: 'declaredMockOnly — ship mock-only intentionally (feature-flagged off, stub for local dev, pre-launch)' },
|
|
28
|
+
{ key: '5', mode: 'notShipped', label: 'notShipped — for local development only; no production path uses it' },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const MODE_BY_KEY = new Map(ATTESTATION_CHOICES.map((c) => [c.key, c.mode]));
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {object} args
|
|
35
|
+
* @param {import('./scanner.js').ServiceCandidate} args.candidate
|
|
36
|
+
* @param {PromptFn} args.prompt
|
|
37
|
+
* @param {WriteFn} args.write
|
|
38
|
+
* @param {string} args.projectRoot
|
|
39
|
+
* @param {NodeJS.ProcessEnv} [args.env]
|
|
40
|
+
* @returns {Promise<import('./manifest-writer.js').PreflightServiceRuling>}
|
|
41
|
+
*/
|
|
42
|
+
async function walkOneServiceCandidate({ candidate, prompt, write, projectRoot, env }) {
|
|
43
|
+
write(`\nService candidate: ${candidate.displayName}`);
|
|
44
|
+
write(`Suggested id: ${candidate.id} (category ${candidate.category})`);
|
|
45
|
+
const refsSummary = candidate.sourceRefs.slice(0, 5)
|
|
46
|
+
.map((r) => ` - ${r.docId}${r.anchor === '/' ? '' : r.anchor} : "${r.phrase}"`)
|
|
47
|
+
.join('\n');
|
|
48
|
+
if (refsSummary.length > 0) write(`Where the scanner spotted it:\n${refsSummary}`);
|
|
49
|
+
if (candidate.sourceRefs.length > 5) {
|
|
50
|
+
write(` (+${candidate.sourceRefs.length - 5} more source refs)`);
|
|
51
|
+
}
|
|
52
|
+
write('\nHow will tests verify this integration?');
|
|
53
|
+
for (const c of ATTESTATION_CHOICES) write(` ${c.key}) ${c.label}`);
|
|
54
|
+
|
|
55
|
+
let mode;
|
|
56
|
+
while (mode === undefined) {
|
|
57
|
+
const ans = (await prompt('Select 1-5: ')).trim();
|
|
58
|
+
if (MODE_BY_KEY.has(ans)) mode = MODE_BY_KEY.get(ans);
|
|
59
|
+
else write('Not one of the five options; try again.');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const sourceRefs = Array.from(new Set(candidate.sourceRefs.map((r) => `${r.docId}${r.anchor === '/' ? '' : r.anchor}`)));
|
|
63
|
+
|
|
64
|
+
/** @type {import('./manifest-writer.js').PreflightServiceRuling} */
|
|
65
|
+
const ruling = {
|
|
66
|
+
id: candidate.id,
|
|
67
|
+
displayName: candidate.displayName,
|
|
68
|
+
sourceRefs,
|
|
69
|
+
attestationMode: mode,
|
|
70
|
+
credentialSupplied: false,
|
|
71
|
+
sandboxProvisioned: false,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (mode === 'live' || mode === 'sandboxed') {
|
|
75
|
+
const envVarName = (await prompt('Which env var carries the credential (name only): ')).trim();
|
|
76
|
+
if (envVarName.length > 0) {
|
|
77
|
+
// recordPreflightSecret reads the shell env by NAME only; the
|
|
78
|
+
// side-file records presence booleans, never a value.
|
|
79
|
+
const secret = await recordPreflightSecret({ projectRoot, serviceId: candidate.id, envVarName, env });
|
|
80
|
+
ruling.credentialSupplied = secret.envVarPresentAtSessionTime;
|
|
81
|
+
if (mode === 'sandboxed') ruling.sandboxProvisioned = true;
|
|
82
|
+
if (!secret.envVarPresentAtSessionTime) {
|
|
83
|
+
write(`Note: ${envVarName} is not set in this shell; the side-file records that so the harness knows to prompt at run time.`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} else if (mode === 'declaredMockOnly' || mode === 'notShipped') {
|
|
87
|
+
const reason = (await prompt('One-line reason (recorded on the chain): ')).trim();
|
|
88
|
+
if (reason.length > 0) ruling.operatorReason = reason;
|
|
89
|
+
} else if (mode === 'mocked') {
|
|
90
|
+
const reason = (await prompt('One-line reason a mock is acceptable here (recorded on the chain): ')).trim();
|
|
91
|
+
if (reason.length > 0) ruling.operatorReason = reason;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return ruling;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {object} args
|
|
99
|
+
* @param {import('./design-shapes.js').DesignShapeQuestion} args.question
|
|
100
|
+
* @param {string} args.reqId
|
|
101
|
+
* @param {PromptFn} args.prompt
|
|
102
|
+
* @param {WriteFn} args.write
|
|
103
|
+
* @returns {Promise<import('./manifest-writer.js').PreflightDesignShapeAnswer|null>}
|
|
104
|
+
*/
|
|
105
|
+
async function walkOneDesignShapeQuestion({ question, reqId, prompt, write }) {
|
|
106
|
+
write(`\nDesign-shape question against ${reqId}: ${question.prompt}`);
|
|
107
|
+
for (let i = 0; i < question.choices.length; i += 1) {
|
|
108
|
+
const c = question.choices[i];
|
|
109
|
+
write(` ${i + 1}) ${c.display}${c.triggersOptOut ? ' (records an opt-out on the baseline ledger)' : ''}`);
|
|
110
|
+
}
|
|
111
|
+
let choice;
|
|
112
|
+
while (choice === undefined) {
|
|
113
|
+
const ans = (await prompt(`Select 1-${question.choices.length}: `)).trim();
|
|
114
|
+
const idx = Number.parseInt(ans, 10);
|
|
115
|
+
if (Number.isFinite(idx) && idx >= 1 && idx <= question.choices.length) {
|
|
116
|
+
choice = question.choices[idx - 1];
|
|
117
|
+
} else {
|
|
118
|
+
write('Not one of the listed options; try again.');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** @type {import('./manifest-writer.js').PreflightDesignShapeAnswer} */
|
|
122
|
+
const answer = { questionId: question.id, reqId, answer: choice.value };
|
|
123
|
+
if (choice.triggersOptOut) {
|
|
124
|
+
let reason;
|
|
125
|
+
while (reason === undefined) {
|
|
126
|
+
const r = (await prompt(`Reason (at least ${question.reasonMinLength} chars, recorded on the ledger): `)).trim();
|
|
127
|
+
if (r.length >= question.reasonMinLength) reason = r;
|
|
128
|
+
else write(`That reason is ${r.length} chars; the ledger floor is ${question.reasonMinLength}.`);
|
|
129
|
+
}
|
|
130
|
+
answer.reason = reason;
|
|
131
|
+
}
|
|
132
|
+
return answer;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Run the interactive session. Returns the composed service rulings and
|
|
137
|
+
* design-shape answers ready for `composePreflightRecord`.
|
|
138
|
+
*
|
|
139
|
+
* @param {object} args
|
|
140
|
+
* @param {string} args.projectRoot
|
|
141
|
+
* @param {import('./scanner.js').ScannerResult} args.scan
|
|
142
|
+
* @param {object[]} args.reqsForDesignShapes
|
|
143
|
+
* @param {import('./design-shapes.js').DesignShapeQuestion[]} [args.catalogue]
|
|
144
|
+
* @param {PromptFn} args.prompt
|
|
145
|
+
* @param {WriteFn} args.write
|
|
146
|
+
* @param {NodeJS.ProcessEnv} [args.env]
|
|
147
|
+
* @returns {Promise<{ services: import('./manifest-writer.js').PreflightServiceRuling[], designShapeAnswers: import('./manifest-writer.js').PreflightDesignShapeAnswer[] }>}
|
|
148
|
+
*/
|
|
149
|
+
export async function runInteractiveSession({
|
|
150
|
+
projectRoot, scan, reqsForDesignShapes, catalogue, prompt, write, env,
|
|
151
|
+
}) {
|
|
152
|
+
const services = [];
|
|
153
|
+
for (const candidate of scan.candidates) {
|
|
154
|
+
const ruling = await walkOneServiceCandidate({ candidate, prompt, write, projectRoot, env });
|
|
155
|
+
services.push(ruling);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Operator additions (candidates the scanner missed). Prompted after
|
|
159
|
+
// the scanner walk so the operator has seen the whole seed set first.
|
|
160
|
+
while (true) {
|
|
161
|
+
const wantMore = (await prompt('Add a service the scanner missed? (y/N): ')).trim().toLowerCase();
|
|
162
|
+
if (wantMore !== 'y' && wantMore !== 'yes') break;
|
|
163
|
+
const id = (await prompt('Service id (camelCase): ')).trim();
|
|
164
|
+
if (id.length === 0) continue;
|
|
165
|
+
const displayName = (await prompt('Display name: ')).trim() || id;
|
|
166
|
+
const synthetic = {
|
|
167
|
+
id, displayName, category: 'operatorAdded', sourceRefs: [],
|
|
168
|
+
};
|
|
169
|
+
const ruling = await walkOneServiceCandidate({ candidate: synthetic, prompt, write, projectRoot, env });
|
|
170
|
+
services.push(ruling);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const designShapeAnswers = [];
|
|
174
|
+
const { selectApplicableQuestions } = await import('./design-shapes.js');
|
|
175
|
+
const applicable = selectApplicableQuestions(reqsForDesignShapes, catalogue);
|
|
176
|
+
for (const { question, reqId } of applicable) {
|
|
177
|
+
const answer = await walkOneDesignShapeQuestion({ question, reqId, prompt, write });
|
|
178
|
+
if (answer) {
|
|
179
|
+
const err = validateDesignShapeAnswer({
|
|
180
|
+
questionId: answer.questionId, answer: answer.answer, reason: answer.reason,
|
|
181
|
+
});
|
|
182
|
+
if (err) throw new Error(err);
|
|
183
|
+
designShapeAnswers.push(answer);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
write('\nSession summary:');
|
|
188
|
+
for (const s of services) {
|
|
189
|
+
write(` ${s.id} (${s.displayName}) -> ${s.attestationMode}${s.credentialSupplied ? ' [cred present]' : ''}`);
|
|
190
|
+
}
|
|
191
|
+
for (const a of designShapeAnswers) {
|
|
192
|
+
write(` ${a.questionId} on ${a.reqId} -> ${a.answer}`);
|
|
193
|
+
}
|
|
194
|
+
const confirm = (await prompt('\nConfirm this pre-flight record and commit? (yes/no): ')).trim().toLowerCase();
|
|
195
|
+
if (confirm !== 'y' && confirm !== 'yes') {
|
|
196
|
+
throw new Error('preflight: session cancelled by operator');
|
|
197
|
+
}
|
|
198
|
+
return { services, designShapeAnswers };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Non-interactive path: read a pre-filled input file and validate its
|
|
203
|
+
* shape enough that a downstream manifest write will validate against
|
|
204
|
+
* the schema.
|
|
205
|
+
*
|
|
206
|
+
* @param {object} input
|
|
207
|
+
* @returns {{ services: import('./manifest-writer.js').PreflightServiceRuling[], designShapeAnswers: import('./manifest-writer.js').PreflightDesignShapeAnswer[] }}
|
|
208
|
+
*/
|
|
209
|
+
export function normaliseNonInteractiveInput(input) {
|
|
210
|
+
if (!input || typeof input !== 'object') {
|
|
211
|
+
throw new Error('preflight: --input file did not parse to an object');
|
|
212
|
+
}
|
|
213
|
+
const rawServices = Array.isArray(input.services) ? input.services : [];
|
|
214
|
+
const services = rawServices.map((s, idx) => {
|
|
215
|
+
if (!s || typeof s !== 'object') throw new Error(`preflight: services[${idx}] must be an object`);
|
|
216
|
+
if (typeof s.id !== 'string' || s.id.length === 0) throw new Error(`preflight: services[${idx}].id required`);
|
|
217
|
+
if (typeof s.attestationMode !== 'string') throw new Error(`preflight: services[${idx}].attestationMode required`);
|
|
218
|
+
return {
|
|
219
|
+
id: s.id,
|
|
220
|
+
displayName: typeof s.displayName === 'string' ? s.displayName : s.id,
|
|
221
|
+
sourceRefs: Array.isArray(s.sourceRefs) ? s.sourceRefs.map(String) : [],
|
|
222
|
+
attestationMode: s.attestationMode,
|
|
223
|
+
credentialSupplied: Boolean(s.credentialSupplied),
|
|
224
|
+
sandboxProvisioned: Boolean(s.sandboxProvisioned),
|
|
225
|
+
operatorReason: typeof s.operatorReason === 'string' ? s.operatorReason : undefined,
|
|
226
|
+
affectedFbsIds: Array.isArray(s.affectedFbsIds) ? s.affectedFbsIds.map(String) : undefined,
|
|
227
|
+
};
|
|
228
|
+
});
|
|
229
|
+
const rawAnswers = Array.isArray(input.designShapeAnswers) ? input.designShapeAnswers : [];
|
|
230
|
+
const designShapeAnswers = rawAnswers.map((a, idx) => {
|
|
231
|
+
if (!a || typeof a !== 'object') throw new Error(`preflight: designShapeAnswers[${idx}] must be an object`);
|
|
232
|
+
if (typeof a.questionId !== 'string') throw new Error(`preflight: designShapeAnswers[${idx}].questionId required`);
|
|
233
|
+
if (typeof a.answer !== 'string') throw new Error(`preflight: designShapeAnswers[${idx}].answer required`);
|
|
234
|
+
const err = validateDesignShapeAnswer({
|
|
235
|
+
questionId: a.questionId, answer: a.answer, reason: a.reason,
|
|
236
|
+
});
|
|
237
|
+
if (err) throw new Error(`preflight: designShapeAnswers[${idx}]: ${err}`);
|
|
238
|
+
return {
|
|
239
|
+
questionId: a.questionId,
|
|
240
|
+
reqId: typeof a.reqId === 'string' ? a.reqId : undefined,
|
|
241
|
+
answer: a.answer,
|
|
242
|
+
reason: typeof a.reason === 'string' ? a.reason : undefined,
|
|
243
|
+
};
|
|
244
|
+
});
|
|
245
|
+
return { services, designShapeAnswers };
|
|
246
|
+
}
|