@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,180 @@
|
|
|
1
|
+
// `codelife audit prep [full | targeted <LENS>…] [--red]` (C-869): everything a campaign does
|
|
2
|
+
// before any agent, from the repository's root. The tree clean but for a campaign under way and a
|
|
3
|
+
// domain lens the close commit carries; the
|
|
4
|
+
// campaign — the one under way, resumed at the commit its index records, or the next number the
|
|
5
|
+
// campaign table gives; the lenses — every applicable one for `full`, the named ones for
|
|
6
|
+
// `targeted`, a retired id or one with no reference refused; the known list — the open issues and
|
|
7
|
+
// what docs/spec/ decided; the fingerprint, before anything runs; then verify's report at HEAD — the
|
|
8
|
+
// push tier, the hygiene scripts, the history, PERF's timings, the cut —, a red tier stopping
|
|
9
|
+
// unless `--red` audits it; then the campaign folder and its index, and outside the tree a folder
|
|
10
|
+
// for the judge of every part whose `NN/` holds no `part.md`, each with a folder per lens holding
|
|
11
|
+
// only what that lens reads. Prints one `### audit prep` summary; a `- stop:` line exits 1.
|
|
12
|
+
import '../node-floor.mjs';
|
|
13
|
+
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { tmpdir } from 'node:os';
|
|
15
|
+
import { join, resolve } from 'node:path';
|
|
16
|
+
import { decided } from '../brainstorm/lib.mjs';
|
|
17
|
+
import { tier as tierOf } from '../verify/lib.mjs';
|
|
18
|
+
import { field } from '../contract.mjs';
|
|
19
|
+
import { cadence } from './cadence.mjs';
|
|
20
|
+
import { env } from '../tracker/lib.mjs';
|
|
21
|
+
import { shown, state } from '../port/lib.mjs';
|
|
22
|
+
import { HYGIENE, KIND, RETIRED, domainIds, fingerprint, idOf, inProgress, lensIds, lines, parts, pkg, read, reference, row, sh } from './lib.mjs';
|
|
23
|
+
|
|
24
|
+
const summary = ['### audit prep'];
|
|
25
|
+
const say = (line) => summary.push(`- ${line}`);
|
|
26
|
+
const done = (code) => { console.log(summary.join('\n')); process.exit(code); };
|
|
27
|
+
const stop = (why) => { say(`stop: ${why}`); done(1); };
|
|
28
|
+
const argv = process.argv.slice(2);
|
|
29
|
+
const red = argv.includes('--red');
|
|
30
|
+
const [mode = 'full', ...named] = argv.filter((a) => a !== '--red');
|
|
31
|
+
if (!['full', 'targeted'].includes(mode) || (mode === 'full') !== !named.length) {
|
|
32
|
+
console.error('usage: codelife audit prep [full | targeted <LENS>…] [--red]');
|
|
33
|
+
process.exit(2);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// The tree: an audit measures a commit, and its scripts write into this tree.
|
|
37
|
+
const dirty = lines(sh('git', ['status', '--porcelain', '--untracked-files=all']).stdout).map((l) => l.slice(3))
|
|
38
|
+
.filter((p) => !/^"?docs\/audit\/(A\d+-|lenses\/)/.test(p));
|
|
39
|
+
if (dirty.length) stop(`the tree is not clean — ${dirty.slice(0, 10).join(', ')}${dirty.length > 10 ? ', …' : ''}: commit or restore it first`);
|
|
40
|
+
const head = sh('git', ['rev-parse', 'HEAD']).stdout.trim();
|
|
41
|
+
const branch = sh('git', ['branch', '--show-current']).stdout.trim() || 'detached HEAD';
|
|
42
|
+
const kind = field('kind') ?? 'none';
|
|
43
|
+
const tracker = field('tracker.kind') ?? 'none';
|
|
44
|
+
|
|
45
|
+
// The campaign: the one under way, or the next number. Resumed, it keeps its lenses and its commit.
|
|
46
|
+
let folder = inProgress(), lenses;
|
|
47
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
48
|
+
if (folder) {
|
|
49
|
+
const index = read(join(folder, '00-index.md'));
|
|
50
|
+
const at = /`([0-9a-f]{7,40})`/.exec(row(index, 'Commit') ?? '')?.[1];
|
|
51
|
+
if (at && !head.startsWith(at)) stop(`the campaign in ${folder} is at ${at}, HEAD is ${head}: check ${at} out to resume it, or delete the folder to abandon it`);
|
|
52
|
+
lenses = (row(index, 'Lenses run') ?? '').split(/, */).filter(Boolean);
|
|
53
|
+
const judged = parts(folder).filter((nn) => existsSync(join(folder, nn, 'part.md'))).length;
|
|
54
|
+
say(`campaign: ${folder}/ — resumed, ${judged} parts judged${named.length ? `; its own lenses kept, not ${named.join(' ')}` : ''}`);
|
|
55
|
+
} else {
|
|
56
|
+
const all = lensIds(), domain = domainIds();
|
|
57
|
+
if (mode === 'full') lenses = [...all.filter((l) => !Object.values(KIND).flat().includes(l)), ...KIND[kind] ?? [], ...domain];
|
|
58
|
+
else {
|
|
59
|
+
for (const id of named) {
|
|
60
|
+
if (RETIRED[id]) stop(`${id} is retired: its question is ${RETIRED[id]}'s now`);
|
|
61
|
+
if (!reference(id)) stop(`${id} has no reference: write docs/audit/lenses/${id}.md from the plugin's skills/audit/templates/lens.md, then run again`);
|
|
62
|
+
}
|
|
63
|
+
lenses = named;
|
|
64
|
+
}
|
|
65
|
+
const n = Math.max(0, ...cadence().rows.map((r) => Number(r[0].match(/^A(\d+)/)[1]))) + 1;
|
|
66
|
+
folder = `docs/audit/A${n}-${date}-${mode === 'full' ? 'full' : `targeted-${named.join('-')}`}`;
|
|
67
|
+
say(`campaign: ${folder}/ — new`);
|
|
68
|
+
}
|
|
69
|
+
lenses = [...new Set(lenses)];
|
|
70
|
+
const hygiene = lenses.filter((l) => HYGIENE.includes(l)), hunting = lenses.filter((l) => !HYGIENE.includes(l));
|
|
71
|
+
say(`lenses: ${hunting.join(', ') || 'none'}${hygiene.length ? ` · hygiene, one agent: ${hygiene.join(', ')}` : ''}`);
|
|
72
|
+
say(`commit: ${head} on ${branch}`);
|
|
73
|
+
|
|
74
|
+
// The known list: the open issues, one line each, and every rule and refusal of docs/spec/.
|
|
75
|
+
const run = mkdtempSync(join(tmpdir(), 'codelife-audit-'));
|
|
76
|
+
let open = [], listed = tracker === 'none';
|
|
77
|
+
if (listed) open = existsSync('issues') ? (await import('../tracker/none.mjs')).lines() : [];
|
|
78
|
+
else {
|
|
79
|
+
const r = sh(process.execPath, [join(pkg, 'scripts/tracker/list.mjs')]);
|
|
80
|
+
listed = r.status === 0;
|
|
81
|
+
if (listed) open = lines(r.stdout); else say(`tracker: not connected — no open issue swept, nothing can be filed (${(r.stderr || r.stdout).trim().split('\n')[0]})`);
|
|
82
|
+
}
|
|
83
|
+
const decisions = existsSync('docs/spec') ? decided(['docs/spec']) : [];
|
|
84
|
+
const known = join(run, 'known.txt');
|
|
85
|
+
writeFileSync(known, `## Open issues\n${open.join('\n')}\n\n## Decisions\n${decisions.join('\n')}\n`);
|
|
86
|
+
say(`sweep: ${open.length} open issues, ${decisions.length} decisions — ${known}`);
|
|
87
|
+
// CONF's measure, the port's own (K-20 — The target, R-19): CONF files only an item it reads to do.
|
|
88
|
+
const measure = join(run, 'measure.txt');
|
|
89
|
+
if (lenses.includes('CONF')) writeFileSync(measure, shown(state({ forge: tracker, connected: listed })).map(([k, v]) => `- ${k}: ${v}\n`).join(''));
|
|
90
|
+
let visibility = 'unknown';
|
|
91
|
+
if (tracker === 'glab' || tracker === 'gh') {
|
|
92
|
+
const v = sh(tracker, tracker === 'glab' ? ['repo', 'view', '-F', 'json'] : ['repo', 'view', '--json', 'visibility'], { env: env() });
|
|
93
|
+
visibility = /"visibility":\s*"(\w+)"/i.exec(v.stdout)?.[1]?.toLowerCase() ?? 'unknown';
|
|
94
|
+
}
|
|
95
|
+
say(`visibility: ${visibility}${visibility === 'private' ? '' : ' — an exploitable reproduction is withheld'}`);
|
|
96
|
+
|
|
97
|
+
// The fingerprint, before anything runs: the push tier is where a script once wrote into the tree.
|
|
98
|
+
const print = existsSync(join(folder, 'fingerprint.tsv')) ? null : fingerprint();
|
|
99
|
+
|
|
100
|
+
// verify's report at HEAD: the push tier, the hygiene scripts, the history, the timings, the cut.
|
|
101
|
+
const push = tierOf('push');
|
|
102
|
+
if (push.missing) stop(`${push.missing}: /setup ports the tiers`);
|
|
103
|
+
const tier = [push.file, ...push.args].join(' ');
|
|
104
|
+
const scripts = hygiene.filter((l) => existsSync(join(pkg, 'scripts/verify', `${l}.mjs`)));
|
|
105
|
+
const r = sh(process.execPath, [join(pkg, 'scripts/verify/report.mjs'), ...(scripts.length ? ['--lenses', scripts.join(',')] : []), '--tree',
|
|
106
|
+
...(lenses.includes('PERF') ? ['--timings'] : []), '--', tier]);
|
|
107
|
+
const block = r.stdout;
|
|
108
|
+
const tierLine = block.split('\n').find((l) => l.startsWith(`- ${tier} — `)) ?? '';
|
|
109
|
+
const suite = /output in (\S+)/.exec(tierLine)?.[1] ?? null;
|
|
110
|
+
if (r.status !== 0) { console.log(block || r.stderr); stop('the cut failed: its block is above'); }
|
|
111
|
+
if (!/ — passed/.test(tierLine) && !red) stop(`the push tier is red, ${tierLine.replace(/^- /, '')} — fix it first, or run again with --red to audit the failure`);
|
|
112
|
+
say(`baseline: ${tierLine.replace(/^- /, '')}`);
|
|
113
|
+
const report = join(run, 'report.md');
|
|
114
|
+
writeFileSync(report, block);
|
|
115
|
+
const cut = block.split('\n').slice(block.split('\n').findIndex((l) => l.startsWith('- parts · ')) + 1).map((l) => l.slice(4));
|
|
116
|
+
const dir = /^parts in (.+), at most \d+ bytes each$/m.exec(cut.join('\n'))[1];
|
|
117
|
+
const count = readdirSync(dir).filter((f) => /^part-\d+\.txt$/.test(f)).length;
|
|
118
|
+
const binary = /^binary files in no part: (\d+)/m.exec(cut.join('\n'))?.[1] ?? '0';
|
|
119
|
+
say(`facts: verify's block in ${report}`);
|
|
120
|
+
say(`parts: ${count}, ${binary} binary files in no part — ${dir}`);
|
|
121
|
+
|
|
122
|
+
// The campaign folder and its index, once: a resumed campaign keeps both.
|
|
123
|
+
if (print) {
|
|
124
|
+
mkdirSync(folder, { recursive: true });
|
|
125
|
+
writeFileSync(join(folder, 'fingerprint.tsv'), print.map((l) => `${l}\n`).join(''));
|
|
126
|
+
const sha = sh('git', ['-C', pkg, 'rev-parse', '--short', 'HEAD']);
|
|
127
|
+
let version = '';
|
|
128
|
+
try { version = JSON.parse(readFileSync(join(pkg, 'package.json'), 'utf8')).version; } catch { /* none */ }
|
|
129
|
+
const last = cadence().last;
|
|
130
|
+
const fill = { id: idOf(`/${folder}`), type: mode === 'full' ? 'full' : `targeted (${named.join(', ')})`, date, commit: head.slice(0, 7), branch,
|
|
131
|
+
lenses: lenses.join(', '), baseline: `\`${tier}\` — ${/ — passed/.test(tierLine) ? 'green' : 'red, audited as a finding'} on ${date}`,
|
|
132
|
+
instrument: `codelife ${version}${sha.status === 0 ? ` at \`${sha.stdout.trim()}\`` : ''}, loaded from \`${pkg.replace(/\/$/, '')}\``,
|
|
133
|
+
previous: last ? last[0] : 'none — this is the first', open: open.length, decisions: decisions.length,
|
|
134
|
+
measure: `${count} parts × ${lenses.length} lenses, ${binary} binary files in no part; never read: {{unread}}` };
|
|
135
|
+
const template = readFileSync(join(pkg, 'skills/audit/templates/index.md'), 'utf8');
|
|
136
|
+
writeFileSync(join(folder, '00-index.md'), template.replace(/\{\{(\w+)\}\}/g, (m, k) => (k in fill ? String(fill[k]) : m)));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// A folder per judge, outside the tree: every part whose NN/ holds no part.md, emptied for it.
|
|
140
|
+
const root = `root: ${process.cwd()}`;
|
|
141
|
+
const id = idOf(`/${folder}`);
|
|
142
|
+
const text = (l) => l.map((x) => `${x}\n`).join('');
|
|
143
|
+
const judges = [];
|
|
144
|
+
let places = 0;
|
|
145
|
+
for (let p = 1; p <= count; p++) {
|
|
146
|
+
const nn = String(p).padStart(2, '0'), out = resolve(folder, nn);
|
|
147
|
+
if (existsSync(join(out, 'part.md'))) continue;
|
|
148
|
+
rmSync(out, { recursive: true, force: true });
|
|
149
|
+
mkdirSync(out, { recursive: true });
|
|
150
|
+
const j = mkdtempSync(join(run, `judge-${nn}.`));
|
|
151
|
+
for (const [f, to] of [[known, 'known.txt'], [report, 'report.md']]) copyFileSync(f, join(j, to));
|
|
152
|
+
writeFileSync(join(j, 'arguments.txt'), text([root, `campaign: ${id}`, `sha: ${head}`, `part: ${nn}`, `visibility: ${visibility}`, `write: ${out}`]));
|
|
153
|
+
const lens = (ids) => {
|
|
154
|
+
const d = mkdtempSync(join(j, `${ids.join('-')}.`));
|
|
155
|
+
const files = [[join(dir, `part-${p}.txt`), 'part.txt'], [join(dir, 'tracked.txt'), 'tracked.txt'], [known, 'known.txt'], [report, 'report.md'],
|
|
156
|
+
...ids.map((l) => [reference(l), `lens-${l}.md`]),
|
|
157
|
+
...(ids.includes('VERIF') && suite ? [[suite, 'suite.txt']] : []),
|
|
158
|
+
...(ids.includes('CONF') ? [[join(pkg, 'scripts/target/target-base.md'), 'target-base.md'],
|
|
159
|
+
[join(pkg, `scripts/target/target-${kind}.md`), `target-${kind}.md`], [measure, 'measure.txt']] : [])].filter(([f]) => f && existsSync(f));
|
|
160
|
+
for (const [f, to] of files) copyFileSync(f, join(d, to));
|
|
161
|
+
writeFileSync(join(d, 'arguments.txt'), text([root, `sha: ${head}`, `lenses: ${ids.join(', ')}`, `files: ${files.map(([, to]) => to).join(', ')}`]));
|
|
162
|
+
return [ids.join(','), d];
|
|
163
|
+
};
|
|
164
|
+
const rows = [...(p === 1 && hygiene.length ? [lens(hygiene)] : []), ...hunting.map((l) => lens([l]))];
|
|
165
|
+
writeFileSync(join(j, 'lenses.tsv'), text(rows.map((x) => x.join('\t'))));
|
|
166
|
+
places = Math.max(places, 1 + rows.length);
|
|
167
|
+
judges.push([nn, j]);
|
|
168
|
+
}
|
|
169
|
+
writeFileSync(join(run, 'judges.tsv'), text(judges.map((x) => x.join('\t'))));
|
|
170
|
+
say(`judges: ${judges.length} of ${count} parts to judge${judges.length ? ` — ${judges.map(([nn]) => nn).join(', ')} · ${places} places a judge` : ''} — ${join(run, 'judges.tsv')}`);
|
|
171
|
+
|
|
172
|
+
// Claude Code's cap on concurrent subagents: a judge holds itself and each lens it dispatches.
|
|
173
|
+
if (judges.length) {
|
|
174
|
+
const setting = 'CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS', raw = process.env[setting];
|
|
175
|
+
const cap = /^[1-9]\d*$/.test(raw ?? '') ? Number(raw) : 20;
|
|
176
|
+
const atOnce = Math.floor(cap / places);
|
|
177
|
+
if (!atOnce) stop(`a judge holds ${places} places, more than the cap ${cap} — raise ${setting}`);
|
|
178
|
+
say(`cap: ${cap} (${raw ? `${setting}=${raw}` : `${setting} unset`}) · ${atOnce} judges at once · ${Math.ceil(judges.length / atOnce)} round${Math.ceil(judges.length / atOnce) > 1 ? 's' : ''} · raised by ${setting}`);
|
|
179
|
+
}
|
|
180
|
+
done(0);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// `codelife audit table <LENS>[,<LENS>…]` (K-33, R-110): the table one lens's findings are confirmed
|
|
2
|
+
// on — or the hygiene lenses' together —, from the campaign under way, from the repository's root.
|
|
3
|
+
// First every part must be judged and accounted for: a part without its part.md, or whose judge
|
|
4
|
+
// received a candidate id no line of the part names again (#825), stops it. Then one row per
|
|
5
|
+
// finding across the parts — its id, title, severity, effort and state: its disposition when one is
|
|
6
|
+
// written, `filed #n` when an issue already carries its id, `known #n`, or `new`, and a title two
|
|
7
|
+
// rows share flagged —, then below the rows each point the table would hide: what its judges watched,
|
|
8
|
+
// read as a deliberate choice or refuted, and the decisions they found contested. Writes nothing.
|
|
9
|
+
import '../node-floor.mjs';
|
|
10
|
+
import { existsSync, mkdtempSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { findings, inProgress, partFile, parts, pkg, sh, unaccounted } from './lib.mjs';
|
|
14
|
+
|
|
15
|
+
const [arg] = process.argv.slice(2);
|
|
16
|
+
if (!arg || !/^[A-Z][A-Z0-9]+(,[A-Z][A-Z0-9]+)*$/.test(arg)) {
|
|
17
|
+
console.error('usage: codelife audit table <LENS>[,<LENS>…]');
|
|
18
|
+
process.exit(2);
|
|
19
|
+
}
|
|
20
|
+
const lenses = arg.split(',');
|
|
21
|
+
const out = [`### audit table — ${lenses.join(', ')}`];
|
|
22
|
+
const stop = (why) => { out.push(`- stop: ${why}`); console.log(out.join('\n')); process.exit(1); };
|
|
23
|
+
const folder = inProgress() ?? stop('no campaign in progress: `codelife audit prep` opens one');
|
|
24
|
+
|
|
25
|
+
const waiting = parts(folder).filter((nn) => !existsSync(join(folder, nn, 'part.md')));
|
|
26
|
+
if (waiting.length) stop(`parts not judged: ${waiting.join(', ')} — run \`codelife audit prep\` again, which dispatches them alone`);
|
|
27
|
+
for (const nn of parts(folder)) {
|
|
28
|
+
const lost = unaccounted(folder, nn);
|
|
29
|
+
if (lost.length) stop(`part ${nn}'s judge left ${lost.join(', ')} unaccounted — delete ${join(folder, nn, 'part.md')} and run \`codelife audit prep\` again`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Already filed: an issue whose title's tag holds the finding's id, open or closed.
|
|
33
|
+
const tmp = mkdtempSync(join(tmpdir(), 'codelife-table-'));
|
|
34
|
+
let connected = true;
|
|
35
|
+
const filed = (id) => {
|
|
36
|
+
if (!connected) return null;
|
|
37
|
+
writeFileSync(join(tmp, 'key'), `audit:${id}`);
|
|
38
|
+
const r = sh(process.execPath, [join(pkg, 'scripts/tracker/search_id.mjs'), join(tmp, 'key'), '--all']);
|
|
39
|
+
if (r.status !== 0) { connected = false; return null; }
|
|
40
|
+
return /^#(\d+)/.exec(r.stdout)?.[1] ?? null;
|
|
41
|
+
};
|
|
42
|
+
const rows = findings(folder, lenses);
|
|
43
|
+
const titles = new Map();
|
|
44
|
+
out.push('| Id | Title | Severity | Effort | State |', '|---|---|---|---|---|');
|
|
45
|
+
for (const f of rows) {
|
|
46
|
+
const n = f.disposition ? null : filed(f.id);
|
|
47
|
+
const twin = titles.get(f.title.toLowerCase());
|
|
48
|
+
titles.set(f.title.toLowerCase(), titles.get(f.title.toLowerCase()) ?? f.id);
|
|
49
|
+
const state = f.disposition ?? (n ? `filed #${n}` : f.known ? `known #${f.known}` : 'new');
|
|
50
|
+
out.push(`| ${f.id} | ${f.title}${twin ? ` — same title as ${twin}` : ''} | ${f.severity}${f.unverified ? ' UNVERIFIED' : ''} | ${f.effort} | ${state} |`);
|
|
51
|
+
}
|
|
52
|
+
if (!rows.length) out.push('| none | | | | |');
|
|
53
|
+
if (!connected) out.push('- tracker: not reached — no row looked up');
|
|
54
|
+
|
|
55
|
+
// Below the rows: one line per point a judge kept out of the table, by part.
|
|
56
|
+
const below = [], contested = [];
|
|
57
|
+
for (const nn of parts(folder)) {
|
|
58
|
+
const { sections } = partFile(folder, nn);
|
|
59
|
+
for (const l of [...new Set(lenses.flatMap((x) => sections[x] ?? []))]) {
|
|
60
|
+
if (/^\*Contested\b/.test(l)) contested.push(`- part ${nn}: ${l}`);
|
|
61
|
+
else if (/^\*(To watch|Deliberate choice|False)\b/.test(l)) below.push(`- part ${nn}: ${l}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
out.push('Below the rows:', ...(below.length ? below : ['- none']), 'Contested:', ...(contested.length ? contested : ['- none']));
|
|
65
|
+
console.log(out.join('\n'));
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// What is decided: one `file:line: text` per rule in force (`### R-<n> — …`) and per refusal (a line
|
|
2
|
+
// of a `## Rejected`) under the directories given: `node decided.mjs docs/spec`.
|
|
3
|
+
import { decided } from './lib.mjs';
|
|
4
|
+
|
|
5
|
+
const out = decided(process.argv.slice(2));
|
|
6
|
+
if (out.length) console.log(out.join('\n'));
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// What the readers of what is decided share — each reads Markdown and prints `file:line: text`
|
|
2
|
+
// lines. From the repository's root.
|
|
3
|
+
import '../node-floor.mjs';
|
|
4
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
5
|
+
|
|
6
|
+
// The `.md` files under each path, sorted, spelled as `find` spells them: the path given, then the entry.
|
|
7
|
+
const files = (path) => (statSync(path).isDirectory()
|
|
8
|
+
? readdirSync(path).flatMap((e) => files(`${path.replace(/\/+$/, '')}/${e}`))
|
|
9
|
+
: path.endsWith('.md') ? [path] : []);
|
|
10
|
+
export const markdown = (paths) => paths.flatMap(files).sort();
|
|
11
|
+
|
|
12
|
+
// Every line of every file for which keep(line, section) holds, as `file:line: text`; the section is
|
|
13
|
+
// the last `## ` heading read in that file.
|
|
14
|
+
export const scan = (paths, keep) => {
|
|
15
|
+
const out = [];
|
|
16
|
+
for (const file of markdown(paths)) {
|
|
17
|
+
let section = '';
|
|
18
|
+
readFileSync(file, 'utf8').split('\n').forEach((line, i) => {
|
|
19
|
+
if (line.startsWith('## ')) section = line;
|
|
20
|
+
if (keep(line, section)) out.push(`${file}:${i + 1}: ${line}`);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// decided <paths> — what is decided: one `file:line: text` per rule in force (`### R-<n> — …`) and per
|
|
27
|
+
// refusal (a line of a `## Rejected`).
|
|
28
|
+
export const decided = (paths) => scan(paths, (line, section) =>
|
|
29
|
+
/^### R-[0-9]+ — /.test(line) || (section === '## Rejected' && line.startsWith('- ')));
|
|
30
|
+
|
|
31
|
+
// unfinished <paths> — what the notes still pending say they are about: one `file:line: text` per
|
|
32
|
+
// note's title and per **Open** item.
|
|
33
|
+
export const unfinished = (paths) => scan(paths, (line) => /^# /.test(line) || line.includes('**Open —**'));
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// The heading each identifier opens, anywhere under docs/spec/: `node named.mjs <id>…`, one
|
|
2
|
+
// `file:line: text` per identifier; nothing for one that resolves to nothing, which the caller names.
|
|
3
|
+
import { scan } from './lib.mjs';
|
|
4
|
+
|
|
5
|
+
const out = process.argv.slice(2).flatMap((id) => scan(['docs/spec'], (line) => {
|
|
6
|
+
const [hashes, name, dash] = line.split(/[ \t]+/);
|
|
7
|
+
return /^#+$/.test(hashes) && name === id && dash === '—';
|
|
8
|
+
}));
|
|
9
|
+
if (out.length) console.log(out.join('\n'));
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Writes CHANGELOG.md: the released part rendered by git-cliff above the marker, everything already
|
|
3
|
+
// below it kept untouched: `node scripts/changelog.mjs [--check] [root]`. The contract names this
|
|
4
|
+
// script as the generated[] command for that file, so it is the single authority: /release runs
|
|
5
|
+
// it, and the guard that compares the tracked file to its generator runs it with `--check`.
|
|
6
|
+
//
|
|
7
|
+
// git-cliff alone does NOT produce this file: it renders an `## [Unreleased]` section for every
|
|
8
|
+
// commit past the newest tag, which the tracked file never carries and which is stale at the very
|
|
9
|
+
// next commit. Dropping that section is what `kept` below does. `--latest` is not the same thing
|
|
10
|
+
// and must not be substituted: with two tags it renders only the newest section, so a comparison
|
|
11
|
+
// against it passes on a file that lost every older release.
|
|
12
|
+
//
|
|
13
|
+
// With `--check` it writes the rebuilt file to stdout and leaves CHANGELOG.md alone, so a guard can
|
|
14
|
+
// compare without mutating a tracked file — an audit campaign runs in place and may not.
|
|
15
|
+
import './node-floor.mjs'
|
|
16
|
+
import { execFileSync, spawnSync } from 'node:child_process'
|
|
17
|
+
import { existsSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
18
|
+
import { dirname, join, resolve } from 'node:path'
|
|
19
|
+
import { fileURLToPath } from 'node:url'
|
|
20
|
+
import { compare, spellings, tagFor } from './tags.mjs'
|
|
21
|
+
import { field } from './contract.mjs'
|
|
22
|
+
import { lf } from './text.mjs'
|
|
23
|
+
|
|
24
|
+
const args = process.argv.slice(2)
|
|
25
|
+
const check = args[0] === '--check'
|
|
26
|
+
if (check) args.shift()
|
|
27
|
+
const root = args[0] ?? '.'
|
|
28
|
+
const f = `${root}/CHANGELOG.md`
|
|
29
|
+
const marker = '<!-- codelife: generated above, history below -->'
|
|
30
|
+
|
|
31
|
+
const stop = (...lines) => { console.error(lines.join('\n')); process.exit(1) }
|
|
32
|
+
const fail = (what, ...more) => stop(`changelog.mjs: ${what}`, ...more.map((l) => ` ${l}`))
|
|
33
|
+
const read = (p) => { try { return lf(readFileSync(p, 'utf8')) } catch { return null } }
|
|
34
|
+
|
|
35
|
+
// The marker line, found once for this refusal and the cut below, says where the generated part
|
|
36
|
+
// stops. Without it there is no safe splice, so this refuses rather than guess and truncate the file.
|
|
37
|
+
const all = read(f)?.split('\n') ?? []
|
|
38
|
+
const at = all.findIndex((l) => l.trimEnd() === marker)
|
|
39
|
+
if (at < 0) fail(`no marker in ${f} — refusing to guess where the generated part ends`, `add this line above the hand-written part: ${marker}`)
|
|
40
|
+
|
|
41
|
+
// git-cliff does NOT fail when its configuration is missing: it prints a warning, exits 0, and
|
|
42
|
+
// renders its own generic template — including the `## [Unreleased]` heading this file forbids.
|
|
43
|
+
// So the configuration is never looked up in the repository: it is the one this package ships,
|
|
44
|
+
// passed with `--config`, and a package without it refuses rather than render with the defaults.
|
|
45
|
+
const pkg = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
46
|
+
const config = join(pkg, 'scripts/target/templates/cliff.toml')
|
|
47
|
+
if (!existsSync(config)) fail(`no ${config} — refusing to render with git-cliff's defaults`, 'the package is incomplete; reinstall it')
|
|
48
|
+
|
|
49
|
+
// The version the contract's release.version_file names, when no tag names it yet, is the release
|
|
50
|
+
// being cut: /release bumps the file, then writes the section with this script, so the section and
|
|
51
|
+
// the guard comparing the file against it come out of one command. git-cliff takes it as `--tag`,
|
|
52
|
+
// in the spelling the release will give it, scripts/tags.mjs's. `0.0.0` is nothing released,
|
|
53
|
+
// as the GOV lens reads it, not a pending 0.0.0; no version read at all is a refusal, never nothing
|
|
54
|
+
// pending, which would drop the release's section with the comparison agreeing.
|
|
55
|
+
const vf = field('release.version_file', root) ?? ''
|
|
56
|
+
// A key of the version file, read at its top level: the first match at any depth can be a nested one.
|
|
57
|
+
const json = (key) => { try { return String(JSON.parse(readFileSync(`${root}/${vf}`, 'utf8'))[key] ?? '') } catch { return '' } }
|
|
58
|
+
const version = vf ? json('version') : ''
|
|
59
|
+
if (!version) fail(`no version read from release.version_file ('${vf}') in ${root}/.claude/codelife.md`)
|
|
60
|
+
let pending = []
|
|
61
|
+
if (version !== '0.0.0') {
|
|
62
|
+
// Released already when a tag of scripts/tags.mjs's spellings names it.
|
|
63
|
+
let tagged = ''
|
|
64
|
+
try { tagged = execFileSync('git', ['-C', root, 'tag', '-l', ...spellings(version, json('name'))], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }) } catch { /* not a repository: nothing is tagged */ }
|
|
65
|
+
if (!tagged.trim()) pending = ['--tag', tagFor(version)]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// git-cliff is the package's pinned dependency, never whatever is on PATH: the version the config
|
|
69
|
+
// was measured on is the one in this package's manifest. Resolved from this script, so it is found
|
|
70
|
+
// where the package manager put it beside the package, and run by node: no `.bin` link is assumed.
|
|
71
|
+
let cliff
|
|
72
|
+
try { cliff = fileURLToPath(import.meta.resolve('git-cliff/cli')) } catch { fail('git-cliff is not resolvable from the package — it is a pinned dependency; install the dependencies') }
|
|
73
|
+
|
|
74
|
+
const r = spawnSync(process.execPath, [cliff, '--config', config, ...pending], { cwd: root, encoding: 'utf8', maxBuffer: 1 << 28 })
|
|
75
|
+
if (r.status !== 0) fail(`git-cliff exited ${r.status ?? r.error?.message}; ${f} was not touched`, ...r.stderr.split('\n').filter(Boolean))
|
|
76
|
+
const out = r.stdout.replace(/\n+$/, '')
|
|
77
|
+
|
|
78
|
+
// Only the versions newer than the newest one the history below the marker carries are rendered.
|
|
79
|
+
// git-cliff renders every tag, and a converted changelog keeps its hand-written releases as
|
|
80
|
+
// history: without this, each of them appeared twice, and an old tag the history never had came
|
|
81
|
+
// out above newer releases. The generated part starts where the history ends.
|
|
82
|
+
const history = all.slice(at).join('\n').replace(/\n+$/, '')
|
|
83
|
+
const seen = history.split('\n').filter((l) => l.startsWith('## ')).map((l) => /[0-9]+\.[0-9]+\.[0-9]+[0-9A-Za-z.+-]*/.exec(l)?.[0]).filter(Boolean)
|
|
84
|
+
const newest = seen.sort(compare).pop()
|
|
85
|
+
const heading = (l) => l.startsWith('## [') ? l.split(/\s+/)[1].replace(/[[\]]/g, '') : undefined
|
|
86
|
+
let skip = false
|
|
87
|
+
const kept = out.split('\n').filter((l) => {
|
|
88
|
+
const v = heading(l)
|
|
89
|
+
if (v !== undefined) skip = v === 'Unreleased' || (newest !== undefined && /^\d/.test(v) && compare(v, newest) <= 0)
|
|
90
|
+
return !skip
|
|
91
|
+
})
|
|
92
|
+
const text = kept.map((l) => `${l}\n`).join('') + `${history}\n`
|
|
93
|
+
|
|
94
|
+
if (check) process.stdout.write(text)
|
|
95
|
+
else {
|
|
96
|
+
// Written beside the file and renamed over it, never written in place: a failure there would
|
|
97
|
+
// leave the tracked file truncated, and a failed write must leave it as it was.
|
|
98
|
+
const tmp = `${f}.${process.pid}.tmp`
|
|
99
|
+
try { writeFileSync(tmp, text); renameSync(tmp, f) } catch { rmSync(tmp, { force: true }); fail(`could not write the rebuilt file; ${f} was not touched`) }
|
|
100
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The publication job's one check: `node scripts/check-tag.mjs <tag> [<npm version>]`. The tag is
|
|
3
|
+
// `v` and the package's version, read from the package.json beside `scripts/`, and the npm that
|
|
4
|
+
// publishes is 11.5.1 or later, the first to publish with a trusted publisher. A refusal prints
|
|
5
|
+
// why and exits 1, so the job goes red before anything is published. Built-in modules only.
|
|
6
|
+
import './node-floor.mjs'
|
|
7
|
+
import { readFileSync } from 'node:fs'
|
|
8
|
+
|
|
9
|
+
const [tag = '', npm] = process.argv.slice(2)
|
|
10
|
+
const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
|
|
11
|
+
const refuse = (why) => { console.error(`check-tag: ${why}`); process.exit(1) }
|
|
12
|
+
|
|
13
|
+
if (!/^v\d+\.\d+\.\d+$/.test(tag)) refuse(`tag '${tag}' is not of the form vX.Y.Z`)
|
|
14
|
+
if (tag !== `v${version}`) refuse(`tag '${tag}' does not name package.json's version ${version}`)
|
|
15
|
+
if (npm !== undefined) {
|
|
16
|
+
const [a, b, c] = npm.split('.').map(Number)
|
|
17
|
+
if (!(a > 11 || (a === 11 && (b > 5 || (b === 5 && c >= 1))))) refuse(`npm ${npm} is older than 11.5.1`)
|
|
18
|
+
}
|
|
19
|
+
console.log(`check-tag: ${tag} names package.json's version${npm ? `, npm ${npm}` : ''}`)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `codelife checks <base> [<commands file>]` (review-mr R-291, C-853): the deterministic checks a
|
|
3
|
+
// review runs before any agent starts — the push tier, then each `guards[]` entry of the contract
|
|
4
|
+
// whose `when` matches a path the diff from <base> to `HEAD` touches, then the hygiene scripts that
|
|
5
|
+
// only read, then each `generated[]` output whose `when` the diff matches, failed when the diff
|
|
6
|
+
// leaves it untouched, then each line of the commands file once, the push tier's own command
|
|
7
|
+
// skipped — one after the other, from the repository's root. Prints one line per check, `passed`, `failed` or,
|
|
8
|
+
// for a hygiene script whose subject is absent, `could not tell`, anything but a pass followed by
|
|
9
|
+
// the last lines of its output and a guard's `note`, and nothing else. Exit 0 when no check failed, 1 when one failed or
|
|
10
|
+
// could not run,
|
|
11
|
+
// 2 on a base git cannot resolve or diff against `HEAD` — no merge base, a shallow clone — since an
|
|
12
|
+
// empty diff would skip every guard and pass.
|
|
13
|
+
import './node-floor.mjs'
|
|
14
|
+
import { readFileSync } from 'node:fs'
|
|
15
|
+
import { fileURLToPath } from 'node:url'
|
|
16
|
+
import { fired, git, guards, runChecks, tier } from './verify/lib.mjs'
|
|
17
|
+
import { field } from './contract.mjs'
|
|
18
|
+
|
|
19
|
+
const [base = '', file] = process.argv.slice(2)
|
|
20
|
+
if (!base || git('rev-parse', '--verify', '--quiet', `${base}^{commit}`).status !== 0) {
|
|
21
|
+
console.error(`usage: codelife checks <base> — '${base}' is no commit git can resolve`)
|
|
22
|
+
process.exit(2)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const diff = git('diff', '--name-only', '-z', `${base}...HEAD`)
|
|
26
|
+
if (diff.status !== 0) {
|
|
27
|
+
console.error(`codelife checks: no diff from ${base} to HEAD — ${diff.err}`)
|
|
28
|
+
process.exit(2)
|
|
29
|
+
}
|
|
30
|
+
const touched = diff.out.split('\0').filter(Boolean)
|
|
31
|
+
|
|
32
|
+
const checks = [tier('push')]
|
|
33
|
+
for (const g of fired(guards(), touched)) for (const c of g.run) checks.push({ name: c, file: c, shell: true, note: g.note })
|
|
34
|
+
// The hygiene scripts that only read, of the lenses the contract's `kind` applies — DEPS and GUARD
|
|
35
|
+
// for every kind, TYPE for a library (scripts/audit/lib.mjs); BUILD rebuilds and PKG
|
|
36
|
+
// packs, the audit's, and GOV and LINES read no diff. One fails by printing its facts, exit 0; any
|
|
37
|
+
// other exit is `could not tell` — its subject absent, exit 3 — never a pass and never red.
|
|
38
|
+
const kind = field('kind')
|
|
39
|
+
for (const lens of ['DEPS', 'GUARD', ...(kind === 'lib' ? ['TYPE'] : [])])
|
|
40
|
+
checks.push({ name: lens, file: process.execPath, args: [fileURLToPath(new URL(`./verify/${lens}.mjs`, import.meta.url))], hygiene: true })
|
|
41
|
+
|
|
42
|
+
// An output is stale when the diff touches its sources and not it: read, never regenerated.
|
|
43
|
+
for (const g of fired(guards('generated'), touched)) {
|
|
44
|
+
const path = (g.path?.[0] ?? '').replace(/\/$/, '')
|
|
45
|
+
const hit = touched.some((t) => t === path || t.startsWith(`${path}/`))
|
|
46
|
+
checks.push(hit ? { name: `generated ${path}`, pass: true } : { name: `generated ${path}`, missing: 'the diff changes its sources and leaves it untouched' })
|
|
47
|
+
}
|
|
48
|
+
// *How to test*'s commands, as the preparation wrote them: the push tier ran above.
|
|
49
|
+
const push = /^\S+( run)? verify:push$/
|
|
50
|
+
for (const c of new Set(file ? readFileSync(file, 'utf8').split('\n').map((l) => l.trim()).filter((l) => l && !push.test(l)) : []))
|
|
51
|
+
checks.push({ name: c, file: c, shell: true })
|
|
52
|
+
|
|
53
|
+
process.exit(runChecks(checks) ? 1 : 0)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The `codelife` command line (K-43): `codelife <verb> [arguments]` runs the verb's script with the
|
|
3
|
+
// arguments that follow its name, where it is called, and exits with that script's status;
|
|
4
|
+
// `codelife help` lists the verbs and `codelife help <verb>` prints one's manual; `codelife help
|
|
5
|
+
// method|template [<name>]` lists the shared methods or the templates, or prints one. The verbs
|
|
6
|
+
// are scripts/verbs.mjs's. An unknown verb, method or template exits 2, naming it.
|
|
7
|
+
import './node-floor.mjs'
|
|
8
|
+
import { spawnSync } from 'node:child_process'
|
|
9
|
+
import { fileURLToPath } from 'node:url'
|
|
10
|
+
import { method, methods, template, templates, verb, VERBS } from './verbs.mjs'
|
|
11
|
+
|
|
12
|
+
const args = process.argv.slice(2)
|
|
13
|
+
const SHELVES = { method: [methods, method], template: [templates, template] }
|
|
14
|
+
const unknown = (words, what = 'verb', lister = 'help') => {
|
|
15
|
+
console.error(`codelife: no ${what} '${words.join(' ')}' — \`codelife ${lister}\` lists them`)
|
|
16
|
+
process.exit(2)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!args.length || args[0] === 'help') {
|
|
20
|
+
const asked = args.slice(1)
|
|
21
|
+
if (!asked.length) {
|
|
22
|
+
const width = Math.max(...Object.keys(VERBS).map((n) => n.length))
|
|
23
|
+
for (const [n, v] of Object.entries(VERBS)) console.log(`${n.padEnd(width)} ${v.summary}`)
|
|
24
|
+
} else if (asked[0] in SHELVES) {
|
|
25
|
+
const [names, one] = SHELVES[asked[0]]
|
|
26
|
+
if (asked.length === 1) console.log(names().join('\n'))
|
|
27
|
+
else if (asked.length > 2 || !one(asked[1])) unknown(asked.slice(1), asked[0], `help ${asked[0]}`)
|
|
28
|
+
else process.stdout.write(one(asked[1]))
|
|
29
|
+
} else {
|
|
30
|
+
const found = verb(asked)
|
|
31
|
+
if (!found || found[1].length) unknown(asked)
|
|
32
|
+
console.log(VERBS[found[0]].manual)
|
|
33
|
+
}
|
|
34
|
+
process.exit(0)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const found = verb(args)
|
|
38
|
+
if (!found) unknown(args)
|
|
39
|
+
const [name, rest] = found
|
|
40
|
+
const { script, lead = [] } = VERBS[name]
|
|
41
|
+
const r = spawnSync(process.execPath, [fileURLToPath(new URL(`../${script}`, import.meta.url)), ...lead, ...rest], { stdio: 'inherit' })
|
|
42
|
+
process.exit(r.status ?? 1)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Refuses a commit message that is not a Conventional Commit, because CHANGELOG.md is generated
|
|
3
|
+
// from these messages and a message that does not parse is a change that never reaches the
|
|
4
|
+
// changelog. Run by the commit-msg hook: `node scripts/commit-msg.mjs <message file>`.
|
|
5
|
+
import './node-floor.mjs'
|
|
6
|
+
import { readFileSync } from 'node:fs'
|
|
7
|
+
|
|
8
|
+
let text
|
|
9
|
+
try { text = readFileSync(process.argv[2], 'utf8') } catch { process.exit(0) }
|
|
10
|
+
|
|
11
|
+
// The subject is the first line that is neither blank nor a comment.
|
|
12
|
+
const subject = text.split('\n').find((l) => !l.startsWith('#') && l.trim() !== '')
|
|
13
|
+
|
|
14
|
+
// Nothing to check: git will refuse an empty message on its own. Git's own machinery goes through.
|
|
15
|
+
if (subject === undefined || /^(Merge |Revert |fixup! |squash! |amend! )/.test(subject)) process.exit(0)
|
|
16
|
+
|
|
17
|
+
const types = ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'build', 'ci', 'chore', 'revert']
|
|
18
|
+
|
|
19
|
+
// The subject line is read in a list of commits, in a changelog and in a blame: 72 characters is
|
|
20
|
+
// what a terminal and a forge show without truncating. Counted as code points, whatever the shell
|
|
21
|
+
// and the locale — the shell version counted bytes under dash and passed on a Mac what Linux
|
|
22
|
+
// refused (#53).
|
|
23
|
+
const n = [...subject].length
|
|
24
|
+
if (n > 72) {
|
|
25
|
+
console.error(`\n The subject line is ${n} characters; 72 is the limit:\n\n ${subject}\n\n Move what does not fit into the body, which is where the why belongs.\n`)
|
|
26
|
+
process.exit(1)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// type(optional scope)optional !: space, then a subject opening on a character that is not a space (#1060).
|
|
30
|
+
if (new RegExp(`^(${types.join('|')})(\\([a-z0-9._/-]+\\))?!?: \\S`).test(subject)) process.exit(0)
|
|
31
|
+
|
|
32
|
+
console.error(`
|
|
33
|
+
This commit message is not a Conventional Commit:
|
|
34
|
+
|
|
35
|
+
${subject}
|
|
36
|
+
|
|
37
|
+
Expected <type>(<optional scope>): <subject>
|
|
38
|
+
Types ${types.join(' ')}
|
|
39
|
+
Breaking put a ! before the colon — feat(api)!: drop the legacy entry point
|
|
40
|
+
|
|
41
|
+
The body says why. Cite the issue in the footer: Refs: #123
|
|
42
|
+
`)
|
|
43
|
+
process.exit(1)
|