@erclx/canon 4.88.1 → 4.90.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 +2 -0
- package/claude/skills/docs-fold/SKILL.md +6 -0
- package/docs/agents/audits.md +1 -1
- package/docs/agents/commands.md +1 -1
- package/docs/agents/context-audit-checks.md +4 -2
- package/docs/agents/context-audit.md +3 -1
- package/docs/agents/index.md +1 -1
- package/docs/agents/review-classification.md +1 -1
- package/docs/agents/routing.md +1 -1
- package/docs/agents/state-scoped-risk.md +1 -1
- package/docs/agents/teach.md +2 -0
- package/governance/rules/claude/540-architecture.md +1 -0
- package/package.json +1 -1
- package/src/audits/catalog.ts +6 -0
- package/src/autoship/paths.ts +1 -1
- package/src/commands/context.ts +22 -4
- package/src/context/architecture.ts +51 -1
- package/src/context/gate.ts +10 -4
- package/src/design/base.css +18 -18
- package/src/design/tokens.ts +26 -20
- package/src/gate/measures.ts +40 -0
- package/src/gate/stages.ts +9 -1
- package/src/teach/workspace.ts +7 -2
- package/standards/architecture.md +36 -11
- package/standards/context.md +2 -2
- package/tooling/claude/seeds/canon/ARCHITECTURE.md +12 -2
|
@@ -28,6 +28,8 @@ This skill writes canonical docs at the end of a long build and never reviews wh
|
|
|
28
28
|
- Write tracked docs at the current worktree root and the task board at the main root, since only the first commits with the branch
|
|
29
29
|
- Count every other citation before archiving a plan, comparing resolved targets rather than raw strings or bare filenames
|
|
30
30
|
- Retarget a closed task at the archived plan, so the reasoning behind finished work stays reachable
|
|
31
|
+
- Write a decision to the domain context entry it constrains unless it fills an architecture slot, since reach admits nearly every decision to an always-loaded file
|
|
32
|
+
- Merge or retire an architecture entry before adding one at the record's stated cap, and name which in the report, so the cap never turns into compressed prose or two decisions packed under one heading
|
|
31
33
|
- Anchor a decision entry this run writes or amends whose reasoning cites a measured number, re-reading the number against the tree before writing the marker
|
|
32
34
|
- Report an anchored decision whose cited path the diff touched, since the number was read before the branch moved what it counted
|
|
33
35
|
- Scan every memory-review receipt rather than the one matching this slug, since the skill that writes them runs after this one in the ship chain and a slug is unique per feature
|
|
@@ -110,6 +110,8 @@ Read `ok` and `reason` out of that record rather than the exit. An operator's sh
|
|
|
110
110
|
- Do not rewrite sections unrelated to what changed.
|
|
111
111
|
- Rewrite a restated or superseded statement in place rather than appending the replacement beside it. State the fact that stands and keep the earlier reasoning only where it is the alternative that lost, per `${CLAUDE_SKILL_DIR}/../../standards/context.md` and `${CLAUDE_SKILL_DIR}/../../standards/architecture.md`.
|
|
112
112
|
- Follow `${CLAUDE_SKILL_DIR}/../../standards/markdown.md` and the `write-human` skill for all edits.
|
|
113
|
+
- Write a session decision into the `canon/context/` entry for the domain it constrains, under that entry's `## Decisions`, by default. Touch `canon/ARCHITECTURE.md` only for a decision that fills one of the slots `${CLAUDE_SKILL_DIR}/../../standards/architecture.md` names, however many domains its reasoning reaches.
|
|
114
|
+
- Read the entry cap the record states before adding a decision to it. At the cap, merge two decisions or retire one to the domain entry it constrains, and name which in the report. Never compress a decision's prose to fit, and never pack two decisions under one heading.
|
|
113
115
|
- Close a decision entry in `canon/ARCHITECTURE.md` with its verification anchor whenever this run writes that entry or amends its reasoning and that reasoning cites a measured number. Re-read the number against the tree first, since the marker records the read rather than the edit. `${CLAUDE_SKILL_DIR}/../../standards/architecture.md` fixes the sentence.
|
|
114
116
|
- Leave every decision entry this run did not write alone, anchored or not. The rule is scoped forward, so an entry written before it is dated by blame rather than by a read. Step 5 reports a stale anchor and no step writes one on an entry it did not amend.
|
|
115
117
|
|
|
@@ -269,6 +271,10 @@ Output one line per file updated:
|
|
|
269
271
|
|
|
270
272
|
`✅ Updated: .claude/<filename>`
|
|
271
273
|
|
|
274
|
+
When Step 3 met the architecture record's cap, add one line naming what it did there:
|
|
275
|
+
|
|
276
|
+
`↪ Architecture at cap: merged <heading> into <heading>` or `↪ Architecture at cap: retired <heading> to <context entry>`
|
|
277
|
+
|
|
272
278
|
Step 10 adds its own lines when it applied or reported a finding, in the exact shape `${CLAUDE_SKILL_DIR}/references/classify.md` gives them under its own Report section. Do not shorten or paraphrase those lines here or in the reply, since the quote and the reason are what a reader checks the finding against.
|
|
273
279
|
|
|
274
280
|
If no files were updated and nothing was swept, output:
|
package/docs/agents/audits.md
CHANGED
|
@@ -36,7 +36,7 @@ Each is invoked as the CLI the caller is running rather than as a global `canon`
|
|
|
36
36
|
|
|
37
37
|
Five findings fail the run: an unresolved context citation, a banned character, word, or spelling, a relative link resolving to nothing on disk, a skill folder carrying no `REQUIREMENT.md`, and a credential-shaped value in the tree the package ships. Each is a fact with no false-positive class behind it.
|
|
38
38
|
|
|
39
|
-
Four of the five are the ones `canon gate run` already fails a push on. The secret scan is the one entry gating without a stage behind it, added on that same fact-or-judgment test rather than as a side effect of registering a measure, and the
|
|
39
|
+
Four of the five are the ones `canon gate run` already fails a push on. The secret scan is the one entry gating without a stage behind it, added on that same fact-or-judgment test rather than as a side effect of registering a measure, and the toolkit already ranks content leaving the repository above content that stays.
|
|
40
40
|
|
|
41
41
|
Everything else reports. A heavy bullet, a long entry, a board row nothing resolves, a degradation term in a comment, and an implementation reaching history ahead of its test are judgments a reader settles. A push failing on one of those teaches contributors to route around the stage, which is the split recorded across every audit here and the one this command inherits rather than moves.
|
|
42
42
|
|
package/docs/agents/commands.md
CHANGED
|
@@ -68,7 +68,7 @@ Full help: `canon <command> --help`. Behavior notes for the install and sync ver
|
|
|
68
68
|
| `canon worktrees list` | Report which worktrees are reclaimable, keyed on the pull request having merged, with every refusal and the removal route named (`--json`) |
|
|
69
69
|
| `canon worktrees reclaim` | Remove every reclaimable worktree and the branch behind it, reporting without acting under `--dry-run` (`--json`) |
|
|
70
70
|
| `canon comments scan` | Measure comment density by language and comment kind, with a trend recomputed from git |
|
|
71
|
-
| `canon context audit` | Report required sections, length, cited paths, reference form, catalog tables, provenance, superseded-decision narration, index drift, the architecture record
|
|
71
|
+
| `canon context audit` | Report required sections, length, cited paths, reference form, catalog tables, provenance, superseded-decision narration, index drift, the architecture record against its own ceiling and entry cap and its word weight, and wireframe states against their evidence folders |
|
|
72
72
|
| `canon context classify diff` | Classify the chunks a git range changed, each with its enclosing section, as keep, replace, history, or move (`--base`, `--doc-types`, `--json`) |
|
|
73
73
|
| `canon context classify sweep` | Classify every section of the five canonical doc types, split at H3, as keep, rewrite, or move (`--doc-types`, `--json`) |
|
|
74
74
|
| `canon context classifier show` | Report the resolved classifier backend and model and which source decided them (`--json`) |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Context audit checks
|
|
3
|
-
description: What each non-gating check reports, the unit each checkpoint is measured in, the architecture record's length
|
|
3
|
+
description: What each non-gating check reports, the unit each checkpoint is measured in, the architecture record's length and entry cap gates and claim coverage, which folders each check reaches, and what moved to the attribute tier
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Context audit checks
|
|
@@ -91,7 +91,9 @@ The JSON record carries the findings per entry as `entries[].narration` and the
|
|
|
91
91
|
|
|
92
92
|
## The architecture record
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
Four findings read `canon/ARCHITECTURE.md` rather than a folder, and only the first two are facts.
|
|
95
|
+
|
|
96
|
+
The entry cap check counts the record's decisions against the cap it states for itself, in a clause of the form `This record holds at most 12 decisions.` Like the length check, the cap belongs to the record rather than to the toolkit, so a record stating none is measured and never gated. A decision is a `###` heading outside a fenced block, so the template a standard shows does not count, and a heading carrying two decisions counts once. The JSON record carries what it read as `architecture.entryCap`, absent where the record states no cap, and the audit catalog counts a record past it as `recordOverCount`.
|
|
95
97
|
|
|
96
98
|
The length check compares the record against the ceiling it derives for itself, and only a record that states its own allowances has one. No standard sets a length rule for this document, so the numbers belong to whichever record declares them. The check reads a frame allowance and an allowance per decision out of the record's own prose and puts the ceiling at the frame plus the allowance times the decision count. The JSON record carries what it read as `architecture.allowances` and the reading as `architecture.lines` against `architecture.ceiling`.
|
|
97
99
|
|
|
@@ -37,7 +37,9 @@ A run where no requested name resolves refuses, whichever list it read. Naming t
|
|
|
37
37
|
|
|
38
38
|
## Exit codes
|
|
39
39
|
|
|
40
|
-
Exit codes are `0` for a clean run, `1` for a refusal, and `2` for a gating finding. An unresolved citation gates under every mode. An architecture record that states its own line allowances gates when it is past the ceiling those derive, on any run that measures it, which is every mode except `--citations-only
|
|
40
|
+
Exit codes are `0` for a clean run, `1` for a refusal, and `2` for a gating finding. An unresolved citation gates under every mode. An architecture record that states its own line allowances gates when it is past the ceiling those derive, on any run that measures it, which is every mode except `--citations-only`. A record that states an entry cap gates the same way when it holds more decisions than the cap, and a record stating neither limit is reported and never gated.
|
|
41
|
+
|
|
42
|
+
`bun run check` in the toolkit repository reads both limits through its own Architecture record stage rather than through this verb, since the verb's only gating stage there runs `--citations-only` and never opens the record.
|
|
41
43
|
|
|
42
44
|
Entry length, reference form, table, provenance, narration, the record's claim classification, and every word figure print and return `0` under every mode, because each is a judgment or a weight read alongside one, and failing a push on either would make the check something to route around. Narration is the weakest of the printed measures, since whether two bullets share a subject is a call the measure approximates from structure alone, and one of the shapes it matches is the rejected alternative the standard asks an entry to keep.
|
|
43
45
|
|
package/docs/agents/index.md
CHANGED
|
@@ -13,7 +13,7 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
|
|
|
13
13
|
- [Census](census.md): Tracked-plus-untracked file count, a breakdown by extension, and a line total that skips whatever reads as binary
|
|
14
14
|
- [Command catalog](commands.md): Every project-level command and every domain subcommand, plus the shape each domain exposes
|
|
15
15
|
- [Comments](comments.md): Comment density by language and kind, the two structural exclusions, and how the degradation sweep finds its vocabulary
|
|
16
|
-
- [Context audit checks](context-audit-checks.md): What each non-gating check reports, the unit each checkpoint is measured in, the architecture record's length
|
|
16
|
+
- [Context audit checks](context-audit-checks.md): What each non-gating check reports, the unit each checkpoint is measured in, the architecture record's length and entry cap gates and claim coverage, which folders each check reaches, and what moved to the attribute tier
|
|
17
17
|
- [Context audit](context-audit.md): Running the audit, its flags and folder scope, the exit codes, the citation gate, and the widened gate the seed stage runs
|
|
18
18
|
- [Doc classifier](context-classify.md): Classifying canonical-doc content as keep, replace/rewrite, history, or move in diff and sweep mode, the classifier setting pair, the record shape, and the exit codes
|
|
19
19
|
- [Self-stated counts](counts.md): Reading a sentence that asserts a closed catalog's size, how a match is decided, the plausibility filter that keeps a generic word from matching a subset, and why the sweep reports rather than gates
|
|
@@ -48,7 +48,7 @@ Most carry two spellings, the one a surface authors at and the one it reaches a
|
|
|
48
48
|
|
|
49
49
|
`CLAUDE.md` is matched as a whole path rather than as a prefix, because a prefix reaches nothing that sits in no folder. A nested `docs/CLAUDE.md` stays informational.
|
|
50
50
|
|
|
51
|
-
The set is data at `src/autoship/paths.ts`. That makes it permanently exempt from any later design that folds a machine-parsed list back into the surface citing it, per the machine-parsed clause
|
|
51
|
+
The set is data at `src/autoship/paths.ts`. That makes it permanently exempt from any later design that folds a machine-parsed list back into the surface citing it, per the toolkit's machine-parsed clause: a rule restating the list a parser reads is two sources for one list.
|
|
52
52
|
|
|
53
53
|
## Exit codes and refusals
|
|
54
54
|
|
package/docs/agents/routing.md
CHANGED
|
@@ -5,7 +5,7 @@ description: Reading per CLAUDE.md section how many bullets name a path, what co
|
|
|
5
5
|
|
|
6
6
|
# Routing report
|
|
7
7
|
|
|
8
|
-
`canon claude routing` reports, per `CLAUDE.md` section, how many top-level bullets name a path and how many of those a path-scoped rule already covers. It answers the firing axis of the tier test in `.claude/rules/canon/claude/592-claude-md.md`, whether a fact applies every session or fires on one path, and which until this verb existed was a judgment nothing counted.
|
|
8
|
+
`canon claude routing` reports, per `CLAUDE.md` section, how many top-level bullets name a path and how many of those a path-scoped rule already covers. It answers the firing axis of the tier test in `.claude/rules/canon/claude/592-claude-md.md`, whether a fact applies every session or fires on one path, and which until this verb existed was a judgment nothing counted. The tier test names two further axes, conditional presence and updatability, that this verb does not check.
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
11
|
canon claude routing
|
|
@@ -32,7 +32,7 @@ Three root files are read whether or not the field names them, being `package.js
|
|
|
32
32
|
|
|
33
33
|
The corpus answers what the package publishes, which is narrower than what the repository holds. Measured against this repository on 2026-08-21, the scan read 544 files and left 593 of the 1139 git lists unread. Those include everything under `.claude/`, `wiki/`, and `internal/`, the workflow definitions under `.github/`, and the trees the publish negations remove, being `scripts/sandbox/`, `scripts/eval/`, and every test file. `src/capture/` was a fourth negation when that reading was taken and is not one now. The folder holds 5 tracked files, 2 of them tests the publish still excludes, so the corpus is 3 wider than the numbers above describe and the unread count is 3 smaller.
|
|
34
34
|
|
|
35
|
-
A public repository makes that gap readable by anyone, so a clean run means no credential in the published tree rather than none in the repository. The run states the number on every pass, including a clean one, so the bound travels with the verdict. Widening the corpus to every tracked file is a separate decision, since the row this implements puts the shipped tree first on the
|
|
35
|
+
A public repository makes that gap readable by anyone, so a clean run means no credential in the published tree rather than none in the repository. The run states the number on every pass, including a clean one, so the bound travels with the verdict. Widening the corpus to every tracked file is a separate decision, since the row this implements puts the shipped tree first on the toolkit's rule that content leaving the repository gates harder than content that stays.
|
|
36
36
|
|
|
37
37
|
### What it keys on
|
|
38
38
|
|
package/docs/agents/teach.md
CHANGED
|
@@ -7,6 +7,8 @@ description: Listing learning workspaces with what their records schedule next,
|
|
|
7
7
|
|
|
8
8
|
Learning workspaces sit under `.canon/teach/<nn>-<topic>/`, and `standards/teach.md` fixes their layout, naming, and file formats, apart from the glossary, whose shape `standards/glossary.md` fixes, cited from the `teach-workspace` skill, so it travels with the file wherever a promotion lands it. Every verb here resolves that folder against the main worktree root rather than against the working directory, so a session standing in a linked worktree reaches the one workspace the learner has rather than opening a second.
|
|
9
9
|
|
|
10
|
+
Passing `--root` a path already named `teach` reaches that folder directly rather than wrapping it in the `.canon/`-or-`.claude/` record-root lookup, which is what lets a workspace committed outside the record root, such as a worked example kept in version control on purpose, take every verb below the same as one under `.canon/teach/`.
|
|
11
|
+
|
|
10
12
|
That root resolution is also why the writing verbs exist at all. The file-editing tools refuse a main-root path from a linked worktree and offer a worktree copy instead, and a caller naming only the destination reports a success that did not happen. A whole-file create still goes out as a shell heredoc. Changing a line inside a file that already exists has no shell route, because the stream editors are banned, so `resource` and `glossary` are the route for the two files a running workspace edits.
|
|
11
13
|
|
|
12
14
|
## List
|
|
@@ -10,3 +10,4 @@ paths:
|
|
|
10
10
|
## Authority
|
|
11
11
|
|
|
12
12
|
- Follow the architecture standard for the overview, named decision entries, and risks. It is the single source. Read it with `canon standards architecture`.
|
|
13
|
+
- Add a decision here only when it fills one of the standard's slots, and write any other decision into the context entry for the domain it constrains. At the record's stated cap, merge or retire an entry before adding one.
|
package/package.json
CHANGED
package/src/audits/catalog.ts
CHANGED
|
@@ -168,6 +168,8 @@ function architectureCounts(
|
|
|
168
168
|
// zero there would read as one measured and found conforming.
|
|
169
169
|
const ceiling =
|
|
170
170
|
typeof record.ceiling === 'number' ? record.ceiling : undefined
|
|
171
|
+
const entryCap =
|
|
172
|
+
typeof record.entryCap === 'number' ? record.entryCap : undefined
|
|
171
173
|
|
|
172
174
|
let unverifiable = 0
|
|
173
175
|
let unchecked = 0
|
|
@@ -188,6 +190,10 @@ function architectureCounts(
|
|
|
188
190
|
...(ceiling !== undefined && {
|
|
189
191
|
recordOverLength: record.lines > ceiling ? 1 : 0,
|
|
190
192
|
}),
|
|
193
|
+
// Absent on a record stating no entry cap, for the same reason.
|
|
194
|
+
...(entryCap !== undefined && {
|
|
195
|
+
recordOverCount: decisions.length > entryCap ? 1 : 0,
|
|
196
|
+
}),
|
|
191
197
|
recordUnverifiable: unverifiable,
|
|
192
198
|
recordUnchecked: unchecked,
|
|
193
199
|
}
|
package/src/autoship/paths.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* application three times, so the set moved here and the body now calls a verb
|
|
8
8
|
* that reads it. Being machine-parsed makes it permanently exempt from any
|
|
9
9
|
* later design that folds a standard back into the surface citing it, per the
|
|
10
|
-
* machine-parsed clause in `canon/
|
|
10
|
+
* machine-parsed clause in `canon/context/standards/resolution.md`.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
/**
|
package/src/commands/context.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
architectureRel,
|
|
21
21
|
type ArchitectureReport,
|
|
22
22
|
coveredCount,
|
|
23
|
+
isOverCount,
|
|
23
24
|
isOverLength,
|
|
24
25
|
measureArchitecture,
|
|
25
26
|
testableCount,
|
|
@@ -98,7 +99,7 @@ export function register(program: Command): void {
|
|
|
98
99
|
context
|
|
99
100
|
.command('audit')
|
|
100
101
|
.description(
|
|
101
|
-
'Report required sections, entry length, citations, reference form, catalog tables, provenance, superseded-decision narration, index drift, the architecture record against its own ceiling and its word weight, and wireframe states against their evidence folders',
|
|
102
|
+
'Report required sections, entry length, citations, reference form, catalog tables, provenance, superseded-decision narration, index drift, the architecture record against its own ceiling, its own entry cap, and its word weight, and wireframe states against their evidence folders',
|
|
102
103
|
)
|
|
103
104
|
.argument('[path]', 'Project root, defaulting to the current directory')
|
|
104
105
|
.helpOption('-h, --help', 'Show this help message')
|
|
@@ -124,9 +125,11 @@ export function register(program: Command): void {
|
|
|
124
125
|
'An unresolved citation always gates. An architecture record that',
|
|
125
126
|
'states its own line allowances gates when it is past the ceiling',
|
|
126
127
|
'those derive, on any run except --citations-only, which never',
|
|
127
|
-
'measures it. A record stating
|
|
128
|
-
'
|
|
129
|
-
'
|
|
128
|
+
'measures it. A record stating an entry cap gates the same way when',
|
|
129
|
+
'it holds more decisions than the cap. A record stating neither is',
|
|
130
|
+
'reported and never gated. --gate widens the gate to the other two',
|
|
131
|
+
'findings that are facts rather than judgments: a missing required',
|
|
132
|
+
'section and index',
|
|
130
133
|
'drift. Entry length, reference form, table, provenance, narration,',
|
|
131
134
|
'and the record claim classification are judgments under both.',
|
|
132
135
|
'',
|
|
@@ -759,6 +762,7 @@ async function runAudit(
|
|
|
759
762
|
const gating = isGating({
|
|
760
763
|
unresolvedCitations: citations.unresolved.length,
|
|
761
764
|
recordOverLength: record !== undefined && isOverLength(record),
|
|
765
|
+
recordOverCount: record !== undefined && isOverCount(record),
|
|
762
766
|
sections,
|
|
763
767
|
drift,
|
|
764
768
|
wireframes,
|
|
@@ -1256,6 +1260,20 @@ function reportRecord(
|
|
|
1256
1260
|
)
|
|
1257
1261
|
}
|
|
1258
1262
|
|
|
1263
|
+
if (report.entryCap === undefined) {
|
|
1264
|
+
logInfo(
|
|
1265
|
+
`${plural(decisions, 'decision')} and no entry cap stated, so the count is reported and never gated.`,
|
|
1266
|
+
)
|
|
1267
|
+
} else if (isOverCount(report)) {
|
|
1268
|
+
logError(
|
|
1269
|
+
`${plural(decisions, 'decision')} against a cap of ${report.entryCap}. Merge two or retire one, never compress.`,
|
|
1270
|
+
)
|
|
1271
|
+
} else {
|
|
1272
|
+
logInfo(
|
|
1273
|
+
`${plural(decisions, 'decision')} against a cap of ${report.entryCap}.`,
|
|
1274
|
+
)
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1259
1277
|
if (decisions === 0) {
|
|
1260
1278
|
logWarn('The record declares no decision, so nothing was classified.')
|
|
1261
1279
|
return
|
|
@@ -80,6 +80,12 @@ export interface ArchitectureReport {
|
|
|
80
80
|
readonly allowances?: Allowances
|
|
81
81
|
/** The frame plus the per-decision allowance, absent alongside it. */
|
|
82
82
|
readonly ceiling?: number
|
|
83
|
+
/**
|
|
84
|
+
* The most decisions the record says it holds, absent when it states no
|
|
85
|
+
* cap. Declared by the record for the reason the allowances are, so a
|
|
86
|
+
* project that never adopted a cap is measured and never gated.
|
|
87
|
+
*/
|
|
88
|
+
readonly entryCap?: number
|
|
83
89
|
readonly decisions: readonly DecisionReport[]
|
|
84
90
|
}
|
|
85
91
|
|
|
@@ -314,14 +320,32 @@ const SPELLED: Record<string, number> = {
|
|
|
314
320
|
eight: 8,
|
|
315
321
|
nine: 9,
|
|
316
322
|
ten: 10,
|
|
323
|
+
eleven: 11,
|
|
324
|
+
twelve: 12,
|
|
325
|
+
thirteen: 13,
|
|
326
|
+
fourteen: 14,
|
|
327
|
+
fifteen: 15,
|
|
328
|
+
sixteen: 16,
|
|
329
|
+
seventeen: 17,
|
|
330
|
+
eighteen: 18,
|
|
331
|
+
nineteen: 19,
|
|
332
|
+
twenty: 20,
|
|
317
333
|
}
|
|
318
334
|
|
|
319
|
-
|
|
335
|
+
// Longest first, so `fourteen` is tried ahead of the `four` it starts with.
|
|
336
|
+
const SPELLED_ALTERNATION = Object.keys(SPELLED)
|
|
337
|
+
.sort((a, b) => b.length - a.length)
|
|
338
|
+
.join('|')
|
|
339
|
+
const CARDINAL = String.raw`(\d+|${SPELLED_ALTERNATION})`
|
|
320
340
|
const FRAME_CLAUSE = new RegExp(String.raw`${CARDINAL}-line frame`, 'i')
|
|
321
341
|
const PER_DECISION_CLAUSE = new RegExp(
|
|
322
342
|
String.raw`${CARDINAL}\s+lines?\s+a\s+decision`,
|
|
323
343
|
'i',
|
|
324
344
|
)
|
|
345
|
+
const ENTRY_CAP_CLAUSE = new RegExp(
|
|
346
|
+
String.raw`\bat\s+most\s+${CARDINAL}\s+decisions?\b`,
|
|
347
|
+
'i',
|
|
348
|
+
)
|
|
325
349
|
|
|
326
350
|
function readCardinal(token: string | undefined): number | undefined {
|
|
327
351
|
if (token === undefined) return undefined
|
|
@@ -348,6 +372,17 @@ export function readAllowances(source: string): Allowances | undefined {
|
|
|
348
372
|
return { frame, perDecision }
|
|
349
373
|
}
|
|
350
374
|
|
|
375
|
+
/**
|
|
376
|
+
* Reads the entry cap a record declares for itself, or nothing.
|
|
377
|
+
*
|
|
378
|
+
* The clause is the record's own, like the allowances above, so a record
|
|
379
|
+
* whose wording drifts past it falls back to reporting rather than to a cap
|
|
380
|
+
* held here that no project wrote.
|
|
381
|
+
*/
|
|
382
|
+
export function readEntryCap(source: string): number | undefined {
|
|
383
|
+
return readCardinal(source.match(ENTRY_CAP_CLAUSE)?.[1])
|
|
384
|
+
}
|
|
385
|
+
|
|
351
386
|
/** Whether a read failed because nothing sits at the path. */
|
|
352
387
|
function isMissing(error: unknown): boolean {
|
|
353
388
|
const code = (error as { code?: unknown }).code
|
|
@@ -385,6 +420,7 @@ export async function measureArchitecture(
|
|
|
385
420
|
}
|
|
386
421
|
|
|
387
422
|
const allowances = readAllowances(source)
|
|
423
|
+
const entryCap = readEntryCap(source)
|
|
388
424
|
const raw = splitDecisions(source)
|
|
389
425
|
const decisions = await Promise.all(
|
|
390
426
|
raw.map(async (entry) => {
|
|
@@ -416,10 +452,24 @@ export async function measureArchitecture(
|
|
|
416
452
|
allowances,
|
|
417
453
|
ceiling: ceilingFor(allowances, raw.length),
|
|
418
454
|
}),
|
|
455
|
+
...(entryCap !== undefined && { entryCap }),
|
|
419
456
|
decisions,
|
|
420
457
|
}
|
|
421
458
|
}
|
|
422
459
|
|
|
460
|
+
/**
|
|
461
|
+
* Whether the record holds more decisions than the cap it states.
|
|
462
|
+
*
|
|
463
|
+
* False for a record stating no cap, for the reason `isOverLength` gives. The
|
|
464
|
+
* count is by heading, so a heading carrying several decisions counts once,
|
|
465
|
+
* which is the undercount `splitDecisions` already names.
|
|
466
|
+
*/
|
|
467
|
+
export function isOverCount(report: ArchitectureReport): boolean {
|
|
468
|
+
return (
|
|
469
|
+
report.entryCap !== undefined && report.decisions.length > report.entryCap
|
|
470
|
+
)
|
|
471
|
+
}
|
|
472
|
+
|
|
423
473
|
/**
|
|
424
474
|
* Whether the record is longer than the ceiling it derives for itself.
|
|
425
475
|
*
|
package/src/context/gate.ts
CHANGED
|
@@ -14,6 +14,11 @@ export interface GateInput {
|
|
|
14
14
|
* construction, so widening it here would gate on a reading it never took.
|
|
15
15
|
*/
|
|
16
16
|
readonly recordOverLength: boolean
|
|
17
|
+
/**
|
|
18
|
+
* Whether the record holds more decisions than the cap it states, which
|
|
19
|
+
* gates under the same two modes and is false in the same two cases.
|
|
20
|
+
*/
|
|
21
|
+
readonly recordOverCount: boolean
|
|
17
22
|
readonly sections: readonly SectionFinding[]
|
|
18
23
|
readonly drift: readonly FolderDrift[]
|
|
19
24
|
/**
|
|
@@ -68,9 +73,9 @@ export function hasSketchWithEvidence(
|
|
|
68
73
|
* Whether the audit found something that should fail the caller.
|
|
69
74
|
*
|
|
70
75
|
* An unresolved citation is a broken pointer and gates unconditionally, and so
|
|
71
|
-
* does a record past its own ceiling: the record states
|
|
72
|
-
*
|
|
73
|
-
*
|
|
76
|
+
* does a record past its own ceiling or its own entry cap: the record states
|
|
77
|
+
* each limit for itself, which makes those the measures here that are facts
|
|
78
|
+
* rather than thresholds a reader weighs. The findings `--gate` adds are
|
|
74
79
|
* the ones answerable from the file itself: a required section it does not
|
|
75
80
|
* declare, an index disagreeing with its folder, and a wireframe's States
|
|
76
81
|
* table disagreeing with its evidence folders. Entry length, depth, bullet,
|
|
@@ -82,13 +87,14 @@ export function hasSketchWithEvidence(
|
|
|
82
87
|
export function isGating({
|
|
83
88
|
unresolvedCitations,
|
|
84
89
|
recordOverLength,
|
|
90
|
+
recordOverCount,
|
|
85
91
|
sections,
|
|
86
92
|
drift,
|
|
87
93
|
wireframes,
|
|
88
94
|
widened,
|
|
89
95
|
}: GateInput): boolean {
|
|
90
96
|
if (unresolvedCitations > 0) return true
|
|
91
|
-
if (recordOverLength) return true
|
|
97
|
+
if (recordOverLength || recordOverCount) return true
|
|
92
98
|
if (!widened) return false
|
|
93
99
|
|
|
94
100
|
return sections.length > 0 || hasDrift(drift) || hasStatesMismatch(wireframes)
|
package/src/design/base.css
CHANGED
|
@@ -4,26 +4,26 @@
|
|
|
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: #0f0e0c;
|
|
8
|
+
--color-surface: #151412;
|
|
9
|
+
--color-chrome: #1b1a18;
|
|
10
|
+
--color-border: #2a2926;
|
|
11
|
+
--color-text: #d9d7d4;
|
|
12
|
+
--color-text-body: #aeada9;
|
|
13
|
+
--color-text-secondary: #8c8b86;
|
|
14
|
+
--color-muted: #7f7f7c;
|
|
15
|
+
--color-accent: #c76b5f;
|
|
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: #fbfaf8;
|
|
18
|
+
--color-light-surface: #f1f1ee;
|
|
19
|
+
--color-light-chrome: #e9e8e5;
|
|
20
|
+
--color-light-text: #2c2c29;
|
|
21
|
+
--color-light-text-body: #4b4947;
|
|
22
|
+
--color-light-text-secondary: #666561;
|
|
23
|
+
--color-light-muted: #6e6d6c;
|
|
24
|
+
--color-light-accent: #ad4a4b;
|
|
25
25
|
--color-light-success: #2d6b22;
|
|
26
|
-
--color-light-border: #
|
|
26
|
+
--color-light-border: #d5d4d1;
|
|
27
27
|
--space-xs: 6px;
|
|
28
28
|
--space-sm: 12px;
|
|
29
29
|
--space-md: 18px;
|
package/src/design/tokens.ts
CHANGED
|
@@ -94,7 +94,7 @@ export const TOKENS: DesignTokens = {
|
|
|
94
94
|
'',
|
|
95
95
|
'The two rendered captures read it as well. `scripts/core/regen-hero.sh` fills `assets/captures/hero.html.tmpl` and `assets/captures/install.html.tmpl` with what `canon design css --no-components` emits, so both frames now carry the custom properties rather than their own copies of the hex, and a value moved here moves what the next capture renders.',
|
|
96
96
|
'',
|
|
97
|
-
'The terminal framing is the one surface left holding its own values, and that is a decision rather than a gap. `scripts/lib/ui.sh` and `src/ui.ts` each spell six escape constants, and `canon/
|
|
97
|
+
'The terminal framing is the one surface left holding its own values, and that is a decision rather than a gap. `scripts/lib/ui.sh` and `src/ui.ts` each spell six escape constants, and `canon/context/scripts/framing.md` records one color source per language with a check behind each, so generating a third spelling from here would break the rule those two checks enforce. What the record is still incomplete about is the other half of those six: `WHITE` and `GREY` name no role below, so the terminal palette is described here in part rather than in whole.',
|
|
98
98
|
].join('\n'),
|
|
99
99
|
|
|
100
100
|
personality: [
|
|
@@ -108,58 +108,64 @@ export const TOKENS: DesignTokens = {
|
|
|
108
108
|
'',
|
|
109
109
|
'Warning and error hold ANSI codes because that is what `scripts/lib/ui.sh` writes and no rendered surface implements an equivalent. Giving either a hex value would invent a mapping no file has, so they carry no contrast reading either.',
|
|
110
110
|
'',
|
|
111
|
-
'Success is the one of the three that does have a rendered equivalent, which is why it carries a hex. `assets/captures/install.html` marks every confirmed step with it, and the shell writes `ANSI 32` for the same role, so the two are one role in two registers rather than one value in two spellings. The hex is what the rendered surface picked and no reading claims the terminal renders that value. It declares `background` alone as its ground, since that is the only role it is drawn on, where every other dark text role is drawn on both.',
|
|
111
|
+
'Success is the one of the three that does have a rendered equivalent, which is why it carries a hex. `assets/captures/install.html` marks every confirmed step with it, and the shell writes `ANSI 32` for the same role, so the two are one role in two registers rather than one value in two spellings. The hex is what the rendered surface picked and no reading claims the terminal renders that value. It declares `background` alone as its ground, since that is the only role it is drawn on, where every other dark text role is drawn on both. It is the one role below that is not derived.',
|
|
112
|
+
'',
|
|
113
|
+
"Every other role is derived rather than picked, solved in OKLCH by binary search for the lightness that hits a target contrast against its ground, using this module's own anchors. Six anchors are the whole system: ground lightness 0.985 light and 0.165 dark, neutral chroma 0.003 light and 0.004 dark, neutral hue 90 both, accent hue 22 light and 28 dark, accent chroma 0.13 light and 0.12 dark for a mark and 0.095 light and 0.09 dark for a fill. Only the mark step is rendered below, in `accent`. That token already stands in for a fill in practice: `assets/captures/hero.html`'s `.cmd` rule paints its whole background with `var(--color-accent)` and sets text on top, which is a fill use rather than a mark one. The fill chroma step has no token of its own yet, so a lower-saturation `accent-fill` would recolor that button rather than introduce a new consumer. Targets, which are inputs rather than results: text 13.5, body 8.6, secondary 5.6, muted 4.6, accent 5.2. Dark is its own anchor set rather than an inversion of light, and every arm measured in the groundwork behind this landed on identical neutral ratios, which is what made the accent choice a question about hue alone.",
|
|
114
|
+
'',
|
|
115
|
+
'`muted` and `light-muted` solve against `surface` rather than `background`. Both declare two grounds and the groundwork solved its target against one, so the recorded hex cleared 4.6 against `background` and read 4.40 dark, 4.24 light against `surface`, the tighter of the two since surface sits a step closer to its text color. The values here are re-solved for the same 4.6 target read against `surface` instead, which clears both: 4.58 dark, 4.56 light against `surface`, and 4.80 dark, 4.95 light against `background`.',
|
|
116
|
+
'',
|
|
117
|
+
'The accent is a quiet red at hue 22 light, 28 dark, chosen over a vivid red that read as an error state against a page reporting success, over indigo which carries less distinctiveness at hue 280 in tooling already dominated by that hue, and over rust at hue 42, a larger temperature shift than the problem required. The debt this accepts: `error` and `warning` now share a register with the most repeated element on every rendered surface, so both need differentiating by lightness or by an icon rather than by hue, and that work is unscheduled.',
|
|
112
118
|
].join('\n'),
|
|
113
119
|
|
|
114
120
|
color: [
|
|
115
121
|
{
|
|
116
122
|
role: 'background',
|
|
117
123
|
intent: 'page canvas',
|
|
118
|
-
value: '#
|
|
124
|
+
value: '#0f0e0c',
|
|
119
125
|
},
|
|
120
126
|
{
|
|
121
127
|
role: 'surface',
|
|
122
128
|
intent: 'cards, panels, raised blocks',
|
|
123
|
-
value: '#
|
|
129
|
+
value: '#151412',
|
|
124
130
|
},
|
|
125
131
|
{
|
|
126
132
|
role: 'chrome',
|
|
127
133
|
intent: 'the window titlebar, one step above the canvas',
|
|
128
|
-
value: '#
|
|
134
|
+
value: '#1b1a18',
|
|
129
135
|
},
|
|
130
136
|
{
|
|
131
137
|
role: 'border',
|
|
132
138
|
intent: 'every rule and panel edge',
|
|
133
|
-
value: '#
|
|
139
|
+
value: '#2a2926',
|
|
134
140
|
},
|
|
135
141
|
{
|
|
136
142
|
role: 'text',
|
|
137
143
|
intent: 'headings, counts, emphasized runs',
|
|
138
|
-
value: '#
|
|
144
|
+
value: '#d9d7d4',
|
|
139
145
|
grounds: DARK_GROUNDS,
|
|
140
146
|
},
|
|
141
147
|
{
|
|
142
148
|
role: 'text-body',
|
|
143
149
|
intent: 'default body copy',
|
|
144
|
-
value: '#
|
|
150
|
+
value: '#aeada9',
|
|
145
151
|
grounds: DARK_GROUNDS,
|
|
146
152
|
},
|
|
147
153
|
{
|
|
148
154
|
role: 'text-secondary',
|
|
149
155
|
intent: 'labels, captions, supporting copy',
|
|
150
|
-
value: '#
|
|
156
|
+
value: '#8c8b86',
|
|
151
157
|
grounds: DARK_GROUNDS,
|
|
152
158
|
},
|
|
153
159
|
{
|
|
154
160
|
role: 'muted',
|
|
155
161
|
intent: 'the faintest step, trailing notes',
|
|
156
|
-
value: '#
|
|
162
|
+
value: '#7f7f7c',
|
|
157
163
|
grounds: DARK_GROUNDS,
|
|
158
164
|
},
|
|
159
165
|
{
|
|
160
166
|
role: 'accent',
|
|
161
167
|
intent: 'install command, mark, primary action',
|
|
162
|
-
value: '#
|
|
168
|
+
value: '#c76b5f',
|
|
163
169
|
grounds: DARK_GROUNDS,
|
|
164
170
|
},
|
|
165
171
|
{
|
|
@@ -173,46 +179,46 @@ export const TOKENS: DesignTokens = {
|
|
|
173
179
|
{
|
|
174
180
|
role: 'light-background',
|
|
175
181
|
intent: 'page canvas on a light ground',
|
|
176
|
-
value: '#
|
|
182
|
+
value: '#fbfaf8',
|
|
177
183
|
},
|
|
178
184
|
{
|
|
179
185
|
role: 'light-surface',
|
|
180
186
|
intent: 'cards and panels on a light ground',
|
|
181
|
-
value: '#
|
|
187
|
+
value: '#f1f1ee',
|
|
182
188
|
},
|
|
183
189
|
{
|
|
184
190
|
role: 'light-chrome',
|
|
185
191
|
intent: 'the window titlebar, one step above the canvas',
|
|
186
|
-
value: '#
|
|
192
|
+
value: '#e9e8e5',
|
|
187
193
|
},
|
|
188
194
|
{
|
|
189
195
|
role: 'light-text',
|
|
190
196
|
intent: 'primary text on a light ground',
|
|
191
|
-
value: '#
|
|
197
|
+
value: '#2c2c29',
|
|
192
198
|
grounds: LIGHT_GROUNDS,
|
|
193
199
|
},
|
|
194
200
|
{
|
|
195
201
|
role: 'light-text-body',
|
|
196
202
|
intent: 'default body copy on a light ground',
|
|
197
|
-
value: '#
|
|
203
|
+
value: '#4b4947',
|
|
198
204
|
grounds: LIGHT_GROUNDS,
|
|
199
205
|
},
|
|
200
206
|
{
|
|
201
207
|
role: 'light-text-secondary',
|
|
202
208
|
intent: 'labels, captions, supporting copy on a light ground',
|
|
203
|
-
value: '#
|
|
209
|
+
value: '#666561',
|
|
204
210
|
grounds: LIGHT_GROUNDS,
|
|
205
211
|
},
|
|
206
212
|
{
|
|
207
213
|
role: 'light-muted',
|
|
208
214
|
intent: 'secondary text on a light ground',
|
|
209
|
-
value: '#
|
|
215
|
+
value: '#6e6d6c',
|
|
210
216
|
grounds: LIGHT_GROUNDS,
|
|
211
217
|
},
|
|
212
218
|
{
|
|
213
219
|
role: 'light-accent',
|
|
214
220
|
intent: 'links and primary action on light',
|
|
215
|
-
value: '#
|
|
221
|
+
value: '#ad4a4b',
|
|
216
222
|
grounds: LIGHT_GROUNDS,
|
|
217
223
|
},
|
|
218
224
|
{
|
|
@@ -224,7 +230,7 @@ export const TOKENS: DesignTokens = {
|
|
|
224
230
|
{
|
|
225
231
|
role: 'light-border',
|
|
226
232
|
intent: 'rules and panel edges on light',
|
|
227
|
-
value: '#
|
|
233
|
+
value: '#d5d4d1',
|
|
228
234
|
verify: true,
|
|
229
235
|
},
|
|
230
236
|
],
|
package/src/gate/measures.ts
CHANGED
|
@@ -7,6 +7,11 @@ import {
|
|
|
7
7
|
type ClientCommand,
|
|
8
8
|
clientCommandCitationsIn,
|
|
9
9
|
} from '@/client-commands'
|
|
10
|
+
import {
|
|
11
|
+
isOverCount,
|
|
12
|
+
isOverLength,
|
|
13
|
+
measureArchitecture,
|
|
14
|
+
} from '@/context/architecture'
|
|
10
15
|
import { listRepositoryFiles } from '@/git-files'
|
|
11
16
|
import {
|
|
12
17
|
isShippedCorpus,
|
|
@@ -222,6 +227,41 @@ function citedPaths(record: { paths?: unknown } | undefined): string[] {
|
|
|
222
227
|
})
|
|
223
228
|
}
|
|
224
229
|
|
|
230
|
+
/**
|
|
231
|
+
* The architecture record against the two limits it states for itself: the
|
|
232
|
+
* line ceiling its allowances derive and the entry cap.
|
|
233
|
+
*
|
|
234
|
+
* Read in-process rather than through `context audit`, whose one gating stage
|
|
235
|
+
* here runs `--citations-only` and never opens the record, which is why the
|
|
236
|
+
* line ceiling went unenforced by `bun run check` until this stage. A project
|
|
237
|
+
* carrying no record, or a record stating neither limit, passes, since both
|
|
238
|
+
* limits belong to the record rather than to the toolkit.
|
|
239
|
+
*/
|
|
240
|
+
export const architectureRecord: Measure = async (ctx) => {
|
|
241
|
+
const report = await measureArchitecture(ctx.root)
|
|
242
|
+
if (report === undefined) {
|
|
243
|
+
return { emissions: [info('No architecture record to measure')] }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const decisions = report.decisions.length
|
|
247
|
+
const failures = [
|
|
248
|
+
isOverCount(report) &&
|
|
249
|
+
`${decisions} decisions against a cap of ${report.entryCap}. Merge two or retire one in ${report.rel}, never compress.`,
|
|
250
|
+
isOverLength(report) &&
|
|
251
|
+
`${report.lines} lines against a ceiling of ${report.ceiling} in ${report.rel}.`,
|
|
252
|
+
].filter((failure): failure is string => typeof failure === 'string')
|
|
253
|
+
|
|
254
|
+
if (failures.length > 0) return { emissions: [], failure: failures.join(' ') }
|
|
255
|
+
|
|
256
|
+
const cap =
|
|
257
|
+
report.entryCap === undefined
|
|
258
|
+
? 'no entry cap stated'
|
|
259
|
+
: `a cap of ${report.entryCap}`
|
|
260
|
+
return {
|
|
261
|
+
emissions: [info(`${decisions} decisions against ${cap}`)],
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
225
265
|
/**
|
|
226
266
|
* A second run of the records move should rewrite nothing, and the count is
|
|
227
267
|
* only knowable once the folders themselves have landed.
|
package/src/gate/stages.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
architectureRecord,
|
|
2
3
|
auditSet,
|
|
3
4
|
captureStamps,
|
|
4
5
|
clientCommandCitations,
|
|
@@ -326,7 +327,7 @@ export const STAGES: readonly Stage[] = [
|
|
|
326
327
|
{
|
|
327
328
|
// A hook, a workflow, or a husky script reaching one side of the seed or
|
|
328
329
|
// config boundary and not the other is a capability withheld with no
|
|
329
|
-
// recorded reason, per the criterion in
|
|
330
|
+
// recorded reason, per the criterion in canon/context/tooling.md.
|
|
330
331
|
id: 'capability-seeding',
|
|
331
332
|
label: 'Capability seeding',
|
|
332
333
|
checks: [
|
|
@@ -362,6 +363,13 @@ export const STAGES: readonly Stage[] = [
|
|
|
362
363
|
],
|
|
363
364
|
success: 'Context citations resolve',
|
|
364
365
|
},
|
|
366
|
+
{
|
|
367
|
+
// The record's two limits are facts it states about itself, unlike the
|
|
368
|
+
// judgment thresholds the stage above leaves out, so they gate here.
|
|
369
|
+
id: 'architecture-record',
|
|
370
|
+
label: 'Architecture record',
|
|
371
|
+
checks: [{ kind: 'measure', measure: architectureRecord }],
|
|
372
|
+
},
|
|
365
373
|
{
|
|
366
374
|
// A rule citing a file that moved fails silently. The consumed-copy drift
|
|
367
375
|
// stage passes an authored rule and its copy that are wrong together, and
|
package/src/teach/workspace.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
2
|
import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'
|
|
3
|
-
import { join, relative } from 'node:path'
|
|
3
|
+
import { basename, join, relative } from 'node:path'
|
|
4
4
|
import { TEACH_STYLESHEET_COMPONENTS } from '@/design/components'
|
|
5
5
|
import { buildDesignCss } from '@/design/css'
|
|
6
6
|
import { parseFrontmatter, readField } from '@/indexes/frontmatter'
|
|
@@ -230,9 +230,14 @@ export function refuse(
|
|
|
230
230
|
* Every workspace sits under the main worktree root rather than under the
|
|
231
231
|
* checkout the caller stands in. Resolving that root belongs to the caller, so
|
|
232
232
|
* this takes one and never reads the working directory.
|
|
233
|
+
*
|
|
234
|
+
* A root already named `teach` is taken as the teach folder itself rather than
|
|
235
|
+
* a project root to wrap, which is what lets a caller point straight at a
|
|
236
|
+
* folder such as `examples/teach` that holds workspaces outside any `.canon/`
|
|
237
|
+
* or `.claude/` record root.
|
|
233
238
|
*/
|
|
234
239
|
export function teachDir(root: string): string {
|
|
235
|
-
return recordDir(root, 'teach')
|
|
240
|
+
return basename(root) === 'teach' ? root : recordDir(root, 'teach')
|
|
236
241
|
}
|
|
237
242
|
|
|
238
243
|
async function listSlugs(dir: string): Promise<string[]> {
|
|
@@ -5,7 +5,7 @@ description: Shape and content rules for canon/ARCHITECTURE.md
|
|
|
5
5
|
|
|
6
6
|
# Architecture reference
|
|
7
7
|
|
|
8
|
-
Applies to `canon/ARCHITECTURE.md`. Describes the system shape and the decisions behind it, not a tutorial, setup guide, or implementation walkthrough. Pair it with `CLAUDE.md`:
|
|
8
|
+
Applies to `canon/ARCHITECTURE.md`. Describes the system shape and the decisions behind it, not a tutorial, setup guide, or implementation walkthrough. Pair it with `CLAUDE.md`: session behavior lives there, and the system's shape lives here. Update when a decision is made or a risk is resolved.
|
|
9
9
|
|
|
10
10
|
## Scope
|
|
11
11
|
|
|
@@ -20,15 +20,26 @@ Does not govern:
|
|
|
20
20
|
## What goes in
|
|
21
21
|
|
|
22
22
|
- A high-level overview of how the system is structured and why
|
|
23
|
-
- Key technical decisions as named H3 entries: what was chosen and why over the alternatives
|
|
23
|
+
- Key technical decisions as named H3 entries, each filling one of the five slots below: what was chosen and why over the alternatives
|
|
24
24
|
- Risks and open questions still unresolved
|
|
25
25
|
|
|
26
|
+
A decision belongs here only when it fills a slot:
|
|
27
|
+
|
|
28
|
+
- Stack and runtime: the language, the runtime, and the major libraries the system is built on
|
|
29
|
+
- Delivery: how the system's content or code reaches the place it runs
|
|
30
|
+
- Enforced boundaries: a boundary the system checks mechanically, and what enforces it
|
|
31
|
+
- Layout: how the project's top-level roots and context tiers divide what they hold
|
|
32
|
+
- Build principles: a rule for how a behavior is built that changes work in more than one domain, stated in one sentence
|
|
33
|
+
|
|
34
|
+
The slots come from a scale test. Ask whether the decision would still be in this file if the project were ten times its size. At that size an always-loaded file holding one entry per decision fits no context window, and what survives is the shape: the stack, the delivery, the boundaries, the layout, and the principles. A decision that fits no slot is a domain decision, and its home is that domain's `canon/context/<domain>.md` entry under `## Decisions`, however many domains its reasoning touches. Reach is not the test, since nearly every decision reaches a second domain.
|
|
35
|
+
|
|
26
36
|
## What does not go in
|
|
27
37
|
|
|
28
38
|
- How individual functions work line by line. The code carries its own behavior.
|
|
29
39
|
- Full type definitions. They live in code. Reference the shape conceptually if needed.
|
|
30
|
-
- A
|
|
31
|
-
- A
|
|
40
|
+
- A decision that fills no slot. It lives in the domain context entry it constrains, and this file carries at most one line pointing at it.
|
|
41
|
+
- A measurement paragraph specific to one domain's own mechanism, even behind a slot decision. Route it to that domain's context entry and keep the choice, the alternative that lost, and one reason here.
|
|
42
|
+
- The instances of a build principle. The principle takes one sentence here, and each instance lives in the domain entry where it applies.
|
|
32
43
|
- 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.
|
|
33
44
|
|
|
34
45
|
## Sections
|
|
@@ -38,7 +49,7 @@ Use `## Overview`, `## Key technical decisions` with one named H3 per decision,
|
|
|
38
49
|
## Keeping it current
|
|
39
50
|
|
|
40
51
|
- 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
|
|
52
|
+
- 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 fills a slot and moving to the domain context entry it constrains when it does not.
|
|
42
53
|
|
|
43
54
|
## Verification anchors
|
|
44
55
|
|
|
@@ -54,27 +65,41 @@ A decision's reasoning stays correct while the numbers it cites move. The anchor
|
|
|
54
65
|
- Do not edit a claim in the pass that first anchors it. The anchor states what the claim was measured against, so changing both at once leaves nothing to check the anchor against.
|
|
55
66
|
- Refresh the anchor whenever the number is re-read, whether or not it moved. A confirmed number and an unread one are the same text without the date.
|
|
56
67
|
|
|
57
|
-
##
|
|
68
|
+
## Entry cap
|
|
58
69
|
|
|
59
|
-
Every session pays for this file before any work starts, so a heavy read is a real cost.
|
|
70
|
+
Every session pays for this file before any work starts, so a heavy read is a real cost. A file that reads heavy is carrying too many decisions, not decisions written too long, so the bound is a count of decisions rather than of words.
|
|
60
71
|
|
|
61
|
-
-
|
|
72
|
+
- State the cap in the record itself as a clause of the form `This record holds at most <n> decisions.` The cap is the record's own, so a checker reads it from the file, and a record stating none is measured and never gated.
|
|
73
|
+
- Set the cap near one decision per slot plus a small margin.
|
|
74
|
+
- At the cap, merge two decisions or retire one before adding another, and name which in the change that does it. Never compress a decision's prose to fit, and never pack two decisions under one heading, which the count cannot see.
|
|
75
|
+
- Retire a decision by moving it to the domain context entry it constrains, not by deleting its reasoning.
|
|
76
|
+
- A word count, for the file and for each decision, is read alongside the judgment when one is available, and it never gates.
|
|
62
77
|
- Yield to the paragraph weight checkpoint in `markdown.md`. A paragraph past the checkpoint is a defect no length guideline licenses.
|
|
63
78
|
|
|
64
79
|
## Template
|
|
65
80
|
|
|
66
|
-
The anchor sentence closes a decision whose reasoning cites a measured number and is absent from one that cites none.
|
|
81
|
+
The anchor sentence closes a decision whose reasoning cites a measured number and is absent from one that cites none. Each heading below names a slot, so a record starts with one decision per slot and renames each heading to the choice it records.
|
|
67
82
|
|
|
68
83
|
```markdown
|
|
69
84
|
# Architecture
|
|
70
85
|
|
|
71
86
|
## Overview
|
|
72
87
|
|
|
88
|
+
This record holds at most 12 decisions.
|
|
89
|
+
|
|
73
90
|
## Key technical decisions
|
|
74
91
|
|
|
75
|
-
###
|
|
92
|
+
### Stack and runtime
|
|
93
|
+
|
|
94
|
+
What was chosen, the alternative that lost, and why. Measured at <short-sha> on <YYYY-MM-DD>.
|
|
95
|
+
|
|
96
|
+
### Delivery
|
|
97
|
+
|
|
98
|
+
### Enforced boundaries
|
|
99
|
+
|
|
100
|
+
### Layout
|
|
76
101
|
|
|
77
|
-
|
|
102
|
+
### Build principles
|
|
78
103
|
|
|
79
104
|
## Risks / open questions
|
|
80
105
|
```
|
package/standards/context.md
CHANGED
|
@@ -13,7 +13,7 @@ Governs per-domain narrative entries under `canon/context/`: their structure, th
|
|
|
13
13
|
|
|
14
14
|
Does not govern:
|
|
15
15
|
|
|
16
|
-
-
|
|
16
|
+
- Decisions filling a stack, delivery, boundary, layout, or build-principle slot, and system-wide risks: `architecture.md`
|
|
17
17
|
- Product scope, goals, and non-goals: `requirements.md`
|
|
18
18
|
- Path-scoped coding rules: `rule.md`
|
|
19
19
|
- Diagrams and wireframes, which answer structure and layout questions an entry hands off: `diagrams.md` and `wireframes.md`
|
|
@@ -82,7 +82,7 @@ Only the `development` entry carries this section. It is not a general-purpose h
|
|
|
82
82
|
## What goes in
|
|
83
83
|
|
|
84
84
|
- Per-domain narrative on how the code is structured and why
|
|
85
|
-
- Decisions
|
|
85
|
+
- Decisions that constrain the domain, whether or not their reasoning touches another one. This is a decision's default home, and `canon/ARCHITECTURE.md` takes only a decision that fills one of the slots its standard names.
|
|
86
86
|
- Constraints, gotchas, things tried and rejected
|
|
87
87
|
- Domain-specific conventions that do not fit a `paths:`-scoped rule
|
|
88
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.
|
|
@@ -4,10 +4,20 @@ Authoring guidance: the architecture standard.
|
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
|
+
This record holds at most 12 decisions.
|
|
8
|
+
|
|
7
9
|
## Key technical decisions
|
|
8
10
|
|
|
9
|
-
###
|
|
11
|
+
### Stack and runtime
|
|
12
|
+
|
|
13
|
+
What was chosen, the alternative that lost, and why.
|
|
14
|
+
|
|
15
|
+
### Delivery
|
|
16
|
+
|
|
17
|
+
### Enforced boundaries
|
|
18
|
+
|
|
19
|
+
### Layout
|
|
10
20
|
|
|
11
|
-
|
|
21
|
+
### Build principles
|
|
12
22
|
|
|
13
23
|
## Risks / open questions
|