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,285 @@
|
|
|
1
|
+
// Build queue semantics (Phase 6 §D2). Pure projection of the FBS
|
|
2
|
+
// documents into a deterministic queue: per-item derived state
|
|
3
|
+
// (actionable / blocked / inProgress / complete / verified), cycle
|
|
4
|
+
// detection over the dependency graph, aggregate totals and next-item
|
|
5
|
+
// selection. No I/O, no wall clock, no mutation of the tree.
|
|
6
|
+
//
|
|
7
|
+
// An FBS item is ACTIONABLE when its executionStatus is `notStarted`
|
|
8
|
+
// AND every id in dependsOnFbsIds resolves to an FBS whose status is
|
|
9
|
+
// `complete` or `verified`. `notStarted` with an unsatisfied dependency
|
|
10
|
+
// is BLOCKED. `inProgress` is neither: it is in flight and selection
|
|
11
|
+
// skips it (the envelope surfaces it so a harness can notice an
|
|
12
|
+
// abandoned claim). Dependency cycles make every member permanently
|
|
13
|
+
// blocked; the queue labels them `blocked (cycle)` rather than crashing
|
|
14
|
+
// (`rcf validate` remains the integrity surface).
|
|
15
|
+
//
|
|
16
|
+
// Each item also carries a TIER: the length of its longest dependency
|
|
17
|
+
// chain (tier 0 = no dependencies). Items sharing a tier have no
|
|
18
|
+
// dependency path between them, so a tier is a parallel-safe group -
|
|
19
|
+
// its members can be built concurrently (AC-502-2). This is a port of
|
|
20
|
+
// the full RCF platform's `computeTiers` (rcf-common build-graph),
|
|
21
|
+
// adapted to the queue's data model and cycle posture: a cycle member,
|
|
22
|
+
// or any item whose dependency chain runs into a cycle, has no defined
|
|
23
|
+
// chain length, so its tier is null and it joins no group (it is
|
|
24
|
+
// already reported `blocked (cycle)` / blocked above).
|
|
25
|
+
|
|
26
|
+
/** Lifecycle order (common.schema.json executionStatus enum). */
|
|
27
|
+
export const LIFECYCLE = ['notStarted', 'inProgress', 'complete', 'verified'];
|
|
28
|
+
|
|
29
|
+
const SATISFIED = new Set(['complete', 'verified']);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {object} QueueItem
|
|
33
|
+
* @property {string} fbsId
|
|
34
|
+
* @property {number} buildOrder
|
|
35
|
+
* @property {string} title
|
|
36
|
+
* @property {string} executionStatus
|
|
37
|
+
* @property {string[]} dependsOnFbsIds
|
|
38
|
+
* @property {number|null} tier - parallel-safe group (longest dependency
|
|
39
|
+
* chain length; null when the chain runs into a cycle)
|
|
40
|
+
* @property {('actionable'|'blocked'|'inProgress'|'complete'|'verified')} state
|
|
41
|
+
* @property {string[]} blockedBy - dependency ids not yet complete/verified
|
|
42
|
+
* @property {boolean} [cycle] - present (true) on blocked cycle members
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @typedef {object} TierGroup
|
|
47
|
+
* @property {number} tier - 0-based tier number (0 = no dependencies)
|
|
48
|
+
* @property {string[]} fbsIds - members, in queue order; no dependency
|
|
49
|
+
* path exists between any two of them, so they are parallel-safe
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @typedef {object} QueueResult
|
|
54
|
+
* @property {{bsId: string, title: string, generationStrategy: string}|null} bs
|
|
55
|
+
* @property {object} totals
|
|
56
|
+
* @property {string|null} nextActionable
|
|
57
|
+
* @property {TierGroup[]} tiers - parallel-safe groups, tier ascending
|
|
58
|
+
* @property {QueueItem[]} items
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Sort comparator: buildOrder ascending, fbsId lexicographic tie-break
|
|
63
|
+
* (§D2 - the schema does not enforce buildOrder uniqueness, so the
|
|
64
|
+
* tie-break keeps the total order deterministic).
|
|
65
|
+
*
|
|
66
|
+
* @param {{buildOrder?: number, fbsId?: string}} a
|
|
67
|
+
* @param {{buildOrder?: number, fbsId?: string}} b
|
|
68
|
+
* @returns {number}
|
|
69
|
+
*/
|
|
70
|
+
export function byBuildOrder(a, b) {
|
|
71
|
+
const orderA = a.buildOrder ?? 0;
|
|
72
|
+
const orderB = b.buildOrder ?? 0;
|
|
73
|
+
if (orderA !== orderB) return orderA - orderB;
|
|
74
|
+
return (a.fbsId ?? '').localeCompare(b.fbsId ?? '');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Find every FBS id that sits on a dependency cycle. Standard
|
|
79
|
+
* iterative DFS with a colour map over the dependsOnFbsIds edges;
|
|
80
|
+
* when a back edge closes a loop, every node on the current stack
|
|
81
|
+
* segment from the loop entry is a cycle member.
|
|
82
|
+
*
|
|
83
|
+
* @param {Map<string, object>} fbsById
|
|
84
|
+
* @returns {Set<string>}
|
|
85
|
+
*/
|
|
86
|
+
function findCycleMembers(fbsById) {
|
|
87
|
+
const members = new Set();
|
|
88
|
+
const state = new Map(); // id -> 'visiting' | 'done'
|
|
89
|
+
const stack = [];
|
|
90
|
+
|
|
91
|
+
const visit = (startId) => {
|
|
92
|
+
// Iterative DFS frame stack: [id, depIndex].
|
|
93
|
+
const frames = [[startId, 0]];
|
|
94
|
+
while (frames.length > 0) {
|
|
95
|
+
const frame = frames[frames.length - 1];
|
|
96
|
+
const [id] = frame;
|
|
97
|
+
if (frame[1] === 0) {
|
|
98
|
+
state.set(id, 'visiting');
|
|
99
|
+
stack.push(id);
|
|
100
|
+
}
|
|
101
|
+
const deps = fbsById.get(id)?.dependsOnFbsIds ?? [];
|
|
102
|
+
if (frame[1] >= deps.length) {
|
|
103
|
+
state.set(id, 'done');
|
|
104
|
+
stack.pop();
|
|
105
|
+
frames.pop();
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const depId = deps[frame[1]];
|
|
109
|
+
frame[1] += 1;
|
|
110
|
+
if (!fbsById.has(depId)) continue;
|
|
111
|
+
const depState = state.get(depId);
|
|
112
|
+
if (depState === 'visiting') {
|
|
113
|
+
// Back edge: every id on the stack from depId onward is a member.
|
|
114
|
+
const from = stack.indexOf(depId);
|
|
115
|
+
for (let i = from; i < stack.length; i += 1) members.add(stack[i]);
|
|
116
|
+
} else if (depState === undefined) {
|
|
117
|
+
frames.push([depId, 0]);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
for (const id of fbsById.keys()) {
|
|
123
|
+
if (!state.has(id)) visit(id);
|
|
124
|
+
}
|
|
125
|
+
return members;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Compute the tier of every FBS id: the length of its longest
|
|
130
|
+
* dependency chain (tier 0 = no dependencies). Port of the platform's
|
|
131
|
+
* `computeTiers` memoised longest-chain DFS, iterative in the style of
|
|
132
|
+
* findCycleMembers. Cycle handling reuses the members findCycleMembers
|
|
133
|
+
* already detected rather than re-implementing detection: a member, or
|
|
134
|
+
* any item whose chain reaches one, gets tier null (no defined chain
|
|
135
|
+
* length) - the walk short-circuits there, so a cyclic graph can never
|
|
136
|
+
* loop it. A dependency id that resolves to no FBS contributes chain
|
|
137
|
+
* length 0 (same as the platform port; `rcf validate` is the
|
|
138
|
+
* broken-reference surface).
|
|
139
|
+
*
|
|
140
|
+
* @param {Map<string, object>} fbsById
|
|
141
|
+
* @param {Set<string>} cycleMembers
|
|
142
|
+
* @returns {Map<string, number|null>}
|
|
143
|
+
*/
|
|
144
|
+
function computeTiers(fbsById, cycleMembers) {
|
|
145
|
+
const tiers = new Map(); // id -> number | null
|
|
146
|
+
|
|
147
|
+
const compute = (startId) => {
|
|
148
|
+
// Iterative DFS frame stack: [id, depIndex, maxDepTier]; maxDepTier
|
|
149
|
+
// null = poisoned by a cycle somewhere beneath this item.
|
|
150
|
+
const frames = [[startId, 0, -1]];
|
|
151
|
+
while (frames.length > 0) {
|
|
152
|
+
const frame = frames[frames.length - 1];
|
|
153
|
+
const [id] = frame;
|
|
154
|
+
if (cycleMembers.has(id)) {
|
|
155
|
+
tiers.set(id, null);
|
|
156
|
+
frames.pop();
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const deps = fbsById.get(id)?.dependsOnFbsIds ?? [];
|
|
160
|
+
if (frame[1] >= deps.length) {
|
|
161
|
+
tiers.set(id, frame[2] === null ? null : frame[2] + 1);
|
|
162
|
+
frames.pop();
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const depId = deps[frame[1]];
|
|
166
|
+
frame[1] += 1;
|
|
167
|
+
if (frame[2] === null) continue; // already poisoned; drain remaining deps
|
|
168
|
+
const depTier = fbsById.has(depId) ? tiers.get(depId) : 0;
|
|
169
|
+
if (depTier === undefined) {
|
|
170
|
+
// Unresolved dependency: compute it first, then revisit this edge.
|
|
171
|
+
frame[1] -= 1;
|
|
172
|
+
frames.push([depId, 0, -1]);
|
|
173
|
+
} else if (depTier === null) {
|
|
174
|
+
frame[2] = null;
|
|
175
|
+
} else if (depTier > frame[2]) {
|
|
176
|
+
frame[2] = depTier;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
for (const id of fbsById.keys()) {
|
|
182
|
+
if (!tiers.has(id)) compute(id);
|
|
183
|
+
}
|
|
184
|
+
return tiers;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Group tiered items into parallel-safe TierGroups, tier ascending,
|
|
189
|
+
* members in queue order. Null-tier (cycle-tainted) items join no
|
|
190
|
+
* group.
|
|
191
|
+
*
|
|
192
|
+
* @param {QueueItem[]} items - already in queue order
|
|
193
|
+
* @returns {TierGroup[]}
|
|
194
|
+
*/
|
|
195
|
+
function groupTiers(items) {
|
|
196
|
+
const byTier = new Map(); // tier -> fbsIds
|
|
197
|
+
for (const item of items) {
|
|
198
|
+
if (item.tier === null) continue;
|
|
199
|
+
if (!byTier.has(item.tier)) byTier.set(item.tier, []);
|
|
200
|
+
byTier.get(item.tier).push(item.fbsId);
|
|
201
|
+
}
|
|
202
|
+
return [...byTier.entries()]
|
|
203
|
+
.sort((a, b) => a[0] - b[0])
|
|
204
|
+
.map(([tier, fbsIds]) => ({ tier, fbsIds }));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Compute the queue over a walked tree. Deterministic: same tree,
|
|
209
|
+
* same result, always.
|
|
210
|
+
*
|
|
211
|
+
* @param {import('#core/store/walker.js').TreeModel} tree
|
|
212
|
+
* @returns {QueueResult}
|
|
213
|
+
*/
|
|
214
|
+
export function computeQueue(tree) {
|
|
215
|
+
const fbsItems = [...(tree.fbsItems ?? [])].sort(byBuildOrder);
|
|
216
|
+
const fbsById = new Map(fbsItems.map((f) => [f.fbsId, f]));
|
|
217
|
+
const cycleMembers = findCycleMembers(fbsById);
|
|
218
|
+
const tierById = computeTiers(fbsById, cycleMembers);
|
|
219
|
+
|
|
220
|
+
const items = fbsItems.map((fbs) => {
|
|
221
|
+
const deps = fbs.dependsOnFbsIds ?? [];
|
|
222
|
+
const blockedBy = deps
|
|
223
|
+
.filter((depId) => !SATISFIED.has(fbsById.get(depId)?.executionStatus))
|
|
224
|
+
.map((depId) => fbsById.get(depId) ?? { fbsId: depId, buildOrder: 0 })
|
|
225
|
+
.sort(byBuildOrder)
|
|
226
|
+
.map((dep) => dep.fbsId);
|
|
227
|
+
let state;
|
|
228
|
+
if (fbs.executionStatus === 'notStarted') {
|
|
229
|
+
state = blockedBy.length === 0 ? 'actionable' : 'blocked';
|
|
230
|
+
} else {
|
|
231
|
+
state = fbs.executionStatus;
|
|
232
|
+
}
|
|
233
|
+
const item = {
|
|
234
|
+
fbsId: fbs.fbsId,
|
|
235
|
+
buildOrder: fbs.buildOrder,
|
|
236
|
+
tier: tierById.get(fbs.fbsId),
|
|
237
|
+
title: fbs.title,
|
|
238
|
+
executionStatus: fbs.executionStatus,
|
|
239
|
+
dependsOnFbsIds: deps,
|
|
240
|
+
state,
|
|
241
|
+
blockedBy,
|
|
242
|
+
};
|
|
243
|
+
if (state === 'blocked' && cycleMembers.has(fbs.fbsId)) item.cycle = true;
|
|
244
|
+
return item;
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const totals = {
|
|
248
|
+
items: items.length,
|
|
249
|
+
notStarted: items.filter((i) => i.executionStatus === 'notStarted').length,
|
|
250
|
+
inProgress: items.filter((i) => i.executionStatus === 'inProgress').length,
|
|
251
|
+
complete: items.filter((i) => i.executionStatus === 'complete').length,
|
|
252
|
+
verified: items.filter((i) => i.executionStatus === 'verified').length,
|
|
253
|
+
actionable: items.filter((i) => i.state === 'actionable').length,
|
|
254
|
+
blocked: items.filter((i) => i.state === 'blocked').length,
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const next = selectNextItem(items);
|
|
258
|
+
const bs = tree.bs
|
|
259
|
+
? { bsId: tree.bs.bsId, title: tree.bs.title, generationStrategy: tree.bs.generationStrategy }
|
|
260
|
+
: null;
|
|
261
|
+
|
|
262
|
+
return { bs, totals, nextActionable: next ? next.fbsId : null, tiers: groupTiers(items), items };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Select the next actionable item from a computed queue: lowest
|
|
267
|
+
* buildOrder (fbsId tie-break) among actionable items. Items are
|
|
268
|
+
* already in total order, so the first actionable wins.
|
|
269
|
+
*
|
|
270
|
+
* @param {QueueItem[]} items
|
|
271
|
+
* @returns {QueueItem|null}
|
|
272
|
+
*/
|
|
273
|
+
function selectNextItem(items) {
|
|
274
|
+
return items.find((i) => i.state === 'actionable') ?? null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Select the next actionable FBS item from a QueueResult.
|
|
279
|
+
*
|
|
280
|
+
* @param {QueueResult} queue
|
|
281
|
+
* @returns {QueueItem|null}
|
|
282
|
+
*/
|
|
283
|
+
export function selectNext(queue) {
|
|
284
|
+
return selectNextItem(queue.items);
|
|
285
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// `rcf browser-verify <fbs-id>` handler
|
|
2
|
+
// (ui-design-gate-0.7.0-spec §8, §9).
|
|
3
|
+
//
|
|
4
|
+
// Two modes:
|
|
5
|
+
// --mode operatorSession record an operator ack (no captures)
|
|
6
|
+
// --mode agentScreenshotCritique run the injected browser driver,
|
|
7
|
+
// capture routes x themes, run
|
|
8
|
+
// invariants + auth smoke, write the
|
|
9
|
+
// browserVerification record.
|
|
10
|
+
//
|
|
11
|
+
// The agent driver is injectable via `deps.browserDriver`; the default
|
|
12
|
+
// stub emits zero captures and the record surfaces a warn on the
|
|
13
|
+
// `agentDriverWired` synthetic invariant, so the CLI is testable
|
|
14
|
+
// end-to-end without a live Playwright.
|
|
15
|
+
//
|
|
16
|
+
// --ack marks operatorAckAt on the latest record for the FBS (used to
|
|
17
|
+
// clear a warn verdict per §8.5).
|
|
18
|
+
|
|
19
|
+
import { parseArgs } from 'node:util';
|
|
20
|
+
import process from 'node:process';
|
|
21
|
+
|
|
22
|
+
import { walkTree } from '#core/store';
|
|
23
|
+
import { writeUnexpectedFailure, rcfError } from '#core/errors';
|
|
24
|
+
|
|
25
|
+
import { findProjectRoot } from '../view/index.js';
|
|
26
|
+
import {
|
|
27
|
+
composeOperatorSessionRecord,
|
|
28
|
+
runAgentScreenshotCritique,
|
|
29
|
+
stubBrowserDriver,
|
|
30
|
+
writeBrowserVerificationAck,
|
|
31
|
+
writeBrowserVerificationRecord,
|
|
32
|
+
} from '../browser-verify/index.js';
|
|
33
|
+
|
|
34
|
+
const OPTION_SPEC = {
|
|
35
|
+
mode: { type: 'string' },
|
|
36
|
+
url: { type: 'string' },
|
|
37
|
+
profile: { type: 'string' },
|
|
38
|
+
'dry-run': { type: 'boolean' },
|
|
39
|
+
json: { type: 'boolean' },
|
|
40
|
+
quiet: { type: 'boolean' },
|
|
41
|
+
ack: { type: 'boolean' },
|
|
42
|
+
notes: { type: 'string' },
|
|
43
|
+
help: { type: 'boolean' },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const HELP = `Usage: rcf browser-verify <fbs-id> [options]
|
|
47
|
+
|
|
48
|
+
Stage 5 browser-verification gate for a UI-bearing FBS. Writes a
|
|
49
|
+
browserVerification record on the manifest; the finalise gate reads it.
|
|
50
|
+
|
|
51
|
+
Options:
|
|
52
|
+
--mode <mode> operatorSession | agentScreenshotCritique
|
|
53
|
+
(default: agentScreenshotCritique)
|
|
54
|
+
--url <url> Runtime URL to drive against (default: taken
|
|
55
|
+
from --profile, else http://127.0.0.1:3000).
|
|
56
|
+
--profile <profile> deployed | ci | local-dev (default: local-dev)
|
|
57
|
+
--notes "..." Agent-critique rubric findings to record on
|
|
58
|
+
the browserVerification.notes field.
|
|
59
|
+
--dry-run Print the composed record; do not write.
|
|
60
|
+
--json Emit the composed record as JSON to stdout.
|
|
61
|
+
--quiet Suppress non-error confirmations.
|
|
62
|
+
--ack Mark operatorAckAt on the latest record for
|
|
63
|
+
this FBS (used to clear a warn verdict).
|
|
64
|
+
--help Print this help.
|
|
65
|
+
|
|
66
|
+
Exit codes:
|
|
67
|
+
0 verdict pass
|
|
68
|
+
1 IO / unexpected runtime failure
|
|
69
|
+
2 usage error
|
|
70
|
+
3 schema or tree validation failure
|
|
71
|
+
4 verdict warn or block (Stage 5 refused; --ack clears a warn)
|
|
72
|
+
`;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {string[]} argv
|
|
76
|
+
* @param {object} [deps]
|
|
77
|
+
* @returns {Promise<number>}
|
|
78
|
+
*/
|
|
79
|
+
export async function main(argv, deps = {}) {
|
|
80
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
81
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
82
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
83
|
+
|
|
84
|
+
let parsed;
|
|
85
|
+
try {
|
|
86
|
+
parsed = parseArgs({ args: argv, options: OPTION_SPEC, allowPositionals: true, strict: true });
|
|
87
|
+
} catch (err) {
|
|
88
|
+
stderr.write(`[error] usage ${err.message}\n`);
|
|
89
|
+
stderr.write(HELP);
|
|
90
|
+
return 2;
|
|
91
|
+
}
|
|
92
|
+
const flags = parsed.values;
|
|
93
|
+
if (flags.help) { stdout.write(HELP); return 0; }
|
|
94
|
+
if (parsed.positionals.length !== 1) {
|
|
95
|
+
stderr.write('[error] usage browser-verify: expected exactly one <fbs-id> positional\n');
|
|
96
|
+
return 2;
|
|
97
|
+
}
|
|
98
|
+
const fbsId = parsed.positionals[0];
|
|
99
|
+
if (!/^FBS-\d{3,}$/.test(fbsId)) {
|
|
100
|
+
stderr.write(`[error] usage browser-verify: expected an FBS id like FBS-011, got '${fbsId}'\n`);
|
|
101
|
+
return 2;
|
|
102
|
+
}
|
|
103
|
+
const mode = flags.mode ?? 'agentScreenshotCritique';
|
|
104
|
+
if (!['operatorSession', 'agentScreenshotCritique'].includes(mode)) {
|
|
105
|
+
stderr.write(`[error] usage browser-verify: unknown --mode '${mode}' (expected operatorSession | agentScreenshotCritique)\n`);
|
|
106
|
+
return 2;
|
|
107
|
+
}
|
|
108
|
+
const profile = flags.profile ?? 'local-dev';
|
|
109
|
+
if (!['deployed', 'ci', 'local-dev'].includes(profile)) {
|
|
110
|
+
stderr.write(`[error] usage browser-verify: unknown --profile '${profile}' (expected deployed | ci | local-dev)\n`);
|
|
111
|
+
return 2;
|
|
112
|
+
}
|
|
113
|
+
const runtimeUrl = flags.url ?? defaultUrlForProfile(profile);
|
|
114
|
+
|
|
115
|
+
const projectRoot = await findProjectRoot(cwd);
|
|
116
|
+
if (!projectRoot) {
|
|
117
|
+
stderr.write('[error] usage no project root found (no rcf/manifest.json in this directory or any ancestor). Run `rcf init` first.\n');
|
|
118
|
+
return 2;
|
|
119
|
+
}
|
|
120
|
+
const walkResult = await walkTree({ projectRoot });
|
|
121
|
+
if (walkResult.errors.length > 0) {
|
|
122
|
+
stderr.write(`[warn] tree has ${walkResult.errors.length} pre-existing issue(s); proceeding\n`);
|
|
123
|
+
}
|
|
124
|
+
const { tree } = walkResult;
|
|
125
|
+
const fbs = tree.byId.get(fbsId);
|
|
126
|
+
if (!fbs || tree.kindById.get(fbsId) !== 'fbs') {
|
|
127
|
+
stderr.write(`[error] usage browser-verify: ${fbsId} not found or not an FBS\n`);
|
|
128
|
+
return 2;
|
|
129
|
+
}
|
|
130
|
+
if (fbs.uiBearing !== true) {
|
|
131
|
+
stderr.write(`[warn] browser-verify: ${fbsId} is not uiBearing; the browser gate is a no-op. Set uiBearing=true if this FBS renders UI.\n`);
|
|
132
|
+
}
|
|
133
|
+
const now = deps.now ? new Date(deps.now()) : new Date();
|
|
134
|
+
|
|
135
|
+
if (flags.ack) {
|
|
136
|
+
const result = await writeBrowserVerificationAck({
|
|
137
|
+
projectRoot, tree, fbsId, operatorAckAt: true, now,
|
|
138
|
+
});
|
|
139
|
+
if (result && 'kind' in result && 'message' in result) {
|
|
140
|
+
if (result.kind === 'ioFailure') { writeUnexpectedFailure(result, stderr); return 1; }
|
|
141
|
+
stderr.write(`[error] ${result.kind} ${result.message}\n`);
|
|
142
|
+
if (result.kind === 'usage') return 2;
|
|
143
|
+
if (result.kind === 'validation') return 3;
|
|
144
|
+
return 1;
|
|
145
|
+
}
|
|
146
|
+
if (!flags.quiet) stdout.write(`browser-verify --ack: recorded operatorAckAt on latest record for ${fbsId}.\n`);
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let record;
|
|
151
|
+
try {
|
|
152
|
+
if (mode === 'operatorSession') {
|
|
153
|
+
record = composeOperatorSessionRecord({
|
|
154
|
+
tree, fbs, runtimeUrl, runtimeProfile: profile,
|
|
155
|
+
declaredRoutes: [],
|
|
156
|
+
invariantTicks: [{ invariant: 'operatorAcknowledged', verdict: 'pass', severity: 'block' }],
|
|
157
|
+
now,
|
|
158
|
+
});
|
|
159
|
+
} else {
|
|
160
|
+
const browserDriver = deps.browserDriver ?? stubBrowserDriver;
|
|
161
|
+
const fetchFn = deps.fetch ?? (typeof fetch === 'function' ? fetch : null);
|
|
162
|
+
if (!fetchFn) {
|
|
163
|
+
stderr.write('[error] usage browser-verify: no fetch available in the runtime; supply deps.fetch\n');
|
|
164
|
+
return 2;
|
|
165
|
+
}
|
|
166
|
+
record = await runAgentScreenshotCritique({
|
|
167
|
+
tree, fbs, runtimeUrl, runtimeProfile: profile,
|
|
168
|
+
browserDriver, fetch: fetchFn,
|
|
169
|
+
artefactDir: `.rcf/artefacts/${fbsId.toLowerCase()}`,
|
|
170
|
+
critiqueNotes: flags.notes,
|
|
171
|
+
now,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
} catch (err) {
|
|
175
|
+
writeUnexpectedFailure(
|
|
176
|
+
rcfError({ kind: 'ioFailure', message: `browser-verify: ${err.message}`, stack: err.stack }),
|
|
177
|
+
stderr,
|
|
178
|
+
);
|
|
179
|
+
return 1;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (flags.json) stdout.write(`${JSON.stringify(record, null, 2)}\n`);
|
|
183
|
+
|
|
184
|
+
if (flags['dry-run']) {
|
|
185
|
+
if (!flags.quiet) {
|
|
186
|
+
renderSummary({ stdout, record });
|
|
187
|
+
}
|
|
188
|
+
return record.verdict === 'pass' ? 0 : 4;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const write = await writeBrowserVerificationRecord({ projectRoot, tree, record });
|
|
192
|
+
if (write && 'kind' in write && 'message' in write) {
|
|
193
|
+
if (write.kind === 'ioFailure') { writeUnexpectedFailure(write, stderr); return 1; }
|
|
194
|
+
stderr.write(`[error] ${write.kind} ${write.message}\n`);
|
|
195
|
+
if (write.kind === 'usage') return 2;
|
|
196
|
+
if (write.kind === 'validation') return 3;
|
|
197
|
+
return 1;
|
|
198
|
+
}
|
|
199
|
+
if (!flags.quiet) {
|
|
200
|
+
renderSummary({ stdout, record });
|
|
201
|
+
stdout.write(`browser-verify ${fbsId}: wrote ${record.id} to rcf/manifest.json (verdict=${record.verdict}).\n`);
|
|
202
|
+
}
|
|
203
|
+
return record.verdict === 'pass' ? 0 : 4;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function defaultUrlForProfile(profile) {
|
|
207
|
+
if (profile === 'ci') return 'http://127.0.0.1:3000';
|
|
208
|
+
if (profile === 'deployed') return '';
|
|
209
|
+
return 'http://127.0.0.1:3000';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function renderSummary({ stdout, record }) {
|
|
213
|
+
stdout.write(`\nbrowser-verify ${record.fbsId} (${record.mode}, ${record.runtimeProfile})\n`);
|
|
214
|
+
if (typeof record.notes === 'string' && record.notes.length > 0) {
|
|
215
|
+
stdout.write(' agent critique:\n');
|
|
216
|
+
for (const line of record.notes.split('\n')) stdout.write(` ${line}\n`);
|
|
217
|
+
}
|
|
218
|
+
stdout.write(' invariant checks:\n');
|
|
219
|
+
for (const c of record.invariantChecks) {
|
|
220
|
+
const detail = c.detail ? ` - ${c.detail}` : '';
|
|
221
|
+
stdout.write(` [${c.verdict}] ${c.invariant}${detail}\n`);
|
|
222
|
+
}
|
|
223
|
+
if (Array.isArray(record.authSmokeChecks) && record.authSmokeChecks.length > 0) {
|
|
224
|
+
stdout.write(' auth smoke checks:\n');
|
|
225
|
+
for (const c of record.authSmokeChecks) {
|
|
226
|
+
const detail = c.detail ? ` - ${c.detail}` : '';
|
|
227
|
+
stdout.write(` [${c.verdict}] ${c.check} (status=${c.status ?? 'n/a'})${detail}\n`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
stdout.write(` verdict: ${record.verdict}\n\n`);
|
|
231
|
+
}
|