@erclx/canon 4.81.0 → 4.83.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/docs-fold/REQUIREMENT.md +13 -0
- package/claude/skills/docs-fold/SKILL.md +20 -0
- package/claude/skills/docs-fold/references/classify.md +78 -0
- package/claude/skills/draft-slides/SKILL.md +1 -1
- package/claude/skills/draft-wireframes/REQUIREMENT.md +1 -1
- package/claude/skills/draft-wireframes/SKILL.md +7 -4
- package/docs/agents/commands.md +8 -4
- package/docs/agents/context-audit-checks.md +22 -2
- package/docs/agents/context-audit.md +13 -3
- package/docs/agents/context-classify.md +99 -0
- package/docs/agents/index.md +1 -0
- package/docs/target-projects.md +15 -0
- package/docs/workflow/ai-workflow.md +4 -3
- package/docs/workflow/visual-design-workflow.md +1 -1
- package/governance/rules/claude/520-wireframes.md +1 -1
- package/governance/rules/claude/545-decisions.md +12 -0
- package/package.json +1 -1
- package/src/claude/seeds.ts +1 -0
- package/src/commands/claude.ts +26 -5
- package/src/commands/context.ts +496 -2
- package/src/context/architecture.ts +73 -0
- package/src/context/audit.ts +18 -2
- package/src/context/classify/extract.ts +450 -0
- package/src/context/classify/ollama.ts +172 -0
- package/src/context/classify/patterns.ts +114 -0
- package/src/context/classify/prompts.ts +73 -0
- package/src/context/classify/run.ts +348 -0
- package/src/context/classify/settings.ts +196 -0
- package/src/context/folders.ts +1 -0
- package/src/context/gate.ts +44 -6
- package/src/context/wireframe-states.ts +238 -0
- package/src/surface-root.ts +1 -0
- package/standards/architecture.md +11 -1
- package/standards/context.md +4 -1
- package/standards/decisions.md +100 -0
- package/standards/design.md +6 -0
- package/standards/index.md +1 -0
- package/standards/requirements.md +2 -1
- package/standards/wireframes.md +45 -29
- package/tooling/claude/reference.md +2 -1
- package/tooling/claude/seeds/CLAUDE.md +2 -1
- package/tooling/claude/seeds/canon/decisions/index.md +8 -0
- package/tooling/claude/seeds/canon/wireframes/index.md +2 -2
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { readFile, readdir, stat } from 'node:fs/promises'
|
|
2
|
+
import { basename, dirname, join, relative } from 'node:path'
|
|
3
|
+
import type { AuditedFolder } from '@/context/folders'
|
|
4
|
+
import { type BodyLine, bodyLines } from '@/markdown/scan'
|
|
5
|
+
|
|
6
|
+
/** `standards/wireframes.md`'s `## States` heading, matched by text alone. */
|
|
7
|
+
const STATES_HEADING = /^##\s+States\s*$/i
|
|
8
|
+
const SECTION_HEADING = /^##\s+\S/
|
|
9
|
+
const TABLE_ROW = /^\s*\|/
|
|
10
|
+
const TABLE_SEPARATOR = /^\s*\|[\s:|-]+\|\s*$/
|
|
11
|
+
|
|
12
|
+
/** The one exception the standard admits to the states/evidence bijection. */
|
|
13
|
+
const NOT_CAPTURED = 'not captured'
|
|
14
|
+
|
|
15
|
+
/** A fenced block's opening delimiter naming the `plaintext` info string. */
|
|
16
|
+
const PLAINTEXT_FENCE = /^`{3,}\s*plaintext\s*$/i
|
|
17
|
+
|
|
18
|
+
export interface StateRow {
|
|
19
|
+
readonly line: number
|
|
20
|
+
readonly state: string
|
|
21
|
+
/** The evidence cell as written, backticks and all. */
|
|
22
|
+
readonly evidence: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface MissingFolderFinding {
|
|
26
|
+
readonly line: number
|
|
27
|
+
readonly state: string
|
|
28
|
+
/** The cited path, so a report names what to go create or fix. */
|
|
29
|
+
readonly path: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface UnlistedFolderFinding {
|
|
33
|
+
/** The evidence root the folder sits under, cited by at least one row. */
|
|
34
|
+
readonly root: string
|
|
35
|
+
readonly folder: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface WireframeStatesReport {
|
|
39
|
+
readonly rel: string
|
|
40
|
+
readonly rows: readonly StateRow[]
|
|
41
|
+
/** A row whose evidence cell names a path with no folder at it. */
|
|
42
|
+
readonly missingFolders: readonly MissingFolderFinding[]
|
|
43
|
+
/** A folder under a cited root that no row names. */
|
|
44
|
+
readonly unlistedFolders: readonly UnlistedFolderFinding[]
|
|
45
|
+
/** Line of the `plaintext` fence, absent from an entry carrying no sketch. */
|
|
46
|
+
readonly sketchLine?: number
|
|
47
|
+
/** Whether that sketch sits beside a state whose evidence already exists. */
|
|
48
|
+
readonly sketchWithEvidence: boolean
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function cells(row: string): string[] {
|
|
52
|
+
const parts = row.split('|')
|
|
53
|
+
if ((parts[0] ?? '').trim() === '') parts.shift()
|
|
54
|
+
if ((parts[parts.length - 1] ?? '').trim() === '') parts.pop()
|
|
55
|
+
return parts.map((cell) => cell.trim())
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function columnIndex(headers: readonly string[], name: string): number {
|
|
59
|
+
return headers.findIndex(
|
|
60
|
+
(header) => header.toLowerCase() === name.toLowerCase(),
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Reads the States table under the `## States` heading, matching its `State`
|
|
66
|
+
* and `Evidence` columns by header text rather than position.
|
|
67
|
+
*
|
|
68
|
+
* Column order is the one part of the standard's own table the standard could
|
|
69
|
+
* still move during its own review, per the plan this ships under. Matching
|
|
70
|
+
* position would break silently on a reorder, where matching text breaks the
|
|
71
|
+
* same way a renamed header would: visibly, by finding nothing.
|
|
72
|
+
*/
|
|
73
|
+
export function parseStatesTable(lines: readonly BodyLine[]): StateRow[] {
|
|
74
|
+
let index = 0
|
|
75
|
+
while (index < lines.length && !STATES_HEADING.test(lines[index].text)) {
|
|
76
|
+
index++
|
|
77
|
+
}
|
|
78
|
+
if (index >= lines.length) return []
|
|
79
|
+
index++
|
|
80
|
+
|
|
81
|
+
while (index < lines.length && !SECTION_HEADING.test(lines[index].text)) {
|
|
82
|
+
const separator = lines[index + 1]
|
|
83
|
+
if (
|
|
84
|
+
TABLE_ROW.test(lines[index].text) &&
|
|
85
|
+
separator !== undefined &&
|
|
86
|
+
TABLE_SEPARATOR.test(separator.text)
|
|
87
|
+
) {
|
|
88
|
+
break
|
|
89
|
+
}
|
|
90
|
+
index++
|
|
91
|
+
}
|
|
92
|
+
if (
|
|
93
|
+
index >= lines.length ||
|
|
94
|
+
SECTION_HEADING.test(lines[index].text) ||
|
|
95
|
+
!TABLE_ROW.test(lines[index].text)
|
|
96
|
+
) {
|
|
97
|
+
return []
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const headers = cells(lines[index].text)
|
|
101
|
+
const stateCol = columnIndex(headers, 'State')
|
|
102
|
+
const evidenceCol = columnIndex(headers, 'Evidence')
|
|
103
|
+
if (stateCol === -1 || evidenceCol === -1) return []
|
|
104
|
+
|
|
105
|
+
index += 2
|
|
106
|
+
const rows: StateRow[] = []
|
|
107
|
+
while (index < lines.length && TABLE_ROW.test(lines[index].text)) {
|
|
108
|
+
const row = cells(lines[index].text)
|
|
109
|
+
rows.push({
|
|
110
|
+
line: lines[index].number,
|
|
111
|
+
state: row[stateCol] ?? '',
|
|
112
|
+
evidence: row[evidenceCol] ?? '',
|
|
113
|
+
})
|
|
114
|
+
index++
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return rows
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Line of the first `plaintext`-fenced sketch, or nothing. */
|
|
121
|
+
export function findSketchLine(lines: readonly BodyLine[]): number | undefined {
|
|
122
|
+
for (const line of lines) {
|
|
123
|
+
if (PLAINTEXT_FENCE.test(line.text.trim())) return line.number
|
|
124
|
+
}
|
|
125
|
+
return undefined
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Reads an evidence cell into the path it cites, or nothing for the one cell
|
|
130
|
+
* value the standard exempts from having one.
|
|
131
|
+
*/
|
|
132
|
+
function evidencePath(cell: string): string | undefined {
|
|
133
|
+
const stripped = cell.replace(/^`+|`+$/g, '').trim()
|
|
134
|
+
if (stripped.toLowerCase() === NOT_CAPTURED) return undefined
|
|
135
|
+
return stripped.replace(/\/+$/, '')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function isDirectory(path: string): Promise<boolean> {
|
|
139
|
+
try {
|
|
140
|
+
return (await stat(path)).isDirectory()
|
|
141
|
+
} catch {
|
|
142
|
+
return false
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function subfolders(path: string): Promise<string[]> {
|
|
147
|
+
try {
|
|
148
|
+
const entries = await readdir(path, { withFileTypes: true })
|
|
149
|
+
return entries
|
|
150
|
+
.filter((entry) => entry.isDirectory())
|
|
151
|
+
.map((entry) => entry.name)
|
|
152
|
+
} catch {
|
|
153
|
+
return []
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Measures one wireframe entry against its own States table.
|
|
159
|
+
*
|
|
160
|
+
* The evidence cell resolves as the literal path it names against `root`,
|
|
161
|
+
* per the plan's operator answer, since the standard puts the path there and a
|
|
162
|
+
* root guessed from convention reads wrong in a monorepo carrying more than
|
|
163
|
+
* one evidence tree.
|
|
164
|
+
*
|
|
165
|
+
* An unlisted folder is reported only under a root at least one row already
|
|
166
|
+
* cites, per the same operator answer, so a project holding evidence with no
|
|
167
|
+
* states table at all reports nothing. `roots` is built from the rows this
|
|
168
|
+
* loop already resolved rather than scanned separately, which is what keeps
|
|
169
|
+
* the two readings in the same pass.
|
|
170
|
+
*/
|
|
171
|
+
export async function measureWireframeStates(
|
|
172
|
+
root: string,
|
|
173
|
+
rel: string,
|
|
174
|
+
source: string,
|
|
175
|
+
): Promise<WireframeStatesReport> {
|
|
176
|
+
const lines = bodyLines(source)
|
|
177
|
+
const rows = parseStatesTable(lines)
|
|
178
|
+
const sketchLine = findSketchLine(lines)
|
|
179
|
+
|
|
180
|
+
const missingFolders: MissingFolderFinding[] = []
|
|
181
|
+
const cited = new Map<string, Set<string>>()
|
|
182
|
+
let anyCaptured = false
|
|
183
|
+
|
|
184
|
+
for (const row of rows) {
|
|
185
|
+
const path = evidencePath(row.evidence)
|
|
186
|
+
if (path === undefined) continue
|
|
187
|
+
|
|
188
|
+
if (!(await isDirectory(join(root, path)))) {
|
|
189
|
+
missingFolders.push({ line: row.line, state: row.state, path })
|
|
190
|
+
continue
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
anyCaptured = true
|
|
194
|
+
const evidenceRoot = dirname(path)
|
|
195
|
+
const folder = basename(path)
|
|
196
|
+
const named = cited.get(evidenceRoot) ?? new Set<string>()
|
|
197
|
+
named.add(folder)
|
|
198
|
+
cited.set(evidenceRoot, named)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const unlistedFolders: UnlistedFolderFinding[] = []
|
|
202
|
+
for (const [evidenceRoot, named] of cited) {
|
|
203
|
+
for (const folder of await subfolders(join(root, evidenceRoot))) {
|
|
204
|
+
if (!named.has(folder)) {
|
|
205
|
+
unlistedFolders.push({ root: evidenceRoot, folder })
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
rel,
|
|
212
|
+
rows,
|
|
213
|
+
missingFolders,
|
|
214
|
+
unlistedFolders,
|
|
215
|
+
...(sketchLine !== undefined && { sketchLine }),
|
|
216
|
+
sketchWithEvidence: sketchLine !== undefined && anyCaptured,
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Measures every wireframe entry in a resolved `wireframes` folder. */
|
|
221
|
+
export async function measureWireframeFolder(
|
|
222
|
+
root: string,
|
|
223
|
+
folder: AuditedFolder,
|
|
224
|
+
): Promise<WireframeStatesReport[]> {
|
|
225
|
+
const reports: WireframeStatesReport[] = []
|
|
226
|
+
|
|
227
|
+
for (const path of folder.entries) {
|
|
228
|
+
reports.push(
|
|
229
|
+
await measureWireframeStates(
|
|
230
|
+
root,
|
|
231
|
+
relative(root, path),
|
|
232
|
+
await readFile(path, 'utf8'),
|
|
233
|
+
),
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return reports
|
|
238
|
+
}
|
package/src/surface-root.ts
CHANGED
|
@@ -28,16 +28,26 @@ Does not govern:
|
|
|
28
28
|
- How individual functions work line by line. The code carries its own behavior.
|
|
29
29
|
- Full type definitions. They live in code. Reference the shape conceptually if needed.
|
|
30
30
|
- A measurement paragraph specific to one domain's own mechanism. Route it to that domain's `canon/context/<domain>.md` entry instead. The choice and its rejected alternative stay here whatever their reach, since reach is what makes a decision cross-domain, not how many domains its supporting measurement happens to touch.
|
|
31
|
+
- A decision that constrains one domain alone. It lives in that domain's context entry, and this file carries at most one line pointing at it.
|
|
32
|
+
- The history of how a decision was reached or revised: rounds of candidates, a figure followed by its correction, a branch or change that moved a number. That trail goes to the decision log or the change that introduced it.
|
|
31
33
|
|
|
32
34
|
## Sections
|
|
33
35
|
|
|
34
36
|
Use `## Overview`, `## Key technical decisions` with one named H3 per decision, and `## Risks / open questions`. Name each decision and give the reasoning, especially for non-obvious choices. Skip entries where the rationale is self-evident.
|
|
35
37
|
|
|
38
|
+
## Keeping it current
|
|
39
|
+
|
|
40
|
+
- Rewrite a decision a later one changed rather than appending the change beside it. A reader should find the design that stands in one place, with the alternative that lost stated once.
|
|
41
|
+
- Hold only what is open under `## Risks / open questions`. An entry leaves the section in the change that settles it, becoming a decision here when it constrains more than one domain and moving to that domain's context entry when it does not.
|
|
42
|
+
|
|
36
43
|
## Verification anchors
|
|
37
44
|
|
|
38
45
|
A decision's reasoning stays correct while the numbers it cites move. The anchor records what a measured claim was read against, so a reader can tell a number that was checked and held from one nobody has looked at since.
|
|
39
46
|
|
|
40
47
|
- Close a decision entry whose reasoning cites a measured number with a trailing sentence naming the short commit SHA and the ISO date that number was read: `Measured at <short-sha> on <YYYY-MM-DD>.`
|
|
48
|
+
- Name a commit, never a pull request number or a branch. A branch is gone after merge, and a pull request number resolves only on the forge.
|
|
49
|
+
- Carry one anchor per figure. When a number is re-measured, rewrite the number and its anchor in place, and leave the old value to the change that moved it.
|
|
50
|
+
- Point at a generated file that already records a figure and its commit, rather than copying the figure and anchoring the copy.
|
|
41
51
|
- Anchor on the number alone. A decision citing none takes no anchor whatever its reasoning rests on, because a marker over a claim nobody can re-measure is one no reader can falsify.
|
|
42
52
|
- Anchor a decision when writing it or when amending its reasoning. Leave an entry written before the rule unanchored rather than dating it by blame, which is archaeology for a marker nothing reads back.
|
|
43
53
|
- Read an absent anchor as unchecked rather than as current. On an entry citing no number there is nothing to check. On one citing a number the number is due a read.
|
|
@@ -46,7 +56,7 @@ A decision's reasoning stays correct while the numbers it cites move. The anchor
|
|
|
46
56
|
|
|
47
57
|
## Length
|
|
48
58
|
|
|
49
|
-
Every session pays for this file before any work starts, so a heavy read is a real cost. Judge weight by reading the file rather than by counting it: a file that reads heavy is carrying too many decisions, not decisions written too long.
|
|
59
|
+
Every session pays for this file before any work starts, so a heavy read is a real cost. Judge weight by reading the file rather than by counting it: a file that reads heavy is carrying too many decisions, not decisions written too long. A word count, for the file and for each decision, is read alongside that judgment when one is available, and it never gates.
|
|
50
60
|
|
|
51
61
|
- Bring a heavy file back by merging two decisions or retiring one, never by compressing a decision's prose.
|
|
52
62
|
- Yield to the paragraph weight checkpoint in `markdown.md`. A paragraph past the checkpoint is a defect no length guideline licenses.
|
package/standards/context.md
CHANGED
|
@@ -70,6 +70,7 @@ Pick by what the domain is. Add domain-specific headings as needed.
|
|
|
70
70
|
## Ordering
|
|
71
71
|
|
|
72
72
|
- Order sections `Overview`, `Layout`, `Decisions`, `Gotchas`, then everything else.
|
|
73
|
+
- Name a heading for its subject as it stands (`## Cache`), never for the event that shaped it (`## The cache was the untested part, and the test moved it`). A heading is what a scan reads, so a narrating heading turns the table of contents into a changelog.
|
|
73
74
|
- Entries get read top-down and often partially, so irreducible content sits above recoverable content.
|
|
74
75
|
|
|
75
76
|
## The development entry
|
|
@@ -84,6 +85,7 @@ Only the `development` entry carries this section. It is not a general-purpose h
|
|
|
84
85
|
- Decisions specific to the domain. Broader cross-domain decisions belong in `canon/ARCHITECTURE.md`.
|
|
85
86
|
- Constraints, gotchas, things tried and rejected
|
|
86
87
|
- Domain-specific conventions that do not fit a `paths:`-scoped rule
|
|
88
|
+
- A measured figure, anchored the way `architecture.md` states under `## Verification anchors`, so an entry and the architecture record date a number the same way.
|
|
87
89
|
- A reference to another entry, spelled as the path that entry sits at rather than as its bare filename. A bare name resolves against whichever folder the reader is already in, so a domain that splits into subfolders strands every inbound reference and the break surfaces nowhere. A reference to a seed, a standard, or a file the project owns elsewhere keeps the form its own surface uses.
|
|
88
90
|
|
|
89
91
|
## What does not go in
|
|
@@ -98,12 +100,13 @@ Only the `development` entry carries this section. It is not a general-purpose h
|
|
|
98
100
|
- Anything already in `canon/REQUIREMENTS.md` or `canon/ARCHITECTURE.md`.
|
|
99
101
|
- The history of how the domain reached its current shape. An entry describes the repository as it stands, so a change number, release label, or date attached to a change goes wherever the project tracks work.
|
|
100
102
|
- A rejected alternative's provenance, which is the same rule at the one place the section above admits history. Keep what was tried and why it lost. Cut who tried it and when.
|
|
103
|
+
- The route to a decision: the rounds of candidates, the tuning steps a threshold passed through, the review pass that caught a miss. State the decision, what lost, and why, once and in the present tense. The route goes to the decision log or the change that introduced it.
|
|
104
|
+
- Measured results a generated file already carries. Point at the file instead of copying the figure, so a re-run cannot leave the entry behind.
|
|
101
105
|
|
|
102
106
|
## Length
|
|
103
107
|
|
|
104
108
|
- Aim for one entry per domain. There is no hard cap. Length is a symptom, not the defect.
|
|
105
109
|
- Past roughly 150 rendered lines, check three things before adding more: whether the entry still covers a single domain, whether it has filled with content `ls` or `--help` reproduces, and whether it has accumulated the history of its own changes. Fix whichever is true rather than trimming to hit a number. Rendered lines count as `markdown.md` defines them.
|
|
106
|
-
- Where a bullet sits past the weight checkpoint `markdown.md` states, the overflow to move is the incident that motivated the decision, which specializes that rule's instruction to send the overflow to prose. Keep the current design and the alternative that lost, and send the incident to the change that introduced it, the issue that tracked it, or the research record behind it.
|
|
107
110
|
- Never cut a `## Decisions` or `## Gotchas` entry to shorten a file. Cut a `## Layout` or `## CLI` section instead.
|
|
108
111
|
- Retire a decision or gotcha once its subject is gone, rewriting the bullet to state the current design rather than leaving the narration of what it replaced beside it. A rejected alternative is not a retired one, so what was tried and why it lost stays whatever its age. The rule above protects content whose subject is live, and this one releases content whose subject is not.
|
|
109
112
|
- Rewrite a decision a later one replaced rather than appending the replacement beside it. The subject is still live, so the rule above does not reach it, and two bullets on one subject leave a reader to work out which of them is current. State the design that stands and keep the superseded reasoning only where it is the alternative that lost.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Decisions reference
|
|
3
|
+
description: Folder layout, ordinal filename, frontmatter, record sections, and the append-only lifecycle for canon/decisions/
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Decisions reference
|
|
7
|
+
|
|
8
|
+
Applies to `canon/decisions/`. Holds the history a canonical doc used to carry itself: pick rounds, superseded figures, and a decision's rejected alternatives, in a tracked folder nothing loads eagerly. A canonical doc points at a record from its own history section rather than restating it.
|
|
9
|
+
|
|
10
|
+
## Scope
|
|
11
|
+
|
|
12
|
+
Governs `canon/decisions/`: folder layout, the ordinal filename, frontmatter, record sections, and the append-only lifecycle.
|
|
13
|
+
|
|
14
|
+
Does not govern:
|
|
15
|
+
|
|
16
|
+
- Which canonical doc points here and when, and what stays behind in that doc's own body: `architecture.md`, `context.md`, `wireframes.md`, `design.md`, `requirements.md`, once each states its own retirement rule
|
|
17
|
+
- Voice, rhythm, and sentence construction: the `write-human` skill
|
|
18
|
+
- Headings, punctuation, word choice, and file references: `markdown.md`
|
|
19
|
+
|
|
20
|
+
## What a working record looks like
|
|
21
|
+
|
|
22
|
+
A record works when a reader who has never opened the project can follow it from the file alone:
|
|
23
|
+
|
|
24
|
+
- What was decided, stated once, without needing the canonical doc that points here
|
|
25
|
+
- What else was considered, and why each alternative lost
|
|
26
|
+
- Which claim rests on a measurement, and what commit that measurement was read against
|
|
27
|
+
|
|
28
|
+
A record failing these is non-conforming even when it satisfies every shape rule below.
|
|
29
|
+
|
|
30
|
+
## Folder name
|
|
31
|
+
|
|
32
|
+
- `canon/decisions/`, resolved the way every tracked surface is: at `canon/decisions/` in a project that has moved, at `.claude/decisions/` in one that has not.
|
|
33
|
+
- Never add `canon/decisions/index.md` to a `CLAUDE.md` `@` import. A log that loads eagerly rebuilds the bloat it exists to absorb. A canonical doc's own pointer is how a reader reaches a record, one file at a time.
|
|
34
|
+
|
|
35
|
+
## Record filename
|
|
36
|
+
|
|
37
|
+
- Name each record `<nn>-<slug>.md`, a two-digit zero-padded ordinal followed by a kebab-case slug, the same shape a groundwork track's folder takes.
|
|
38
|
+
- The ordinal is the order the record was written in, which is what lets a listing sort by when a decision landed rather than alphabetically by subject.
|
|
39
|
+
- Never renumber an existing record. A later reader cites it by that name, and a record whose number moved is a record a stale citation can no longer find.
|
|
40
|
+
|
|
41
|
+
## Frontmatter
|
|
42
|
+
|
|
43
|
+
- `title` (required): the decision in sentence case
|
|
44
|
+
- `description` (required): one line naming what was decided
|
|
45
|
+
|
|
46
|
+
## Sections
|
|
47
|
+
|
|
48
|
+
Use `## Context`, `## Decision`, `## Alternatives`, and `## Measurements`.
|
|
49
|
+
|
|
50
|
+
- `## Context`: the problem as it stood, stated so a reader needs nothing else open. Restate a fact rather than pointing at where it was found.
|
|
51
|
+
- `## Decision`: what was chosen, and why, in enough detail that a reader can tell it apart from an alternative that sounds similar.
|
|
52
|
+
- `## Alternatives`: each one considered and dropped, with the reason it lost. An alternative with no stated reason reads as a claim nobody checked.
|
|
53
|
+
- `## Measurements`: skip when the decision cites no number. When it does, state the number and close with the commit it was read against, per Verification anchors below.
|
|
54
|
+
|
|
55
|
+
## Verification anchors
|
|
56
|
+
|
|
57
|
+
A record's reasoning stays correct while the numbers it cites move. The anchor records what a measured claim was read against, so a reader can tell a number that was checked and held from one nobody has looked at since.
|
|
58
|
+
|
|
59
|
+
- Close a `## Measurements` section with a trailing sentence naming the short commit SHA and the ISO date that number was read: `Measured at <short-sha> on <YYYY-MM-DD>.`
|
|
60
|
+
- Anchor on the number alone. A record citing none carries no `## Measurements` section at all.
|
|
61
|
+
- Read an absent section as unchecked rather than as current. A record with no measurements has nothing due a re-read.
|
|
62
|
+
|
|
63
|
+
## Lifecycle
|
|
64
|
+
|
|
65
|
+
- Append-only. A written record is never edited to reflect a later reversal.
|
|
66
|
+
- Write a new record when a later decision supersedes an earlier one, naming the record it supersedes. The old record stays as it was written, since it is history rather than a live statement of the current shape.
|
|
67
|
+
- Never auto-loaded. A canonical doc's own history section links to a record by relative path, and a reader reaches it by following that link, not by the folder loading with the session.
|
|
68
|
+
|
|
69
|
+
## Citation
|
|
70
|
+
|
|
71
|
+
- Never cite `.canon/`. A record restates what it needs, since a clone without the gitignored records folder resolves nothing there.
|
|
72
|
+
- Cite a same-repository pull request or commit the way `publish.md` fixes for any tracked document.
|
|
73
|
+
|
|
74
|
+
## Template
|
|
75
|
+
|
|
76
|
+
```markdown
|
|
77
|
+
---
|
|
78
|
+
title: <Decision, in sentence case>
|
|
79
|
+
description: <one line naming what was decided>
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
# <Decision title>
|
|
83
|
+
|
|
84
|
+
## Context
|
|
85
|
+
|
|
86
|
+
<The problem as it stood, self-contained.>
|
|
87
|
+
|
|
88
|
+
## Decision
|
|
89
|
+
|
|
90
|
+
<What was chosen, and why.>
|
|
91
|
+
|
|
92
|
+
## Alternatives
|
|
93
|
+
|
|
94
|
+
- **<Alternative>.** <Why it lost.>
|
|
95
|
+
- **<Alternative>.** <Why it lost.>
|
|
96
|
+
|
|
97
|
+
## Measurements
|
|
98
|
+
|
|
99
|
+
<The number the decision rests on.> Measured at <short-sha> on <YYYY-MM-DD>.
|
|
100
|
+
```
|
package/standards/design.md
CHANGED
|
@@ -27,6 +27,7 @@ Does not govern:
|
|
|
27
27
|
|
|
28
28
|
- CSS classes and prop names. Those live in code.
|
|
29
29
|
- Anything that needs updating every time the code is refactored
|
|
30
|
+
- The history of how a value or a mark was chosen: the candidates, the rounds, the date of a pick. State the rule and the current value. The trail goes to the decision log.
|
|
30
31
|
|
|
31
32
|
## Format
|
|
32
33
|
|
|
@@ -48,6 +49,11 @@ A prose section takes its uncertainty inline instead, in a sentence saying what
|
|
|
48
49
|
|
|
49
50
|
Use `## Personality`, `## Color`, `## Typography`, `## Spacing`, `## Borders`, `## Motion`, and `## Iconography`. The token tables carry fixed headers the renderer reads.
|
|
50
51
|
|
|
52
|
+
Add either optional section when the project has content for it:
|
|
53
|
+
|
|
54
|
+
- `## Layout`: page width, breakpoints, and grid rules, as current constraints
|
|
55
|
+
- `## Mark`: the logo's construction rules and the files that carry it
|
|
56
|
+
|
|
51
57
|
## Template
|
|
52
58
|
|
|
53
59
|
The column headers are the strings the renderer parses, read by exact key, so they stay verbatim. Row names are not. Each is slugged into the variable name it emits, which leaves a project free to rename a row, add one, or drop one it has no use for.
|
package/standards/index.md
CHANGED
|
@@ -11,6 +11,7 @@ Reference docs for consistent authoring across the toolkit and target projects.
|
|
|
11
11
|
- [Branch reference](branch.md): Branch naming format and type conventions
|
|
12
12
|
- [Commit reference](commit.md): Commit message format and type conventions
|
|
13
13
|
- [Context entry reference](context.md): Shape and content rules for canon/context/<domain>.md entries
|
|
14
|
+
- [Decisions reference](decisions.md): Folder layout, ordinal filename, frontmatter, record sections, and the append-only lifecycle for canon/decisions/
|
|
14
15
|
- [Design reference](design.md): Shape and content rules for canon/DESIGN.md
|
|
15
16
|
- [Diagram reference](diagrams.md): Shape and content rules for .canon/diagrams/<kind>.md files
|
|
16
17
|
- [Docs reference](docs.md): Reader and jurisdiction, frontmatter, page structure, what a page links out to, the diagram permission, and when a category earns a subfolder
|
|
@@ -30,10 +30,11 @@ Does not govern:
|
|
|
30
30
|
|
|
31
31
|
- Implementation details, API names, or internal component references
|
|
32
32
|
- Anything that describes how a feature is built rather than what it does
|
|
33
|
+
- Measured results, such as scores, benchmark figures, or token counts. They move on every run and this file changes least. Name where the results live instead.
|
|
33
34
|
|
|
34
35
|
## Sections
|
|
35
36
|
|
|
36
|
-
Use `## Problem`, `## Goals`, `## Non-goals`, `## MVP features`, `## Tech stack`, and `## Constraints`. Add `## Distribution` when the rule below applies. Drop a section rather than pad it with filler.
|
|
37
|
+
Use `## Problem`, `## Goals`, `## Non-goals`, `## MVP features`, `## Tech stack`, and `## Constraints`. Add `## Distribution` when the rule below applies. Add `## Premise` when the project exists to answer a question, stated as the question and what would count as an answer, never as the answer measured so far. Drop a section rather than pad it with filler.
|
|
37
38
|
|
|
38
39
|
## Lifecycle
|
|
39
40
|
|
package/standards/wireframes.md
CHANGED
|
@@ -24,11 +24,12 @@ Does not govern:
|
|
|
24
24
|
|
|
25
25
|
A wireframe works when someone can rebuild the surface from it without opening the components:
|
|
26
26
|
|
|
27
|
-
- What
|
|
28
|
-
- Which states can a visitor reach,
|
|
27
|
+
- What regions are on screen, by name, and where does each sit relative to the others?
|
|
28
|
+
- Which states can a visitor reach, what triggers each, and where is its captured evidence?
|
|
29
29
|
- What does its structural copy say, word for word, and where does its long-form content come from?
|
|
30
|
+
- What was deliberately left off the surface?
|
|
30
31
|
|
|
31
|
-
A wireframe that fails these is non-conforming regardless of whether it satisfies every section rule below. The
|
|
32
|
+
A wireframe that fails these is non-conforming regardless of whether it satisfies every section rule below. The sections are the means. These four questions are the test.
|
|
32
33
|
|
|
33
34
|
## Transcription wireframes
|
|
34
35
|
|
|
@@ -45,18 +46,20 @@ Route detail the source citation does not already carry to `canon/context/` as u
|
|
|
45
46
|
|
|
46
47
|
Both fields feed `canon/wireframes/index.md` when regenerated.
|
|
47
48
|
|
|
48
|
-
##
|
|
49
|
+
## Regions
|
|
49
50
|
|
|
50
|
-
-
|
|
51
|
-
- Label regions with `←` annotations
|
|
52
|
-
-
|
|
53
|
-
-
|
|
51
|
+
- Name every region the surface holds, in a bullet list ordered as a reader scans the page, each with where it sits relative to the others: `- Act pane: right of the answer from 1024 up, an overlay below 1024`. These names are the vocabulary plans, picks, and tests use, and the placement is what a list without a sketch would otherwise lose. Pixel sizes stay in `canon/DESIGN.md`.
|
|
52
|
+
- Draw an ASCII block inside a `plaintext` fence only for a layout that is not built yet. Label regions with `←` annotations, show a region's role rather than its styling, and keep the proportions honest.
|
|
53
|
+
- Remove the fence in the change that first captures evidence for the layout. The capture is the exact picture, and a sketch kept beside it drifts from it. A wireframe carrying both a sketch and an evidence folder for the same layout is non-conforming.
|
|
54
|
+
- Give a layout that changes across a breakpoint its own entry under the list, named by what triggers it (`At 1024 and wider`), not by an arbitrary label. A wider gutter alone is not a new layout.
|
|
55
|
+
- Keep one surface per file, indexed by `index.md`.
|
|
54
56
|
|
|
55
|
-
##
|
|
57
|
+
## States
|
|
56
58
|
|
|
57
|
-
-
|
|
58
|
-
- Name
|
|
59
|
-
-
|
|
59
|
+
- List every state a visitor can reach in one table: the state name, what reaches it, what it shows in words, and its evidence folder.
|
|
60
|
+
- Name a state in plain kebab case (`answered`, `refused`) and name its evidence folder the same way, with no ordinal prefix. The table carries the reading order, so a state added in the middle renames nothing.
|
|
61
|
+
- Keep the table and the evidence folders one-to-one. A state with no capture yet reads `not captured` in its evidence cell, which is the one exception, and it stays visible until the capture lands.
|
|
62
|
+
- Add an H3 below the table only for a state that needs more than its row: what differs from the regions in words, and its own copy or behavior.
|
|
60
63
|
|
|
61
64
|
## Copy
|
|
62
65
|
|
|
@@ -66,9 +69,19 @@ Both fields feed `canon/wireframes/index.md` when regenerated.
|
|
|
66
69
|
|
|
67
70
|
## Behavior
|
|
68
71
|
|
|
69
|
-
- Describe interaction intent: what the visitor does
|
|
72
|
+
- Describe interaction intent: what the visitor does and what changes on screen.
|
|
70
73
|
- State the rule, not the mechanism. `The rail tracks the active section as the visitor scrolls` is intent. The scroll handler, throttle, and observer margins are not.
|
|
71
|
-
- Keep it to a short list. A Behavior section longer than the
|
|
74
|
+
- Keep it to a short list. A Behavior section longer than the regions and states is a sign implementation detail has leaked in.
|
|
75
|
+
|
|
76
|
+
## Not on this surface
|
|
77
|
+
|
|
78
|
+
- State what was deliberately left off, one bullet per exclusion, as a present-tense rule: `- No navigation rail`. This is the section that stops a draft from proposing what was already ruled out.
|
|
79
|
+
- Keep the reason to a clause where one is needed. The rounds that ruled it out go to the decision log.
|
|
80
|
+
|
|
81
|
+
## What moves out
|
|
82
|
+
|
|
83
|
+
- Which candidate beat which, and when: the decision log
|
|
84
|
+
- Measurements such as contrast ratios and timings: `canon/DESIGN.md` for tokens and floors, the surface's context entry for the rest
|
|
72
85
|
|
|
73
86
|
## What moves to canon/context/
|
|
74
87
|
|
|
@@ -84,13 +97,12 @@ Reference the context entry from the wireframe by path when a reader needs the m
|
|
|
84
97
|
## Maintenance
|
|
85
98
|
|
|
86
99
|
- When a surface's layout or interaction changes, update its wireframe file in the same PR. A wireframe showing a defunct layout is worse than none.
|
|
100
|
+
- When a state is added, removed, or renamed, update its row and its evidence folder in the same PR.
|
|
87
101
|
- The Behavior and Copy prose around an ASCII block is prose and follows `markdown.md` and the `write-human` skill. The fenced block itself is not, so a check scoped to prose is the wrong thing to rely on for what sits inside it.
|
|
88
102
|
|
|
89
103
|
## Template
|
|
90
104
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
````markdown
|
|
105
|
+
```markdown
|
|
94
106
|
---
|
|
95
107
|
title: <Surface name>
|
|
96
108
|
description: <when and where the surface appears>
|
|
@@ -98,17 +110,18 @@ description: <when and where the surface appears>
|
|
|
98
110
|
|
|
99
111
|
# <Surface name>
|
|
100
112
|
|
|
101
|
-
|
|
113
|
+
<One paragraph: what the surface is for and what it covers.>
|
|
102
114
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
115
|
+
## Regions
|
|
116
|
+
|
|
117
|
+
- <Region>: <what it holds>, <where it sits relative to the others>
|
|
118
|
+
- <Region>: <what it holds>, <where it sits relative to the others>
|
|
119
|
+
|
|
120
|
+
## States
|
|
121
|
+
|
|
122
|
+
| State | Reached when | Shows | Evidence |
|
|
123
|
+
| ------- | ------------ | ---------- | -------------------------- |
|
|
124
|
+
| <state> | <trigger> | <in words> | `<evidence-root>/<state>/` |
|
|
112
125
|
|
|
113
126
|
## Copy
|
|
114
127
|
|
|
@@ -119,5 +132,8 @@ description: <when and where the surface appears>
|
|
|
119
132
|
## Behavior
|
|
120
133
|
|
|
121
134
|
- <what the visitor does, and what changes on screen>
|
|
122
|
-
|
|
123
|
-
|
|
135
|
+
|
|
136
|
+
## Not on this surface
|
|
137
|
+
|
|
138
|
+
- <what was deliberately left out, as a present-tense rule>
|
|
139
|
+
```
|
|
@@ -14,7 +14,8 @@ canon/
|
|
|
14
14
|
├── ARCHITECTURE.md ← seeded. Technical design decisions and open questions
|
|
15
15
|
├── DESIGN.md ← seeded. Visual intent and the decisions behind it
|
|
16
16
|
├── context/ ← seeded. Per-domain narrative. `index.md` is the discovery anchor.
|
|
17
|
-
|
|
17
|
+
├── decisions/ ← seeded. Decision history a canonical doc points at, never loaded eagerly. `<nn>-<slug>.md` records.
|
|
18
|
+
└── wireframes/ ← seeded. Per-surface regions and states. `index.md` is the discovery anchor; `<surface>.md` files hold the regions list, the states table, and the behavior bullets.
|
|
18
19
|
|
|
19
20
|
.claude/
|
|
20
21
|
├── GOV.md ← retired. Removed by `canon gov sync` if present from a prior install
|
|
@@ -21,4 +21,5 @@
|
|
|
21
21
|
- `src/`: [description]
|
|
22
22
|
- `canon/DESIGN.md`: design tokens and the visual system
|
|
23
23
|
- `canon/context/`: per-domain narrative (how a domain is structured, decisions, gotchas), indexed via `canon/context/index.md`
|
|
24
|
-
- `canon/wireframes/`: per-surface
|
|
24
|
+
- `canon/wireframes/`: per-surface regions and states loaded on demand, indexed via `canon/wireframes/index.md`
|
|
25
|
+
- `canon/decisions/`: decision history a project doc points at, never loaded eagerly
|