@erclx/canon 5.14.1 → 5.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +3 -2
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/draft-screencast/REQUIREMENT.md +2 -1
  4. package/claude/skills/draft-screencast/SKILL.md +14 -9
  5. package/claude/skills/draft-wiki/SKILL.md +1 -1
  6. package/claude/skills/plan-groundwork/SKILL.md +1 -1
  7. package/claude/skills/read-frames/SKILL.md +3 -2
  8. package/claude/skills/record-screencast/REQUIREMENT.md +3 -0
  9. package/claude/skills/record-screencast/SKILL.md +28 -5
  10. package/claude/skills/search-craft/REQUIREMENT.md +57 -0
  11. package/claude/skills/search-craft/SKILL.md +83 -0
  12. package/docs/agents/canvas.md +36 -0
  13. package/docs/agents/commands.md +3 -0
  14. package/docs/agents/demo.md +34 -7
  15. package/docs/agents/index.md +1 -0
  16. package/docs/agents/migrate.md +1 -1
  17. package/docs/agents/sandbox.md +25 -0
  18. package/docs/target/migrations.md +1 -1
  19. package/docs/workflow/ai-workflow.md +2 -3
  20. package/docs/workflow/skill-map.md +2 -1
  21. package/governance/renames.toml +7 -5
  22. package/governance/rules/canon/630-requirements.md +1 -1
  23. package/package.json +5 -2
  24. package/scripts/lib/sandbox-git.sh +2 -2
  25. package/src/canvas/address.ts +184 -0
  26. package/src/canvas/capture.ts +172 -0
  27. package/src/canvas/client/app.tsx +87 -0
  28. package/src/canvas/client/inspector.tsx +121 -0
  29. package/src/canvas/client/layers.tsx +118 -0
  30. package/src/canvas/client/main.tsx +62 -0
  31. package/src/canvas/client/pages-panel.tsx +157 -0
  32. package/src/canvas/client/shell.css +449 -0
  33. package/src/canvas/client/state.ts +372 -0
  34. package/src/canvas/client/surface.tsx +465 -0
  35. package/src/canvas/content.ts +617 -0
  36. package/src/canvas/index.html +14 -0
  37. package/src/canvas/server.ts +471 -0
  38. package/src/canvas/tokens.ts +71 -0
  39. package/src/claude/cases/misc.ts +5 -0
  40. package/src/claude/seeds.ts +0 -1
  41. package/src/cli.ts +2 -0
  42. package/src/commands/canvas.ts +522 -0
  43. package/src/commands/claude.ts +9 -14
  44. package/src/commands/demo.ts +6 -6
  45. package/src/commands/migrate.ts +1 -1
  46. package/src/commands/sandbox.ts +108 -0
  47. package/src/context/classify/prompts.ts +1 -1
  48. package/src/context/folders.ts +0 -1
  49. package/src/demo/compile.ts +8 -4
  50. package/src/demo/container.ts +1 -1
  51. package/src/gov/renames.ts +3 -2
  52. package/src/help.ts +1 -0
  53. package/src/migrate/rename.ts +5 -4
  54. package/src/record-root.ts +1 -0
  55. package/src/sandbox/equivalence.ts +847 -0
  56. package/src/serve/static.ts +25 -11
  57. package/src/surface-root.ts +0 -1
  58. package/src/teach/browser/compile.ts +44 -0
  59. package/src/teach/browser/dismiss.ts +76 -0
  60. package/src/teach/browser/focus-line.ts +19 -0
  61. package/src/teach/browser/glossary-filter.ts +44 -0
  62. package/src/teach/browser/head.ts +30 -0
  63. package/src/teach/browser/quiz.ts +19 -0
  64. package/src/teach/browser/sidebar.ts +233 -0
  65. package/src/teach/browser/theme.ts +18 -0
  66. package/src/teach/nav.ts +19 -374
  67. package/standards/architecture.md +1 -1
  68. package/standards/context.md +1 -1
  69. package/standards/design.md +1 -1
  70. package/standards/index.md +0 -1
  71. package/standards/requirements.md +4 -6
  72. package/standards/wireframes.md +2 -2
  73. package/tooling/claude/reference.md +0 -1
  74. package/tooling/claude/seeds/canon/REQUIREMENTS.md +0 -2
  75. package/governance/rules/canon/645-decisions.md +0 -12
  76. package/standards/decisions.md +0 -105
  77. package/tooling/claude/seeds/canon/decisions/index.md +0 -8
