@ersbeth/codelife 0.0.0-bootstrap.0 → 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 +66 -0
- 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 +40 -1
- 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,34 @@
|
|
|
1
|
+
// GOV's facts, read from origin: no merged branch left, every name in the model, the shipped
|
|
2
|
+
// version tagged, a licence declared — and this clone's refs pruned. Whether the licence is a
|
|
3
|
+
// decision is the hygiene subagent's.
|
|
4
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { fail, git, lines, remoteHeads } from './lib.mjs';
|
|
6
|
+
import { field } from '../contract.mjs';
|
|
7
|
+
|
|
8
|
+
const heads = remoteHeads();
|
|
9
|
+
const model = /^(main|develop|(feature|fix|hotfix|lot|support)\/.+)$/;
|
|
10
|
+
const develop = heads.get('develop');
|
|
11
|
+
if (!develop) fail('origin has no develop branch');
|
|
12
|
+
|
|
13
|
+
for (const [b, sha] of heads) {
|
|
14
|
+
if (!model.test(b)) fail(`${b}: a branch name outside the model`);
|
|
15
|
+
if (!develop || b === 'main' || b === 'develop') continue;
|
|
16
|
+
if (git('cat-file', '-e', `${sha}^{commit}`).status !== 0) fail(`${b}: not in this clone, ancestry unjudged`);
|
|
17
|
+
else if (git('merge-base', '--is-ancestor', sha, develop).status === 0)
|
|
18
|
+
fail(`${b} is merged into develop and still a head on origin`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const file = field('release.version_file');
|
|
22
|
+
if (file && file.endsWith('.json') && existsSync(file)) {
|
|
23
|
+
const m = JSON.parse(readFileSync(file, 'utf8'));
|
|
24
|
+
const tags = new Set(lines(git('ls-remote', '--tags', 'origin').out).map((l) => l.split('\t')[1].replace(/^refs\/tags\/|\^\{\}$/g, '')));
|
|
25
|
+
const v = m.version;
|
|
26
|
+
if (v && v !== '0.0.0' && ![`v${v}`, v, `${String(m.name).replace(/^@[^/]+\//, '')}--v${v}`].some((t) => tags.has(t)))
|
|
27
|
+
fail(`version ${v} of ${file} has no tag`);
|
|
28
|
+
if (!m.license) fail(`${file} declares no license`);
|
|
29
|
+
}
|
|
30
|
+
if (!['LICENSE', 'LICENSE.md', 'LICENSE.txt'].some(existsSync)) fail('no LICENSE file');
|
|
31
|
+
|
|
32
|
+
for (const ref of lines(git('for-each-ref', '--format=%(refname:lstrip=3)', 'refs/remotes/origin').out))
|
|
33
|
+
if (ref !== 'HEAD' && !heads.has(ref)) fail(`${ref}: a local ref to a branch origin no longer has`);
|
|
34
|
+
if (git('config', '--get', 'fetch.prune').out !== 'true') fail('fetch.prune is not set to true');
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// GUARD's facts: the hooks are wired and executable, the tiers exist and run something, and every
|
|
2
|
+
// guard of the contract resolves. What a diff should have honoured is the hygiene subagent's.
|
|
3
|
+
import { accessSync, constants, existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { fail, git, guards, sh } from './lib.mjs';
|
|
6
|
+
|
|
7
|
+
const hooks = git('config', '--get', 'core.hooksPath').out;
|
|
8
|
+
if (!hooks) fail('core.hooksPath is not set');
|
|
9
|
+
else if (!existsSync(hooks)) fail(`core.hooksPath names ${hooks}, which does not exist`);
|
|
10
|
+
else
|
|
11
|
+
for (const h of readdirSync(hooks)) {
|
|
12
|
+
try {
|
|
13
|
+
accessSync(join(hooks, h), constants.X_OK);
|
|
14
|
+
} catch {
|
|
15
|
+
fail(`hook ${join(hooks, h)} is not executable`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const scripts = existsSync('package.json') ? JSON.parse(readFileSync('package.json', 'utf8')).scripts || {} : {};
|
|
20
|
+
for (const tier of ['verify:commit', 'verify:push']) {
|
|
21
|
+
const s = scripts[tier];
|
|
22
|
+
if (!s) fail(`no ${tier} script`);
|
|
23
|
+
else if (s.split(/&&|;|\|\|/).every((part) => /^\s*echo\b/.test(part))) fail(`${tier} is hollow: ${s}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// The guards' `run` commands.
|
|
27
|
+
const runs = guards().flatMap((g) => g.run ?? []);
|
|
28
|
+
const found = (command) => {
|
|
29
|
+
const [bin, arg] = command.replace(/^['"]|['"]$/g, '').split(/\s+/);
|
|
30
|
+
if (['node', 'bash', 'sh'].includes(bin) && arg && /\.(m?js|cjs|sh)$/.test(arg)) return existsSync(arg);
|
|
31
|
+
if (bin.includes('/')) return existsSync(bin);
|
|
32
|
+
return sh('sh', ['-c', `command -v "$1"`, 'sh', bin], { env: { ...process.env, PATH: `node_modules/.bin:${process.env.PATH}` } }).status === 0;
|
|
33
|
+
};
|
|
34
|
+
for (const r of runs) if (!found(r)) fail(`guard command not found: ${r}`);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// LINES's facts, read from origin: every older line against main, both directions. A line diverged
|
|
2
|
+
// one way only is abandoned in the other; one diverged neither way can be closed. What ports a fix
|
|
3
|
+
// across is the hygiene subagent's.
|
|
4
|
+
import { absent, fail, git, remoteHeads } from './lib.mjs';
|
|
5
|
+
|
|
6
|
+
const heads = remoteHeads();
|
|
7
|
+
const older = [...heads.keys()].filter((b) => b.startsWith('support/'));
|
|
8
|
+
if (!older.length) absent('one release line — LINES has no subject');
|
|
9
|
+
const main = heads.get('main');
|
|
10
|
+
if (!main) fail('origin has no main');
|
|
11
|
+
|
|
12
|
+
for (const b of main ? older : []) {
|
|
13
|
+
const head = heads.get(b);
|
|
14
|
+
if ([head, main].some((s) => git('cat-file', '-e', `${s}^{commit}`).status !== 0)) {
|
|
15
|
+
fail(`${b}: not in this clone, divergence unmeasured`);
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const ahead = Number(git('rev-list', '--count', `${main}..${head}`).out);
|
|
19
|
+
const behind = Number(git('rev-list', '--count', `${head}..${main}`).out);
|
|
20
|
+
if (!ahead && !behind) fail(`${b} has not diverged from main: it can be closed`);
|
|
21
|
+
else if (!ahead || !behind) fail(`${b} has diverged in one direction only: ${ahead} ahead, ${behind} behind main`);
|
|
22
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// PKG's facts: every entry point is in the tree and in the tarball, nothing runs at install time,
|
|
2
|
+
// and no tracked file carries a secret, by its name or in its contents.
|
|
3
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
4
|
+
import { fail, manifest, sh, tracked } from './lib.mjs';
|
|
5
|
+
|
|
6
|
+
const pkg = manifest();
|
|
7
|
+
const clean = (p) => p.replace(/^\.\//, '');
|
|
8
|
+
|
|
9
|
+
const entries = [];
|
|
10
|
+
for (const k of ['main', 'module', 'types', 'typings', 'browser'])
|
|
11
|
+
if (typeof pkg[k] === 'string') entries.push([k, pkg[k]]);
|
|
12
|
+
if (typeof pkg.bin === 'string') entries.push(['bin', pkg.bin]);
|
|
13
|
+
else for (const [k, v] of Object.entries(pkg.bin || {})) entries.push([`bin > ${k}`, v]);
|
|
14
|
+
const walk = (e, path) => {
|
|
15
|
+
if (typeof e === 'string') entries.push([path, e]);
|
|
16
|
+
else if (e && typeof e === 'object') for (const [k, v] of Object.entries(e)) walk(v, `${path} > ${k}`);
|
|
17
|
+
};
|
|
18
|
+
walk(pkg.exports, 'exports');
|
|
19
|
+
|
|
20
|
+
const pack = sh('npm', ['pack', '--dry-run', '--json', '--ignore-scripts']);
|
|
21
|
+
let packed = null;
|
|
22
|
+
try {
|
|
23
|
+
packed = new Set(JSON.parse(pack.out)[0].files.map((f) => f.path));
|
|
24
|
+
} catch {
|
|
25
|
+
fail(`npm pack --dry-run failed: ${pack.err.split('\n').pop()}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (const [field, target] of entries) {
|
|
29
|
+
if (target.includes('*')) continue;
|
|
30
|
+
const p = clean(target);
|
|
31
|
+
if (!existsSync(p)) fail(`entry ${field} → ${p}: not in the tree`);
|
|
32
|
+
else if (packed && !packed.has(p)) fail(`entry ${field} → ${p}: not in the tarball`);
|
|
33
|
+
}
|
|
34
|
+
for (const k of ['preinstall', 'install', 'postinstall', 'prepare'])
|
|
35
|
+
if (pkg.scripts?.[k]) fail(`lifecycle script ${k} runs on the consumer's machine`);
|
|
36
|
+
// A secret, by its name and by its shape in the contents of every tracked file at this commit —
|
|
37
|
+
// never the history. Its value never leaves this script whole: four characters, then masked.
|
|
38
|
+
const shapes = [
|
|
39
|
+
['private key', /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY-----/],
|
|
40
|
+
['GitLab token', /\bgl(?:pat|dt|rt|ptt)-[0-9A-Za-z_-]{20,}/],
|
|
41
|
+
['GitHub token', /\b(?:gh[pousr]_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{22,})/],
|
|
42
|
+
['npm token', /\bnpm_[A-Za-z0-9]{36}/],
|
|
43
|
+
];
|
|
44
|
+
for (const { path: f, kind } of tracked()) {
|
|
45
|
+
if (/(^|\/)(\.env(\..*)?|[^/]*\.(pem|key|p12)|id_rsa)$/i.test(f)) fail(`a tracked file named like a secret: ${f}`);
|
|
46
|
+
// A link's target is not the repository's: the cut reads none through a link, nor does this.
|
|
47
|
+
if (kind === 'link' || !existsSync(f) || !statSync(f).isFile()) continue;
|
|
48
|
+
const bytes = readFileSync(f);
|
|
49
|
+
if (bytes.subarray(0, 8000).includes(0)) continue;
|
|
50
|
+
bytes.toString('utf8').split('\n').forEach((line, i) => {
|
|
51
|
+
for (const [kind, shape] of shapes) {
|
|
52
|
+
const m = line.match(shape);
|
|
53
|
+
if (m) fail(`a credential in ${f}:${i + 1}: ${m[0].slice(0, 4)}… (${kind})`);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// TYPE's facts: a types entry exists, its condition comes first wherever it is declared, and the
|
|
2
|
+
// declaration entries import nothing a consumer does not receive. Whether the surface matches the
|
|
3
|
+
// documentation is the hygiene subagent's.
|
|
4
|
+
import { builtinModules } from 'node:module';
|
|
5
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { fail, manifest } from './lib.mjs';
|
|
7
|
+
|
|
8
|
+
const pkg = manifest();
|
|
9
|
+
const declarations = new Set([pkg.types, pkg.typings].filter((t) => typeof t === 'string'));
|
|
10
|
+
const walk = (e, path) => {
|
|
11
|
+
if (!e || typeof e !== 'object') return;
|
|
12
|
+
const keys = Object.keys(e);
|
|
13
|
+
const conditions = keys.filter((k) => !k.startsWith('.'));
|
|
14
|
+
if (conditions.includes('types')) {
|
|
15
|
+
if (conditions[0] !== 'types') fail(`types is not the first condition of ${path}: ${conditions.join(', ')}`);
|
|
16
|
+
if (typeof e.types === 'string') declarations.add(e.types);
|
|
17
|
+
}
|
|
18
|
+
for (const k of keys) walk(e[k], `${path} > ${k}`);
|
|
19
|
+
};
|
|
20
|
+
walk(pkg.exports, 'exports');
|
|
21
|
+
if (!declarations.size) fail('no types entry');
|
|
22
|
+
|
|
23
|
+
const received = new Set([...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})]);
|
|
24
|
+
const nameOf = (s) => (s.startsWith('@') ? s.split('/').slice(0, 2).join('/') : s.split('/')[0]);
|
|
25
|
+
for (const d of declarations) {
|
|
26
|
+
const file = d.replace(/^\.\//, '');
|
|
27
|
+
if (d.includes('*') || !existsSync(file)) continue;
|
|
28
|
+
const text = readFileSync(file, 'utf8');
|
|
29
|
+
const specifiers = [...text.matchAll(/(?:\bfrom|\bimport)\s*\(?\s*['"]([^'"]+)['"]/g)].map((m) => m[1]);
|
|
30
|
+
for (const s of new Set(specifiers)) {
|
|
31
|
+
if (s.startsWith('.') || s.startsWith('node:') || builtinModules.includes(s)) continue;
|
|
32
|
+
if (!received.has(nameOf(s))) fail(`${file} imports ${nameOf(s)}, in neither dependencies nor peerDependencies`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// The cut: what the lenses read, into parts one subagent reads whole, each written as a file into a
|
|
2
|
+
// directory this script creates outside the tree. With no argument, the tracked text files in path
|
|
3
|
+
// order — the contract's generated[] paths, the binary files, the submodules and the links in no
|
|
4
|
+
// part — and beside the parts `tracked.txt`, every tracked file with its size, a submodule or a link
|
|
5
|
+
// marked as one, which a lens searches rather than reads. The cut names the tracked links, which
|
|
6
|
+
// no lens opens, each escaped in its note as `named` escapes it. A part closes
|
|
7
|
+
// when the next file would overflow it, and a file larger than a part is a part of its own, so the
|
|
8
|
+
// same tree gives the same parts.
|
|
9
|
+
// `node cut.mjs`, from the repository root. It never exits 0 on a cut it could not
|
|
10
|
+
// make: the parts are the plan the lenses are dispatched on, and report.mjs stops the run on it. A
|
|
11
|
+
// tracked name holding a newline is such a cut: a part is one name per line, and the escaped form
|
|
12
|
+
// names no file, so the tree's cut fails on it, the name escaped in its error to be renamed.
|
|
13
|
+
import { accessSync, constants, lstatSync, mkdtempSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { tmpdir } from 'node:os';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { generatedPaths, named, tracked } from './lib.mjs';
|
|
17
|
+
|
|
18
|
+
// The size, here and nowhere else: what a lens subagent was measured to read whole, raised only by
|
|
19
|
+
// a campaign's context peaks and its lenses' *Not reached:* lines.
|
|
20
|
+
const TREE = 200000;
|
|
21
|
+
|
|
22
|
+
// pack <items> <limit> — consecutive { name, bytes, text } items, a part closing when the next
|
|
23
|
+
// would overflow it.
|
|
24
|
+
const pack = (items, limit) => {
|
|
25
|
+
const parts = [];
|
|
26
|
+
let part;
|
|
27
|
+
for (const item of items) {
|
|
28
|
+
if (!part || (part.items.length && part.bytes + item.bytes > limit)) parts.push((part = { bytes: 0, items: [] }));
|
|
29
|
+
part.items.push(item);
|
|
30
|
+
part.bytes += item.bytes;
|
|
31
|
+
}
|
|
32
|
+
return parts;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const files = tracked(), links = files.filter((f) => f.kind === 'link').map((f) => f.path);
|
|
36
|
+
const split = files.filter((f) => f.path.includes('\n')).map((f) => named(f.path));
|
|
37
|
+
if (split.length) throw new Error(`a tracked name holds a newline, which no part can carry: ${split.join(', ')}`);
|
|
38
|
+
const out = mkdtempSync(join(tmpdir(), 'codelife-parts-'));
|
|
39
|
+
const notes = [], items = [];
|
|
40
|
+
{
|
|
41
|
+
const generated = generatedPaths();
|
|
42
|
+
let binary = 0;
|
|
43
|
+
const left = [], submodules = [], sizes = [];
|
|
44
|
+
for (const { path: name, kind } of files.filter((f) => f.kind !== 'link')) {
|
|
45
|
+
if (kind !== 'submodule') sizes.push({ name, bytes: lstatSync(name).size });
|
|
46
|
+
if (kind === 'binary') binary++;
|
|
47
|
+
else if (kind === 'submodule') submodules.push(name);
|
|
48
|
+
else if (generated.some((g) => name === g || name.startsWith(`${g}/`))) left.push(name);
|
|
49
|
+
else {
|
|
50
|
+
accessSync(name, constants.R_OK);
|
|
51
|
+
items.push({ ...sizes.at(-1), text: `${name}\n` });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
notes.push(`binary files in no part: ${binary}`, `generated paths in no part: ${left.join(', ') || 'none'}`,
|
|
55
|
+
`submodules in no part: ${submodules.join(', ') || 'none'}`);
|
|
56
|
+
writeFileSync(join(out, 'tracked.txt'), [...sizes.map((s) => `${s.bytes} ${s.name}`), ...submodules.map((s) => `submodule ${s}`),
|
|
57
|
+
...links.map((l) => `link ${l}`)].map((line) => `${line}\n`).join(''));
|
|
58
|
+
const total = sizes.reduce((sum, s) => sum + s.bytes, 0);
|
|
59
|
+
const largest = [...sizes].sort((a, b) => b.bytes - a.bytes).slice(0, 10).map((s) => `${s.bytes} ${s.name}`);
|
|
60
|
+
notes.push(`tracked files in ${join(out, 'tracked.txt')}: ${sizes.length} files, ${total} bytes; largest: ${largest.join(', ') || 'none'}`);
|
|
61
|
+
}
|
|
62
|
+
notes.push(`links in no part: ${links.map(named).join(', ') || 'none'}`);
|
|
63
|
+
|
|
64
|
+
const parts = pack(items, TREE);
|
|
65
|
+
console.log(`parts in ${out}, at most ${TREE} bytes each`);
|
|
66
|
+
parts.forEach((part, i) => {
|
|
67
|
+
const file = `part-${i + 1}.txt`;
|
|
68
|
+
writeFileSync(join(out, file), part.items.map((item) => item.text).join(''));
|
|
69
|
+
const n = part.items.length, first = part.items[0].name, last = part.items[n - 1].name;
|
|
70
|
+
console.log(` ${file} — ${part.bytes} bytes, ${n} file${n > 1 ? 's' : ''}: ${first}${n > 1 ? ` … ${last}` : ''}`);
|
|
71
|
+
});
|
|
72
|
+
if (!parts.length) console.log(' none');
|
|
73
|
+
for (const note of notes) console.log(note);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// The history the lenses read, each answer under the command that produced it: the files changed
|
|
2
|
+
// most in six months, the latest fix commits, and when a document and anything else last changed.
|
|
3
|
+
import { absent, git, lines, named, paths, tracked } from './lib.mjs';
|
|
4
|
+
|
|
5
|
+
// log <git log's arguments> — its lines, or `absent` when there is no history to read.
|
|
6
|
+
const log = (...args) => {
|
|
7
|
+
const r = git('log', ...args);
|
|
8
|
+
if (r.status !== 0) absent(`git log — ${r.err}`);
|
|
9
|
+
return lines(r.out);
|
|
10
|
+
};
|
|
11
|
+
// section <title> <the command, as a reader would type it> <its answer>
|
|
12
|
+
const section = (title, shown, found) => {
|
|
13
|
+
console.log(`${title} — ${shown}`);
|
|
14
|
+
for (const line of found.length ? found : ['none']) console.log(` ${line}`);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Each path under its name at HEAD: the log runs newest first, so a rename is read before the
|
|
18
|
+
// commits under its old name, and a path HEAD does not track is left out.
|
|
19
|
+
const counts = new Map(), renamed = new Map(), head = new Set(tracked().map((f) => f.path));
|
|
20
|
+
const changes = paths('log', '--since=6 months ago', '--name-status', '-M', '--format=');
|
|
21
|
+
for (let i = 0; i < changes.length; i++) {
|
|
22
|
+
const status = changes[i], p = changes[++i], to = /^[RC]/.test(status) ? changes[++i] : p, name = renamed.get(to) ?? to;
|
|
23
|
+
if (status[0] === 'R') renamed.set(p, name);
|
|
24
|
+
counts.set(name, (counts.get(name) || 0) + 1);
|
|
25
|
+
}
|
|
26
|
+
section('files changed most in six months', "git log --since='6 months ago' --name-status -M --format=, each path under its name at HEAD",
|
|
27
|
+
[...counts].filter(([p]) => head.has(p)).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 20).map(([p, n]) => `${n} ${named(p)}`));
|
|
28
|
+
section('fix commits, the 20 latest', "git log --format='%h %s', the subjects opening on fix",
|
|
29
|
+
log('--format=%h %s').filter((c) => /^\S+ fix/i.test(c)).slice(0, 20));
|
|
30
|
+
section('last change to a document', "git log -1 --format=%ci -- '*.md'", log('-1', '--format=%ci', '--', '*.md'));
|
|
31
|
+
section('last change to anything else', "git log -1 --format=%ci -- . ':(exclude)*.md'",
|
|
32
|
+
log('-1', '--format=%ci', '--', '.', ':(exclude)*.md'));
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// What the hygiene scripts share. Each runs from the repository's root, prints one line per failed
|
|
2
|
+
// fact and exits 0, or calls `absent` and exits 3 — the protocol report.mjs reads.
|
|
3
|
+
import '../node-floor.mjs';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { closeSync, existsSync, mkdtempSync, openSync, readFileSync, rmSync } from 'node:fs';
|
|
6
|
+
import { constants, tmpdir } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { entries } from '../contract.mjs';
|
|
9
|
+
|
|
10
|
+
// sh <file> [args] [options] — its stdout and stderr trimmed (stdout as is with `trim: false`), and
|
|
11
|
+
// its exit status, stdin closed; never throws.
|
|
12
|
+
// A signal is 128 + its number, as a shell says it, never spawnSync's `null`, which every
|
|
13
|
+
// `if (r.status)` would take for a success.
|
|
14
|
+
export const sh = (file, args = [], { trim = true, ...options } = {}) => {
|
|
15
|
+
const r = spawnSync(file, args, { encoding: 'utf8', input: '', maxBuffer: 256 * 1024 * 1024, ...options });
|
|
16
|
+
const status = r.error ? 127 : r.signal ? 128 + (constants.signals[r.signal] ?? 0) : r.status;
|
|
17
|
+
return { status, out: trim ? (r.stdout || '').trim() : r.stdout || '', err: (r.stderr || r.error?.message || (r.signal ? `killed by ${r.signal}` : '')).trim() };
|
|
18
|
+
};
|
|
19
|
+
export const git = (...args) => sh('git', args);
|
|
20
|
+
export const lines = (text) => text.split('\n').filter(Boolean);
|
|
21
|
+
// paths <git args> — the paths git lists, read NUL-separated, or `absent` when git cannot list them:
|
|
22
|
+
// without -z, git quotes a name holding a byte above 0x7F, a quote, a backslash or a control
|
|
23
|
+
// character, and the quoted form names no file. Read as UTF-8: a name whose bytes are not valid
|
|
24
|
+
// UTF-8 still names none.
|
|
25
|
+
export const paths = (...args) => {
|
|
26
|
+
const r = git(args[0], '-z', ...args.slice(1));
|
|
27
|
+
if (r.status !== 0) absent(`git ${args[0]} — ${r.err}`);
|
|
28
|
+
return r.out.split('\0').filter(Boolean);
|
|
29
|
+
};
|
|
30
|
+
// named <path> — a path as a line of a block: a control character escaped as git escapes it, so a
|
|
31
|
+
// name holding a newline cannot print a line of its own.
|
|
32
|
+
export const named = (p) => p.replace(/[\0-\x1f\x7f]/g, (c) =>
|
|
33
|
+
({ '\t': '\\t', '\n': '\\n' })[c] ?? `\\${c.charCodeAt(0).toString(8).padStart(3, '0')}`);
|
|
34
|
+
|
|
35
|
+
// absent <reason> — the subject is not there: could not tell, never a pass.
|
|
36
|
+
export const absent = (reason) => {
|
|
37
|
+
console.error(reason);
|
|
38
|
+
process.exit(3);
|
|
39
|
+
};
|
|
40
|
+
export const fail = (fact) => console.log(fact);
|
|
41
|
+
|
|
42
|
+
// tracked — every tracked path with its kind, read from the index: `submodule`, `link`, `binary` or
|
|
43
|
+
// `text`; `absent` when git cannot list them.
|
|
44
|
+
export const tracked = () =>
|
|
45
|
+
paths('ls-files', '--eol', '-s').map((entry) => {
|
|
46
|
+
const i = entry.indexOf('\t'), j = entry.indexOf('\t', i + 1);
|
|
47
|
+
const mode = entry.slice(0, entry.indexOf(' ')), eol = entry.slice(i + 1, j);
|
|
48
|
+
const kind = { 160000: 'submodule', 120000: 'link' }[mode] || (eol.startsWith('i/-text') ? 'binary' : 'text');
|
|
49
|
+
return { path: entry.slice(j + 1), kind };
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// manifest — package.json parsed, or `absent` when there is none.
|
|
53
|
+
export const manifest = () => {
|
|
54
|
+
if (!existsSync('package.json')) absent('no package.json — no subject for this lens');
|
|
55
|
+
return JSON.parse(readFileSync('package.json', 'utf8'));
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// runner — the package manager's run prefix, from the lockfile, then from `packageManager`.
|
|
59
|
+
export const runner = (pkg) => {
|
|
60
|
+
if (existsSync('pnpm-lock.yaml')) return ['pnpm', ['run']];
|
|
61
|
+
if (existsSync('yarn.lock')) return ['yarn', ['run']];
|
|
62
|
+
if (existsSync('package-lock.json')) return ['npm', ['run']];
|
|
63
|
+
const named = (pkg.packageManager || '').split('@')[0];
|
|
64
|
+
return [named || 'npm', ['run']];
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// guards [key] — the contract's guards[] entries, or another list such as generated[], each value a
|
|
68
|
+
// list (scripts/contract.mjs); generatedPaths — generated[]'s paths, a folder's without its slash.
|
|
69
|
+
export const guards = (key = 'guards') => entries(key);
|
|
70
|
+
export const generatedPaths = () => entries('generated').flatMap((g) => g.path).map((p) => p.replace(/\/$/, ''));
|
|
71
|
+
|
|
72
|
+
// glob <pattern> — the pattern as a regular expression over a whole path: `**/` any number of
|
|
73
|
+
// whole folders, `**` anything, `*` and `?` within one segment, `{a,b}` either.
|
|
74
|
+
export const glob = (p) => new RegExp(`^${p.replace(/[.+^$()|[\]\\]/g, '\\$&').replace(/\{([^}]*)\}/g, (_, a) => `(${a.split(',').join('|')})`)
|
|
75
|
+
.replace(/\*\*\/?|\*|\?/g, (m) => ({ '**/': '(?:.*/)?', '**': '.*', '*': '[^/]*' })[m] ?? '[^/]')}$`);
|
|
76
|
+
|
|
77
|
+
// The checks `codelife checks` and `codelife verify` run, one after the other, from the root.
|
|
78
|
+
// tier <name> — the check running the `verify:<name>` script through the package manager, or the
|
|
79
|
+
// one saying it is not ported. On Windows a package manager is a `.cmd`, which only a shell resolves.
|
|
80
|
+
export const tier = (name) => {
|
|
81
|
+
const pkg = existsSync('package.json') ? JSON.parse(readFileSync('package.json', 'utf8')) : null;
|
|
82
|
+
if (!pkg?.scripts?.[`verify:${name}`]) return { name: `the ${name} tier`, missing: `no verify:${name} script — not ported` };
|
|
83
|
+
const [pm, run] = runner(pkg);
|
|
84
|
+
return { name: `the ${name} tier`, file: pm, args: [...run, `verify:${name}`], shell: process.platform === 'win32' };
|
|
85
|
+
};
|
|
86
|
+
// fired <entries> <paths> — the contract's entries whose `when` matches one of the paths.
|
|
87
|
+
export const fired = (entries, touched) => entries.filter((g) => g.when.some((w) => touched.some((t) => glob(w).test(t))));
|
|
88
|
+
// logged <name> <file> [args] [shell] — runs it with its whole output, stdout and stderr in the
|
|
89
|
+
// order printed, in a file outside the tree named on stderr when it starts, so that a run a timeout
|
|
90
|
+
// cuts leaves it readable: its status (`signal <name>` when killed), its output, and the file — none,
|
|
91
|
+
// and the file removed, when the output is empty. The one runner of the verbs and the audit (R-282).
|
|
92
|
+
let logs, count = 0;
|
|
93
|
+
export const logged = (name, file, args = [], shell = false) => {
|
|
94
|
+
logs ??= mkdtempSync(join(tmpdir(), 'verify-'));
|
|
95
|
+
const log = join(logs, `${++count}.log`);
|
|
96
|
+
console.error(`running ${name}, output in ${log}`);
|
|
97
|
+
const fd = openSync(log, 'w');
|
|
98
|
+
const r = spawnSync(file, args, { shell, stdio: ['ignore', fd, fd] });
|
|
99
|
+
closeSync(fd);
|
|
100
|
+
const out = readFileSync(log, 'utf8');
|
|
101
|
+
if (!out.trim()) rmSync(log);
|
|
102
|
+
return { status: r.error ? r.error.code || r.error.message : r.signal ? `signal ${r.signal}` : r.status, out, file: out.trim() ? log : null };
|
|
103
|
+
};
|
|
104
|
+
// runChecks <checks> [lead] — one line each, `passed`, `failed` or, for a hygiene script whose subject
|
|
105
|
+
// is absent, `could not tell`, anything but a pass followed by its last lines and a guard's `note`;
|
|
106
|
+
// true when one failed.
|
|
107
|
+
// `lead` opens each check's line: `- ` in a verb whose block is a list.
|
|
108
|
+
export const runChecks = (checks, lead = '') => {
|
|
109
|
+
let red = false;
|
|
110
|
+
for (const c of checks) {
|
|
111
|
+
if (c.pass) { console.log(`${lead}passed: ${c.name}`); continue; }
|
|
112
|
+
const r = c.missing ? null : logged(c.name, c.file, c.args, c.shell);
|
|
113
|
+
const out = c.missing ?? r.out.trimEnd();
|
|
114
|
+
const outcome = r?.status !== 0 ? (c.hygiene ? 'could not tell' : 'failed') : c.hygiene && out ? 'failed' : 'passed';
|
|
115
|
+
if (outcome === 'passed') { console.log(`${lead}passed: ${c.name}`); continue; }
|
|
116
|
+
red ||= outcome === 'failed';
|
|
117
|
+
console.log(`${lead}${outcome}: ${c.name}${r && r.status !== 0 ? ` — exit ${r.status}` : ''}${r?.file ? `, output in ${r.file}` : ''}`);
|
|
118
|
+
for (const l of out.split('\n').slice(-20)) console.log(` ${l}`);
|
|
119
|
+
if (c.note?.length) console.log(` note: ${c.note.join(' ')}`);
|
|
120
|
+
}
|
|
121
|
+
return red;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// remoteHeads — origin's branches by name → sha, or `absent` when origin cannot be read.
|
|
125
|
+
export const remoteHeads = () => {
|
|
126
|
+
const r = git('ls-remote', '--heads', 'origin');
|
|
127
|
+
if (r.status !== 0) absent(`origin unreachable — ${r.err || 'git ls-remote failed'}`);
|
|
128
|
+
return new Map(lines(r.out).map((l) => l.split('\t')).map(([sha, ref]) => [ref.replace('refs/heads/', ''), sha]));
|
|
129
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// verify's reporting operation: runs, one after the other and with no model, the hygiene script of
|
|
2
|
+
// each lens named, with `--tree` the history every lens reads, with
|
|
3
|
+
// `--timings` PERF's timings, each distinct command given after `--` once, and last, with `--tree`, the tree's cut,
|
|
4
|
+
// and prints one `### verify result` block.
|
|
5
|
+
// Nothing stops the run, and it exits 0 whatever it found — a failed fact is a candidate to write,
|
|
6
|
+
// not a gate — but on a cut that failed, the plan the lenses are dispatched on.
|
|
7
|
+
// `node <this directory>/report.mjs [--lenses A,B,…] [--tree] [--timings] [-- '<command>'…]`, from the repository root: one quoted argument per command, each run whole.
|
|
8
|
+
//
|
|
9
|
+
// How each run is read:
|
|
10
|
+
// hygiene script — exit 0 and silent: passed; exit 0 with lines: failed, one line per fact;
|
|
11
|
+
// any other exit, a crash included: could not tell.
|
|
12
|
+
// tree fact, timing
|
|
13
|
+
// — exit 0: passed, its output handed to its lens; otherwise could not tell.
|
|
14
|
+
// command — exit 0: passed; otherwise failed. Run through lib.mjs's `logged`, as the verbs'
|
|
15
|
+
// checks are; the block names its file, and under a failure carries the output's
|
|
16
|
+
// last lines — or says the output is empty.
|
|
17
|
+
// cut — exit 0: passed, its parts listed; otherwise failed, and the report exits 1.
|
|
18
|
+
import '../node-floor.mjs';
|
|
19
|
+
import { spawnSync } from 'node:child_process';
|
|
20
|
+
import { existsSync } from 'node:fs';
|
|
21
|
+
import { dirname, join } from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
import { logged } from './lib.mjs';
|
|
24
|
+
|
|
25
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const argv = process.argv.slice(2);
|
|
27
|
+
const cut = argv.indexOf('--');
|
|
28
|
+
const options = cut < 0 ? argv : argv.slice(0, cut);
|
|
29
|
+
const commands = cut < 0 ? [] : argv.slice(cut + 1);
|
|
30
|
+
let lenses = [], tree = false, timings = false;
|
|
31
|
+
for (let i = 0; i < options.length; i++) {
|
|
32
|
+
if (options[i] === '--lenses' && options[i + 1]) lenses = options[++i].split(',').filter(Boolean);
|
|
33
|
+
else if (options[i] === '--tree') tree = true;
|
|
34
|
+
else if (options[i] === '--timings') timings = true;
|
|
35
|
+
else {
|
|
36
|
+
console.error('usage: node report.mjs [--lenses A,B,…] [--tree] [--timings] [-- \'<command>\'…]');
|
|
37
|
+
process.exit(2);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// The scripts PERF's timings cover.
|
|
41
|
+
const timed = ['test:all', 'verify:push'];
|
|
42
|
+
|
|
43
|
+
const lines = ['### verify result'];
|
|
44
|
+
const report = (label, outcome, output = '') => {
|
|
45
|
+
lines.push(`- ${label} — ${outcome}`);
|
|
46
|
+
if (output.trim()) lines.push(...output.trimEnd().split('\n').map((l) => ` ${l}`));
|
|
47
|
+
};
|
|
48
|
+
const run = (file, args) => {
|
|
49
|
+
const r = spawnSync(file, args, { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
50
|
+
const status = r.error ? r.error.code || r.error.message : r.signal ? `signal ${r.signal}` : r.status;
|
|
51
|
+
return { status, out: r.stdout || '', err: r.stderr || '' };
|
|
52
|
+
};
|
|
53
|
+
// probe <label> <file> <args> — exit 0: passed, its output handed on; otherwise could not tell.
|
|
54
|
+
const probe = (label, file, args) => {
|
|
55
|
+
const r = run(file, args);
|
|
56
|
+
if (r.status === 0) report(label, 'passed', r.out);
|
|
57
|
+
else report(label, `could not tell: exit ${r.status}`, r.err || r.out);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
for (const lens of lenses) {
|
|
61
|
+
const script = join(here, `${lens}.mjs`);
|
|
62
|
+
if (!existsSync(script)) {
|
|
63
|
+
lines.push(`- ${lens} — no script`);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const r = run(process.execPath, [script]);
|
|
67
|
+
const label = `${lens} · node ${script}`;
|
|
68
|
+
if (r.status !== 0) report(label, `could not tell: exit ${r.status}`, r.err || r.out);
|
|
69
|
+
else if (r.out.trim()) report(label, 'failed', r.out);
|
|
70
|
+
else report(label, 'passed');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const name of tree ? ['history'] : []) {
|
|
74
|
+
const script = join(here, `${name}.mjs`);
|
|
75
|
+
probe(`tree · node ${script}`, process.execPath, [script]);
|
|
76
|
+
}
|
|
77
|
+
for (const name of timings ? timed : []) {
|
|
78
|
+
const script = join(here, 'timing.mjs');
|
|
79
|
+
probe(`PERF · node ${script} ${name}`, process.execPath, [script, name]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// The lines of a failed command's output the block carries: the end, where a runner prints its
|
|
83
|
+
// summary and its failures' detail.
|
|
84
|
+
const last = 40;
|
|
85
|
+
for (const command of new Set(commands)) {
|
|
86
|
+
const r = logged(command, command, [], true);
|
|
87
|
+
const outcome = r.status === 0 ? 'passed' : `failed: exit ${r.status}`;
|
|
88
|
+
if (!r.file) report(command, `${outcome}, output empty`);
|
|
89
|
+
else report(command, `${outcome}, output in ${r.file}`, r.status === 0 ? '' : r.out.trimEnd().split('\n').slice(-last).join('\n'));
|
|
90
|
+
}
|
|
91
|
+
let status = 0;
|
|
92
|
+
if (tree) {
|
|
93
|
+
const args = [join(here, 'cut.mjs')];
|
|
94
|
+
const r = run(process.execPath, args);
|
|
95
|
+
const label = `parts · node ${args.join(' ')}`;
|
|
96
|
+
if (r.status === 0) report(label, 'passed', r.out);
|
|
97
|
+
else {
|
|
98
|
+
report(label, `failed: exit ${r.status}`, r.err || r.out);
|
|
99
|
+
status = 1;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.log(lines.join('\n'));
|
|
104
|
+
process.exit(status);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// One script's timing, for PERF: three runs through the repository's run prefix, the median and the
|
|
2
|
+
// runs in seconds. A manifest without the script, or a run that fails, is a timing nobody took.
|
|
3
|
+
// `node timing.mjs <script>`, from the repository root.
|
|
4
|
+
import { absent, manifest, runner, sh } from './lib.mjs';
|
|
5
|
+
|
|
6
|
+
const name = process.argv[2];
|
|
7
|
+
const pkg = manifest();
|
|
8
|
+
if (!pkg.scripts?.[name]) absent(`no ${name} script in package.json`);
|
|
9
|
+
const [bin, args] = runner(pkg);
|
|
10
|
+
const command = [bin, ...args, name].join(' ');
|
|
11
|
+
const runs = [];
|
|
12
|
+
for (let i = 0; i < 3; i++) {
|
|
13
|
+
const start = process.hrtime.bigint();
|
|
14
|
+
const r = sh(bin, [...args, name]);
|
|
15
|
+
if (r.status !== 0) absent(`${command} exits ${r.status} — ${(r.err || r.out).split('\n').slice(-5).join('\n')}`);
|
|
16
|
+
runs.push(Number(process.hrtime.bigint() - start) / 1e9);
|
|
17
|
+
}
|
|
18
|
+
const s = (seconds) => `${seconds.toFixed(2)} s`;
|
|
19
|
+
console.log(`${command} — median ${s([...runs].sort((a, b) => a - b)[1])} (${runs.map(s).join(', ')})`);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `codelife verify commit|push [<base>]` (R-199, C-857): an implementer's verification before each
|
|
3
|
+
// commit, and the push tier before a lot's merge request — each `generated[]` entry of the contract
|
|
4
|
+
// whose `when` matches a path of the working tree's change (with the diff from <base> to `HEAD` when
|
|
5
|
+
// given), its path staged, then the `verify:<tier>` script and each matching `guards[]` entry. One
|
|
6
|
+
// line per check, as `codelife checks` prints them. Exit 0 when none failed, 1 when one failed or the tier is not ported, 2 on a usage error.
|
|
7
|
+
import './node-floor.mjs'
|
|
8
|
+
import { fired, git, guards, runChecks, tier } from './verify/lib.mjs'
|
|
9
|
+
|
|
10
|
+
const [name, base] = process.argv.slice(2)
|
|
11
|
+
if (!['commit', 'push'].includes(name) || (base && git('rev-parse', '--verify', '--quiet', `${base}^{commit}`).status !== 0)) {
|
|
12
|
+
console.error(`usage: codelife verify commit|push [<base>]${base ? ` — '${base}' is no commit git can resolve` : ''}`)
|
|
13
|
+
process.exit(2)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const names = (...args) => git(...args).out.split('\0').filter(Boolean)
|
|
17
|
+
const touched = () => [...names('diff', '--name-only', '-z', 'HEAD'), ...names('ls-files', '--others', '--exclude-standard', '-z'),
|
|
18
|
+
...(base ? names('diff', '--name-only', '-z', `${base}...HEAD`) : [])]
|
|
19
|
+
const shell = (cs, note) => (cs ?? []).map((c) => ({ name: c, file: c, shell: true, note }))
|
|
20
|
+
// The outputs first, each staged before the tier and the guards run (R-125): a check that fails then
|
|
21
|
+
// leaves them staged, and a tier that checks an output is fresh reads it regenerated.
|
|
22
|
+
const outputs = fired(guards('generated'), touched())
|
|
23
|
+
if (runChecks(outputs.flatMap((g) => shell(g.command)))) process.exit(1)
|
|
24
|
+
for (const p of outputs.flatMap((g) => g.path ?? [])) {
|
|
25
|
+
const r = git('add', '-A', '--', p)
|
|
26
|
+
console.log(r.status ? `failed: staging ${p} — ${r.err}` : `staged: ${p}`)
|
|
27
|
+
if (r.status) process.exit(1)
|
|
28
|
+
}
|
|
29
|
+
if (runChecks([tier(name), ...fired(guards(), touched()).flatMap((g) => shell(g.run, g.note))])) process.exit(1)
|