@intentic/sandbox-contract 1.176.3 → 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 (334) 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.map +1 -1
  33. package/dist/chores/probes.js +127 -3
  34. package/dist/chores/probes.js.map +1 -1
  35. package/dist/chores/stack.d.ts +27 -0
  36. package/dist/chores/stack.d.ts.map +1 -0
  37. package/dist/chores/stack.js +121 -0
  38. package/dist/chores/stack.js.map +1 -0
  39. package/dist/chores/verdict.d.ts +2 -1
  40. package/dist/chores/verdict.d.ts.map +1 -1
  41. package/dist/chores/verdict.js +53 -6
  42. package/dist/chores/verdict.js.map +1 -1
  43. package/dist/contract-lock.d.ts +3 -0
  44. package/dist/contract-lock.d.ts.map +1 -0
  45. package/dist/contract-lock.js +35 -0
  46. package/dist/contract-lock.js.map +1 -0
  47. package/dist/contracts/activity.contract.d.ts +10 -0
  48. package/dist/contracts/activity.contract.d.ts.map +1 -1
  49. package/dist/contracts/agent.contract.d.ts +230 -20
  50. package/dist/contracts/agent.contract.d.ts.map +1 -1
  51. package/dist/contracts/agent.contract.js +2 -1
  52. package/dist/contracts/agent.contract.js.map +1 -1
  53. package/dist/contracts/agents.contract.d.ts +847 -1
  54. package/dist/contracts/agents.contract.d.ts.map +1 -1
  55. package/dist/contracts/agents.contract.js +3 -1
  56. package/dist/contracts/agents.contract.js.map +1 -1
  57. package/dist/contracts/automations.contract.d.ts +89 -0
  58. package/dist/contracts/automations.contract.d.ts.map +1 -1
  59. package/dist/contracts/automations.contract.js +3 -1
  60. package/dist/contracts/automations.contract.js.map +1 -1
  61. package/dist/contracts/capabilities.contract.d.ts +108 -32
  62. package/dist/contracts/capabilities.contract.d.ts.map +1 -1
  63. package/dist/contracts/capabilities.contract.js +5 -1
  64. package/dist/contracts/capabilities.contract.js.map +1 -1
  65. package/dist/contracts/chores.contract.d.ts +43 -0
  66. package/dist/contracts/chores.contract.d.ts.map +1 -1
  67. package/dist/contracts/ci.contract.d.ts +5 -0
  68. package/dist/contracts/ci.contract.d.ts.map +1 -1
  69. package/dist/contracts/ci.contract.js +2 -2
  70. package/dist/contracts/ci.contract.js.map +1 -1
  71. package/dist/contracts/claude.contract.d.ts +3 -14
  72. package/dist/contracts/claude.contract.d.ts.map +1 -1
  73. package/dist/contracts/claude.contract.js +2 -3
  74. package/dist/contracts/claude.contract.js.map +1 -1
  75. package/dist/contracts/drafts.contract.d.ts +4 -0
  76. package/dist/contracts/drafts.contract.d.ts.map +1 -1
  77. package/dist/contracts/endpoints.contract.d.ts +28 -12
  78. package/dist/contracts/endpoints.contract.d.ts.map +1 -1
  79. package/dist/contracts/endpoints.contract.js +9 -0
  80. package/dist/contracts/endpoints.contract.js.map +1 -1
  81. package/dist/contracts/extensions.contract.d.ts +399 -69
  82. package/dist/contracts/extensions.contract.d.ts.map +1 -1
  83. package/dist/contracts/extensions.contract.js +15 -1
  84. package/dist/contracts/extensions.contract.js.map +1 -1
  85. package/dist/contracts/git.contract.d.ts +209 -11
  86. package/dist/contracts/git.contract.d.ts.map +1 -1
  87. package/dist/contracts/git.contract.js +12 -2
  88. package/dist/contracts/git.contract.js.map +1 -1
  89. package/dist/contracts/grok.contract.d.ts +0 -13
  90. package/dist/contracts/grok.contract.d.ts.map +1 -1
  91. package/dist/contracts/grok.contract.js +1 -2
  92. package/dist/contracts/grok.contract.js.map +1 -1
  93. package/dist/contracts/host.contract.d.ts +39 -0
  94. package/dist/contracts/host.contract.d.ts.map +1 -1
  95. package/dist/contracts/host.contract.js +3 -2
  96. package/dist/contracts/host.contract.js.map +1 -1
  97. package/dist/contracts/loops.contract.d.ts +343 -0
  98. package/dist/contracts/loops.contract.d.ts.map +1 -0
  99. package/dist/contracts/loops.contract.js +11 -0
  100. package/dist/contracts/loops.contract.js.map +1 -0
  101. package/dist/contracts/panels.contract.d.ts +6 -0
  102. package/dist/contracts/panels.contract.d.ts.map +1 -1
  103. package/dist/contracts/personas.contract.d.ts +112 -0
  104. package/dist/contracts/personas.contract.d.ts.map +1 -0
  105. package/dist/contracts/personas.contract.js +13 -0
  106. package/dist/contracts/personas.contract.js.map +1 -0
  107. package/dist/contracts/ports.contract.d.ts +1 -0
  108. package/dist/contracts/ports.contract.d.ts.map +1 -1
  109. package/dist/contracts/prepush.contract.d.ts +1 -0
  110. package/dist/contracts/prepush.contract.d.ts.map +1 -1
  111. package/dist/contracts/prepush.contract.js.map +1 -1
  112. package/dist/contracts/{codex.contract.d.ts → providers.contract.d.ts} +11 -3
  113. package/dist/contracts/providers.contract.d.ts.map +1 -0
  114. package/dist/contracts/providers.contract.js +6 -0
  115. package/dist/contracts/providers.contract.js.map +1 -0
  116. package/dist/contracts/public.contract.d.ts +24 -0
  117. package/dist/contracts/public.contract.d.ts.map +1 -0
  118. package/dist/contracts/public.contract.js +8 -0
  119. package/dist/contracts/public.contract.js.map +1 -0
  120. package/dist/contracts/push.contract.d.ts +3 -3
  121. package/dist/contracts/secrets.contract.d.ts +9 -0
  122. package/dist/contracts/secrets.contract.d.ts.map +1 -1
  123. package/dist/contracts/sessions.contract.d.ts +16 -1
  124. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  125. package/dist/contracts/sessions.contract.js +1 -1
  126. package/dist/contracts/sessions.contract.js.map +1 -1
  127. package/dist/contracts/settings.contract.d.ts +352 -58
  128. package/dist/contracts/settings.contract.d.ts.map +1 -1
  129. package/dist/contracts/settings.contract.js +2 -1
  130. package/dist/contracts/settings.contract.js.map +1 -1
  131. package/dist/contracts/share.contract.d.ts +55 -0
  132. package/dist/contracts/share.contract.d.ts.map +1 -0
  133. package/dist/contracts/share.contract.js +9 -0
  134. package/dist/contracts/share.contract.js.map +1 -0
  135. package/dist/contracts/skills.contract.d.ts +41 -0
  136. package/dist/contracts/skills.contract.d.ts.map +1 -0
  137. package/dist/contracts/skills.contract.js +9 -0
  138. package/dist/contracts/skills.contract.js.map +1 -0
  139. package/dist/contracts/system.contract.d.ts +204 -8
  140. package/dist/contracts/system.contract.d.ts.map +1 -1
  141. package/dist/contracts/system.contract.js +10 -2
  142. package/dist/contracts/system.contract.js.map +1 -1
  143. package/dist/contracts/workflows.contract.d.ts +575 -0
  144. package/dist/contracts/workflows.contract.d.ts.map +1 -0
  145. package/dist/contracts/workflows.contract.js +13 -0
  146. package/dist/contracts/workflows.contract.js.map +1 -0
  147. package/dist/contracts/workspace.contract.d.ts +18 -4
  148. package/dist/contracts/workspace.contract.d.ts.map +1 -1
  149. package/dist/contracts/workspace.contract.js +6 -3
  150. package/dist/contracts/workspace.contract.js.map +1 -1
  151. package/dist/conversation-ids.d.ts +2 -0
  152. package/dist/conversation-ids.d.ts.map +1 -0
  153. package/dist/conversation-ids.js +113 -0
  154. package/dist/conversation-ids.js.map +1 -0
  155. package/dist/events.d.ts +523 -17
  156. package/dist/events.d.ts.map +1 -1
  157. package/dist/events.js +106 -13
  158. package/dist/events.js.map +1 -1
  159. package/dist/history-state.d.ts.map +1 -1
  160. package/dist/history-state.js +10 -0
  161. package/dist/history-state.js.map +1 -1
  162. package/dist/hostnames.d.ts +4 -0
  163. package/dist/hostnames.d.ts.map +1 -1
  164. package/dist/hostnames.js +7 -2
  165. package/dist/hostnames.js.map +1 -1
  166. package/dist/index.d.ts +3791 -478
  167. package/dist/index.d.ts.map +1 -1
  168. package/dist/index.js +39 -17
  169. package/dist/index.js.map +1 -1
  170. package/dist/listener-protocol.d.ts +77 -0
  171. package/dist/listener-protocol.d.ts.map +1 -0
  172. package/dist/listener-protocol.js +33 -0
  173. package/dist/listener-protocol.js.map +1 -0
  174. package/dist/model-order.d.ts +1 -0
  175. package/dist/model-order.d.ts.map +1 -1
  176. package/dist/model-order.js +16 -0
  177. package/dist/model-order.js.map +1 -1
  178. package/dist/output-fields.d.ts +30 -0
  179. package/dist/output-fields.d.ts.map +1 -0
  180. package/dist/output-fields.js +57 -0
  181. package/dist/output-fields.js.map +1 -0
  182. package/dist/path-refs.d.ts.map +1 -1
  183. package/dist/path-refs.js.map +1 -1
  184. package/dist/publish-drafts.d.ts +7 -0
  185. package/dist/publish-drafts.d.ts.map +1 -0
  186. package/dist/publish-drafts.js +20 -0
  187. package/dist/publish-drafts.js.map +1 -0
  188. package/dist/quick-model.d.ts +2 -1
  189. package/dist/quick-model.d.ts.map +1 -1
  190. package/dist/quick-model.js +18 -14
  191. package/dist/quick-model.js.map +1 -1
  192. package/dist/routes.d.ts +3 -0
  193. package/dist/routes.d.ts.map +1 -1
  194. package/dist/routes.js +63 -0
  195. package/dist/routes.js.map +1 -1
  196. package/dist/runtime-state.d.ts +33 -0
  197. package/dist/runtime-state.d.ts.map +1 -0
  198. package/dist/runtime-state.js +14 -0
  199. package/dist/runtime-state.js.map +1 -0
  200. package/dist/schemas.d.ts +4886 -851
  201. package/dist/schemas.d.ts.map +1 -1
  202. package/dist/schemas.js +993 -192
  203. package/dist/schemas.js.map +1 -1
  204. package/dist/search-globs.d.ts +6 -0
  205. package/dist/search-globs.d.ts.map +1 -0
  206. package/dist/search-globs.js +39 -0
  207. package/dist/search-globs.js.map +1 -0
  208. package/dist/share-paths.d.ts +8 -0
  209. package/dist/share-paths.d.ts.map +1 -0
  210. package/dist/share-paths.js +21 -0
  211. package/dist/share-paths.js.map +1 -0
  212. package/dist/title.d.ts.map +1 -1
  213. package/dist/title.js +1 -1
  214. package/dist/title.js.map +1 -1
  215. package/dist/tunnel-ids.d.ts +1 -0
  216. package/dist/tunnel-ids.d.ts.map +1 -1
  217. package/dist/tunnel-ids.js +1 -0
  218. package/dist/tunnel-ids.js.map +1 -1
  219. package/dist/versions.d.ts +4 -0
  220. package/dist/versions.d.ts.map +1 -0
  221. package/dist/versions.js +15 -0
  222. package/dist/versions.js.map +1 -0
  223. package/dist/workflow-faults.d.ts +4 -0
  224. package/dist/workflow-faults.d.ts.map +1 -0
  225. package/dist/workflow-faults.js +115 -0
  226. package/dist/workflow-faults.js.map +1 -0
  227. package/dist/workspace-state.d.ts +280 -1
  228. package/dist/workspace-state.d.ts.map +1 -1
  229. package/dist/workspace-state.js +163 -21
  230. package/dist/workspace-state.js.map +1 -1
  231. package/package.json +30 -6
  232. package/src/agent-catalog.test.ts +147 -7
  233. package/src/agent-catalog.ts +233 -21
  234. package/src/agent-run-model.test.ts +60 -0
  235. package/src/agent-run-model.ts +51 -0
  236. package/src/capability-env.ts +10 -0
  237. package/src/capability-ledger.test.ts +110 -0
  238. package/src/chores/chores.test.ts +32 -0
  239. package/src/chores/chores.ts +456 -32
  240. package/src/chores/digest.ts +0 -0
  241. package/src/chores/extension-update.ts +37 -0
  242. package/src/chores/fix-deps.ts +40 -0
  243. package/src/chores/index.ts +7 -2
  244. package/src/chores/probes.test.ts +155 -3
  245. package/src/chores/probes.ts +200 -4
  246. package/src/chores/stack.test.ts +126 -0
  247. package/src/chores/stack.ts +244 -0
  248. package/src/chores/verdict.test.ts +357 -19
  249. package/src/chores/verdict.ts +95 -21
  250. package/src/contract-lock.test.ts +17 -0
  251. package/src/contract-lock.ts +66 -0
  252. package/src/contracts/agent.contract.ts +6 -0
  253. package/src/contracts/agents.contract.ts +12 -0
  254. package/src/contracts/automations.contract.ts +17 -1
  255. package/src/contracts/capabilities.contract.ts +22 -0
  256. package/src/contracts/ci.contract.ts +10 -2
  257. package/src/contracts/claude.contract.ts +8 -4
  258. package/src/contracts/endpoints.contract.ts +21 -0
  259. package/src/contracts/extensions.contract.ts +42 -0
  260. package/src/contracts/git.contract.ts +33 -6
  261. package/src/contracts/grok.contract.ts +4 -3
  262. package/src/contracts/host.contract.ts +13 -2
  263. package/src/contracts/loops.contract.ts +62 -0
  264. package/src/contracts/personas.contract.ts +56 -0
  265. package/src/contracts/prepush.contract.ts +4 -3
  266. package/src/contracts/providers.contract.ts +23 -0
  267. package/src/contracts/public.contract.ts +14 -0
  268. package/src/contracts/sessions.contract.ts +5 -1
  269. package/src/contracts/settings.contract.ts +5 -0
  270. package/src/contracts/share.contract.ts +20 -0
  271. package/src/contracts/skills.contract.ts +24 -0
  272. package/src/contracts/system.contract.ts +27 -3
  273. package/src/contracts/workflows.contract.ts +78 -0
  274. package/src/contracts/workspace.contract.ts +10 -2
  275. package/src/conversation-ids.test.ts +25 -0
  276. package/src/conversation-ids.ts +160 -0
  277. package/src/events.test.ts +20 -1
  278. package/src/events.ts +404 -43
  279. package/src/history-state.ts +30 -1
  280. package/src/hostnames.ts +20 -6
  281. package/src/index.ts +58 -17
  282. package/src/listener-protocol.ts +96 -0
  283. package/src/model-order.test.ts +32 -1
  284. package/src/model-order.ts +56 -3
  285. package/src/output-fields.ts +111 -0
  286. package/src/path-refs.test.ts +4 -4
  287. package/src/path-refs.ts +5 -5
  288. package/src/publish-drafts.ts +59 -0
  289. package/src/quick-model.test.ts +70 -27
  290. package/src/quick-model.ts +76 -40
  291. package/src/routes.test.ts +91 -3
  292. package/src/routes.ts +135 -0
  293. package/src/runtime-state.test.ts +45 -0
  294. package/src/runtime-state.ts +124 -0
  295. package/src/schemas.test.ts +16 -29
  296. package/src/schemas.ts +3475 -552
  297. package/src/search-globs.test.ts +46 -0
  298. package/src/search-globs.ts +76 -0
  299. package/src/share-paths.test.ts +48 -0
  300. package/src/share-paths.ts +68 -0
  301. package/src/state-portability.ts +8 -8
  302. package/src/title.test.ts +20 -1
  303. package/src/title.ts +12 -3
  304. package/src/tunnel-ids.ts +15 -1
  305. package/src/versions.test.ts +39 -0
  306. package/src/versions.ts +48 -0
  307. package/src/workflow-faults.test.ts +127 -0
  308. package/src/workflow-faults.ts +188 -0
  309. package/src/workspace-state.test.ts +169 -15
  310. package/src/workspace-state.ts +427 -35
  311. package/dist/contracts/codex.contract.d.ts.map +0 -1
  312. package/dist/contracts/codex.contract.js +0 -6
  313. package/dist/contracts/codex.contract.js.map +0 -1
  314. package/dist/contracts/gemini.contract.d.ts +0 -16
  315. package/dist/contracts/gemini.contract.d.ts.map +0 -1
  316. package/dist/contracts/gemini.contract.js +0 -6
  317. package/dist/contracts/gemini.contract.js.map +0 -1
  318. package/dist/contracts/kimi.contract.d.ts +0 -16
  319. package/dist/contracts/kimi.contract.d.ts.map +0 -1
  320. package/dist/contracts/kimi.contract.js +0 -6
  321. package/dist/contracts/kimi.contract.js.map +0 -1
  322. package/dist/contracts/komodo.contract.d.ts +0 -125
  323. package/dist/contracts/komodo.contract.d.ts.map +0 -1
  324. package/dist/contracts/komodo.contract.js +0 -14
  325. package/dist/contracts/komodo.contract.js.map +0 -1
  326. package/dist/contracts/memory.contract.d.ts +0 -34
  327. package/dist/contracts/memory.contract.d.ts.map +0 -1
  328. package/dist/contracts/memory.contract.js +0 -9
  329. package/dist/contracts/memory.contract.js.map +0 -1
  330. package/src/contracts/codex.contract.ts +0 -10
  331. package/src/contracts/gemini.contract.ts +0 -11
  332. package/src/contracts/kimi.contract.ts +0 -9
  333. package/src/contracts/komodo.contract.ts +0 -36
  334. package/src/contracts/memory.contract.ts +0 -13
