@erclx/canon 5.33.0 → 5.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/README.md +3 -5
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/api-design/REQUIREMENT.md +2 -3
  4. package/claude/skills/api-design/SKILL.md +2 -3
  5. package/claude/skills/api-design/references/adopted.md +1 -1
  6. package/claude/skills/auto-ship/references/draft-mark.md +1 -1
  7. package/claude/skills/canon-cli/SKILL.md +10 -11
  8. package/claude/skills/canon-feedback/REQUIREMENT.md +1 -0
  9. package/claude/skills/canon-feedback/SKILL.md +10 -28
  10. package/claude/skills/canon-feedback/references/template.md +53 -0
  11. package/claude/skills/canvas-board/SKILL.md +4 -1
  12. package/claude/skills/canvas-board/references/canvas.md +26 -3
  13. package/claude/skills/code-craft/REQUIREMENT.md +1 -1
  14. package/claude/skills/code-craft/SKILL.md +0 -1
  15. package/claude/skills/create-rule/SKILL.md +1 -1
  16. package/claude/skills/create-rule/references/rule.md +3 -1
  17. package/claude/skills/create-skill/SKILL.md +2 -1
  18. package/claude/skills/create-skill/references/skill-paths.md +12 -15
  19. package/{standards/standard.md → claude/skills/create-skill/references/skill-reference.md} +50 -45
  20. package/claude/skills/create-skill/references/skill.md +10 -4
  21. package/claude/skills/draft-and-pick/SKILL.md +1 -1
  22. package/claude/skills/git-commit/references/versioning.md +13 -13
  23. package/claude/skills/git-issue/references/issue.md +4 -1
  24. package/claude/skills/git-pr/SKILL.md +1 -1
  25. package/claude/skills/git-pr/references/publish.md +1 -1
  26. package/claude/skills/review-address/REQUIREMENT.md +1 -1
  27. package/claude/skills/review-address/SKILL.md +2 -2
  28. package/claude/skills/review-craft/references/ui.md +1 -1
  29. package/claude/skills/review-pr/SKILL.md +3 -3
  30. package/claude/skills/review-pr/references/post.md +1 -1
  31. package/claude/skills/review-ui/REQUIREMENT.md +1 -1
  32. package/claude/skills/review-ui/SKILL.md +8 -6
  33. package/claude/skills/review-ui/references/driving.md +8 -8
  34. package/claude/skills/review-ui/references/post.md +52 -16
  35. package/claude/skills/role-orchestrator/REQUIREMENT.md +2 -2
  36. package/claude/skills/role-orchestrator/SKILL.md +11 -12
  37. package/claude/skills/role-orchestrator/references/orchestrator-cloud-launch.md +3 -1
  38. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +11 -8
  39. package/claude/skills/role-orchestrator/references/orchestrator-handback.md +10 -4
  40. package/claude/skills/role-orchestrator/references/orchestrator-launch.md +11 -2
  41. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +11 -12
  42. package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +12 -15
  43. package/claude/skills/role-orchestrator/references/orchestrator-reviewer-launch.md +6 -4
  44. package/claude/skills/role-orchestrator/scripts/poll.ts +50 -1
  45. package/claude/skills/role-reviewer/REQUIREMENT.md +17 -7
  46. package/claude/skills/role-reviewer/SKILL.md +17 -10
  47. package/claude/skills/role-worker/REQUIREMENT.md +1 -1
  48. package/claude/skills/role-worker/SKILL.md +6 -6
  49. package/claude/skills/session-compact/references/handoff-note.md +1 -1
  50. package/claude/skills/target-setup/SKILL.md +12 -6
  51. package/claude/skills/target-setup/references/detect.md +22 -5
  52. package/claude/skills/test-craft/REQUIREMENT.md +2 -3
  53. package/claude/skills/test-craft/SKILL.md +15 -3
  54. package/docs/agents/audits.md +1 -1
  55. package/docs/agents/canvas.md +5 -1
  56. package/docs/agents/commands.md +3 -5
  57. package/docs/agents/counts.md +5 -6
  58. package/docs/agents/drift-surfaces.md +15 -19
  59. package/docs/agents/gov-detect.md +47 -0
  60. package/docs/agents/index.md +6 -4
  61. package/docs/agents/install-and-sync.md +11 -31
  62. package/docs/agents/markdown-audit.md +10 -12
  63. package/docs/agents/migrate.md +1 -1
  64. package/docs/agents/pr-evidence-layout.md +70 -0
  65. package/docs/agents/pr-evidence.md +8 -9
  66. package/docs/agents/pr-frames.md +7 -2
  67. package/docs/agents/pr-lift.md +132 -0
  68. package/docs/agents/records-stale.md +1 -1
  69. package/docs/agents/records-validate.md +11 -23
  70. package/docs/agents/records.md +1 -1
  71. package/docs/agents/routing.md +1 -1
  72. package/docs/agents/rule-citations.md +11 -14
  73. package/docs/agents/scripting.md +4 -21
  74. package/docs/agents/state-scoped-risk.md +1 -1
  75. package/docs/agents/tasks-validate.md +1 -1
  76. package/docs/agents/teach.md +1 -1
  77. package/docs/target/index.md +1 -0
  78. package/docs/target/migrations.md +4 -0
  79. package/docs/target/projects.md +11 -11
  80. package/docs/target/scenarios.md +5 -6
  81. package/docs/target/standards-moved.md +32 -0
  82. package/docs/target/sync.md +5 -5
  83. package/docs/workflow/operating-model.md +16 -18
  84. package/docs/workflow/skill-map.md +1 -1
  85. package/governance/detect.toml +197 -0
  86. package/governance/renames.toml +0 -1
  87. package/governance/rules/code/000-code.md +1 -13
  88. package/governance/rules/code/010-testing.md +2 -30
  89. package/governance/rules/ui/400-ui.md +1 -1
  90. package/governance/rules/ui/480-canvas.md +13 -0
  91. package/governance/stacks/base.toml +15 -8
  92. package/governance/stacks/go.toml +2 -0
  93. package/governance/stacks/node.toml +1 -1
  94. package/governance/stacks/php.toml +2 -0
  95. package/governance/stacks/python.toml +1 -1
  96. package/package.json +1 -4
  97. package/scripts/core/install-check.sh +2 -3
  98. package/scripts/core/regen-hero.ts +0 -6
  99. package/scripts/lib/ui.sh +0 -9
  100. package/src/audits/catalog.ts +0 -9
  101. package/src/canvas/arrange.ts +66 -0
  102. package/src/canvas/capture.ts +20 -6
  103. package/src/canvas/content.ts +39 -0
  104. package/src/canvas/fit.ts +238 -0
  105. package/src/canvas/tokens.ts +28 -10
  106. package/src/capture/frame-values.ts +2 -32
  107. package/src/claude/routing.ts +1 -1
  108. package/src/claude/skills-reach.ts +4 -6
  109. package/src/cli/help.ts +3 -4
  110. package/src/cli.ts +0 -2
  111. package/src/commands/canvas.ts +179 -1
  112. package/src/commands/gov.ts +149 -10
  113. package/src/commands/init.ts +1 -1
  114. package/src/commands/labels.ts +4 -44
  115. package/src/commands/migrate.ts +1 -1
  116. package/src/commands/pr.ts +376 -1
  117. package/src/commands/records.ts +4 -20
  118. package/src/commands/tooling.ts +2 -4
  119. package/src/counts/catalogs.ts +0 -6
  120. package/src/feedback/body.ts +26 -5
  121. package/src/gate/boundaries.ts +2 -2
  122. package/src/gate/measures.ts +64 -72
  123. package/src/gate/sequencer.ts +51 -2
  124. package/src/gate/stages.ts +3 -22
  125. package/src/gov/citations.ts +11 -43
  126. package/src/gov/detect.ts +301 -0
  127. package/src/gov/list.ts +23 -2
  128. package/src/gov/stacks.ts +9 -0
  129. package/src/markdown/bans.ts +1 -1
  130. package/src/migrate/scratch-evidence.ts +2 -2
  131. package/src/pr/evidence.ts +218 -45
  132. package/src/pr/frames.ts +23 -3
  133. package/src/pr/lift.ts +418 -0
  134. package/src/pr/rest.ts +3 -0
  135. package/src/pr/review-scope.ts +76 -0
  136. package/src/records/validate.ts +6 -194
  137. package/src/sandbox/equivalence.ts +14 -7
  138. package/src/sandbox/provision.ts +0 -5
  139. package/src/secrets/shipped.ts +1 -2
  140. package/src/serve/static.ts +1 -2
  141. package/src/shipped/references.ts +29 -80
  142. package/src/sync/check.ts +18 -14
  143. package/src/tooling/inject.ts +6 -37
  144. package/src/tooling/read.ts +2 -3
  145. package/src/tooling/scan.ts +4 -17
  146. package/src/tooling/subfolder.ts +2 -66
  147. package/tooling/base/configs/scripts/verify.json +1 -1
  148. package/tooling/base/configs/scripts/verify.ts +1 -12
  149. package/tooling/base/manifest.toml +1 -2
  150. package/tooling/base/reference.md +10 -16
  151. package/tooling/base/seeds/.github/workflows/verify.yml +0 -3
  152. package/tooling/base/seeds/.lintstagedrc +1 -2
  153. package/tooling/base/seeds/canon/context/ci.md +0 -1
  154. package/tooling/base/seeds/canon/context/development.md +1 -1
  155. package/tooling/claude/reference.md +1 -1
  156. package/tooling/claude/seeds/.claude/hooks/review-post-scan.sh +13 -1
  157. package/tooling/go/reference.md +1 -1
  158. package/tooling/php/reference.md +1 -1
  159. package/tooling/python/reference.md +2 -2
  160. package/tooling/web/configs/.github/workflows/verify.yml +0 -3
  161. package/tooling/web/manifest.toml +0 -1
  162. package/tooling/web/reference.md +2 -4
  163. package/docs/agents/standards-audit.md +0 -40
  164. package/governance/rules/tooling/765-spelling.md +0 -19
  165. package/governance/stacks/astro.toml +0 -2
  166. package/governance/stacks/nextjs.toml +0 -2
  167. package/governance/stacks/node-server.toml +0 -2
  168. package/governance/stacks/python-fastapi.toml +0 -2
  169. package/governance/stacks/react.toml +0 -2
  170. package/src/commands/standards.ts +0 -278
  171. package/src/labels/spelling.ts +0 -120
  172. package/src/standards/audit.ts +0 -146
  173. package/src/standards/list.ts +0 -33
  174. package/src/standards/read.ts +0 -177
  175. package/src/standards/scope.ts +0 -56
  176. package/standards/index.md +0 -10
  177. package/tooling/base/configs/.vscode/extensions.json +0 -9
  178. package/tooling/base/configs/.vscode/settings.json +0 -3
  179. package/tooling/base/seeds/.cspell/project-terms.txt +0 -0
  180. package/tooling/base/seeds/.cspell/tech-stack.txt +0 -25
  181. package/tooling/base/seeds/cspell.json +0 -20
  182. package/tooling/go/seeds/.cspell/tech-stack.txt +0 -16
  183. package/tooling/nextjs/seeds/.cspell/tech-stack.txt +0 -3
  184. package/tooling/php/seeds/.cspell/tech-stack.txt +0 -5
  185. package/tooling/python/seeds/.cspell/tech-stack.txt +0 -18
  186. package/tooling/vite-react/seeds/.cspell/project-terms.txt +0 -1
  187. package/tooling/vite-react/seeds/.cspell/tech-stack.txt +0 -2
  188. package/tooling/web/configs/.vscode/extensions.json +0 -13
  189. package/tooling/web/configs/.vscode/settings.json +0 -10
  190. package/tooling/web/seeds/.cspell/tech-stack.txt +0 -21
