@erclx/aitk 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 (359) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -0
  3. package/claude/.claude-plugin/plugin.json +19 -0
  4. package/claude/skills/bash-script/SKILL.md +96 -0
  5. package/claude/skills/bash-script/references/patterns.md +349 -0
  6. package/claude/skills/ci-workflow/SKILL.md +52 -0
  7. package/claude/skills/ci-workflow/references/workflows.md +74 -0
  8. package/claude/skills/claude-address-review/SKILL.md +107 -0
  9. package/claude/skills/claude-autoship/SKILL.md +137 -0
  10. package/claude/skills/claude-design-extract/SKILL.md +69 -0
  11. package/claude/skills/claude-design-propose/SKILL.md +75 -0
  12. package/claude/skills/claude-diagram/SKILL.md +126 -0
  13. package/claude/skills/claude-docs/SKILL.md +204 -0
  14. package/claude/skills/claude-feature/REQUIREMENT.md +35 -0
  15. package/claude/skills/claude-feature/SKILL.md +148 -0
  16. package/claude/skills/claude-groundwork/SKILL.md +148 -0
  17. package/claude/skills/claude-groundwork/references/folder-format.md +107 -0
  18. package/claude/skills/claude-memory-capture/SKILL.md +80 -0
  19. package/claude/skills/claude-memory-review/REQUIREMENT.md +37 -0
  20. package/claude/skills/claude-memory-review/SKILL.md +206 -0
  21. package/claude/skills/claude-orchestrate/SKILL.md +125 -0
  22. package/claude/skills/claude-pr-review/SKILL.md +150 -0
  23. package/claude/skills/claude-review/SKILL.md +136 -0
  24. package/claude/skills/claude-roadmap/SKILL.md +61 -0
  25. package/claude/skills/claude-roadmap/references/roadmap.md +52 -0
  26. package/claude/skills/claude-screencast/SKILL.md +160 -0
  27. package/claude/skills/claude-seed-sync/SKILL.md +141 -0
  28. package/claude/skills/claude-slides-draft/SKILL.md +62 -0
  29. package/claude/skills/claude-standards-audit/REQUIREMENT.md +33 -0
  30. package/claude/skills/claude-standards-audit/SKILL.md +79 -0
  31. package/claude/skills/claude-standards-audit/references/branch.md +49 -0
  32. package/claude/skills/claude-standards-audit/references/pr.md +124 -0
  33. package/claude/skills/claude-tasks/SKILL.md +123 -0
  34. package/claude/skills/claude-ui-test/SKILL.md +75 -0
  35. package/claude/skills/claude-ux-audit/SKILL.md +78 -0
  36. package/claude/skills/claude-worktree/SKILL.md +79 -0
  37. package/claude/skills/cli-script/SKILL.md +47 -0
  38. package/claude/skills/cli-script/references/template.md +43 -0
  39. package/claude/skills/create-rule/SKILL.md +68 -0
  40. package/claude/skills/create-skill/REQUIREMENT.md +30 -0
  41. package/claude/skills/create-skill/SKILL.md +24 -0
  42. package/claude/skills/create-snippet/SKILL.md +28 -0
  43. package/claude/skills/create-snippet/references/snippets.md +57 -0
  44. package/claude/skills/create-standard/SKILL.md +31 -0
  45. package/claude/skills/create-standard/references/standard.md +52 -0
  46. package/claude/skills/docs-sync/SKILL.md +87 -0
  47. package/claude/skills/git-branch/REQUIREMENT.md +38 -0
  48. package/claude/skills/git-branch/SKILL.md +60 -0
  49. package/claude/skills/git-branch/references/branch.md +49 -0
  50. package/claude/skills/git-commit/REQUIREMENT.md +33 -0
  51. package/claude/skills/git-commit/SKILL.md +51 -0
  52. package/claude/skills/git-commit/references/commit.md +63 -0
  53. package/claude/skills/git-followup/REQUIREMENT.md +38 -0
  54. package/claude/skills/git-followup/SKILL.md +35 -0
  55. package/claude/skills/git-issue/REQUIREMENT.md +38 -0
  56. package/claude/skills/git-issue/SKILL.md +69 -0
  57. package/claude/skills/git-issue/references/issue.md +84 -0
  58. package/claude/skills/git-pr/REQUIREMENT.md +37 -0
  59. package/claude/skills/git-pr/SKILL.md +119 -0
  60. package/claude/skills/git-pr/references/branch.md +49 -0
  61. package/claude/skills/git-pr/references/pr.md +124 -0
  62. package/claude/skills/git-ship/REQUIREMENT.md +34 -0
  63. package/claude/skills/git-ship/SKILL.md +39 -0
  64. package/claude/skills/git-split/REQUIREMENT.md +38 -0
  65. package/claude/skills/git-split/SKILL.md +160 -0
  66. package/claude/skills/git-split/references/branch.md +49 -0
  67. package/claude/skills/git-split/references/pr.md +124 -0
  68. package/claude/skills/git-stage/REQUIREMENT.md +36 -0
  69. package/claude/skills/git-stage/SKILL.md +74 -0
  70. package/claude/skills/git-stage/references/commit.md +63 -0
  71. package/claude/skills/git-worktree/REQUIREMENT.md +37 -0
  72. package/claude/skills/git-worktree/SKILL.md +120 -0
  73. package/claude/skills/migration-claude-md/SKILL.md +74 -0
  74. package/claude/skills/migration-context/SKILL.md +93 -0
  75. package/claude/skills/migration-standards/SKILL.md +75 -0
  76. package/claude/skills/project-commands/SKILL.md +73 -0
  77. package/claude/skills/session-resume/SKILL.md +36 -0
  78. package/claude/skills/setup-gov/SKILL.md +71 -0
  79. package/claude/skills/setup-indexes/SKILL.md +148 -0
  80. package/claude/skills/setup-init/SKILL.md +114 -0
  81. package/claude/skills/setup-plugins/SKILL.md +81 -0
  82. package/claude/skills/setup-plugins/references/plugin-catalog.md +53 -0
  83. package/claude/skills/setup-verify/SKILL.md +51 -0
  84. package/claude/skills/systematic-debugging/SKILL.md +70 -0
  85. package/claude/skills/toolkit-cli/SKILL.md +38 -0
  86. package/claude/skills/toolkit-feedback/SKILL.md +80 -0
  87. package/claude/skills/toolkit-operator/SKILL.md +45 -0
  88. package/claude/skills/toolkit-triage/SKILL.md +63 -0
  89. package/claude/skills/youtube-transcripts/SKILL.md +34 -0
  90. package/docs/agents.md +371 -0
  91. package/docs/ai-workflow.md +157 -0
  92. package/docs/index.md +20 -0
  93. package/docs/operating-model.md +106 -0
  94. package/docs/target-projects.md +183 -0
  95. package/docs/visual-design-workflow.md +136 -0
  96. package/docs/zshrc-aliases.md +65 -0
  97. package/governance/rules/claude/500-prose.md +12 -0
  98. package/governance/rules/claude/510-context.md +16 -0
  99. package/governance/rules/claude/520-wireframes.md +12 -0
  100. package/governance/rules/claude/530-requirements.md +11 -0
  101. package/governance/rules/claude/540-architecture.md +11 -0
  102. package/governance/rules/claude/550-design.md +11 -0
  103. package/governance/rules/claude/555-tasks.md +12 -0
  104. package/governance/rules/claude/560-diagrams.md +11 -0
  105. package/governance/rules/claude/570-skill.md +18 -0
  106. package/governance/rules/claude/580-readme.md +11 -0
  107. package/governance/rules/claude/590-rule-authoring.md +12 -0
  108. package/governance/rules/core/000-constitution.md +30 -0
  109. package/governance/rules/core/010-testing.md +35 -0
  110. package/governance/rules/core/020-concurrency.md +22 -0
  111. package/governance/rules/core/030-error-handling.md +31 -0
  112. package/governance/rules/core/040-performance.md +20 -0
  113. package/governance/rules/core/050-logging.md +20 -0
  114. package/governance/rules/core/060-naming.md +19 -0
  115. package/governance/rules/core/070-planning.md +13 -0
  116. package/governance/rules/core/080-config-comments.md +22 -0
  117. package/governance/rules/framework/200-react.md +51 -0
  118. package/governance/rules/framework/210-astro.md +41 -0
  119. package/governance/rules/framework/220-fastapi.md +43 -0
  120. package/governance/rules/framework/230-nextjs.md +48 -0
  121. package/governance/rules/framework/250-tailwind.md +32 -0
  122. package/governance/rules/framework/260-shadcn.md +34 -0
  123. package/governance/rules/lang/100-typescript.md +40 -0
  124. package/governance/rules/lang/110-python.md +42 -0
  125. package/governance/rules/lib/300-testing-ts.md +38 -0
  126. package/governance/rules/lib/310-zod.md +25 -0
  127. package/governance/rules/lib/320-tanstack-query.md +32 -0
  128. package/governance/rules/lib/330-testing-py.md +44 -0
  129. package/governance/rules/lib/340-pydantic.md +38 -0
  130. package/governance/rules/lib/350-security-web.md +31 -0
  131. package/governance/rules/ui/400-ui.md +34 -0
  132. package/governance/rules/ui/410-a11y.md +44 -0
  133. package/governance/rules/ui/420-forms.md +34 -0
  134. package/governance/rules/ui/430-ux-completeness.md +62 -0
  135. package/governance/stacks/astro.toml +2 -0
  136. package/governance/stacks/base.toml +2 -0
  137. package/governance/stacks/node.toml +2 -0
  138. package/governance/stacks/python-fastapi.toml +2 -0
  139. package/governance/stacks/python.toml +2 -0
  140. package/governance/stacks/react.toml +2 -0
  141. package/package.json +67 -0
  142. package/scripts/config.sh +11 -0
  143. package/scripts/core/bootstrap.sh +81 -0
  144. package/scripts/core/check-plugin-boundary.sh +45 -0
  145. package/scripts/core/check-skill-paths.sh +25 -0
  146. package/scripts/core/clean.sh +36 -0
  147. package/scripts/core/install-check.sh +93 -0
  148. package/scripts/core/regen-claude-copies.sh +26 -0
  149. package/scripts/core/regen-indexes.sh +10 -0
  150. package/scripts/core/regen-skill-references.sh +27 -0
  151. package/scripts/core/snapshot.sh +132 -0
  152. package/scripts/core/update.sh +35 -0
  153. package/scripts/core/verify.sh +201 -0
  154. package/scripts/docs/list.sh +129 -0
  155. package/scripts/gov/list.sh +234 -0
  156. package/scripts/lib/frontmatter.sh +30 -0
  157. package/scripts/lib/gov.sh +14 -0
  158. package/scripts/lib/sandbox-fixtures.sh +109 -0
  159. package/scripts/lib/sandbox-git.sh +73 -0
  160. package/scripts/lib/tooling.sh +24 -0
  161. package/scripts/lib/ui.sh +173 -0
  162. package/scripts/lib/worktree.sh +20 -0
  163. package/scripts/manage-sandbox.sh +453 -0
  164. package/scripts/snippets/create.sh +156 -0
  165. package/scripts/standards/list.sh +91 -0
  166. package/scripts/tooling/create.sh +109 -0
  167. package/scripts/tooling/ref.sh +141 -0
  168. package/scripts/tooling/verify.sh +179 -0
  169. package/snippets/claude/decision-memo.md +39 -0
  170. package/snippets/claude/feature-recap.md +19 -0
  171. package/snippets/claude/figma-steps.md +18 -0
  172. package/snippets/claude/orchestrator-resume.md +23 -0
  173. package/snippets/claude/orchestrator-sweep.md +10 -0
  174. package/snippets/compact-summary.md +5 -0
  175. package/snippets/create-snippet.md +7 -0
  176. package/snippets/decision-help.md +6 -0
  177. package/snippets/meta-prompt.md +14 -0
  178. package/snippets/research-prompt.md +7 -0
  179. package/snippets/session-notes.md +11 -0
  180. package/snippets/snippets.toml +6 -0
  181. package/snippets/step-by-step.md +10 -0
  182. package/snippets/web-research.md +21 -0
  183. package/src/claude/gitignore.ts +51 -0
  184. package/src/claude/seeds-list.ts +47 -0
  185. package/src/claude/seeds.ts +124 -0
  186. package/src/claude/settings.ts +151 -0
  187. package/src/cli-run.ts +43 -0
  188. package/src/cli.ts +111 -0
  189. package/src/commands/claude.ts +349 -0
  190. package/src/commands/design.ts +39 -0
  191. package/src/commands/docs.ts +59 -0
  192. package/src/commands/feedback-format.ts +23 -0
  193. package/src/commands/feedback.ts +103 -0
  194. package/src/commands/gov.ts +268 -0
  195. package/src/commands/indexes.ts +182 -0
  196. package/src/commands/init.ts +117 -0
  197. package/src/commands/pass-through.ts +39 -0
  198. package/src/commands/sandbox.ts +315 -0
  199. package/src/commands/slides.ts +106 -0
  200. package/src/commands/snippets.ts +236 -0
  201. package/src/commands/standards.ts +89 -0
  202. package/src/commands/sync.ts +211 -0
  203. package/src/commands/tasks.ts +185 -0
  204. package/src/commands/tooling.ts +432 -0
  205. package/src/commands/transcripts.ts +48 -0
  206. package/src/commands/wiki.ts +101 -0
  207. package/src/copy.ts +30 -0
  208. package/src/design/parse.ts +79 -0
  209. package/src/design/render.ts +170 -0
  210. package/src/docs/read.ts +59 -0
  211. package/src/exec.ts +17 -0
  212. package/src/frontmatter.ts +13 -0
  213. package/src/github-format.ts +13 -0
  214. package/src/github.ts +20 -0
  215. package/src/gov/adapter.ts +73 -0
  216. package/src/gov/install.ts +103 -0
  217. package/src/gov/payload.ts +39 -0
  218. package/src/gov/stacks.ts +124 -0
  219. package/src/indexes/frontmatter.ts +46 -0
  220. package/src/indexes/regen.ts +84 -0
  221. package/src/indexes/render.ts +181 -0
  222. package/src/indexes/walk.ts +111 -0
  223. package/src/init/flags.ts +66 -0
  224. package/src/init/plan.ts +128 -0
  225. package/src/init/run.ts +46 -0
  226. package/src/init/steps.ts +91 -0
  227. package/src/sandbox/coverage.ts +115 -0
  228. package/src/sandbox/expect.ts +412 -0
  229. package/src/slides/layouts.ts +391 -0
  230. package/src/slides/open.ts +18 -0
  231. package/src/slides/parse.ts +84 -0
  232. package/src/slides/render.ts +88 -0
  233. package/src/slides/styles.ts +44 -0
  234. package/src/snippets/adapter.ts +34 -0
  235. package/src/snippets/categories.ts +59 -0
  236. package/src/snippets/install.ts +125 -0
  237. package/src/snippets/list.ts +32 -0
  238. package/src/snippets/presets.ts +50 -0
  239. package/src/standards/adapter.ts +51 -0
  240. package/src/standards/index-refresh.ts +44 -0
  241. package/src/standards/install.ts +52 -0
  242. package/src/sync/check.ts +269 -0
  243. package/src/sync/engine.ts +416 -0
  244. package/src/sync/git.ts +204 -0
  245. package/src/sync/stamp.ts +202 -0
  246. package/src/sync/target.ts +79 -0
  247. package/src/sync/workflow.ts +201 -0
  248. package/src/target.ts +43 -0
  249. package/src/tasks/archive.ts +304 -0
  250. package/src/tooling/gitignore.ts +122 -0
  251. package/src/tooling/inject.ts +214 -0
  252. package/src/tooling/list.ts +39 -0
  253. package/src/tooling/manifest.ts +178 -0
  254. package/src/tooling/package.ts +166 -0
  255. package/src/tooling/scan.ts +186 -0
  256. package/src/transcripts/fetch.ts +156 -0
  257. package/src/transcripts/metadata.ts +54 -0
  258. package/src/transcripts/vtt.ts +114 -0
  259. package/src/ui.ts +187 -0
  260. package/src/wiki/init.ts +85 -0
  261. package/standards/architecture.md +41 -0
  262. package/standards/bundled/branch.md +49 -0
  263. package/standards/bundled/commit.md +63 -0
  264. package/standards/bundled/issue.md +84 -0
  265. package/standards/bundled/pr.md +124 -0
  266. package/standards/bundled/roadmap.md +52 -0
  267. package/standards/bundled/snippets.md +57 -0
  268. package/standards/bundled/standard.md +52 -0
  269. package/standards/context.md +132 -0
  270. package/standards/design.md +31 -0
  271. package/standards/diagrams.md +55 -0
  272. package/standards/index.md +21 -0
  273. package/standards/prose.md +122 -0
  274. package/standards/readme.md +163 -0
  275. package/standards/requirements.md +61 -0
  276. package/standards/rule.md +79 -0
  277. package/standards/skill.md +257 -0
  278. package/standards/tasks.md +140 -0
  279. package/standards/versioning.md +47 -0
  280. package/standards/wireframes.md +67 -0
  281. package/tooling/astro/configs/astro.config.mjs +17 -0
  282. package/tooling/astro/configs/eslint.config.js +79 -0
  283. package/tooling/astro/configs/playwright.config.ts +25 -0
  284. package/tooling/astro/configs/tsconfig.json +12 -0
  285. package/tooling/astro/configs/vitest.config.ts +22 -0
  286. package/tooling/astro/manifest.toml +32 -0
  287. package/tooling/astro/reference.md +60 -0
  288. package/tooling/base/configs/.editorconfig +5 -0
  289. package/tooling/base/configs/.github/pull_request_template.md +18 -0
  290. package/tooling/base/configs/.github/workflows/verify.yml +35 -0
  291. package/tooling/base/configs/.husky/commit-msg +1 -0
  292. package/tooling/base/configs/.husky/post-merge +61 -0
  293. package/tooling/base/configs/.husky/post-rewrite +21 -0
  294. package/tooling/base/configs/.husky/pre-commit +1 -0
  295. package/tooling/base/configs/.husky/pre-push +1 -0
  296. package/tooling/base/configs/.prettierrc +12 -0
  297. package/tooling/base/configs/.shellcheckrc +1 -0
  298. package/tooling/base/configs/.vscode/extensions.json +9 -0
  299. package/tooling/base/configs/.vscode/settings.json +3 -0
  300. package/tooling/base/configs/commitlint.config.js +11 -0
  301. package/tooling/base/configs/scripts/clean.sh +45 -0
  302. package/tooling/base/configs/scripts/update.sh +49 -0
  303. package/tooling/base/configs/scripts/verify.sh +64 -0
  304. package/tooling/base/manifest.toml +32 -0
  305. package/tooling/base/reference.md +114 -0
  306. package/tooling/base/seeds/.claude/context/ci.md +27 -0
  307. package/tooling/base/seeds/.claude/context/development.md +32 -0
  308. package/tooling/base/seeds/.cspell/project-terms.txt +0 -0
  309. package/tooling/base/seeds/.cspell/tech-stack.txt +19 -0
  310. package/tooling/base/seeds/.lintstagedrc +8 -0
  311. package/tooling/base/seeds/.prettierignore +0 -0
  312. package/tooling/base/seeds/cspell.json +20 -0
  313. package/tooling/claude/manifest.toml +11 -0
  314. package/tooling/claude/reference.md +62 -0
  315. package/tooling/claude/seeds/.claude/ARCHITECTURE.md +13 -0
  316. package/tooling/claude/seeds/.claude/DESIGN.md +62 -0
  317. package/tooling/claude/seeds/.claude/REQUIREMENTS.md +18 -0
  318. package/tooling/claude/seeds/.claude/context/index.md +8 -0
  319. package/tooling/claude/seeds/.claude/hooks/index-reminder.sh +43 -0
  320. package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +45 -0
  321. package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +61 -0
  322. package/tooling/claude/seeds/.claude/hooks/tasks-index.sh +60 -0
  323. package/tooling/claude/seeds/.claude/settings.json +39 -0
  324. package/tooling/claude/seeds/.claude/tasks/index.md +8 -0
  325. package/tooling/claude/seeds/.claude/wireframes/index.md +8 -0
  326. package/tooling/claude/seeds/CLAUDE.md +87 -0
  327. package/tooling/claude/user/settings.template.json +10 -0
  328. package/tooling/claude/user/statusline-command.sh +53 -0
  329. package/tooling/python/configs/.coveragerc +14 -0
  330. package/tooling/python/configs/.python-version +1 -0
  331. package/tooling/python/configs/mypy.ini +6 -0
  332. package/tooling/python/configs/pytest.ini +4 -0
  333. package/tooling/python/configs/ruff.toml +15 -0
  334. package/tooling/python/configs/scripts/verify.sh +77 -0
  335. package/tooling/python/manifest.toml +16 -0
  336. package/tooling/python/reference.md +66 -0
  337. package/tooling/python/seeds/.cspell/tech-stack.txt +19 -0
  338. package/tooling/python/seeds/tests/test_smoke.py +2 -0
  339. package/tooling/vite-react/configs/playwright.config.ts +25 -0
  340. package/tooling/vite-react/configs/tsconfig.json +35 -0
  341. package/tooling/vite-react/configs/vite.config.ts +14 -0
  342. package/tooling/vite-react/configs/vitest.config.ts +26 -0
  343. package/tooling/vite-react/manifest.toml +21 -0
  344. package/tooling/vite-react/reference.md +55 -0
  345. package/tooling/vite-react/seeds/.cspell/project-terms.txt +1 -0
  346. package/tooling/vite-react/seeds/.cspell/tech-stack.txt +1 -0
  347. package/tooling/web/configs/.github/workflows/verify.yml +134 -0
  348. package/tooling/web/configs/.vscode/extensions.json +13 -0
  349. package/tooling/web/configs/.vscode/settings.json +10 -0
  350. package/tooling/web/configs/e2e/home.spec.ts +6 -0
  351. package/tooling/web/configs/e2e/screenshot.ts +41 -0
  352. package/tooling/web/configs/eslint.config.js +82 -0
  353. package/tooling/web/configs/scripts/screenshot.sh +28 -0
  354. package/tooling/web/configs/scripts/verify.sh +80 -0
  355. package/tooling/web/configs/src/test/setup.ts +8 -0
  356. package/tooling/web/manifest.toml +57 -0
  357. package/tooling/web/reference.md +95 -0
  358. package/tooling/web/seeds/.cspell/tech-stack.txt +18 -0
  359. package/tsconfig.json +14 -0
