@erclx/aitk 1.7.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +2 -2
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/claude-autoship/SKILL.md +2 -2
  4. package/claude/skills/claude-docs/SKILL.md +11 -0
  5. package/claude/skills/claude-groundwork/SKILL.md +1 -1
  6. package/claude/skills/claude-seed-sync/REQUIREMENT.md +2 -2
  7. package/claude/skills/claude-seed-sync/SKILL.md +15 -18
  8. package/claude/skills/create-standard/REQUIREMENT.md +7 -10
  9. package/claude/skills/create-standard/SKILL.md +10 -11
  10. package/claude/skills/git-pr/references/labels.md +7 -1
  11. package/claude/skills/migration-standards/REQUIREMENT.md +10 -7
  12. package/claude/skills/migration-standards/SKILL.md +24 -22
  13. package/claude/skills/migration-superseded/REQUIREMENT.md +2 -2
  14. package/claude/skills/migration-superseded/SKILL.md +7 -9
  15. package/claude/skills/setup-gov/REQUIREMENT.md +2 -2
  16. package/claude/skills/toolkit-cli/SKILL.md +13 -14
  17. package/docs/agents/commands.md +1 -1
  18. package/docs/agents/install-and-sync.md +27 -41
  19. package/docs/agents/records.md +1 -1
  20. package/docs/agents/scripting.md +2 -9
  21. package/docs/target-projects.md +14 -15
  22. package/package.json +1 -1
  23. package/scripts/core/install-check.sh +5 -1
  24. package/scripts/manage-sandbox.sh +4 -7
  25. package/scripts/standards/list.sh +6 -4
  26. package/src/cli.ts +1 -1
  27. package/src/commands/gov.ts +1 -7
  28. package/src/commands/init.ts +1 -4
  29. package/src/commands/standards.ts +9 -141
  30. package/src/commands/sync.ts +1 -2
  31. package/src/gov/install.ts +0 -9
  32. package/src/init/flags.ts +3 -10
  33. package/src/init/plan.ts +8 -26
  34. package/src/init/steps.ts +0 -24
  35. package/src/records/backup.ts +11 -4
  36. package/src/standards/read.ts +16 -14
  37. package/src/sync/check.ts +4 -7
  38. package/src/sync/layout.ts +8 -10
  39. package/src/sync/stamp.ts +18 -15
  40. package/src/sync/target.ts +1 -8
  41. package/standards/skill.md +6 -6
  42. package/standards/standard.md +1 -1
  43. package/tooling/claude/manifest.toml +1 -1
  44. package/tooling/claude/reference.md +1 -1
  45. package/tooling/claude/seeds/.claude/ARCHITECTURE.md +1 -1
  46. package/tooling/claude/seeds/.claude/DESIGN.md +1 -1
  47. package/tooling/claude/seeds/.claude/REQUIREMENTS.md +1 -1
  48. package/tooling/claude/seeds/CLAUDE.md +2 -2
  49. package/src/standards/adapter.ts +0 -51
  50. package/src/standards/closure.ts +0 -200
  51. package/src/standards/index-refresh.ts +0 -44
  52. package/src/standards/install.ts +0 -52
