@erclx/canon 5.73.0 → 5.75.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/CHANGELOG.md +41 -0
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/api-design/references/adopted.md +5 -3
- package/claude/skills/build-in-slices/references/adopted.md +4 -2
- package/claude/skills/code-craft/references/adopted.md +9 -7
- package/claude/skills/codebase-layout/references/adopted.md +8 -6
- package/claude/skills/create-rule/SKILL.md +5 -1
- package/claude/skills/create-rule/references/ledger.md +73 -0
- package/claude/skills/create-rule/references/rule.md +1 -1
- package/claude/skills/create-skill/references/skill-practice.md +6 -1
- package/claude/skills/deprecation-migration/references/adopted.md +5 -3
- package/claude/skills/design-pick/references/board.md +2 -2
- package/claude/skills/plan-feature/SKILL.md +2 -1
- package/claude/skills/plan-groundwork/SKILL.md +2 -0
- package/claude/skills/pr-evidence/REQUIREMENT.md +1 -0
- package/claude/skills/pr-evidence/SKILL.md +2 -0
- package/claude/skills/pr-evidence/references/evidence.md +11 -4
- package/claude/skills/pr-evidence/references/reel.md +108 -0
- package/claude/skills/proposal-critique/REQUIREMENT.md +34 -0
- package/claude/skills/proposal-critique/SKILL.md +126 -0
- package/claude/skills/proposal-critique/references/adopted.md +44 -0
- package/claude/skills/review-craft/references/adopted.md +4 -4
- package/claude/skills/search-craft/references/adopted.md +9 -7
- package/claude/skills/systematic-debugging/references/adopted.md +4 -2
- package/claude/skills/teach-craft/references/adopted.md +8 -6
- package/claude/skills/test-craft/references/adopted.md +14 -12
- package/claude/skills/test-first/SKILL.md +1 -1
- package/claude/skills/test-first/references/adopted.md +9 -5
- package/claude/skills/ui-film/REQUIREMENT.md +4 -5
- package/claude/skills/ui-film/SKILL.md +21 -19
- package/claude/skills/ui-film/references/compose-craft.md +2 -2
- package/claude/skills/ui-film/references/handoff.md +2 -7
- package/claude/skills/write-human/references/adopted.md +8 -3
- package/docs/agents/audits.md +24 -24
- package/docs/agents/migrate.md +10 -10
- package/docs/migrations/by-verb.md +2 -0
- package/docs/target/index.md +1 -0
- package/docs/target/outside-tools.md +39 -0
- package/docs/workflow/skill-map.md +1 -0
- package/governance/detect.toml +4 -0
- package/governance/ledger/claude/rule.md +16 -0
- package/governance/ledger/skills/build-in-slices/SKILL.md +55 -0
- package/governance/ledger/skills/proposal-critique/SKILL.md +66 -0
- package/governance/ledger/skills/search-craft/SKILL.md +54 -0
- package/governance/ledger/skills/systematic-debugging/SKILL.md +64 -0
- package/governance/ledger/skills/test-first/SKILL.md +28 -0
- package/governance/ledger/skills/write-human/SKILL.md +62 -0
- package/governance/ledger/skills/write-human/density.md +36 -0
- package/governance/ledger/skills/write-human/machine-tells.md +30 -0
- package/governance/rules/claude/rule.md +13 -0
- package/package.json +1 -1
- package/scripts/ci/docs-only.sh +24 -0
- package/scripts/ci/refresh-frames-pr.sh +11 -12
- package/src/audits/catalog.ts +7 -2
- package/src/capture/stamp.ts +3 -5
- package/src/claude/cases/misc.ts +14 -0
- package/src/claude/skills-audit.ts +154 -8
- package/src/cli/help.ts +1 -1
- package/src/commands/claude.ts +6 -2
- package/src/commands/gov.ts +12 -7
- package/src/commands/migrate.ts +6 -2
- package/src/commands/pr.ts +110 -13
- package/src/commands/take.ts +36 -7
- package/src/counts/catalogs.ts +3 -5
- package/src/gate/measures.ts +2 -145
- package/src/gate/sequencer.ts +7 -1
- package/src/gate/stages.ts +36 -47
- package/src/gov/audit.ts +59 -13
- package/src/markdown/scope.ts +4 -0
- package/src/migrate/session-defaults.ts +114 -4
- package/src/pr/checks.ts +75 -3
- package/src/pr/evidence.ts +62 -1
- package/src/pr/lift.ts +23 -2
- package/src/pr/merge.ts +13 -3
- package/src/take/encode.ts +9 -1
- package/src/take/film.ts +17 -2
- package/src/take/fixtures/busy.html +107 -0
- package/src/take/perform.ts +99 -25
- package/src/take/screencast.ts +4 -3
- package/src/take/stepper.ts +33 -20
- package/src/targets/pulls.ts +42 -0
- package/tooling/claude/reference.md +4 -2
- package/tooling/claude/seeds/.claude/hooks/plan-guard.sh +239 -0
- package/tooling/claude/seeds/.claude/settings.json +8 -0
- package/claude/skills/ui-film/references/pr-clip.md +0 -108
- package/scripts/core/regen-hero.ts +0 -113
- package/src/capture/frame-values.ts +0 -326
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Write human skill ledger
|
|
3
|
+
description: Sources for claude/skills/write-human/SKILL.md, one row per bullet and ordered step in its concern groups
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Write human skill ledger
|
|
7
|
+
|
|
8
|
+
The field is plain-language writing and technical editing. Government plain-language guidance from GOV.UK and digital.gov, the Google and Microsoft developer style guides, and Nielsen Norman Group's reading research carry most rows. Purdue OWL carries the cadence rows, whose thresholds are the toolkit's own.
|
|
9
|
+
|
|
10
|
+
`## The bans stay in force`, `## Machine tells`, and `## Source material` point elsewhere and hold no bullet, so they take no table. The five ordered steps under `## Revising a passage` each take a row, the way a bullet does.
|
|
11
|
+
|
|
12
|
+
## What yields and what does not
|
|
13
|
+
|
|
14
|
+
| Bullet | Source | Read |
|
|
15
|
+
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- |
|
|
16
|
+
| A surface's own voice wins | House choice: a changelog or commit standard that fixes its own voice would otherwise answer to two authorities, and the session picks whichever it read last | 2026-10-28 |
|
|
17
|
+
| The yield covers voice alone | House choice: a declared warmer voice would otherwise excuse the fragment, the padding, and the tells, which read as defects in every register | 2026-10-28 |
|
|
18
|
+
| Terse reference prose is the default | [Microsoft Learn style quick start](https://learn.microsoft.com/en-us/contribute/content/style-quick-start): "Don't waste words. Be affirmative and don't use extra words or lots of qualifiers" | 2026-10-28 |
|
|
19
|
+
|
|
20
|
+
## Voice
|
|
21
|
+
|
|
22
|
+
| Bullet | Source | Read |
|
|
23
|
+
| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
|
|
24
|
+
| A reader scanning, not studying | [How users read on the web](https://www.nngroup.com/articles/how-users-read-on-the-web/): "79 percent of our test users always scanned any new page they came across", and 16 percent read word by word | 2026-10-28 |
|
|
25
|
+
| Active voice and present tense | [digital.gov, Writing for understanding](https://digital.gov/guides/plain-language/writing): "Active voice makes it clear who should do what", and "The simplest and strongest form of a verb is present tense" | 2026-10-28 |
|
|
26
|
+
| Name the actor | [Google style guide, Active voice](https://developers.google.com/style/voice): "In passive voice, it's easy to neglect to indicate who or what is performing a particular action" | 2026-10-28 |
|
|
27
|
+
| Direct verbs and plain words | [GOV.UK, Use clear language](https://guidance.publishing.service.gov.uk/writing-to-gov-uk-standards/writing-guidelines/clear-language/): "Do not use formal or long words when easy or short ones will do" | 2026-10-28 |
|
|
28
|
+
| Drop marketing words | [GOV.UK A to Z, Words to avoid](https://guidance.publishing.service.gov.uk/writing-to-gov-uk-standards/style-guides/a-to-z-style-guide/#words-to-avoid): "leverage (unless in the financial sense), use 'influence' or 'use'" | 2026-10-28 |
|
|
29
|
+
| Drop vague qualifiers | [Google style guide, Word list](https://developers.google.com/style/word-list): "just" is "Usually, just is a filler word that you can delete without affecting your meaning" | 2026-10-28 |
|
|
30
|
+
| Direct on fact, hedged on doubt | [Microsoft Learn style quick start](https://learn.microsoft.com/en-us/contribute/content/style-quick-start): "Be affirmative and don't use extra words or lots of qualifiers" | 2026-10-28 |
|
|
31
|
+
| Assume developer-level knowledge | [GOV.UK, Meet user needs](https://guidance.publishing.service.gov.uk/writing-to-gov-uk-standards/writing-guidelines/meet-user-needs/): "only include what someone needs to know so they can complete a task" | 2026-10-28 |
|
|
32
|
+
| Front-load, and every sentence is new | [Microsoft, Top 10 tips](https://learn.microsoft.com/en-us/style-guide/top-10-tips-style-voice): "Lead with what's most important. Front-load keywords for scanning" | 2026-10-28 |
|
|
33
|
+
|
|
34
|
+
## Rhythm
|
|
35
|
+
|
|
36
|
+
| Bullet | Source | Read |
|
|
37
|
+
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
|
|
38
|
+
| Read the paragraph as one block | House choice: cadence is a property of the run, so an edit made line by line clears each sentence and leaves the paragraph as flat as it was | 2026-10-28 |
|
|
39
|
+
| Longest and shortest within five | [Purdue OWL, Sentence variety](https://owl.purdue.edu/owl/general_writing/academic_writing/sentence_variety/index.html): "Too many sentences with the same structure and length can grow monotonous for readers". The five-word spread is the toolkit's own threshold | 2026-10-28 |
|
|
40
|
+
| Three alike openings is a pattern | [Purdue OWL, Sentence variety](https://owl.purdue.edu/owl/general_writing/academic_writing/sentence_variety/index.html): "If too many sentences start with the same word, especially The, It, This, or I, prose can grow tedious for readers" | 2026-10-28 |
|
|
41
|
+
| Vary the shape, not the word | [Purdue OWL, Sentence variety](https://owl.purdue.edu/owl/general_writing/academic_writing/sentence_variety/index.html): the monotony it names comes from "the same structure and length", so a new first word over one structure leaves it in place | 2026-10-28 |
|
|
42
|
+
| Never buy variety with a fragment | [Microsoft Learn style quick start](https://learn.microsoft.com/en-us/contribute/content/style-quick-start): small words "(such as "a," "the," "that," and "is") are crucial for machine translation. Be sure to include them" | 2026-10-28 |
|
|
43
|
+
| A connective for flow, never rhythm | House choice: a connective added to vary cadence names no relation the sentences carry, so it reads as the padding the density rules cut | 2026-10-28 |
|
|
44
|
+
|
|
45
|
+
## Density
|
|
46
|
+
|
|
47
|
+
| Bullet | Source | Read |
|
|
48
|
+
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- |
|
|
49
|
+
| One claim per sentence | [digital.gov, Write short sentences](https://digital.gov/guides/plain-language/writing/clear-short): "Express only one idea in each sentence" | 2026-10-28 |
|
|
50
|
+
| Cut a restatement | [Microsoft, Top 10 tips](https://learn.microsoft.com/en-us/style-guide/top-10-tips-style-voice): "Give customers just enough information to make decisions confidently. Prune every excess word" | 2026-10-28 |
|
|
51
|
+
| Compress without the verb going | [Microsoft Learn style quick start](https://learn.microsoft.com/en-us/contribute/content/style-quick-start): "Include the "small words"", since words "understood for context" are still needed | 2026-10-28 |
|
|
52
|
+
| Resolve a pronoun to its noun | [Google style guide, Pronouns](https://developers.google.com/style/pronouns): "Avoid vague and confusing references between a pronoun and its antecedent" | 2026-10-28 |
|
|
53
|
+
|
|
54
|
+
## Revising a passage
|
|
55
|
+
|
|
56
|
+
| Bullet | Source | Read |
|
|
57
|
+
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
|
|
58
|
+
| Read the whole passage first | House choice: an edit made before the whole is read polishes a sentence a later paragraph makes redundant | 2026-10-28 |
|
|
59
|
+
| Mark lengths and openings | House choice: the counts the rhythm rules name are invisible until written down, and the flattest run carries most of the uniformity a reader hears | 2026-10-28 |
|
|
60
|
+
| Cut a sentence adding no claim | House choice: shortening a sentence that carries no claim leaves the padding in place at a smaller size | 2026-10-28 |
|
|
61
|
+
| Re-read model text for the tells | [Wikipedia, Signs of AI writing](https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing): it catalogs patterns model output carries, such as "LLMs overuse the rule of three" | 2026-10-28 |
|
|
62
|
+
| Run the audit and rewrite each hit | House choice: a near-synonym swapped for a flagged token clears the scan and keeps the habit, the failure that narrowed the markdown standard to characters | 2026-10-28 |
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Information density reference ledger
|
|
3
|
+
description: Sources for claude/skills/write-human/references/density.md, one row per bullet
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Information density reference ledger
|
|
7
|
+
|
|
8
|
+
The field is plain-language writing and technical editing. The Google and Microsoft developer style guides and digital.gov's plain-language guide carry the sourced rows. `## Measuring a paragraph` holds prose and no bullet, so it takes no table.
|
|
9
|
+
|
|
10
|
+
## What to cut
|
|
11
|
+
|
|
12
|
+
| Bullet | Source | Read |
|
|
13
|
+
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
|
|
14
|
+
| A restatement at another altitude | [Microsoft, Top 10 tips](https://learn.microsoft.com/en-us/style-guide/top-10-tips-style-voice): "Give customers just enough information to make decisions confidently. Prune every excess word" | 2026-10-28 |
|
|
15
|
+
| A setup clause naming the subject | [Microsoft, Top 10 tips](https://learn.microsoft.com/en-us/style-guide/top-10-tips-style-voice): "Edit out you can when it isn't necessary. Avoid weak phrasing like there is, there are, and there were" | 2026-10-28 |
|
|
16
|
+
| A qualifier changing no truth | [Microsoft Learn style quick start](https://learn.microsoft.com/en-us/contribute/content/style-quick-start): "Be affirmative and don't use extra words or lots of qualifiers" | 2026-10-28 |
|
|
17
|
+
| An example repeating the one above | House choice: a second example teaching what the first taught doubles the reading for no new case, and the reader goes looking for the difference | 2026-10-28 |
|
|
18
|
+
| A transition naming the relation | House choice: a transition such as `Additionally` before an addition restates the relation the sentence order already shows, so it adds a word and no claim | 2026-10-28 |
|
|
19
|
+
|
|
20
|
+
## What never to cut
|
|
21
|
+
|
|
22
|
+
| Bullet | Source | Read |
|
|
23
|
+
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
|
|
24
|
+
| The verb | [Microsoft Learn style quick start](https://learn.microsoft.com/en-us/contribute/content/style-quick-start): small words "(such as "a," "the," "that," and "is") are crucial for machine translation" | 2026-10-28 |
|
|
25
|
+
| A qualifier that bounds a claim | [Google style guide, Excessive claims](https://developers.google.com/style/excessive-claims): "avoid superlatives like best, simplest, fastest, never, and always", the claim a dropped bound turns `most` into | 2026-10-28 |
|
|
26
|
+
| The source and the date of a claim | House choice: a measurement with no ref or date cannot be re-run, so a later reader cannot tell a current number from a stale one | 2026-10-28 |
|
|
27
|
+
| The actor | [Google style guide, Active voice](https://developers.google.com/style/voice): "Make clear who's performing the action" | 2026-10-28 |
|
|
28
|
+
|
|
29
|
+
## Keeping a compressed sentence unambiguous
|
|
30
|
+
|
|
31
|
+
| Bullet | Source | Read |
|
|
32
|
+
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
|
|
33
|
+
| Resolve every pronoun | [Google style guide, Write for a global audience](https://developers.google.com/style/translation): "if a pronoun is ambiguous, then replace it with the appropriate noun" | 2026-10-28 |
|
|
34
|
+
| Attach every modifier | [Google style guide, Write for a global audience](https://developers.google.com/style/translation): "place a word like only immediately before the word or phrase that it relates to" | 2026-10-28 |
|
|
35
|
+
| Quantify an amount | [Google style guide, Excessive claims](https://developers.google.com/style/excessive-claims): avoid a statement "about performance or cost that isn't easily verifiable with data that's available to the reader" | 2026-10-28 |
|
|
36
|
+
| Split two claims joined by `and` | [digital.gov, Write short sentences](https://digital.gov/guides/plain-language/writing/clear-short): "Break up your idea into parts and make each one the subject of its own sentence" | 2026-10-28 |
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Machine tells reference ledger
|
|
3
|
+
description: Sources for claude/skills/write-human/references/machine-tells.md, one entry per tell, held as lists since the reference carries no ledgered bullet
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Machine tells reference ledger
|
|
7
|
+
|
|
8
|
+
The field is plain-language writing and technical editing, narrowed to the patterns editors flag in model-written text. Wikipedia's Signs of AI writing, which the humanizer catalog `adopted.md` names is built on, carries most tells. Every source below was read on 2026-10-28.
|
|
9
|
+
|
|
10
|
+
The reference states each tell as prose under its own H2, and its one list is `## Contents`, which the audit skips. A table here would count rows against no bullet and fail, so each tell takes a list entry instead.
|
|
11
|
+
|
|
12
|
+
## Sourced to the catalog of model tells
|
|
13
|
+
|
|
14
|
+
- Rule of three: [Signs of AI writing](https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing) says "LLMs overuse the rule of three"
|
|
15
|
+
- Inline-header lists: the same page says model output "often includes vertical lists formatted in a specific way"
|
|
16
|
+
- Closing restatement: the same page names section summaries, which older models "often added" to longer outputs
|
|
17
|
+
- Escalating parallel clauses: the same page names negative parallelisms, which read "as though it is clearing up a common misconception"
|
|
18
|
+
- Empty superlative frames: the same page says "LLM writing often puffs up the importance of the subject matter"
|
|
19
|
+
|
|
20
|
+
## Sourced to a style guide
|
|
21
|
+
|
|
22
|
+
- Synonym cycling: [Google, Write for a global audience](https://developers.google.com/style/translation) says "If you use different names for the same thing", readers "might think you're referring to different concepts"
|
|
23
|
+
- Adverb propping: [Google, Excessive claims](https://developers.google.com/style/excessive-claims) rules out a claim "that isn't easily verifiable with data that's available to the reader"
|
|
24
|
+
- Passive voice with an unnamed actor: [Google, Active voice](https://developers.google.com/style/voice) says "Make clear who's performing the action"
|
|
25
|
+
|
|
26
|
+
## House choices
|
|
27
|
+
|
|
28
|
+
- False ranges: a `from X to Y` over items with no scale claims a coverage the sentence never lists, so a reader cannot check what falls inside
|
|
29
|
+
- Question as transition: a question standing between paragraphs delays the answer by one line and adds no claim, the padding the density rules cut
|
|
30
|
+
- A colon or comma where the dash was: a session told to drop a banned dash swaps the mark and keeps the join, so the ban clears and the habit stays
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Route edits to a rule file to the create-rule skill for its shape and checklist
|
|
3
|
+
paths:
|
|
4
|
+
- '.claude/rules/project/**/*.md'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Rule standards
|
|
8
|
+
|
|
9
|
+
## Rule files
|
|
10
|
+
|
|
11
|
+
- Load the `canon:create-rule` skill before a hand edit to a rule, since it carries the frontmatter, the shape limits, and the bullet checklist.
|
|
12
|
+
- Report it rather than proceeding silently when `canon:create-rule` does not resolve, as in a project that installed governance without the plugin.
|
|
13
|
+
- Run `canon gov audit` over each rule the edit touched before committing it.
|
package/package.json
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Reports whether a pull request changes anything besides Markdown, called by
|
|
3
|
+
# the changes job in verify.yml to skip the macOS tests on a docs-only change.
|
|
4
|
+
# Prints code=false only when git lists at least one changed file and every
|
|
5
|
+
# one ends in .md, so a missing base, a failed diff, or an empty diff runs the
|
|
6
|
+
# tests rather than skipping them. Renames are split into a delete and an add,
|
|
7
|
+
# so a code file renamed to .md still counts as code.
|
|
8
|
+
#
|
|
9
|
+
# Usage: docs-only.sh <base>
|
|
10
|
+
# Writes to $GITHUB_OUTPUT, or stdout outside Actions, and always exits 0.
|
|
11
|
+
|
|
12
|
+
set -e
|
|
13
|
+
|
|
14
|
+
out=${GITHUB_OUTPUT:-/dev/stdout}
|
|
15
|
+
code=true
|
|
16
|
+
|
|
17
|
+
if [ $# -eq 1 ] && files=$(git diff --name-only --no-renames "$1" HEAD) && [ -n "$files" ]; then
|
|
18
|
+
if ! grep -qv '\.md$' <<<"$files"; then
|
|
19
|
+
code=false
|
|
20
|
+
fi
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
echo "code=$code" >>"$out"
|
|
24
|
+
echo "code=$code" >&2
|
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Commits the
|
|
2
|
+
# Commits the recaptured home baseline onto the refresh branch and opens or
|
|
3
3
|
# updates its pull request, called by refresh-capture-frames.yml after the
|
|
4
|
-
# capture
|
|
4
|
+
# capture step. The step holds GH_TOKEN and HUSKY, since the token scope is
|
|
5
5
|
# read off the workflow.
|
|
6
6
|
#
|
|
7
7
|
# Hooks stay off, since pre-push runs a check whose format step needs shfmt,
|
|
8
8
|
# which the runner lacks. verify.yml checks the pull request.
|
|
9
9
|
#
|
|
10
|
-
# The
|
|
10
|
+
# The baseline commits where they rendered, then only the files that commit wrote
|
|
11
11
|
# or dropped carry onto main fetched now, so a merge landing mid-run neither
|
|
12
|
-
# leaves the branch on an older base nor loses a frame it moved. The pull
|
|
12
|
+
# leaves the branch on an older base nor loses a baseline frame it moved. The pull
|
|
13
13
|
# request resolves by number, since every merged refresh leaves a closed one on
|
|
14
14
|
# the same head.
|
|
15
15
|
set -e
|
|
16
16
|
|
|
17
17
|
branch=chore/refresh-capture-frames
|
|
18
|
-
title="chore(web): refresh
|
|
18
|
+
title="chore(web): refresh home baseline"
|
|
19
19
|
|
|
20
|
-
if [ -z "$(git status --porcelain assets/
|
|
21
|
-
echo "
|
|
20
|
+
if [ -z "$(git status --porcelain assets/evidence)" ]; then
|
|
21
|
+
echo "The home baseline is current, nothing to refresh."
|
|
22
22
|
exit 0
|
|
23
23
|
fi
|
|
24
24
|
git config user.name "github-actions[bot]"
|
|
25
25
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com" # canon-allow-reference: the bot account id, not a commit
|
|
26
|
-
git add assets/
|
|
26
|
+
git add assets/evidence
|
|
27
27
|
git commit -m "$title"
|
|
28
28
|
rendered=$(git rev-parse HEAD)
|
|
29
29
|
written=$(git diff --name-only --no-renames --diff-filter=d HEAD~1 HEAD)
|
|
@@ -36,8 +36,8 @@ fi
|
|
|
36
36
|
if [ -n "$dropped" ]; then
|
|
37
37
|
printf '%s\n' "$dropped" | xargs -d '\n' git rm -q --ignore-unmatch --
|
|
38
38
|
fi
|
|
39
|
-
if git diff --cached --quiet -- assets/
|
|
40
|
-
echo "Main already carries
|
|
39
|
+
if git diff --cached --quiet -- assets/evidence; then
|
|
40
|
+
echo "Main already carries this baseline, nothing to refresh."
|
|
41
41
|
exit 0
|
|
42
42
|
fi
|
|
43
43
|
git commit -m "$title"
|
|
@@ -50,11 +50,10 @@ if [ -z "$number" ]; then
|
|
|
50
50
|
--title "$title" \
|
|
51
51
|
--body "## Summary
|
|
52
52
|
|
|
53
|
-
- Refreshes the
|
|
53
|
+
- Refreshes the home page baseline from main so canon pr evidence compares against a current render.
|
|
54
54
|
|
|
55
55
|
## Key changes
|
|
56
56
|
|
|
57
|
-
- Regenerates the count and token bearing frames, their images, and their stamps from main.
|
|
58
57
|
- Recaptures the home page baseline under assets/evidence/home.
|
|
59
58
|
- Merges itself by squash once its required check passes."
|
|
60
59
|
number=$(gh pr list --head "$branch" --state open --json number --jq '.[0].number')
|
package/src/audits/catalog.ts
CHANGED
|
@@ -379,7 +379,8 @@ function restatedCounts(record: unknown): Record<string, number> | undefined {
|
|
|
379
379
|
* a short one are fixed by different passes.
|
|
380
380
|
*/
|
|
381
381
|
function ruleCounts(record: unknown): Record<string, number> | undefined {
|
|
382
|
-
const
|
|
382
|
+
const root = asObject(record)
|
|
383
|
+
const rules = root?.rules
|
|
383
384
|
if (!Array.isArray(rules)) return undefined
|
|
384
385
|
|
|
385
386
|
let shapeRules = 0
|
|
@@ -397,7 +398,11 @@ function ruleCounts(record: unknown): Record<string, number> | undefined {
|
|
|
397
398
|
if ((lengthOf(ledger.mismatches) ?? 0) > 0) ledgerMismatches += 1
|
|
398
399
|
}
|
|
399
400
|
|
|
400
|
-
|
|
401
|
+
// A target ledger with no rule sits beside `rules` rather than in it. A
|
|
402
|
+
// binary predating it carries no `orphans` key, which reads as none.
|
|
403
|
+
const orphanLedgers = lengthOf(root?.orphans) ?? 0
|
|
404
|
+
|
|
405
|
+
return { shapeRules, missingLedgers, ledgerMismatches, orphanLedgers }
|
|
401
406
|
}
|
|
402
407
|
|
|
403
408
|
function findingsOnly(record: unknown): Record<string, number> | undefined {
|
package/src/capture/stamp.ts
CHANGED
|
@@ -6,13 +6,11 @@ const STAMP_EXTENSION = '.stamp'
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Provenance for one rendered PNG: the markup it came from and a digest over
|
|
9
|
-
* each side of the pair.
|
|
10
|
-
*
|
|
11
|
-
* colon, and the space after it are a contract between the writer and the gate
|
|
12
|
-
* rather than formatting.
|
|
9
|
+
* each side of the pair. Nothing reads the digests back today, so no check
|
|
10
|
+
* fails when either side moves.
|
|
13
11
|
*
|
|
14
12
|
* Both sides are recorded because either can move alone. The markup digest
|
|
15
|
-
*
|
|
13
|
+
* would show an edit committed with no capture, and the image digest a PNG
|
|
16
14
|
* replaced under markup that never changed.
|
|
17
15
|
*/
|
|
18
16
|
export interface CaptureStamp {
|
package/src/claude/cases/misc.ts
CHANGED
|
@@ -7,6 +7,20 @@ export const MISC_CASES: readonly SkillCase[] = [
|
|
|
7
7
|
'Bundle up every open call that only I can make and ask me all at once.',
|
|
8
8
|
expect: 'decision-escalate',
|
|
9
9
|
},
|
|
10
|
+
{
|
|
11
|
+
prompt: 'What do you need me to decide before you keep going?',
|
|
12
|
+
expect: 'decision-escalate',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
prompt:
|
|
16
|
+
'Poke holes in this plan before we build it. Is the problem even real?',
|
|
17
|
+
expect: 'proposal-critique',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
prompt:
|
|
21
|
+
'CI keeps failing on the install step and the plan is to add a retry. Is the cause actually ours, and is this fix worth it?',
|
|
22
|
+
expect: 'proposal-critique',
|
|
23
|
+
},
|
|
10
24
|
{
|
|
11
25
|
prompt:
|
|
12
26
|
'Show me a few different treatments for this callout so I can pick one by looking.',
|
|
@@ -82,6 +82,7 @@ export const LEDGER_SKILLS: readonly string[] = [
|
|
|
82
82
|
join(SHIPPED_CORPUS, 'write-human'),
|
|
83
83
|
join(SHIPPED_CORPUS, 'test-first'),
|
|
84
84
|
join(SHIPPED_CORPUS, 'ui-craft'),
|
|
85
|
+
join(SHIPPED_CORPUS, 'proposal-critique'),
|
|
85
86
|
]
|
|
86
87
|
|
|
87
88
|
function references(...names: string[]): string[] {
|
|
@@ -119,7 +120,13 @@ export const BULLET_LEDGER_SKILLS: Readonly<Record<string, BulletLedgerScope>> =
|
|
|
119
120
|
},
|
|
120
121
|
[join(SHIPPED_CORPUS, 'teach-craft')]: { exempt: [], body: true },
|
|
121
122
|
[join(SHIPPED_CORPUS, 'test-craft')]: { exempt: [], body: true },
|
|
123
|
+
[join(SHIPPED_CORPUS, 'test-first')]: { exempt: [], body: true },
|
|
122
124
|
[join(SHIPPED_CORPUS, 'codebase-layout')]: { exempt: [], body: true },
|
|
125
|
+
[join(SHIPPED_CORPUS, 'search-craft')]: { exempt: [], body: true },
|
|
126
|
+
[join(SHIPPED_CORPUS, 'write-human')]: { exempt: [], body: true },
|
|
127
|
+
[join(SHIPPED_CORPUS, 'systematic-debugging')]: { exempt: [], body: true },
|
|
128
|
+
[join(SHIPPED_CORPUS, 'build-in-slices')]: { exempt: [], body: true },
|
|
129
|
+
[join(SHIPPED_CORPUS, 'proposal-critique')]: { exempt: [], body: true },
|
|
123
130
|
}
|
|
124
131
|
|
|
125
132
|
/** Where a per-bullet ledger lives, as `<skill>/<file>.md` below it. */
|
|
@@ -134,6 +141,15 @@ export function skillLedgerPath(folder: string, file: string): string {
|
|
|
134
141
|
return join(SKILL_LEDGERS, folder, inside.startsWith('..') ? file : inside)
|
|
135
142
|
}
|
|
136
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Where a target keeps the ledgers behind its own skills, as `<skill>/<file>.md`
|
|
146
|
+
* below it. A tree holding `governance/rules/` is the toolkit, which keeps its
|
|
147
|
+
* ledgers at `SKILL_LEDGERS`, so this folder is never read there.
|
|
148
|
+
*/
|
|
149
|
+
export const PROJECT_SKILL_LEDGERS = join('canon', 'ledger', 'skills')
|
|
150
|
+
|
|
151
|
+
const TOOLKIT_MARKER = join('governance', 'rules')
|
|
152
|
+
|
|
137
153
|
/** The closing H2s a practice skill carries, matched exactly outside fences. */
|
|
138
154
|
export const PRACTICE_SECTIONS: readonly string[] = [
|
|
139
155
|
'Excuses and rebuttals',
|
|
@@ -161,6 +177,9 @@ export const PRACTICE_LEDGER = join('references', 'adopted.md')
|
|
|
161
177
|
*/
|
|
162
178
|
const EXTERNAL_LINK = /\]\(\s*https?:\/\/|<https?:\/\//
|
|
163
179
|
|
|
180
|
+
/** The address an inline link or an autolink cites, captured for comparison. */
|
|
181
|
+
const EXTERNAL_ADDRESS = /\]\(\s*(https?:\/\/[^)\s]+)|<(https?:\/\/[^>\s]+)>/g
|
|
182
|
+
|
|
164
183
|
/**
|
|
165
184
|
* A table cell opening on the marker the governance ledgers already use for a
|
|
166
185
|
* rule the toolkit chose, so one check reads every ledger alike.
|
|
@@ -277,6 +296,8 @@ interface ReferenceLedger {
|
|
|
277
296
|
readonly ledgerRel: string
|
|
278
297
|
/** Undefined when the ledger file is absent. */
|
|
279
298
|
readonly mismatches: readonly LedgerMismatch[] | undefined
|
|
299
|
+
/** Every source the ledger links, normalized by `sourceKey`. */
|
|
300
|
+
readonly sources: readonly string[]
|
|
280
301
|
}
|
|
281
302
|
|
|
282
303
|
interface SkillText {
|
|
@@ -302,6 +323,9 @@ export async function auditSkills(root: string): Promise<SkillsAudit> {
|
|
|
302
323
|
present.map((corpus) => readCorpus(root, corpus.rel, corpus.dir)),
|
|
303
324
|
)
|
|
304
325
|
const sources = perCorpus.flat()
|
|
326
|
+
const projectLedgers = existsSync(join(root, TOOLKIT_MARKER))
|
|
327
|
+
? []
|
|
328
|
+
: await readProjectLedgers(root, sources)
|
|
305
329
|
|
|
306
330
|
return {
|
|
307
331
|
corpora: present.map((corpus, index) => ({
|
|
@@ -334,6 +358,7 @@ export async function auditSkills(root: string): Promise<SkillsAudit> {
|
|
|
334
358
|
sources,
|
|
335
359
|
),
|
|
336
360
|
...sources.flatMap(practiceFindings),
|
|
361
|
+
...projectLedgers,
|
|
337
362
|
],
|
|
338
363
|
referenceContents: sources.flatMap((source) =>
|
|
339
364
|
source.bodies.flatMap(contentsFindings),
|
|
@@ -453,17 +478,76 @@ async function readReferenceLedgers(
|
|
|
453
478
|
files.map(async ({ path, text }) => {
|
|
454
479
|
const ledgerRel = skillLedgerPath(folder, path)
|
|
455
480
|
const ledgerPath = join(root, ledgerRel)
|
|
456
|
-
const
|
|
457
|
-
?
|
|
458
|
-
ledgerGroups(text, path === 'SKILL.md'),
|
|
459
|
-
countLedgerRows(await Bun.file(ledgerPath).text()),
|
|
460
|
-
)
|
|
481
|
+
const ledger = existsSync(ledgerPath)
|
|
482
|
+
? await Bun.file(ledgerPath).text()
|
|
461
483
|
: undefined
|
|
462
|
-
|
|
484
|
+
const mismatches =
|
|
485
|
+
ledger === undefined
|
|
486
|
+
? undefined
|
|
487
|
+
: compareLedger(
|
|
488
|
+
ledgerGroups(text, path === 'SKILL.md'),
|
|
489
|
+
countLedgerRows(ledger),
|
|
490
|
+
)
|
|
491
|
+
return {
|
|
492
|
+
rel: join(skillRel, path),
|
|
493
|
+
ledgerRel,
|
|
494
|
+
mismatches,
|
|
495
|
+
sources: ledger === undefined ? [] : linkedSources(ledger),
|
|
496
|
+
}
|
|
463
497
|
}),
|
|
464
498
|
)
|
|
465
499
|
}
|
|
466
500
|
|
|
501
|
+
/**
|
|
502
|
+
* Pairs each file a target wrote under `PROJECT_SKILL_LEDGERS` with the skill
|
|
503
|
+
* file of that name, `SKILL.md` with the body and any other with the reference
|
|
504
|
+
* at `references/<file>`. Nothing is owed, since a project has no list to owe a
|
|
505
|
+
* ledger against, so only a ledger that exists is read, and one naming a skill
|
|
506
|
+
* or a file that is gone is reported rather than skipped.
|
|
507
|
+
*/
|
|
508
|
+
async function readProjectLedgers(
|
|
509
|
+
root: string,
|
|
510
|
+
sources: readonly SkillSource[],
|
|
511
|
+
): Promise<SkillFinding[]> {
|
|
512
|
+
const dir = join(root, PROJECT_SKILL_LEDGERS)
|
|
513
|
+
if (!existsSync(dir)) return []
|
|
514
|
+
const paths = [
|
|
515
|
+
...new Bun.Glob('**/*.md').scanSync({ cwd: dir, onlyFiles: true }),
|
|
516
|
+
].sort()
|
|
517
|
+
|
|
518
|
+
const perLedger = await Promise.all(
|
|
519
|
+
paths.map(async (path): Promise<SkillFinding[]> => {
|
|
520
|
+
const ledgerRel = join(PROJECT_SKILL_LEDGERS, path)
|
|
521
|
+
const [folder, ...rest] = path.split('/')
|
|
522
|
+
const owners = sources.filter((source) => source.folder === folder)
|
|
523
|
+
if (rest.length === 0 || owners.length === 0) {
|
|
524
|
+
return [{ rel: ledgerRel, detail: `missing skill: ${folder}` }]
|
|
525
|
+
}
|
|
526
|
+
const file = rest.join('/')
|
|
527
|
+
const target = file === 'SKILL.md' ? file : join('references', file)
|
|
528
|
+
const rows = countLedgerRows(await Bun.file(join(dir, path)).text())
|
|
529
|
+
|
|
530
|
+
return owners.flatMap((owner) => {
|
|
531
|
+
const rel = join(owner.rel, target)
|
|
532
|
+
const body = owner.bodies.find((entry) => entry.rel === rel)
|
|
533
|
+
if (body === undefined) {
|
|
534
|
+
return [{ rel: ledgerRel, detail: `missing file: ${rel}` }]
|
|
535
|
+
}
|
|
536
|
+
return referenceLedgerFindings({
|
|
537
|
+
rel,
|
|
538
|
+
ledgerRel,
|
|
539
|
+
mismatches: compareLedger(
|
|
540
|
+
ledgerGroups(body.text, target === 'SKILL.md'),
|
|
541
|
+
rows,
|
|
542
|
+
),
|
|
543
|
+
sources: [],
|
|
544
|
+
})
|
|
545
|
+
})
|
|
546
|
+
}),
|
|
547
|
+
)
|
|
548
|
+
return perLedger.flat()
|
|
549
|
+
}
|
|
550
|
+
|
|
467
551
|
/**
|
|
468
552
|
* Directives per H2, `null` for any above the first. A contents list repeats
|
|
469
553
|
* the headings rather than stating a rule, so `## Contents` takes no rows. A
|
|
@@ -681,6 +765,7 @@ function practiceFindings(source: SkillSource): SkillFinding[] {
|
|
|
681
765
|
return [
|
|
682
766
|
...sections,
|
|
683
767
|
...ledgerFindings(source),
|
|
768
|
+
...duplicateFindings(source),
|
|
684
769
|
...source.referenceLedgers.flatMap(referenceLedgerFindings),
|
|
685
770
|
...source.staleExemptions.map((rel) => ({
|
|
686
771
|
rel,
|
|
@@ -710,17 +795,78 @@ function ledgerFindings(source: SkillSource): SkillFinding[] {
|
|
|
710
795
|
return [{ rel: source.rel, detail: `missing ledger: ${PRACTICE_LEDGER}` }]
|
|
711
796
|
}
|
|
712
797
|
|
|
713
|
-
const
|
|
714
|
-
|
|
798
|
+
const lines = bodyLines(source.ledger).filter((line) => !line.fenced)
|
|
799
|
+
const cites = lines
|
|
715
800
|
.map((line) => maskCodeSpans(line.text))
|
|
716
801
|
.some((text) => EXTERNAL_LINK.test(text) || HOUSE_CHOICE.test(text))
|
|
717
802
|
if (cites) return []
|
|
803
|
+
if (pointsAtLedgers(source, lines)) return []
|
|
718
804
|
|
|
719
805
|
return [
|
|
720
806
|
{ rel: source.rel, detail: `ledger cites nothing: ${PRACTICE_LEDGER}` },
|
|
721
807
|
]
|
|
722
808
|
}
|
|
723
809
|
|
|
810
|
+
/**
|
|
811
|
+
* A skill on the per-bullet list cites its sources in its ledger folder, so a
|
|
812
|
+
* record whose every link moved there is left pointing at the folder instead.
|
|
813
|
+
* A skill off the list has nowhere else for a source to live.
|
|
814
|
+
*/
|
|
815
|
+
function pointsAtLedgers(
|
|
816
|
+
source: SkillSource,
|
|
817
|
+
lines: readonly { text: string }[],
|
|
818
|
+
): boolean {
|
|
819
|
+
if (BULLET_LEDGER_SKILLS[source.rel] === undefined) return false
|
|
820
|
+
const folder = `${join(SKILL_LEDGERS, source.folder)}/`
|
|
821
|
+
return lines.some((line) => line.text.includes(folder))
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* The source record keeps a source no ledger row cites, so a link in both
|
|
826
|
+
* places is a second copy that drifts on the first edit to either.
|
|
827
|
+
*/
|
|
828
|
+
function duplicateFindings(source: SkillSource): SkillFinding[] {
|
|
829
|
+
if (source.ledger === undefined) return []
|
|
830
|
+
if (BULLET_LEDGER_SKILLS[source.rel] === undefined) return []
|
|
831
|
+
|
|
832
|
+
const ledgered = new Map<string, string>()
|
|
833
|
+
for (const entry of source.referenceLedgers) {
|
|
834
|
+
for (const key of entry.sources) {
|
|
835
|
+
if (!ledgered.has(key)) ledgered.set(key, entry.ledgerRel)
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
return [...new Set(linkedSources(source.ledger))].flatMap((key) => {
|
|
840
|
+
const ledgerRel = ledgered.get(key)
|
|
841
|
+
if (ledgerRel === undefined) return []
|
|
842
|
+
return [
|
|
843
|
+
{
|
|
844
|
+
rel: join(source.rel, PRACTICE_LEDGER),
|
|
845
|
+
detail: `duplicate source: ${key} in ${ledgerRel}`,
|
|
846
|
+
},
|
|
847
|
+
]
|
|
848
|
+
})
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/** Every external address a text links outside fences and code spans. */
|
|
852
|
+
function linkedSources(text: string): string[] {
|
|
853
|
+
return bodyLines(text)
|
|
854
|
+
.filter((line) => !line.fenced)
|
|
855
|
+
.flatMap((line) =>
|
|
856
|
+
[...maskCodeSpans(line.text).matchAll(EXTERNAL_ADDRESS)].map((match) =>
|
|
857
|
+
sourceKey(match[1] ?? match[2] ?? ''),
|
|
858
|
+
),
|
|
859
|
+
)
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* An anchor and a trailing slash reach the same page, so both drop. A commit
|
|
864
|
+
* pin names a different version, so the path stays as written.
|
|
865
|
+
*/
|
|
866
|
+
function sourceKey(address: string): string {
|
|
867
|
+
return address.replace(/#.*$/, '').replace(/\/+$/, '')
|
|
868
|
+
}
|
|
869
|
+
|
|
724
870
|
/**
|
|
725
871
|
* Asks only the shipped corpus for a family, since the family names the skill
|
|
726
872
|
* map group a skill's row sits under and an internal skill takes no row.
|
package/src/cli/help.ts
CHANGED
|
@@ -99,7 +99,7 @@ const EXAMPLES = [
|
|
|
99
99
|
'canon indexes regen --dry-run --json',
|
|
100
100
|
'canon docs list --json',
|
|
101
101
|
'canon docs agents',
|
|
102
|
-
'canon capture
|
|
102
|
+
'canon capture web/public/previews/design-tokens/index.html --selector body --out assets/evidence',
|
|
103
103
|
'canon inventory focus --json',
|
|
104
104
|
'canon drive http://localhost:4173 run.json --json',
|
|
105
105
|
'canon take http://localhost:4173 plan.json --out takes --json',
|
package/src/commands/claude.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
SKILL_LEDGERS,
|
|
35
35
|
PRACTICE_SECTIONS,
|
|
36
36
|
PRACTICE_SKILLS,
|
|
37
|
+
PROJECT_SKILL_LEDGERS,
|
|
37
38
|
REQUIREMENT_LIMITS,
|
|
38
39
|
REQUIREMENT_SECTIONS,
|
|
39
40
|
RETIRED_REQUIREMENT_SECTION,
|
|
@@ -1772,7 +1773,7 @@ function reportProvenance(report: SkillsAudit): void {
|
|
|
1772
1773
|
/**
|
|
1773
1774
|
* Reports without failing the run for the reason provenance does. The list
|
|
1774
1775
|
* names shipped folders alone, which the step says so a target reading a pass
|
|
1775
|
-
* knows its own skills were
|
|
1776
|
+
* knows its own skills were read only against a ledger it wrote.
|
|
1776
1777
|
*/
|
|
1777
1778
|
function reportPracticeShape(report: SkillsAudit): void {
|
|
1778
1779
|
logStep('Practice shape')
|
|
@@ -1783,7 +1784,10 @@ function reportPracticeShape(report: SkillsAudit): void {
|
|
|
1783
1784
|
`A skill on the per-bullet list, ${plural(Object.keys(BULLET_LEDGER_SKILLS).length, 'skill')} here, also matches each reference it does not exempt, and a listed body, to a ledger under ${SKILL_LEDGERS}, one row per bullet under each H2.`,
|
|
1784
1785
|
)
|
|
1785
1786
|
logInfo(
|
|
1786
|
-
`Both lists are kept here and name ${plural(PRACTICE_SKILLS.length, 'practice skill')} and ${plural(LEDGER_SKILLS.length, 'ledger skill')}, so a skill outside ${SHIPPED_CORPUS} is never read.`,
|
|
1787
|
+
`Both lists are kept here and name ${plural(PRACTICE_SKILLS.length, 'practice skill')} and ${plural(LEDGER_SKILLS.length, 'ledger skill')}, so a skill outside ${SHIPPED_CORPUS} is never read against them.`,
|
|
1788
|
+
)
|
|
1789
|
+
logInfo(
|
|
1790
|
+
`A tree with no governance/rules/ has each file under ${PROJECT_SKILL_LEDGERS} matched to the skill file of its name, one row per bullet under each H2, and owes none.`,
|
|
1787
1791
|
)
|
|
1788
1792
|
|
|
1789
1793
|
if (report.practiceShape.length === 0) {
|
package/src/commands/gov.ts
CHANGED
|
@@ -646,7 +646,9 @@ export function register(program: Command): void {
|
|
|
646
646
|
` A rule under ${CORE_FOLDERS.join(', ')} has its ledger checked where`,
|
|
647
647
|
' one exists, and under --touched owes one once the branch changed it.',
|
|
648
648
|
' With no baseline against main, --touched reads no rule as touched.',
|
|
649
|
-
' Any other tree reads .claude/rules/project/
|
|
649
|
+
' Any other tree reads .claude/rules/project/ and checks the ledger of',
|
|
650
|
+
' each rule that has one at canon/ledger/rules/<subfolder>/<slug>.md.',
|
|
651
|
+
' A ledger there with no rule is a finding on a whole-corpus read.',
|
|
650
652
|
' .claude/rules/canon/ is fixed at the toolkit source and never read',
|
|
651
653
|
' unless named. A named path resolves against --root and must sit',
|
|
652
654
|
' inside it.',
|
|
@@ -757,7 +759,10 @@ function reportAudit(
|
|
|
757
759
|
intro('canon gov audit')
|
|
758
760
|
|
|
759
761
|
const shaped = report.rules.filter((reading) => reading.findings.length > 0)
|
|
760
|
-
const ledgers =
|
|
762
|
+
const ledgers = [
|
|
763
|
+
...report.rules.flatMap(describeLedger),
|
|
764
|
+
...report.orphans.map((path) => `${path}: no rule for this ledger`),
|
|
765
|
+
]
|
|
761
766
|
|
|
762
767
|
logStep('Corpus')
|
|
763
768
|
logInfo(
|
|
@@ -791,12 +796,12 @@ function reportAudit(
|
|
|
791
796
|
'no baseline against main, so --touched reads no core rule as touched',
|
|
792
797
|
)
|
|
793
798
|
}
|
|
794
|
-
if (
|
|
795
|
-
logInfo('not read, since ledgers live in the toolkit and never ship')
|
|
796
|
-
} else if (ledgers.length === 0) {
|
|
797
|
-
logInfo('every rule owing a ledger has one, with a row per bullet')
|
|
798
|
-
} else {
|
|
799
|
+
if (ledgers.length > 0) {
|
|
799
800
|
for (const line of ledgers) logWarn(line)
|
|
801
|
+
} else if (report.scope === 'target') {
|
|
802
|
+
logInfo('every project ledger has its rule and a row per bullet')
|
|
803
|
+
} else {
|
|
804
|
+
logInfo('every rule owing a ledger has one, with a row per bullet')
|
|
800
805
|
}
|
|
801
806
|
|
|
802
807
|
const failing = report.rules.filter(hasFinding).length
|