@erclx/aitk 0.106.0 → 0.107.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aitk",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "0.106.0",
4
+ "version": "0.107.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -39,6 +39,12 @@ A line is wrapped at the width it renders at, so a link counts as its anchor tex
39
39
 
40
40
  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.
41
41
 
42
+ Every entry past the checkpoint carries the three questions the standard asks of it rather than a count alone: whether it still covers one domain, whether it has filled with content `ls` or `--help` reproduces, and whether it has accumulated the history of its own changes. The checkpoint is not a cap, so the count is not the finding. What a reader acts on is which question came back true, and the standard directs the fix at that rather than at the number.
43
+
44
+ Only the third is mechanical, and it is the provenance check already reported below, so the length finding cites that count rather than measuring it again. An entry outside the governed folder reads open there too, since provenance is scoped to the standard stating it and a clean list there is a scan that never ran. The other two stay open beside every entry, because whether a domain is still one domain is a judgment about its subject and recognizing reproduced content needs a reader who knows what those commands emit. Omitting them would read as an entry nothing found rather than one nothing checked.
45
+
46
+ It reports and never gates, which the standard settles rather than the usual judgment-against-fact line: there is no hard cap, so an entry that answers all three and is still long is a correct outcome. The JSON record carries the joined findings as `length`, each with a `causes` array in the standard's order, and the jurisdiction the join reads as `entries[].governed`. That flag is what parts a provenance list measured and empty from one never scanned, which the count alone cannot say. The join is published rather than left to a consumer, since deriving it means restating which question the provenance count answers.
47
+
42
48
  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.
43
49
 
44
50
  ## Tables
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/aitk",
3
3
  "type": "module",
