@erclx/canon 5.25.0 → 5.26.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 (123) hide show
  1. package/README.md +2 -1
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/canvas/SKILL.md +1 -1
  4. package/claude/skills/context-fold/REQUIREMENT.md +6 -2
  5. package/claude/skills/context-fold/SKILL.md +8 -6
  6. package/claude/skills/context-fold/references/anchor-sweep.md +2 -2
  7. package/claude/skills/design-taste/REQUIREMENT.md +1 -1
  8. package/claude/skills/design-taste/SKILL.md +4 -2
  9. package/claude/skills/design-taste/references/adopted.md +40 -0
  10. package/claude/skills/design-taste/references/kinds.md +1 -1
  11. package/claude/skills/design-taste/references/systems.md +1 -7
  12. package/claude/skills/design-taste/references/tells.md +2 -24
  13. package/claude/skills/docs-sync/REQUIREMENT.md +2 -1
  14. package/claude/skills/document-health/REQUIREMENT.md +2 -0
  15. package/claude/skills/document-health/SKILL.md +21 -0
  16. package/claude/skills/document-health/references/axes.md +12 -0
  17. package/claude/skills/draft-and-pick/SKILL.md +1 -1
  18. package/claude/skills/draft-identity/SKILL.md +1 -1
  19. package/claude/skills/draft-screencast/SKILL.md +9 -7
  20. package/claude/skills/draft-slides/REQUIREMENT.md +17 -12
  21. package/claude/skills/draft-slides/SKILL.md +31 -20
  22. package/claude/skills/git-issue/references/issue.md +10 -0
  23. package/claude/skills/git-pr/references/evidence.md +8 -0
  24. package/claude/skills/git-pr/references/labels.md +12 -0
  25. package/claude/skills/git-split/SKILL.md +19 -8
  26. package/claude/skills/markdown-propose/references/format.md +9 -0
  27. package/claude/skills/memory-review/references/later-phases.md +7 -0
  28. package/claude/skills/review-ui/references/post.md +4 -2
  29. package/claude/skills/role-orchestrator/references/orchestrator-cloud-launch.md +8 -0
  30. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +13 -0
  31. package/claude/skills/search-craft/REQUIREMENT.md +1 -1
  32. package/claude/skills/search-craft/SKILL.md +3 -1
  33. package/claude/skills/search-craft/references/adopted.md +30 -0
  34. package/claude/skills/target-setup/references/detect.md +8 -0
  35. package/claude/skills/target-setup/references/indexes.md +13 -0
  36. package/claude/skills/target-setup/references/verify.md +8 -0
  37. package/claude/skills/test-first/SKILL.md +2 -0
  38. package/claude/skills/test-first/references/adopted.md +36 -0
  39. package/claude/skills/write-human/SKILL.md +1 -1
  40. package/claude/skills/write-human/references/machine-tells.md +13 -0
  41. package/docs/agents/canvas.md +3 -1
  42. package/docs/agents/capture.md +1 -1
  43. package/docs/agents/commands.md +3 -3
  44. package/docs/agents/index.md +2 -2
  45. package/docs/agents/records-stale.md +22 -9
  46. package/docs/agents/records.md +1 -1
  47. package/docs/agents/sandbox.md +1 -1
  48. package/docs/agents/skills-audit.md +10 -3
  49. package/docs/agents/slides.md +15 -3
  50. package/docs/workflow/ai-workflow.md +1 -1
  51. package/docs/workflow/index.md +1 -0
  52. package/docs/workflow/parallel-features.md +1 -1
  53. package/docs/workflow/skill-map-on-request.md +42 -0
  54. package/docs/workflow/skill-map.md +1 -34
  55. package/package.json +1 -1
  56. package/scripts/lib/sandbox-dispatch.sh +1 -1
  57. package/scripts/lib/sandbox-git.sh +1 -9
  58. package/scripts/lib/sandbox-path.sh +1 -1
  59. package/scripts/sandbox-hook.sh +99 -0
  60. package/src/canvas/capture.ts +1 -9
  61. package/src/canvas/client/inspector/color-field.tsx +368 -0
  62. package/src/canvas/client/inspector/fields.css +163 -22
  63. package/src/canvas/client/inspector/values.ts +123 -0
  64. package/src/canvas/client/inspector.tsx +46 -69
  65. package/src/capture/render.ts +45 -7
  66. package/src/capture/sources.ts +43 -0
  67. package/src/census/count.ts +1 -1
  68. package/src/claude/cases/workflow.ts +5 -0
  69. package/src/claude/seeds.ts +1 -1
  70. package/src/claude/skills-audit.ts +68 -14
  71. package/src/client-commands.ts +1 -1
  72. package/src/commands/claude.ts +27 -2
  73. package/src/commands/feedback.ts +3 -3
  74. package/src/commands/records.ts +88 -6
  75. package/src/commands/sandbox.ts +50 -10
  76. package/src/commands/slides.ts +31 -77
  77. package/src/context/audit.ts +1 -1
  78. package/src/design/tokens.ts +2 -18
  79. package/src/docs/read.ts +1 -1
  80. package/src/{github.ts → feedback/github.ts} +1 -1
  81. package/src/gate/measures.ts +6 -4
  82. package/src/gov/citations.ts +1 -1
  83. package/src/gov/payload.ts +1 -1
  84. package/src/gov/superseded.ts +2 -2
  85. package/src/markdown/ceiling.ts +1 -1
  86. package/src/record-root.ts +1 -0
  87. package/src/records/backup.ts +1 -0
  88. package/src/records/push-guard.ts +1 -1
  89. package/src/records/stale-canonical.ts +146 -0
  90. package/src/records/stale.ts +1 -1
  91. package/src/sandbox/hooks.ts +128 -0
  92. package/src/sandbox/provision.ts +596 -0
  93. package/src/sandbox/tree.ts +103 -4
  94. package/src/secrets/marker.ts +2 -2
  95. package/src/secrets/scan.ts +1 -1
  96. package/src/shipped/references.ts +2 -2
  97. package/src/slides/convert/export.ts +2 -16
  98. package/src/slides/decks.ts +110 -0
  99. package/src/standards/read.ts +1 -1
  100. package/src/sync/seeds-report.ts +1 -1
  101. package/src/tasks/answers.ts +1 -1
  102. package/src/tasks/archive.ts +1 -1
  103. package/src/teach/nav.ts +1 -1
  104. package/src/tooling/inject.ts +1 -1
  105. package/src/web/readme-citations.ts +1 -1
  106. package/standards/architecture.md +1 -0
  107. package/standards/canvas.md +1 -1
  108. package/standards/requirements.md +2 -0
  109. package/standards/skill-practice.md +2 -1
  110. package/standards/skill.md +1 -0
  111. package/scripts/manage-sandbox.sh +0 -472
  112. package/src/slides/layouts.ts +0 -478
  113. package/src/slides/parse.ts +0 -84
  114. package/src/slides/render.ts +0 -110
  115. package/src/slides/styles.ts +0 -96
  116. /package/claude/skills/write-human/references/{source-material.md → adopted.md} +0 -0
  117. /package/src/{commands/feedback-format.ts → feedback/body.ts} +0 -0
  118. /package/src/{github-format.ts → feedback/github-format.ts} +0 -0
  119. /package/src/{frontmatter.ts → markdown/frontmatter.ts} +0 -0
  120. /package/src/{binary.ts → sweep/binary.ts} +0 -0
  121. /package/src/{exempt-marker.ts → sweep/exempt-marker.ts} +0 -0
  122. /package/src/{paths.ts → tasks/paths.ts} +0 -0
  123. /package/src/{seed-marker.ts → tooling/seed-marker.ts} +0 -0
