@erclx/canon 4.115.0 → 4.117.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 (68) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/auto-ship/SKILL.md +1 -1
  3. package/claude/skills/canon-cli/SKILL.md +12 -12
  4. package/claude/skills/create-skill/REQUIREMENT.md +3 -0
  5. package/claude/skills/create-skill/SKILL.md +3 -1
  6. package/claude/skills/docs-fold/SKILL.md +1 -1
  7. package/claude/skills/memory-review/SKILL.md +4 -4
  8. package/claude/skills/review-pr/REQUIREMENT.md +2 -0
  9. package/claude/skills/review-pr/SKILL.md +2 -2
  10. package/claude/skills/role-orchestrator/REQUIREMENT.md +2 -0
  11. package/claude/skills/role-orchestrator/SKILL.md +2 -2
  12. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +14 -14
  13. package/claude/skills/role-orchestrator/references/orchestrator-handoff.md +1 -1
  14. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +2 -2
  15. package/claude/skills/role-worker/REQUIREMENT.md +2 -0
  16. package/claude/skills/role-worker/SKILL.md +2 -2
  17. package/claude/skills/session-compact/REQUIREMENT.md +2 -1
  18. package/claude/skills/session-compact/SKILL.md +5 -1
  19. package/claude/skills/teach-workspace/SKILL.md +5 -3
  20. package/claude/skills/teach-workspace/references/lesson-craft.md +7 -3
  21. package/claude/skills/write-human/references/source-material.md +2 -2
  22. package/docs/agents/commands.md +3 -1
  23. package/docs/agents/context-audit-checks.md +1 -1
  24. package/docs/agents/index.md +1 -1
  25. package/docs/agents/install-and-sync.md +1 -1
  26. package/docs/agents/markdown-audit.md +7 -3
  27. package/docs/agents/sandbox.md +1 -1
  28. package/docs/agents/sessions.md +84 -1
  29. package/docs/agents/skills-audit.md +8 -1
  30. package/docs/agents/teach.md +20 -4
  31. package/docs/workflow/ai-workflow.md +9 -9
  32. package/governance/rules/ui/440-surface-capture.md +2 -0
  33. package/package.json +1 -1
  34. package/scripts/core/check-capability-seeding.sh +4 -2
  35. package/src/claude/cases/misc.ts +5 -0
  36. package/src/claude/skills-audit.ts +59 -2
  37. package/src/commands/claude.ts +30 -0
  38. package/src/commands/markdown.ts +63 -2
  39. package/src/commands/sessions.ts +222 -0
  40. package/src/commands/teach.ts +18 -3
  41. package/src/context/audit.ts +6 -8
  42. package/src/gate/measures.ts +60 -0
  43. package/src/gate/stages.ts +12 -2
  44. package/src/markdown/ceiling.ts +70 -0
  45. package/src/markdown/structure.ts +22 -0
  46. package/src/records/backup.ts +1 -1
  47. package/src/sandbox/census.ts +1 -1
  48. package/src/sessions/registry.ts +11 -7
  49. package/src/sessions/transfer/bundle.ts +546 -0
  50. package/src/sessions/transfer/transcript.ts +148 -0
  51. package/src/teach/components/paragraph.tsx +19 -2
  52. package/src/teach/components/refs.tsx +28 -0
  53. package/src/teach/cross-references.ts +78 -0
  54. package/src/teach/lesson.ts +1 -1
  55. package/src/teach/nav.ts +75 -7
  56. package/src/teach/render.ts +94 -6
  57. package/src/tooling/inject.ts +13 -5
  58. package/src/tooling/manifest.ts +9 -0
  59. package/src/tooling/scan.ts +23 -6
  60. package/src/tooling/subfolder.ts +3 -3
  61. package/standards/context.md +2 -2
  62. package/standards/markdown.md +2 -0
  63. package/standards/skill.md +1 -0
  64. package/tooling/base/reference.md +2 -2
  65. package/tooling/web/configs/e2e/screenshot.ts +43 -7
  66. package/tooling/web/reference.md +1 -1
  67. /package/tooling/base/{configs → seeds}/.github/pull_request_template.md +0 -0
  68. /package/tooling/base/{configs → seeds}/.github/workflows/verify.yml +0 -0
