rcf-lite 0.0.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +288 -0
- package/LICENSE +202 -0
- package/README.md +92 -4
- package/bin/rcf-verify.js +122 -0
- package/bin/rcf.js +174 -0
- package/bin/view-supervisor-child.mjs +14 -0
- package/fixtures/canary-manifest.json +103 -0
- package/guidance/README.md +25 -0
- package/guidance/build-cycle-playbook.md +492 -0
- package/guidance/build-cycle.md +40 -0
- package/guidance/document-model.md +45 -0
- package/guidance/elicitation-playbook.md +442 -0
- package/guidance/harness-template.md +246 -0
- package/guidance/managed/README.md +63 -0
- package/guidance/managed/agent-instructions-block.hash +1 -0
- package/guidance/managed/agent-instructions-block.md +220 -0
- package/guidance/managed/legacy-fragment-hashes.json +22 -0
- package/guidance/manifest.json +21 -0
- package/guidance/overview.md +48 -0
- package/guidance/persona-programme.md +44 -0
- package/package.json +69 -8
- package/rcf/adrs/adr-001.json +25 -0
- package/rcf/adrs/adr-002.json +25 -0
- package/rcf/adrs/adr-003.json +25 -0
- package/rcf/adrs/adr-004.json +20 -0
- package/rcf/adrs/adr-005.json +20 -0
- package/rcf/adrs/adr-006.json +25 -0
- package/rcf/adrs/adr-007.json +25 -0
- package/rcf/adrs/adr-008.json +25 -0
- package/rcf/adrs/adr-009.json +25 -0
- package/rcf/build-sequence.json +11 -0
- package/rcf/code-nodes/cn-001.json +14 -0
- package/rcf/code-nodes/cn-002.json +14 -0
- package/rcf/code-nodes/cn-003.json +15 -0
- package/rcf/code-nodes/cn-004.json +17 -0
- package/rcf/code-nodes/cn-005.json +19 -0
- package/rcf/code-nodes/cn-006.json +17 -0
- package/rcf/code-nodes/cn-007.json +16 -0
- package/rcf/code-nodes/cn-008.json +19 -0
- package/rcf/code-nodes/cn-009.json +16 -0
- package/rcf/code-nodes/cn-010.json +18 -0
- package/rcf/code-nodes/cn-011.json +18 -0
- package/rcf/code-nodes/cn-012.json +18 -0
- package/rcf/code-nodes/cn-013.json +19 -0
- package/rcf/code-nodes/cn-014.json +16 -0
- package/rcf/code-nodes/cn-015.json +16 -0
- package/rcf/code-nodes/cn-016.json +16 -0
- package/rcf/code-nodes/cn-017.json +16 -0
- package/rcf/code-nodes/cn-018.json +15 -0
- package/rcf/code-nodes/cn-019.json +16 -0
- package/rcf/code-nodes/cn-020.json +16 -0
- package/rcf/code-nodes/cn-021.json +16 -0
- package/rcf/code-nodes/cn-022.json +16 -0
- package/rcf/code-nodes/cn-023.json +16 -0
- package/rcf/code-nodes/cn-024.json +16 -0
- package/rcf/code-nodes/cn-025.json +16 -0
- package/rcf/code-nodes/cn-026.json +16 -0
- package/rcf/code-nodes/cn-027.json +16 -0
- package/rcf/code-nodes/cn-028.json +16 -0
- package/rcf/code-nodes/cn-029.json +19 -0
- package/rcf/code-nodes/cn-030.json +14 -0
- package/rcf/code-nodes/cn-031.json +14 -0
- package/rcf/code-nodes/cn-032.json +14 -0
- package/rcf/code-nodes/cn-033.json +14 -0
- package/rcf/code-nodes/cn-034.json +14 -0
- package/rcf/code-nodes/cn-035.json +14 -0
- package/rcf/code-nodes/cn-036.json +14 -0
- package/rcf/code-nodes/cn-037.json +14 -0
- package/rcf/code-nodes/cn-038.json +14 -0
- package/rcf/code-nodes/cn-039.json +14 -0
- package/rcf/code-nodes/cn-040.json +14 -0
- package/rcf/code-nodes/cn-041.json +14 -0
- package/rcf/code-nodes/cn-042.json +14 -0
- package/rcf/code-nodes/cn-043.json +14 -0
- package/rcf/code-nodes/cn-044.json +14 -0
- package/rcf/code-nodes/cn-045.json +14 -0
- package/rcf/code-nodes/cn-046.json +14 -0
- package/rcf/code-nodes/cn-047.json +14 -0
- package/rcf/code-nodes/cn-048.json +14 -0
- package/rcf/code-nodes/cn-049.json +14 -0
- package/rcf/code-nodes/cn-050.json +14 -0
- package/rcf/code-nodes/cn-051.json +14 -0
- package/rcf/code-nodes/cn-052.json +14 -0
- package/rcf/code-nodes/cn-053.json +14 -0
- package/rcf/code-nodes/cn-054.json +14 -0
- package/rcf/code-nodes/cn-055.json +14 -0
- package/rcf/code-nodes/cn-056.json +14 -0
- package/rcf/code-nodes/cn-057.json +14 -0
- package/rcf/fbs/fbs-001.json +49 -0
- package/rcf/fbs/fbs-002.json +42 -0
- package/rcf/fbs/fbs-003.json +37 -0
- package/rcf/fbs/fbs-004.json +39 -0
- package/rcf/fbs/fbs-005.json +38 -0
- package/rcf/fbs/fbs-006.json +48 -0
- package/rcf/fbs/fbs-007.json +39 -0
- package/rcf/fbs/fbs-008.json +40 -0
- package/rcf/fbs/fbs-009.json +36 -0
- package/rcf/fbs/fbs-010.json +41 -0
- package/rcf/fbs/fbs-011.json +36 -0
- package/rcf/fbs/fbs-012.json +46 -0
- package/rcf/fbs/fbs-013.json +42 -0
- package/rcf/fbs/fbs-014.json +49 -0
- package/rcf/fbs/fbs-015.json +32 -0
- package/rcf/manifest.json +17 -0
- package/rcf/prd.json +47 -0
- package/rcf/requirements/req-001.json +19 -0
- package/rcf/requirements/req-002.json +19 -0
- package/rcf/requirements/req-003.json +19 -0
- package/rcf/requirements/req-004.json +19 -0
- package/rcf/requirements/req-005.json +19 -0
- package/rcf/requirements/req-006.json +19 -0
- package/rcf/requirements/req-007.json +19 -0
- package/rcf/requirements/req-008.json +19 -0
- package/rcf/requirements/req-009.json +19 -0
- package/rcf/tacs/tac-001.json +45 -0
- package/rcf/tacs/tac-002.json +109 -0
- package/rcf/tacs/tac-003.json +40 -0
- package/rcf/tacs/tac-004.json +51 -0
- package/rcf/tacs/tac-005.json +52 -0
- package/rcf/tacs/tac-006.json +104 -0
- package/rcf/tacs/tac-007.json +38 -0
- package/rcf/tacs/tac-008.json +51 -0
- package/rcf/tad.json +51 -0
- package/rcf/test-suites/PENDING.md +23 -0
- package/rcf/test-suites/ts-001.json +38 -0
- package/rcf/test-suites/ts-002.json +38 -0
- package/rcf/test-suites/ts-003.json +43 -0
- package/rcf/test-suites/ts-004.json +44 -0
- package/rcf/test-suites/ts-005.json +30 -0
- package/rcf/test-suites/ts-006.json +36 -0
- package/rcf/test-suites/ts-007.json +43 -0
- package/rcf/test-suites/ts-008.json +37 -0
- package/rcf/test-suites/ts-009.json +38 -0
- package/rcf/test-suites/ts-010.json +38 -0
- package/rcf/test-suites/ts-011.json +44 -0
- package/rcf/test-suites/ts-012.json +36 -0
- package/rcf/test-suites/ts-013.json +38 -0
- package/rcf/test-suites/ts-014.json +38 -0
- package/rcf/test-suites/ts-015.json +38 -0
- package/rcf/test-suites/ts-016.json +37 -0
- package/rcf/test-suites/ts-017.json +38 -0
- package/rcf/test-suites/ts-018.json +38 -0
- package/rcf/test-suites/ts-019.json +37 -0
- package/rcf/test-suites/ts-020.json +46 -0
- package/rcf/test-suites/ts-021.json +46 -0
- package/rcf/test-suites/ts-022.json +46 -0
- package/rcf/test-suites/ts-023.json +46 -0
- package/rcf/test-suites/ts-024.json +46 -0
- package/rcf/test-suites/ts-025.json +52 -0
- package/rcf/user-stories/us-101.json +40 -0
- package/rcf/user-stories/us-102.json +40 -0
- package/rcf/user-stories/us-201.json +40 -0
- package/rcf/user-stories/us-202.json +40 -0
- package/rcf/user-stories/us-203.json +32 -0
- package/rcf/user-stories/us-301.json +40 -0
- package/rcf/user-stories/us-302.json +40 -0
- package/rcf/user-stories/us-303.json +40 -0
- package/rcf/user-stories/us-304.json +40 -0
- package/rcf/user-stories/us-401.json +40 -0
- package/rcf/user-stories/us-402.json +40 -0
- package/rcf/user-stories/us-403.json +40 -0
- package/rcf/user-stories/us-501.json +40 -0
- package/rcf/user-stories/us-502.json +40 -0
- package/rcf/user-stories/us-503.json +40 -0
- package/rcf/user-stories/us-601.json +40 -0
- package/rcf/user-stories/us-602.json +40 -0
- package/rcf/user-stories/us-701.json +40 -0
- package/rcf/user-stories/us-702.json +40 -0
- package/rcf/user-stories/us-801.json +49 -0
- package/rcf/user-stories/us-802.json +49 -0
- package/rcf/user-stories/us-803.json +49 -0
- package/rcf/user-stories/us-804.json +49 -0
- package/rcf/user-stories/us-805.json +49 -0
- package/rcf/user-stories/us-901.json +40 -0
- package/src/.gitkeep +0 -0
- package/src/browser-verify/auth-smoke.js +109 -0
- package/src/browser-verify/index.js +29 -0
- package/src/browser-verify/invariants.js +336 -0
- package/src/browser-verify/manifest-writer.js +189 -0
- package/src/browser-verify/runner.js +171 -0
- package/src/build/bundle.js +198 -0
- package/src/build/formatters/json.js +18 -0
- package/src/build/formatters/markdown.js +406 -0
- package/src/build/index.js +14 -0
- package/src/build/mark.js +177 -0
- package/src/build/queue.js +285 -0
- package/src/cli/browser-verify.js +231 -0
- package/src/cli/build.js +584 -0
- package/src/cli/coverage.js +219 -0
- package/src/cli/create.js +355 -0
- package/src/cli/delete.js +127 -0
- package/src/cli/design.js +353 -0
- package/src/cli/doctor.js +548 -0
- package/src/cli/fbs.js +171 -0
- package/src/cli/finalise.js +377 -0
- package/src/cli/guidance.js +206 -0
- package/src/cli/help.js +156 -0
- package/src/cli/impact.js +119 -0
- package/src/cli/init.js +282 -0
- package/src/cli/intake.js +153 -0
- package/src/cli/link.js +128 -0
- package/src/cli/mcp.js +160 -0
- package/src/cli/preflight.js +220 -0
- package/src/cli/read.js +162 -0
- package/src/cli/req-baseline.js +269 -0
- package/src/cli/req-classify.js +135 -0
- package/src/cli/review.js +295 -0
- package/src/cli/test-suite.js +216 -0
- package/src/cli/trace.js +172 -0
- package/src/cli/ui-baseline.js +292 -0
- package/src/cli/ui-classify.js +108 -0
- package/src/cli/update.js +197 -0
- package/src/cli/validate.js +168 -0
- package/src/cli/view.js +452 -0
- package/src/core/baseline-catalog/data/auth.json +42 -0
- package/src/core/baseline-catalog/data/http-api.json +42 -0
- package/src/core/baseline-catalog/data/notifications.json +33 -0
- package/src/core/baseline-catalog/data/persistence.json +33 -0
- package/src/core/baseline-catalog/data/web-ui.json +60 -0
- package/src/core/baseline-catalog/index.js +121 -0
- package/src/core/errors/index.js +167 -0
- package/src/core/fixtures/register-canary/canary-prompt-01.json +18 -0
- package/src/core/fixtures/register-canary/canary-prompt-02.json +21 -0
- package/src/core/fixtures/register-canary/canary-prompt-03.json +17 -0
- package/src/core/isolation/index.js +60 -0
- package/src/core/mcp/framing.js +103 -0
- package/src/core/mcp/index.js +8 -0
- package/src/core/mcp/server.js +228 -0
- package/src/core/patterns/register-canary.js +209 -0
- package/src/core/patterns/req-shapes.js +158 -0
- package/src/core/patterns/services.js +358 -0
- package/src/core/patterns/ui-shapes.js +166 -0
- package/src/core/store/cn-resolve.js +134 -0
- package/src/core/store/derive-deps.js +93 -0
- package/src/core/store/ids.js +78 -0
- package/src/core/store/index.js +20 -0
- package/src/core/store/init.js +255 -0
- package/src/core/store/loader.js +211 -0
- package/src/core/store/tp-resolve.js +176 -0
- package/src/core/store/validator.js +191 -0
- package/src/core/store/walker.js +898 -0
- package/src/core/store/writer.js +1849 -0
- package/src/design/index.js +11 -0
- package/src/design/writer.js +271 -0
- package/src/finalise/detect.js +129 -0
- package/src/finalise/index.js +17 -0
- package/src/finalise/ingest.js +119 -0
- package/src/finalise/install.js +119 -0
- package/src/finalise/ship-without-verified.js +131 -0
- package/src/finalise/spawn.js +84 -0
- package/src/intake/fidelity.js +105 -0
- package/src/intake/index.js +6 -0
- package/src/intake/manifest-writer.js +100 -0
- package/src/intake/orchestrator.js +138 -0
- package/src/intake/validate.js +80 -0
- package/src/mcp/map-errors.js +131 -0
- package/src/mcp/prompts.js +49 -0
- package/src/mcp/resources.js +244 -0
- package/src/mcp/tools.js +1204 -0
- package/src/preflight/design-shapes.js +185 -0
- package/src/preflight/index.js +90 -0
- package/src/preflight/manifest-writer.js +264 -0
- package/src/preflight/scanner.js +206 -0
- package/src/preflight/secrets.js +134 -0
- package/src/preflight/session.js +246 -0
- package/src/query/attestation.js +285 -0
- package/src/query/coverage.js +308 -0
- package/src/query/formatters/json.js +21 -0
- package/src/query/formatters/mermaid.js +209 -0
- package/src/query/formatters/table.js +203 -0
- package/src/query/impact.js +173 -0
- package/src/query/index.js +9 -0
- package/src/query/trace.js +345 -0
- package/src/register-canary/fixture-loader.js +87 -0
- package/src/register-canary/index.js +10 -0
- package/src/register-canary/record-writer.js +132 -0
- package/src/register-canary/runner.js +156 -0
- package/src/req-baseline/gate.js +86 -0
- package/src/req-baseline/index.js +27 -0
- package/src/req-baseline/open-candidates.js +143 -0
- package/src/req-baseline/opt-out.js +195 -0
- package/src/req-baseline/sweep.js +230 -0
- package/src/req-detection/classifier.js +181 -0
- package/src/req-detection/index.js +9 -0
- package/src/req-detection/persist.js +55 -0
- package/src/review/index.js +325 -0
- package/src/review/mutation.js +117 -0
- package/src/review/ui-baseline-drift.js +138 -0
- package/src/server/index.js +178 -0
- package/src/server/routes.js +110 -0
- package/src/server/sse.js +118 -0
- package/src/setup/agent-setup.js +362 -0
- package/src/setup/identity-seed.js +104 -0
- package/src/setup/knowledge-seed.js +123 -0
- package/src/setup/managed-block.js +193 -0
- package/src/setup/managed-gitignore.js +166 -0
- package/src/setup/managed-markers.js +49 -0
- package/src/ui-baseline/defaults.js +119 -0
- package/src/ui-baseline/index.js +25 -0
- package/src/ui-baseline/manifest-writer.js +282 -0
- package/src/ui-baseline/session.js +178 -0
- package/src/ui-detection/classifier.js +192 -0
- package/src/verify/chain/index.js +190 -0
- package/src/verify/cli/cleanup.js +61 -0
- package/src/verify/cli/help.js +56 -0
- package/src/verify/cli/mcp.js +98 -0
- package/src/verify/cli/provision.js +71 -0
- package/src/verify/cli/report.js +71 -0
- package/src/verify/cli/run.js +155 -0
- package/src/verify/engine/brief.js +87 -0
- package/src/verify/engine/index.js +177 -0
- package/src/verify/engine/launcher.js +307 -0
- package/src/verify/mcp/tools.js +107 -0
- package/src/verify/profile/index.js +146 -0
- package/src/verify/provision/index.js +256 -0
- package/src/verify/report/index.js +139 -0
- package/src/verify/report/renderer.js +118 -0
- package/src/verify/verdict/index.js +246 -0
- package/src/view/doc-renderers/adr.js +44 -0
- package/src/view/doc-renderers/build-sequence.js +40 -0
- package/src/view/doc-renderers/fbs.js +128 -0
- package/src/view/doc-renderers/helpers.js +159 -0
- package/src/view/doc-renderers/index.js +12 -0
- package/src/view/doc-renderers/prd.js +45 -0
- package/src/view/doc-renderers/req.js +43 -0
- package/src/view/doc-renderers/tac.js +38 -0
- package/src/view/doc-renderers/tad.js +74 -0
- package/src/view/doc-renderers/test-suite.js +45 -0
- package/src/view/doc-renderers/user-story.js +63 -0
- package/src/view/html-page.js +462 -0
- package/src/view/index.js +63 -0
- package/src/view/live-client.js +338 -0
- package/src/view/mermaid-diagram.js +178 -0
- package/src/view/style.css +735 -0
- package/src/view/tree-model.js +152 -0
- package/src/view/vendored/mermaid.min.js +2607 -0
- package/src/view-supervisor/index.js +26 -0
- package/src/view-supervisor/logs.js +32 -0
- package/src/view-supervisor/manifest-writer.js +178 -0
- package/src/view-supervisor/persist-until.js +85 -0
- package/src/view-supervisor/supervisor.js +276 -0
- package/src/watch/index.js +152 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
// Third-party service seed pattern set - single source of truth for
|
|
2
|
+
// service-dependency detection across the 0.7.0 verification-integrity
|
|
3
|
+
// train.
|
|
4
|
+
//
|
|
5
|
+
// Consumed by:
|
|
6
|
+
// - `packages/rcf-lite/src/preflight/scanner.js` (Track A pre-flight service
|
|
7
|
+
// candidate scanner, verification-integrity-cluster-spec §4.3 / §8.3);
|
|
8
|
+
// the scanner walks PRD + TAD prose, calls `matchServiceSignals(text)`,
|
|
9
|
+
// and packages each hit into a `preFlightConfig.servicesInScope[]`
|
|
10
|
+
// candidate with per-match provenance (doc path, section anchor, the
|
|
11
|
+
// matched phrase and its category).
|
|
12
|
+
// - `packages/rcf-lite/src/verify/provision/index.js` (verify's provisioning
|
|
13
|
+
// heuristic, spec §5.3 / §8.4); verify's existing `SERVICE_PATTERNS`
|
|
14
|
+
// regex is superseded by this shared set so the "email channel" miss
|
|
15
|
+
// that motivated the whole cluster (d-2026-07-30-142 investigation)
|
|
16
|
+
// cannot recur from regex drift between packages.
|
|
17
|
+
//
|
|
18
|
+
// The pattern set is data, not a classifier: consumers own the "what field
|
|
19
|
+
// the signal came from" packaging and any interactive-session grouping.
|
|
20
|
+
// This module exposes the categorised keyword sets plus a convenience
|
|
21
|
+
// matcher that returns per-hit provenance in document order.
|
|
22
|
+
//
|
|
23
|
+
// Match rules per spec §4.4:
|
|
24
|
+
// - Case-insensitive.
|
|
25
|
+
// - Word-boundary anchored on the leading side (the matcher wraps every
|
|
26
|
+
// pattern as `\b<pattern>`, mirroring the sibling ui-shapes / req-shapes
|
|
27
|
+
// idiom); stem-suffixed forms (`emails`, `webhooks`, `providers`) match
|
|
28
|
+
// by design.
|
|
29
|
+
// - Any single match triggers a service candidate. False positives are
|
|
30
|
+
// cheap (the interactive session dismisses them); false negatives are
|
|
31
|
+
// the failure mode this seed set exists to close (`email channel` was
|
|
32
|
+
// missed by the old regex — d-142 rec 2).
|
|
33
|
+
// - Categorisation is non-exclusive: the same phrase (`provider`, `api`)
|
|
34
|
+
// legitimately fires under multiple categories, and every hit is
|
|
35
|
+
// recorded independently so the scanner can present them as separate
|
|
36
|
+
// candidates for the operator to merge or dismiss.
|
|
37
|
+
//
|
|
38
|
+
// Interpretation of the spec's "× verbs" shorthand (`storageCdn`, `llmAi`,
|
|
39
|
+
// `analyticsTelemetry`, `featureFlags`): where §4.4 writes "× verbs"
|
|
40
|
+
// without enumerating a per-category verb list, the shared
|
|
41
|
+
// `GENERIC_SERVICE_VERBS` set below is used — the union of every explicit
|
|
42
|
+
// verb spelled out by the four categories that DO enumerate (email,
|
|
43
|
+
// payment, sms/voice, auth, search). This is the sole interpretation call
|
|
44
|
+
// in transcribing §4.4; every token, vendor and explicitly-enumerated verb
|
|
45
|
+
// is ship-verbatim.
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @typedef {'emailDelivery'|'payment'|'smsVoice'|'auth'|'storageCdn'|'llmAi'|'analyticsTelemetry'|'featureFlags'|'search'} ServiceCategory
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @typedef {'token'|'verb'|'vendor'} ServiceSignalRole
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @typedef {object} ServiceSignalMatch
|
|
57
|
+
* @property {ServiceCategory} category
|
|
58
|
+
* @property {ServiceSignalRole} role
|
|
59
|
+
* @property {string} pattern source pattern string that matched
|
|
60
|
+
* @property {string} match exact substring the pattern captured
|
|
61
|
+
* @property {number} index offset of the match inside `text`
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
// Canonical category id list. Order is spec §4.4 document order so the
|
|
65
|
+
// scanner's presentation to the operator stays stable across runs.
|
|
66
|
+
export const SERVICE_CATEGORY_KEYS = Object.freeze([
|
|
67
|
+
'emailDelivery',
|
|
68
|
+
'payment',
|
|
69
|
+
'smsVoice',
|
|
70
|
+
'auth',
|
|
71
|
+
'storageCdn',
|
|
72
|
+
'llmAi',
|
|
73
|
+
'analyticsTelemetry',
|
|
74
|
+
'featureFlags',
|
|
75
|
+
'search',
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
// Shared verb set used by every category whose spec §4.4 entry writes
|
|
79
|
+
// "× verbs" without an explicit enumeration. Composed as the union of every
|
|
80
|
+
// verb the spec DOES enumerate, in first-appearance document order.
|
|
81
|
+
// Exported for tests and for consumers that want to reason about the
|
|
82
|
+
// interpretation call directly.
|
|
83
|
+
export const GENERIC_SERVICE_VERBS = Object.freeze([
|
|
84
|
+
'send',
|
|
85
|
+
'deliver',
|
|
86
|
+
'dispatch',
|
|
87
|
+
'notify',
|
|
88
|
+
'route',
|
|
89
|
+
'channel',
|
|
90
|
+
'provider',
|
|
91
|
+
'processor',
|
|
92
|
+
'gateway',
|
|
93
|
+
'api',
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The nine-category third-party-service seed set, transcribed verbatim from
|
|
98
|
+
* verification-integrity-cluster-spec §4.4. Each category exposes three
|
|
99
|
+
* frozen arrays:
|
|
100
|
+
*
|
|
101
|
+
* - `tokens` the generic terms that name the service concept.
|
|
102
|
+
* - `verbs` the actions that co-occur with tokens in service-mentioning
|
|
103
|
+
* prose; equals `GENERIC_SERVICE_VERBS` for the four
|
|
104
|
+
* categories whose spec entry writes "× verbs".
|
|
105
|
+
* - `vendors` the explicit vendor names spec §4.4 lists separately (empty
|
|
106
|
+
* for `emailDelivery`, whose §4.4 entry lists vendors inline
|
|
107
|
+
* with tokens — this transcription preserves that spec shape
|
|
108
|
+
* rather than re-sorting vendors out of the token list).
|
|
109
|
+
*
|
|
110
|
+
* Every array is `Object.freeze`d so downstream classifiers cannot mutate
|
|
111
|
+
* the shared source.
|
|
112
|
+
*
|
|
113
|
+
* @type {Readonly<Record<ServiceCategory, {tokens: readonly string[], verbs: readonly string[], vendors: readonly string[]}>>}
|
|
114
|
+
*/
|
|
115
|
+
export const SERVICE_SEED_PATTERNS_V1 = Object.freeze({
|
|
116
|
+
// Email delivery §4.4: `email|smtp|mailer|resend|sendgrid|mailgun|postmark|ses\b|mailtrap|sparkpost`
|
|
117
|
+
// × `send|deliver|dispatch|notify|route|channel|provider`
|
|
118
|
+
// (Vendor names live inline in the token list per §4.4; `vendors` empty
|
|
119
|
+
// here preserves that spec transcription.)
|
|
120
|
+
emailDelivery: Object.freeze({
|
|
121
|
+
tokens: Object.freeze([
|
|
122
|
+
'email',
|
|
123
|
+
'smtp',
|
|
124
|
+
'mailer',
|
|
125
|
+
'resend',
|
|
126
|
+
'sendgrid',
|
|
127
|
+
'mailgun',
|
|
128
|
+
'postmark',
|
|
129
|
+
'ses\\b',
|
|
130
|
+
'mailtrap',
|
|
131
|
+
'sparkpost',
|
|
132
|
+
]),
|
|
133
|
+
verbs: Object.freeze([
|
|
134
|
+
'send',
|
|
135
|
+
'deliver',
|
|
136
|
+
'dispatch',
|
|
137
|
+
'notify',
|
|
138
|
+
'route',
|
|
139
|
+
'channel',
|
|
140
|
+
'provider',
|
|
141
|
+
]),
|
|
142
|
+
vendors: Object.freeze([]),
|
|
143
|
+
}),
|
|
144
|
+
// Payment §4.4: `payment|checkout|billing|invoice|charge|payout|refund`
|
|
145
|
+
// × `provider|processor|gateway|api`;
|
|
146
|
+
// explicit vendors `stripe|braintree|adyen|paypal|square`.
|
|
147
|
+
payment: Object.freeze({
|
|
148
|
+
tokens: Object.freeze([
|
|
149
|
+
'payment',
|
|
150
|
+
'checkout',
|
|
151
|
+
'billing',
|
|
152
|
+
'invoice',
|
|
153
|
+
'charge',
|
|
154
|
+
'payout',
|
|
155
|
+
'refund',
|
|
156
|
+
]),
|
|
157
|
+
verbs: Object.freeze([
|
|
158
|
+
'provider',
|
|
159
|
+
'processor',
|
|
160
|
+
'gateway',
|
|
161
|
+
'api',
|
|
162
|
+
]),
|
|
163
|
+
vendors: Object.freeze([
|
|
164
|
+
'stripe',
|
|
165
|
+
'braintree',
|
|
166
|
+
'adyen',
|
|
167
|
+
'paypal',
|
|
168
|
+
'square',
|
|
169
|
+
]),
|
|
170
|
+
}),
|
|
171
|
+
// SMS/voice §4.4: `sms|voice|call|dial` × `provider|api`;
|
|
172
|
+
// explicit vendors `twilio|vonage|messagebird`.
|
|
173
|
+
smsVoice: Object.freeze({
|
|
174
|
+
tokens: Object.freeze([
|
|
175
|
+
'sms',
|
|
176
|
+
'voice',
|
|
177
|
+
'call',
|
|
178
|
+
'dial',
|
|
179
|
+
]),
|
|
180
|
+
verbs: Object.freeze([
|
|
181
|
+
'provider',
|
|
182
|
+
'api',
|
|
183
|
+
]),
|
|
184
|
+
vendors: Object.freeze([
|
|
185
|
+
'twilio',
|
|
186
|
+
'vonage',
|
|
187
|
+
'messagebird',
|
|
188
|
+
]),
|
|
189
|
+
}),
|
|
190
|
+
// Auth (identity provider) §4.4: `oauth|oidc|saml|sso|social login`
|
|
191
|
+
// × `provider`;
|
|
192
|
+
// explicit vendors `auth0|clerk|okta|firebase auth|cognito|supabase auth`.
|
|
193
|
+
auth: Object.freeze({
|
|
194
|
+
tokens: Object.freeze([
|
|
195
|
+
'oauth',
|
|
196
|
+
'oidc',
|
|
197
|
+
'saml',
|
|
198
|
+
'sso',
|
|
199
|
+
'social login',
|
|
200
|
+
]),
|
|
201
|
+
verbs: Object.freeze([
|
|
202
|
+
'provider',
|
|
203
|
+
]),
|
|
204
|
+
vendors: Object.freeze([
|
|
205
|
+
'auth0',
|
|
206
|
+
'clerk',
|
|
207
|
+
'okta',
|
|
208
|
+
'firebase auth',
|
|
209
|
+
'cognito',
|
|
210
|
+
'supabase auth',
|
|
211
|
+
]),
|
|
212
|
+
}),
|
|
213
|
+
// Storage/CDN §4.4: `s3|r2|gcs|blob storage|cdn` × verbs;
|
|
214
|
+
// explicit vendors `cloudflare r2|aws s3|gcs|azure blob`.
|
|
215
|
+
storageCdn: Object.freeze({
|
|
216
|
+
tokens: Object.freeze([
|
|
217
|
+
's3',
|
|
218
|
+
'r2',
|
|
219
|
+
'gcs',
|
|
220
|
+
'blob storage',
|
|
221
|
+
'cdn',
|
|
222
|
+
]),
|
|
223
|
+
verbs: GENERIC_SERVICE_VERBS,
|
|
224
|
+
vendors: Object.freeze([
|
|
225
|
+
'cloudflare r2',
|
|
226
|
+
'aws s3',
|
|
227
|
+
'gcs',
|
|
228
|
+
'azure blob',
|
|
229
|
+
]),
|
|
230
|
+
}),
|
|
231
|
+
// LLM/AI §4.4: `llm|inference|completion|embedding|model api` × verbs;
|
|
232
|
+
// explicit vendors `anthropic|openai|gemini|mistral|cohere|replicate`.
|
|
233
|
+
llmAi: Object.freeze({
|
|
234
|
+
tokens: Object.freeze([
|
|
235
|
+
'llm',
|
|
236
|
+
'inference',
|
|
237
|
+
'completion',
|
|
238
|
+
'embedding',
|
|
239
|
+
'model api',
|
|
240
|
+
]),
|
|
241
|
+
verbs: GENERIC_SERVICE_VERBS,
|
|
242
|
+
vendors: Object.freeze([
|
|
243
|
+
'anthropic',
|
|
244
|
+
'openai',
|
|
245
|
+
'gemini',
|
|
246
|
+
'mistral',
|
|
247
|
+
'cohere',
|
|
248
|
+
'replicate',
|
|
249
|
+
]),
|
|
250
|
+
}),
|
|
251
|
+
// Analytics/telemetry §4.4: `analytics|telemetry|event tracking` × verbs;
|
|
252
|
+
// explicit vendors `posthog|segment|mixpanel|amplitude|datadog`.
|
|
253
|
+
analyticsTelemetry: Object.freeze({
|
|
254
|
+
tokens: Object.freeze([
|
|
255
|
+
'analytics',
|
|
256
|
+
'telemetry',
|
|
257
|
+
'event tracking',
|
|
258
|
+
]),
|
|
259
|
+
verbs: GENERIC_SERVICE_VERBS,
|
|
260
|
+
vendors: Object.freeze([
|
|
261
|
+
'posthog',
|
|
262
|
+
'segment',
|
|
263
|
+
'mixpanel',
|
|
264
|
+
'amplitude',
|
|
265
|
+
'datadog',
|
|
266
|
+
]),
|
|
267
|
+
}),
|
|
268
|
+
// Feature flags §4.4: `feature flag|toggle` × verbs;
|
|
269
|
+
// explicit vendors `launchdarkly|growthbook|flagsmith`.
|
|
270
|
+
featureFlags: Object.freeze({
|
|
271
|
+
tokens: Object.freeze([
|
|
272
|
+
'feature flag',
|
|
273
|
+
'toggle',
|
|
274
|
+
]),
|
|
275
|
+
verbs: GENERIC_SERVICE_VERBS,
|
|
276
|
+
vendors: Object.freeze([
|
|
277
|
+
'launchdarkly',
|
|
278
|
+
'growthbook',
|
|
279
|
+
'flagsmith',
|
|
280
|
+
]),
|
|
281
|
+
}),
|
|
282
|
+
// Search §4.4: `search|index` × `api|provider`;
|
|
283
|
+
// explicit vendors `algolia|elasticsearch|typesense|meilisearch`.
|
|
284
|
+
search: Object.freeze({
|
|
285
|
+
tokens: Object.freeze([
|
|
286
|
+
'search',
|
|
287
|
+
'index',
|
|
288
|
+
]),
|
|
289
|
+
verbs: Object.freeze([
|
|
290
|
+
'api',
|
|
291
|
+
'provider',
|
|
292
|
+
]),
|
|
293
|
+
vendors: Object.freeze([
|
|
294
|
+
'algolia',
|
|
295
|
+
'elasticsearch',
|
|
296
|
+
'typesense',
|
|
297
|
+
'meilisearch',
|
|
298
|
+
]),
|
|
299
|
+
}),
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Role iteration order for the matcher; consumers reading per-hit
|
|
303
|
+
// provenance can rely on this being stable across releases.
|
|
304
|
+
const ROLES = /** @type {const} */ (['tokens', 'verbs', 'vendors']);
|
|
305
|
+
const ROLE_TO_SINGULAR = /** @type {const} */ ({
|
|
306
|
+
tokens: 'token',
|
|
307
|
+
verbs: 'verb',
|
|
308
|
+
vendors: 'vendor',
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Scan `text` against `SERVICE_SEED_PATTERNS_V1` and return every match, in
|
|
313
|
+
* document order, tagged with the category and role (`token` / `verb` /
|
|
314
|
+
* `vendor`) that produced it. A single `text` may yield many hits — bare
|
|
315
|
+
* `provider` legitimately fires under email, payment, sms/voice, auth and
|
|
316
|
+
* search — and each hit is recorded separately so the scanner can present
|
|
317
|
+
* them as candidates for the operator to merge or dismiss (spec §4.5).
|
|
318
|
+
*
|
|
319
|
+
* @param {string} text
|
|
320
|
+
* @returns {ServiceSignalMatch[]}
|
|
321
|
+
*/
|
|
322
|
+
export function matchServiceSignals(text) {
|
|
323
|
+
if (typeof text !== 'string' || text.length === 0) return [];
|
|
324
|
+
/** @type {ServiceSignalMatch[]} */
|
|
325
|
+
const results = [];
|
|
326
|
+
for (const category of SERVICE_CATEGORY_KEYS) {
|
|
327
|
+
const bucket = SERVICE_SEED_PATTERNS_V1[category];
|
|
328
|
+
for (const roleKey of ROLES) {
|
|
329
|
+
const patterns = bucket[roleKey];
|
|
330
|
+
const role = ROLE_TO_SINGULAR[roleKey];
|
|
331
|
+
for (const pattern of patterns) {
|
|
332
|
+
// Leading `\b` prevents matching inside a longer word (e.g. `sms`
|
|
333
|
+
// inside `smsapi` would still be blocked by `\bsms`, but `smsapi`
|
|
334
|
+
// as a bare id has no leading word char anyway; the anchor blocks
|
|
335
|
+
// things like `damsms`). NO trailing `\b`, so stem-suffixed forms
|
|
336
|
+
// (`emails`, `providers`, `webhooks`, `deliveries`) still match.
|
|
337
|
+
// Spec §4.4 over-collects by design.
|
|
338
|
+
const re = new RegExp(`\\b${pattern}`, 'gi');
|
|
339
|
+
let m;
|
|
340
|
+
while ((m = re.exec(text)) !== null) {
|
|
341
|
+
results.push({
|
|
342
|
+
category,
|
|
343
|
+
role,
|
|
344
|
+
pattern,
|
|
345
|
+
match: m[0],
|
|
346
|
+
index: m.index,
|
|
347
|
+
});
|
|
348
|
+
if (m.index === re.lastIndex) re.lastIndex += 1; // zero-width safety
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// Document-order stable sort (matches accumulate category-by-category,
|
|
354
|
+
// role-by-role, then pattern-by-pattern; this sort restores overall
|
|
355
|
+
// offset order so the scanner walks hits left-to-right through the doc).
|
|
356
|
+
results.sort((a, b) => a.index - b.index);
|
|
357
|
+
return results;
|
|
358
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// UI seed pattern set - single source of truth for UI-shape detection.
|
|
2
|
+
//
|
|
3
|
+
// Consumed by:
|
|
4
|
+
// - `packages/rcf-lite/src/ui-detection/classifier.js` (Track B UI-bearing FBS
|
|
5
|
+
// classifier, ui-design-gate-0.7.0-spec §4.3).
|
|
6
|
+
// - `packages/rcf-lite/src/core/patterns/req-shapes.js` (Track C+D REQ-shape
|
|
7
|
+
// classifier's `webUi` shape, elicitation-and-playbook-hardening-0.7.0
|
|
8
|
+
// spec §4.3, which requires this file as its single source of truth for
|
|
9
|
+
// the Web UI signal).
|
|
10
|
+
//
|
|
11
|
+
// The pattern set is data, not a classifier: consumers own the "what field
|
|
12
|
+
// did the signal come from" packaging and the shape of the returned
|
|
13
|
+
// `signals[]` array on the FBS / REQ `*Classification` block. This module
|
|
14
|
+
// exposes the categorised keyword sets plus a convenience matcher.
|
|
15
|
+
//
|
|
16
|
+
// Match rules per spec §4.3:
|
|
17
|
+
// - Case-insensitive.
|
|
18
|
+
// - Word-boundary anchored so `nav` does not match inside `navigate` when
|
|
19
|
+
// the word-boundary hits (regex `\b` handles that).
|
|
20
|
+
// - Any single match triggers the classifier's `ui` verdict (match count
|
|
21
|
+
// is not a threshold; false positives are cheap, false negatives are the
|
|
22
|
+
// failure mode).
|
|
23
|
+
// - Sentences containing an excluded phrase from `UI_EXCLUDED_PHRASES`
|
|
24
|
+
// have their signals suppressed (avoids the API-endpoint / CLI /
|
|
25
|
+
// JSON-response false positive class); ambiguous cases favour `ui`.
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {'explicitUiNouns'|'htmlRenderVerbs'|'responseShapeSignals'|'authFlowSignals'|'accessibilitySignals'} UiSeedCategory
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {object} UiSignalMatch
|
|
33
|
+
* @property {UiSeedCategory} category
|
|
34
|
+
* @property {string} pattern the source pattern string that matched
|
|
35
|
+
* @property {string} match the exact substring the pattern captured
|
|
36
|
+
* @property {number} index offset of the match inside `text`
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
// Categorised keyword sets. Each entry is a regex source string (no flags,
|
|
40
|
+
// no anchors); the matcher below wraps them with `\b<source>\b` and the
|
|
41
|
+
// `gi` flags at scan time so consumers cannot accidentally use the raw
|
|
42
|
+
// source without word boundaries.
|
|
43
|
+
export const UI_SEED_PATTERNS_V1 = Object.freeze({
|
|
44
|
+
explicitUiNouns: Object.freeze([
|
|
45
|
+
'web ui', 'browser', 'page', 'dashboard', 'admin', 'screen', 'form',
|
|
46
|
+
'route', 'nav', 'navigation', 'layout', 'style', 'theme',
|
|
47
|
+
'dark mode', 'light mode',
|
|
48
|
+
'component', 'button', 'table', 'badge', 'header', 'footer',
|
|
49
|
+
'sidebar', 'modal', 'tooltip',
|
|
50
|
+
]),
|
|
51
|
+
htmlRenderVerbs: Object.freeze([
|
|
52
|
+
'render(?:ed|s|ing)?',
|
|
53
|
+
'display(?:ed|s|ing)?',
|
|
54
|
+
'shows?',
|
|
55
|
+
'visible',
|
|
56
|
+
'see(?:s|ing)?',
|
|
57
|
+
'click(?:s|ed|ing)?',
|
|
58
|
+
'tap(?:s|ped|ping)?',
|
|
59
|
+
]),
|
|
60
|
+
responseShapeSignals: Object.freeze([
|
|
61
|
+
'HTML',
|
|
62
|
+
'markup',
|
|
63
|
+
'template',
|
|
64
|
+
'view',
|
|
65
|
+
'serve.*page',
|
|
66
|
+
'content-type.*text/html',
|
|
67
|
+
]),
|
|
68
|
+
authFlowSignals: Object.freeze([
|
|
69
|
+
'login form',
|
|
70
|
+
'sign(?:-|\\s)?in page',
|
|
71
|
+
'logout button',
|
|
72
|
+
'password reset page',
|
|
73
|
+
'magic(?:-|\\s)?link (?:page|landing)',
|
|
74
|
+
]),
|
|
75
|
+
accessibilitySignals: Object.freeze([
|
|
76
|
+
'contrast',
|
|
77
|
+
'WCAG',
|
|
78
|
+
'screen reader',
|
|
79
|
+
'keyboard nav',
|
|
80
|
+
'focus ring',
|
|
81
|
+
'aria-',
|
|
82
|
+
]),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Sentences containing any of these phrases have their UI signals
|
|
86
|
+
// suppressed. Guards against the API-endpoint / CLI / JSON-response false
|
|
87
|
+
// positive class named in spec §4.3.
|
|
88
|
+
export const UI_EXCLUDED_PHRASES = Object.freeze([
|
|
89
|
+
'API endpoint',
|
|
90
|
+
'CLI command',
|
|
91
|
+
'shell prompt',
|
|
92
|
+
'JSON response',
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
// Split `text` into rough sentence-shaped chunks. Boundary is `.!?` plus a
|
|
96
|
+
// line break; keeps the trailing terminator with the sentence so length /
|
|
97
|
+
// index stays useful for downstream reporting. Deliberately unaware of
|
|
98
|
+
// English abbreviations (`e.g.`, `i.e.`, `Dr.`); the excluded-phrase filter
|
|
99
|
+
// is intended as a coarse noise gate, not a linguistic parser.
|
|
100
|
+
function splitIntoSentences(text) {
|
|
101
|
+
if (typeof text !== 'string' || text.length === 0) return [];
|
|
102
|
+
const chunks = [];
|
|
103
|
+
const re = /[^.!?\n]+[.!?\n]?/g;
|
|
104
|
+
let match;
|
|
105
|
+
while ((match = re.exec(text)) !== null) {
|
|
106
|
+
const chunk = match[0];
|
|
107
|
+
const start = match.index;
|
|
108
|
+
const end = start + chunk.length;
|
|
109
|
+
const trimmed = chunk.trim();
|
|
110
|
+
if (trimmed.length > 0) chunks.push({ text: chunk, start, end });
|
|
111
|
+
}
|
|
112
|
+
return chunks;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function sentenceContainsExcluded(sentence) {
|
|
116
|
+
const lower = sentence.toLowerCase();
|
|
117
|
+
for (const phrase of UI_EXCLUDED_PHRASES) {
|
|
118
|
+
if (lower.includes(phrase.toLowerCase())) return true;
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Scan `text` against `UI_SEED_PATTERNS_V1` and return every match, in
|
|
125
|
+
* document order, with the pattern that produced it. Sentences containing
|
|
126
|
+
* any `UI_EXCLUDED_PHRASES` phrase are skipped in full per spec §4.3.
|
|
127
|
+
*
|
|
128
|
+
* @param {string} text
|
|
129
|
+
* @returns {UiSignalMatch[]}
|
|
130
|
+
*/
|
|
131
|
+
export function matchUiSignals(text) {
|
|
132
|
+
if (typeof text !== 'string' || text.length === 0) return [];
|
|
133
|
+
/** @type {UiSignalMatch[]} */
|
|
134
|
+
const results = [];
|
|
135
|
+
const sentences = splitIntoSentences(text);
|
|
136
|
+
// Fall back to the whole text as a single "sentence" when the regex
|
|
137
|
+
// parse produced nothing (e.g. a text without terminators).
|
|
138
|
+
if (sentences.length === 0) sentences.push({ text, start: 0, end: text.length });
|
|
139
|
+
for (const sentence of sentences) {
|
|
140
|
+
if (sentenceContainsExcluded(sentence.text)) continue;
|
|
141
|
+
for (const category of Object.keys(UI_SEED_PATTERNS_V1)) {
|
|
142
|
+
for (const pattern of UI_SEED_PATTERNS_V1[category]) {
|
|
143
|
+
// Leading `\b` prevents matching inside a longer word (e.g. `nav`
|
|
144
|
+
// inside `navigate`); NO trailing `\b`, so stem-suffixed forms
|
|
145
|
+
// (`shows`, `dashboards`, `delivers`) still match. Spec §4.3
|
|
146
|
+
// over-collects by design; false-positive cost is one operator
|
|
147
|
+
// override, false-negative cost is a dated UI shipping unnoticed.
|
|
148
|
+
const re = new RegExp(`\\b${pattern}`, 'gi');
|
|
149
|
+
let m;
|
|
150
|
+
while ((m = re.exec(sentence.text)) !== null) {
|
|
151
|
+
results.push({
|
|
152
|
+
category,
|
|
153
|
+
pattern,
|
|
154
|
+
match: m[0],
|
|
155
|
+
index: sentence.start + m.index,
|
|
156
|
+
});
|
|
157
|
+
if (m.index === re.lastIndex) re.lastIndex += 1; // safety for zero-width
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Document-order stable sort (matches happen sentence-by-sentence, then
|
|
163
|
+
// pattern-by-pattern; sort restores overall offset order).
|
|
164
|
+
results.sort((a, b) => a.index - b.index);
|
|
165
|
+
return results;
|
|
166
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// Phase 10 (X2 CodeNode bridge): Code Node working-tree resolution.
|
|
2
|
+
//
|
|
3
|
+
// This is the STALENESS DETECTOR - X2's headline advantage over sidecar
|
|
4
|
+
// approaches (S5/S6; testPointer was the same shape until tp-resolve.js
|
|
5
|
+
// gave it its own working-tree check for the test axis, w-2026-07-28-005).
|
|
6
|
+
// Every CN declares a `path` (file-level
|
|
7
|
+
// "src/foo.js" or symbol-level "src/foo.js#symbol"). This module checks
|
|
8
|
+
// each path against the CHECKED-OUT WORKING TREE and reports the CNs whose
|
|
9
|
+
// target no longer resolves. A rename/move/deletion that a sidecar map
|
|
10
|
+
// would silently carry as a dead pointer surfaces here as a `staleCode`
|
|
11
|
+
// error at `rcf validate` time.
|
|
12
|
+
//
|
|
13
|
+
// Determinism (spec D6/D13 hold): file existence is `fs.stat`; symbol
|
|
14
|
+
// presence is a fixed set of declaration-anchor regexes (function / class /
|
|
15
|
+
// const|let|var / method-at-line-start / object-field). No LLM, no
|
|
16
|
+
// semantic judgement, no parsing beyond regex. Fully reproducible given a
|
|
17
|
+
// working tree.
|
|
18
|
+
//
|
|
19
|
+
// HONEST LIMITATION (documented in docs/ and the PoC report): the symbol
|
|
20
|
+
// check proves a DECLARATION with that name exists - not that it still does
|
|
21
|
+
// what the AC says. A symbol renamed is caught; a symbol whose body was
|
|
22
|
+
// gutted but whose name survived is NOT caught (semantic drift is out of
|
|
23
|
+
// reach for a deterministic check, out of scope per D13). File-level CNs
|
|
24
|
+
// cannot detect intra-file symbol moves at all. A symbol moved to another
|
|
25
|
+
// file while a same-file namesake declaration survives can false-clean
|
|
26
|
+
// (the anchor scan has no lexical scope awareness).
|
|
27
|
+
|
|
28
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
29
|
+
import { join } from 'node:path';
|
|
30
|
+
|
|
31
|
+
import { rcfError } from '../errors/index.js';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Split a CN path into its file part and optional symbol part.
|
|
35
|
+
* @param {string} cnPath e.g. "src/store/validator.js#getAjv"
|
|
36
|
+
* @returns {{ file: string, symbol: string | null }}
|
|
37
|
+
*/
|
|
38
|
+
export function splitCnPath(cnPath) {
|
|
39
|
+
const hash = cnPath.indexOf('#');
|
|
40
|
+
if (hash < 0) return { file: cnPath, symbol: null };
|
|
41
|
+
return { file: cnPath.slice(0, hash), symbol: cnPath.slice(hash + 1) };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Build the deterministic declaration-anchor matchers for a symbol name.
|
|
46
|
+
* A symbol is considered PRESENT if any anchor matches the file text.
|
|
47
|
+
* @param {string} symbol
|
|
48
|
+
* @returns {RegExp[]}
|
|
49
|
+
*/
|
|
50
|
+
function anchorsFor(symbol) {
|
|
51
|
+
// Escape regex metacharacters in the symbol (identifiers won't normally
|
|
52
|
+
// carry any, but `$` is a legal JS identifier char and a regex metachar).
|
|
53
|
+
const s = symbol.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
54
|
+
return [
|
|
55
|
+
new RegExp(`\\b(?:async\\s+)?function\\s*\\*?\\s+${s}\\b`), // function decl
|
|
56
|
+
new RegExp(`\\bclass\\s+${s}\\b`), // class decl
|
|
57
|
+
new RegExp(`\\b(?:const|let|var)\\s+${s}\\b`), // binding decl
|
|
58
|
+
new RegExp(`^\\s*(?:async\\s+)?(?:static\\s+)?(?:get\\s+|set\\s+)?${s}\\s*\\(`, 'm'), // method/def at line start
|
|
59
|
+
new RegExp(`^\\s*${s}\\s*[:=]`, 'm'), // object-literal / class-field form
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Check every Code Node in the tree against the working tree. Returns a
|
|
65
|
+
* list of structured `staleCode` errors (empty when all CN paths resolve).
|
|
66
|
+
* A per-file read cache avoids re-reading a file that multiple symbol-level
|
|
67
|
+
* CNs anchor into (spec D8).
|
|
68
|
+
*
|
|
69
|
+
* @param {object} args
|
|
70
|
+
* @param {string} args.projectRoot - absolute path to project root
|
|
71
|
+
* @param {object} args.tree - walker TreeModel (carries tree.codeNodes)
|
|
72
|
+
* @returns {Promise<import('../errors/index.js').RcfError[]>}
|
|
73
|
+
*/
|
|
74
|
+
export async function checkCodeNodeResolution({ projectRoot, tree }) {
|
|
75
|
+
/** @type {import('../errors/index.js').RcfError[]} */
|
|
76
|
+
const errors = [];
|
|
77
|
+
const fileCache = new Map();
|
|
78
|
+
|
|
79
|
+
const readFileCached = async (absFile) => {
|
|
80
|
+
if (fileCache.has(absFile)) return fileCache.get(absFile);
|
|
81
|
+
let text = null;
|
|
82
|
+
try {
|
|
83
|
+
text = await readFile(absFile, 'utf8');
|
|
84
|
+
} catch {
|
|
85
|
+
text = null;
|
|
86
|
+
}
|
|
87
|
+
fileCache.set(absFile, text);
|
|
88
|
+
return text;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
for (const cn of tree.codeNodes ?? []) {
|
|
92
|
+
const cnPath = cn.path ?? '';
|
|
93
|
+
const relFile = `rcf/code-nodes/${(cn.cnId ?? '').toLowerCase()}.json`;
|
|
94
|
+
const { file, symbol } = splitCnPath(cnPath);
|
|
95
|
+
const absFile = join(projectRoot, file);
|
|
96
|
+
|
|
97
|
+
// 1. File existence.
|
|
98
|
+
let fileOk = false;
|
|
99
|
+
try {
|
|
100
|
+
const st = await stat(absFile);
|
|
101
|
+
fileOk = st.isFile();
|
|
102
|
+
} catch {
|
|
103
|
+
fileOk = false;
|
|
104
|
+
}
|
|
105
|
+
if (!fileOk) {
|
|
106
|
+
errors.push(rcfError({
|
|
107
|
+
kind: 'staleCode',
|
|
108
|
+
message: `CN ${cn.cnId} path is stale: file ${file} does not exist in the working tree`,
|
|
109
|
+
documentId: cn.cnId,
|
|
110
|
+
filePath: relFile,
|
|
111
|
+
field: 'path',
|
|
112
|
+
rule: 'fileResolves',
|
|
113
|
+
}));
|
|
114
|
+
continue; // no point checking the symbol in a missing file
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 2. Symbol presence (deterministic anchor scan).
|
|
118
|
+
if (symbol) {
|
|
119
|
+
const text = await readFileCached(absFile);
|
|
120
|
+
const present = text != null && anchorsFor(symbol).some((re) => re.test(text));
|
|
121
|
+
if (!present) {
|
|
122
|
+
errors.push(rcfError({
|
|
123
|
+
kind: 'staleCode',
|
|
124
|
+
message: `CN ${cn.cnId} path is stale: symbol '${symbol}' not found in ${file} (renamed, removed, or moved to another file)`,
|
|
125
|
+
documentId: cn.cnId,
|
|
126
|
+
filePath: relFile,
|
|
127
|
+
field: 'path',
|
|
128
|
+
rule: 'symbolResolves',
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return errors;
|
|
134
|
+
}
|