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,228 @@
|
|
|
1
|
+
// MCP protocol lifecycle (Phase 7 §D3 / §D18). Pure protocol shell:
|
|
2
|
+
// initialize / initialized handshake, single version constant, ping,
|
|
3
|
+
// request dispatch to injected handlers, tolerated notifications,
|
|
4
|
+
// JSON-RPC error mapping, EOF shutdown. Knows nothing about RCF -
|
|
5
|
+
// domain behaviour arrives through the `handlers` map.
|
|
6
|
+
//
|
|
7
|
+
// Implemented against the pinned MCP revision 2025-11-25 (D3): the
|
|
8
|
+
// server responds to `initialize` with its own protocol version
|
|
9
|
+
// regardless of the client's requested version (the spec-sanctioned
|
|
10
|
+
// server behaviour under version negotiation - the server MUST respond
|
|
11
|
+
// with a version it supports; the client disconnects if incompatible).
|
|
12
|
+
|
|
13
|
+
import { attachLineReader, writeMessage } from './framing.js';
|
|
14
|
+
|
|
15
|
+
export const PROTOCOL_VERSION = '2025-11-25';
|
|
16
|
+
|
|
17
|
+
// JSON-RPC 2.0 error codes used by this server. -32002 is the MCP
|
|
18
|
+
// resources error for "resource not found" (2025-11-25 resources §
|
|
19
|
+
// error handling).
|
|
20
|
+
export const PARSE_ERROR = -32700;
|
|
21
|
+
export const INVALID_REQUEST = -32600;
|
|
22
|
+
export const METHOD_NOT_FOUND = -32601;
|
|
23
|
+
export const INVALID_PARAMS = -32602;
|
|
24
|
+
export const INTERNAL_ERROR = -32603;
|
|
25
|
+
export const RESOURCE_NOT_FOUND = -32002;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Structured protocol error a handler can throw to produce a JSON-RPC
|
|
29
|
+
* error response (as opposed to a tool execution error, which is a
|
|
30
|
+
* successful result carrying `isError: true` - see map-errors.js).
|
|
31
|
+
*/
|
|
32
|
+
export class JsonRpcError extends Error {
|
|
33
|
+
/**
|
|
34
|
+
* @param {number} code
|
|
35
|
+
* @param {string} message
|
|
36
|
+
* @param {object} [data]
|
|
37
|
+
*/
|
|
38
|
+
constructor(code, message, data) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = 'JsonRpcError';
|
|
41
|
+
this.code = code;
|
|
42
|
+
this.data = data;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create an MCP server core. Transport-free: `handleMessage` takes one
|
|
48
|
+
* parsed JSON-RPC message and resolves to a response object (requests)
|
|
49
|
+
* or null (notifications / ignorable traffic). Wire it to real streams
|
|
50
|
+
* with `serveStreams`.
|
|
51
|
+
*
|
|
52
|
+
* @param {object} opts
|
|
53
|
+
* @param {{name: string, version: string}} opts.serverInfo
|
|
54
|
+
* @param {string} [opts.instructions]
|
|
55
|
+
* @param {object} opts.capabilities - e.g. {tools: {}, resources: {}, prompts: {}}
|
|
56
|
+
* @param {Record<string, (params: object) => Promise<object>>} opts.handlers
|
|
57
|
+
* - method name to handler; handler returns the JSON-RPC `result`
|
|
58
|
+
* @param {{info: (line: string) => void, error: (line: string) => void}} [opts.log]
|
|
59
|
+
* @returns {{
|
|
60
|
+
* handleMessage: (message: unknown) => Promise<object | null>,
|
|
61
|
+
* parseErrorResponse: () => object,
|
|
62
|
+
* initialized: () => boolean,
|
|
63
|
+
* }}
|
|
64
|
+
*/
|
|
65
|
+
export function createMcpServer({ serverInfo, instructions, capabilities, handlers, log }) {
|
|
66
|
+
const logger = log ?? { info: () => {}, error: () => {} };
|
|
67
|
+
let receivedInitialized = false;
|
|
68
|
+
|
|
69
|
+
const errorResponse = (id, code, message, data) => ({
|
|
70
|
+
jsonrpc: '2.0',
|
|
71
|
+
id: id ?? null,
|
|
72
|
+
error: { code, message, ...(data !== undefined ? { data } : {}) },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const resultResponse = (id, result) => ({ jsonrpc: '2.0', id, result });
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {unknown} message
|
|
79
|
+
* @returns {Promise<object | null>}
|
|
80
|
+
*/
|
|
81
|
+
async function handleMessage(message) {
|
|
82
|
+
if (!message || typeof message !== 'object' || Array.isArray(message)) {
|
|
83
|
+
return errorResponse(null, INVALID_REQUEST, 'Invalid request: expected a JSON-RPC object');
|
|
84
|
+
}
|
|
85
|
+
const msg = /** @type {{jsonrpc?: unknown, id?: unknown, method?: unknown, params?: unknown, result?: unknown, error?: unknown}} */ (message);
|
|
86
|
+
|
|
87
|
+
// A message with no method but a result / error member is a
|
|
88
|
+
// response. This server never sends requests, so any incoming
|
|
89
|
+
// response is stale or misaddressed traffic - tolerate and ignore.
|
|
90
|
+
if (msg.method === undefined && (msg.result !== undefined || msg.error !== undefined)) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (typeof msg.method !== 'string' || msg.jsonrpc !== '2.0') {
|
|
95
|
+
return errorResponse(
|
|
96
|
+
validId(msg.id) ? msg.id : null,
|
|
97
|
+
INVALID_REQUEST,
|
|
98
|
+
'Invalid request: missing jsonrpc "2.0" or method',
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const isNotification = msg.id === undefined;
|
|
103
|
+
const params = (msg.params && typeof msg.params === 'object') ? msg.params : {};
|
|
104
|
+
|
|
105
|
+
if (isNotification) {
|
|
106
|
+
// D18: unknown notifications (including notifications/cancelled)
|
|
107
|
+
// are tolerated and ignored - every tool is synchronous and
|
|
108
|
+
// fast, so cancellation has nothing to cancel.
|
|
109
|
+
if (msg.method === 'notifications/initialized') {
|
|
110
|
+
receivedInitialized = true;
|
|
111
|
+
logger.info('client initialized');
|
|
112
|
+
} else {
|
|
113
|
+
logger.info(`ignoring notification ${msg.method}`);
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!validId(msg.id)) {
|
|
119
|
+
return errorResponse(null, INVALID_REQUEST, 'Invalid request: id must be a string or number');
|
|
120
|
+
}
|
|
121
|
+
const id = msg.id;
|
|
122
|
+
|
|
123
|
+
if (msg.method === 'initialize') {
|
|
124
|
+
return resultResponse(id, {
|
|
125
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
126
|
+
capabilities,
|
|
127
|
+
serverInfo,
|
|
128
|
+
...(instructions ? { instructions } : {}),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
if (msg.method === 'ping') {
|
|
132
|
+
return resultResponse(id, {});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const handler = handlers[msg.method];
|
|
136
|
+
if (!handler) {
|
|
137
|
+
return errorResponse(id, METHOD_NOT_FOUND, `Method not found: ${msg.method}`);
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
const result = await handler(params);
|
|
141
|
+
return resultResponse(id, result);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
if (err instanceof JsonRpcError) {
|
|
144
|
+
return errorResponse(id, err.code, err.message, err.data);
|
|
145
|
+
}
|
|
146
|
+
// Unexpected failure: message to the client, full stack to
|
|
147
|
+
// stderr only - never into model context (D10).
|
|
148
|
+
const e = /** @type {Error} */ (err);
|
|
149
|
+
logger.error(`[rcf mcp] unexpected failure in ${msg.method}: ${e.message}\n${e.stack ?? ''}`);
|
|
150
|
+
return errorResponse(id, INTERNAL_ERROR, `Internal error: ${e.message}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
handleMessage,
|
|
156
|
+
parseErrorResponse: () => errorResponse(null, PARSE_ERROR, 'Parse error'),
|
|
157
|
+
initialized: () => receivedInitialized,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function validId(id) {
|
|
162
|
+
return typeof id === 'string' || (typeof id === 'number' && Number.isFinite(id));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Wire a server core to real (or injected) streams. Messages are
|
|
167
|
+
* processed strictly in arrival order over a serial promise chain so
|
|
168
|
+
* responses hit stdout in request order and every fresh tree walk is
|
|
169
|
+
* serialised (D14). Resolves 0 on input EOF - the 2025-11-25 stdio
|
|
170
|
+
* termination path (client closes stdin; server exits).
|
|
171
|
+
*
|
|
172
|
+
* @param {ReturnType<typeof createMcpServer>} server
|
|
173
|
+
* @param {object} opts
|
|
174
|
+
* @param {NodeJS.ReadableStream} opts.input
|
|
175
|
+
* @param {NodeJS.WritableStream} opts.output
|
|
176
|
+
* @param {{info: (line: string) => void, error: (line: string) => void}} [opts.log]
|
|
177
|
+
* @returns {{ done: Promise<number>, stop: () => void }}
|
|
178
|
+
*/
|
|
179
|
+
export function serveStreams(server, { input, output, log }) {
|
|
180
|
+
const logger = log ?? { info: () => {}, error: () => {} };
|
|
181
|
+
let queue = Promise.resolve();
|
|
182
|
+
let resolveDone;
|
|
183
|
+
const done = new Promise((resolve) => { resolveDone = resolve; });
|
|
184
|
+
let stopped = false;
|
|
185
|
+
|
|
186
|
+
const send = (response) => {
|
|
187
|
+
if (stopped || !response) return;
|
|
188
|
+
try {
|
|
189
|
+
writeMessage(output, response);
|
|
190
|
+
} catch (err) {
|
|
191
|
+
// Embedded-newline refusal or a closed pipe. Log and drop the
|
|
192
|
+
// message rather than corrupting the stdout framing.
|
|
193
|
+
logger.error(`[rcf mcp] write failed: ${/** @type {Error} */ (err).message}`);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const reader = attachLineReader(input, {
|
|
198
|
+
onMessage: (message) => {
|
|
199
|
+
queue = queue.then(async () => {
|
|
200
|
+
if (stopped) return;
|
|
201
|
+
const response = await server.handleMessage(message);
|
|
202
|
+
send(response);
|
|
203
|
+
});
|
|
204
|
+
},
|
|
205
|
+
onParseError: (rawLine) => {
|
|
206
|
+
queue = queue.then(() => {
|
|
207
|
+
if (stopped) return;
|
|
208
|
+
logger.info(`parse error on input line (${rawLine.length} chars)`);
|
|
209
|
+
send(server.parseErrorResponse());
|
|
210
|
+
});
|
|
211
|
+
},
|
|
212
|
+
onEnd: () => {
|
|
213
|
+
// Drain in-flight work, then resolve the exit code.
|
|
214
|
+
queue = queue.then(() => {
|
|
215
|
+
stop();
|
|
216
|
+
});
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
function stop() {
|
|
221
|
+
if (stopped) return;
|
|
222
|
+
stopped = true;
|
|
223
|
+
reader.detach();
|
|
224
|
+
resolveDone(0);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return { done, stop };
|
|
228
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// Register-regression canary pattern set - single source of truth for the
|
|
2
|
+
// five graded dimensions defined in Track D §7.2 of the
|
|
3
|
+
// elicitation-and-playbook-hardening-0.7.0 spec.
|
|
4
|
+
//
|
|
5
|
+
// Consumed by `packages/rcf-lite/scripts/canary-register.js` (or wherever the
|
|
6
|
+
// build package eventually places its CI runner) which replays the fixture
|
|
7
|
+
// pack at `./fixtures/register-canary/*.json` against the shipping build,
|
|
8
|
+
// captures the first response, and runs each dimension against the
|
|
9
|
+
// response body only (system prompts, tool-use, and code blocks are
|
|
10
|
+
// excluded from the grep before evaluation).
|
|
11
|
+
//
|
|
12
|
+
// Each dimension is `{ patterns: RegExp[], evaluate(context) => Result }`
|
|
13
|
+
// where `Result` is `{ verdict: 'pass'|'fail', matches: string[], ...extra }`.
|
|
14
|
+
// Dimensions that need more than pattern data (e.g. `redundantPermissionAsk`
|
|
15
|
+
// needs the fixture's `grantedPermissions[]`; `wordCountBudget` needs the
|
|
16
|
+
// numeric target) accept a `context` argument.
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {'internalRuleCitation'|'unglossedJargon'|'redundantPermissionAsk'|'bypassOffer'|'wordCountBudget'} CanaryDimension
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {object} DimensionEvaluation
|
|
24
|
+
* @property {'pass'|'fail'} verdict
|
|
25
|
+
* @property {string[]} [matches]
|
|
26
|
+
* @property {number} [target] populated by `wordCountBudget`
|
|
27
|
+
* @property {number} [actual] populated by `wordCountBudget`
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// -- internalRuleCitation ---------------------------------------------------
|
|
31
|
+
// Any citation of an internal rule (RULE 1, CLAUDE.md, __NOTES__, AGENTS.md,
|
|
32
|
+
// or "per rule X") is a fail. Rationale: the 0.5.1 first response cited
|
|
33
|
+
// "RULE 1" and Baz's Entry-1 response was "why would I care about CLAUDE
|
|
34
|
+
// RULE 1?". Internal rule references leaking to the operator is the sin.
|
|
35
|
+
const INTERNAL_RULE_CITATION_PATTERNS = Object.freeze([
|
|
36
|
+
/\bRULE\s?\d+\b/gi,
|
|
37
|
+
/\bCLAUDE\.md\b/g,
|
|
38
|
+
/\b__NOTES__\b/g,
|
|
39
|
+
/\bAGENTS\.md\b/g,
|
|
40
|
+
/\bper rule\s+\d+\b/gi,
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
// -- unglossedJargon --------------------------------------------------------
|
|
44
|
+
// Jargon tokens that need to be glossed. A token followed by a full stop,
|
|
45
|
+
// line break, or clause-ending punctuation with no parenthesised definition
|
|
46
|
+
// or "which is" clause within a short window counts as unglossed.
|
|
47
|
+
const JARGON_TOKENS = Object.freeze([
|
|
48
|
+
'FBS', 'AC', 'TAC', 'ADR', 'BS', 'PRD',
|
|
49
|
+
'docs-review gate', 'RCF chain',
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
// "Immediately followed by an explanation" - a parenthesised gloss, an
|
|
53
|
+
// em-dash / colon / "which is" definer clause within the next ~80 chars.
|
|
54
|
+
// Deliberately generous window; unglossed_jargon is a heuristic and false
|
|
55
|
+
// positives fail the canary and force a wording fix.
|
|
56
|
+
const GLOSS_LOOKAHEAD = /(?:\s*\(|\s*[-:]\s|\s+which\s+is\b|\s+meaning\b|\s+i\.e\.)/i;
|
|
57
|
+
|
|
58
|
+
function isJargonGlossed(text, tokenEnd) {
|
|
59
|
+
const window = text.slice(tokenEnd, tokenEnd + 80);
|
|
60
|
+
return GLOSS_LOOKAHEAD.test(window);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// -- redundantPermissionAsk -------------------------------------------------
|
|
64
|
+
// "Would/shall/do you want I/me to X" where X is a permission verb.
|
|
65
|
+
// Fails ONLY when the fixture prompt has already granted the corresponding
|
|
66
|
+
// permission. `PERMISSION_VERBS_BY_GRANT` maps a grantedPermission id to
|
|
67
|
+
// the verbs it authorises; a match on any verb whose grant is present
|
|
68
|
+
// fails the dimension.
|
|
69
|
+
const REDUNDANT_ASK_PATTERN = /\b(would|shall|do you want)\s+(?:i|me\s+to)\s+(create|add|push|open|initialise|initialize|commit|merge)\b/gi;
|
|
70
|
+
|
|
71
|
+
// Grant id -> verbs it authorises. Verbs listed here are lower-case; the
|
|
72
|
+
// matcher lowercases the captured verb before lookup.
|
|
73
|
+
const PERMISSION_VERBS_BY_GRANT = Object.freeze({
|
|
74
|
+
gitPush: ['push', 'commit', 'merge'],
|
|
75
|
+
githubRepoManagement: ['create', 'open', 'initialise', 'initialize', 'merge'],
|
|
76
|
+
actionsWorkflowManagement: ['create', 'add', 'open'],
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
function verbIsGranted(verb, grantedPermissions) {
|
|
80
|
+
const needle = verb.toLowerCase();
|
|
81
|
+
for (const grant of grantedPermissions ?? []) {
|
|
82
|
+
const verbs = PERMISSION_VERBS_BY_GRANT[grant];
|
|
83
|
+
if (verbs && verbs.includes(needle)) return true;
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// -- bypassOffer ------------------------------------------------------------
|
|
89
|
+
// Never-skip-RCF invariant enforced at the response level. Any offer of a
|
|
90
|
+
// shortcut / bypass fails; the OFFER is the defect (§8, "@dave note this"
|
|
91
|
+
// ruling). Deliberately broad; false positives fail the canary and force
|
|
92
|
+
// a wording fix in the guidance surface, which is the correct cost balance.
|
|
93
|
+
const BYPASS_OFFER_PATTERNS = Object.freeze([
|
|
94
|
+
/\bskip\s+(?:the\s+)?RCF\b/gi,
|
|
95
|
+
/\bshortcut\b/gi,
|
|
96
|
+
/\bjust\s+push\s+.*(?:fix|patch)\b/gi,
|
|
97
|
+
/\bfast[-\s]path\b/gi,
|
|
98
|
+
/\b(?:rather|instead)\b[^.\n]*\bskip\b/gi,
|
|
99
|
+
/\b(?:want|prefer)\b[^.\n]*\bbypass\b/gi,
|
|
100
|
+
/\b(?:if\s+you|shall\s+i)\b[^.\n]*\bjust\b/gi,
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
// -- wordCountBudget --------------------------------------------------------
|
|
104
|
+
// Word count of the response body, excluding code blocks. Default target
|
|
105
|
+
// is 200; per-fixture override supplied by the caller.
|
|
106
|
+
export const DEFAULT_WORD_COUNT_BUDGET = 200;
|
|
107
|
+
|
|
108
|
+
function stripCodeBlocks(text) {
|
|
109
|
+
// Fenced code blocks (```...```) and single-line back-ticked spans.
|
|
110
|
+
return text
|
|
111
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
112
|
+
.replace(/`[^`\n]*`/g, ' ');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function countWords(text) {
|
|
116
|
+
const stripped = stripCodeBlocks(text).trim();
|
|
117
|
+
if (stripped.length === 0) return 0;
|
|
118
|
+
return stripped.split(/\s+/).length;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// -- Dimension evaluators ---------------------------------------------------
|
|
122
|
+
|
|
123
|
+
/** @type {Record<CanaryDimension, (context: { responseBody: string, grantedPermissions?: string[], wordCountBudget?: number }) => DimensionEvaluation>} */
|
|
124
|
+
export const REGISTER_CANARY_DIMENSIONS_V1 = Object.freeze({
|
|
125
|
+
internalRuleCitation({ responseBody }) {
|
|
126
|
+
const matches = [];
|
|
127
|
+
for (const re of INTERNAL_RULE_CITATION_PATTERNS) {
|
|
128
|
+
const local = new RegExp(re.source, re.flags);
|
|
129
|
+
let m;
|
|
130
|
+
while ((m = local.exec(responseBody)) !== null) matches.push(m[0]);
|
|
131
|
+
}
|
|
132
|
+
return { verdict: matches.length === 0 ? 'pass' : 'fail', matches };
|
|
133
|
+
},
|
|
134
|
+
unglossedJargon({ responseBody }) {
|
|
135
|
+
const matches = [];
|
|
136
|
+
for (const token of JARGON_TOKENS) {
|
|
137
|
+
const re = new RegExp(`\\b${token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g');
|
|
138
|
+
let m;
|
|
139
|
+
while ((m = re.exec(responseBody)) !== null) {
|
|
140
|
+
const end = m.index + m[0].length;
|
|
141
|
+
if (!isJargonGlossed(responseBody, end)) matches.push(m[0]);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return { verdict: matches.length === 0 ? 'pass' : 'fail', matches };
|
|
145
|
+
},
|
|
146
|
+
redundantPermissionAsk({ responseBody, grantedPermissions }) {
|
|
147
|
+
const matches = [];
|
|
148
|
+
const re = new RegExp(REDUNDANT_ASK_PATTERN.source, REDUNDANT_ASK_PATTERN.flags);
|
|
149
|
+
let m;
|
|
150
|
+
while ((m = re.exec(responseBody)) !== null) {
|
|
151
|
+
const verb = m[2];
|
|
152
|
+
if (verbIsGranted(verb, grantedPermissions)) matches.push(m[0]);
|
|
153
|
+
}
|
|
154
|
+
return { verdict: matches.length === 0 ? 'pass' : 'fail', matches };
|
|
155
|
+
},
|
|
156
|
+
bypassOffer({ responseBody }) {
|
|
157
|
+
const matches = [];
|
|
158
|
+
for (const re of BYPASS_OFFER_PATTERNS) {
|
|
159
|
+
const local = new RegExp(re.source, re.flags);
|
|
160
|
+
let m;
|
|
161
|
+
while ((m = local.exec(responseBody)) !== null) matches.push(m[0]);
|
|
162
|
+
}
|
|
163
|
+
return { verdict: matches.length === 0 ? 'pass' : 'fail', matches };
|
|
164
|
+
},
|
|
165
|
+
wordCountBudget({ responseBody, wordCountBudget }) {
|
|
166
|
+
const target = typeof wordCountBudget === 'number' ? wordCountBudget : DEFAULT_WORD_COUNT_BUDGET;
|
|
167
|
+
const actual = countWords(responseBody);
|
|
168
|
+
return {
|
|
169
|
+
verdict: actual <= target ? 'pass' : 'fail',
|
|
170
|
+
target,
|
|
171
|
+
actual,
|
|
172
|
+
matches: [],
|
|
173
|
+
};
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Ordered list of dimension keys. Downstream consumers iterate this to
|
|
179
|
+
* assemble the `grades` object on a `registerCanary[]` record in a
|
|
180
|
+
* deterministic order.
|
|
181
|
+
*/
|
|
182
|
+
export const CANARY_DIMENSION_KEYS = Object.freeze([
|
|
183
|
+
'internalRuleCitation',
|
|
184
|
+
'unglossedJargon',
|
|
185
|
+
'redundantPermissionAsk',
|
|
186
|
+
'bypassOffer',
|
|
187
|
+
'wordCountBudget',
|
|
188
|
+
]);
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Aggregate: run every dimension and collapse to a single verdict. Any
|
|
192
|
+
* `fail` on any dimension fails the top-level verdict.
|
|
193
|
+
*
|
|
194
|
+
* @param {object} context
|
|
195
|
+
* @param {string} context.responseBody
|
|
196
|
+
* @param {string[]} [context.grantedPermissions]
|
|
197
|
+
* @param {number} [context.wordCountBudget]
|
|
198
|
+
* @returns {{ verdict: 'pass'|'fail', grades: Record<CanaryDimension, DimensionEvaluation> }}
|
|
199
|
+
*/
|
|
200
|
+
export function gradeResponse(context) {
|
|
201
|
+
/** @type {Record<CanaryDimension, DimensionEvaluation>} */
|
|
202
|
+
const grades = {};
|
|
203
|
+
let aggregate = 'pass';
|
|
204
|
+
for (const key of CANARY_DIMENSION_KEYS) {
|
|
205
|
+
grades[key] = REGISTER_CANARY_DIMENSIONS_V1[key](context);
|
|
206
|
+
if (grades[key].verdict === 'fail') aggregate = 'fail';
|
|
207
|
+
}
|
|
208
|
+
return { verdict: aggregate, grades };
|
|
209
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// REQ-shape seed pattern set - single source of truth for REQ-shape
|
|
2
|
+
// classification.
|
|
3
|
+
//
|
|
4
|
+
// Consumed by `packages/rcf-lite/src/req-detection/classifier.js` (Track C+D
|
|
5
|
+
// classifier, elicitation-and-playbook-hardening-0.7.0-spec §4.3), which
|
|
6
|
+
// walks each in-scope field (`title`, `description`, `rationale` on the
|
|
7
|
+
// REQ, plus parent PRD `intent` / `problem` as fallback context), matches
|
|
8
|
+
// against every shape's pattern list, and records the matched signals on
|
|
9
|
+
// `req.shapeClassification.signals[]`.
|
|
10
|
+
//
|
|
11
|
+
// The `webUi` shape imports `UI_SEED_PATTERNS_V1` from
|
|
12
|
+
// `./ui-shapes.js` (Track B) so a UI signal is defined in ONE place across
|
|
13
|
+
// build's UI-bearing FBS classifier, Track C+D's Web UI REQ classifier and
|
|
14
|
+
// Track B's browser-verification invariants. When the UI pattern set moves,
|
|
15
|
+
// consumers move together.
|
|
16
|
+
//
|
|
17
|
+
// Match rules per spec §4.3:
|
|
18
|
+
// - Case-insensitive.
|
|
19
|
+
// - Word-boundary anchored on plain-letter patterns (the matcher wraps
|
|
20
|
+
// each pattern with `\b<pattern>\b`).
|
|
21
|
+
// - `shapes` are non-exclusive: a REQ can carry `[webUi, auth]` (e.g.
|
|
22
|
+
// "sign-in page"). One shape per pattern list; the classifier layers
|
|
23
|
+
// the matches from all lists.
|
|
24
|
+
|
|
25
|
+
import { UI_SEED_PATTERNS_V1 } from './ui-shapes.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {'webUi'|'httpApi'|'auth'|'persistence'|'notifications'} ReqShape
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {object} ReqShapeSignalMatch
|
|
33
|
+
* @property {ReqShape} shape
|
|
34
|
+
* @property {string} pattern source pattern string
|
|
35
|
+
* @property {string} match exact substring matched
|
|
36
|
+
* @property {number} index offset inside `text`
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
// Canonical shape id list. `none` is a legitimate REQ-level verdict but
|
|
40
|
+
// carries no pattern set (the classifier records `shapes: []` and derives
|
|
41
|
+
// `none` from the empty list); it is included here so downstream schemas
|
|
42
|
+
// can reference `SHAPE_KEYS_WITH_NONE` when they need the full enum.
|
|
43
|
+
export const SHAPE_KEYS = Object.freeze(['webUi', 'httpApi', 'auth', 'persistence', 'notifications']);
|
|
44
|
+
export const SHAPE_KEYS_WITH_NONE = Object.freeze([...SHAPE_KEYS, 'none']);
|
|
45
|
+
|
|
46
|
+
// Flatten the UI pattern set into a single array of pattern strings so
|
|
47
|
+
// the shape-classifier can use the same matcher for every shape. This
|
|
48
|
+
// preserves single-source-of-truth (both files reference
|
|
49
|
+
// `UI_SEED_PATTERNS_V1`); if Track B evolves the categories the flatten
|
|
50
|
+
// picks them up automatically.
|
|
51
|
+
function flattenUiSeedPatterns() {
|
|
52
|
+
const patterns = [];
|
|
53
|
+
for (const category of Object.keys(UI_SEED_PATTERNS_V1)) {
|
|
54
|
+
for (const pattern of UI_SEED_PATTERNS_V1[category]) patterns.push(pattern);
|
|
55
|
+
}
|
|
56
|
+
return patterns;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Non-webUi shape patterns. Each entry is a regex source string; the
|
|
60
|
+
// matcher wraps with `\b<pattern>\b` and the `gi` flags. Escaping caveats
|
|
61
|
+
// for maintainers: `\\s` in a source string escapes to `\s` in the regex;
|
|
62
|
+
// literal `.` inside a pattern (`serve.*page`, `webhook.*deliver`) is
|
|
63
|
+
// intentional wildcard, not a literal full stop.
|
|
64
|
+
const HTTP_API_PATTERNS = Object.freeze([
|
|
65
|
+
'api', 'endpoint', 'REST', 'HTTP method',
|
|
66
|
+
'POST', 'GET', 'PUT', 'DELETE',
|
|
67
|
+
'webhook',
|
|
68
|
+
'json (?:response|body|payload)',
|
|
69
|
+
'rate limit', 'request body',
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
const AUTH_PATTERNS = Object.freeze([
|
|
73
|
+
'login',
|
|
74
|
+
'sign(?:-|\\s)?in',
|
|
75
|
+
'sign(?:-|\\s)?up',
|
|
76
|
+
'logout',
|
|
77
|
+
'session',
|
|
78
|
+
'token',
|
|
79
|
+
'credential',
|
|
80
|
+
'password',
|
|
81
|
+
'magic(?:-|\\s)?link',
|
|
82
|
+
'api key',
|
|
83
|
+
'authoriz(?:e|ation)',
|
|
84
|
+
'permission',
|
|
85
|
+
'role',
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
const PERSISTENCE_PATTERNS = Object.freeze([
|
|
89
|
+
'store',
|
|
90
|
+
'storage',
|
|
91
|
+
'persist(?:ed|ent|ing)?',
|
|
92
|
+
'database',
|
|
93
|
+
'table',
|
|
94
|
+
'schema',
|
|
95
|
+
'migration',
|
|
96
|
+
'backup',
|
|
97
|
+
'restore',
|
|
98
|
+
'survives? (?:a )?restart',
|
|
99
|
+
'durable',
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
const NOTIFICATIONS_PATTERNS = Object.freeze([
|
|
103
|
+
'notify',
|
|
104
|
+
'notification',
|
|
105
|
+
'alert',
|
|
106
|
+
'email',
|
|
107
|
+
'SMS',
|
|
108
|
+
'push notification',
|
|
109
|
+
'slack',
|
|
110
|
+
'webhook.*deliver',
|
|
111
|
+
'reminder',
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The full REQ-shape seed pattern set. `webUi` is composed from the Track B
|
|
116
|
+
* UI seed set at module load; the other shapes are declared inline. The
|
|
117
|
+
* object is frozen so consumers cannot mutate the shared source.
|
|
118
|
+
*
|
|
119
|
+
* @type {Readonly<Record<ReqShape, readonly string[]>>}
|
|
120
|
+
*/
|
|
121
|
+
export const REQ_SHAPE_PATTERNS_V1 = Object.freeze({
|
|
122
|
+
webUi: Object.freeze(flattenUiSeedPatterns()),
|
|
123
|
+
httpApi: HTTP_API_PATTERNS,
|
|
124
|
+
auth: AUTH_PATTERNS,
|
|
125
|
+
persistence: PERSISTENCE_PATTERNS,
|
|
126
|
+
notifications: NOTIFICATIONS_PATTERNS,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Scan `text` against every REQ shape's pattern list and return each match
|
|
131
|
+
* tagged with the shape it fed into. Sentence-level exclusion is NOT
|
|
132
|
+
* applied here (unlike `matchUiSignals`, which filters API/CLI/JSON-response
|
|
133
|
+
* false positives inside the UI classifier); the REQ-shape classifier
|
|
134
|
+
* deliberately over-collects and defers the operator override per spec
|
|
135
|
+
* §4.5.
|
|
136
|
+
*
|
|
137
|
+
* @param {string} text
|
|
138
|
+
* @returns {ReqShapeSignalMatch[]}
|
|
139
|
+
*/
|
|
140
|
+
export function matchReqShapeSignals(text) {
|
|
141
|
+
if (typeof text !== 'string' || text.length === 0) return [];
|
|
142
|
+
/** @type {ReqShapeSignalMatch[]} */
|
|
143
|
+
const results = [];
|
|
144
|
+
for (const shape of Object.keys(REQ_SHAPE_PATTERNS_V1)) {
|
|
145
|
+
for (const pattern of REQ_SHAPE_PATTERNS_V1[shape]) {
|
|
146
|
+
// Leading `\b` only; stem-suffixed forms (`stores`, `logins`,
|
|
147
|
+
// `webhook delivers`) match by design per the over-collect posture.
|
|
148
|
+
const re = new RegExp(`\\b${pattern}`, 'gi');
|
|
149
|
+
let m;
|
|
150
|
+
while ((m = re.exec(text)) !== null) {
|
|
151
|
+
results.push({ shape, pattern, match: m[0], index: m.index });
|
|
152
|
+
if (m.index === re.lastIndex) re.lastIndex += 1;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
results.sort((a, b) => a.index - b.index);
|
|
157
|
+
return results;
|
|
158
|
+
}
|