@@ -0,0 +1,146 @@
1
+ import { $ } from 'bun'
2
+ import { existsSync } from 'node:fs'
3
+ import { readFile } from 'node:fs/promises'
4
+ import { relative } from 'node:path'
5
+ import { gitEnv } from '@/git-env'
6
+ import { readReviewed } from '@/records/stale'
7
+ import { surfaceDir } from '@/surface-root'
8
+
9
+ /** The two records whose drift is identity rather than a stale path. */
10
+ const CANONICAL_DOCS = ['REQUIREMENTS.md', 'ARCHITECTURE.md'] as const
11
+
12
+ export interface CanonicalDoc {
13
+ /** Relative to the root, at whichever surface root carries the doc. */
14
+ readonly path: string
15
+ /** `YYYY-MM-DD`, or null when the doc carries no readable review date. */
16
+ readonly reviewed: string | null
17
+ /** The raw value of a `reviewed` field that is not a date, present only then. */
18
+ readonly invalidReviewed?: string
19
+ /** Release tags merged into HEAD after the reviewed day, or null when never reviewed. */
20
+ readonly releasesSince: number | null
21
+ /** The newest release tag merged into HEAD, or null when there is none. */
22
+ readonly latestRelease: string | null
23
+ }
24
+
25
+ export interface CanonicalReport {
26
+ readonly ok: true
27
+ readonly root: string
28
+ /** False when HEAD has merged no tag, so a zero count measured nothing. */
29
+ readonly tagged: boolean
30
+ readonly docs: readonly CanonicalDoc[]
31
+ }
32
+
33
+ export interface CanonicalRefused {
34
+ readonly ok: false
35
+ readonly reason: 'unreadable-tags'
36
+ readonly message: string
37
+ }
38
+
39
+ export type CanonicalOutcome = CanonicalReport | CanonicalRefused
40
+
41
+ interface Release {
42
+ readonly tag: string
43
+ readonly day: string
44
+ }
45
+
46
+ /**
47
+ * A tag ending in a dotted version, bare or behind a component prefix such as
48
+ * `canon-v2.1.0`. An eval, deploy, or snapshot tag on the trunk names no
49
+ * release and would otherwise inflate the count.
50
+ */
51
+ const RELEASE_TAG = /(?:^|[-/@_])v?\d+\.\d+(?:\.\d+)?(?:[-+][0-9A-Za-z.-]+)?$/
52
+
53
+ /**
54
+ * A repository before its first commit has no HEAD for `--merged` to resolve,
55
+ * which git reports as a failed read. It has no tags either, so it reads as an
56
+ * empty history rather than an unreadable one.
57
+ */
58
+ async function isUnborn(root: string): Promise<boolean> {
59
+ const [gitDir, head] = await Promise.all([
60
+ $`git -C ${root} rev-parse --git-dir`.env(gitEnv()).quiet().nothrow(),
61
+ $`git -C ${root} rev-parse --verify -q HEAD`
62
+ .env(gitEnv())
63
+ .quiet()
64
+ .nothrow(),
65
+ ])
66
+ return gitDir.exitCode === 0 && head.exitCode !== 0
67
+ }
68
+
69
+ /**
70
+ * Every release tag HEAD has merged, newest first, with the day it was created.
71
+ *
72
+ * The creator date is the tagging date on an annotated tag and the commit date
73
+ * on a lightweight one, so a history mixing both counts by two clocks.
74
+ */
75
+ async function mergedReleases(root: string): Promise<Release[] | undefined> {
76
+ // Interpolated rather than inline, since Bun's shell parses the bare
77
+ // parentheses in `%(refname)` as syntax of its own.
78
+ const args = [
79
+ '--merged',
80
+ 'HEAD',
81
+ '--sort=-creatordate',
82
+ '--format=%(creatordate:short) %(refname:short)',
83
+ 'refs/tags',
84
+ ]
85
+ const result = await $`git -C ${root} for-each-ref ${args}`
86
+ .env(gitEnv())
87
+ .quiet()
88
+ .nothrow()
89
+ if (result.exitCode !== 0) return (await isUnborn(root)) ? [] : undefined
90
+
91
+ return result.stdout
92
+ .toString()
93
+ .split('\n')
94
+ .filter((line) => line.trim().length > 0)
95
+ .map((line) => {
96
+ const [day, tag] = line.trim().split(' ')
97
+ return { day, tag }
98
+ })
99
+ .filter((release) => RELEASE_TAG.test(release.tag))
100
+ }
101
+
102
+ /**
103
+ * Reports when each canonical doc was last reviewed and how many releases
104
+ * have shipped since, read from an optional `reviewed:` frontmatter field.
105
+ *
106
+ * The review point is a date rather than a commit, since a commit written on a
107
+ * feature branch never reaches a trunk that squash-merges. A release tagged on
108
+ * the reviewed day itself is not counted, as the review is read as covering
109
+ * that day. It reports and never writes.
110
+ */
111
+ export async function staleCanonical(root: string): Promise<CanonicalOutcome> {
112
+ const releases = await mergedReleases(root)
113
+
114
+ if (releases === undefined) {
115
+ return {
116
+ ok: false,
117
+ reason: 'unreadable-tags',
118
+ message: `Could not read the release tags at ${root}, so no count can be taken.`,
119
+ }
120
+ }
121
+
122
+ const latestRelease = releases[0]?.tag ?? null
123
+ const present = CANONICAL_DOCS.map((entry) => surfaceDir(root, entry)).filter(
124
+ (path) => existsSync(path),
125
+ )
126
+
127
+ const docs = await Promise.all(
128
+ present.map(async (path): Promise<CanonicalDoc> => {
129
+ const text = (await readFile(path, 'utf8')).replaceAll('\r\n', '\n')
130
+ const review = readReviewed(text)
131
+ const { reviewed } = review
132
+
133
+ return {
134
+ path: relative(root, path),
135
+ ...review,
136
+ releasesSince:
137
+ reviewed === null
138
+ ? null
139
+ : releases.filter((release) => release.day > reviewed).length,
140
+ latestRelease,
141
+ }
142
+ }),
143
+ )
144
+
145
+ return { ok: true, root, tagged: releases.length > 0, docs }
146
+ }
@@ -65,7 +65,7 @@ const RAW_FRONTMATTER = /^---\n([\s\S]*?)\n---/
65
65
  * A value that is not a calendar date reads as never reviewed and is carried
