rcf-lite 0.28.2 → 0.29.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 (327) hide show
  1. package/CHANGELOG.md +317 -0
  2. package/bin/rcf.js +8 -0
  3. package/fixtures/canary-manifest.json +9 -9
  4. package/guidance/define-draft-shapes.md +89 -0
  5. package/guidance/define-intake.md +97 -0
  6. package/guidance/define-intent.md +107 -0
  7. package/guidance/discover-prototype.md +67 -0
  8. package/guidance/discovery-prototypes.md +69 -0
  9. package/guidance/harness-template.md +34 -0
  10. package/guidance/managed/agent-instructions-block.hash +1 -1
  11. package/guidance/managed/agent-instructions-block.md +34 -0
  12. package/guidance/manifest.json +9 -2
  13. package/package.json +1 -1
  14. package/rcf/adrs/adr-4120-canonical-json-hashing.json +30 -0
  15. package/rcf/adrs/adr-4121-define-sidecars.json +30 -0
  16. package/rcf/adrs/adr-4122-define-gates-blocking-and-optout.json +30 -0
  17. package/rcf/adrs/adr-4123-readiness-single-object.json +30 -0
  18. package/rcf/adrs/adr-4124-define-freeze-verb.json +30 -0
  19. package/rcf/adrs/adr-4126-persona-tagged-blockers-two-levels.json +40 -0
  20. package/rcf/adrs/adr-4127-readiness-tab-order-and-register.json +40 -0
  21. package/rcf/adrs/adr-4129-questions-harness-driven-loop.json +25 -0
  22. package/rcf/adrs/adr-4130-intake-harness-split-tool-scanned.json +20 -0
  23. package/rcf/adrs/adr-4132-prototypes-in-discovery-as-evidence.json +20 -0
  24. package/rcf/adrs/adr-4133-dag-dependency-depth-layout.json +40 -0
  25. package/rcf/adrs/adr-4134-index-json-route.json +35 -0
  26. package/rcf/adrs/adr-4135-readiness-po-register-and-adapter.json +35 -0
  27. package/rcf/adrs/adr-4136-embedding-host-fixture.json +35 -0
  28. package/rcf/code-nodes/cn-700.json +23 -0
  29. package/rcf/code-nodes/cn-701.json +14 -0
  30. package/rcf/code-nodes/cn-702.json +14 -0
  31. package/rcf/code-nodes/cn-703.json +16 -0
  32. package/rcf/code-nodes/cn-720.json +15 -0
  33. package/rcf/code-nodes/cn-721.json +15 -0
  34. package/rcf/code-nodes/cn-722.json +15 -0
  35. package/rcf/code-nodes/cn-723.json +18 -0
  36. package/rcf/code-nodes/cn-724.json +14 -0
  37. package/rcf/code-nodes/cn-725.json +37 -0
  38. package/rcf/code-nodes/cn-726.json +25 -0
  39. package/rcf/code-nodes/cn-727.json +23 -0
  40. package/rcf/code-nodes/cn-728.json +37 -0
  41. package/rcf/code-nodes/cn-729.json +26 -0
  42. package/rcf/code-nodes/cn-730.json +24 -0
  43. package/rcf/code-nodes/cn-731.json +20 -0
  44. package/rcf/code-nodes/cn-732.json +14 -0
  45. package/rcf/code-nodes/cn-733.json +14 -0
  46. package/rcf/code-nodes/cn-734.json +14 -0
  47. package/rcf/code-nodes/cn-735.json +20 -0
  48. package/rcf/code-nodes/cn-736.json +14 -0
  49. package/rcf/code-nodes/cn-737.json +14 -0
  50. package/rcf/code-nodes/cn-738.json +14 -0
  51. package/rcf/code-nodes/cn-739.json +18 -0
  52. package/rcf/code-nodes/cn-740.json +19 -0
  53. package/rcf/code-nodes/cn-741.json +16 -0
  54. package/rcf/code-nodes/cn-742.json +16 -0
  55. package/rcf/code-nodes/cn-743.json +15 -0
  56. package/rcf/code-nodes/cn-744.json +22 -0
  57. package/rcf/code-nodes/cn-745.json +16 -0
  58. package/rcf/code-nodes/cn-746.json +14 -0
  59. package/rcf/code-nodes/cn-747.json +18 -0
  60. package/rcf/code-nodes/cn-748.json +16 -0
  61. package/rcf/code-nodes/cn-749.json +16 -0
  62. package/rcf/code-nodes/cn-750.json +15 -0
  63. package/rcf/code-nodes/cn-751.json +18 -0
  64. package/rcf/code-nodes/cn-752.json +14 -0
  65. package/rcf/fbs/fbs-190.json +37 -0
  66. package/rcf/fbs/fbs-191.json +34 -0
  67. package/rcf/fbs/fbs-192.json +17 -0
  68. package/rcf/fbs/fbs-193.json +37 -0
  69. package/rcf/fbs/fbs-194.json +35 -0
  70. package/rcf/fbs/fbs-195.json +37 -0
  71. package/rcf/fbs/fbs-196.json +45 -0
  72. package/rcf/fbs/fbs-197.json +41 -0
  73. package/rcf/fbs/fbs-198.json +42 -0
  74. package/rcf/fbs/fbs-199.json +59 -0
  75. package/rcf/fbs/fbs-200.json +46 -0
  76. package/rcf/fbs/fbs-201.json +49 -0
  77. package/rcf/fbs/fbs-202.json +110 -0
  78. package/rcf/fbs/fbs-203.json +100 -0
  79. package/rcf/requirements/req-001.json +2 -1
  80. package/rcf/requirements/req-002.json +3 -2
  81. package/rcf/requirements/req-003.json +2 -1
  82. package/rcf/requirements/req-004.json +2 -1
  83. package/rcf/requirements/req-005.json +2 -1
  84. package/rcf/requirements/req-006.json +2 -1
  85. package/rcf/requirements/req-007.json +2 -1
  86. package/rcf/requirements/req-008.json +2 -1
  87. package/rcf/requirements/req-009.json +2 -1
  88. package/rcf/requirements/req-010.json +5 -2
  89. package/rcf/requirements/req-011.json +2 -1
  90. package/rcf/requirements/req-012.json +2 -1
  91. package/rcf/requirements/req-013.json +2 -1
  92. package/rcf/requirements/req-014.json +2 -1
  93. package/rcf/requirements/req-015.json +2 -1
  94. package/rcf/requirements/req-016.json +5 -2
  95. package/rcf/requirements/req-017.json +6 -2
  96. package/rcf/requirements/req-018.json +5 -2
  97. package/rcf/requirements/req-019.json +5 -2
  98. package/rcf/requirements/req-020.json +5 -2
  99. package/rcf/requirements/req-021.json +5 -2
  100. package/rcf/requirements/req-022.json +5 -2
  101. package/rcf/requirements/req-023.json +6 -4
  102. package/rcf/requirements/req-024.json +5 -2
  103. package/rcf/requirements/req-025.json +5 -2
  104. package/rcf/requirements/req-026.json +5 -2
  105. package/rcf/requirements/req-027.json +5 -2
  106. package/rcf/requirements/req-028.json +5 -2
  107. package/rcf/requirements/req-029.json +5 -2
  108. package/rcf/requirements/req-030.json +5 -2
  109. package/rcf/requirements/req-040.json +5 -2
  110. package/rcf/requirements/req-041.json +5 -2
  111. package/rcf/requirements/req-042.json +5 -2
  112. package/rcf/requirements/req-043.json +5 -2
  113. package/rcf/requirements/req-050.json +5 -2
  114. package/rcf/requirements/req-051.json +5 -2
  115. package/rcf/requirements/req-052.json +5 -2
  116. package/rcf/requirements/req-053.json +5 -2
  117. package/rcf/requirements/req-054.json +5 -2
  118. package/rcf/requirements/req-060.json +5 -2
  119. package/rcf/requirements/req-061.json +5 -2
  120. package/rcf/requirements/req-062.json +5 -2
  121. package/rcf/requirements/req-063.json +5 -2
  122. package/rcf/requirements/req-070.json +5 -2
  123. package/rcf/requirements/req-071.json +5 -2
  124. package/rcf/requirements/req-072.json +5 -2
  125. package/rcf/requirements/req-073.json +5 -2
  126. package/rcf/requirements/req-074.json +5 -2
  127. package/rcf/requirements/req-075.json +5 -2
  128. package/rcf/requirements/req-076.json +5 -2
  129. package/rcf/requirements/req-077.json +5 -2
  130. package/rcf/requirements/req-080.json +5 -2
  131. package/rcf/requirements/req-081.json +5 -2
  132. package/rcf/requirements/req-082.json +5 -2
  133. package/rcf/requirements/req-083.json +5 -2
  134. package/rcf/requirements/req-084.json +5 -2
  135. package/rcf/requirements/req-085.json +5 -2
  136. package/rcf/requirements/req-086.json +5 -2
  137. package/rcf/requirements/req-090.json +5 -2
  138. package/rcf/requirements/req-091.json +5 -2
  139. package/rcf/requirements/req-092.json +5 -2
  140. package/rcf/requirements/req-093.json +5 -2
  141. package/rcf/requirements/req-094.json +5 -2
  142. package/rcf/requirements/req-100.json +5 -2
  143. package/rcf/requirements/req-101.json +5 -2
  144. package/rcf/requirements/req-102.json +5 -2
  145. package/rcf/requirements/req-103.json +5 -2
  146. package/rcf/requirements/req-104.json +5 -2
  147. package/rcf/requirements/req-110.json +5 -2
  148. package/rcf/requirements/req-111.json +5 -2
  149. package/rcf/requirements/req-112.json +5 -2
  150. package/rcf/requirements/req-113.json +5 -2
  151. package/rcf/requirements/req-114.json +5 -2
  152. package/rcf/requirements/req-115.json +5 -2
  153. package/rcf/requirements/req-120.json +5 -2
  154. package/rcf/requirements/req-121.json +5 -2
  155. package/rcf/requirements/req-122.json +5 -2
  156. package/rcf/requirements/req-123.json +5 -2
  157. package/rcf/requirements/req-124.json +5 -2
  158. package/rcf/requirements/req-125.json +5 -2
  159. package/rcf/requirements/req-130.json +6 -2
  160. package/rcf/requirements/req-131.json +6 -2
  161. package/rcf/requirements/req-132.json +6 -2
  162. package/rcf/requirements/req-133.json +6 -2
  163. package/rcf/requirements/req-134.json +6 -2
  164. package/rcf/requirements/req-140.json +5 -2
  165. package/rcf/requirements/req-145.json +5 -2
  166. package/rcf/requirements/req-150.json +6 -2
  167. package/rcf/requirements/req-151.json +6 -2
  168. package/rcf/requirements/req-152.json +6 -2
  169. package/rcf/requirements/req-153.json +6 -2
  170. package/rcf/requirements/req-154.json +5 -2
  171. package/rcf/requirements/req-155.json +4 -1
  172. package/rcf/requirements/req-156.json +4 -1
  173. package/rcf/requirements/req-157.json +4 -1
  174. package/rcf/requirements/req-158.json +4 -1
  175. package/rcf/requirements/req-159.json +4 -1
  176. package/rcf/requirements/req-160.json +4 -1
  177. package/rcf/requirements/req-161.json +4 -1
  178. package/rcf/requirements/req-162.json +4 -1
  179. package/rcf/requirements/req-163.json +5 -2
  180. package/rcf/requirements/req-164.json +5 -2
  181. package/rcf/requirements/req-165.json +5 -2
  182. package/rcf/requirements/req-170.json +33 -0
  183. package/rcf/requirements/req-171.json +44 -0
  184. package/rcf/requirements/req-172.json +25 -0
  185. package/rcf/requirements/req-173.json +25 -0
  186. package/rcf/requirements/req-174.json +31 -0
  187. package/rcf/requirements/req-175.json +31 -0
  188. package/rcf/requirements/req-176.json +25 -0
  189. package/rcf/requirements/req-180.json +31 -0
  190. package/rcf/requirements/req-181.json +31 -0
  191. package/rcf/requirements/req-182.json +25 -0
  192. package/rcf/requirements/req-186.json +25 -0
  193. package/rcf/requirements/req-187.json +31 -0
  194. package/rcf/requirements/req-188.json +25 -0
  195. package/rcf/tacs/tac-4120-define-delta-and-freeze.json +21 -0
  196. package/rcf/tacs/tac-4121-define-ledgers.json +35 -0
  197. package/rcf/tacs/tac-4122-define-gates.json +61 -0
  198. package/rcf/tacs/tac-4123-define-readiness.json +61 -0
  199. package/rcf/tacs/tac-4124-define-freeze.json +28 -0
  200. package/rcf/tacs/tac-4127-readiness-view.json +49 -0
  201. package/rcf/tacs/tac-4128-define-guidance.json +53 -0
  202. package/rcf/tacs/tac-4129-define-questions.json +45 -0
  203. package/rcf/tacs/tac-4130-define-intake-brief.json +35 -0
  204. package/rcf/tacs/tac-4131-build-dag-view.json +39 -0
  205. package/rcf/tacs/tac-4132-id-lookup.json +42 -0
  206. package/rcf/tacs/tac-4133-readiness-po-layer.json +37 -0
  207. package/rcf/tacs/tac-4134-wespa-host-fixture.json +54 -0
  208. package/rcf/test-suites/ts-016.json +3 -3
  209. package/rcf/test-suites/ts-204.json +30 -0
  210. package/rcf/test-suites/ts-205.json +22 -0
  211. package/rcf/test-suites/ts-206.json +22 -0
  212. package/rcf/test-suites/ts-207.json +22 -0
  213. package/rcf/test-suites/ts-208.json +22 -0
  214. package/rcf/test-suites/ts-209.json +22 -0
  215. package/rcf/test-suites/ts-210.json +22 -0
  216. package/rcf/test-suites/ts-211.json +22 -0
  217. package/rcf/test-suites/ts-212.json +22 -0
  218. package/rcf/test-suites/ts-213.json +50 -0
  219. package/rcf/test-suites/ts-214.json +74 -0
  220. package/rcf/test-suites/ts-215.json +211 -0
  221. package/rcf/test-suites/ts-216.json +94 -0
  222. package/rcf/test-suites/ts-217.json +62 -0
  223. package/rcf/test-suites/ts-218.json +78 -0
  224. package/rcf/test-suites/ts-219.json +70 -0
  225. package/rcf/test-suites/ts-220.json +78 -0
  226. package/rcf/test-suites/ts-221.json +78 -0
  227. package/rcf/test-suites/ts-222.json +100 -0
  228. package/rcf/test-suites/ts-223.json +94 -0
  229. package/rcf/test-suites/ts-224.json +70 -0
  230. package/rcf/test-suites/ts-225.json +78 -0
  231. package/rcf/test-suites/ts-226.json +158 -0
  232. package/rcf/test-suites/ts-227.json +38 -0
  233. package/rcf/test-suites/ts-228.json +86 -0
  234. package/rcf/test-suites/ts-229.json +83 -0
  235. package/rcf/user-stories/us-15601.json +4 -4
  236. package/rcf/user-stories/us-17001.json +27 -0
  237. package/rcf/user-stories/us-17002.json +22 -0
  238. package/rcf/user-stories/us-17003.json +22 -0
  239. package/rcf/user-stories/us-17004.json +22 -0
  240. package/rcf/user-stories/us-17005.json +22 -0
  241. package/rcf/user-stories/us-17006.json +21 -0
  242. package/rcf/user-stories/us-17007.json +21 -0
  243. package/rcf/user-stories/us-17008.json +22 -0
  244. package/rcf/user-stories/us-17101.json +22 -0
  245. package/rcf/user-stories/us-17102.json +22 -0
  246. package/rcf/user-stories/us-17201.json +38 -0
  247. package/rcf/user-stories/us-17301.json +38 -0
  248. package/rcf/user-stories/us-17302.json +50 -0
  249. package/rcf/user-stories/us-17401.json +70 -0
  250. package/rcf/user-stories/us-17402.json +55 -0
  251. package/rcf/user-stories/us-17403.json +60 -0
  252. package/rcf/user-stories/us-17501.json +48 -0
  253. package/rcf/user-stories/us-17502.json +60 -0
  254. package/rcf/user-stories/us-17503.json +65 -0
  255. package/rcf/user-stories/us-17601.json +58 -0
  256. package/rcf/user-stories/us-18001.json +55 -0
  257. package/rcf/user-stories/us-18002.json +35 -0
  258. package/rcf/user-stories/us-18101.json +35 -0
  259. package/rcf/user-stories/us-18202.json +40 -0
  260. package/rcf/user-stories/us-18601.json +55 -0
  261. package/rcf/user-stories/us-18602.json +50 -0
  262. package/rcf/user-stories/us-18603.json +45 -0
  263. package/rcf/user-stories/us-18701.json +50 -0
  264. package/rcf/user-stories/us-18702.json +50 -0
  265. package/rcf/user-stories/us-18801.json +45 -0
  266. package/rcf/user-stories/us-204.json +59 -0
  267. package/rcf/user-stories/us-205.json +64 -0
  268. package/rcf/user-stories/us-206.json +54 -0
  269. package/releases/releases.yaml +31 -1
  270. package/src/cli/feedback.js +126 -10
  271. package/src/cli/freeze.js +730 -0
  272. package/src/cli/help.js +23 -0
  273. package/src/cli/ledger.js +615 -0
  274. package/src/cli/questions.js +288 -0
  275. package/src/cli/readiness.js +498 -0
  276. package/src/cli/view.js +29 -0
  277. package/src/core/store/writer.js +86 -6
  278. package/src/define/freeze-record.js +247 -0
  279. package/src/define/ledgers.js +712 -0
  280. package/src/feedback/redact.js +105 -19
  281. package/src/intake/index.js +1 -1
  282. package/src/intake/orchestrator.js +117 -0
  283. package/src/mcp/tools.js +354 -0
  284. package/src/query/delta.js +282 -0
  285. package/src/query/gates.js +1107 -0
  286. package/src/query/index.js +50 -0
  287. package/src/query/questions.js +438 -0
  288. package/src/query/readiness.js +574 -0
  289. package/src/server/index.js +60 -1
  290. package/src/server/routes.js +133 -4
  291. package/src/setup/identity-seed.js +17 -0
  292. package/src/view/build-dag.js +563 -0
  293. package/src/view/components/badge.js +41 -0
  294. package/src/view/components/diff.js +72 -0
  295. package/src/view/components/doc-row.js +62 -0
  296. package/src/view/components/empty-state.js +35 -0
  297. package/src/view/components/entity-selector.js +137 -0
  298. package/src/view/components/filter-bar.js +97 -0
  299. package/src/view/components/findings-list.js +49 -0
  300. package/src/view/components/index.js +12 -0
  301. package/src/view/components/pill.js +46 -0
  302. package/src/view/components/sub-tab-strip.js +43 -0
  303. package/src/view/components/toast.js +25 -0
  304. package/src/view/components-fixture.js +265 -0
  305. package/src/view/doc-renderers/architecture.js +304 -0
  306. package/src/view/doc-renderers/build.js +160 -0
  307. package/src/view/doc-renderers/helpers.js +2 -2
  308. package/src/view/doc-renderers/index.js +2 -0
  309. package/src/view/doc-renderers/prd.js +35 -6
  310. package/src/view/doc-renderers/req.js +10 -2
  311. package/src/view/doc-renderers/user-story.js +10 -5
  312. package/src/view/html-page.js +717 -300
  313. package/src/view/index.js +109 -3
  314. package/src/view/live-client.js +61 -8
  315. package/src/view/needs-work.js +79 -0
  316. package/src/view/page-init.js +2193 -0
  317. package/src/view/product-map.js +751 -0
  318. package/src/view/readiness/phrasebook.js +115 -0
  319. package/src/view/readiness/po-layer.js +288 -0
  320. package/src/view/readiness/question-adapter.js +329 -0
  321. package/src/view/readiness/stage-legend.js +71 -0
  322. package/src/view/readiness.js +505 -0
  323. package/src/view/style.css +2279 -266
  324. package/src/view/test-host/embed-client.js +121 -0
  325. package/src/view/test-host/fixture.js +183 -0
  326. package/src/view/test-host/lints.js +220 -0
  327. package/src/view/view-index.js +323 -0
