@erclx/aitk 0.95.0 → 0.97.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/claude/.claude-plugin/plugin.json +1 -1
- package/docs/agents/commands.md +1 -1
- package/docs/agents/index.md +1 -1
- package/docs/agents/records.md +23 -11
- package/docs/ai-workflow.md +114 -21
- package/governance/rules/ui/440-surface-capture.md +26 -0
- package/governance/stacks/astro.toml +1 -1
- package/governance/stacks/react.toml +1 -1
- package/package.json +1 -1
- package/src/commands/records.ts +25 -7
- package/src/records/backup.ts +3 -2
- package/src/records/validate.ts +228 -11
- package/src/worktree.ts +16 -0
- package/standards/standard.md +8 -0
- package/tooling/base/reference.md +2 -0
- package/tooling/web/configs/e2e/screenshot.ts +36 -24
- package/tooling/web/reference.md +4 -2
package/docs/agents/commands.md
CHANGED
|
@@ -32,7 +32,7 @@ Full help: `aitk <command> --help`. Behavior notes for the install and sync verb
|
|
|
32
32
|
| `aitk tasks validate` | Report board rows whose plan, task file, group, or file set does not hold (`--json`) |
|
|
33
33
|
| `aitk intake list` | Report intake folder counts, or one folder's items, keeping what is unread with `--unread` (`--json`) |
|
|
34
34
|
| `aitk intake answer` | Write selections into one cluster's answer slots, repeating `--set <item>=<answer>` (`--json`) |
|
|
35
|
-
| `aitk records validate` | Report a session record against the standard governing it, per kind (`--json`)
|
|
35
|
+
| `aitk records validate` | Report a session record or a standard against the standard governing it, per kind (`--json`) |
|
|
36
36
|
| `aitk records push` | Commit the eight backed record folders and push them to a private records remote (`--json`) |
|
|
37
37
|
| `aitk records pull` | Fetch the records remote and write it back, refusing rather than discarding unpushed records (`--json`) |
|
|
38
38
|
| `aitk sessions list` | Resolve live sessions to the worktree and branch each holds, filtered by `--branch` (`--json`) |
|
package/docs/agents/index.md
CHANGED
|
@@ -20,7 +20,7 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
|
|
|
20
20
|
- [Markdown audit](markdown-audit.md): Running the audit over any markdown path, where its bans and checkpoints are read from, what each check reports, and why the ban half gates while the structural half reports
|
|
21
21
|
- [Output shape](output-shape.md): Two framed shapes every command renders into, how JSON and --names modes keep stdout clean, and the exit discipline that lets piped output drain
|
|
22
22
|
- [Overview](overview.md): What this folder covers, the invocation rules every command inherits, and where domain behavior is documented instead
|
|
23
|
-
- [Records](records.md): Validating the
|
|
23
|
+
- [Records](records.md): Validating the session records under .claude/ and the standards corpus, the per-kind checks, the refusal reasons, backing the folders to a private remote, and which root each kind defaults to
|
|
24
24
|
- [Sandbox](sandbox.md): Scenario routing, the expectation scoring surface, and the coverage census over scenarios and skills
|
|
25
25
|
- [Scripting](scripting.md): The runtime catalogs that replace hardcoded names, what each carries, and a headless invocation per domain
|
|
26
26
|
- [Sessions](sessions.md): Resolving live peer sessions to the worktree and branch each holds, the liveness confidence field, the unresolved reasons, and what the read depends on
|
package/docs/agents/records.md
CHANGED
|
@@ -1,28 +1,31 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Records
|
|
3
|
-
description: Validating the
|
|
3
|
+
description: Validating the session records under .claude/ and the standards corpus, the per-kind checks, the refusal reasons, backing the folders to a private remote, and which root each kind defaults to
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Records
|
|
7
7
|
|
|
8
8
|
## Validate
|
|
9
9
|
|
|
10
|
-
`aitk records validate <kind>` reports where a
|
|
10
|
+
`aitk records validate <kind>` reports where a file and the standard governing it disagree. Four kinds are gitignored folders under `.claude/`: `plans`, `groundwork`, `intake`, and `memory`. The fifth is `standards`, the authoring corpus, which is tracked and installed rather than scratch.
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
13
|
aitk records validate plans
|
|
14
14
|
aitk records validate memory
|
|
15
|
+
aitk records validate standards
|
|
15
16
|
aitk records validate intake --json
|
|
16
17
|
```
|
|
17
18
|
|
|
18
|
-
| Option | Behavior
|
|
19
|
-
| --------------- |
|
|
20
|
-
| `--json` | Add a machine-readable record on stdout
|
|
21
|
-
| `--root <path>` | Project root, defaulting to the main worktree |
|
|
19
|
+
| Option | Behavior |
|
|
20
|
+
| --------------- | ------------------------------------------------------------------- |
|
|
21
|
+
| `--json` | Add a machine-readable record on stdout |
|
|
22
|
+
| `--root <path>` | Project root, defaulting to the main worktree except on `standards` |
|
|
22
23
|
|
|
23
|
-
It reads and never writes.
|
|
24
|
+
It reads and never writes, and the reason splits by kind. A session record is per-machine scratch with no history behind it, so a repair that guessed wrong could not be undone. A standard installs into every target and is cited by bare filename, so a rename the verb performed would reach further than the file it moved, which is why a finding naming one says so.
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
`standards` reads the authoring root at `standards/` where it exists and the installed copy at `.claude/standards/` otherwise. The authoring root wins because the installed tree is generated from it in the toolkit, where a fix written to the copy is discarded by the next regen. A project that consumed the corpus holds only the second, so one precedence serves both. The walk stays flat, matching install and the catalog, so `standards/bundled/` is out of range: its members are named for the skill that reads them rather than for a path they govern.
|
|
27
|
+
|
|
28
|
+
Nothing fires it automatically. The four record folders are gitignored, so the standards-audit hook exits early on them and any check reading changed files from git never lists one. The corpus is tracked and still unreached, since the markdown audit reads content across the files git lists and rules on no filename. The verb runs at the moment a session claims the record is finished, which is the same placement `aitk tasks validate` takes over the board.
|
|
26
29
|
|
|
27
30
|
### What each kind checks
|
|
28
31
|
|
|
@@ -32,6 +35,7 @@ Nothing fires it automatically. The folders are gitignored, so the standards-aud
|
|
|
32
35
|
| `groundwork` | A track with no `README.md` or no `01-current-state.md`, a file missing `title` or `description`, a `README.md` with no `date` as `YYYY-MM-DD`, an unnumbered file, and a track holding a decision without its handoff or the reverse |
|
|
33
36
|
| `intake` | A dump with no `00-overview.md`, the same frontmatter and numbering checks, an item missing any of `Problem`, `Fix`, `Worth it`, or `You`, and an item carrying `Open` with no `Suggested` |
|
|
34
37
|
| `memory` | A filename whose prefix names none of the four types, an entry missing `title`, `description`, or `category`, a `category` disagreeing with that prefix, a title repeating the filename, and a rule-bearing body missing a part |
|
|
38
|
+
| `standards` | A standard missing `title` or `description`, an absent `## Scope` section, a scope section carrying no `Does not govern:` list, a statement anchoring nothing, and a filename naming no part of the path the statement governs |
|
|
35
39
|
|
|
36
40
|
The half-closed track is the groundwork check a reader cannot run by eye. A folder holding `06` without `07` reads as closed to anyone scanning filenames while the file a returning session actually opens is absent.
|
|
37
41
|
|
|
@@ -39,15 +43,23 @@ The item check skips `00-overview.md` and `99-next-session.md`, since neither ho
|
|
|
39
43
|
|
|
40
44
|
A memory `category` is compared against the sentence-case form of the filename prefix rather than checked field by field, so one finding covers a prefix outside the four types, a field disagreeing with the prefix, and a casing drift that would open a second group in the catalog. The body check runs on `feedback` and `project` entries alone, because a `user` or `reference` entry is a single sentence by design and has no rule to apply.
|
|
41
45
|
|
|
46
|
+
The standards filename check derives a word from the governed path rather than counting words in the name. Every member of the corpus is named for the artifact its scope statement governs, and one word is what that produces rather than the rule itself, so a check keyed on word count would pass a conforming single word naming the wrong artifact. Each path segment offers its own word, a dotted container segment offers none, a hyphenated segment offers its parts, and the singular and the plural both match.
|
|
47
|
+
|
|
48
|
+
The scope statement is read exactly as `scripts/standards/list.sh` reads it for the catalog's `appliesTo` field: backticked spans in the first sentence alone, with an attribute standard resolving where that sentence backticks nothing and the statement says it governs an attribute. One sentence read two ways would let a standard pass the check while publishing a different jurisdiction to every consumer of the catalog. An attribute standard is exempt from the filename derivation because it governs no path to derive from, and a statement that anchors nothing and claims no attribute is reported as `scope-unanchored` rather than passed, since silence there would let the backticks be removed to disable the check.
|
|
49
|
+
|
|
50
|
+
An absent `## Scope` section suppresses the filename finding. The name derives from the statement, so a missing section leaves nothing to derive against, and reporting both would name one defect twice and point the fix at the wrong file. The walk skips `index.md`, which is generated from its siblings rather than authored.
|
|
51
|
+
|
|
42
52
|
A plan section opens as a bold label or as an H2 and the check counts both, naming the standard's spelling when it reports one missing. The corpus splits roughly four to one between the two forms, so failing the variant would report nearly every plan on a rule that costs a reader nothing.
|
|
43
53
|
|
|
44
54
|
A section runs to the next marker-shaped line whatever it names, so a plan carrying a label of its own closes the section above it rather than collecting into it. Fenced blocks are dropped before any of this, since a plan showing the shape it writes puts real-looking bullets and headings inside a fence.
|
|
45
55
|
|
|
46
56
|
### Exit codes and refusals
|
|
47
57
|
|
|
48
|
-
Exit codes: `0` every check passed, `1` refused, `2` at least one record carries a finding. A `reason` field carries which gate fired: `no-folder` when the kind's
|
|
58
|
+
Exit codes: `0` every check passed, `1` refused, `2` at least one record carries a finding. A `reason` field carries which gate fired: `no-folder` when none of the kind's directories exist, and `unknown-kind` when the argument names no published kind. A `no-folder` message names every candidate, so the `standards` refusal names both roots.
|
|
59
|
+
|
|
60
|
+
The four record folders are shared scratch at the main worktree root, so `--root` defaults to the first entry of `git worktree list` rather than the working directory. A linked worktree validates the same records every other session reads.
|
|
49
61
|
|
|
50
|
-
|
|
62
|
+
`standards` takes the other default, the root of the checkout the caller stands in. The corpus is tracked, so a linked worktree holds its own edited copy, and resolving the main root there would report on a tree the session never touched while saying nothing about which one it read. A session that adds or renames a standard inside a worktree is the case, and it is the one the check exists for.
|
|
51
63
|
|
|
52
64
|
Skills branch on the findings rather than on the exit code:
|
|
53
65
|
|
|
@@ -55,7 +67,7 @@ Skills branch on the findings rather than on the exit code:
|
|
|
55
67
|
aitk records validate plans --json | jq -r '.findings[] | "\(.kind): \(.subject)"'
|
|
56
68
|
```
|
|
57
69
|
|
|
58
|
-
For the shapes each check enforces, see `.claude/standards/plan.md`, `.claude/standards/groundwork.md`, `.claude/standards/intake.md`, and `.claude/standards/
|
|
70
|
+
For the shapes each check enforces, see `.claude/standards/plan.md`, `.claude/standards/groundwork.md`, `.claude/standards/intake.md`, `.claude/standards/memory.md`, and `.claude/standards/standard.md`.
|
|
59
71
|
|
|
60
72
|
## Push and pull
|
|
61
73
|
|
package/docs/ai-workflow.md
CHANGED
|
@@ -164,27 +164,120 @@ Before the first feature session on a UI-heavy project, pick a design tier. The
|
|
|
164
164
|
|
|
165
165
|
## Skills
|
|
166
166
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
|
174
|
-
|
|
|
175
|
-
| `aitk:
|
|
176
|
-
| `aitk:
|
|
177
|
-
| `aitk:
|
|
178
|
-
| `aitk:
|
|
179
|
-
| `aitk:
|
|
180
|
-
| `aitk:
|
|
181
|
-
| `aitk:claude-
|
|
182
|
-
| `aitk:claude-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
|
187
|
-
|
|
|
167
|
+
Groups run in the order a project meets them, so a reader at a known point scans to that group and reads across. The set reconciles the scenarios above with the lifecycle [target projects](target-projects.md) describes, rather than inventing a third vocabulary beside those two, so a group name matches neither source exactly and every moment either one names has a group. Each row says when to reach for the skill. What it does is the skill's own description.
|
|
168
|
+
|
|
169
|
+
This section is the corpus the coverage claim is measured against: every name `aitk claude skills list --names` reports takes exactly one row here. A skill serving two moments sits at the earlier one, and mentions elsewhere in this file are prose rather than routing.
|
|
170
|
+
|
|
171
|
+
### Set up a project
|
|
172
|
+
|
|
173
|
+
| Skill | When to use |
|
|
174
|
+
| ---------------------------- | --------------------------------------------------------------------------------------- |
|
|
175
|
+
| `aitk:setup-init` | On a fresh scaffold, to detect the stack and run the whole install chain in one pass |
|
|
176
|
+
| `aitk:toolkit-operator` | On a project that already exists, to read what it carries before an install is picked |
|
|
177
|
+
| `aitk:setup-gov` | When the governance rules are wanted without the tooling chain |
|
|
178
|
+
| `aitk:setup-indexes` | When a markdown-heavy folder needs an `index.md` a session can browse |
|
|
179
|
+
| `aitk:setup-plugins` | On a new machine, to install the community and official plugins user-scoped |
|
|
180
|
+
| `aitk:setup-verify` | After the agent generates configs, to run the installed scripts and report pass or fail |
|
|
181
|
+
| `aitk:claude-design-extract` | Before the first UI feature, to draft `.claude/DESIGN.md` |
|
|
182
|
+
| `aitk:claude-diagram` | Once the architecture is written, to draft per-kind entries under `.claude/diagrams/` |
|
|
183
|
+
|
|
184
|
+
### Decide what to build
|
|
185
|
+
|
|
186
|
+
| Skill | When to use |
|
|
187
|
+
| --------------------------- | ------------------------------------------------------------------------------- |
|
|
188
|
+
| `aitk:claude-intake` | When the input is a pile of findings rather than one feature |
|
|
189
|
+
| `aitk:claude-intake-answer` | When an intake folder holds unread slots waiting on your decision |
|
|
190
|
+
| `aitk:claude-groundwork` | When the state is unmeasured and more than one approach is live |
|
|
191
|
+
| `aitk:decision-escalate` | When open decisions turn on your preference and want batching into one set |
|
|
192
|
+
| `aitk:claude-roadmap` | When MVP scope needs sequencing into ordered versions |
|
|
193
|
+
| `aitk:claude-tasks` | When a decided item needs a file on the board, or a shipped one needs archiving |
|
|
194
|
+
| `aitk:claude-feature` | When the approach is settled and the next step is a plan |
|
|
195
|
+
|
|
196
|
+
### Build the feature
|
|
197
|
+
|
|
198
|
+
| Skill | When to use |
|
|
199
|
+
| --------------------------- | ------------------------------------------------------------------- |
|
|
200
|
+
| `aitk:claude-worktree` | At the plan-to-execute boundary, to get an isolated tree and branch |
|
|
201
|
+
| `aitk:claude-autoship` | After plan approval, to chain implement, verify, review, draft PR |
|
|
202
|
+
| `aitk:project-commands` | When the project's own command needs running |
|
|
203
|
+
| `aitk:systematic-debugging` | When a test fails or a bug surfaces, to force root cause first |
|
|
204
|
+
| `aitk:claude-ui-test` | After a UI change, to generate e2e tests and a visual checklist |
|
|
205
|
+
|
|
206
|
+
### Check the work before it leaves the branch
|
|
207
|
+
|
|
208
|
+
| Skill | When to use |
|
|
209
|
+
| ----------------------------- | --------------------------------------------------------------------- |
|
|
210
|
+
| `aitk:claude-review` | On the local branch diff, before anything is pushed |
|
|
211
|
+
| `aitk:claude-standards-audit` | When changed markdown has to answer to the authoring standards |
|
|
212
|
+
| `aitk:claude-ux-audit` | To read UI source for missing states, edge cases, and inconsistencies |
|
|
213
|
+
| `aitk:claude-ux-measure` | To start the interface and measure paint, processor, and layout cost |
|
|
214
|
+
|
|
215
|
+
### Ship it
|
|
216
|
+
|
|
217
|
+
| Skill | When to use |
|
|
218
|
+
| ---------------------------- | ---------------------------------------------------------------------------------- |
|
|
219
|
+
| `aitk:git-ship` | To run the whole post-feature chain from docs sync through open PR |
|
|
220
|
+
| `aitk:claude-memory-capture` | First in that chain, to route what the session learned to the surface owning it |
|
|
221
|
+
| `aitk:claude-docs` | When decisions diverged from the plan, or a shipped task needs its outcomes marked |
|
|
222
|
+
| `aitk:docs-sync` | When a change since main left `README.md` or `docs/` stale |
|
|
223
|
+
| `aitk:git-stage` | When the staged set spans several concerns and wants one commit each |
|
|
224
|
+
| `aitk:git-commit` | When the staged set is one concern, or was staged hunk by hand |
|
|
225
|
+
| `aitk:git-branch` | When a branch name needs generating or renaming to conventional form |
|
|
226
|
+
| `aitk:git-pr` | When a pull request needs a title and body written from the diff |
|
|
227
|
+
| `aitk:claude-memory-review` | After capture writes an entry, to propose where each one belongs |
|
|
228
|
+
|
|
229
|
+
### After the pull request opens
|
|
230
|
+
|
|
231
|
+
| Skill | When to use |
|
|
232
|
+
| ---------------------------- | -------------------------------------------------------------------- |
|
|
233
|
+
| `aitk:claude-pr-review` | From an independent session, to post findings on the PR itself |
|
|
234
|
+
| `aitk:claude-address-review` | On the worker's side, to fix posted findings and push a follow-up |
|
|
235
|
+
| `aitk:git-followup` | For a small self-review edit on a branch whose PR is already open |
|
|
236
|
+
| `aitk:git-split` | When a branch turns out to carry unrelated commits |
|
|
237
|
+
| `aitk:git-issue` | When something surfaced that belongs on the tracker rather than here |
|
|
238
|
+
| `aitk:git-worktree` | After a PR merges, to list worktrees and reclaim the slot |
|
|
239
|
+
|
|
240
|
+
### Run several tracks at once
|
|
241
|
+
|
|
242
|
+
| Skill | When to use |
|
|
243
|
+
| ------------------------- | ------------------------------------------------------------------------------ |
|
|
244
|
+
| `aitk:claude-orchestrate` | To assert the control session that owns the queue and reviews each worker's PR |
|
|
245
|
+
| `aitk:session-resume` | At the start of a session, to pick up what a previous one left |
|
|
246
|
+
|
|
247
|
+
### Keep the project current with the toolkit
|
|
248
|
+
|
|
249
|
+
| Skill | When to use |
|
|
250
|
+
| --------------------------- | ---------------------------------------------------------------------------------- |
|
|
251
|
+
| `aitk:claude-seed-sync` | After a toolkit update, to reconcile installed seeds without losing customizations |
|
|
252
|
+
| `aitk:migration-claude-md` | When `CLAUDE.md` grew past what always-load context should carry |
|
|
253
|
+
| `aitk:migration-context` | When `docs/` holds agent-flavored files belonging in `.claude/context/` |
|
|
254
|
+
| `aitk:migration-standards` | When standards sit at the root while rules point into `.claude/` |
|
|
255
|
+
| `aitk:migration-superseded` | When a drift report names a `.claude/` file a folder has replaced |
|
|
256
|
+
| `aitk:toolkit-feedback` | When something in the toolkit is broken, missing, or off |
|
|
257
|
+
| `aitk:toolkit-triage` | In the toolkit repo, to work through the open feedback issues |
|
|
258
|
+
|
|
259
|
+
### Generate an artifact on demand
|
|
260
|
+
|
|
261
|
+
| Skill | When to use |
|
|
262
|
+
| -------------------------- | --------------------------------------------------------------------- |
|
|
263
|
+
| `aitk:create-rule` | For a project-specific governance rule the toolkit does not ship |
|
|
264
|
+
| `aitk:create-skill` | For a new `SKILL.md` |
|
|
265
|
+
| `aitk:create-snippet` | For a reusable prompt |
|
|
266
|
+
| `aitk:create-standard` | For a new authoring convention |
|
|
267
|
+
| `aitk:bash-script` | For an interactive, human-facing shell tool |
|
|
268
|
+
| `aitk:cli-script` | For a non-interactive automation, CI, or pipeline script |
|
|
269
|
+
| `aitk:ci-workflow` | For a GitHub Actions workflow file |
|
|
270
|
+
| `aitk:claude-slides-draft` | For a deck, drafted as `.claude/SLIDES.md` and rendered to PowerPoint |
|
|
271
|
+
| `aitk:claude-screencast` | For a recording script with beats and defaults already seeded |
|
|
272
|
+
|
|
273
|
+
### Answer a question at any point
|
|
274
|
+
|
|
275
|
+
| Skill | When to use |
|
|
276
|
+
| -------------------------- | ------------------------------------------------------------------------------ |
|
|
277
|
+
| `aitk:toolkit-cli` | Before a sync or install, to learn what it overwrites, merges, or leaves alone |
|
|
278
|
+
| `aitk:youtube-transcripts` | When a video transcript is wanted in the repo as context |
|
|
279
|
+
|
|
280
|
+
Both rows answer a question rather than mark a point in a project's life, so a phase above would send a reader to the wrong group.
|
|
188
281
|
|
|
189
282
|
## Feedback routing
|
|
190
283
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Require a route capture against a running preview after a page or route surface changes
|
|
3
|
+
paths:
|
|
4
|
+
- '**/routes/**/*.{tsx,jsx,vue,svelte,astro}'
|
|
5
|
+
- '**/pages/**/*.{tsx,jsx,vue,svelte,astro}'
|
|
6
|
+
- '**/app/**/page.{tsx,jsx}'
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Surface capture standards
|
|
10
|
+
|
|
11
|
+
## When to capture
|
|
12
|
+
|
|
13
|
+
- Run `bun run screenshot` after changing what a route renders.
|
|
14
|
+
- Capture against a running preview server. Do not capture against a dev server.
|
|
15
|
+
- Capture every theme the route ships. Do not capture the default theme alone.
|
|
16
|
+
|
|
17
|
+
## What a capture covers
|
|
18
|
+
|
|
19
|
+
- Capture the full page at the viewport its case declares. Do not capture a component in isolation.
|
|
20
|
+
- Add a case to the capture record when adding a route.
|
|
21
|
+
- Remove a route's case in the change that removes the route.
|
|
22
|
+
|
|
23
|
+
## Sharing a capture
|
|
24
|
+
|
|
25
|
+
- Attach a capture to the pull request by hand when a reviewer needs to see it.
|
|
26
|
+
- Do not commit a capture. Do not remove the capture folder from `.gitignore`.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
extends = "node"
|
|
2
|
-
rules = ["210-astro", "350-security-web", "400-ui", "410-a11y", "430-ux-completeness"]
|
|
2
|
+
rules = ["210-astro", "350-security-web", "400-ui", "410-a11y", "430-ux-completeness", "440-surface-capture"]
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
extends = "node"
|
|
2
|
-
rules = ["200-react", "230-nextjs", "250-tailwind", "300-testing-ts", "310-zod", "350-security-web", "400-ui", "410-a11y", "420-forms", "430-ux-completeness"]
|
|
2
|
+
rules = ["200-react", "230-nextjs", "250-tailwind", "300-testing-ts", "310-zod", "350-security-web", "400-ui", "410-a11y", "420-forms", "430-ux-completeness", "440-surface-capture"]
|
package/package.json
CHANGED
package/src/commands/records.ts
CHANGED
|
@@ -9,7 +9,9 @@ import {
|
|
|
9
9
|
import {
|
|
10
10
|
type Finding,
|
|
11
11
|
isRecordKind,
|
|
12
|
+
isSharedScratch,
|
|
12
13
|
RECORD_KINDS,
|
|
14
|
+
type RecordKind,
|
|
13
15
|
type ValidateOutcome,
|
|
14
16
|
validateRecords,
|
|
15
17
|
} from '@/records/validate'
|
|
@@ -22,7 +24,7 @@ import {
|
|
|
22
24
|
outro,
|
|
23
25
|
pipeOutput,
|
|
24
26
|
} from '@/ui'
|
|
25
|
-
import { mainWorktreeRoot } from '@/worktree'
|
|
27
|
+
import { currentWorktreeRoot, mainWorktreeRoot } from '@/worktree'
|
|
26
28
|
|
|
27
29
|
/** Returned when a record carries a finding, which is the gating result. */
|
|
28
30
|
const EXIT_FINDINGS = 2
|
|
@@ -38,17 +40,20 @@ export function register(program: Command): void {
|
|
|
38
40
|
const records = program
|
|
39
41
|
.command('records')
|
|
40
42
|
.description(
|
|
41
|
-
'Check and back up the
|
|
43
|
+
'Check a governed corpus against its standard, and back up the session records under .claude/',
|
|
42
44
|
)
|
|
43
45
|
.helpOption('-h, --help', 'Show this help message')
|
|
44
46
|
|
|
45
47
|
records
|
|
46
48
|
.command('validate')
|
|
47
|
-
.description('Report where a
|
|
49
|
+
.description('Report where a file and the standard governing it disagree')
|
|
48
50
|
.argument('<kind>', `Record folder: ${RECORD_KINDS.join(', ')}`)
|
|
49
51
|
.helpOption('-h, --help', 'Show this help message')
|
|
50
52
|
.option('--json', 'Add a machine-readable record on stdout')
|
|
51
|
-
.option(
|
|
53
|
+
.option(
|
|
54
|
+
'--root <path>',
|
|
55
|
+
'Project root, defaulting to the main worktree except on standards',
|
|
56
|
+
)
|
|
52
57
|
.addHelpText(
|
|
53
58
|
'after',
|
|
54
59
|
[
|
|
@@ -58,18 +63,22 @@ export function register(program: Command): void {
|
|
|
58
63
|
' groundwork README and current-state files, numbering, dating, and a half-closed track',
|
|
59
64
|
' intake overview file, numbering, dating, and the four bullets every item carries',
|
|
60
65
|
' memory filename and type prefix, frontmatter, and the body shape each type carries',
|
|
66
|
+
' standards frontmatter, the scope section and its handoff list, and a filename',
|
|
67
|
+
' derived from the governed path',
|
|
61
68
|
'',
|
|
62
69
|
'Exit codes:',
|
|
63
70
|
' 0 every check passed',
|
|
64
71
|
' 1 refused, with the reason on stderr or in the JSON record',
|
|
65
72
|
' 2 at least one record carries a finding',
|
|
66
73
|
'',
|
|
67
|
-
'It reports and never writes
|
|
68
|
-
'history
|
|
74
|
+
'It reports and never writes, so a session fixes what the report names. A',
|
|
75
|
+
'session record has no history to undo a wrong repair from, and a standard',
|
|
76
|
+
'is installed and cited everywhere, so a rename is larger than a file move.',
|
|
69
77
|
'',
|
|
70
78
|
'Examples:',
|
|
71
79
|
' aitk records validate plans',
|
|
72
80
|
' aitk records validate memory',
|
|
81
|
+
' aitk records validate standards',
|
|
73
82
|
' aitk records validate intake --json',
|
|
74
83
|
'',
|
|
75
84
|
].join('\n'),
|
|
@@ -218,6 +227,15 @@ function reportRefusal(
|
|
|
218
227
|
return 1
|
|
219
228
|
}
|
|
220
229
|
|
|
230
|
+
/**
|
|
231
|
+
* A shared-scratch kind reads the main worktree root, so a linked worktree
|
|
232
|
+
* validates the records every other session reads. A tracked corpus reads the
|
|
233
|
+
* checkout the caller stands in, which is the copy that session has edited.
|
|
234
|
+
*/
|
|
235
|
+
function defaultRoot(kind: RecordKind): Promise<string> {
|
|
236
|
+
return isSharedScratch(kind) ? mainWorktreeRoot() : currentWorktreeRoot()
|
|
237
|
+
}
|
|
238
|
+
|
|
221
239
|
async function runValidate(
|
|
222
240
|
kind: string,
|
|
223
241
|
opts: ValidateCommandOptions,
|
|
@@ -236,7 +254,7 @@ async function runValidate(
|
|
|
236
254
|
)
|
|
237
255
|
}
|
|
238
256
|
|
|
239
|
-
const root = opts.root ?? (await
|
|
257
|
+
const root = opts.root ?? (await defaultRoot(kind))
|
|
240
258
|
|
|
241
259
|
return report(await validateRecords(root, kind), emitJson, root)
|
|
242
260
|
}
|
package/src/records/backup.ts
CHANGED
|
@@ -10,8 +10,9 @@ import { gitEnv } from '@/git-env'
|
|
|
10
10
|
* enclosing repository already. The list is spelled out rather than read off
|
|
11
11
|
* that group so adding an ignore entry cannot silently enlarge the payload.
|
|
12
12
|
*
|
|
13
|
-
* `RECORD_KINDS` in `validate.ts`
|
|
14
|
-
*
|
|
13
|
+
* `RECORD_KINDS` in `validate.ts` overlaps this on four names and carries one
|
|
14
|
+
* more that no backup reaches. The two lists differ on purpose: one is what a
|
|
15
|
+
* standard governs, this is what a disk loss would take.
|
|
15
16
|
*/
|
|
16
17
|
export const BACKED_FOLDERS = [
|
|
17
18
|
'groundwork',
|
package/src/records/validate.ts
CHANGED
|
@@ -4,15 +4,31 @@ import { join } from 'node:path'
|
|
|
4
4
|
import { parseFrontmatter, readField } from '@/indexes/frontmatter'
|
|
5
5
|
import { linesOutsideFences } from '@/markdown/scan'
|
|
6
6
|
|
|
7
|
-
export const RECORD_KINDS = [
|
|
7
|
+
export const RECORD_KINDS = [
|
|
8
|
+
'plans',
|
|
9
|
+
'groundwork',
|
|
10
|
+
'intake',
|
|
11
|
+
'memory',
|
|
12
|
+
'standards',
|
|
13
|
+
] as const
|
|
8
14
|
|
|
9
15
|
export type RecordKind = (typeof RECORD_KINDS)[number]
|
|
10
16
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
/**
|
|
18
|
+
* The folders each kind reads, in precedence order.
|
|
19
|
+
*
|
|
20
|
+
* Standards carry two because the corpus authors at the project root and
|
|
21
|
+
* installs under `.claude/`. The authoring root wins where both exist, since the
|
|
22
|
+
* installed tree is a generated copy here and a finding fixed there is
|
|
23
|
+
* overwritten by the next regen. A project that consumed the corpus holds only
|
|
24
|
+
* the second, so one order serves both.
|
|
25
|
+
*/
|
|
26
|
+
const FOLDERS_BY_KIND: Readonly<Record<RecordKind, readonly string[]>> = {
|
|
27
|
+
plans: [join('.claude', 'plans')],
|
|
28
|
+
groundwork: [join('.claude', 'groundwork')],
|
|
29
|
+
intake: [join('.claude', 'intake')],
|
|
30
|
+
memory: [join('.claude', 'memory')],
|
|
31
|
+
standards: ['standards', join('.claude', 'standards')],
|
|
16
32
|
}
|
|
17
33
|
|
|
18
34
|
/**
|
|
@@ -30,6 +46,7 @@ export const FINDING_KINDS = [
|
|
|
30
46
|
'title-missing',
|
|
31
47
|
'title-is-slug',
|
|
32
48
|
'section-missing',
|
|
49
|
+
'scope-unanchored',
|
|
33
50
|
'entry-unreasoned',
|
|
34
51
|
'suggestion-missing',
|
|
35
52
|
'question-unanswerable',
|
|
@@ -67,14 +84,37 @@ export interface ValidateRefused {
|
|
|
67
84
|
|
|
68
85
|
export type ValidateOutcome = ValidateReport | ValidateRefused
|
|
69
86
|
|
|
87
|
+
/** Every folder a kind would accept, whether or not it is on disk. */
|
|
88
|
+
export function recordDirs(root: string, kind: RecordKind): string[] {
|
|
89
|
+
return FOLDERS_BY_KIND[kind].map((folder) => join(root, folder))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The folder a kind reads. The first candidate on disk wins, and the first
|
|
94
|
+
* candidate stands in when none exists, so a refusal and a test fixture both
|
|
95
|
+
* name the location the kind prefers.
|
|
96
|
+
*/
|
|
70
97
|
export function recordsDir(root: string, kind: RecordKind): string {
|
|
71
|
-
|
|
98
|
+
const dirs = recordDirs(root, kind)
|
|
99
|
+
return dirs.find((dir) => existsSync(dir)) ?? dirs[0]
|
|
72
100
|
}
|
|
73
101
|
|
|
74
102
|
export function isRecordKind(value: string): value is RecordKind {
|
|
75
103
|
return (RECORD_KINDS as readonly string[]).includes(value)
|
|
76
104
|
}
|
|
77
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Whether a kind's folder is shared session scratch at the main worktree root.
|
|
108
|
+
*
|
|
109
|
+
* The four record folders are, so every session validates the records every
|
|
110
|
+
* other session reads. The corpus is tracked instead, so a linked worktree holds
|
|
111
|
+
* its own edited copy, and defaulting that kind to the main root would report on
|
|
112
|
+
* a tree the session never touched and say nothing about which one it read.
|
|
113
|
+
*/
|
|
114
|
+
export function isSharedScratch(kind: RecordKind): boolean {
|
|
115
|
+
return kind !== 'standards'
|
|
116
|
+
}
|
|
117
|
+
|
|
78
118
|
const NONE_IDENTIFIED = 'None identified.'
|
|
79
119
|
const NUMBERED_FILE = /^\d{2}-[a-z0-9]+(-[a-z0-9]+)*\.md$/
|
|
80
120
|
|
|
@@ -673,6 +713,165 @@ function checkMemoryBody(
|
|
|
673
713
|
return findings
|
|
674
714
|
}
|
|
675
715
|
|
|
716
|
+
const STANDARD_INDEX = 'index.md'
|
|
717
|
+
const STANDARD_FIELDS = ['title', 'description'] as const
|
|
718
|
+
|
|
719
|
+
const SCOPE_HEADING = /^##[ \t]+Scope[ \t]*$/
|
|
720
|
+
const ANY_HEADING = /^#{1,6}[ \t]+\S/
|
|
721
|
+
const DOES_NOT_GOVERN = 'Does not govern:'
|
|
722
|
+
const ATTRIBUTE_MARKER = 'attribute standard'
|
|
723
|
+
const CODE_SPAN = /`([^`]+)`/g
|
|
724
|
+
|
|
725
|
+
interface Scope {
|
|
726
|
+
/** The first non-blank line under the heading, which is the statement. */
|
|
727
|
+
readonly statement: string
|
|
728
|
+
readonly lines: readonly string[]
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
export function readScope(text: string): Scope | undefined {
|
|
732
|
+
const lines = linesOutsideFences(text)
|
|
733
|
+
const opened = lines.findIndex((line) => SCOPE_HEADING.test(line.trim()))
|
|
734
|
+
if (opened === -1) return undefined
|
|
735
|
+
|
|
736
|
+
const body: string[] = []
|
|
737
|
+
|
|
738
|
+
for (const line of lines.slice(opened + 1)) {
|
|
739
|
+
if (ANY_HEADING.test(line.trim())) break
|
|
740
|
+
body.push(line)
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const statement = body.find((line) => line.trim().length > 0)
|
|
744
|
+
|
|
745
|
+
return { statement: statement?.trim() ?? '', lines: body }
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* The paths a scope statement declares, read the way `scripts/standards/list.sh`
|
|
750
|
+
* reads them for the catalog's `appliesTo` field: backticked spans in the first
|
|
751
|
+
* sentence alone. One sentence read two ways would let a standard pass here
|
|
752
|
+
* while publishing a different jurisdiction to every consumer of the catalog.
|
|
753
|
+
*/
|
|
754
|
+
export function governedPaths(statement: string): string[] {
|
|
755
|
+
const [sentence] = statement.split('. ')
|
|
756
|
+
return [...sentence.matchAll(CODE_SPAN)].map((match) => match[1])
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* The words a governed path offers a filename. Each segment gives its own word
|
|
761
|
+
* and, where it carries a prefix or a placeholder, the parts either side of a
|
|
762
|
+
* hyphen, so `.claude/tasks/session-<slug>.md` offers `tasks` and `session`.
|
|
763
|
+
*
|
|
764
|
+
* A dotted segment gives nothing. It names the folder holding the artifact
|
|
765
|
+
* rather than the artifact, and a standard named for it would pass this check
|
|
766
|
+
* while naming the container every sibling shares.
|
|
767
|
+
*/
|
|
768
|
+
export function pathWords(path: string): string[] {
|
|
769
|
+
const words: string[] = []
|
|
770
|
+
|
|
771
|
+
for (const segment of path.split('/')) {
|
|
772
|
+
if (segment.startsWith('.')) continue
|
|
773
|
+
|
|
774
|
+
const stem = segment.replace(/\.[a-z]+$/i, '').toLowerCase()
|
|
775
|
+
words.push(stem)
|
|
776
|
+
if (stem.includes('-')) words.push(...stem.split('-'))
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
return words.filter((word) => /^[a-z]+$/.test(word))
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Accepts the singular and the plural of one word. A standard over a single
|
|
784
|
+
* document is named for the document and one over a folder of them is named for
|
|
785
|
+
* either, and picking a side would report a conforming half of the corpus.
|
|
786
|
+
*/
|
|
787
|
+
function namesWord(stem: string, word: string): boolean {
|
|
788
|
+
return stem === word || `${stem}s` === word || stem === `${word}s`
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function checkStandardName(name: string, statement: string): Finding[] {
|
|
792
|
+
const paths = governedPaths(statement)
|
|
793
|
+
|
|
794
|
+
// The marker is read only where the first sentence backticks nothing, which
|
|
795
|
+
// is the catalog's own rule. A statement naming a path publishes that path
|
|
796
|
+
// however the rest of the statement describes itself.
|
|
797
|
+
if (paths.length === 0) {
|
|
798
|
+
if (statement.includes(ATTRIBUTE_MARKER)) return []
|
|
799
|
+
|
|
800
|
+
return [
|
|
801
|
+
finding(
|
|
802
|
+
'scope-unanchored',
|
|
803
|
+
name,
|
|
804
|
+
name,
|
|
805
|
+
'backticks no path in its first scope sentence and does not call itself an attribute standard, so it names no artifact to be named for.',
|
|
806
|
+
),
|
|
807
|
+
]
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const stem = name.replace(/\.md$/, '')
|
|
811
|
+
const words = paths.flatMap(pathWords)
|
|
812
|
+
|
|
813
|
+
if (words.some((word) => namesWord(stem, word))) return []
|
|
814
|
+
|
|
815
|
+
return [
|
|
816
|
+
finding(
|
|
817
|
+
'name-malformed',
|
|
818
|
+
name,
|
|
819
|
+
name,
|
|
820
|
+
`names no part of ${paths.join(', ')}, which is what it governs. A rename reaches every target that installed the corpus and every surface citing it by bare filename.`,
|
|
821
|
+
),
|
|
822
|
+
]
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
export function checkStandard(name: string, text: string): Finding[] {
|
|
826
|
+
const findings: Finding[] = []
|
|
827
|
+
const frontmatter = parseFrontmatter(text)
|
|
828
|
+
|
|
829
|
+
const missing = STANDARD_FIELDS.filter(
|
|
830
|
+
(field) => !readField(frontmatter, field),
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
if (missing.length > 0) {
|
|
834
|
+
findings.push(
|
|
835
|
+
finding(
|
|
836
|
+
'frontmatter-incomplete',
|
|
837
|
+
name,
|
|
838
|
+
name,
|
|
839
|
+
`carries no ${missing.join(' and no ')}.`,
|
|
840
|
+
),
|
|
841
|
+
)
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const scope = readScope(text)
|
|
845
|
+
|
|
846
|
+
// The name derives from the scope statement, so an absent section leaves
|
|
847
|
+
// nothing to derive against. Reporting the name as well would name one defect
|
|
848
|
+
// twice and point the fix at the wrong file.
|
|
849
|
+
if (!scope) {
|
|
850
|
+
return [
|
|
851
|
+
...findings,
|
|
852
|
+
finding(
|
|
853
|
+
'section-missing',
|
|
854
|
+
name,
|
|
855
|
+
'## Scope',
|
|
856
|
+
'is absent, so the standard claims no jurisdiction and can refuse no rule.',
|
|
857
|
+
),
|
|
858
|
+
]
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if (!scope.lines.some((line) => line.trim().startsWith(DOES_NOT_GOVERN))) {
|
|
862
|
+
findings.push(
|
|
863
|
+
finding(
|
|
864
|
+
'section-missing',
|
|
865
|
+
name,
|
|
866
|
+
DOES_NOT_GOVERN,
|
|
867
|
+
'is absent from the scope section, so no boundary names the owner it hands off to.',
|
|
868
|
+
),
|
|
869
|
+
)
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
return [...findings, ...checkStandardName(name, scope.statement)]
|
|
873
|
+
}
|
|
874
|
+
|
|
676
875
|
function refuse(reason: ValidateRefusal, message: string): ValidateRefused {
|
|
677
876
|
return { ok: false, reason, message }
|
|
678
877
|
}
|
|
@@ -696,9 +895,12 @@ async function validateFiles(
|
|
|
696
895
|
}
|
|
697
896
|
|
|
698
897
|
/**
|
|
699
|
-
* Reports what every record in one
|
|
700
|
-
*
|
|
701
|
-
* no history behind it, so a repair
|
|
898
|
+
* Reports what every record in one folder claims against the shape its standard
|
|
899
|
+
* fixes. It writes nothing whichever kind runs, and the reason differs by kind.
|
|
900
|
+
* A session record is per-machine scratch with no history behind it, so a repair
|
|
901
|
+
* that guessed wrong could not be undone. A standard installs into every target
|
|
902
|
+
* and is cited by bare filename, so a rename costs more than the file move it
|
|
903
|
+
* looks like.
|
|
702
904
|
*/
|
|
703
905
|
export async function validateRecords(
|
|
704
906
|
root: string,
|
|
@@ -707,7 +909,10 @@ export async function validateRecords(
|
|
|
707
909
|
const dir = recordsDir(root, kind)
|
|
708
910
|
|
|
709
911
|
if (!existsSync(dir)) {
|
|
710
|
-
return refuse(
|
|
912
|
+
return refuse(
|
|
913
|
+
'no-folder',
|
|
914
|
+
`No ${kind} folder at ${recordDirs(root, kind).join(' or ')}.`,
|
|
915
|
+
)
|
|
711
916
|
}
|
|
712
917
|
|
|
713
918
|
if (kind === 'plans') return validateFiles(dir, kind, checkPlan)
|
|
@@ -721,6 +926,18 @@ export async function validateRecords(
|
|
|
721
926
|
)
|
|
722
927
|
}
|
|
723
928
|
|
|
929
|
+
// The walk stays flat, matching install and the catalog. `standards/bundled/`
|
|
930
|
+
// is a subfolder whose members are named for the skill that reads them rather
|
|
931
|
+
// than for a path they govern, so the derivation below reports every one.
|
|
932
|
+
if (kind === 'standards') {
|
|
933
|
+
return validateFiles(
|
|
934
|
+
dir,
|
|
935
|
+
kind,
|
|
936
|
+
checkStandard,
|
|
937
|
+
(file) => file === STANDARD_INDEX,
|
|
938
|
+
)
|
|
939
|
+
}
|
|
940
|
+
|
|
724
941
|
const folders = await listFolders(dir)
|
|
725
942
|
const check = kind === 'groundwork' ? checkTrack : checkDump
|
|
726
943
|
const perFolder = await Promise.all(
|
package/src/worktree.ts
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import { $ } from 'bun'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Resolves the root of the checkout the caller is standing in, which is the
|
|
5
|
+
* linked worktree rather than the main one when a session is inside one.
|
|
6
|
+
*
|
|
7
|
+
* A tracked tree differs per worktree, so a verb reading one answers about the
|
|
8
|
+
* files the session has edited only if it resolves the root this way. The
|
|
9
|
+
* working directory is not a substitute, since a caller invoking from a
|
|
10
|
+
* subdirectory would resolve a root holding none of the trees a verb reads.
|
|
11
|
+
*/
|
|
12
|
+
export async function currentWorktreeRoot(): Promise<string> {
|
|
13
|
+
const result = await $`git rev-parse --show-toplevel`.quiet().nothrow()
|
|
14
|
+
if (result.exitCode !== 0) return process.cwd()
|
|
15
|
+
|
|
16
|
+
return result.stdout.toString().trim() || process.cwd()
|
|
17
|
+
}
|
|
18
|
+
|
|
3
19
|
/**
|
|
4
20
|
* Resolves the root every shared-scratch folder lives under. `git worktree
|
|
5
21
|
* list` puts the main worktree first, and trusting the working directory
|
package/standards/standard.md
CHANGED
|
@@ -46,6 +46,14 @@ A standard failing these questions is non-conforming even when it satisfies ever
|
|
|
46
46
|
- Separate a jurisdiction exclusion from a content exclusion. The first names a concern another standard owns and belongs in `## Scope`. The second names what does not belong inside the document and stays with the shape rules. Merging them puts a boundary claim where no sibling will read it.
|
|
47
47
|
- Stay silent on a section the standard holds today but should not own. Claiming it makes the scope statement false the moment it moves, and the mismatch is the evidence that moves it.
|
|
48
48
|
|
|
49
|
+
### Naming the file
|
|
50
|
+
|
|
51
|
+
- Name the file for the artifact the scope statement governs, taking the word from the deepest path segment that names the artifact rather than the folder holding it. `plan.md` over `.claude/plans/feature-<slug>.md` and `session.md` over `.claude/tasks/session-<slug>.md` both follow it.
|
|
52
|
+
- Take the singular or the plural of that word, whichever reads as the artifact. A standard over one document takes the singular, and one over a set may take either.
|
|
53
|
+
- Name an attribute standard for the attribute itself, since it governs no path to derive a word from.
|
|
54
|
+
- Write the derived word alone. A standard installs as a sibling and is cited by bare filename, so a qualifier makes an outlier that every later citation has to carry.
|
|
55
|
+
- Rename an outlier at the moment it is found, and state the reach in the change that does it. The name is installed in every target and cited in surfaces that never see the rename, so the cost only grows.
|
|
56
|
+
|
|
49
57
|
### Staying inside it
|
|
50
58
|
|
|
51
59
|
- Name no path, filename, or folder outside the document type the standard governs. A standard reaches projects whose layout is their own, so a path borrowed from the authoring repository is wrong in a target and nothing reports it.
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
The base layer covers every project the toolkit scaffolds, whatever language sits on top. It ships formatting, spelling, shell linting, conventional commits, git hooks, CI, and three maintenance scripts. Every other stack extends it, so a decision made here is one every stack inherits.
|
|
6
6
|
|
|
7
|
+
A repository with several language roots layers this stack once at the root and skips it per subtree, since git honors only one `core.hooksPath` and a re-dropped husky silently breaks. `docs/target-projects.md` carries the recipe.
|
|
8
|
+
|
|
7
9
|
## What ships as golden configs
|
|
8
10
|
|
|
9
11
|
Golden config files live in `tooling/base/configs/` and are copied into the target on `aitk tooling sync base .`. They are the source of truth. The reference covers rationale and tradeoffs. Configs show the concrete setup.
|
|
@@ -1,41 +1,53 @@
|
|
|
1
1
|
import type { Page } from '@playwright/test'
|
|
2
|
-
|
|
3
|
-
const ROUTES = [{ name: 'home', path: '/', width: 1280, height: 800 }]
|
|
4
|
-
|
|
5
|
-
type State = { name: string; setup?: (page: Page) => Promise<void> }
|
|
6
|
-
const STATES: State[] = [
|
|
7
|
-
{ name: 'default' },
|
|
8
|
-
{ name: 'dark', setup: async (p) => p.emulateMedia({ colorScheme: 'dark' }) },
|
|
9
|
-
]
|
|
10
|
-
|
|
11
2
|
import { chromium } from '@playwright/test'
|
|
12
3
|
import { mkdir } from 'fs/promises'
|
|
13
4
|
import path from 'path'
|
|
14
5
|
|
|
6
|
+
interface CaptureCase {
|
|
7
|
+
section: string
|
|
8
|
+
theme: string
|
|
9
|
+
route: string
|
|
10
|
+
width: number
|
|
11
|
+
height: number
|
|
12
|
+
setup?: (page: Page) => Promise<void>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const CASES: CaptureCase[] = [
|
|
16
|
+
{ section: 'home', theme: 'default', route: '/', width: 1280, height: 800 },
|
|
17
|
+
{
|
|
18
|
+
section: 'home',
|
|
19
|
+
theme: 'dark',
|
|
20
|
+
route: '/',
|
|
21
|
+
width: 1280,
|
|
22
|
+
height: 800,
|
|
23
|
+
setup: (page) => page.emulateMedia({ colorScheme: 'dark' }),
|
|
24
|
+
},
|
|
25
|
+
]
|
|
26
|
+
|
|
15
27
|
const BASE_URL = process.env.SCREENSHOT_BASE_URL ?? 'http://localhost:4173'
|
|
16
28
|
const OUT_DIR = 'screenshots'
|
|
17
29
|
|
|
18
30
|
const browser = await chromium.launch()
|
|
19
|
-
await mkdir(OUT_DIR, { recursive: true })
|
|
20
31
|
|
|
21
|
-
for (const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
for (const captureCase of CASES) {
|
|
33
|
+
const context = await browser.newContext({
|
|
34
|
+
viewport: { width: captureCase.width, height: captureCase.height },
|
|
35
|
+
})
|
|
36
|
+
const page = await context.newPage()
|
|
37
|
+
|
|
38
|
+
if (captureCase.setup) await captureCase.setup(page)
|
|
27
39
|
|
|
28
|
-
|
|
40
|
+
await page.goto(`${BASE_URL}${captureCase.route}`)
|
|
41
|
+
await page.waitForLoadState('networkidle')
|
|
29
42
|
|
|
30
|
-
|
|
31
|
-
|
|
43
|
+
const sectionDir = path.join(OUT_DIR, captureCase.section)
|
|
44
|
+
await mkdir(sectionDir, { recursive: true })
|
|
32
45
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
const file = path.join(sectionDir, `${captureCase.theme}.png`)
|
|
47
|
+
await page.screenshot({ path: file, fullPage: true })
|
|
48
|
+
console.log(`captured ${file}`)
|
|
36
49
|
|
|
37
|
-
|
|
38
|
-
}
|
|
50
|
+
await context.close()
|
|
39
51
|
}
|
|
40
52
|
|
|
41
53
|
await browser.close()
|
package/tooling/web/reference.md
CHANGED
|
@@ -12,7 +12,7 @@ Golden config files live in `tooling/web/configs/` and are copied into the targe
|
|
|
12
12
|
|
|
13
13
|
- `eslint.config.js`: flat config with `@eslint/js`, `typescript-eslint`, React hooks, import sort, check-file, vitest rules scoped to test files, `eslint-config-prettier` last.
|
|
14
14
|
- `src/test/setup.ts`: `@testing-library/jest-dom` import, `cleanup` after each test.
|
|
15
|
-
- `e2e/screenshot.ts`: capture template.
|
|
15
|
+
- `e2e/screenshot.ts`: capture template. A single `CASES` record at the top carries one entry per output file, each naming a section, a theme, a route, and its own viewport, and the loop below writes `screenshots/<section>/<theme>.png`. Per-project cases extend the one record. A route's themes sit together under its section folder, so the filename carries the theme alone.
|
|
16
16
|
- `.vscode/extensions.json` and `.vscode/settings.json`: editor wiring for ESLint, Tailwind, Playwright, Vitest.
|
|
17
17
|
- `.github/workflows/verify.yml`: `static-checks`, `unit-tests`, `build-verify`, and `e2e-tests` jobs.
|
|
18
18
|
- `scripts/verify.sh`: extends base verify with typecheck, lint, unit tests, and build in the full order.
|
|
@@ -102,7 +102,9 @@ Append rows:
|
|
|
102
102
|
| `bun run test:e2e` | Run Playwright E2E tests. |
|
|
103
103
|
| `bun run screenshot` | Build, preview, then capture screenshots. |
|
|
104
104
|
|
|
105
|
-
`aitk tooling verify <stack>` is the only automated caller of `bun run screenshot`, running it for any stack whose `package.json` declares the script and asserting that PNG files land under `screenshots/`. No ship chain captures a screenshot, so the output path the seed writes is a contract that one verifier reads rather than a default a ship step depends on.
|
|
105
|
+
`aitk tooling verify <stack>` is the only automated caller of `bun run screenshot`, running it for any stack whose `package.json` declares the script and asserting that PNG files land under `screenshots/`. It counts them with a recursive find carrying no depth limit, so the section folders the seed writes satisfy the assertion without a change to it. Do not flatten the layout to protect that check. No ship chain captures a screenshot, so the output path the seed writes is a contract that one verifier reads rather than a default a ship step depends on.
|
|
106
|
+
|
|
107
|
+
`governance/rules/ui/440-surface-capture.md` is what asks a session to run the capture after a route changes. It fires on route and page files rather than on every component, so a shared component changing every screen fires nothing and the operator runs the capture by hand.
|
|
106
108
|
|
|
107
109
|
## Verify script
|
|
108
110
|
|