@ersbeth/codelife 0.0.0-stage → 0.11.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/plugin.json +19 -0
- package/LICENSE +21 -0
- package/README.md +65 -2
- package/agents/evaluate.md +97 -0
- package/agents/ground.md +49 -0
- package/agents/implementer.md +49 -0
- package/agents/judge.md +128 -0
- package/agents/planner.md +45 -0
- package/agents/prerequisites.md +62 -0
- package/agents/reader.md +40 -0
- package/agents/reproduce.md +65 -0
- package/bin/codelife +4 -0
- package/package.json +38 -4
- package/presets/tsconfig.json +14 -0
- package/presets/vite.config.d.mts +3 -0
- package/presets/vite.config.mjs +49 -0
- package/scripts/audit/cadence.mjs +24 -0
- package/scripts/audit/close.mjs +57 -0
- package/scripts/audit/file.mjs +70 -0
- package/scripts/audit/fold.mjs +48 -0
- package/scripts/audit/lib.mjs +93 -0
- package/scripts/audit/prepare.mjs +180 -0
- package/scripts/audit/table.mjs +65 -0
- package/scripts/brainstorm/decided.mjs +6 -0
- package/scripts/brainstorm/lib.mjs +33 -0
- package/scripts/brainstorm/named.mjs +9 -0
- package/scripts/changelog.mjs +100 -0
- package/scripts/check-tag.mjs +19 -0
- package/scripts/checks.mjs +53 -0
- package/scripts/codelife.mjs +42 -0
- package/scripts/commit-msg.mjs +43 -0
- package/scripts/contract.mjs +179 -0
- package/scripts/former-form.mjs +170 -0
- package/scripts/git/branches.mjs +6 -0
- package/scripts/git/commit.mjs +40 -0
- package/scripts/git/diff.mjs +17 -0
- package/scripts/git/diff_lines.mjs +5 -0
- package/scripts/git/held.mjs +53 -0
- package/scripts/git/is_ancestor.mjs +28 -0
- package/scripts/git/lib.mjs +20 -0
- package/scripts/git/log_since.mjs +27 -0
- package/scripts/git/parent.mjs +51 -0
- package/scripts/git/show.mjs +4 -0
- package/scripts/git/status.mjs +25 -0
- package/scripts/git/submodules.mjs +5 -0
- package/scripts/init.mjs +21 -0
- package/scripts/install.mjs +32 -0
- package/scripts/lenses/A11Y.md +91 -0
- package/scripts/lenses/ARCH.md +90 -0
- package/scripts/lenses/BUG.md +52 -0
- package/scripts/lenses/BUILD.md +40 -0
- package/scripts/lenses/COMPAT.md +67 -0
- package/scripts/lenses/CONF.md +49 -0
- package/scripts/lenses/DEPS.md +43 -0
- package/scripts/lenses/DOC.md +73 -0
- package/scripts/lenses/FAIL.md +57 -0
- package/scripts/lenses/GOV.md +56 -0
- package/scripts/lenses/GUARD.md +47 -0
- package/scripts/lenses/INTENT.md +52 -0
- package/scripts/lenses/LEAN.md +49 -0
- package/scripts/lenses/LINES.md +48 -0
- package/scripts/lenses/PERF.md +61 -0
- package/scripts/lenses/PKG.md +58 -0
- package/scripts/lenses/SEC.md +49 -0
- package/scripts/lenses/STANDARDS.md +62 -0
- package/scripts/lenses/TYPE.md +56 -0
- package/scripts/lenses/VERIF.md +81 -0
- package/scripts/link.mjs +40 -0
- package/scripts/methods/cause.md +21 -0
- package/scripts/methods/comments.md +157 -0
- package/scripts/methods/description.md +67 -0
- package/scripts/methods/message.md +24 -0
- package/scripts/methods/red-green.md +28 -0
- package/scripts/methods/refusal.md +22 -0
- package/scripts/mr/open.mjs +219 -0
- package/scripts/node-floor.mjs +19 -0
- package/scripts/onboard/prep.mjs +93 -0
- package/scripts/overview/codelife-line.mjs +33 -0
- package/scripts/overview/next.mjs +76 -0
- package/scripts/overview/overview.mjs +308 -0
- package/scripts/package-manager.mjs +20 -0
- package/scripts/port/item.mjs +94 -0
- package/scripts/port/lib.mjs +194 -0
- package/scripts/port/prep.mjs +33 -0
- package/scripts/port/version.mjs +50 -0
- package/scripts/pre-commit.mjs +43 -0
- package/scripts/pre-push.mjs +53 -0
- package/scripts/prepare-lib.mjs +81 -0
- package/scripts/read-roadmap/lib.mjs +116 -0
- package/scripts/read-roadmap/moved.mjs +70 -0
- package/scripts/read-roadmap/range.mjs +15 -0
- package/scripts/release/cut.mjs +120 -0
- package/scripts/release/entry.mjs +52 -0
- package/scripts/release/lib.mjs +136 -0
- package/scripts/release/prep.mjs +58 -0
- package/scripts/review-mr/prepare.mjs +163 -0
- package/scripts/roadmap/assemble.mjs +269 -0
- package/scripts/roadmap/cut.mjs +88 -0
- package/scripts/roadmap/prepare.mjs +249 -0
- package/scripts/roadmap/roadmap.md +30 -0
- package/scripts/roadmap/write.mjs +53 -0
- package/scripts/search.mjs +29 -0
- package/scripts/setup/contract.mjs +25 -0
- package/scripts/setup/item.mjs +166 -0
- package/scripts/setup/lib.mjs +205 -0
- package/scripts/setup/prep.mjs +106 -0
- package/scripts/spec-form.mjs +444 -0
- package/scripts/spec.mjs +56 -0
- package/scripts/tags.mjs +27 -0
- package/scripts/target/target-app.md +134 -0
- package/scripts/target/target-base.md +127 -0
- package/scripts/target/target-lib.md +143 -0
- package/scripts/target/templates/audit/README.md +21 -0
- package/scripts/target/templates/characterisation/bundle.test.ts +43 -0
- package/scripts/target/templates/characterisation/declarations.test.ts +44 -0
- package/scripts/target/templates/characterisation/entries.ts +50 -0
- package/scripts/target/templates/characterisation/examples.test.ts +18 -0
- package/scripts/target/templates/characterisation/import.test.ts +22 -0
- package/scripts/target/templates/characterisation/surface.test.ts +68 -0
- package/scripts/target/templates/cliff.toml +61 -0
- package/scripts/target/templates/githooks/commit-msg +9 -0
- package/scripts/target/templates/githooks/pre-commit +9 -0
- package/scripts/target/templates/githooks/pre-push +9 -0
- package/scripts/target/templates/playground/index.html +11 -0
- package/scripts/target/templates/playground/main.ts +9 -0
- package/scripts/target/templates/spec/README.md +35 -0
- package/scripts/target/templates/spec/capability.md +36 -0
- package/scripts/target/templates/spec/glossary.md +5 -0
- package/scripts/target/templates/spec/journey.md +29 -0
- package/scripts/target/templates/spec/principles.md +15 -0
- package/scripts/target/templates/spec/users.md +15 -0
- package/scripts/target/templates/tsconfig.json +9 -0
- package/scripts/target/templates/vite.config.ts +4 -0
- package/scripts/text.mjs +6 -0
- package/scripts/tracker/account.mjs +5 -0
- package/scripts/tracker/auth.mjs +9 -0
- package/scripts/tracker/cli.version.mjs +5 -0
- package/scripts/tracker/close.mjs +20 -0
- package/scripts/tracker/create.mjs +47 -0
- package/scripts/tracker/issue.section.mjs +25 -0
- package/scripts/tracker/issue.update.mjs +13 -0
- package/scripts/tracker/issue.view.mjs +16 -0
- package/scripts/tracker/lib.mjs +130 -0
- package/scripts/tracker/list.mjs +14 -0
- package/scripts/tracker/list.whole.mjs +20 -0
- package/scripts/tracker/lot.row.mjs +42 -0
- package/scripts/tracker/mr.checkout_command.mjs +5 -0
- package/scripts/tracker/mr.comments.mjs +50 -0
- package/scripts/tracker/mr.create.mjs +12 -0
- package/scripts/tracker/mr.description.mjs +8 -0
- package/scripts/tracker/mr.fetch_head.mjs +5 -0
- package/scripts/tracker/mr.find_for_branch.mjs +9 -0
- package/scripts/tracker/mr.post.mjs +40 -0
- package/scripts/tracker/mr.update_description.mjs +8 -0
- package/scripts/tracker/mr.verdict.mjs +34 -0
- package/scripts/tracker/mr.view.mjs +20 -0
- package/scripts/tracker/none.mjs +79 -0
- package/scripts/tracker/note.mjs +12 -0
- package/scripts/tracker/reopen.mjs +8 -0
- package/scripts/tracker/search.mjs +8 -0
- package/scripts/tracker/search_id.mjs +18 -0
- package/scripts/tracker/templates/bug.md +35 -0
- package/scripts/tracker/templates/feature.md +40 -0
- package/scripts/tracker/templates/lot.md +46 -0
- package/scripts/tracker/title.join.mjs +6 -0
- package/scripts/tracker/title.leave.mjs +7 -0
- package/scripts/tracker/title.lot_of.mjs +8 -0
- package/scripts/tracker/title.search_id_filter.mjs +17 -0
- package/scripts/tracker/title.sentence_of.mjs +5 -0
- package/scripts/tracker/view.mjs +25 -0
- package/scripts/verbs.mjs +579 -0
- package/scripts/verify/BUILD.mjs +33 -0
- package/scripts/verify/DEPS.mjs +15 -0
- package/scripts/verify/GOV.mjs +34 -0
- package/scripts/verify/GUARD.mjs +34 -0
- package/scripts/verify/LINES.mjs +22 -0
- package/scripts/verify/PKG.mjs +56 -0
- package/scripts/verify/TYPE.mjs +34 -0
- package/scripts/verify/cut.mjs +73 -0
- package/scripts/verify/history.mjs +32 -0
- package/scripts/verify/lib.mjs +129 -0
- package/scripts/verify/report.mjs +104 -0
- package/scripts/verify/timing.mjs +19 -0
- package/scripts/verify.mjs +29 -0
- package/scripts/work/branch.mjs +81 -0
- package/scripts/work/close.mjs +74 -0
- package/scripts/work/member.mjs +73 -0
- package/scripts/work/prepare.mjs +116 -0
- package/scripts/work/reopen.mjs +40 -0
- package/scripts/work-feedback/prepare.mjs +96 -0
- package/skills/audit/SKILL.md +110 -0
- package/skills/audit/templates/index.md +36 -0
- package/skills/audit/templates/lens.md +44 -0
- package/skills/brainstorm/SKILL.md +144 -0
- package/skills/brainstorm/references/exit.md +57 -0
- package/skills/brainstorm/templates/note.md +79 -0
- package/skills/commit/SKILL.md +68 -0
- package/skills/mr/SKILL.md +82 -0
- package/skills/new-issue/SKILL.md +87 -0
- package/skills/onboard/SKILL.md +103 -0
- package/skills/onboard/templates/CLAUDE.md +31 -0
- package/skills/onboard/templates/CONTRIBUTING.md +34 -0
- package/skills/onboard/templates/README.md +30 -0
- package/skills/onboard/templates/SKILL.md +24 -0
- package/skills/onboard/templates/package-CLAUDE.md +15 -0
- package/skills/overview/SKILL.md +59 -0
- package/skills/port-toolchain/SKILL.md +127 -0
- package/skills/release/SKILL.md +78 -0
- package/skills/review-mr/SKILL.md +106 -0
- package/skills/review-mr/templates/pass.md +29 -0
- package/skills/roadmap/SKILL.md +137 -0
- package/skills/setup/SKILL.md +112 -0
- package/skills/work/SKILL.md +139 -0
- package/skills/work-feedback/SKILL.md +113 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// The cut: the cases a range of the specification covers, assembled into lots from the blocks
|
|
2
|
+
// `prerequisites` returned, one per capability. A case the suite cites — a tracked file outside
|
|
3
|
+
// `docs/` with a quoted string opening on its identifier and a space, a test's title — is built: it
|
|
4
|
+
// leaves the cut, and counts as a prerequisite met. Cases that presume each other are one unit, never
|
|
5
|
+
// split. A unit's stage is the highest of its prerequisites', one more for a prerequisite in another
|
|
6
|
+
// capability, so a case shares a lot with what its own capability built before it and waits for
|
|
7
|
+
// what another builds; a lot is one capability at one stage, and one over the bound is split in
|
|
8
|
+
// consecutive parts, each after the one before. Then the fill: a lot under the bound takes in the
|
|
9
|
+
// next lot of the plan's order its decision last wrote, within the bound, and never one that would
|
|
10
|
+
// make it wait for itself. A prerequisite outside the cut is not this plan's
|
|
11
|
+
// to build: it orders nothing here, and its lot's `outside` line names it for the filing to find
|
|
12
|
+
// the open lot that builds it, if one does. A lot also waits for the lot before it in each of its
|
|
13
|
+
// capabilities, so two lots never change one area of the code at once; and the lots print in the order
|
|
14
|
+
// that brings the journeys to playable as early as possible, the most preferred first.
|
|
15
|
+
// `node assemble.mjs <cases> <cut> <journeys> <block>…`, where <cases> is what the form script's
|
|
16
|
+
// `cases` mode printed, <cut> the case identifiers the range covers, one per line, each followed
|
|
17
|
+
// by a tab and the commit of the range that last wrote it or `none`, and <journeys>
|
|
18
|
+
// the journeys most preferred first, one per line — one it omits comes after, in <cases>' order,
|
|
19
|
+
// and a lot that completes none last. One fact per line,
|
|
20
|
+
// tab-separated, kind first: `refused <block> <case> <identifier>` for each prerequisite that is no
|
|
21
|
+
// case of the specification, `built <case> <files>` for each case of the cut the suite cites, then
|
|
22
|
+
// `lot <first case> <capabilities> <count> <after|none>`, a merged lot's capabilities space-separated, then that lot's
|
|
23
|
+
// `member <lot> <rule> <cases>`, `journey <lot> <J-n>`, `outside <lot> <cases>` and
|
|
24
|
+
// `flag <lot> <cycle|over|members|split|fill|spread> <what>` — every grouping the table must show.
|
|
25
|
+
// What it cannot judge — a cut identifier that is no case or hands no decision, a case no block
|
|
26
|
+
// answers, a block that read nothing, a preferred line that is no journey, a suite it could not
|
|
27
|
+
// read — exits 1, each named, with nothing cut: an unjudged case would look like one that presumes
|
|
28
|
+
// nothing, a lookup not made like a case no commit wrote, an unread preference like none given, an
|
|
29
|
+
// unread suite like one that cites nothing.
|
|
30
|
+
import '../node-floor.mjs';
|
|
31
|
+
import { execFileSync } from 'node:child_process';
|
|
32
|
+
import { readFileSync } from 'node:fs';
|
|
33
|
+
|
|
34
|
+
// The bound, here and nowhere else: about ten cases a lot.
|
|
35
|
+
const BOUND = 10;
|
|
36
|
+
|
|
37
|
+
const [casesFile, cutFile, journeysFile, ...blockFiles] = process.argv.slice(2);
|
|
38
|
+
if (!journeysFile || !blockFiles.length) {
|
|
39
|
+
console.error('usage: node assemble.mjs <cases> <cut> <journeys> <block>…');
|
|
40
|
+
process.exit(2);
|
|
41
|
+
}
|
|
42
|
+
const lines = (file) => readFileSync(file, 'utf8').split('\n').filter((l) => l.trim());
|
|
43
|
+
|
|
44
|
+
const of = new Map(), capabilities = new Map(), journeys = [];
|
|
45
|
+
for (const line of lines(casesFile)) {
|
|
46
|
+
const [kind, id, a = '', b] = line.split('\t');
|
|
47
|
+
if (kind === 'case') {
|
|
48
|
+
if (!capabilities.has(a)) capabilities.set(a, capabilities.size);
|
|
49
|
+
of.set(id, { capability: a, rule: b, place: of.size });
|
|
50
|
+
}
|
|
51
|
+
if (kind === 'journey') journeys.push([id, a.split(' ').filter(Boolean)]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// An identifier that is no case of the specification — one no heading carries, or a rule's — is
|
|
55
|
+
// refused, named with its block, and builds nothing: a case presumes cases.
|
|
56
|
+
const stops = [], refused = [], presumes = new Map();
|
|
57
|
+
for (const file of blockFiles) {
|
|
58
|
+
const text = readFileSync(file, 'utf8');
|
|
59
|
+
if (/^- (capability|index): not passed/m.test(text)) stops.push(`${file}: the block read nothing`);
|
|
60
|
+
else for (const m of text.matchAll(/^- (C-\d+): (.+)$/gm)) {
|
|
61
|
+
const cited = m[2].trim() === 'none' ? [] : m[2].trim().split(/\s+/);
|
|
62
|
+
for (const id of cited.filter((id) => !of.has(id))) refused.push(['refused', file, m[1], id]);
|
|
63
|
+
presumes.set(m[1], cited.filter((id) => of.has(id)));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// The citations, by case: `git grep` reads the tracked files only, and exits 1 when nothing matches;
|
|
67
|
+
// `-z` ends each path on a NUL, so a name git would quote comes back as it is.
|
|
68
|
+
const citing = new Map();
|
|
69
|
+
let grep = '';
|
|
70
|
+
try {
|
|
71
|
+
grep = execFileSync('git', ['grep', '--full-name', '--no-color', '--no-line-number', '--no-column', '-z', '-I', '-o', '-E', '["\'`]C-[0-9]+', '--', ':/', ':(top,exclude)docs'], { encoding: 'utf8', maxBuffer: 1 << 28 });
|
|
72
|
+
} catch (e) {
|
|
73
|
+
if (e.status !== 1) stops.push(`the suite's citations: ${String(e.stderr || e.message).trim()}`);
|
|
74
|
+
}
|
|
75
|
+
for (const m of grep.matchAll(/^([^\0\n]*)\0["'`](C-\d+)$/gm)) citing.set(m[2], [...new Set([...(citing.get(m[2]) || []), m[1]])]);
|
|
76
|
+
// A cut line is a case, then the commit of the range that last wrote it — its decision — or `none`,
|
|
77
|
+
// which fills nothing.
|
|
78
|
+
const cut = [], built = [], decisionOf = new Map();
|
|
79
|
+
for (const [c, commit] of lines(cutFile).map((l) => l.trim().split(/\s+/))) {
|
|
80
|
+
if (cut.includes(c) || built.includes(c)) continue;
|
|
81
|
+
if (!commit) stops.push(`${c}: no decision handed, nor none`);
|
|
82
|
+
else if (commit !== 'none') decisionOf.set(c, commit);
|
|
83
|
+
(of.has(c) && citing.has(c) ? built : cut).push(c);
|
|
84
|
+
}
|
|
85
|
+
for (const c of cut) {
|
|
86
|
+
if (!of.has(c)) stops.push(`${c}: no case of the specification`);
|
|
87
|
+
else if (!presumes.has(c)) stops.push(`${c}: no block answers it`);
|
|
88
|
+
}
|
|
89
|
+
const preferred = lines(journeysFile).map((l) => l.trim());
|
|
90
|
+
for (const j of preferred) if (!journeys.some(([id]) => id === j)) stops.push(`${j}: no journey of the specification`);
|
|
91
|
+
if (stops.length) {
|
|
92
|
+
for (const s of stops) console.log(s);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
cut.sort((a, b) => of.get(a).place - of.get(b).place);
|
|
96
|
+
const inCut = new Set(cut);
|
|
97
|
+
const deps = (c) => presumes.get(c).filter((p) => inCut.has(p) && p !== c);
|
|
98
|
+
|
|
99
|
+
// The units, Tarjan's components: each is emitted after every unit it presumes, so the list is in
|
|
100
|
+
// prerequisite order.
|
|
101
|
+
const units = [], unitOf = new Map(), index = new Map(), low = new Map(), stack = [], held = new Set();
|
|
102
|
+
const visit = (c) => {
|
|
103
|
+
index.set(c, index.size);
|
|
104
|
+
low.set(c, index.get(c));
|
|
105
|
+
stack.push(c);
|
|
106
|
+
held.add(c);
|
|
107
|
+
for (const p of deps(c)) {
|
|
108
|
+
if (!index.has(p)) {
|
|
109
|
+
visit(p);
|
|
110
|
+
low.set(c, Math.min(low.get(c), low.get(p)));
|
|
111
|
+
} else if (held.has(p)) low.set(c, Math.min(low.get(c), index.get(p)));
|
|
112
|
+
}
|
|
113
|
+
if (low.get(c) !== index.get(c)) return;
|
|
114
|
+
const unit = { cases: [] };
|
|
115
|
+
for (let p; p !== c; ) {
|
|
116
|
+
p = stack.pop();
|
|
117
|
+
held.delete(p);
|
|
118
|
+
unit.cases.push(p);
|
|
119
|
+
unitOf.set(p, unit);
|
|
120
|
+
}
|
|
121
|
+
unit.cases.sort((a, b) => of.get(a).place - of.get(b).place);
|
|
122
|
+
unit.capability = of.get(unit.cases[0]).capability;
|
|
123
|
+
unit.place = of.get(unit.cases[0]).place;
|
|
124
|
+
units.push(unit);
|
|
125
|
+
};
|
|
126
|
+
for (const c of cut) if (!index.has(c)) visit(c);
|
|
127
|
+
const unitDeps = (u) => [...new Set(u.cases.flatMap(deps).map((p) => unitOf.get(p)))].filter((d) => d !== u);
|
|
128
|
+
for (const u of units) u.stage = Math.max(0, ...unitDeps(u).map((d) => d.stage + (d.capability === u.capability ? 0 : 1)));
|
|
129
|
+
|
|
130
|
+
// ordered <items> <before> — items in prerequisite order, the earliest place first among those
|
|
131
|
+
// ready; what a cycle leaves is appended in place order and returned as stuck.
|
|
132
|
+
const ordered = (items, before, place) => {
|
|
133
|
+
const out = [], left = [...items].sort((a, b) => place(a) - place(b));
|
|
134
|
+
while (left.length) {
|
|
135
|
+
const i = left.findIndex((x) => before(x).every((y) => !left.includes(y)));
|
|
136
|
+
if (i < 0) return { out: out.concat(left), stuck: left };
|
|
137
|
+
out.push(...left.splice(i, 1));
|
|
138
|
+
}
|
|
139
|
+
return { out, stuck: [] };
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const groups = new Map();
|
|
143
|
+
for (const u of units) {
|
|
144
|
+
const key = `${u.stage}\t${u.capability}`;
|
|
145
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
146
|
+
groups.get(key).push(u);
|
|
147
|
+
}
|
|
148
|
+
const lots = [], lotOf = new Map();
|
|
149
|
+
for (const group of [...groups.values()].sort((a, b) => a[0].stage - b[0].stage || capabilities.get(a[0].capability) - capabilities.get(b[0].capability))) {
|
|
150
|
+
const inGroup = ordered(group, (u) => unitDeps(u).filter((d) => group.includes(d)), (u) => u.place).out;
|
|
151
|
+
const size = inGroup.reduce((n, u) => n + u.cases.length, 0);
|
|
152
|
+
const target = Math.ceil(size / Math.ceil(size / BOUND));
|
|
153
|
+
let lot;
|
|
154
|
+
const parts = [];
|
|
155
|
+
for (const u of inGroup) {
|
|
156
|
+
if (!lot || lot.cases.length + u.cases.length > target) {
|
|
157
|
+
lots.push((lot = { capabilities: [u.capability], cases: [], units: [], split: [], took: [] }));
|
|
158
|
+
parts.push(lot);
|
|
159
|
+
}
|
|
160
|
+
lot.cases.push(...u.cases);
|
|
161
|
+
lot.units.push(u);
|
|
162
|
+
}
|
|
163
|
+
if (parts.length > 1) parts.forEach((p, i) => p.split.push(`part ${i + 1} of ${parts.length}`));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// The lots a lot's cases presume; its members, its cases by rule in their prerequisites' order; and
|
|
167
|
+
// its tag, the first case its first member builds.
|
|
168
|
+
const presumed = (lot) => [...new Set(lot.cases.flatMap(deps).map((p) => lotOf.get(p)))].filter((l) => l !== lot);
|
|
169
|
+
const casesOf = (lot, r) => lot.cases.filter((c) => of.get(c).rule === r);
|
|
170
|
+
const membersOf = (lot) => {
|
|
171
|
+
const rules = [...new Set(lot.cases.map((c) => of.get(c).rule))];
|
|
172
|
+
return ordered(rules, (r) => [...new Set(casesOf(lot, r).flatMap(deps).filter((p) => lotOf.get(p) === lot).map((p) => of.get(p).rule))].filter((x) => x !== r), (r) => lot.cases.indexOf(casesOf(lot, r)[0]));
|
|
173
|
+
};
|
|
174
|
+
const tagOf = (lot, members = membersOf(lot)) => casesOf(lot, members.out[0])[0];
|
|
175
|
+
|
|
176
|
+
// settle — the lots in their prerequisites' order; a journey named on the last lot holding its
|
|
177
|
+
// cases, which waits for the others, its independent test not passing before they close; a lot
|
|
178
|
+
// waiting for its cases' lots and for the lot before it in each of its capabilities, so two lots
|
|
179
|
+
// never change one area of the code at once; then the plan's order, each journey's last lot in
|
|
180
|
+
// preference, everything it waits for placed before it.
|
|
181
|
+
const last = new Map();
|
|
182
|
+
const rank = (j) => (preferred.includes(j) ? preferred.indexOf(j) : preferred.length);
|
|
183
|
+
const settle = () => {
|
|
184
|
+
lotOf.clear();
|
|
185
|
+
for (const lot of lots) for (const c of lot.cases) lotOf.set(c, lot);
|
|
186
|
+
lots.splice(0, lots.length, ...ordered(lots, presumed, (l) => lots.indexOf(l)).out);
|
|
187
|
+
last.clear();
|
|
188
|
+
const after = new Map(lots.map((l) => [l, []]));
|
|
189
|
+
for (const [j, cited] of journeys) {
|
|
190
|
+
const mine = cited.filter((c) => inCut.has(c)).map((c) => lotOf.get(c));
|
|
191
|
+
if (!mine.length) continue;
|
|
192
|
+
const at = lots[Math.max(...mine.map((l) => lots.indexOf(l)))];
|
|
193
|
+
last.set(j, at);
|
|
194
|
+
after.get(at).push(...mine.filter((l) => l !== at));
|
|
195
|
+
}
|
|
196
|
+
for (const lot of lots) {
|
|
197
|
+
const earlier = lots.slice(0, lots.indexOf(lot));
|
|
198
|
+
const before = lot.capabilities.map((k) => earlier.filter((l) => l.capabilities.includes(k)).pop());
|
|
199
|
+
lot.waits = [...new Set([...after.get(lot), ...presumed(lot), ...before])].filter((l) => l && l !== lot);
|
|
200
|
+
}
|
|
201
|
+
const placed = [];
|
|
202
|
+
const place = (lot) => {
|
|
203
|
+
if (placed.includes(lot)) return;
|
|
204
|
+
for (const l of [...lot.waits].sort((a, b) => lots.indexOf(a) - lots.indexOf(b))) place(l);
|
|
205
|
+
placed.push(lot);
|
|
206
|
+
};
|
|
207
|
+
for (const j of [...last.keys()].sort((a, b) => rank(a) - rank(b))) place(last.get(j));
|
|
208
|
+
for (const lot of lots) place(lot);
|
|
209
|
+
return placed;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// The fill. A lot's decision is the one commit that last wrote all its cases, none when they name
|
|
213
|
+
// two or none. Walking the plan's order, a lot under the bound takes in the next lot of that order
|
|
214
|
+
// of its decision — a lot of another passed over — while the merged lot stays within the bound and
|
|
215
|
+
// nothing it presumes presumes it in turn; the first that does not fit ends its fill. A merged lot
|
|
216
|
+
// counts as each of its capabilities, and the order is settled again around it.
|
|
217
|
+
const decision = (lot) => {
|
|
218
|
+
const d = new Set(lot.cases.map((c) => decisionOf.get(c)));
|
|
219
|
+
return d.size === 1 ? [...d][0] : undefined;
|
|
220
|
+
};
|
|
221
|
+
const cycles = (a, b) => {
|
|
222
|
+
const seen = new Set(), todo = [...presumed(a), ...presumed(b)].filter((l) => l !== a && l !== b);
|
|
223
|
+
for (let l; (l = todo.pop()); ) {
|
|
224
|
+
if (l === a || l === b) return true;
|
|
225
|
+
if (!seen.has(l)) seen.add(l), todo.push(...presumed(l));
|
|
226
|
+
}
|
|
227
|
+
return false;
|
|
228
|
+
};
|
|
229
|
+
const planned = settle();
|
|
230
|
+
for (const lot of planned) {
|
|
231
|
+
const d = decision(lot);
|
|
232
|
+
if (lot.into || !d) continue;
|
|
233
|
+
for (const next of planned.slice(planned.indexOf(lot) + 1)) {
|
|
234
|
+
if (next.into || decision(next) !== d) continue;
|
|
235
|
+
if (lot.cases.length + next.cases.length > BOUND || cycles(lot, next)) break;
|
|
236
|
+
next.into = lot;
|
|
237
|
+
lot.took.push(tagOf(next));
|
|
238
|
+
lot.cases.push(...next.cases);
|
|
239
|
+
lot.units.push(...next.units);
|
|
240
|
+
lot.split.push(...next.split);
|
|
241
|
+
lot.capabilities = [...new Set([...lot.capabilities, ...next.capabilities])];
|
|
242
|
+
for (const c of next.cases) lotOf.set(c, lot);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
lots.splice(0, lots.length, ...lots.filter((l) => !l.into));
|
|
246
|
+
const placed = settle();
|
|
247
|
+
for (const lot of lots) (lot.members = membersOf(lot)), (lot.tag = tagOf(lot, lot.members));
|
|
248
|
+
|
|
249
|
+
const print = (...fields) => console.log(fields.join('\t'));
|
|
250
|
+
for (const r of refused) print(...r);
|
|
251
|
+
for (const c of built.sort((a, b) => of.get(a).place - of.get(b).place)) print('built', c, citing.get(c).join(' '));
|
|
252
|
+
for (const lot of placed) {
|
|
253
|
+
const first = lot.tag, members = lot.members;
|
|
254
|
+
const after = [...lot.waits].sort((a, b) => placed.indexOf(a) - placed.indexOf(b));
|
|
255
|
+
print('lot', first, lot.capabilities.join(' '), lot.cases.length, after.map((l) => l.tag).join(' ') || 'none');
|
|
256
|
+
for (const r of members.out) print('member', first, r, casesOf(lot, r).join(' '));
|
|
257
|
+
for (const [j, l] of last) if (l === lot) print('journey', first, j);
|
|
258
|
+
const outside = [...new Set(lot.cases.flatMap((c) => presumes.get(c)).filter((p) => !inCut.has(p) && !citing.has(p)))];
|
|
259
|
+
if (outside.length) print('outside', first, outside.join(' '));
|
|
260
|
+
for (const u of lot.units) if (u.cases.length > 1) print('flag', first, 'cycle', u.cases.join(' '));
|
|
261
|
+
for (const u of lot.units) if (u.cases.length > BOUND) print('flag', first, 'over', `${u.cases.length} cases presume each other, over the bound of ${BOUND}`);
|
|
262
|
+
if (members.stuck.length) print('flag', first, 'members', `${members.stuck.join(' ')} presume each other's cases`);
|
|
263
|
+
for (const s of lot.split) print('flag', first, 'split', s);
|
|
264
|
+
if (lot.took.length) print('flag', first, 'fill', lot.took.join(' '));
|
|
265
|
+
for (const r of members.out) {
|
|
266
|
+
const also = lots.filter((l) => l !== lot && l.cases.some((c) => of.get(c).rule === r));
|
|
267
|
+
if (also.length) print('flag', first, 'spread', `${r} also in ${also.map((l) => l.tag).join(' ')}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// /roadmap's cut, one command (K-8): the lots of a run folder `codelife roadmap prep` wrote, from the
|
|
2
|
+
// `prerequisites` blocks saved in its blocks/ — assemble.mjs, then what it cannot see: each lot's
|
|
3
|
+
// place after the confirmed order, the open lot building a case it presumes outside the cut, the
|
|
4
|
+
// open lot of each of its capabilities it waits for (R-264), an orphan citing one of a member's
|
|
5
|
+
// cases proposed as that member, and an effort from the member's count of cases. Writes the whole
|
|
6
|
+
// table to table.md in the folder and prints a `### cut` summary, one line per lot; a `- stop:`
|
|
7
|
+
// line — what assemble could not judge — exits 1. `codelife roadmap cut <run folder>`.
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { dirname, join } from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { prep, said } from '../prepare-lib.mjs';
|
|
13
|
+
|
|
14
|
+
const dir = process.argv[2];
|
|
15
|
+
if (!dir || !existsSync(join(dir, 'cut.tsv'))) {
|
|
16
|
+
console.error('usage: codelife roadmap cut <the run folder codelife roadmap prep printed>');
|
|
17
|
+
process.exit(2);
|
|
18
|
+
}
|
|
19
|
+
const { say, stop, done } = prep('cut');
|
|
20
|
+
const read = (f) => readFileSync(join(dir, f), 'utf8');
|
|
21
|
+
const tsv = (f) => read(f).split('\n').filter(Boolean).map((l) => l.split('\t'));
|
|
22
|
+
const cases = tsv('cases.tsv');
|
|
23
|
+
const caseOf = new Map(cases.filter((x) => x[0] === 'case').map(([, id, k, rule]) => [id, { k, rule }]));
|
|
24
|
+
const ruleOf = new Map(cases.filter((x) => x[0] === 'rule').map(([, id, k]) => [id, k]));
|
|
25
|
+
const heading = new Map(cases.filter((x) => x[0] === 'index').map(([, l]) => [l.split(' — ')[0], l]));
|
|
26
|
+
const issues = JSON.parse(read('issues.json')), map = JSON.parse(read('roadmap.json'));
|
|
27
|
+
const section = (text, h) => text.split(new RegExp(`^## ${h}[ \\t]*$`, 'm'))[1]?.split(/^## /m)[0] ?? '';
|
|
28
|
+
const cites = (text, id) => new RegExp(`(^|[^\\w-])${id}(?![\\w-])`).test(section(text, 'Where it comes from'));
|
|
29
|
+
|
|
30
|
+
const lots = [];
|
|
31
|
+
if (tsv('cut.tsv').length) {
|
|
32
|
+
const blocks = existsSync(join(dir, 'blocks')) ? readdirSync(join(dir, 'blocks')).map((f) => join(dir, 'blocks', f)) : [];
|
|
33
|
+
if (!blocks.length) stop(`no block in ${join(dir, 'blocks')} — save each prerequisites agent's block there`);
|
|
34
|
+
const a = spawnSync(process.execPath, [join(dirname(fileURLToPath(import.meta.url)), 'assemble.mjs'), join(dir, 'cases.tsv'), join(dir, 'cut.tsv'), join(dir, 'journeys.txt'), ...blocks], { encoding: 'utf8' });
|
|
35
|
+
if (a.status !== 0) stop(`assemble could not judge: ${said(a).split('\n').join('; ')}`);
|
|
36
|
+
for (const [kind, lot, ...f] of a.stdout.split('\n').filter(Boolean).map((l) => l.split('\t'))) {
|
|
37
|
+
if (kind === 'refused') say(`refused: ${f[1]}, given for ${f[0]} in ${lot.split('/').pop()}, is no case of the specification`);
|
|
38
|
+
else if (kind === 'built') say(`built: ${lot} — cited by ${f[0]}`);
|
|
39
|
+
else if (kind === 'lot') lots.push({ first: lot, caps: f[0].split(' '), after: f[2] === 'none' ? [] : f[2].split(' '), members: [], journeys: [], outside: [], flags: [] });
|
|
40
|
+
else {
|
|
41
|
+
const l = lots.find((x) => x.first === lot);
|
|
42
|
+
if (kind === 'member') l.members.push({ rule: f[0], cases: f[1].split(' ') });
|
|
43
|
+
else if (kind === 'journey') l.journeys.push(f[0]);
|
|
44
|
+
else if (kind === 'outside') l.outside.push(...f[0].split(' '));
|
|
45
|
+
else if (kind === 'flag') l.flags.push(`${f[0]} ${f[1]}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// What an open umbrella builds: the capabilities its origin and its members' origins name.
|
|
51
|
+
const capsOf = (u) => {
|
|
52
|
+
const ids = [u.title, ...issues.filter((i) => i.lot === u.n).map((i) => i.title)].map((t) => t.match(/^\[(?:lot · )?spec:([A-Z]-\d+)/)?.[1]).filter(Boolean);
|
|
53
|
+
return new Set(ids.map((id) => id.startsWith('K-') ? id : caseOf.get(id)?.k ?? ruleOf.get(id)).filter(Boolean));
|
|
54
|
+
};
|
|
55
|
+
const umbrellas = issues.filter((i) => i.labels.includes('lot'));
|
|
56
|
+
const ranked = [...map.order.map((o) => umbrellas.find((u) => u.n === o.lot)).filter(Boolean), ...umbrellas.filter((u) => !map.order.some((o) => o.lot === u.n))];
|
|
57
|
+
const seen = new Set();
|
|
58
|
+
const effort = (n) => (n <= 2 ? 'S' : n <= 5 ? 'M' : 'L');
|
|
59
|
+
const table = [`# The cut`, '', `Run folder: ${dir}. Each lot after the roadmap's ${map.order.length} confirmed lines, in this order.`, ''];
|
|
60
|
+
lots.forEach((l, i) => {
|
|
61
|
+
// Outside the cut: the open lot whose issue builds the case; none, and the case is built.
|
|
62
|
+
for (const c of l.outside) {
|
|
63
|
+
const b = issues.find((x) => x.lot && cites(x.body, c));
|
|
64
|
+
if (b && !l.after.includes(`#${b.lot}`)) l.after.push(`#${b.lot}`);
|
|
65
|
+
}
|
|
66
|
+
// A capability's first lot of this cut waits for the last open lot of that capability.
|
|
67
|
+
for (const k of l.caps) {
|
|
68
|
+
if (seen.has(k)) continue;
|
|
69
|
+
seen.add(k);
|
|
70
|
+
const u = ranked.filter((x) => capsOf(x).has(k)).at(-1);
|
|
71
|
+
if (u && !l.after.includes(`#${u.n}`)) l.after.push(`#${u.n}`);
|
|
72
|
+
}
|
|
73
|
+
for (const m of l.members) m.existing = issues.find((x) => !x.lot && !x.labels.includes('lot') && m.cases.some((id) => cites(x.body, id)))?.n;
|
|
74
|
+
const efforts = l.members.map((m) => effort(m.cases.length));
|
|
75
|
+
const rank = map.order.length + i + 1;
|
|
76
|
+
say(`lot: ${rank} — spec:${l.first} — ${l.caps.join(' ')} — ${l.members.length} members, ${['S', 'M', 'L'].map((e) => `${efforts.filter((x) => x === e).length}${e}`).filter((x) => !x.startsWith('0')).join(' ')} — after ${l.after.join(', ') || 'none'} — journeys ${l.journeys.join(' ') || 'none'}`);
|
|
77
|
+
for (const f of l.flags) say(`flag: spec:${l.first} — ${f}`);
|
|
78
|
+
table.push(`## ${rank}. [lot · spec:${l.first}] — ${l.caps.join(', ')} — after ${l.after.join(', ') || 'none'}`, '',
|
|
79
|
+
`Journeys it completes: ${l.journeys.join(', ') || 'none'}. Outside the cut: ${l.outside.join(', ') || 'none'}. Flags: ${l.flags.join('; ') || 'none'}.`, '',
|
|
80
|
+
'| # | Rule | Cases | Effort | Issue |', '|---|---|---|---|---|',
|
|
81
|
+
...l.members.map((m, j) => `| ${j + 1} | ${heading.get(m.rule) ?? m.rule} | ${m.cases.map((c) => heading.get(c) ?? c).join('<br>')} | ${effort(m.cases.length)} | ${m.existing ? `#${m.existing} existing` : 'new'} |`), '');
|
|
82
|
+
});
|
|
83
|
+
const built = tsv('built.tsv');
|
|
84
|
+
table.push('## Proposed built', '', ...built.map(([c, m]) => `- ${heading.get(c) ?? c} — merged with its code in ${m}`), ...(built.length ? [] : ['None.']), '');
|
|
85
|
+
writeFileSync(join(dir, 'table.md'), table.join('\n'));
|
|
86
|
+
say(`lots: ${lots.length}${lots.length ? '' : ' — nothing to cut'}`);
|
|
87
|
+
say(`table: ${join(dir, 'table.md')}`);
|
|
88
|
+
done();
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// /roadmap's preparation, one command (K-8): everything a plan is made of that no judgement decides.
|
|
2
|
+
// The roadmap, its mark and its order, or a first run's starting point; what docs/spec/ changed from
|
|
3
|
+
// there to HEAD, each case with its decision — the commit of the range that last wrote it — and
|
|
4
|
+
// proposed built when the merge that brought that commit changed code too (R-184); the open
|
|
5
|
+
// issues, each written to a file, sorted into the umbrellas, the members and the orphans; a member
|
|
6
|
+
// whose latest plan waits for a case, and who builds it; the open issues citing what the range
|
|
7
|
+
// changed; one folder per capability to cut. Writes nothing on the forge or in the tree: a run folder
|
|
8
|
+
// outside it, and a `### prepare` summary. A `- stop:` line ends the run before any table, exit 1.
|
|
9
|
+
// `codelife roadmap prep [<label-or-milestone>] [--from <sha>]`, from the repository's root.
|
|
10
|
+
import { spawnSync } from 'node:child_process';
|
|
11
|
+
import { copyFileSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import { basename, dirname, join } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { first, prep, said } from '../prepare-lib.mjs';
|
|
16
|
+
import { lotCount, orderLine, range, roadmap } from '../read-roadmap/lib.mjs';
|
|
17
|
+
|
|
18
|
+
const usage = () => { console.error('usage: codelife roadmap prep [<label-or-milestone>] [--from <sha>]'); process.exit(2); };
|
|
19
|
+
const argv = process.argv.slice(2);
|
|
20
|
+
let from = null, scope = null;
|
|
21
|
+
for (let i = 0; i < argv.length; i++) {
|
|
22
|
+
if (argv[i] === '--from') from = argv[++i] ?? usage();
|
|
23
|
+
else if (scope === null) scope = argv[i];
|
|
24
|
+
else usage();
|
|
25
|
+
}
|
|
26
|
+
if (from !== null && !/^[0-9a-f]{7,64}$/.test(from)) usage();
|
|
27
|
+
|
|
28
|
+
const { say, stop, done, op, must, title, kind } = prep('prepare');
|
|
29
|
+
const git = (...a) => spawnSync('git', a, { encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
|
|
30
|
+
const out = (...a) => { const r = git(...a); if (r.status !== 0) stop(`git ${a[0]} — ${said(r)}`); return r.stdout.trim(); };
|
|
31
|
+
const section = (text, heading) => text.split(new RegExp(`^## ${heading}[ \\t]*$`, 'm'))[1]?.split(/^## /m)[0] ?? '';
|
|
32
|
+
const cites = (text, id) => new RegExp(`(^|[^\\w-])${id}(?![\\w-])`).test(text);
|
|
33
|
+
|
|
34
|
+
// The forge, as /work's preparation reads it: one that does not answer is a tracker nobody can plan.
|
|
35
|
+
const forge = kind();
|
|
36
|
+
if (forge !== 'none') {
|
|
37
|
+
const auth = op('tracker', 'auth');
|
|
38
|
+
if (auth.status !== 0) stop(`the forge's authentication — ${said(auth)}`);
|
|
39
|
+
}
|
|
40
|
+
const head = out('rev-parse', 'HEAD');
|
|
41
|
+
const dir = mkdtempSync(join(tmpdir(), 'roadmap-'));
|
|
42
|
+
say(`run: ${dir}`);
|
|
43
|
+
say(`forge: ${forge}`);
|
|
44
|
+
say(`head: ${head.slice(0, 8)} — ${out('log', '-1', '--format=%s', 'HEAD')}`);
|
|
45
|
+
|
|
46
|
+
// The roadmap (R-263): its mark is where the range starts, its order the confirmed one (R-264).
|
|
47
|
+
const r = roadmap();
|
|
48
|
+
if (r.roadmap.startsWith('could not tell')) stop(`the roadmap — ${r.roadmap}`);
|
|
49
|
+
if (r.roadmap.startsWith('two')) stop(`${r.roadmap.replace('two: ', 'two roadmaps: ')} — close the one you do not keep on the forge, then run again`);
|
|
50
|
+
const map = { n: r.roadmap.match(/^#(\d+)$/)?.[1] ?? null, head, order: [], left: [] };
|
|
51
|
+
let start = null, firstRun = false, citation = 'none';
|
|
52
|
+
if (map.n) {
|
|
53
|
+
say(`roadmap: #${map.n}`);
|
|
54
|
+
if (/^(out of shape|unresolved)/.test(r.mark)) stop(`the roadmap's mark is ${r.mark} — correct it on the forge`);
|
|
55
|
+
if (r.mark.endsWith('not contained') && !from) {
|
|
56
|
+
stop(`the mark ${r.sha} is not an ancestor of HEAD (on ${out('branch', '--show-current') || 'no branch'}) — name the commit to read from: \`codelife roadmap prep --from <sha>\``);
|
|
57
|
+
}
|
|
58
|
+
start = from ?? r.sha;
|
|
59
|
+
say(`mark: ${r.sha}${from ? `, read from ${from} as you named it` : ''}`);
|
|
60
|
+
const body = JSON.parse(must('tracker', 'view', [map.n])).body;
|
|
61
|
+
for (const line of section(body, 'The order').split('\n').filter((l) => /^\d+\. /.test(l))) {
|
|
62
|
+
const o = orderLine(line);
|
|
63
|
+
if (!o) stop(`a line of the roadmap's order is out of shape: ${line} — correct it on the forge`);
|
|
64
|
+
map.order.push(o);
|
|
65
|
+
}
|
|
66
|
+
map.left = [...section(body, 'Left alone').matchAll(/^- #(\d+)/gm)].map((m) => m[1]);
|
|
67
|
+
say(`order: ${map.order.map((o) => `#${o.lot}${o.after.length ? ` after ${o.after.map((a) => `#${a}`).join(', ')}` : ''}`).join('; ') || 'none'}`);
|
|
68
|
+
} else {
|
|
69
|
+
// A first run (R-263): the lots of origin spec: or citation ever filed, closed ones too.
|
|
70
|
+
firstRun = true;
|
|
71
|
+
say('roadmap: none — a first run');
|
|
72
|
+
const lots = must('tracker', 'list', forge === 'gh' ? ['--label', 'lot', '--state', 'all'] : ['-l', 'lot', '-A']).split('\n');
|
|
73
|
+
const tagged = (tag) => lots.map((l) => l.match(new RegExp(`^#(\\d+)\\t(\\[lot · ${tag}[^\\]]*\\].*)$`))).filter(Boolean);
|
|
74
|
+
const cited = tagged('citation');
|
|
75
|
+
if (cited.length) citation = citationLot(cited);
|
|
76
|
+
if (from) { start = from; say(`start: ${from}, as you named it`); }
|
|
77
|
+
else if (tagged('spec:').length) {
|
|
78
|
+
stop(`no roadmap, but lots of origin spec: exist — name the commit to read from, proposed ${out('log', '-1', '--format=%h', '--', 'docs/spec')}, the last that touched docs/spec/: \`codelife roadmap prep --from <sha>\``);
|
|
79
|
+
} else say('start: the specification from its first commit — no lot was ever filed from it');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// A citation lot already filed decides the pass (R-263): open, or closed before its merge, it stops.
|
|
83
|
+
function citationLot(found) {
|
|
84
|
+
const [, n] = found[0];
|
|
85
|
+
const lot = JSON.parse(must('tracker', 'view', [n]));
|
|
86
|
+
if (!/^closed$/i.test(lot.state)) stop(`the citation lot #${n} is open — the plan is cut once it is merged`);
|
|
87
|
+
for (const [, m] of lot.body.matchAll(/^- \[[ xX]\] #(\d+)/gm)) {
|
|
88
|
+
const closing = JSON.parse(must('tracker', 'view', [m])).comments.map((c) => c.body).filter((b) => first(b).startsWith('codelife: closing — ')).at(-1) ?? '';
|
|
89
|
+
for (const [h] of closing.matchAll(/\b[0-9a-f]{7,40}\b/g)) {
|
|
90
|
+
if (git('merge-base', '--is-ancestor', h, 'HEAD').status !== 0) stop(`the citation lot #${n} is closed, but HEAD lacks ${h} from #${m} — the plan is cut once its merge request is merged`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
say(`citation lot: #${n}, merged — no citation pass`);
|
|
94
|
+
return 'merged';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// What the plan is cut from (K-23's R-271): every rule and case, their capabilities, the journeys.
|
|
98
|
+
const form = spawnSync(process.execPath, [join(dirname(fileURLToPath(import.meta.url)), '..', 'spec-form.mjs'), 'cases'], { encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
|
|
99
|
+
if (form.status !== 0) stop(`codelife form cases — ${said(form)}`);
|
|
100
|
+
writeFileSync(join(dir, 'cases.tsv'), form.stdout);
|
|
101
|
+
const rows = form.stdout.split('\n').map((l) => l.split('\t'));
|
|
102
|
+
const caseOf = new Map(rows.filter((x) => x[0] === 'case').map(([, id, k, rule]) => [id, { k, rule }]));
|
|
103
|
+
const ruleOf = new Map(rows.filter((x) => x[0] === 'rule').map(([, id, k]) => [id, k]));
|
|
104
|
+
const index = rows.filter((x) => x[0] === 'index').map((x) => x.slice(1).join('\t'));
|
|
105
|
+
|
|
106
|
+
// The range: what changed from the start to HEAD, or everything when the run reads from the start.
|
|
107
|
+
let touched, retired = [];
|
|
108
|
+
if (start) {
|
|
109
|
+
const g = range(start, head);
|
|
110
|
+
if (g.unresolved) stop(`the range from ${start} — ${g.unresolved}`);
|
|
111
|
+
touched = [...g.changed, ...g.created].map((t) => t.id);
|
|
112
|
+
retired = g.retired.map((t) => t.id);
|
|
113
|
+
} else touched = [...caseOf.keys()];
|
|
114
|
+
const cut = touched.filter((id) => caseOf.has(id));
|
|
115
|
+
const noCase = touched.filter((id) => ruleOf.has(id) && !cut.some((c) => caseOf.get(c).rule === id));
|
|
116
|
+
|
|
117
|
+
// Each capability's document, by its `# K-<n> — ` heading.
|
|
118
|
+
const docs = new Map();
|
|
119
|
+
const walk = (d) => readdirSync(d, { withFileTypes: true }).forEach((e) => {
|
|
120
|
+
const p = join(d, e.name);
|
|
121
|
+
if (e.isDirectory()) walk(p);
|
|
122
|
+
else if (p.endsWith('.md')) { const k = readFileSync(p, 'utf8').match(/^# (K-\d+) — (.*)$/m); if (k) docs.set(k[1], { path: p, heading: k[2] }); }
|
|
123
|
+
});
|
|
124
|
+
walk('docs/spec');
|
|
125
|
+
|
|
126
|
+
// Each case's decision — the range's last commit writing its lines at HEAD, its heading to its last
|
|
127
|
+
// line that is not blank — and the first-parent commit of HEAD that brought it: a merge that also
|
|
128
|
+
// changed a file outside docs/ brought the case with its code, and it is proposed built.
|
|
129
|
+
const firstParent = new Set(out('rev-list', '--first-parent', 'HEAD').split('\n'));
|
|
130
|
+
const brought = new Map();
|
|
131
|
+
const broughtBy = (sha) => {
|
|
132
|
+
if (!brought.has(sha)) {
|
|
133
|
+
const m = firstParent.has(sha) ? sha : out('rev-list', '--ancestry-path', `${sha}..HEAD`).split('\n').filter((c) => firstParent.has(c)).at(-1);
|
|
134
|
+
const parent = git('rev-parse', '--verify', '-q', `${m}^1`).stdout.trim();
|
|
135
|
+
const files = (parent ? out('diff', '--name-only', parent, m) : out('show', '--name-only', '--format=', m)).split('\n').filter(Boolean);
|
|
136
|
+
brought.set(sha, { merge: m.slice(0, 8), code: files.some((f) => !f.startsWith('docs/')) });
|
|
137
|
+
}
|
|
138
|
+
return brought.get(sha);
|
|
139
|
+
};
|
|
140
|
+
// decision <id> <capability> — the range's last commit writing an identifier's lines at HEAD.
|
|
141
|
+
const decision = (id, k) => {
|
|
142
|
+
const doc = docs.get(k);
|
|
143
|
+
if (!doc) stop(`${id}: no document carries ${k}`);
|
|
144
|
+
const lines = readFileSync(doc.path, 'utf8').split('\n');
|
|
145
|
+
const a = lines.findIndex((l) => new RegExp(`^#+ ${id} — `).test(l));
|
|
146
|
+
let b = lines.findIndex((l, i) => i > a && /^#/.test(l));
|
|
147
|
+
if (b < 0) b = lines.length;
|
|
148
|
+
while (b > a + 1 && !lines[b - 1].trim()) b--;
|
|
149
|
+
const r = git('log', '-1', '--format=%H', '-s', start ? `${start}..HEAD` : 'HEAD', '-L', `${a + 1},${b}:${doc.path}`);
|
|
150
|
+
if (r.status !== 0) stop(`${id}: its decision — ${said(r)}`);
|
|
151
|
+
return r.stdout.trim().split('\n')[0];
|
|
152
|
+
};
|
|
153
|
+
const cutLines = [], builtLines = [];
|
|
154
|
+
for (const c of cut) {
|
|
155
|
+
const sha = decision(c, caseOf.get(c).k), by = sha && broughtBy(sha);
|
|
156
|
+
if (by?.code) builtLines.push(`${c}\t${by.merge}`);
|
|
157
|
+
else cutLines.push(`${c}\t${sha || 'none'}`);
|
|
158
|
+
}
|
|
159
|
+
// A rule whose statement alone changed is a row of no case, unless it came with its code too.
|
|
160
|
+
const unplaced = noCase.filter((id) => { const sha = decision(id, ruleOf.get(id)); return !(sha && broughtBy(sha).code); });
|
|
161
|
+
writeFileSync(join(dir, 'cut.tsv'), cutLines.map((l) => `${l}\n`).join(''));
|
|
162
|
+
writeFileSync(join(dir, 'built.tsv'), builtLines.map((l) => `${l}\n`).join(''));
|
|
163
|
+
const merges = [...new Set(builtLines.map((l) => l.split('\t')[1]))];
|
|
164
|
+
say(`range: ${start ? `${start.slice(0, 8)}..${head.slice(0, 8)}` : 'the whole specification'} — ${cut.length} cases, ${builtLines.length} proposed built, merged with their code${merges.length ? ` in ${merges.join(', ')}` : ''}; ${cutLines.length} to cut`);
|
|
165
|
+
if (unplaced.length) say(`no case: ${unplaced.join(', ')} — a rule's statement alone, for you to place`);
|
|
166
|
+
|
|
167
|
+
// The journeys, most preferred first: their Priority., ties in the index's order (R-264).
|
|
168
|
+
const rank = { first: 1, second: 2, third: 3, fourth: 4, fifth: 5 };
|
|
169
|
+
const listed = [...readFileSync('docs/spec/README.md', 'utf8').matchAll(/^- \[(J-\d+) — .*\]\((journeys\/[^)]+)\)/gm)];
|
|
170
|
+
const known = new Set(rows.filter((x) => x[0] === 'journey').map((x) => x[1]));
|
|
171
|
+
const journeys = listed.filter(([, j]) => known.has(j)).map(([, j, p], i) => {
|
|
172
|
+
const word = readFileSync(join('docs/spec', p), 'utf8').match(/^\*\*Priority\.\*\* (\w+)/m)?.[1]?.toLowerCase();
|
|
173
|
+
return { j, i, p: rank[word] ?? 99 };
|
|
174
|
+
}).sort((x, y) => x.p - y.p || x.i - y.i).map((x) => x.j);
|
|
175
|
+
writeFileSync(join(dir, 'journeys.txt'), journeys.map((j) => `${j}\n`).join(''));
|
|
176
|
+
|
|
177
|
+
// One folder per capability the cut covers: a copy of its document and the index (K-40).
|
|
178
|
+
const caps = [...new Set(cutLines.map((l) => caseOf.get(l.split('\t')[0]).k))];
|
|
179
|
+
for (const k of caps) {
|
|
180
|
+
const f = join(dir, k);
|
|
181
|
+
mkdirSync(f);
|
|
182
|
+
copyFileSync(docs.get(k).path, join(f, basename(docs.get(k).path)));
|
|
183
|
+
writeFileSync(join(f, 'index.txt'), index.map((l) => `${l}\n`).join(''));
|
|
184
|
+
say(`capability: ${k} — ${docs.get(k).heading} — ${cutLines.filter((l) => caseOf.get(l.split('\t')[0]).k === k).length} cases — ${k}/${basename(docs.get(k).path)}, ${k}/index.txt`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// A first run over a repository with tests, no citation lot filed: the pass (R-263), each
|
|
188
|
+
// capability handed the tracked test files whose path holds a word of its heading.
|
|
189
|
+
if (firstRun && citation === 'none') {
|
|
190
|
+
const tests = out('ls-files').split('\n').filter((f) => /(^|\/)(tests?|__tests__)\/|\.(test|spec)\.[cm]?[jt]sx?$/.test(f));
|
|
191
|
+
let handed = 0;
|
|
192
|
+
for (const k of caps) {
|
|
193
|
+
const words = docs.get(k).heading.toLowerCase().split(/[^a-z]+/).filter((w) => w.length > 3);
|
|
194
|
+
const mine = tests.filter((f) => words.some((w) => f.toLowerCase().includes(w)));
|
|
195
|
+
if (!mine.length) continue;
|
|
196
|
+
handed++;
|
|
197
|
+
writeFileSync(join(dir, k, 'tests.txt'), mine.map((f) => `${f}\n`).join(''));
|
|
198
|
+
say(`citation: ${k} — ${mine.length} test files — ${k}/tests.txt`);
|
|
199
|
+
}
|
|
200
|
+
say(`citation pass: ${handed ? `${handed} capabilities handed tests` : tests.length ? 'none — no test file matches a capability' : 'none — no test file'}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// The open issues, each written to a file; the roadmap aside, the umbrellas, the members, the orphans.
|
|
204
|
+
const numbers = must('tracker', 'list', []).split('\n').map((l) => l.match(/^#(\d+)\t/)?.[1]).filter(Boolean);
|
|
205
|
+
mkdirSync(join(dir, 'issues'));
|
|
206
|
+
const issues = numbers.filter((n) => n !== map.n).map((n) => {
|
|
207
|
+
const i = JSON.parse(must('tracker', 'view', [n]));
|
|
208
|
+
const plan = i.comments.map((c) => c.body).filter((b) => first(b).startsWith('codelife: plan — ')).at(-1) ?? '';
|
|
209
|
+
writeFileSync(join(dir, 'issues', `${n}.md`), `# ${i.title}\n\nLabels: ${i.labels.join(', ') || 'none'}. Milestone: ${i.milestone || 'none'}.\n\n${i.body}\n${plan ? `\n## Latest plan\n\n${plan}\n` : ''}`);
|
|
210
|
+
return { n, title: i.title, labels: i.labels, milestone: i.milestone, body: i.body, plan, lot: title('title.lot_of.mjs', i.title) };
|
|
211
|
+
});
|
|
212
|
+
writeFileSync(join(dir, 'issues.json'), JSON.stringify(issues, null, 1));
|
|
213
|
+
writeFileSync(join(dir, 'roadmap.json'), JSON.stringify(map, null, 1));
|
|
214
|
+
say(`issues: ${issues.length} open, each in issues/<n>.md`);
|
|
215
|
+
|
|
216
|
+
const sentence = (t) => title('title.sentence_of.mjs', t);
|
|
217
|
+
for (const u of issues.filter((i) => i.labels.includes('lot'))) {
|
|
218
|
+
const { open, total, effort } = lotCount(u.body);
|
|
219
|
+
const done = section(u.body, 'Done when').split('\n').find((l) => l.trim()) ?? 'none';
|
|
220
|
+
const place = map.order.findIndex((o) => o.lot === u.n);
|
|
221
|
+
say(`lot: #${u.n} — ${u.title.match(/^\[lot · ([^\]]+)\]/)?.[1] ?? 'no origin'} — ${sentence(u.title)} — ${open} of ${total} open, ${effort} — ${place < 0 ? 'not on the roadmap' : `rank ${place + 1}`} — done when: ${done.trim()}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// A member whose latest plan waits for a case (R-266), and the open issue that builds it.
|
|
225
|
+
for (const m of issues.filter((i) => i.lot && i.plan)) {
|
|
226
|
+
const w = m.plan.match(/^Waits for: (.*?)\s*$/m)?.[1];
|
|
227
|
+
if (!w) continue;
|
|
228
|
+
if (!/^C-\d+$/.test(w)) { say(`waits: #${m.n} (lot #${m.lot}) — unreadable: ${w}`); continue; }
|
|
229
|
+
const b = issues.find((i) => i.n !== m.n && !i.labels.includes('lot') && cites(section(i.body, 'Where it comes from'), w));
|
|
230
|
+
say(`waits: #${m.n} (lot #${m.lot}) — ${w} — ${b ? `built by #${b.n}, ${b.lot ? `lot #${b.lot}` : 'in no lot'}` : 'no open issue builds it'}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// The open issues citing what the range changed (R-45): a rewrite, a closure, or the member of a row.
|
|
234
|
+
if (start) {
|
|
235
|
+
// A rule is changed when one of its cases is: an issue citing it is about the rule and its cases.
|
|
236
|
+
const ids = [...new Set([...touched, ...touched.filter((id) => caseOf.has(id)).map((id) => caseOf.get(id).rule), ...retired])];
|
|
237
|
+
for (const i of issues) {
|
|
238
|
+
const hit = ids.filter((id) => cites(section(i.body, 'Where it comes from'), id));
|
|
239
|
+
if (hit.length) say(`cites: #${i.n} — ${hit.map((id) => `${id} ${retired.includes(id) ? 'retired' : 'changed'}`).join(', ')} — ${i.labels.includes('lot') ? 'umbrella' : i.lot ? `lot #${i.lot}` : 'orphan'} — ${sentence(i.title)}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const orphans = issues.filter((i) => !i.lot && !i.labels.includes('lot'));
|
|
244
|
+
const inScope = orphans.filter((i) => scope === null || i.labels.includes(scope) || i.milestone === scope);
|
|
245
|
+
say(`orphans: ${inScope.length}${scope === null ? '' : ` in ${scope}, of ${orphans.length}`}`);
|
|
246
|
+
for (const o of inScope) say(`orphan: #${o.n} — ${o.labels.join(', ') || 'no label'} — ${o.milestone || 'no milestone'}${map.left.includes(o.n) ? ' — left alone last run' : ''} — ${o.title}`);
|
|
247
|
+
const out_of = orphans.filter((o) => !inScope.includes(o) && map.left.includes(o.n)).map((o) => o.n);
|
|
248
|
+
writeFileSync(join(dir, 'kept.txt'), out_of.map((n) => `${n}\n`).join(''));
|
|
249
|
+
done();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
The roadmap: the one issue that never closes. Title: [roadmap] <sentence>, filed by
|
|
3
|
+
`codelife roadmap write` with the origin `roadmap`, and found by that key in a title's tag
|
|
4
|
+
(scripts/tracker/search_id.mjs). There is one; /roadmap
|
|
5
|
+
rewrites it last at every run, so an interrupted run reads the same range again.
|
|
6
|
+
It carries only what exists nowhere else — the mark, the orphans left alone and the order —
|
|
7
|
+
never a lot's progress, which is read on the lot itself.
|
|
8
|
+
-->
|
|
9
|
+
|
|
10
|
+
What remains to implement, in order. `/roadmap` rewrites this issue at the end of every run and
|
|
11
|
+
never closes it; reorder the list below here, and the next run keeps your order.
|
|
12
|
+
|
|
13
|
+
## Mark
|
|
14
|
+
|
|
15
|
+
`<commit>` — the commit of `docs/spec/` the open issues are aligned with: `/roadmap` reads what
|
|
16
|
+
changed from there to `HEAD`.
|
|
17
|
+
|
|
18
|
+
## Left alone
|
|
19
|
+
|
|
20
|
+
The orphans the last run grouped with nothing, each to work on its own — `/overview` offers them to
|
|
21
|
+
`/work` — or *None.*:
|
|
22
|
+
|
|
23
|
+
- #<n> <the issue's sentence>
|
|
24
|
+
|
|
25
|
+
## The order
|
|
26
|
+
|
|
27
|
+
The open lots, the next one first, one umbrella per line, with the lots it waits for:
|
|
28
|
+
|
|
29
|
+
1. #<n> <the umbrella's sentence>
|
|
30
|
+
2. #<n> <the umbrella's sentence> — after #<a>, #<b>
|