@intentic/sandbox-contract 1.176.2 → 1.209.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 (340) hide show
  1. package/README.md +65 -0
  2. package/dist/agent-catalog.d.ts +15 -2
  3. package/dist/agent-catalog.d.ts.map +1 -1
  4. package/dist/agent-catalog.js +50 -1
  5. package/dist/agent-catalog.js.map +1 -1
  6. package/dist/agent-run-model.d.ts +3 -0
  7. package/dist/agent-run-model.d.ts.map +1 -0
  8. package/dist/agent-run-model.js +10 -0
  9. package/dist/agent-run-model.js.map +1 -0
  10. package/dist/capability-env.d.ts +2 -0
  11. package/dist/capability-env.d.ts.map +1 -0
  12. package/dist/capability-env.js +2 -0
  13. package/dist/capability-env.js.map +1 -0
  14. package/dist/chores/chores.d.ts +9 -0
  15. package/dist/chores/chores.d.ts.map +1 -1
  16. package/dist/chores/chores.js +257 -18
  17. package/dist/chores/chores.js.map +1 -1
  18. package/dist/chores/digest.js +0 -0
  19. package/dist/chores/digest.js.map +1 -1
  20. package/dist/chores/extension-update.d.ts +9 -0
  21. package/dist/chores/extension-update.d.ts.map +1 -0
  22. package/dist/chores/extension-update.js +15 -0
  23. package/dist/chores/extension-update.js.map +1 -0
  24. package/dist/chores/fix-deps.d.ts +10 -0
  25. package/dist/chores/fix-deps.d.ts.map +1 -0
  26. package/dist/chores/fix-deps.js +18 -0
  27. package/dist/chores/fix-deps.js.map +1 -0
  28. package/dist/chores/index.d.ts +7 -2
  29. package/dist/chores/index.d.ts.map +1 -1
  30. package/dist/chores/index.js +4 -1
  31. package/dist/chores/index.js.map +1 -1
  32. package/dist/chores/probes.d.ts +1 -0
  33. package/dist/chores/probes.d.ts.map +1 -1
  34. package/dist/chores/probes.js +131 -3
  35. package/dist/chores/probes.js.map +1 -1
  36. package/dist/chores/stack.d.ts +27 -0
  37. package/dist/chores/stack.d.ts.map +1 -0
  38. package/dist/chores/stack.js +121 -0
  39. package/dist/chores/stack.js.map +1 -0
  40. package/dist/chores/verdict.d.ts +2 -1
  41. package/dist/chores/verdict.d.ts.map +1 -1
  42. package/dist/chores/verdict.js +57 -7
  43. package/dist/chores/verdict.js.map +1 -1
  44. package/dist/contract-lock.d.ts +3 -0
  45. package/dist/contract-lock.d.ts.map +1 -0
  46. package/dist/contract-lock.js +35 -0
  47. package/dist/contract-lock.js.map +1 -0
  48. package/dist/contracts/activity.contract.d.ts +10 -0
  49. package/dist/contracts/activity.contract.d.ts.map +1 -1
  50. package/dist/contracts/agent.contract.d.ts +230 -20
  51. package/dist/contracts/agent.contract.d.ts.map +1 -1
  52. package/dist/contracts/agent.contract.js +2 -1
  53. package/dist/contracts/agent.contract.js.map +1 -1
  54. package/dist/contracts/agents.contract.d.ts +847 -1
  55. package/dist/contracts/agents.contract.d.ts.map +1 -1
  56. package/dist/contracts/agents.contract.js +3 -1
  57. package/dist/contracts/agents.contract.js.map +1 -1
  58. package/dist/contracts/automations.contract.d.ts +89 -0
  59. package/dist/contracts/automations.contract.d.ts.map +1 -1
  60. package/dist/contracts/automations.contract.js +3 -1
  61. package/dist/contracts/automations.contract.js.map +1 -1
  62. package/dist/contracts/capabilities.contract.d.ts +108 -32
  63. package/dist/contracts/capabilities.contract.d.ts.map +1 -1
  64. package/dist/contracts/capabilities.contract.js +5 -1
  65. package/dist/contracts/capabilities.contract.js.map +1 -1
  66. package/dist/contracts/chores.contract.d.ts +43 -0
  67. package/dist/contracts/chores.contract.d.ts.map +1 -1
  68. package/dist/contracts/ci.contract.d.ts +5 -0
  69. package/dist/contracts/ci.contract.d.ts.map +1 -1
  70. package/dist/contracts/ci.contract.js +2 -2
  71. package/dist/contracts/ci.contract.js.map +1 -1
  72. package/dist/contracts/claude.contract.d.ts +3 -14
  73. package/dist/contracts/claude.contract.d.ts.map +1 -1
  74. package/dist/contracts/claude.contract.js +2 -3
  75. package/dist/contracts/claude.contract.js.map +1 -1
  76. package/dist/contracts/drafts.contract.d.ts +4 -0
  77. package/dist/contracts/drafts.contract.d.ts.map +1 -1
  78. package/dist/contracts/endpoints.contract.d.ts +28 -12
  79. package/dist/contracts/endpoints.contract.d.ts.map +1 -1
  80. package/dist/contracts/endpoints.contract.js +9 -0
  81. package/dist/contracts/endpoints.contract.js.map +1 -1
  82. package/dist/contracts/extensions.contract.d.ts +399 -69
  83. package/dist/contracts/extensions.contract.d.ts.map +1 -1
  84. package/dist/contracts/extensions.contract.js +15 -1
  85. package/dist/contracts/extensions.contract.js.map +1 -1
  86. package/dist/contracts/git.contract.d.ts +209 -11
  87. package/dist/contracts/git.contract.d.ts.map +1 -1
  88. package/dist/contracts/git.contract.js +12 -2
  89. package/dist/contracts/git.contract.js.map +1 -1
  90. package/dist/contracts/grok.contract.d.ts +0 -13
  91. package/dist/contracts/grok.contract.d.ts.map +1 -1
  92. package/dist/contracts/grok.contract.js +1 -2
  93. package/dist/contracts/grok.contract.js.map +1 -1
  94. package/dist/contracts/host.contract.d.ts +39 -0
  95. package/dist/contracts/host.contract.d.ts.map +1 -1
  96. package/dist/contracts/host.contract.js +3 -2
  97. package/dist/contracts/host.contract.js.map +1 -1
  98. package/dist/contracts/loops.contract.d.ts +343 -0
  99. package/dist/contracts/loops.contract.d.ts.map +1 -0
  100. package/dist/contracts/loops.contract.js +11 -0
  101. package/dist/contracts/loops.contract.js.map +1 -0
  102. package/dist/contracts/panels.contract.d.ts +6 -0
  103. package/dist/contracts/panels.contract.d.ts.map +1 -1
  104. package/dist/contracts/personas.contract.d.ts +112 -0
  105. package/dist/contracts/personas.contract.d.ts.map +1 -0
  106. package/dist/contracts/personas.contract.js +13 -0
  107. package/dist/contracts/personas.contract.js.map +1 -0
  108. package/dist/contracts/ports.contract.d.ts +1 -0
  109. package/dist/contracts/ports.contract.d.ts.map +1 -1
  110. package/dist/contracts/prepush.contract.d.ts +1 -0
  111. package/dist/contracts/prepush.contract.d.ts.map +1 -1
  112. package/dist/contracts/prepush.contract.js.map +1 -1
  113. package/dist/contracts/{codex.contract.d.ts → providers.contract.d.ts} +11 -3
  114. package/dist/contracts/providers.contract.d.ts.map +1 -0
  115. package/dist/contracts/providers.contract.js +6 -0
  116. package/dist/contracts/providers.contract.js.map +1 -0
  117. package/dist/contracts/public.contract.d.ts +24 -0
  118. package/dist/contracts/public.contract.d.ts.map +1 -0
  119. package/dist/contracts/public.contract.js +8 -0
  120. package/dist/contracts/public.contract.js.map +1 -0
  121. package/dist/contracts/push.contract.d.ts +3 -3
  122. package/dist/contracts/secrets.contract.d.ts +9 -0
  123. package/dist/contracts/secrets.contract.d.ts.map +1 -1
  124. package/dist/contracts/sessions.contract.d.ts +16 -1
  125. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  126. package/dist/contracts/sessions.contract.js +1 -1
  127. package/dist/contracts/sessions.contract.js.map +1 -1
  128. package/dist/contracts/settings.contract.d.ts +352 -58
  129. package/dist/contracts/settings.contract.d.ts.map +1 -1
  130. package/dist/contracts/settings.contract.js +2 -1
  131. package/dist/contracts/settings.contract.js.map +1 -1
  132. package/dist/contracts/share.contract.d.ts +55 -0
  133. package/dist/contracts/share.contract.d.ts.map +1 -0
  134. package/dist/contracts/share.contract.js +9 -0
  135. package/dist/contracts/share.contract.js.map +1 -0
  136. package/dist/contracts/skills.contract.d.ts +41 -0
  137. package/dist/contracts/skills.contract.d.ts.map +1 -0
  138. package/dist/contracts/skills.contract.js +9 -0
  139. package/dist/contracts/skills.contract.js.map +1 -0
  140. package/dist/contracts/system.contract.d.ts +218 -22
  141. package/dist/contracts/system.contract.d.ts.map +1 -1
  142. package/dist/contracts/system.contract.js +10 -2
  143. package/dist/contracts/system.contract.js.map +1 -1
  144. package/dist/contracts/workflows.contract.d.ts +575 -0
  145. package/dist/contracts/workflows.contract.d.ts.map +1 -0
  146. package/dist/contracts/workflows.contract.js +13 -0
  147. package/dist/contracts/workflows.contract.js.map +1 -0
  148. package/dist/contracts/workspace.contract.d.ts +18 -4
  149. package/dist/contracts/workspace.contract.d.ts.map +1 -1
  150. package/dist/contracts/workspace.contract.js +6 -3
  151. package/dist/contracts/workspace.contract.js.map +1 -1
  152. package/dist/conversation-ids.d.ts +2 -0
  153. package/dist/conversation-ids.d.ts.map +1 -0
  154. package/dist/conversation-ids.js +113 -0
  155. package/dist/conversation-ids.js.map +1 -0
  156. package/dist/events.d.ts +523 -17
  157. package/dist/events.d.ts.map +1 -1
  158. package/dist/events.js +106 -13
  159. package/dist/events.js.map +1 -1
  160. package/dist/history-state.d.ts +3 -0
  161. package/dist/history-state.d.ts.map +1 -0
  162. package/dist/history-state.js +53 -0
  163. package/dist/history-state.js.map +1 -0
  164. package/dist/hostnames.d.ts +4 -0
  165. package/dist/hostnames.d.ts.map +1 -1
  166. package/dist/hostnames.js +7 -2
  167. package/dist/hostnames.js.map +1 -1
  168. package/dist/index.d.ts +3793 -478
  169. package/dist/index.d.ts.map +1 -1
  170. package/dist/index.js +41 -17
  171. package/dist/index.js.map +1 -1
  172. package/dist/listener-protocol.d.ts +77 -0
  173. package/dist/listener-protocol.d.ts.map +1 -0
  174. package/dist/listener-protocol.js +33 -0
  175. package/dist/listener-protocol.js.map +1 -0
  176. package/dist/model-order.d.ts +1 -0
  177. package/dist/model-order.d.ts.map +1 -1
  178. package/dist/model-order.js +16 -0
  179. package/dist/model-order.js.map +1 -1
  180. package/dist/output-fields.d.ts +30 -0
  181. package/dist/output-fields.d.ts.map +1 -0
  182. package/dist/output-fields.js +57 -0
  183. package/dist/output-fields.js.map +1 -0
  184. package/dist/path-refs.d.ts.map +1 -1
  185. package/dist/path-refs.js.map +1 -1
  186. package/dist/publish-drafts.d.ts +7 -0
  187. package/dist/publish-drafts.d.ts.map +1 -0
  188. package/dist/publish-drafts.js +20 -0
  189. package/dist/publish-drafts.js.map +1 -0
  190. package/dist/quick-model.d.ts +2 -1
  191. package/dist/quick-model.d.ts.map +1 -1
  192. package/dist/quick-model.js +18 -14
  193. package/dist/quick-model.js.map +1 -1
  194. package/dist/routes.d.ts +3 -0
  195. package/dist/routes.d.ts.map +1 -1
  196. package/dist/routes.js +63 -0
  197. package/dist/routes.js.map +1 -1
  198. package/dist/runtime-state.d.ts +33 -0
  199. package/dist/runtime-state.d.ts.map +1 -0
  200. package/dist/runtime-state.js +14 -0
  201. package/dist/runtime-state.js.map +1 -0
  202. package/dist/schemas.d.ts +4938 -866
  203. package/dist/schemas.d.ts.map +1 -1
  204. package/dist/schemas.js +1011 -192
  205. package/dist/schemas.js.map +1 -1
  206. package/dist/search-globs.d.ts +6 -0
  207. package/dist/search-globs.d.ts.map +1 -0
  208. package/dist/search-globs.js +39 -0
  209. package/dist/search-globs.js.map +1 -0
  210. package/dist/share-paths.d.ts +8 -0
  211. package/dist/share-paths.d.ts.map +1 -0
  212. package/dist/share-paths.js +21 -0
  213. package/dist/share-paths.js.map +1 -0
  214. package/dist/state-portability.d.ts +8 -0
  215. package/dist/state-portability.d.ts.map +1 -0
  216. package/dist/state-portability.js +4 -0
  217. package/dist/state-portability.js.map +1 -0
  218. package/dist/title.d.ts.map +1 -1
  219. package/dist/title.js +1 -1
  220. package/dist/title.js.map +1 -1
  221. package/dist/tunnel-ids.d.ts +1 -0
  222. package/dist/tunnel-ids.d.ts.map +1 -1
  223. package/dist/tunnel-ids.js +1 -0
  224. package/dist/tunnel-ids.js.map +1 -1
  225. package/dist/versions.d.ts +4 -0
  226. package/dist/versions.d.ts.map +1 -0
  227. package/dist/versions.js +15 -0
  228. package/dist/versions.js.map +1 -0
  229. package/dist/workflow-faults.d.ts +4 -0
  230. package/dist/workflow-faults.d.ts.map +1 -0
  231. package/dist/workflow-faults.js +115 -0
  232. package/dist/workflow-faults.js.map +1 -0
  233. package/dist/workspace-state.d.ts +282 -3
  234. package/dist/workspace-state.d.ts.map +1 -1
  235. package/dist/workspace-state.js +198 -12
  236. package/dist/workspace-state.js.map +1 -1
  237. package/package.json +30 -6
  238. package/src/agent-catalog.test.ts +147 -7
  239. package/src/agent-catalog.ts +233 -21
  240. package/src/agent-run-model.test.ts +60 -0
  241. package/src/agent-run-model.ts +51 -0
  242. package/src/capability-env.ts +10 -0
  243. package/src/capability-ledger.test.ts +110 -0
  244. package/src/chores/chores.test.ts +32 -0
  245. package/src/chores/chores.ts +456 -32
  246. package/src/chores/digest.ts +0 -0
  247. package/src/chores/extension-update.ts +37 -0
  248. package/src/chores/fix-deps.ts +40 -0
  249. package/src/chores/index.ts +7 -2
  250. package/src/chores/probes.test.ts +155 -3
  251. package/src/chores/probes.ts +209 -4
  252. package/src/chores/stack.test.ts +126 -0
  253. package/src/chores/stack.ts +244 -0
  254. package/src/chores/verdict.test.ts +357 -19
  255. package/src/chores/verdict.ts +102 -22
  256. package/src/contract-lock.test.ts +17 -0
  257. package/src/contract-lock.ts +66 -0
  258. package/src/contracts/agent.contract.ts +6 -0
  259. package/src/contracts/agents.contract.ts +12 -0
  260. package/src/contracts/automations.contract.ts +17 -1
  261. package/src/contracts/capabilities.contract.ts +22 -0
  262. package/src/contracts/ci.contract.ts +10 -2
  263. package/src/contracts/claude.contract.ts +8 -4
  264. package/src/contracts/endpoints.contract.ts +21 -0
  265. package/src/contracts/extensions.contract.ts +42 -0
  266. package/src/contracts/git.contract.ts +33 -6
  267. package/src/contracts/grok.contract.ts +4 -3
  268. package/src/contracts/host.contract.ts +13 -2
  269. package/src/contracts/loops.contract.ts +62 -0
  270. package/src/contracts/personas.contract.ts +56 -0
  271. package/src/contracts/prepush.contract.ts +4 -3
  272. package/src/contracts/providers.contract.ts +23 -0
  273. package/src/contracts/public.contract.ts +14 -0
  274. package/src/contracts/sessions.contract.ts +5 -1
  275. package/src/contracts/settings.contract.ts +5 -0
  276. package/src/contracts/share.contract.ts +20 -0
  277. package/src/contracts/skills.contract.ts +24 -0
  278. package/src/contracts/system.contract.ts +27 -3
  279. package/src/contracts/workflows.contract.ts +78 -0
  280. package/src/contracts/workspace.contract.ts +10 -2
  281. package/src/conversation-ids.test.ts +25 -0
  282. package/src/conversation-ids.ts +160 -0
  283. package/src/events.test.ts +20 -1
  284. package/src/events.ts +404 -43
  285. package/src/history-state.ts +129 -0
  286. package/src/hostnames.ts +20 -6
  287. package/src/index.ts +60 -17
  288. package/src/listener-protocol.ts +96 -0
  289. package/src/model-order.test.ts +32 -1
  290. package/src/model-order.ts +56 -3
  291. package/src/output-fields.ts +111 -0
  292. package/src/path-refs.test.ts +4 -4
  293. package/src/path-refs.ts +5 -5
  294. package/src/publish-drafts.ts +59 -0
  295. package/src/quick-model.test.ts +70 -27
  296. package/src/quick-model.ts +76 -40
  297. package/src/routes.test.ts +91 -3
  298. package/src/routes.ts +135 -0
  299. package/src/runtime-state.test.ts +45 -0
  300. package/src/runtime-state.ts +124 -0
  301. package/src/schemas.test.ts +16 -29
  302. package/src/schemas.ts +3526 -553
  303. package/src/search-globs.test.ts +46 -0
  304. package/src/search-globs.ts +76 -0
  305. package/src/share-paths.test.ts +48 -0
  306. package/src/share-paths.ts +68 -0
  307. package/src/state-portability.ts +56 -0
  308. package/src/title.test.ts +20 -1
  309. package/src/title.ts +12 -3
  310. package/src/tunnel-ids.ts +15 -1
  311. package/src/versions.test.ts +39 -0
  312. package/src/versions.ts +48 -0
  313. package/src/workflow-faults.test.ts +127 -0
  314. package/src/workflow-faults.ts +188 -0
  315. package/src/workspace-state.test.ts +189 -17
  316. package/src/workspace-state.ts +500 -30
  317. package/dist/contracts/codex.contract.d.ts.map +0 -1
  318. package/dist/contracts/codex.contract.js +0 -6
  319. package/dist/contracts/codex.contract.js.map +0 -1
  320. package/dist/contracts/gemini.contract.d.ts +0 -16
  321. package/dist/contracts/gemini.contract.d.ts.map +0 -1
  322. package/dist/contracts/gemini.contract.js +0 -6
  323. package/dist/contracts/gemini.contract.js.map +0 -1
  324. package/dist/contracts/kimi.contract.d.ts +0 -16
  325. package/dist/contracts/kimi.contract.d.ts.map +0 -1
  326. package/dist/contracts/kimi.contract.js +0 -6
  327. package/dist/contracts/kimi.contract.js.map +0 -1
  328. package/dist/contracts/komodo.contract.d.ts +0 -125
  329. package/dist/contracts/komodo.contract.d.ts.map +0 -1
  330. package/dist/contracts/komodo.contract.js +0 -14
  331. package/dist/contracts/komodo.contract.js.map +0 -1
  332. package/dist/contracts/memory.contract.d.ts +0 -34
  333. package/dist/contracts/memory.contract.d.ts.map +0 -1
  334. package/dist/contracts/memory.contract.js +0 -9
  335. package/dist/contracts/memory.contract.js.map +0 -1
  336. package/src/contracts/codex.contract.ts +0 -10
  337. package/src/contracts/gemini.contract.ts +0 -11
  338. package/src/contracts/kimi.contract.ts +0 -9
  339. package/src/contracts/komodo.contract.ts +0 -36
  340. package/src/contracts/memory.contract.ts +0 -13
