@warren-dean/code-kit 0.7.0 → 0.8.1
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/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +33 -0
- package/README.md +15 -3
- package/bin/code-kit.mjs +171 -3
- package/hooks/guard-bash.mjs +29 -2
- package/hooks/lib/adapters/context-graph.mjs +18 -19
- package/hooks/lib/journal.mjs +224 -0
- package/hooks/lib/loop-state.mjs +26 -0
- package/hooks/lib/next.mjs +13 -3
- package/hooks/lib/plan.mjs +8 -1
- package/hooks/lib/storymap.mjs +155 -0
- package/hooks/mod/codemap.mjs +328 -0
- package/hooks/mod/decisions.mjs +146 -0
- package/hooks/mod/loop.mjs +7 -3
- package/hooks/mod/mission.mjs +116 -0
- package/hooks/mod/panes.mjs +469 -34
- package/hooks/mod/register.mjs +196 -2
- package/hooks/mod/register.test.ts +456 -25
- package/hooks/mod/trail.mjs +152 -0
- package/hooks/mod/view.mjs +324 -116
- package/package.json +1 -1
- package/skills/init/SKILL.md +3 -3
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// The trail (docs/specs/14-mission-and-maps.md, TRAIL-1), drawn: each requirement of a spec flowing to
|
|
2
|
+
// the stories that deliver it, the code they changed and the tests that name it, with a gap where any
|
|
3
|
+
// link is missing. Box-drawing text in the terminal, an SVG elsewhere. Its data is `code-kit trail`.
|
|
4
|
+
|
|
5
|
+
/** A story's state as the trail colours it: done, moving (in progress or review), or waiting. */
|
|
6
|
+
export const storyTone = (state) =>
|
|
7
|
+
state === 'done'
|
|
8
|
+
? 'done'
|
|
9
|
+
: ['in progress', 'review', 'sent back'].includes(state)
|
|
10
|
+
? 'moving'
|
|
11
|
+
: 'waiting';
|
|
12
|
+
|
|
13
|
+
const GLYPH = { done: '✓', moving: '●', waiting: '■' };
|
|
14
|
+
|
|
15
|
+
const fit = (s, n) => (s.length > n ? `${s.slice(0, Math.max(0, n - 1))}…` : s.padEnd(n));
|
|
16
|
+
const short = (p) => p.split('/').pop();
|
|
17
|
+
const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* TRAIL-1 in the terminal: one row per requirement, as runs [{ text, style }]. Consecutive
|
|
21
|
+
* requirements delivered by the same stories meet at one story node (┬ … ┘). Styles: req, selected,
|
|
22
|
+
* dim, done, moving, waiting, gap (red), test (green).
|
|
23
|
+
*/
|
|
24
|
+
export function trailRows(rows, { selected = null, width = 100 } = {}) {
|
|
25
|
+
const titleW = Math.max(14, Math.min(34, width - 70));
|
|
26
|
+
const key = (r) => r.stories.map((s) => s.id).join(' ');
|
|
27
|
+
return rows.map((r, i) => {
|
|
28
|
+
const runs = [];
|
|
29
|
+
const add = (text, style) => runs.push({ text, style });
|
|
30
|
+
add(r.id.padEnd(9), r.id === selected ? 'selected' : 'req');
|
|
31
|
+
add(fit(r.title, titleW), r.id === selected ? 'selected' : 'dim');
|
|
32
|
+
const same = i > 0 && key(rows[i - 1]) === key(r) && r.stories.length > 0;
|
|
33
|
+
const next = i < rows.length - 1 && key(rows[i + 1]) === key(r) && r.stories.length > 0;
|
|
34
|
+
if (!r.stories.length) {
|
|
35
|
+
add(' ╌╌╌ ', 'gap');
|
|
36
|
+
add(fit('no story', 16), 'gap');
|
|
37
|
+
} else if (same) {
|
|
38
|
+
add(next ? ' ───┤' : ' ───┘', 'dim');
|
|
39
|
+
add(''.padEnd(16), 'dim');
|
|
40
|
+
} else {
|
|
41
|
+
add(next ? ' ───┬ ' : ' ──── ', 'dim');
|
|
42
|
+
const tone = r.stories.every((s) => s.state === 'done')
|
|
43
|
+
? 'done'
|
|
44
|
+
: r.stories.some((s) => storyTone(s.state) === 'moving')
|
|
45
|
+
? 'moving'
|
|
46
|
+
: 'waiting';
|
|
47
|
+
add(fit(`${GLYPH[tone]} ${r.stories.map((s) => s.id).join(' ')}`, 15), tone);
|
|
48
|
+
}
|
|
49
|
+
if (r.files.length) {
|
|
50
|
+
add(' ─── ', 'dim');
|
|
51
|
+
add(fit(plural(r.files.length, 'file', 'files'), 9), 'dim');
|
|
52
|
+
} else {
|
|
53
|
+
add(' ╌╌╌ ', 'gap');
|
|
54
|
+
add(fit('no code', 9), 'gap');
|
|
55
|
+
}
|
|
56
|
+
if (r.tests.length) {
|
|
57
|
+
add(' ─── ', 'dim');
|
|
58
|
+
add(`✓ ${short(r.tests[0])}${r.tests.length > 1 ? ` +${r.tests.length - 1}` : ''}`, 'test');
|
|
59
|
+
} else {
|
|
60
|
+
add(' ╌╌╌ ', 'gap');
|
|
61
|
+
add('no test names it', 'gap');
|
|
62
|
+
}
|
|
63
|
+
return runs;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const esc = (s) =>
|
|
68
|
+
String(s).replace(
|
|
69
|
+
/[&<>"]/g,
|
|
70
|
+
(c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c],
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const COLOR = {
|
|
74
|
+
done: '#78c98a',
|
|
75
|
+
moving: '#8fb8ff',
|
|
76
|
+
waiting: '#e06a6a',
|
|
77
|
+
dim: '#6e8286',
|
|
78
|
+
fg: '#cfd9da',
|
|
79
|
+
line: '#3b4c50',
|
|
80
|
+
gap: '#e06a6a',
|
|
81
|
+
test: '#78c98a',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* TRAIL-1 on the remote surfaces: the flow as an SVG: requirements on the left, curves meeting at each
|
|
86
|
+
* story, then the files and the test that names it; a gap drawn dashed in red.
|
|
87
|
+
*/
|
|
88
|
+
export function trailSvg(rows, { selected = null } = {}) {
|
|
89
|
+
const rowH = 30;
|
|
90
|
+
const W = 820;
|
|
91
|
+
const H = rows.length * rowH + 44;
|
|
92
|
+
const X = { req: 12, story: 300, files: 430, test: 640 };
|
|
93
|
+
const parts = [];
|
|
94
|
+
for (const [label, x] of [
|
|
95
|
+
['requirement', X.req],
|
|
96
|
+
['story', X.story],
|
|
97
|
+
['code', X.files],
|
|
98
|
+
['test that names it', X.test],
|
|
99
|
+
])
|
|
100
|
+
parts.push(`<text x="${x}" y="16" fill="${COLOR.dim}" font-size="11">${esc(label)}</text>`);
|
|
101
|
+
const storyY = {};
|
|
102
|
+
rows.forEach((r, i) => {
|
|
103
|
+
const y = 38 + i * rowH;
|
|
104
|
+
const key = r.stories.map((s) => s.id).join(' ');
|
|
105
|
+
if (key && storyY[key] === undefined) storyY[key] = y;
|
|
106
|
+
const sy = key ? storyY[key] : y;
|
|
107
|
+
const sel = r.id === selected;
|
|
108
|
+
parts.push(
|
|
109
|
+
`<text x="${X.req}" y="${y + 4}" fill="${sel ? '#8fb8ff' : COLOR.fg}" font-size="12"${sel ? ' font-weight="700"' : ''}>${esc(r.id)}</text>`,
|
|
110
|
+
);
|
|
111
|
+
parts.push(
|
|
112
|
+
`<text x="${X.req + 70}" y="${y + 4}" fill="${COLOR.dim}" font-size="11">${esc(r.title.length > 26 ? `${r.title.slice(0, 25)}…` : r.title)}</text>`,
|
|
113
|
+
);
|
|
114
|
+
if (key) {
|
|
115
|
+
parts.push(
|
|
116
|
+
`<path d="M${X.story - 50},${y} C${X.story - 25},${y} ${X.story - 25},${sy} ${X.story - 6},${sy}" fill="none" stroke="${COLOR.line}"/>`,
|
|
117
|
+
);
|
|
118
|
+
if (sy === y) {
|
|
119
|
+
const tone = r.stories.every((s) => s.state === 'done')
|
|
120
|
+
? 'done'
|
|
121
|
+
: r.stories.some((s) => storyTone(s.state) === 'moving')
|
|
122
|
+
? 'moving'
|
|
123
|
+
: 'waiting';
|
|
124
|
+
parts.push(`<circle cx="${X.story + 3}" cy="${y}" r="4" fill="${COLOR[tone]}"/>`);
|
|
125
|
+
parts.push(
|
|
126
|
+
`<text x="${X.story + 13}" y="${y + 4}" fill="${COLOR.fg}" font-size="12">${esc(key)}</text>`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
parts.push(
|
|
130
|
+
`<path d="M${X.story + 80},${sy} C${X.files - 30},${sy} ${X.files - 30},${y} ${X.files - 6},${y}" fill="none" stroke="${r.files.length ? COLOR.line : COLOR.gap}"${r.files.length ? '' : ' stroke-dasharray="3 3"'}/>`,
|
|
131
|
+
);
|
|
132
|
+
} else {
|
|
133
|
+
parts.push(
|
|
134
|
+
`<line x1="${X.story - 50}" y1="${y}" x2="${X.story - 6}" y2="${y}" stroke="${COLOR.gap}" stroke-dasharray="3 3"/>`,
|
|
135
|
+
);
|
|
136
|
+
parts.push(
|
|
137
|
+
`<text x="${X.story}" y="${y + 4}" fill="${COLOR.gap}" font-size="11">no story</text>`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
const files = r.files.map(short).join(', ');
|
|
141
|
+
parts.push(
|
|
142
|
+
`<text x="${X.files}" y="${y + 4}" fill="${r.files.length ? COLOR.dim : COLOR.gap}" font-size="11">${esc(r.files.length ? (files.length > 30 ? `${files.slice(0, 29)}…` : files) : 'no code yet')}</text>`,
|
|
143
|
+
);
|
|
144
|
+
parts.push(
|
|
145
|
+
`<line x1="${X.test - 18}" y1="${y}" x2="${X.test - 6}" y2="${y}" stroke="${r.tests.length ? COLOR.test : COLOR.gap}"${r.tests.length ? '' : ' stroke-dasharray="3 3"'}/>`,
|
|
146
|
+
);
|
|
147
|
+
parts.push(
|
|
148
|
+
`<text x="${X.test}" y="${y + 4}" fill="${r.tests.length ? COLOR.test : COLOR.gap}" font-size="11">${esc(r.tests.length ? `✓ ${short(r.tests[0])}${r.tests.length > 1 ? ` +${r.tests.length - 1}` : ''}` : 'no test names it yet')}</text>`,
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" font-family="ui-monospace, Menlo, monospace">${parts.join('')}</svg>`;
|
|
152
|
+
}
|