@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.
- package/LICENSE +21 -0
- package/README.md +99 -0
- package/claude/.claude-plugin/plugin.json +19 -0
- package/claude/skills/bash-script/SKILL.md +96 -0
- package/claude/skills/bash-script/references/patterns.md +349 -0
- package/claude/skills/ci-workflow/SKILL.md +52 -0
- package/claude/skills/ci-workflow/references/workflows.md +74 -0
- package/claude/skills/claude-address-review/SKILL.md +107 -0
- package/claude/skills/claude-autoship/SKILL.md +137 -0
- package/claude/skills/claude-design-extract/SKILL.md +69 -0
- package/claude/skills/claude-design-propose/SKILL.md +75 -0
- package/claude/skills/claude-diagram/SKILL.md +126 -0
- package/claude/skills/claude-docs/SKILL.md +204 -0
- package/claude/skills/claude-feature/REQUIREMENT.md +35 -0
- package/claude/skills/claude-feature/SKILL.md +148 -0
- package/claude/skills/claude-groundwork/SKILL.md +148 -0
- package/claude/skills/claude-groundwork/references/folder-format.md +107 -0
- package/claude/skills/claude-memory-capture/SKILL.md +80 -0
- package/claude/skills/claude-memory-review/REQUIREMENT.md +37 -0
- package/claude/skills/claude-memory-review/SKILL.md +206 -0
- package/claude/skills/claude-orchestrate/SKILL.md +125 -0
- package/claude/skills/claude-pr-review/SKILL.md +150 -0
- package/claude/skills/claude-review/SKILL.md +136 -0
- package/claude/skills/claude-roadmap/SKILL.md +61 -0
- package/claude/skills/claude-roadmap/references/roadmap.md +52 -0
- package/claude/skills/claude-screencast/SKILL.md +160 -0
- package/claude/skills/claude-seed-sync/SKILL.md +141 -0
- package/claude/skills/claude-slides-draft/SKILL.md +62 -0
- package/claude/skills/claude-standards-audit/REQUIREMENT.md +33 -0
- package/claude/skills/claude-standards-audit/SKILL.md +79 -0
- package/claude/skills/claude-standards-audit/references/branch.md +49 -0
- package/claude/skills/claude-standards-audit/references/pr.md +124 -0
- package/claude/skills/claude-tasks/SKILL.md +123 -0
- package/claude/skills/claude-ui-test/SKILL.md +75 -0
- package/claude/skills/claude-ux-audit/SKILL.md +78 -0
- package/claude/skills/claude-worktree/SKILL.md +79 -0
- package/claude/skills/cli-script/SKILL.md +47 -0
- package/claude/skills/cli-script/references/template.md +43 -0
- package/claude/skills/create-rule/SKILL.md +68 -0
- package/claude/skills/create-skill/REQUIREMENT.md +30 -0
- package/claude/skills/create-skill/SKILL.md +24 -0
- package/claude/skills/create-snippet/SKILL.md +28 -0
- package/claude/skills/create-snippet/references/snippets.md +57 -0
- package/claude/skills/create-standard/SKILL.md +31 -0
- package/claude/skills/create-standard/references/standard.md +52 -0
- package/claude/skills/docs-sync/SKILL.md +87 -0
- package/claude/skills/git-branch/REQUIREMENT.md +38 -0
- package/claude/skills/git-branch/SKILL.md +60 -0
- package/claude/skills/git-branch/references/branch.md +49 -0
- package/claude/skills/git-commit/REQUIREMENT.md +33 -0
- package/claude/skills/git-commit/SKILL.md +51 -0
- package/claude/skills/git-commit/references/commit.md +63 -0
- package/claude/skills/git-followup/REQUIREMENT.md +38 -0
- package/claude/skills/git-followup/SKILL.md +35 -0
- package/claude/skills/git-issue/REQUIREMENT.md +38 -0
- package/claude/skills/git-issue/SKILL.md +69 -0
- package/claude/skills/git-issue/references/issue.md +84 -0
- package/claude/skills/git-pr/REQUIREMENT.md +37 -0
- package/claude/skills/git-pr/SKILL.md +119 -0
- package/claude/skills/git-pr/references/branch.md +49 -0
- package/claude/skills/git-pr/references/pr.md +124 -0
- package/claude/skills/git-ship/REQUIREMENT.md +34 -0
- package/claude/skills/git-ship/SKILL.md +39 -0
- package/claude/skills/git-split/REQUIREMENT.md +38 -0
- package/claude/skills/git-split/SKILL.md +160 -0
- package/claude/skills/git-split/references/branch.md +49 -0
- package/claude/skills/git-split/references/pr.md +124 -0
- package/claude/skills/git-stage/REQUIREMENT.md +36 -0
- package/claude/skills/git-stage/SKILL.md +74 -0
- package/claude/skills/git-stage/references/commit.md +63 -0
- package/claude/skills/git-worktree/REQUIREMENT.md +37 -0
- package/claude/skills/git-worktree/SKILL.md +120 -0
- package/claude/skills/migration-claude-md/SKILL.md +74 -0
- package/claude/skills/migration-context/SKILL.md +93 -0
- package/claude/skills/migration-standards/SKILL.md +75 -0
- package/claude/skills/project-commands/SKILL.md +73 -0
- package/claude/skills/session-resume/SKILL.md +36 -0
- package/claude/skills/setup-gov/SKILL.md +71 -0
- package/claude/skills/setup-indexes/SKILL.md +148 -0
- package/claude/skills/setup-init/SKILL.md +114 -0
- package/claude/skills/setup-plugins/SKILL.md +81 -0
- package/claude/skills/setup-plugins/references/plugin-catalog.md +53 -0
- package/claude/skills/setup-verify/SKILL.md +51 -0
- package/claude/skills/systematic-debugging/SKILL.md +70 -0
- package/claude/skills/toolkit-cli/SKILL.md +38 -0
- package/claude/skills/toolkit-feedback/SKILL.md +80 -0
- package/claude/skills/toolkit-operator/SKILL.md +45 -0
- package/claude/skills/toolkit-triage/SKILL.md +63 -0
- package/claude/skills/youtube-transcripts/SKILL.md +34 -0
- package/docs/agents.md +371 -0
- package/docs/ai-workflow.md +157 -0
- package/docs/index.md +20 -0
- package/docs/operating-model.md +106 -0
- package/docs/target-projects.md +183 -0
- package/docs/visual-design-workflow.md +136 -0
- package/docs/zshrc-aliases.md +65 -0
- package/governance/rules/claude/500-prose.md +12 -0
- package/governance/rules/claude/510-context.md +16 -0
- package/governance/rules/claude/520-wireframes.md +12 -0
- package/governance/rules/claude/530-requirements.md +11 -0
- package/governance/rules/claude/540-architecture.md +11 -0
- package/governance/rules/claude/550-design.md +11 -0
- package/governance/rules/claude/555-tasks.md +12 -0
- package/governance/rules/claude/560-diagrams.md +11 -0
- package/governance/rules/claude/570-skill.md +18 -0
- package/governance/rules/claude/580-readme.md +11 -0
- package/governance/rules/claude/590-rule-authoring.md +12 -0
- package/governance/rules/core/000-constitution.md +30 -0
- package/governance/rules/core/010-testing.md +35 -0
- package/governance/rules/core/020-concurrency.md +22 -0
- package/governance/rules/core/030-error-handling.md +31 -0
- package/governance/rules/core/040-performance.md +20 -0
- package/governance/rules/core/050-logging.md +20 -0
- package/governance/rules/core/060-naming.md +19 -0
- package/governance/rules/core/070-planning.md +13 -0
- package/governance/rules/core/080-config-comments.md +22 -0
- package/governance/rules/framework/200-react.md +51 -0
- package/governance/rules/framework/210-astro.md +41 -0
- package/governance/rules/framework/220-fastapi.md +43 -0
- package/governance/rules/framework/230-nextjs.md +48 -0
- package/governance/rules/framework/250-tailwind.md +32 -0
- package/governance/rules/framework/260-shadcn.md +34 -0
- package/governance/rules/lang/100-typescript.md +40 -0
- package/governance/rules/lang/110-python.md +42 -0
- package/governance/rules/lib/300-testing-ts.md +38 -0
- package/governance/rules/lib/310-zod.md +25 -0
- package/governance/rules/lib/320-tanstack-query.md +32 -0
- package/governance/rules/lib/330-testing-py.md +44 -0
- package/governance/rules/lib/340-pydantic.md +38 -0
- package/governance/rules/lib/350-security-web.md +31 -0
- package/governance/rules/ui/400-ui.md +34 -0
- package/governance/rules/ui/410-a11y.md +44 -0
- package/governance/rules/ui/420-forms.md +34 -0
- package/governance/rules/ui/430-ux-completeness.md +62 -0
- package/governance/stacks/astro.toml +2 -0
- package/governance/stacks/base.toml +2 -0
- package/governance/stacks/node.toml +2 -0
- package/governance/stacks/python-fastapi.toml +2 -0
- package/governance/stacks/python.toml +2 -0
- package/governance/stacks/react.toml +2 -0
- package/package.json +67 -0
- package/scripts/config.sh +11 -0
- package/scripts/core/bootstrap.sh +81 -0
- package/scripts/core/check-plugin-boundary.sh +45 -0
- package/scripts/core/check-skill-paths.sh +25 -0
- package/scripts/core/clean.sh +36 -0
- package/scripts/core/install-check.sh +93 -0
- package/scripts/core/regen-claude-copies.sh +26 -0
- package/scripts/core/regen-indexes.sh +10 -0
- package/scripts/core/regen-skill-references.sh +27 -0
- package/scripts/core/snapshot.sh +132 -0
- package/scripts/core/update.sh +35 -0
- package/scripts/core/verify.sh +201 -0
- package/scripts/docs/list.sh +129 -0
- package/scripts/gov/list.sh +234 -0
- package/scripts/lib/frontmatter.sh +30 -0
- package/scripts/lib/gov.sh +14 -0
- package/scripts/lib/sandbox-fixtures.sh +109 -0
- package/scripts/lib/sandbox-git.sh +73 -0
- package/scripts/lib/tooling.sh +24 -0
- package/scripts/lib/ui.sh +173 -0
- package/scripts/lib/worktree.sh +20 -0
- package/scripts/manage-sandbox.sh +453 -0
- package/scripts/snippets/create.sh +156 -0
- package/scripts/standards/list.sh +91 -0
- package/scripts/tooling/create.sh +109 -0
- package/scripts/tooling/ref.sh +141 -0
- package/scripts/tooling/verify.sh +179 -0
- package/snippets/claude/decision-memo.md +39 -0
- package/snippets/claude/feature-recap.md +19 -0
- package/snippets/claude/figma-steps.md +18 -0
- package/snippets/claude/orchestrator-resume.md +23 -0
- package/snippets/claude/orchestrator-sweep.md +10 -0
- package/snippets/compact-summary.md +5 -0
- package/snippets/create-snippet.md +7 -0
- package/snippets/decision-help.md +6 -0
- package/snippets/meta-prompt.md +14 -0
- package/snippets/research-prompt.md +7 -0
- package/snippets/session-notes.md +11 -0
- package/snippets/snippets.toml +6 -0
- package/snippets/step-by-step.md +10 -0
- package/snippets/web-research.md +21 -0
- package/src/claude/gitignore.ts +51 -0
- package/src/claude/seeds-list.ts +47 -0
- package/src/claude/seeds.ts +124 -0
- package/src/claude/settings.ts +151 -0
- package/src/cli-run.ts +43 -0
- package/src/cli.ts +111 -0
- package/src/commands/claude.ts +349 -0
- package/src/commands/design.ts +39 -0
- package/src/commands/docs.ts +59 -0
- package/src/commands/feedback-format.ts +23 -0
- package/src/commands/feedback.ts +103 -0
- package/src/commands/gov.ts +268 -0
- package/src/commands/indexes.ts +182 -0
- package/src/commands/init.ts +117 -0
- package/src/commands/pass-through.ts +39 -0
- package/src/commands/sandbox.ts +315 -0
- package/src/commands/slides.ts +106 -0
- package/src/commands/snippets.ts +236 -0
- package/src/commands/standards.ts +89 -0
- package/src/commands/sync.ts +211 -0
- package/src/commands/tasks.ts +185 -0
- package/src/commands/tooling.ts +432 -0
- package/src/commands/transcripts.ts +48 -0
- package/src/commands/wiki.ts +101 -0
- package/src/copy.ts +30 -0
- package/src/design/parse.ts +79 -0
- package/src/design/render.ts +170 -0
- package/src/docs/read.ts +59 -0
- package/src/exec.ts +17 -0
- package/src/frontmatter.ts +13 -0
- package/src/github-format.ts +13 -0
- package/src/github.ts +20 -0
- package/src/gov/adapter.ts +73 -0
- package/src/gov/install.ts +103 -0
- package/src/gov/payload.ts +39 -0
- package/src/gov/stacks.ts +124 -0
- package/src/indexes/frontmatter.ts +46 -0
- package/src/indexes/regen.ts +84 -0
- package/src/indexes/render.ts +181 -0
- package/src/indexes/walk.ts +111 -0
- package/src/init/flags.ts +66 -0
- package/src/init/plan.ts +128 -0
- package/src/init/run.ts +46 -0
- package/src/init/steps.ts +91 -0
- package/src/sandbox/coverage.ts +115 -0
- package/src/sandbox/expect.ts +412 -0
- package/src/slides/layouts.ts +391 -0
- package/src/slides/open.ts +18 -0
- package/src/slides/parse.ts +84 -0
- package/src/slides/render.ts +88 -0
- package/src/slides/styles.ts +44 -0
- package/src/snippets/adapter.ts +34 -0
- package/src/snippets/categories.ts +59 -0
- package/src/snippets/install.ts +125 -0
- package/src/snippets/list.ts +32 -0
- package/src/snippets/presets.ts +50 -0
- package/src/standards/adapter.ts +51 -0
- package/src/standards/index-refresh.ts +44 -0
- package/src/standards/install.ts +52 -0
- package/src/sync/check.ts +269 -0
- package/src/sync/engine.ts +416 -0
- package/src/sync/git.ts +204 -0
- package/src/sync/stamp.ts +202 -0
- package/src/sync/target.ts +79 -0
- package/src/sync/workflow.ts +201 -0
- package/src/target.ts +43 -0
- package/src/tasks/archive.ts +304 -0
- package/src/tooling/gitignore.ts +122 -0
- package/src/tooling/inject.ts +214 -0
- package/src/tooling/list.ts +39 -0
- package/src/tooling/manifest.ts +178 -0
- package/src/tooling/package.ts +166 -0
- package/src/tooling/scan.ts +186 -0
- package/src/transcripts/fetch.ts +156 -0
- package/src/transcripts/metadata.ts +54 -0
- package/src/transcripts/vtt.ts +114 -0
- package/src/ui.ts +187 -0
- package/src/wiki/init.ts +85 -0
- package/standards/architecture.md +41 -0
- package/standards/bundled/branch.md +49 -0
- package/standards/bundled/commit.md +63 -0
- package/standards/bundled/issue.md +84 -0
- package/standards/bundled/pr.md +124 -0
- package/standards/bundled/roadmap.md +52 -0
- package/standards/bundled/snippets.md +57 -0
- package/standards/bundled/standard.md +52 -0
- package/standards/context.md +132 -0
- package/standards/design.md +31 -0
- package/standards/diagrams.md +55 -0
- package/standards/index.md +21 -0
- package/standards/prose.md +122 -0
- package/standards/readme.md +163 -0
- package/standards/requirements.md +61 -0
- package/standards/rule.md +79 -0
- package/standards/skill.md +257 -0
- package/standards/tasks.md +140 -0
- package/standards/versioning.md +47 -0
- package/standards/wireframes.md +67 -0
- package/tooling/astro/configs/astro.config.mjs +17 -0
- package/tooling/astro/configs/eslint.config.js +79 -0
- package/tooling/astro/configs/playwright.config.ts +25 -0
- package/tooling/astro/configs/tsconfig.json +12 -0
- package/tooling/astro/configs/vitest.config.ts +22 -0
- package/tooling/astro/manifest.toml +32 -0
- package/tooling/astro/reference.md +60 -0
- package/tooling/base/configs/.editorconfig +5 -0
- package/tooling/base/configs/.github/pull_request_template.md +18 -0
- package/tooling/base/configs/.github/workflows/verify.yml +35 -0
- package/tooling/base/configs/.husky/commit-msg +1 -0
- package/tooling/base/configs/.husky/post-merge +61 -0
- package/tooling/base/configs/.husky/post-rewrite +21 -0
- package/tooling/base/configs/.husky/pre-commit +1 -0
- package/tooling/base/configs/.husky/pre-push +1 -0
- package/tooling/base/configs/.prettierrc +12 -0
- package/tooling/base/configs/.shellcheckrc +1 -0
- package/tooling/base/configs/.vscode/extensions.json +9 -0
- package/tooling/base/configs/.vscode/settings.json +3 -0
- package/tooling/base/configs/commitlint.config.js +11 -0
- package/tooling/base/configs/scripts/clean.sh +45 -0
- package/tooling/base/configs/scripts/update.sh +49 -0
- package/tooling/base/configs/scripts/verify.sh +64 -0
- package/tooling/base/manifest.toml +32 -0
- package/tooling/base/reference.md +114 -0
- package/tooling/base/seeds/.claude/context/ci.md +27 -0
- package/tooling/base/seeds/.claude/context/development.md +32 -0
- package/tooling/base/seeds/.cspell/project-terms.txt +0 -0
- package/tooling/base/seeds/.cspell/tech-stack.txt +19 -0
- package/tooling/base/seeds/.lintstagedrc +8 -0
- package/tooling/base/seeds/.prettierignore +0 -0
- package/tooling/base/seeds/cspell.json +20 -0
- package/tooling/claude/manifest.toml +11 -0
- package/tooling/claude/reference.md +62 -0
- package/tooling/claude/seeds/.claude/ARCHITECTURE.md +13 -0
- package/tooling/claude/seeds/.claude/DESIGN.md +62 -0
- package/tooling/claude/seeds/.claude/REQUIREMENTS.md +18 -0
- package/tooling/claude/seeds/.claude/context/index.md +8 -0
- package/tooling/claude/seeds/.claude/hooks/index-reminder.sh +43 -0
- package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +45 -0
- package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +61 -0
- package/tooling/claude/seeds/.claude/hooks/tasks-index.sh +60 -0
- package/tooling/claude/seeds/.claude/settings.json +39 -0
- package/tooling/claude/seeds/.claude/tasks/index.md +8 -0
- package/tooling/claude/seeds/.claude/wireframes/index.md +8 -0
- package/tooling/claude/seeds/CLAUDE.md +87 -0
- package/tooling/claude/user/settings.template.json +10 -0
- package/tooling/claude/user/statusline-command.sh +53 -0
- package/tooling/python/configs/.coveragerc +14 -0
- package/tooling/python/configs/.python-version +1 -0
- package/tooling/python/configs/mypy.ini +6 -0
- package/tooling/python/configs/pytest.ini +4 -0
- package/tooling/python/configs/ruff.toml +15 -0
- package/tooling/python/configs/scripts/verify.sh +77 -0
- package/tooling/python/manifest.toml +16 -0
- package/tooling/python/reference.md +66 -0
- package/tooling/python/seeds/.cspell/tech-stack.txt +19 -0
- package/tooling/python/seeds/tests/test_smoke.py +2 -0
- package/tooling/vite-react/configs/playwright.config.ts +25 -0
- package/tooling/vite-react/configs/tsconfig.json +35 -0
- package/tooling/vite-react/configs/vite.config.ts +14 -0
- package/tooling/vite-react/configs/vitest.config.ts +26 -0
- package/tooling/vite-react/manifest.toml +21 -0
- package/tooling/vite-react/reference.md +55 -0
- package/tooling/vite-react/seeds/.cspell/project-terms.txt +1 -0
- package/tooling/vite-react/seeds/.cspell/tech-stack.txt +1 -0
- package/tooling/web/configs/.github/workflows/verify.yml +134 -0
- package/tooling/web/configs/.vscode/extensions.json +13 -0
- package/tooling/web/configs/.vscode/settings.json +10 -0
- package/tooling/web/configs/e2e/home.spec.ts +6 -0
- package/tooling/web/configs/e2e/screenshot.ts +41 -0
- package/tooling/web/configs/eslint.config.js +82 -0
- package/tooling/web/configs/scripts/screenshot.sh +28 -0
- package/tooling/web/configs/scripts/verify.sh +80 -0
- package/tooling/web/configs/src/test/setup.ts +8 -0
- package/tooling/web/manifest.toml +57 -0
- package/tooling/web/reference.md +95 -0
- package/tooling/web/seeds/.cspell/tech-stack.txt +18 -0
- 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
|
+
}
|