@@ -1,4 +1,4 @@
1
- import type { Page } from '@playwright/test'
1
+ import type { Locator, Page } from '@playwright/test'
2
2
  import { chromium } from '@playwright/test'
3
3
  import { mkdir } from 'fs/promises'
4
4
  import path from 'path'
@@ -37,6 +37,8 @@ interface CaptureCase {
37
37
  /** Names of viewports this case skips, for a route that reflows rather than breaks at that width. */
38
38
  excludeViewports?: readonly string[]
39
39
  evidence?: boolean
40
+ /** Selectors painted over in every frame the case writes, for a value that moves between runs with no code change. */
41
+ mask?: readonly string[]
40
42
  sections?: CaptureSection[]
41
43
  setup?: (page: Page) => Promise<void>
42
44
  }
@@ -72,12 +74,14 @@ const CASES: CaptureCase[] = [
72
74
  theme: 'default',
73
75
  route: '/',
74
76
  evidence: true,
77
+ mask: ['.version'],
75
78
  sections: HOME_SECTIONS,
76
79
  },
77
80
  {
78
81
  section: 'home',
79
82
  theme: 'dark',
80
83
  route: '/',
84
+ mask: ['.version'],
81
85
  sections: HOME_SECTIONS,
82
86
  setup: (page) => page.emulateMedia({ colorScheme: 'dark' }),
83
87
  },
@@ -234,16 +238,47 @@ async function markFloatingChrome(page: Page) {
234
238
  }, FLOATING_ATTRIBUTE)
235
239
  }
236
240
 