@@ -1,4 +1,4 @@
1
- import type { ChoreLedgerEntry, ChorePackage, ChoresReport, ChoreShape, ChoreSignals, ProbeResult } from "../schemas.js";
1
+ import type { Bundle, ChoreLedgerEntry, ChorePackage, ChoresReport, ChoreShape, ChoreSignals, Duplication, ProbeResult, UiScan } from "../schemas.js";
2
2
  import { describe, expect, test } from "vitest";
3
3
  import { choreById, CHORES } from "./chores.js";
4
4
  import { assessReport, ledgerKey, unseenVerdicts } from "./verdict.js";
@@ -20,14 +20,15 @@ const pkg = (over: Partial<ChorePackage> = {}): ChorePackage => ({
20
20
  ...over,
21
21
  });
22
22
 
23
- // A repository that is a Node workspace with documents, a pipeline and an image — so every chore APPLIES by
24
- // default and each applicability test can turn off exactly the one fact it is about.
23
+ // A repository that is a Node workspace with documents, a pipeline, an image and a Tailwind front-end — so every
24
+ // chore APPLIES by default and each applicability test can turn off exactly the one fact it is about.
25
25
  const shape = (over: Partial<ChoreShape> = {}): ChoreShape => ({
26
26
  docs: [`docs/architecture/repo.md`],
27
27
  dockerfiles: [`Dockerfile`],
28
28
  ci: [`.github/workflows/ci.yml`],
29
29
  lockfile: true,
30
30
  packageManifest: true,
31
+ deps: [`tailwindcss`, `vue`],
31
32
  ...over,
32
33
  });
