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.
Files changed (349) hide show
  1. package/CHANGELOG.md +344 -0
  2. package/LICENSE +202 -0
  3. package/README.md +92 -4
  4. package/bin/rcf-verify.js +122 -0
  5. package/bin/rcf.js +174 -0
  6. package/bin/view-supervisor-child.mjs +14 -0
  7. package/fixtures/canary-manifest.json +103 -0
  8. package/guidance/README.md +25 -0
  9. package/guidance/build-cycle-playbook.md +492 -0
  10. package/guidance/build-cycle.md +40 -0
  11. package/guidance/document-model.md +45 -0
  12. package/guidance/elicitation-playbook.md +442 -0
  13. package/guidance/harness-template.md +246 -0
  14. package/guidance/managed/README.md +63 -0
  15. package/guidance/managed/agent-instructions-block.hash +1 -0
  16. package/guidance/managed/agent-instructions-block.md +220 -0
  17. package/guidance/managed/legacy-fragment-hashes.json +22 -0
  18. package/guidance/manifest.json +21 -0
  19. package/guidance/overview.md +48 -0
  20. package/guidance/persona-programme.md +44 -0
  21. package/package.json +71 -8
  22. package/rcf/adrs/adr-001.json +25 -0
  23. package/rcf/adrs/adr-002.json +25 -0
  24. package/rcf/adrs/adr-003.json +25 -0
  25. package/rcf/adrs/adr-004.json +20 -0
  26. package/rcf/adrs/adr-005.json +20 -0
  27. package/rcf/adrs/adr-006.json +25 -0
  28. package/rcf/adrs/adr-007.json +25 -0
  29. package/rcf/adrs/adr-008.json +25 -0
  30. package/rcf/adrs/adr-009.json +25 -0
  31. package/rcf/build-sequence.json +11 -0
  32. package/rcf/code-nodes/cn-001.json +14 -0
  33. package/rcf/code-nodes/cn-002.json +14 -0
  34. package/rcf/code-nodes/cn-003.json +15 -0
  35. package/rcf/code-nodes/cn-004.json +17 -0
  36. package/rcf/code-nodes/cn-005.json +19 -0
  37. package/rcf/code-nodes/cn-006.json +17 -0
  38. package/rcf/code-nodes/cn-007.json +16 -0
  39. package/rcf/code-nodes/cn-008.json +19 -0
  40. package/rcf/code-nodes/cn-009.json +16 -0
  41. package/rcf/code-nodes/cn-010.json +18 -0
  42. package/rcf/code-nodes/cn-011.json +18 -0
  43. package/rcf/code-nodes/cn-012.json +18 -0
  44. package/rcf/code-nodes/cn-013.json +19 -0
  45. package/rcf/code-nodes/cn-014.json +16 -0
  46. package/rcf/code-nodes/cn-015.json +16 -0
  47. package/rcf/code-nodes/cn-016.json +16 -0
  48. package/rcf/code-nodes/cn-017.json +16 -0
  49. package/rcf/code-nodes/cn-018.json +15 -0
  50. package/rcf/code-nodes/cn-019.json +16 -0
  51. package/rcf/code-nodes/cn-020.json +16 -0
  52. package/rcf/code-nodes/cn-021.json +16 -0
  53. package/rcf/code-nodes/cn-022.json +16 -0
  54. package/rcf/code-nodes/cn-023.json +16 -0
  55. package/rcf/code-nodes/cn-024.json +16 -0
  56. package/rcf/code-nodes/cn-025.json +16 -0
  57. package/rcf/code-nodes/cn-026.json +16 -0
  58. package/rcf/code-nodes/cn-027.json +16 -0
  59. package/rcf/code-nodes/cn-028.json +16 -0
  60. package/rcf/code-nodes/cn-029.json +19 -0
  61. package/rcf/code-nodes/cn-030.json +14 -0
  62. package/rcf/code-nodes/cn-031.json +14 -0
  63. package/rcf/code-nodes/cn-032.json +14 -0
  64. package/rcf/code-nodes/cn-033.json +14 -0
  65. package/rcf/code-nodes/cn-034.json +14 -0
  66. package/rcf/code-nodes/cn-035.json +14 -0
  67. package/rcf/code-nodes/cn-036.json +14 -0
  68. package/rcf/code-nodes/cn-037.json +14 -0
  69. package/rcf/code-nodes/cn-038.json +14 -0
  70. package/rcf/code-nodes/cn-039.json +14 -0
  71. package/rcf/code-nodes/cn-040.json +14 -0
  72. package/rcf/code-nodes/cn-041.json +14 -0
  73. package/rcf/code-nodes/cn-042.json +14 -0
  74. package/rcf/code-nodes/cn-043.json +14 -0
  75. package/rcf/code-nodes/cn-044.json +14 -0
  76. package/rcf/code-nodes/cn-045.json +14 -0
  77. package/rcf/code-nodes/cn-046.json +14 -0
  78. package/rcf/code-nodes/cn-047.json +14 -0
  79. package/rcf/code-nodes/cn-048.json +14 -0
  80. package/rcf/code-nodes/cn-049.json +14 -0
  81. package/rcf/code-nodes/cn-050.json +14 -0
  82. package/rcf/code-nodes/cn-051.json +14 -0
  83. package/rcf/code-nodes/cn-052.json +14 -0
  84. package/rcf/code-nodes/cn-053.json +14 -0
  85. package/rcf/code-nodes/cn-054.json +14 -0
  86. package/rcf/code-nodes/cn-055.json +14 -0
  87. package/rcf/code-nodes/cn-056.json +14 -0
  88. package/rcf/code-nodes/cn-057.json +14 -0
  89. package/rcf/fbs/fbs-001.json +49 -0
  90. package/rcf/fbs/fbs-002.json +42 -0
  91. package/rcf/fbs/fbs-003.json +37 -0
  92. package/rcf/fbs/fbs-004.json +39 -0
  93. package/rcf/fbs/fbs-005.json +38 -0
  94. package/rcf/fbs/fbs-006.json +48 -0
  95. package/rcf/fbs/fbs-007.json +39 -0
  96. package/rcf/fbs/fbs-008.json +40 -0
  97. package/rcf/fbs/fbs-009.json +36 -0
  98. package/rcf/fbs/fbs-010.json +41 -0
  99. package/rcf/fbs/fbs-011.json +36 -0
  100. package/rcf/fbs/fbs-012.json +46 -0
  101. package/rcf/fbs/fbs-013.json +42 -0
  102. package/rcf/fbs/fbs-014.json +49 -0
  103. package/rcf/fbs/fbs-015.json +32 -0
  104. package/rcf/manifest.json +17 -0
  105. package/rcf/prd.json +47 -0
  106. package/rcf/requirements/req-001.json +19 -0
  107. package/rcf/requirements/req-002.json +19 -0
  108. package/rcf/requirements/req-003.json +19 -0
  109. package/rcf/requirements/req-004.json +19 -0
  110. package/rcf/requirements/req-005.json +19 -0
  111. package/rcf/requirements/req-006.json +19 -0
  112. package/rcf/requirements/req-007.json +19 -0
  113. package/rcf/requirements/req-008.json +19 -0
  114. package/rcf/requirements/req-009.json +19 -0
  115. package/rcf/tacs/tac-001.json +45 -0
  116. package/rcf/tacs/tac-002.json +109 -0
  117. package/rcf/tacs/tac-003.json +40 -0
  118. package/rcf/tacs/tac-004.json +51 -0
  119. package/rcf/tacs/tac-005.json +52 -0
  120. package/rcf/tacs/tac-006.json +104 -0
  121. package/rcf/tacs/tac-007.json +38 -0
  122. package/rcf/tacs/tac-008.json +51 -0
  123. package/rcf/tad.json +51 -0
  124. package/rcf/test-suites/PENDING.md +23 -0
  125. package/rcf/test-suites/ts-001.json +38 -0
  126. package/rcf/test-suites/ts-002.json +38 -0
  127. package/rcf/test-suites/ts-003.json +43 -0
  128. package/rcf/test-suites/ts-004.json +44 -0
  129. package/rcf/test-suites/ts-005.json +30 -0
  130. package/rcf/test-suites/ts-006.json +36 -0
  131. package/rcf/test-suites/ts-007.json +43 -0
  132. package/rcf/test-suites/ts-008.json +37 -0
  133. package/rcf/test-suites/ts-009.json +38 -0
  134. package/rcf/test-suites/ts-010.json +38 -0
  135. package/rcf/test-suites/ts-011.json +44 -0
  136. package/rcf/test-suites/ts-012.json +36 -0
  137. package/rcf/test-suites/ts-013.json +38 -0
  138. package/rcf/test-suites/ts-014.json +38 -0
  139. package/rcf/test-suites/ts-015.json +38 -0
  140. package/rcf/test-suites/ts-016.json +37 -0
  141. package/rcf/test-suites/ts-017.json +38 -0
  142. package/rcf/test-suites/ts-018.json +38 -0
  143. package/rcf/test-suites/ts-019.json +37 -0
  144. package/rcf/test-suites/ts-020.json +46 -0
  145. package/rcf/test-suites/ts-021.json +46 -0
  146. package/rcf/test-suites/ts-022.json +46 -0
  147. package/rcf/test-suites/ts-023.json +46 -0
  148. package/rcf/test-suites/ts-024.json +46 -0
  149. package/rcf/test-suites/ts-025.json +52 -0
  150. package/rcf/user-stories/us-101.json +40 -0
  151. package/rcf/user-stories/us-102.json +40 -0
  152. package/rcf/user-stories/us-201.json +40 -0
  153. package/rcf/user-stories/us-202.json +40 -0
  154. package/rcf/user-stories/us-203.json +32 -0
  155. package/rcf/user-stories/us-301.json +40 -0
  156. package/rcf/user-stories/us-302.json +40 -0
  157. package/rcf/user-stories/us-303.json +40 -0
  158. package/rcf/user-stories/us-304.json +40 -0
  159. package/rcf/user-stories/us-401.json +40 -0
  160. package/rcf/user-stories/us-402.json +40 -0
  161. package/rcf/user-stories/us-403.json +40 -0
  162. package/rcf/user-stories/us-501.json +40 -0
  163. package/rcf/user-stories/us-502.json +40 -0
  164. package/rcf/user-stories/us-503.json +40 -0
  165. package/rcf/user-stories/us-601.json +40 -0
  166. package/rcf/user-stories/us-602.json +40 -0
  167. package/rcf/user-stories/us-701.json +40 -0
  168. package/rcf/user-stories/us-702.json +40 -0
  169. package/rcf/user-stories/us-801.json +49 -0
  170. package/rcf/user-stories/us-802.json +49 -0
  171. package/rcf/user-stories/us-803.json +49 -0
  172. package/rcf/user-stories/us-804.json +49 -0
  173. package/rcf/user-stories/us-805.json +49 -0
  174. package/rcf/user-stories/us-901.json +40 -0
  175. package/src/.gitkeep +0 -0
  176. package/src/admissibility/enforce.js +142 -0
  177. package/src/admissibility/index.js +8 -0
  178. package/src/admissibility/markers.js +104 -0
  179. package/src/admissibility/scope-lint.js +163 -0
  180. package/src/browser-verify/auth-smoke.js +109 -0
  181. package/src/browser-verify/index.js +29 -0
  182. package/src/browser-verify/invariants.js +336 -0
  183. package/src/browser-verify/manifest-writer.js +189 -0
  184. package/src/browser-verify/runner.js +171 -0
  185. package/src/build/bundle.js +198 -0
  186. package/src/build/formatters/json.js +18 -0
  187. package/src/build/formatters/markdown.js +406 -0
  188. package/src/build/index.js +14 -0
  189. package/src/build/mark.js +177 -0
  190. package/src/build/queue.js +285 -0
  191. package/src/cli/browser-verify.js +231 -0
  192. package/src/cli/build.js +584 -0
  193. package/src/cli/coverage.js +219 -0
  194. package/src/cli/create.js +358 -0
  195. package/src/cli/delete.js +127 -0
  196. package/src/cli/design.js +353 -0
  197. package/src/cli/doctor.js +548 -0
  198. package/src/cli/fbs.js +171 -0
  199. package/src/cli/finalise.js +377 -0
  200. package/src/cli/guidance.js +206 -0
  201. package/src/cli/help.js +156 -0
  202. package/src/cli/impact.js +119 -0
  203. package/src/cli/init.js +282 -0
  204. package/src/cli/intake.js +153 -0
  205. package/src/cli/link.js +128 -0
  206. package/src/cli/mcp.js +160 -0
  207. package/src/cli/preflight.js +220 -0
  208. package/src/cli/read.js +168 -0
  209. package/src/cli/req-baseline.js +269 -0
  210. package/src/cli/req-classify.js +135 -0
  211. package/src/cli/review.js +295 -0
  212. package/src/cli/test-suite.js +221 -0
  213. package/src/cli/trace.js +172 -0
  214. package/src/cli/ui-baseline.js +292 -0
  215. package/src/cli/ui-classify.js +108 -0
  216. package/src/cli/update.js +197 -0
  217. package/src/cli/validate.js +168 -0
  218. package/src/cli/view.js +452 -0
  219. package/src/core/baseline-catalog/data/auth.json +42 -0
  220. package/src/core/baseline-catalog/data/http-api.json +42 -0
  221. package/src/core/baseline-catalog/data/notifications.json +33 -0
  222. package/src/core/baseline-catalog/data/persistence.json +33 -0
  223. package/src/core/baseline-catalog/data/web-ui.json +60 -0
  224. package/src/core/baseline-catalog/index.js +121 -0
  225. package/src/core/errors/index.js +167 -0
  226. package/src/core/fixtures/register-canary/canary-prompt-01.json +18 -0
  227. package/src/core/fixtures/register-canary/canary-prompt-02.json +21 -0
  228. package/src/core/fixtures/register-canary/canary-prompt-03.json +17 -0
  229. package/src/core/isolation/index.js +60 -0
  230. package/src/core/mcp/framing.js +103 -0
  231. package/src/core/mcp/index.js +8 -0
  232. package/src/core/mcp/server.js +228 -0
  233. package/src/core/patterns/register-canary.js +209 -0
  234. package/src/core/patterns/req-shapes.js +158 -0
  235. package/src/core/patterns/services.js +358 -0
  236. package/src/core/patterns/ui-shapes.js +166 -0
  237. package/src/core/store/cn-resolve.js +134 -0
  238. package/src/core/store/derive-deps.js +93 -0
  239. package/src/core/store/ids.js +78 -0
  240. package/src/core/store/index.js +20 -0
  241. package/src/core/store/init.js +255 -0
  242. package/src/core/store/loader.js +214 -0
  243. package/src/core/store/tp-resolve.js +176 -0
  244. package/src/core/store/validator.js +191 -0
  245. package/src/core/store/walker.js +944 -0
  246. package/src/core/store/writer.js +1879 -0
  247. package/src/design/index.js +11 -0
  248. package/src/design/writer.js +271 -0
  249. package/src/finalise/detect.js +151 -0
  250. package/src/finalise/index.js +31 -0
  251. package/src/finalise/ingest.js +160 -0
  252. package/src/finalise/install.js +119 -0
  253. package/src/finalise/ship-without-verified.js +131 -0
  254. package/src/finalise/spawn.js +84 -0
  255. package/src/intake/fidelity.js +105 -0
  256. package/src/intake/index.js +6 -0
  257. package/src/intake/manifest-writer.js +100 -0
  258. package/src/intake/orchestrator.js +138 -0
  259. package/src/intake/validate.js +80 -0
  260. package/src/mcp/map-errors.js +131 -0
  261. package/src/mcp/prompts.js +49 -0
  262. package/src/mcp/resources.js +244 -0
  263. package/src/mcp/tools.js +1212 -0
  264. package/src/preflight/design-shapes.js +185 -0
  265. package/src/preflight/index.js +90 -0
  266. package/src/preflight/manifest-writer.js +264 -0
  267. package/src/preflight/scanner.js +206 -0
  268. package/src/preflight/secrets.js +134 -0
  269. package/src/preflight/session.js +246 -0
  270. package/src/query/attestation.js +285 -0
  271. package/src/query/coverage.js +308 -0
  272. package/src/query/formatters/json.js +21 -0
  273. package/src/query/formatters/mermaid.js +209 -0
  274. package/src/query/formatters/table.js +203 -0
  275. package/src/query/impact.js +173 -0
  276. package/src/query/index.js +13 -0
  277. package/src/query/refuse-on-admissibility.js +73 -0
  278. package/src/query/trace.js +345 -0
  279. package/src/register-canary/fixture-loader.js +87 -0
  280. package/src/register-canary/index.js +10 -0
  281. package/src/register-canary/record-writer.js +132 -0
  282. package/src/register-canary/runner.js +156 -0
  283. package/src/req-baseline/gate.js +86 -0
  284. package/src/req-baseline/index.js +27 -0
  285. package/src/req-baseline/open-candidates.js +143 -0
  286. package/src/req-baseline/opt-out.js +195 -0
  287. package/src/req-baseline/sweep.js +230 -0
  288. package/src/req-detection/classifier.js +181 -0
  289. package/src/req-detection/index.js +9 -0
  290. package/src/req-detection/persist.js +55 -0
  291. package/src/review/index.js +325 -0
  292. package/src/review/mutation.js +117 -0
  293. package/src/review/ui-baseline-drift.js +138 -0
  294. package/src/ruleset/index.js +140 -0
  295. package/src/ruleset/ruleset.json +146 -0
  296. package/src/server/index.js +178 -0
  297. package/src/server/routes.js +110 -0
  298. package/src/server/sse.js +118 -0
  299. package/src/setup/agent-setup.js +362 -0
  300. package/src/setup/identity-seed.js +104 -0
  301. package/src/setup/knowledge-seed.js +123 -0
  302. package/src/setup/managed-block.js +193 -0
  303. package/src/setup/managed-gitignore.js +166 -0
  304. package/src/setup/managed-markers.js +49 -0
  305. package/src/ui-baseline/defaults.js +119 -0
  306. package/src/ui-baseline/index.js +25 -0
  307. package/src/ui-baseline/manifest-writer.js +282 -0
  308. package/src/ui-baseline/session.js +178 -0
  309. package/src/ui-detection/classifier.js +192 -0
  310. package/src/verify/chain/index.js +221 -0
  311. package/src/verify/cli/cleanup.js +61 -0
  312. package/src/verify/cli/help.js +56 -0
  313. package/src/verify/cli/mcp.js +98 -0
  314. package/src/verify/cli/provision.js +71 -0
  315. package/src/verify/cli/report.js +71 -0
  316. package/src/verify/cli/run.js +155 -0
  317. package/src/verify/engine/brief.js +87 -0
  318. package/src/verify/engine/index.js +177 -0
  319. package/src/verify/engine/launcher.js +307 -0
  320. package/src/verify/mcp/tools.js +107 -0
  321. package/src/verify/profile/index.js +146 -0
  322. package/src/verify/provision/index.js +256 -0
  323. package/src/verify/report/index.js +139 -0
  324. package/src/verify/report/renderer.js +118 -0
  325. package/src/verify/verdict/index.js +313 -0
  326. package/src/view/doc-renderers/adr.js +44 -0
  327. package/src/view/doc-renderers/build-sequence.js +40 -0
  328. package/src/view/doc-renderers/fbs.js +128 -0
  329. package/src/view/doc-renderers/helpers.js +159 -0
  330. package/src/view/doc-renderers/index.js +12 -0
  331. package/src/view/doc-renderers/prd.js +45 -0
  332. package/src/view/doc-renderers/req.js +43 -0
  333. package/src/view/doc-renderers/tac.js +38 -0
  334. package/src/view/doc-renderers/tad.js +74 -0
  335. package/src/view/doc-renderers/test-suite.js +45 -0
  336. package/src/view/doc-renderers/user-story.js +63 -0
  337. package/src/view/html-page.js +462 -0
  338. package/src/view/index.js +63 -0
  339. package/src/view/live-client.js +338 -0
  340. package/src/view/mermaid-diagram.js +178 -0
  341. package/src/view/style.css +735 -0
  342. package/src/view/tree-model.js +152 -0
  343. package/src/view/vendored/mermaid.min.js +2607 -0
  344. package/src/view-supervisor/index.js +26 -0
  345. package/src/view-supervisor/logs.js +32 -0
  346. package/src/view-supervisor/manifest-writer.js +178 -0
  347. package/src/view-supervisor/persist-until.js +85 -0
  348. package/src/view-supervisor/supervisor.js +276 -0
  349. 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
+ }