@@ -104,28 +104,6 @@ const info = (text: string): Emission => ({ kind: 'info', text })
104
104
  const warn = (text: string): Emission => ({ kind: 'warn', text })
105
105
  const output = (text: string): Emission => ({ kind: 'output', text })
106
106
 
107
- /**
108
- * Rules no stack reaches, sorted the way `canon gov list` emits them.
109
- * `260-shadcn` and `320-tanstack-query` are opt-in libraries a project may not
110
- * want.
111
- *
112
- * `130-go`, `140-php`, `335-testing-go`, and `336-testing-php` are opt-in by
113
- * `--add` until a Go or PHP stack ships, which `target-setup` reaches by
114
- * matching a detected language against each rule's description.
115
- *
116
- * All are recorded here rather than in a config file: the list is what a
117
- * reader compares a new arrival against, and a config file would absorb the
118
- * arrival silently.
119
- */
120
- export const GOV_EXPECTED_UNREFERENCED = [
121
- '130-go',
122
- '140-php',
123
- '260-shadcn',
124
- '320-tanstack-query',
125
- '335-testing-go',
126
- '336-testing-php',
127
- ]
128
-
129
107
  /**
130
108
  * Scenarios declaring no expectation, taken from `canon sandbox coverage`
131
109
  * against a clean tree. Raising it is a deliberate edit that says which
@@ -170,16 +148,19 @@ function parseJson(payload: string): unknown {
170
148
  }
171
149
 
172
150
  /**
173
- * A rule no stack names is a report rather than a gate. All three standing
174
- * findings ship that way on purpose, so failing would fail every push over the
175
- * deliberate case and teach a reader to route around the stage.
151
+ * A rule no stack reaches arrives at a project only through `canon gov
152
+ * detect`, so each one needs a `governance/detect.toml` entry or nothing ever
153
+ * proposes it. A key naming a retired rule fails too, since it would hide from
154
+ * the rename sweep that keys the file by basename.
176
155
  *
177
- * The catalog is parsed here rather than piped through `bun --eval`, which the
178
- * script this replaces had to do and had to guard: that interpreter exits 0
179
- * when its script throws while stdin is a pipe, so a payload that is not JSON
180
- * printed nothing and exited clean, and empty already means every rule is
181
- * reached. A sentinel string carried success past that. Parsing in process
182
- * leaves the two states distinguishable with nothing to carry.
156
+ * The check replaced a fixed list of expected unreferenced rules. Once the
157
+ * stacks are thin, that list is most of the catalog and would be rewritten on
158
+ * every new rule, which is a list nobody reads.
159
+ *
160
+ * The catalog is parsed here rather than piped through `bun --eval`, whose
161
+ * interpreter exits 0 when its script throws while stdin is a pipe, so a
162
+ * payload that is not JSON would print nothing and read as every rule
163
+ * reached. Parsing in process keeps the two states distinguishable.
183
164
  */