33
34
 
@@ -46,10 +47,21 @@ const probe = (over: Partial<ProbeResult> & Pick<ProbeResult, "id">): ProbeResul
46
47
  const auditProbe = (names: readonly string[]): ProbeResult =>
47
48
  probe({
48
49
  id: `audit`,
49
- facts: { id: `audit`, advisories: names.map((name) => ({ name, severity: `high` as const, title: `${name} is bad`, patched: `>=2`, dev: false })) },
50
- });
51
-
52
- const report = (over: Partial<ChoresReport> = {}): ChoresReport => ({ repos: [{ repo: `app`, probes: [], signals: signals() }], ledger: [], node: `v24.18.0`, ...over });
50
+ facts: {
51
+ id: `audit`,
52
+ advisories: names.map((name) => ({ name, severity: `high` as const, title: `${name} is bad`, patched: `>=2`, dev: false })),
53
+ },
54
+ });
55
+
56
+ const report = (over: Partial<ChoresReport> = {}): ChoresReport => ({
57
+ repos: [{ repo: `app`, probes: [], signals: signals() }],
58
+ ledger: [],
59
+ // Verdicts are about EVIDENCE, never about work in flight — a probe running does not make a chore more or
60
+ // less due, it only makes the panel say so. Empty here because no assertion in this file should depend on it.
61
+ running: [],
62
+ node: `v24.18.0`,
63
+ ...over,
64
+ });
53
65
 
