@erclx/aitk 3.9.0 → 3.11.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/claude/skills/claude-markdown-propose/REQUIREMENT.md +48 -0
- package/claude/skills/claude-markdown-propose/SKILL.md +118 -0
- package/claude/skills/claude-markdown-propose/references/format.md +107 -0
- package/claude/skills/claude-pr-review/SKILL.md +1 -1
- package/claude/skills/claude-teach/SKILL.md +1 -1
- package/claude/skills/claude-worktree/SKILL.md +1 -1
- package/claude/skills/create-snippet/SKILL.md +1 -1
- package/claude/skills/git-branch/SKILL.md +1 -1
- package/claude/skills/git-commit/SKILL.md +1 -1
- package/claude/skills/git-issue/SKILL.md +2 -2
- package/claude/skills/git-pr/SKILL.md +5 -5
- package/claude/skills/git-split/SKILL.md +2 -2
- package/claude/skills/git-stage/SKILL.md +1 -1
- package/claude/skills/toolkit-cli/SKILL.md +0 -2
- package/docs/agents/audits.md +4 -4
- package/docs/agents/census.md +23 -0
- package/docs/agents/commands.md +1 -0
- package/docs/agents/index.md +1 -0
- package/docs/agents/records.md +1 -1
- package/docs/agents/teach.md +1 -1
- package/docs/ai-workflow.md +7 -6
- package/package.json +1 -1
- package/scripts/core/check-skill-paths.sh +1 -2
- package/scripts/core/verify.sh +0 -5
- package/src/audits/catalog.ts +48 -0
- package/src/census/count.ts +113 -0
- package/src/claude/skills-audit.ts +7 -0
- package/src/cli.ts +4 -0
- package/src/commands/census.ts +105 -0
- package/src/commands/claude.ts +20 -6
- package/src/commands/comments.ts +6 -0
- package/src/commands/context.ts +32 -7
- package/src/commands/gov.ts +2 -2
- package/src/commands/markdown.ts +18 -2
- package/src/context/audit.ts +16 -0
- package/src/gov/test-order.ts +31 -7
- package/src/markdown/files.ts +10 -0
- package/src/records/backup.ts +1 -0
- package/src/records/validate.ts +1 -3
- package/{claude/skills/git-split/references → standards}/branch.md +0 -1
- package/{claude/skills/git-commit/references → standards}/commit.md +0 -1
- package/{claude/skills/claude-teach/references → standards}/glossary.md +0 -1
- package/standards/index.md +6 -0
- package/standards/{bundled/issue.md → issue.md} +0 -1
- package/standards/{bundled/pr.md → pr.md} +0 -1
- package/standards/{bundled/snippets.md → snippets.md} +0 -1
- package/tooling/base/manifest.toml +0 -2
- package/tooling/base/reference.md +12 -12
- package/tooling/base/seeds/.claude/context/development.md +4 -6
- package/claude/skills/claude-worktree/references/branch.md +0 -60
- package/claude/skills/create-snippet/references/snippets.md +0 -78
- package/claude/skills/git-branch/references/branch.md +0 -60
- package/claude/skills/git-issue/references/issue.md +0 -95
- package/claude/skills/git-pr/references/branch.md +0 -60
- package/claude/skills/git-pr/references/pr.md +0 -139
- package/claude/skills/git-split/references/pr.md +0 -139
- package/claude/skills/git-stage/references/commit.md +0 -73
- package/scripts/core/regen-skill-references.sh +0 -27
- package/standards/bundled/branch.md +0 -60
- package/standards/bundled/commit.md +0 -73
- package/standards/bundled/glossary.md +0 -76
- package/tooling/base/configs/scripts/clean.sh +0 -45
- package/tooling/base/configs/scripts/update.sh +0 -49
package/src/audits/catalog.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import type { SkillsAuditRefusal } from '@/claude/skills-audit'
|
|
1
2
|
import type { ReachRefusal } from '@/claude/skills-reach'
|
|
3
|
+
import type { ContextAuditRefusal } from '@/context/audit'
|
|
2
4
|
import type { AuditRefusal } from '@/deps/audit'
|
|
3
5
|
import type { RestatedRefusal } from '@/gov/restated'
|
|
4
6
|
import type { LabelAuditRefusal } from '@/labels/audit'
|
|
@@ -405,6 +407,23 @@ function commentCounts(record: unknown): Record<string, number> | undefined {
|
|
|
405
407
|
return { degradationHits }
|
|
406
408
|
}
|
|
407
409
|
|
|
410
|
+
/**
|
|
411
|
+
* Reads the census's three headline totals, leaving the extension breakdown
|
|
412
|
+
* out. The breakdown is what a caller reads for its own sake, and folding
|
|
413
|
+
* every extension into the retained baseline would grow the recorded key set
|
|
414
|
+
* with every language the tree ever picks up.
|
|
415
|
+
*/
|
|
416
|
+
function censusCounts(record: unknown): Record<string, number> | undefined {
|
|
417
|
+
const root = asObject(record)
|
|
418
|
+
if (root === undefined || !Array.isArray(root.byExtension)) return undefined
|
|
419
|
+
|
|
420
|
+
return allOf({
|
|
421
|
+
files: typeof root.files === 'number' ? root.files : undefined,
|
|
422
|
+
skipped: typeof root.skipped === 'number' ? root.skipped : undefined,
|
|
423
|
+
lines: typeof root.lines === 'number' ? root.lines : undefined,
|
|
424
|
+
})
|
|
425
|
+
}
|
|
426
|
+
|
|
408
427
|
function testOrderCounts(record: unknown): Record<string, number> | undefined {
|
|
409
428
|
const root = asObject(record)
|
|
410
429
|
if (root === undefined) return undefined
|
|
@@ -448,6 +467,12 @@ export const AUDITS: readonly AuditSpec[] = [
|
|
|
448
467
|
argv: ['context', 'audit', '--json'],
|
|
449
468
|
gatingExits: [EXIT_FINDINGS],
|
|
450
469
|
corpus: 'tracked',
|
|
470
|
+
// The one reason this verb refuses for that is an absence rather than a
|
|
471
|
+
// break, on the same test the reach check and the skill audit take: no
|
|
472
|
+
// target adopts `.claude/context/`, `.claude/diagrams/`, and
|
|
473
|
+
// `.claude/wireframes/` all at once, so without the allowance every such
|
|
474
|
+
// project reports the verb unmeasured on every run and never changes.
|
|
475
|
+
absentReasons: ['no-folders'] satisfies ContextAuditRefusal[],
|
|
451
476
|
counts: contextCounts,
|
|
452
477
|
},
|
|
453
478
|
{
|
|
@@ -467,6 +492,10 @@ export const AUDITS: readonly AuditSpec[] = [
|
|
|
467
492
|
argv: ['claude', 'skills', 'audit', '--json'],
|
|
468
493
|
gatingExits: [EXIT_FINDINGS],
|
|
469
494
|
corpus: 'tracked',
|
|
495
|
+
// The one reason this verb refuses for, and it is an absence for the same
|
|
496
|
+
// reason the reach check's is: a project carrying neither `claude/skills/`
|
|
497
|
+
// nor `.claude/skills/` has adopted no skill convention this audit reads.
|
|
498
|
+
absentReasons: ['no-corpus'] satisfies SkillsAuditRefusal[],
|
|
470
499
|
counts: skillCounts,
|
|
471
500
|
},
|
|
472
501
|
{
|
|
@@ -503,6 +532,14 @@ export const AUDITS: readonly AuditSpec[] = [
|
|
|
503
532
|
argv: ['records', 'validate', kind, '--json'],
|
|
504
533
|
gatingExits: [],
|
|
505
534
|
corpus,
|
|
535
|
+
// `standards` is the one tracked kind here, so it takes none of the
|
|
536
|
+
// per-machine default the other five inherit from their corpus. A target
|
|
537
|
+
// reads standards through `aitk standards` rather than a copy in its own
|
|
538
|
+
// tree, so carrying neither `standards/` nor `.claude/standards/` is the
|
|
539
|
+
// ordinary state of every project but this repository.
|
|
540
|
+
...(kind === 'standards' && {
|
|
541
|
+
absentReasons: ['no-folder'] satisfies RecordRefusal[],
|
|
542
|
+
}),
|
|
506
543
|
counts: findingsOnly,
|
|
507
544
|
})),
|
|
508
545
|
{
|
|
@@ -597,6 +634,17 @@ export const AUDITS: readonly AuditSpec[] = [
|
|
|
597
634
|
] satisfies RestatedRefusal[],
|
|
598
635
|
counts: restatedCounts,
|
|
599
636
|
},
|
|
637
|
+
{
|
|
638
|
+
id: 'census',
|
|
639
|
+
label: 'Codebase census',
|
|
640
|
+
argv: ['census', '--json'],
|
|
641
|
+
// Reports rather than gates. A file count is a measure, not a judgment, so
|
|
642
|
+
// registering it here exists to retain growth against the baseline rather
|
|
643
|
+
// than to fail a push on a number moving.
|
|
644
|
+
gatingExits: [],
|
|
645
|
+
corpus: 'tracked',
|
|
646
|
+
counts: censusCounts,
|
|
647
|
+
},
|
|
600
648
|
{
|
|
601
649
|
id: 'deps',
|
|
602
650
|
label: 'Dependency advisories',
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { extname, join } from 'node:path'
|
|
3
|
+
import { isBinary } from '@/binary'
|
|
4
|
+
import { listRepositoryFiles } from '@/git-files'
|
|
5
|
+
|
|
6
|
+
export type CensusRefusal = 'no-git'
|
|
7
|
+
|
|
8
|
+
export interface ExtensionCount {
|
|
9
|
+
readonly extension: string
|
|
10
|
+
readonly files: number
|
|
11
|
+
readonly lines: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CensusResult {
|
|
15
|
+
readonly kind: 'measured'
|
|
16
|
+
readonly files: number
|
|
17
|
+
/** Binary or unreadable, counted toward `files` and excluded from `lines`. */
|
|
18
|
+
readonly skipped: number
|
|
19
|
+
readonly lines: number
|
|
20
|
+
readonly byExtension: readonly ExtensionCount[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CensusFailure {
|
|
24
|
+
readonly kind: 'refused'
|
|
25
|
+
readonly reason: CensusRefusal
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Open descriptors allowed at once while reading the tree, matching
|
|
30
|
+
* `src/comments/scan.ts`. A single `Promise.all` over every path opens one
|
|
31
|
+
* descriptor per file, which exhausts a default 1024 limit on a large
|
|
32
|
+
* repository and fails the whole census with EMFILE.
|
|
33
|
+
*/
|
|
34
|
+
const CONCURRENT_READS = 64
|
|
35
|
+
|
|
36
|
+
interface ReadOutcome {
|
|
37
|
+
readonly path: string
|
|
38
|
+
/** Absent when the read failed: a symlink leaving the tree, or a file git
|
|
39
|
+
* listed and the working tree no longer holds. */
|
|
40
|
+
readonly text?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function readOne(root: string, path: string): Promise<ReadOutcome> {
|
|
44
|
+
try {
|
|
45
|
+
return { path, text: await readFile(join(root, path), 'utf8') }
|
|
46
|
+
} catch {
|
|
47
|
+
return { path }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function extensionFor(path: string): string {
|
|
52
|
+
const ext = extname(path)
|
|
53
|
+
return ext === '' ? 'no-extension' : ext.slice(1).toLowerCase()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A trailing newline is the file's terminator rather than an empty final line,
|
|
58
|
+
* which is what keeps a file ending `a\nb\n` at two lines rather than three.
|
|
59
|
+
*/
|
|
60
|
+
function countLines(text: string): number {
|
|
61
|
+
if (text === '') return 0
|
|
62
|
+
const lines = text.split('\n')
|
|
63
|
+
return text.endsWith('\n') ? lines.length - 1 : lines.length
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Censuses the tracked-plus-untracked tree under `root`: a file count, a
|
|
68
|
+
* breakdown by extension, and a line total that skips whatever reads as
|
|
69
|
+
* binary.
|
|
70
|
+
*
|
|
71
|
+
* Reads `listRepositoryFiles`, the same corpus the citation check, the
|
|
72
|
+
* markdown corpus, and the secret scan already share, so this is not a
|
|
73
|
+
* fourth definition of what counts.
|
|
74
|
+
*/
|
|
75
|
+
export async function census(
|
|
76
|
+
root: string,
|
|
77
|
+
): Promise<CensusResult | CensusFailure> {
|
|
78
|
+
const listed = await listRepositoryFiles(root)
|
|
79
|
+
if (listed === undefined) return { kind: 'refused', reason: 'no-git' }
|
|
80
|
+
|
|
81
|
+
const totals = new Map<string, { files: number; lines: number }>()
|
|
82
|
+
let skipped = 0
|
|
83
|
+
let lines = 0
|
|
84
|
+
|
|
85
|
+
for (let start = 0; start < listed.length; start += CONCURRENT_READS) {
|
|
86
|
+
const batch = listed.slice(start, start + CONCURRENT_READS)
|
|
87
|
+
const outcomes = await Promise.all(batch.map((path) => readOne(root, path)))
|
|
88
|
+
|
|
89
|
+
for (const outcome of outcomes) {
|
|
90
|
+
const extension = extensionFor(outcome.path)
|
|
91
|
+
const entry = totals.get(extension) ?? { files: 0, lines: 0 }
|
|
92
|
+
entry.files += 1
|
|
93
|
+
|
|
94
|
+
// A read that failed and a file that read as binary are counted the
|
|
95
|
+
// same way: toward `files`, excluded from every line count.
|
|
96
|
+
if (outcome.text === undefined || isBinary(outcome.text)) {
|
|
97
|
+
skipped += 1
|
|
98
|
+
} else {
|
|
99
|
+
const counted = countLines(outcome.text)
|
|
100
|
+
entry.lines += counted
|
|
101
|
+
lines += counted
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
totals.set(extension, entry)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const byExtension = [...totals.entries()]
|
|
109
|
+
.map(([extension, counts]) => ({ extension, ...counts }))
|
|
110
|
+
.sort((a, b) => b.files - a.files || a.extension.localeCompare(b.extension))
|
|
111
|
+
|
|
112
|
+
return { kind: 'measured', files: listed.length, skipped, lines, byExtension }
|
|
113
|
+
}
|
|
@@ -25,6 +25,13 @@ export const CORPORA: readonly string[] = [
|
|
|
25
25
|
join('.claude', 'skills'),
|
|
26
26
|
]
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* The one reason this audit refuses. A project carrying neither corpus is the
|
|
30
|
+
* ordinary state of a target that has not adopted either skills convention,
|
|
31
|
+
* the same absence `no-skills` reads for the shipped citation reach check.
|
|
32
|
+
*/
|
|
33
|
+
export type SkillsAuditRefusal = 'no-corpus'
|
|
34
|
+
|
|
28
35
|
/** Kebab-case, which the standard states as no spaces, capitals, or underscores. */
|
|
29
36
|
const KEBAB_CASE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
30
37
|
|
package/src/cli.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { register as audits } from '@/commands/audits'
|
|
|
30
30
|
import { register as secrets } from '@/commands/secrets'
|
|
31
31
|
import { register as deps } from '@/commands/deps'
|
|
32
32
|
import { register as labels } from '@/commands/labels'
|
|
33
|
+
import { register as census } from '@/commands/census'
|
|
33
34
|
import { register as upgrade } from '@/commands/upgrade'
|
|
34
35
|
import { readInstalled, UNKNOWN_LABEL } from '@/version/installed'
|
|
35
36
|
import { palette } from '@/ui'
|
|
@@ -71,6 +72,7 @@ function showHelp(): void {
|
|
|
71
72
|
`${GREY}│${NC} secrets [cmd] ${GREY}# Read the shipped tree for credential-shaped values (scan)${NC}`,
|
|
72
73
|
`${GREY}│${NC} deps [cmd] ${GREY}# Read the resolved dependency set for advisories (audit)${NC}`,
|
|
73
74
|
`${GREY}│${NC} labels [cmd] ${GREY}# Read a changed set against the pull request label map (audit)${NC}`,
|
|
75
|
+
`${GREY}│${NC} census [path] ${GREY}# Report tracked file count, extension breakdown, and line totals${NC}`,
|
|
74
76
|
`${GREY}│${NC} audits [cmd] ${GREY}# Run every health check as one set (run, list)${NC}`,
|
|
75
77
|
`${GREY}│${NC} upgrade ${GREY}# Reinstall the CLI globally with the manager that installed it${NC}`,
|
|
76
78
|
`${GREY}│${NC}`,
|
|
@@ -116,6 +118,7 @@ function showHelp(): void {
|
|
|
116
118
|
`${GREY}│${NC} aitk secrets scan --json`,
|
|
117
119
|
`${GREY}│${NC} aitk deps audit --json`,
|
|
118
120
|
`${GREY}│${NC} aitk labels audit --json`,
|
|
121
|
+
`${GREY}│${NC} aitk census --json`,
|
|
119
122
|
`${GREY}│${NC} aitk audits run --json`,
|
|
120
123
|
`${GREY}│${NC} aitk upgrade --json`,
|
|
121
124
|
`${GREY}└${NC}`,
|
|
@@ -164,6 +167,7 @@ sessions(program)
|
|
|
164
167
|
secrets(program)
|
|
165
168
|
deps(program)
|
|
166
169
|
labels(program)
|
|
170
|
+
census(program)
|
|
167
171
|
audits(program)
|
|
168
172
|
upgrade(program)
|
|
169
173
|
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
import type { Command } from 'commander'
|
|
3
|
+
import { census, type CensusRefusal } from '@/census/count'
|
|
4
|
+
import { intro, logInfo, logStep, logWarn, outro, plural } from '@/ui'
|
|
5
|
+
|
|
6
|
+
interface CensusCommandOptions {
|
|
7
|
+
readonly json?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const REFUSALS: Record<CensusRefusal, string> = {
|
|
11
|
+
'no-git': 'git could not list this tree, so the corpus is unknown.',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function register(program: Command): void {
|
|
15
|
+
program
|
|
16
|
+
.command('census')
|
|
17
|
+
.description(
|
|
18
|
+
'Report tracked file count, a breakdown by extension, and line totals',
|
|
19
|
+
)
|
|
20
|
+
.argument('[path]', 'Tree to census, defaulting to the current directory')
|
|
21
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
22
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
23
|
+
.addHelpText(
|
|
24
|
+
'after',
|
|
25
|
+
[
|
|
26
|
+
'',
|
|
27
|
+
'Scope:',
|
|
28
|
+
' Tracked files plus untracked files git does not ignore, the same',
|
|
29
|
+
' corpus the citation check, the markdown corpus, and the secret',
|
|
30
|
+
' scan already read. A line total skips whatever reads as binary.',
|
|
31
|
+
'',
|
|
32
|
+
'Exit codes:',
|
|
33
|
+
' 0 the census completed',
|
|
34
|
+
' 1 refused, with the reason on stderr',
|
|
35
|
+
'',
|
|
36
|
+
'Examples:',
|
|
37
|
+
' aitk census',
|
|
38
|
+
' aitk census src --json',
|
|
39
|
+
'',
|
|
40
|
+
].join('\n'),
|
|
41
|
+
)
|
|
42
|
+
.action(async (path: string | undefined, opts: CensusCommandOptions) => {
|
|
43
|
+
process.exitCode = await runCensus(path, opts)
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function runCensus(
|
|
48
|
+
path: string | undefined,
|
|
49
|
+
opts: CensusCommandOptions,
|
|
50
|
+
): Promise<number> {
|
|
51
|
+
const root = resolve(path ?? process.cwd())
|
|
52
|
+
const emitJson = opts.json ?? false
|
|
53
|
+
|
|
54
|
+
intro('aitk census')
|
|
55
|
+
|
|
56
|
+
const result = await census(root)
|
|
57
|
+
|
|
58
|
+
if (result.kind === 'refused') {
|
|
59
|
+
logStep('Refused')
|
|
60
|
+
logWarn(REFUSALS[result.reason])
|
|
61
|
+
outro()
|
|
62
|
+
|
|
63
|
+
if (emitJson) {
|
|
64
|
+
process.stdout.write(
|
|
65
|
+
`${JSON.stringify({
|
|
66
|
+
root,
|
|
67
|
+
reason: result.reason,
|
|
68
|
+
message: REFUSALS[result.reason],
|
|
69
|
+
})}\n`,
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
return 1
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
logStep('Files')
|
|
76
|
+
logInfo(
|
|
77
|
+
`${plural(result.files, 'file')}, ${result.skipped} skipped as binary or unreadable`,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
logStep('By extension')
|
|
81
|
+
for (const entry of result.byExtension) {
|
|
82
|
+
logInfo(
|
|
83
|
+
`${entry.extension}: ${plural(entry.files, 'file')}, ${entry.lines} lines`,
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
logStep('Lines')
|
|
88
|
+
logInfo(`${result.lines} counted, skipping binary or unreadable files`)
|
|
89
|
+
|
|
90
|
+
outro()
|
|
91
|
+
|
|
92
|
+
if (emitJson) {
|
|
93
|
+
process.stdout.write(
|
|
94
|
+
`${JSON.stringify({
|
|
95
|
+
root,
|
|
96
|
+
files: result.files,
|
|
97
|
+
skipped: result.skipped,
|
|
98
|
+
lines: result.lines,
|
|
99
|
+
byExtension: result.byExtension,
|
|
100
|
+
})}\n`,
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return 0
|
|
105
|
+
}
|
package/src/commands/claude.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
REQUIREMENT_SECTIONS,
|
|
21
21
|
type SkillFinding,
|
|
22
22
|
type SkillsAudit,
|
|
23
|
+
type SkillsAuditRefusal,
|
|
23
24
|
} from '@/claude/skills-audit'
|
|
24
25
|
import {
|
|
25
26
|
type RoutingRefusal,
|
|
@@ -862,8 +863,11 @@ async function runSkillsAudit(
|
|
|
862
863
|
|
|
863
864
|
if (report.corpora.length === 0) {
|
|
864
865
|
return refuseAudit(
|
|
866
|
+
'no-corpus',
|
|
865
867
|
`No skill corpus under ${root}. Looked for ${CORPORA.join(' and ')}.`,
|
|
866
868
|
gateOnly,
|
|
869
|
+
root,
|
|
870
|
+
opts.json ?? false,
|
|
867
871
|
)
|
|
868
872
|
}
|
|
869
873
|
|
|
@@ -910,16 +914,26 @@ async function runSkillsAudit(
|
|
|
910
914
|
return auditExitCode(report)
|
|
911
915
|
}
|
|
912
916
|
|
|
913
|
-
function refuseAudit(
|
|
917
|
+
function refuseAudit(
|
|
918
|
+
reason: SkillsAuditRefusal,
|
|
919
|
+
message: string,
|
|
920
|
+
gateOnly: boolean,
|
|
921
|
+
root: string,
|
|
922
|
+
emitJson: boolean,
|
|
923
|
+
): number {
|
|
914
924
|
if (gateOnly) {
|
|
915
925
|
frameError(message)
|
|
916
|
-
|
|
926
|
+
} else {
|
|
927
|
+
intro('aitk claude skills audit')
|
|
928
|
+
logStep('Refused')
|
|
929
|
+
logWarn(message)
|
|
930
|
+
outro()
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
if (emitJson) {
|
|
934
|
+
process.stdout.write(`${JSON.stringify({ root, reason, message })}\n`)
|
|
917
935
|
}
|
|
918
936
|
|
|
919
|
-
intro('aitk claude skills audit')
|
|
920
|
-
logStep('Refused')
|
|
921
|
-
logWarn(message)
|
|
922
|
-
outro()
|
|
923
937
|
return 1
|
|
924
938
|
}
|
|
925
939
|
|
package/src/commands/comments.ts
CHANGED
package/src/commands/context.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolve } from 'node:path'
|
|
2
2
|
import type { Command } from 'commander'
|
|
3
3
|
import {
|
|
4
|
+
type ContextAuditRefusal,
|
|
4
5
|
type EntryReport,
|
|
5
6
|
governsContent,
|
|
6
7
|
type LengthCause,
|
|
@@ -157,12 +158,17 @@ async function runAudit(
|
|
|
157
158
|
// short a required section, which is the pass a gate exists to prevent.
|
|
158
159
|
if (gateOnly && widened) {
|
|
159
160
|
return refuse(
|
|
161
|
+
'conflicting-options',
|
|
160
162
|
'--citations-only runs the citation check alone, so --gate would widen the gate to findings the run never measures. Pass one.',
|
|
161
163
|
gateOnly,
|
|
164
|
+
root,
|
|
165
|
+
opts.json ?? false,
|
|
162
166
|
)
|
|
163
167
|
}
|
|
164
168
|
|
|
165
|
-
if (typeof names === 'string')
|
|
169
|
+
if (typeof names === 'string') {
|
|
170
|
+
return refuse('bad-folder-list', names, gateOnly, root, opts.json ?? false)
|
|
171
|
+
}
|
|
166
172
|
|
|
167
173
|
// The root base is opt-in. A target carrying a root `wireframes/` would
|
|
168
174
|
// otherwise be audited against a standard it never adopted, on a bare run
|
|
@@ -173,8 +179,11 @@ async function runAudit(
|
|
|
173
179
|
})
|
|
174
180
|
if (folders.length === 0) {
|
|
175
181
|
return refuse(
|
|
182
|
+
'no-folders',
|
|
176
183
|
`No audited folder found ${named ? 'under .claude/ or the project root' : 'under .claude/'}. Looked for: ${names.join(', ')}.`,
|
|
177
184
|
gateOnly,
|
|
185
|
+
root,
|
|
186
|
+
opts.json ?? false,
|
|
178
187
|
)
|
|
179
188
|
}
|
|
180
189
|
|
|
@@ -190,16 +199,22 @@ async function runAudit(
|
|
|
190
199
|
const cited = presentNames(folders)
|
|
191
200
|
if (gateOnly && cited.length === 0) {
|
|
192
201
|
return refuse(
|
|
202
|
+
'no-citation-scope',
|
|
193
203
|
`The citation check spells the .claude/ prefix and no audited folder resolved there. Looked for: ${names.join(', ')}.`,
|
|
194
204
|
gateOnly,
|
|
205
|
+
root,
|
|
206
|
+
opts.json ?? false,
|
|
195
207
|
)
|
|
196
208
|
}
|
|
197
209
|
|
|
198
210
|
const citations = await auditCitations(root, cited)
|
|
199
211
|
if (citations.kind === 'unavailable') {
|
|
200
212
|
return refuse(
|
|
213
|
+
'no-git',
|
|
201
214
|
'git could not list the tree, so no citation was checked. Run inside a git repository.',
|
|
202
215
|
gateOnly,
|
|
216
|
+
root,
|
|
217
|
+
opts.json ?? false,
|
|
203
218
|
)
|
|
204
219
|
}
|
|
205
220
|
|
|
@@ -308,16 +323,26 @@ async function runAudit(
|
|
|
308
323
|
return gating ? EXIT_GATE : 0
|
|
309
324
|
}
|
|
310
325
|
|
|
311
|
-
function refuse(
|
|
326
|
+
function refuse(
|
|
327
|
+
reason: ContextAuditRefusal,
|
|
328
|
+
message: string,
|
|
329
|
+
gateOnly: boolean,
|
|
330
|
+
root: string,
|
|
331
|
+
emitJson: boolean,
|
|
332
|
+
): number {
|
|
312
333
|
if (gateOnly) {
|
|
313
334
|
frameError(message)
|
|
314
|
-
|
|
335
|
+
} else {
|
|
336
|
+
intro('aitk context audit')
|
|
337
|
+
logStep('Refused')
|
|
338
|
+
logWarn(message)
|
|
339
|
+
outro()
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (emitJson) {
|
|
343
|
+
process.stdout.write(`${JSON.stringify({ root, reason, message })}\n`)
|
|
315
344
|
}
|
|
316
345
|
|
|
317
|
-
intro('aitk context audit')
|
|
318
|
-
logStep('Refused')
|
|
319
|
-
logWarn(message)
|
|
320
|
-
outro()
|
|
321
346
|
return 1
|
|
322
347
|
}
|
|
323
348
|
|
package/src/commands/gov.ts
CHANGED
|
@@ -506,12 +506,12 @@ function runTestOrder(opts: TestOrderOptions): number {
|
|
|
506
506
|
if (report.kind === 'unreadable') {
|
|
507
507
|
intro('aitk gov test-order')
|
|
508
508
|
logStep('Refused')
|
|
509
|
-
logError(report.
|
|
509
|
+
logError(report.message)
|
|
510
510
|
outro()
|
|
511
511
|
|
|
512
512
|
if (emitJson) {
|
|
513
513
|
process.stdout.write(
|
|
514
|
-
`${JSON.stringify({
|
|
514
|
+
`${JSON.stringify({ root, reason: report.reason, message: report.message })}\n`,
|
|
515
515
|
)
|
|
516
516
|
}
|
|
517
517
|
|
package/src/commands/markdown.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises'
|
|
|
2
2
|
import { resolve } from 'node:path'
|
|
3
3
|
import type { Command } from 'commander'
|
|
4
4
|
import { BAN_SETS, emptyBanSets } from '@/markdown/bans'
|
|
5
|
-
import { resolveMarkdown } from '@/markdown/files'
|
|
5
|
+
import { type MarkdownAuditRefusal, resolveMarkdown } from '@/markdown/files'
|
|
6
6
|
import { isGating } from '@/markdown/gate'
|
|
7
7
|
import {
|
|
8
8
|
type BanFinding,
|
|
@@ -119,15 +119,21 @@ async function runAudit(
|
|
|
119
119
|
|
|
120
120
|
if (scope.kind === 'unavailable') {
|
|
121
121
|
return refuse(
|
|
122
|
+
'no-git',
|
|
122
123
|
'git could not list the tree, so no corpus was built. Run inside a git repository.',
|
|
124
|
+
root,
|
|
125
|
+
opts.json ?? false,
|
|
123
126
|
)
|
|
124
127
|
}
|
|
125
128
|
|
|
126
129
|
if (scope.files.length === 0) {
|
|
127
130
|
return refuse(
|
|
131
|
+
paths.length === 0 ? 'no-markdown' : 'no-match',
|
|
128
132
|
paths.length === 0
|
|
129
133
|
? 'No markdown file in the tree.'
|
|
130
134
|
: `No markdown file matched: ${scope.unmatched.join(', ')}`,
|
|
135
|
+
root,
|
|
136
|
+
opts.json ?? false,
|
|
131
137
|
)
|
|
132
138
|
}
|
|
133
139
|
|
|
@@ -203,11 +209,21 @@ async function runAudit(
|
|
|
203
209
|
return gating ? EXIT_GATE : 0
|
|
204
210
|
}
|
|
205
211
|
|
|
206
|
-
function refuse(
|
|
212
|
+
function refuse(
|
|
213
|
+
reason: MarkdownAuditRefusal,
|
|
214
|
+
message: string,
|
|
215
|
+
root: string,
|
|
216
|
+
emitJson: boolean,
|
|
217
|
+
): number {
|
|
207
218
|
intro('aitk markdown audit')
|
|
208
219
|
logStep('Refused')
|
|
209
220
|
logWarn(message)
|
|
210
221
|
outro()
|
|
222
|
+
|
|
223
|
+
if (emitJson) {
|
|
224
|
+
process.stdout.write(`${JSON.stringify({ root, reason, message })}\n`)
|
|
225
|
+
}
|
|
226
|
+
|
|
211
227
|
return EXIT_REFUSED
|
|
212
228
|
}
|
|
213
229
|
|
package/src/context/audit.ts
CHANGED
|
@@ -15,6 +15,22 @@ import { isStubSeed } from '@/seed-marker'
|
|
|
15
15
|
* any markdown file, so they are stated at the attribute tier and measured by
|
|
16
16
|
* `aitk markdown audit` rather than here.
|
|
17
17
|
*/
|
|
18
|
+
/**
|
|
19
|
+
* Every reason `aitk context audit` refuses for.
|
|
20
|
+
*
|
|
21
|
+
* `no-folders` is the one ordinary absence: a project that never adopted
|
|
22
|
+
* `.claude/context/`, `.claude/diagrams/`, or `.claude/wireframes/` names no
|
|
23
|
+
* corpus this audit can measure, the same state `no-skills` reads for the
|
|
24
|
+
* skill corpora. The other four are a malformed invocation or a checkout git
|
|
25
|
+
* cannot read, which stay a break rather than an absence.
|
|
26
|
+
*/
|
|
27
|
+
export type ContextAuditRefusal =
|
|
28
|
+
| 'conflicting-options'
|
|
29
|
+
| 'bad-folder-list'
|
|
30
|
+
| 'no-folders'
|
|
31
|
+
| 'no-citation-scope'
|
|
32
|
+
| 'no-git'
|
|
33
|
+
|
|
18
34
|
export const LENGTH_CHECKPOINT = 150
|
|
19
35
|
|
|
20
36
|
/**
|
package/src/gov/test-order.ts
CHANGED
|
@@ -44,6 +44,21 @@ export interface Scope {
|
|
|
44
44
|
readonly testSuffixes: readonly string[]
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Every reason `aitk gov test-order` refuses for.
|
|
49
|
+
*
|
|
50
|
+
* None is an ordinary absence. A depth-1 checkout falls back to the root
|
|
51
|
+
* commit and reports zero rather than reaching any of these, so what remains
|
|
52
|
+
* is a repository with no commit at all or a git operation that failed
|
|
53
|
+
* outright, both a broken checkout rather than a target's ordinary state.
|
|
54
|
+
*/
|
|
55
|
+
export type TestOrderRefusal =
|
|
56
|
+
| 'no-history'
|
|
57
|
+
| 'no-log'
|
|
58
|
+
| 'no-tree'
|
|
59
|
+
| 'bad-base'
|
|
60
|
+
| 'no-base'
|
|
61
|
+
|
|
47
62
|
export type TestOrderReport =
|
|
48
63
|
| {
|
|
49
64
|
readonly kind: 'measured'
|
|
@@ -56,7 +71,11 @@ export type TestOrderReport =
|
|
|
56
71
|
/** Changed paths outside the pairing's reach, named rather than counted. */
|
|
57
72
|
readonly ignored: readonly string[]
|
|
58
73
|
}
|
|
59
|
-
| {
|
|
74
|
+
| {
|
|
75
|
+
readonly kind: 'unreadable'
|
|
76
|
+
readonly reason: TestOrderRefusal
|
|
77
|
+
readonly message: string
|
|
78
|
+
}
|
|
60
79
|
|
|
61
80
|
export interface TestOrderOptions {
|
|
62
81
|
/** The far side of the range, defaulting to the merge base against the trunk. */
|
|
@@ -258,7 +277,8 @@ export function readTestOrder(
|
|
|
258
277
|
if (head === undefined) {
|
|
259
278
|
return {
|
|
260
279
|
kind: 'unreadable',
|
|
261
|
-
reason:
|
|
280
|
+
reason: 'no-history',
|
|
281
|
+
message: `No git history under ${root}. History is the only surface carrying the ordering, so there is nothing to read.`,
|
|
262
282
|
}
|
|
263
283
|
}
|
|
264
284
|
|
|
@@ -277,7 +297,8 @@ export function readTestOrder(
|
|
|
277
297
|
if (log === undefined) {
|
|
278
298
|
return {
|
|
279
299
|
kind: 'unreadable',
|
|
280
|
-
reason:
|
|
300
|
+
reason: 'no-log',
|
|
301
|
+
message: `Reading history between ${base} and HEAD failed under ${root}.`,
|
|
281
302
|
}
|
|
282
303
|
}
|
|
283
304
|
|
|
@@ -285,7 +306,8 @@ export function readTestOrder(
|
|
|
285
306
|
if (tree === undefined) {
|
|
286
307
|
return {
|
|
287
308
|
kind: 'unreadable',
|
|
288
|
-
reason:
|
|
309
|
+
reason: 'no-tree',
|
|
310
|
+
message: `Reading the tree at ${base} failed under ${root}. Without it a test written before the range reads as absent.`,
|
|
289
311
|
}
|
|
290
312
|
}
|
|
291
313
|
|
|
@@ -323,13 +345,14 @@ function resolveBase(
|
|
|
323
345
|
root: string,
|
|
324
346
|
ref: string | undefined,
|
|
325
347
|
head: string,
|
|
326
|
-
): string | { kind: 'unreadable'; reason: string } {
|
|
348
|
+
): string | { kind: 'unreadable'; reason: TestOrderRefusal; message: string } {
|
|
327
349
|
if (ref !== undefined) {
|
|
328
350
|
const resolved = revParse(root, ref)
|
|
329
351
|
if (resolved === undefined) {
|
|
330
352
|
return {
|
|
331
353
|
kind: 'unreadable',
|
|
332
|
-
reason:
|
|
354
|
+
reason: 'bad-base',
|
|
355
|
+
message: `Ref ${ref} resolves to no commit in ${root}. Pass a commit this tree carries.`,
|
|
333
356
|
}
|
|
334
357
|
}
|
|
335
358
|
return resolved
|
|
@@ -345,7 +368,8 @@ function resolveBase(
|
|
|
345
368
|
if (rootCommits === undefined || rootCommits === '') {
|
|
346
369
|
return {
|
|
347
370
|
kind: 'unreadable',
|
|
348
|
-
reason:
|
|
371
|
+
reason: 'no-base',
|
|
372
|
+
message: `No base resolves against ${root}. Fetch origin or pass --base.`,
|
|
349
373
|
}
|
|
350
374
|
}
|
|
351
375
|
|