@erclx/aitk 0.101.0 → 0.103.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-docs/REQUIREMENT.md +7 -1
- package/claude/skills/claude-docs/SKILL.md +26 -3
- package/claude/skills/claude-orchestrate/SKILL.md +9 -2
- package/claude/skills/claude-orchestrate/references/orchestrator-sweep.md +1 -1
- package/claude/skills/claude-tasks/SKILL.md +10 -1
- package/claude/skills/claude-teach/REQUIREMENT.md +9 -2
- package/claude/skills/claude-teach/SKILL.md +51 -5
- package/claude/skills/claude-teach/references/promotion.md +54 -0
- package/claude/skills/session-resume/SKILL.md +1 -1
- package/docs/agents/index.md +1 -1
- package/docs/agents/markdown-audit.md +21 -11
- package/docs/agents/tasks.md +8 -4
- package/docs/agents/teach.md +1 -1
- package/docs/ai-workflow.md +2 -0
- package/docs/target-projects.md +1 -1
- package/package.json +1 -1
- package/scripts/core/verify.sh +3 -0
- package/src/commands/markdown.ts +44 -28
- package/src/commands/tasks.ts +6 -3
- package/src/markdown/bans.ts +71 -210
- package/src/markdown/structure.ts +9 -71
- package/src/tasks/archive.ts +9 -4
- package/src/tasks/validate.ts +103 -3
- package/standards/glossary.md +75 -0
- package/standards/index.md +2 -1
- package/standards/prose.md +0 -2
- package/standards/tasks.md +51 -8
- package/standards/teach.md +5 -7
- package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +43 -47
package/src/commands/markdown.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises'
|
|
2
2
|
import { resolve } from 'node:path'
|
|
3
3
|
import type { Command } from 'commander'
|
|
4
|
-
import {
|
|
4
|
+
import { BAN_SETS, emptyBanSets } from '@/markdown/bans'
|
|
5
5
|
import { resolveMarkdown } from '@/markdown/files'
|
|
6
6
|
import { isGating } from '@/markdown/gate'
|
|
7
|
-
import { type BanFinding, bodyLines, scanBans } from '@/markdown/scan'
|
|
8
7
|
import {
|
|
8
|
+
type BanFinding,
|
|
9
|
+
type BanSets,
|
|
10
|
+
bodyLines,
|
|
11
|
+
scanBans,
|
|
12
|
+
} from '@/markdown/scan'
|
|
13
|
+
import {
|
|
14
|
+
CHECKPOINTS,
|
|
9
15
|
type Checkpoints,
|
|
10
16
|
measureStructure,
|
|
11
|
-
parseCheckpoints,
|
|
12
17
|
type StructureReport,
|
|
13
18
|
} from '@/markdown/structure'
|
|
14
19
|
import {
|
|
@@ -21,8 +26,19 @@ import {
|
|
|
21
26
|
plural,
|
|
22
27
|
} from '@/ui'
|
|
23
28
|
|
|
29
|
+
const EXIT_REFUSED = 1
|
|
24
30
|
const EXIT_GATE = 2
|
|
25
31
|
|
|
32
|
+
/**
|
|
33
|
+
* A set shipped empty, so the run measured a corpus against nothing.
|
|
34
|
+
*
|
|
35
|
+
* Distinct from `EXIT_REFUSED` because the two want different responses. A
|
|
36
|
+
* refusal means no corpus was built and a push stage is right to skip, while an
|
|
37
|
+
* empty set means the corpus was walked and nothing was looked for, which is a
|
|
38
|
+
* defect in the build and has to fail.
|
|
39
|
+
*/
|
|
40
|
+
const EXIT_UNUSABLE = 3
|
|
41
|
+
|
|
26
42
|
interface AuditCommandOptions {
|
|
27
43
|
readonly json?: boolean
|
|
28
44
|
}
|
|
@@ -58,6 +74,7 @@ export function register(program: Command): void {
|
|
|
58
74
|
' 0 the audit completed with no gating finding',
|
|
59
75
|
' 1 refused, with the reason on stderr',
|
|
60
76
|
' 2 a banned character, word, or spelling is present',
|
|
77
|
+
' 3 a shipped ban set is empty, so the run measured nothing',
|
|
61
78
|
'',
|
|
62
79
|
'A ban hit is a fact and gates unconditionally. Bullet, paragraph, and',
|
|
63
80
|
'depth weight are judgments a reader settles, so all three report and',
|
|
@@ -68,12 +85,11 @@ export function register(program: Command): void {
|
|
|
68
85
|
'where the token is genuinely an identifier under discussion, which is',
|
|
69
86
|
'what markdown.md reserves the span for.',
|
|
70
87
|
'',
|
|
71
|
-
'Bans and checkpoints
|
|
72
|
-
'
|
|
73
|
-
'
|
|
74
|
-
'
|
|
75
|
-
'
|
|
76
|
-
'are in reach.',
|
|
88
|
+
'Bans and checkpoints ship with the aitk package rather than being read',
|
|
89
|
+
'out of a standards file, so a project that installed no standards is',
|
|
90
|
+
'measured the same as one that did. markdown.md and prose.md still state',
|
|
91
|
+
'every rule for a reader. No folder has to resolve and no index.md has',
|
|
92
|
+
'to exist, so .claude/rules/, governance/, and snippets/ are in reach.',
|
|
77
93
|
'',
|
|
78
94
|
'Examples:',
|
|
79
95
|
' aitk markdown audit',
|
|
@@ -110,9 +126,9 @@ async function runAudit(
|
|
|
110
126
|
)
|
|
111
127
|
}
|
|
112
128
|
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
const checkpoints =
|
|
129
|
+
const bans = BAN_SETS
|
|
130
|
+
const empty = emptyBanSets(bans)
|
|
131
|
+
const checkpoints = CHECKPOINTS
|
|
116
132
|
|
|
117
133
|
const reports: FileReport[] = await Promise.all(
|
|
118
134
|
scope.files.map(async (rel) => {
|
|
@@ -127,7 +143,7 @@ async function runAudit(
|
|
|
127
143
|
|
|
128
144
|
intro('aitk markdown audit')
|
|
129
145
|
reportScope(scope.files, scope.unmatched)
|
|
130
|
-
reportBans(reports, bans)
|
|
146
|
+
reportBans(reports, bans, empty)
|
|
131
147
|
reportBullets(reports, checkpoints)
|
|
132
148
|
reportParagraphs(reports, checkpoints)
|
|
133
149
|
reportDepth(reports, checkpoints)
|
|
@@ -143,8 +159,7 @@ async function runAudit(
|
|
|
143
159
|
characters: bans.characters,
|
|
144
160
|
words: bans.words,
|
|
145
161
|
spellings: bans.spellings,
|
|
146
|
-
|
|
147
|
-
missingStandards: bans.missing,
|
|
162
|
+
emptySets: empty,
|
|
148
163
|
},
|
|
149
164
|
checkpoints: {
|
|
150
165
|
run: checkpoints.run,
|
|
@@ -153,7 +168,6 @@ async function runAudit(
|
|
|
153
168
|
paragraph: checkpoints.paragraph,
|
|
154
169
|
sentences: checkpoints.sentences,
|
|
155
170
|
renderWidth: checkpoints.renderWidth,
|
|
156
|
-
fellBack: checkpoints.fellBack,
|
|
157
171
|
},
|
|
158
172
|
entries: reports.map((report) => ({
|
|
159
173
|
path: report.rel,
|
|
@@ -167,6 +181,10 @@ async function runAudit(
|
|
|
167
181
|
)
|
|
168
182
|
}
|
|
169
183
|
|
|
184
|
+
// An empty set finds nothing and would exit clean, which reports a corpus
|
|
185
|
+
// nobody checked as a corpus carrying no violation.
|
|
186
|
+
if (empty.length > 0) return EXIT_UNUSABLE
|
|
187
|
+
|
|
170
188
|
const gating = isGating({
|
|
171
189
|
bans: reports.flatMap((report) => report.bans),
|
|
172
190
|
structure: reports.map((report) => report.structure),
|
|
@@ -180,7 +198,7 @@ function refuse(message: string): number {
|
|
|
180
198
|
logStep('Refused')
|
|
181
199
|
logWarn(message)
|
|
182
200
|
outro()
|
|
183
|
-
return
|
|
201
|
+
return EXIT_REFUSED
|
|
184
202
|
}
|
|
185
203
|
|
|
186
204
|
function reportScope(
|
|
@@ -203,18 +221,22 @@ function reportScope(
|
|
|
203
221
|
* the sentence and every voice rule is a judgment, so a report listing hits
|
|
204
222
|
* without naming those would read as a verdict on the whole standard.
|
|
205
223
|
*/
|
|
206
|
-
function reportBans(
|
|
224
|
+
function reportBans(
|
|
225
|
+
reports: readonly FileReport[],
|
|
226
|
+
bans: BanSets,
|
|
227
|
+
empty: readonly string[],
|
|
228
|
+
): void {
|
|
207
229
|
logStep('Bans')
|
|
208
230
|
|
|
209
|
-
if (
|
|
231
|
+
if (empty.length > 0) {
|
|
210
232
|
logWarn(
|
|
211
|
-
`Not measured.
|
|
233
|
+
`Not measured. The shipped set is empty for: ${empty.join(', ')}. The sets ship with the aitk package, so an empty one is a defect in the build rather than a missing install.`,
|
|
212
234
|
)
|
|
213
|
-
|
|
235
|
+
return
|
|
214
236
|
}
|
|
215
237
|
|
|
216
238
|
logInfo(
|
|
217
|
-
`${plural(bans.characters.length, 'character')}, ${plural(bans.words.length, 'word')}, and ${plural(bans.spellings.length, 'spelling')}
|
|
239
|
+
`${plural(bans.characters.length, 'character')}, ${plural(bans.words.length, 'word')}, and ${plural(bans.spellings.length, 'spelling')} shipped with the aitk package`,
|
|
218
240
|
)
|
|
219
241
|
logInfo(
|
|
220
242
|
'Frontmatter, fenced blocks, code spans, and link destinations are excluded.',
|
|
@@ -379,12 +401,6 @@ function reportDepth(
|
|
|
379
401
|
'A run that is entirely table rows is excluded too, since a heading inside a table splits the table rather than the run.',
|
|
380
402
|
)
|
|
381
403
|
|
|
382
|
-
if (checkpoints.fellBack.length > 0) {
|
|
383
|
-
logWarn(
|
|
384
|
-
`Read no number from the standard for: ${checkpoints.fellBack.join(', ')}. Measured against the shipped default instead.`,
|
|
385
|
-
)
|
|
386
|
-
}
|
|
387
|
-
|
|
388
404
|
const over = reports
|
|
389
405
|
.filter((report) => report.structure.longestRun > checkpoints.run)
|
|
390
406
|
.sort((a, b) => b.structure.longestRun - a.structure.longestRun)
|
package/src/commands/tasks.ts
CHANGED
|
@@ -104,7 +104,7 @@ export function register(program: Command): void {
|
|
|
104
104
|
'',
|
|
105
105
|
'Checks:',
|
|
106
106
|
' every Run now row points at a plan file that resolves',
|
|
107
|
-
' every
|
|
107
|
+
' every task file carries a board row or a backlog line, never both',
|
|
108
108
|
' no task carries more than one row',
|
|
109
109
|
' no two Run now rows touch the same file',
|
|
110
110
|
'',
|
|
@@ -450,12 +450,14 @@ function reportValidation(
|
|
|
450
450
|
intro('aitk tasks validate')
|
|
451
451
|
logStep('Board')
|
|
452
452
|
logInfo(
|
|
453
|
-
`${outcome.rows} row(s) across the readiness groups, ${outcome.tasks} task file(s)`,
|
|
453
|
+
`${outcome.rows} row(s) across the readiness groups, ${outcome.backlog} backlog line(s), ${outcome.tasks} task file(s)`,
|
|
454
454
|
)
|
|
455
455
|
|
|
456
456
|
logStep(outcome.findings.length === 0 ? 'Clean' : 'Findings')
|
|
457
457
|
if (outcome.findings.length === 0) {
|
|
458
|
-
logInfo(
|
|
458
|
+
logInfo(
|
|
459
|
+
'every row resolves, every task sits on one surface, and each touches its own files',
|
|
460
|
+
)
|
|
459
461
|
} else {
|
|
460
462
|
for (const finding of outcome.findings) logWarn(describe(finding))
|
|
461
463
|
}
|
|
@@ -481,6 +483,7 @@ function reportValidation(
|
|
|
481
483
|
ok: true,
|
|
482
484
|
root,
|
|
483
485
|
rows: outcome.rows,
|
|
486
|
+
backlog: outcome.backlog,
|
|
484
487
|
tasks: outcome.tasks,
|
|
485
488
|
findings: outcome.findings,
|
|
486
489
|
untested: outcome.untested,
|
package/src/markdown/bans.ts
CHANGED
|
@@ -1,233 +1,94 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { linesOutsideFences } from '@/markdown/scan'
|
|
3
|
-
import { resolveStandard } from '@/standards/read'
|
|
1
|
+
import type { BanSets } from '@/markdown/scan'
|
|
4
2
|
|
|
5
3
|
/**
|
|
6
|
-
*
|
|
4
|
+
* Single characters `markdown.md` bans under `## Punctuation`.
|
|
7
5
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* position-based read would narrow the check and still print a count.
|
|
6
|
+
* The parenthetical-aside ban in that section is absent because it quotes a
|
|
7
|
+
* whole clause, and a literal match built from a clause reports the compliant
|
|
8
|
+
* text and reaches none of the violations.
|
|
12
9
|
*/
|
|
13
|
-
|
|
14
|
-
export const CHARACTER_BAN_HEADING = '## Punctuation'
|
|
15
|
-
|
|
16
|
-
/** Bullet lead-ins the two closed sets are stated behind. */
|
|
17
|
-
const BAN_LEAD = '- Do not use '
|
|
18
|
-
const SPELLING_LEAD = '- Use American English spelling'
|
|
19
|
-
|
|
20
|
-
const BACKTICKED = /`([^`]+)`/g
|
|
21
|
-
const WORD = /^[a-z]+$/
|
|
22
|
-
const SUFFIX = /^-[a-z]+$/
|
|
23
|
-
|
|
24
|
-
export interface BanReport {
|
|
25
|
-
/** Single characters the mechanics standard bans outright. */
|
|
26
|
-
readonly characters: readonly string[]
|
|
27
|
-
/** Single lowercase words the prose standard bans outright. */
|
|
28
|
-
readonly words: readonly string[]
|
|
29
|
-
/** Spellings derived from the prose standard's own examples. */
|
|
30
|
-
readonly spellings: readonly string[]
|
|
31
|
-
/**
|
|
32
|
-
* Paths of the standards read, in the order read. Repo-relative for a project
|
|
33
|
-
* copy, and spelled under `<aitk>/` for the package copy, so a reader can
|
|
34
|
-
* tell which root answered without joining a path that resolves nowhere.
|
|
35
|
-
*/
|
|
36
|
-
readonly sources: readonly string[]
|
|
37
|
-
/**
|
|
38
|
-
* Standards that resolved under none of the roots. Absent is a distinct state
|
|
39
|
-
* from empty: a scan with no terms finds nothing, and reporting that as a
|
|
40
|
-
* clean file claims the prose passed when nothing was looked for.
|
|
41
|
-
*/
|
|
42
|
-
readonly missing: readonly string[]
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function section(markdown: string, heading: string): string[] {
|
|
46
|
-
const lines = linesOutsideFences(markdown)
|
|
47
|
-
const start = lines.findIndex((line) => line.trim() === heading)
|
|
48
|
-
if (start === -1) return []
|
|
49
|
-
|
|
50
|
-
const body = lines.slice(start + 1)
|
|
51
|
-
const end = body.findIndex((line) => line.startsWith('## '))
|
|
52
|
-
|
|
53
|
-
return end === -1 ? body : body.slice(0, end)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function backticked(line: string): string[] {
|
|
57
|
-
return [...line.matchAll(BACKTICKED)].map((match) => match[1].trim())
|
|
58
|
-
}
|
|
10
|
+
const CHARACTERS = ['—', ';'] as const
|
|
59
11
|
|
|
60
12
|
/**
|
|
61
|
-
*
|
|
13
|
+
* Single lowercase words `prose.md` bans under `## Language`.
|
|
62
14
|
*
|
|
63
|
-
* A multi-word
|
|
64
|
-
* a pattern like `It's not X, it's Y` with a placeholder standing
|
|
65
|
-
* rest of the sentence,
|
|
66
|
-
* stay a reader's judgment
|
|
15
|
+
* A multi-word ban is absent by the same test the character set applies. The
|
|
16
|
+
* standard bans a pattern like `It's not X, it's Y` with a placeholder standing
|
|
17
|
+
* in for the rest of the sentence, so no literal match reaches it and the
|
|
18
|
+
* phrase bans stay a reader's judgment.
|
|
67
19
|
*/
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
20
|
+
const WORDS = [
|
|
21
|
+
'seamless',
|
|
22
|
+
'robust',
|
|
23
|
+
'powerful',
|
|
24
|
+
'revolutionary',
|
|
25
|
+
'enhanced',
|
|
26
|
+
'allows',
|
|
27
|
+
'leverage',
|
|
28
|
+
'simply',
|
|
29
|
+
'just',
|
|
30
|
+
'easily',
|
|
31
|
+
'quickly',
|
|
32
|
+
'very',
|
|
33
|
+
'really',
|
|
34
|
+
] as const
|
|
80
35
|
|
|
81
36
|
/**
|
|
82
|
-
*
|
|
37
|
+
* British spellings of the American examples `prose.md` lists.
|
|
83
38
|
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
|
|
88
|
-
export function parseCharacterBans(markdown: string): string[] {
|
|
89
|
-
const terms: string[] = []
|
|
90
|
-
|
|
91
|
-
for (const line of section(markdown, CHARACTER_BAN_HEADING)) {
|
|
92
|
-
if (!line.startsWith(BAN_LEAD)) continue
|
|
93
|
-
for (const term of backticked(line)) {
|
|
94
|
-
if (
|
|
95
|
-
term.length === 1 &&
|
|
96
|
-
!/[a-z0-9]/i.test(term) &&
|
|
97
|
-
!terms.includes(term)
|
|
98
|
-
) {
|
|
99
|
-
terms.push(term)
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return terms
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Derives the banned spellings by applying the standard's own suffix rules to
|
|
109
|
-
* the standard's own examples.
|
|
39
|
+
* The set is carried whole rather than derived from a suffix rule, because a
|
|
40
|
+
* suffix pattern run over prose produced 46 of the 58 false positives measured
|
|
41
|
+
* during intake: `exercises`, `promises`, and `revised` all end in `-ise` and
|
|
42
|
+
* none is a British spelling. Matching whole words reaches none of them.
|
|
110
43
|
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* listed American words into their British forms yields a closed set that
|
|
116
|
-
* matches whole words and reaches none of them, and an example added to the
|
|
117
|
-
* standard extends the check without a code edit.
|
|
44
|
+
* `analyse` is absent because the standard's example is `analyze`, which ends
|
|
45
|
+
* in `-yze` rather than the `-ize` its rule states. The set records what the
|
|
46
|
+
* standard reaches rather than what a reader would extend it to, so adding the
|
|
47
|
+
* spelling here would widen the check past the prose it answers to.
|
|
118
48
|
*/
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const examples = terms.filter((term) => WORD.test(term))
|
|
128
|
-
|
|
129
|
-
// Stated as `preferred` over `banned`, so the run of suffix terms pairs off
|
|
130
|
-
// in order. An odd count means the sentence was rewritten into a shape this
|
|
131
|
-
// cannot read, and pairing what is there would invent a rule from half of it.
|
|
132
|
-
if (suffixes.length === 0 || suffixes.length % 2 !== 0) return []
|
|
133
|
-
|
|
134
|
-
const pairs: { preferred: string; banned: string }[] = []
|
|
135
|
-
for (let index = 0; index < suffixes.length; index += 2) {
|
|
136
|
-
pairs.push({
|
|
137
|
-
preferred: suffixes[index].slice(1),
|
|
138
|
-
banned: suffixes[index + 1].slice(1),
|
|
139
|
-
})
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const spellings: string[] = []
|
|
143
|
-
for (const example of examples) {
|
|
144
|
-
const pair = pairs.find((each) => example.endsWith(each.preferred))
|
|
145
|
-
if (!pair) continue
|
|
146
|
-
|
|
147
|
-
const banned = `${example.slice(0, -pair.preferred.length)}${pair.banned}`
|
|
148
|
-
if (!spellings.includes(banned)) spellings.push(banned)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return spellings
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export interface StandardText {
|
|
155
|
-
/** Path of the copy read, so a report says which root won. */
|
|
156
|
-
readonly source: string
|
|
157
|
-
readonly text: string
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export interface Standards {
|
|
161
|
-
readonly markdown: StandardText | undefined
|
|
162
|
-
readonly prose: StandardText | undefined
|
|
163
|
-
/**
|
|
164
|
-
* Standards that resolved under none of the roots. Absent is a distinct state
|
|
165
|
-
* from empty: a scan with no terms finds nothing, and reporting that as a
|
|
166
|
-
* clean file claims the prose passed when nothing was looked for.
|
|
167
|
-
*/
|
|
168
|
-
readonly missing: readonly string[]
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
async function loadStandard(
|
|
172
|
-
root: string,
|
|
173
|
-
name: string,
|
|
174
|
-
): Promise<StandardText | undefined> {
|
|
175
|
-
const resolved = resolveStandard(root, name)
|
|
176
|
-
if (!resolved) return undefined
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
source: resolved.source,
|
|
180
|
-
text: await readFile(resolved.path, 'utf8'),
|
|
181
|
-
}
|
|
182
|
-
}
|
|
49
|
+
const SPELLINGS = [
|
|
50
|
+
'organise',
|
|
51
|
+
'summarise',
|
|
52
|
+
'recognise',
|
|
53
|
+
'behaviour',
|
|
54
|
+
'colour',
|
|
55
|
+
'centre',
|
|
56
|
+
] as const
|
|
183
57
|
|
|
184
58
|
/**
|
|
185
|
-
*
|
|
59
|
+
* The three closed sets the audit measures, owned here rather than harvested
|
|
60
|
+
* from the standards stating them.
|
|
61
|
+
*
|
|
62
|
+
* Parsing the prose was the alternative and it put a parser contract on two
|
|
63
|
+
* documents authored for people, which the prose standard had to carry a
|
|
64
|
+
* paragraph of its own to protect. The sets are a prior an author already
|
|
65
|
+
* knows rather than a filter: measured at `60fc97bf` on 2026-08-19, these 21
|
|
66
|
+
* terms ran across 483 markdown files for a clean exit, and every one of the
|
|
67
|
+
* 70 occurrences of a banned word sat inside the ban list itself or inside an
|
|
68
|
+
* example demonstrating the ban.
|
|
186
69
|
*
|
|
187
|
-
* The
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
* this one
|
|
70
|
+
* The set is closed rather than extensible. Enumeration cannot close the gap
|
|
71
|
+
* it aims at, and each addition costs a false-positive class, since `just`,
|
|
72
|
+
* `allows`, and `very` have honest uses no literal match separates. A project
|
|
73
|
+
* wanting its own vocabulary is asking for a different feature than this one.
|
|
191
74
|
*/
|
|
192
|
-
export
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
])
|
|
197
|
-
|
|
198
|
-
return {
|
|
199
|
-
markdown,
|
|
200
|
-
prose,
|
|
201
|
-
missing: [
|
|
202
|
-
markdown ? undefined : 'markdown.md',
|
|
203
|
-
prose ? undefined : 'prose.md',
|
|
204
|
-
].filter((name): name is string => name !== undefined),
|
|
205
|
-
}
|
|
75
|
+
export const BAN_SETS: BanSets = {
|
|
76
|
+
characters: CHARACTERS,
|
|
77
|
+
words: WORDS,
|
|
78
|
+
spellings: SPELLINGS,
|
|
206
79
|
}
|
|
207
80
|
|
|
208
81
|
/**
|
|
209
|
-
*
|
|
82
|
+
* Names any set that arrived empty, so a run measuring nothing says so.
|
|
210
83
|
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
84
|
+
* Empty is not the same state as finding no hit. A scan with no terms reports
|
|
85
|
+
* a clean file having looked for nothing, which is the silence the audit
|
|
86
|
+
* refused to ship back when a standard could go missing. The sets ship with
|
|
87
|
+
* the package now, so the only way one empties is an edit to this file, and
|
|
88
|
+
* the guard is what keeps that edit loud rather than quiet.
|
|
216
89
|
*/
|
|
217
|
-
export function
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
characters: markdown ? parseCharacterBans(markdown.text) : [],
|
|
222
|
-
words: prose ? parseWordBans(prose.text) : [],
|
|
223
|
-
spellings: prose ? parseSpellingBans(prose.text) : [],
|
|
224
|
-
sources: [markdown?.source, prose?.source].filter(
|
|
225
|
-
(source): source is string => source !== undefined,
|
|
226
|
-
),
|
|
227
|
-
missing: standards.missing,
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
export async function loadBans(root: string): Promise<BanReport> {
|
|
232
|
-
return banReport(await loadStandards(root))
|
|
90
|
+
export function emptyBanSets(sets: BanSets = BAN_SETS): string[] {
|
|
91
|
+
return (['characters', 'words', 'spellings'] as const).filter(
|
|
92
|
+
(name) => sets[name].length === 0,
|
|
93
|
+
)
|
|
233
94
|
}
|
|
@@ -24,30 +24,17 @@ const BLOCKQUOTE = /^\s*>/
|
|
|
24
24
|
*/
|
|
25
25
|
const SENTENCE_END = /[.!?]["'’”)\]]*(?=\s+(?:["'“(\[]*[A-Z]|`)|\s*$)/g
|
|
26
26
|
|
|
27
|
-
const NUMBER_WORDS: Record<string, number> = {
|
|
28
|
-
one: 1,
|
|
29
|
-
two: 2,
|
|
30
|
-
three: 3,
|
|
31
|
-
four: 4,
|
|
32
|
-
five: 5,
|
|
33
|
-
six: 6,
|
|
34
|
-
seven: 7,
|
|
35
|
-
eight: 8,
|
|
36
|
-
nine: 9,
|
|
37
|
-
ten: 10,
|
|
38
|
-
}
|
|
39
|
-
|
|
40
27
|
/**
|
|
41
|
-
* Checkpoints
|
|
28
|
+
* Checkpoints the structural measures run against.
|
|
42
29
|
*
|
|
43
|
-
* These are the
|
|
44
|
-
*
|
|
45
|
-
* a
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
30
|
+
* These are the definition rather than a fallback. Reading each number out of
|
|
31
|
+
* the sentence `standards/markdown.md` states it in was the alternative, and it
|
|
32
|
+
* put a parser contract on a document authored for people, where a rewording
|
|
33
|
+
* degraded a number and the report had to carry a legend saying which one. The
|
|
34
|
+
* standard still states every number for a reader, and moving one is an edit to
|
|
35
|
+
* both.
|
|
49
36
|
*/
|
|
50
|
-
export const
|
|
37
|
+
export const CHECKPOINTS = {
|
|
51
38
|
run: 40,
|
|
52
39
|
peerBullet: 130,
|
|
53
40
|
bullet: 400,
|
|
@@ -56,15 +43,13 @@ export const DEFAULT_CHECKPOINTS = {
|
|
|
56
43
|
renderWidth: 80,
|
|
57
44
|
} as const
|
|
58
45
|
|
|
59
|
-
export type CheckpointName = keyof typeof DEFAULT_CHECKPOINTS
|
|
60
|
-
|
|
61
46
|
/**
|
|
62
47
|
* Columns a source line wraps at when rendered.
|
|
63
48
|
*
|
|
64
49
|
* Nothing in this repository sets a line width and entries are authored one
|
|
65
50
|
* line per bullet, so the rendered width is the viewer's rather than the file's.
|
|
66
51
|
*/
|
|
67
|
-
export const RENDER_WIDTH: number =
|
|
52
|
+
export const RENDER_WIDTH: number = CHECKPOINTS.renderWidth
|
|
68
53
|
|
|
69
54
|
export interface Checkpoints {
|
|
70
55
|
readonly run: number
|
|
@@ -73,8 +58,6 @@ export interface Checkpoints {
|
|
|
73
58
|
readonly paragraph: number
|
|
74
59
|
readonly sentences: number
|
|
75
60
|
readonly renderWidth: number
|
|
76
|
-
/** Names that fell back, so the report can say which number is not the standard's. */
|
|
77
|
-
readonly fellBack: readonly CheckpointName[]
|
|
78
61
|
}
|
|
79
62
|
|
|
80
63
|
export interface BulletFinding {
|
|
@@ -99,51 +82,6 @@ export interface StructureReport {
|
|
|
99
82
|
readonly heavyParagraphs: readonly ParagraphFinding[]
|
|
100
83
|
}
|
|
101
84
|
|
|
102
|
-
const PATTERNS: Record<CheckpointName, RegExp> = {
|
|
103
|
-
run: /Past roughly (\d+) rendered lines/,
|
|
104
|
-
peerBullet: /averaging under roughly (\d+) characters/,
|
|
105
|
-
bullet: /Past roughly (\d+) characters in one top-level bullet/,
|
|
106
|
-
paragraph: /Past roughly (\d+) characters in one paragraph/,
|
|
107
|
-
sentences: /Keep paragraphs to ([a-z]+|\d+) sentences or fewer/,
|
|
108
|
-
renderWidth: /wrapping each source line at (\d+) columns/,
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Reads each checkpoint out of the standard stating it.
|
|
113
|
-
*
|
|
114
|
-
* A number held in code is a second place the rule lives, and the two drift
|
|
115
|
-
* silently because nothing compares them. What this buys is that raising a
|
|
116
|
-
* checkpoint is an edit to the sentence a reader is pointed at, and what it
|
|
117
|
-
* costs is a reader of prose, which is bounded by falling back per checkpoint
|
|
118
|
-
* rather than per file.
|
|
119
|
-
*/
|
|
120
|
-
export function parseCheckpoints(markdown: string): Checkpoints {
|
|
121
|
-
const fellBack: CheckpointName[] = []
|
|
122
|
-
|
|
123
|
-
const read = (name: CheckpointName): number => {
|
|
124
|
-
const match = markdown.match(PATTERNS[name])
|
|
125
|
-
const raw = match?.[1]
|
|
126
|
-
const value = raw ? (NUMBER_WORDS[raw] ?? Number(raw)) : Number.NaN
|
|
127
|
-
|
|
128
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
129
|
-
fellBack.push(name)
|
|
130
|
-
return DEFAULT_CHECKPOINTS[name]
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return value
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return {
|
|
137
|
-
run: read('run'),
|
|
138
|
-
peerBullet: read('peerBullet'),
|
|
139
|
-
bullet: read('bullet'),
|
|
140
|
-
paragraph: read('paragraph'),
|
|
141
|
-
sentences: read('sentences'),
|
|
142
|
-
renderWidth: read('renderWidth'),
|
|
143
|
-
fellBack,
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
85
|
/**
|
|
148
86
|
* Height a source line occupies once wrapped.
|
|
149
87
|
*
|
package/src/tasks/archive.ts
CHANGED
|
@@ -9,11 +9,16 @@ const PLANS_DIR = join('.claude', 'plans')
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Siblings that sit on the board without being tasks: the generated index, the
|
|
12
|
-
* hand-maintained ordering,
|
|
13
|
-
* one name per session. `validate` reads the
|
|
14
|
-
* count a sibling as a task the other does not.
|
|
12
|
+
* hand-maintained ordering, the unordered backlog beside it, and a handoff a
|
|
13
|
+
* session wrote before the file took one name per session. `validate` reads the
|
|
14
|
+
* same list, so neither verb can count a sibling as a task the other does not.
|
|
15
15
|
*/
|
|
16
|
-
export const RESERVED_STEMS = [
|
|
16
|
+
export const RESERVED_STEMS = [
|
|
17
|
+
'index',
|
|
18
|
+
'priority',
|
|
19
|
+
'backlog',
|
|
20
|
+
'session',
|
|
21
|
+
] as const
|
|
17
22
|
|
|
18
23
|
/** The pre-compaction handoff takes one file per session, so its stems vary. */
|
|
19
24
|
const SESSION_MAP_PREFIX = 'session-'
|