@arjunkhera/atlas 0.3.22 → 0.3.24
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-renderer.md +2 -0
- package/door/lib/design.mjs +199 -7
- package/door/lib/markdown.mjs +33 -5
- package/door/lib/ste.mjs +5 -3
- package/package.json +1 -1
- package/skills/sdlc-task/SKILL.md +2 -1
- package/skills/sdlc-task/design/README.md +40 -12
- package/skills/sdlc-task/design/parts/problem.md +18 -1
- package/skills/sdlc-task/design/parts/proposal.md +29 -1
- package/skills/sdlc-task/design/parts/states.md +17 -1
- package/skills/sdlc-task/design/parts/stories.md +43 -5
- package/skills/sdlc-task/design/parts/summary.md +2 -1
- package/skills/sdlc-task/design/parts.yaml +12 -0
|
@@ -47,6 +47,8 @@ are in `${CLAUDE_PLUGIN_ROOT}/skills/sdlc-task/design/README.md`, under
|
|
|
47
47
|
- A step has a title and one or two sentences. The reader sets the pace.
|
|
48
48
|
The figure prints as stills, and it works with its script removed.
|
|
49
49
|
- Meaning is never by colour alone.
|
|
50
|
+
- A figure for a story walks the story: it draws each step in order, so
|
|
51
|
+
the reader sees the flow move. The check fails a story with no such figure.
|
|
50
52
|
- Close every tag. `atlas design check <folder>` flags a figure that does
|
|
51
53
|
not.
|
|
52
54
|
|
package/door/lib/design.mjs
CHANGED
|
@@ -5,12 +5,22 @@
|
|
|
5
5
|
//
|
|
6
6
|
// The check flags what a script can see: a needed part that is missing or
|
|
7
7
|
// empty, a state line (state lives in the tracker), a code with no row in
|
|
8
|
-
// the Key, a part the registry does not know,
|
|
9
|
-
// close
|
|
8
|
+
// the Key, a part the registry does not know, a figure whose tags do not
|
|
9
|
+
// close or that woodcut cannot draw, and a part with no picture where the
|
|
10
|
+
// registry asks for one. It proves the shape, not the quality. It never
|
|
11
|
+
// rewrites a word.
|
|
12
|
+
//
|
|
13
|
+
// Pictures are needed (owner, 21 October 2026): "any user is a visual
|
|
14
|
+
// person, they need to see things flowing to make sense of it." The `picture`
|
|
15
|
+
// key in parts.yaml says which part needs one figure, and that each story
|
|
16
|
+
// needs a walkthrough figure and a worked example.
|
|
10
17
|
import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
|
|
11
18
|
import { join, dirname, resolve, basename } from 'node:path';
|
|
12
19
|
import { fileURLToPath } from 'node:url';
|
|
13
20
|
import { parseYaml, insideRepo } from '../../shape/check.mjs';
|
|
21
|
+
import { WOODCUT_TYPES, fenceOpens, fenceCloses } from './markdown.mjs';
|
|
22
|
+
|
|
23
|
+
export { WOODCUT_TYPES };
|
|
14
24
|
|
|
15
25
|
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
16
26
|
export const PARTS_FILE = join(PACKAGE_ROOT, 'skills', 'sdlc-task', 'design', 'parts.yaml');
|
|
@@ -20,6 +30,11 @@ export const NEEDS = Object.freeze(['needed', 'optional', 'none', 'shared']);
|
|
|
20
30
|
export const DESIGN_KEYS = Object.freeze(['title', 'item', 'product', 'kinds', 'shared', 'look']);
|
|
21
31
|
export const STATE_KEYS = Object.freeze(['status', 'state', 'stage', 'approved', 'locked', 'lock', 'shipped', 'delivered']);
|
|
22
32
|
export const PART_KEYS = Object.freeze(['part', 'title']);
|
|
33
|
+
// What a part's `picture` key may say: `one` means the part holds at least one
|
|
34
|
+
// figure; `each-story` means each story holds a walkthrough figure and a
|
|
35
|
+
// worked example.
|
|
36
|
+
export const PICTURES = Object.freeze(['one', 'each-story']);
|
|
37
|
+
|
|
23
38
|
|
|
24
39
|
// A code: capitals then a number, such as M1, A1.7, US-3 or D-6. Common
|
|
25
40
|
// technical names that look like codes are not codes.
|
|
@@ -48,6 +63,7 @@ export function loadRegistry(path = PARTS_FILE) {
|
|
|
48
63
|
const data = parseYaml(readFileSync(path, 'utf8'));
|
|
49
64
|
const kinds = data.kinds.map((one) => one.id);
|
|
50
65
|
for (const part of data.parts) {
|
|
66
|
+
if (part.picture !== undefined && !PICTURES.includes(part.picture)) throw new Error(`${basename(path)}: part "${part.id}" has picture "${part.picture}"; use ${PICTURES.join(' or ')}`);
|
|
51
67
|
for (const kind of kinds) {
|
|
52
68
|
if (!NEEDS.includes(part[kind])) throw new Error(`${basename(path)}: part "${part.id}" has no valid "${kind}" value; use ${NEEDS.join(', ')}`);
|
|
53
69
|
}
|
|
@@ -103,9 +119,9 @@ function scanLines(body, firstLine) {
|
|
|
103
119
|
let fence = null;
|
|
104
120
|
body.split('\n').forEach((text, index) => {
|
|
105
121
|
const line = firstLine + index;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (
|
|
122
|
+
if (fence && fenceCloses(text, fence.mark)) { if (fence.info === 'figure' || fence.info === 'woodcut') figures.push(fence); fence = null; return; }
|
|
123
|
+
const mark = !fence && fenceOpens(text);
|
|
124
|
+
if (mark) { const [info, type] = text.trim().slice(mark.length).trim().split(/\s+/); fence = { mark, info: info ?? '', type: type ?? null, line, lines: [] }; return; }
|
|
109
125
|
if (fence) { fence.lines.push(text); return; }
|
|
110
126
|
prose.push({ text, line });
|
|
111
127
|
});
|
|
@@ -143,6 +159,175 @@ export function figureFault(html) {
|
|
|
143
159
|
return stack.length ? `<${stack[stack.length - 1]}> does not close` : null;
|
|
144
160
|
}
|
|
145
161
|
|
|
162
|
+
// Why woodcut cannot draw this figure, or null. It reads the shape the
|
|
163
|
+
// renderer reads: a viewBox, the lists the type draws, the fields each entry
|
|
164
|
+
// needs, and the ids each step names. Any other throw is a finding too: a
|
|
165
|
+
// figure the check cannot read is a figure the page cannot draw.
|
|
166
|
+
export function woodcutFault(type, data) {
|
|
167
|
+
try { return woodcutShapeFault(type, data); } catch (error) { return `woodcut cannot read the figure data: ${error.message}`; }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const isObject = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
171
|
+
// A list the renderer walks: absent is an empty list; anything else must be
|
|
172
|
+
// an array of objects.
|
|
173
|
+
function listOf(v, key) {
|
|
174
|
+
if (v[key] === undefined) return { list: [] };
|
|
175
|
+
if (!Array.isArray(v[key])) return { fault: `"${key}" must be a list` };
|
|
176
|
+
if (!v[key].every(isObject)) return { fault: `each entry of "${key}" must be an object` };
|
|
177
|
+
return { list: v[key] };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function woodcutShapeFault(type, data) {
|
|
181
|
+
const shape = WOODCUT_TYPES[type];
|
|
182
|
+
if (!shape) return `woodcut has no figure "${type}", so the page draws nothing; use ${Object.keys(WOODCUT_TYPES).join(', ')}, or draw your own in a \`figure\` block`;
|
|
183
|
+
if (!isObject(data)) return 'the woodcut data is not one JSON object';
|
|
184
|
+
if (data.variants !== undefined && !Array.isArray(data.variants)) return 'the woodcut "variants" must be a list';
|
|
185
|
+
const variants = data.variants ?? [data];
|
|
186
|
+
if (!variants.length) return 'the woodcut "variants" list is empty';
|
|
187
|
+
for (const [n, v] of variants.entries()) {
|
|
188
|
+
const at = variants.length > 1 ? ` in variant ${n + 1}` : '';
|
|
189
|
+
if (!isObject(v)) return `variant ${n + 1} is not an object`;
|
|
190
|
+
if (!Array.isArray(v.viewBox) || v.viewBox.length !== 2 || !v.viewBox.every(Number.isFinite)) return `the woodcut data needs "viewBox": [width, height]${at}`;
|
|
191
|
+
const lists = {};
|
|
192
|
+
for (const key of [shape.needs, ...shape.ids, ...(shape.more ?? []), 'scenarios']) {
|
|
193
|
+
const { list, fault } = listOf(v, key);
|
|
194
|
+
if (fault) return fault + at;
|
|
195
|
+
lists[key] = list;
|
|
196
|
+
}
|
|
197
|
+
if (!lists[shape.needs].length) return `a ${type} needs a non-empty "${shape.needs}" list${at}`;
|
|
198
|
+
if (lists[shape.needs].some((one) => one.id === undefined)) return `each entry of "${shape.needs}" needs an "id"${at}`;
|
|
199
|
+
const fault = elementFault(type, v, lists);
|
|
200
|
+
if (fault) return fault + at;
|
|
201
|
+
const ids = new Set(shape.ids.flatMap((key) => lists[key].map((one) => one.id)).filter((id) => id !== undefined));
|
|
202
|
+
if (type === 'wc-state-machine' && v.start) ids.add('start');
|
|
203
|
+
for (const sc of lists.scenarios) {
|
|
204
|
+
if (!Array.isArray(sc.steps)) return `the scenario "${sc.name ?? sc.id ?? '?'}" needs a "steps" list${at}`;
|
|
205
|
+
for (const step of sc.steps) {
|
|
206
|
+
if (!isObject(step)) return `each step of the scenario "${sc.name ?? sc.id ?? '?'}" must be an object${at}`;
|
|
207
|
+
for (const key of ['active', 'denied']) if (step[key] !== undefined && !Array.isArray(step[key])) return `a step's "${key}" must be a list of ids${at}`;
|
|
208
|
+
for (const id of [...(step.active ?? []), ...(step.denied ?? [])]) {
|
|
209
|
+
if (!ids.has(id)) return `a step names "${id}", which is not an element of the figure${at}`;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// The fields each element needs, or the renderer throws and the figure stays
|
|
218
|
+
// blank: a place for each box, the points of each line, and a place for each
|
|
219
|
+
// label on a line.
|
|
220
|
+
const num = (value) => Number.isFinite(value);
|
|
221
|
+
const pair = (value) => Array.isArray(value) && value.length === 2 && value.every(num);
|
|
222
|
+
function elementFault(type, v, lists) {
|
|
223
|
+
const name = (one) => `"${one.id ?? one.label ?? '?'}"`;
|
|
224
|
+
if (type === 'wc-sequence') {
|
|
225
|
+
const actors = new Set(lists.actors.map((one) => one.id));
|
|
226
|
+
for (const a of lists.actors) if (!num(a.x)) return `the actor ${name(a)} needs a number "x"`;
|
|
227
|
+
for (const m of lists.messages) {
|
|
228
|
+
if (!num(m.y)) return `the message ${name(m)} needs a number "y"`;
|
|
229
|
+
if (!actors.has(m.from) || !actors.has(m.to)) return `the message ${name(m)} names an actor that is not in "actors"`;
|
|
230
|
+
}
|
|
231
|
+
for (const act of lists.activations) if (!actors.has(act.actor) || !num(act.y1) || !num(act.y2)) return `each activation needs an "actor" from "actors" and numbers "y1" and "y2"`;
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
if (type === 'wc-state-machine') {
|
|
235
|
+
for (const st of lists.states) if (![st.x, st.y, st.w, st.h].every(num)) return `the state ${name(st)} needs numbers "x", "y", "w" and "h"`;
|
|
236
|
+
for (const t of lists.transitions) {
|
|
237
|
+
if (typeof t.path !== 'string' || !pair(t.tip) || !pair(t.tipDir)) return `the transition ${name(t)} needs "path", "tip": [x, y] and "tipDir": [dx, dy]`;
|
|
238
|
+
if (t.label && !pair(t.labelAt)) return `the transition ${name(t)} has a label, so it needs "labelAt": [x, y]`;
|
|
239
|
+
}
|
|
240
|
+
if (v.start !== undefined && !(isObject(v.start) && pair(v.start.at))) return 'the start needs "at": [x, y]';
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
for (const n of lists.nodes) {
|
|
244
|
+
if (n.shape === 'end' ? ![n.x, n.y].every(num) : ![n.x, n.y, n.w, n.h].every(num)) return `the node ${name(n)} needs numbers "x", "y", "w" and "h"`;
|
|
245
|
+
if (n.shape === 'end' && n.label && !pair(n.labelAt)) return `the end node ${name(n)} has a label, so it needs "labelAt": [x, y]`;
|
|
246
|
+
}
|
|
247
|
+
for (const e of lists.edges) {
|
|
248
|
+
const fewest = e.noTip ? 1 : 2;
|
|
249
|
+
if (!Array.isArray(e.points) || e.points.length < fewest || !e.points.every(pair)) return `the edge ${name(e)} needs "points": ${fewest === 1 ? 'one or more' : 'two or more'} [x, y] pairs`;
|
|
250
|
+
if (e.label && !pair(e.labelAt)) return `the edge ${name(e)} has a label, so it needs "labelAt": [x, y]`;
|
|
251
|
+
}
|
|
252
|
+
for (const lane of lists.lanes) if (![lane.y1, lane.y2].every(num)) return `the lane "${lane.label ?? '?'}" needs numbers "y1" and "y2"`;
|
|
253
|
+
for (const g of lists.groups) if (![g.x, g.y, g.w, g.h].every(num)) return `the group "${g.label ?? '?'}" needs numbers "x", "y", "w" and "h"`;
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// A woodcut figure walks when one of its scenarios has two or more steps and
|
|
258
|
+
// each step has a title: the reader steps through it, and the panel tells
|
|
259
|
+
// what happens at each step. It runs only on data that woodcutFault passed.
|
|
260
|
+
export function woodcutWalks(data) {
|
|
261
|
+
const variants = Array.isArray(data?.variants) ? data.variants : [data];
|
|
262
|
+
return variants.some((v) => (v?.scenarios ?? []).some((sc) => Array.isArray(sc.steps) && sc.steps.length >= 2 && sc.steps.every((step) => step && step.title)));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// A custom figure counts as a picture when it draws: an SVG shape, or HTML
|
|
266
|
+
// with at least three elements. An empty <svg> or a line of text is not one.
|
|
267
|
+
const SHAPE = /<(?:rect|circle|ellipse|path|line|polyline|polygon|text|image)\b/i;
|
|
268
|
+
export function figureDraws(html) {
|
|
269
|
+
const text = html.replace(/<!--[\s\S]*?-->/g, '');
|
|
270
|
+
return SHAPE.test(text) || (text.match(/<[a-zA-Z][\w-]*\b/g)?.length ?? 0) >= 3;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// A story heading carries a code, such as "### US-1: a test goes red". The
|
|
274
|
+
// stories are the coded headings at the level of the first one; a deeper
|
|
275
|
+
// heading, or an uncoded heading such as "## Overview", stays inside the story
|
|
276
|
+
// before it. HTML comments and fenced blocks hold no stories.
|
|
277
|
+
const STORY_CODE = /(?<![\w./-])[A-Z]{1,4}-?\d+(?:\.\d+)*(?![\w/-])/;
|
|
278
|
+
function storiesOf(body, firstLine) {
|
|
279
|
+
const rows = [];
|
|
280
|
+
let fence = null;
|
|
281
|
+
let comment = false;
|
|
282
|
+
body.split('\n').forEach((text, index) => {
|
|
283
|
+
const line = firstLine + index;
|
|
284
|
+
if (fence) { if (fenceCloses(text, fence)) fence = null; rows.push({ text, line }); return; }
|
|
285
|
+
const open = fenceOpens(text);
|
|
286
|
+
if (open) { fence = open; rows.push({ text, line }); return; }
|
|
287
|
+
if (comment) { if (text.includes('-->')) comment = false; rows.push({ text, line }); return; }
|
|
288
|
+
if (/^\s*<!--/.test(text) && !text.includes('-->')) { comment = true; rows.push({ text, line }); return; }
|
|
289
|
+
const heading = text.match(/^(#{2,6})\s+(.*)$/);
|
|
290
|
+
rows.push({ text, line, heading: heading && STORY_CODE.test(heading[2].replace(/`[^`]*`/g, '')) ? { level: heading[1].length, name: heading[2].trim() } : null });
|
|
291
|
+
});
|
|
292
|
+
const level = rows.find((row) => row.heading)?.heading.level;
|
|
293
|
+
const stories = [];
|
|
294
|
+
for (const row of rows) {
|
|
295
|
+
if (row.heading && row.heading.level === level) stories.push({ name: row.heading.name, line: row.line, lines: [] });
|
|
296
|
+
else if (stories.length) stories[stories.length - 1].lines.push(row.text);
|
|
297
|
+
}
|
|
298
|
+
return stories;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// A worked example: a label that names the input and one that names the
|
|
302
|
+
// output, such as "**Input:** ..." and "**Output:** ...". "Ask" and "Answer",
|
|
303
|
+
// "Given" and "Result" count too, and so does a table with Input and Output
|
|
304
|
+
// columns.
|
|
305
|
+
const LABEL = (words) => new RegExp(`(?:^|[^\\w])(?:\\*\\*|__)?(?:${words})(?:\\*\\*|__)?\\s*(?::|—|–)|(?:\\*\\*|__)(?:${words})[:.](?:\\*\\*|__)|^\\s*\\|.*\\b(?:${words})\\b.*\\|`, 'i');
|
|
306
|
+
const EXAMPLE_INPUT = LABEL('inputs?|ask|given|example input');
|
|
307
|
+
const EXAMPLE_OUTPUT = LABEL('outputs?|answer|result|example output');
|
|
308
|
+
// A part that says "No change" and why needs no picture, as the empty rule allows.
|
|
309
|
+
const NO_CHANGE = /^\s*(?:#{1,6}\s.*\n\s*)*(?:\*\*|__)?no change\b/i;
|
|
310
|
+
|
|
311
|
+
function pictureFindings(one, rule, figures, finding) {
|
|
312
|
+
if (NO_CHANGE.test(one.body)) return;
|
|
313
|
+
const drawn = figures.filter((figure) => !figure.open && !figure.fault && (figure.info !== 'figure' || figureDraws(figure.lines.join('\n'))));
|
|
314
|
+
if (rule === 'one') {
|
|
315
|
+
if (!drawn.length) finding(one.file, one.bodyLine, 'picture', `part "${one.part}" needs a figure; a reader must see it, not only read it. Add a \`woodcut\` or a \`figure\` block`);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const stories = storiesOf(one.body, one.bodyLine);
|
|
319
|
+
if (!stories.length) { finding(one.file, one.bodyLine, 'picture', 'write each story under a heading with its code, such as "### US-1: a test goes red", with a walkthrough figure and a worked example'); return; }
|
|
320
|
+
for (const story of stories) {
|
|
321
|
+
const inside = drawn.filter((figure) => figure.line > story.line && figure.line <= story.line + story.lines.length);
|
|
322
|
+
const walks = inside.some((figure) => figure.info === 'figure' || figure.walks);
|
|
323
|
+
if (!walks) finding(one.file, story.line, 'picture', `the story "${story.name}" needs a walkthrough figure: a woodcut figure with a scenario whose steps each have a title, or a figure of your own`);
|
|
324
|
+
const words = story.lines;
|
|
325
|
+
if (!words.some((line) => EXAMPLE_INPUT.test(line)) || !words.some((line) => EXAMPLE_OUTPUT.test(line))) {
|
|
326
|
+
finding(one.file, story.line, 'picture', `the story "${story.name}" needs a worked example: an "Input:" line and an "Output:" line with real values`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
146
331
|
export function checkDesign(design, registry = loadRegistry()) {
|
|
147
332
|
const findings = [];
|
|
148
333
|
const finding = (file, line, rule, why) => findings.push({ file, line, rule, why });
|
|
@@ -196,10 +381,17 @@ export function checkDesign(design, registry = loadRegistry()) {
|
|
|
196
381
|
let fault = figure.open ? 'the figure block does not close' : null;
|
|
197
382
|
if (!fault && figure.info === 'woodcut') {
|
|
198
383
|
if (!/^wc-[a-z-]+$/.test(figure.type ?? '')) fault = 'a woodcut block names its figure type, such as "woodcut wc-flowchart"';
|
|
199
|
-
else {
|
|
384
|
+
else {
|
|
385
|
+
let data;
|
|
386
|
+
try { data = JSON.parse(figure.lines.join('\n')); } catch (error) { fault = `the woodcut data is not JSON: ${error.message}`; }
|
|
387
|
+
if (!fault) fault = woodcutFault(figure.type, data);
|
|
388
|
+
if (!fault) figure.walks = woodcutWalks(data);
|
|
389
|
+
}
|
|
200
390
|
} else if (!fault) fault = figureFault(figure.lines.join('\n'));
|
|
201
|
-
if (fault) finding(one.file, figure.line, 'figure', fault);
|
|
391
|
+
if (fault) { figure.fault = fault; finding(one.file, figure.line, 'figure', fault); }
|
|
202
392
|
}
|
|
393
|
+
const picture = registry.byId.get(one.part).picture;
|
|
394
|
+
if (picture && !isEmpty(one.body)) pictureFindings(one, picture, figures, finding);
|
|
203
395
|
}
|
|
204
396
|
for (const [id, { need, by }] of needs) {
|
|
205
397
|
if (need === 'needed' && !seen.has(id)) {
|
package/door/lib/markdown.mjs
CHANGED
|
@@ -6,6 +6,31 @@
|
|
|
6
6
|
//
|
|
7
7
|
// No dependencies. The text is the repo's own; a custom figure is trusted
|
|
8
8
|
// the same way a page in the repo is.
|
|
9
|
+
// The figure types that woodcut 0.4.0 defines (WOODCUT in design-build.mjs), and the list each one cannot
|
|
10
|
+
// draw without. A name outside this map is an element the browser does not
|
|
11
|
+
// know, so it draws nothing. The ids lists name the arrays whose ids a
|
|
12
|
+
// walkthrough step may name.
|
|
13
|
+
export const WOODCUT_TYPES = Object.freeze({
|
|
14
|
+
'wc-block-diagram': { needs: 'nodes', ids: ['nodes', 'edges', 'groups'], more: ['lanes'] },
|
|
15
|
+
'wc-flowchart': { needs: 'nodes', ids: ['nodes', 'edges', 'groups'], more: ['lanes'] },
|
|
16
|
+
'wc-swimlane': { needs: 'nodes', ids: ['nodes', 'edges', 'groups'], more: ['lanes'] },
|
|
17
|
+
'wc-sequence': { needs: 'actors', ids: ['actors', 'messages'], more: ['activations'] },
|
|
18
|
+
'wc-state-machine': { needs: 'states', ids: ['states', 'transitions'] },
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// One fence rule for every reader of a page (the check, this renderer and
|
|
22
|
+
// atlas ste), as CommonMark has it: a fence opens with three or more of one
|
|
23
|
+
// mark, and closes only on a run of the same mark at least as long, with
|
|
24
|
+
// nothing after it. So a ```` block may hold a ``` block.
|
|
25
|
+
export function fenceOpens(line) {
|
|
26
|
+
const match = /^\s*(`{3,}|~{3,})/.exec(line);
|
|
27
|
+
return match ? match[1] : null;
|
|
28
|
+
}
|
|
29
|
+
export function fenceCloses(line, mark) {
|
|
30
|
+
const match = /^\s*(`{3,}|~{3,})\s*$/.exec(line);
|
|
31
|
+
return Boolean(match) && match[1][0] === mark[0] && match[1].length >= mark.length;
|
|
32
|
+
}
|
|
33
|
+
|
|
9
34
|
export const esc = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
10
35
|
export const slug = (text) => String(text).toLowerCase().replace(/<[^>]+>/g, '').replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
11
36
|
|
|
@@ -60,16 +85,19 @@ export function render(body, ctx) {
|
|
|
60
85
|
};
|
|
61
86
|
while (i < lines.length) {
|
|
62
87
|
const line = lines[i];
|
|
63
|
-
const
|
|
64
|
-
if (
|
|
88
|
+
const mark = fenceOpens(line);
|
|
89
|
+
if (mark) {
|
|
65
90
|
flush();
|
|
66
|
-
const info =
|
|
91
|
+
const info = line.trim().slice(mark.length).trim();
|
|
67
92
|
const buf = [];
|
|
68
93
|
i += 1;
|
|
69
|
-
while (i < lines.length &&
|
|
94
|
+
while (i < lines.length && !fenceCloses(lines[i], mark)) buf.push(lines[i++]);
|
|
70
95
|
i += 1;
|
|
71
96
|
const woodcut = info.match(/^woodcut\s+(wc-[a-z-]+)/);
|
|
72
|
-
if (woodcut) {
|
|
97
|
+
if (woodcut && !WOODCUT_TYPES[woodcut[1]]) {
|
|
98
|
+
// A draft page shows the fault where the figure should be, never a blank.
|
|
99
|
+
out.push(`<p class="draft">This figure does not draw: woodcut has no figure "${esc(woodcut[1])}". Run atlas design check.</p>`);
|
|
100
|
+
} else if (woodcut) {
|
|
73
101
|
let data;
|
|
74
102
|
try { data = JSON.parse(buf.join('\n')); } catch (error) { throw new Error(`${ctx.file}: bad woodcut JSON in ${woodcut[1]}: ${error.message}`); }
|
|
75
103
|
ctx.figures.push(woodcut[1]);
|
package/door/lib/ste.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
|
13
13
|
import { dirname, extname, join, relative } from 'node:path';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
import { privacyFindings, loadPrivateTerms } from './privacy.mjs';
|
|
16
|
+
import { fenceOpens, fenceCloses } from './markdown.mjs';
|
|
16
17
|
|
|
17
18
|
export const LIMITS = Object.freeze({ step: 20, sentence: 25, paragraph: 6 });
|
|
18
19
|
|
|
@@ -64,9 +65,10 @@ export function markdownUnits(text) {
|
|
|
64
65
|
for (; index < lines.length; index += 1) {
|
|
65
66
|
const line = lines[index];
|
|
66
67
|
const number = index + 1;
|
|
67
|
-
|
|
68
|
-
if (fence) { if (
|
|
69
|
-
|
|
68
|
+
// The fence rule of the design renderer: a ```` block may hold a ``` block.
|
|
69
|
+
if (fence) { if (fenceCloses(line, fence)) fence = null; continue; }
|
|
70
|
+
const mark = fenceOpens(line);
|
|
71
|
+
if (mark) { close(); fence = mark; continue; }
|
|
70
72
|
if (!line.trim()) { close(); continue; }
|
|
71
73
|
if (/^\s*>/.test(line)) { close(); continue; }
|
|
72
74
|
if (comment) { if (line.includes('-->')) comment = false; continue; }
|
package/package.json
CHANGED
|
@@ -114,7 +114,8 @@ A lock is the owner's approval of a short design.
|
|
|
114
114
|
shows in bold at the top.
|
|
115
115
|
2. **Show the design's page before you ask.** Run
|
|
116
116
|
`atlas design check <folder>` and `atlas ste` on the folder, and fix each
|
|
117
|
-
finding by hand.
|
|
117
|
+
finding by hand. The check needs pictures, as `design/README.md`
|
|
118
|
+
"Figures" says. A part that needs a custom figure goes to the
|
|
118
119
|
`atlas:artifact-renderer` crew first. Run
|
|
119
120
|
`atlas design build <folder> --tracker <item.json>`, with the tracker
|
|
120
121
|
data that `design/README.md` names. Run `atlas ste` on the page and
|
|
@@ -90,12 +90,10 @@ gets. The kind sets which parts the design has. The two are separate: an
|
|
|
90
90
|
initiative-tier piece of work can be a Decide design, and an Initiative
|
|
91
91
|
design holds a family of designs.
|
|
92
92
|
|
|
93
|
-
At
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
2. At least two options get full depth in Alternatives: their costs, their
|
|
98
|
-
risks and why each one lost.
|
|
93
|
+
At every tier, the Problem today part has a figure of today and the
|
|
94
|
+
Proposal part has a figure of the target. At initiative tier, a Decide
|
|
95
|
+
design also goes deep. At least two options get full depth in
|
|
96
|
+
Alternatives: their costs, their risks and why each one lost.
|
|
99
97
|
|
|
100
98
|
At standard tier, one chosen approach and one rejected option are enough.
|
|
101
99
|
|
|
@@ -167,18 +165,48 @@ Without the key, the design folder is `docs/design-docs`.
|
|
|
167
165
|
An empty part fails the check.
|
|
168
166
|
4. Put every code you use, such as `US-3` or `M1`, in the Key.
|
|
169
167
|
5. Never write a state line, such as "Status: approved". The check flags it.
|
|
168
|
+
6. Draw the parts that the Figures section names. The check fails a part
|
|
169
|
+
with no figure where one is needed.
|
|
170
170
|
|
|
171
171
|
## Figures
|
|
172
172
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
173
|
+
A reader must see the design move, not only read it (owner, 21 October
|
|
174
|
+
2026: "any user is a visual person, they need to see things flowing to make
|
|
175
|
+
sense of it"). The check enforces it. The `picture` key in
|
|
176
|
+
[parts.yaml](parts.yaml) says where:
|
|
177
|
+
|
|
178
|
+
| Part | The check needs |
|
|
179
|
+
|---|---|
|
|
180
|
+
| Summary | One figure. |
|
|
181
|
+
| Problem today | One figure of today. |
|
|
182
|
+
| Proposal | One figure of the target. |
|
|
183
|
+
| States | One state figure. |
|
|
184
|
+
| Stories | For each story: a walkthrough figure, and a worked example with real values. The example has an `Input:` label and an `Output:` label. `Ask` and `Answer`, or a table with Input and Output columns, also count. |
|
|
185
|
+
|
|
186
|
+
A story starts at a heading that holds its code, such as
|
|
187
|
+
`### US-1: a test goes red`. A deeper heading stays inside the story. A part
|
|
188
|
+
that says "No change" and why needs no figure.
|
|
189
|
+
|
|
190
|
+
A walkthrough figure is a woodcut figure with a scenario of two or more
|
|
191
|
+
steps, each with a title and one or two sentences. The reader clicks or
|
|
192
|
+
presses the arrow keys, and each step lights up the part that moves. A
|
|
193
|
+
figure of your own also counts.
|
|
194
|
+
|
|
195
|
+
1. Use a woodcut figure when a stock figure fits. Use a fenced block with
|
|
196
|
+
the info string `woodcut <type>`. It holds the figure data as JSON.
|
|
197
|
+
The template for Stories has a full example.
|
|
198
|
+
2. Pick the type by its question. `wc-sequence`: in what order?
|
|
199
|
+
`wc-swimlane`: who does it? `wc-flowchart`: which way?
|
|
200
|
+
`wc-state-machine`: what state is it in? `wc-block-diagram`: what are
|
|
201
|
+
the parts?
|
|
202
|
+
3. Any other type name draws nothing, so the check refuses it. The check
|
|
203
|
+
also refuses data that the figure cannot draw. Examples are a missing
|
|
204
|
+
`viewBox`, a box with no place and a line with no points.
|
|
205
|
+
4. Draw your own figure when the subject has its own shape. Git lanes, a
|
|
178
206
|
time line and a data diff are examples. Use a fenced block with the info
|
|
179
207
|
string `figure`. It holds the HTML. Use the page tokens, such as
|
|
180
208
|
`var(--accent)` and `var(--line)`, so the figure works in both themes.
|
|
181
|
-
|
|
209
|
+
5. Both kinds follow the same rules. A step has a title and one or two
|
|
182
210
|
sentences. The reader sets the pace. The figure prints as stills.
|
|
183
211
|
Meaning is never by colour alone.
|
|
184
212
|
|
|
@@ -8,7 +8,8 @@ Rules:
|
|
|
8
8
|
|
|
9
9
|
1. Say what happens today, with one real example.
|
|
10
10
|
2. Cite the source: a pull request, an issue or a log line.
|
|
11
|
-
3.
|
|
11
|
+
3. Draw today with a figure. The check fails the part with no figure.
|
|
12
|
+
4. Do not propose a fix here.
|
|
12
13
|
|
|
13
14
|
Example, in a file of its own:
|
|
14
15
|
|
|
@@ -19,4 +20,20 @@ part: problem
|
|
|
19
20
|
# Problem today
|
|
20
21
|
|
|
21
22
|
Today the tests run after the merge. On 2 October pull request 88 merged, and its tests went red one hour later.
|
|
23
|
+
|
|
24
|
+
```woodcut wc-flowchart
|
|
25
|
+
{
|
|
26
|
+
"label": "Today: the tests run after the merge",
|
|
27
|
+
"viewBox": [460, 70],
|
|
28
|
+
"nodes": [
|
|
29
|
+
{ "id": "pr", "x": 10, "y": 18, "w": 110, "h": 34, "label": "Pull request", "shape": "pill" },
|
|
30
|
+
{ "id": "merge", "x": 175, "y": 18, "w": 110, "h": 34, "label": "Merge" },
|
|
31
|
+
{ "id": "tests", "x": 340, "y": 18, "w": 110, "h": 34, "label": "Tests: red", "kind": "accent" }
|
|
32
|
+
],
|
|
33
|
+
"edges": [
|
|
34
|
+
{ "id": "e1", "points": [[120, 35], [173, 35]] },
|
|
35
|
+
{ "id": "e2", "points": [[285, 35], [338, 35]] }
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
```
|
|
22
39
|
````
|
|
@@ -8,7 +8,7 @@ Rules:
|
|
|
8
8
|
|
|
9
9
|
1. Name the option we pick, and say how it works in steps.
|
|
10
10
|
2. Give a worked example: the input, the process and the output.
|
|
11
|
-
3.
|
|
11
|
+
3. Draw the target with a figure. The check fails the part with no figure.
|
|
12
12
|
|
|
13
13
|
Example, in a file of its own:
|
|
14
14
|
|
|
@@ -18,7 +18,35 @@ part: proposal
|
|
|
18
18
|
---
|
|
19
19
|
# Proposal
|
|
20
20
|
|
|
21
|
+
The tests run before the merge.
|
|
22
|
+
|
|
23
|
+
```woodcut wc-flowchart
|
|
24
|
+
{
|
|
25
|
+
"label": "Target: the tests run before the merge",
|
|
26
|
+
"viewBox": [460, 70],
|
|
27
|
+
"nodes": [
|
|
28
|
+
{ "id": "pr", "x": 10, "y": 18, "w": 110, "h": 34, "label": "Pull request", "shape": "pill" },
|
|
29
|
+
{ "id": "tests", "x": 175, "y": 18, "w": 110, "h": 34, "label": "Tests", "kind": "accent" },
|
|
30
|
+
{ "id": "merge", "x": 340, "y": 18, "w": 110, "h": 34, "label": "Merge" }
|
|
31
|
+
],
|
|
32
|
+
"edges": [
|
|
33
|
+
{ "id": "e1", "points": [[120, 35], [173, 35]] },
|
|
34
|
+
{ "id": "e2", "points": [[285, 35], [338, 35]], "label": "green only", "labelAt": [311, 12] }
|
|
35
|
+
],
|
|
36
|
+
"scenarios": [{
|
|
37
|
+
"name": "Pull request 88",
|
|
38
|
+
"steps": [
|
|
39
|
+
{ "title": "A pull request opens", "body": "Pull request 88 changes src/pay.mjs.", "active": ["pr"] },
|
|
40
|
+
{ "title": "The gate runs the tests", "body": "The test pay rounds down goes red.", "active": ["e1", "tests"] },
|
|
41
|
+
{ "title": "The merge waits", "body": "Only a green run opens the merge.", "active": ["e2", "merge"] }
|
|
42
|
+
]
|
|
43
|
+
}]
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
21
47
|
1. A pull request opens.
|
|
22
48
|
2. The gate runs the tests on its head.
|
|
23
49
|
3. A red test blocks the merge button.
|
|
50
|
+
|
|
51
|
+
Input: pull request 88 with one failing test. Output: the merge button stays grey.
|
|
24
52
|
````
|
|
@@ -8,7 +8,7 @@ Rules:
|
|
|
8
8
|
|
|
9
9
|
1. List the states a thing moves through.
|
|
10
10
|
2. Say what moves it from each state to the next.
|
|
11
|
-
3. Draw it as a state figure.
|
|
11
|
+
3. Draw it as a state figure. The check fails the part with no figure.
|
|
12
12
|
|
|
13
13
|
Example, in a file of its own:
|
|
14
14
|
|
|
@@ -18,6 +18,22 @@ part: states
|
|
|
18
18
|
---
|
|
19
19
|
# States
|
|
20
20
|
|
|
21
|
+
```woodcut wc-state-machine
|
|
22
|
+
{
|
|
23
|
+
"label": "A test run",
|
|
24
|
+
"viewBox": [440, 90],
|
|
25
|
+
"states": [
|
|
26
|
+
{ "id": "waiting", "x": 10, "y": 30, "w": 100, "h": 32, "label": "Waiting" },
|
|
27
|
+
{ "id": "running", "x": 170, "y": 30, "w": 100, "h": 32, "label": "Running" },
|
|
28
|
+
{ "id": "red", "x": 330, "y": 30, "w": 100, "h": 32, "label": "Red", "terminal": true }
|
|
29
|
+
],
|
|
30
|
+
"transitions": [
|
|
31
|
+
{ "id": "start", "path": "M 110 46 L 168 46", "tip": [168, 46], "tipDir": [1, 0], "label": "tests start", "labelAt": [139, 22] },
|
|
32
|
+
{ "id": "fail", "path": "M 270 46 L 328 46", "tip": [328, 46], "tipDir": [1, 0], "label": "a test fails", "labelAt": [299, 22] }
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
21
37
|
| From | Event | To |
|
|
22
38
|
|---|---|---|
|
|
23
39
|
| Waiting | The tests start | Running |
|
|
@@ -6,9 +6,18 @@ Needs: Decide: optional, Flow: needed, Contract: optional, Initiative: none.
|
|
|
6
6
|
|
|
7
7
|
Rules:
|
|
8
8
|
|
|
9
|
-
1. Write
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
1. Write each story under its own heading, with a code, such as `US-1`.
|
|
10
|
+
Put the code in the Key.
|
|
11
|
+
2. Give each story a walkthrough figure. The reader steps through it and
|
|
12
|
+
sees what moves at each step. Use a woodcut figure with a scenario. Give
|
|
13
|
+
each step a title and one or two sentences. A figure of your own also
|
|
14
|
+
counts. The check fails a story with no walkthrough figure.
|
|
15
|
+
3. Give each story a worked example with real values: an `Input:` line, the
|
|
16
|
+
steps, and an `Output:` line. The check fails a story with no worked
|
|
17
|
+
example.
|
|
18
|
+
4. Pick the figure by the question it answers: `wc-sequence` for "in what
|
|
19
|
+
order", `wc-swimlane` for "who does it", `wc-flowchart` for "which
|
|
20
|
+
way", `wc-state-machine` for "what state is it in".
|
|
12
21
|
|
|
13
22
|
Example, in a file of its own:
|
|
14
23
|
|
|
@@ -20,7 +29,36 @@ part: stories
|
|
|
20
29
|
|
|
21
30
|
### US-1: a test goes red
|
|
22
31
|
|
|
23
|
-
|
|
24
|
-
|
|
32
|
+
**Input:** pull request 88 changes `src/pay.mjs`. The test `pay rounds down` fails on it.
|
|
33
|
+
|
|
34
|
+
```woodcut wc-sequence
|
|
35
|
+
{
|
|
36
|
+
"label": "US-1: a test goes red",
|
|
37
|
+
"viewBox": [460, 190],
|
|
38
|
+
"actors": [
|
|
39
|
+
{ "id": "lead", "x": 70, "label": "Lead" },
|
|
40
|
+
{ "id": "gate", "x": 230, "label": "Gate" },
|
|
41
|
+
{ "id": "owner", "x": 390, "label": "Owner" }
|
|
42
|
+
],
|
|
43
|
+
"messages": [
|
|
44
|
+
{ "id": "open", "from": "lead", "to": "gate", "y": 70, "label": "open pull request 88" },
|
|
45
|
+
{ "id": "red", "from": "gate", "to": "lead", "y": 110, "label": "1 test red", "kind": "return" },
|
|
46
|
+
{ "id": "block", "from": "gate", "to": "owner", "y": 150, "label": "merge blocked" }
|
|
47
|
+
],
|
|
48
|
+
"scenarios": [{
|
|
49
|
+
"name": "US-1",
|
|
50
|
+
"steps": [
|
|
51
|
+
{ "title": "The lead opens a pull request", "body": "Pull request 88 changes src/pay.mjs.", "active": ["lead", "open"] },
|
|
52
|
+
{ "title": "The gate runs the tests", "body": "The test pay rounds down fails.", "active": ["gate", "red"] },
|
|
53
|
+
{ "title": "The owner sees the block", "body": "The merge button stays grey and names the failing check.", "active": ["owner", "block"] }
|
|
54
|
+
]
|
|
55
|
+
}]
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
1. The lead opens pull request 88.
|
|
60
|
+
2. The gate runs the tests, and `pay rounds down` goes red.
|
|
25
61
|
3. The merge button stays blocked, and the owner sees the failing check.
|
|
62
|
+
|
|
63
|
+
**Output:** the merge button is grey. The check list shows `pay rounds down: failed`.
|
|
26
64
|
````
|
|
@@ -7,7 +7,8 @@ Needs: Decide: needed, Flow: needed, Contract: needed, Initiative: needed.
|
|
|
7
7
|
Rules:
|
|
8
8
|
|
|
9
9
|
1. Lead with the answer: what changes, for whom, and why now.
|
|
10
|
-
2. Keep it to two short paragraphs and one picture.
|
|
10
|
+
2. Keep it to two short paragraphs and one picture. The check fails a
|
|
11
|
+
Summary with no figure.
|
|
11
12
|
3. A reader who stops here knows the whole design.
|
|
12
13
|
|
|
13
14
|
Example, in a file of its own:
|
|
@@ -12,6 +12,13 @@
|
|
|
12
12
|
# none the kind does not use it
|
|
13
13
|
# shared needed when design.yaml says `shared: true`, optional when not
|
|
14
14
|
#
|
|
15
|
+
# A part may say what picture it needs (owner, 21 October 2026: "any user is
|
|
16
|
+
# a visual person, they need to see things flowing to make sense of it"):
|
|
17
|
+
# picture: one the part holds at least one figure that draws
|
|
18
|
+
# picture: each-story each story holds a walkthrough figure and a worked
|
|
19
|
+
# example, with an Input line and an Output line
|
|
20
|
+
# The check reads it whenever the part is in the folder.
|
|
21
|
+
#
|
|
15
22
|
# A design keeps no state. The tracker parts at the end are read from the
|
|
16
23
|
# tracker at build time. No part file may hold them.
|
|
17
24
|
|
|
@@ -50,6 +57,7 @@ parts:
|
|
|
50
57
|
name: Summary
|
|
51
58
|
group: start
|
|
52
59
|
holds: The answer in a few lines, and one picture.
|
|
60
|
+
picture: one
|
|
53
61
|
decide: needed
|
|
54
62
|
flow: needed
|
|
55
63
|
contract: needed
|
|
@@ -75,6 +83,7 @@ parts:
|
|
|
75
83
|
name: Problem today
|
|
76
84
|
group: decide
|
|
77
85
|
holds: What happens today, with a real example.
|
|
86
|
+
picture: one
|
|
78
87
|
decide: needed
|
|
79
88
|
flow: optional
|
|
80
89
|
contract: optional
|
|
@@ -91,6 +100,7 @@ parts:
|
|
|
91
100
|
name: Proposal
|
|
92
101
|
group: decide
|
|
93
102
|
holds: The option we pick, and how it works.
|
|
103
|
+
picture: one
|
|
94
104
|
decide: needed
|
|
95
105
|
flow: none
|
|
96
106
|
contract: none
|
|
@@ -132,6 +142,7 @@ parts:
|
|
|
132
142
|
name: Stories
|
|
133
143
|
group: flow
|
|
134
144
|
holds: One walkthrough for each story, step by step.
|
|
145
|
+
picture: each-story
|
|
135
146
|
decide: optional
|
|
136
147
|
flow: needed
|
|
137
148
|
contract: optional
|
|
@@ -156,6 +167,7 @@ parts:
|
|
|
156
167
|
name: States
|
|
157
168
|
group: flow
|
|
158
169
|
holds: The states a thing moves through, and what moves it.
|
|
170
|
+
picture: one
|
|
159
171
|
decide: none
|
|
160
172
|
flow: optional
|
|
161
173
|
contract: none
|