@arjunkhera/atlas 0.3.2 → 0.3.3

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.
@@ -23,6 +23,30 @@ itself carries). Write the finished HTML to the output path the caller names, an
23
23
  return only a one-paragraph summary of what you rendered (sections, diagram count,
24
24
  any content you had to omit and why).
25
25
 
26
+ ## Two formats: walkthrough for designs, Organic pages for the rest
27
+
28
+ **A design doc made from the walkthrough template** (it has `## Part N —`
29
+ and `### Step N —` lines) renders in the **walkthrough format**. The owner
30
+ chose it on 7 October 2026, for designs explained to a new joiner with
31
+ examples and pictures. Its reference file ships with this crew:
32
+ `${CLAUDE_PLUGIN_ROOT}/agents/artifact-format/walkthrough.html`. Copy its
33
+ CSS, script and structure, and swap in the doc's content:
34
+
35
+ - One tab for each part, then a last "Reference" tab for the rest of the doc.
36
+ - One step block for each step: the picture on the left, and the step text
37
+ and an example card on the right. The example card holds the doc's
38
+ `Example:` block, word for word.
39
+ - One hand-built SVG for each part, built from the steps' `Picture:` lines.
40
+ Each piece carries `data-step` with the step that adds it, so the picture
41
+ builds up. Never use mermaid on the page.
42
+ - The page works with all scripts removed. Each step block keeps its own
43
+ copy of the picture, at that step's state. The reference file's comment
44
+ states the contract.
45
+ - The summary sits in a card at the top of Part 1.
46
+
47
+ **Every other doc** (an older design, a PRD, a review, a task explainer)
48
+ renders in the Organic page format below.
49
+
26
50
  ## The ratified treatment (the owner's format — do not drift)
27
51
 
28
52
  **THE RATIFIED FORMAT is the "Organic" quiet-light design-doc format**, from an
package/door/cli.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // The atlas command: install, upgrade, doctor, check, status, the tooling
3
- // writer, the kind drift report, the code-read resolver and the STE check.
3
+ // writer, the kind drift report, the code-read resolver, the STE check and
4
+ // the design check.
4
5
  //
5
6
  // Stage 1 of the target design retired `setup`, `change`, `helpers` and
6
7
  // `test onboarding`: code that guesses meaning retires (decided 28
@@ -21,6 +22,7 @@ import { scan, toText, verdict, CHECK_VERSION, SPECS } from '../shape/check.mjs'
21
22
  import { packagePackageVersion } from './lib/releases.mjs';
22
23
  import { install, upgrade, doctor } from './lib/install.mjs';
23
24
  import { checkPaths, LIMITS } from './lib/ste.mjs';
25
+ import { checkDesignFile, MAX_STEP_SENTENCES } from './lib/design.mjs';
24
26
  import { loadPrivateTerms } from './lib/privacy.mjs';
25
27
 
26
28
  export const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