237
- async function shoot(page: Page, dir: string, file: string, selector?: string) {
241
+ /**
242
+ * Turns a case's mask selectors into the locators every frame paints over. A
243
+ * selector matching nothing is reported and kept out rather than raised, for
244
+ * the same template reason `settle` gives for a missing section.
245
+ */
246
+ async function resolveMask(
247
+ page: Page,
248
+ label: string,
249
+ selectors: readonly string[],
250
+ ): Promise<Locator[]> {
251
+ const mask: Locator[] = []
252
+ for (const selector of selectors) {
253
+ const element = page.locator(selector)
254
+ if ((await element.count()) === 0) {
255
+ console.warn(
256
+ `${label}: no element matches ${selector}, named as a mask. Skipped.`,
257
+ )
258
+ continue
259
+ }
260
+ mask.push(element)
261
+ }
262
+ return mask
263
+ }
264
+
265
+ async function shoot(
266
+ page: Page,
267
+ dir: string,
268
+ file: string,
269
+ mask: Locator[],
270
+ selector?: string,
271
+ ) {
238
272
  await mkdir(dir, { recursive: true })
239
273
  const target = path.join(dir, file)
240
274
  if (selector === undefined) {
241
- await page.screenshot({ path: target, fullPage: true })
275
+ await page.screenshot({ path: target, fullPage: true, mask })
242
276
  } else {
243
277
  // Floating chrome keeps the space it already occupies, so dropping it out
244
278
  // of its own layer moves nothing and leaves every box the size it was.
245
279
  await page.locator(selector).screenshot({
246
280
  path: target,
281
+ mask,
247
282
  style: `[${FLOATING_ATTRIBUTE}] { position: static !important; }`,
248
283
  })
249
284
  }
@@ -281,21 +316,22 @@ for (const { captureCase, viewport, label } of RUNS) {
281
316
  await page.waitForLoadState('networkidle')
282
317
 
283
318
  const sections = await settle(page, label, captureCase.sections ?? [])
319
+ const mask = await resolveMask(page, label, captureCase.mask ?? [])
284
320
 
285
321
  const sweepDir = path.join(OUT_DIR, captureCase.section)
286
322
  const evidenceDir = path.join('evidence', captureCase.section)
287
323
  const file = `${viewport.name}--${captureCase.theme}.png`
288
324
  const writesEvidence = captureCase.evidence && viewport.evidence
289
325
 
290
- await shoot(page, sweepDir, file)
291
- if (writesEvidence) await shoot(page, evidenceDir, file)
326
+ await shoot(page, sweepDir, file, mask)
327
+ if (writesEvidence) await shoot(page, evidenceDir, file, mask)
292
328
 
293
329
  if (sections.length > 0) await markFloatingChrome(page)
294
330
 
295
331
  for (const { name, selector } of sections) {
296
- await shoot(page, path.join(sweepDir, name), file, selector)
332
+ await shoot(page, path.join(sweepDir, name), file, mask, selector)
297
333
  if (writesEvidence) {
298
- await shoot(page, path.join(evidenceDir, name), file, selector)
334
+ await shoot(page, path.join(evidenceDir, name), file, mask, selector)
299
335
  }
300
336
  }
301
337
 
@@ -12,7 +12,7 @@ Golden config files live in `tooling/web/configs/` and are copied into the targe
12
12
 
13
13
  - `eslint.config.js`: flat config with `@eslint/js`, `typescript-eslint`, React hooks, import sort, check-file, vitest rules scoped to test files, `eslint-config-prettier` last.
14
14
  - `src/test/setup.ts`: `@testing-library/jest-dom` import, `cleanup` after each test.
15
- - `e2e/screenshot.ts`: capture template. A single `CASES` record at the top carries one entry per output file, each naming a section, a theme, and a route, and the loop below crosses every case with the `VIEWPORTS` array and writes `screenshots/<hostname>/<section>/<width>--<theme>.png`, keyed on `SCREENSHOT_BASE_URL`'s hostname so a local and a deployed run land in different folders. `VIEWPORTS` holds four breakpoint buckets named by width, 320, 768, 1280 and 1536, with 320 as the base a page has to hold without wrapping or clipping. A case flagged `evidence: true` additionally writes `evidence/<section>/<width>--<theme>.png`, with no hostname segment, at the viewports flagged `evidence` alone, which are 320 and 1280. Committing all four multiplies the tracked frames by the width count, so the flag is what keeps the tracked set at twice the sections rather than four times. A case may name `excludeViewports` to skip a width, for a route whose long-form prose reflows at 320 instead of breaking. `SCREENSHOT_FILTER` narrows a run to the cases whose label, `<section>/<width>--<theme>`, contains any comma-separated term, exits 1 when nothing matches and lists the labels that exist, and leaves the rest of the sweep alone. `--require-base-url` skips a flagged case entirely, so a production smoke run neither writes to the committed path nor counts an evidence route in its console-clean check, and exits 1 when that leaves zero cases run, so flagging every case cannot silence the production check without saying so. Per-project cases extend the one record. A route's widths and themes sit together under its section folder, so the filename carries both. A case may also name `sections`, one `{ name, selector }` per addressable part of the route, and each writes its own frame at `screenshots/<hostname>/<section>/<name>/<width>--<theme>.png` beside the whole-page one, plus `evidence/<section>/<name>/<width>--<theme>.png` where the case is flagged and the width carries `evidence`. The field is optional, so a case that names none captures the page alone as before, and a selector matching nothing is reported and skipped rather than raised, since `CASES` is a template a project rewrites and a target that syncs this file before editing its own list would otherwise capture nothing at all. Testing that every named section resolves belongs to the project that owns the selectors, beside its own page. Every case settles before anything is shot: the context is created with `reducedMotion: 'reduce'`, each named section is scrolled into view so an `IntersectionObserver` fires and a frame marked `loading="lazy"` loads, the page returns to the top, and fonts, images and same-origin frames are waited out on a bounded condition rather than on a second `networkidle`, which is a lifecycle event of the navigation and does not re-arm for a load a scroll started. A section frame drops the page's floating chrome, being whatever computes to `position: sticky` or `fixed`, because a section taller than the viewport is scrolled into view to be cut and a sticky bar otherwise rides that scroll into the middle of the frame. The whole-page frame is shot first and keeps its chrome. `--check-console-clean` collects `console`-level error messages per case and exits 1 with the list if any fired, turning the capture into a smoke check. `--require-base-url` exits 1 before launching a browser when `SCREENSHOT_BASE_URL` is unset, guarding a script meant to run against a real deployment from silently capturing `localhost`.
15
+ - `e2e/screenshot.ts`: capture template. A single `CASES` record at the top carries one entry per output file, each naming a section, a theme, and a route, and the loop below crosses every case with the `VIEWPORTS` array and writes `screenshots/<hostname>/<section>/<width>--<theme>.png`, keyed on `SCREENSHOT_BASE_URL`'s hostname so a local and a deployed run land in different folders. `VIEWPORTS` holds four breakpoint buckets named by width, 320, 768, 1280 and 1536, with 320 as the base a page has to hold without wrapping or clipping. A case flagged `evidence: true` additionally writes `evidence/<section>/<width>--<theme>.png`, with no hostname segment, at the viewports flagged `evidence` alone, which are 320 and 1280. Committing all four multiplies the tracked frames by the width count, so the flag is what keeps the tracked set at twice the sections rather than four times. A case may name `excludeViewports` to skip a width, for a route whose long-form prose reflows at 320 instead of breaking. `SCREENSHOT_FILTER` narrows a run to the cases whose label, `<section>/<width>--<theme>`, contains any comma-separated term, exits 1 when nothing matches and lists the labels that exist, and leaves the rest of the sweep alone. `--require-base-url` skips a flagged case entirely, so a production smoke run neither writes to the committed path nor counts an evidence route in its console-clean check, and exits 1 when that leaves zero cases run, so flagging every case cannot silence the production check without saying so. Per-project cases extend the one record. A route's widths and themes sit together under its section folder, so the filename carries both. A case may also name `sections`, one `{ name, selector }` per addressable part of the route, and each writes its own frame at `screenshots/<hostname>/<section>/<name>/<width>--<theme>.png` beside the whole-page one, plus `evidence/<section>/<name>/<width>--<theme>.png` where the case is flagged and the width carries `evidence`. The field is optional, so a case that names none captures the page alone as before, and a selector matching nothing is reported and skipped rather than raised, since `CASES` is a template a project rewrites and a target that syncs this file before editing its own list would otherwise capture nothing at all. Testing that every named section resolves belongs to the project that owns the selectors, beside its own page. A case may also name `mask`, a list of selectors whose boxes are painted over in every frame the case writes, whole-page and section alike, for a value that moves between runs with no code change, so a double capture still compares everything else. A mask selector matching nothing is reported and skipped the same way. Every case settles before anything is shot: the context is created with `reducedMotion: 'reduce'`, each named section is scrolled into view so an `IntersectionObserver` fires and a frame marked `loading="lazy"` loads, the page returns to the top, and fonts, images and same-origin frames are waited out on a bounded condition rather than on a second `networkidle`, which is a lifecycle event of the navigation and does not re-arm for a load a scroll started. A section frame drops the page's floating chrome, being whatever computes to `position: sticky` or `fixed`, because a section taller than the viewport is scrolled into view to be cut and a sticky bar otherwise rides that scroll into the middle of the frame. The whole-page frame is shot first and keeps its chrome. `--check-console-clean` collects `console`-level error messages per case and exits 1 with the list if any fired, turning the capture into a smoke check. `--require-base-url` exits 1 before launching a browser when `SCREENSHOT_BASE_URL` is unset, guarding a script meant to run against a real deployment from silently capturing `localhost`.
16
16
  - `scripts/screenshot.sh`: sources `scripts/lib/preview-server.sh`, then runs the capture against the served preview.
17
17
  - `scripts/lib/preview-server.sh`: sourced, never executed. Builds, serves on `PREVIEW_PORT`, blocks until the port answers, and traps the whole process group on exit, including the detached grandchild `lsof` finds. Both capture scripts source it so the startup lives once.
18
18
  - `scripts/readme-screenshot.sh`: captures one case under `SCREENSHOT_FILTER` and copies its light and dark frames to `assets/evidence/readme/`. `README_FILTER` ships empty with `header/1280` as its example, and the script exits 1 until a project fills it, since a shipped default would capture the wrong part of the page in every project but one. Its copy reads from `screenshots/<hostname>/`, which is where the harness writes.