@erclx/canon 5.37.0 → 5.39.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/README.md +1 -1
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/api-design/SKILL.md +12 -9
- package/claude/skills/auto-ship/SKILL.md +43 -23
- package/claude/skills/backlog-triage/SKILL.md +62 -53
- package/claude/skills/backlog-triage/references/apply.md +69 -0
- package/claude/skills/build-in-slices/SKILL.md +8 -4
- package/claude/skills/canon-cli/SKILL.md +5 -1
- package/claude/skills/canon-feedback/SKILL.md +18 -8
- package/claude/skills/canon-feedback-triage/SKILL.md +10 -3
- package/claude/skills/canvas-board/SKILL.md +36 -17
- package/claude/skills/ci-workflow/SKILL.md +5 -4
- package/claude/skills/code-craft/SKILL.md +10 -6
- package/claude/skills/codebase-layout/SKILL.md +3 -5
- package/claude/skills/context-fold/REQUIREMENT.md +4 -4
- package/claude/skills/context-fold/SKILL.md +42 -23
- package/claude/skills/context-fold/references/context-refresh.md +4 -3
- package/claude/skills/context-fold/references/context.md +79 -64
- package/claude/skills/create-rule/SKILL.md +7 -3
- package/claude/skills/create-skill/SKILL.md +27 -7
- package/claude/skills/create-skill/references/skill-reference.md +41 -17
- package/claude/skills/decision-escalate/SKILL.md +8 -5
- package/claude/skills/deploy-app/SKILL.md +1 -1
- package/claude/skills/deprecation-migration/SKILL.md +12 -4
- package/claude/skills/design-taste/SKILL.md +17 -11
- package/claude/skills/docs-sync/REQUIREMENT.md +1 -0
- package/claude/skills/docs-sync/SKILL.md +17 -6
- package/claude/skills/docs-sync/references/docs.md +54 -21
- package/claude/skills/docs-sync/references/readme.md +32 -8
- package/claude/skills/document-health/SKILL.md +3 -4
- package/claude/skills/draft-and-pick/SKILL.md +73 -47
- package/claude/skills/draft-doc/SKILL.md +9 -4
- package/claude/skills/draft-doc/references/context.md +4 -4
- package/claude/skills/draft-doc/references/docs.md +1 -0
- package/claude/skills/draft-doc/references/wiki.md +3 -2
- package/claude/skills/draft-figure/SKILL.md +9 -7
- package/claude/skills/draft-identity/SKILL.md +24 -52
- package/claude/skills/draft-identity/references/card-route.md +46 -0
- package/claude/skills/draft-ready/SKILL.md +19 -7
- package/claude/skills/draft-screencast/SKILL.md +6 -112
- package/claude/skills/draft-screencast/references/beats-template.md +119 -0
- package/claude/skills/draft-slides/SKILL.md +7 -4
- package/claude/skills/git-branch/SKILL.md +3 -3
- package/claude/skills/git-commit/SKILL.md +5 -3
- package/claude/skills/git-followup/SKILL.md +16 -9
- package/claude/skills/git-issue/SKILL.md +6 -5
- package/claude/skills/git-issue/references/issue.md +11 -10
- package/claude/skills/git-pr/SKILL.md +56 -44
- package/claude/skills/git-ship/SKILL.md +43 -34
- package/claude/skills/git-ship/references/ci-watch.md +31 -0
- package/claude/skills/git-split/SKILL.md +28 -100
- package/claude/skills/git-split/references/independent.md +40 -0
- package/claude/skills/git-split/references/stacked.md +73 -0
- package/claude/skills/git-stage/SKILL.md +9 -7
- package/claude/skills/git-worktree/SKILL.md +27 -49
- package/claude/skills/git-worktree/references/cleanup.md +55 -0
- package/claude/skills/markdown-craft/SKILL.md +2 -2
- package/claude/skills/markdown-craft/references/markdown.md +82 -52
- package/claude/skills/plan-feature/SKILL.md +52 -46
- package/claude/skills/plan-feature/references/batches.md +46 -0
- package/claude/skills/plan-groundwork/SKILL.md +60 -74
- package/claude/skills/plan-groundwork/references/groundwork.md +53 -24
- package/claude/skills/plan-groundwork/references/output.md +48 -0
- package/claude/skills/plan-intake/SKILL.md +28 -21
- package/claude/skills/plan-intake-answer/SKILL.md +14 -13
- package/claude/skills/read-frames/SKILL.md +2 -0
- package/claude/skills/record-screencast/SKILL.md +31 -11
- package/claude/skills/review-address/SKILL.md +76 -158
- package/claude/skills/review-address/references/rebase-only.md +34 -0
- package/claude/skills/review-craft/SKILL.md +27 -11
- package/claude/skills/review-pr/SKILL.md +91 -40
- package/claude/skills/review-pr/references/post.md +1 -1
- package/claude/skills/review-ui/SKILL.md +62 -37
- package/claude/skills/review-ui/references/hosted-preview.md +30 -0
- package/claude/skills/role-orchestrator/SKILL.md +105 -74
- package/claude/skills/role-orchestrator/references/orchestrator-filing.md +26 -0
- package/claude/skills/role-orchestrator/references/orchestrator-parked.md +1 -1
- package/claude/skills/role-planner/SKILL.md +66 -64
- package/claude/skills/role-reviewer/SKILL.md +57 -46
- package/claude/skills/role-worker/SKILL.md +68 -63
- package/claude/skills/search-craft/SKILL.md +7 -5
- package/claude/skills/session-compact/SKILL.md +16 -11
- package/claude/skills/session-map/SKILL.md +31 -10
- package/claude/skills/session-relay/SKILL.md +32 -65
- package/claude/skills/session-resume/SKILL.md +18 -13
- package/claude/skills/session-worktree/SKILL.md +70 -42
- package/claude/skills/systematic-debugging/SKILL.md +6 -1
- package/claude/skills/target-check/SKILL.md +13 -6
- package/claude/skills/target-setup/SKILL.md +41 -30
- package/claude/skills/target-setup/references/verify.md +6 -5
- package/claude/skills/task-board/SKILL.md +68 -60
- package/claude/skills/task-board/references/create-report.md +36 -0
- package/claude/skills/task-board/references/tasks.md +3 -1
- package/claude/skills/task-complete/SKILL.md +20 -18
- package/claude/skills/teach-workspace/SKILL.md +61 -33
- package/claude/skills/test-craft/SKILL.md +2 -1
- package/claude/skills/test-first/SKILL.md +15 -9
- package/claude/skills/ui-checklist/SKILL.md +18 -16
- package/claude/skills/ux-audit/SKILL.md +9 -10
- package/claude/skills/ux-measure/SKILL.md +22 -18
- package/claude/skills/video-craft/SKILL.md +15 -7
- package/claude/skills/write-human/SKILL.md +9 -1
- package/claude/skills/youtube-transcripts/SKILL.md +2 -1
- package/docs/agents/context-audit-checks.md +19 -1
- package/docs/agents/context-audit.md +2 -2
- package/docs/agents/driver.md +1 -1
- package/docs/agents/hooks.md +21 -1
- package/docs/agents/index.md +10 -14
- package/docs/agents/indexes.md +25 -29
- package/docs/agents/install-and-sync.md +124 -235
- package/docs/agents/markdown-audit-density.md +39 -0
- package/docs/agents/markdown-audit-weight.md +9 -7
- package/docs/agents/markdown-audit.md +18 -14
- package/docs/agents/migrate.md +41 -16
- package/docs/agents/output-shape.md +60 -45
- package/docs/agents/overview.md +36 -12
- package/docs/agents/routing.md +1 -1
- package/docs/agents/rule-citations.md +1 -1
- package/docs/agents/skills-reach.md +1 -1
- package/docs/agents/targets.md +25 -77
- package/docs/target/migrations.md +1 -1
- package/docs/target/projects.md +3 -3
- package/docs/target/scenarios.md +1 -1
- package/docs/workflow/ai-workflow.md +2 -2
- package/docs/workflow/parallel-features.md +2 -2
- package/docs/workflow/skill-map.md +1 -1
- package/docs/workflow/visual-design-workflow.md +1 -1
- package/governance/detect.toml +6 -1
- package/package.json +6 -3
- package/scripts/core/install-check.sh +1 -1
- package/src/commands/claude.ts +1 -2
- package/src/commands/context.ts +106 -7
- package/src/commands/hooks.ts +63 -0
- package/src/commands/markdown.ts +169 -32
- package/src/commands/pr.ts +1 -1
- package/src/commands/records.ts +0 -1
- package/src/context/audit.ts +256 -5
- package/src/context/classify/prompts.ts +1 -1
- package/src/context/gate.ts +23 -7
- package/src/context/shape-gated.ts +28 -0
- package/src/design/document.ts +1 -1
- package/src/design/tokens.ts +1 -1
- package/src/docs/read.ts +15 -21
- package/src/gate/measures.ts +15 -190
- package/src/gate/stages.ts +21 -23
- package/src/hooks/reply-links.ts +110 -0
- package/src/markdown/density.ts +258 -0
- package/src/markdown/gate.ts +57 -18
- package/src/markdown/reference-shape.ts +103 -0
- package/src/markdown/scope.ts +174 -0
- package/src/markdown/structure.ts +10 -60
- package/src/sandbox/equivalence.ts +7 -9
- package/src/shipped/references.ts +1 -1
- package/src/teach/browser/head.ts +2 -2
- package/src/teach/browser/sidebar.ts +3 -3
- package/src/teach/browser/theme.ts +2 -2
- package/src/tooling/inject.ts +2 -2
- package/src/tooling/package.ts +15 -18
- package/src/tooling/verify-stack.ts +6 -3
- package/tooling/astro/configs/.oxfmtrc.json +8 -0
- package/tooling/astro/configs/.prettierrc +13 -0
- package/tooling/astro/manifest.toml +13 -0
- package/tooling/astro/reference.md +15 -5
- package/tooling/base/configs/.oxfmtrc.json +7 -0
- package/tooling/base/configs/scripts/verify.ts +1 -1
- package/tooling/base/manifest.toml +6 -6
- package/tooling/base/reference.md +12 -9
- package/tooling/base/seeds/.lintstagedrc +1 -3
- package/tooling/base/seeds/canon/context/ci.md +3 -3
- package/tooling/base/seeds/canon/context/development.md +2 -2
- package/tooling/claude/manifest.toml +5 -1
- package/tooling/claude/seeds/.claude/hooks/reply-links.sh +72 -0
- package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +22 -1
- package/tooling/claude/seeds/.claude/settings.json +4 -0
- package/tooling/go/reference.md +2 -2
- package/tooling/nextjs/configs/.oxlintrc.json +223 -0
- package/tooling/nextjs/manifest.toml +2 -5
- package/tooling/nextjs/reference.md +4 -4
- package/tooling/php/manifest.toml +6 -1
- package/tooling/php/reference.md +2 -2
- package/tooling/python/manifest.toml +6 -1
- package/tooling/python/reference.md +4 -4
- package/tooling/vite-react/manifest.toml +1 -4
- package/tooling/vite-react/reference.md +3 -3
- package/tooling/web/configs/.oxlintrc.json +213 -0
- package/tooling/web/manifest.toml +9 -13
- package/tooling/web/reference.md +14 -14
- package/docs/agents/commands.md +0 -154
- package/docs/agents/docs.md +0 -27
- package/docs/agents/drift-surfaces.md +0 -201
- package/docs/agents/gov-detect.md +0 -47
- package/docs/agents/scripting.md +0 -142
- package/src/markdown/growth.ts +0 -43
- package/tooling/base/configs/.prettierrc +0 -12
- package/tooling/base/seeds/.prettierignore +0 -0
- package/tooling/nextjs/configs/eslint.config.js +0 -91
- package/tooling/web/configs/eslint.config.js +0 -82
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { type BodyLine, visibleText } from '@/markdown/scan'
|
|
2
|
+
import { bulletStacks, CAP } from '@/markdown/stacks'
|
|
3
|
+
import {
|
|
4
|
+
BLOCKQUOTE,
|
|
5
|
+
CHECKPOINTS,
|
|
6
|
+
HEADING,
|
|
7
|
+
isSectionMarker,
|
|
8
|
+
LIST_ITEM,
|
|
9
|
+
TABLE_ROW,
|
|
10
|
+
} from '@/markdown/structure'
|
|
11
|
+
|
|
12
|
+
/** The most prose paragraphs one stretch may carry before a list, table, or heading breaks it. */
|
|
13
|
+
export const PARAGRAPH_RUN = 2
|
|
14
|
+
|
|
15
|
+
/** The deepest heading level a document may use. */
|
|
16
|
+
export const HEADING_DEPTH = 3
|
|
17
|
+
|
|
18
|
+
export type DensityKind =
|
|
19
|
+
| 'paragraph-length'
|
|
20
|
+
| 'paragraph-run'
|
|
21
|
+
| 'bullet-length'
|
|
22
|
+
| 'bullet-run'
|
|
23
|
+
| 'heading-depth'
|
|
24
|
+
| 'h3-opening'
|
|
25
|
+
|
|
26
|
+
export interface DensityFinding {
|
|
27
|
+
readonly kind: DensityKind
|
|
28
|
+
/** The line of the block that breaches, or of a run's first block. */
|
|
29
|
+
readonly line: number
|
|
30
|
+
/** What was measured, or null for the H3 opening, which is a shape rather than a count. */
|
|
31
|
+
readonly value: number | null
|
|
32
|
+
readonly limit: number | null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A block as a reader meets it. `other` is everything that is neither prose,
|
|
37
|
+
* a list item, nor a heading, being a table, a fence, a blockquote, a section
|
|
38
|
+
* marker, and raw HTML, and each one breaks a paragraph run.
|
|
39
|
+
*/
|
|
40
|
+
type Block =
|
|
41
|
+
| { readonly kind: 'heading'; readonly line: number; readonly level: number }
|
|
42
|
+
| { readonly kind: 'paragraph'; readonly line: number; readonly text: string }
|
|
43
|
+
| { readonly kind: 'item'; readonly line: number; readonly text: string }
|
|
44
|
+
| { readonly kind: 'other'; readonly line: number; readonly table: boolean }
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Splits the body into the blocks the six limits read.
|
|
48
|
+
*
|
|
49
|
+
* Its own walk rather than `paragraphBlocks` in `src/markdown/structure.ts`,
|
|
50
|
+
* because the two disagree on one construct and the disagreement is the point.
|
|
51
|
+
* That walk reads an indented paragraph under a list item as a paragraph, and
|
|
52
|
+
* here it belongs to the item: it counts toward the bullet's length and never
|
|
53
|
+
* toward a paragraph run, since a reader sees it inside the list.
|
|
54
|
+
*
|
|
55
|
+
* A nested item is left out of its parent, as `heavyBullets` leaves it, and so
|
|
56
|
+
* is everything indented under the nested item. An indented fence inside an
|
|
57
|
+
* item stays with the item, and a column-zero one ends it.
|
|
58
|
+
*/
|
|
59
|
+
function blocksOf(lines: readonly BodyLine[]): Block[] {
|
|
60
|
+
const blocks: Block[] = []
|
|
61
|
+
let paragraph: { line: number; parts: string[] } | undefined
|
|
62
|
+
let item: { line: number; parts: string[]; nested: boolean } | undefined
|
|
63
|
+
let inFence = false
|
|
64
|
+
let afterBlank = false
|
|
65
|
+
|
|
66
|
+
const closeParagraph = (): void => {
|
|
67
|
+
if (paragraph) {
|
|
68
|
+
blocks.push({
|
|
69
|
+
kind: 'paragraph',
|
|
70
|
+
line: paragraph.line,
|
|
71
|
+
text: paragraph.parts.join(' '),
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
paragraph = undefined
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const closeItem = (): void => {
|
|
78
|
+
if (item) {
|
|
79
|
+
blocks.push({ kind: 'item', line: item.line, text: item.parts.join(' ') })
|
|
80
|
+
}
|
|
81
|
+
item = undefined
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const closeAll = (): void => {
|
|
85
|
+
closeParagraph()
|
|
86
|
+
closeItem()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const line of lines) {
|
|
90
|
+
const raw = line.text
|
|
91
|
+
const text = raw.trim()
|
|
92
|
+
const indented = /^\s/.test(raw)
|
|
93
|
+
|
|
94
|
+
if (line.fenced) {
|
|
95
|
+
// The opening delimiter decides who owns the block. `bodyLines` marks a
|
|
96
|
+
// delimiter and the body alike, so the opener is the first fenced line.
|
|
97
|
+
if (!inFence) {
|
|
98
|
+
if (!(item && indented)) {
|
|
99
|
+
closeAll()
|
|
100
|
+
blocks.push({ kind: 'other', line: line.number, table: false })
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
inFence = true
|
|
104
|
+
afterBlank = false
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
inFence = false
|
|
108
|
+
|
|
109
|
+
if (text === '') {
|
|
110
|
+
closeParagraph()
|
|
111
|
+
afterBlank = true
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (indented && item) {
|
|
116
|
+
if (LIST_ITEM.test(raw)) item.nested = true
|
|
117
|
+
else if (!item.nested && !TABLE_ROW.test(raw)) item.parts.push(text)
|
|
118
|
+
afterBlank = false
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (HEADING.test(raw)) {
|
|
123
|
+
closeAll()
|
|
124
|
+
blocks.push({
|
|
125
|
+
kind: 'heading',
|
|
126
|
+
line: line.number,
|
|
127
|
+
level: (raw.match(/^#+/)?.[0] ?? '').length,
|
|
128
|
+
})
|
|
129
|
+
} else if (!indented && LIST_ITEM.test(raw)) {
|
|
130
|
+
closeAll()
|
|
131
|
+
item = {
|
|
132
|
+
line: line.number,
|
|
133
|
+
parts: [text.replace(LIST_ITEM, '')],
|
|
134
|
+
nested: false,
|
|
135
|
+
}
|
|
136
|
+
} else if (
|
|
137
|
+
TABLE_ROW.test(raw) ||
|
|
138
|
+
BLOCKQUOTE.test(raw) ||
|
|
139
|
+
isSectionMarker(raw) ||
|
|
140
|
+
text.startsWith('<')
|
|
141
|
+
) {
|
|
142
|
+
closeAll()
|
|
143
|
+
blocks.push({
|
|
144
|
+
kind: 'other',
|
|
145
|
+
line: line.number,
|
|
146
|
+
table: TABLE_ROW.test(raw),
|
|
147
|
+
})
|
|
148
|
+
} else if (paragraph) {
|
|
149
|
+
paragraph.parts.push(text)
|
|
150
|
+
} else if (item && !afterBlank) {
|
|
151
|
+
// A lazy continuation, which renders inside the item above it.
|
|
152
|
+
item.parts.push(text)
|
|
153
|
+
} else {
|
|
154
|
+
closeItem()
|
|
155
|
+
paragraph = { line: line.number, parts: [text] }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
afterBlank = false
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
closeAll()
|
|
162
|
+
|
|
163
|
+
return blocks
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Every breach of the six density limits, in line order.
|
|
168
|
+
*
|
|
169
|
+
* Each limit is a count rather than a judgment, so a finding is a fact a scan
|
|
170
|
+
* settles and the caller decides whether it gates. The limits are a 350-character
|
|
171
|
+
* paragraph, 2 paragraphs in a row, a 200-character bullet, 5 plain bullets in
|
|
172
|
+
* a row, no heading past H3, and a paragraph or a table as the first block
|
|
173
|
+
* under every H3. The two length numbers are `CHECKPOINTS`, so the advisory
|
|
174
|
+
* Bullets and Paragraphs sections and this measure read one number, and the
|
|
175
|
+
* bullet run is `bulletStacks`, so a bold-labeled run stays exempt here too.
|
|
176
|
+
*/
|
|
177
|
+
export function densityFindings(lines: readonly BodyLine[]): DensityFinding[] {
|
|
178
|
+
const found: DensityFinding[] = []
|
|
179
|
+
const blocks = blocksOf(lines)
|
|
180
|
+
let run: { line: number; count: number } | undefined
|
|
181
|
+
|
|
182
|
+
const closeRun = (): void => {
|
|
183
|
+
if (run && run.count > PARAGRAPH_RUN) {
|
|
184
|
+
found.push({
|
|
185
|
+
kind: 'paragraph-run',
|
|
186
|
+
line: run.line,
|
|
187
|
+
value: run.count,
|
|
188
|
+
limit: PARAGRAPH_RUN,
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
run = undefined
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
for (const [index, block] of blocks.entries()) {
|
|
195
|
+
if (block.kind !== 'paragraph') closeRun()
|
|
196
|
+
|
|
197
|
+
if (block.kind === 'paragraph') {
|
|
198
|
+
run ??= { line: block.line, count: 0 }
|
|
199
|
+
run.count++
|
|
200
|
+
const length = visibleText(block.text).length
|
|
201
|
+
if (length > CHECKPOINTS.paragraph) {
|
|
202
|
+
found.push({
|
|
203
|
+
kind: 'paragraph-length',
|
|
204
|
+
line: block.line,
|
|
205
|
+
value: length,
|
|
206
|
+
limit: CHECKPOINTS.paragraph,
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
} else if (block.kind === 'item') {
|
|
210
|
+
const length = visibleText(block.text).length
|
|
211
|
+
if (length > CHECKPOINTS.bullet) {
|
|
212
|
+
found.push({
|
|
213
|
+
kind: 'bullet-length',
|
|
214
|
+
line: block.line,
|
|
215
|
+
value: length,
|
|
216
|
+
limit: CHECKPOINTS.bullet,
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
} else if (block.kind === 'heading') {
|
|
220
|
+
if (block.level > HEADING_DEPTH) {
|
|
221
|
+
found.push({
|
|
222
|
+
kind: 'heading-depth',
|
|
223
|
+
line: block.line,
|
|
224
|
+
value: block.level,
|
|
225
|
+
limit: HEADING_DEPTH,
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
if (block.level === 3 && !opensWithLead(blocks[index + 1])) {
|
|
229
|
+
found.push({
|
|
230
|
+
kind: 'h3-opening',
|
|
231
|
+
line: block.line,
|
|
232
|
+
value: null,
|
|
233
|
+
limit: null,
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
closeRun()
|
|
240
|
+
|
|
241
|
+
for (const stack of bulletStacks(lines)) {
|
|
242
|
+
found.push({
|
|
243
|
+
kind: 'bullet-run',
|
|
244
|
+
line: stack.line,
|
|
245
|
+
value: stack.count,
|
|
246
|
+
limit: CAP,
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return found.sort((a, b) => a.line - b.line)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Whether the block under an H3 is the paragraph or table the limit asks for. */
|
|
254
|
+
function opensWithLead(next: Block | undefined): boolean {
|
|
255
|
+
if (next === undefined) return false
|
|
256
|
+
if (next.kind === 'paragraph') return true
|
|
257
|
+
return next.kind === 'other' && next.table
|
|
258
|
+
}
|
package/src/markdown/gate.ts
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import type { ColonFinding } from '@/markdown/colons'
|
|
2
|
+
import type { DensityFinding } from '@/markdown/density'
|
|
2
3
|
import type { HeadingCaseFinding } from '@/markdown/heading-case'
|
|
3
4
|
import type { LinkFinding } from '@/markdown/links'
|
|
5
|
+
import type { ReferenceShapeFinding } from '@/markdown/reference-shape'
|
|
4
6
|
import type { BanFinding } from '@/markdown/scan'
|
|
5
|
-
import
|
|
7
|
+
import { GATED_PATHS } from '@/markdown/scope'
|
|
6
8
|
import type { StructureReport } from '@/markdown/structure'
|
|
7
9
|
|
|
10
|
+
export interface GatedDensityFinding extends DensityFinding {
|
|
11
|
+
readonly gating: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface GatedReferenceShapeFinding extends ReferenceShapeFinding {
|
|
15
|
+
readonly gating: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
8
18
|
export interface GateInput {
|
|
9
19
|
/** Every ban hit across every file measured, flattened. */
|
|
10
20
|
readonly bans: readonly BanFinding[]
|
|
@@ -13,10 +23,11 @@ export interface GateInput {
|
|
|
13
23
|
/**
|
|
14
24
|
* Every structural measure the run made, read by nothing here.
|
|
15
25
|
*
|
|
16
|
-
* Naming it is what makes the split checkable:
|
|
17
|
-
*
|
|
18
|
-
* contributor to route around the stage.
|
|
19
|
-
*
|
|
26
|
+
* Naming it is what makes the split checkable: depth weight, cadence, and the
|
|
27
|
+
* sentence count are judgments a reader settles, and a push failing on one
|
|
28
|
+
* teaches a contributor to route around the stage. The bullet and paragraph
|
|
29
|
+
* weight a reader settles here are the advisory half, and the same numbers
|
|
30
|
+
* gate through `density` below, which knows the path each one sits on.
|
|
20
31
|
*/
|
|
21
32
|
readonly structure: readonly StructureReport[]
|
|
22
33
|
/**
|
|
@@ -33,25 +44,53 @@ export interface GateInput {
|
|
|
33
44
|
*/
|
|
34
45
|
readonly headingCase: readonly HeadingCaseFinding[]
|
|
35
46
|
/**
|
|
36
|
-
* Every
|
|
47
|
+
* Every density finding, each marked with whether its path gates.
|
|
37
48
|
*
|
|
38
|
-
* The
|
|
39
|
-
*
|
|
40
|
-
* groundwork
|
|
41
|
-
*
|
|
49
|
+
* The mark is decided by the caller from `src/markdown/scope.ts`, since only
|
|
50
|
+
* the caller knows the path and whether the tree is a canon checkout. A plan,
|
|
51
|
+
* a groundwork record, and a rule carry no density finding at all, and an
|
|
52
|
+
* in-scope path under no gated prefix carries one marked `gating: false`.
|
|
53
|
+
*/
|
|
54
|
+
readonly density: readonly GatedDensityFinding[]
|
|
55
|
+
/**
|
|
56
|
+
* Every reference shape finding, each marked with whether its path gates.
|
|
57
|
+
* Only a page whose frontmatter carries `kind: reference` has any.
|
|
42
58
|
*/
|
|
43
|
-
readonly
|
|
59
|
+
readonly referenceShape: readonly GatedReferenceShapeFinding[]
|
|
44
60
|
}
|
|
45
61
|
|
|
46
62
|
/**
|
|
47
63
|
* Whether the audit found something that should fail the caller.
|
|
48
64
|
*
|
|
49
|
-
* A banned character and a relative link resolving to
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
65
|
+
* A banned character and a relative link resolving to nothing on disk are both
|
|
66
|
+
* facts rather than a threshold, so either gates unconditionally and there is
|
|
67
|
+
* no widened mode to reach for. A density finding is a fact too, a count
|
|
68
|
+
* against a fixed number, and it gates wherever the caller marked its path as
|
|
69
|
+
* gated, and so does a reference shape finding. The standards decide what
|
|
70
|
+
* counts as banned, which keeps this answering how many rather than which.
|
|
71
|
+
*/
|
|
72
|
+
export function isGating({
|
|
73
|
+
bans,
|
|
74
|
+
links,
|
|
75
|
+
density,
|
|
76
|
+
referenceShape,
|
|
77
|
+
}: GateInput): boolean {
|
|
78
|
+
return (
|
|
79
|
+
bans.length > 0 ||
|
|
80
|
+
links.length > 0 ||
|
|
81
|
+
density.some((finding) => finding.gating) ||
|
|
82
|
+
referenceShape.some((finding) => finding.gating)
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Whether a reference shape finding on this path fails the run, under the
|
|
88
|
+
* prefixes density gates on. A page is opted in by its own marker, so the
|
|
89
|
+
* density scope and the always-gated bullet run have nothing to add here.
|
|
54
90
|
*/
|
|
55
|
-
export function
|
|
56
|
-
|
|
91
|
+
export function referenceShapeGates(
|
|
92
|
+
rel: string,
|
|
93
|
+
gated: readonly string[] = GATED_PATHS,
|
|
94
|
+
): boolean {
|
|
95
|
+
return gated.some((prefix) => rel.startsWith(prefix))
|
|
57
96
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { bodyLines } from '@/markdown/scan'
|
|
2
|
+
import { TABLE_ROW } from '@/markdown/structure'
|
|
3
|
+
|
|
4
|
+
/** The H2 sections a reference page may hold below its opening. */
|
|
5
|
+
export const REFERENCE_SECTIONS: readonly string[] = [
|
|
6
|
+
'Output',
|
|
7
|
+
'Refusals',
|
|
8
|
+
'See also',
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
export type ReferenceShapeKind =
|
|
12
|
+
| 'extra-section'
|
|
13
|
+
| 'section-order'
|
|
14
|
+
| 'subsection'
|
|
15
|
+
| 'flag-table'
|
|
16
|
+
|
|
17
|
+
export interface ReferenceShapeFinding {
|
|
18
|
+
readonly kind: ReferenceShapeKind
|
|
19
|
+
readonly line: number
|
|
20
|
+
/** The heading text, or the first header cell of a flag table. */
|
|
21
|
+
readonly text: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const FRONTMATTER = /^---\n([\s\S]*?)\n---\n?/
|
|
25
|
+
const MARKER = /^kind:\s*(["']?)reference\1\s*$/m
|
|
26
|
+
const HEADING = /^(#{2,3})\s+(.*?)(?:\s+#+)?\s*$/
|
|
27
|
+
const SEPARATOR_ROW = /^\s*\|?\s*:?-+:?\s*(?:\|\s*:?-+:?\s*)*\|?\s*$/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A first header cell naming flags or options, read with its code span and
|
|
31
|
+
* emphasis stripped. Only the first column is read, so a refusal table headed
|
|
32
|
+
* `Reason` stays out of reach whatever its later columns say.
|
|
33
|
+
*/
|
|
34
|
+
const FLAG_HEADER = /^(?:flags?|options?)$/i
|
|
35
|
+
|
|
36
|
+
/** Whether the page's frontmatter opts it into the reference shape. */
|
|
37
|
+
export function isReferencePage(source: string): boolean {
|
|
38
|
+
const frontmatter = FRONTMATTER.exec(source)
|
|
39
|
+
return frontmatter !== null && MARKER.test(frontmatter[1])
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Every breach of the reference shape on a page marked `kind: reference`, in
|
|
44
|
+
* line order, and nothing on an unmarked page.
|
|
45
|
+
*
|
|
46
|
+
* The shape is an opening holding the command, then `## Output`,
|
|
47
|
+
* `## Refusals`, and `## See also`, each at most once and in that order, though
|
|
48
|
+
* a page may leave one out, with no H3 and no table of flags, since
|
|
49
|
+
* `--help` already prints them. The check reads headings and table headers
|
|
50
|
+
* outside fences and leaves the opening to a reader.
|
|
51
|
+
*/
|
|
52
|
+
export function referenceShapeFindings(
|
|
53
|
+
source: string,
|
|
54
|
+
): ReferenceShapeFinding[] {
|
|
55
|
+
if (!isReferencePage(source)) return []
|
|
56
|
+
|
|
57
|
+
const found: ReferenceShapeFinding[] = []
|
|
58
|
+
const lines = bodyLines(source)
|
|
59
|
+
let lastSection = -1
|
|
60
|
+
|
|
61
|
+
for (const [index, line] of lines.entries()) {
|
|
62
|
+
if (line.fenced) continue
|
|
63
|
+
|
|
64
|
+
const heading = HEADING.exec(line.text)
|
|
65
|
+
if (heading) {
|
|
66
|
+
const [, marks, text] = heading
|
|
67
|
+
if (marks.length === 3) {
|
|
68
|
+
found.push({ kind: 'subsection', line: line.number, text })
|
|
69
|
+
} else {
|
|
70
|
+
const position = REFERENCE_SECTIONS.indexOf(text)
|
|
71
|
+
if (position === -1) {
|
|
72
|
+
found.push({ kind: 'extra-section', line: line.number, text })
|
|
73
|
+
} else if (position <= lastSection) {
|
|
74
|
+
found.push({ kind: 'section-order', line: line.number, text })
|
|
75
|
+
} else {
|
|
76
|
+
lastSection = position
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const previous = lines[index - 1]
|
|
83
|
+
const next = lines[index + 1]
|
|
84
|
+
const opensTable =
|
|
85
|
+
TABLE_ROW.test(line.text) &&
|
|
86
|
+
!(previous && !previous.fenced && TABLE_ROW.test(previous.text)) &&
|
|
87
|
+
next !== undefined &&
|
|
88
|
+
SEPARATOR_ROW.test(next.text)
|
|
89
|
+
if (!opensTable) continue
|
|
90
|
+
|
|
91
|
+
const first = firstCell(line.text)
|
|
92
|
+
if (FLAG_HEADER.test(first)) {
|
|
93
|
+
found.push({ kind: 'flag-table', line: line.number, text: first })
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return found
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function firstCell(row: string): string {
|
|
101
|
+
const cells = row.trim().replace(/^\|/, '').split('|')
|
|
102
|
+
return (cells[0] ?? '').replace(/[`*_]/g, '').trim()
|
|
103
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { DensityKind } from '@/markdown/density'
|
|
2
|
+
|
|
3
|
+
/** The surfaces the retired bullet-stacks stage held, per `ALWAYS_GATED`. */
|
|
4
|
+
const BULLET_RUN_SCOPE = [
|
|
5
|
+
/^canon\/context\/.+\.md$/,
|
|
6
|
+
/^docs\/.+\.md$/,
|
|
7
|
+
/^(?:claude|\.claude)\/skills\/[^/]+\/SKILL\.md$/,
|
|
8
|
+
/^(?:claude|\.claude)\/skills\/[^/]+\/references\/.+\.md$/,
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The tracked documents the density limits hold: the context entries, the docs
|
|
13
|
+
* pages, the skill bodies and references of both skill trees, the tooling
|
|
14
|
+
* seeds and stack references, the wiki, and the two root guides.
|
|
15
|
+
*
|
|
16
|
+
* Rules stay out, since a rule is one imperative per bullet. So do
|
|
17
|
+
* `CLAUDE.md`, the requirement and design records, a seed carrying one of
|
|
18
|
+
* those three names, the changelog, the examples, and every `.canon/` record,
|
|
19
|
+
* each with a shape and cap of its own. Named here and nowhere else, so the
|
|
20
|
+
* audit, the gate stage reading its exit, and their tests read one scope.
|
|
21
|
+
*/
|
|
22
|
+
const DENSITY_SCOPE = [
|
|
23
|
+
...BULLET_RUN_SCOPE,
|
|
24
|
+
/^tooling\/[^/]+\/seeds\/(?:.+\/)?(?!(?:CLAUDE|REQUIREMENTS|DESIGN)\.md$)[^/]+\.md$/,
|
|
25
|
+
/^tooling\/[^/]+\/reference\.md$/,
|
|
26
|
+
/^wiki\/.+\.md$/,
|
|
27
|
+
/^(?:README|CONTRIBUTING)\.md$/,
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The path prefixes whose density findings fail a run, each added by the
|
|
32
|
+
* rewrite batch that brought its folders to zero.
|
|
33
|
+
*
|
|
34
|
+
* It describes this repository's rewrite progress and nothing about a target's
|
|
35
|
+
* corpus, which is why the audit reads it only inside a canon checkout.
|
|
36
|
+
*/
|
|
37
|
+
export const GATED_PATHS: readonly string[] = [
|
|
38
|
+
'canon/context/claude-plugin/',
|
|
39
|
+
'canon/context/claude-internal/',
|
|
40
|
+
'canon/context/features/',
|
|
41
|
+
'canon/context/cli/',
|
|
42
|
+
'canon/context/sandbox/',
|
|
43
|
+
'canon/context/governance/',
|
|
44
|
+
'canon/context/tooling/',
|
|
45
|
+
'canon/context/web.md',
|
|
46
|
+
'canon/context/context-model.md',
|
|
47
|
+
'canon/context/design.md',
|
|
48
|
+
'canon/context/index.md',
|
|
49
|
+
'claude/skills/api-design/SKILL.md',
|
|
50
|
+
'claude/skills/auto-ship/SKILL.md',
|
|
51
|
+
'claude/skills/backlog-triage/SKILL.md',
|
|
52
|
+
'claude/skills/backlog-triage/references/apply.md',
|
|
53
|
+
'claude/skills/build-in-slices/SKILL.md',
|
|
54
|
+
'claude/skills/canon-cli/SKILL.md',
|
|
55
|
+
'claude/skills/canon-feedback-triage/SKILL.md',
|
|
56
|
+
'claude/skills/canon-feedback/SKILL.md',
|
|
57
|
+
'claude/skills/canvas-board/SKILL.md',
|
|
58
|
+
'claude/skills/ci-workflow/SKILL.md',
|
|
59
|
+
'claude/skills/code-craft/SKILL.md',
|
|
60
|
+
'claude/skills/codebase-layout/SKILL.md',
|
|
61
|
+
'claude/skills/context-fold/SKILL.md',
|
|
62
|
+
'claude/skills/create-rule/SKILL.md',
|
|
63
|
+
'claude/skills/create-skill/SKILL.md',
|
|
64
|
+
'claude/skills/decision-escalate/SKILL.md',
|
|
65
|
+
'claude/skills/deploy-app/SKILL.md',
|
|
66
|
+
'claude/skills/deprecation-migration/SKILL.md',
|
|
67
|
+
'claude/skills/design-taste/SKILL.md',
|
|
68
|
+
'claude/skills/docs-sync/SKILL.md',
|
|
69
|
+
'claude/skills/document-health/SKILL.md',
|
|
70
|
+
'claude/skills/draft-and-pick/SKILL.md',
|
|
71
|
+
'claude/skills/draft-doc/SKILL.md',
|
|
72
|
+
'claude/skills/draft-figure/SKILL.md',
|
|
73
|
+
'claude/skills/draft-identity/SKILL.md',
|
|
74
|
+
'claude/skills/draft-identity/references/card-route.md',
|
|
75
|
+
'claude/skills/draft-ready/SKILL.md',
|
|
76
|
+
'claude/skills/draft-screencast/SKILL.md',
|
|
77
|
+
'claude/skills/draft-screencast/references/beats-template.md',
|
|
78
|
+
'claude/skills/draft-slides/SKILL.md',
|
|
79
|
+
'claude/skills/git-branch/SKILL.md',
|
|
80
|
+
'claude/skills/git-commit/SKILL.md',
|
|
81
|
+
'claude/skills/git-followup/SKILL.md',
|
|
82
|
+
'claude/skills/git-issue/SKILL.md',
|
|
83
|
+
'claude/skills/git-pr/SKILL.md',
|
|
84
|
+
'claude/skills/git-ship/SKILL.md',
|
|
85
|
+
'claude/skills/git-ship/references/ci-watch.md',
|
|
86
|
+
'claude/skills/git-split/SKILL.md',
|
|
87
|
+
'claude/skills/git-split/references/independent.md',
|
|
88
|
+
'claude/skills/git-split/references/stacked.md',
|
|
89
|
+
'claude/skills/git-stage/SKILL.md',
|
|
90
|
+
'claude/skills/git-worktree/SKILL.md',
|
|
91
|
+
'claude/skills/git-worktree/references/cleanup.md',
|
|
92
|
+
'claude/skills/markdown-craft/SKILL.md',
|
|
93
|
+
'claude/skills/plan-feature/SKILL.md',
|
|
94
|
+
'claude/skills/plan-feature/references/batches.md',
|
|
95
|
+
'claude/skills/plan-groundwork/SKILL.md',
|
|
96
|
+
'claude/skills/plan-groundwork/references/output.md',
|
|
97
|
+
'claude/skills/plan-intake-answer/SKILL.md',
|
|
98
|
+
'claude/skills/plan-intake/SKILL.md',
|
|
99
|
+
'claude/skills/read-frames/SKILL.md',
|
|
100
|
+
'claude/skills/record-screencast/SKILL.md',
|
|
101
|
+
'claude/skills/review-address/SKILL.md',
|
|
102
|
+
'claude/skills/review-address/references/rebase-only.md',
|
|
103
|
+
'claude/skills/review-craft/SKILL.md',
|
|
104
|
+
'claude/skills/review-pr/SKILL.md',
|
|
105
|
+
'claude/skills/review-ui/SKILL.md',
|
|
106
|
+
'claude/skills/review-ui/references/hosted-preview.md',
|
|
107
|
+
'claude/skills/role-orchestrator/SKILL.md',
|
|
108
|
+
'claude/skills/role-orchestrator/references/orchestrator-filing.md',
|
|
109
|
+
'claude/skills/role-planner/SKILL.md',
|
|
110
|
+
'claude/skills/role-reviewer/SKILL.md',
|
|
111
|
+
'claude/skills/role-worker/SKILL.md',
|
|
112
|
+
'claude/skills/search-craft/SKILL.md',
|
|
113
|
+
'claude/skills/session-compact/SKILL.md',
|
|
114
|
+
'claude/skills/session-map/SKILL.md',
|
|
115
|
+
'claude/skills/session-relay/SKILL.md',
|
|
116
|
+
'claude/skills/session-resume/SKILL.md',
|
|
117
|
+
'claude/skills/session-worktree/SKILL.md',
|
|
118
|
+
'claude/skills/systematic-debugging/SKILL.md',
|
|
119
|
+
'claude/skills/target-check/SKILL.md',
|
|
120
|
+
'claude/skills/target-setup/SKILL.md',
|
|
121
|
+
'claude/skills/task-board/SKILL.md',
|
|
122
|
+
'claude/skills/task-board/references/create-report.md',
|
|
123
|
+
'claude/skills/task-complete/SKILL.md',
|
|
124
|
+
'claude/skills/teach-workspace/SKILL.md',
|
|
125
|
+
'claude/skills/test-craft/SKILL.md',
|
|
126
|
+
'claude/skills/test-first/SKILL.md',
|
|
127
|
+
'claude/skills/ui-checklist/SKILL.md',
|
|
128
|
+
'claude/skills/ux-audit/SKILL.md',
|
|
129
|
+
'claude/skills/ux-measure/SKILL.md',
|
|
130
|
+
'claude/skills/video-craft/SKILL.md',
|
|
131
|
+
'claude/skills/write-human/SKILL.md',
|
|
132
|
+
'claude/skills/youtube-transcripts/SKILL.md',
|
|
133
|
+
'docs/agents/indexes.md',
|
|
134
|
+
'docs/agents/install-and-sync.md',
|
|
135
|
+
'docs/agents/migrate.md',
|
|
136
|
+
'docs/agents/output-shape.md',
|
|
137
|
+
'docs/agents/overview.md',
|
|
138
|
+
'docs/agents/targets.md',
|
|
139
|
+
'canon/context/development/',
|
|
140
|
+
'canon/context/ci/',
|
|
141
|
+
'canon/context/scripts/',
|
|
142
|
+
'.claude/skills/internal-',
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The limit that gates before any folder is listed, and the surfaces it gates
|
|
147
|
+
* on. A run of more than 5 plain bullets failed the push in the context
|
|
148
|
+
* entries, the docs pages, and the skill bodies and references through a stage
|
|
149
|
+
* of its own before this measure existed, and waiting on `GATED_PATHS` would
|
|
150
|
+
* reopen that hole. The seeds, the stack references, the wiki, and the root
|
|
151
|
+
* guides were never held to it, and seven runs in the stack references would
|
|
152
|
+
* fail the push on the day this lands, so those wait for their batch.
|
|
153
|
+
*/
|
|
154
|
+
const ALWAYS_GATED: readonly DensityKind[] = ['bullet-run']
|
|
155
|
+
|
|
156
|
+
/** Whether a path takes density findings at all. */
|
|
157
|
+
export function inDensityScope(rel: string): boolean {
|
|
158
|
+
return DENSITY_SCOPE.some((pattern) => pattern.test(rel))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Whether a density finding of this kind on this path fails the run. */
|
|
162
|
+
export function densityGates(
|
|
163
|
+
rel: string,
|
|
164
|
+
kind: DensityKind,
|
|
165
|
+
gated: readonly string[] = GATED_PATHS,
|
|
166
|
+
): boolean {
|
|
167
|
+
if (!inDensityScope(rel)) return false
|
|
168
|
+
if (
|
|
169
|
+
ALWAYS_GATED.includes(kind) &&
|
|
170
|
+
BULLET_RUN_SCOPE.some((pattern) => pattern.test(rel))
|
|
171
|
+
)
|
|
172
|
+
return true
|
|
173
|
+
return gated.some((prefix) => rel.startsWith(prefix))
|
|
174
|
+
}
|