@erclx/aitk 0.64.0 → 0.64.1
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.
|
@@ -19,7 +19,7 @@ It reports rather than gates by default, the closer call because a missing secti
|
|
|
19
19
|
|
|
20
20
|
## Length
|
|
21
21
|
|
|
22
|
-
Length quotes its checkpoint from `.claude/standards/context.md`: roughly 150 rendered lines for an entry. It counts rendered lines rather than source lines, wrapping each line at 80 columns and summing the heights. Entries here are authored one line per bullet, so a block of fifteen paragraph-bullets occupies fifteen source lines and renders past sixty, which source counting cannot see. The measure counts fenced blocks and frontmatter, so a reference-heavy entry ranks by its examples, which the legend states on every run alongside the width, since a number in rendered lines cannot be reproduced without it.
|
|
22
|
+
Length quotes its checkpoint from `.claude/standards/context.md`: roughly 150 rendered lines for an entry. It counts rendered lines rather than source lines, wrapping each line at 80 columns and summing the heights. A line is wrapped at the width it renders at, so a link counts as its anchor text and a backticked path counts whole. That moves 2 of the 48 entries measured, `extensions.md` by 2 lines and `governance.md` by 1, because an entry cites a path in backticks rather than as a link. Entries here are authored one line per bullet, so a block of fifteen paragraph-bullets occupies fifteen source lines and renders past sixty, which source counting cannot see. The measure counts fenced blocks and frontmatter, so a reference-heavy entry ranks by its examples, which the legend states on every run alongside the width, since a number in rendered lines cannot be reproduced without it.
|
|
23
23
|
|
|
24
24
|
Depth and bullet weight are quoted from `.claude/standards/markdown.md`, which states both over every markdown file rather than over a context entry, so `aitk markdown audit` measures them and this command no longer does. They share `renderedHeight` with the length measure, since the two checkpoints sit in one section of that standard and a reader compares them. What the split costs is that a session wanting both numbers for one entry runs two commands, and what it buys is that either number can be had for a file in a folder this audit refuses to resolve.
|
|
25
25
|
|
|
@@ -41,13 +41,21 @@ Deriving the spellings rather than pattern-matching a suffix is what keeps `exer
|
|
|
41
41
|
|
|
42
42
|
Frontmatter, fenced blocks, inline code spans, and link destinations are excluded. Without the code-span exclusion each standard would report its own backticked examples, and without the link exclusion a semicolon in a query string would report as prose no rewrite can fix.
|
|
43
43
|
|
|
44
|
+
A banned word is bounded on a word character or a hyphen either side. A plain word boundary sits after a hyphen, so a banned word ending a hyphenated compound reported from inside one, and a compound is a single word to the reader who wrote it.
|
|
45
|
+
|
|
46
|
+
A banned spelling keeps the plain word boundary, hyphens included. The two bans target different things: a word ban targets the word, so reading a compound as one word is correct, while a spelling ban targets the orthography inside it, which sits in `behaviour-driven` as plainly as it sits alone.
|
|
47
|
+
|
|
44
48
|
Two ban shapes stay unmeasured and the report says so on every run. A phrase ban carries a placeholder standing in for the rest of the sentence, so no literal match reaches it, and every rule under `## Voice` is a judgment. A report listing hits without naming those would read as a verdict on the whole standard.
|
|
45
49
|
|
|
46
50
|
### Bullets, paragraphs, and depth
|
|
47
51
|
|
|
48
52
|
Bullet weight and depth are the checks that moved off `aitk context audit`, unchanged in what they measure. A top-level bullet reports past roughly 400 characters with continuation lines folded in and nested items left out. A run of lines no heading breaks reports past roughly 40 rendered lines, measured at 80 columns, skipping fenced blocks and exempting a flat peer list averaging under 130 characters a bullet and a run that is entirely table rows.
|
|
49
53
|
|
|
50
|
-
|
|
54
|
+
Every weight and depth measure counts the text a reader is shown. A link reduces to its anchor text and an autolink drops whole, since no reader is shown either destination. A backticked path stays counted, which is where these measures part from the ban scan above: that one blanks a code span so a standard quoting its own banned character does not report itself, and discounting the same span here would under-report a paragraph carrying several. One file holds both span sets and each answers its own question.
|
|
55
|
+
|
|
56
|
+
A code span is walked around rather than through, so a path quoting link or angle-bracket syntax keeps the width the page gives it. Masking inside one takes back the decision to count it, and the placeholders this toolkit writes are where that shows.
|
|
57
|
+
|
|
58
|
+
The paragraph check is new and measures both halves of one rule. `markdown.md` caps a paragraph at four sentences, and a sentence cap on its own is satisfied by writing fewer and longer ones: measured across 3374 paragraphs, 486 sit inside four sentences and past 400 characters, and the heaviest of those runs 1121 characters. The standard therefore states a weight beside the sentence cap, and the verb reads it as its own checkpoint.
|
|
51
59
|
|
|
52
60
|
The paragraph weight and the bullet weight are the same number today because the two shapes measure one population, sharing a median near 170 characters with no gap behind either candidate. They are separate checkpoints in the standard and separate patterns in the parser, so retuning one leaves the other where it is.
|
|
53
61
|
|
|
@@ -59,7 +67,9 @@ Exit codes are `0` for a completed run and `1` for a refusal. Every finding repo
|
|
|
59
67
|
|
|
60
68
|
A banned character is a fact rather than a judgment, which is the test that would ordinarily make it gate. What holds it back is that gating on day one against a corpus never checked mechanically fails loudly on work nobody has had a chance to fix. The order is to land the verb reporting, measure the corpus once, fix what it finds, and turn the gate on as its own change. Bullet, paragraph, and depth weight are judgments and stay advisory under any later gate.
|
|
61
69
|
|
|
62
|
-
Measured across
|
|
70
|
+
Measured across 444 files once the weight measures moved to visible text: 8 word hits, no character or spelling hits, 119 heavy bullets, 615 heavy paragraphs, and 40 files carrying a run past the depth checkpoint. The ban count is what a gate would have to hold at zero, and it is the only one of the five a gate should ever read.
|
|
71
|
+
|
|
72
|
+
Masking took 7 of the 493 weight-only paragraphs and 4 of the 44 files under their checkpoints, and no bullet at all. The first corpus triage put those at 31 paragraphs and 2 bullets, and neither reproduces: a code span is walked around, so a backticked path holding an angle-bracket placeholder keeps the width the page gives it, and both bullets the triage counted were that shape.
|
|
63
73
|
|
|
64
74
|
## What it does not cover
|
|
65
75
|
|
package/package.json
CHANGED
package/src/markdown/scan.ts
CHANGED
|
@@ -23,6 +23,17 @@ const CODE_SPAN = /(`+)(?:(?!\1).)*\1/g
|
|
|
23
23
|
const LINK_DESTINATION = /\]\([^)]*\)/g
|
|
24
24
|
const AUTOLINK = /<[^>\s]+>/g
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* A whole inline link, capturing the anchor text a reader is shown.
|
|
28
|
+
*
|
|
29
|
+
* `LINK_DESTINATION` covers the span both measures drop and this covers the
|
|
30
|
+
* brackets only the weight measure drops, which a reader is no more shown than
|
|
31
|
+
* the destination. The narrower pattern still runs after this one, since a link
|
|
32
|
+
* wrapped across two source lines puts its opening bracket on a line this one
|
|
33
|
+
* never matches.
|
|
34
|
+
*/
|
|
35
|
+
const LINK = /\[([^\]]*)\]\([^)]*\)/g
|
|
36
|
+
|
|
26
37
|
export interface BodyLine {
|
|
27
38
|
readonly number: number
|
|
28
39
|
readonly text: string
|
|
@@ -128,16 +139,81 @@ export function maskDisplayed(text: string): string {
|
|
|
128
139
|
.replace(AUTOLINK, blank)
|
|
129
140
|
}
|
|
130
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Drops the spans a reader is never shown, returning the text they read.
|
|
144
|
+
*
|
|
145
|
+
* This is what a weight measure counts, and it is deliberately not
|
|
146
|
+
* `maskDisplayed`. That one holds each span's width so a ban finding can name a
|
|
147
|
+
* column, which leaves behind the very characters a weight measure exists to
|
|
148
|
+
* discount. The two also disagree on the span set: a backticked path is text a
|
|
149
|
+
* reader reads and stays counted here, while the ban scan blanks it so a
|
|
150
|
+
* standard quoting its own banned character does not report itself. One file
|
|
151
|
+
* therefore holds two answers to what a reader sees, each correct for its own
|
|
152
|
+
* measure, and collapsing them into one helper breaks whichever loses.
|
|
153
|
+
*
|
|
154
|
+
* A code span is walked around rather than through, since keeping it counted
|
|
155
|
+
* and then dropping spans from inside it takes back the decision. The
|
|
156
|
+
* placeholders this repository writes are the case: a reader is shown all of
|
|
157
|
+
* `.claude/context/<domain>.md` and the autolink pattern reaches the angle
|
|
158
|
+
* brackets in the middle of it.
|
|
159
|
+
*/
|
|
160
|
+
export function visibleText(text: string): string {
|
|
161
|
+
const drop = (segment: string): string =>
|
|
162
|
+
segment
|
|
163
|
+
.replace(LINK, '$1')
|
|
164
|
+
.replace(LINK_DESTINATION, '')
|
|
165
|
+
.replace(AUTOLINK, '')
|
|
166
|
+
|
|
167
|
+
let visible = ''
|
|
168
|
+
let read = 0
|
|
169
|
+
|
|
170
|
+
for (const span of text.matchAll(CODE_SPAN)) {
|
|
171
|
+
visible += drop(text.slice(read, span.index)) + span[0]
|
|
172
|
+
read = span.index + span[0].length
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return visible + drop(text.slice(read))
|
|
176
|
+
}
|
|
177
|
+
|
|
131
178
|
function escape(term: string): string {
|
|
132
179
|
return term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
133
180
|
}
|
|
134
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Bounds a banned word on a word character or a hyphen either side.
|
|
184
|
+
*
|
|
185
|
+
* `\b` sits after a hyphen, so a banned word ending a hyphenated compound
|
|
186
|
+
* reports from inside it: `allows` came back out of `auto-allows`. A compound
|
|
187
|
+
* is one word to a reader, and the ban is on the word rather than on a morpheme
|
|
188
|
+
* of it. Stripping hyphens before matching was the alternative and it joins the
|
|
189
|
+
* compound into a token neither half reaches.
|
|
190
|
+
*
|
|
191
|
+
* A spelling ban takes `wordBoundary` instead. This is not that rule with a
|
|
192
|
+
* wider fence, since the two bans target different things.
|
|
193
|
+
*/
|
|
194
|
+
function bannedWord(term: string): RegExp {
|
|
195
|
+
return new RegExp(`(?<![\\w-])${escape(term)}(?![\\w-])`, 'gi')
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Bounds a banned spelling on a word character alone, hyphens included.
|
|
200
|
+
*
|
|
201
|
+
* A word ban targets the word, so a compound reading as one word is correct.
|
|
202
|
+
* A spelling ban targets the orthography inside it, and a compound is exactly
|
|
203
|
+
* where the orthography still sits: `behaviour-driven` carries the banned
|
|
204
|
+
* spelling as plainly as `behaviour` does, and rejecting a hyphen here would
|
|
205
|
+
* leave the usual spelling of that phrase unreported.
|
|
206
|
+
*/
|
|
207
|
+
function bannedSpelling(term: string): RegExp {
|
|
208
|
+
return new RegExp(`\\b${escape(term)}\\b`, 'gi')
|
|
209
|
+
}
|
|
210
|
+
|
|
135
211
|
/**
|
|
136
212
|
* Finds every banned term outside a fence, a code span, and a link.
|
|
137
213
|
*
|
|
138
|
-
* A word ban matches
|
|
139
|
-
*
|
|
140
|
-
*
|
|
214
|
+
* A word ban matches in either casing, since the standard states each in
|
|
215
|
+
* lowercase and bans the word rather than a spelling of it. A closed set of
|
|
216
|
+
* whole words separates the check from the pattern that produced most of the
|
|
141
217
|
* intake's false positives: `exercises` and `promises` end in the banned
|
|
142
218
|
* suffix and are not the banned words, and a closed set never reaches them.
|
|
143
219
|
*/
|
|
@@ -151,12 +227,12 @@ export function scanBans(
|
|
|
151
227
|
...bans.words.map((term) => ({
|
|
152
228
|
kind: 'word' as const,
|
|
153
229
|
term,
|
|
154
|
-
pattern:
|
|
230
|
+
pattern: bannedWord(term),
|
|
155
231
|
})),
|
|
156
232
|
...bans.spellings.map((term) => ({
|
|
157
233
|
kind: 'spelling' as const,
|
|
158
234
|
term,
|
|
159
|
-
pattern:
|
|
235
|
+
pattern: bannedSpelling(term),
|
|
160
236
|
})),
|
|
161
237
|
]
|
|
162
238
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type BodyLine, visibleText } from '@/markdown/scan'
|
|
2
2
|
|
|
3
3
|
const HEADING = /^#{1,6}\s/
|
|
4
4
|
const LIST_ITEM = /^(\s*)([-*+]|\d+\.)\s+/
|
|
@@ -143,9 +143,13 @@ export function parseCheckpoints(markdown: string): Checkpoints {
|
|
|
143
143
|
*
|
|
144
144
|
* A blank line renders as the gap it is rather than as nothing, which keeps it
|
|
145
145
|
* the distance the source measure already counted it as.
|
|
146
|
+
*
|
|
147
|
+
* The width is measured against what renders, since a rendered line shows a
|
|
148
|
+
* link's anchor text rather than its destination and a source measure
|
|
149
|
+
* over-counts exactly where this rule cares how far a reader travels.
|
|
146
150
|
*/
|
|
147
151
|
export function renderedHeight(text: string, width = RENDER_WIDTH): number {
|
|
148
|
-
return Math.max(1, Math.ceil(text.length / width))
|
|
152
|
+
return Math.max(1, Math.ceil(visibleText(text).length / width))
|
|
149
153
|
}
|
|
150
154
|
|
|
151
155
|
/**
|
|
@@ -173,7 +177,7 @@ function isScannablePeerList(
|
|
|
173
177
|
if (!match) return false
|
|
174
178
|
indents.add(match[1].length)
|
|
175
179
|
items++
|
|
176
|
-
characters += text.length
|
|
180
|
+
characters += visibleText(text).length
|
|
177
181
|
}
|
|
178
182
|
|
|
179
183
|
if (indents.size !== 1) return false
|
|
@@ -297,10 +301,13 @@ export function heavyBullets(
|
|
|
297
301
|
const item = line.text.match(LIST_ITEM)
|
|
298
302
|
const text = line.text.trim()
|
|
299
303
|
|
|
304
|
+
// Structure is read off the raw line and only the weight is masked. A line
|
|
305
|
+
// carrying nothing but an autolink has no visible text at all, and reading
|
|
306
|
+
// its masked form as blank would close the bullet it continues.
|
|
300
307
|
if (item) {
|
|
301
308
|
close()
|
|
302
309
|
if (item[1].length === 0) {
|
|
303
|
-
open = { line: line.number, characters: text.length }
|
|
310
|
+
open = { line: line.number, characters: visibleText(text).length }
|
|
304
311
|
}
|
|
305
312
|
continue
|
|
306
313
|
}
|
|
@@ -312,7 +319,11 @@ export function heavyBullets(
|
|
|
312
319
|
|
|
313
320
|
// The joining space a wrapped line would have carried, so folding two
|
|
314
321
|
// source lines measures what one unwrapped line would have.
|
|
315
|
-
if (open)
|
|
322
|
+
if (open)
|
|
323
|
+
open = {
|
|
324
|
+
...open,
|
|
325
|
+
characters: open.characters + visibleText(text).length + 1,
|
|
326
|
+
}
|
|
316
327
|
}
|
|
317
328
|
|
|
318
329
|
close()
|
|
@@ -353,16 +364,13 @@ export function heavyParagraphs(
|
|
|
353
364
|
if (block.length > 0) {
|
|
354
365
|
const text = block.map((line) => line.text.trim()).join(' ')
|
|
355
366
|
const sentences = countSentences(text)
|
|
367
|
+
const characters = visibleText(text).length
|
|
356
368
|
|
|
357
369
|
if (
|
|
358
370
|
sentences > checkpoints.sentences ||
|
|
359
|
-
|
|
371
|
+
characters > checkpoints.paragraph
|
|
360
372
|
) {
|
|
361
|
-
findings.push({
|
|
362
|
-
line: block[0].number,
|
|
363
|
-
sentences,
|
|
364
|
-
characters: text.length,
|
|
365
|
-
})
|
|
373
|
+
findings.push({ line: block[0].number, sentences, characters })
|
|
366
374
|
}
|
|
367
375
|
}
|
|
368
376
|
block = []
|