@@ -20,6 +20,7 @@
20
20
  // secret-shape check the last line of defence, which is what the
21
21
  // residual-match refusal in rule 8 leans on.
22
22
 
23
+ import { randomUUID } from 'node:crypto';
23
24
  import { readFileSync } from 'node:fs';
24
25
  import { dirname, resolve } from 'node:path';
25
26
  import { fileURLToPath } from 'node:url';
@@ -43,6 +44,14 @@ export const BODY_CAP_BYTES = 8 * 1024;
43
44
  * conservative: anything a normal profile.md ## Name line might carry
44
45
  * as a common English or engineering noun.
45
46
  */
47
+
48
+ // Codex P1-4: per-call collision-safe stash marker suffix. The suffix is
49
+ // 32 hex chars from randomUUID, which is astronomically unlikely to
50
+ // appear as a literal substring in prose the operator would submit.
51
+ function gitStashUuid() {
52
+ return randomUUID().replace(/-/g, '');
53
+ }
54
+
46
55
  export const RESERVED_IDENTITY_TOKENS = Object.freeze(new Set([
47
56
  'agent', 'agents', 'user', 'users', 'operator', 'operators',
48
57
  'admin', 'admins', 'root', 'client', 'clients', 'server', 'servers',
@@ -435,30 +444,98 @@ export function redact(input, context = {}) {
435
444
  }
436
445
  }
437
446
 
447
+ // 0.28.3 (Codex P1-1): move the rule-4 allowlist plumbing up so
448
+ // R5f's bare-remote stash can decide whether to preserve or fold the
449
+ // host before it hides the token. Duplicated later in the rule-4
450
+ // block was removed at the same time.
451
+ const allowHosts = new Set([
452
+ ...BUNDLED_ALLOWLIST.hosts,
453
+ ...(Array.isArray(context.allowHosts) ? context.allowHosts : []),
454
+ ].map((h) => h.toLowerCase()));
455
+ const allowExtensions = new Set([
456
+ ...(Array.isArray(BUNDLED_ALLOWLIST.extensions) ? BUNDLED_ALLOWLIST.extensions : []),
457
+ ...(Array.isArray(context.allowExtensions) ? context.allowExtensions : []),
458
+ ].map((e) => String(e).toLowerCase()));
459
+
460
+ // Rule 3-guard (design amendment R5f, issue #243, 0.28.3): protect the
461
+ // git protocol sentinel `git@<host>` before rule 3 runs. In a URL whose
462
+ // scheme is git+ssh/ssh/git, and in the bare git-remote shape
463
+ // `git@<host>:<owner>/<repo>`, `git@<host>` is URI userinfo, not an
464
+ // email. Folding it destroys evidence pointers copied from git remotes
465
+ // and clone URLs. We stash the full matched span before the email regex
466
+ // runs and restore it before rule 4 so the host inside the span is
467
+ // still eligible for the URL / bare-host fold.
468
+ const gitProtoRe = /(?:git\+ssh|ssh|git):\/\/git@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
469
+ const gitRemoteBareRe = /(?<![A-Za-z0-9._%+-])git@[A-Za-z0-9.-]+\.[A-Za-z]{2,}(?=:[^\s]+\/)/g;
470
+ const gitStash = [];
471
+ // Use a per-invocation random marker so a literal `GITPROTO0GITPROTO`
472
+ // in the operator's prose cannot be replayed as a restore token
473
+ // (Codex P1-4). The marker is 32 hex chars from randomUUID, wrapped
474
+ // in an unusual sigil (`\u0002...\u0003`) that would not survive
475
+ // the ctrl-char strip pass at rule 8a-late anyway, guaranteeing
476
+ // collision-free restoration for the duration of this call.
477
+ const GIT_STASH_TAG = `GITPROTO_${gitStashUuid()}`;
478
+ // Precompute a bare-remote host allowlist check per candidate:
479
+ // Codex P1-1 called out that a bare `git@<host>:...` remote used
480
+ // to have its host folded via the pre-fix email rule, and the R5f
481
+ // protection accidentally shielded the host too. Restore the host-
482
+ // level allowlist gate here: an allowlisted host stays verbatim,
483
+ // an unallowlisted host has the host portion folded to `<host>`
484
+ // before we stash, so the sentinel-plus-<host> shape survives
485
+ // through the pipeline.
486
+ function stashBareRemote(m) {
487
+ const at = m.indexOf('@');
488
+ const host = m.slice(at + 1);
489
+ if (isHostAllowed(host, allowHosts)) {
490
+ gitStash.push(m);
491
+ } else {
492
+ // Record a hostname ledger row so the operator disclosure names
493
+ // what was folded, matching the shape rule 4 would have used.
494
+ const before = host;
495
+ const after = '<host>';
496
+ // We push a synthetic ledger row for the hostname family (one
497
+ // per bare-remote fold). Deduplicated within the pass.
498
+ ledger.push({ rule: 'hostname', before, after, count: 1 });
499
+ gitStash.push(`git@${after}`);
500
+ }
501
+ return `${GIT_STASH_TAG}${gitStash.length - 1}${GIT_STASH_TAG}`;
502
+ }
503
+ text = text.replace(gitProtoRe, (m) => {
504
+ gitStash.push(m);
505
+ return `${GIT_STASH_TAG}${gitStash.length - 1}${GIT_STASH_TAG}`;
506
+ });
507
+ text = text.replace(gitRemoteBareRe, stashBareRemote);
508
+
438
509
  // Rule 3: emails -> <email>. RFC-5322 lite; case-insensitive.
439
510
  const emailRe = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
440
511
  text = replaceRegex(text, emailRe, () => '<email>', 'email', ledger);
441
512
 
513
+ // R5f: restore git protocol sentinels. The stashed spans now re-enter
514
+ // the pipeline for rule 4 (URL / bare-host). Rule 4's URL regex is
515
+ // extended below to match git+ssh/ssh/git schemes with optional
516
+ // userinfo, so a non-allowlisted host inside a restored clone URL
517
+ // (e.g. `ssh://git@example.internal/owner/repo.git`) folds via that
518
+ // path while the `git@` userinfo is preserved. A bare
519
+ // `git@<host>:<owner>/<repo>` remote is left as-is by rule 4 because
520
+ // it has no scheme prefix and the bareHost lookbehind excludes `@`;
521
+ // the sentinel-protection posture is sufficient for that shape.
522
+ if (gitStash.length > 0) {
523
+ text = text.replace(new RegExp(`${GIT_STASH_TAG}(\\d+)${GIT_STASH_TAG}`, 'g'), (_m, i) => gitStash[Number(i)]);
524
+ }
525
+
442
526
  // Rule 4: hostnames and URLs whose registrable domain is not on the
443
527
  // allowlist. URLs keep their path suffix; bare hostnames collapse.
444
528
  // Loopback (localhost, 127.0.0.1) and everything on the allowlist
445
529
  // survive; every other public hostname or URL loses its host.
446
- const allowHosts = new Set([
447
- ...BUNDLED_ALLOWLIST.hosts,
448
- ...(Array.isArray(context.allowHosts) ? context.allowHosts : []),
449
- ].map((h) => h.toLowerCase()));
450
-
451
- // Rule 4 narrowing (0.28.2, issue #233, design amendment R5d): a
452
- // token whose trailing dot-label is a known file extension is NOT
453
- // a hostname. This kills the `Backstory-product-brief.md` and
530
+ // allowHosts / allowExtensions declared earlier (Codex P1-1) so the
531
+ // rule-3 bare-remote stash can gate on the host allowlist too. Rule
532
+ // 4 narrowing (0.28.2, issue #233, design amendment R5d): a token
533
+ // whose trailing dot-label is a known file extension is NOT a
534
+ // hostname, killing the `Backstory-product-brief.md` and
454
535
  // `intake.json` false positives Barry hit in the WSD round-trip.
455
536
  // The bundled shortlist lives in redact-allowlist.json under the
456
- // new `extensions` field; the operator overlay merges via
537
+ // `extensions` field; the operator overlay merges via
457
538
  // context.allowExtensions.
458
- const allowExtensions = new Set([
459
- ...(Array.isArray(BUNDLED_ALLOWLIST.extensions) ? BUNDLED_ALLOWLIST.extensions : []),
460
- ...(Array.isArray(context.allowExtensions) ? context.allowExtensions : []),
461
- ].map((e) => String(e).toLowerCase()));
462
539
 
463
540
  // Rule 9: URL-embedded credentials (design amendment R3b, added
464
541
  // round 4 for F-P0-B). Runs BEFORE the rule-4 hostname pass so a
@@ -471,11 +548,17 @@ export function redact(input, context = {}) {
471
548
  // `url-credential`; false positives are visible in the disclosure.
472
549
  text = redactUrlCredentials(text, ledger);
473
550
 
474
- // URLs first (http/https), then bare hostnames.
475
- const urlRe = /https?:\/\/([A-Za-z0-9.-]+)((?::[0-9]+)?(?:\/[^\s)]*)?)/g;
476
- text = replaceRegex(text, urlRe, (_m, host, tail) => {
477
- if (isHostAllowed(host, allowHosts)) return `https://${host}${tail}`;
478
- return `<host>${tail}`;
551
+ // URLs first (http/https/git+ssh/ssh/git), then bare hostnames.
552
+ // 0.28.3 (R5f, issue #243): the URL rule accepts git+ssh/ssh/git
553
+ // schemes with an optional userinfo prefix so the host inside a git
554
+ // clone URL can be folded by the same code path that folds an https
555
+ // URL host. The rule-3 guard above has already protected the `git@`
556
+ // sentinel from the email rule; here the sentinel is preserved and
557
+ // only the host portion is folded.
558
+ const urlRe = /(https?|git\+ssh|ssh|git):\/\/((?:[A-Za-z0-9._%+-]+@)?)([A-Za-z0-9.-]+)((?::[0-9]+)?(?:\/[^\s)]*)?)/g;
559
+ text = replaceRegex(text, urlRe, (_m, scheme, userinfo, host, tail) => {
560
+ if (isHostAllowed(host, allowHosts)) return `${scheme}://${userinfo}${host}${tail}`;
561
+ return `${scheme}://${userinfo}<host>${tail}`;
479
562
  }, 'hostname', ledger);
480
563
  // Bare hostnames: a word that looks like a domain (has a dot,
481
564
  // ends with a 2+ letter TLD) and is not on the allowlist. Skip if
@@ -487,7 +570,10 @@ export function redact(input, context = {}) {
487
570
  // lookbehind is the 0.28.2 (issue #233) fix that stops
488
571
  // `Backstory-product-brief.md` folding on its internal
489
572
  // `product-brief.md` slice; the extension-refusal step below is the
490
- // second layer.
573
+ // second layer. Design amendment R5e (0.28.3, issue #247): the
574
+ // `{2,}` trailing class also functions as the single-letter-TLD
575
+ // exclusion - a token like `a.b.c` has a one-letter tail and is
576
+ // treated as a filename fragment, not a hostname.
491
577
  const bareHostRe = /(?<![@/\\\w.\-])(?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,}(?![\w.-])/g;
492
578
  text = replaceRegex(text, bareHostRe, (host) => {
493
579
  // 0.28.2 (issue #233, design amendment R5d): a candidate whose
@@ -2,5 +2,5 @@
2
2
 
3
3
  export { classifyFidelity, FIDELITY_LEVELS } from './fidelity.js';
4
4
  export { scanArtefactForFindings } from './validate.js';
5
- export { runIntakePhases } from './orchestrator.js';
5
+ export { runIntakePhases, runIntakeScansOnDelta } from './orchestrator.js';
6
6
  export { writeIntakeRecord, composeIntakeRecord, nextIntakeId } from './manifest-writer.js';
@@ -18,6 +18,123 @@ import { classifyFidelity } from './fidelity.js';
18
18
  import { scanArtefactForFindings } from './validate.js';
19
19
  import { composeIntakeRecord } from './manifest-writer.js';
20
20
 
21
+ /**
22
+ * Run the mechanical intake scans over newly minted brief statements
23
+ * against every pre-existing brief statement. The scans operate on
24
+ * the raw text of the statements; a finding is attributed to the
25
+ * ids of every statement whose text the scan matched (an
26
+ * `impliedButNotStated` cue in one new statement with the matching
27
+ * negated-login phrase elsewhere attributes to both). On a frozen
28
+ * tree the caller passes every pre-existing statement as `existing`;
29
+ * the comparison set is `existing` by definition.
30
+ *
31
+ * Each returned finding carries:
32
+ * { kind, detail, scanName, ids: number[] }
33
+ * where `kind` is one of the three mechanical kinds from
34
+ * `scanArtefactForFindings`, `ids` lists the involved statement ids
35
+ * from `newStatements` first and then `existing` (duplicates
36
+ * removed), and `scanName` is `kind` for the DEFINE PR 1 wiring.
37
+ *
38
+ * @param {Array<{ id: number, text?: string }>} newStatements
39
+ * @param {Array<{ id: number, text?: string }>} existing
40
+ * @returns {Array<{ kind: string, detail: string, scanName: string, ids: number[] }>}
41
+ */
42
+ export function runIntakeScansOnDelta(newStatements, existing) {
43
+ const safeNew = Array.isArray(newStatements) ? newStatements : [];
44
+ const safeExisting = Array.isArray(existing) ? existing : [];
45
+ if (safeNew.length === 0) return [];
46
+
47
+ const combined = [...safeNew, ...safeExisting];
48
+ const combinedText = combined.map((s) => (typeof s?.text === 'string' ? s.text : '')).join('\n');
49
+ const rawFindings = scanArtefactForFindings(combinedText);
50
+ if (rawFindings.length === 0) return [];
51
+
52
+ // A finding only counts when at least one of the newStatements
53
+ // participates. We attribute by re-running the scan over the
54
+ // `newStatements` alone (if that yields the same kind of finding,
55
+ // the new side is sufficient) OR by running the scan over any
56
+ // (new, existing) pair that contains at least one new statement:
57
+ // the second case detects a contradiction between a new and an
58
+ // existing statement.
59
+ /** @type {Array<{ kind: string, detail: string, scanName: string, ids: number[] }>} */
60
+ const out = [];
61
+ const seen = new Set();
62
+ for (const f of rawFindings) {
63
+ const key = `${f.kind}::${f.detail}`;
64
+ if (seen.has(key)) continue;
65
+ seen.add(key);
66
+ const ids = attributeFinding(f, safeNew, safeExisting);
67
+ if (ids.length === 0) continue;
68
+ out.push({ kind: f.kind, detail: f.detail, scanName: f.kind, ids });
69
+ }
70
+ return out;
71
+ }
72
+
73
+ /**
74
+ * @param {{ kind: string, detail: string }} finding
75
+ * @param {Array<{ id: number, text?: string }>} newStatements
76
+ * @param {Array<{ id: number, text?: string }>} existing
77
+ * @returns {number[]}
78
+ */
79
+ function attributeFinding(finding, newStatements, existing) {
80
+ /** @type {number[]} */
81
+ const ids = [];
82
+ for (const s of newStatements) {
83
+ if (statementParticipates(finding, s, [...newStatements, ...existing])) {
84
+ ids.push(Number(s.id));
85
+ }
86
+ }
87
+ if (ids.length === 0) return ids;
88
+ // Also attribute an existing statement that participates on the other side.
89
+ for (const s of existing) {
90
+ if (statementParticipates(finding, s, [...newStatements, ...existing])) {
91
+ if (!ids.includes(Number(s.id))) ids.push(Number(s.id));
92
+ }
93
+ }
94
+ return ids;
95
+ }
96
+
97
+ /**
98
+ * Decide whether `statement.text` contributes to `finding`. The scan
99
+ * is pattern-based and finds the same finding kind on the text in
100
+ * isolation if the statement itself carries the full pattern; for
101
+ * cross-statement findings (a contradiction between two statements)
102
+ * the statement contributes when it carries at least one half of the
103
+ * pattern and the other half appears somewhere in the combined set.
104
+ *
105
+ * @param {{ kind: string, detail: string }} finding
106
+ * @param {{ id: number, text?: string }} statement
107
+ * @param {Array<{ id: number, text?: string }>} combined
108
+ */
109
+ function statementParticipates(finding, statement, combined) {
110
+ const text = typeof statement?.text === 'string' ? statement.text : '';
111
+ if (!text) return false;
112
+ const soloFindings = scanArtefactForFindings(text).map((f) => f.kind);
113
+ if (soloFindings.includes(finding.kind)) return true;
114
+ // Cross-statement: pair this statement's text with every other
115
+ // combined text and see whether the pair raises the same finding.
116
+ for (const other of combined) {
117
+ if (other === statement) continue;
118
+ const otherText = typeof other?.text === 'string' ? other.text : '';
119
+ if (!otherText) continue;
120
+ const pairFindings = scanArtefactForFindings(`${text}\n${otherText}`).map((f) => f.kind);
121
+ // Codex review 2026-10-02: a pre-existing finding already
122
+ // raised by `other` alone must not be re-attributed to an
123
+ // unrelated `statement` merely because concatenation leaves
124
+ // the pattern intact. Require the pair to raise the finding
125
+ // that neither side raises alone.
126
+ const otherSolo = scanArtefactForFindings(otherText).map((f) => f.kind);
127
+ if (
128
+ pairFindings.includes(finding.kind)
129
+ && !soloFindings.includes(finding.kind)
130
+ && !otherSolo.includes(finding.kind)
131
+ ) {
132
+ return true;
133
+ }
134
+ }
135
+ return false;
136
+ }
137
+
21
138
  /**
22
139
  * @typedef {'napkin'|'productBrief'|'prd'|'tad'|'other'} ArtefactKind
23
140
  */