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,140 @@
|
|
|
1
|
+
// Shared standards ruleset loader (NV-BL-SR-01, NV-BL-SR-02, NV-BL-SR-03).
|
|
2
|
+
//
|
|
3
|
+
// The ruleset is a single machine-readable artefact bundled inside the
|
|
4
|
+
// rcf-lite umbrella package (JSON, camelCase per estate convention). Both
|
|
5
|
+
// build-lite (as a gate) and rcf-define-lite (as an elicitation checklist,
|
|
6
|
+
// from the umbrella release that adds the define payload) consume the
|
|
7
|
+
// identical ruleset from the identical umbrella version.
|
|
8
|
+
//
|
|
9
|
+
// Per NV-BL-SR-02 (ratified 2026-08-11, ruling-sheet items 2 and 6): the
|
|
10
|
+
// ruleset carries no separate semver. Its version IS the rcf-lite umbrella
|
|
11
|
+
// package version. This loader stamps `rulesetVersion` at read time from
|
|
12
|
+
// the umbrella's package.json so a redeploy of the same JSON on a bumped
|
|
13
|
+
// umbrella version reports the new version without a data edit.
|
|
14
|
+
//
|
|
15
|
+
// Ruleset scope covers chain admissibility AND the estate's traceability
|
|
16
|
+
// and query tooling (ratified 2026-08-11, ruling-sheet item 1 addendum).
|
|
17
|
+
// See toolScope on the artefact.
|
|
18
|
+
|
|
19
|
+
import { readFile } from 'node:fs/promises';
|
|
20
|
+
import { dirname, join, resolve } from 'node:path';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
|
|
23
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const rulesetPath = join(here, 'ruleset.json');
|
|
25
|
+
const packageJsonPath = resolve(here, '..', '..', 'package.json');
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {object} RulesetRule
|
|
29
|
+
* @property {string} id
|
|
30
|
+
* @property {string} title
|
|
31
|
+
* @property {boolean} [refuseByDefault]
|
|
32
|
+
* @property {string} [overrideChannel]
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @typedef {object} Ruleset
|
|
37
|
+
* @property {string} id
|
|
38
|
+
* @property {string} rulesetVersion - the rcf-lite umbrella package version at load time.
|
|
39
|
+
* @property {RulesetRule[]} admissibilityRules
|
|
40
|
+
* @property {RulesetRule[]} gateRules
|
|
41
|
+
* @property {object} scopeTagVocabulary
|
|
42
|
+
* @property {Array<{ marker: string, caseInsensitive: boolean }>} sourceCommentMarkers
|
|
43
|
+
* @property {Array<{ id: string, title: string, surface: string }>} tcTemplateFamily
|
|
44
|
+
* @property {Array<{ id: string, title: string, kind: string }>} rulingConsistencyChecks
|
|
45
|
+
* @property {{ chainAdmissibility: boolean, traceabilityAndQueryTools: boolean }} toolScope
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
let cachedRuleset = null;
|
|
49
|
+
let cachedUmbrellaVersion = null;
|
|
50
|
+
|
|
51
|
+
async function readJson(path) {
|
|
52
|
+
const raw = await readFile(path, 'utf8');
|
|
53
|
+
return JSON.parse(raw);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The rcf-lite umbrella package version at load time. Used by
|
|
58
|
+
* `getRuleset()` to stamp `rulesetVersion` per NV-BL-SR-02; also exported
|
|
59
|
+
* so consumers can read the umbrella version without opening
|
|
60
|
+
* `package.json` themselves.
|
|
61
|
+
*
|
|
62
|
+
* @returns {Promise<string>}
|
|
63
|
+
*/
|
|
64
|
+
export async function getUmbrellaVersion() {
|
|
65
|
+
if (cachedUmbrellaVersion) return cachedUmbrellaVersion;
|
|
66
|
+
const pkg = await readJson(packageJsonPath);
|
|
67
|
+
if (typeof pkg?.version !== 'string' || pkg.version.length === 0) {
|
|
68
|
+
throw new Error('rcf-lite umbrella package.json is missing a version string');
|
|
69
|
+
}
|
|
70
|
+
cachedUmbrellaVersion = pkg.version;
|
|
71
|
+
return cachedUmbrellaVersion;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Load the shared standards ruleset artefact and stamp its `rulesetVersion`
|
|
76
|
+
* from the umbrella package.json. The artefact itself carries no version
|
|
77
|
+
* field (NV-BL-SR-02); read-time stamping is the single source of truth.
|
|
78
|
+
*
|
|
79
|
+
* @param {object} [opts]
|
|
80
|
+
* @param {boolean} [opts.fresh] - bypass the module-scope cache and re-read
|
|
81
|
+
* @returns {Promise<Ruleset>}
|
|
82
|
+
*/
|
|
83
|
+
export async function getRuleset({ fresh = false } = {}) {
|
|
84
|
+
if (!fresh && cachedRuleset) return cachedRuleset;
|
|
85
|
+
const [artefact, umbrellaVersion] = await Promise.all([
|
|
86
|
+
readJson(rulesetPath),
|
|
87
|
+
getUmbrellaVersion(),
|
|
88
|
+
]);
|
|
89
|
+
// Defensive: strip any accidental rulesetVersion baked into the JSON so
|
|
90
|
+
// the umbrella version is authoritative. NV-BL-SR-02 is emphatic about
|
|
91
|
+
// this: a divergence here is a spec drift, not a data field.
|
|
92
|
+
delete artefact.rulesetVersion;
|
|
93
|
+
cachedRuleset = Object.freeze({ ...artefact, rulesetVersion: umbrellaVersion });
|
|
94
|
+
return cachedRuleset;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Detect whether the ruleset version on a chain differs from the shipping
|
|
99
|
+
* ruleset version, and classify the drift for NV-BL-ADM-06 (build-stage
|
|
100
|
+
* refusal) and DL-REQ-VALIDATE-03 (define-stage warning).
|
|
101
|
+
*
|
|
102
|
+
* Additive-only drift (only new rule ids appear on the shipping side) is
|
|
103
|
+
* classified `additive` and warns rather than refuses. Any other version
|
|
104
|
+
* mismatch is classified `behavioural` and refuses at build stage.
|
|
105
|
+
*
|
|
106
|
+
* Same-version comparisons return `{ drift: 'none' }`.
|
|
107
|
+
*
|
|
108
|
+
* @param {object} args
|
|
109
|
+
* @param {string|null|undefined} args.chainRulesetVersion - version the chain declared it was authored against.
|
|
110
|
+
* @param {Ruleset} [args.ruleset] - shipping ruleset; defaults to the loaded artefact.
|
|
111
|
+
* @returns {Promise<{ drift: 'none' | 'additive' | 'behavioural' | 'missing', shippingVersion: string, chainVersion: string | null }>}
|
|
112
|
+
*/
|
|
113
|
+
export async function detectRulesetDrift({ chainRulesetVersion, ruleset } = {}) {
|
|
114
|
+
const shipping = ruleset ?? (await getRuleset());
|
|
115
|
+
const shippingVersion = shipping.rulesetVersion;
|
|
116
|
+
const chainVersion = typeof chainRulesetVersion === 'string' && chainRulesetVersion.length > 0
|
|
117
|
+
? chainRulesetVersion
|
|
118
|
+
: null;
|
|
119
|
+
if (chainVersion === null) {
|
|
120
|
+
return { drift: 'missing', shippingVersion, chainVersion };
|
|
121
|
+
}
|
|
122
|
+
if (chainVersion === shippingVersion) {
|
|
123
|
+
return { drift: 'none', shippingVersion, chainVersion };
|
|
124
|
+
}
|
|
125
|
+
// v1 policy: any version mismatch is treated as behavioural drift for
|
|
126
|
+
// the build stage refusal path (NV-BL-ADM-06). Additive-only drift
|
|
127
|
+
// becomes distinguishable once the umbrella starts landing patch bumps
|
|
128
|
+
// that only add rules; the classifier lives here so define-stage
|
|
129
|
+
// warning (DL-REQ-VALIDATE-03) can consume it without duplicating logic.
|
|
130
|
+
return { drift: 'behavioural', shippingVersion, chainVersion };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Reset the module-scope cache. For tests that mutate the on-disk artefact
|
|
135
|
+
* or the package version.
|
|
136
|
+
*/
|
|
137
|
+
export function resetRulesetCache() {
|
|
138
|
+
cachedRuleset = null;
|
|
139
|
+
cachedUmbrellaVersion = null;
|
|
140
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schemaComment": "rcf-lite shared standards ruleset v1. Machine-readable contract consumed by build-lite's chain-admissibility lint, verify's per-AC scope check, and (from the umbrella release that adds it) rcf-define-lite's elicitation checklist. camelCase per estate convention. The rulesetVersion is stamped at load time from the umbrella package.json (see src/ruleset/index.js) per NV-BL-SR-02: there is no separate ruleset semver, its version is the rcf-lite umbrella version.",
|
|
3
|
+
"id": "rcf-lite-ruleset",
|
|
4
|
+
"provenance": {
|
|
5
|
+
"requirementsDoc": "projects/rcf-build-lite/docs/2026-08-06_build-lite-nextver-requirements.md",
|
|
6
|
+
"rulingSheet": "projects/rcf-build-lite/docs/2026-08-06_nextver-ruling-sheet.md",
|
|
7
|
+
"designWorkItem": "w-2026-07-28-012",
|
|
8
|
+
"ratifiedItems": [1, 2, 3, 4, 6, 7, 8, 11, 14, 15, 16, 17, 19]
|
|
9
|
+
},
|
|
10
|
+
"admissibilityRules": [
|
|
11
|
+
{
|
|
12
|
+
"id": "NV-BL-ADM-01",
|
|
13
|
+
"title": "Every functional REQ terminates in at least one AC observable as behaviour of the running product",
|
|
14
|
+
"requiresScopeTagVocabulary": true,
|
|
15
|
+
"refuseByDefault": true,
|
|
16
|
+
"overrideChannel": "recordedInChain",
|
|
17
|
+
"notes": "For every REQ classified functional, the chain must contain at least one AC that is observable in the running product, not only in a library or a fixture. AC-shape classifier tags govern which REQ-shapes must terminate in a runtime- or deployed-scope AC."
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": "NV-BL-ADM-02",
|
|
21
|
+
"title": "AC-shape classifier assigns each AC a scope tag (library / runtime / deployed)",
|
|
22
|
+
"requiresScopeTagVocabulary": true,
|
|
23
|
+
"refuseByDefault": true,
|
|
24
|
+
"overrideChannel": "migrationStateUnclassified",
|
|
25
|
+
"notes": "The scope-tag vocabulary and the AC schema field that carries the tag live in rcf-schemas. See scopeTagVocabulary below."
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "NV-BL-ADM-03",
|
|
29
|
+
"title": "Bound TCs must exercise their AC's declared scope",
|
|
30
|
+
"requiresScopeTagVocabulary": true,
|
|
31
|
+
"refuseByDefault": true,
|
|
32
|
+
"overrideChannel": "migrationStateUnclassified",
|
|
33
|
+
"notes": "For each AC, the bound TC's scope must be equal to or wider than the AC's scope tag. Bootstrap window: chains authored before scope tags land carry scope 'unclassified' and are tolerated for one release cycle."
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "NV-BL-ADM-04",
|
|
37
|
+
"title": "Source-comment honesty gate",
|
|
38
|
+
"refuseByDefault": true,
|
|
39
|
+
"overrideChannel": "adrExternalBoundaryOnly",
|
|
40
|
+
"markerVocabularyRef": "sourceCommentMarkers",
|
|
41
|
+
"notes": "Source files bound to any FBS at status complete are scanned for admission markers. Any match refuses FBS completion. Per ratified ruling-sheet item 16, the only permitted override channel is an ADR recording the deferral for a genuine external-boundary blocker; the ADR names the marker text, the blocker, and the reason. NV-BL-ADM-05's generic recorded-override channel no longer covers source markers."
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": "NV-BL-ADM-05",
|
|
45
|
+
"title": "Chain-admissibility gate is refuse-first, override-recorded",
|
|
46
|
+
"refuseByDefault": true,
|
|
47
|
+
"overrideChannel": "recordedInChain",
|
|
48
|
+
"notes": "A chain failing any admissibility check is refused before build starts. Explicit overrides are permitted for non-source-marker checks, recorded in the chain with rule id, reason, and authoring identity, and surfaced in the finalise summary. Source-comment markers (NV-BL-ADM-04) fall outside this generic override channel."
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"id": "NV-BL-ADM-06",
|
|
52
|
+
"title": "Build-stage refusal on ruleset-version drift",
|
|
53
|
+
"refuseByDefault": true,
|
|
54
|
+
"overrideChannel": "recordedInChain",
|
|
55
|
+
"notes": "At build start, build-lite compares the ruleset version the chain declares it was authored against with the version the shipping lint enforces. Behaviour-changing drift refuses; additive-only drift warns. Ratified 2026-08-11 ruling-sheet item 7: define-stage drift is a warning-with-acknowledgement; build-stage drift is a refusal."
|
|
56
|
+
}
|
|
57
|
+
],
|
|
58
|
+
"gateRules": [
|
|
59
|
+
{
|
|
60
|
+
"id": "NV-BL-GATE-01",
|
|
61
|
+
"title": "Pull rcf-verify profile-vs-AC check into the REVIEW stage",
|
|
62
|
+
"notes": "The profile-vs-AC scope-mismatch check runs at REVIEW time for every FBS, not only at finalise. Verify carries the check; the review-stage gate consumes the same helper."
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"id": "NV-BL-GATE-02",
|
|
66
|
+
"title": "Deploy-smoke FBS is a chain-mandatory element for any REQ with deployed-scope ACs",
|
|
67
|
+
"requiresScopeTagVocabulary": true,
|
|
68
|
+
"notes": "Any chain whose REQ set contains at least one deployed-scope AC must contain at least one FBS whose build sequence boots the built artefact against a real config and asserts operator-visible behaviour."
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"id": "NV-BL-GATE-03",
|
|
72
|
+
"title": "Boot-integration TC template family for runtime-scope ACs",
|
|
73
|
+
"templateFamilyRef": "tcTemplateFamily",
|
|
74
|
+
"notes": "Runtime-scope ACs whose bound TC is not a boot-integration TC or a wider-scope TC fail admissibility (via NV-BL-ADM-03) and REVIEW (via NV-BL-GATE-01)."
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"id": "NV-BL-GATE-04",
|
|
78
|
+
"title": "Ruling-set consistency light-check at ruling-application time",
|
|
79
|
+
"checkFamilyRef": "rulingConsistencyChecks",
|
|
80
|
+
"notes": "Light mechanical checks only per ratified ruling-sheet item 4: no two rulings name the same external resource with contradictory constraints; no ruling implies a capability the ruled-in service does not have on the ruled-in tier. Probabilistic and suitability work belongs in rcf-define-lite under DL-REQ-VALIDATE-04."
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
"scopeTagVocabulary": {
|
|
84
|
+
"sourcePackage": "@stravica-ai/rcf-schemas",
|
|
85
|
+
"schemaRef": "https://schemas.stravica.io/rcf/v0.4.0/common.schema.json#/$defs/scopeTag",
|
|
86
|
+
"values": [
|
|
87
|
+
{ "value": "library", "description": "Observable in an isolated library or fixture." },
|
|
88
|
+
{ "value": "runtime", "description": "Observable in the running product against local integration." },
|
|
89
|
+
{ "value": "deployed", "description": "Observable on a booted artefact against real config and real network." },
|
|
90
|
+
{ "value": "unclassified", "description": "Migration state for chains authored before the scope-tag vocabulary shipped; ruleset-enforcing consumers decide whether unclassified is currently accepted." }
|
|
91
|
+
],
|
|
92
|
+
"unclassifiedMigrationTolerance": {
|
|
93
|
+
"toleratedFor": "one release cycle from the umbrella release that first ships this ruleset",
|
|
94
|
+
"expiresWhen": "rcf-define-lite ships and can take over elicitation"
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"sourceCommentMarkers": [
|
|
98
|
+
{ "marker": "TODO", "caseInsensitive": true },
|
|
99
|
+
{ "marker": "FIXME", "caseInsensitive": true },
|
|
100
|
+
{ "marker": "XXX", "caseInsensitive": true },
|
|
101
|
+
{ "marker": "HACK", "caseInsensitive": true },
|
|
102
|
+
{ "marker": "placeholder", "caseInsensitive": true },
|
|
103
|
+
{ "marker": "v1 refinement", "caseInsensitive": true },
|
|
104
|
+
{ "marker": "deferred", "caseInsensitive": true },
|
|
105
|
+
{ "marker": "stub", "caseInsensitive": true }
|
|
106
|
+
],
|
|
107
|
+
"tcTemplateFamily": [
|
|
108
|
+
{
|
|
109
|
+
"id": "TCT-SERVER-BOOT",
|
|
110
|
+
"title": "Server-boot integration TC",
|
|
111
|
+
"surface": "server",
|
|
112
|
+
"notes": "Boots the built server against a real config and asserts an operator-visible observable (probe fires, page returns 200 with expected content, and so on)."
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"id": "TCT-CLI-INVOKE",
|
|
116
|
+
"title": "CLI-invoke integration TC",
|
|
117
|
+
"surface": "cli",
|
|
118
|
+
"notes": "Invokes the packaged CLI as an installed artefact (not via the workspace source) against a real config and asserts an operator-visible observable."
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"id": "TCT-CONTAINER-RUN",
|
|
122
|
+
"title": "Container-run integration TC",
|
|
123
|
+
"surface": "container",
|
|
124
|
+
"notes": "Runs the built container against a real config and asserts an operator-visible observable."
|
|
125
|
+
}
|
|
126
|
+
],
|
|
127
|
+
"rulingConsistencyChecks": [
|
|
128
|
+
{
|
|
129
|
+
"id": "RCC-EXTERNAL-RESOURCE-CONTRADICTION",
|
|
130
|
+
"title": "No two rulings name the same external resource with contradictory constraints",
|
|
131
|
+
"kind": "lightMechanical",
|
|
132
|
+
"notes": "Detects the Resend-collision class: Ruling A names service.tier=free and Ruling B names service.domainCount=N with N > tier-limit(free)."
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"id": "RCC-TIER-CAPABILITY-MISMATCH",
|
|
136
|
+
"title": "No ruling implies a capability the ruled-in service does not have on the ruled-in tier",
|
|
137
|
+
"kind": "lightMechanical",
|
|
138
|
+
"notes": "Detects the ruling-in of a capability against a tier that cannot deliver it. Deep suitability checks live in rcf-define-lite under DL-REQ-VALIDATE-04."
|
|
139
|
+
}
|
|
140
|
+
],
|
|
141
|
+
"toolScope": {
|
|
142
|
+
"chainAdmissibility": true,
|
|
143
|
+
"traceabilityAndQueryTools": true,
|
|
144
|
+
"notes": "Per ratified ruling-sheet item 1 addendum: the same rule set governs what a traceability or query tool may return as much as it governs what build may consume. A query tool that hides an admissibility failure is the same class of defect as a build that hides one."
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// HTTP + SSE server for the live-view surface. Wires the watch primitive
|
|
2
|
+
// to the walker to the SSE hub: any change under `rcf/` triggers a full
|
|
3
|
+
// re-walk (D4) and a `tree-update` broadcast (D12). Binds 127.0.0.1 only
|
|
4
|
+
// (D11); EADDRINUSE is a hard failure (D10).
|
|
5
|
+
//
|
|
6
|
+
// Public surface:
|
|
7
|
+
// startServer({ projectRoot, port, host, log, heartbeatMs, watchImpl })
|
|
8
|
+
// -> Promise<{ url, port, close, hub, currentState }>
|
|
9
|
+
// close() drains SSE with a `shutdown` event and releases the port. The
|
|
10
|
+
// caller (bin/rcf-view.js) is responsible for the 2s force-exit budget
|
|
11
|
+
// on top; the server itself does not `process.exit`.
|
|
12
|
+
|
|
13
|
+
import { createServer } from 'node:http';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
|
|
16
|
+
import { LIVE_CLIENT_PATH, STYLE_CSS_PATH, VENDORED_MERMAID_PATH, renderModelToPage } from '../view/index.js';
|
|
17
|
+
import { watch as defaultWatch } from '../watch/index.js';
|
|
18
|
+
import { createRouter } from './routes.js';
|
|
19
|
+
import { createSseHub } from './sse.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {object} StartServerOptions
|
|
23
|
+
* @property {string} projectRoot
|
|
24
|
+
* @property {number} [port=4373]
|
|
25
|
+
* @property {string} [host='127.0.0.1']
|
|
26
|
+
* @property {number} [heartbeatMs=30000]
|
|
27
|
+
* @property {number} [debounceMs=50]
|
|
28
|
+
* @property {(line: string) => void} [log] - stderr sink
|
|
29
|
+
* @property {typeof defaultWatch} [watchImpl] - injectable watch primitive for tests
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {StartServerOptions} args
|
|
34
|
+
* @returns {Promise<{
|
|
35
|
+
* url: string,
|
|
36
|
+
* port: number,
|
|
37
|
+
* host: string,
|
|
38
|
+
* close: () => Promise<void>,
|
|
39
|
+
* currentState: () => { version: number, fullPageHtml: string, contentHtml: string } | null,
|
|
40
|
+
* sse: ReturnType<typeof createSseHub>,
|
|
41
|
+
* rewalk: () => Promise<void>,
|
|
42
|
+
* }>}
|
|
43
|
+
*/
|
|
44
|
+
export async function startServer(args) {
|
|
45
|
+
const projectRoot = args.projectRoot;
|
|
46
|
+
const port = typeof args.port === 'number' ? args.port : 4373;
|
|
47
|
+
const host = typeof args.host === 'string' ? args.host : '127.0.0.1';
|
|
48
|
+
const heartbeatMs = typeof args.heartbeatMs === 'number' ? args.heartbeatMs : 30000;
|
|
49
|
+
const debounceMs = typeof args.debounceMs === 'number' ? args.debounceMs : 50;
|
|
50
|
+
const log = typeof args.log === 'function' ? args.log : () => {};
|
|
51
|
+
const watchImpl = typeof args.watchImpl === 'function' ? args.watchImpl : defaultWatch;
|
|
52
|
+
|
|
53
|
+
/** @type {{ version: number, fullPageHtml: string, contentHtml: string, errors: import('#core/errors').RcfError[] } | null} */
|
|
54
|
+
let state = null;
|
|
55
|
+
let version = 0;
|
|
56
|
+
let rewalkInFlight = null;
|
|
57
|
+
let closed = false;
|
|
58
|
+
|
|
59
|
+
const sse = createSseHub({ heartbeatMs, log });
|
|
60
|
+
|
|
61
|
+
async function rewalk() {
|
|
62
|
+
if (closed) return;
|
|
63
|
+
if (rewalkInFlight) {
|
|
64
|
+
// Coalesce concurrent walks. The trailing one will pick up the
|
|
65
|
+
// final on-disk state; a middle one adds nothing.
|
|
66
|
+
return rewalkInFlight;
|
|
67
|
+
}
|
|
68
|
+
rewalkInFlight = (async () => {
|
|
69
|
+
try {
|
|
70
|
+
const result = await renderModelToPage({ projectRoot });
|
|
71
|
+
if (closed) return;
|
|
72
|
+
version += 1;
|
|
73
|
+
state = {
|
|
74
|
+
version,
|
|
75
|
+
fullPageHtml: result.fullPageHtml,
|
|
76
|
+
contentHtml: result.contentHtml,
|
|
77
|
+
errors: result.errors,
|
|
78
|
+
};
|
|
79
|
+
sse.broadcast('tree-update', { version, contentHtml: result.contentHtml });
|
|
80
|
+
if (result.errors && result.errors.length > 0) {
|
|
81
|
+
sse.broadcast('walker-error', { errors: result.errors });
|
|
82
|
+
}
|
|
83
|
+
} catch (err) {
|
|
84
|
+
log(`[server] walker failed: ${/** @type {Error} */ (err).message}`);
|
|
85
|
+
sse.broadcast('walker-error', {
|
|
86
|
+
errors: [{ kind: 'ioFailure', message: /** @type {Error} */ (err).message }],
|
|
87
|
+
});
|
|
88
|
+
} finally {
|
|
89
|
+
rewalkInFlight = null;
|
|
90
|
+
}
|
|
91
|
+
})();
|
|
92
|
+
return rewalkInFlight;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Initial walk before we bind, so the first HTTP GET / has content ready
|
|
96
|
+
// and the first SSE connect gets a real payload.
|
|
97
|
+
await rewalk();
|
|
98
|
+
|
|
99
|
+
const router = createRouter({
|
|
100
|
+
currentState: () => state,
|
|
101
|
+
sse,
|
|
102
|
+
stylePath: STYLE_CSS_PATH,
|
|
103
|
+
mermaidPath: VENDORED_MERMAID_PATH,
|
|
104
|
+
liveClientPath: LIVE_CLIENT_PATH,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const server = createServer(router);
|
|
108
|
+
|
|
109
|
+
// Watcher: any .json change under rcf/ triggers a re-walk. Debounced
|
|
110
|
+
// per D4. Non-JSON files are filtered in the primitive itself.
|
|
111
|
+
const watchDir = join(projectRoot, 'rcf');
|
|
112
|
+
const watcher = watchImpl({
|
|
113
|
+
paths: [watchDir],
|
|
114
|
+
onChange: () => { rewalk().catch(() => {}); },
|
|
115
|
+
debounceMs,
|
|
116
|
+
onError: (err) => { log(`[watch] ${err.message}`); },
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Bind. EADDRINUSE surfaces as a rejected Promise so the caller can
|
|
120
|
+
// print a clear message and exit 2 (D10). On bind failure we also tear
|
|
121
|
+
// down the watcher and close the (unbound) http.Server so the process
|
|
122
|
+
// has no lingering handles.
|
|
123
|
+
try {
|
|
124
|
+
await new Promise((resolve, reject) => {
|
|
125
|
+
function onError(err) {
|
|
126
|
+
server.off('listening', onListening);
|
|
127
|
+
reject(err);
|
|
128
|
+
}
|
|
129
|
+
function onListening() {
|
|
130
|
+
server.off('error', onError);
|
|
131
|
+
resolve();
|
|
132
|
+
}
|
|
133
|
+
server.once('error', onError);
|
|
134
|
+
server.once('listening', onListening);
|
|
135
|
+
server.listen(port, host);
|
|
136
|
+
});
|
|
137
|
+
} catch (err) {
|
|
138
|
+
try { watcher.close(); } catch { /* swallow */ }
|
|
139
|
+
try { sse.close(); } catch { /* swallow */ }
|
|
140
|
+
try { server.close(); } catch { /* swallow */ }
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const boundPort = /** @type {import('node:net').AddressInfo} */ (server.address()).port;
|
|
145
|
+
|
|
146
|
+
async function close() {
|
|
147
|
+
if (closed) return;
|
|
148
|
+
closed = true;
|
|
149
|
+
try { watcher.close(); } catch { /* swallow */ }
|
|
150
|
+
await sse.drain('shutdown');
|
|
151
|
+
// Kick any lingering keep-alive sockets out first so `server.close()`
|
|
152
|
+
// resolves quickly. Node 18.2+ exposes both APIs.
|
|
153
|
+
try { server.closeIdleConnections?.(); } catch { /* swallow */ }
|
|
154
|
+
try { server.closeAllConnections?.(); } catch { /* swallow */ }
|
|
155
|
+
await new Promise((resolve) => {
|
|
156
|
+
let done = false;
|
|
157
|
+
server.close(() => { if (!done) { done = true; resolve(); } });
|
|
158
|
+
// Backstop: never let the promise hang beyond the bin's 2s budget.
|
|
159
|
+
const t = setTimeout(() => {
|
|
160
|
+
if (done) return;
|
|
161
|
+
done = true;
|
|
162
|
+
try { server.closeAllConnections?.(); } catch { /* swallow */ }
|
|
163
|
+
resolve();
|
|
164
|
+
}, 500);
|
|
165
|
+
if (typeof t.unref === 'function') t.unref();
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
url: `http://${host}:${boundPort}/`,
|
|
171
|
+
port: boundPort,
|
|
172
|
+
host,
|
|
173
|
+
close,
|
|
174
|
+
currentState: () => state,
|
|
175
|
+
sse,
|
|
176
|
+
rewalk,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// HTTP route table for the live-view server. Only five routes are served:
|
|
2
|
+
// GET / -> rendered page (text/html)
|
|
3
|
+
// GET /events -> SSE stream (text/event-stream)
|
|
4
|
+
// GET /style.css -> shipped stylesheet
|
|
5
|
+
// GET /mermaid.min.js-> vendored mermaid runtime
|
|
6
|
+
// GET /live-client.js-> phase 3.8 live client script
|
|
7
|
+
// Everything else -> 404 text/plain.
|
|
8
|
+
//
|
|
9
|
+
// No CORS headers, no cache headers on static assets beyond what the
|
|
10
|
+
// browser derives from same-origin/localhost trust (D5, D11).
|
|
11
|
+
|
|
12
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
13
|
+
|
|
14
|
+
const MIME = {
|
|
15
|
+
html: 'text/html; charset=utf-8',
|
|
16
|
+
css: 'text/css; charset=utf-8',
|
|
17
|
+
js: 'application/javascript; charset=utf-8',
|
|
18
|
+
txt: 'text/plain; charset=utf-8',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {object} RouterDeps
|
|
23
|
+
* @property {() => { fullPageHtml: string, contentHtml: string, version: number } | null} currentState
|
|
24
|
+
* @property {ReturnType<typeof import('./sse.js').createSseHub>} sse
|
|
25
|
+
* @property {string} stylePath
|
|
26
|
+
* @property {string} mermaidPath
|
|
27
|
+
* @property {string} liveClientPath
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create a request handler bound to the current state provider and the
|
|
32
|
+
* SSE hub. The returned function has signature `(req, res) -> void` and
|
|
33
|
+
* is ready to pass into `http.createServer`.
|
|
34
|
+
*
|
|
35
|
+
* @param {RouterDeps} deps
|
|
36
|
+
* @returns {(req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse) => void}
|
|
37
|
+
*/
|
|
38
|
+
export function createRouter(deps) {
|
|
39
|
+
return function handle(req, res) {
|
|
40
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
41
|
+
res.writeHead(405, { 'content-type': MIME.txt, allow: 'GET, HEAD' });
|
|
42
|
+
res.end('method not allowed\n');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
46
|
+
const path = url.pathname;
|
|
47
|
+
|
|
48
|
+
if (path === '/' || path === '/index.html') {
|
|
49
|
+
const state = deps.currentState();
|
|
50
|
+
if (!state) {
|
|
51
|
+
res.writeHead(503, { 'content-type': MIME.txt });
|
|
52
|
+
res.end('view server initialising\n');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
res.writeHead(200, { 'content-type': MIME.html });
|
|
56
|
+
res.end(state.fullPageHtml);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (path === '/events') {
|
|
60
|
+
const state = deps.currentState();
|
|
61
|
+
const payload = state ? { version: state.version, contentHtml: state.contentHtml } : null;
|
|
62
|
+
deps.sse.handle(req, res, payload);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (path === '/style.css') {
|
|
66
|
+
serveFile(res, deps.stylePath, MIME.css).catch((err) => fail(res, err));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (path === '/mermaid.min.js') {
|
|
70
|
+
serveFile(res, deps.mermaidPath, MIME.js).catch((err) => fail(res, err));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (path === '/live-client.js') {
|
|
74
|
+
serveFile(res, deps.liveClientPath, MIME.js).catch((err) => fail(res, err));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
res.writeHead(404, { 'content-type': MIME.txt });
|
|
79
|
+
res.end('not found\n');
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function serveFile(res, path, contentType) {
|
|
84
|
+
let body;
|
|
85
|
+
let size;
|
|
86
|
+
try {
|
|
87
|
+
const [buf, s] = await Promise.all([readFile(path), stat(path)]);
|
|
88
|
+
body = buf;
|
|
89
|
+
size = s.size;
|
|
90
|
+
} catch (err) {
|
|
91
|
+
if (/** @type {NodeJS.ErrnoException} */ (err).code === 'ENOENT') {
|
|
92
|
+
res.writeHead(404, { 'content-type': MIME.txt });
|
|
93
|
+
res.end('not found\n');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
98
|
+
res.writeHead(200, {
|
|
99
|
+
'content-type': contentType,
|
|
100
|
+
'content-length': String(size),
|
|
101
|
+
});
|
|
102
|
+
res.end(body);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function fail(res, err) {
|
|
106
|
+
try {
|
|
107
|
+
res.writeHead(500, { 'content-type': MIME.txt });
|
|
108
|
+
res.end(`internal error: ${err && err.message ? err.message : 'unknown'}\n`);
|
|
109
|
+
} catch { /* swallow */ }
|
|
110
|
+
}
|