@@ -42,6 +44,7 @@ const HELP = `atlas — the door into a repo's Atlas files
42
44
  atlas ste <file or folder> … measure markdown and HTML against the STE limits
43
45
  --share also run the privacy filter, quotes included
44
46
  --terms <file> private terms; default ~/.config/atlas/private-terms.txt
47
+ atlas design <file> … check a walkthrough design doc: summary, parts, an example and a picture in each step
45
48
 
46
49
  atlas install install Atlas for every folder on this Mac
47
50
  atlas upgrade [--version <v>] check, show and install a newer release
@@ -168,6 +171,19 @@ function steCommand(chosen) {
168
171
  return count ? 1 : 0;
169
172
  }
170
173
 
174
+ function designCommand(chosen) {
175
+ const paths = chosen._.slice(1);
176
+ if (!paths.length) throw new Error('give a design doc: atlas design <file>');
177
+ let count = 0;
178
+ for (const path of paths) {
179
+ for (const finding of checkDesignFile(resolve(path))) { count += 1; line(`${path}:${finding.line} ${finding.rule} ${finding.why}`); }
180
+ }
181
+ line('');
182
+ line(`${paths.length} design doc(s) read; ${count} finding(s). Each step needs an Example block and a picture, and at most ${MAX_STEP_SENTENCES} sentences before its example.`);
183
+ line(count ? 'Fix the doc by hand. This check never rewrites.' : 'GREEN');
184
+ return count ? 1 : 0;
185
+ }
186
+
171
187
  function checkCommand(chosen) {
172
188
  const root = rootOf(chosen);
173
189
  const report = scan({ root, repoId: chosen.repo === true ? null : chosen.repo ?? null });
@@ -195,6 +211,7 @@ export async function main(argv = process.argv.slice(2)) {
195
211
  case 'kind-drift': return kindDriftCommand(chosen);
196
212
  case 'check': return checkCommand(chosen);
197
213
  case 'ste': return steCommand(chosen);
214
+ case 'design': return designCommand(chosen);
198
215
  default: throw new Error(`there is no verb "${command}". Run "atlas help".`);
199
216
  }
200
217
  }
@@ -0,0 +1,148 @@
1
+ // The design check. It reads one markdown design doc made from the
2
+ // walkthrough template and flags each place where the doc breaks the shape:
3
+ // a summary, four named parts of steps, an example and a picture in every
4
+ // step, a definition of done, the kind of change, and a reference part.
5
+ //
6
+ // It proves the shape, not the quality. A person still judges whether an
7
+ // example is good. It never rewrites a word.
8
+ //
9
+ // Grammar: a part is a line `## Part N — <name>`; a step is a line
10
+ // `### Step N — <title>`. A step's text is the prose between its heading and
11
+ // its `Example:` line. Lists, tables, code and quotes do not count as prose.
12
+ import { readFileSync } from 'node:fs';
13
+
14
+ export const PARTS = Object.freeze(['What and why', 'How it flows', 'How it works', 'Decisions and done']);
15
+ export const REFERENCE = Object.freeze({ name: 'Reference', sections: ['Approaches considered', 'Risks', 'Test plan'] });
16
+ export const REQUIRED_SECTIONS = Object.freeze(['Definition of done', 'Kind of change, impact and undo']);
17
+ export const MAX_STEP_SENTENCES = 3;
18
+ export const MAX_SUMMARY_PARAGRAPHS = 2;
19
+
20
+ const PART_LINE = /^## Part (\d+) [—–-] (.+?)\s*$/;
21
+ const STEP_LINE = /^### Step (\d+) [—–-] (.+?)\s*$/;
22
+ const EXAMPLE_LINE = /^Example\b[^:\n]*:/;
23
+ const PICTURE_LINE = /^Picture:\s*(.*)$/;
24
+ const DIAGRAM_FENCE = /^```\s*(mermaid|svg|diagram|dot|graphviz)\b/i;
25
+
26
+ // Code spans and links go first, so a dot in `atlas.yaml` or a URL never
27
+ // ends a sentence. A sentence ends at . ! or ? before a space and a capital.
28
+ export function sentenceCount(text) {
29
+ const plain = text
30
+ .replace(/`[^`]*`/g, ' Code ')
31
+ .replace(/!\[[^\]]*\]\([^)]*\)/g, ' ')
32
+ .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
33
+ .replace(/\s+/g, ' ')
34
+ .trim();
35
+ if (!plain) return 0;
36
+ return plain.split(/(?<=[.!?])\s+(?=["“(]?[A-Z])/).filter((one) => /[A-Za-z0-9]/.test(one)).length;
37
+ }
38
+
39
+ // Prose lines only: not a list item, a table row, a quote or a code block.
40
+ function proseOf(lines) {
41
+ const out = [];
42
+ let fenced = false;
43
+ for (const line of lines) {
44
+ if (/^```/.test(line.trim())) { fenced = !fenced; continue; }
45
+ if (fenced) continue;
46
+ const trimmed = line.trim();
47
+ if (!trimmed || /^([-*+]|\d+[.)])\s/.test(trimmed) || trimmed.startsWith('|') || trimmed.startsWith('>')) { out.push(''); continue; }
48
+ if (/^\s{2,}\S/.test(line) && out.length && out[out.length - 1] === '') { out.push(''); continue; }
49
+ out.push(trimmed);
50
+ }
51
+ return out;
52
+ }
53
+
54
+ function paragraphs(lines) {
55
+ return proseOf(lines).join('\n').split(/\n\s*\n/).map((one) => one.trim()).filter(Boolean);
56
+ }
57
+
58
+ // Split the doc into level-2 sections, each with its start line and body.
59
+ function sectionsOf(lines) {
60
+ const sections = [];
61
+ let current = null;
62
+ let fenced = false;
63
+ lines.forEach((line, index) => {
64
+ if (/^```/.test(line.trim())) fenced = !fenced;
65
+ if (!fenced && line.startsWith('## ')) {
66
+ current = { heading: line.slice(3).trim(), line: index + 1, body: [] };
67
+ sections.push(current);
68
+ } else if (current) current.body.push({ text: line, line: index + 1 });
69
+ });
70
+ return sections;
71
+ }
72
+
73
+ function stepsOf(section) {
74
+ const steps = [];
75
+ let current = null;
76
+ let fenced = false;
77
+ for (const row of section.body) {
78
+ if (/^```/.test(row.text.trim())) fenced = !fenced;
79
+ const match = !fenced && row.text.match(STEP_LINE);
80
+ if (match) { current = { number: Number(match[1]), title: match[2], line: row.line, body: [] }; steps.push(current); continue; }
81
+ if (!fenced && row.text.startsWith('### ')) { current = null; continue; }
82
+ if (current) current.body.push(row);
83
+ }
84
+ return steps;
85
+ }
86
+
87
+ function checkStep(step, finding) {
88
+ const label = `step "${step.title}"`;
89
+ const exampleIndex = step.body.findIndex((row) => EXAMPLE_LINE.test(row.text.trim()));
90
+ if (exampleIndex === -1) finding(step.line, 'example', `${label} has no Example block`);
91
+ const before = (exampleIndex === -1 ? step.body : step.body.slice(0, exampleIndex)).map((row) => row.text);
92
+ const count = paragraphs(before).reduce((sum, one) => sum + sentenceCount(one), 0);
93
+ if (count > MAX_STEP_SENTENCES) finding(step.line, 'step-text', `${label} has ${count} sentences before its example; the limit is ${MAX_STEP_SENTENCES}`);
94
+ const pictures = step.body.filter((row) => PICTURE_LINE.test(row.text.trim()));
95
+ const sketched = pictures.some((row) => {
96
+ const sketch = row.text.trim().match(PICTURE_LINE)[1];
97
+ return /→|->/.test(sketch) && !/\bTBD\b/i.test(sketch);
98
+ });
99
+ const drawn = step.body.some((row) => DIAGRAM_FENCE.test(row.text.trim()) || /<svg[\s>]/i.test(row.text) || /!\[[^\]]*\]\([^)]+\)/.test(row.text));
100
+ if (!sketched && !drawn) {
101
+ const why = pictures.length ? 'its Picture line has no arrow (→), or says TBD' : 'it has no Picture line and no diagram block';
102
+ finding(step.line, 'picture', `${label} has no picture: ${why}`);
103
+ }
104
+ }
105
+
106
+ export function checkDesign(text) {
107
+ const findings = [];
108
+ const finding = (line, rule, why) => findings.push({ line, rule, why });
109
+ const lines = String(text).split(/\r?\n/);
110
+ const sections = sectionsOf(lines);
111
+ const named = (name) => sections.find((section) => section.heading.toLowerCase() === name.toLowerCase());
112
+
113
+ const summary = named('Summary');
114
+ if (!summary) finding(1, 'summary', 'the doc has no "## Summary" section');
115
+ else {
116
+ const count = paragraphs(summary.body.map((row) => row.text)).length;
117
+ if (count === 0) finding(summary.line, 'summary', 'the Summary is empty');
118
+ if (count > MAX_SUMMARY_PARAGRAPHS) finding(summary.line, 'summary', `the Summary has ${count} paragraphs; the limit is ${MAX_SUMMARY_PARAGRAPHS}`);
119
+ }
120
+
121
+ const parts = sections.map((section) => ({ section, match: section.heading.match(/^Part (\d+) [—–-] (.+?)\s*$/) })).filter((one) => one.match);
122
+ for (const name of PARTS) {
123
+ const part = parts.find((one) => one.match[2].toLowerCase() === name.toLowerCase());
124
+ if (!part) { finding(1, 'part', `the doc has no part "${name}" (a line "## Part N — ${name}")`); continue; }
125
+ const steps = stepsOf(part.section);
126
+ if (!steps.length) finding(part.section.line, 'part', `part "${name}" has no "### Step N — <title>" line`);
127
+ for (const step of steps) checkStep(step, finding);
128
+ }
129
+
130
+ for (const name of REQUIRED_SECTIONS) {
131
+ if (!named(name)) finding(1, 'section', `the doc has no "## ${name}" section`);
132
+ }
133
+
134
+ const reference = parts.find((one) => one.match[2].toLowerCase() === REFERENCE.name.toLowerCase());
135
+ if (!reference) finding(1, 'reference', `the doc has no part "${REFERENCE.name}" (a line "## Part N — ${REFERENCE.name}")`);
136
+ else {
137
+ const headings = reference.section.body.filter((row) => row.text.startsWith('### ')).map((row) => row.text.slice(4).trim().toLowerCase());
138
+ for (const name of REFERENCE.sections) {
139
+ if (!headings.includes(name.toLowerCase())) finding(reference.section.line, 'reference', `the Reference part has no "### ${name}"`);
140
+ }
141
+ }
142
+
143
+ return findings.sort((a, b) => a.line - b.line);
144
+ }
145
+
146
+ export function checkDesignFile(path) {
147
+ return checkDesign(readFileSync(path, 'utf8'));
148
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arjunkhera/atlas",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Atlas: the delivery lifecycle, its crews and the Atlas tools, as a Claude Code plugin for any repository.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -99,5 +99,6 @@ set. A test keeps the two lists equal.
99
99
  | `atlas kind-drift` | When the repo record and `atlas.yaml` may disagree on the kind; this command retires in stage 2, the onboarding stage |
100
100
  | `atlas code-read` | To resolve the citations of a code digest |
101
101
  | `atlas ste` | Before each publish; `--share` before a page goes to anyone else |
102
+ | `atlas design` | Before you render a design doc made from the walkthrough template |
102
103
 
103
104
  A person merges every file that `atlas tooling` writes.
@@ -66,7 +66,7 @@ with the date. (Target design, flow 4: long frozen texts with hashes retire.)
66
66
  sessions use `claude/`. The pull request targets that branch.
67
67
  4. **Design doc** (standard and above): copy
68
68
  [`templates/design-doc.md`](templates/design-doc.md) to
69
- `docs/design-docs/<slug>.md`, fill Context and goal, and list it in
69
+ `docs/design-docs/<slug>.md`, fill the Summary and Part 1, and list it in
70
70
  `docs/index.md` in the commit that first lands it. Hotfix tier skips the
71
71
  doc: the definition of done lives on the work item, and a resume anchor
72
72
  covers pauses.
@@ -105,22 +105,29 @@ A lock is the owner's approval of a short design (target design, flow 4).
105
105
  `lifecycle.yaml` `lock.holds` lists: the definition of done, written as checks; the kind of change, its impact and its undo; and
106
106
  each existing test it expects to change. A change that cannot be undone
107
107
  shows in bold at the top.
108
- 2. **The owner approves it in the owner's own words.** Gate on
108
+ 2. **Show it as a walkthrough page before you ask.** Run `atlas design` and
109
+ `atlas ste` on the design doc, and fix each finding by hand. Render the
110
+ page with the `atlas:artifact-renderer` crew, in the walkthrough format,
111
+ and publish it. Call `item_link` twice: kind `design-doc` for the doc, and
112
+ kind `artifact` for the page. Link the page from the design doc, and
113
+ rebuild the hub (see "The design hub" below).
114
+ 3. **The owner approves it in the owner's own words.** Gate on
109
115
  `lifecycle.yaml` `lock.preconditions`. Record the words with
110
116
  `decision_record`, and mark the design doc "Approved <date>", with the words.
111
- 3. Call `item_lock` with the definition of done as `scope.text` and the design
117
+ 4. Call `item_lock` with the definition of done as `scope.text` and the design
112
118
  file as `scope.design_doc`. The verb keeps a fingerprint of that text for
113
119
  the tools. No person reads or writes a hash, and no frozen text is copied
114
120
  into the doc.
115
- 4. Build against the approved design. **Tripwires** (`lifecycle.yaml`
121
+ Rebuild the hub, so the design reads "Building".
122
+ 5. Build against the approved design. **Tripwires** (`lifecycle.yaml`
116
123
  `tripwires`): a discovery that touches the definition of done, security, a
117
124
  schema or cost stops the build and goes back to the owner, as a change after
118
125
  approval (above).
119
- 5. Test with the repo's own `test` procedure, which `atlas.yaml` names. Open a
126
+ 6. Test with the repo's own `test` procedure, which `atlas.yaml` names. Open a
120
127
  pull request. Its text holds the definition of done, the kind of change, the
121
128
  undo, and the list of existing tests it changes or removes, and it links the
122
129
  tracker item.
123
- 6. **Proof before the merge.** Run the `atlas:verifier` agent on the pull
130
+ 7. **Proof before the merge.** Run the `atlas:verifier` agent on the pull
124
131
  request. It proves each line of the definition of done from a fresh run and
125
132
  posts its own proof comment. Never edit that comment. Ask the owner for the
126
133
  merge only after the proof is there, and show the proof first.
@@ -158,6 +165,27 @@ next action. One short paragraph.
158
165
  2. If anything was a struggle, encode the fix where it stops a repeat — a lint,
159
166
  then a tool, then a skill, then a doc — and say which.
160
167
  3. Design doc status: `SHIPPED <date>`.
168
+ 4. Rebuild the hub, so the design reads "Shipped".
169
+
170
+ ## The design hub
171
+
172
+ One pinned page, "Design docs", lists every design of every product. The
173
+ tracker holds the data; the hub holds none of its own. Rebuild it after each
174
+ design publish, each lock and each done:
175
+
176
+ 1. Call `design_index`. It returns one row for each item with a `design-doc`
177
+ link, newest change first, and `design_hub_url`.
178
+ 2. Fill [`templates/design-hub.html`](templates/design-hub.html). Give each
179
+ design one table row. The row holds the date, the product, the title, the
180
+ state and the design doc. Link the title to the newest page.
181
+ 3. Run `atlas ste` on the page.
182
+ 4. When `design_hub_url` is set, publish to that link. When it is null, list
183
+ the owner's pages and look for one named "Design docs". Publish to it when
184
+ it exists, or publish a new page and pin it. Then call `product_edit` with
185
+ `design_hub_url` on any product. That call needs the owner. Without the
186
+ owner, skip it: the next session finds the hub by its name.
187
+ 5. When the read lists `design_hub_urls`, the products disagree. Keep the one
188
+ the owner has pinned, and set it on each product.
161
189
 
162
190
  ## Model tiering
163
191
 
@@ -1,123 +1,126 @@
1
- <!-- Living design document — template (sdlc-task skill, "The design loop").
2
- Copy to docs/design-docs/<slug>.md at `sdlc-task start` (standard/initiative
3
- tiers). Sections may be trimmed at standard tier; never at initiative.
4
- The artifact projection republishes on change (D14: git is truth).
5
- Structure rules that keep the doc easy to read by machine:
6
- sections addressable by heading, decisions/questions as list items with
7
- stable ids (D-n / OQ-n / RQ-n), no prose that depends on rendering.
8
- Reference rule (the owner's directive): ids are for the machine,
9
- names are for the human — every cross-reference to a stable id links to
10
- its definition (markdown section link here; anchor + tooltip in the HTML
11
- artifact, see the atlas:artifact-renderer crew) and spells out what
12
- it is on first mention. Never leave a bare acronym for the reader to
13
- decode. -->
14
-
15
1
  # <Feature name> — design
16
2
 
17
- > Status: DRAFT (in design loop) | APPROVED <date> | SHIPPED <date>
18
- > Work item: <work item id> · Tier: hotfix | standard | initiative
19
- > Artifact: <claude.ai artifact url once published>
3
+ > Status: DRAFT | APPROVED <date> | SHIPPED <date>
4
+ > Work item: <work item id> · Tier: standard | initiative
5
+ > Page: <artifact url once published>
6
+
7
+ *Write for a new joiner who knows nothing about this repo. Lead with the
8
+ idea, then show it. Run `atlas design` and `atlas ste` on this file
9
+ before you render it.*
10
+
11
+ ## Summary
12
+
13
+ *At most two short paragraphs. Say what changes and why, in plain words.
14
+ Use no ids.*
15
+
16
+ ## Part 1 — What and why
17
+
18
+ ### Step 1 — <one idea, in a few words>
19
+
20
+ *At most three short sentences before the example.*
21
+
22
+ Example:
20
23
 
21
- ## Context & goal
24
+ | Field | Value |
25
+ |---|---|
26
+ | <name> | <a real value> |
22
27
 
23
- <One paragraph: the problem in the owner's words (verbatim where possible),
24
- and the one-line goal. Agent interpretation kept separate.>
28
+ Picture: <who or what> → <what happens> → <result>
25
29
 
26
- ## Goals & non-goals
30
+ ### Step 2 — <the problem today>
27
31
 
28
- <!-- From the owner's exemplar PRD shape. Bullets, testable
29
- in spirit: what this change is FOR, and what it deliberately is not.
30
- Success metrics where they exist — how we'd know it worked. -->
32
+ *Show the problem with a real case.*
31
33
 
32
- - Goals: …
33
- - Non-goals: …
34
- - Success metrics: …
34
+ Example: <the real case>
35
35
 
36
- ## Flows
36
+ Picture: <today's flow> → <where it breaks>
37
37
 
38
- <The user-visible flows this touches, before → after. Sequence diagrams
39
- (mermaid) where they earn their keep.>
38
+ ## Part 2 — How it flows
40
39
 
41
- ## User stories
40
+ ### Step 3 — <the first move in the new flow>
42
41
 
43
- <!-- Mandatory at standard/initiative tier (the owner's words: "I need
44
- to see how a user would use the product, or like how the flow works").
45
- Concrete scenes with stable ids (US-n): who, the moment, what they do,
46
- what the system does, what they see — written as narrative the owner can
47
- react to, each tagged to the slice that ships it. Illustrative dialogue
48
- is fine; mark invented values as illustrative. Draft these BEFORE
49
- the definition of done — its checks formalize what the stories show. -->
42
+ *Each step adds one piece to the part's picture.*
50
43
 
51
- - **US-1 — <scene name>** *(slice)*: …
44
+ Example:
52
45
 
53
- ## Architecture
46
+ ```text
47
+ <a real command and its real output>
48
+ ```
54
49
 
55
- <High-level shape: components touched, data model changes, boundaries crossed.
56
- Initiative tier: current-state diagram + target-state diagram.>
50
+ Picture: <input> → <step> → <output>
57
51
 
58
- ## Approaches considered
52
+ ## Part 3 — How it works
59
53
 
60
- <A/B with real depth at initiative tier — costs, risks, why the loser lost.
61
- Standard tier: may collapse to "approach + rejected alternative, one line".>
54
+ ### Step 4 — <one component>
62
55
 
63
- - **Approach A — <name>**: …
64
- - **Approach B — <name>**: …
65
- - **Chosen**: <A/B> because <reason>.
56
+ *Name the files, the data and the boundaries this step touches.*
66
57
 
67
- ## Open questions
58
+ Example: <a real record, call or file>
68
59
 
69
- <!-- The lock precondition: this checklist must be EMPTY (all items moved to
70
- Resolved) before the owner is asked to approve the design. -->
60
+ Picture: <component> → <component>
71
61
 
72
- - [ ] OQ-1 —
62
+ ## Part 4 — Decisions and done
73
63
 
74
- ## Resolved questions
64
+ ### Step 5 — Choices we made
75
65
 
76
- - RQ-1 — <question> → <resolution, owner-verbatim where it was an owner call> (<date>)
66
+ *List each decision with its reason. Say what you rejected.*
77
67
 
78
- ## Decision log
68
+ Example: <the owner's words, or the evidence>
79
69
 
80
- - D-1 — <decision> — <rationale> (<date>, <who decided>)
70
+ Picture: <choice> → <reason>
81
71
 
82
- ## Risks & failure modes
72
+ ## Part 5 — Reference
83
73
 
84
- <!-- What breaks, degrades, or bites later; per-risk mitigation or "accepted".
85
- From the exemplar PRD shape; trimmable at standard tier. -->
74
+ ### Approaches considered
86
75
 
87
- ## Test & eval plan
76
+ - **A — <name>**: <cost, risk>. Rejected because <reason>.
77
+ - **B — <name>**: <cost, risk>. Chosen.
88
78
 
89
- <!-- How each slice proves itself: suites, scenarios, eval questions, prod
90
- verification. The red spec's table of contents. Trimmable at standard. -->
79
+ ### Risks
80
+
81
+ 1. <what breaks or bites later> — <mitigation, or "accepted">
82
+
83
+ ### Test plan
84
+
85
+ <how each check is proved: suites, scenarios, live checks>
86
+
87
+ ## Questions
88
+
89
+ *Each open question has an id and a default. Empty this list before you
90
+ ask for approval.*
91
+
92
+ - Q-1 — <question>? Default: <answer>.
91
93
 
92
94
  ## Definition of done
93
95
 
94
- <!-- Written as checks, before the build. The verifier proves each line from a
95
- fresh run. Name each existing test this work expects to change. -->
96
+ *Numbered checks, written before the build. The verifier proves each one.
97
+ Name each existing test this work changes.*
96
98
 
97
- 1. …
99
+ 1. <check>
98
100
 
99
101
  ## Kind of change, impact and undo
100
102
 
101
- <!-- A change that cannot be undone goes in bold at the top of this doc. -->
103
+ *A change that cannot be undone goes in bold at the top of this file.*
102
104
 
103
- - Kind: …
104
- - Impact: …
105
- - Undo: …
106
- - Existing tests changed or removed: …
105
+ - Kind: <what kind of change>
106
+ - Impact: <who or what it touches>
107
+ - Undo: <how to undo it>
108
+ - Existing tests changed: <list, or none>
107
109
 
108
110
  ## Approval
109
111
 
110
- <!-- The owner's approval of this short design is the lock (target design,
111
- flow 4). Record the owner's words here and with decision_record. A change
112
- after approval is a new approval, dated, in the same place. -->
112
+ *Record the owner's own words and the date. A change after approval needs
113
+ a new approval, written here with its date.*
113
114
 
114
115
  Approved: <date>. The owner's words: "<words>".
115
116
 
116
- ## Deviation log (post-lock)
117
+ ## Changes after approval
118
+
119
+ | Date | What changed | Why | Owner's words |
120
+ |---|---|---|---|
121
+
122
+ ## Key
117
123
 
118
- <!-- Every post-lock divergence from this document. Review checks this list.
119
- A deviation touching a tripwire (definition of done, security, schema, cost) must halt
120
- and return to the owner instead of landing here. -->
124
+ *Every id, code and short name used in this file, with its meaning.*
121
125
 
122
- | # | What changed vs the doc | Why | Tripwire? | Reviewed |
123
- |---|---|---|---|---|
126
+ - **Q-1** — <meaning>
@@ -0,0 +1,43 @@
1
+ <!doctype html>
2
+ <!--
3
+ The "Design docs" hub. The lead fills it from design_index and publishes it at
4
+ design_hub_url. The hub holds no data of its own: rebuild it whole each time.
5
+ - Copy one <tr> per design, newest first, in the order design_index returns.
6
+ - Title cell: the item title, linked to the newest page (an "artifact" link).
7
+ With no page, link the design doc.
8
+ - State cell: the state design_index gives. Use the class that matches it:
9
+ wait (Waiting on the owner), build (Building), ship (Shipped), other.
10
+ - Replace the date in the header with the date of the rebuild.
11
+ - One light scheme only.
12
+ -->
13
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
14
+ <title>Design docs</title>
15
+ <style>
16
+ :root { --bg: #faf9f5; --surface: #ffffff; --text: #1f1e1d; --muted: #6b6862; --line: #e6e2d9; --accent: #c6613f; --wait: #b4541a; --build: #2f6f9f; --ship: #3f7a46; }
17
+ * { box-sizing: border-box; }
18
+ body { margin: 0; background: var(--bg); color: var(--text); font: 16px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; }
19
+ main { max-width: 960px; margin: 0 auto; padding: 32px 16px 48px; }
20
+ h1 { margin: 0 0 4px; font-size: 28px; }
21
+ .lead { margin: 0 0 24px; color: var(--muted); }
22
+ .wrap { overflow-x: auto; background: var(--surface); border: 1px solid var(--line); border-radius: 12px; }
23
+ table { width: 100%; border-collapse: collapse; }
24
+ th, td { padding: 12px 14px; text-align: left; border-bottom: 1px solid var(--line); vertical-align: top; }
25
+ th { font-size: 13px; color: var(--muted); font-weight: 600; }
26
+ tr:last-child td { border-bottom: 0; }
27
+ a { color: var(--accent); }
28
+ .state { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 13px; font-weight: 600; white-space: nowrap; background: #f0ede6; }
29
+ .state.wait { color: var(--wait); background: #fbeee4; }
30
+ .state.build { color: var(--build); background: #e8f1f8; }
31
+ .state.ship { color: var(--ship); background: #e9f3ea; }
32
+ .when, .product { white-space: nowrap; color: var(--muted); }
33
+ </style></head>
34
+ <body><main>
35
+ <h1>Design docs</h1>
36
+ <p class="lead">Every design, for every product, newest first. Rebuilt on 2026-10-07 from the tracker.</p>
37
+ <div class="wrap"><table>
38
+ <thead><tr><th>Changed</th><th>Product</th><th>Design</th><th>State</th><th>Source</th></tr></thead>
39
+ <tbody>
40
+ <tr><td class="when">2026-10-07</td><td class="product">atlas</td><td><a href="https://claude.ai/artifact/EXAMPLE">Design docs as walkthroughs, in one place</a></td><td><span class="state build">Building</span></td><td><a href="https://github.com/ACCOUNT/REPO/blob/master/docs/design-docs/EXAMPLE.md">design doc</a></td></tr>
41
+ </tbody>
42
+ </table></div>
43
+ </main></body></html>
@@ -19,7 +19,11 @@ import { oneLine, scrubSecrets, ROW_LINE_MAX, priorityOrderOf, byPriority, order
19
19
  import { listLine, legacySealOf, marksByItem, movementOf, pullsOfItem, readPullsByRepo, correctionsOf, daysBetween } from './delivery.mjs';
20
20
  import { acceptCodeRead, codeReadBrief } from './code-read.mjs';
21
21
 
22
- export const CAPABILITY_READ_VERBS = Object.freeze(['code_read_brief', 'where_are_we', 'built_pending_possible']);
22
+ export const CAPABILITY_READ_VERBS = Object.freeze(['code_read_brief', 'where_are_we', 'built_pending_possible', 'design_index']);
23
+
24
+ // The hub column for each lifecycle (design "Design docs as walkthroughs, in
25
+ // one place", step 12). An unknown lifecycle reads as itself.
26
+ export const DESIGN_STATES = Object.freeze({ proposed: 'Waiting on the owner', active: 'Building', paused: 'Paused', done: 'Shipped', dropped: 'Dropped' });
23
27
 
24
28
  const MAX_DIGESTS = 8;
25
29
  const MAX_TERMS = 12;
@@ -493,6 +497,49 @@ export function createCapabilityVerbs(core) {
493
497
  redactions: redactions + rendered.redactions, text: rendered.text,
494
498
  });
495
499
  },
500
+
501
+ // The source of the design hub: every item that links a design doc, in
502
+ // any lifecycle, newest change first. The hub page holds no data of its
503
+ // own, so a lost or raced rebuild is repaired by the next one.
504
+ async design_index(args = {}) {
505
+ const reg = await registry();
506
+ let only = null;
507
+ if (args.product !== undefined) only = (await requireProduct(args.product, reg)).fields.registry_id;
508
+ const rows = await allItems();
509
+ let redactions = 0;
510
+ const clean = (value, max = 200) => { const out = scrubSecrets(oneLine(value, max)); redactions += out.redactions; return out.text; };
511
+ const linksOf = (item, kind) => (Array.isArray(item.fields?.links) ? item.fields.links : [])
512
+ .filter((link) => link?.kind === kind && typeof link.url === 'string')
513
+ .map((link) => ({ url: clean(link.url, 500), role: clean(link.role ?? '', 200) }));
514
+ const designs = [];
515
+ for (const item of rows) {
516
+ const docs = linksOf(item, 'design-doc');
517
+ if (!docs.length) continue;
518
+ const { product, named } = productOf(item, reg);
519
+ if (only && product !== only) continue;
520
+ const lifecycle = clean(item.fields?.lifecycle ?? 'unknown', 32);
521
+ designs.push({
522
+ item: item.id, title: clean(item.title, 200), product, ...(named ? { product_named: clean(named, 64) } : {}),
523
+ lifecycle, state: DESIGN_STATES[lifecycle] ?? lifecycle,
524
+ design_docs: docs, pages: linksOf(item, 'artifact'),
525
+ updated: day(item.updated_at ?? item.created_at),
526
+ });
527
+ }
528
+ designs.sort((a, b) => b.updated.localeCompare(a.updated) || a.title.localeCompare(b.title));
529
+ const lines = designs.map((row) => `${row.updated} ${row.product} ${row.title} [${row.state}]\n ${row.design_docs.map((doc) => doc.url).join('\n ')}`);
530
+ const rendered = scrubSecrets([`Design docs (${plural(designs.length, 'design')})`, ...lines].join('\n'));
531
+ return { verb: 'design_index', read_at: now(), product: only, designs, rows: designs.length, design_hub_url: hubOf(reg).design_hub_url, redactions: redactions + rendered.redactions, text: rendered.text };
532
+ },
496
533
  };
497
534
  return verbs;
498
535
  }
536
+
537
+ // The one hub link for the tenant. It lives on product records, set by
538
+ // product_edit. The product type is strict but takes undeclared fields, as
539
+ // check_paths proved on the live tenant. When the products disagree, no
540
+ // link wins:
541
+ // the read returns null and lists each link, so the lead sets one.
542
+ export function hubOf(reg) {
543
+ const urls = [...new Set([...reg.products.values()].map((row) => row.fields?.design_hub_url).filter((value) => typeof value === 'string' && value))].sort();
544
+ return { design_hub_url: urls.length === 1 ? urls[0] : null, ...(urls.length > 1 ? { design_hub_urls: urls } : {}) };
545
+ }