@@ -0,0 +1,115 @@
1
+ import { existsSync, readdirSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ /**
5
+ * The arm a scenario declares when it carries no named arm. `expectFilePath`
6
+ * joins an empty arm away, so the declaration lands at the command root and has
7
+ * no name to print.
8
+ */
9
+ export const DEFAULT_ARM = '(default)'
10
+
11
+ /** Holds fixture content rather than scenarios. Twin of the filter in `sandbox.ts`. */
12
+ const FIXTURES_DIR = 'fixtures'
13
+
14
+ export interface ScenarioCoverage {
15
+ readonly category: string
16
+ readonly command: string
17
+ readonly armed: readonly string[]
18
+ }
19
+
20
+ export interface CoverageReport {
21
+ readonly scenarios: readonly ScenarioCoverage[]
22
+ readonly totalScenarios: number
23
+ readonly armedScenarios: number
24
+ readonly armedArms: number
25
+ }
26
+
27
+ function directories(path: string): string[] {
28
+ if (!existsSync(path)) return []
29
+
30
+ return readdirSync(path, { withFileTypes: true })
31
+ .filter((entry) => entry.isDirectory())
32
+ .map((entry) => entry.name)
33
+ .sort()
34
+ }
35
+
36
+ /**
37
+ * Enumerates from the scenario scripts rather than from the fixture tree. A
38
+ * scenario with no fixtures has no directory to find, and counting only what
39
+ * fixtures exist would hide exactly the arms this report exists to surface.
40
+ */
41
+ function listScenarios(sandboxDir: string): ScenarioCoverage[] {
42
+ const scenarios: ScenarioCoverage[] = []
43
+
44
+ for (const category of directories(sandboxDir)) {
45
+ if (category === FIXTURES_DIR) continue
46
+
47
+ const commands = readdirSync(join(sandboxDir, category), {
48
+ withFileTypes: true,
49
+ })
50
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.sh'))
51
+ .map((entry) => entry.name.replace(/\.sh$/, ''))
52
+ .sort()
53
+
54
+ for (const command of commands) {
55
+ scenarios.push({ category, command, armed: [] })
56
+ }
57
+ }
58
+
59
+ return scenarios
60
+ }
61
+
62
+ /**
63
+ * Names every arm of a scenario carrying an `expect.toml`. A declaration at the
64
+ * command root belongs to the unnamed arm and reports under `DEFAULT_ARM`, since
65
+ * an empty string in a report reads as a bug rather than as the default.
66
+ */
67
+ function armsFor(
68
+ fixturesDir: string,
69
+ category: string,
70
+ command: string,
71
+ ): string[] {
72
+ const commandDir = join(fixturesDir, category, command)
73
+ if (!existsSync(commandDir)) return []
74
+
75
+ const armed: string[] = []
76
+ if (existsSync(join(commandDir, 'expect.toml'))) armed.push(DEFAULT_ARM)
77
+
78
+ for (const arm of directories(commandDir)) {
79
+ if (existsSync(join(commandDir, arm, 'expect.toml'))) armed.push(arm)
80
+ }
81
+
82
+ return armed
83
+ }
84
+
85
+ /**
86
+ * Reports which scenarios declare expectations and which only provision a state.
87
+ * The undeclared ones exit zero today, so the count is the only thing separating
88
+ * a suite that proves something from one that runs.
89
+ */
90
+ export function collectCoverage(root: string): CoverageReport {
91
+ const sandboxDir = join(root, 'scripts', 'sandbox')
92
+ const fixturesDir = join(sandboxDir, FIXTURES_DIR)
93
+
94
+ const scenarios = listScenarios(sandboxDir).map((scenario) => ({
95
+ ...scenario,
96
+ armed: armsFor(fixturesDir, scenario.category, scenario.command),
97
+ }))
98
+
99
+ return {
100
+ scenarios,
101
+ totalScenarios: scenarios.length,
102
+ armedScenarios: scenarios.filter((s) => s.armed.length > 0).length,
103
+ armedArms: scenarios.reduce((total, s) => total + s.armed.length, 0),
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Whole percent, floored. A rollout at 13 percent should not round to 13 from
109
+ * above, since the number exists to look worse than a wall of green does.
110
+ */
111
+ export function coveragePercent(report: CoverageReport): number {
112
+ if (report.totalScenarios === 0) return 0
113
+
114
+ return Math.floor((report.armedScenarios / report.totalScenarios) * 100)
115
+ }
@@ -0,0 +1,412 @@
1
+ import { existsSync, readFileSync, statSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ /**
5
+ * Three states rather than two. An arm with no declaration is `unchecked`: it
6
+ * cannot pass, since nothing was asserted, and it does not fail, since failing
7
+ * every undeclared arm would make the harness unusable while expectations roll
8
+ * out. `aitk sandbox coverage` is what keeps the count from hiding.
9
+ */
10
+ export type VerdictState = 'pass' | 'fail' | 'unchecked'
11
+
12
+ export interface ContentAssertion {
13
+ readonly path: string
14
+ readonly pattern: string
15
+ }
16
+
17
+ export interface Expectation {
18
+ readonly paths: readonly string[]
19
+ readonly absent: readonly string[]
20
+ readonly content: readonly ContentAssertion[]
21
+ readonly writeScope: readonly string[]
22
+ readonly manual: readonly string[]
23
+ readonly maxTurns?: number
24
+ }
25
+
26
+ export interface AssertionResult {
27
+ readonly ok: boolean
28
+ readonly message: string
29
+ }
30
+
31
+ export interface RunEnvelope {
32
+ readonly isError: boolean
33
+ readonly turns: number
34
+ readonly denials: number
35
+ }
36
+
37
+ export interface Verdict {
38
+ readonly state: VerdictState
39
+ readonly asserted: number
40
+ readonly failed: number
41
+ readonly unchecked: number
42
+ readonly results: readonly AssertionResult[]
43
+ readonly manual: readonly string[]
44
+ readonly skipped: readonly string[]
45
+ readonly note?: string
46
+ }
47
+
48
+ /**
49
+ * `writes` and `envelope` are absent when the caller supplied no data for them,
50
+ * which is not the same as a run that wrote nothing or reported nothing. The
51
+ * assertion kinds that depend on them report as skipped rather than silently
52
+ * dropping out of the count, so the cheap standalone path cannot claim more
53
+ * coverage than it had.
54
+ */
55
+ export interface CheckInput {
56
+ readonly sandboxDir: string
57
+ readonly writes?: readonly string[]
58
+ readonly envelope?: RunEnvelope
59
+ }
60
+
61
+ interface KindOutcome {
62
+ readonly results: AssertionResult[]
63
+ readonly skipped: string[]
64
+ }
65
+
66
+ const EXIT_CODE: Record<VerdictState, number> = {
67
+ pass: 0,
68
+ fail: 1,
69
+ unchecked: 0,
70
+ }
71
+
72
+ /**
73
+ * `unchecked` exits zero so an undeclared arm does not break a gate while
74
+ * expectations roll out. Pass `strict` to make it exit one, which is how a
75
+ * caller that has finished arming its scenarios keeps them armed.
76
+ */
77
+ export function verdictExitCode(state: VerdictState, strict = false): number {
78
+ if (strict && state === 'unchecked') return 1
79
+
80
+ return EXIT_CODE[state]
81
+ }
82
+
83
+ export interface ScenarioTarget {
84
+ readonly category: string
85
+ readonly command: string
86
+ }
87
+
88
+ /**
89
+ * Splits `<category>:<command>`. Returns undefined rather than a partial target,
90
+ * so a typo cannot resolve to a declaration path that happens not to exist and
91
+ * report `unchecked` on what is really a caller error.
92
+ */
93
+ export function parseTarget(target: string): ScenarioTarget | undefined {
94
+ const [category, command, ...rest] = target.split(':')
95
+ if (rest.length > 0) return undefined
96
+ if (category === undefined || category === '') return undefined
97
+ if (command === undefined || command === '') return undefined
98
+
99
+ return { category, command }
100
+ }
101
+
102
+ export function expectFilePath(
103
+ root: string,
104
+ category: string,
105
+ command: string,
106
+ arm: string,
107
+ ): string {
108
+ return join(
109
+ root,
110
+ 'scripts',
111
+ 'sandbox',
112
+ 'fixtures',
113
+ category,
114
+ command,
115
+ arm,
116
+ 'expect.toml',
117
+ )
118
+ }
119
+
120
+ function stringArray(value: unknown): string[] {
121
+ if (!Array.isArray(value)) return []
122
+
123
+ return value.filter(
124
+ (entry): entry is string => typeof entry === 'string' && entry !== '',
125
+ )
126
+ }
127
+
128
+ function contentArray(value: unknown): ContentAssertion[] {
129
+ if (!Array.isArray(value)) return []
130
+
131
+ const assertions: ContentAssertion[] = []
132
+ for (const entry of value) {
133
+ if (typeof entry !== 'object' || entry === null) continue
134
+
135
+ const record = entry as Record<string, unknown>
136
+ if (typeof record.path !== 'string' || record.path === '') continue
137
+ if (typeof record.pattern !== 'string' || record.pattern === '') continue
138
+
139
+ assertions.push({ path: record.path, pattern: record.pattern })
140
+ }
141
+
142
+ return assertions
143
+ }
144
+
145
+ /**
146
+ * Throws on malformed TOML. `resolveVerdict` turns that into a failed verdict, so
147
+ * a typo in a declaration reads the same way a pattern that does not compile
148
+ * does, rather than surfacing as a stack trace.
149
+ */
150
+ export function parseExpectation(source: string): Expectation {
151
+ const parsed = Bun.TOML.parse(source) as Record<string, unknown>
152
+
153
+ return {
154
+ paths: stringArray(parsed.paths),
155
+ absent: stringArray(parsed.absent),
156
+ content: contentArray(parsed.content),
157
+ writeScope: stringArray(parsed.write_scope),
158
+ manual: stringArray(parsed.manual),
159
+ maxTurns:
160
+ typeof parsed.max_turns === 'number' ? parsed.max_turns : undefined,
161
+ }
162
+ }
163
+
164
+ /**
165
+ * `manual` is deliberately excluded. Counting it would let an arm declare five
166
+ * prose lines, assert nothing, and still read green, which is the vacuous case
167
+ * `scripts/core/install-check.sh` warns about in its own comment.
168
+ */
169
+ export function countMechanicalAssertions(expectation: Expectation): number {
170
+ return (
171
+ expectation.paths.length +
172
+ expectation.absent.length +
173
+ expectation.content.length +
174
+ expectation.writeScope.length
175
+ )
176
+ }
177
+
178
+ function checkPaths(
179
+ expectation: Expectation,
180
+ sandboxDir: string,
181
+ ): AssertionResult[] {
182
+ return expectation.paths.map((path) =>
183
+ existsSync(join(sandboxDir, path))
184
+ ? { ok: true, message: `exists: ${path}` }
185
+ : { ok: false, message: `missing: ${path}` },
186
+ )
187
+ }
188
+
189
+ function checkAbsent(
190
+ expectation: Expectation,
191
+ sandboxDir: string,
192
+ ): AssertionResult[] {
193
+ return expectation.absent.map((path) =>
194
+ existsSync(join(sandboxDir, path))
195
+ ? { ok: false, message: `should not exist: ${path}` }
196
+ : { ok: true, message: `absent: ${path}` },
197
+ )
198
+ }
199
+
200
+ /**
201
+ * A pattern against a missing file reports as a content miss rather than
202
+ * throwing, since an arm may assert content without also listing the path. A
203
+ * pattern that does not compile is a defect in the declaration, so it fails the
204
+ * assertion it belongs to rather than aborting the whole verdict.
205
+ */
206
+ function checkContent(
207
+ expectation: Expectation,
208
+ sandboxDir: string,
209
+ ): AssertionResult[] {
210
+ return expectation.content.map(({ path, pattern }) => {
211
+ const full = join(sandboxDir, path)
212
+ const label = `${path} =~ ${pattern}`
213
+
214
+ let matcher: RegExp
215
+ try {
216
+ matcher = new RegExp(pattern, 'm')
217
+ } catch {
218
+ return { ok: false, message: `invalid pattern for ${path}: ${pattern}` }
219
+ }
220
+
221
+ if (!existsSync(full) || !statSync(full).isFile()) {
222
+ return { ok: false, message: `no file to match: ${path}` }
223
+ }
224
+ if (matcher.test(readFileSync(full, 'utf8'))) {
225
+ return { ok: true, message: `matches: ${label}` }
226
+ }
227
+
228
+ return { ok: false, message: `no match: ${label}` }
229
+ })
230
+ }
231
+
232
+ /**
233
+ * The probe on 2026-07-30 found no permission mode that both allows writes under
234
+ * `.claude/` and scopes them, so the run goes wide and this assertion carries the
235
+ * property the permission layer used to. An arm declaring no scope skips it.
236
+ */
237
+ function checkWriteScope(
238
+ expectation: Expectation,
239
+ writes: readonly string[] | undefined,
240
+ ): KindOutcome {
241
+ if (expectation.writeScope.length === 0) return { results: [], skipped: [] }
242
+
243
+ if (writes === undefined) {
244
+ return {
245
+ results: [],
246
+ skipped: ['write scope: no write data supplied, pass --writes'],
247
+ }
248
+ }
249
+
250
+ const globs = expectation.writeScope.map((glob) => new Bun.Glob(glob))
251
+
252
+ return {
253
+ results: writes.map((path) =>
254
+ globs.some((glob) => glob.match(path))
255
+ ? { ok: true, message: `in scope: ${path}` }
256
+ : { ok: false, message: `wrote outside declared scope: ${path}` },
257
+ ),
258
+ skipped: [],
259
+ }
260
+ }
261
+
262
+ /**
263
+ * The envelope never determines a pass. It can only fail a run the expectations
264
+ * would otherwise have passed. Under `bypassPermissions` the denial count is
265
+ * always zero, so the write-scope assertion is what replaced it.
266
+ */
267
+ function checkEnvelope(
268
+ expectation: Expectation,
269
+ envelope: RunEnvelope | undefined,
270
+ ): KindOutcome {
271
+ if (envelope === undefined) {
272
+ const skipped =
273
+ expectation.maxTurns === undefined
274
+ ? []
275
+ : ['turn ceiling: no envelope supplied, pass --envelope']
276
+
277
+ return { results: [], skipped }
278
+ }
279
+
280
+ const results: AssertionResult[] = []
281
+
282
+ if (envelope.isError) {
283
+ results.push({ ok: false, message: 'envelope: is_error true' })
284
+ }
285
+ if (envelope.denials > 0) {
286
+ results.push({
287
+ ok: false,
288
+ message: `envelope: ${envelope.denials} permission denials`,
289
+ })
290
+ }
291
+ if (
292
+ expectation.maxTurns !== undefined &&
293
+ envelope.turns > expectation.maxTurns
294
+ ) {
295
+ results.push({
296
+ ok: false,
297
+ message: `envelope: ${envelope.turns} turns over ceiling of ${expectation.maxTurns}`,
298
+ })
299
+ }
300
+
301
+ return { results, skipped: [] }
302
+ }
303
+
304
+ export function checkExpectation(
305
+ expectation: Expectation,
306
+ input: CheckInput,
307
+ ): Verdict {
308
+ const scope = checkWriteScope(expectation, input.writes)
309
+ const envelope = checkEnvelope(expectation, input.envelope)
310
+
311
+ const results = [
312
+ ...checkPaths(expectation, input.sandboxDir),
313
+ ...checkAbsent(expectation, input.sandboxDir),
314
+ ...checkContent(expectation, input.sandboxDir),
315
+ ...scope.results,
316
+ ...envelope.results,
317
+ ]
318
+ const skipped = [...scope.skipped, ...envelope.skipped]
319
+
320
+ const failed = results.filter((result) => !result.ok).length
321
+
322
+ // A declaration counts what it declares, this counts what ran, and the two
323
+ // diverge on `write_scope`, which produces one result per write rather than one
324
+ // per glob. An arm declaring only a write scope against a run that wrote
325
+ // nothing would otherwise report pass having asserted nothing, which is the
326
+ // vacuous pass the whole feature exists to remove.
327
+ if (results.length === 0) {
328
+ return {
329
+ state: 'fail',
330
+ asserted: 0,
331
+ failed: 1,
332
+ unchecked: expectation.manual.length + skipped.length,
333
+ results: [
334
+ { ok: false, message: 'no assertion ran against this sandbox' },
335
+ ...results,
336
+ ],
337
+ manual: expectation.manual,
338
+ skipped,
339
+ note: 'The declaration asserts something, but nothing was checkable here. A pass with zero assertions is not a pass.',
340
+ }
341
+ }
342
+
343
+ return {
344
+ state: failed > 0 ? 'fail' : 'pass',
345
+ asserted: results.length,
346
+ failed,
347
+ unchecked: expectation.manual.length + skipped.length,
348
+ results,
349
+ manual: expectation.manual,
350
+ skipped,
351
+ }
352
+ }
353
+
354
+ /**
355
+ * A missing declaration and an empty one are different verdicts. Absence is the
356
+ * rollout state for most arms. A declaration that exists and asserts nothing is
357
+ * silent truncation, and it goes red.
358
+ */
359
+ export function resolveVerdict(expectFile: string, input: CheckInput): Verdict {
360
+ if (!existsSync(expectFile)) {
361
+ return {
362
+ state: 'unchecked',
363
+ asserted: 0,
364
+ failed: 0,
365
+ unchecked: 0,
366
+ results: [],
367
+ manual: [],
368
+ skipped: [],
369
+ note: `No expect.toml at ${expectFile}. Nothing was asserted.`,
370
+ }
371
+ }
372
+
373
+ let expectation: Expectation
374
+ try {
375
+ expectation = parseExpectation(readFileSync(expectFile, 'utf8'))
376
+ } catch (error) {
377
+ const reason = error instanceof Error ? error.message : String(error)
378
+
379
+ return {
380
+ state: 'fail',
381
+ asserted: 0,
382
+ failed: 1,
383
+ unchecked: 0,
384
+ results: [
385
+ { ok: false, message: `expect.toml does not parse: ${reason}` },
386
+ ],
387
+ manual: [],
388
+ skipped: [],
389
+ note: `Fix the declaration at ${expectFile}.`,
390
+ }
391
+ }
392
+
393
+ if (countMechanicalAssertions(expectation) === 0) {
394
+ return {
395
+ state: 'fail',
396
+ asserted: 0,
397
+ failed: 1,
398
+ unchecked: expectation.manual.length,
399
+ results: [
400
+ {
401
+ ok: false,
402
+ message: `expect.toml declares no mechanical assertion: ${expectFile}`,
403
+ },
404
+ ],
405
+ manual: expectation.manual,
406
+ skipped: [],
407
+ note: 'An expectation file that asserts nothing passes every run. Declare one or delete the file.',
408
+ }
409
+ }
410
+
411
+ return checkExpectation(expectation, input)
412
+ }