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,269 @@
1
+ // `rcf req-baseline <verb>` subcommand handler
2
+ // (elicitation-and-playbook-hardening-0.7.0-spec §5).
3
+ //
4
+ // Verbs (spec §5.4 / §13.3):
5
+ // rcf req-baseline sweep [--req <id> | --all] [--dry-run] [--json] [--yes]
6
+ // rcf req-baseline sweep --status [--json]
7
+ // rcf req-baseline opt-out --req <id> --key <baselineKey> --reason "..." [--scope req|project]
8
+ // rcf req-baseline opt-out --remove --req <id> --key <baselineKey>
9
+ //
10
+ // The sweep proposes any baseline AC not yet present on a US under the
11
+ // target REQ; the operator answers accept / opt-out / skip per candidate.
12
+ // Silence is refused: candidates left undecided stay OPEN, and Stage 1
13
+ // (rcf build --next) refuses any FBS binding an AC on a US that has
14
+ // open sweeps (spec §5.4 + §5.5 canonical text).
15
+ //
16
+ // Non-interactive callers (--yes accept-all, --dry-run preview) are
17
+ // legitimate; the moment-4 auto-enqueue path (spec §5.3) also produces
18
+ // OPEN candidates that this verb resolves.
19
+
20
+ import { parseArgs } from 'node:util';
21
+ import { createInterface } from 'node:readline/promises';
22
+ import process from 'node:process';
23
+
24
+ import { walkTree } from '#core/store';
25
+
26
+ import { findProjectRoot } from '../view/index.js';
27
+ import {
28
+ planSweep,
29
+ applySweepDecisions,
30
+ writeOptOut,
31
+ removeOptOut,
32
+ listOpenCandidates,
33
+ } from '../req-baseline/index.js';
34
+
35
+ const OPTION_SPEC = {
36
+ req: { type: 'string' },
37
+ all: { type: 'boolean' },
38
+ key: { type: 'string' },
39
+ reason: { type: 'string' },
40
+ scope: { type: 'string' },
41
+ remove: { type: 'boolean' },
42
+ status: { type: 'boolean' },
43
+ 'dry-run': { type: 'boolean' },
44
+ yes: { type: 'boolean' },
45
+ json: { type: 'boolean' },
46
+ quiet: { type: 'boolean' },
47
+ help: { type: 'boolean' },
48
+ };
49
+
50
+ export const HELP = `Usage: rcf req-baseline <verb> [options]
51
+
52
+ Verbs:
53
+ sweep [--req <id> | --all] [--status] [--dry-run] [--yes] [--json]
54
+ Walk every US under the target REQ (or all
55
+ classified REQs) and propose any baseline
56
+ ACs not yet present. Interactive by default
57
+ on a TTY; --yes accepts every candidate;
58
+ --dry-run prints the plan without writing.
59
+ --status prints the open-candidate list
60
+ (silence-refuses-build queue) without
61
+ prompting.
62
+
63
+ opt-out --req <id> --key <baselineKey> --reason "..." [--scope req|project]
64
+ Record an operator ruling that removes a
65
+ baseline AC from the REQ (or the whole
66
+ project) and inherit the ruling on future
67
+ sweeps. Reason is at least 20 characters
68
+ (schema-enforced).
69
+
70
+ opt-out --remove --req <id> --key <baselineKey>
71
+ Delete an existing opt-out; the next sweep
72
+ will re-propose the AC.
73
+
74
+ Options:
75
+ --req <id> REQ to target
76
+ --all Target every classified REQ in the tree
77
+ --key <baselineKey> Catalogue key (for example, auth.htmlLoginPage)
78
+ --reason "..." Operator ruling (at least 20 characters)
79
+ --scope req|project Opt-out scope; defaults to req
80
+ --dry-run Preview only, do not write
81
+ --yes Accept every proposed candidate (non-interactive)
82
+ --status Show open-sweep candidates without prompting
83
+ --json Emit machine-readable output
84
+ --quiet Suppress non-error confirmations
85
+ --help Print this help
86
+
87
+ Exit codes:
88
+ 0 success
89
+ 2 usage error (unknown flag, missing --req, no project root)
90
+ 3 validation failure on the written manifest
91
+ 4 refused (opt-out reason below the 20-character floor)
92
+ `;
93
+
94
+ /**
95
+ * @param {string[]} argv
96
+ * @param {object} [deps]
97
+ * @returns {Promise<number>}
98
+ */
99
+ export async function main(argv, deps = {}) {
100
+ const stdout = deps.stdout ?? process.stdout;
101
+ const stderr = deps.stderr ?? process.stderr;
102
+ const cwd = deps.cwd ?? process.cwd();
103
+
104
+ let parsed;
105
+ try {
106
+ parsed = parseArgs({ args: argv, options: OPTION_SPEC, allowPositionals: true, strict: true });
107
+ } catch (err) {
108
+ stderr.write(`[error] usage ${err.message}\n`);
109
+ stderr.write(HELP);
110
+ return 2;
111
+ }
112
+ const flags = parsed.values;
113
+ const positionals = parsed.positionals;
114
+ if (flags.help) { stdout.write(HELP); return 0; }
115
+ if (positionals.length === 0) {
116
+ stderr.write('[error] usage req-baseline: missing verb (sweep|opt-out)\n');
117
+ stderr.write(HELP);
118
+ return 2;
119
+ }
120
+ const verb = positionals[0];
121
+ if (!['sweep', 'opt-out'].includes(verb)) {
122
+ stderr.write(`[error] usage req-baseline: unknown verb ${verb} (expected sweep|opt-out)\n`);
123
+ return 2;
124
+ }
125
+
126
+ const projectRoot = await findProjectRoot(cwd);
127
+ if (!projectRoot) {
128
+ stderr.write('[error] usage no project root found (no rcf/manifest.json in this directory or any ancestor). Run `npx rcf init` to create and wire a project.\n');
129
+ return 2;
130
+ }
131
+
132
+ const walkResult = await walkTree({ projectRoot });
133
+ if (walkResult.errors.length > 0) {
134
+ stderr.write(`[warn] tree has ${walkResult.errors.length} pre-existing issue(s); proceeding - writes are validated against the post-write state (run 'rcf validate' for details)\n`);
135
+ }
136
+
137
+ if (verb === 'sweep') {
138
+ if (flags.status) {
139
+ const open = listOpenCandidates(walkResult.tree, { reqId: flags.req ?? null });
140
+ if (flags.json) {
141
+ stdout.write(`${JSON.stringify(open, null, 2)}\n`);
142
+ return 0;
143
+ }
144
+ if (open.length === 0) {
145
+ stdout.write('no open baseline sweep candidates\n');
146
+ return 0;
147
+ }
148
+ for (const item of open) {
149
+ stdout.write(`${item.usId} (${item.reqId}) open: ${item.baselineKeys.join(', ')}\n`);
150
+ }
151
+ return 0;
152
+ }
153
+
154
+ if (!flags.req && !flags.all) {
155
+ stderr.write('[error] usage req-baseline sweep: --req <id> or --all is required\n');
156
+ return 2;
157
+ }
158
+
159
+ const plan = planSweep(walkResult.tree, { reqId: flags.req ?? null, all: Boolean(flags.all) });
160
+ if (plan.candidates.length === 0) {
161
+ if (!flags.quiet) stdout.write('no baseline candidates to propose\n');
162
+ return 0;
163
+ }
164
+
165
+ if (flags['dry-run']) {
166
+ if (flags.json) {
167
+ stdout.write(`${JSON.stringify(plan, null, 2)}\n`);
168
+ } else {
169
+ for (const c of plan.candidates) {
170
+ stdout.write(`[dry-run] would propose ${c.baselineKey} on ${c.usId} (${c.reqId})\n`);
171
+ }
172
+ }
173
+ return 0;
174
+ }
175
+
176
+ // Resolve decisions.
177
+ const decisions = [];
178
+ if (flags.yes) {
179
+ for (const c of plan.candidates) decisions.push({ candidate: c, action: 'accept' });
180
+ } else {
181
+ const isTty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
182
+ if (!isTty) {
183
+ // Non-interactive with no --yes: leave every candidate OPEN. This
184
+ // matches the moment-4 auto-enqueue posture (spec §5.3): a
185
+ // baseline-blind US never silently ships; the operator must
186
+ // resolve open sweeps before Stage 1 accepts an FBS binding it.
187
+ if (!flags.quiet) stdout.write(`sweep left ${plan.candidates.length} candidate(s) OPEN (non-interactive; rerun with --yes to accept every candidate)\n`);
188
+ return 0;
189
+ }
190
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
191
+ for (const c of plan.candidates) {
192
+ stdout.write(`\nREQ ${c.reqId} (${c.reqShape}) baseline for US ${c.usId}:\n`);
193
+ stdout.write(` ${c.baselineKey}: ${c.canonicalText}\n`);
194
+ // eslint-disable-next-line no-await-in-loop
195
+ const ans = (await rl.question('[a]ccept / [o]pt-out / [s]kip? ')).trim().toLowerCase();
196
+ if (ans === 'a' || ans === 'accept') {
197
+ decisions.push({ candidate: c, action: 'accept' });
198
+ } else if (ans === 'o' || ans === 'opt-out') {
199
+ // eslint-disable-next-line no-await-in-loop
200
+ const reason = (await rl.question('reason (>= 20 chars): ')).trim();
201
+ if (reason.length < 20) {
202
+ stderr.write(`[error] refused opt-out: reason below 20-character floor for ${c.baselineKey}; leaving candidate OPEN\n`);
203
+ continue;
204
+ }
205
+ decisions.push({ candidate: c, action: 'opt-out', reason });
206
+ } else {
207
+ // skip: leaves the candidate OPEN.
208
+ }
209
+ }
210
+ rl.close();
211
+ }
212
+
213
+ const result = await applySweepDecisions({ projectRoot, tree: walkResult.tree, decisions });
214
+ if (result && result.kind && typeof result.message === 'string') {
215
+ stderr.write(`[error] ${result.kind} ${result.message}\n`);
216
+ return 3;
217
+ }
218
+ if (flags.json) {
219
+ stdout.write(`${JSON.stringify(result, null, 2)}\n`);
220
+ return 0;
221
+ }
222
+ if (!flags.quiet) {
223
+ stdout.write(`sweep: ${result.accepted} accepted, ${result.optedOut} opted out, ${result.left} left OPEN\n`);
224
+ }
225
+ return 0;
226
+ }
227
+
228
+ // verb === 'opt-out'
229
+ if (flags.remove) {
230
+ if (!flags.req || !flags.key) {
231
+ stderr.write('[error] usage req-baseline opt-out --remove: --req and --key are required\n');
232
+ return 2;
233
+ }
234
+ const result = await removeOptOut({ projectRoot, tree: walkResult.tree, reqId: flags.req, baselineKey: flags.key });
235
+ if (result && result.kind && typeof result.message === 'string') {
236
+ stderr.write(`[error] ${result.kind} ${result.message}\n`);
237
+ return 3;
238
+ }
239
+ if (!flags.quiet) stdout.write(`opt-out removed for ${flags.req} ${flags.key}${result.removed ? '' : ' (no matching record found)'}\n`);
240
+ return 0;
241
+ }
242
+ if (!flags.req || !flags.key || !flags.reason) {
243
+ stderr.write('[error] usage req-baseline opt-out: --req, --key and --reason are required\n');
244
+ return 2;
245
+ }
246
+ if (flags.reason.length < 20) {
247
+ stderr.write(`[error] refused opt-out: --reason below 20-character floor (got ${flags.reason.length} chars); a one-word answer reads as silence, which is exactly what this ledger exists to prevent\n`);
248
+ return 4;
249
+ }
250
+ const scope = flags.scope ?? 'req';
251
+ if (!['req', 'project'].includes(scope)) {
252
+ stderr.write(`[error] usage req-baseline opt-out: --scope must be req or project (got ${scope})\n`);
253
+ return 2;
254
+ }
255
+ const result = await writeOptOut({
256
+ projectRoot,
257
+ tree: walkResult.tree,
258
+ reqId: flags.req,
259
+ baselineKey: flags.key,
260
+ reason: flags.reason,
261
+ scope,
262
+ });
263
+ if (result && result.kind && typeof result.message === 'string') {
264
+ stderr.write(`[error] ${result.kind} ${result.message}\n`);
265
+ return 3;
266
+ }
267
+ if (!flags.quiet) stdout.write(`opt-out ${result.id} recorded for ${flags.req} ${flags.key} (scope=${scope})\n`);
268
+ return 0;
269
+ }
@@ -0,0 +1,135 @@
1
+ // `rcf req-classify <req-id>` subcommand handler
2
+ // (elicitation-and-playbook-hardening-0.7.0-spec §4.4).
3
+ //
4
+ // Reruns the REQ-shape classifier for one REQ (or all REQs when
5
+ // --all is passed) and writes the resulting shapeClassification block
6
+ // back onto the REQ. Useful after the pattern-set version bumps or when
7
+ // the operator has updated a description and wants to re-check.
8
+
9
+ import { parseArgs } from 'node:util';
10
+
11
+ import { formatErrors, writeUnexpectedFailure, rcfError } from '#core/errors';
12
+ import { walkTree } from '#core/store';
13
+
14
+ import { findProjectRoot } from '../view/index.js';
15
+ import { classifyAndPersistReq } from '../req-detection/index.js';
16
+
17
+ const OPTION_SPEC = {
18
+ all: { type: 'boolean' },
19
+ json: { type: 'boolean' },
20
+ quiet: { type: 'boolean' },
21
+ help: { type: 'boolean' },
22
+ };
23
+
24
+ export const HELP = `Usage: rcf req-classify <req-id> [options]
25
+ rcf req-classify --all [options]
26
+
27
+ Rerun the REQ-shape classifier for one requirement (or every requirement
28
+ in the tree with --all) and write the resulting shapeClassification
29
+ block back onto the REQ.
30
+
31
+ Reads REQ title, description and rationale plus the parent PRD's intent
32
+ and problem as fallback context; matches against the versioned pattern
33
+ set in #core/patterns/req-shapes and writes shapes,
34
+ signals and classifiedAt. Preserves any prior operatorOverride block.
35
+
36
+ Options:
37
+ --all Classify every REQ in the tree
38
+ --json Emit the classification block as JSON on stdout
39
+ --quiet Suppress non-error confirmations
40
+ --help Print this help
41
+
42
+ Exit codes:
43
+ 0 success (or nothing to classify)
44
+ 2 usage error (unknown id, no project root, bad flags)
45
+ 3 validation failure on the merged REQ document
46
+ `;
47
+
48
+ /**
49
+ * @param {string[]} argv
50
+ * @param {object} [deps]
51
+ * @returns {Promise<number>}
52
+ */
53
+ export async function main(argv, deps = {}) {
54
+ const stdout = deps.stdout ?? process.stdout;
55
+ const stderr = deps.stderr ?? process.stderr;
56
+ const cwd = deps.cwd ?? process.cwd();
57
+
58
+ let parsed;
59
+ try {
60
+ parsed = parseArgs({ args: argv, options: OPTION_SPEC, allowPositionals: true, strict: true });
61
+ } catch (err) {
62
+ stderr.write(`[error] usage ${err.message}\n`);
63
+ stderr.write(HELP);
64
+ return 2;
65
+ }
66
+ const flags = parsed.values;
67
+ const positionals = parsed.positionals;
68
+ if (flags.help) { stdout.write(HELP); return 0; }
69
+
70
+ const useAll = Boolean(flags.all);
71
+ if (useAll && positionals.length > 0) {
72
+ stderr.write('[error] usage req-classify: --all takes no positional\n');
73
+ return 2;
74
+ }
75
+ if (!useAll && positionals.length !== 1) {
76
+ stderr.write('[error] usage req-classify: expected exactly one <req-id> (or --all)\n');
77
+ stderr.write(HELP);
78
+ return 2;
79
+ }
80
+
81
+ const projectRoot = await findProjectRoot(cwd);
82
+ if (!projectRoot) {
83
+ stderr.write('[error] usage no project root found (no rcf/manifest.json in this directory or any ancestor). Run `npx rcf init` to create and wire a project.\n');
84
+ return 2;
85
+ }
86
+
87
+ const walkResult = await walkTree({ projectRoot });
88
+ if (walkResult.errors.length > 0) {
89
+ stderr.write(`[warn] tree has ${walkResult.errors.length} pre-existing issue(s); proceeding - writes are validated against the post-write state (run 'rcf validate' for details)\n`);
90
+ }
91
+
92
+ const reqIds = [];
93
+ if (useAll) {
94
+ for (const [id, kind] of walkResult.tree.kindById) {
95
+ if (kind === 'requirement') reqIds.push(id);
96
+ }
97
+ reqIds.sort();
98
+ } else {
99
+ reqIds.push(positionals[0]);
100
+ }
101
+
102
+ /** @type {Array<{ reqId: string, block: object|null, changed: boolean }>} */
103
+ const results = [];
104
+ for (const reqId of reqIds) {
105
+ const outcome = await classifyAndPersistReq({
106
+ projectRoot,
107
+ tree: walkResult.tree,
108
+ reqId,
109
+ });
110
+ if (!outcome.ok) {
111
+ stderr.write(`[error] ${outcome.message}\n`);
112
+ return 3;
113
+ }
114
+ results.push({ reqId, block: outcome.block, changed: outcome.changed });
115
+ }
116
+
117
+ if (flags.json) {
118
+ stdout.write(`${JSON.stringify(results, null, 2)}\n`);
119
+ return 0;
120
+ }
121
+ if (!flags.quiet) {
122
+ for (const r of results) {
123
+ const verdict = r.block?.shapes?.join(', ') || 'none';
124
+ const reason = r.block?.reason ?? 'unknown';
125
+ const state = r.changed ? 'classified' : 'unchanged';
126
+ stdout.write(`${r.reqId}: shapes=[${verdict}] reason=${reason} (${state})\n`);
127
+ }
128
+ }
129
+ return 0;
130
+ }
131
+
132
+ // Silence linter on unused imports where the CLI helper does not need
133
+ // the RcfError formatters directly today; they are the canonical import
134
+ // surface for future error-path expansion.
135
+ void formatErrors; void writeUnexpectedFailure; void rcfError;
@@ -0,0 +1,295 @@
1
+ // `rcf review <fbs-id>` subcommand handler
2
+ // (verification-integrity-cluster-spec §5.5, §6).
3
+ //
4
+ // Runs the REVIEW-stage audit (test-theatre + mutation-sampling),
5
+ // writes the reviewAudit record to the manifest, and exits with:
6
+ // 0 on pass
7
+ // 4 on warn or block
8
+ // 3 on schema/tree validation failure
9
+ // 1 on IO / unexpected runtime failure
10
+ // 2 on usage error
11
+ //
12
+ // The mutation-sampling agent is injected via `deps.mutationRunner`;
13
+ // v1 default is a no-op runner that emits a valid schema record with
14
+ // a `notes` explaining that no runner was wired. `--skip-mutation`
15
+ // records the skip explicitly. `--dry-run` runs the audit without
16
+ // writing the record.
17
+
18
+ import { access, mkdir, readdir, rename, unlink, writeFile } from 'node:fs/promises';
19
+ import { join, dirname, relative } from 'node:path';
20
+ import { parseArgs } from 'node:util';
21
+ import process from 'node:process';
22
+
23
+ import { rcfError, writeUnexpectedFailure } from '#core/errors';
24
+ import { resolveTestPointers, validateDocument, walkTree } from '#core/store';
25
+
26
+ import { findProjectRoot } from '../view/index.js';
27
+ import {
28
+ auditTestTheatre,
29
+ composeReviewAuditRecord,
30
+ } from '../review/index.js';
31
+ import { auditUiBaselineDrift } from '../review/ui-baseline-drift.js';
32
+ import {
33
+ defaultMutationRunner,
34
+ defaultSizing,
35
+ resolveTestCommand,
36
+ skippedMutationRunner,
37
+ } from '../review/mutation.js';
38
+
39
+ const OPTION_SPEC = {
40
+ 'dry-run': { type: 'boolean' },
41
+ json: { type: 'boolean' },
42
+ 'skip-mutation': { type: 'boolean' },
43
+ 'test-cmd': { type: 'string' },
44
+ quiet: { type: 'boolean' },
45
+ help: { type: 'boolean' },
46
+ };
47
+
48
+ export const HELP = `Usage: rcf review <fbs-id> [options]
49
+
50
+ Run the REVIEW-stage audit (test-theatre + mutation-sampling) and write
51
+ the reviewAudit record to the manifest. Exit 4 on any warn or block
52
+ verdict, so the audit is a Stage 3 -> Stage 4 gate.
53
+
54
+ Options:
55
+ --dry-run Run the audit and print findings without
56
+ writing the reviewAudit record.
57
+ --json Emit the composed record to stdout as JSON.
58
+ --skip-mutation Record that mutation-sampling was skipped
59
+ without running it; useful when the harness
60
+ has no runner wired yet. The reviewAudit
61
+ record still validates.
62
+ --test-cmd <command> Test command for the mutation-sampling agent
63
+ (default: manifest.testCommand, else
64
+ 'npm test' when package.json exists).
65
+ --quiet Suppress non-error stdout
66
+ --help Print this help
67
+
68
+ Exit codes:
69
+ 0 audit passed
70
+ 1 IO / unexpected runtime failure
71
+ 2 usage error
72
+ 3 schema or tree validation refused the write
73
+ 4 audit warn or block verdict (Stage 4 refused)
74
+ `;
75
+
76
+ /**
77
+ * @param {string[]} argv
78
+ * @param {object} [deps]
79
+ * @returns {Promise<number>}
80
+ */
81
+ export async function main(argv, deps = {}) {
82
+ const stdout = deps.stdout ?? process.stdout;
83
+ const stderr = deps.stderr ?? process.stderr;
84
+ const cwd = deps.cwd ?? process.cwd();
85
+
86
+ let parsed;
87
+ try {
88
+ parsed = parseArgs({ args: argv, options: OPTION_SPEC, allowPositionals: true, strict: true });
89
+ } catch (err) {
90
+ stderr.write(`[error] usage ${err.message}\n`);
91
+ stderr.write(HELP);
92
+ return 2;
93
+ }
94
+ const flags = parsed.values;
95
+ if (flags.help) { stdout.write(HELP); return 0; }
96
+ if (parsed.positionals.length !== 1) {
97
+ stderr.write('[error] usage review: expected exactly one <fbs-id> positional\n');
98
+ return 2;
99
+ }
100
+ const fbsId = parsed.positionals[0];
101
+ if (!/^FBS-\d{3,}$/.test(fbsId)) {
102
+ stderr.write(`[error] usage review: expected an FBS id like FBS-011, got '${fbsId}'\n`);
103
+ return 2;
104
+ }
105
+
106
+ const projectRoot = await findProjectRoot(cwd);
107
+ if (!projectRoot) {
108
+ stderr.write('[error] usage no project root found (no rcf/manifest.json in this directory or any ancestor).\n');
109
+ return 2;
110
+ }
111
+
112
+ const walkResult = await walkTree({ projectRoot });
113
+ if (walkResult.errors.length > 0) {
114
+ stderr.write(`[warn] tree has ${walkResult.errors.length} pre-existing issue(s); the audit proceeds but coverage-drift findings may be noisy\n`);
115
+ }
116
+ const tree = walkResult.tree;
117
+ const fbs = tree.byId.get(fbsId);
118
+ if (!fbs || tree.kindById.get(fbsId) !== 'fbs') {
119
+ stderr.write(`[error] usage review: ${fbsId} not found or not an FBS\n`);
120
+ return 2;
121
+ }
122
+
123
+ // Test-pointer resolution feeds the testPointerBroken detector.
124
+ const testPointers = await resolveTestPointers({ projectRoot, tree });
125
+ const findings = auditTestTheatre({ tree, fbs, testPointers });
126
+
127
+ // Track B (ui-design-gate-0.7.0-spec §3.4): run the UI-baseline drift
128
+ // audit alongside the Track A test-theatre audit for uiBearing FBS.
129
+ // One brief, one worker, one record per FBS: the same reviewAudit
130
+ // record carries both Track A and Track B kinds (spec §12 O-12).
131
+ const listFiles = deps.listFiles ?? defaultListFiles(projectRoot);
132
+ const uiFindings = await auditUiBaselineDrift({
133
+ projectRoot,
134
+ fbs,
135
+ uiBaseline: tree.manifest?.uiBaseline ?? null,
136
+ listFiles,
137
+ });
138
+ for (const f of uiFindings) findings.push(f);
139
+
140
+ // Mutation-sampling.
141
+ let mutationRunner = deps.mutationRunner;
142
+ if (!mutationRunner) mutationRunner = flags['skip-mutation'] ? skippedMutationRunner : defaultMutationRunner;
143
+ const hasPackageJson = await fileExists(join(projectRoot, 'package.json'));
144
+ const { command: testCommand } = resolveTestCommand({
145
+ manifest: tree.manifest, cliFlag: flags['test-cmd'], hasPackageJson,
146
+ });
147
+ const fbsAcIds = new Set(fbs.acIds ?? []);
148
+ const coveringTs = (tree.testSuites ?? []).filter((ts) => (ts.acIds ?? []).some((a) => fbsAcIds.has(a)));
149
+ const runnerInput = {
150
+ fbsId, acIds: fbs.acIds ?? [], testSuites: coveringTs,
151
+ sizing: defaultSizing(), testCommand: testCommand ?? undefined, projectRoot,
152
+ };
153
+ let mutationSampling;
154
+ try {
155
+ const runnerOut = await mutationRunner(runnerInput);
156
+ mutationSampling = runnerOut?.record;
157
+ } catch (err) {
158
+ writeUnexpectedFailure(
159
+ rcfError({ kind: 'ioFailure', message: `review: mutation runner failed: ${err.message}`, stack: err.stack }),
160
+ stderr,
161
+ );
162
+ return 1;
163
+ }
164
+
165
+ const record = composeReviewAuditRecord({
166
+ tree, fbs, findings, mutationSampling,
167
+ now: deps.now ? new Date(deps.now()) : new Date(),
168
+ });
169
+
170
+ if (flags.json) stdout.write(`${JSON.stringify(record, null, 2)}\n`);
171
+
172
+ if (flags['dry-run']) {
173
+ if (!flags.quiet) {
174
+ stdout.write(`[dry-run] review ${fbsId}: ${findings.length} finding(s); verdict=${record.verdict}\n`);
175
+ for (const f of findings) {
176
+ stdout.write(` [${f.severity}] ${f.kind} ${f.tsId}${f.tcId ? '/' + f.tcId : ''}: ${f.detail}\n`);
177
+ }
178
+ }
179
+ return record.verdict === 'pass' ? 0 : 4;
180
+ }
181
+
182
+ // Write the record to the manifest.
183
+ const nextManifest = { ...(tree.manifest ?? {}) };
184
+ const existing = Array.isArray(nextManifest.reviewAudit) ? nextManifest.reviewAudit : [];
185
+ nextManifest.reviewAudit = [...existing, record];
186
+ if (testCommand && nextManifest.testCommand !== testCommand && !nextManifest.testCommand) {
187
+ // Optionally record the resolved testCommand when not already set,
188
+ // so a later re-audit inherits the same resolution without a flag.
189
+ nextManifest.testCommand = testCommand;
190
+ }
191
+ const relPath = 'rcf/manifest.json';
192
+ const validation = validateDocument({ doc: nextManifest, kind: 'manifest', filePath: relPath });
193
+ if (validation) {
194
+ stderr.write(`[error] validation ${validation.message}\n`);
195
+ return 3;
196
+ }
197
+ const absPath = join(projectRoot, 'rcf', 'manifest.json');
198
+ try {
199
+ await mkdir(dirname(absPath), { recursive: true });
200
+ const tmp = `${absPath}.tmp`;
201
+ await writeFile(tmp, `${JSON.stringify(nextManifest, null, 2)}\n`, 'utf8');
202
+ try {
203
+ await rename(tmp, absPath);
204
+ } catch (err) {
205
+ try { await unlink(tmp); } catch { /* ignore */ }
206
+ throw err;
207
+ }
208
+ } catch (err) {
209
+ writeUnexpectedFailure(
210
+ rcfError({ kind: 'ioFailure', message: `review: manifest write failed: ${err.message}`, filePath: relPath, stack: err.stack }),
211
+ stderr,
212
+ );
213
+ return 1;
214
+ }
215
+ if (!flags.quiet) {
216
+ stdout.write(`review ${fbsId}: wrote ${record.id} to rcf/manifest.json (${findings.length} findings, verdict=${record.verdict}).\n`);
217
+ }
218
+ return record.verdict === 'pass' ? 0 : 4;
219
+ }
220
+
221
+ async function fileExists(path) {
222
+ try {
223
+ await access(path);
224
+ return true;
225
+ } catch {
226
+ return false;
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Default file lister used by the UI-baseline drift audit. Walks the
232
+ * project tree once and matches each path against the supplied glob
233
+ * patterns. Supports the `**` suffix (recursive) and `*` mid-path
234
+ * wildcards. Deliberately minimal: the audit's callers pass either
235
+ * `src/ui/**` or `src/routes/**` in v1, so a formal glob library
236
+ * would be scope-creep. Returns project-root-relative paths.
237
+ *
238
+ * @param {string} projectRoot
239
+ * @returns {(patterns: string[]) => Promise<string[]>}
240
+ */
241
+ function defaultListFiles(projectRoot) {
242
+ return async (patterns) => {
243
+ const out = new Set();
244
+ for (const raw of Array.isArray(patterns) ? patterns : []) {
245
+ const pattern = String(raw);
246
+ const idx = pattern.indexOf('**');
247
+ let root;
248
+ let recursive;
249
+ let suffix;
250
+ if (idx >= 0) {
251
+ root = pattern.slice(0, Math.max(0, idx - 1));
252
+ recursive = true;
253
+ suffix = pattern.slice(idx + 2).replace(/^\//, '');
254
+ } else if (pattern.includes('*')) {
255
+ const slash = pattern.lastIndexOf('/');
256
+ root = slash >= 0 ? pattern.slice(0, slash) : '.';
257
+ recursive = false;
258
+ suffix = pattern.slice(slash + 1);
259
+ } else {
260
+ // Literal path; include as-is.
261
+ root = pattern;
262
+ recursive = false;
263
+ suffix = '';
264
+ }
265
+ const absRoot = join(projectRoot, root);
266
+ const collected = [];
267
+ await walkDir(absRoot, recursive, collected);
268
+ for (const abs of collected) {
269
+ const rel = relative(projectRoot, abs);
270
+ if (matchesSuffix(rel, suffix)) out.add(rel);
271
+ }
272
+ }
273
+ return [...out];
274
+ };
275
+ }
276
+
277
+ async function walkDir(root, recursive, out) {
278
+ let entries;
279
+ try { entries = await readdir(root, { withFileTypes: true }); } catch { return; }
280
+ for (const entry of entries) {
281
+ const abs = join(root, entry.name);
282
+ if (entry.isDirectory()) {
283
+ if (recursive) await walkDir(abs, true, out);
284
+ } else if (entry.isFile()) {
285
+ out.push(abs);
286
+ }
287
+ }
288
+ }
289
+
290
+ function matchesSuffix(rel, suffix) {
291
+ if (!suffix || suffix.length === 0) return true;
292
+ // Support a single `*` wildcard on the suffix segment.
293
+ const escaped = suffix.split('*').map((p) => p.replace(/[.+?^${}()|[\]\\]/g, '\\$&')).join('.*');
294
+ return new RegExp(`(?:^|/)${escaped}$`).test(rel);
295
+ }