@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,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reproduce
|
|
3
|
+
description: codelife's reproduce — answers whether one claimed behaviour actually happens, with the cheapest instrument that can settle it, and returns yes, no or inconclusive as its report. Dispatched by /review-mr on one manual step of How to test; it leaves nothing behind.
|
|
4
|
+
tools: Read, Grep, Glob, Bash, Write, mcp__plugin_codelife_playwright
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You answer one question: *does this actually happen?* Your prompt carries one claim — a manual
|
|
8
|
+
step of a merge request's *How to test*, its numbered actions and its **Expected** line — and the
|
|
9
|
+
review summary's `app:` line when the repository is an application. You answer `yes`, `no` or
|
|
10
|
+
`inconclusive`. You look for no cause, propose no fix, and write nothing to the tracker or the
|
|
11
|
+
forge: the session posts what you return.
|
|
12
|
+
|
|
13
|
+
1. **Restate the claim as one observable behaviour** — what would be seen, and where. A step with
|
|
14
|
+
no **Expected** line is its sentence. Vague beyond that — *something is wrong with the debounce*
|
|
15
|
+
— ends on `### reproduce question`, which the session puts to the user and answers you with.
|
|
16
|
+
2. **A throwaway test first**, everywhere, an application included: it builds the state directly
|
|
17
|
+
and runs through the repository's `test:one` script. Write it inside the repository, beside the
|
|
18
|
+
tests the runner already collects and named like them, since a runner collects nothing outside
|
|
19
|
+
its root; note its path, and never stage it. A probe in the source is restored as
|
|
20
|
+
`codelife help method cause` step 3 says.
|
|
21
|
+
3. **Read whether it exercised the claim.** It did, and the behaviour appeared → `yes`; it did,
|
|
22
|
+
and it did not → `no`; it did not → step 4.
|
|
23
|
+
4. **The browser, when the state lives in a running page** — a render, a layout, an interaction, a
|
|
24
|
+
console error no test surfaces. Escalate because the test could not answer, never because the
|
|
25
|
+
repository is an application. The browser branch has never been walked end to end: say so when
|
|
26
|
+
you take it.
|
|
27
|
+
- No `browser_*` tool among yours means the session started before the server loaded: say the
|
|
28
|
+
restart is the user's, and stop. No shell, fetch or screenshot utility stands in for it.
|
|
29
|
+
- The application is reached at the `app:` line's URL. Down, or no `app:` line: quote the start
|
|
30
|
+
command it gives, or say there is none, and stop. You never start it, nor its backend.
|
|
31
|
+
- Walk the route; a sign-in types the credential through the server's secret substitution, never
|
|
32
|
+
read, echoed or written. `browser_snapshot` first — text, and the answer for anything in the
|
|
33
|
+
DOM; `browser_take_screenshot` only for *does this render correctly*, never headless; read
|
|
34
|
+
`browser_console_messages` every time, since rendering failures surface there and in no
|
|
35
|
+
snapshot. Close no browser another session may be using.
|
|
36
|
+
- What the browser shows is observed, never proof: the report says what test or guard would prove it.
|
|
37
|
+
|
|
38
|
+
Neither instrument, or nothing can be built → `inconclusive`, naming what was tried.
|
|
39
|
+
5. **Restore the tree**: the throwaway test deleted, every probe back; `git status` shows nothing of
|
|
40
|
+
yours.
|
|
41
|
+
|
|
42
|
+
`no` means the instrument ran and the behaviour did not appear; `inconclusive` means nothing was
|
|
43
|
+
learned about the problem. Never return `no` for a test that did not exercise the claim.
|
|
44
|
+
|
|
45
|
+
Your report is your last message, quoting what happened rather than summarising it:
|
|
46
|
+
|
|
47
|
+
```markdown
|
|
48
|
+
### reproduce result
|
|
49
|
+
- verdict: yes | no | inconclusive
|
|
50
|
+
- claim: <the claim, as one observable behaviour>
|
|
51
|
+
- instrument: throwaway test | browser | none could be built
|
|
52
|
+
- what happened: <the command and its output, or what the browser showed and the console said — quoted>
|
|
53
|
+
- not proof: <with the browser: the test or guard that would prove it> | not needed
|
|
54
|
+
- escalated: <why the test could not answer> | not needed
|
|
55
|
+
- left behind: nothing — the throwaway test deleted, every probe restored
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Or, at step 1:
|
|
59
|
+
|
|
60
|
+
```markdown
|
|
61
|
+
### reproduce question
|
|
62
|
+
- situation: <the claim as given, and why it is not one observable behaviour>
|
|
63
|
+
- options: <the behaviours it could mean>
|
|
64
|
+
- recommendation: <the one you would test, and why>
|
|
65
|
+
```
|
package/bin/codelife
ADDED
package/package.json
CHANGED
|
@@ -1 +1,40 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"name": "@ersbeth/codelife",
|
|
3
|
+
"version": "0.11.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://gitlab.com/ersbeth-web/codelife.git"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"codelife": "bin/codelife"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
".claude-plugin/plugin.json",
|
|
14
|
+
"bin/",
|
|
15
|
+
"skills/",
|
|
16
|
+
"agents/",
|
|
17
|
+
"scripts/",
|
|
18
|
+
"presets/"
|
|
19
|
+
],
|
|
20
|
+
"packageManager": "pnpm@11.9.0",
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=22.13.0",
|
|
23
|
+
"pnpm": ">=11.9.0"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"prepare": "node scripts/install.mjs",
|
|
27
|
+
"verify:commit": "node --test tests/skills.test.mjs tests/references.test.mjs tests/shell.test.mjs tests/spec-form.test.mjs",
|
|
28
|
+
"verify:push": "pnpm verify:commit && claude plugin validate . && pnpm test:all",
|
|
29
|
+
"verify:release": "pnpm verify:push && node tests/release.mjs",
|
|
30
|
+
"test:all": "node tests/prereqs.mjs && node --test \"tests/*.test.mjs\"",
|
|
31
|
+
"test:one": "node --test"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"git-cliff": "2.13.1"
|
|
35
|
+
},
|
|
36
|
+
"exports": {
|
|
37
|
+
"./presets/*": "./presets/*",
|
|
38
|
+
"./package.json": "./package.json"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"strict": true,
|
|
4
|
+
"verbatimModuleSyntax": true,
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"noEmit": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"noUncheckedIndexedAccess": true,
|
|
10
|
+
"noImplicitOverride": true,
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
"resolveJsonModule": true
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// One configuration for every mode: the build, the type check, the tests and the playground's dev
|
|
2
|
+
// server all read it. Two configurations is a build that depends on which command was typed.
|
|
3
|
+
// The base a repository's `vite.config.ts` composes: it passes its package name, its entries and
|
|
4
|
+
// its own directory, and adds what is its own with `mergeConfig`. That merge concatenates arrays,
|
|
5
|
+
// so what varies from one repository to the next is a parameter here, never an array to replace.
|
|
6
|
+
// A plain object, not `defineConfig`'s: the repository's file wraps it, so this file imports nothing.
|
|
7
|
+
export function library({ entry }) {
|
|
8
|
+
return {
|
|
9
|
+
// The tests and the playground import the library by its name, which the repository's
|
|
10
|
+
// `tsconfig.json` maps to the source in `paths`: one mapping that the type checker and Vite
|
|
11
|
+
// both read, so they always show the working tree rather than the last build.
|
|
12
|
+
resolve: {
|
|
13
|
+
tsconfigPaths: true,
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
// The dev server serves the playground, not the package root: the package root has no page.
|
|
17
|
+
root: process.env.PLAYGROUND ? 'playground' : undefined,
|
|
18
|
+
|
|
19
|
+
// `vp pack` is tsdown, and this block is all it reads: a Vite `build` block builds nothing here.
|
|
20
|
+
// No `external`: tsdown already leaves every `dependencies` and `peerDependencies` entry out of
|
|
21
|
+
// the bundle, so a consumer resolves one copy of each.
|
|
22
|
+
pack: {
|
|
23
|
+
entry,
|
|
24
|
+
format: ['esm'],
|
|
25
|
+
// Written rather than inferred: tsdown emits declarations unprompted only when the manifest
|
|
26
|
+
// names a `types` field.
|
|
27
|
+
dts: true,
|
|
28
|
+
// `dist/` is versioned and `pre-commit` stages its diff: a chunk named by its content's hash is
|
|
29
|
+
// renamed by every change, and the diff reads as a file deleted and another added.
|
|
30
|
+
hash: false,
|
|
31
|
+
// The extension follows the manifest's `type` — `.js` under `"type": "module"`, which is what
|
|
32
|
+
// `exports` names — rather than tsdown's fixed `.mjs`.
|
|
33
|
+
fixedExtension: false,
|
|
34
|
+
// The declarations carry the documentation a consumer's editor reads, so the JavaScript keeps
|
|
35
|
+
// no comment: tsdown would keep them all, and the size compared at the port would measure that.
|
|
36
|
+
outputOptions: { comments: false },
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
// Without type information every rule that needs the checker is silent, even set to `error`, and
|
|
40
|
+
// `vp check` still passes.
|
|
41
|
+
lint: {
|
|
42
|
+
options: { typeAware: true, typeCheck: true },
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
test: {
|
|
46
|
+
include: ['test/**/*.test.ts'],
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// The audit cadence (audit's R-248, R-172): the campaign table of docs/audit/README.md, its rows in
|
|
2
|
+
// campaign order whatever order the table lists them in, and whether a full campaign is due — none
|
|
3
|
+
// ever run, or the last one older than six months. `codelife overview` prints it; `codelife release
|
|
4
|
+
// prep` adds what a bump asks.
|
|
5
|
+
import '../node-floor.mjs';
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
|
|
8
|
+
export function cadence(now = new Date()) {
|
|
9
|
+
let text = '';
|
|
10
|
+
try { text = readFileSync('docs/audit/README.md', 'utf8'); } catch { /* no campaign yet */ }
|
|
11
|
+
// A row's first cell is the campaign's id, bare or as a link to its frozen report.
|
|
12
|
+
const number = (row) => Number(row[0].match(/^A(\d+)/)[1]);
|
|
13
|
+
const rows = text.split('\n').filter((l) => /^\| ?\[?`?A\d+/.test(l)).map((l) => l.split('|').slice(1, -1).map((c) => c.trim()))
|
|
14
|
+
.map(([id, ...rest]) => [id.match(/A\d+[\w-]*/)[0], ...rest]).sort((a, b) => number(a) - number(b));
|
|
15
|
+
const full = rows.filter((r) => /^full/i.test(r[2] ?? '')).at(-1) ?? null;
|
|
16
|
+
let next = null, due = null;
|
|
17
|
+
if (full) {
|
|
18
|
+
// In UTC, the day clamped to the target month's length: August 31 is due February 28.
|
|
19
|
+
const [y, m, d] = full[1].split('-').map(Number), at = new Date(Date.UTC(y, m + 5, Math.min(d, new Date(Date.UTC(y, m + 6, 0)).getUTCDate())));
|
|
20
|
+
next = at.toISOString().slice(0, 10);
|
|
21
|
+
if (at < now) due = `the last full campaign, ${full[0]}, is older than six months`;
|
|
22
|
+
} else if (rows.length) due = 'no full campaign was ever run';
|
|
23
|
+
return { rows, last: rows.at(-1) ?? null, full, next, due };
|
|
24
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// `codelife audit close` (R-226, R-227, R-260): closes the campaign under way, from the repository's
|
|
2
|
+
// root. Checks first, changing nothing: every finding carries its disposition, and the index holds
|
|
3
|
+
// no placeholder but the two this verb fills. Then writes the index's findings by lens and the paths
|
|
4
|
+
// no lens read in the end, turns its banner FROZEN, compares the tree with the fingerprint taken at
|
|
5
|
+
// the open — each file that differs named, never restored — and removes the fingerprint. Prints the
|
|
6
|
+
// paths the close commit carries: the campaign folder, a new domain lens, the refusal lines, and the
|
|
7
|
+
// issues filed under tracker.kind none. Exit 1 on a check that fails, nothing written.
|
|
8
|
+
import '../node-floor.mjs';
|
|
9
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { fingerprint, findings, idOf, inProgress, lines, partFile, parts, row, sh } from './lib.mjs';
|
|
12
|
+
|
|
13
|
+
const say = (l) => console.log(`- ${l}`);
|
|
14
|
+
const stop = (why) => { say(`stop: ${why}`); process.exit(1); };
|
|
15
|
+
console.log('### audit close');
|
|
16
|
+
const folder = inProgress() ?? stop('no campaign in progress');
|
|
17
|
+
const index = join(folder, '00-index.md');
|
|
18
|
+
let text = readFileSync(index, 'utf8');
|
|
19
|
+
|
|
20
|
+
const all = findings(folder);
|
|
21
|
+
const open = all.filter((f) => !f.disposition);
|
|
22
|
+
if (open.length) stop(`findings with no disposition: ${open.map((f) => f.id).join(', ')} — file their lens's table first`);
|
|
23
|
+
const left = [...new Set(text.match(/\{\{[^}]*\}\}/g) ?? [])].filter((p) => !['{{findings}}', '{{unread}}'].includes(p));
|
|
24
|
+
if (left.length) stop(`the index still holds ${left.map((p) => p.slice(0, 40)).join(' · ')} — write it first`);
|
|
25
|
+
|
|
26
|
+
// What no lens read in the end: each lens's last *Not reached:* line, as its judge copied it.
|
|
27
|
+
const unread = [];
|
|
28
|
+
for (const nn of parts(folder)) for (const [lens, body] of Object.entries(partFile(folder, nn).sections)) {
|
|
29
|
+
const last = body.filter((l) => l.startsWith('Not reached:')).at(-1);
|
|
30
|
+
if (last) unread.push(`${last.replace(/^Not reached:\s*/, '').split(' — ')[0]} (${lens}, part ${nn})`);
|
|
31
|
+
}
|
|
32
|
+
const table = ['| Lens | Finding | Severity | Disposition |', '|---|---|---|---|',
|
|
33
|
+
...all.map((f) => `| ${f.lens} | [\`${f.id}\`](${f.part}/${f.path.split('/').pop()}) ${f.title} | ${f.severity} | ${f.disposition} |`)];
|
|
34
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
35
|
+
const commit = /`([0-9a-f]+)`/.exec(row(text, 'Commit') ?? '')?.[1] ?? '?';
|
|
36
|
+
text = text.replace('{{findings}}', () => (all.length ? table.join('\n') : 'None.')).replace('{{unread}}', () => unread.join(', ') || 'none')
|
|
37
|
+
.replace(/^> \*\*⚠ IN PROGRESS\.\*\*.*\n(?:>.*\n)*/m, `> **FROZEN** on ${date}. This report records what was true at \`${commit}\`. Its folder leaves the tree at the fold
|
|
38
|
+
> commit; the campaign table links it at the close commit, and a finding found wrong later is corrected in its issue.\n`);
|
|
39
|
+
writeFileSync(index, text);
|
|
40
|
+
say(`index: ${all.length} findings by lens, never read: ${unread.length ? unread.join(', ') : 'none'}, banner FROZEN`);
|
|
41
|
+
|
|
42
|
+
// The fingerprint: every file outside docs/audit/ that differs from the open. docs/spec/ holds the
|
|
43
|
+
// refusal lines this campaign wrote, issues/ under tracker.kind none the issues it filed: its own.
|
|
44
|
+
const print = join(folder, 'fingerprint.tsv');
|
|
45
|
+
if (existsSync(print)) {
|
|
46
|
+
const before = new Map(lines(readFileSync(print, 'utf8')).map((l) => l.split('\t')));
|
|
47
|
+
const now = new Map(fingerprint().map((l) => l.split('\t')));
|
|
48
|
+
const differ = [...new Set([...before.keys(), ...now.keys()])].filter((p) => before.get(p) !== now.get(p)).sort();
|
|
49
|
+
const mine = (p) => /^(docs\/spec|issues)\//.test(p);
|
|
50
|
+
const own = differ.filter(mine), other = differ.filter((p) => !mine(p));
|
|
51
|
+
rmSync(print);
|
|
52
|
+
say(`fingerprint: ${other.length ? `${other.length} files differ from the open — ${other.join(', ')}` : 'the tree is as the campaign found it'}`);
|
|
53
|
+
if (own.length) say(`the campaign's own: ${own.join(', ')}`);
|
|
54
|
+
} else say('fingerprint: none was taken at the open — the tree cannot be compared');
|
|
55
|
+
|
|
56
|
+
const docs = lines(sh('git', ['status', '--porcelain', '--untracked-files=all', '--', 'docs/audit/lenses', 'docs/spec', 'issues']).stdout).map((l) => l.slice(3));
|
|
57
|
+
say(`commit: docs(audits): close campaign ${idOf(folder)} — ${[folder, ...docs].join(' ')}`);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// `codelife audit file <rows file>` (K-33, R-111, R-112): files one confirmed table of the campaign
|
|
2
|
+
// under way, from the repository's root. One row a line, `<id> <bug|feature> <severity>` for a
|
|
3
|
+
// finding to file, `<id> known` for one an open issue carries, `<id> false`, `<id> refused
|
|
4
|
+
// <address of its Rejected line>` or `<id> merged <id>`. Every row is checked before anything is
|
|
5
|
+
// written. A new finding becomes an issue tagged with its id — never a second one: an issue already
|
|
6
|
+
// carrying the id is taken as filed, and given the comment it lacks — its body the finding's lead,
|
|
7
|
+
// then the finding whole as its first comment, its severity rewritten to the row's; a known one gets a `re-reported` comment. Each disposition is written into its
|
|
8
|
+
// finding's file as the row is done, so a run cut half-way is run again on the same file.
|
|
9
|
+
import '../node-floor.mjs';
|
|
10
|
+
import { appendFileSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { findings, idOf, inProgress, lines, pkg, sh } from './lib.mjs';
|
|
14
|
+
|
|
15
|
+
const [file] = process.argv.slice(2);
|
|
16
|
+
if (!file) {
|
|
17
|
+
console.error('usage: codelife audit file <rows file>');
|
|
18
|
+
process.exit(2);
|
|
19
|
+
}
|
|
20
|
+
const say = (l) => console.log(`- ${l}`);
|
|
21
|
+
const stop = (why) => { say(`stop: ${why}`); process.exit(1); };
|
|
22
|
+
console.log('### audit file');
|
|
23
|
+
const folder = inProgress() ?? stop('no campaign in progress');
|
|
24
|
+
const all = new Map(findings(folder).map((f) => [f.id, f]));
|
|
25
|
+
let rows;
|
|
26
|
+
try { rows = lines(readFileSync(file, 'utf8')).map((l) => l.trim().split(/\s+/)); } catch (e) { console.error(e.message); process.exit(2); }
|
|
27
|
+
for (const [id, what, extra, ...rest] of rows) {
|
|
28
|
+
const f = all.get(id);
|
|
29
|
+
if (!f) stop(`${id}: no such finding in ${folder}`);
|
|
30
|
+
const ok = (['bug', 'feature'].includes(what) && ['critical', 'high', 'mid', 'low'].includes(extra) && !rest.length)
|
|
31
|
+
|| (what === 'known' && f.known && !extra) || (what === 'false' && !extra) || (what === 'refused' && extra)
|
|
32
|
+
|| (what === 'merged' && all.has(extra) && extra !== id);
|
|
33
|
+
if (!ok) stop(`${id}: '${[what, extra, ...rest].filter(Boolean).join(' ')}' is not a disposition${what === 'known' && !f.known ? ' — its finding carries no Known: #n' : ''}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const tmp = mkdtempSync(join(tmpdir(), 'codelife-file-'));
|
|
37
|
+
const date = new Date().toISOString().slice(0, 10), campaign = idOf(folder);
|
|
38
|
+
const tracker = (script, args) => {
|
|
39
|
+
const r = sh(process.execPath, [join(pkg, 'scripts/tracker', script), ...args]);
|
|
40
|
+
if (r.status !== 0) stop(`${script.replace('.mjs', '')} failed — ${(r.stderr || r.stdout).trim()}`);
|
|
41
|
+
return r.stdout.trim();
|
|
42
|
+
};
|
|
43
|
+
// The finding as an issue's comment carries it: without the lines that only the campaign reads.
|
|
44
|
+
const whole = (f) => f.text.split('\n').filter((l) => !/^\*\*(Disposition|From):\*\*/.test(l)).join('\n').trim();
|
|
45
|
+
const lead = (f) => f.text.split(/\n\s*\n/).map((p) => p.trim()).find((p) => p && !/^(#|\*\*)/.test(p)) ?? f.title;
|
|
46
|
+
const write = (f, disposition) => { appendFileSync(f.path, `\n**Disposition:** ${disposition}\n`); say(`${f.id} → ${disposition}`); };
|
|
47
|
+
|
|
48
|
+
for (const [id, what, extra, ...rest] of rows) {
|
|
49
|
+
const f = all.get(id);
|
|
50
|
+
if (f.disposition) { say(`${id} → ${f.disposition} (already)`); continue; }
|
|
51
|
+
if (what === 'false') write(f, 'false');
|
|
52
|
+
else if (what === 'refused') write(f, `refused — ${[extra, ...rest].join(' ')}`);
|
|
53
|
+
else if (what === 'merged') write(f, `merged into ${extra}`);
|
|
54
|
+
else if (what === 'known') {
|
|
55
|
+
writeFileSync(join(tmp, 'note.md'), `codelife: re-reported — /audit — ${date}\n\nCampaign ${campaign} observed this again, as finding ${id}: ${lead(f)}\n`);
|
|
56
|
+
tracker('note.mjs', [f.known, join(tmp, 'note.md')]);
|
|
57
|
+
write(f, `re-reported on #${f.known}`);
|
|
58
|
+
} else {
|
|
59
|
+
writeFileSync(f.path, f.text = f.text.replace(/(\*\*Severity:\*\* )(critical|high|mid|low)/, `$1${extra}`));
|
|
60
|
+
writeFileSync(join(tmp, 'issue.md'), `# ${f.title}\n\n${lead(f)}\n\n## Where it comes from\n\nAudit ${campaign}, finding ${id}: the finding whole is this issue's first comment.\n`);
|
|
61
|
+
const created = tracker('create.mjs', ['--once', what, extra, `audit:${id}`, join(tmp, 'issue.md')]);
|
|
62
|
+
const n = /#(\d+)/.exec(created)[1];
|
|
63
|
+
const posted = () => JSON.parse(tracker('view.mjs', [n])).comments.some((c) => c.body.startsWith('codelife: finding —'));
|
|
64
|
+
if (created.startsWith('created') || !posted()) {
|
|
65
|
+
writeFileSync(join(tmp, 'note.md'), `codelife: finding — /audit — ${date}\n\n${whole(f)}\n`);
|
|
66
|
+
tracker('note.mjs', [n, join(tmp, 'note.md')]);
|
|
67
|
+
}
|
|
68
|
+
write(f, `#${n}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// `codelife audit fold` (R-226): run once the close commit is HEAD, from the repository's root.
|
|
2
|
+
// Deletes the campaign folder that commit carries, staged, and writes the campaign's row at the top
|
|
3
|
+
// of docs/audit/README.md's table — its id linked to the index at the close commit on the forge,
|
|
4
|
+
// its date, type, commit, findings by severity and state — in the form the cadence reads it back
|
|
5
|
+
// (scripts/audit/cadence.mjs). Prints the paths the fold commit carries.
|
|
6
|
+
import '../node-floor.mjs';
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { findings, folders, idOf, pkg, read, row, sh } from './lib.mjs';
|
|
10
|
+
import { parseOrigin } from '../tracker/lib.mjs';
|
|
11
|
+
import { field } from '../contract.mjs';
|
|
12
|
+
|
|
13
|
+
const say = (l) => console.log(`- ${l}`);
|
|
14
|
+
const stop = (why) => { say(`stop: ${why}`); process.exit(1); };
|
|
15
|
+
console.log('### audit fold');
|
|
16
|
+
const subject = sh('git', ['log', '-1', '--format=%s']).stdout.trim();
|
|
17
|
+
const id = /^docs\(audits\): close campaign (A\d+)$/.exec(subject)?.[1] ?? stop(`HEAD is '${subject}', not a campaign's close commit`);
|
|
18
|
+
const folder = folders().find((d) => idOf(d) === id && /\*\*FROZEN\*\*/.test(read(join(d, '00-index.md'))))
|
|
19
|
+
?? stop(`no frozen folder of ${id} under docs/audit/`);
|
|
20
|
+
const sha = sh('git', ['rev-parse', '--short', 'HEAD']).stdout.trim();
|
|
21
|
+
const index = readFileSync(join(folder, '00-index.md'), 'utf8');
|
|
22
|
+
const name = folder.split('/').pop(), path = `${folder}/00-index.md`;
|
|
23
|
+
|
|
24
|
+
// The link: the index at the close commit on the forge, or the sha and the path git shows it at.
|
|
25
|
+
const tracker = field('tracker.kind') ?? 'none';
|
|
26
|
+
const forge = parseOrigin(sh('git', ['remote', 'get-url', 'origin']).stdout.trim());
|
|
27
|
+
const cell = forge && tracker !== 'none'
|
|
28
|
+
? `[\`${name}\`](https://${forge.host}/${forge.project}/${tracker === 'glab' ? '-/' : ''}blob/${sha}/${path})`
|
|
29
|
+
: `\`${name}\` at \`${sha}\`:\`${path}\``;
|
|
30
|
+
|
|
31
|
+
const kept = findings(folder).filter((f) => !/^(false|merged)/.test(f.disposition ?? ''));
|
|
32
|
+
const by = (s) => kept.filter((f) => f.severity === s).length;
|
|
33
|
+
const filed = kept.filter((f) => /^#\d+$/.test(f.disposition)).length, again = kept.filter((f) => /^re-reported/.test(f.disposition)).length;
|
|
34
|
+
const date = /^A\d+-(\d{4}-\d{2}-\d{2})-/.exec(name)?.[1] ?? '?';
|
|
35
|
+
const line = `| ${cell} | ${date} | ${row(index, 'Type') ?? '?'} | \`${/`([0-9a-f]+)`/.exec(row(index, 'Commit') ?? '')?.[1] ?? '?'}\` | `
|
|
36
|
+
+ `${kept.length} — ${by('critical')} critical, ${by('high')} high, ${by('mid')} mid, ${by('low')} low | `
|
|
37
|
+
+ `**frozen** ${new Date().toISOString().slice(0, 10)} — ${filed} filed, ${again} re-reported |`;
|
|
38
|
+
|
|
39
|
+
const readme = 'docs/audit/README.md';
|
|
40
|
+
let text = existsSync(readme) ? readFileSync(readme, 'utf8') : readFileSync(join(pkg, 'scripts/target/templates/audit/README.md'), 'utf8');
|
|
41
|
+
const l = text.split('\n'), header = l.findIndex((x) => /^\| Campaign \|/.test(x));
|
|
42
|
+
if (header < 0 || !/^\|[-| ]+\|$/.test(l[header + 1] ?? '')) stop(`${readme} holds no campaign table`);
|
|
43
|
+
const rm = sh('git', ['rm', '-r', '-q', '--', folder]);
|
|
44
|
+
if (rm.status !== 0) stop(`git rm ${folder}: ${rm.stderr.trim()}`);
|
|
45
|
+
l.splice(header + 2, 0, line);
|
|
46
|
+
writeFileSync(readme, l.join('\n'));
|
|
47
|
+
say(`row: ${line}`);
|
|
48
|
+
say(`commit: docs(audits): fold campaign ${id} — ${folder} ${readme}`);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// What /audit's verbs share: the lens catalogue, the campaign folder and what its judges wrote in
|
|
2
|
+
// it, the fingerprint. From the repository's root.
|
|
3
|
+
import '../node-floor.mjs';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
export const pkg = fileURLToPath(new URL('../../', import.meta.url));
|
|
10
|
+
export const LENSES = join(pkg, 'scripts/lenses');
|
|
11
|
+
|
|
12
|
+
// The lens catalogue (K-31): the plugin's lenses are exactly its references; a `kind` adds its own;
|
|
13
|
+
// the hygiene lenses take one agent together, their facts measured first; five ids are retired.
|
|
14
|
+
export const lensIds = () => readdirSync(LENSES).filter((f) => f.endsWith('.md')).map((f) => f.slice(0, -3)).sort();
|
|
15
|
+
export const KIND = { none: [], lib: ['PKG', 'TYPE', 'LINES', 'CONF'], app: ['COMPAT', 'A11Y', 'CONF'] };
|
|
16
|
+
export const HYGIENE = ['BUILD', 'DEPS', 'PKG', 'TYPE', 'GOV', 'LINES', 'GUARD', 'CONF'];
|
|
17
|
+
export const RETIRED = { CORR: 'BUG, FAIL', TEST: 'VERIF', PATT: 'STANDARDS', AI: 'DOC, VERIF', FILES: 'ARCH' };
|
|
18
|
+
export const domainIds = () => (existsSync('docs/audit/lenses') ? readdirSync('docs/audit/lenses') : [])
|
|
19
|
+
.filter((f) => /^[A-Z][A-Z0-9]+\.md$/.test(f)).map((f) => f.slice(0, -3)).sort();
|
|
20
|
+
// The reference a lens id reads: the plugin's, or the repository's own for a domain lens.
|
|
21
|
+
export const reference = (id) => [join(LENSES, `${id}.md`), `docs/audit/lenses/${id}.md`].find((p) => existsSync(p)) ?? null;
|
|
22
|
+
|
|
23
|
+
export const sh = (cmd, args, opts = {}) => spawnSync(cmd, args, { encoding: 'utf8', maxBuffer: 256 * 1024 * 1024, ...opts });
|
|
24
|
+
export const lines = (text) => text.split('\n').filter(Boolean);
|
|
25
|
+
export const read = (p) => { try { return readFileSync(p, 'utf8'); } catch { return ''; } };
|
|
26
|
+
|
|
27
|
+
// The campaign folders beside the table, and the one under way: its index carries IN PROGRESS.
|
|
28
|
+
export const folders = () => (existsSync('docs/audit') ? readdirSync('docs/audit') : [])
|
|
29
|
+
.filter((d) => /^A\d+-/.test(d) && statSync(join('docs/audit', d)).isDirectory()).map((d) => join('docs/audit', d));
|
|
30
|
+
export const inProgress = () => folders().find((d) => /IN PROGRESS/.test(read(join(d, '00-index.md')))) ?? null;
|
|
31
|
+
export const idOf = (folder) => folder.match(/\/(A\d+)-/)[1];
|
|
32
|
+
// A row of the index's campaign table, `| **<label>** | <value> |`.
|
|
33
|
+
export const row = (index, label) => new RegExp(`^\\| \\*\\*${label}\\*\\* \\| (.*?) \\|$`, 'm').exec(index)?.[1] ?? null;
|
|
34
|
+
|
|
35
|
+
export const parts = (folder) => readdirSync(folder).filter((d) => /^\d{2,}$/.test(d)).sort((a, b) => a - b);
|
|
36
|
+
|
|
37
|
+
// The findings its judges wrote, `NN/<LENS>-<NN>.<n>.md`, as the fields /audit reads from them.
|
|
38
|
+
export function findings(folder, lenses) {
|
|
39
|
+
const out = [];
|
|
40
|
+
for (const nn of parts(folder)) for (const f of readdirSync(join(folder, nn)).sort(byNumber)) {
|
|
41
|
+
const m = /^([A-Z][A-Z0-9]+)-(\d{2,})\.(\d+)\.md$/.exec(f);
|
|
42
|
+
if (!m || (lenses && !lenses.includes(m[1]))) continue;
|
|
43
|
+
const path = join(folder, nn, f), text = read(path);
|
|
44
|
+
out.push({
|
|
45
|
+
path, text, part: nn, lens: m[1], id: `${idOf(folder)}-${m[1]}-${m[2]}.${m[3]}`,
|
|
46
|
+
title: /^#### \S+ — (.*)$/m.exec(text)?.[1]?.trim() ?? '',
|
|
47
|
+
severity: /\*\*Severity:\*\* (critical|high|mid|low)/.exec(text)?.[1] ?? '?',
|
|
48
|
+
unverified: /\*\*UNVERIFIED\*\*/.test(text),
|
|
49
|
+
effort: /\*\*Effort:\*\* ([SML])\b/.exec(text)?.[1] ?? '?',
|
|
50
|
+
known: /^\*\*Known:\*\* #(\d+)/m.exec(text)?.[1] ?? null,
|
|
51
|
+
from: lines(/^\*\*From:\*\* (.*)$/m.exec(text)?.[1]?.replace(/,/g, '\n') ?? '').map((s) => s.trim()),
|
|
52
|
+
disposition: /^\*\*Disposition:\*\* (.*)$/m.exec(text)?.[1] ?? null,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
function byNumber(a, b) {
|
|
58
|
+
const n = (f) => Number(/\.(\d+)\.md$/.exec(f)?.[1] ?? 0);
|
|
59
|
+
return a.split('-')[0].localeCompare(b.split('-')[0]) || n(a) - n(b);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// A part's `part.md`: its `**Received:**` ids, and per lens `## <LENS>` the lines under it.
|
|
63
|
+
export function partFile(folder, nn) {
|
|
64
|
+
const text = read(join(folder, nn, 'part.md'));
|
|
65
|
+
const received = lines(/^\*\*Received:\*\* (.*)$/m.exec(text)?.[1]?.replace(/,/g, '\n') ?? '').map((s) => s.trim()).filter((s) => s !== 'none');
|
|
66
|
+
const sections = {};
|
|
67
|
+
let lens = null;
|
|
68
|
+
for (const l of text.split('\n')) {
|
|
69
|
+
const h = /^## ([A-Z][A-Z0-9]+(?:, ?[A-Z][A-Z0-9]+)*)\s*$/.exec(l);
|
|
70
|
+
if (h) { lens = h[1].split(/, ?/); for (const id of lens) sections[id] ??= []; continue; }
|
|
71
|
+
if (lens) for (const id of lens) sections[id].push(l);
|
|
72
|
+
}
|
|
73
|
+
return { text, received, sections };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// The ids a judge received that no other line of its part names as a whole id (#825).
|
|
77
|
+
export function unaccounted(folder, nn) {
|
|
78
|
+
const { text, received } = partFile(folder, nn);
|
|
79
|
+
const others = [text.split('\n').filter((l) => !l.startsWith('**Received:**')).join('\n'),
|
|
80
|
+
...findings(folder).filter((f) => f.part === nn).map((f) => f.text)].join('\n');
|
|
81
|
+
const named = new Set(others.match(/(?<![A-Za-z0-9-])[A-Z][A-Z0-9]+-[0-9]+(?![A-Za-z0-9.])/g) ?? []);
|
|
82
|
+
return received.filter((id) => !named.has(id));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Every tracked and untracked file outside docs/audit/, with its hash: what an audit must leave as
|
|
86
|
+
// it found it (R-227). `path\thash` lines.
|
|
87
|
+
export function fingerprint() {
|
|
88
|
+
const files = sh('git', ['ls-files', '-z', '--cached', '--others', '--exclude-standard']).stdout.split('\0')
|
|
89
|
+
.filter((f) => f && !f.startsWith('docs/audit/') && existsSync(f) && statSync(f).isFile());
|
|
90
|
+
const unique = [...new Set(files)].sort();
|
|
91
|
+
const hashes = lines(sh('git', ['hash-object', '--stdin-paths'], { input: unique.join('\n') }).stdout);
|
|
92
|
+
return unique.map((f, i) => `${f}\t${hashes[i]}`);
|
|
93
|
+
}
|