@erclx/aitk 1.7.1 → 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.
- package/README.md +2 -2
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/claude-autoship/SKILL.md +2 -2
- package/claude/skills/claude-docs/SKILL.md +11 -0
- package/claude/skills/claude-groundwork/SKILL.md +1 -1
- package/claude/skills/claude-seed-sync/REQUIREMENT.md +2 -2
- package/claude/skills/claude-seed-sync/SKILL.md +15 -18
- package/claude/skills/create-standard/REQUIREMENT.md +7 -10
- package/claude/skills/create-standard/SKILL.md +10 -11
- package/claude/skills/git-pr/references/labels.md +7 -1
- package/claude/skills/migration-standards/REQUIREMENT.md +10 -7
- package/claude/skills/migration-standards/SKILL.md +24 -22
- package/claude/skills/migration-superseded/REQUIREMENT.md +2 -2
- package/claude/skills/migration-superseded/SKILL.md +7 -9
- package/claude/skills/setup-gov/REQUIREMENT.md +2 -2
- package/claude/skills/toolkit-cli/SKILL.md +13 -14
- package/docs/agents/commands.md +1 -1
- package/docs/agents/install-and-sync.md +27 -41
- package/docs/agents/scripting.md +2 -9
- package/docs/target-projects.md +14 -15
- package/package.json +1 -1
- package/scripts/core/install-check.sh +5 -1
- package/scripts/manage-sandbox.sh +4 -7
- package/scripts/standards/list.sh +6 -4
- package/src/cli.ts +1 -1
- package/src/commands/gov.ts +1 -7
- package/src/commands/init.ts +1 -4
- package/src/commands/standards.ts +9 -141
- package/src/commands/sync.ts +1 -2
- package/src/gov/install.ts +0 -9
- package/src/init/flags.ts +3 -10
- package/src/init/plan.ts +8 -26
- package/src/init/steps.ts +0 -24
- package/src/standards/read.ts +16 -14
- package/src/sync/check.ts +4 -7
- package/src/sync/layout.ts +8 -10
- package/src/sync/stamp.ts +18 -15
- package/src/sync/target.ts +1 -8
- package/standards/skill.md +6 -6
- package/standards/standard.md +1 -1
- package/tooling/claude/seeds/.claude/ARCHITECTURE.md +1 -1
- package/tooling/claude/seeds/.claude/DESIGN.md +1 -1
- package/tooling/claude/seeds/.claude/REQUIREMENTS.md +1 -1
- package/tooling/claude/seeds/CLAUDE.md +2 -2
- package/src/standards/adapter.ts +0 -51
- package/src/standards/closure.ts +0 -200
- package/src/standards/index-refresh.ts +0 -44
- package/src/standards/install.ts +0 -52
package/src/standards/closure.ts
DELETED
|
@@ -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
|
-
}
|
package/src/standards/install.ts
DELETED
|
@@ -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
|
-
}
|