@erclx/canon 4.116.0 → 4.118.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.
Files changed (77) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/auto-ship/SKILL.md +1 -1
  3. package/claude/skills/canon-cli/SKILL.md +12 -12
  4. package/claude/skills/canon-operator/SKILL.md +1 -1
  5. package/claude/skills/create-skill/REQUIREMENT.md +3 -0
  6. package/claude/skills/create-skill/SKILL.md +3 -1
  7. package/claude/skills/docs-fold/SKILL.md +1 -1
  8. package/claude/skills/draft-ready/references/assembly.md +8 -0
  9. package/claude/skills/memory-review/SKILL.md +4 -4
  10. package/claude/skills/plan-feature/REQUIREMENT.md +1 -0
  11. package/claude/skills/plan-feature/SKILL.md +7 -0
  12. package/claude/skills/review-pr/REQUIREMENT.md +3 -0
  13. package/claude/skills/review-pr/SKILL.md +5 -3
  14. package/claude/skills/role-orchestrator/REQUIREMENT.md +2 -0
  15. package/claude/skills/role-orchestrator/SKILL.md +2 -2
  16. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +14 -14
  17. package/claude/skills/role-orchestrator/references/orchestrator-handoff.md +1 -1
  18. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +2 -2
  19. package/claude/skills/role-worker/REQUIREMENT.md +2 -0
  20. package/claude/skills/role-worker/SKILL.md +2 -2
  21. package/claude/skills/session-compact/REQUIREMENT.md +2 -1
  22. package/claude/skills/session-compact/SKILL.md +5 -1
  23. package/claude/skills/target-check/references/domains.md +1 -1
  24. package/claude/skills/write-human/references/source-material.md +2 -2
  25. package/docs/agents/commands.md +1 -1
  26. package/docs/agents/context-audit-checks.md +3 -29
  27. package/docs/agents/context-audit.md +1 -1
  28. package/docs/agents/install-and-sync.md +24 -18
  29. package/docs/agents/markdown-audit.md +7 -3
  30. package/docs/agents/skills-audit.md +8 -1
  31. package/docs/target-projects.md +2 -2
  32. package/docs/workflow/ai-workflow.md +9 -9
  33. package/docs/workflow/operating-model.md +1 -1
  34. package/docs/workflow/visual-design-workflow.md +1 -1
  35. package/governance/rules/claude/510-context.md +0 -12
  36. package/governance/rules/core/005-behavior.md +1 -2
  37. package/governance/rules/core/015-output.md +1 -3
  38. package/governance/rules/core/035-tasks.md +1 -3
  39. package/governance/rules/core/045-memory.md +0 -1
  40. package/governance/rules/core/055-scratch.md +2 -4
  41. package/governance/rules/core/070-planning.md +1 -4
  42. package/governance/rules/core/087-git.md +2 -2
  43. package/governance/rules/core/090-code-comments.md +1 -1
  44. package/governance/rules/ui/440-surface-capture.md +2 -0
  45. package/governance/rules/ui/460-design-taste.md +0 -15
  46. package/package.json +1 -1
  47. package/scripts/core/check-capability-seeding.sh +4 -2
  48. package/src/claude/cases/misc.ts +5 -0
  49. package/src/claude/skills-audit.ts +59 -2
  50. package/src/commands/claude.ts +30 -0
  51. package/src/commands/context.ts +11 -111
  52. package/src/commands/markdown.ts +63 -2
  53. package/src/commands/sync.ts +2 -2
  54. package/src/context/audit.ts +9 -153
  55. package/src/gate/measures.ts +97 -0
  56. package/src/gate/stages.ts +21 -2
  57. package/src/gov/adapter.ts +8 -6
  58. package/src/markdown/ceiling.ts +70 -0
  59. package/src/markdown/structure.ts +22 -0
  60. package/src/records/validate.ts +5 -1
  61. package/src/sync/check.ts +7 -1
  62. package/src/sync/engine.ts +90 -10
  63. package/src/sync/stamp.ts +44 -0
  64. package/src/tooling/inject.ts +13 -5
  65. package/src/tooling/manifest.ts +9 -0
  66. package/src/tooling/scan.ts +23 -6
  67. package/src/tooling/subfolder.ts +3 -3
  68. package/standards/context.md +2 -2
  69. package/standards/markdown.md +2 -0
  70. package/standards/plan.md +24 -1
  71. package/standards/skill.md +1 -0
  72. package/tooling/base/reference.md +2 -2
  73. package/tooling/web/configs/e2e/screenshot.ts +43 -7
  74. package/tooling/web/reference.md +1 -1
  75. package/src/context/narration.ts +0 -99
  76. /package/tooling/base/{configs → seeds}/.github/pull_request_template.md +0 -0
  77. /package/tooling/base/{configs → seeds}/.github/workflows/verify.yml +0 -0