@@ -1,200 +0,0 @@
1
- import { readFileSync } from 'node:fs'
2
- import { basename } from 'node:path'
3
- import type { StandardsSource } from '@/standards/install'
4
-
5
- export const ALL_SELECTION = 'all'
6
-
7
- export interface Citations {
8
- /** Siblings the body depends on, which the closure follows. */
9
- readonly cited: readonly string[]
10
- /** Siblings a `Does not govern:` entry hands off to, which it does not. */
11
- readonly delegated: readonly string[]
12
- }
13
-
14
- export interface StandardsSelection {
15
- readonly files: readonly StandardsSource[]
16
- /** What the caller named, in the order the flat root lists it. */
17
- readonly requested: readonly string[]
18
- /** What a requested standard cites and the caller did not name. */
19
- readonly added: readonly string[]
20
- /** Handoff targets that did not land, so their pointers will not resolve. */
21
- readonly unresolved: readonly string[]
22
- }
23
-
24
- export type SelectionResult =
25
- | { readonly ok: true; readonly selection: StandardsSelection }
26
- | { readonly ok: false; readonly unknown: readonly string[] }
27
-
28
- const CITATION = /`([^`\n]+?\.md)`/g
29
- const DELEGATION_START = /^Does not govern:/
30
- const HEADING = /^#{1,6}\s/
31
-
32
- /** Accepts `skill` and `skill.md` alike, since the catalog lists both spellings. */
33
- export function normalizeName(raw: string): string {
34
- const name = raw.trim()
35
- return name.endsWith('.md') ? name : `${name}.md`
36
- }
37
-
38
- export function parseSelection(csv: string): string[] {
39
- return csv
40
- .split(',')
41
- .map((raw) => raw.trim())
42
- .filter((raw) => raw !== '')
43
- .map(normalizeName)
44
- }
45
-
46
- /**
47
- * Splits a body at the `Does not govern:` list, which runs to the next heading.
48
- * Every standard in the flat root carries exactly one, directly under `## Scope`.
49
- */
50
- function splitDelegatedScope(body: string): {
51
- governing: string
52
- delegated: string
53
- } {
54
- const governing: string[] = []
55
- const delegated: string[] = []
56
- let inDelegation = false
57
-
58
- for (const line of body.split('\n')) {
59
- if (DELEGATION_START.test(line)) inDelegation = true
60
- else if (inDelegation && HEADING.test(line)) inDelegation = false
61
-
62
- if (inDelegation) delegated.push(line)
63
- else governing.push(line)
64
- }
65
-
66
- return { governing: governing.join('\n'), delegated: delegated.join('\n') }
67
- }
68
-
69
- function matchNames(text: string, available: ReadonlySet<string>): string[] {
70
- const found = new Set<string>()
71
-
72
- for (const match of text.matchAll(CITATION)) {
73
- const token = match[1]
74
- if (token === undefined) continue
75
-
76
- const name = basename(token)
77
- if (available.has(name)) found.add(name)
78
- }
79
-
80
- return [...found]
81
- }
82
-
83
- /**
84
- * Reads the sibling standards a body cites, split by whether the citation is a
85
- * dependency or a handoff. A citation is a backticked token ending in `.md`,
86
- * which is the only place either relationship is written, so the parse is a
87
- * heuristic and every candidate is resolved against `available` before it
88
- * counts. That resolution is what drops a fenced example, a target project's
89
- * `.claude/ARCHITECTURE.md`, and a bundled standard the flat root does not
90
- * install, none of which a selection should pull in.
91
- *
92
- * A citation inside the `Does not govern:` list is `delegated` rather than
93
- * `cited`, because that entry says the sibling owns a concern this standard
94
- * does not. Expanding on it pulls in a file the caller declined by not naming
95
- * it, and nearly all the corpus density sits in those lists, which is what
96
- * collapsed every selection into the whole corpus. A name appearing in the list
97
- * and also outside it stays `cited`, since a real dependency outranks a handoff.
98
- *
99
- * Matching is case-exact against the listing rather than a filesystem probe,
100
- * because a case-insensitive volume would otherwise resolve `SKILL.md` onto
101
- * `skill.md` and expand a selection on a citation that names a target's own
102
- * file. The basename is what resolves, so `standards/versioning.md` and a bare
103
- * `versioning.md` read as the same dependency.
104
- */
105
- export function citedStandards(
106
- body: string,
107
- available: ReadonlySet<string>,
108
- ): Citations {
109
- const { governing, delegated } = splitDelegatedScope(body)
110
- const cited = matchNames(governing, available)
111
- const citedSet = new Set(cited)
112
-
113
- return {
114
- cited,
115
- delegated: matchNames(delegated, available).filter(
116
- (name) => !citedSet.has(name),
117
- ),
118
- }
119
- }
120
-
121
- /**
122
- * Expands a selection to the transitive closure of what it cites, so an install
123
- * cannot land a standard whose citations dangle. `all` and an empty selection
124
- * both mean every standard, which is what keeps the existing callers unchanged.
125
- *
126
- * The closure follows dependencies alone. A `Does not govern:` handoff names a
127
- * concern the caller declined by not selecting it, so the target stays out and
128
- * is reported through `unresolved` instead. Following those too pulls the whole
129
- * corpus in behind any single name.
130
- *
131
- * An unrecognized name fails the whole selection rather than being dropped with
132
- * a warning, unlike `--skip` on `aitk init`. A typo here silently omits a
133
- * standard the caller asked for, and the closure would then be computed over
134
- * the wrong set.
135
- */
136
- export function selectStandards(
137
- available: readonly StandardsSource[],
138
- selection: string,
139
- ): SelectionResult {
140
- const byName = new Map(available.map((file) => [file.name, file]))
141
-
142
- if (selection.trim() === '' || selection.trim() === ALL_SELECTION) {
143
- return {
144
- ok: true,
145
- selection: {
146
- files: available,
147
- requested: available.map((file) => file.name),
148
- added: [],
149
- unresolved: [],
150
- },
151
- }
152
- }
153
-
154
- const requested = parseSelection(selection)
155
- const unknown = requested.filter((name) => !byName.has(name))
156
- if (unknown.length > 0) return { ok: false, unknown }
157
-
158
- const names = new Set(byName.keys())
159
- const resolved = new Set(requested)
160
- const handoffs = new Set<string>()
161
- const queue = [...requested]
162
-
163
- for (let index = 0; index < queue.length; index += 1) {
164
- const name = queue[index]
165
- if (name === undefined) continue
166
-
167
- const file = byName.get(name)
168
- if (file === undefined) continue
169
-
170
- const citations = citedStandards(readFileSync(file.path, 'utf8'), names)
171
- for (const handoff of citations.delegated) handoffs.add(handoff)
172
-
173
- for (const cited of citations.cited) {
174
- if (resolved.has(cited)) continue
175
-
176
- resolved.add(cited)
177
- queue.push(cited)
178
- }
179
- }
180
-
181
- const requestedSet = new Set(requested)
182
-
183
- return {
184
- ok: true,
185
- selection: {
186
- files: available.filter((file) => resolved.has(file.name)),
187
- requested: available
188
- .filter((file) => requestedSet.has(file.name))
189
- .map((file) => file.name),
190
- added: available
191
- .filter(
192
- (file) => resolved.has(file.name) && !requestedSet.has(file.name),
193
- )
194
- .map((file) => file.name),
195
- unresolved: available
196
- .filter((file) => handoffs.has(file.name) && !resolved.has(file.name))
197
- .map((file) => file.name),
198
- },
199
- }
200
- }
@@ -1,44 +0,0 @@
1
- import { existsSync } from 'node:fs'
2
- import { join } from 'node:path'
3
- import { copyPreservingMode } from '@/copy'
4
- import { regenOne } from '@/indexes/regen'
5
- import { logAdd, logWarn } from '@/ui'
6
-
7
- export const STANDARDS_REL = join('.claude', 'standards')
8
- export const INDEX_FILE = 'index.md'
9
-
10
- export function standardsInstallDir(target: string): string {
11
- return join(target, STANDARDS_REL)
12
- }
13
-
14
- /**
15
- * Replaces the target's `index.md` from source and rebuilds it against what
16
- * actually landed. It runs on every completed install and every completed sync,
17
- * including one with no changes, because the catalog can go stale from a file
18
- * the toolkit stopped shipping rather than from drift in a file it still does.
19
- *
20
- * `install` and `sync` are peers, so this sits beside both rather than inside
21
- * the sync adapter, where an install would be importing from the other verb.
22
- */
23
- export async function refreshIndex(
24
- sourceDir: string,
25
- target: string,
26
- ): Promise<void> {
27
- const installedDir = standardsInstallDir(target)
28
- const source = join(sourceDir, INDEX_FILE)
29
-
30
- if (!existsSync(source)) {
31
- logWarn(`No ${INDEX_FILE} in toolkit standards, leaving the target catalog`)
32
- return
33
- }
34
-
35
- await copyPreservingMode(source, join(installedDir, INDEX_FILE))
36
- const result = await regenOne(installedDir, { dryRun: false })
37
-
38
- if (result.action === 'error') {
39
- logWarn(`${join(STANDARDS_REL, INDEX_FILE)} regen failed: ${result.reason}`)
40
- return
41
- }
42
-
43
- logAdd(join(STANDARDS_REL, INDEX_FILE))
44
- }
@@ -1,52 +0,0 @@
1
- import { existsSync, readdirSync } from 'node:fs'
2
- import { mkdir } from 'node:fs/promises'
3
- import { join } from 'node:path'
4
- import { copyPreservingMode } from '@/copy'
5
- import { INDEX_FILE, STANDARDS_REL } from '@/standards/index-refresh'
6
-
7
- export interface StandardsSource {
8
- readonly path: string
9
- readonly name: string
10
- }
11
-
12
- /**
13
- * Lists the flat `standards/` root only, which is the same set the sync adapter
14
- * matches installed files against. A standard in a source subfolder such as
15
- * `bundled/` is not installed wholesale, so the two verbs agree on scope.
16
- *
17
- * `index.md` is excluded because install copies it separately and then rebuilds
18
- * it against what landed, rather than shipping the toolkit's own catalog.
19
- */
20
- export function planInstall(sourceDir: string): StandardsSource[] {
21
- if (!existsSync(sourceDir)) return []
22
-
23
- return readdirSync(sourceDir, { withFileTypes: true })
24
- .filter(
25
- (entry) =>
26
- entry.isFile() &&
27
- entry.name.endsWith('.md') &&
28
- entry.name !== INDEX_FILE,
29
- )
30
- .map((entry) => ({ path: join(sourceDir, entry.name), name: entry.name }))
31
- .sort((left, right) => left.name.localeCompare(right.name))
32
- }
33
-
34
- /**
35
- * Copies every standard and returns the labels to log. Routes through
36
- * `copyPreservingMode` because the `cp` it replaces left an existing
37
- * destination's mode alone, so a target file held at 600 stays there.
38
- */
39
- export async function applyInstall(
40
- files: readonly StandardsSource[],
41
- destDir: string,
42
- ): Promise<string[]> {
43
- await mkdir(destDir, { recursive: true })
44
-
45
- await Promise.all(
46
- files.map((file) =>
47
- copyPreservingMode(file.path, join(destDir, file.name)),
48
- ),
49
- )
50
-
51
- return files.map((file) => join(STANDARDS_REL, file.name))
52
- }