package/src/teach/nav.ts CHANGED
@@ -10,6 +10,14 @@ import { FAVICON_COLORS, faviconLink, renderFavicon } from '@/design/favicon'
10
10
  import { stripFrontmatter } from '@/frontmatter'
11
11
  import { parseFrontmatter, readField } from '@/indexes/frontmatter'
12
12
  import { PROJECT_ROOT } from '@/project-root'
13
+ import { compileScript } from '@/teach/browser/compile'
14
+ import { dismiss } from '@/teach/browser/dismiss'
15
+ import { focusLine } from '@/teach/browser/focus-line'
16
+ import { glossaryFilter } from '@/teach/browser/glossary-filter'
17
+ import { head } from '@/teach/browser/head'
18
+ import { quiz } from '@/teach/browser/quiz'
19
+ import { sidebar } from '@/teach/browser/sidebar'
20
+ import { theme } from '@/teach/browser/theme'
13
21
  import { linkLessonReferences } from '@/teach/cross-references'
14
22
  import { TEACH_FONT_FACES } from '@/teach/fonts'
15
23
  import { LESSON_NUMBER } from '@/teach/lesson'
@@ -82,76 +90,6 @@ const CARET =
82
90
  const THEME_BUTTON =
83
91
  '<button class="theme" type="button" aria-label="Switch between light and dark"><svg class="sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg><svg class="moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg></button>'
84
92
 
85
- const THEME_SCRIPT =
86
- '<script>(function(){var r=document.documentElement;try{var s=localStorage.getItem("course-theme");if(s)r.dataset.theme=s;}catch(e){}document.addEventListener("click",function(e){var b=e.target.closest(".theme");if(!b)return;var d=r.dataset.theme==="dark"||(!r.dataset.theme&&matchMedia("(prefers-color-scheme: dark)").matches);r.dataset.theme=d?"light":"dark";try{localStorage.setItem("course-theme",r.dataset.theme);}catch(e){}});})();</script>'
87
-
88
- const CLOSE_OUTSIDE_CLICK_SCRIPT = `<script>
89
- (function () {
90
- var MENUS = "details.jump[open], details.sb-ws[open]";
91
-
92
- function close(except) {
93
- document.querySelectorAll(MENUS).forEach(function (d) {
94
- if (d !== except) d.open = false;
95
- });
96
- }
97
- document.addEventListener("click", function (e) {
98
- var inside = e.target.closest("details.jump, details.sb-ws");
99
- close(inside);
100
- });
101
- document.addEventListener("keydown", function (e) {
102
- if (e.key !== "Escape") return;
103
- var open = document.querySelector(MENUS);
104
- if (!open) return;
105
- open.open = false;
106
- var s = open.querySelector("summary");
107
- if (s) s.focus();
108
- });
109
-
110
- /* Hover intent on the breadcrumb only. The sidebar's workspace switcher stays
111
- click-only on purpose: its panel opens directly over the lesson list, which
112
- is where the pointer is headed, so hovering it would cover the thing
113
- being reached for. The breadcrumb's panel drops over body text instead. */
114
- var OPEN = 120, SHUT = 260;
115
-
116
- document.querySelectorAll("details.jump").forEach(function (d) {
117
- var host = d.closest(".crumb-item") || d;
118
- var timer = null;
119
- var openedByHover = false;
120
-
121
- function arm(want) {
122
- clearTimeout(timer);
123
- timer = setTimeout(function () {
124
- if (want) close(d);
125
- d.open = want;
126
- }, want ? OPEN : SHUT);
127
- }
128
-
129
- host.addEventListener("mouseenter", function () {
130
- if (!d.open) openedByHover = true;
131
- arm(true);
132
- });
133
- host.addEventListener("mouseleave", function () {
134
- openedByHover = false;
135
- arm(false);
136
- });
137
- /* Keeps it open while the pointer is inside the panel, without cancelling a
138
- pending open, which is what silently disabled this on one of two menus. */
139
- d.addEventListener("mouseenter", function () { if (d.open) clearTimeout(timer); });
140
-
141
- /* Hover and click were wired to one disclosure and fought: hover opened the
142
- panel, then the summary's native click toggled what hover had already
143
- opened, so reaching for an item shut it. A click on a panel hover opened
144
- keeps it open, and a second click closes. The click path stays live for
145
- touch, where no hover exists, and for the keyboard, where Enter is the
146
- only way in. */
147
- var summary = d.querySelector("summary");
148
- if (summary) summary.addEventListener("click", function (e) {
149
- if (d.open && openedByHover) { e.preventDefault(); openedByHover = false; }
150
- });
151
- });
152
- })();
153
- </script>`
154
-
155
93
  /**
156
94
  * The stepper, as CSS over the radio inputs a lesson's quiz is written from.
157
95
  *
@@ -195,307 +133,6 @@ const QUIZ_CSS = `@supports selector(:has(*)) {
195
133
  */