4
- "version": "0.106.0",
4
+ "version": "0.107.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -3,6 +3,10 @@ import type { Command } from 'commander'
3
3
  import {
4
4
  type EntryReport,
5
5
  governsContent,
6
+ type LengthCause,
7
+ type LengthFinding,
8
+ lengthFindings,
9
+ type LengthQuestion,
6
10
  LENGTH_CHECKPOINT,
7
11
  matchesSiblings,
8
12
  measureFolders,
@@ -200,6 +204,7 @@ async function runAudit(
200
204
  )
201
205
  const drift = gateOnly ? [] : await auditIndexes(folders)
202
206
  const sections = gateOnly ? [] : missingSections(root, folders, entries)
207
+ const length = gateOnly ? undefined : lengthFindings(entries)
203
208
 
204
209
  if (gateOnly) {
205
210
  reportGate(citations)
@@ -209,7 +214,7 @@ async function runAudit(
209
214
  reportCitations(citations, cited)
210
215
  reportReferenceForm(entries, folders)
211
216
  reportSections(sections, folders)
212
- reportLength(entries)
217
+ reportLength(length ?? [])
213
218
  reportTables(entries)
214
219
  reportProvenance(entries, folders)
215
220
  reportNarration(entries, folders, narration)
@@ -234,6 +239,16 @@ async function runAudit(
234
239
  unresolved: citations.unresolved,
235
240
  },
236
241
  entries,
242
+ // The join is published rather than left to a consumer, since deriving
243
+ // it from `entries` means restating which question the provenance count
244
+ // answers, and one wrong restatement is a cause reported against the
245
+ // wrong entry.
246
+ //
247
+ // Absent rather than empty under `--citations-only`, for the reason
248
+ // `checkpoints.narration` below carries. That mode measures no entry,
249
+ // so an empty array here reads as a corpus with nothing past the
250
+ // checkpoint rather than as a run that never looked.
251
+ length,
237
252
  missingSections: sections,
238
253
  indexDrift: drift,
239
254
  checkpoints: {
@@ -475,7 +490,32 @@ function reportSections(
475
490
  )
476
491
  }
477
492
 
478
- function reportLength(entries: readonly EntryReport[]): void {
493
+ /** How each question reads in the report, in the standard's own order. */
494
+ const QUESTION_LABEL: Record<LengthQuestion, string> = {
495
+ domain: 'one domain',
496
+ reproduced: 'reproduced content',
497
+ history: 'own history',
498
+ }
499
+
500
+ function readCause(cause: LengthCause): string {
501
+ const label = QUESTION_LABEL[cause.question]
502
+
503
+ if (cause.state === 'unanswered') return `${label}: open`
504
+ if (cause.state === 'no') return `${label}: no`
505
+
506
+ return `${label}: yes, ${plural(cause.markers ?? 0, 'marker')}`
507
+ }
508
+
509
+ /**
510
+ * Names each entry past the checkpoint with the three questions the standard
511
+ * asks of it, rather than a count a reader has to cross-reference by eye.
512
+ *
513
+ * The checkpoint is not a cap, so the legend says what the list is for. An
514
+ * entry that answers all three and is still long is a correct outcome under a
515
+ * standard stating there is no hard cap, and a run reporting success as a
516
+ * smaller count would have adopted the number the standard declines.
517
+ */
518
+ function reportLength(over: readonly LengthFinding[]): void {
479
519
  logStep('Length')
480
520
  logInfo(
481
521
  `Entries measure rendered lines at ${RENDER_WIDTH} columns, counting frontmatter and fenced blocks.`,
@@ -484,19 +524,26 @@ function reportLength(entries: readonly EntryReport[]): void {
484
524
  'A reference-heavy entry therefore ranks by its examples, which the depth check in `aitk markdown audit` excludes.',
485
525
  )
486
526
 
487
- const over = entries
488
- .filter((entry) => entry.lines > LENGTH_CHECKPOINT)
489
- .sort((a, b) => b.lines - a.lines)
490
-
491
527
  if (over.length === 0) {
492
528
  logInfo(`No entry past the ${LENGTH_CHECKPOINT}-line checkpoint.`)
493
529
  return
494
530
  }
495
531
 
496
532
  logWarn(`${over.length} past the ${LENGTH_CHECKPOINT}-line checkpoint`)
533
+ logInfo(
534
+ 'The checkpoint is not a cap. Each entry carries the three questions the standard asks past it, and the fix goes to whichever is true.',
535
+ )
536
+ logInfo(
537
+ 'Own history is answered from the provenance markers below. The other two are judgments no measure settles, so they stay open for a reader.',
538
+ )
497
539
  pipeOutput(
498
540
  over
499
- .map((entry) => `${entry.rel} ${entry.lines} rendered lines`)
541
+ .map(
542
+ (finding) =>
543
+ `${finding.rel} ${finding.lines} rendered lines\n ${finding.causes
544
+ .map(readCause)
545
+ .join(' ')}`,
546
+ )
500
547
  .join('\n'),
501
548
  )
502
549
  }
@@ -166,6 +166,52 @@ export interface EntryReport {
166
166
  * exempt from owing sections rather than from being well formed.
167
167
  */
168
168
  readonly stub: boolean
169
+ /**
170
+ * Whether a content rule reached this entry, which is what parts an empty
171
+ * `provenance` list that was measured from one that was never scanned. The
172
+ * length finding reads it, since reporting a clean history for an entry
173
+ * outside the governed folder answers a question nothing asked.
174
+ */
175
+ readonly governed: boolean
176
+ }
177
+
178
+ /**
179
+ * The three questions `standards/context.md` asks of an entry past the
180
+ * checkpoint, in the order it states them.
181
+ *
182
+ * The standard calls no entry over the checkpoint wrong. It asks whether the
183
+ * entry still covers a single domain, whether it has filled with content `ls`
184
+ * or `--help` reproduces, and whether it has accumulated the history of its own
185
+ * changes, then directs a fix at whichever is true. A report naming the count
186
+ * alone leaves all three unasked, which is why the finding carries them.
187
+ */
188
+ export type LengthQuestion = 'domain' | 'reproduced' | 'history'
189
+
190
+ /**
191
+ * What the audit can say about one question for one entry.
192
+ *
193
+ * `unanswered` is a state rather than an omission. Two of the three questions
194
+ * are judgments no measure settles, and dropping them would read as an entry
195
+ * nothing found rather than one nothing checked.
196
+ */
197
+ export type QuestionState = 'yes' | 'no' | 'unanswered'
198
+
199
+ export interface LengthCause {
200
+ readonly question: LengthQuestion
201
+ readonly state: QuestionState
202
+ /**
203
+ * Markers behind a `yes`, and absent wherever nothing was counted. It cites
204
+ * the provenance finding rather than restating it, so the two sections of the
205
+ * report describe the same measurement once.
206
+ */
207
+ readonly markers?: number
208
+ }
209
+
210
+ export interface LengthFinding {
211
+ readonly rel: string
212
+ readonly lines: number
213
+ /** One entry per question in `LengthQuestion` order, never empty. */
214
+ readonly causes: readonly LengthCause[]
169
215
  }
170
216
 
171
217
  export interface SectionFinding {
@@ -481,9 +527,52 @@ export function measureEntry(
481
527
  bareReferences: bareReferences(lines, siblings),
482
528
  sections: governsContent ? declaredSections(lines) : [],
483
529
  stub: isStubSeed(source),
530
+ governed: governsContent,
484
531
  }
485
532
  }
486
533
 
534
+ /**
535
+ * Names each entry past the checkpoint with the standard's three questions
536
+ * answered as far as anything measures them.
537
+ *
538
+ * Only accumulated history is mechanical, and it is already measured by the
539
+ * provenance check, so this joins that count rather than counting again. The
540
+ * other two are read by a person: whether an entry still covers one domain is a
541
+ * judgment about its subject, and recognizing content `ls` or `--help`
542
+ * reproduces needs a reader who knows what those emit.
543
+ *
544
+ * An entry outside the governed folder has no measured question at all, since
545
+ * provenance is scoped to the standard stating it, and reporting `no` there
546
+ * would answer from a scan that never ran.
547
+ *
548
+ * Sorted longest first, which is the order the report already printed and the
549
+ * order the questions are worth asking in.
550
+ */
551
+ export function lengthFindings(
552
+ entries: readonly EntryReport[],
553
+ ): LengthFinding[] {
554
+ return entries
555
+ .filter((entry) => entry.lines > LENGTH_CHECKPOINT)
556
+ .sort((a, b) => b.lines - a.lines)
557
+ .map((entry) => ({
558
+ rel: entry.rel,
559
+ lines: entry.lines,
560
+ causes: [
561
+ { question: 'domain', state: 'unanswered' },
562
+ { question: 'reproduced', state: 'unanswered' },
563
+ historyCause(entry),
564
+ ] satisfies LengthCause[],
565
+ }))
566
+ }
567
+
568
+ function historyCause(entry: EntryReport): LengthCause {
569
+ if (!entry.governed) return { question: 'history', state: 'unanswered' }
570
+
571
+ return entry.provenance.length > 0
572
+ ? { question: 'history', state: 'yes', markers: entry.provenance.length }
573
+ : { question: 'history', state: 'no' }
574
+ }
575
+
487
576
  /**
488
577
  * Measures every entry in the audited folders. A generated `index.md` is not
489
578
  * among them, since its body is rewritten on every regen and no checkpoint