@erclx/canon 4.117.0 → 4.119.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/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/canon-operator/SKILL.md +1 -1
- package/claude/skills/canon-rollout/SKILL.md +2 -2
- package/claude/skills/design-extract/SKILL.md +1 -1
- package/claude/skills/docs-fold/SKILL.md +2 -2
- package/claude/skills/draft-ready/REQUIREMENT.md +1 -1
- package/claude/skills/draft-ready/SKILL.md +1 -1
- package/claude/skills/draft-ready/references/assembly.md +8 -0
- package/claude/skills/draft-screencast/SKILL.md +1 -1
- package/claude/skills/git-pr/SKILL.md +5 -3
- package/claude/skills/memory-capture/SKILL.md +3 -3
- package/claude/skills/memory-review/SKILL.md +3 -3
- package/claude/skills/plan-feature/REQUIREMENT.md +1 -0
- package/claude/skills/plan-feature/SKILL.md +8 -1
- package/claude/skills/review-branch/SKILL.md +1 -1
- package/claude/skills/review-pr/REQUIREMENT.md +1 -0
- package/claude/skills/review-pr/SKILL.md +4 -2
- package/claude/skills/role-planner/SKILL.md +1 -1
- package/claude/skills/role-worker/REQUIREMENT.md +1 -1
- package/claude/skills/role-worker/SKILL.md +10 -41
- package/claude/skills/seed-sync/SKILL.md +1 -1
- package/claude/skills/session-compact/SKILL.md +1 -1
- package/claude/skills/session-map/SKILL.md +1 -1
- package/claude/skills/session-relay/REQUIREMENT.md +7 -1
- package/claude/skills/session-relay/SKILL.md +4 -7
- package/claude/skills/session-worktree/REQUIREMENT.md +5 -1
- package/claude/skills/session-worktree/SKILL.md +7 -1
- package/claude/skills/session-worktree/references/main-root-writes.md +24 -0
- package/claude/skills/target-check/references/domains.md +1 -1
- package/claude/skills/task-board/SKILL.md +4 -3
- package/claude/skills/teach-workspace/SKILL.md +4 -4
- package/claude/skills/ui-checklist/SKILL.md +1 -1
- package/claude/skills/ux-audit/SKILL.md +1 -1
- package/claude/skills/ux-measure/SKILL.md +1 -1
- package/claude/skills/ux-walkthrough/references/relay.md +1 -1
- package/docs/agents/commands.md +3 -3
- package/docs/agents/context-audit-checks.md +2 -28
- package/docs/agents/context-audit.md +1 -1
- package/docs/agents/install-and-sync.md +28 -17
- package/docs/agents/tasks.md +4 -4
- package/docs/target-projects.md +2 -2
- package/docs/workflow/ai-workflow.md +9 -9
- package/docs/workflow/operating-model.md +1 -1
- package/docs/workflow/visual-design-workflow.md +2 -2
- package/governance/renames.toml +13 -0
- package/governance/rules/claude/510-context.md +0 -12
- package/governance/rules/core/005-behavior.md +1 -2
- package/governance/rules/core/015-output.md +1 -3
- package/governance/rules/core/035-tasks.md +1 -3
- package/governance/rules/core/045-memory.md +0 -1
- package/governance/rules/core/055-scratch.md +2 -4
- package/governance/rules/core/070-planning.md +1 -4
- package/governance/rules/core/085-worktrees.md +2 -8
- package/governance/rules/core/087-git.md +2 -2
- package/governance/rules/core/090-code-comments.md +1 -1
- package/governance/rules/ui/460-design-taste.md +0 -15
- package/package.json +1 -1
- package/src/claude/cases/workflow.ts +10 -0
- package/src/commands/context.ts +11 -111
- package/src/commands/sync.ts +11 -3
- package/src/commands/tasks.ts +9 -8
- package/src/context/audit.ts +3 -145
- package/src/design/base.css +19 -19
- package/src/design/board.ts +18 -7
- package/src/design/neutral.ts +127 -0
- package/src/design/regen.ts +6 -4
- package/src/design/tokens.ts +5 -2
- package/src/gate/measures.ts +37 -0
- package/src/gate/stages.ts +13 -4
- package/src/gov/adapter.ts +59 -7
- package/src/gov/install.ts +13 -3
- package/src/gov/renames.ts +76 -0
- package/src/gov/stacks.ts +5 -1
- package/src/records/validate.ts +5 -1
- package/src/sync/check.ts +25 -4
- package/src/sync/engine.ts +154 -10
- package/src/sync/stamp.ts +44 -0
- package/src/tasks/archive.ts +28 -7
- package/src/tasks/record.ts +16 -9
- package/src/tasks/validate.ts +3 -1
- package/standards/plan.md +24 -1
- package/standards/tasks.md +5 -3
- package/governance/rules/core/091-channel.md +0 -13
- package/src/context/narration.ts +0 -99
|
@@ -163,11 +163,21 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
|
|
|
163
163
|
prompt: 'Get me set up in a fresh Claude Code worktree for this branch.',
|
|
164
164
|
expect: 'session-worktree',
|
|
165
165
|
},
|
|
166
|
+
{
|
|
167
|
+
prompt:
|
|
168
|
+
'My Write to the plan file at the main root got refused for session isolation. How do I land it there?',
|
|
169
|
+
expect: 'session-worktree',
|
|
170
|
+
},
|
|
166
171
|
{
|
|
167
172
|
prompt:
|
|
168
173
|
'Relay this over to the session that dispatched me. It gave me a sessionId rather than a name, so work out who that is and send it.',
|
|
169
174
|
expect: 'session-relay',
|
|
170
175
|
},
|
|
176
|
+
{
|
|
177
|
+
prompt:
|
|
178
|
+
'I am stuck on a question only the operator can answer. Before I stop and ask it, tell the session that dispatched me, it gave me a sessionId.',
|
|
179
|
+
expect: 'session-relay',
|
|
180
|
+
},
|
|
171
181
|
|
|
172
182
|
// Negatives: pairs whose bodies already carry an explicit Do NOT boundary.
|
|
173
183
|
{
|
package/src/commands/context.ts
CHANGED
|
@@ -56,12 +56,6 @@ import {
|
|
|
56
56
|
} from '@/context/folders'
|
|
57
57
|
import { isGating } from '@/context/gate'
|
|
58
58
|
import { auditIndexes, type FolderDrift } from '@/context/index-drift'
|
|
59
|
-
import {
|
|
60
|
-
loadNarration,
|
|
61
|
-
type Narration,
|
|
62
|
-
PRONOUN_HEADING,
|
|
63
|
-
VERB_HEADING,
|
|
64
|
-
} from '@/context/narration'
|
|
65
59
|
import {
|
|
66
60
|
measureWireframeFolder,
|
|
67
61
|
type WireframeStatesReport,
|
|
@@ -101,7 +95,7 @@ export function register(program: Command): void {
|
|
|
101
95
|
context
|
|
102
96
|
.command('audit')
|
|
103
97
|
.description(
|
|
104
|
-
'Report required sections, entry length, citations, reference form, catalog tables, provenance,
|
|
98
|
+
'Report required sections, entry length, citations, reference form, catalog tables, provenance, index drift, the architecture record against its own ceiling, its own entry cap, and its word weight, and wireframe states against their evidence folders',
|
|
105
99
|
)
|
|
106
100
|
.argument('[path]', 'Project root, defaulting to the current directory')
|
|
107
101
|
.helpOption('-h, --help', 'Show this help message')
|
|
@@ -133,8 +127,8 @@ export function register(program: Command): void {
|
|
|
133
127
|
'findings that are facts rather than judgments: a missing required',
|
|
134
128
|
'section and index drift. A context entry requires Overview and',
|
|
135
129
|
'Layout, and a wireframe requires Regions, States, Copy, and Not on',
|
|
136
|
-
'this surface. Entry length, reference form, table, provenance,
|
|
137
|
-
'
|
|
130
|
+
'this surface. Entry length, reference form, table, provenance, and the',
|
|
131
|
+
'record claim classification are judgments under both.',
|
|
138
132
|
'',
|
|
139
133
|
'Depth and bullet weight are stated over every markdown file rather',
|
|
140
134
|
'than over a context entry, so `canon markdown audit` measures them.',
|
|
@@ -653,24 +647,13 @@ async function runAudit(
|
|
|
653
647
|
)
|
|
654
648
|
}
|
|
655
649
|
|
|
656
|
-
const
|
|
657
|
-
? { kind: 'absent' }
|
|
658
|
-
: await loadNarration(root)
|
|
659
|
-
const entries = gateOnly
|
|
660
|
-
? []
|
|
661
|
-
: await measureFolders(
|
|
662
|
-
root,
|
|
663
|
-
folders,
|
|
664
|
-
narration.kind === 'loaded' ? narration : undefined,
|
|
665
|
-
)
|
|
650
|
+
const entries = gateOnly ? [] : await measureFolders(root, folders)
|
|
666
651
|
const drift = gateOnly ? [] : await auditIndexes(folders)
|
|
667
652
|
const sections = gateOnly ? [] : missingSections(root, folders, entries)
|
|
668
653
|
const length = gateOnly ? undefined : lengthFindings(entries)
|
|
669
654
|
// Absent under `--citations-only` and null when the project carries no
|
|
670
|
-
// record
|
|
671
|
-
//
|
|
672
|
-
// are different answers, and one value for both reports the second as the
|
|
673
|
-
// first.
|
|
655
|
+
// record. A run that never looked and a project with nothing to look at are
|
|
656
|
+
// different answers, and one value for both reports the second as the first.
|
|
674
657
|
const record = gateOnly ? undefined : await measureArchitecture(root)
|
|
675
658
|
const wireframes = gateOnly
|
|
676
659
|
? []
|
|
@@ -693,7 +676,6 @@ async function runAudit(
|
|
|
693
676
|
reportLength(length ?? [])
|
|
694
677
|
reportTables(entries)
|
|
695
678
|
reportProvenance(entries, folders)
|
|
696
|
-
reportNarration(entries, folders, narration)
|
|
697
679
|
reportDrift(drift)
|
|
698
680
|
reportRecord(record, root)
|
|
699
681
|
reportWireframeStates(wireframes)
|
|
@@ -722,10 +704,11 @@ async function runAudit(
|
|
|
722
704
|
// answers, and one wrong restatement is a cause reported against the
|
|
723
705
|
// wrong entry.
|
|
724
706
|
//
|
|
725
|
-
// Absent rather than empty under `--citations-only
|
|
726
|
-
//
|
|
727
|
-
//
|
|
728
|
-
//
|
|
707
|
+
// Absent rather than empty under `--citations-only`. That mode
|
|
708
|
+
// measures no entry, so an empty array here reads as a corpus with
|
|
709
|
+
// nothing past the checkpoint rather than as a run that never looked,
|
|
710
|
+
// and collapsing the two reports a measured answer for a run that
|
|
711
|
+
// never opened an entry.
|
|
729
712
|
length,
|
|
730
713
|
missingSections: sections,
|
|
731
714
|
indexDrift: drift,
|
|
@@ -747,20 +730,6 @@ async function runAudit(
|
|
|
747
730
|
// caller that read it before wireframes owed a section, since turning
|
|
748
731
|
// it into this map would break that caller without an error.
|
|
749
732
|
requiredSectionsByFolder: REQUIRED_SECTIONS_BY_FOLDER,
|
|
750
|
-
// Three states rather than two, so a record showing no finding says
|
|
751
|
-
// which terms were looked for. The key is absent when the run never
|
|
752
|
-
// scanned, which is `--citations-only`, and null when it scanned and
|
|
753
|
-
// no rule published both headings. Collapsing the first into null
|
|
754
|
-
// reports an absent rule against a run that never opened one.
|
|
755
|
-
narration: gateOnly
|
|
756
|
-
? undefined
|
|
757
|
-
: narration.kind === 'loaded'
|
|
758
|
-
? {
|
|
759
|
-
source: narration.source,
|
|
760
|
-
pronouns: narration.pronouns,
|
|
761
|
-
verbs: narration.verbs,
|
|
762
|
-
}
|
|
763
|
-
: null,
|
|
764
733
|
},
|
|
765
734
|
})}\n`,
|
|
766
735
|
)
|
|
@@ -1144,75 +1113,6 @@ function reportProvenance(
|
|
|
1144
1113
|
)
|
|
1145
1114
|
}
|
|
1146
1115
|
|
|
1147
|
-
/**
|
|
1148
|
-
* Reports the bullets narrating a decision the bullet above them replaced.
|
|
1149
|
-
*
|
|
1150
|
-
* The reach line names the rule the sets were read from, since the check is
|
|
1151
|
-
* silent when no rule publishes them and a run that scanned nothing otherwise
|
|
1152
|
-
* prints the same clean line as a run that scanned everything.
|
|
1153
|
-
*
|
|
1154
|
-
* The legitimate-hit line is here for the same reason the provenance section
|
|
1155
|
-
* says a marker is a judgment. The standard keeps a rejected alternative and
|
|
1156
|
-
* why it lost, which is a back-reference in the past tense by construction, and
|
|
1157
|
-
* no measure separates one from the shape the rule bans.
|
|
1158
|
-
*/
|
|
1159
|
-
function reportNarration(
|
|
1160
|
-
entries: readonly EntryReport[],
|
|
1161
|
-
folders: readonly AuditedFolder[],
|
|
1162
|
-
narration: Narration,
|
|
1163
|
-
): void {
|
|
1164
|
-
logStep('Narration')
|
|
1165
|
-
|
|
1166
|
-
const governed = folders.filter(governsContent)
|
|
1167
|
-
if (governed.length === 0) {
|
|
1168
|
-
logInfo(
|
|
1169
|
-
`Out of scope. The rule is stated in the standard governing canon/${PROVENANCE_FOLDER}/, and no audited folder is that one.`,
|
|
1170
|
-
)
|
|
1171
|
-
return
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
if (narration.kind === 'absent') {
|
|
1175
|
-
logWarn(
|
|
1176
|
-
`Not scanned. No rule under .claude/rules/ or governance/rules/ publishes both ${PRONOUN_HEADING} and ${VERB_HEADING}.`,
|
|
1177
|
-
)
|
|
1178
|
-
return
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
logInfo(
|
|
1182
|
-
`Covers canon/${PROVENANCE_FOLDER}/ alone, reading ${plural(narration.pronouns.length, 'pronoun')} and ${plural(narration.verbs.length, 'verb')} from ${narration.source}.`,
|
|
1183
|
-
)
|
|
1184
|
-
logInfo(
|
|
1185
|
-
'A rejected alternative is a legitimate hit, since the standard keeps what was tried and why it lost.',
|
|
1186
|
-
)
|
|
1187
|
-
|
|
1188
|
-
const carrying = entries
|
|
1189
|
-
.filter((entry) => entry.narration.length > 0)
|
|
1190
|
-
.sort((a, b) => b.narration.length - a.narration.length)
|
|
1191
|
-
|
|
1192
|
-
if (carrying.length === 0) {
|
|
1193
|
-
logInfo('No bullet narrates a decision the bullet above it replaced.')
|
|
1194
|
-
return
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
const total = carrying.reduce((sum, entry) => sum + entry.narration.length, 0)
|
|
1198
|
-
logWarn(
|
|
1199
|
-
`${plural(total, 'bullet')} to read across ${carrying.length} ${carrying.length === 1 ? 'entry' : 'entries'}`,
|
|
1200
|
-
)
|
|
1201
|
-
pipeOutput(
|
|
1202
|
-
carrying
|
|
1203
|
-
.map(
|
|
1204
|
-
(entry) =>
|
|
1205
|
-
`${entry.rel} ${plural(entry.narration.length, 'bullet')}\n${entry.narration
|
|
1206
|
-
.map(
|
|
1207
|
-
(found) =>
|
|
1208
|
-
` :${found.line} ${found.pronoun} with ${found.verb}`,
|
|
1209
|
-
)
|
|
1210
|
-
.join('\n')}`,
|
|
1211
|
-
)
|
|
1212
|
-
.join('\n'),
|
|
1213
|
-
)
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
1116
|
/** How each classification reads in the report. */
|
|
1217
1117
|
const CLAIM_LABEL: Record<string, string> = {
|
|
1218
1118
|
countable: 'countable claim',
|
package/src/commands/sync.ts
CHANGED
|
@@ -133,9 +133,17 @@ function renderCheck(report: CheckReport): void {
|
|
|
133
133
|
logWarn(entry.notice ?? `${entry.rel} (${entry.state})`)
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
const {
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
const {
|
|
137
|
+
stale,
|
|
138
|
+
customized,
|
|
139
|
+
drifted,
|
|
140
|
+
stranded,
|
|
141
|
+
retired,
|
|
142
|
+
renamed,
|
|
143
|
+
orphaned,
|
|
144
|
+
missing,
|
|
145
|
+
} = domain.counts
|
|
146
|
+
if (stale + customized + drifted + stranded + retired + renamed === 0) {
|
|
139
147
|
logInfo(orphaned === 0 ? 'up to date' : `up to date (${orphaned} local)`)
|
|
140
148
|
}
|
|
141
149
|
// A local file is not a deficiency and a missing rule is, so this never
|
package/src/commands/tasks.ts
CHANGED
|
@@ -409,12 +409,14 @@ export function register(program: Command): void {
|
|
|
409
409
|
[
|
|
410
410
|
'',
|
|
411
411
|
'Exit codes:',
|
|
412
|
-
' 0 the line was added, corrected, or already correct',
|
|
412
|
+
' 0 the line was added, appended to, corrected, or already correct',
|
|
413
413
|
' 1 refused, with the reason on stderr or in the JSON record',
|
|
414
414
|
'',
|
|
415
415
|
'It adds Pull request: #NNN under the origin lines the task carries,',
|
|
416
|
-
'and
|
|
417
|
-
'
|
|
416
|
+
'and appends the number to the line when one exists, so a task shipped',
|
|
417
|
+
'in slices lists every pull request. A line that does not read as a',
|
|
418
|
+
'list is replaced. The board is shared scratch, so a linked worktree',
|
|
419
|
+
'records against the same board.',
|
|
418
420
|
'',
|
|
419
421
|
'Examples:',
|
|
420
422
|
' canon tasks pull-request 673 v28.1-trigger-escalation',
|
|
@@ -448,11 +450,10 @@ export function register(program: Command): void {
|
|
|
448
450
|
' 0 the line was added, corrected, or already correct',
|
|
449
451
|
' 1 refused, with the reason on stderr or in the JSON record',
|
|
450
452
|
'',
|
|
451
|
-
'It writes Plan: [<label>](<target>) right after the H1, the
|
|
452
|
-
'
|
|
453
|
-
'
|
|
454
|
-
'
|
|
455
|
-
'resolves the board root in-process.',
|
|
453
|
+
'It writes Plan: [<label>](<target>) right after the H1, corrects the',
|
|
454
|
+
'target in place when the line exists, and resolves both a bare slug',
|
|
455
|
+
'and a board-relative path the way canon tasks plan-answers does. Safe',
|
|
456
|
+
'from a linked worktree, since it resolves the board root in-process.',
|
|
456
457
|
'',
|
|
457
458
|
'Examples:',
|
|
458
459
|
' canon tasks plan-link v28.1-trigger-escalation dispatch-answer-gate',
|
package/src/context/audit.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { readFile } from 'node:fs/promises'
|
|
|
2
2
|
import { basename, relative } from 'node:path'
|
|
3
3
|
import { BARE_NAME, IGNORE_MARKER } from '@/context/citations'
|
|
4
4
|
import type { AuditedFolder } from '@/context/folders'
|
|
5
|
-
import type { NarrationTerms } from '@/context/narration'
|
|
6
5
|
import { type BodyLine, bodyLines, maskDisplayed } from '@/markdown/scan'
|
|
7
6
|
import { documentHeight } from '@/markdown/structure'
|
|
8
7
|
import { isStubSeed } from '@/seed-marker'
|
|
@@ -80,10 +79,6 @@ const TABLE_ROW = /^\s*\|/
|
|
|
80
79
|
const TABLE_SEPARATOR = /^\s*\|[\s:|-]+\|\s*$/
|
|
81
80
|
const NAMED_CELL = /`[^`]+`|\[[^\]]+\]\([^)]+\)/
|
|
82
81
|
|
|
83
|
-
const TOP_BULLET = /^-\s+(.+)$/
|
|
84
|
-
/** Any indented line, which is a nested bullet or a wrapped continuation. */
|
|
85
|
-
const INSIDE_LIST = /^\s+\S/
|
|
86
|
-
|
|
87
82
|
/**
|
|
88
83
|
* Spellings of how the domain reached its shape rather than what it is now.
|
|
89
84
|
*
|
|
@@ -156,9 +151,9 @@ const SENTENCE_END = /[.!?:;]\s(?=[^.!?:;]*$)/
|
|
|
156
151
|
/**
|
|
157
152
|
* The folder whose standard carries the exclusion above.
|
|
158
153
|
*
|
|
159
|
-
*
|
|
160
|
-
* sections
|
|
161
|
-
*
|
|
154
|
+
* Two measures narrow here rather than one: provenance and the required
|
|
155
|
+
* sections. The name is the first of them because it was the first, and it
|
|
156
|
+
* stays because renaming a constant the
|
|
162
157
|
* JSON record publishes as `checkpoints.provenanceFolder` breaks every consumer
|
|
163
158
|
* reading that field for a gain of one word.
|
|
164
159
|
*
|
|
@@ -202,14 +197,6 @@ export interface BareReferenceFinding {
|
|
|
202
197
|
readonly name: string
|
|
203
198
|
}
|
|
204
199
|
|
|
205
|
-
export interface NarrationFinding {
|
|
206
|
-
readonly line: number
|
|
207
|
-
/** The opening that points back at the bullet above. */
|
|
208
|
-
readonly pronoun: string
|
|
209
|
-
/** The past-tense verb that turns the back-reference into a narration. */
|
|
210
|
-
readonly verb: string
|
|
211
|
-
}
|
|
212
|
-
|
|
213
200
|
export interface EntryReport {
|
|
214
201
|
readonly rel: string
|
|
215
202
|
/**
|
|
@@ -229,12 +216,6 @@ export interface EntryReport {
|
|
|
229
216
|
readonly catalogTables: readonly TableFinding[]
|
|
230
217
|
/** Empty for an entry no standard bans a change narrative in. */
|
|
231
218
|
readonly provenance: readonly ProvenanceFinding[]
|
|
232
|
-
/**
|
|
233
|
-
* Empty outside the governed folder, and empty on a run whose caller loaded
|
|
234
|
-
* no term sets. The report distinguishes the two from the vocabulary itself,
|
|
235
|
-
* since an empty list here is silent about which one produced it.
|
|
236
|
-
*/
|
|
237
|
-
readonly narration: readonly NarrationFinding[]
|
|
238
219
|
/**
|
|
239
220
|
* References naming a sibling entry by bare filename, and empty for a caller
|
|
240
221
|
* that passed no sibling names. Which entries have siblings worth matching is
|
|
@@ -434,125 +415,6 @@ function stampsMeasurement(text: string, index: number): boolean {
|
|
|
434
415
|
return STAMPING.test(clause) || STAMPING_RUN.test(clause)
|
|
435
416
|
}
|
|
436
417
|
|
|
437
|
-
function escape(term: string): string {
|
|
438
|
-
return term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* Copulas that turn the verb behind them into a present-tense passive.
|
|
443
|
-
*
|
|
444
|
-
* `used to` is the term this exists for, since `is used to resolve the folder`
|
|
445
|
-
* is the passive of `use` rather than the past habitual the set means. This
|
|
446
|
-
* list is English grammar rather than corpus vocabulary, so it stays in code
|
|
447
|
-
* while the two tunable sets stay in the rule, and a rule publishing two of
|
|
448
|
-
* three headings would be a fourth absent state to carry for no tuning anyone
|
|
449
|
-
* wants.
|
|
450
|
-
*
|
|
451
|
-
* What it costs is the perfect passive. `has been superseded` narrates a
|
|
452
|
-
* supersession and is rejected with the rest, which is a recall gap taken
|
|
453
|
-
* knowingly on the trade this check already makes everywhere else.
|
|
454
|
-
*/
|
|
455
|
-
const COPULA = ['is', 'are', 'be', 'been', 'being', 'was', 'were']
|
|
456
|
-
|
|
457
|
-
/**
|
|
458
|
-
* Finds the bullets narrating a decision the bullet above them replaced.
|
|
459
|
-
*
|
|
460
|
-
* The signal is structural rather than lexical, and the corpus is what decides
|
|
461
|
-
* that. The terms carrying clean signal for a supersession are too rare to
|
|
462
|
-
* catch anything, and the one term that would have caught the known case is
|
|
463
|
-
* `now`, which appears 57 times across 24 entries in correct present-tense
|
|
464
|
-
* prose. What separates the shape instead is a bullet pointing back at its
|
|
465
|
-
* sibling and putting the sibling's design in the past: an opening pronoun with
|
|
466
|
-
* no antecedent of its own, plus a past-tense verb, plus a bullet above it to
|
|
467
|
-
* refer to. Precision is the whole value, so recall is the accepted exposure
|
|
468
|
-
* and a narration written as a single bullet is not reached.
|
|
469
|
-
*
|
|
470
|
-
* The pronoun is matched cased and anchored, since a mid-sentence `this` is a
|
|
471
|
-
* determiner rather than a back-reference. The verb is matched uncased anywhere
|
|
472
|
-
* in the bullet, since the tense is what carries the signal wherever it sits,
|
|
473
|
-
* and rejected behind a `COPULA` for the reason stated there. Displayed spans
|
|
474
|
-
* are masked so a term quoted in backticks is not read as prose the entry
|
|
475
|
-
* writes, and fenced blocks are skipped for the reason the scans above skip
|
|
476
|
-
* them.
|
|
477
|
-
*
|
|
478
|
-
* A blank line does not end the run. Markdown reads two bullets around one as a
|
|
479
|
-
* single loose list, so breaking there would leave the shape reachable by
|
|
480
|
-
* anyone who spaced their bullets out. What ends a run is content that is
|
|
481
|
-
* neither a bullet nor indented under one, which the fenced branch below has to
|
|
482
|
-
* answer for itself because a fenced line is skipped before that test.
|
|
483
|
-
*/
|
|
484
|
-
function narration(
|
|
485
|
-
lines: readonly BodyLine[],
|
|
486
|
-
terms: NarrationTerms,
|
|
487
|
-
): NarrationFinding[] {
|
|
488
|
-
if (terms.pronouns.length === 0 || terms.verbs.length === 0) return []
|
|
489
|
-
|
|
490
|
-
const findings: NarrationFinding[] = []
|
|
491
|
-
let following = false
|
|
492
|
-
let fenceBlock: number | undefined
|
|
493
|
-
let fenceInsideList = false
|
|
494
|
-
|
|
495
|
-
for (const line of lines) {
|
|
496
|
-
// A fenced line is never a bullet, but an unindented block still ends the
|
|
497
|
-
// run. CommonMark reads a fence at column zero as interrupting the list, so
|
|
498
|
-
// the bullets around it are two lists and the second has no antecedent
|
|
499
|
-
// above it. A block indented under its bullet stays inside the item.
|
|
500
|
-
//
|
|
501
|
-
// The opening delimiter decides for every line of its own block. Reading
|
|
502
|
-
// each line instead ends the run on a blank line inside an indented fence,
|
|
503
|
-
// which has no indentation to read, and on a content line at column zero,
|
|
504
|
-
// which CommonMark permits since only the fence's own indent is stripped.
|
|
505
|
-
//
|
|
506
|
-
// The block index is what re-opens that decision, so two blocks written
|
|
507
|
-
// with nothing between them are answered separately rather than the second
|
|
508
|
-
// inheriting the first. Indentation stays the test here because what
|
|
509
|
-
// interrupts a list is this check's own judgment, and the walker only says
|
|
510
|
-
// which lines share a block.
|
|
511
|
-
if (line.fenced) {
|
|
512
|
-
if (line.fenceBlock !== fenceBlock) {
|
|
513
|
-
fenceBlock = line.fenceBlock
|
|
514
|
-
fenceInsideList = INSIDE_LIST.test(line.text)
|
|
515
|
-
}
|
|
516
|
-
if (!fenceInsideList) following = false
|
|
517
|
-
continue
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
if (line.text.trim() === '') continue
|
|
521
|
-
|
|
522
|
-
const bullet = line.text.match(TOP_BULLET)
|
|
523
|
-
if (!bullet) {
|
|
524
|
-
// A nested bullet and a wrapped continuation both sit inside the list, so
|
|
525
|
-
// neither ends the run. Anything else does, which is what keeps a bullet
|
|
526
|
-
// opening the list under a heading from reading as a reply to the last
|
|
527
|
-
// bullet of the list before it.
|
|
528
|
-
if (!INSIDE_LIST.test(line.text)) following = false
|
|
529
|
-
continue
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
const text = maskDisplayed(bullet[1])
|
|
533
|
-
|
|
534
|
-
if (following) {
|
|
535
|
-
const pronoun = terms.pronouns.find((term) =>
|
|
536
|
-
new RegExp(`^${escape(term)}\\b`).test(text),
|
|
537
|
-
)
|
|
538
|
-
const verb = pronoun
|
|
539
|
-
? terms.verbs.find((term) =>
|
|
540
|
-
new RegExp(
|
|
541
|
-
`(?<!\\b(?:${COPULA.join('|')})\\s+)\\b${escape(term)}\\b`,
|
|
542
|
-
'i',
|
|
543
|
-
).test(text),
|
|
544
|
-
)
|
|
545
|
-
: undefined
|
|
546
|
-
|
|
547
|
-
if (pronoun && verb) findings.push({ line: line.number, pronoun, verb })
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
following = true
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
return findings
|
|
554
|
-
}
|
|
555
|
-
|
|
556
418
|
/**
|
|
557
419
|
* Finds which required sections the entry declares.
|
|
558
420
|
*
|
|
@@ -639,7 +501,6 @@ export function measureEntry(
|
|
|
639
501
|
rel: string,
|
|
640
502
|
source: string,
|
|
641
503
|
governsContent = true,
|
|
642
|
-
terms?: NarrationTerms,
|
|
643
504
|
siblings: readonly string[] = [],
|
|
644
505
|
required: readonly string[] = governsContent ? REQUIRED_SECTIONS : [],
|
|
645
506
|
): EntryReport {
|
|
@@ -650,7 +511,6 @@ export function measureEntry(
|
|
|
650
511
|
lines: documentHeight(source),
|
|
651
512
|
catalogTables: catalogTables(lines),
|
|
652
513
|
provenance: governsContent ? provenance(lines) : [],
|
|
653
|
-
narration: governsContent && terms ? narration(lines, terms) : [],
|
|
654
514
|
bareReferences: bareReferences(lines, siblings),
|
|
655
515
|
sections: declaredSections(lines, required),
|
|
656
516
|
stub: isStubSeed(source),
|
|
@@ -714,7 +574,6 @@ function historyCause(entry: EntryReport): LengthCause {
|
|
|
714
574
|
export async function measureFolders(
|
|
715
575
|
root: string,
|
|
716
576
|
folders: readonly AuditedFolder[],
|
|
717
|
-
terms?: NarrationTerms,
|
|
718
577
|
): Promise<EntryReport[]> {
|
|
719
578
|
const reports: EntryReport[] = []
|
|
720
579
|
|
|
@@ -731,7 +590,6 @@ export async function measureFolders(
|
|
|
731
590
|
relative(root, path),
|
|
732
591
|
await readFile(path, 'utf8'),
|
|
733
592
|
governsContent(folder),
|
|
734
|
-
terms,
|
|
735
593
|
names.filter((name) => name !== self),
|
|
736
594
|
requiredSections(folder),
|
|
737
595
|
),
|
package/src/design/base.css
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
/* Generated by `canon design regen` from src/design/
|
|
1
|
+
/* Generated by `canon design regen` from src/design/neutral.ts. Do not edit.
|
|
2
2
|
This is the base half of a design install. A target overrides a value in
|
|
3
3
|
.claude/design/project/, which the three-way merge leaves alone, so this
|
|
4
4
|
file changes freely and nothing a project wrote lives in it. */
|
|
5
5
|
|
|
6
6
|
:root {
|
|
7
|
-
--color-background: #
|
|
8
|
-
--color-surface: #
|
|
9
|
-
--color-chrome: #
|
|
10
|
-
--color-border: #
|
|
11
|
-
--color-text: #
|
|
12
|
-
--color-text-body: #
|
|
13
|
-
--color-text-secondary: #
|
|
14
|
-
--color-muted: #
|
|
15
|
-
--color-accent: #
|
|
7
|
+
--color-background: #0e0e0e;
|
|
8
|
+
--color-surface: #141414;
|
|
9
|
+
--color-chrome: #1a1a1a;
|
|
10
|
+
--color-border: #292929;
|
|
11
|
+
--color-text: #dddddd;
|
|
12
|
+
--color-text-body: #b2b2b2;
|
|
13
|
+
--color-text-secondary: #8e8e8e;
|
|
14
|
+
--color-muted: #7f7f7f;
|
|
15
|
+
--color-accent: #ececec;
|
|
16
16
|
--color-success: #61c454;
|
|
17
|
-
--color-light-background: #
|
|
18
|
-
--color-light-surface: #
|
|
19
|
-
--color-light-chrome: #
|
|
20
|
-
--color-light-text: #
|
|
21
|
-
--color-light-text-body: #
|
|
22
|
-
--color-light-text-secondary: #
|
|
23
|
-
--color-light-muted: #
|
|
24
|
-
--color-light-accent: #
|
|
17
|
+
--color-light-background: #fafafa;
|
|
18
|
+
--color-light-surface: #f1f1f1;
|
|
19
|
+
--color-light-chrome: #e8e8e8;
|
|
20
|
+
--color-light-text: #252525;
|
|
21
|
+
--color-light-text-body: #444444;
|
|
22
|
+
--color-light-text-secondary: #5f5f5f;
|
|
23
|
+
--color-light-muted: #6c6c6c;
|
|
24
|
+
--color-light-accent: #191919;
|
|
25
25
|
--color-light-success: #2d6b22;
|
|
26
|
-
--color-light-border: #
|
|
26
|
+
--color-light-border: #d4d4d4;
|
|
27
27
|
--space-xs: 0.25rem;
|
|
28
28
|
--space-sm: 0.5rem;
|
|
29
29
|
--space-md: 0.875rem;
|
package/src/design/board.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { join, relative, sep } from 'node:path'
|
|
11
11
|
import { DESIGN_DOCUMENT } from '@/design/regen'
|
|
12
12
|
import { renderDesignDoc } from '@/design/render'
|
|
13
|
+
import { NEUTRAL_TOKENS } from '@/design/neutral'
|
|
13
14
|
import { colorValue } from '@/design/tokens'
|
|
14
15
|
import { parseFrontmatter, readField } from '@/indexes/frontmatter'
|
|
15
16
|
import { recordDir } from '@/record-root'
|
|
@@ -131,6 +132,16 @@ const SCROLLSPY_SCRIPT = [
|
|
|
131
132
|
'})();</script>',
|
|
132
133
|
].join('')
|
|
133
134
|
|
|
135
|
+
/**
|
|
136
|
+
* A role's toolkit value, falling back to the neutral install base's where the
|
|
137
|
+
* toolkit record drops the role, so the board never restates a hex of its own.
|
|
138
|
+
*/
|
|
139
|
+
function roleColor(role: string): string {
|
|
140
|
+
const value = colorValue(role) ?? colorValue(role, NEUTRAL_TOKENS)
|
|
141
|
+
if (value === undefined) throw new Error(`No design token declares ${role}`)
|
|
142
|
+
return value
|
|
143
|
+
}
|
|
144
|
+
|
|
134
145
|
/**
|
|
135
146
|
* The same brand mark `src/design/render.ts` embeds in its own preview,
|
|
136
147
|
* colored with the dark accent since the board's default chrome is dark
|
|
@@ -139,7 +150,7 @@ const SCROLLSPY_SCRIPT = [
|
|
|
139
150
|
* copies rather than a shared icon helper for the first second caller.
|
|
140
151
|
*/
|
|
141
152
|
function faviconLink(): string {
|
|
142
|
-
const color =
|
|
153
|
+
const color = roleColor('accent')
|
|
143
154
|
const href = `data:image/svg+xml,${encodeURIComponent(
|
|
144
155
|
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="10 10 80 80"><path d="M34,20 L15,28 L15,72 L34,80 Z M66,20 L85,28 L85,72 L66,80 Z" fill="${color}" /><rect x="44" y="15" width="12" height="70" rx="2" fill="${color}" /></svg>`,
|
|
145
156
|
)}`
|
|
@@ -232,12 +243,12 @@ ${SCROLLSPY_SCRIPT}
|
|
|
232
243
|
}
|
|
233
244
|
|
|
234
245
|
function panelPage(title: string, body: string): string {
|
|
235
|
-
const accent =
|
|
236
|
-
const surface =
|
|
237
|
-
const border =
|
|
238
|
-
const muted =
|
|
239
|
-
const text =
|
|
240
|
-
const background =
|
|
246
|
+
const accent = roleColor('light-accent')
|
|
247
|
+
const surface = roleColor('light-surface')
|
|
248
|
+
const border = roleColor('light-border')
|
|
249
|
+
const muted = roleColor('light-muted')
|
|
250
|
+
const text = roleColor('light-text')
|
|
251
|
+
const background = roleColor('light-background')
|
|
241
252
|
|
|
242
253
|
return `<!doctype html>
|
|
243
254
|
<html lang="en">
|