@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.
- package/.claude-plugin/plugin.json +1 -1
- package/agents/artifact-format/walkthrough.html +706 -0
- package/agents/artifact-renderer.md +24 -0
- package/door/cli.mjs +18 -1
- package/door/lib/design.mjs +148 -0
- package/package.json +1 -1
- package/skills/lead/SKILL.md +1 -0
- package/skills/sdlc-task/SKILL.md +34 -6
- package/skills/sdlc-task/templates/design-doc.md +82 -79
- package/skills/sdlc-task/templates/design-hub.html +43 -0
- package/work/lib/capability.mjs +48 -1
- package/work/lib/verbs.mjs +10 -4
- package/work/mcp.mjs +2 -1
|
@@ -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
|
|
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
package/skills/lead/SKILL.md
CHANGED
|
@@ -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
|
|
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. **
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
18
|
-
> Work item: <work item id> · Tier:
|
|
19
|
-
>
|
|
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
|
-
|
|
24
|
+
| Field | Value |
|
|
25
|
+
|---|---|
|
|
26
|
+
| <name> | <a real value> |
|
|
22
27
|
|
|
23
|
-
|
|
24
|
-
and the one-line goal. Agent interpretation kept separate.>
|
|
28
|
+
Picture: <who or what> → <what happens> → <result>
|
|
25
29
|
|
|
26
|
-
|
|
30
|
+
### Step 2 — <the problem today>
|
|
27
31
|
|
|
28
|
-
|
|
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
|
-
|
|
33
|
-
- Non-goals: …
|
|
34
|
-
- Success metrics: …
|
|
34
|
+
Example: <the real case>
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
Picture: <today's flow> → <where it breaks>
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
(mermaid) where they earn their keep.>
|
|
38
|
+
## Part 2 — How it flows
|
|
40
39
|
|
|
41
|
-
|
|
40
|
+
### Step 3 — <the first move in the new flow>
|
|
42
41
|
|
|
43
|
-
|
|
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
|
-
|
|
44
|
+
Example:
|
|
52
45
|
|
|
53
|
-
|
|
46
|
+
```text
|
|
47
|
+
<a real command and its real output>
|
|
48
|
+
```
|
|
54
49
|
|
|
55
|
-
|
|
56
|
-
Initiative tier: current-state diagram + target-state diagram.>
|
|
50
|
+
Picture: <input> → <step> → <output>
|
|
57
51
|
|
|
58
|
-
##
|
|
52
|
+
## Part 3 — How it works
|
|
59
53
|
|
|
60
|
-
|
|
61
|
-
Standard tier: may collapse to "approach + rejected alternative, one line".>
|
|
54
|
+
### Step 4 — <one component>
|
|
62
55
|
|
|
63
|
-
|
|
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
|
-
|
|
58
|
+
Example: <a real record, call or file>
|
|
68
59
|
|
|
69
|
-
|
|
70
|
-
Resolved) before the owner is asked to approve the design. -->
|
|
60
|
+
Picture: <component> → <component>
|
|
71
61
|
|
|
72
|
-
|
|
62
|
+
## Part 4 — Decisions and done
|
|
73
63
|
|
|
74
|
-
|
|
64
|
+
### Step 5 — Choices we made
|
|
75
65
|
|
|
76
|
-
|
|
66
|
+
*List each decision with its reason. Say what you rejected.*
|
|
77
67
|
|
|
78
|
-
|
|
68
|
+
Example: <the owner's words, or the evidence>
|
|
79
69
|
|
|
80
|
-
|
|
70
|
+
Picture: <choice> → <reason>
|
|
81
71
|
|
|
82
|
-
##
|
|
72
|
+
## Part 5 — Reference
|
|
83
73
|
|
|
84
|
-
|
|
85
|
-
From the exemplar PRD shape; trimmable at standard tier. -->
|
|
74
|
+
### Approaches considered
|
|
86
75
|
|
|
87
|
-
|
|
76
|
+
- **A — <name>**: <cost, risk>. Rejected because <reason>.
|
|
77
|
+
- **B — <name>**: <cost, risk>. Chosen.
|
|
88
78
|
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
##
|
|
117
|
+
## Changes after approval
|
|
118
|
+
|
|
119
|
+
| Date | What changed | Why | Owner's words |
|
|
120
|
+
|---|---|---|---|
|
|
121
|
+
|
|
122
|
+
## Key
|
|
117
123
|
|
|
118
|
-
|
|
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
|
-
|
|
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>
|
package/work/lib/capability.mjs
CHANGED
|
@@ -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
|
+
}
|