184
165
  export const unreferencedRules: Measure = async (ctx) => {
185
166
  const { exitCode, stdout } = await ctx.cli(['gov', 'list', '--json'])
@@ -190,34 +171,67 @@ export const unreferencedRules: Measure = async (ctx) => {
190
171
  }
191
172
  }
192
173
 
193
- const record = parseJson(stdout) as { unreferenced?: unknown } | undefined
194
- if (!Array.isArray(record?.unreferenced)) {
174
+ const record = parseJson(stdout) as
175
+ | { unreferenced?: unknown; rules?: unknown; staleDetect?: unknown }
176
+ | undefined
177
+ if (
178
+ !Array.isArray(record?.unreferenced) ||
179
+ !Array.isArray(record?.rules) ||
180
+ !Array.isArray(record?.staleDetect)
181
+ ) {
195
182
  return {
196
183
  emissions: [],
197
184
  unmeasured:
198
- 'The governance catalog carried no readable unreferenced list.',
185
+ 'The governance catalog carried no readable unreferenced, rules, or staleDetect list.',
199
186
  }
200
187
  }
201
188
 
189
+ const detected = new Set(
190
+ record.rules
191
+ .filter(
192
+ (rule): rule is { name: string; detect: unknown } =>
193
+ typeof rule === 'object' &&
194
+ rule !== null &&
195
+ typeof (rule as { name?: unknown }).name === 'string',
196
+ )
197
+ .filter((rule) => rule.detect !== null && rule.detect !== undefined)
198
+ .map((rule) => rule.name),
199
+ )
202
200
  const unreferenced = record.unreferenced.map(String)
203
- if (unreferenced.length === 0) {
204
- return { emissions: [info('Every rule is reached by a stack')] }
201
+ const undetected = unreferenced.filter((rule) => !detected.has(rule))
202
+ const stale = record.staleDetect.map(String)
203
+
204
+ const emissions: Emission[] = []
205
+ if (undetected.length > 0) {
206
+ emissions.push(
207
+ warn(
208
+ `Reached by no stack and proposed by nothing: ${undetected.join(' ')}`,
209
+ ),
210
+ warn(
211
+ 'Name each in a stack, or add a table to governance/detect.toml with the deps or files that mark a project as wanting it.',
212
+ ),
213
+ )
214
+ }
215
+ if (stale.length > 0) {
216
+ emissions.push(
217
+ warn(`governance/detect.toml names no live rule: ${stale.join(' ')}`),
218
+ warn('Rename the key to the rule it became, or delete the table.'),
219
+ )
205
220
  }
206
221
 
207
- const listed = unreferenced.join(' ')
208
- if (listed === GOV_EXPECTED_UNREFERENCED.join(' ')) {
222
+ if (emissions.length > 0) {
209
223
  return {
210
- emissions: [
211
- info(`Reached by no stack: ${listed} (each recorded above with why)`),
212
- ],
224
+ emissions,
225
+ failure: 'An opt-in rule has no route to a project',
213
226
  }
214
227
  }
215
228
 
216
229
  return {
217
230
  emissions: [
218
- warn(`Reached by no stack: ${listed}`),
219
- warn(
220
- `Expected: ${GOV_EXPECTED_UNREFERENCED.join(' ')}. Name the new rule in a stack, or update GOV_EXPECTED_UNREFERENCED in src/gate/measures.ts and say why it reaches no stack.`,
231
+ info(
232
+ unreferenced.length === 0
233
+ ? 'Every rule is reached by a stack'
234
+ : `${plural(unreferenced.length, 'rule')} reached by no stack, each proposed by governance/detect.toml`,
221
235
  ),
222
236
  ],
223
237
  }
@@ -491,7 +505,7 @@ export const documentCeiling: Measure = async (ctx) => {
491
505
  * Whether a path's stem, its basename up to the first `.`, repeats its
492
506
  * immediate parent folder, either whole or as a `<folder>-` prefix.
493
507
  *
494
- * Exact rather than singular-aware, so `standards/standard.md` passes. A false
508
+ * Exact rather than singular-aware, so `skills/skill.md` passes. A false
495
509
  * positive on a gate stage teaches a contributor to route around it, and a
496
510
  * widened match is cheap to add once a real echo slips through.
497
511
  */
@@ -757,28 +771,6 @@ export function seedEntryCount(payload: string): number {
757
771
  )
758
772
  }
759
773
 
760
- /**
761
- * Scoped to arrival rather than the corpus, since `standards/standard.md`
762
- * forbids writing a criterion into an existing standard outside the change that
763
- * exercises it. Gating the known gaps would fail every push until someone
764
- * closed them all, which is the sweep that rule exists to prevent.
765
- */
766
- export const standardCriteria: Measure = async (ctx) => {
767
- const run = await ctx.cli(['standards', 'audit', '--arrivals-only'])
768
-
769
- if (run.exitCode === 0) {
770
- return { emissions: [info('Arriving standards carry a success criterion')] }
771
- }
772
-
773
- return {
774
- emissions: [output(run.all)],
775
- failure:
776
- run.exitCode === 2
777
- ? 'A standard new to this branch carries no success-criterion section. Run bun src/cli.ts standards audit.'
778
- : 'canon standards audit could not read which standards arrived on this branch. Run bun src/cli.ts standards audit --json to see why.',
779
- }
780
- }
781
-
782
774
  /**
783
775
  * Every file in the gated corpus, walked from `ctx.root` rather than from the
784
776
  * changed set.
@@ -790,7 +782,7 @@ export const standardCriteria: Measure = async (ctx) => {
790
782
  * nothing it would notice.
791
783
  *
792
784
  * `dot` is what reaches the seeds, which is half of what `tooling/` ships. Every
793
- * seeded `.claude/` tree, `.cspell/` list, and `.husky/` hook sits behind a
785
+ * seeded `.claude/` tree, `.github/` workflow, and `.husky/` hook sits behind a
794
786
  * dotted segment, so the default scan walks `tooling/` and returns none of the
795
787
  * files a scaffolded project actually receives.
796
788
  */
@@ -827,8 +819,8 @@ function describeShippedReference(reference: ShippedReference): string {
827
819
  return 'a pull request number that resolves elsewhere for a reader in a target'
828
820
  case 'docs-path':
829
821
  return 'a path into this repository that a registry install never carries'
830
- case 'standards-path':
831
- return 'a bare standards/ path that has nothing to expand it in an installed plugin cache'
822
+ case 'retired-verb':
823
+ return 'a call to canon standards, a verb no installed CLI carries'
832
824
  case 'phase-label':
833
825
  return 'a phase label that names a board no target holds'
834
826
  case 'rule-path':
@@ -889,8 +881,8 @@ export const shippedReferences: Measure = async (ctx) => {
889
881
  ),
890
882
  failure:
891
883
  found.length === 1
892
- ? `One reference in the shipped corpora resolves wrong for a reader in a target. Qualify a cross-repository citation as owner/repo#123 or owner/repo@abc1234, cite a docs page through canon docs <name>, rewrite a bare standards/ path under claude/skills/ as \${CLAUDE_SKILL_DIR}/../../standards/<name>.md, state a same-repository citation or a phase label as a fact instead and relocate the evidence to the owning canon/context/ entry, state the fact a shipped skill body's cited rule enforces instead of its path, or mark the line ${REFERENCE_MARKER}: <reason> where the bare form is the point.`
893
- : `${found.length} references in the shipped corpora resolve wrong for a reader in a target. Qualify a cross-repository citation as owner/repo#123 or owner/repo@abc1234, cite a docs page through canon docs <name>, rewrite a bare standards/ path under claude/skills/ as \${CLAUDE_SKILL_DIR}/../../standards/<name>.md, state a same-repository citation or a phase label as a fact instead and relocate the evidence to the owning canon/context/ entry, state the fact a shipped skill body's cited rule enforces instead of its path, or mark each line ${REFERENCE_MARKER}: <reason> where the bare form is the point.`,
884
+ ? `One reference in the shipped corpora resolves wrong for a reader in a target. Qualify a cross-repository citation as owner/repo#123 or owner/repo@abc1234, cite a docs page through canon docs <name>, cite the owner skill's reference in place of a canon standards call, state a same-repository citation or a phase label as a fact instead and relocate the evidence to the owning canon/context/ entry, state the fact a shipped skill body's cited rule enforces instead of its path, or mark the line ${REFERENCE_MARKER}: <reason> where the bare form is the point.`
885
+ : `${found.length} references in the shipped corpora resolve wrong for a reader in a target. Qualify a cross-repository citation as owner/repo#123 or owner/repo@abc1234, cite a docs page through canon docs <name>, cite the owner skill's reference in place of a canon standards call, state a same-repository citation or a phase label as a fact instead and relocate the evidence to the owning canon/context/ entry, state the fact a shipped skill body's cited rule enforces instead of its path, or mark each line ${REFERENCE_MARKER}: <reason> where the bare form is the point.`,
894
886
  }
895
887
  }
896
888
 
@@ -50,6 +50,45 @@ export interface LoadReading {
50
50
  /** The 1-minute load average. */
51
51
  readonly loadAverage: number
52
52
  readonly cores: number
53
+ /**
54
+ * The `some avg10` stall share per resource from `/proc/pressure`, which the
55
+ * load average folds into one number. Absent where the kernel has no PSI.
56
+ */
57
+ readonly pressure?: PressureReading
58
+ }
59
+
60
+ export interface PressureReading {
61
+ readonly cpu: number
62
+ readonly memory: number
63
+ readonly io: number
64
+ }
65
+
66
+ /**
67
+ * Reads the `some avg10` percentage from one `/proc/pressure/<resource>` file.
68
+ * A file that does not parse answers `undefined`, which drops the clause.
69
+ */
70
+ export function parsePressure(text: string): number | undefined {
71
+ const match = /^some\b.*\bavg10=(\d+(?:\.\d+)?)\b/m.exec(text)
72
+ if (match === null) return undefined
73
+ const value = Number.parseFloat(match[1])
74
+ return Number.isFinite(value) ? value : undefined
75
+ }
76
+
77
+ function readPressure(): PressureReading | undefined {
78
+ try {
79
+ const read = (name: string) =>
80
+ parsePressure(readFileSync(`/proc/pressure/${name}`, 'utf8'))
81
+ const cpu = read('cpu')
82
+ const memory = read('memory')
83
+ const io = read('io')
84
+ if (cpu === undefined || memory === undefined || io === undefined) {
85
+ return undefined
86
+ }
87
+ return { cpu, memory, io }
88
+ } catch {
89
+ // PSI off or absent: the files may exist and still fail with EOPNOTSUPP.
90
+ return undefined
91
+ }
53
92
  }
54
93
 
55
94
  /**
@@ -62,7 +101,12 @@ export function readProcLoad(): LoadReading | undefined {
62
101
  const first = readFileSync('/proc/loadavg', 'utf8').split(' ')[0]
63
102
  const loadAverage = Number.parseFloat(first)
64
103
  if (!Number.isFinite(loadAverage)) return undefined
65
- return { loadAverage, cores: availableParallelism() }
104
+ const pressure = readPressure()
105
+ return {
106
+ loadAverage,
107
+ cores: availableParallelism(),
108
+ ...(pressure === undefined ? {} : { pressure }),
109
+ }
66
110
  } catch {
67
111
  return undefined
68
112
  }
@@ -252,7 +296,12 @@ function withLoadLine(
252
296
  const reading = (ctx.readLoad ?? readProcLoad)()
253
297
  if (reading === undefined) return outcome
254
298
 
255
- const text = `Load average ${reading.loadAverage} on ${reading.cores} cores when the ${stage.label} stage failed`
299
+ const { pressure } = reading
300
+ const stalls =
301
+ pressure === undefined
302
+ ? ''
303
+ : `, pressure cpu ${pressure.cpu.toFixed(1)}% memory ${pressure.memory.toFixed(1)}% io ${pressure.io.toFixed(1)}% over 10s,`
304
+ const text = `Load average ${reading.loadAverage} on ${reading.cores} cores${stalls} when the ${stage.label} stage failed`
256
305
  return {
257
306
  ...outcome,
258
307
  emissions: [...outcome.emissions, { kind: 'info', text }],
@@ -25,7 +25,6 @@ import {
25
25
  skillFamily,
26
26
  skillPracticeShape,
27
27
  skillProvenance,
28
- standardCriteria,
29
28
  unreferencedRules,
30
29
  visualPathGlobs,
31
30
  } from '@/gate/measures'
@@ -328,10 +327,9 @@ export const STAGES: readonly Stage[] = [
328
327
  success: 'Capability seeding clean',
329
328
  },
330
329
  {
331
- // A stack entry naming a rule folder takes every rule in it, which is what
332
- // stops a new rule from needing a second edit to reach a target. The
333
- // failure it leaves open is a rule authored into a folder no stack names,
334
- // which `canon gov install` never reaches on its own.
330
+ // A rule no stack names reaches a target only when `canon gov detect`
331
+ // proposes it, so every such rule needs a governance/detect.toml entry,
332
+ // and every key there has to name a live rule.
335
333
  id: 'unreferenced-rules',
336
334
  label: 'Unreferenced rules',
337
335
  checks: [{ kind: 'measure', measure: unreferencedRules }],
@@ -503,11 +501,6 @@ export const STAGES: readonly Stage[] = [
503
501
  label: 'Skill family',
504
502
  checks: [{ kind: 'measure', measure: skillFamily }],
505
503
  },
506
- {
507
- id: 'standard-criteria',
508
- label: 'Standard success criteria',
509
- checks: [{ kind: 'measure', measure: standardCriteria }],
510
- },
511
504
  {
512
505
  id: 'sandbox-coverage',
513
506
  label: 'Sandbox coverage',
@@ -523,18 +516,6 @@ export const STAGES: readonly Stage[] = [
523
516
  label: 'Plugin manifests',
524
517
  checks: [{ kind: 'measure', measure: pluginManifests }],
525
518
  },
526
- {
527
- id: 'spelling',
528
- label: 'Spelling',
529
- checks: [
530
- {
531
- kind: 'command',
532
- argv: ['bun', 'run', 'check:spell'],
533
- failure: 'Spell check failed',
534
- },
535
- ],
536
- success: 'Spell check passed',
537
- },
538
519
  {
539
520
  id: 'shell',
540
521
  label: 'Shell',
@@ -6,7 +6,6 @@ import { isMarked } from '@/sweep/exempt-marker'
6
6
  import { gitEnv } from '@/git/env'
7
7
  import { listRuleFiles } from '@/gov/payload'
8
8
  import { parseFrontmatter } from '@/indexes/frontmatter'
9
- import { movedStandardPath } from '@/standards/read'
10
9
  import { findBacktickSpans } from '@/markdown/backticks'
11
10
 
12
11
  /**
@@ -42,14 +41,14 @@ export const RULE_DIRS: readonly string[] = [
42
41
  export const GLOB_CORPUS = join('internal', 'rules')
43
42
 
44
43
  /**
45
- * How the citation was written, kept on the finding because the three resolve
44
+ * How the citation was written, kept on the finding because the two resolve
46
45
  * against different roots and a reader repairing one needs to know which.
47
46
  *
48
- * `standard` is the live form, carried by 20 rules. `path` is a backticked
49
- * repository path. `sibling` is a rule naming another rule by filename alone,
50
- * which resolves inside the folder the citing rule sits in.
47
+ * `path` is a backticked repository path. `sibling` is a rule naming another
48
+ * rule by filename alone, which resolves inside the folder the citing rule
49
+ * sits in.
51
50
  */
52
- export type CitationForm = 'path' | 'standard' | 'sibling'
51
+ export type CitationForm = 'path' | 'sibling'
53
52
 
54
53
  /**
55
54
  * What the sweep decided about one citation.
@@ -116,13 +115,6 @@ const FENCE = /^\s*(?:```|~~~)/
116
115
 
117
116
  const FRONTMATTER_DELIMITER = /^---\s*$/
118
117
 
119
- /**
120
- * The verb form, with the name captured. A leading letter or digit is required,
121
- * which is what leaves `canon standards <name>` unmatched: that line teaches the
122
- * form rather than citing a standard, and it is the only one in either corpus.
123
- */
124
- const STANDARD_CALL = /canon standards ([A-Za-z0-9][A-Za-z0-9._-]*)/g
125
-
126
118
  /** A rule filename, which is how a rule names a sibling with no folder around it. */
127
119
  const SIBLING_RULE = /^\d{3}-[a-z0-9-]+\.md$/
128
120
 
@@ -146,8 +138,8 @@ function isNotRepositoryPath(span: string): boolean {
146
138
  * This is what separates a citation from the folder and module conventions the
147
139
  * corpus is full of. `next/font`, `try/except`, `react-hooks/set-state-in-effect`,
148
140
  * and `oven-sh/setup-bun@v2` all carry a slash and name no file, and a trailing
149
- * slash is a folder rather than a document. The cost is that `claude/standards`
150
- * is a real path this declines to check, which is the bound the report states.
141
+ * slash is a folder rather than a document. The cost is that a real folder path
142
+ * goes unchecked, which is the bound the report states.
151
143
  */
152
144
  function namesAFile(span: string): boolean {
153
145
  if (span.endsWith('/')) return false
@@ -160,10 +152,10 @@ function namesAFile(span: string): boolean {
160
152
  * sweep can resolve.
161
153
  *
162
154
  * The whole span is classified rather than a trailing pattern inside it.
163
- * Extracting `standards/tooling-reference.md` out of
164
- * `internal/standards/tooling-reference.md` and resolving that against the
165
- * standards root manufactures a dead citation out of a file that exists, which
166
- * a session measuring this corpus did before the check was written.
155
+ * Extracting a trailing `<folder>/<name>.md` out of a longer path and resolving
156
+ * that against another root manufactures a dead citation out of a file that
157
+ * exists, which a session measuring this corpus did before the check was
158
+ * written.
167
159
  */
168
160
  export function classifySpan(span: string): CitationForm | undefined {
169
161
  if (isNotRepositoryPath(span)) return undefined
@@ -176,15 +168,6 @@ export function classifySpan(span: string): CitationForm | undefined {
176
168
  * Where a citation could answer, which is wherever the reader's own tools look
177
169
  * and nowhere else.
178
170
  *
179
- * A standard name takes the authoring root alone, matching `standardRoots` in
180
- * `@/standards/read`, which reads `standards/` at the working root and then the
181
- * package corpus, plus the skill reference a retired name moved to. This verb
182
- * refuses a tree holding no rule corpus, so it runs only where those two roots
183
- * are one directory. `internal/standards/` is
184
- * deliberately absent: `canon standards <name>` never reaches it, so admitting it
185
- * here would pass a citation that refuses for the session opening it, which is a
186
- * gate failing open.
187
- *
188
171
  * A sibling resolves inside the folder the citing rule sits in, since that is
189
172
  * the only place a bare rule filename means anything.
190
173
  */
@@ -193,11 +176,6 @@ function candidatesFor(
193
176
  cited: string,
194
177
  ruleFile: string,
195
178
  ): string[] {
196
- if (form === 'standard') {
197
- const moved = movedStandardPath(cited)
198
-
199
- return [join('standards', `${cited}.md`), ...(moved ? [moved] : [])]
200
- }
201
179
  if (form === 'sibling') return [join(dirname(ruleFile), cited)]
202
180
  return [cited]
203
181
  }
@@ -244,16 +222,6 @@ export function collectCitations(text: string): RawCitation[] {
244
222
  : trimmed
245
223
  const marked = isMarked(lines, index, CITATION_MARKER)
246
224
 
247
- for (const match of line.matchAll(STANDARD_CALL)) {
248
- found.push({
249
- line: index + 1,
250
- form: 'standard',
251
- cited: match[1] ?? '',
252
- preview,
253
- marked,
254
- })
255
- }
256
-
257
225
  // A backticked span is the only carrier a rule writes a citation in. No
258
226
  // rule in either corpus uses a markdown link, and matching running prose
259
227
  // would report every sentence that happens to name a file.