@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.
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/auto-ship/SKILL.md +1 -1
- package/claude/skills/canon-cli/SKILL.md +12 -12
- package/claude/skills/canon-operator/SKILL.md +1 -1
- package/claude/skills/create-skill/REQUIREMENT.md +3 -0
- package/claude/skills/create-skill/SKILL.md +3 -1
- package/claude/skills/docs-fold/SKILL.md +1 -1
- package/claude/skills/draft-ready/references/assembly.md +8 -0
- package/claude/skills/memory-review/SKILL.md +4 -4
- package/claude/skills/plan-feature/REQUIREMENT.md +1 -0
- package/claude/skills/plan-feature/SKILL.md +7 -0
- package/claude/skills/review-pr/REQUIREMENT.md +3 -0
- package/claude/skills/review-pr/SKILL.md +5 -3
- package/claude/skills/role-orchestrator/REQUIREMENT.md +2 -0
- package/claude/skills/role-orchestrator/SKILL.md +2 -2
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +14 -14
- package/claude/skills/role-orchestrator/references/orchestrator-handoff.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-poll.md +2 -2
- package/claude/skills/role-worker/REQUIREMENT.md +2 -0
- package/claude/skills/role-worker/SKILL.md +2 -2
- package/claude/skills/session-compact/REQUIREMENT.md +2 -1
- package/claude/skills/session-compact/SKILL.md +5 -1
- package/claude/skills/target-check/references/domains.md +1 -1
- package/claude/skills/write-human/references/source-material.md +2 -2
- package/docs/agents/commands.md +1 -1
- package/docs/agents/context-audit-checks.md +3 -29
- package/docs/agents/context-audit.md +1 -1
- package/docs/agents/install-and-sync.md +24 -18
- package/docs/agents/markdown-audit.md +7 -3
- package/docs/agents/skills-audit.md +8 -1
- package/docs/target-projects.md +2 -2
- package/docs/workflow/ai-workflow.md +9 -9
- package/docs/workflow/operating-model.md +1 -1
- package/docs/workflow/visual-design-workflow.md +1 -1
- package/governance/rules/claude/510-context.md +0 -12
- package/governance/rules/core/005-behavior.md +1 -2
- package/governance/rules/core/015-output.md +1 -3
- package/governance/rules/core/035-tasks.md +1 -3
- package/governance/rules/core/045-memory.md +0 -1
- package/governance/rules/core/055-scratch.md +2 -4
- package/governance/rules/core/070-planning.md +1 -4
- package/governance/rules/core/087-git.md +2 -2
- package/governance/rules/core/090-code-comments.md +1 -1
- package/governance/rules/ui/440-surface-capture.md +2 -0
- package/governance/rules/ui/460-design-taste.md +0 -15
- package/package.json +1 -1
- package/scripts/core/check-capability-seeding.sh +4 -2
- package/src/claude/cases/misc.ts +5 -0
- package/src/claude/skills-audit.ts +59 -2
- package/src/commands/claude.ts +30 -0
- package/src/commands/context.ts +11 -111
- package/src/commands/markdown.ts +63 -2
- package/src/commands/sync.ts +2 -2
- package/src/context/audit.ts +9 -153
- package/src/gate/measures.ts +97 -0
- package/src/gate/stages.ts +21 -2
- package/src/gov/adapter.ts +8 -6
- package/src/markdown/ceiling.ts +70 -0
- package/src/markdown/structure.ts +22 -0
- package/src/records/validate.ts +5 -1
- package/src/sync/check.ts +7 -1
- package/src/sync/engine.ts +90 -10
- package/src/sync/stamp.ts +44 -0
- package/src/tooling/inject.ts +13 -5
- package/src/tooling/manifest.ts +9 -0
- package/src/tooling/scan.ts +23 -6
- package/src/tooling/subfolder.ts +3 -3
- package/standards/context.md +2 -2
- package/standards/markdown.md +2 -0
- package/standards/plan.md +24 -1
- package/standards/skill.md +1 -0
- package/tooling/base/reference.md +2 -2
- package/tooling/web/configs/e2e/screenshot.ts +43 -7
- package/tooling/web/reference.md +1 -1
- package/src/context/narration.ts +0 -99
- /package/tooling/base/{configs → seeds}/.github/pull_request_template.md +0 -0
- /package/tooling/base/{configs → seeds}/.github/workflows/verify.yml +0 -0
package/src/commands/claude.ts
CHANGED
|
@@ -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
|
}
|
package/src/commands/context.ts
CHANGED
|
@@ -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,
|
|
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,
|
|
137
|
-
'
|
|
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
|
|
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
|
|
671
|
-
//
|
|
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
|
|
726
|
-
//
|
|
727
|
-
//
|
|
728
|
-
//
|
|
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',
|
package/src/commands/markdown.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
+
}
|
package/src/commands/sync.ts
CHANGED
|
@@ -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
|
package/src/context/audit.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
*
|
|
160
|
-
* sections
|
|
161
|
-
*
|
|
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
|
|
218
|
-
* `
|
|
219
|
-
*
|
|
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
|
),
|