196
134
  const LEGACY_OPTION = '<button class="opt"'
197
135
 
198
- /**
199
- * What reveals feedback in a lesson written against `LEGACY_OPTION`. The radio
200
- * shape needs none of it, so a lesson carrying no button option gets no script,
201
- * and removing this outright would leave the lessons already written showing no
202
- * feedback at all.
203
- */
204
- const QUIZ_SCRIPT =
205
- '<script>document.querySelectorAll(".q").forEach(function(q){var f=q.querySelector(".fb");q.querySelectorAll(".opt").forEach(function(b){b.addEventListener("click",function(){if(f.classList.contains("show"))return;q.querySelectorAll(".opt").forEach(function(o){o.dataset.state=o.dataset.a==="1"?"right":(o===b?"chosen":"wrong");});f.classList.add("show");});});});</script>'
206
-
207
- const GLOSSARY_FILTER_SCRIPT = `<script>
208
- (function () {
209
- var input = document.getElementById("gfilter");
210
- var list = document.getElementById("gloss");
211
- var count = document.getElementById("gloss-count");
212
- if (!input || !list) return;
213
- function updateGroups() {
214
- list.querySelectorAll(".gloss-group").forEach(function (heading) {
215
- var el = heading.nextElementSibling;
216
- var any = false;
217
- while (el && !el.classList.contains("gloss-group")) {
218
- if (el.style.display !== "none") any = true;
219
- el = el.nextElementSibling;
220
- }
221
- heading.style.display = any ? "" : "none";
222
- });
223
- }
224
- input.addEventListener("input", function () {
225
- var q = input.value.toLowerCase();
226
- var n = 0;
227
- list.querySelectorAll(".gterm").forEach(function (entry) {
228
- var match = entry.textContent.toLowerCase().includes(q);
229
- entry.style.display = match ? "" : "none";
230
- if (match) n++;
231
- });
232
- list.classList.toggle("none", n === 0);
233
- updateGroups();
234
- if (count) {
235
- var total = list.querySelectorAll(".gterm").length;
236
- count.textContent = (n === total ? total : n + " of " + total) + (total === 1 ? " term" : " terms");
237
- }
238
- });
239
- var clear = list.querySelector(".clear");
240
- if (clear) {
241
- clear.addEventListener("click", function () {
242
- input.value = "";
243
- input.dispatchEvent(new Event("input"));
244
- input.focus();
245
- });
246
- }
247
- })();
248
- </script>`
249
-
250
- /**
251
- * The outline rail's focus-line ramp, as the JavaScript source `OUTLINE_SCRIPT`
252
- * embeds verbatim, so there is one copy of the formula rather than a TS
253
- * reimplementation that could drift from what a browser actually runs.
254
- *
255
- * It ramps the line from near the top at scroll 0 to the viewport's bottom
256
- * edge at max scroll, so the last heading is reachable regardless of how
257
- * little content trails it. The prior formula ended the ramp 120px short of
258
- * the edge, which left a heading followed by under 120px of trailing content
259
- * permanently unmarked, since its top never fell below the line even at max
260
- * scroll.
261
- */
262
- const FOCUS_LINE_BODY = `if (max <= 0) return innerHeight;
263
- var progress = Math.min(1, Math.max(0, scrollY / max));
264
- return 120 + progress * Math.max(0, innerHeight - 120);`
265
-
266
- /**
267
- * Compiles and runs `FOCUS_LINE_BODY`, so a test exercises the exact source
268
- * the browser runs rather than a parallel copy of it.
269
- */
270
- export function focusLine(
271
- scrollY: number,
272
- max: number,
273
- innerHeight: number,
274
- ): number {
275
- const compiled = new Function(
276
- 'scrollY',
277
- 'max',
278
- 'innerHeight',
279
- FOCUS_LINE_BODY,
280
- ) as (scrollY: number, max: number, innerHeight: number) => number
281
-
282
- return compiled(scrollY, max, innerHeight)
283
- }
284
-
285
- /**
286
- * The panel's state, settled before the first paint rather than after it. An
287
- * arm that set the narrow default afterwards slid the panel in and back out on
288
- * every load, and a restored custom width animated in from the default.
289
- *
290
- * A listing page starts shut because the body already lists what the panel
291
- * would, and a lesson starts open because there the panel is the only
292
- * cross-lesson navigation on the page. A stored preference beats both. A count
293
- * threshold on its own was built and reverted, since it hides the panel on a
294
- * lesson too.
295
- */
296
- function headScript(page: 'index' | 'lesson'): string {
297
- return `<script>(function(){var r=document.documentElement,s=null;
298
- r.dataset.page="${page}";
299
- if(matchMedia("(max-width: ${TEACH_SIDEBAR_BREAKPOINT}px)").matches){r.classList.add("sb-shut");return}
300
- try{s=localStorage.getItem("teach-sb")}catch(e){}
301
- var idx=r.dataset.page==="index";
302
- if(s==="shut"||(s===null&&idx))r.classList.add("sb-shut");
303
- var w=null;try{w=localStorage.getItem("teach-sb-w")}catch(e){}
304
- if(w)r.style.setProperty("--sb-w",w+"px")})();</script>`
305
- }
306
-
307
- const SIDEBAR_SCRIPT = `<script>
308
- (function () {
309
- var root = document.documentElement;
310
- var panel = document.querySelector(".sb");
311
- if (!panel) return;
312
-
313
- var fold = document.querySelector(".sb-fold");
314
- var narrow = matchMedia("(max-width: ${TEACH_SIDEBAR_BREAKPOINT}px)");
315
-
316
- var FOCUSABLE = "a[href], button:not([disabled]), summary, input, [tabindex]:not([tabindex='-1'])";
317
-
318
- function panelStops() {
319
- return Array.prototype.slice.call(panel.querySelectorAll(FOCUSABLE))
320
- .filter(function (el) { return el.offsetParent !== null || el === document.activeElement; });
321
- }
322
-
323
- /* The focus return belongs here rather than on the close control, because all
324
- three routes out land here and only one of them used to move focus. The
325
- shut panel takes visibility: hidden 180ms later, so a reader who pressed
326
- Escape kept focus on a control that then disappeared under them and the
327
- browser dropped them at the top of the document. */
328
- function shut() {
329
- var inside = panel.contains(document.activeElement);
330
- root.classList.add("sb-shut");
331
- try { localStorage.setItem("teach-sb", "shut"); } catch (e) {}
332
- if (inside && fold) fold.focus();
333
- }
334
-
335
- if (fold) fold.addEventListener("click", function () {
336
- root.classList.toggle("sb-shut");
337
- try {
338
- localStorage.setItem("teach-sb", root.classList.contains("sb-shut") ? "shut" : "open");
339
- } catch (e) {}
340
- /* An overlay takes focus with it. Without this a keyboard reader opens the
341
- panel and goes on tabbing through the lesson behind the scrim. */
342
- if (!narrow.matches || root.classList.contains("sb-shut")) return;
343
- var first = panel.querySelector("a, button, summary, input");
344
- if (first) first.focus();
345
- });
346
-
347
- var scrim = document.createElement("div");
348
- scrim.className = "sb-scrim";
349
- document.body.appendChild(scrim);
350
- scrim.addEventListener("click", shut);
351
-
352
- document.addEventListener("keydown", function (e) {
353
- if (e.key === "Escape" && narrow.matches) { shut(); return; }
354
-
355
- /* The scrim says the lesson is unavailable, so Tab must not reach it.
356
- Moving focus into the panel on open only defers this by however many
357
- controls the panel holds. */
358
- if (e.key !== "Tab") return;
359
- if (!narrow.matches || root.classList.contains("sb-shut")) return;
360
-
361
- var stops = panelStops();
362
- if (!stops.length) return;
363
-
364
- var first = stops[0];
365
- var last = stops[stops.length - 1];
366
-
367
- if (!panel.contains(document.activeElement)) {
368
- (e.shiftKey ? last : first).focus();
369
- e.preventDefault();
370
- } else if (e.shiftKey && document.activeElement === first) {
371
- last.focus();
372
- e.preventDefault();
373
- } else if (!e.shiftKey && document.activeElement === last) {
374
- first.focus();
375
- e.preventDefault();
376
- }
377
- });
378
-
379
- /* The panel covers the masthead and the toggle that opened it, so it carries
380
- its own way out rather than leaving the scrim as the only route. */
381
- var close = document.createElement("button");
382
- close.type = "button";
383
- close.className = "sb-close";
384
- close.setAttribute("aria-label", "Close the course panel");
385
- close.textContent = "\\u00d7";
386
- close.addEventListener("click", shut);
387
- panel.appendChild(close);
388
-
389
- var MIN = 208, MAX = 296, DEF = 256;
390
- var grip = document.querySelector(".sb-grip");
391
- var lastWidth = parseInt(root.style.getPropertyValue("--sb-w"), 10) || DEF;
392
-
393
- function setWidth(px, save) {
394
- var w = Math.max(MIN, Math.min(MAX, Math.round(px)));
395
- root.style.setProperty("--sb-w", w + "px");
396
- lastWidth = w;
397
- if (save) { try { localStorage.setItem("teach-sb-w", String(w)); } catch (e) {} }
398
- }
399
-
400
- if (grip) {
401
- var dragging = false;
402
- grip.addEventListener("pointerdown", function (e) {
403
- dragging = true;
404
- grip.setPointerCapture(e.pointerId);
405
- root.classList.add("sb-drag");
406
- e.preventDefault();
407
- });
408
- grip.addEventListener("pointermove", function (e) {
409
- if (dragging) setWidth(e.clientX, false);
410
- });
411
- grip.addEventListener("pointerup", function (e) {
412
- if (!dragging) return;
413
- dragging = false;
414
- root.classList.remove("sb-drag");
415
- grip.releasePointerCapture(e.pointerId);
416
- setWidth(lastWidth, true);
417
- });
418
- grip.addEventListener("dblclick", function () { setWidth(DEF, true); });
419
- grip.addEventListener("keydown", function (e) {
420
- var step = e.shiftKey ? 32 : 8;
421
- if (e.key === "ArrowLeft") { setWidth(lastWidth - step, true); e.preventDefault(); }
422
- else if (e.key === "ArrowRight") { setWidth(lastWidth + step, true); e.preventDefault(); }
423
- else if (e.key === "Home") { setWidth(DEF, true); e.preventDefault(); }
424
- });
425
- }
426
-
427
- var filter = document.querySelector(".sb-filter input");
428
- if (filter) filter.addEventListener("input", function () {
429
- var q = filter.value.trim().toLowerCase();
430
- document.querySelectorAll(".sb-list > li").forEach(function (li) {
431
- li.classList.toggle("hide", q !== "" && li.textContent.toLowerCase().indexOf(q) === -1);
432
- });
433
- });
434
-
435
- var slot = document.querySelector(".sb-out-slot");
436
- var hs = Array.prototype.slice.call(document.querySelectorAll("main h2"));
437
- var links = [];
438
-
439
- if (slot && hs.length) {
440
- var list = document.createElement("ul");
441
- list.className = "sb-out";
442
- hs.forEach(function (h, i) {
443
- if (!h.id) h.id = "s" + i;
444
- var li = document.createElement("li");
445
- var a = document.createElement("a");
446
- a.href = "#" + h.id;
447
- a.textContent = h.textContent.trim();
448
- li.appendChild(a);
449
- list.appendChild(li);
450
- });
451
- slot.parentNode.replaceChild(list, slot);
452
- links = Array.prototype.slice.call(list.querySelectorAll("a"));
453
- }
454
-
455
- /* The bar reports position inside the lesson, which nothing reported before
456
- the segmented track retired. The breadcrumb's last segment carries the
457
- lesson title once the real one has scrolled off. */
458
- var bar = document.querySelector(".bar");
459
- var here = document.querySelector(".crumb-here");
460
- var h1 = document.querySelector("main h1");
461
- var counter = here ? here.textContent.trim() : "";
462
- var title = h1 ? h1.textContent.trim() : "";
463
-
464
- function focusLine() {
465
- var max = document.documentElement.scrollHeight - innerHeight;
466
- ${FOCUS_LINE_BODY}
467
- }
468
-
469
- function sync() {
470
- if (bar) {
471
- var max = document.documentElement.scrollHeight - innerHeight;
472
- var pct = max > 0 ? Math.min(100, Math.max(0, (scrollY / max) * 100)) : 0;
473
- bar.style.setProperty("--read", pct.toFixed(1) + "%");
474
- }
475
-
476
- if (here && h1 && title && counter) {
477
- var want = h1.getBoundingClientRect().bottom < 56 ? title : counter;
478
- if (here.textContent !== want) here.textContent = want;
479
- }
480
-
481
- if (links.length) {
482
- /* Nothing is marked until a heading has actually passed the line, so the
483
- first section is not reported as current while the title is on screen. */
484
- var best = -1;
485
- var line = focusLine();
486
- for (var i = 0; i < hs.length; i++) {
487
- if (hs[i].getBoundingClientRect().top <= line) best = i;
488
- }
489
- links.forEach(function (l, j) { l.classList.toggle("on", j === best); });
490
- }
491
- }
492
-
493
- addEventListener("scroll", sync, { passive: true });
494
- addEventListener("resize", sync);
495
- sync();
496
- })();
497
- </script>`
498
-
499
136
  function escapeHtml(text: string): string {
500
137
  return text
501
138
  .replace(/&/g, '&amp;')
@@ -685,13 +322,21 @@ function renderHeader(
685
322
  </header>`
686
323
  }
687
324
 
325
+ function headScript(page: 'index' | 'lesson'): string {
326
+ return compileScript(head, [page, TEACH_SIDEBAR_BREAKPOINT])
327
+ }
328
+
688
329
  function renderScripts(
689
330
  includeQuiz: boolean,
690
331
  includeGlossaryFilter: boolean,
691
332
  ): string {
692
- const scripts = [THEME_SCRIPT, CLOSE_OUTSIDE_CLICK_SCRIPT, SIDEBAR_SCRIPT]
693
- if (includeGlossaryFilter) scripts.push(GLOSSARY_FILTER_SCRIPT)
694
- if (includeQuiz) scripts.push(QUIZ_SCRIPT)
333
+ const scripts = [
334
+ compileScript(theme),
335
+ compileScript(dismiss),
336
+ compileScript(sidebar, [TEACH_SIDEBAR_BREAKPOINT], [focusLine]),
337
+ ]
338
+ if (includeGlossaryFilter) scripts.push(compileScript(glossaryFilter))
339
+ if (includeQuiz) scripts.push(compileScript(quiz))
695
340
  return scripts.join('\n')
696
341
  }
697
342
 
@@ -40,7 +40,7 @@ The slots come from a scale test. Ask whether the decision would still be in thi
40
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
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
42
  - The instances of a build principle. The principle takes one sentence here, and each instance lives in the domain entry where it applies.
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.
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. Cut it. Git and the change that introduced it keep the trail.
44
44
 
45
45
  ## Sections
46
46
 
@@ -100,7 +100,7 @@ Only the `development` entry carries this section. It is not a general-purpose h
100
100
  - Anything already in `canon/REQUIREMENTS.md` or `canon/ARCHITECTURE.md`.
101
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.
102
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.
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. Cut the route. Git and the change that introduced it keep it.
104
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.
105
105
 
106
106
  ## Length
@@ -27,7 +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
+ - 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, and cut the trail. Git keeps it.
31
31
 
32
32
  ## Format
33
33
 
@@ -12,7 +12,6 @@ Reference docs for consistent authoring across the toolkit and target projects.
12
12
  - [Branch reference](branch.md): Branch naming format and type conventions
13
13
  - [Commit reference](commit.md): Commit message format and type conventions
14
14
  - [Context entry reference](context.md): Shape and content rules for canon/context/<domain>.md entries
15
- - [Decisions reference](decisions.md): Folder layout, ordinal filename, frontmatter, record sections, and the append-only lifecycle for canon/decisions/
16
15
  - [Design reference](design.md): Shape and content rules for canon/DESIGN.md
17
16
  - [Diagram reference](diagrams.md): Shape and content rules for .canon/diagrams/<kind>.md files
18
17
  - [Docs reference](docs.md): Reader and jurisdiction, frontmatter, page structure, what a page links out to, the diagram permission, and when a category earns a subfolder
@@ -9,7 +9,7 @@ Applies to `canon/REQUIREMENTS.md`. Describes what the product does and why, not
9
9
 
10
10
  ## Scope
11
11
 
12
- Governs the product-scope document at `canon/REQUIREMENTS.md`: problem, goals, non-goals, MVP features, distribution, stack, and constraints.
12
+ Governs the product-scope document at `canon/REQUIREMENTS.md`: problem, goals, non-goals, MVP features, distribution, and constraints.
13
13
 
14
14
  Does not govern:
15
15
 
@@ -23,7 +23,6 @@ Does not govern:
23
23
  - User-facing goals stated as outcomes, not implementation
24
24
  - Explicit non-goals that prevent feature creep. Mark deferred items "(deferred)" so they read as paused, not excluded. A non-goal resting on how an outside tool, an installer, or the model behaves may close with one `Revisit when <finding>.` sentence, and a non-goal resting on scope alone takes none.
25
25
  - MVP features as a numbered list: feature name and one-line description
26
- - Tech stack as a plain list of tools
27
26
  - Hard constraints that shape every decision
28
27
 
29
28
  ## What does not go in
@@ -31,14 +30,15 @@ Does not govern:
31
30
  - Implementation details, API names, or internal component references
32
31
  - Anything that describes how a feature is built rather than what it does
33
32
  - 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
+ - Content another canonical doc owns, such as the stack list, which the stack-and-runtime slot of `canon/ARCHITECTURE.md` holds, or the rules of this standard itself
34
34
 
35
35
  ## Sections
36
36
 
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
+ Use `## Problem`, `## Goals`, `## Non-goals`, `## MVP features`, and `## Constraints`. Add `## Distribution` when the rule below applies. Add `## Worldview` when the project holds beliefs that shape every decision, and keep a belief there only when it changes a decision, since a padded worldview turns into a second goals list. 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.
38
38
 
39
39
  ## Lifecycle
40
40
 
41
- The MVP list is a historical record of the original scope. Once those features ship it stays as written. Do not renumber it, do not append to it, and do not annotate entries with status. A reader telling the original scope apart from what followed depends on the first one staying legible.
41
+ Once every entry in the MVP list, or in a later scope section, has shipped, delete the section. Git keeps the old text, and the document states the project as it stands. Do not annotate an entry as shipped and leave it in place. While any entry is unshipped, do not renumber the list or append to it.
42
42
 
43
43
  Later scope arrives as a new section rather than as an extension of the MVP list. Name the section for what it delivers and state its entries as outcomes, the same way the goals are stated. Nothing sequences either list into versions. Work reaches the board as discrete tasks under `tasks.md`, and `board.md` orders them by readiness, so a section here states what is wanted and never when it lands.
44
44
 
@@ -65,7 +65,5 @@ State each entry as an outcome the consumer reaches, never as the mechanism that
65
65
 
66
66
  <!-- ## Distribution: include only when shipping outside the repository -->
67
67
 
68
- ## Tech stack
69
-
70
68
  ## Constraints
71
69
  ```
@@ -76,11 +76,11 @@ Both fields feed `canon/wireframes/index.md` when regenerated.
76
76
  ## Not on this surface
77
77
 
78
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.
79
+ - Keep the reason to a clause where one is needed. Cut the rounds that ruled it out.
80
80
 
81
81
  ## What moves out
82
82
 
83
- - Which candidate beat which, and when: the decision log
83
+ - Which candidate beat which, and when: nowhere. Cut it, since git keeps the old text
84
84
  - Measurements such as contrast ratios and timings: `canon/DESIGN.md` for tokens and floors, the surface's context entry for the rest
85
85
 
86
86
  ## What moves to canon/context/
@@ -14,7 +14,6 @@ canon/
14
14
  ├── ARCHITECTURE.md ← seeded. Technical design decisions and open questions
15
15
  ├── DESIGN.md ← seeded. Visual intent and the decisions behind it
16
16
  ├── context/ ← seeded. Per-domain narrative. `index.md` is the discovery anchor.
17
- ├── decisions/ ← seeded. Decision history a canonical doc points at, never loaded eagerly. `<nn>-<slug>.md` records.
18
17
  └── 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
18
 
20
19
  .claude/
@@ -13,6 +13,4 @@ Authoring guidance: the requirements standard.
13
13
  1. Feature: description
14
14
  2. Feature: description
15
15
 
16
- ## Tech stack
17
-
18
16
  ## Constraints
@@ -1,12 +0,0 @@
1
- ---
2
- description: Route canon/decisions/ edits to the decisions standard for record shape and lifecycle
3
- paths:
4
- - 'canon/decisions/**'
5
- - '.claude/decisions/**'
6
- ---
7
-
8
- # Decisions standards
9
-
10
- ## Authority
11
-
12
- - Follow the decisions standard for folder layout, the ordinal filename, frontmatter, record sections, and the append-only lifecycle. It is the single source. Read it with `canon standards decisions`.
@@ -1,105 +0,0 @@
1
- ---
2
- title: Decisions reference
3
- description: Folder layout, ordinal filename, frontmatter, record sections, and the append-only lifecycle for canon/decisions/
4
- ---
5
-
6
- # Decisions reference
7
-
8
- Applies to `canon/decisions/`. Holds the history a canonical doc used to carry itself: pick rounds, superseded figures, and a decision's rejected alternatives, in a tracked folder nothing loads eagerly. A canonical doc points at a record from its own history section rather than restating it.
9
-
10
- ## Scope
11
-
12
- Governs `canon/decisions/`: folder layout, the ordinal filename, frontmatter, record sections, and the append-only lifecycle.
13
-
14
- Does not govern:
15
-
16
- - Which canonical doc points here and when, and what stays behind in that doc's own body: `architecture.md`, `context.md`, `wireframes.md`, `design.md`, `requirements.md`, once each states its own retirement rule
17
- - Voice, rhythm, and sentence construction: the `write-human` skill
18
- - Headings, punctuation, word choice, and file references: `markdown.md`
19
-
20
- ## What a working record looks like
21
-
22
- A record works when a reader who has never opened the project can follow it from the file alone:
23
-
24
- - What was decided, stated once, without needing the canonical doc that points here
25
- - What else was considered, and why each alternative lost
26
- - Which claim rests on a measurement, and what commit that measurement was read against
27
-
28
- A record failing these is non-conforming even when it satisfies every shape rule below.
29
-
30
- ## Folder name
31
-
32
- - `canon/decisions/`, resolved the way every tracked surface is: at `canon/decisions/` in a project that has moved, at `.claude/decisions/` in one that has not.
33
- - Never add `canon/decisions/index.md` to a `CLAUDE.md` `@` import. A log that loads eagerly rebuilds the bloat it exists to absorb. A canonical doc's own pointer is how a reader reaches a record, one file at a time.
34
-
35
- ## Record filename
36
-
37
- - Name each record `<nn>-<slug>.md`, a two-digit zero-padded ordinal followed by a kebab-case slug, the same shape a groundwork track's folder takes.
38
- - The ordinal is the order the record was written in, which is what lets a listing sort by when a decision landed rather than alphabetically by subject.
39
- - Never renumber an existing record. A later reader cites it by that name, and a record whose number moved is a record a stale citation can no longer find.
40
-
41
- ## Frontmatter
42
-
43
- - `title` (required): the decision in sentence case
44
- - `description` (required): one line naming what was decided
45
-
46
- ## Sections
47
-
48
- Use `## Context`, `## Decision`, `## Alternatives`, `## Revisit when`, and `## Measurements`.
49
-
50
- - `## Context`: the problem as it stood, stated so a reader needs nothing else open. Restate a fact rather than pointing at where it was found.
51
- - `## Decision`: what was chosen, and why, in enough detail that a reader can tell it apart from an alternative that sounds similar.
52
- - `## Alternatives`: each one considered and dropped, with the reason it lost. An alternative with no stated reason reads as a claim nobody checked.
53
- - `## Revisit when`: one bullet per finding that would reopen the decision. Required when the record came from a groundwork track whose deciding leans carried "Overturned by" lines, which carry over as written. It is optional otherwise, for a decision resting on how an outside tool, an installer, or the model behaves. Skip it for a decision resting on the project's own code.
54
- - `## Measurements`: skip when the decision cites no number. When it does, state the number and close with the commit it was read against, per Verification anchors below.
55
-
56
- ## Verification anchors
57
-
58
- A record's reasoning stays correct while the numbers it cites move. The anchor records what a measured claim was read against, so a reader can tell a number that was checked and held from one nobody has looked at since.
59
-
60
- - Close a `## Measurements` section with a trailing sentence naming the short commit SHA and the ISO date that number was read: `Measured at <short-sha> on <YYYY-MM-DD>.`
61
- - Anchor on the number alone. A record citing none carries no `## Measurements` section at all.
62
- - Read an absent section as unchecked rather than as current. A record with no measurements has nothing due a re-read.
63
-
64
- ## Lifecycle
65
-
66
- - Append-only. A written record is never edited to reflect a later reversal.
67
- - Write a new record when a later decision supersedes an earlier one, naming the record it supersedes. The old record stays as it was written, since it is history rather than a live statement of the current shape.
68
- - Never auto-loaded. A canonical doc's own history section links to a record by relative path, and a reader reaches it by following that link, not by the folder loading with the session.
69
-
70
- ## Citation
71
-
72
- - Never cite `.canon/`. A record restates what it needs, since a clone without the gitignored records folder resolves nothing there.
73
- - Cite a same-repository pull request or commit the way `publish.md` fixes for any tracked document.
74
-
75
- ## Template
76
-
77
- ```markdown
78
- ---
79
- title: <Decision, in sentence case>
80
- description: <one line naming what was decided>
81
- ---
82
-
83
- # <Decision title>
84
-
85
- ## Context
86
-
87
- <The problem as it stood, self-contained.>
88
-
89
- ## Decision
90
-
91
- <What was chosen, and why.>
92
-
93
- ## Alternatives
94
-
95
- - **<Alternative>.** <Why it lost.>
96
- - **<Alternative>.** <Why it lost.>
97
-
98
- ## Revisit when
99
-
100
- - <The finding that would reopen the decision.>
101
-
102
- ## Measurements
103
-
104
- <The number the decision rests on.> Measured at <short-sha> on <YYYY-MM-DD>.
105
- ```