@erclx/canon 5.35.0 → 5.37.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 +11 -11
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/api-design/SKILL.md +6 -0
- package/claude/skills/backlog-triage/SKILL.md +7 -7
- package/claude/skills/build-in-slices/SKILL.md +6 -0
- package/claude/skills/build-in-slices/references/adopted.md +3 -0
- package/claude/skills/canon-cli/REQUIREMENT.md +2 -2
- package/claude/skills/canon-cli/SKILL.md +5 -1
- package/claude/skills/canon-feedback/SKILL.md +5 -0
- package/claude/skills/canon-feedback-triage/SKILL.md +3 -0
- package/claude/skills/canvas-board/REQUIREMENT.md +1 -1
- package/claude/skills/canvas-board/SKILL.md +5 -2
- package/claude/skills/canvas-board/references/canvas.md +12 -0
- package/claude/skills/canvas-board/references/design.md +53 -36
- package/claude/skills/ci-workflow/SKILL.md +5 -0
- package/claude/skills/code-craft/SKILL.md +6 -0
- package/claude/skills/codebase-layout/REQUIREMENT.md +1 -1
- package/claude/skills/codebase-layout/SKILL.md +2 -2
- package/claude/skills/context-fold/references/context.md +48 -34
- package/claude/skills/create-rule/REQUIREMENT.md +6 -8
- package/claude/skills/create-rule/SKILL.md +19 -17
- package/claude/skills/create-rule/references/rule.md +17 -23
- package/claude/skills/create-skill/SKILL.md +11 -2
- package/claude/skills/create-skill/references/skill-disclosure.md +26 -0
- package/claude/skills/create-skill/references/skill-paths.md +3 -0
- package/claude/skills/create-skill/references/skill-reference.md +12 -0
- package/claude/skills/create-skill/references/skill.md +26 -25
- package/claude/skills/deprecation-migration/SKILL.md +6 -0
- package/claude/skills/deprecation-migration/references/adopted.md +3 -0
- package/claude/skills/design-taste/REQUIREMENT.md +6 -6
- package/claude/skills/design-taste/SKILL.md +8 -2
- package/claude/skills/design-taste/references/craft.md +4 -1
- package/claude/skills/design-taste/references/preflight.md +18 -2
- package/claude/skills/design-taste/references/tells.md +1 -1
- package/claude/skills/docs-sync/references/docs.md +6 -0
- package/claude/skills/docs-sync/references/readme.md +15 -0
- package/claude/skills/draft-and-pick/SKILL.md +12 -0
- package/claude/skills/draft-doc/references/context.md +1 -1
- package/claude/skills/draft-doc/references/readme.md +3 -0
- package/claude/skills/draft-figure/references/mermaid.md +6 -0
- package/claude/skills/draft-identity/SKILL.md +3 -2
- package/claude/skills/draft-slides/SKILL.md +3 -0
- package/claude/skills/git-branch/references/branch.md +16 -11
- package/claude/skills/git-commit/references/commit.md +19 -11
- package/claude/skills/git-commit/references/versioning.md +9 -0
- package/claude/skills/git-issue/references/issue.md +12 -9
- package/claude/skills/git-pr/SKILL.md +5 -0
- package/claude/skills/git-pr/references/pr.md +24 -6
- package/claude/skills/git-ship/SKILL.md +3 -0
- package/claude/skills/git-split/SKILL.md +6 -0
- package/claude/skills/git-stage/SKILL.md +3 -0
- package/claude/skills/git-worktree/SKILL.md +6 -6
- package/claude/skills/markdown-craft/references/markdown.md +27 -8
- package/claude/skills/plan-feature/SKILL.md +10 -1
- package/claude/skills/plan-feature/references/plan.md +20 -8
- package/claude/skills/plan-groundwork/SKILL.md +6 -0
- package/claude/skills/plan-groundwork/references/groundwork.md +10 -7
- package/claude/skills/plan-intake/references/intake.md +6 -6
- package/claude/skills/read-frames/SKILL.md +3 -0
- package/claude/skills/review-craft/SKILL.md +3 -0
- package/claude/skills/review-craft/references/ui.md +1 -1
- package/claude/skills/review-ui/SKILL.md +6 -3
- package/claude/skills/role-orchestrator/SKILL.md +3 -0
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +7 -4
- package/claude/skills/role-orchestrator/references/orchestrator-refill.md +3 -0
- package/claude/skills/role-planner/SKILL.md +3 -0
- package/claude/skills/role-worker/SKILL.md +12 -0
- package/claude/skills/search-craft/SKILL.md +3 -0
- package/claude/skills/systematic-debugging/SKILL.md +6 -0
- package/claude/skills/target-check/references/domains.md +5 -0
- package/claude/skills/target-setup/SKILL.md +5 -0
- package/claude/skills/task-board/references/tasks.md +9 -6
- package/claude/skills/task-complete/references/archive.md +3 -0
- package/claude/skills/teach-workspace/SKILL.md +3 -0
- package/claude/skills/teach-workspace/references/glossary.md +3 -0
- package/claude/skills/teach-workspace/references/lesson-build.md +3 -0
- package/claude/skills/teach-workspace/references/lesson-craft.md +3 -0
- package/claude/skills/teach-workspace/references/teach.md +8 -8
- package/claude/skills/test-craft/SKILL.md +7 -2
- package/claude/skills/ux-audit/REQUIREMENT.md +1 -1
- package/claude/skills/ux-audit/SKILL.md +2 -2
- package/claude/skills/ux-measure/REQUIREMENT.md +1 -1
- package/claude/skills/video-craft/SKILL.md +6 -0
- package/claude/skills/write-human/SKILL.md +9 -0
- package/claude/skills/write-human/references/machine-tells.md +12 -0
- package/docs/agents/capture.md +2 -0
- package/docs/agents/commands.md +59 -56
- package/docs/agents/comments.md +1 -1
- package/docs/agents/context-audit-checks.md +6 -0
- package/docs/agents/context-audit.md +8 -8
- package/docs/agents/context-classify.md +2 -0
- package/docs/agents/drift-surfaces.md +5 -5
- package/docs/agents/gate.md +6 -7
- package/docs/agents/gov-detect.md +1 -1
- package/docs/agents/index.md +2 -2
- package/docs/agents/install-and-sync.md +6 -6
- package/docs/agents/markdown-audit-weight.md +11 -3
- package/docs/agents/markdown-audit.md +14 -2
- package/docs/agents/output-shape.md +1 -1
- package/docs/agents/overview.md +2 -2
- package/docs/agents/pr-frames.md +17 -3
- package/docs/agents/rule-citations.md +5 -10
- package/docs/agents/scripting.md +1 -1
- package/docs/agents/sessions-transfer.md +8 -6
- package/docs/agents/skills-audit.md +9 -1
- package/docs/target/projects.md +2 -2
- package/docs/target/scenarios.md +5 -5
- package/docs/target/standards-moved.md +20 -18
- package/docs/workflow/ai-workflow.md +4 -1
- package/docs/workflow/visual-design-workflow.md +8 -3
- package/governance/detect.toml +32 -32
- package/governance/renames.toml +44 -0
- package/governance/rules/lib/{305-e2e-reliability.md → e2e-reliability.md} +1 -1
- package/governance/rules/lib/{306-test-scope.md → test-scope.md} +2 -2
- package/governance/rules/ui/{410-a11y.md → a11y.md} +1 -1
- package/governance/rules/ui/{440-surface-capture.md → surface-capture.md} +3 -3
- package/governance/stacks/base.toml +6 -6
- package/governance/stacks/go.toml +1 -1
- package/governance/stacks/node.toml +1 -1
- package/governance/stacks/php.toml +1 -1
- package/governance/stacks/python.toml +1 -1
- package/package.json +2 -2
- package/scripts/core/install-check.sh +2 -7
- package/src/capture/frame-values.ts +3 -6
- package/src/cli/help-all.ts +96 -0
- package/src/cli/help.ts +28 -27
- package/src/cli.ts +8 -1
- package/src/commands/claude.ts +29 -0
- package/src/commands/context.ts +29 -4
- package/src/commands/design.ts +22 -1
- package/src/commands/gate.ts +1 -8
- package/src/commands/gov.ts +25 -3
- package/src/commands/init.ts +10 -1
- package/src/commands/markdown.ts +148 -3
- package/src/commands/pr.ts +17 -10
- package/src/commands/records.ts +1 -1
- package/src/commands/sync.ts +11 -0
- package/src/commands/tooling.ts +8 -0
- package/src/context/audit.ts +83 -0
- package/src/context/classify/prompts.ts +6 -1
- package/src/context/gate.ts +6 -3
- package/src/design/budget.ts +14 -10
- package/src/design/document.ts +5 -5
- package/src/design/parse.ts +21 -4
- package/src/design/render.ts +3 -3
- package/src/gate/measures.ts +187 -2
- package/src/gate/sequencer.ts +0 -4
- package/src/gate/stages.ts +32 -27
- package/src/git/files.ts +30 -0
- package/src/gov/adapter.ts +2 -2
- package/src/gov/citations.ts +10 -33
- package/src/gov/install.ts +2 -2
- package/src/gov/list.ts +1 -1
- package/src/gov/payload.ts +1 -1
- package/src/gov/stacks.ts +4 -4
- package/src/inventory/subjects.ts +1 -1
- package/src/markdown/ceiling.ts +6 -1
- package/src/markdown/colons.ts +86 -0
- package/src/markdown/gate.ts +25 -0
- package/src/markdown/growth.ts +43 -0
- package/src/markdown/heading-case.ts +74 -0
- package/src/markdown/stacks.ts +74 -0
- package/src/markdown/structure.ts +72 -4
- package/src/migrate/rule-layout.ts +12 -12
- package/src/pr/frames.ts +66 -1
- package/src/shipped/references.ts +14 -14
- package/src/sync/check.ts +23 -23
- package/src/sync/copy.ts +4 -5
- package/tooling/astro/configs/tsconfig.json +1 -1
- package/tooling/astro/manifest.toml +0 -1
- package/tooling/astro/reference.md +2 -10
- package/tooling/base/configs/scripts/verify.json +1 -1
- package/tooling/base/configs/scripts/verify.ts +23 -13
- package/tooling/base/manifest.toml +6 -5
- package/tooling/base/reference.md +15 -22
- package/tooling/base/seeds/.github/workflows/verify.yml +0 -6
- package/tooling/base/seeds/.lintstagedrc +1 -2
- package/tooling/base/seeds/canon/context/ci.md +7 -8
- package/tooling/base/seeds/canon/context/development.md +10 -10
- package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +9 -1
- package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +27 -2
- package/tooling/claude/seeds/CLAUDE.md +1 -1
- package/tooling/claude/seeds/canon/DESIGN.md +24 -37
- package/tooling/shell/configs/.github/workflows/shell.yml +30 -0
- package/tooling/shell/manifest.toml +12 -0
- package/tooling/shell/reference.md +46 -0
- package/tooling/vite-react/configs/tsconfig.json +1 -1
- package/tooling/vite-react/configs/vitest.config.ts +1 -1
- package/tooling/vite-react/manifest.toml +0 -1
- package/tooling/vite-react/reference.md +2 -10
- package/tooling/web/configs/.github/workflows/readme-screenshot.yml +3 -5
- package/tooling/web/configs/.github/workflows/verify.yml +0 -6
- package/tooling/web/configs/src/test/setup.ts +1 -1
- package/tooling/web/manifest.toml +4 -4
- package/tooling/web/reference.md +4 -4
- package/tooling/base/configs/scripts/verify.sh +0 -2
- /package/governance/rules/canon/{606-git.md → git.md} +0 -0
- /package/governance/rules/canon/{611-indexes.md → indexes.md} +0 -0
- /package/governance/rules/canon/{604-scratch.md → scratch.md} +0 -0
- /package/governance/rules/canon/{602-tasks.md → task-scope.md} +0 -0
- /package/governance/rules/canon/{605-worktrees.md → worktrees.md} +0 -0
- /package/governance/rules/claude/{566-output.md → output.md} +0 -0
- /package/governance/rules/code/{000-code.md → routing.md} +0 -0
- /package/governance/rules/code/{010-testing.md → testing.md} +0 -0
- /package/governance/rules/framework/{210-astro.md → astro.md} +0 -0
- /package/governance/rules/framework/{220-fastapi.md → fastapi.md} +0 -0
- /package/governance/rules/framework/{230-nextjs.md → nextjs.md} +0 -0
- /package/governance/rules/framework/{200-react.md → react.md} +0 -0
- /package/governance/rules/framework/{260-shadcn.md → shadcn.md} +0 -0
- /package/governance/rules/framework/{250-tailwind.md → tailwind.md} +0 -0
- /package/governance/rules/lang/{130-go.md → go.md} +0 -0
- /package/governance/rules/lang/{140-php.md → php.md} +0 -0
- /package/governance/rules/lang/{110-python.md → python.md} +0 -0
- /package/governance/rules/lang/{100-typescript.md → typescript.md} +0 -0
- /package/governance/rules/lib/{370-database.md → database.md} +0 -0
- /package/governance/rules/lib/{340-pydantic.md → pydantic.md} +0 -0
- /package/governance/rules/lib/{360-security-server.md → security-server.md} +0 -0
- /package/governance/rules/lib/{350-security-web.md → security-web.md} +0 -0
- /package/governance/rules/lib/{320-tanstack-query.md → tanstack-query.md} +0 -0
- /package/governance/rules/lib/{335-testing-go.md → testing-go.md} +0 -0
- /package/governance/rules/lib/{336-testing-php.md → testing-php.md} +0 -0
- /package/governance/rules/lib/{330-testing-py.md → testing-py.md} +0 -0
- /package/governance/rules/lib/{300-testing-ts.md → testing-ts.md} +0 -0
- /package/governance/rules/lib/{310-zod.md → zod.md} +0 -0
- /package/governance/rules/tooling/{700-ci-workflow.md → ci-workflow.md} +0 -0
- /package/governance/rules/tooling/{790-code-comments.md → code-comments.md} +0 -0
- /package/governance/rules/tooling/{780-config-comments.md → config-comments.md} +0 -0
- /package/governance/rules/tooling/{775-dependencies.md → dependencies.md} +0 -0
- /package/governance/rules/ui/{480-canvas.md → canvas.md} +0 -0
- /package/governance/rules/ui/{400-ui.md → copy.md} +0 -0
- /package/governance/rules/ui/{460-design-taste.md → design-taste.md} +0 -0
- /package/governance/rules/ui/{420-forms.md → forms.md} +0 -0
- /package/governance/rules/ui/{465-interface-casing.md → interface-casing.md} +0 -0
- /package/governance/rules/ui/{450-link-behavior.md → link-behavior.md} +0 -0
- /package/governance/rules/ui/{470-motion.md → motion.md} +0 -0
- /package/governance/rules/ui/{430-ux-completeness.md → ux-completeness.md} +0 -0
- /package/tooling/{base → shell}/configs/.editorconfig +0 -0
- /package/tooling/{base → shell}/configs/.shellcheckrc +0 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { type BodyLine, LINK, maskDisplayed } from '@/markdown/scan'
|
|
2
|
+
|
|
3
|
+
export interface ColonFinding {
|
|
4
|
+
readonly line: number
|
|
5
|
+
/** Zero-based, the colon itself. */
|
|
6
|
+
readonly column: number
|
|
7
|
+
/** The whole source line, so a reader can match it against new text. */
|
|
8
|
+
readonly text: string
|
|
9
|
+
/**
|
|
10
|
+
* The run of non-space characters ending at the colon. A hook matches it
|
|
11
|
+
* against new text, since `column` is a UTF-16 offset that a reader counting
|
|
12
|
+
* code points would misplace after an emoji.
|
|
13
|
+
*/
|
|
14
|
+
readonly lead: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const HEADING = /^#{1,6}\s/
|
|
18
|
+
const TABLE_ROW = /^\s*\|/
|
|
19
|
+
const LIST_MARKER = /^\s*(?:[-*+]|\d+[.)])\s+/
|
|
20
|
+
const QUOTE_MARKER = /^(?:\s*>\s?)+/
|
|
21
|
+
|
|
22
|
+
/** A colon followed by whitespace and more text, ruling out `10:30` and a scheme. */
|
|
23
|
+
const COLON = /:(?=\s+\S)/g
|
|
24
|
+
|
|
25
|
+
const BOLD_LABEL = /^(\*\*|__)[^*_]+\1$/
|
|
26
|
+
|
|
27
|
+
/** Blanks a span while holding its width, so a column stays where it was. */
|
|
28
|
+
function blank(match: string): string {
|
|
29
|
+
return ' '.repeat(match.length)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Whether the text before a line's first colon is a label rather than a
|
|
34
|
+
* clause: the lead of a list item, or a prefix that is only emphasis and a
|
|
35
|
+
* masked code span.
|
|
36
|
+
*/
|
|
37
|
+
function isLabel(prefix: string, listItem: boolean): boolean {
|
|
38
|
+
if (BOLD_LABEL.test(prefix.trim())) return true
|
|
39
|
+
|
|
40
|
+
const words = prefix.replace(/[*_]/g, ' ').trim()
|
|
41
|
+
if (words === '') return true
|
|
42
|
+
return listItem && words.split(/\s+/).length <= 4
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Finds a colon joining two clauses in the middle of a prose sentence.
|
|
47
|
+
*
|
|
48
|
+
* A heading, a table row, a fence, a code span, a link, and a line-final colon
|
|
49
|
+
* are left alone, and so is the first colon of a list item or of a line opening
|
|
50
|
+
* on a bold or code label. The measure is advisory, because a colon is
|
|
51
|
+
* sometimes right and the corpus count shows a gate would be a sweep nobody
|
|
52
|
+
* asked for.
|
|
53
|
+
*/
|
|
54
|
+
export function findColons(lines: readonly BodyLine[]): ColonFinding[] {
|
|
55
|
+
const found: ColonFinding[] = []
|
|
56
|
+
|
|
57
|
+
for (const line of lines) {
|
|
58
|
+
if (line.fenced || HEADING.test(line.text) || TABLE_ROW.test(line.text)) {
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const masked = maskDisplayed(line.text.replace(LINK, blank))
|
|
63
|
+
const quote = QUOTE_MARKER.exec(masked)?.[0] ?? ''
|
|
64
|
+
const marker = LIST_MARKER.exec(masked.slice(quote.length))?.[0] ?? ''
|
|
65
|
+
const start = quote.length + marker.length
|
|
66
|
+
let labelled = false
|
|
67
|
+
|
|
68
|
+
for (const match of masked.matchAll(COLON)) {
|
|
69
|
+
if (match.index < start) continue
|
|
70
|
+
|
|
71
|
+
if (!labelled) {
|
|
72
|
+
labelled = true
|
|
73
|
+
if (isLabel(masked.slice(start, match.index), marker !== '')) continue
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
found.push({
|
|
77
|
+
line: line.number,
|
|
78
|
+
column: match.index,
|
|
79
|
+
text: line.text,
|
|
80
|
+
lead: line.text.slice(0, match.index).replace(/^.*\s/, ''),
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return found
|
|
86
|
+
}
|
package/src/markdown/gate.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import type { ColonFinding } from '@/markdown/colons'
|
|
2
|
+
import type { HeadingCaseFinding } from '@/markdown/heading-case'
|
|
1
3
|
import type { LinkFinding } from '@/markdown/links'
|
|
2
4
|
import type { BanFinding } from '@/markdown/scan'
|
|
5
|
+
import type { StackFinding } from '@/markdown/stacks'
|
|
3
6
|
import type { StructureReport } from '@/markdown/structure'
|
|
4
7
|
|
|
5
8
|
export interface GateInput {
|
|
@@ -16,6 +19,28 @@ export interface GateInput {
|
|
|
16
19
|
* alone states the same rule and leaves no place to assert it.
|
|
17
20
|
*/
|
|
18
21
|
readonly structure: readonly StructureReport[]
|
|
22
|
+
/**
|
|
23
|
+
* Every mid-sentence colon the run measured, read by nothing here.
|
|
24
|
+
*
|
|
25
|
+
* A colon is sometimes right and the corpus carries over a thousand
|
|
26
|
+
* candidates, so the measure warns and a push never fails on it.
|
|
27
|
+
*/
|
|
28
|
+
readonly colons: readonly ColonFinding[]
|
|
29
|
+
/**
|
|
30
|
+
* Every heading or title with a capital after its first word, read by
|
|
31
|
+
* nothing here. A proper noun is the one case it cannot tell from title
|
|
32
|
+
* case, and gating it would need a names list and an escape marker.
|
|
33
|
+
*/
|
|
34
|
+
readonly headingCase: readonly HeadingCaseFinding[]
|
|
35
|
+
/**
|
|
36
|
+
* Every run of plain bullets past the cap, read by nothing here.
|
|
37
|
+
*
|
|
38
|
+
* The stage that gates it is `bullet-stacks` in `src/gate/stages.ts`, which
|
|
39
|
+
* reads a scope of tracked files. This audit also runs on plans and
|
|
40
|
+
* groundwork records kept out of version control, where a long list is not
|
|
41
|
+
* a defect, which is the reason `documentCeiling` gates apart from it too.
|
|
42
|
+
*/
|
|
43
|
+
readonly bulletStacks: readonly StackFinding[]
|
|
19
44
|
}
|
|
20
45
|
|
|
21
46
|
/**
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { bodyLines } from '@/markdown/scan'
|
|
2
|
+
import {
|
|
3
|
+
CHECKPOINTS,
|
|
4
|
+
type Checkpoints,
|
|
5
|
+
type LongBlock,
|
|
6
|
+
longBlocks,
|
|
7
|
+
} from '@/markdown/structure'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Reduces text to the words a reader meets, so a block reflowed, re-marked, or
|
|
11
|
+
* moved under another heading still matches the copy it came from.
|
|
12
|
+
*
|
|
13
|
+
* List markers and emphasis marks are dropped because turning a bullet into a
|
|
14
|
+
* labelled bullet or a paragraph changes the shape of a statement and none of
|
|
15
|
+
* its words, which is the edit the sweep makes. A word changed is a text
|
|
16
|
+
* changed, and that is what the growth stage exists to see.
|
|
17
|
+
*/
|
|
18
|
+
export function normalizeProse(text: string): string {
|
|
19
|
+
return text
|
|
20
|
+
.replace(/^\s*(?:[-*+]|\d+\.)\s+/gm, '')
|
|
21
|
+
.replace(/[*_]/g, '')
|
|
22
|
+
.replace(/\s+/g, ' ')
|
|
23
|
+
.trim()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The paragraphs and top-level bullets of `source` past their checkpoints whose
|
|
28
|
+
* text appears nowhere in `base`.
|
|
29
|
+
*
|
|
30
|
+
* Containment rather than line matching, so an unchanged long block passes
|
|
31
|
+
* wherever it sits and an edited or new one fails. `base` is every document the
|
|
32
|
+
* caller counts as the copy it may have come from, already normalized, since a
|
|
33
|
+
* block moved into a sibling entry is as unchanged as one left in place.
|
|
34
|
+
*/
|
|
35
|
+
export function grownBlocks(
|
|
36
|
+
source: string,
|
|
37
|
+
base: string,
|
|
38
|
+
checkpoints: Checkpoints = CHECKPOINTS,
|
|
39
|
+
): LongBlock[] {
|
|
40
|
+
return longBlocks(bodyLines(source), checkpoints).filter(
|
|
41
|
+
(block) => !base.includes(normalizeProse(block.text)),
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { bodyLines, maskDisplayed } from '@/markdown/scan'
|
|
2
|
+
|
|
3
|
+
export interface HeadingCaseFinding {
|
|
4
|
+
readonly line: number
|
|
5
|
+
/** The heading or title as written, markers removed. */
|
|
6
|
+
readonly text: string
|
|
7
|
+
/** Every word after the first that opens on a capital. */
|
|
8
|
+
readonly words: readonly string[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const HEADING = /^#{1,6}\s+(.*?)(?:\s+#+)?\s*$/
|
|
12
|
+
const FRONTMATTER = /^---\n([\s\S]*?)\n---\n?/
|
|
13
|
+
const TITLE = /^title:\s*(["']?)(.*?)\1\s*$/
|
|
14
|
+
const CODE_SPAN = /(`+)(?:(?!\1).)*\1/g
|
|
15
|
+
const CAPITALIZED = /^[A-Z][a-z]/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The words of `text` that break sentence case.
|
|
19
|
+
*
|
|
20
|
+
* A code span stands for one lowercase word, so a heading opening on one
|
|
21
|
+
* counts that span as its first word. The first word with a letter in it is
|
|
22
|
+
* exempt, which keeps a leading numeral from promoting the next word.
|
|
23
|
+
*/
|
|
24
|
+
function capitalizedAfterFirst(text: string): string[] {
|
|
25
|
+
const masked = maskDisplayed(text.replace(CODE_SPAN, 'code'))
|
|
26
|
+
.replace(/\s+/g, ' ')
|
|
27
|
+
.trim()
|
|
28
|
+
const words = masked === '' ? [] : masked.split(' ')
|
|
29
|
+
const flagged: string[] = []
|
|
30
|
+
let seenFirst = false
|
|
31
|
+
|
|
32
|
+
for (const raw of words) {
|
|
33
|
+
const word = raw.replace(/^[^\p{L}]+/u, '')
|
|
34
|
+
if (!/\p{L}/u.test(raw)) continue
|
|
35
|
+
if (!seenFirst) {
|
|
36
|
+
seenFirst = true
|
|
37
|
+
continue
|
|
38
|
+
}
|
|
39
|
+
if (CAPITALIZED.test(word)) flagged.push(word)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return flagged
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Finds a heading or a frontmatter `title` carrying a capitalized word after
|
|
47
|
+
* its first, outside code spans and all-caps tokens.
|
|
48
|
+
*
|
|
49
|
+
* A proper noun is the one case it cannot tell from title case, so the finding
|
|
50
|
+
* is a warning and no names list backs it.
|
|
51
|
+
*/
|
|
52
|
+
export function findHeadingCase(source: string): HeadingCaseFinding[] {
|
|
53
|
+
const found: HeadingCaseFinding[] = []
|
|
54
|
+
const check = (line: number, text: string): void => {
|
|
55
|
+
const words = capitalizedAfterFirst(text)
|
|
56
|
+
if (words.length > 0) found.push({ line, text, words })
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const frontmatter = FRONTMATTER.exec(source)
|
|
60
|
+
if (frontmatter) {
|
|
61
|
+
for (const [index, text] of frontmatter[1].split('\n').entries()) {
|
|
62
|
+
const title = TITLE.exec(text)
|
|
63
|
+
if (title) check(index + 2, title[2])
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const line of bodyLines(source)) {
|
|
68
|
+
if (line.fenced) continue
|
|
69
|
+
const heading = HEADING.exec(line.text)
|
|
70
|
+
if (heading) check(line.number, heading[1])
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return found
|
|
74
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { BodyLine } from '@/markdown/scan'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The most plain bullets one run may carry. Past it a reader holds the list as
|
|
5
|
+
* a whole rather than item by item, and the shape wants prose, an H3 per item,
|
|
6
|
+
* or a split at the seam.
|
|
7
|
+
*/
|
|
8
|
+
export const CAP = 5
|
|
9
|
+
|
|
10
|
+
/** A top-level bullet marker followed by its content, at column zero only. */
|
|
11
|
+
const BULLET = /^[-*+][ \t]+(.*)$/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* An item opening with a bold label or a link names its own subject. A task
|
|
15
|
+
* box (`[ ]`, `[x]`) opens with a bracket too and is not a link, so a checklist
|
|
16
|
+
* counts as a plain run.
|
|
17
|
+
*/
|
|
18
|
+
const LABELED = /^(?:\*\*|__|\[(?![ xX]\]))/
|
|
19
|
+
|
|
20
|
+
export interface StackFinding {
|
|
21
|
+
/** The line of the run's first item, as an editor numbers it. */
|
|
22
|
+
readonly line: number
|
|
23
|
+
readonly count: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface Run {
|
|
27
|
+
readonly line: number
|
|
28
|
+
count: number
|
|
29
|
+
labeled: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Every run of top-level bullets past `CAP`, in document order.
|
|
34
|
+
*
|
|
35
|
+
* A run is consecutive column-zero `-`, `*`, or `+` items. A blank line, an
|
|
36
|
+
* indented continuation, and a nested item do not break it, and any other
|
|
37
|
+
* column-zero line does, since a paragraph or heading between two lists makes
|
|
38
|
+
* them two. Fenced lines are skipped rather than breaking the run, so a code
|
|
39
|
+
* block inside an item does not split it. A run whose every item opens with a
|
|
40
|
+
* bold label or a link is exempt, because each item carries its own subject
|
|
41
|
+
* and reads as a table of entries rather than a list to hold.
|
|
42
|
+
*
|
|
43
|
+
* Numbered items do not count: a numbered list states an order the reader
|
|
44
|
+
* follows one step at a time.
|
|
45
|
+
*/
|
|
46
|
+
export function bulletStacks(lines: readonly BodyLine[]): StackFinding[] {
|
|
47
|
+
const found: StackFinding[] = []
|
|
48
|
+
let run: Run | undefined
|
|
49
|
+
|
|
50
|
+
const close = (): void => {
|
|
51
|
+
if (run && run.count > CAP && run.labeled < run.count) {
|
|
52
|
+
found.push({ line: run.line, count: run.count })
|
|
53
|
+
}
|
|
54
|
+
run = undefined
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const { number, text, fenced } of lines) {
|
|
58
|
+
if (fenced) continue
|
|
59
|
+
if (text.trim() === '' || /^\s/.test(text)) continue
|
|
60
|
+
|
|
61
|
+
const match = BULLET.exec(text)
|
|
62
|
+
if (!match) {
|
|
63
|
+
close()
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
run ??= { line: number, count: 0, labeled: 0 }
|
|
68
|
+
run.count++
|
|
69
|
+
if (LABELED.test(match[1])) run.labeled++
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
close()
|
|
73
|
+
return found
|
|
74
|
+
}
|
|
@@ -109,7 +109,7 @@ const SENTENCE_END = /[.!?]["'’”)\]]*(?=\s+(?:["'“(\[]*[A-Z]|`)|\s*$)/g
|
|
|
109
109
|
export const CHECKPOINTS = {
|
|
110
110
|
run: 40,
|
|
111
111
|
peerBullet: 130,
|
|
112
|
-
bullet:
|
|
112
|
+
bullet: 250,
|
|
113
113
|
paragraph: 700,
|
|
114
114
|
sentences: 4,
|
|
115
115
|
renderWidth: 80,
|
|
@@ -427,8 +427,28 @@ export function heavyBullets(
|
|
|
427
427
|
lines: readonly BodyLine[],
|
|
428
428
|
checkpoints: Checkpoints,
|
|
429
429
|
): BulletFinding[] {
|
|
430
|
-
|
|
431
|
-
|
|
430
|
+
return foldBullets(lines, checkpoints).map(({ line, characters }) => ({
|
|
431
|
+
line,
|
|
432
|
+
characters,
|
|
433
|
+
}))
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
interface FoldedBullet extends BulletFinding {
|
|
437
|
+
/** The bullet's source lines joined by the space a wrap would have carried. */
|
|
438
|
+
readonly text: string
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* The walk `heavyBullets` reports from, keeping each past-checkpoint bullet's
|
|
443
|
+
* text so `longBlocks` can ask whether a base copy of it exists. One walk
|
|
444
|
+
* rather than two, for the reason `paragraphBlocks` is shared.
|
|
445
|
+
*/
|
|
446
|
+
function foldBullets(
|
|
447
|
+
lines: readonly BodyLine[],
|
|
448
|
+
checkpoints: Checkpoints,
|
|
449
|
+
): FoldedBullet[] {
|
|
450
|
+
const findings: FoldedBullet[] = []
|
|
451
|
+
let open: FoldedBullet | null = null
|
|
432
452
|
|
|
433
453
|
const close = (): void => {
|
|
434
454
|
if (open && open.characters > checkpoints.bullet) findings.push(open)
|
|
@@ -450,7 +470,11 @@ export function heavyBullets(
|
|
|
450
470
|
if (item) {
|
|
451
471
|
close()
|
|
452
472
|
if (item[1].length === 0) {
|
|
453
|
-
open = {
|
|
473
|
+
open = {
|
|
474
|
+
line: line.number,
|
|
475
|
+
characters: visibleText(text).length,
|
|
476
|
+
text,
|
|
477
|
+
}
|
|
454
478
|
}
|
|
455
479
|
continue
|
|
456
480
|
}
|
|
@@ -466,6 +490,7 @@ export function heavyBullets(
|
|
|
466
490
|
open = {
|
|
467
491
|
...open,
|
|
468
492
|
characters: open.characters + visibleText(text).length + 1,
|
|
493
|
+
text: `${open.text} ${text}`,
|
|
469
494
|
}
|
|
470
495
|
}
|
|
471
496
|
|
|
@@ -581,6 +606,49 @@ export function heavyParagraphs(
|
|
|
581
606
|
return findings
|
|
582
607
|
}
|
|
583
608
|
|
|
609
|
+
export interface LongBlock {
|
|
610
|
+
readonly kind: 'paragraph' | 'bullet'
|
|
611
|
+
readonly line: number
|
|
612
|
+
readonly characters: number
|
|
613
|
+
/** The block's source lines folded into one, as the paragraph and bullet folds read it. */
|
|
614
|
+
readonly text: string
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* The paragraphs past the paragraph checkpoint and the top-level bullets past
|
|
619
|
+
* the bullet checkpoint, each with its text.
|
|
620
|
+
*
|
|
621
|
+
* Weight alone, never the sentence cap, since the growth stage asks about the
|
|
622
|
+
* two numbers the standard states in characters. The definitions are the
|
|
623
|
+
* audit's own, so a block this names and a block the weight report names are
|
|
624
|
+
* one set.
|
|
625
|
+
*/
|
|
626
|
+
export function longBlocks(
|
|
627
|
+
lines: readonly BodyLine[],
|
|
628
|
+
checkpoints: Checkpoints,
|
|
629
|
+
): LongBlock[] {
|
|
630
|
+
const blocks: LongBlock[] = []
|
|
631
|
+
|
|
632
|
+
for (const block of paragraphBlocks(lines)) {
|
|
633
|
+
const text = paragraphText(block)
|
|
634
|
+
const characters = visibleText(text).length
|
|
635
|
+
if (characters > checkpoints.paragraph) {
|
|
636
|
+
blocks.push({
|
|
637
|
+
kind: 'paragraph',
|
|
638
|
+
line: block[0].number,
|
|
639
|
+
characters,
|
|
640
|
+
text,
|
|
641
|
+
})
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
for (const bullet of foldBullets(lines, checkpoints)) {
|
|
646
|
+
blocks.push({ kind: 'bullet', ...bullet })
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return blocks.sort((a, b) => a.line - b.line)
|
|
650
|
+
}
|
|
651
|
+
|
|
584
652
|
/** Punctuation either side of a word, so an opener is compared on its letters. */
|
|
585
653
|
const WORD_EDGE = /^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu
|
|
586
654
|
|
|
@@ -34,12 +34,12 @@ export const RENUMBERED_RULES: Readonly<Record<string, string>> = {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
38
|
-
* exclusion holds even for a
|
|
37
|
+
* Rule folders the walk skips by name rather than by content, so the
|
|
38
|
+
* exclusion holds even for a folder the toolkit has not shipped yet.
|
|
39
39
|
* `canon/` and `internal/` are already-migrated destinations, and
|
|
40
40
|
* `project/` is a target's own rules, which this verb never touches.
|
|
41
41
|
*/
|
|
42
|
-
const
|
|
42
|
+
const EXCLUDED_FOLDERS: readonly string[] = ['project', 'canon', 'internal']
|
|
43
43
|
|
|
44
44
|
/** One flat rule file, before classification. */
|
|
45
45
|
export interface FlatRuleFile {
|
|
@@ -57,9 +57,9 @@ export function flatRulesRoot(target: string): string {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
* Every rule file sitting directly under a flat
|
|
61
|
-
* the
|
|
62
|
-
* nests a rule deeper than its
|
|
60
|
+
* Every rule file sitting directly under a flat rule folder. One level for
|
|
61
|
+
* the folder and one glob below it for `*.md`, since `governance/rules/` never
|
|
62
|
+
* nests a rule deeper than its folder, and the flat layout this verb migrates
|
|
63
63
|
* off of mirrors that depth.
|
|
64
64
|
*/
|
|
65
65
|
export async function walkFlatRules(target: string): Promise<FlatRuleFile[]> {
|
|
@@ -69,20 +69,20 @@ export async function walkFlatRules(target: string): Promise<FlatRuleFile[]> {
|
|
|
69
69
|
)
|
|
70
70
|
if (entries === undefined) return []
|
|
71
71
|
|
|
72
|
-
const
|
|
73
|
-
(entry) => entry.isDirectory() && !
|
|
72
|
+
const folders = entries.filter(
|
|
73
|
+
(entry) => entry.isDirectory() && !EXCLUDED_FOLDERS.includes(entry.name),
|
|
74
74
|
)
|
|
75
75
|
|
|
76
76
|
const files: FlatRuleFile[] = []
|
|
77
|
-
for (const
|
|
77
|
+
for (const folder of folders) {
|
|
78
78
|
const glob = new Bun.Glob('*.md')
|
|
79
79
|
for (const name of glob.scanSync({
|
|
80
|
-
cwd: join(root,
|
|
80
|
+
cwd: join(root, folder.name),
|
|
81
81
|
onlyFiles: true,
|
|
82
82
|
})) {
|
|
83
83
|
files.push({
|
|
84
|
-
rel: relPath('.claude', 'rules',
|
|
85
|
-
subdir:
|
|
84
|
+
rel: relPath('.claude', 'rules', folder.name, name),
|
|
85
|
+
subdir: folder.name,
|
|
86
86
|
name: name.slice(0, -'.md'.length),
|
|
87
87
|
})
|
|
88
88
|
}
|
package/src/pr/frames.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises'
|
|
2
2
|
|
|
3
3
|
/** The branch holding review frames. It never merges into a branch anyone checks out. */
|
|
4
|
-
export const FRAMES_BRANCH = 'canon
|
|
4
|
+
export const FRAMES_BRANCH = 'canon/frames'
|
|
5
|
+
|
|
6
|
+
/** The branch frames lived on before the rename. A drop and a prune drain it and an add never writes it. */
|
|
7
|
+
export const LEGACY_FRAMES_BRANCH = 'canon-frames'
|
|
5
8
|
|
|
6
9
|
/** The eight bytes every PNG opens with. A frame that lacks them embeds as a broken image. */
|
|
7
10
|
const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]
|
|
@@ -94,6 +97,7 @@ export type DropResult =
|
|
|
94
97
|
readonly removed: readonly number[]
|
|
95
98
|
readonly commit?: string
|
|
96
99
|
readonly deleted?: boolean
|
|
100
|
+
readonly legacy?: true
|
|
97
101
|
}
|
|
98
102
|
| { readonly kind: 'refused'; readonly reason: FramesRefusal }
|
|
99
103
|
|
|
@@ -105,6 +109,7 @@ export type PruneResult =
|
|
|
105
109
|
readonly unread: readonly number[]
|
|
106
110
|
readonly commit?: string
|
|
107
111
|
readonly deleted?: boolean
|
|
112
|
+
readonly legacy?: true
|
|
108
113
|
}
|
|
109
114
|
| { readonly kind: 'refused'; readonly reason: FramesRefusal }
|
|
110
115
|
|
|
@@ -341,3 +346,63 @@ export async function pruneFrames(
|
|
|
341
346
|
unread: unread.sort((a, b) => a - b),
|
|
342
347
|
}
|
|
343
348
|
}
|
|
349
|
+
|
|
350
|
+
function mergeNumbers(
|
|
351
|
+
first: readonly number[],
|
|
352
|
+
second: readonly number[],
|
|
353
|
+
): number[] {
|
|
354
|
+
return [...new Set([...first, ...second])].sort((a, b) => a - b)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Whether a drain pass changed the legacy ref, by removing a pull request or deleting it. */
|
|
358
|
+
function touchedLegacy(result: DropResult): boolean {
|
|
359
|
+
return (
|
|
360
|
+
result.kind === 'dropped' &&
|
|
361
|
+
(result.removed.length > 0 || result.deleted === true)
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Drops on the current branch, then on the legacy one. The legacy pass only
|
|
367
|
+
* removes, and a refusal there never fails a drop the current branch already
|
|
368
|
+
* finished, since a target that never held the legacy ref reads it as missing
|
|
369
|
+
* and a token may be unable to read it at all.
|
|
370
|
+
*/
|
|
371
|
+
export async function dropFramesWithLegacy(
|
|
372
|
+
api: FramesApi,
|
|
373
|
+
legacy: FramesApi,
|
|
374
|
+
numbers: readonly number[],
|
|
375
|
+
): Promise<DropResult> {
|
|
376
|
+
const current = await dropFrames(api, numbers)
|
|
377
|
+
if (current.kind === 'refused') return current
|
|
378
|
+
const drained = await dropFrames(legacy, numbers)
|
|
379
|
+
if (drained.kind === 'refused' || !touchedLegacy(drained)) return current
|
|
380
|
+
return {
|
|
381
|
+
...current,
|
|
382
|
+
removed: mergeNumbers(current.removed, drained.removed),
|
|
383
|
+
legacy: true,
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** The prune counterpart of `dropFramesWithLegacy`, with the same legacy semantics. */
|
|
388
|
+
export async function pruneFramesWithLegacy(
|
|
389
|
+
api: FramesApi,
|
|
390
|
+
legacy: FramesApi,
|
|
391
|
+
days: number,
|
|
392
|
+
now: Date,
|
|
393
|
+
): Promise<PruneResult> {
|
|
394
|
+
const current = await pruneFrames(api, days, now)
|
|
395
|
+
if (current.kind === 'refused') return current
|
|
396
|
+
const drained = await pruneFrames(legacy, days, now)
|
|
397
|
+
if (drained.kind === 'refused') return current
|
|
398
|
+
const touched = drained.removed.length > 0 || drained.deleted === true
|
|
399
|
+
const kept = mergeNumbers(current.kept, drained.kept)
|
|
400
|
+
const unread = mergeNumbers(current.unread, drained.unread)
|
|
401
|
+
return {
|
|
402
|
+
...current,
|
|
403
|
+
removed: mergeNumbers(current.removed, drained.removed),
|
|
404
|
+
kept,
|
|
405
|
+
unread,
|
|
406
|
+
...(touched && { legacy: true as const }),
|
|
407
|
+
}
|
|
408
|
+
}
|
|
@@ -54,7 +54,7 @@ export const REFERENCE_MARKER = 'canon-allow-reference'
|
|
|
54
54
|
* already ships `anthropics/claude-code#58345` in exactly that form.
|
|
55
55
|
*
|
|
56
56
|
* The trailing boundary is what the first shape of this pattern lacked, and it
|
|
57
|
-
* costs a live finding to omit. `governance/rules/framework/
|
|
57
|
+
* costs a live finding to omit. `governance/rules/framework/tailwind.md`
|
|
58
58
|
* writes `bg-[#316ff6]`, a Tailwind arbitrary hex color, which an unbounded
|
|
59
59
|
* `#[0-9]+` reads as pull request 316. Requiring a non-word character after the
|
|
60
60
|
* digits rejects it and rejects nothing real, since a citation is always
|
|
@@ -171,20 +171,20 @@ const STANDARDS_CALL = /\bcanon standards(?![\w-])/g
|
|
|
171
171
|
const PHASE_LABEL = /\bv\d+\.\d+(?!\.\d)\b/g
|
|
172
172
|
|
|
173
173
|
/**
|
|
174
|
-
* A `.claude/rules/<segments>/<
|
|
175
|
-
* `claude/skills/`, the installed-path spelling `
|
|
176
|
-
*
|
|
174
|
+
* A `.claude/rules/<segments>/<slug>.md` citation from a body under
|
|
175
|
+
* `claude/skills/`, the installed-path spelling `authoring-layout.md` bans a
|
|
176
|
+
* shipped skill body from citing as authority for its own behavior.
|
|
177
177
|
*
|
|
178
|
-
* Anchored on
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
* (`
|
|
184
|
-
*
|
|
185
|
-
*
|
|
178
|
+
* Anchored on a trailing `.md` file rather than on the bare `.claude/rules/`
|
|
179
|
+
* prefix, which is what keeps a folder mention, such as `create-rule`,
|
|
180
|
+
* `target-check`, and `target-setup` write, from matching: it ends in `/` or
|
|
181
|
+
* names no `.md`. The segments between `rules/` and the file admit both a
|
|
182
|
+
* governance-namespace path (`canon/canon/scratch.md`) and a
|
|
183
|
+
* project-namespace one (`project/<subdir>/<slug>.md`) without distinguishing
|
|
184
|
+
* them, since either shape is the same broken citation, and the placeholder
|
|
185
|
+
* check spares the bracketed illustration.
|
|
186
186
|
*/
|
|
187
|
-
const RULE_PATH = /(?<![\w./-])\.claude\/rules\/[^\s`)\]]
|
|
187
|
+
const RULE_PATH = /(?<![\w./-])\.claude\/rules\/[^\s`)\]]*?[\w-]+\.md\b/g
|
|
188
188
|
|
|
189
189
|
export interface ShippedReference {
|
|
190
190
|
readonly file: string
|
|
@@ -246,7 +246,7 @@ function isDocsPathReportable(
|
|
|
246
246
|
* Whether `file` sits in the one corpus `RULE_PATH` gates: a shipped skill
|
|
247
247
|
* body, minus its own `REQUIREMENT.md`.
|
|
248
248
|
*
|
|
249
|
-
* The ban lives in `
|
|
249
|
+
* The ban lives in `authoring-layout.md`, stated for one reader: a
|
|
250
250
|
* session loading the `SKILL.md` body a target actually receives.
|
|
251
251
|
* `REQUIREMENT.md` is excluded for the reason that file states there, since a
|
|
252
252
|
* maintainer or an audit command reads it rather than a session loading it,
|