@@ -1397,6 +1397,7 @@ async function runSkillsAudit(
1397
1397
  reportFrontmatter(report)
1398
1398
  reportFolder(report)
1399
1399
  reportRequirementShape(report)
1400
+ reportProvenance(report)
1400
1401
  reportUnmeasured()
1401
1402
  outro()
1402
1403
  }
@@ -1418,6 +1419,7 @@ async function runSkillsAudit(
1418
1419
  readme: report.readme,
1419
1420
  folderName: report.folderName,
1420
1421
  requirementSections: report.requirementSections,
1422
+ datedProvenance: report.datedProvenance,
1421
1423
  },
1422
1424
  checkpoints: {
1423
1425
  descriptionLimit: DESCRIPTION_LIMIT,
@@ -1588,6 +1590,31 @@ function reportRequirementShape(report: SkillsAudit): void {
1588
1590
  reportFindings(report.requirementSections)
1589
1591
  }
1590
1592
 
1593
+ /**
1594
+ * Names where the date belongs, since the finding is a date to move rather
1595
+ * than one to delete, and reports without failing the run for the same reason
1596
+ * the frontmatter measures do.
1597
+ */
1598
+ function reportProvenance(report: SkillsAudit): void {
1599
+ logStep('Provenance')
1600
+ logInfo(
1601
+ 'A SKILL.md and its references state the current rule, with no ISO date outside a fence or code span.',
1602
+ )
1603
+ logInfo(
1604
+ 'The incident and date that earned a rule go in REQUIREMENT.md under Gap, or stay in git.',
1605
+ )
1606
+
1607
+ if (report.datedProvenance.length === 0) {
1608
+ logInfo('No body or reference carries a date.')
1609
+ return
1610
+ }
1611
+
1612
+ logWarn(
1613
+ `${plural(report.datedProvenance.length, 'date')} in a body or reference`,
1614
+ )
1615
+ reportFindings(report.datedProvenance)
1616
+ }
1617
+
1591
1618
  /**
1592
1619
  * Stated on every run, including the run where everything above passed. A
1593
1620
  * report that lists only what it measured reads as a verdict on the standard
@@ -1604,4 +1631,7 @@ function reportUnmeasured(): void {
1604
1631
  logInfo(
1605
1632
  'The 150-line body checkpoint is mechanical and still absent here, and it would print a count rather than a defect.',
1606
1633
  )
1634
+ logInfo(
1635
+ 'Undated provenance, such as "a pull request dropped...", and a date written in words or without its day are not read.',
1636
+ )
1607
1637
  }
@@ -56,12 +56,6 @@ import {
56
56
  } from '@/context/folders'
57
57
  import { isGating } from '@/context/gate'
58
58
  import { auditIndexes, type FolderDrift } from '@/context/index-drift'
59
- import {
60
- loadNarration,
61
- type Narration,
62
- PRONOUN_HEADING,
63
- VERB_HEADING,
64
- } from '@/context/narration'
65
59
  import {
66
60
  measureWireframeFolder,
67
61
  type WireframeStatesReport,
@@ -101,7 +95,7 @@ export function register(program: Command): void {
101
95
  context
102
96
  .command('audit')
103
97
  .description(
104
- 'Report required sections, entry length, citations, reference form, catalog tables, provenance, superseded-decision narration, index drift, the architecture record against its own ceiling, its own entry cap, and its word weight, and wireframe states against their evidence folders',
98
+ 'Report required sections, entry length, citations, reference form, catalog tables, provenance, index drift, the architecture record against its own ceiling, its own entry cap, and its word weight, and wireframe states against their evidence folders',
105
99
  )
106
100
  .argument('[path]', 'Project root, defaulting to the current directory')
107
101
  .helpOption('-h, --help', 'Show this help message')
@@ -133,8 +127,8 @@ export function register(program: Command): void {
133
127
  'findings that are facts rather than judgments: a missing required',
134
128
  'section and index drift. A context entry requires Overview and',
135
129
  'Layout, and a wireframe requires Regions, States, Copy, and Not on',
136
- 'this surface. Entry length, reference form, table, provenance, narration,',
137
- 'and the record claim classification are judgments under both.',
130
+ 'this surface. Entry length, reference form, table, provenance, and the',
131
+ 'record claim classification are judgments under both.',
138
132
  '',
139
133
  'Depth and bullet weight are stated over every markdown file rather',
140
134
  'than over a context entry, so `canon markdown audit` measures them.',
@@ -653,24 +647,13 @@ async function runAudit(
653
647
  )
654
648
  }
655
649
 
656
- const narration: Narration = gateOnly
657
- ? { kind: 'absent' }
658
- : await loadNarration(root)
659
- const entries = gateOnly
660
- ? []
661
- : await measureFolders(
662
- root,
663
- folders,
664
- narration.kind === 'loaded' ? narration : undefined,
665
- )
650
+ const entries = gateOnly ? [] : await measureFolders(root, folders)
666
651
  const drift = gateOnly ? [] : await auditIndexes(folders)
667
652
  const sections = gateOnly ? [] : missingSections(root, folders, entries)
668
653
  const length = gateOnly ? undefined : lengthFindings(entries)
669
654
  // Absent under `--citations-only` and null when the project carries no
670
- // record, for the reason `checkpoints.narration` states about its own two
671
- // absences. A run that never looked and a project with nothing to look at
672
- // are different answers, and one value for both reports the second as the
673
- // first.
655
+ // record. A run that never looked and a project with nothing to look at are
656
+ // different answers, and one value for both reports the second as the first.
674
657
  const record = gateOnly ? undefined : await measureArchitecture(root)
675
658
  const wireframes = gateOnly
676
659
  ? []
@@ -693,7 +676,6 @@ async function runAudit(
693
676
  reportLength(length ?? [])
694
677
  reportTables(entries)
695
678
  reportProvenance(entries, folders)
696
- reportNarration(entries, folders, narration)
697
679
  reportDrift(drift)
698
680
  reportRecord(record, root)
699
681
  reportWireframeStates(wireframes)
@@ -722,10 +704,11 @@ async function runAudit(
722
704
  // answers, and one wrong restatement is a cause reported against the
723
705
  // wrong entry.
724
706
  //
725
- // Absent rather than empty under `--citations-only`, for the reason
726
- // `checkpoints.narration` below carries. That mode measures no entry,
727
- // so an empty array here reads as a corpus with nothing past the
728
- // checkpoint rather than as a run that never looked.
707
+ // Absent rather than empty under `--citations-only`. That mode
708
+ // measures no entry, so an empty array here reads as a corpus with
709
+ // nothing past the checkpoint rather than as a run that never looked,
710
+ // and collapsing the two reports a measured answer for a run that
711
+ // never opened an entry.
729
712
  length,
730
713
  missingSections: sections,
731
714
  indexDrift: drift,
@@ -747,20 +730,6 @@ async function runAudit(
747
730
  // caller that read it before wireframes owed a section, since turning
748
731
  // it into this map would break that caller without an error.
749
732
  requiredSectionsByFolder: REQUIRED_SECTIONS_BY_FOLDER,
750
- // Three states rather than two, so a record showing no finding says
751
- // which terms were looked for. The key is absent when the run never
752
- // scanned, which is `--citations-only`, and null when it scanned and
753
- // no rule published both headings. Collapsing the first into null
754
- // reports an absent rule against a run that never opened one.
755
- narration: gateOnly
756
- ? undefined
757
- : narration.kind === 'loaded'
758
- ? {
759
- source: narration.source,
760
- pronouns: narration.pronouns,
761
- verbs: narration.verbs,
762
- }
763
- : null,
764
733
  },
765
734
  })}\n`,
766
735
  )
@@ -1144,75 +1113,6 @@ function reportProvenance(
1144
1113
  )
1145
1114
  }
1146
1115
 
1147
- /**
1148
- * Reports the bullets narrating a decision the bullet above them replaced.
1149
- *
1150
- * The reach line names the rule the sets were read from, since the check is
1151
- * silent when no rule publishes them and a run that scanned nothing otherwise
1152
- * prints the same clean line as a run that scanned everything.
1153
- *
1154
- * The legitimate-hit line is here for the same reason the provenance section
1155
- * says a marker is a judgment. The standard keeps a rejected alternative and
1156
- * why it lost, which is a back-reference in the past tense by construction, and
1157
- * no measure separates one from the shape the rule bans.
1158
- */
1159
- function reportNarration(
1160
- entries: readonly EntryReport[],
1161
- folders: readonly AuditedFolder[],
1162
- narration: Narration,
1163
- ): void {
1164
- logStep('Narration')
1165
-
1166
- const governed = folders.filter(governsContent)
1167
- if (governed.length === 0) {
1168
- logInfo(
1169
- `Out of scope. The rule is stated in the standard governing canon/${PROVENANCE_FOLDER}/, and no audited folder is that one.`,
1170
- )
1171
- return
1172
- }
1173
-
1174
- if (narration.kind === 'absent') {
1175
- logWarn(
1176
- `Not scanned. No rule under .claude/rules/ or governance/rules/ publishes both ${PRONOUN_HEADING} and ${VERB_HEADING}.`,
1177
- )
1178
- return
1179
- }
1180
-
1181
- logInfo(
1182
- `Covers canon/${PROVENANCE_FOLDER}/ alone, reading ${plural(narration.pronouns.length, 'pronoun')} and ${plural(narration.verbs.length, 'verb')} from ${narration.source}.`,
1183
- )
1184
- logInfo(
1185
- 'A rejected alternative is a legitimate hit, since the standard keeps what was tried and why it lost.',
1186
- )
1187
-
1188
- const carrying = entries
1189
- .filter((entry) => entry.narration.length > 0)
1190
- .sort((a, b) => b.narration.length - a.narration.length)
1191
-
1192
- if (carrying.length === 0) {
1193
- logInfo('No bullet narrates a decision the bullet above it replaced.')
1194
- return
1195
- }
1196
-
1197
- const total = carrying.reduce((sum, entry) => sum + entry.narration.length, 0)
1198
- logWarn(
1199
- `${plural(total, 'bullet')} to read across ${carrying.length} ${carrying.length === 1 ? 'entry' : 'entries'}`,
1200
- )
1201
- pipeOutput(
1202
- carrying
1203
- .map(
1204
- (entry) =>
1205
- `${entry.rel} ${plural(entry.narration.length, 'bullet')}\n${entry.narration
1206
- .map(
1207
- (found) =>
1208
- ` :${found.line} ${found.pronoun} with ${found.verb}`,
1209
- )
1210
- .join('\n')}`,
1211
- )
1212
- .join('\n'),
1213
- )
1214
- }
1215
-
1216
1116
  /** How each classification reads in the report. */
1217
1117
  const CLAIM_LABEL: Record<string, string> = {
1218
1118
  countable: 'countable claim',
@@ -3,6 +3,7 @@ import { readFile } from 'node:fs/promises'
3
3
  import { dirname, relative, resolve } from 'node:path'
4
4
  import type { Command } from 'commander'
5
5
  import { BAN_SETS, emptyBanSets } from '@/markdown/bans'
6
+ import { lengthExemption } from '@/markdown/ceiling'
6
7
  import { type MarkdownAuditRefusal, resolveMarkdown } from '@/markdown/files'
7
8
  import { isGating } from '@/markdown/gate'
8
9
  import { decodePath, findBrokenLinks, type LinkFinding } from '@/markdown/links'
@@ -16,6 +17,7 @@ import {
16
17
  BASELINE,
17
18
  CHECKPOINTS,
18
19
  type Checkpoints,
20
+ documentHeight,
19
21
  measureStructure,
20
22
  type StructureReport,
21
23
  } from '@/markdown/structure'
@@ -51,6 +53,9 @@ interface FileReport {
51
53
  readonly bans: readonly BanFinding[]
52
54
  readonly links: readonly LinkFinding[]
53
55
  readonly structure: StructureReport
56
+ readonly renderedLines: number
57
+ /** The stated reason, or null where the document is not exempt. */
58
+ readonly exempt: string | null
54
59
  }
55
60
 
56
61
  export function register(program: Command): void {
@@ -62,7 +67,7 @@ export function register(program: Command): void {
62
67
  markdown
63
68
  .command('audit')
64
69
  .description(
65
- 'Fail on a banned character, word, or spelling, and report bullet, paragraph, cadence, and depth',
70
+ 'Fail on a banned character, word, or spelling, and report bullet, paragraph, cadence, depth, and length',
66
71
  )
67
72
  .argument(
68
73
  '[path...]',
@@ -85,6 +90,11 @@ export function register(program: Command): void {
85
90
  'Bullet, paragraph, and depth weight are judgments a reader settles,',
86
91
  'so all three report and none of them fails a run.',
87
92
  '',
93
+ 'Length reports a whole document past the ceiling and never exits 2.',
94
+ 'The Document ceiling gate stage owns that verdict, since this audit also',
95
+ 'runs on a record kept out of version control, where a long plan is not',
96
+ 'a defect.',
97
+ '',
88
98
  'Cadence reports the same way and carries one more caveat. Its range is',
89
99
  'drawn from prose a person reads, so terse reference prose sits below',
90
100
  'it correctly and a flat paragraph there is not a defect.',
@@ -148,7 +158,8 @@ async function runAudit(
148
158
  const reports: FileReport[] = await Promise.all(
149
159
  scope.files.map(async (rel) => {
150
160
  const abs = resolve(root, rel)
151
- const lines = bodyLines(await readFile(abs, 'utf8'))
161
+ const source = await readFile(abs, 'utf8')
162
+ const lines = bodyLines(source)
152
163
  return {
153
164
  rel,
154
165
  bans: scanBans(lines, bans),
@@ -156,6 +167,8 @@ async function runAudit(
156
167
  existsSync(resolve(dirname(abs), path)),
157
168
  ),
158
169
  structure: measureStructure(rel, lines, checkpoints),
170
+ renderedLines: documentHeight(source, checkpoints.renderWidth),
171
+ exempt: lengthExemption(rel, source),
159
172
  }
160
173
  }),
161
174
  )
@@ -168,6 +181,7 @@ async function runAudit(
168
181
  reportParagraphs(reports, checkpoints)
169
182
  reportCadence(reports, checkpoints)
170
183
  reportDepth(reports, checkpoints)
184
+ reportLength(reports, checkpoints)
171
185
  outro()
172
186
 
173
187
  if (opts.json) {
@@ -192,6 +206,7 @@ async function runAudit(
192
206
  cadence: checkpoints.cadence,
193
207
  spread: checkpoints.spread,
194
208
  opener: checkpoints.opener,
209
+ ceiling: checkpoints.ceiling,
195
210
  },
196
211
  entries: reports.map((report) => ({
197
212
  path: report.rel,
@@ -202,6 +217,8 @@ async function runAudit(
202
217
  heavyBullets: report.structure.heavyBullets,
203
218
  heavyParagraphs: report.structure.heavyParagraphs,
204
219
  cadence: report.structure.cadence,
220
+ renderedLines: report.renderedLines,
221
+ exempt: report.exempt,
205
222
  })),
206
223
  })}\n`,
207
224
  )
@@ -606,3 +623,47 @@ function reportDepth(
606
623
  .join('\n'),
607
624
  )
608
625
  }
626
+
627
+ /**
628
+ * Lists every document past the ceiling and counts the exempt ones rather than
629
+ * naming them, since an exemption is settled where its marker sits.
630
+ */
631
+ function reportLength(
632
+ reports: readonly FileReport[],
633
+ checkpoints: Checkpoints,
634
+ ): void {
635
+ logStep('Length')
636
+ logInfo(
637
+ `Whole documents measure rendered lines at ${checkpoints.renderWidth} columns, frontmatter and fenced blocks included.`,
638
+ )
639
+ logInfo(
640
+ 'A changelog is exempt by name, and so is a document carrying a canon-length-exempt marker that names a reason.',
641
+ )
642
+
643
+ const past = reports.filter(
644
+ (report) => report.renderedLines > checkpoints.ceiling,
645
+ )
646
+ const over = past
647
+ .filter((report) => report.exempt === null)
648
+ .sort((a, b) => b.renderedLines - a.renderedLines)
649
+ const exempt = past.length - over.length
650
+
651
+ if (over.length === 0) {
652
+ logInfo(`No document past the ${checkpoints.ceiling}-line ceiling.`)
653
+ } else {
654
+ logWarn(
655
+ `${plural(over.length, 'document')} past the ${checkpoints.ceiling}-line ceiling`,
656
+ )
657
+ pipeOutput(
658
+ over
659
+ .map(
660
+ (report) => `${report.rel} ${report.renderedLines} rendered lines`,
661
+ )
662
+ .join('\n'),
663
+ )
664
+ }
665
+
666
+ if (exempt > 0) {
667
+ logInfo(`${plural(exempt, 'exempt document')} past the ceiling.`)
668
+ }
669
+ }
@@ -133,9 +133,9 @@ function renderCheck(report: CheckReport): void {
133
133
  logWarn(entry.notice ?? `${entry.rel} (${entry.state})`)
134
134
  }
135
135
 
136
- const { stale, customized, drifted, stranded, orphaned, missing } =
136
+ const { stale, customized, drifted, stranded, retired, orphaned, missing } =
137
137
  domain.counts
138
- if (stale + customized + drifted + stranded === 0) {
138
+ if (stale + customized + drifted + stranded + retired === 0) {
139
139
  logInfo(orphaned === 0 ? 'up to date' : `up to date (${orphaned} local)`)
140
140
  }
141
141
  // A local file is not a deficiency and a missing rule is, so this never
@@ -2,9 +2,8 @@ import { readFile } from 'node:fs/promises'
2
2
  import { basename, relative } from 'node:path'
3
3
  import { BARE_NAME, IGNORE_MARKER } from '@/context/citations'
4
4
  import type { AuditedFolder } from '@/context/folders'
5
- import type { NarrationTerms } from '@/context/narration'
6
5
  import { type BodyLine, bodyLines, maskDisplayed } from '@/markdown/scan'
7
- import { renderedHeight } from '@/markdown/structure'
6
+ import { documentHeight } from '@/markdown/structure'
8
7
  import { isStubSeed } from '@/seed-marker'
9
8
 
10
9
  /**
@@ -80,10 +79,6 @@ const TABLE_ROW = /^\s*\|/
80
79
  const TABLE_SEPARATOR = /^\s*\|[\s:|-]+\|\s*$/
81
80
  const NAMED_CELL = /`[^`]+`|\[[^\]]+\]\([^)]+\)/
82
81
 
83
- const TOP_BULLET = /^-\s+(.+)$/
84
- /** Any indented line, which is a nested bullet or a wrapped continuation. */
85
- const INSIDE_LIST = /^\s+\S/
86
-
87
82
  /**
88
83
  * Spellings of how the domain reached its shape rather than what it is now.
89
84
  *
@@ -156,9 +151,9 @@ const SENTENCE_END = /[.!?:;]\s(?=[^.!?:;]*$)/
156
151
  /**
157
152
  * The folder whose standard carries the exclusion above.
158
153
  *
159
- * Three measures narrow here rather than one: provenance, the required
160
- * sections, and the superseded-decision narration. The name is the first of
161
- * them because it was the first, and it stays because renaming a constant the
154
+ * Two measures narrow here rather than one: provenance and the required
155
+ * sections. The name is the first of them because it was the first, and it
156
+ * stays because renaming a constant the
162
157
  * JSON record publishes as `checkpoints.provenanceFolder` breaks every consumer
163
158
  * reading that field for a gain of one word.
164
159
  *
@@ -202,21 +197,14 @@ export interface BareReferenceFinding {
202
197
  readonly name: string
203
198
  }
204
199
 
205
- export interface NarrationFinding {
206
- readonly line: number
207
- /** The opening that points back at the bullet above. */
208
- readonly pronoun: string
209
- /** The past-tense verb that turns the back-reference into a narration. */
210
- readonly verb: string
211
- }
212
-
213
200
  export interface EntryReport {
214
201
  readonly rel: string
215
202
  /**
216
203
  * Rendered lines across the whole file, counting frontmatter and fenced
217
- * blocks. It shares `renderedHeight` with the depth checkpoint in
218
- * `src/markdown/structure.ts`, since the two sit in one section of the
219
- * standard and a reader compares them.
204
+ * blocks. It counts through `documentHeight` in `src/markdown/structure.ts`,
205
+ * which shares `renderedHeight` with the depth checkpoint and with the
206
+ * document ceiling, since all three sit beside each other in the standard
207
+ * and a reader compares them.
220
208
  *
221
209
  * Fences are counted here and excluded there. The depth measure skips one so
222
210
  * an example cannot break the run around it, and a file measure has no run to
@@ -228,12 +216,6 @@ export interface EntryReport {
228
216
  readonly catalogTables: readonly TableFinding[]
229
217
  /** Empty for an entry no standard bans a change narrative in. */
230
218
  readonly provenance: readonly ProvenanceFinding[]
231
- /**
232
- * Empty outside the governed folder, and empty on a run whose caller loaded
233
- * no term sets. The report distinguishes the two from the vocabulary itself,
234
- * since an empty list here is silent about which one produced it.
235
- */
236
- readonly narration: readonly NarrationFinding[]
237
219
  /**
238
220
  * References naming a sibling entry by bare filename, and empty for a caller
239
221
  * that passed no sibling names. Which entries have siblings worth matching is
@@ -433,125 +415,6 @@ function stampsMeasurement(text: string, index: number): boolean {
433
415
  return STAMPING.test(clause) || STAMPING_RUN.test(clause)
434
416
  }
435
417
 
436
- function escape(term: string): string {
437
- return term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
438
- }
439
-
440
- /**
441
- * Copulas that turn the verb behind them into a present-tense passive.
442
- *
443
- * `used to` is the term this exists for, since `is used to resolve the folder`
444
- * is the passive of `use` rather than the past habitual the set means. This
445
- * list is English grammar rather than corpus vocabulary, so it stays in code
446
- * while the two tunable sets stay in the rule, and a rule publishing two of
447
- * three headings would be a fourth absent state to carry for no tuning anyone
448
- * wants.
449
- *
450
- * What it costs is the perfect passive. `has been superseded` narrates a
451
- * supersession and is rejected with the rest, which is a recall gap taken
452
- * knowingly on the trade this check already makes everywhere else.
453
- */
454
- const COPULA = ['is', 'are', 'be', 'been', 'being', 'was', 'were']
455
-
456
- /**
457
- * Finds the bullets narrating a decision the bullet above them replaced.
458
- *
459
- * The signal is structural rather than lexical, and the corpus is what decides
460
- * that. The terms carrying clean signal for a supersession are too rare to
461
- * catch anything, and the one term that would have caught the known case is
462
- * `now`, which appears 57 times across 24 entries in correct present-tense
463
- * prose. What separates the shape instead is a bullet pointing back at its
464
- * sibling and putting the sibling's design in the past: an opening pronoun with
465
- * no antecedent of its own, plus a past-tense verb, plus a bullet above it to
466
- * refer to. Precision is the whole value, so recall is the accepted exposure
467
- * and a narration written as a single bullet is not reached.
468
- *
469
- * The pronoun is matched cased and anchored, since a mid-sentence `this` is a
470
- * determiner rather than a back-reference. The verb is matched uncased anywhere
471
- * in the bullet, since the tense is what carries the signal wherever it sits,
472
- * and rejected behind a `COPULA` for the reason stated there. Displayed spans
473
- * are masked so a term quoted in backticks is not read as prose the entry
474
- * writes, and fenced blocks are skipped for the reason the scans above skip
475
- * them.
476
- *
477
- * A blank line does not end the run. Markdown reads two bullets around one as a
478
- * single loose list, so breaking there would leave the shape reachable by
479
- * anyone who spaced their bullets out. What ends a run is content that is
480
- * neither a bullet nor indented under one, which the fenced branch below has to
481
- * answer for itself because a fenced line is skipped before that test.
482
- */
483
- function narration(
484
- lines: readonly BodyLine[],
485
- terms: NarrationTerms,
486
- ): NarrationFinding[] {
487
- if (terms.pronouns.length === 0 || terms.verbs.length === 0) return []
488
-
489
- const findings: NarrationFinding[] = []
490
- let following = false
491
- let fenceBlock: number | undefined
492
- let fenceInsideList = false
493
-
494
- for (const line of lines) {
495
- // A fenced line is never a bullet, but an unindented block still ends the
496
- // run. CommonMark reads a fence at column zero as interrupting the list, so
497
- // the bullets around it are two lists and the second has no antecedent
498
- // above it. A block indented under its bullet stays inside the item.
499
- //
500
- // The opening delimiter decides for every line of its own block. Reading
501
- // each line instead ends the run on a blank line inside an indented fence,
502
- // which has no indentation to read, and on a content line at column zero,
503
- // which CommonMark permits since only the fence's own indent is stripped.
504
- //
505
- // The block index is what re-opens that decision, so two blocks written
506
- // with nothing between them are answered separately rather than the second
507
- // inheriting the first. Indentation stays the test here because what
508
- // interrupts a list is this check's own judgment, and the walker only says
509
- // which lines share a block.
510
- if (line.fenced) {
511
- if (line.fenceBlock !== fenceBlock) {
512
- fenceBlock = line.fenceBlock
513
- fenceInsideList = INSIDE_LIST.test(line.text)
514
- }
515
- if (!fenceInsideList) following = false
516
- continue
517
- }
518
-
519
- if (line.text.trim() === '') continue
520
-
521
- const bullet = line.text.match(TOP_BULLET)
522
- if (!bullet) {
523
- // A nested bullet and a wrapped continuation both sit inside the list, so
524
- // neither ends the run. Anything else does, which is what keeps a bullet
525
- // opening the list under a heading from reading as a reply to the last
526
- // bullet of the list before it.
527
- if (!INSIDE_LIST.test(line.text)) following = false
528
- continue
529
- }
530
-
531
- const text = maskDisplayed(bullet[1])
532
-
533
- if (following) {
534
- const pronoun = terms.pronouns.find((term) =>
535
- new RegExp(`^${escape(term)}\\b`).test(text),
536
- )
537
- const verb = pronoun
538
- ? terms.verbs.find((term) =>
539
- new RegExp(
540
- `(?<!\\b(?:${COPULA.join('|')})\\s+)\\b${escape(term)}\\b`,
541
- 'i',
542
- ).test(text),
543
- )
544
- : undefined
545
-
546
- if (pronoun && verb) findings.push({ line: line.number, pronoun, verb })
547
- }
548
-
549
- following = true
550
- }
551
-
552
- return findings
553
- }
554
-
555
418
  /**
556
419
  * Finds which required sections the entry declares.
557
420
  *
@@ -638,7 +501,6 @@ export function measureEntry(
638
501
  rel: string,
639
502
  source: string,
640
503
  governsContent = true,
641
- terms?: NarrationTerms,
642
504
  siblings: readonly string[] = [],
643
505
  required: readonly string[] = governsContent ? REQUIRED_SECTIONS : [],
644
506
  ): EntryReport {
@@ -646,13 +508,9 @@ export function measureEntry(
646
508
 
647
509
  return {
648
510
  rel,
649
- lines: source
650
- .replace(/\n$/, '')
651
- .split('\n')
652
- .reduce((sum, text) => sum + renderedHeight(text), 0),
511
+ lines: documentHeight(source),
653
512
  catalogTables: catalogTables(lines),
654
513
  provenance: governsContent ? provenance(lines) : [],
655
- narration: governsContent && terms ? narration(lines, terms) : [],
656
514
  bareReferences: bareReferences(lines, siblings),
657
515
  sections: declaredSections(lines, required),
658
516
  stub: isStubSeed(source),
@@ -716,7 +574,6 @@ function historyCause(entry: EntryReport): LengthCause {
716
574
  export async function measureFolders(
717
575
  root: string,
718
576
  folders: readonly AuditedFolder[],
719
- terms?: NarrationTerms,
720
577
  ): Promise<EntryReport[]> {
721
578
  const reports: EntryReport[] = []
722
579
 
@@ -733,7 +590,6 @@ export async function measureFolders(
733
590
  relative(root, path),
734
591
  await readFile(path, 'utf8'),
735
592
  governsContent(folder),
736
- terms,
737
593
  names.filter((name) => name !== self),
738
594
  requiredSections(folder),
739
595
  ),