@@ -0,0 +1,126 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { componentStem, frameworksOf, IDIOM_RULES, idiomRule, UI_FRAMEWORKS, usesTailwind } from "./stack.js";
3
+
4
+ /* The table's own invariants, and the name normaliser under it.
5
+ *
6
+ * Most of what is below guards a failure that CANNOT be seen by reading the table: a pattern is interpolated into
7
+ * a shell command that runs on someone else's machine at three in the morning, so a stray quote is not a typo
8
+ * anyone reviews — it is a probe that dies in a workspace nobody is watching, with a shell error for a reason. */
9
+
10
+ describe(`the patterns are safe to interpolate`, () => {
11
+ // The scan wraps every pattern and glob in shell single quotes. One apostrophe inside ends the quoting and
12
+ // hands the remainder of the regex to sh, which is how a pattern becomes a command.
13
+ test(`no pattern or glob contains an apostrophe`, () => {
14
+ for (const rule of IDIOM_RULES) {
15
+ expect(rule.pattern, rule.id).not.toContain(`'`);
16
+ for (const glob of rule.globs) {
17
+ expect(glob, rule.id).not.toContain(`'`);
18
+ }
19
+ }
20
+ });
21
+
22
+ // Not the dialect ripgrep will use, but it catches the unbalanced bracket and the stray backslash, which is
23
+ // what actually goes wrong when someone adds a rule.
24
+ test(`every pattern parses as a regex`, () => {
25
+ for (const rule of IDIOM_RULES) {
26
+ expect(() => new RegExp(rule.pattern), rule.id).not.toThrow();
27
+ }
28
+ });
29
+
30
+ /* Rust's regex crate has no lookaround, and getting it means ripgrep's -P, which is a compile-time option on
31
+ * the box the sweep happens to run on. A rule that seems to need one is asking a question about the FILE
32
+ * rather than about a line — which is what `absent` is. */
33
+ test(`no pattern uses a lookaround`, () => {
34
+ for (const rule of IDIOM_RULES) {
35
+ expect(rule.pattern, rule.id).not.toMatch(/\(\?<?[=!]/);
36
+ }
37
+ });
38
+
39
+ /* An absent rule's population is every file its globs match, so a glob that is merely broad on a normal rule
40
+ * is catastrophic on this one: `*.ts` would name every TypeScript file in the repository as legacy code. A
41
+ * component extension is safe because every file wearing it is the thing the migration is about. */
42
+ test(`an absent rule is scoped to a component file type, never to a whole language`, () => {
43
+ for (const rule of IDIOM_RULES.filter((candidate) => candidate.absent !== undefined)) {
44
+ for (const glob of rule.globs) {
45
+ expect([`*.ts`, `*.js`, `*.mts`, `*.cts`, `*.mjs`, `*.cjs`], rule.id).not.toContain(glob);
46
+ }
47
+ }
48
+ });
49
+
50
+ test(`every rule has a unique id and belongs to a framework in the table`, () => {
51
+ const ids = new Set(IDIOM_RULES.map((rule) => rule.id));
52
+ expect(ids.size).toBe(IDIOM_RULES.length);
53
+ for (const rule of IDIOM_RULES) {
54
+ expect(
55
+ UI_FRAMEWORKS.map((framework) => framework.id),
56
+ rule.id,
57
+ ).toContain(rule.framework);
58
+ }
59
+ });
60
+
61
+ // The chore names the replacement in its prompt; a rule without one would wake an agent, tell it what to stop
62
+ // doing and leave it to guess a destination.
63
+ test(`every rule names what replaced it`, () => {
64
+ for (const rule of IDIOM_RULES) {
65
+ expect(rule.replacement.length, rule.id).toBeGreaterThan(3);
66
+ }
67
+ });
68
+
69
+ test(`idiomRule finds a rule the scan reports, and admits when it cannot`, () => {
70
+ expect(idiomRule(`vue-options-api`)?.framework).toBe(`vue`);
71
+ expect(idiomRule(`from-a-newer-daemon`)).toBeUndefined();
72
+ });
73
+ });
74
+
75
+ describe(`recognising the stack`, () => {
76
+ test(`a framework is recognised from any manifest's dependency names`, () => {
77
+ expect(frameworksOf([`vue`, `vite`]).map((framework) => framework.id)).toEqual([`vue`]);
78
+ expect(frameworksOf([`react`, `@angular/core`]).map((framework) => framework.id)).toEqual([`react`, `angular`]);
79
+ expect(frameworksOf([`pino`, `zod`])).toEqual([]);
80
+ });
81
+
82
+ // A near-miss must not read as a hit: plenty of packages are named after the framework they plug into, and
83
+ // `@vueuse/core` in a repo with no Vue is a dependency somebody left behind rather than a Vue application.
84
+ test(`a package merely named after a framework is not that framework`, () => {
85
+ expect(frameworksOf([`@vueuse/core`, `react-hook-form`, `eslint-plugin-vue`])).toEqual([]);
86
+ });
87
+
88
+ test(`Tailwind is recognised on its own, without a framework`, () => {
89
+ expect(usesTailwind([`tailwindcss`])).toBe(true);
90
+ expect(usesTailwind([`@tailwindcss/typography`])).toBe(false);
91
+ });
92
+ });
93
+
94
+ /* THE NORMALISER, which is the whole evidence of the component-overlap chore and therefore the place a false
95
+ * finding would come from. Each case below is a family that must form, or one that must not. */
96
+ describe(`the name two components share`, () => {
97
+ test(`framework and qualifier noise falls away`, () => {
98
+ expect(componentStem(`src/components/Button.vue`)).toBe(`button`);
99
+ expect(componentStem(`src/ui/BaseButton.vue`)).toBe(`button`);
100
+ expect(componentStem(`src/legacy/ButtonV2.tsx`)).toBe(`button`);
101
+ expect(componentStem(`src/app/user-card.component.ts`)).toBe(`usercard`);
102
+ expect(componentStem(`src/UserCard.tsx`)).toBe(`usercard`);
103
+ });
104
+
105
+ // Every barrel file in the repository is called this. A family of forty is a fact about the convention, not
106
+ // about duplication, and it would be the largest finding in every repo that has one.
107
+ test(`index files never form a family`, () => {
108
+ expect(componentStem(`src/components/Button/index.tsx`)).toBeUndefined();
109
+ });
110
+
111
+ /* The trap in stripping a trailing number. `H1` and `H2` are different components and reduce to the same
112
+ * single letter, so the stem is only accepted when what survives is still long enough to mean something —
113
+ * otherwise the untouched name is kept and the two stay apart. */
114
+ test(`short names keep their digits rather than collapsing together`, () => {
115
+ expect(componentStem(`src/type/H1.tsx`)).toBe(`h1`);
116
+ expect(componentStem(`src/type/H2.tsx`)).toBe(`h2`);
117
+ expect(componentStem(`src/type/H1.tsx`)).not.toBe(componentStem(`src/type/H2.tsx`));
118
+ });
119
+
120
+ // Same guard on the prefix side: `Theme` begins with `the`, and stripping it would leave `me` and put Theme
121
+ // in a family with anything else that reduced to it.
122
+ test(`a word that merely starts with a qualifier is left alone`, () => {
123
+ expect(componentStem(`src/Theme.tsx`)).toBe(`theme`);
124
+ expect(componentStem(`src/TheHeader.vue`)).toBe(`header`);
125
+ });
126
+ });
@@ -0,0 +1,244 @@
1
+ /* WHAT THIS REPOSITORY IS BUILT WITH, and the patterns that follow from it — the table the UI chores are written
2
+ * against, kept apart from both the probe that runs it and the chores that read it.
3
+ *
4
+ * It sits in the middle on purpose. probes.ts composes ONE ripgrep sweep out of the rules below, and chores.ts
5
+ * turns the counts that come back into findings and prose. Put the rules in either of those and the other one
6
+ * grows a copy: the probe would hard-code patterns the chore has to describe in words, or the chore would restate
7
+ * globs the probe already walked. Both copies would then be free to disagree about what a component is.
8
+ *
9
+ * NOTHING HERE IS A VERDICT. A framework is a set of dependency names, an idiom is a regex and the name of what
10
+ * replaced it. Which of those amount to work worth doing is the chore book's judgement, made in chores.ts against
11
+ * the same three rules everything else there obeys.
12
+ *
13
+ * THE PATTERNS ARE RIPGREP'S DIALECT, and they carry two constraints that are not obvious from reading them:
14
+ *
15
+ * No literal apostrophe, ever. The scan command wraps each pattern in shell single quotes, so a `'` inside one
16
+ * would end the quoting and hand the rest of the regex to the shell. Match quotes as `[\x22\x27]` instead —
17
+ * Rust's regex crate reads those escapes, and the shell never sees a quote character at all. stack.test.ts
18
+ * enforces this, because the failure is a probe that dies at three in the morning in someone else's workspace
19
+ * rather than anything a reader would notice here.
20
+ *
21
+ * No lookaround. Rust's regex crate has none, and reaching for ripgrep's PCRE2 mode to get it would make the
22
+ * sweep depend on how the box's ripgrep was compiled. A rule that seems to need it is usually asking a question
23
+ * about the FILE rather than about a line — see `absent` below, which is what that question actually is. */
24
+
25
+ export interface UiFramework {
26
+ readonly id: string;
27
+ readonly label: string;
28
+ // The dependency names that mean "this repository is built with it". Any one of them is enough.
29
+ readonly packages: readonly string[];
30
+ }
31
+
32
+ /* The three the maintenance surface knows how to say something specific about. Deliberately not "every framework
33
+ * with a npm package": a framework earns a row here by having idiom rules underneath it, and a table entry with
34
+ * no rules would let a chore announce it recognised Svelte and then have nothing to report. */
35
+ export const UI_FRAMEWORKS: readonly UiFramework[] = [
36
+ { id: `react`, label: `React`, packages: [`react`] },
37
+ { id: `vue`, label: `Vue`, packages: [`vue`] },
38
+ { id: `angular`, label: `Angular`, packages: [`@angular/core`] },
39
+ ];
40
+
41
+ // Tailwind is not in the table above because it is not a UI framework and does not own any idiom rules — it is a
42
+ // styling system that any of the three can be wearing, and it gates exactly one chore.
43
+ export const TAILWIND_PACKAGES: readonly string[] = [`tailwindcss`];
44
+
45
+ export const frameworksOf = (deps: readonly string[]): UiFramework[] =>
46
+ UI_FRAMEWORKS.filter((framework) => framework.packages.some((name) => deps.includes(name)));
47
+
48
+ export const usesTailwind = (deps: readonly string[]): boolean => TAILWIND_PACKAGES.some((name) => deps.includes(name));
49
+
50
+ /* Directories a UI scan must never walk, spelled out rather than left to the repository's .gitignore. ripgrep
51
+ * does honour .gitignore, and relying on that is how a repo whose build output is not ignored gets its own
52
+ * minified bundle reported back to it as a thousand hard-coded colours. Tests, specs and stories are excluded for
53
+ * a different reason: they are component-shaped files that are not components, and counting them would put a
54
+ * fixture at the top of every finding. */
55
+ export const SCAN_IGNORES: readonly string[] = [
56
+ `!**/node_modules/**`,
57
+ `!**/dist/**`,
58
+ `!**/build/**`,
59
+ `!**/.next/**`,
60
+ `!**/out/**`,
61
+ `!**/coverage/**`,
62
+ `!**/vendor/**`,
63
+ `!**/generated/**`,
64
+ `!**/*.{test,spec,stories}.*`,
65
+ ];
66
+
67
+ // What counts as a component file, across all three frameworks at once. The sweep cannot vary by repository — a
68
+ // probe's command is a fixed string — so it asks for all of them and a Vue-only repo simply has no `.tsx` files.
69
+ export const COMPONENT_GLOBS: readonly string[] = [`*.vue`, `*.tsx`, `*.jsx`, `*.component.ts`];
70
+
71
+ // Where a Tailwind class can appear. Wider than COMPONENT_GLOBS because a class list lives in markup as often as
72
+ // in a component — an Angular template and a plain .html page both style with the same utilities.
73
+ export const MARKUP_GLOBS: readonly string[] = [`*.vue`, `*.tsx`, `*.jsx`, `*.html`, `*.svelte`, `*.astro`];
74
+
75
+ /* THE DESIGN SYSTEM BYPASS. Not "any arbitrary value" — `grid-cols-[1fr_auto]` and `w-[calc(100%-2rem)]` are
76
+ * Tailwind working as designed, and a chore that counted them would be objecting to the feature rather than to
77
+ * anything wrong. What this matches is the two arbitrary values that route around a decision the theme already
78
+ * made: a colour that is not in the palette, and a pixel size that is not on the spacing or type scale.
79
+ *
80
+ * The leading `-` is load-bearing. It anchors the match to a utility prefix (`bg-`, `text-`, `w-`), so a bare
81
+ * `[...]` in ordinary prose or an array index cannot be mistaken for a class. */
82
+ export const BYPASS_PATTERN = `-\\[(#[0-9a-fA-F]{3,8}|(rgb|hsl)a?\\(|[0-9]+(\\.[0-9]+)?px)`;
83
+
84
+ export interface IdiomRule {
85
+ readonly id: string;
86
+ // Which framework's migration this belongs to, so a finding can be grouped under the thing that moved on.
87
+ readonly framework: string;
88
+ // What the repository still has, named as the reader would name it.
89
+ readonly label: string;
90
+ // What replaced it. Carried so the prompt can say where to go rather than only what to leave — an agent told
91
+ // "you still use NgModule" and nothing else will pick a destination, and it may not pick this one.
92
+ readonly replacement: string;
93
+ readonly pattern: string;
94
+ readonly globs: readonly string[];
95
+ /* THE IDIOM IS THE PATTERN BEING MISSING, not present — `pattern` names the NEW way, and the file is on the
96
+ * old one precisely because the new one does not appear in it anywhere. The sweep spells this
97
+ * `--files-without-match`.
98
+ *
99
+ * This exists because the alternative got it wrong in a way that is worth remembering. "A Vue file not using
100
+ * <script setup>" was first written as a lookahead over `<script`, which matches per LINE: a migrated
101
+ * component with a second plain `<script>` block for defineOptions, or one that merely mentions `<script` in
102
+ * a comment, both read as un-migrated. It reported five files in an application whose 167 SFCs are every one
103
+ * of them migrated. The question was never "is there a line like this" — it is "does this file contain the
104
+ * new idiom at all", which is one flag rather than a cleverer regex, and it costs no PCRE2.
105
+ *
106
+ * The globs carry more weight on an absent rule than on a normal one, and narrowly is the only safe way to
107
+ * write them: the population is every file they match, so `*.ts` on an absent rule reports the entire
108
+ * repository. Keep them to the file type the migration is actually about. */
109
+ readonly absent?: true;
110
+ }
111
+
112
+ /* THE IDIOMS THEIR OWN MAINTAINERS HAVE MOVED ON FROM. Every rule here names something the framework's own
113
+ * documentation now steers people away from, and every one of them still works — which is exactly why they
114
+ * accumulate, and why no editor and no linter will bring them up unprompted.
115
+ *
116
+ * High confidence over coverage. Each pattern is one a reader can check by eye against a file, and the ones that
117
+ * would need real parsing to get right are left out rather than approximated: a rule that is wrong a third of the
118
+ * time trains people to stop reading the row it appears in. */
119
+ export const IDIOM_RULES: readonly IdiomRule[] = [
120
+ {
121
+ id: `react-class-component`,
122
+ framework: `react`,
123
+ label: `class components`,
124
+ replacement: `function components with hooks`,
125
+ pattern: `extends\\s+(React\\.)?(Pure)?Component\\b`,
126
+ globs: [`*.tsx`, `*.jsx`],
127
+ },
128
+ {
129
+ id: `react-legacy-render`,
130
+ framework: `react`,
131
+ label: `the legacy ReactDOM.render entry point`,
132
+ replacement: `createRoot from react-dom/client`,
133
+ pattern: `ReactDOM\\.render\\(`,
134
+ globs: [`*.tsx`, `*.jsx`, `*.ts`, `*.js`],
135
+ },
136
+ {
137
+ id: `react-unsafe-lifecycle`,
138
+ framework: `react`,
139
+ // Named as what they are rather than by the UNSAFE_ prefix: a reader who has never renamed one would not
140
+ // recognise "UNSAFE_componentWillMount" as describing their own file.
141
+ label: `the pre-16.3 lifecycle methods`,
142
+ replacement: `effects, or the UNSAFE_ prefixed names if the behaviour is genuinely wanted`,
143
+ pattern: `\\bcomponentWill(Mount|ReceiveProps|Update)\\b`,
144
+ globs: [`*.tsx`, `*.jsx`],
145
+ },
146
+ {
147
+ id: `react-prop-types`,
148
+ framework: `react`,
149
+ label: `runtime prop-types`,
150
+ replacement: `the component's own TypeScript props type`,
151
+ pattern: `from\\s+[\\x22\\x27]prop-types[\\x22\\x27]`,
152
+ globs: [`*.tsx`, `*.jsx`],
153
+ },
154
+ {
155
+ id: `vue-options-api`,
156
+ framework: `vue`,
157
+ label: `the Options API`,
158
+ replacement: `<script setup> with the Composition API`,
159
+ // The new idiom, inverted by `absent` below — an SFC that never opens a `<script setup>` tag is still on
160
+ // the old one. A file with no script block at all is swept up too, and that is the honest reading: it has
161
+ // not been migrated because there was nothing there to migrate.
162
+ pattern: `<script[^>]*\\bsetup\\b`,
163
+ globs: [`*.vue`],
164
+ absent: true,
165
+ },
166
+ {
167
+ id: `vue-2-lifecycle`,
168
+ framework: `vue`,
169
+ label: `the Vue 2 teardown hooks`,
170
+ replacement: `beforeUnmount and unmounted`,
171
+ pattern: `\\b(beforeDestroy|destroyed)\\s*[(:]`,
172
+ globs: [`*.vue`, `*.ts`, `*.js`],
173
+ },
174
+ {
175
+ id: `vue-global-api`,
176
+ framework: `vue`,
177
+ label: `the Vue 2 global constructor`,
178
+ replacement: `createApp and defineComponent`,
179
+ pattern: `\\b(new\\s+Vue\\(|Vue\\.extend\\()`,
180
+ globs: [`*.vue`, `*.ts`, `*.js`],
181
+ },
182
+ {
183
+ id: `angular-ngmodule`,
184
+ framework: `angular`,
185
+ label: `NgModule declarations`,
186
+ replacement: `standalone components`,
187
+ pattern: `@NgModule\\(`,
188
+ globs: [`*.ts`],
189
+ },
190
+ {
191
+ id: `angular-structural-directives`,
192
+ framework: `angular`,
193
+ label: `the structural directives`,
194
+ replacement: `the built-in control flow blocks`,
195
+ pattern: `\\*ng(If|For|Switch)\\b`,
196
+ globs: [`*.html`, `*.ts`],
197
+ },
198
+ {
199
+ id: `angular-module-providers`,
200
+ framework: `angular`,
201
+ label: `the module-based providers`,
202
+ replacement: `the provide* functions in the application config`,
203
+ pattern: `\\b(HttpClientModule|BrowserAnimationsModule|RouterModule\\.forRoot)\\b`,
204
+ globs: [`*.ts`],
205
+ },
206
+ ];
207
+
208
+ export const idiomRule = (id: string): IdiomRule | undefined => IDIOM_RULES.find((rule) => rule.id === id);
209
+
210
+ /* Two tools naming the same file two ways. The scan reports repo-relative paths because that is what ripgrep
211
+ * prints; jscpd prints whatever it was handed, which for a `.` scan is the same path behind a `./`. Normalising
212
+ * here rather than at each comparison keeps the component-overlap chore from quietly matching nothing because one
213
+ * side had two extra characters. */
214
+ export const normalizePath = (path: string): string => path.replace(/^\.\//, ``);
215
+
216
+ // Below this a stem is too short to have survived the stripping above with its meaning intact — `H1` and `H2`
217
+ // would both reduce to `h` and read as one family of heading components that are not duplicates of anything.
218
+ const MIN_STEM = 3;
219
+
220
+ const QUALIFIER_PREFIX = /^(base|the)/;
221
+ const QUALIFIER_SUFFIX = /(v[0-9]+|new|old|legacy|copy|component|[0-9]+)$/;
222
+
223
+ /* THE NAME TWO COMPONENTS SHARE WHEN THEY ARE THE SAME COMPONENT TWICE — or `undefined` when the file has no
224
+ * name worth comparing.
225
+ *
226
+ * This is a normaliser, not a similarity score, and that is the point: it answers a question the reader can check
227
+ * by eye. `BaseButton.vue` and `ButtonV2.tsx` both reduce to `button`, so a panel claiming they are the same
228
+ * component is making a claim anyone can agree or disagree with in a second. A fuzzy distance would be right more
229
+ * often and checkable never, and an unarguable finding is one nobody can improve.
230
+ *
231
+ * `index` is dropped rather than normalised. Every barrel file in the repository is called it, and a family of
232
+ * forty index files is a finding about the naming convention rather than about any duplication. */
233
+ export const componentStem = (path: string): string | undefined => {
234
+ const file = normalizePath(path).split(`/`).pop() ?? ``;
235
+ // `.component.ts` loses both suffixes, `.vue` loses one — taking everything before the first dot handles both
236
+ // without a table, since a component's name is never the part after a dot.
237
+ const base = (file.split(`.`)[0] ?? ``).toLowerCase().replace(/[^a-z0-9]/g, ``);
238
+ if (base === `` || base === `index`) {
239
+ return undefined;
240
+ }
241
+ const withoutSuffix = base.replace(QUALIFIER_SUFFIX, ``);
242
+ const stem = (withoutSuffix.length >= MIN_STEM ? withoutSuffix : base).replace(QUALIFIER_PREFIX, ``);
243
+ return stem.length >= MIN_STEM ? stem : base;
244
+ };