54
66
  const verdictFor = (input: ChoresReport, chore: string, repo = `app`) => {
55
67
  const found = assessReport(input, NOW).find((verdict) => verdict.chore.id === chore && verdict.repo === repo);
@@ -70,7 +82,9 @@ describe(`what "we have not measured this" means`, () => {
70
82
  // does not have is not evidence of anything, and it carries the tool's own reason rather than an invented one.
71
83
  test(`a probe the repository cannot run says so, and never badges`, () => {
72
84
  const input = report({
73
- repos: [{ repo: `app`, probes: [probe({ id: `knip`, state: `unavailable`, reason: `knip is not a devDependency` })], signals: signals() }],
85
+ repos: [
86
+ { repo: `app`, probes: [probe({ id: `knip`, state: `unavailable`, reason: `knip is not a devDependency` })], signals: signals() },
87
+ ],
74
88
  });
75
89
  const verdict = verdictFor(input, `dead-code`);
76
90
  expect(verdict.state).toBe(`unavailable`);
@@ -107,7 +121,10 @@ describe(`the ledger debounces; it cannot hide`, () => {
107
121
  // The point of digesting evidence rather than stamping a time: a fix landing, or a NEW advisory arriving,
108
122
  // both move the evidence and both deserve to be heard again.
109
123
  test(`evidence that has moved since the run is unsettled again`, () => {
110
- const moved = report({ repos: [{ repo: `app`, probes: [auditProbe([`left-pad`, `minimist`])], signals: signals() }], ledger: [ledgerEntry()] });
124
+ const moved = report({
125
+ repos: [{ repo: `app`, probes: [auditProbe([`left-pad`, `minimist`])], signals: signals() }],
126
+ ledger: [ledgerEntry()],
127
+ });
111
128
  const verdict = verdictFor(moved, `security-advisories`);
112
129
  expect(verdict.state).toBe(`due`);
113
130
  expect(verdict.settled).toBe(false);
@@ -143,6 +160,76 @@ describe(`the ledger debounces; it cannot hide`, () => {
143
160
  });
144
161
  });
145
162
 
163
+ /* The distinction the digest alone cannot draw, and the one whose absence had the panel quoting a six-day-old
164
+ * dead-code count an hour after the turn that deleted it. An unchanged digest is what a probe that never re-ran
165
+ * produces, so "the fix did not move the numbers" and "we have not looked since the fix" reached the same branch
166
+ * and both came out as a confident `due`. */
167
+ describe(`a measurement older than the work is not evidence about the work`, () => {
168
+ const withAdvisories = report({ repos: [{ repo: `app`, probes: [auditProbe([`left-pad`])], signals: signals() }] });
169
+ const ledgerEntry = (over: Partial<ChoreLedgerEntry> = {}): ChoreLedgerEntry => ({
170
+ repo: `app`,
171
+ chore: `security-advisories`,
172
+ ranAt: NOW - DAY,
173
+ runId: `r1`,
174
+ outcome: `acted`,
175
+ digest: verdictFor(withAdvisories, `security-advisories`).digest,
176
+ ...over,
177
+ });
178
+
179
+ // The probe ran a day ago; the turn started an hour ago. Whatever it did, nothing has looked since.
180
+ test(`a turn that landed after the measurement steps the chore down from due`, () => {
181
+ const verdict = verdictFor({ ...withAdvisories, ledger: [ledgerEntry({ ranAt: NOW - 3_600_000 })] }, `security-advisories`);
182
+ expect(verdict.state).toBe(`stale`);
183
+ // The evidence stays on the row — it is what the reader checks the claim against — and the claim comes off.
184
+ expect(verdict.detail).not.toEqual([]);
185
+ expect(verdict.settled).toBe(false);
186
+ });
187
+
188
+ // No prompt, so the row cannot offer to spend a second turn on a finding nobody has re-checked; and no badge,
189
+ // which is what stops the tile lighting for work that is already done.
190
+ test(`a stale chore offers no turn and never badges`, () => {
191
+ const verdict = verdictFor({ ...withAdvisories, ledger: [ledgerEntry({ ranAt: NOW - 3_600_000 })] }, `security-advisories`);
192
+ expect(verdict.prompt).toBeUndefined();
193
+ expect(unseenVerdicts([verdict], {})).toEqual([]);
194
+ });
195
+
196
+ // Re-measuring is the whole cure: the same run, against evidence taken after it, is settled rather than stale.
197
+ test(`re-measuring after the turn restores the verdict — due, and now genuinely settled`, () => {
198
+ const remeasured = report({
199
+ repos: [{ repo: `app`, probes: [{ ...auditProbe([`left-pad`]), ranAt: NOW - 60_000 }], signals: signals() }],
200
+ ledger: [ledgerEntry({ ranAt: NOW - 3_600_000 })],
201
+ });
202
+ const verdict = verdictFor(remeasured, `security-advisories`);
203
+ expect(verdict.state).toBe(`due`);
204
+ expect(verdict.settled).toBe(true);
205
+ });
206
+
207
+ // The agent's own "these were false positives" is a judgement about the live tree, made after the measurement.
208
+ // It outranks staleness, or every clean run would be re-opened by the very probe age it was reported against.
209
+ test(`an agent reporting the findings did not hold up still clears the chore`, () => {
210
+ const verdict = verdictFor({ ...withAdvisories, ledger: [ledgerEntry({ ranAt: NOW - 3_600_000, outcome: `clean` })] }, `security-advisories`);
211
+ expect(verdict.state).toBe(`clear`);
212
+ });
213
+
214
+ // A snooze is the owner speaking, and it outranks both.
215
+ test(`a snooze still wins`, () => {
216
+ const verdict = verdictFor(
217
+ { ...withAdvisories, ledger: [ledgerEntry({ ranAt: NOW - 3_600_000, snoozedUntil: NOW + DAY })] },
218
+ `security-advisories`,
219
+ );
220
+ expect(verdict.state).toBe(`snoozed`);
221
+ });
222
+
223
+ // Every measured row carries when it was taken, so no row can pass off a week-old count as this morning's.
224
+ test(`every measured verdict says when it was measured, and the unmeasurable ones say nothing`, () => {
225
+ expect(verdictFor(withAdvisories, `security-advisories`).measuredAt).toBe(NOW - DAY);
226
+ // A survey rests on no measurement — it is decided by the calendar, and has nothing to be out of date.
227
+ expect(verdictFor(report(), `standardize-patterns`).measuredAt).toBeUndefined();
228
+ // Nor does a probe that never ran.
229
+ expect(verdictFor(report(), `security-advisories`).measuredAt).toBeUndefined();
230
+ });
231
+ });
232
+
146
233
  describe(`surveys are due because time passed, and say so`, () => {
147
234
  const surveyLedger = (ranAt: number): ChoreLedgerEntry => ({
148
235
  repo: `app`,
@@ -178,7 +265,10 @@ describe(`the badge speaks about transitions, not about statistics`, () => {
178
265
  const seen = { [ledgerKey(`app`, `security-advisories`)]: first.digest };
179
266
  expect(unseenVerdicts([first], seen)).toEqual([]);
180
267
 
181
- const next = verdictFor(report({ repos: [{ repo: `app`, probes: [auditProbe([`left-pad`, `tar`])], signals: signals() }] }), `security-advisories`);
268
+ const next = verdictFor(
269
+ report({ repos: [{ repo: `app`, probes: [auditProbe([`left-pad`, `tar`])], signals: signals() }] }),
270
+ `security-advisories`,
271
+ );
182
272
  expect(unseenVerdicts([next], seen)).toHaveLength(1);
183
273
  });
184
274
 
@@ -202,7 +292,15 @@ describe(`the badge speaks about transitions, not about statistics`, () => {
202
292
 
203
293
  describe(`the findings themselves`, () => {
204
294
  test(`complexity reports only files that are load-bearing or out of proportion, never just the ranking's top`, () => {
205
- const hotspot = (path: string, complexity: number) => ({ path, commits: 20, adds: 100, dels: 50, complexity, score: complexity * 20, latestMs: NOW });
295
+ const hotspot = (path: string, complexity: number) => ({
296
+ path,
297
+ commits: 20,
298
+ adds: 100,
299
+ dels: 50,
300
+ complexity,
301
+ score: complexity * 20,
302
+ latestMs: NOW,
303
+ });
206
304
  // An even ranking has no outlier and no key module in it: a healthy repository, and an empty finding.
207
305
  const even = report({
208
306
  repos: [{ repo: `app`, probes: [], signals: signals({ hotspots: [hotspot(`a.ts`, 30), hotspot(`b.ts`, 28), hotspot(`c.ts`, 26)] }) }],
@@ -224,7 +322,10 @@ describe(`the findings themselves`, () => {
224
322
  {
225
323
  repo: `app`,
226
324
  probes: [],
227
- signals: signals({ indexed: false, hotspots: [{ path: `a.ts`, commits: 9, adds: 1, dels: 1, complexity: 900, score: 8100, latestMs: NOW }] }),
325
+ signals: signals({
326
+ indexed: false,
327
+ hotspots: [{ path: `a.ts`, commits: 9, adds: 1, dels: 1, complexity: 900, score: 8100, latestMs: NOW }],
328
+ }),
228
329
  },
229
330
  ],
230
331
  });
@@ -259,7 +360,8 @@ describe(`the findings themselves`, () => {
259
360
  });
260
361
 
261
362
  describe(`the prompts`, () => {
262
- const dueVerdict = () => verdictFor(report({ repos: [{ repo: `app`, probes: [auditProbe([`left-pad`])], signals: signals() }] }), `security-advisories`);
363
+ const dueVerdict = () =>
364
+ verdictFor(report({ repos: [{ repo: `app`, probes: [auditProbe([`left-pad`])], signals: signals() }] }), `security-advisories`);
263
365
 
264
366
  /* A prompt that counts without NAMING sends the agent off to re-derive a list we are already holding — slowly,
265
367
  * and against a tree that has moved since. Every measured chore names its artefacts. */
@@ -283,9 +385,27 @@ describe(`the prompts`, () => {
283
385
  repo: `app`,
284
386
  probes: [
285
387
  auditProbe([`left-pad`]),
286
- probe({ id: `outdated`, facts: { id: `outdated`, packages: [{ name: `vue`, current: `1.0.0`, latest: `2.0.0`, kind: `major`, section: `dependencies` }] } }),
287
- probe({ id: `knip`, facts: { id: `knip`, deadCode: { files: 3, exports: 2, types: 0, dependencies: 1, devDependencies: 0, sample: [`a.ts`] } } }),
288
- probe({ id: `jscpd`, facts: { id: `jscpd`, duplication: { percentage: 9, clones: 4, top: [{ lines: 20, first: `a.ts`, second: `b.ts` }] } } }),
388
+ probe({
389
+ id: `outdated`,
390
+ facts: {
391
+ id: `outdated`,
392
+ packages: [{ name: `vue`, current: `1.0.0`, latest: `2.0.0`, kind: `major`, section: `dependencies` }],
393
+ },
394
+ }),
395
+ probe({
396
+ id: `knip`,
397
+ facts: {
398
+ id: `knip`,
399
+ deadCode: { files: 3, exports: 2, types: 0, dependencies: 1, devDependencies: 0, sample: [`a.ts`] },
400
+ },
401
+ }),
402
+ probe({
403
+ id: `jscpd`,
404
+ facts: {
405
+ id: `jscpd`,
406
+ duplication: { percentage: 9, clones: 4, top: [{ lines: 20, first: `a.ts`, second: `b.ts` }] },
407
+ },
408
+ }),
289
409
  ],
290
410
  signals: signals({ packages: [pkg({ documented: false, dependencies: [`zod`, `joi`] })] }),
291
411
  },
@@ -323,7 +443,7 @@ describe(`what does not apply here`, () => {
323
443
  test(`a repository with no documents is not asked to re-read its documentation`, () => {
324
444
  const verdict = verdictFor(withShape({ docs: [] }), `documentation-drift`);
325
445
  expect(verdict.state).toBe(`not-applicable`);
326
- expect(verdict.headline).toBe(`this repository has no architecture documents to re-read`);
446
+ expect(verdict.headline).toBe(`no architecture documents`);
327
447
  expect(verdict.prompt).toBeUndefined();
328
448
  });
329
449
 
@@ -352,10 +472,12 @@ describe(`what does not apply here`, () => {
352
472
  // fires forever in repositories where its subject does not exist. This is the regression that motivated
353
473
  // making `applies` a required field on SurveySpec rather than an optional one.
354
474
  test(`a tiny repository is not surveyed for cross-cutting patterns it cannot have`, () => {
355
- const tiny = report({ repos: [{ repo: `app`, probes: [], signals: signals({ totals: { files: 4, symbols: 10, complexity: 5, hotspots: 0 } }) }] });
475
+ const tiny = report({
476
+ repos: [{ repo: `app`, probes: [], signals: signals({ totals: { files: 4, symbols: 10, complexity: 5, hotspots: 0 } }) }],
477
+ });
356
478
  const verdict = verdictFor(tiny, `standardize-patterns`);
357
479
  expect(verdict.state).toBe(`not-applicable`);
358
- expect(verdict.headline).toContain(`4 indexed files`);
480
+ expect(verdict.headline).toBe(`only 4 indexed files`);
359
481
  });
360
482
 
361
483
  test(`applicability is decided before measurement, so a missing probe never masks it`, () => {
@@ -373,6 +495,222 @@ describe(`what does not apply here`, () => {
373
495
  test(`a fully-equipped repository rules nothing out`, () => {
374
496
  expect(assessReport(withShape({}), NOW).filter((verdict) => verdict.state === `not-applicable`)).toEqual([]);
375
497
  });
498
+
499
+ /* THE CAUSES HAVE TO GROUP, and that is a fact about the STRINGS rather than about the gates. The scope strip
500
+ * prints one line per distinct cause with the chores it costs listed beside it, so two gates that both mean
501
+ * "there is no package.json here" and say it in different words print two lines — and a workspace root, where
502
+ * a dozen chores are ruled out by three facts, is back to the paragraph-per-chore wall this phrasing replaced.
503
+ * Bounded rather than enumerated: a new gate may invent a new cause, it may not invent a new sentence. */
504
+ test(`applicability reasons are bare causes, so the ones that mean the same thing group`, () => {
505
+ const bare = withShape({ packageManifest: false, lockfile: false, docs: [], ci: [], dockerfiles: [], deps: [] });
506
+ const causes = assessReport(bare, NOW)
507
+ .filter((verdict) => verdict.state === `not-applicable`)
508
+ .map((verdict) => verdict.headline);
509
+ for (const cause of causes) {
510
+ expect(cause, cause).toMatch(/^[a-z]/);
511
+ expect(cause.split(` `).length, cause).toBeLessThanOrEqual(4);
512
+ }
513
+ // Four chores are ruled out by ONE absent file, and the strip says so once rather than four times.
514
+ expect(causes.filter((cause) => cause === `no package.json`)).toHaveLength(4);
515
+ });
516
+ });
517
+
518
+ /* THE FRONT-END CHORES. Four chores over two probes, tested where they decide something — the share that makes a
519
+ * bundle a finding, the names that make two components one component, and above all the digests, because three of
520
+ * these four measure things that move every time anyone writes a line of markup. */
521
+ const uiProbe = (scan: Partial<UiScan> = {}): ProbeResult =>
522
+ probe({ id: `ui`, facts: { id: `ui`, scan: { components: [], bypasses: [], idioms: [], ...scan } } });
523
+
524
+ const bundleProbe = (assets: Bundle["assets"]): ProbeResult =>
525
+ probe({
526
+ id: `bundle`,
527
+ facts: {
528
+ id: `bundle`,
529
+ bundle: {
530
+ dir: `dist`,
531
+ assets,
532
+ totalBytes: assets.reduce((sum, asset) => sum + asset.bytes, 0),
533
+ totalGzip: assets.reduce((sum, asset) => sum + asset.gzip, 0),
534
+ },
535
+ },
536
+ });
537
+
538
+ const jscpdProbe = (top: Duplication["top"]): ProbeResult =>
539
+ probe({ id: `jscpd`, facts: { id: `jscpd`, duplication: { percentage: 1, clones: top.length, top } } });
540
+
541
+ const withProbes = (probes: readonly ProbeResult[], over: Partial<ChoreShape> = {}): ChoresReport =>
542
+ report({ repos: [{ repo: `app`, probes: [...probes], signals: signals({ shape: shape(over) }) }] });
543
+
544
+ const chunk = (path: string, gzip: number): Bundle["assets"][number] => ({ path, bytes: gzip * 3, gzip });
545
+
546
+ describe(`what the browser downloads`, () => {
547
+ test(`one chunk over half the transfer is the finding, and the headline names it`, () => {
548
+ const verdict = verdictFor(
549
+ withProbes([bundleProbe([chunk(`dist/vendor-DlAUqK2U.js`, 800), chunk(`dist/index-a1.js`, 100), chunk(`dist/s-b2.css`, 50)])]),
550
+ `bundle-weight`,
551
+ );
552
+ expect(verdict.state).toBe(`due`);
553
+ expect(verdict.headline).toContain(`dist/vendor-DlAUqK2U.js`);
554
+ expect(verdict.headline).toContain(`84%`);
555
+ });
556
+
557
+ test(`a build that is actually split says nothing`, () => {
558
+ const even = [chunk(`dist/a-1.js`, 100), chunk(`dist/b-2.js`, 100), chunk(`dist/c-3.js`, 100), chunk(`dist/d-4.js`, 100)];
559
+ expect(verdictFor(withProbes([bundleProbe(even)]), `bundle-weight`).state).toBe(`clear`);
560
+ });
561
+
562
+ // Two files cannot tell you how a build is divided, and the larger of them is over half by arithmetic.
563
+ test(`too few assets to have a shape is clear, not due`, () => {
564
+ expect(verdictFor(withProbes([bundleProbe([chunk(`dist/a-1.js`, 900), chunk(`dist/b-2.js`, 10)])]), `bundle-weight`).state).toBe(`clear`);
565
+ });
566
+
567
+ /* THE CASE THIS CHORE WOULD OTHERWISE FAIL EVERY DAY. A content hash changing is what a content hash is for,
568
+ * so rebuilding identical code renames every asset. Digesting the raw paths would mint new evidence on every
569
+ * `pnpm build` and badge forever while reporting nothing new. */
570
+ test(`rebuilding the same code does not read as new evidence`, () => {
571
+ const before = verdictFor(
572
+ withProbes([bundleProbe([chunk(`dist/vendor-DlAUqK2U.js`, 800), chunk(`dist/index-a1b2c3d4.js`, 100), chunk(`dist/s.css`, 50)])]),
573
+ `bundle-weight`,
574
+ );
575
+ const after = verdictFor(
576
+ withProbes([bundleProbe([chunk(`dist/vendor-Zq99XxYw.js`, 802), chunk(`dist/index-9z8y7x6w.js`, 101), chunk(`dist/s.css`, 50)])]),
577
+ `bundle-weight`,
578
+ );
579
+ expect(after.digest).toBe(before.digest);
580
+ });
581
+
582
+ test(`a genuinely different chunk appearing does`, () => {
583
+ const before = verdictFor(
584
+ withProbes([bundleProbe([chunk(`dist/vendor-DlAUqK2U.js`, 800), chunk(`dist/index-a1b2c3d4.js`, 100), chunk(`dist/s.css`, 50)])]),
585
+ `bundle-weight`,
586
+ );
587
+ const after = verdictFor(
588
+ withProbes([bundleProbe([chunk(`dist/vendor-DlAUqK2U.js`, 800), chunk(`dist/charting-a1b2c3d4.js`, 100), chunk(`dist/s.css`, 50)])]),
589
+ `bundle-weight`,
590
+ );
591
+ expect(after.digest).not.toBe(before.digest);
592
+ });
593
+ });
594
+
595
+ describe(`idioms the framework has replaced`, () => {
596
+ const idioms = (id: string, count: number) => ({ id, files: Array.from({ length: count }, (_, index) => `src/C${index}.vue`) });
597
+
598
+ test(`names what is still in use and what replaced it`, () => {
599
+ const verdict = verdictFor(withProbes([uiProbe({ idioms: [idioms(`vue-options-api`, 3)] })]), `framework-idiom`);
600
+ expect(verdict.state).toBe(`due`);
601
+ expect(verdict.detail).toEqual([`3 files · the Options API → <script setup> with the Composition API`]);
602
+ });
603
+
604
+ /* A migration in progress is a set that changes on every commit, so digesting the file identities — which is
605
+ * right for the documentation chore, whose set is packages — would badge continuously through exactly the
606
+ * period someone is doing the work. The bucketed count moves on real progress and not on daily churn. */
607
+ test(`one more file in a large migration is not news`, () => {
608
+ const before = verdictFor(withProbes([uiProbe({ idioms: [idioms(`vue-options-api`, 40)] })]), `framework-idiom`);
609
+ const after = verdictFor(withProbes([uiProbe({ idioms: [idioms(`vue-options-api`, 41)] })]), `framework-idiom`);
610
+ expect(after.digest).toBe(before.digest);
611
+ });
612
+
613
+ test(`a kind of legacy code the repository did not have before is`, () => {
614
+ const before = verdictFor(withProbes([uiProbe({ idioms: [idioms(`vue-options-api`, 40)] })]), `framework-idiom`);
615
+ const after = verdictFor(withProbes([uiProbe({ idioms: [idioms(`vue-options-api`, 40), idioms(`vue-global-api`, 1)] })]), `framework-idiom`);
616
+ expect(after.digest).not.toBe(before.digest);
617
+ });
618
+
619
+ // The daemon composes the sweep from its own copy of the table, so an image ahead of the browser can report a
620
+ // rule this build has no label or replacement for. A row about it could say nothing useful.
621
+ test(`an idiom this build of the book does not know is dropped, not shown unnamed`, () => {
622
+ expect(verdictFor(withProbes([uiProbe({ idioms: [idioms(`react-from-2029`, 5)] })]), `framework-idiom`).state).toBe(`clear`);
623
+ });
624
+
625
+ /* A probe's command is a fixed string, so every rule sweeps every repository and the Angular patterns get
626
+ * their chance in a Vue codebase. Run against this workspace it found one file: the rule table itself, which
627
+ * quotes `RouterModule.forRoot` as a pattern. What the repository declares is what settles it. */
628
+ test(`an idiom from a framework the repository does not declare is not its problem`, () => {
629
+ const vue = withProbes([uiProbe({ idioms: [idioms(`angular-ngmodule`, 4)] })], { deps: [`vue`] });
630
+ const angular = withProbes([uiProbe({ idioms: [idioms(`angular-ngmodule`, 4)] })], { deps: [`@angular/core`] });
631
+ expect(verdictFor(vue, `framework-idiom`).state).toBe(`clear`);
632
+ expect(verdictFor(angular, `framework-idiom`).state).toBe(`due`);
633
+ });
634
+ });
635
+
636
+ describe(`components built twice`, () => {
637
+ const components = (...paths: readonly string[]) => uiProbe({ components: [...paths] });
638
+
639
+ test(`a shared name across two directories is a family, whatever each file is called`, () => {
640
+ const verdict = verdictFor(
641
+ withProbes([components(`src/ui/BaseButton.vue`, `src/checkout/ButtonV2.tsx`), jscpdProbe([])]),
642
+ `component-overlap`,
643
+ );
644
+ expect(verdict.state).toBe(`due`);
645
+ expect(verdict.headline).toBe(`1 name used by more than one component`);
646
+ expect(verdict.detail).toEqual([`button · src/checkout/ButtonV2.tsx, src/ui/BaseButton.vue`]);
647
+ });
648
+
649
+ test(`components that merely coexist are not a finding`, () => {
650
+ expect(verdictFor(withProbes([components(`src/Button.vue`, `src/Card.vue`), jscpdProbe([])]), `component-overlap`).state).toBe(`clear`);
651
+ });
652
+
653
+ // The other half of the evidence: two components with unrelated names doing the same work, which no name
654
+ // comparison can reach and jscpd already measured.
655
+ test(`a clone counts only when a component sits on both sides of it`, () => {
656
+ const ui = components(`src/Chart.vue`, `src/Graph.vue`);
657
+ const shared = verdictFor(
658
+ withProbes([ui, jscpdProbe([{ lines: 40, first: `./src/Chart.vue`, second: `./src/Graph.vue` }])]),
659
+ `component-overlap`,
660
+ );
661
+ const oneSided = verdictFor(
662
+ withProbes([ui, jscpdProbe([{ lines: 40, first: `./src/Chart.vue`, second: `./src/utils/format.ts` }])]),
663
+ `component-overlap`,
664
+ );
665
+ expect(shared.state).toBe(`due`);
666
+ expect(shared.headline).toBe(`1 clone spanning two of them`);
667
+ expect(oneSided.state).toBe(`clear`);
668
+ });
669
+
670
+ /* Half a measurement would let the row claim it looked for shared logic in a repository where jscpd has never
671
+ * run — the "measured and found nothing" lie the unavailable state exists to prevent. */
672
+ test(`without the clone sweep the chore is unavailable, not clear`, () => {
673
+ expect(verdictFor(withProbes([components(`src/Button.vue`, `src/ui/Button.vue`)]), `component-overlap`).state).toBe(`unavailable`);
674
+ });
675
+ });
676
+
677
+ describe(`hard-coded styles`, () => {
678
+ test(`counts the values and leads with the heaviest files`, () => {
679
+ const verdict = verdictFor(
680
+ withProbes([
681
+ uiProbe({
682
+ bypasses: [
683
+ { path: `src/Checkout.vue`, count: 11 },
684
+ { path: `src/Nav.vue`, count: 2 },
685
+ ],
686
+ }),
687
+ ]),
688
+ `tailwind-arbitrary-values`,
689
+ );
690
+ expect(verdict.state).toBe(`due`);
691
+ expect(verdict.headline).toBe(`13 hard-coded values across 2 files`);
692
+ expect(verdict.detail[0]).toBe(`src/Checkout.vue · 11 values`);
693
+ });
694
+
695
+ // Tailwind gates this one alone — a Vue repository with no Tailwind has no theme scale to have bypassed, and
696
+ // a row saying so would be the surface inventing a subject.
697
+ test(`a repository without Tailwind is not asked the question at all`, () => {
698
+ const verdict = verdictFor(
699
+ withProbes([uiProbe({ bypasses: [{ path: `src/Nav.vue`, count: 2 }] })], { deps: [`vue`] }),
700
+ `tailwind-arbitrary-values`,
701
+ );
702
+ expect(verdict.state).toBe(`not-applicable`);
703
+ expect(verdict.headline).toBe(`no Tailwind`);
704
+ });
705
+
706
+ // And the framework gate the other four share, from the other side: deps come from shape, not from packages,
707
+ // because a single-package Vite app has no workspace packages at all.
708
+ test(`a repository with no framework rules the front-end chores out entirely`, () => {
709
+ const states = [`bundle-weight`, `framework-idiom`, `component-overlap`, `tailwind-arbitrary-values`].map(
710
+ (id) => verdictFor(withProbes([], { deps: [`pino`] }), id).state,
711
+ );
712
+ expect(states).toEqual([`not-applicable`, `not-applicable`, `not-applicable`, `not-applicable`]);
713
+ });
376
714
  });
377
715
 
378
716
  /* THE CRITERION — the rule in words, next to the evidence that met it. A row that reports a number without the