@erclx/aitk 0.22.1 → 0.23.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-standards-audit/SKILL.md +3 -3
- package/claude/skills/git-branch/references/branch.md +1 -1
- package/claude/skills/git-pr/references/branch.md +1 -1
- package/claude/skills/git-pr/references/pr.md +1 -1
- package/claude/skills/git-split/references/branch.md +1 -1
- package/claude/skills/git-split/references/pr.md +1 -1
- package/docs/agents.md +10 -5
- package/package.json +1 -1
- package/src/commands/sandbox.ts +99 -5
- package/src/sandbox/census.ts +228 -0
- package/standards/bundled/branch.md +1 -1
- package/standards/bundled/pr.md +1 -1
- package/claude/skills/claude-standards-audit/references/branch.md +0 -49
- package/claude/skills/claude-standards-audit/references/pr.md +0 -124
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: claude-standards-audit
|
|
3
|
-
description: Audits changed markdown files against applicable authoring standards (prose, skill, readme
|
|
3
|
+
description: Audits changed markdown files against applicable authoring standards (prose, skill, readme) and reports violations without fixing. Maps each file to its standards, greps for banned tokens, and groups findings by file. Use when asked to "audit prose", "audit standards", "check standards", "standards audit", or after editing markdown where standards compliance matters. Do NOT fix violations. Reporting only.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Claude standards audit
|
|
@@ -44,11 +44,11 @@ For each changed markdown file, pick the applicable standards:
|
|
|
44
44
|
- Any markdown with prose: `.claude/standards/prose.md`
|
|
45
45
|
- `SKILL.md` under `.claude/skills/` or `claude/skills/`: also `.claude/standards/skill.md`
|
|
46
46
|
- `README.md` at any level: also `.claude/standards/readme.md`
|
|
47
|
-
- Branch names proposed in the session: `${CLAUDE_SKILL_DIR}/references/branch.md`
|
|
48
|
-
- PR titles or bodies drafted in the session: `${CLAUDE_SKILL_DIR}/references/pr.md`
|
|
49
47
|
|
|
50
48
|
Read a standard from `${CLAUDE_SKILL_DIR}/../../standards/` instead when the project does not have it.
|
|
51
49
|
|
|
50
|
+
Every mapping names a changed markdown file, which is the only thing Step 1 produces. Text that never lands in the tree, such as a branch name or a pull request body, is checked by the skill that publishes it rather than here.
|
|
51
|
+
|
|
52
52
|
## Step 3: read standards and audit
|
|
53
53
|
|
|
54
54
|
Read each applicable standard once. For each changed file, audit against every rule:
|
package/docs/agents.md
CHANGED
|
@@ -70,7 +70,7 @@ Full help: `aitk <command> --help`.
|
|
|
70
70
|
| `aitk sandbox reset` | Reset sandbox to baseline |
|
|
71
71
|
| `aitk sandbox clean` | Wipe the sandbox |
|
|
72
72
|
| `aitk sandbox check` | Score a provisioned sandbox against a scenario expectation (`--json` for the verdict) |
|
|
73
|
-
| `aitk sandbox coverage` | Report which scenarios declare expectations (`--json`, `--strict`)
|
|
73
|
+
| `aitk sandbox coverage` | Report which scenarios declare expectations (`--json`, `--strict`, `--skills`) |
|
|
74
74
|
| `aitk indexes regen` | Regenerate `index.md` files from sibling frontmatter |
|
|
75
75
|
| `aitk docs [topic]` | Emit toolkit reference docs (`list`, or a topic by name) |
|
|
76
76
|
| `aitk design render` | Render `.claude/DESIGN.md` tokens to HTML and CSS |
|
|
@@ -228,13 +228,18 @@ Exit 0 means `pass` or `unchecked`. Exit 1 means `fail`, or a caller error: a ma
|
|
|
228
228
|
aitk sandbox coverage --json
|
|
229
229
|
```
|
|
230
230
|
|
|
231
|
-
| Flag | Effect
|
|
232
|
-
| ---------- |
|
|
233
|
-
| `--json` | Emit the coverage record on stdout
|
|
234
|
-
| `--strict` | Exit 1 while any scenario declares no expectation
|
|
231
|
+
| Flag | Effect |
|
|
232
|
+
| ---------- | ----------------------------------------------------------- |
|
|
233
|
+
| `--json` | Emit the coverage record on stdout |
|
|
234
|
+
| `--strict` | Exit 1 while any scenario declares no expectation |
|
|
235
|
+
| `--skills` | Add a per-skill asserted, should-be-asserted, exempt census |
|
|
235
236
|
|
|
236
237
|
The record carries every scenario with the arms that declare, plus `totalScenarios`, `armedScenarios`, and `armedArms`. Scenarios and arms count separately, since several arms can share one scenario and dividing one by the other overstates the rollout.
|
|
237
238
|
|
|
239
|
+
`--skills` answers what the scenario count cannot, which is whether anything can fail a given skill. It adds `skills`, `totalSkills`, `asserted`, `shouldBeAsserted`, `exempt`, `staleExemptions`, and `supersededExemptions` to the record, and keeps the scenario view rather than replacing it. The two denominators disagree on purpose: an armed scenario under `infra/` or `tooling/` exercises a CLI domain and pairs with no skill at all.
|
|
240
|
+
|
|
241
|
+
A skill pairs to a scenario by filename, `<category>-<command>` first and bare `<command>` second, so `claude/setup-init.sh` reaches the `setup-init` skill. `should-be-asserted` is the default and is the work queue for writing arms. `exempt` means no arm should be written and holds only with a reason, declared in `scripts/sandbox/exempt.toml` and limited to a harness limit the checker cannot reach past or a skill that writes no artifact. An armed arm outranks an exemption. An exemption naming no shipped skill, or naming one an arm now asserts, exits 1 without `--strict`. Each armed arm reports as `<category>:<command>/<arm>`, so two same-named arms under different scenarios stay distinct.
|
|
242
|
+
|
|
238
243
|
`scripts/sandbox/run.sh` calls this after a headless run and merges the verdict into the envelope it prints. It also writes that merged record to `.claude/.tmp/sandbox-runs/<target>-<arm>-<timestamp>.json` with a `writes` array appended, and logs the path on stderr. Both fields are what a later re-score needs, since `--envelope` and `--writes` read files the run deletes on exit. Stdout carries the same bytes it did before the record existed.
|
|
239
244
|
|
|
240
245
|
## Docs
|
package/package.json
CHANGED
package/src/commands/sandbox.ts
CHANGED
|
@@ -3,6 +3,11 @@ import { homedir } from 'node:os'
|
|
|
3
3
|
import { join } from 'node:path'
|
|
4
4
|
import type { Command } from 'commander'
|
|
5
5
|
import { PROJECT_ROOT, execScript } from '@/exec'
|
|
6
|
+
import {
|
|
7
|
+
assertedPercent,
|
|
8
|
+
collectCensus,
|
|
9
|
+
type CensusReport,
|
|
10
|
+
} from '@/sandbox/census'
|
|
6
11
|
import {
|
|
7
12
|
collectCoverage,
|
|
8
13
|
coveragePercent,
|
|
@@ -100,6 +105,7 @@ interface CheckOptions {
|
|
|
100
105
|
interface CoverageOptions {
|
|
101
106
|
readonly json?: boolean
|
|
102
107
|
readonly strict?: boolean
|
|
108
|
+
readonly skills?: boolean
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
function getCategories(): string[] {
|
|
@@ -245,20 +251,105 @@ function reportCoverage(report: CoverageReport): void {
|
|
|
245
251
|
else logInfo(summary)
|
|
246
252
|
}
|
|
247
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Names the skills nothing can fail, which is the work queue the arm batches
|
|
256
|
+
* consume. Exempt entries print their reason, since an exemption with no reason
|
|
257
|
+
* beside it is indistinguishable from a skill nobody has got to yet.
|
|
258
|
+
*/
|
|
259
|
+
function reportCensus(report: CensusReport): void {
|
|
260
|
+
const shouldBe = report.skills.filter(
|
|
261
|
+
(s) => s.verdict === 'should-be-asserted',
|
|
262
|
+
)
|
|
263
|
+
const exempt = report.skills.filter((s) => s.verdict === 'exempt')
|
|
264
|
+
|
|
265
|
+
if (shouldBe.length > 0) {
|
|
266
|
+
logStep('No arm can fail these')
|
|
267
|
+
for (const entry of shouldBe)
|
|
268
|
+
logWarn(
|
|
269
|
+
`${entry.skill} (${entry.scenarios.length === 0 ? 'no scenario' : entry.scenarios.join(', ')})`,
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (exempt.length > 0) {
|
|
274
|
+
logStep('Exempt')
|
|
275
|
+
for (const entry of exempt) logInfo(`${entry.skill}: ${entry.reason}`)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// A wrong exemption is the one way this report can overstate itself, so both
|
|
279
|
+
// kinds print as errors even on a run that is otherwise clean. They separate
|
|
280
|
+
// because the reader checks a different thing: whether the skill left the
|
|
281
|
+
// tree, or whether an arm landed and the entry outlived its reason.
|
|
282
|
+
if (report.staleExemptions.length > 0) {
|
|
283
|
+
logStep('Exemptions naming no shipped skill')
|
|
284
|
+
for (const skill of report.staleExemptions) logError(skill)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (report.supersededExemptions.length > 0) {
|
|
288
|
+
logStep('Exemptions an arm now asserts')
|
|
289
|
+
for (const skill of report.supersededExemptions)
|
|
290
|
+
logError(`${skill}, delete the entry`)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
logStep('Skills')
|
|
294
|
+
const summary =
|
|
295
|
+
`${report.asserted}/${report.totalSkills} skills asserted ` +
|
|
296
|
+
`(${assertedPercent(report)}%), ${report.shouldBeAsserted} should be, ` +
|
|
297
|
+
`${report.exempt} exempt`
|
|
298
|
+
if (report.shouldBeAsserted > 0) logWarn(summary)
|
|
299
|
+
else logInfo(summary)
|
|
300
|
+
}
|
|
301
|
+
|
|
248
302
|
function runCoverage(options: CoverageOptions): void {
|
|
249
303
|
if (reportAbsentScenarioTree()) return
|
|
250
304
|
|
|
251
305
|
intro('aitk sandbox coverage')
|
|
252
306
|
|
|
253
307
|
const report = collectCoverage(PROJECT_ROOT)
|
|
308
|
+
|
|
309
|
+
// A broken `exempt.toml` reads as a caller error, not as a crash. The parser
|
|
310
|
+
// throws rather than returning a smaller set, since a dropped exemption is
|
|
311
|
+
// invisible in the counts, and the conversion belongs here for the reason
|
|
312
|
+
// `resolveVerdict` catches its own parse: a typo in a declaration should read
|
|
313
|
+
// the way a pattern that does not compile does rather than as a stack trace.
|
|
314
|
+
let census: CensusReport | undefined
|
|
315
|
+
if (options.skills === true) {
|
|
316
|
+
try {
|
|
317
|
+
census = collectCensus(PROJECT_ROOT, report)
|
|
318
|
+
} catch (error) {
|
|
319
|
+
const reason = error instanceof Error ? error.message : String(error)
|
|
320
|
+
reportCoverage(report)
|
|
321
|
+
logStep('Skills')
|
|
322
|
+
logError(`Cannot read the exemptions: ${reason}`)
|
|
323
|
+
outro()
|
|
324
|
+
process.exitCode = 1
|
|
325
|
+
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// The scenario view stays whichever way `--skills` is set. The two count
|
|
331
|
+
// different denominators, and replacing one with the other loses the scenario
|
|
332
|
+
// rollout the strict gate is written against.
|
|
254
333
|
reportCoverage(report)
|
|
255
|
-
if (
|
|
334
|
+
if (census !== undefined) reportCensus(census)
|
|
335
|
+
|
|
336
|
+
if (options.json === true)
|
|
337
|
+
process.stdout.write(
|
|
338
|
+
`${JSON.stringify(census === undefined ? report : { ...report, ...census })}\n`,
|
|
339
|
+
)
|
|
256
340
|
|
|
257
341
|
outro()
|
|
258
|
-
|
|
342
|
+
|
|
343
|
+
// A wrong exemption exits non-zero without `--strict`. It is a wrong claim in
|
|
344
|
+
// committed data rather than a rollout still in progress, and the whole point
|
|
345
|
+
// of the verdict is that an exemption someone can no longer check is worse
|
|
346
|
+
// than no exemption at all.
|
|
347
|
+
const wrongExemption =
|
|
348
|
+
(census?.staleExemptions.length ?? 0) > 0 ||
|
|
349
|
+
(census?.supersededExemptions.length ?? 0) > 0
|
|
350
|
+
const rolloutIncomplete =
|
|
259
351
|
options.strict === true && report.armedScenarios < report.totalScenarios
|
|
260
|
-
|
|
261
|
-
: 0
|
|
352
|
+
process.exitCode = wrongExemption || rolloutIncomplete ? 1 : 0
|
|
262
353
|
}
|
|
263
354
|
|
|
264
355
|
function runCheck(
|
|
@@ -361,6 +452,7 @@ export function register(program: Command): void {
|
|
|
361
452
|
.helpOption('-h, --help', 'Show this help message')
|
|
362
453
|
.option('--json', 'Emit the report as JSON on stdout')
|
|
363
454
|
.option('--strict', 'Exit non-zero while any scenario declares nothing')
|
|
455
|
+
.option('--skills', 'Add a per-skill asserted, should-be, or exempt census')
|
|
364
456
|
.addHelpText(
|
|
365
457
|
'after',
|
|
366
458
|
[
|
|
@@ -368,8 +460,10 @@ export function register(program: Command): void {
|
|
|
368
460
|
'Examples:',
|
|
369
461
|
' aitk sandbox coverage',
|
|
370
462
|
' aitk sandbox coverage --json',
|
|
463
|
+
' aitk sandbox coverage --skills',
|
|
371
464
|
'',
|
|
372
|
-
'Exit codes: 0, unless --strict and a scenario declares nothing
|
|
465
|
+
'Exit codes: 0, unless --strict and a scenario declares nothing, or',
|
|
466
|
+
'--skills and an exemption names no shipped skill or one an arm asserts.',
|
|
373
467
|
'Where the scenario tree does not ship, exits 1 without a report.',
|
|
374
468
|
].join('\n'),
|
|
375
469
|
)
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { collectCoverage, type CoverageReport } from '@/sandbox/coverage'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Holds the exemption declarations, beside the scenario categories rather than
|
|
7
|
+
* inside one. `listScenarios` in `coverage.ts` walks directories only, so a file
|
|
8
|
+
* at that level joins no category and never reads as a scenario.
|
|
9
|
+
*/
|
|
10
|
+
const EXEMPT_FILE = 'exempt.toml'
|
|
11
|
+
|
|
12
|
+
export type SkillVerdict = 'asserted' | 'should-be-asserted' | 'exempt'
|
|
13
|
+
|
|
14
|
+
export interface SkillCensusEntry {
|
|
15
|
+
readonly skill: string
|
|
16
|
+
readonly verdict: SkillVerdict
|
|
17
|
+
/**
|
|
18
|
+
* Every `<category>:<command>` pairing to this skill, empty when none does.
|
|
19
|
+
* Plural because the mapping is many-to-one: two scenarios can drive one
|
|
20
|
+
* skill, and naming only the first would credit its arms to the wrong file.
|
|
21
|
+
*/
|
|
22
|
+
readonly scenarios: readonly string[]
|
|
23
|
+
/**
|
|
24
|
+
* Each armed arm as `<category>:<command>/<arm>`. Qualified rather than bare,
|
|
25
|
+
* because a skill two scenarios drive can hold two arms of the same name and
|
|
26
|
+
* a bare list renders them as one label twice. Deduplicating instead would
|
|
27
|
+
* read as one arm where two assert, which understates in the one direction
|
|
28
|
+
* this report exists to keep honest.
|
|
29
|
+
*/
|
|
30
|
+
readonly armed: readonly string[]
|
|
31
|
+
/** Why no arm asserts this skill. Present only on `exempt`. */
|
|
32
|
+
readonly reason?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CensusReport {
|
|
36
|
+
readonly skills: readonly SkillCensusEntry[]
|
|
37
|
+
readonly totalSkills: number
|
|
38
|
+
readonly asserted: number
|
|
39
|
+
readonly shouldBeAsserted: number
|
|
40
|
+
readonly exempt: number
|
|
41
|
+
/** Exemptions naming a skill the tree does not carry. */
|
|
42
|
+
readonly staleExemptions: readonly string[]
|
|
43
|
+
/**
|
|
44
|
+
* Exemptions on a skill an arm now asserts. The claim is wrong in the one
|
|
45
|
+
* direction a verdict decays, and the entry outranks nothing, so dropping it
|
|
46
|
+
* silently leaves committed data nobody is told to delete.
|
|
47
|
+
*/
|
|
48
|
+
readonly supersededExemptions: readonly string[]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function directories(path: string): string[] {
|
|
52
|
+
if (!existsSync(path)) return []
|
|
53
|
+
|
|
54
|
+
return readdirSync(path, { withFileTypes: true })
|
|
55
|
+
.filter((entry) => entry.isDirectory())
|
|
56
|
+
.map((entry) => entry.name)
|
|
57
|
+
.sort()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Every skill the plugin ships, which is the census denominator. Reads the
|
|
62
|
+
* authoring root rather than `.claude/skills/`, since the latter holds
|
|
63
|
+
* toolkit-internal skills that reach no target and would inflate the count.
|
|
64
|
+
*/
|
|
65
|
+
export function listSkills(root: string): string[] {
|
|
66
|
+
return directories(join(root, 'claude', 'skills'))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Maps a scenario to the skill it drives, trying two spellings in order.
|
|
71
|
+
*
|
|
72
|
+
* `<category>-<command>` is the rule `.claude/context/sandbox.md` states, and it
|
|
73
|
+
* alone pairs 29 of 55 skills. The bare `<command>` fallback is what reaches the
|
|
74
|
+
* rest: `claude/setup-init.sh` drives the `setup-init` skill, not a
|
|
75
|
+
* `claude-setup-init` that does not exist. Stating one spelling and shipping two
|
|
76
|
+
* is what let the audit report a paired skill as unpaired.
|
|
77
|
+
*
|
|
78
|
+
* Returns undefined for a scenario driving no skill at all, which is every
|
|
79
|
+
* `infra/` and `tooling/` scenario. Those exercise a CLI domain rather than a
|
|
80
|
+
* skill and belong to the scenario count, not to this one.
|
|
81
|
+
*/
|
|
82
|
+
export function skillForScenario(
|
|
83
|
+
category: string,
|
|
84
|
+
command: string,
|
|
85
|
+
skills: ReadonlySet<string>,
|
|
86
|
+
): string | undefined {
|
|
87
|
+
const prefixed = `${category}-${command}`
|
|
88
|
+
if (skills.has(prefixed)) return prefixed
|
|
89
|
+
if (skills.has(command)) return command
|
|
90
|
+
|
|
91
|
+
return undefined
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Reads the exemption declarations, keyed by skill with a `reason` each.
|
|
96
|
+
*
|
|
97
|
+
* The reason cannot live in the arm's `manual` array, which is where `#723` put
|
|
98
|
+
* prose a checker could not assert. `resolveVerdict` fails any declaration
|
|
99
|
+
* carrying zero mechanical assertions, so an `expect.toml` holding only an
|
|
100
|
+
* exempt reason goes red at the moment it is written. An exempt skill has no
|
|
101
|
+
* assertion to pair the prose with, which is what makes it exempt, so the two
|
|
102
|
+
* cases cannot share a home.
|
|
103
|
+
*
|
|
104
|
+
* Throws on a malformed file and on a table carrying no usable `reason`, rather
|
|
105
|
+
* than reporting a smaller set. Both losses are invisible downstream: the skill
|
|
106
|
+
* reclassifies to should-be-asserted and rejoins a work queue someone already
|
|
107
|
+
* ruled it out of, with nothing naming the entry that stopped counting. This is
|
|
108
|
+
* the stray-key rule `contentArray` applies in `expect.ts` for the same reason,
|
|
109
|
+
* where the declaration is well-formed and silently lost a key it appears to
|
|
110
|
+
* carry. `runCoverage` catches both and frames them.
|
|
111
|
+
*/
|
|
112
|
+
export function parseExemptions(source: string): Map<string, string> {
|
|
113
|
+
const parsed = Bun.TOML.parse(source) as Record<string, unknown>
|
|
114
|
+
const exemptions = new Map<string, string>()
|
|
115
|
+
|
|
116
|
+
for (const [skill, value] of Object.entries(parsed)) {
|
|
117
|
+
if (typeof value !== 'object' || value === null) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`exemption ${skill} is not a table. Write [${skill}] with a reason below it.`,
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const reason = (value as Record<string, unknown>).reason
|
|
124
|
+
if (typeof reason !== 'string' || reason === '') {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`exemption ${skill} declares no reason. An exemption without one cannot be checked or overturned.`,
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
exemptions.set(skill, reason)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return exemptions
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function readExemptions(root: string): Map<string, string> {
|
|
137
|
+
const path = join(root, 'scripts', 'sandbox', EXEMPT_FILE)
|
|
138
|
+
if (!existsSync(path)) return new Map()
|
|
139
|
+
|
|
140
|
+
return parseExemptions(readFileSync(path, 'utf8'))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Gives every shipped skill an asserted, should-be-asserted, or exempt verdict
|
|
145
|
+
* against declared expectations rather than against a scenario existing.
|
|
146
|
+
*
|
|
147
|
+
* A paired scenario is not an asserted skill. A scenario with no `expect.toml`
|
|
148
|
+
* provisions a state and prints a human-readable `Expect:` line, so a run over
|
|
149
|
+
* it exits zero having proved nothing. Counting pairs would report coverage the
|
|
150
|
+
* suite does not have, which is the measure this report exists to replace.
|
|
151
|
+
*
|
|
152
|
+
* An armed arm outranks an exemption. A skill listed exempt that acquired an arm
|
|
153
|
+
* is asserted in fact, and reporting the stale claim instead would hide the one
|
|
154
|
+
* direction the verdict decays in.
|
|
155
|
+
*/
|
|
156
|
+
export function collectCensus(
|
|
157
|
+
root: string,
|
|
158
|
+
coverage: CoverageReport = collectCoverage(root),
|
|
159
|
+
): CensusReport {
|
|
160
|
+
const skills = listSkills(root)
|
|
161
|
+
const known = new Set(skills)
|
|
162
|
+
const exemptions = readExemptions(root)
|
|
163
|
+
|
|
164
|
+
const pairings = new Map<string, { scenarios: string[]; armed: string[] }>()
|
|
165
|
+
for (const scenario of coverage.scenarios) {
|
|
166
|
+
const skill = skillForScenario(scenario.category, scenario.command, known)
|
|
167
|
+
if (skill === undefined) continue
|
|
168
|
+
|
|
169
|
+
// Accumulate rather than assign. Dropping a second scenario would report a
|
|
170
|
+
// skill unarmed while an arm under the other spelling asserts it.
|
|
171
|
+
const pair = `${scenario.category}:${scenario.command}`
|
|
172
|
+
const existing = pairings.get(skill) ?? { scenarios: [], armed: [] }
|
|
173
|
+
existing.scenarios.push(pair)
|
|
174
|
+
existing.armed.push(...scenario.armed.map((arm) => `${pair}/${arm}`))
|
|
175
|
+
pairings.set(skill, existing)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const entries = skills.map((skill): SkillCensusEntry => {
|
|
179
|
+
const pairing = pairings.get(skill)
|
|
180
|
+
const scenarios = pairing?.scenarios ?? []
|
|
181
|
+
const armed = pairing?.armed ?? []
|
|
182
|
+
const reason = exemptions.get(skill)
|
|
183
|
+
|
|
184
|
+
if (armed.length > 0) {
|
|
185
|
+
return { skill, verdict: 'asserted', scenarios, armed }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (reason !== undefined) {
|
|
189
|
+
return { skill, verdict: 'exempt', scenarios, armed, reason }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return { skill, verdict: 'should-be-asserted', scenarios, armed }
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
// An exemption outranked by an arm is as wrong as one naming no skill, and it
|
|
196
|
+
// is the case the armed-wins rule creates rather than one the tree arrives
|
|
197
|
+
// with. Reading `entries` rather than recomputing keeps the two in step, so a
|
|
198
|
+
// change to the precedence cannot leave the report contradicting the verdict.
|
|
199
|
+
const assertedSkills = new Set(
|
|
200
|
+
entries.filter((e) => e.verdict === 'asserted').map((e) => e.skill),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
skills: entries,
|
|
205
|
+
totalSkills: entries.length,
|
|
206
|
+
asserted: assertedSkills.size,
|
|
207
|
+
shouldBeAsserted: entries.filter((e) => e.verdict === 'should-be-asserted')
|
|
208
|
+
.length,
|
|
209
|
+
exempt: entries.filter((e) => e.verdict === 'exempt').length,
|
|
210
|
+
staleExemptions: [...exemptions.keys()]
|
|
211
|
+
.filter((skill) => !known.has(skill))
|
|
212
|
+
.sort(),
|
|
213
|
+
supersededExemptions: [...exemptions.keys()]
|
|
214
|
+
.filter((skill) => assertedSkills.has(skill))
|
|
215
|
+
.sort(),
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Whole percent, floored, over skills rather than scenarios. Floored for the
|
|
221
|
+
* reason `coveragePercent` is: the number exists to look worse than a wall of
|
|
222
|
+
* green does.
|
|
223
|
+
*/
|
|
224
|
+
export function assertedPercent(report: CensusReport): number {
|
|
225
|
+
if (report.totalSkills === 0) return 0
|
|
226
|
+
|
|
227
|
+
return Math.floor((report.asserted / report.totalSkills) * 100)
|
|
228
|
+
}
|
package/standards/bundled/pr.md
CHANGED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Branch reference
|
|
3
|
-
description: Branch naming format and type conventions
|
|
4
|
-
consumers: git-branch, git-split, git-pr, claude-standards-audit
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# Branch reference
|
|
8
|
-
|
|
9
|
-
## Format
|
|
10
|
-
|
|
11
|
-
- Structure: `<type>/<description>` or `<type>/<ticket>-<description>`
|
|
12
|
-
- Length: 50 characters maximum
|
|
13
|
-
- Casing: kebab-case only, no underscores or camelCase
|
|
14
|
-
- Description: 2 words maximum, 3 only when genuinely needed for specificity
|
|
15
|
-
- Capture the core change, not the commit message verbatim
|
|
16
|
-
- For branches with multiple commits, use the unifying concern as the description.
|
|
17
|
-
- Do not duplicate type in description (e.g., `feat/feature-login`)
|
|
18
|
-
|
|
19
|
-
## Types
|
|
20
|
-
|
|
21
|
-
- `feat`: new feature or capability
|
|
22
|
-
- `fix`: bug fix
|
|
23
|
-
- `refactor`: structural changes (not a fix or feature)
|
|
24
|
-
- `docs`: documentation only (README)
|
|
25
|
-
- `chore`: maintenance tasks (deps, tooling, configs)
|
|
26
|
-
- `perf`: performance improvements
|
|
27
|
-
- `test`: add or modify tests
|
|
28
|
-
- `style`: code formatting (whitespace, semicolons)
|
|
29
|
-
- `build`: build system changes (webpack, npm scripts)
|
|
30
|
-
- `ci`: CI/CD pipeline changes (GitHub Actions)
|
|
31
|
-
- `revert`: revert a previous commit
|
|
32
|
-
|
|
33
|
-
## Examples
|
|
34
|
-
|
|
35
|
-
### Correct
|
|
36
|
-
|
|
37
|
-
```plaintext
|
|
38
|
-
feat/jwt-expiration # clear feature scope
|
|
39
|
-
fix/AUTH-123-connection-pool # includes ticket ID
|
|
40
|
-
refactor/remove-deprecated-endpoints # clear refactor intent
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
### Incorrect
|
|
44
|
-
|
|
45
|
-
```plaintext
|
|
46
|
-
feature/auth_stuff # wrong type + underscore
|
|
47
|
-
feat/feature-add-login # duplicates type in description
|
|
48
|
-
fix/DB-456-fix-the-database-connection-pool-memory-leak # exceeds 50 chars + verbatim message
|
|
49
|
-
```
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Pull request reference
|
|
3
|
-
description: Pull request title and body conventions
|
|
4
|
-
consumers: git-split, git-pr, claude-standards-audit
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# Pull request reference
|
|
8
|
-
|
|
9
|
-
## Title
|
|
10
|
-
|
|
11
|
-
- Format: `<type>(<scope>): <subject>`
|
|
12
|
-
- Casing: lowercase for `<type>`, `<scope>`, and first word of `<subject>`
|
|
13
|
-
- Length: 72 characters maximum
|
|
14
|
-
|
|
15
|
-
## Content
|
|
16
|
-
|
|
17
|
-
- Use imperative mood for all content (`add`, `fix`, `refactor`)
|
|
18
|
-
- Do not start with "This PR," "This commit," "Included are," or "I have"
|
|
19
|
-
- Do not use buzzwords (`seamless`, `robust`, `game-changer`, `enhanced`)
|
|
20
|
-
- Do not describe historical behavior or unchanged code. Describe new behavior only.
|
|
21
|
-
- Do not include future promises or speculative documentation
|
|
22
|
-
- Do not explain obvious changes (formatting, renaming variables)
|
|
23
|
-
- Do not duplicate commit messages verbatim
|
|
24
|
-
|
|
25
|
-
## Sections
|
|
26
|
-
|
|
27
|
-
- `## Summary`: 1-2 sentences following `<Action Verb> <Direct Object> to <Result>`, expand for clarity if needed
|
|
28
|
-
- `## Key Changes`: name actual files, functions, or modules (e.g., `AuthService.verify()` not "auth handler"). Always use bullet points, never prose.
|
|
29
|
-
- `## Technical Context` (optional): 1-2 lines of architectural reasoning explaining why, not what
|
|
30
|
-
- Omit Technical Context for docs, config, or trivial changes
|
|
31
|
-
- Use bullet points for multiple reasons, one sentence for a single reason
|
|
32
|
-
- `## Testing` (optional): specify exact commands or test cases run
|
|
33
|
-
- Omit Testing for docs, config, or trivial sync changes
|
|
34
|
-
- Use checkboxes, never prose. See Testing discipline for which box gets ticked.
|
|
35
|
-
- `## For the reviewer` (optional): what the reviewer should confirm, one bullet per request
|
|
36
|
-
- Visuals: include only when they clarify architecture, UI, or complex logic flows
|
|
37
|
-
|
|
38
|
-
## Testing discipline
|
|
39
|
-
|
|
40
|
-
- Run the check before writing its line. A `- [ ]` reports a check that has not run rather than one that is planned.
|
|
41
|
-
- Tick the box and state the observed result. `- [x] npm test passes, 42 tests` beats `- [ ] run npm test`.
|
|
42
|
-
- Quote the count or output the run reported, never a figure carried from elsewhere.
|
|
43
|
-
- Leave a box unchecked only when a human is required, and name which human and why on the same line.
|
|
44
|
-
- Human-only covers visual or aesthetic judgment, anything needing credentials or a live third-party service, anything needing a second machine or a fresh OS, and judgment about whether a boundary or an abstraction reads correctly. The agent runs everything else.
|
|
45
|
-
- Put a request for the reviewer under `## For the reviewer`. It is a request rather than unfinished testing, so it never appears as an unchecked Testing box.
|
|
46
|
-
|
|
47
|
-
## Formatting
|
|
48
|
-
|
|
49
|
-
- End every bullet point with a period
|
|
50
|
-
|
|
51
|
-
## Examples
|
|
52
|
-
|
|
53
|
-
### Template
|
|
54
|
-
|
|
55
|
-
```markdown
|
|
56
|
-
## Summary
|
|
57
|
-
|
|
58
|
-
<Action Verb> <Direct Object> to <Result>.
|
|
59
|
-
|
|
60
|
-
## Key Changes
|
|
61
|
-
|
|
62
|
-
- <Verb> <specific component/file/function> (<reason if non-obvious>)
|
|
63
|
-
- <Verb> <specific component/file/function>
|
|
64
|
-
|
|
65
|
-
## Technical Context
|
|
66
|
-
|
|
67
|
-
- <Architectural reasoning explaining why, not what>
|
|
68
|
-
|
|
69
|
-
## Testing
|
|
70
|
-
|
|
71
|
-
- [x] <Command run> <observed result>
|
|
72
|
-
- [x] <Edge case verified> <what was observed>
|
|
73
|
-
- [ ] <Human-only check> (<which human, why>)
|
|
74
|
-
|
|
75
|
-
## For the reviewer
|
|
76
|
-
|
|
77
|
-
- <What the reviewer should confirm>
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
### Correct
|
|
81
|
-
|
|
82
|
-
```markdown
|
|
83
|
-
## Summary
|
|
84
|
-
|
|
85
|
-
Update auth middleware to enforce jwt expiration checks. # imperative + direct object + result
|
|
86
|
-
|
|
87
|
-
## Key Changes
|
|
88
|
-
|
|
89
|
-
- Add `verifyExpiration()` to `src/auth/validators.ts`. # specific function + file path
|
|
90
|
-
- Refactor `AuthService.authenticate()` to handle 401 codes. # named component + clear change
|
|
91
|
-
|
|
92
|
-
## Technical Context
|
|
93
|
-
|
|
94
|
-
- Migration to stateless session management for horizontal scalability. # why, not what
|
|
95
|
-
|
|
96
|
-
## Testing
|
|
97
|
-
|
|
98
|
-
- [x] `npm run test:auth` passes, 42 tests. # command run + observed result
|
|
99
|
-
- [x] Expired token rejected with a 401 against a local server. # edge case + what happened
|
|
100
|
-
- [ ] Staging smoke test (release owner, needs staging credentials). # unchecked + which human + why
|
|
101
|
-
|
|
102
|
-
## For the reviewer
|
|
103
|
-
|
|
104
|
-
- Confirm the 401 and 403 split reads correctly for the public API. # a request, not a test result
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
### Incorrect
|
|
108
|
-
|
|
109
|
-
```markdown
|
|
110
|
-
## Summary
|
|
111
|
-
|
|
112
|
-
This PR updates the authentication system to be more robust. # "This PR" opener + buzzword
|
|
113
|
-
|
|
114
|
-
## Key Changes
|
|
115
|
-
|
|
116
|
-
- Updated auth middleware files # vague, no specific component, no period
|
|
117
|
-
- The old system used to check tokens differently # describes historical behavior
|
|
118
|
-
|
|
119
|
-
## Testing
|
|
120
|
-
|
|
121
|
-
- Tested manually # no specific command or case
|
|
122
|
-
- [ ] `npm run test:auth` # unchecked box for a check the agent can run
|
|
123
|
-
- [ ] Reviewer confirms the error split reads correctly # a reviewer request, belongs under `## For the reviewer`
|
|
124
|
-
```
|