@erclx/canon 4.82.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 +1 -1
- package/docs/agents/context-audit-checks.md +22 -2
- package/docs/agents/context-audit.md +12 -2
- package/docs/agents/context-classify.md +1 -1
- package/docs/workflow/ai-workflow.md +1 -1
- package/docs/workflow/visual-design-workflow.md +1 -1
- package/governance/rules/claude/520-wireframes.md +1 -1
- package/package.json +1 -1
- package/src/commands/context.ts +71 -2
- package/src/context/architecture.ts +73 -0
- package/src/context/audit.ts +18 -2
- package/src/context/gate.ts +44 -6
- package/src/context/wireframe-states.ts +238 -0
- package/standards/architecture.md +11 -1
- package/standards/context.md +4 -1
- package/standards/design.md +6 -0
- package/standards/requirements.md +2 -1
- package/standards/wireframes.md +45 -29
- package/tooling/claude/reference.md +1 -1
- package/tooling/claude/seeds/CLAUDE.md +1 -1
- package/tooling/claude/seeds/canon/wireframes/index.md +2 -2
package/src/context/audit.ts
CHANGED
|
@@ -77,6 +77,14 @@ const INSIDE_LIST = /^\s+\S/
|
|
|
77
77
|
* and which release labelled it. A marker is a judgment rather than a defect,
|
|
78
78
|
* so this is measured and reported and never gates.
|
|
79
79
|
*
|
|
80
|
+
* The change pattern matches any digit count rather than the three-plus this
|
|
81
|
+
* used to require, since a project young enough to sit in single or double
|
|
82
|
+
* digits, `PR #7`, went entirely unread under the old floor. Widening it reads
|
|
83
|
+
* a change reference wherever a document is scanned as prose, so `provenance`
|
|
84
|
+
* masks a line's displayed spans first, which is what keeps a heading's own
|
|
85
|
+
* auto-derived link destination, `(#7-open-questions)`, from reading as a
|
|
86
|
+
* change number: `#7` there names the heading, not a pull request.
|
|
87
|
+
*
|
|
80
88
|
* The release pattern accepts three segments without a leading `v`, since the
|
|
81
89
|
* standard cuts a release label rather than a spelling of one and `a CLI at
|
|
82
90
|
* 0.83.0` names a release exactly as `v0.83.0` does. Two segments still require
|
|
@@ -88,7 +96,7 @@ const INSIDE_LIST = /^\s+\S/
|
|
|
88
96
|
*/
|
|
89
97
|
const PROVENANCE: readonly { kind: ProvenanceKind; pattern: RegExp }[] = [
|
|
90
98
|
{ kind: 'date', pattern: /\b\d{4}-\d{2}-\d{2}\b/g },
|
|
91
|
-
{ kind: 'change', pattern: /#\d
|
|
99
|
+
{ kind: 'change', pattern: /#\d+\b/g },
|
|
92
100
|
{ kind: 'release', pattern: /\b(?:v\d+\.\d+(?:\.\d+)?|\d+\.\d+\.\d+)\b/g },
|
|
93
101
|
]
|
|
94
102
|
|
|
@@ -349,6 +357,12 @@ function catalogTables(entry: readonly BodyLine[]): TableFinding[] {
|
|
|
349
357
|
* rather than a claim it makes, and a version pinned in an install line is the
|
|
350
358
|
* ordinary shape of one.
|
|
351
359
|
*
|
|
360
|
+
* Displayed spans are masked for the same reason. A code span quoting a hex
|
|
361
|
+
* color, `#000`, and a link destination naming a heading's own anchor,
|
|
362
|
+
* `(#7-open-questions)`, both read as text the entry shows rather than a claim
|
|
363
|
+
* it makes, which is the same distinction the fence skip draws at the block
|
|
364
|
+
* level.
|
|
365
|
+
*
|
|
352
366
|
* A date stamping a measurement is dropped rather than reported under a kind of
|
|
353
367
|
* its own. One list with one meaning is what lets every consumer read it
|
|
354
368
|
* without filtering: the report names what the standard cuts, and the length
|
|
@@ -365,8 +379,10 @@ function provenance(lines: readonly BodyLine[]): ProvenanceFinding[] {
|
|
|
365
379
|
for (const line of lines) {
|
|
366
380
|
if (line.fenced) continue
|
|
367
381
|
|
|
382
|
+
const text = maskDisplayed(line.text)
|
|
383
|
+
|
|
368
384
|
for (const { kind, pattern } of PROVENANCE) {
|
|
369
|
-
for (const match of
|
|
385
|
+
for (const match of text.matchAll(pattern)) {
|
|
370
386
|
if (kind === 'date' && stampsMeasurement(line.text, match.index)) {
|
|
371
387
|
continue
|
|
372
388
|
}
|
package/src/context/gate.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SectionFinding } from '@/context/audit'
|
|
2
2
|
import type { FolderDrift } from '@/context/index-drift'
|
|
3
|
+
import type { WireframeStatesReport } from '@/context/wireframe-states'
|
|
3
4
|
|
|
4
5
|
export interface GateInput {
|
|
5
6
|
/** Cited paths that resolved to nothing, which gate under either mode. */
|
|
@@ -15,6 +16,22 @@ export interface GateInput {
|
|
|
15
16
|
readonly recordOverLength: boolean
|
|
16
17
|
readonly sections: readonly SectionFinding[]
|
|
17
18
|
readonly drift: readonly FolderDrift[]
|
|
19
|
+
/**
|
|
20
|
+
* Every wireframe entry's states report, empty for a project carrying no
|
|
21
|
+
* `canon/wireframes/` folder. A states/evidence mismatch is a fact read off
|
|
22
|
+
* the file, the same standing the missing-section and index-drift findings
|
|
23
|
+
* already have, so it gates alongside them under the widened mode.
|
|
24
|
+
*
|
|
25
|
+
* A sketch beside existing evidence stays out of the gate on both modes.
|
|
26
|
+
* `sketchWithEvidence` reads the whole entry against whether any of its
|
|
27
|
+
* states has evidence, not the sketched layout against the evidence for
|
|
28
|
+
* that same layout, since the standard's `## States` table names states
|
|
29
|
+
* rather than layouts and the report has no narrower unit to match on. An
|
|
30
|
+
* entry with a captured default layout and a sketch for a breakpoint layout
|
|
31
|
+
* that is not built yet is conforming and still trips this reading, so the
|
|
32
|
+
* finding stays a judgment for a reader rather than a fact a push fails on.
|
|
33
|
+
*/
|
|
34
|
+
readonly wireframes: readonly WireframeStatesReport[]
|
|
18
35
|
/**
|
|
19
36
|
* Whether the caller asked for the widened gate. False leaves a missing
|
|
20
37
|
* section and a drifted index advisory, which is what the project-root stage
|
|
@@ -30,28 +47,49 @@ export function hasDrift(drift: readonly FolderDrift[]): boolean {
|
|
|
30
47
|
)
|
|
31
48
|
}
|
|
32
49
|
|
|
50
|
+
/** Whether any wireframe's States table disagrees with its evidence folders. */
|
|
51
|
+
export function hasStatesMismatch(
|
|
52
|
+
wireframes: readonly WireframeStatesReport[],
|
|
53
|
+
): boolean {
|
|
54
|
+
return wireframes.some(
|
|
55
|
+
(entry) =>
|
|
56
|
+
entry.missingFolders.length > 0 || entry.unlistedFolders.length > 0,
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Whether any wireframe carries a sketch beside evidence that already exists. */
|
|
61
|
+
export function hasSketchWithEvidence(
|
|
62
|
+
wireframes: readonly WireframeStatesReport[],
|
|
63
|
+
): boolean {
|
|
64
|
+
return wireframes.some((entry) => entry.sketchWithEvidence)
|
|
65
|
+
}
|
|
66
|
+
|
|
33
67
|
/**
|
|
34
68
|
* Whether the audit found something that should fail the caller.
|
|
35
69
|
*
|
|
36
70
|
* An unresolved citation is a broken pointer and gates unconditionally, and so
|
|
37
71
|
* does a record past its own ceiling: the record states the limit for itself
|
|
38
72
|
* and derives it from a count, which makes it the one measure here that is a
|
|
39
|
-
* fact rather than a threshold a reader weighs. The
|
|
40
|
-
*
|
|
41
|
-
* declare,
|
|
42
|
-
*
|
|
43
|
-
*
|
|
73
|
+
* fact rather than a threshold a reader weighs. The findings `--gate` adds are
|
|
74
|
+
* the ones answerable from the file itself: a required section it does not
|
|
75
|
+
* declare, an index disagreeing with its folder, and a wireframe's States
|
|
76
|
+
* table disagreeing with its evidence folders. Entry length, depth, bullet,
|
|
77
|
+
* table, provenance, the record's claim coverage, and a sketch beside
|
|
78
|
+
* existing evidence are judgments, so they stay out under both modes. The
|
|
79
|
+
* `wireframes` field's own doc states why the sketch finding is a judgment
|
|
80
|
+
* rather than a fact.
|
|
44
81
|
*/
|
|
45
82
|
export function isGating({
|
|
46
83
|
unresolvedCitations,
|
|
47
84
|
recordOverLength,
|
|
48
85
|
sections,
|
|
49
86
|
drift,
|
|
87
|
+
wireframes,
|
|
50
88
|
widened,
|
|
51
89
|
}: GateInput): boolean {
|
|
52
90
|
if (unresolvedCitations > 0) return true
|
|
53
91
|
if (recordOverLength) return true
|
|
54
92
|
if (!widened) return false
|
|
55
93
|
|
|
56
|
-
return sections.length > 0 || hasDrift(drift)
|
|
94
|
+
return sections.length > 0 || hasDrift(drift) || hasStatesMismatch(wireframes)
|
|
57
95
|
}
|
|
@@ -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
|
+
}
|
|
@@ -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.
|
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.
|
|
@@ -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
|
+
```
|
|
@@ -15,7 +15,7 @@ canon/
|
|
|
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
|
|
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.
|
|
19
19
|
|
|
20
20
|
.claude/
|
|
21
21
|
├── GOV.md ← retired. Removed by `canon gov sync` if present from a prior install
|
|
@@ -21,5 +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
25
|
- `canon/decisions/`: decision history a project doc points at, never loaded eagerly
|