@erclx/aitk 0.84.0 → 0.86.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/markdown-audit.md +10 -2
- package/docs/target-projects.md +1 -1
- package/governance/rules/claude/520-wireframes.md +9 -1
- package/governance/rules/claude/530-requirements.md +8 -0
- package/governance/rules/claude/540-architecture.md +10 -0
- package/governance/rules/claude/550-design.md +8 -0
- package/governance/rules/claude/555-tasks.md +8 -0
- package/governance/rules/claude/580-readme.md +8 -0
- package/governance/rules/claude/590-rule-authoring.md +11 -0
- package/governance/rules/claude/591-standard-authoring.md +8 -0
- package/package.json +1 -1
- package/src/cli.ts +2 -2
- package/src/commands/claude.ts +2 -1
- package/src/commands/docs.ts +2 -1
- package/src/commands/feedback.ts +1 -1
- package/src/commands/gov.ts +1 -1
- package/src/commands/init.ts +1 -1
- package/src/commands/markdown.ts +5 -3
- package/src/commands/sandbox.ts +2 -1
- package/src/commands/snippets.ts +1 -1
- package/src/commands/standards.ts +59 -2
- package/src/commands/sync.ts +1 -1
- package/src/commands/tooling.ts +2 -1
- package/src/commands/wiki.ts +1 -1
- package/src/exec.ts +2 -3
- package/src/github.ts +1 -1
- package/src/markdown/bans.ts +22 -30
- package/src/project-root.ts +19 -0
- package/src/standards/read.ts +97 -0
package/docs/agents/commands.md
CHANGED
|
@@ -47,7 +47,7 @@ Each domain exposes a consistent shape where applicable: `list`, `install`, `syn
|
|
|
47
47
|
| ----------- | --------------------------------------------------------------------------- |
|
|
48
48
|
| `tooling` | `list`, `sync`, `ref`, `create`, `verify`, `inject`, `prune-gitignore` |
|
|
49
49
|
| `snippets` | `list`, `install`, `sync`, `create` |
|
|
50
|
-
| `standards` | `list`, `install`, `sync
|
|
50
|
+
| `standards` | `list`, `install`, `sync`, `<name>` |
|
|
51
51
|
| `gov` | `list`, `install`, `sync`, `build` |
|
|
52
52
|
| `claude` | `init`, `sync`, `seeds list`, `skills list`, `skills audit`, `setup [dest]` |
|
|
53
53
|
| `wiki` | `init` |
|
|
@@ -27,7 +27,13 @@ A bare run measures every markdown file git lists, tracked plus untracked-and-no
|
|
|
27
27
|
|
|
28
28
|
## Where the rules come from
|
|
29
29
|
|
|
30
|
-
Both ban sets and all five checkpoints are read out of the standards per run, resolved under `.claude/standards/` first
|
|
30
|
+
Both ban sets and all five checkpoints are read out of the standards per run, resolved under `.claude/standards/` first, the authoring root second, and the corpus inside the `aitk` package last, so a target project measures against the copy it actually has and a project that installed none is measured all the same. Holding the lists in code was the alternative and it puts each ban in two places, where an author adding one gets no enforcement until someone edits TypeScript.
|
|
31
|
+
|
|
32
|
+
A project copy wins wherever one exists. An installed standard is a seed a project edits, so a package copy overriding it would discard that edit with nothing said, and the fallback answers absence rather than staleness. The package copy is reachable because `package.json` ships `standards/` beside the code doing the reading, which is what leaves the install optional for every machine consumer.
|
|
33
|
+
|
|
34
|
+
The report names the copy it read on every run, spelling a package copy under `<aitk>/` rather than as a path anything would join to a project root. A published package can lag the toolkit a session reads from, the same two-speed skew `.claude/ARCHITECTURE.md` records between the plugin cache and the CLI, and naming the source is what keeps that diagnosable instead of invisible.
|
|
35
|
+
|
|
36
|
+
`aitk standards <name>` resolves the same three roots in the same order and prints the standard, so the human catalog reads without a project copy on disk too.
|
|
31
37
|
|
|
32
38
|
The trade is a reader of prose that a reformat can break. `src/markdown/bans.test.ts` answers it by asserting the parsed sets against the shipped standards, so a rewrite that narrows a set fails there rather than passing quietly. A checkpoint falls back per number rather than per file, and the depth legend names every checkpoint that fell back on the run that used one.
|
|
33
39
|
|
|
@@ -116,7 +122,9 @@ Four surfaces read the ban sets and two of them go through this verb. `.claude/h
|
|
|
116
122
|
|
|
117
123
|
The other two read the standards directly and neither is a consolidation left half done. `claude/skills/claude-standards-audit/SKILL.md` greps the banned tokens agent-side, which is a session reading prose rather than a process it can shell out to, and it ships to every target. The seed copy of the hook keeps its awk, because a scaffolded project may carry no `aitk` and `scripts/core/check-seed-independence.sh` exists to catch seed content depending on the toolkit CLI. Both are the likelier place for the next drift, since nothing compares either against the verb.
|
|
118
124
|
|
|
119
|
-
The hook prefers a checkout's own `src/cli.ts` over a globally installed binary, so it and the push stage read one build. A published binary lags a branch by whatever has not been released, which would put a ban kind added on the branch into the push and not into the edit. It reads its findings out of the `--json` record rather than off the exit code, so an older binary still reports where the fallback applies. It reads `bans.missingStandards` out of the same record, so a standard the verb found under
|
|
125
|
+
The hook prefers a checkout's own `src/cli.ts` over a globally installed binary, so it and the push stage read one build. A published binary lags a branch by whatever has not been released, which would put a ban kind added on the branch into the push and not into the edit. It reads its findings out of the `--json` record rather than off the exit code, so an older binary still reports where the fallback applies. It reads `bans.missingStandards` out of the same record, so a standard the verb found under none of the three roots reaches the author as a check narrowed to what it could read.
|
|
126
|
+
|
|
127
|
+
The package fallback leaves that message reachable in one case rather than none. A CLI running out of a package carrying the corpus always resolves a standard, so the narrowed-check path fires only for a checkout whose own source tree lacks the file. The message stays right for that case and the hook keeps reading the field, since a reader cannot tell a narrowed check from a clean one without it.
|
|
120
128
|
|
|
121
129
|
A machine with neither runner still blocks no edit, and it says so rather than exiting clean. The push stage holds either way. An edit nobody checked and an edit carrying no violation are one silence to a reader, so the enforcement a machine lacks is reported rather than inferred.
|
|
122
130
|
|
package/docs/target-projects.md
CHANGED
|
@@ -83,7 +83,7 @@ Run `aitk tooling list --json` and `aitk gov list --json` to see the current cat
|
|
|
83
83
|
`governance`, `standards`, and `wiki` are skippable:
|
|
84
84
|
|
|
85
85
|
- `--skip governance`: leave `.claude/rules/` empty. Standards still install, so `.claude/standards/prose.md` lands with nothing pointing at it and no coding standard loads on a file match. The preview names any `--add` rules the skip drops, and the run prints the `aitk gov install <stack> <path>` command to add rules afterward, carrying those extras so one paste restores what the skip declined.
|
|
86
|
-
- `--skip standards`: leave standards out. The governance rules still reference `.claude/standards/`, so their authority lines resolve to nothing. Toolkit skills are unaffected, since each falls back to the copy in its own plugin root.
|
|
86
|
+
- `--skip standards`: leave standards out. The governance rules still reference `.claude/standards/`, so their authority lines resolve to nothing for a reader following the path. Toolkit skills are unaffected, since each falls back to the copy in its own plugin root, and so is the CLI, which searches the corpus inside its own package behind both project roots. `aitk markdown audit` and `aitk standards <name>` therefore answer in a project that skipped the install, and what the skip costs is the editable copy rather than the enforcement.
|
|
87
87
|
- `--skip wiki`: skip the `.claude/wiki/` scaffold. A target that already carries a root `wiki/` keeps it, since the verb reports that folder rather than migrating it.
|
|
88
88
|
|
|
89
89
|
That standards fallback carries runtime behavior rather than reference prose alone, because the pre-publish scan and the branch-slug transform each have a standard of their own, `publish.md` and `slug.md`, cited by the skills that run them.
|
|
@@ -6,7 +6,15 @@ paths:
|
|
|
6
6
|
|
|
7
7
|
# Wireframe standards
|
|
8
8
|
|
|
9
|
+
## Layout and intent
|
|
10
|
+
|
|
11
|
+
- Draw each surface as an ASCII block inside a `plaintext` fence, one fence per distinct layout.
|
|
12
|
+
- Label a region with its role. Never label one with a class name, a token name, or a computed value.
|
|
13
|
+
- Carry on-screen copy verbatim, and mark copy the surface templates.
|
|
14
|
+
- State interaction intent, never the mechanism behind it. Send algorithms, handlers, and thresholds to a `.claude/context/` entry.
|
|
15
|
+
- Update a surface's wireframe in the same pull request that changes its layout or its interaction.
|
|
16
|
+
|
|
9
17
|
## Authority
|
|
10
18
|
|
|
11
19
|
- Follow `.claude/standards/wireframes.md` for layout and interaction intent: ASCII layout, region labels, variants, copy, and what moves to `.claude/context/`. It is the single source.
|
|
12
|
-
-
|
|
20
|
+
- Read it before adding or revising a surface.
|
|
@@ -6,6 +6,14 @@ paths:
|
|
|
6
6
|
|
|
7
7
|
# Requirements standards
|
|
8
8
|
|
|
9
|
+
## Scope and non-goals
|
|
10
|
+
|
|
11
|
+
- State a goal as a user-facing outcome, never as the implementation delivering it.
|
|
12
|
+
- Record explicit non-goals. Mark a deferred item `(deferred)`.
|
|
13
|
+
- Leave the MVP list as written once its features ship. Do not renumber it, extend it, or annotate an entry with status.
|
|
14
|
+
- Add later scope as a new section rather than as an extension of the MVP list.
|
|
15
|
+
- Drop a section rather than pad it. Include `## Distribution` only when the project ships outside its own repository.
|
|
16
|
+
|
|
9
17
|
## Authority
|
|
10
18
|
|
|
11
19
|
- Follow `.claude/standards/requirements.md` for what belongs in the problem, goals, non-goals, MVP features, tech stack, and constraints. It is the single source.
|
|
@@ -6,6 +6,16 @@ paths:
|
|
|
6
6
|
|
|
7
7
|
# Architecture standards
|
|
8
8
|
|
|
9
|
+
## Decisions
|
|
10
|
+
|
|
11
|
+
- Give each decision its own H3 under `## Key technical decisions`, naming what it chose and the alternative it passed over.
|
|
12
|
+
- Keep line-by-line function behavior and full type definitions out.
|
|
13
|
+
|
|
14
|
+
## Verification anchors
|
|
15
|
+
|
|
16
|
+
- Close a decision citing a measured number with `Measured at <short-sha> on <YYYY-MM-DD>.` Leave a decision citing none unanchored.
|
|
17
|
+
- Refresh that anchor whenever the number is re-read, whether or not it moved.
|
|
18
|
+
|
|
9
19
|
## Authority
|
|
10
20
|
|
|
11
21
|
- Follow `.claude/standards/architecture.md` for the overview, named decision entries, and risks. It is the single source.
|
|
@@ -6,6 +6,14 @@ paths:
|
|
|
6
6
|
|
|
7
7
|
# Design standards
|
|
8
8
|
|
|
9
|
+
## Tokens
|
|
10
|
+
|
|
11
|
+
- Describe a token as intent rather than as a computed value.
|
|
12
|
+
- Keep CSS classes, component filenames, and prop names out.
|
|
13
|
+
- Carry a token system as a table, one row per token, and a component rule as a short bullet.
|
|
14
|
+
- Keep the token table headers verbatim.
|
|
15
|
+
- Record a non-obvious omission, such as no motion or no custom icons.
|
|
16
|
+
|
|
9
17
|
## Authority
|
|
10
18
|
|
|
11
19
|
- Follow `.claude/standards/design.md` for visual intent, the token tables, and the render contract. It is the single source.
|
|
@@ -6,6 +6,14 @@ paths:
|
|
|
6
6
|
|
|
7
7
|
# Tasks standards
|
|
8
8
|
|
|
9
|
+
## What a task carries
|
|
10
|
+
|
|
11
|
+
- Name where the task came from, through a `Plan:`, `Groundwork:`, `Intake:`, or `Issue:` line under the title.
|
|
12
|
+
- Size the outcomes so one pull request closes all of them. Split the task before handing it off.
|
|
13
|
+
- Use `## Outcomes` and `## Findings` and add no third heading. Note status inline on an outcome.
|
|
14
|
+
- Keep class names, file paths, function names, and prop names out of every entry and title.
|
|
15
|
+
- Never delete a task file. A shipped task moves to the archive under its own name.
|
|
16
|
+
|
|
9
17
|
## Authority
|
|
10
18
|
|
|
11
19
|
- Follow `.claude/standards/tasks.md` for filenames, frontmatter, what belongs, and the task file format. It is the single source.
|
|
@@ -6,6 +6,14 @@ paths:
|
|
|
6
6
|
|
|
7
7
|
# README standards
|
|
8
8
|
|
|
9
|
+
## Audience and voice
|
|
10
|
+
|
|
11
|
+
- Write a repository-root README for someone deciding whether to commit to the project. A nested README keeps the reference voice.
|
|
12
|
+
- Address the reader in second person.
|
|
13
|
+
- Ground a claim in a command, a number, or a named constraint rather than an adjective.
|
|
14
|
+
- Name what the project does not do.
|
|
15
|
+
- Link out to full API documentation, license text, contribution guidelines, and troubleshooting rather than carrying them.
|
|
16
|
+
|
|
9
17
|
## Authority
|
|
10
18
|
|
|
11
19
|
- Follow `.claude/standards/readme.md` for voice, structure, required and optional sections, heading hierarchy, badge classes, per-project-type content, and what to link out rather than inline. It is the single source.
|
|
@@ -6,6 +6,17 @@ paths:
|
|
|
6
6
|
|
|
7
7
|
# Rule authoring standards
|
|
8
8
|
|
|
9
|
+
## Numbering
|
|
10
|
+
|
|
11
|
+
- Name a rule `.claude/rules/<subdirectory>/<n>-<slug>.md`, taking a number that collides with neither the project's rules nor any installed shared set.
|
|
12
|
+
- Give every rule a numeric prefix and keep the slug to one to three kebab words.
|
|
13
|
+
|
|
14
|
+
## Body
|
|
15
|
+
|
|
16
|
+
- State one directive per bullet in imperative voice. Do not explain the reasoning behind a rule.
|
|
17
|
+
- Cut a rule that resists crisp one-line phrasing.
|
|
18
|
+
- Do not restate a rule a sibling rule or `CLAUDE.md` already owns. Point once.
|
|
19
|
+
|
|
9
20
|
## Authority
|
|
10
21
|
|
|
11
22
|
- Follow `.claude/standards/rule.md` for rule frontmatter, body shape, and voice. It is the single source.
|
|
@@ -7,6 +7,14 @@ paths:
|
|
|
7
7
|
|
|
8
8
|
# Standard authoring standards
|
|
9
9
|
|
|
10
|
+
## The scope statement
|
|
11
|
+
|
|
12
|
+
- Govern one document type per standard, or one attribute across every document.
|
|
13
|
+
- Open with a `## Scope` section naming what the standard governs and a `Does not govern:` list, placed above the shape rules.
|
|
14
|
+
- Put the governed path in backticks in the first sentence, anchored deep enough to resolve from a project root. Keep an attribute standard's first sentence free of backticks and say in it that it governs an attribute.
|
|
15
|
+
- Give each `Does not govern:` entry an excluded concern and the owner it goes to. Cut an entry naming no owner.
|
|
16
|
+
- Declare a yield, an exemption, or a handoff from both sides of the boundary.
|
|
17
|
+
|
|
10
18
|
## Authority
|
|
11
19
|
|
|
12
20
|
- Follow `.claude/standards/standard.md` for a standard's frontmatter, shape, scope, and success criterion. It is the single source.
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -24,7 +24,7 @@ import { register as comments } from '@/commands/comments'
|
|
|
24
24
|
import { register as context } from '@/commands/context'
|
|
25
25
|
import { register as markdown } from '@/commands/markdown'
|
|
26
26
|
import { register as records } from '@/commands/records'
|
|
27
|
-
import { PROJECT_ROOT } from '@/
|
|
27
|
+
import { PROJECT_ROOT } from '@/project-root'
|
|
28
28
|
|
|
29
29
|
const GREY = '\x1b[0;90m'
|
|
30
30
|
const WHITE = '\x1b[1;37m'
|
|
@@ -40,7 +40,7 @@ function showHelp(): void {
|
|
|
40
40
|
`${GREY}│${NC} sync [path] ${GREY}# Sync all installed domains in a project${NC}`,
|
|
41
41
|
`${GREY}│${NC} sandbox [cat:cmd] ${GREY}# Provision and run sandbox scenarios${NC}`,
|
|
42
42
|
`${GREY}│${NC} gov [command] ${GREY}# Governance commands (install, sync)${NC}`,
|
|
43
|
-
`${GREY}│${NC} standards [cmd] ${GREY}# Standards commands (install, sync)${NC}`,
|
|
43
|
+
`${GREY}│${NC} standards [cmd] ${GREY}# Standards commands (install, sync, list, <name>)${NC}`,
|
|
44
44
|
`${GREY}│${NC} snippets [cmd] ${GREY}# Snippets commands (install, sync)${NC}`,
|
|
45
45
|
`${GREY}│${NC} tooling [cmd] ${GREY}# Manage tooling stacks (sync, ref, create)${NC}`,
|
|
46
46
|
`${GREY}│${NC} claude [cmd] ${GREY}# Claude workflow (init, sync, setup)${NC}`,
|
package/src/commands/claude.ts
CHANGED
|
@@ -29,7 +29,8 @@ import {
|
|
|
29
29
|
writeSettings,
|
|
30
30
|
} from '@/claude/settings'
|
|
31
31
|
import { copyPreservingMode } from '@/copy'
|
|
32
|
-
import { execScript
|
|
32
|
+
import { execScript } from '@/exec'
|
|
33
|
+
import { PROJECT_ROOT } from '@/project-root'
|
|
33
34
|
import { isDirectory, resolveTarget } from '@/target'
|
|
34
35
|
import { injectGitignore, pruneGitignore } from '@/tooling/inject'
|
|
35
36
|
import {
|
package/src/commands/docs.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { Command } from 'commander'
|
|
2
2
|
import { registerPassThroughVerbs } from '@/commands/pass-through'
|
|
3
3
|
import { listTopics, readTopic, resolveTopic } from '@/docs/read'
|
|
4
|
-
import { execScript
|
|
4
|
+
import { execScript } from '@/exec'
|
|
5
|
+
import { PROJECT_ROOT } from '@/project-root'
|
|
5
6
|
import { intro, logError, logInfo, logStep, logWarn, outro } from '@/ui'
|
|
6
7
|
|
|
7
8
|
export function register(program: Command): void {
|
package/src/commands/feedback.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
import type { Command } from 'commander'
|
|
4
4
|
import { deriveSlug, deriveTitle } from '@/commands/feedback-format'
|
|
5
|
-
import { PROJECT_ROOT } from '@/
|
|
5
|
+
import { PROJECT_ROOT } from '@/project-root'
|
|
6
6
|
import { createGithubIssue } from '@/github'
|
|
7
7
|
import { frameError, frameSuccess } from '@/ui'
|
|
8
8
|
|
package/src/commands/gov.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync } from 'node:fs'
|
|
|
2
2
|
import { mkdir, writeFile } from 'node:fs/promises'
|
|
3
3
|
import { basename, dirname, join, resolve } from 'node:path'
|
|
4
4
|
import type { Command } from 'commander'
|
|
5
|
-
import { PROJECT_ROOT } from '@/
|
|
5
|
+
import { PROJECT_ROOT } from '@/project-root'
|
|
6
6
|
import { createGovAdapter } from '@/gov/adapter'
|
|
7
7
|
import { regenConsumedRules } from '@/gov/consumed'
|
|
8
8
|
import { hasStandards, installRules, lookupRules } from '@/gov/install'
|
package/src/commands/init.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Command } from 'commander'
|
|
2
2
|
import { cliPath, cliRun } from '@/cli-run'
|
|
3
|
-
import { PROJECT_ROOT } from '@/
|
|
3
|
+
import { PROJECT_ROOT } from '@/project-root'
|
|
4
4
|
import { applyInitOptions, flagsProvided } from '@/init/flags'
|
|
5
5
|
import { type InitFlags, parseSkip, planInit } from '@/init/plan'
|
|
6
6
|
import { runDomains } from '@/init/run'
|
package/src/commands/markdown.ts
CHANGED
|
@@ -69,8 +69,10 @@ export function register(program: Command): void {
|
|
|
69
69
|
'what markdown.md reserves the span for.',
|
|
70
70
|
'',
|
|
71
71
|
'Bans and checkpoints are read from markdown.md and prose.md, resolved',
|
|
72
|
-
'under .claude/standards
|
|
73
|
-
'
|
|
72
|
+
'under .claude/standards/, then standards/, then the corpus inside the',
|
|
73
|
+
'aitk package, so a project that installed neither is still measured.',
|
|
74
|
+
'The report names the copy it read. No folder has to resolve and no',
|
|
75
|
+
'index.md has to exist, so .claude/rules/, governance/, and snippets/',
|
|
74
76
|
'are in reach.',
|
|
75
77
|
'',
|
|
76
78
|
'Examples:',
|
|
@@ -206,7 +208,7 @@ function reportBans(reports: readonly FileReport[], bans: BanReport): void {
|
|
|
206
208
|
|
|
207
209
|
if (bans.missing.length > 0) {
|
|
208
210
|
logWarn(
|
|
209
|
-
`Not measured. Found
|
|
211
|
+
`Not measured. Found no copy of: ${bans.missing.join(', ')}. Looked under .claude/standards/, then standards/, then the aitk package.`,
|
|
210
212
|
)
|
|
211
213
|
if (bans.sources.length === 0) return
|
|
212
214
|
}
|
package/src/commands/sandbox.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
|
2
2
|
import { homedir } from 'node:os'
|
|
3
3
|
import { join } from 'node:path'
|
|
4
4
|
import type { Command } from 'commander'
|
|
5
|
-
import {
|
|
5
|
+
import { execScript } from '@/exec'
|
|
6
|
+
import { PROJECT_ROOT } from '@/project-root'
|
|
6
7
|
import {
|
|
7
8
|
assertedPercent,
|
|
8
9
|
collectCensus,
|
package/src/commands/snippets.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Command } from 'commander'
|
|
2
2
|
import { registerPassThroughVerbs } from '@/commands/pass-through'
|
|
3
|
-
import { PROJECT_ROOT } from '@/
|
|
3
|
+
import { PROJECT_ROOT } from '@/project-root'
|
|
4
4
|
import { createSnippetsAdapter } from '@/snippets/adapter'
|
|
5
5
|
import { BASE_CATEGORY } from '@/snippets/categories'
|
|
6
6
|
import {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
2
|
import type { Command } from 'commander'
|
|
3
3
|
import { registerPassThroughVerbs } from '@/commands/pass-through'
|
|
4
|
-
import { PROJECT_ROOT } from '@/
|
|
4
|
+
import { PROJECT_ROOT } from '@/project-root'
|
|
5
5
|
import { createStandardsAdapter, standardsSourceDir } from '@/standards/adapter'
|
|
6
6
|
import { ALL_SELECTION, selectStandards } from '@/standards/closure'
|
|
7
7
|
import {
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
standardsInstallDir,
|
|
11
11
|
} from '@/standards/index-refresh'
|
|
12
12
|
import { applyInstall, planInstall } from '@/standards/install'
|
|
13
|
+
import { listStandards, readStandard, resolveStandard } from '@/standards/read'
|
|
13
14
|
import { recordStamp, runDomainSync } from '@/sync/engine'
|
|
14
15
|
import { resolveTarget } from '@/target'
|
|
15
16
|
import {
|
|
@@ -35,8 +36,35 @@ interface InstallOptions {
|
|
|
35
36
|
export function register(program: Command): void {
|
|
36
37
|
const standards = program
|
|
37
38
|
.command('standards')
|
|
38
|
-
.description('Standards commands (install, sync, list)')
|
|
39
|
+
.description('Standards commands (install, sync, list, <name>)')
|
|
40
|
+
.argument('[name]', 'Standard to print, by name with or without .md')
|
|
39
41
|
.helpOption('-h, --help', 'Show this help message')
|
|
42
|
+
.addHelpText(
|
|
43
|
+
'after',
|
|
44
|
+
[
|
|
45
|
+
'',
|
|
46
|
+
'A name resolves under .claude/standards/, then standards/, then the',
|
|
47
|
+
'corpus inside the aitk package, so a standard prints without a project',
|
|
48
|
+
'copy on disk. The frame names the copy it read.',
|
|
49
|
+
'',
|
|
50
|
+
'Examples:',
|
|
51
|
+
' aitk standards prose',
|
|
52
|
+
' aitk standards markdown.md',
|
|
53
|
+
'',
|
|
54
|
+
].join('\n'),
|
|
55
|
+
)
|
|
56
|
+
.action((name: string | undefined, _options: unknown, cmd: Command) => {
|
|
57
|
+
if (name === undefined) {
|
|
58
|
+
// Registering an action replaces commander's own no-action fallback,
|
|
59
|
+
// which writes this help to stderr. Help is UI rather than data, and
|
|
60
|
+
// the default here is stdout.
|
|
61
|
+
cmd.outputHelp({ error: true })
|
|
62
|
+
process.exitCode = 1
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
process.exitCode = print(name)
|
|
67
|
+
})
|
|
40
68
|
|
|
41
69
|
standards
|
|
42
70
|
.command('sync')
|
|
@@ -82,6 +110,35 @@ export function register(program: Command): void {
|
|
|
82
110
|
registerPassThroughVerbs(standards, 'standards', ['list'])
|
|
83
111
|
}
|
|
84
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Writes the standard to stdout and every frame line to stderr, so a caller
|
|
115
|
+
* capturing the output with `$(...)` receives the document alone.
|
|
116
|
+
*
|
|
117
|
+
* The root is the caller's directory rather than the toolkit's, since a project
|
|
118
|
+
* copy is what governs wherever one exists and the package copy answers only
|
|
119
|
+
* its absence.
|
|
120
|
+
*/
|
|
121
|
+
function print(name: string): number {
|
|
122
|
+
intro('aitk standards')
|
|
123
|
+
|
|
124
|
+
const root = process.cwd()
|
|
125
|
+
const resolved = resolveStandard(root, name)
|
|
126
|
+
|
|
127
|
+
if (!resolved) {
|
|
128
|
+
logWarn(`Unknown standard: ${name}`)
|
|
129
|
+
logStep('Available standards')
|
|
130
|
+
for (const each of listStandards(root)) logInfo(each)
|
|
131
|
+
logError("Run 'aitk standards list' for descriptions.")
|
|
132
|
+
outro()
|
|
133
|
+
return 1
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
logStep(resolved.source)
|
|
137
|
+
process.stdout.write(readStandard(resolved))
|
|
138
|
+
outro()
|
|
139
|
+
return 0
|
|
140
|
+
}
|
|
141
|
+
|
|
85
142
|
async function runInstall(target: string, selection: string): Promise<number> {
|
|
86
143
|
intro('aitk standards')
|
|
87
144
|
|
package/src/commands/sync.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync } from 'node:fs'
|
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
import type { Command } from 'commander'
|
|
4
4
|
import { cliPath, cliRun } from '@/cli-run'
|
|
5
|
-
import { PROJECT_ROOT } from '@/
|
|
5
|
+
import { PROJECT_ROOT } from '@/project-root'
|
|
6
6
|
import {
|
|
7
7
|
buildCheckReport,
|
|
8
8
|
type CheckReport,
|
package/src/commands/tooling.ts
CHANGED
package/src/commands/wiki.ts
CHANGED
package/src/exec.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { join
|
|
1
|
+
import { join } from 'node:path'
|
|
2
2
|
import { execa } from 'execa'
|
|
3
|
-
|
|
4
|
-
export const PROJECT_ROOT = resolve(import.meta.dir, '..')
|
|
3
|
+
import { PROJECT_ROOT } from '@/project-root'
|
|
5
4
|
|
|
6
5
|
export async function execScript(
|
|
7
6
|
script: string,
|
package/src/github.ts
CHANGED
package/src/markdown/bans.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises'
|
|
2
|
-
import { resolve } from 'node:path'
|
|
3
2
|
import { linesOutsideFences } from '@/markdown/scan'
|
|
3
|
+
import { resolveStandard } from '@/standards/read'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Headings the two standards state their closed sets under.
|
|
@@ -17,13 +17,6 @@ export const CHARACTER_BAN_HEADING = '## Punctuation'
|
|
|
17
17
|
const BAN_LEAD = '- Do not use '
|
|
18
18
|
const SPELLING_LEAD = '- Use American English spelling'
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
* Roots searched in order. The installed copy wins over the authoring root, so
|
|
22
|
-
* a target project measures against the standard it actually has rather than
|
|
23
|
-
* one only the toolkit carries.
|
|
24
|
-
*/
|
|
25
|
-
const STANDARD_ROOTS = ['.claude/standards', 'standards']
|
|
26
|
-
|
|
27
20
|
const BACKTICKED = /`([^`]+)`/g
|
|
28
21
|
const WORD = /^[a-z]+$/
|
|
29
22
|
const SUFFIX = /^-[a-z]+$/
|
|
@@ -35,12 +28,16 @@ export interface BanReport {
|
|
|
35
28
|
readonly words: readonly string[]
|
|
36
29
|
/** Spellings derived from the prose standard's own examples. */
|
|
37
30
|
readonly spellings: readonly string[]
|
|
38
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Paths of the standards read, in the order read. Repo-relative for a project
|
|
33
|
+
* copy, and spelled under `<aitk>/` for the package copy, so a reader can
|
|
34
|
+
* tell which root answered without joining a path that resolves nowhere.
|
|
35
|
+
*/
|
|
39
36
|
readonly sources: readonly string[]
|
|
40
37
|
/**
|
|
41
|
-
* Standards that resolved under
|
|
42
|
-
* empty: a scan with no terms finds nothing, and reporting that as a
|
|
43
|
-
* file claims the prose passed when nothing was looked for.
|
|
38
|
+
* Standards that resolved under none of the roots. Absent is a distinct state
|
|
39
|
+
* from empty: a scan with no terms finds nothing, and reporting that as a
|
|
40
|
+
* clean file claims the prose passed when nothing was looked for.
|
|
44
41
|
*/
|
|
45
42
|
readonly missing: readonly string[]
|
|
46
43
|
}
|
|
@@ -155,7 +152,7 @@ export function parseSpellingBans(markdown: string): string[] {
|
|
|
155
152
|
}
|
|
156
153
|
|
|
157
154
|
export interface StandardText {
|
|
158
|
-
/**
|
|
155
|
+
/** Path of the copy read, so a report says which root won. */
|
|
159
156
|
readonly source: string
|
|
160
157
|
readonly text: string
|
|
161
158
|
}
|
|
@@ -164,29 +161,24 @@ export interface Standards {
|
|
|
164
161
|
readonly markdown: StandardText | undefined
|
|
165
162
|
readonly prose: StandardText | undefined
|
|
166
163
|
/**
|
|
167
|
-
* Standards that resolved under
|
|
168
|
-
* empty: a scan with no terms finds nothing, and reporting that as a
|
|
169
|
-
* file claims the prose passed when nothing was looked for.
|
|
164
|
+
* Standards that resolved under none of the roots. Absent is a distinct state
|
|
165
|
+
* from empty: a scan with no terms finds nothing, and reporting that as a
|
|
166
|
+
* clean file claims the prose passed when nothing was looked for.
|
|
170
167
|
*/
|
|
171
168
|
readonly missing: readonly string[]
|
|
172
169
|
}
|
|
173
170
|
|
|
174
|
-
async function
|
|
171
|
+
async function loadStandard(
|
|
175
172
|
root: string,
|
|
176
173
|
name: string,
|
|
177
174
|
): Promise<StandardText | undefined> {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const file = Bun.file(path)
|
|
181
|
-
if (!(await file.exists())) continue
|
|
182
|
-
|
|
183
|
-
return {
|
|
184
|
-
source: `${standardRoot}/${name}`,
|
|
185
|
-
text: await readFile(path, 'utf8'),
|
|
186
|
-
}
|
|
187
|
-
}
|
|
175
|
+
const resolved = resolveStandard(root, name)
|
|
176
|
+
if (!resolved) return undefined
|
|
188
177
|
|
|
189
|
-
return
|
|
178
|
+
return {
|
|
179
|
+
source: resolved.source,
|
|
180
|
+
text: await readFile(resolved.path, 'utf8'),
|
|
181
|
+
}
|
|
190
182
|
}
|
|
191
183
|
|
|
192
184
|
/**
|
|
@@ -199,8 +191,8 @@ async function readStandard(
|
|
|
199
191
|
*/
|
|
200
192
|
export async function loadStandards(root: string): Promise<Standards> {
|
|
201
193
|
const [markdown, prose] = await Promise.all([
|
|
202
|
-
|
|
203
|
-
|
|
194
|
+
loadStandard(root, 'markdown.md'),
|
|
195
|
+
loadStandard(root, 'prose.md'),
|
|
204
196
|
])
|
|
205
197
|
|
|
206
198
|
return {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { dirname, resolve } from 'node:path'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The root of the package this CLI is running out of, which is the repository
|
|
6
|
+
* root in a checkout and the installed package directory in a target.
|
|
7
|
+
*
|
|
8
|
+
* Derived from `import.meta.url` rather than Bun's `import.meta.dir`, since the
|
|
9
|
+
* test runner resolves the first and leaves the second undefined, which puts
|
|
10
|
+
* every module reading this root out of reach of a test.
|
|
11
|
+
*
|
|
12
|
+
* It sits in a module of its own rather than beside `execScript`, because a
|
|
13
|
+
* path constant and a process spawn share no reason to change and an importer
|
|
14
|
+
* of the first would otherwise load `execa` to read one string.
|
|
15
|
+
*/
|
|
16
|
+
export const PROJECT_ROOT = resolve(
|
|
17
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
18
|
+
'..',
|
|
19
|
+
)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { basename, join } from 'node:path'
|
|
3
|
+
import { stripFrontmatter } from '@/frontmatter'
|
|
4
|
+
import { PROJECT_ROOT } from '@/project-root'
|
|
5
|
+
import { standardsSourceDir } from '@/standards/adapter'
|
|
6
|
+
import { INDEX_FILE } from '@/standards/index-refresh'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Spells the package root in a report where every other root spells a
|
|
10
|
+
* project-relative path. The `source` field promises a path inside the project
|
|
11
|
+
* and a package copy is the one source that promise cannot cover, so the
|
|
12
|
+
* spelling has to be one nothing will join to a project root.
|
|
13
|
+
*/
|
|
14
|
+
const PACKAGE_LABEL = '<aitk>'
|
|
15
|
+
|
|
16
|
+
export interface StandardRoot {
|
|
17
|
+
/** Absolute directory to search. */
|
|
18
|
+
readonly dir: string
|
|
19
|
+
/** How a report spells a copy found under this root. */
|
|
20
|
+
readonly label: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ResolvedStandard {
|
|
24
|
+
readonly path: string
|
|
25
|
+
/** The label of the root that won, joined to the filename. */
|
|
26
|
+
readonly source: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The three roots a standard resolves against, in precedence order.
|
|
31
|
+
*
|
|
32
|
+
* A project copy wins wherever one exists. An installed standard is a seed a
|
|
33
|
+
* project edits, so a package copy overriding it would discard that edit with
|
|
34
|
+
* nothing said, and the fallback answers absence rather than staleness. The
|
|
35
|
+
* package root is what lets a project that installed no standards still be
|
|
36
|
+
* measured against one, since `package.json` ships the corpus beside the code
|
|
37
|
+
* doing the reading.
|
|
38
|
+
*/
|
|
39
|
+
export function standardRoots(root: string): StandardRoot[] {
|
|
40
|
+
return [
|
|
41
|
+
{
|
|
42
|
+
dir: join(root, '.claude', 'standards'),
|
|
43
|
+
label: join('.claude', 'standards'),
|
|
44
|
+
},
|
|
45
|
+
{ dir: join(root, 'standards'), label: 'standards' },
|
|
46
|
+
{
|
|
47
|
+
dir: standardsSourceDir(PROJECT_ROOT),
|
|
48
|
+
label: join(PACKAGE_LABEL, 'standards'),
|
|
49
|
+
},
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function filename(name: string): string {
|
|
54
|
+
return name.endsWith('.md') ? name : `${name}.md`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Resolves a standard by name, with or without the extension. */
|
|
58
|
+
export function resolveStandard(
|
|
59
|
+
root: string,
|
|
60
|
+
name: string,
|
|
61
|
+
): ResolvedStandard | undefined {
|
|
62
|
+
const file = filename(name)
|
|
63
|
+
|
|
64
|
+
for (const { dir, label } of standardRoots(root)) {
|
|
65
|
+
const path = join(dir, file)
|
|
66
|
+
if (existsSync(path)) return { path, source: join(label, file) }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return undefined
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Names every standard a resolve could reach, deduplicated across the roots and
|
|
74
|
+
* sorted. Shown when a name misses, so it doubles as the answer to what the
|
|
75
|
+
* caller should have typed.
|
|
76
|
+
*/
|
|
77
|
+
export function listStandards(root: string): string[] {
|
|
78
|
+
const names = new Set<string>()
|
|
79
|
+
|
|
80
|
+
for (const { dir } of standardRoots(root)) {
|
|
81
|
+
if (!existsSync(dir)) continue
|
|
82
|
+
|
|
83
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
84
|
+
if (!entry.isFile()) continue
|
|
85
|
+
if (!entry.name.endsWith('.md')) continue
|
|
86
|
+
if (entry.name === INDEX_FILE) continue
|
|
87
|
+
|
|
88
|
+
names.add(basename(entry.name, '.md'))
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return [...names].sort()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function readStandard(standard: ResolvedStandard): string {
|
|
96
|
+
return stripFrontmatter(readFileSync(standard.path, 'utf8'))
|
|
97
|
+
}
|