66
66
  * back so the review can see it, rather than throwing over one hand edit.
67
67
  */
68
- function readReviewed(
68
+ export function readReviewed(
69
69
  text: string,
70
70
  ): Pick<StaleEntry, 'reviewed' | 'invalidReviewed'> {
71
71
  const block = RAW_FRONTMATTER.exec(text)?.[1]
@@ -0,0 +1,128 @@
1
+ import { spawnSync } from 'node:child_process'
2
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
3
+ import { tmpdir } from 'node:os'
4
+ import { join } from 'node:path'
5
+ import { PROJECT_ROOT } from '@/project-root'
6
+
7
+ /**
8
+ * The bash entry a scenario's hooks run through. Scenarios and the library they
9
+ * call stay bash, so this contract is the whole of what the TypeScript harness
10
+ * knows about one: which hooks it declares and what `use_config` exports.
11
+ */
12
+ const HOOK_SCRIPT = join(PROJECT_ROOT, 'scripts', 'sandbox-hook.sh')
13
+
14
+ const ANCHOR_RECORD = '@anchor='
15
+
16
+ export interface Probe {
17
+ readonly exports: Readonly<Record<string, string>>
18
+ readonly isAnchor: boolean
19
+ }
20
+
21
+ export type ProbeResult =
22
+ | ({ readonly ok: true } & Probe)
23
+ | { readonly ok: false; readonly status: number }
24
+
25
+ /**
26
+ * Reads the NUL-separated `NAME=value` pairs the probe prints. NUL rather than a
27
+ * newline, since an exported value can hold one. The anchor record closes every
28
+ * well-formed probe, so output missing it is a probe that did not finish.
29
+ */
30
+ export function parseProbe(stdout: string): Probe {
31
+ const { exports, anchor } = parsePairs(stdout)
32
+ if (anchor === undefined)
33
+ throw new Error('the probe printed no anchor record')
34
+
35
+ return { exports, isAnchor: anchor === '1' }
36
+ }
37
+
38
+ function parsePairs(text: string): {
39
+ exports: Record<string, string>
40
+ anchor: string | undefined
41
+ } {
42
+ const exports: Record<string, string> = {}
43
+ let anchor: string | undefined
44
+
45
+ for (const record of text.split('\0')) {
46
+ if (record.startsWith(ANCHOR_RECORD)) {
47
+ anchor = record.slice(ANCHOR_RECORD.length)
48
+ continue
49
+ }
50
+ const split = record.indexOf('=')
51
+ if (split > 0) exports[record.slice(0, split)] = record.slice(split + 1)
52
+ }
53
+
54
+ return { exports, anchor }
55
+ }
56
+
57
+ /**
58
+ * Runs `use_config` and `use_anchor` in a child and returns what they exported.
59
+ * Stderr is inherited by default, so a hook that fails narrates into the same
60
+ * frame the harness writes. A non-zero status passes through rather than being
61
+ * flattened, since the harness exits with it.
62
+ */
63
+ export function probeScenario(
64
+ file: string,
65
+ env: NodeJS.ProcessEnv,
66
+ stderr: 'inherit' | 'pipe' = 'inherit',
67
+ ): ProbeResult {
68
+ const result = spawnSync('bash', [HOOK_SCRIPT, 'probe', file], {
69
+ env,
70
+ encoding: 'utf8',
71
+ stdio: ['ignore', 'pipe', stderr],
72
+ })
73
+ const status = result.status ?? 1
74
+ if (status !== 0) return { ok: false, status }
75
+
76
+ return { ok: true, ...parseProbe(result.stdout) }
77
+ }
78
+
79
+ /**
80
+ * How `stage_setup` ended. The retired dispatcher ran it in its own shell, so
81
+ * an `exit` there ended provisioning through the frame-closing trap, and an
82
+ * `exec` replaced the harness outright, leaving its program's frame as the last
83
+ * one written. The harness reproduces both from this.
84
+ */
85
+ export type StageEnding = 'returned' | 'exited' | 'replaced'
86
+
87
+ export interface StageResult {
88
+ readonly status: number
89
+ readonly ending: StageEnding
90
+ readonly exports: Readonly<Record<string, string>>
91
+ }
92
+
93
+ const EXITED_RECORD = '@exited'
94
+
95
+ /**
96
+ * Runs `stage_setup` inside the tree with every stream inherited, so a scenario
97
+ * prompting through `select_or_route_scenario` still reaches a terminal and its
98
+ * narration lands in order with the harness's own.
99
+ *
100
+ * The exports `stage_setup` changed come back through a file rather than a
101
+ * pipe. Stdout belongs to the scenario, and a background process a scenario
102
+ * starts would hold an extra pipe open past the child's exit.
103
+ */
104
+ export function stageScenario(
105
+ file: string,
106
+ env: NodeJS.ProcessEnv,
107
+ stdio: 'inherit' | 'ignore' = 'inherit',
108
+ ): StageResult {
109
+ const scratch = mkdtempSync(join(tmpdir(), 'canon-sandbox-hook-'))
110
+ const report = join(scratch, 'exports')
111
+
112
+ try {
113
+ const result = spawnSync('bash', [HOOK_SCRIPT, 'stage', file, report], {
114
+ env,
115
+ stdio,
116
+ })
117
+ const status = result.status ?? 1
118
+ if (!existsSync(report)) return { status, ending: 'replaced', exports: {} }
119
+
120
+ const text = readFileSync(report, 'utf8')
121
+ if (text.split('\0').includes(EXITED_RECORD))
122
+ return { status, ending: 'exited', exports: {} }
123
+
124
+ return { status, ending: 'returned', exports: parsePairs(text).exports }
125
+ } finally {
126
+ rmSync(scratch, { recursive: true, force: true })
127
+ }
128
+ }