done-done-axi 0.6.15 → 0.6.17
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/dist/cli.js +12 -8
- package/dist/lint.js +49 -16
- package/dist/proof.js +6 -24
- package/docs/example-create-maintain.md +1 -1
- package/docs/reference.md +10 -10
- package/docs/releasing.md +5 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -281,7 +281,7 @@ each prints the files it changed and runs the journey once as a dirty-tree trial
|
|
|
281
281
|
when the repo (the current folder) has none; repro is not n/a on a fix PR. evidence names files in the evidence folder (or absolute):
|
|
282
282
|
each exists, is non-empty and is newer than the folder itself; no two are byte-identical; at least one has a hash no other PR used
|
|
283
283
|
(hash store lint.hashes in ~/.done-done, or $DONE_DONE_HOME). "evidence: not proven: <cause>" passes. A PR with 1,000 or more added
|
|
284
|
-
non-generated lines fails (lockfiles, vendor/, dist/, LICENSE-type files, deletions and moved lines do not count); a bot PR passes.
|
|
284
|
+
non-generated lines fails; the whole PR is counted (every page of its file list, its compare diff), and a failed page or diff fails instead of counting part of it (lockfiles, vendor/, dist/, LICENSE-type files, deletions and moved lines do not count); a bot PR passes.
|
|
285
285
|
A PR URL whose diff changes a src/ file and no journeys/ file prints WARN instead of PASS (still exit 0): run the journey and commit it before the PR opens.
|
|
286
286
|
In .done-done.yaml, checks.upstream_catch_up: true also drops a fork's upstream catch-up lines, and checks.moved_from: true drops lines
|
|
287
287
|
a "moved-from: <owner>/<repo>@<ref> <dst>=<src>" body line says came from a repo of the same owner; both are off by default.
|
|
@@ -860,8 +860,11 @@ async function runCheck(argv, run) {
|
|
|
860
860
|
findings.push({ level: 'fail', why: `${name}: byte-identical to ${seen.get(hash)}` });
|
|
861
861
|
if (!pair)
|
|
862
862
|
seen.set(hash, name);
|
|
863
|
-
// A file in the evidence root is claimed by the record that lists its path;
|
|
864
|
-
|
|
863
|
+
// A file in the evidence root is claimed by the record that lists its path; any other copy is claimed by its bytes, so a
|
|
864
|
+
// sealed or renamed copy of a run's own file still ties to that run. The path is only a tie-breaker between records.
|
|
865
|
+
const byHash = records.filter((r) => r.files.some((f) => f.sha256 === hash));
|
|
866
|
+
const byPath = inRoot === null ? [] : byHash.filter((r) => r.files.some((f) => f.path === inRoot));
|
|
867
|
+
const claims = byPath.length ? byPath : byHash;
|
|
865
868
|
if (!claims.length && inRoot !== null) {
|
|
866
869
|
const edited = records.filter((r) => r.files.some((f) => f.path === inRoot));
|
|
867
870
|
if (edited.length) {
|
|
@@ -947,9 +950,10 @@ async function runCheck(argv, run) {
|
|
|
947
950
|
const codeRuns = runs.size ? runsFault([...runs.values()]) : null;
|
|
948
951
|
if (codeRuns)
|
|
949
952
|
findings.push({ level: 'fail', why: codeRuns });
|
|
950
|
-
// The user steps behind the captures: a cited real run needs a step record
|
|
953
|
+
// The user steps behind the captures: a cited real run needs a well-formed step record (its words are never matched to the
|
|
954
|
+
// claim's: what the captures show is the judge's call). Runs from before step records warn.
|
|
951
955
|
if (claim && runs.size && !codeRuns) {
|
|
952
|
-
const s = stepsFault([...runs.values()]
|
|
956
|
+
const s = stepsFault([...runs.values()]);
|
|
953
957
|
if (s.fault)
|
|
954
958
|
findings.push({ level: 'fail', why: s.fault });
|
|
955
959
|
if (s.warning)
|
|
@@ -1127,11 +1131,11 @@ function receiptFault(rec, short) {
|
|
|
1127
1131
|
const code = runsFault(rec.runs);
|
|
1128
1132
|
if (code)
|
|
1129
1133
|
return `receipt for ${short} is no real-product proof: ${code}`;
|
|
1130
|
-
const steps = stepsFault(rec.runs
|
|
1131
|
-
return steps.fault ? `receipt for ${short} has no step record
|
|
1134
|
+
const steps = stepsFault(rec.runs);
|
|
1135
|
+
return steps.fault ? `receipt for ${short} has no well-formed step record: ${steps.fault}` : null;
|
|
1132
1136
|
}
|
|
1133
1137
|
// The warning a well-formed receipt still carries: its runs were recorded before step records.
|
|
1134
|
-
const receiptWarning = (rec) => stepsFault(rec.runs
|
|
1138
|
+
const receiptWarning = (rec) => stepsFault(rec.runs).warning;
|
|
1135
1139
|
// Verify a recorded receipt for `headArg`, which may be a full sha or a ref (HEAD, a branch or a tag).
|
|
1136
1140
|
// Read-only, so a missing or changed record is a failure, not a usage error.
|
|
1137
1141
|
function verifyReceipt(repo, cfg, headArg) {
|
package/dist/lint.js
CHANGED
|
@@ -61,6 +61,34 @@ function without(a, b) {
|
|
|
61
61
|
}
|
|
62
62
|
const total = (n) => [...n.values()].reduce((sum, c) => sum + c, 0);
|
|
63
63
|
const stdout = (r) => (r.status === 0 ? r.stdout.trim() : '');
|
|
64
|
+
const worst = (r) => r.stderr.trim().split('\n').pop()?.slice(0, 160) || '?';
|
|
65
|
+
// `gh pr view --json files` stops at 100 files, so the whole list comes from the paginated pull-files endpoint instead.
|
|
66
|
+
// GitHub stops that endpoint at 3000 files, so page 30 being full is already past what it will list.
|
|
67
|
+
const MAX_PAGES = 30;
|
|
68
|
+
function prFiles(run, nwo, num) {
|
|
69
|
+
const paths = [];
|
|
70
|
+
for (let page = 1; page <= MAX_PAGES; page++) {
|
|
71
|
+
const call = `repos/${nwo}/pulls/${num}/files?per_page=100&page=${page}`;
|
|
72
|
+
const r = run('api', call);
|
|
73
|
+
if (r.status)
|
|
74
|
+
return { why: `${call} failed: ${worst(r)}` };
|
|
75
|
+
let files;
|
|
76
|
+
try {
|
|
77
|
+
files = JSON.parse(r.stdout);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return { why: `${call} returned no file list` };
|
|
81
|
+
}
|
|
82
|
+
if (!Array.isArray(files))
|
|
83
|
+
return { why: `${call} returned no file list` };
|
|
84
|
+
for (const f of files)
|
|
85
|
+
if (f.path)
|
|
86
|
+
paths.push(f.path);
|
|
87
|
+
if (files.length < 100)
|
|
88
|
+
return { paths };
|
|
89
|
+
}
|
|
90
|
+
return { why: `${nwo}/pulls/${num}/files is still full on page ${MAX_PAGES}: more files than lint can count` };
|
|
91
|
+
}
|
|
64
92
|
// checks.upstream_catch_up: lines the fork's parent repo already shipped are not authored here, so subtract the upstream delta the PR
|
|
65
93
|
// merges in (PR base ... the merge base with the parent's default branch). A plain fork PR merges no new upstream commit, so its delta
|
|
66
94
|
// is empty. Any API failure keeps the full count.
|
|
@@ -107,9 +135,9 @@ export function lint(src, ev, dir, rules = {}, run = gh) {
|
|
|
107
135
|
task = basename(dirname(resolve(src)));
|
|
108
136
|
}
|
|
109
137
|
else {
|
|
110
|
-
const r = run('pr', 'view', src, '--json', 'author,body,title,
|
|
138
|
+
const r = run('pr', 'view', src, '--json', 'author,body,title,headRefOid,headRefName,baseRefOid');
|
|
111
139
|
if (r.status)
|
|
112
|
-
return log({ ok: false, why: `gh pr view failed: ${r
|
|
140
|
+
return log({ ok: false, why: `gh pr view failed: ${worst(r)}`, head });
|
|
113
141
|
const pr = JSON.parse(r.stdout);
|
|
114
142
|
({ body, title } = { body: pr.body ?? '', title: pr.title ?? '' });
|
|
115
143
|
head = pr.headRefOid ?? '-';
|
|
@@ -118,26 +146,31 @@ export function lint(src, ev, dir, rules = {}, run = gh) {
|
|
|
118
146
|
if (/^(app\/)?(dependabot|renovate)(\[bot\])?$/.test(author))
|
|
119
147
|
return log({ ok: true, why: 'bot', head });
|
|
120
148
|
const nwo = /^https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\//.exec(src)?.[1];
|
|
121
|
-
const
|
|
149
|
+
const num = /^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/(\d+)/.exec(src)?.[1];
|
|
150
|
+
if (!nwo || !num)
|
|
151
|
+
return log({ ok: false, why: `not a GitHub pull URL: ${src.slice(0, 120)}`, head });
|
|
152
|
+
const listed = prFiles(run, nwo, num);
|
|
153
|
+
if (!listed.paths)
|
|
154
|
+
return log({ ok: false, why: listed.why ?? 'the pull file list failed', head });
|
|
155
|
+
const paths = listed.paths;
|
|
156
|
+
const attrs = [head, pr.baseRefOid].map((ref) => ref && run('api', '-H', 'Accept: application/vnd.github.raw', `repos/${nwo}/contents/.gitattributes?ref=${ref}`)).find((a) => a && a.status === 0);
|
|
122
157
|
if (attrs) {
|
|
123
158
|
const byAttr = attrGenerated(attrs.stdout);
|
|
124
159
|
isGenerated = (p) => byConfig(p) || byAttr(p);
|
|
125
160
|
}
|
|
126
|
-
const paths = (pr.files ?? []).map((f) => f.path ?? '');
|
|
127
161
|
if (paths.some((p) => p.startsWith('src/')) && !paths.some((p) => p.startsWith('journeys/')))
|
|
128
162
|
warn = 'the diff changes src/ and no journeys/ file: run the journey, commit it, then open the PR';
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
163
|
+
// The PR's whole net comes from the compare diff (three dots: from the merge base), never from the per-file additions.
|
|
164
|
+
const cmp = `repos/${nwo}/compare/${pr.baseRefOid ?? ''}...${head}`;
|
|
165
|
+
const d = run('api', '-H', 'Accept: application/vnd.github.diff', cmp);
|
|
166
|
+
if (d.status)
|
|
167
|
+
return log({ ok: false, why: `${cmp} failed: ${worst(d)}`, head });
|
|
168
|
+
let net = netLines(d.stdout, isGenerated);
|
|
169
|
+
if (rules.upstream_catch_up && total(net) >= SIZE_LIMIT)
|
|
170
|
+
net = without(net, upstreamDelta(run, nwo, head, pr.baseRefOid ?? '', isGenerated));
|
|
171
|
+
if (rules.moved_from && total(net) >= SIZE_LIMIT)
|
|
172
|
+
net = without(net, movedLines(run, nwo, body, paths, isGenerated));
|
|
173
|
+
const size = total(net);
|
|
141
174
|
if (size >= SIZE_LIMIT)
|
|
142
175
|
return log({ ok: false, why: `${size} added non-generated lines (moved lines excluded): split it (limit ${SIZE_LIMIT})`, head });
|
|
143
176
|
}
|
package/dist/proof.js
CHANGED
|
@@ -130,26 +130,11 @@ export function runsFault(runs) {
|
|
|
130
130
|
return null;
|
|
131
131
|
return `every cited capture comes from a code-only run (${[...new Set(runs.map((r) => r.journey ?? r.command))].join(', ')}): ${RULE}`;
|
|
132
132
|
}
|
|
133
|
-
//
|
|
134
|
-
const GENERIC = new Set('that this with from into when then than have will does your their they them user users able now works work working shows show shown once after before also each every what which while where there here only just more most some such same been being were should would could done proof proves prove outcome correct correctly properly real product the and for are can its'.split(' '));
|
|
135
|
-
const words = (t) => t.toLowerCase().match(/[a-z0-9]{3,}/g) ?? [];
|
|
136
|
-
// A word of the claim is in the record when the record has a word that starts like it (attach, attaches, attached).
|
|
137
|
-
const same = (a, b) => a.startsWith(b.slice(0, Math.min(5, b.length))) || b.startsWith(a.slice(0, Math.min(5, a.length)));
|
|
138
|
-
// The words of the claim that the step record never mentions, or [] when it covers the claim: at least half of the claim's
|
|
139
|
-
// words are in the record, and one of them in its result. A pure word match, no model: a test log's record ("ran the unit
|
|
140
|
-
// suite") cannot cover a claim about a pane, and a CLI transcript whose record names the command and its output covers a CLI claim.
|
|
141
|
-
export function uncovered(outcome, s) {
|
|
142
|
-
const terms = [...new Set(words(outcome).filter((w) => w.length >= 4 && !GENERIC.has(w)))];
|
|
143
|
-
if (!terms.length)
|
|
144
|
-
return [];
|
|
145
|
-
const all = words([s.start, ...s.steps, s.result].join(' '));
|
|
146
|
-
const result = words(s.result);
|
|
147
|
-
const hit = terms.filter((t) => all.some((w) => same(t, w)));
|
|
148
|
-
return hit.length * 2 >= terms.length && hit.some((t) => result.some((w) => same(t, w))) ? [] : terms.filter((t) => !hit.includes(t));
|
|
149
|
-
}
|
|
150
|
-
// Whether the runs behind a claim carry a step record covering it. A fault: no real run (one that is not code-only) has one.
|
|
133
|
+
// Whether the runs behind a claim carry a well-formed step record. A fault: no real run (one that is not code-only) has one.
|
|
151
134
|
// A warning: a real run was recorded before step records existed, so it keeps working but nothing ties its captures to user steps.
|
|
152
|
-
|
|
135
|
+
// The record's words are never matched against the claim's: a claim describes an outcome in its own words, and whether the
|
|
136
|
+
// captures show it is the judge's call, not a word test's. Requiring the record is what keeps the user steps behind a capture.
|
|
137
|
+
export function stepsFault(runs) {
|
|
153
138
|
const real = runs.filter((r) => !codeOnly(r.command));
|
|
154
139
|
const why = (r) => {
|
|
155
140
|
if (r.steps_error)
|
|
@@ -157,10 +142,7 @@ export function stepsFault(runs, outcome) {
|
|
|
157
142
|
if (!r.steps)
|
|
158
143
|
return `run ${r.run_id} has no step record (${STEPS_FILE} beside its captures)`;
|
|
159
144
|
const bad = stepRecordFault(r.steps);
|
|
160
|
-
|
|
161
|
-
return `run ${r.run_id}'s step record ${STEPS_FILE} is invalid: ${bad}`;
|
|
162
|
-
const miss = uncovered(outcome, r.steps);
|
|
163
|
-
return miss.length ? `run ${r.run_id}'s step record (${r.steps.steps.length} step${r.steps.steps.length > 1 ? 's' : ''}, target ${r.steps.target.version}) never mentions ${miss.join(', ')}` : '';
|
|
145
|
+
return bad ? `run ${r.run_id}'s step record ${STEPS_FILE} is invalid: ${bad}` : '';
|
|
164
146
|
};
|
|
165
147
|
const reasons = real.map(why);
|
|
166
148
|
if (!real.length || reasons.some((w) => !w))
|
|
@@ -168,7 +150,7 @@ export function stepsFault(runs, outcome) {
|
|
|
168
150
|
const old = real.filter((r) => !r.tool);
|
|
169
151
|
if (old.length)
|
|
170
152
|
return { fault: null, warning: `run ${old.map((r) => r.run_id).join(', ')} was recorded before step records, so nothing shows the user steps behind its captures; re-run the journey so it writes ${STEPS_FILE}` };
|
|
171
|
-
return { fault: `no cited capture comes from a run
|
|
153
|
+
return { fault: `no cited capture comes from a run with a well-formed step record: ${reasons.join('; ')}`, warning: null };
|
|
172
154
|
}
|
|
173
155
|
export const MARK = { core: '(core journey)', upgrade: '(upgrade journey)' };
|
|
174
156
|
// Where the role is named: the journey that carries it and drives the product, else the feature-map entry with its mark, else null.
|
|
@@ -56,7 +56,7 @@ journeys:
|
|
|
56
56
|
core: true
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
`scripts/prove-core.mjs` (already in the sample; `npm install` fetched `playwright-core`) opens `$DONE_DONE_BASE_URL` in headless Chrome at 390x844 with `playwright-core` (`executablePath` from `DONE_DONE_CHROME`, else `/usr/bin/chromium`), fills `Type a note` with `milk`, clicks `Save note`, waits for `milk` in the list and saves `$DONE_DONE_EVIDENCE_ROOT/note-listed.png`. It also writes `steps.json` beside it (target version, start state, the user's steps, the end result): `check`
|
|
59
|
+
`scripts/prove-core.mjs` (already in the sample; `npm install` fetched `playwright-core`) opens `$DONE_DONE_BASE_URL` in headless Chrome at 390x844 with `playwright-core` (`executablePath` from `DONE_DONE_CHROME`, else `/usr/bin/chromium`), fills `Type a note` with `milk`, clicks `Save note`, waits for `milk` in the list and saves `$DONE_DONE_EVIDENCE_ROOT/note-listed.png`. It also writes `steps.json` beside it (target version, start state, the user's steps, the end result): `check` requires that record and never matches its words to the claim's - the judge says whether the capture shows the outcome ([Step record](reference.md#step-record)).
|
|
60
60
|
|
|
61
61
|
The repo's own `journeys/create-maintain.mjs` runs this whole walkthrough unattended (`done-done run create-maintain`).
|
|
62
62
|
|
package/docs/reference.md
CHANGED
|
@@ -314,7 +314,7 @@ journeys:
|
|
|
314
314
|
|
|
315
315
|
### Lint check
|
|
316
316
|
|
|
317
|
-
`done-done lint <PR URL | report file> <evidence folder>` is a deterministic gate on a worker's claim: no judge, no model, and the project config only switches on two optional size rules. It prints `PASS`, `WARN: <reason>` or `FAIL: <reason>` and exits 0 (PASS or WARN), 1, or 2 on a usage error. A PR URL is read with `gh pr view`; a file is read as the report.
|
|
317
|
+
`done-done lint <PR URL | report file> <evidence folder>` is a deterministic gate on a worker's claim: no judge, no model, and the project config only switches on two optional size rules. It prints `PASS`, `WARN: <reason>` or `FAIL: <reason>` and exits 0 (PASS or WARN), 1, or 2 on a usage error. A PR URL is read with `gh pr view` and `gh api`; a file is read as the report.
|
|
318
318
|
|
|
319
319
|
- The newest `## Checks` block needs `outcome:`, `repro:`, `proof:` and `evidence:`.
|
|
320
320
|
- `proof:` names a command or path, and the repo's `done-done-verify-<app>` skill (a legacy `verify-<app>` skill is read too); it may say `no verify skill` only when `.agents/skills/` in the current folder holds none.
|
|
@@ -322,8 +322,8 @@ journeys:
|
|
|
322
322
|
- `evidence: not proven: <cause>` passes.
|
|
323
323
|
- A PR URL whose changed files include one under `src/` and none under `journeys/` prints `WARN` instead of `PASS` (exit 0; a `FAIL` still wins). It needs no config and never applies to a report file. The worker rule it backs: run the journey, commit it, then open the PR.
|
|
324
324
|
- Otherwise each named file (relative to the evidence folder, or absolute) exists, is non-empty and is newer than the evidence folder itself; no two are byte-identical; and at least one has a hash no other PR used. Hashes are kept in `lint.hashes` and each run is appended to `lint.log`, both in `~/.done-done` (`$DONE_DONE_HOME`). A later lint of the same PR is not reuse.
|
|
325
|
-
- A PR with 1,000 or more added non-generated lines fails. Lockfiles, `vendor/`, `dist/` and `LICENSE`-type files do not count, and neither do deletions or lines moved between files. A dependabot or renovate PR passes.
|
|
326
|
-
- Two optional size rules, both off by default and switched on per repo in `.done-done.yaml` (read from the current folder; they apply to a PR URL, never a report file). With both off a big PR is counted exactly as above. Each runs only when the PR is still at 1,000 lines
|
|
325
|
+
- A PR with 1,000 or more added non-generated lines fails. The whole PR is counted: its file list comes from every page of `pulls/<number>/files` (100 a page, up to GitHub's 3,000-file ceiling) and its net lines from the compare diff from the merge base, so a PR of any size is counted whole. Lockfiles, `vendor/`, `dist/` and `LICENSE`-type files do not count, and neither do deletions or lines moved between files. A dependabot or renovate PR passes. A page of the file list or the compare diff that fails makes lint `FAIL` with that call, never a partial count.
|
|
326
|
+
- Two optional size rules, both off by default and switched on per repo in `.done-done.yaml` (read from the current folder; they apply to a PR URL, never a report file). With both off a big PR is counted exactly as above. Each runs only when the PR is still at 1,000 lines.
|
|
327
327
|
- `checks.upstream_catch_up: true`: in a fork, lines the parent repo already shipped are not authored here, so lint subtracts the changes between the PR's base and the merge base with the parent's default branch. A PR that merges in no new upstream commit loses nothing.
|
|
328
328
|
- `checks.moved_from: true`: a line in the PR body `moved-from: <owner>/<repo>@<ref> <dst-prefix>=<src-prefix>` (more `dst=src` pairs may follow) cancels added lines that match, byte for byte after trimming, the file at the same path under `<src-prefix>` in that repo at `<ref>`. The source repo must have the PR's owner; authored lines still count.
|
|
329
329
|
- `checks.generated_paths: ["gen/**", "*.snap"]`: globs for the repo's own generated files, counted as generated beside the built-in ones above, so they stay out of the size count. Default none, so results do not change. Like the two rules above it applies to a PR URL, never a report file.
|
|
@@ -365,10 +365,10 @@ A capture shows a screen; the **step record** says what a user did to reach it.
|
|
|
365
365
|
|
|
366
366
|
`target.version` is the version of the app driven, `start` the state it began from (for an upgrade journey: the previous version installed), `steps` the steps a user takes, in order, and `result` what a user can see at the end. All four are required; a file that is not JSON or lacks one is recorded as invalid. `run` and `verify` read the file when the run closes and keep it in the run record (`steps`, with `tool`, the done-done version that recorded the run, and `steps_error` when the file is invalid); it is a record, not a capture, so it is not listed in the run's files and the judge never reads it. A journey writes it as it drives, from what it really did, never from the claim.
|
|
367
367
|
|
|
368
|
-
The check
|
|
368
|
+
The check requires that record, and never compares its words with the claim's. A claim is written in its author's own words, and the judge, not a word test, says whether the captures show it; the record is what shows the user steps behind them. This runs after the code-only rule, which stays the first filter, and with it:
|
|
369
369
|
|
|
370
|
-
- `check` and `receipt` stop before the judge (exit 2) when no cited capture comes from a run (not code-only)
|
|
371
|
-
- A receipt keeps each run's step record (`runs[].steps`), so `receipt --verify` and `gate`
|
|
370
|
+
- `check` and `receipt` stop before the judge (exit 2) when no cited capture comes from a run (not code-only) with a well-formed step record, naming each run and what its record is missing. The record's prose is never matched to the claim's: a run whose record describes the same journey in different words still reaches the judge, and a claim naming rows, numbers or files its record never mentions is judged like any other. A log from a wrapper script whose record says `ran the unit suite` is the judge's to weigh against a claim about a pane, not a word rule's. A cited run with no step record, or an invalid one, is refused the same way, with the shape to write.
|
|
371
|
+
- A receipt keeps each run's step record (`runs[].steps`), so `receipt --verify` and `gate` require a well-formed record for the receipt's runs with no config or evidence: a receipt whose runs have none fails.
|
|
372
372
|
- A run recorded before step records existed (its run record has no `tool`, or a receipt run has none) keeps working: `check`, `receipt`, `receipt --verify` and `gate` still pass, but say so in `warnings` (`gate` prints `WARN: ...` after `PASS`): re-run the journey so it writes `steps.json`.
|
|
373
373
|
|
|
374
374
|
### Core journey and release path
|
|
@@ -444,9 +444,9 @@ evidence: <paths of the evidence files, or "not proven: <cause>">
|
|
|
444
444
|
|
|
445
445
|
A plain check is never a verdict on its own. A cited file that fails one of these checks, or a Checks block that breaks a form rule, stops the check before its judge is asked: it exits 2 with every cause in `error` and the next command in `help`, and gives no result. A check with no cited capture the judge reads (only video or other files, or none from a done-done run) stops the same way, before the judge is asked.
|
|
446
446
|
|
|
447
|
-
Files that are in no run record give an inconclusive result when another cited capture is one the judge reads: nothing ties them to a commit. So does a file that more than one run record claims when none of those runs is at the current commit (possible only with records from before each run had its own folder). Byte-identical captures from several runs are fine when one of those runs is at the current commit: that run ties the file to the commit. A file whose path is named in a run record but whose bytes no longer match fails as `changed after run <run-id>` (exit 2, before the judge), not inconclusive: the path ties it to that run, so editing it after the run is tampering, not untied evidence.
|
|
447
|
+
Files that are in no run record give an inconclusive result when another cited capture is one the judge reads: nothing ties them to a commit. So does a file that more than one run record claims when none of those runs is at the current commit (possible only with records from before each run had its own folder). A file ties to a run by its bytes: a sealed or renamed copy of a file a run wrote, cited outside the evidence root or at another path inside it, is that run's evidence, and the record's own path for those bytes picks the run when several wrote them. Byte-identical captures from several runs are fine when one of those runs is at the current commit: that run ties the file to the commit. A file whose path is named in a run record but whose bytes no longer match fails as `changed after run <run-id>` (exit 2, before the judge), not inconclusive: the path ties it to that run, so editing it after the run is tampering, not untied evidence.
|
|
448
448
|
|
|
449
|
-
A config whose journeys are all code-only, or cited captures that all come from code-only runs, stop the check the same way, before the judge, naming the journeys ([Real-product evidence](#real-product-evidence)). So does a claim
|
|
449
|
+
A config whose journeys are all code-only, or cited captures that all come from code-only runs, stop the check the same way, before the judge, naming the journeys ([Real-product evidence](#real-product-evidence)). So does a claim whose cited runs have no well-formed step record ([Step record](#step-record)).
|
|
450
450
|
|
|
451
451
|
`evidence: not proven: <cause>` stops the check before its judge is asked, always: exit 2, no result, no receipt. `checks.not_proven` is gone: a config that still sets it is a config error (exit 2) that says to delete the line.
|
|
452
452
|
|
|
@@ -612,7 +612,7 @@ done-done receipt --verify <head> # read-only: check a recorded receipt
|
|
|
612
612
|
|
|
613
613
|
`receipt` takes the same claim inputs as `check` and runs that check. It writes `<root>/.done-done/receipts/<head>.json` only when the check passes on the current HEAD, the tracked tree is clean, and the check cited at least one evidence file; a dirty tracked tree, a failed or inconclusive check, a pass that cited no evidence, or a moved HEAD writes nothing and exits non-zero. The record holds the repo, the head and tree sha, the claim outcome, every cited file with its sha256, the run behind each cited capture (`runs`: run id, journey and command, the command `null` for a run opened by hand or driven by the agent) and a timestamp. Every receipt is schema 2 and holds a `judge` block: the route, model, independence and author, and each judged file's path, sha256 and verdict. A cited file is stored relative to the repo, or as `<evidence root>/<path>` when it is under an evidence root outside the repo, so the receipt still finds it from another checkout or a restored evidence root.
|
|
614
614
|
|
|
615
|
-
`receipt --verify <head>` changes nothing. The argument may be a full commit sha or a ref (`HEAD`, a branch or a tag), resolved to the commit with `git rev-parse --verify <arg>^{commit}` before the receipt lookup; an unresolvable argument fails with the same missing-receipt shape. It succeeds only when the receipt exists, is PASS, the current HEAD is still that head, and every cited file still hashes to the recorded sha256. A receipt is written only after a judge pass, so it reads schema 2 only: any other receipt (one recorded without the judge, or by an older build that let a run past its gate) fails as malformed, and a schema 2 receipt needs a well-formed `judge` block whose every verdict is `pass` for a cited file with the same sha256, which the result shows, and `runs` that are not all code-only ([Real-product evidence](#real-product-evidence)) and carry a step record
|
|
615
|
+
`receipt --verify <head>` changes nothing. The argument may be a full commit sha or a ref (`HEAD`, a branch or a tag), resolved to the commit with `git rev-parse --verify <arg>^{commit}` before the receipt lookup; an unresolvable argument fails with the same missing-receipt shape. It succeeds only when the receipt exists, is PASS, the current HEAD is still that head, and every cited file still hashes to the recorded sha256. A receipt is written only after a judge pass, so it reads schema 2 only: any other receipt (one recorded without the judge, or by an older build that let a run past its gate) fails as malformed, and a schema 2 receipt needs a well-formed `judge` block whose every verdict is `pass` for a cited file with the same sha256, which the result shows, and `runs` that are not all code-only ([Real-product evidence](#real-product-evidence)) and each carry a well-formed step record ([Step record](#step-record); the record's words are never compared with the claim's; a run recorded before step records passes with a `warnings` entry); a receipt with no `runs`, written before that rule, fails: re-run `receipt` for the head. A receipt from `--from` (or a `verify` run that drove the skill) also records the verify skill that ran as `skill` (for example `done-done-verify-<app>`), which `--verify` and so the merge gate show; an older receipt without it still verifies. A missing or malformed receipt and a hash mismatch are failures (exit 1), not usage errors. When the mismatched path is named in a run record, the failure names it as `changed after run <run-id>`.
|
|
616
616
|
|
|
617
617
|
### Verify a change in one run
|
|
618
618
|
|
|
@@ -753,7 +753,7 @@ npx -y done-done-axi gate "$PR_HEAD_SHA"
|
|
|
753
753
|
|
|
754
754
|
The receipt is committed in a follow-up commit, because a receipt cannot sit inside the commit it proves (its hash would change). `gate [<head>] [--dir <folder>] [--pushed <time>]` prints `PASS` or `FAIL: <reason>` and exits 0, 1, or 2 on a usage error. It passes when a receipt in `.done-done-receipts/` (or `--dir`):
|
|
755
755
|
|
|
756
|
-
- is a well-formed pass from a judged run (the same shape `receipt --verify` requires, so its `runs` are not all code-only, see [Real-product evidence](#real-product-evidence), and
|
|
756
|
+
- is a well-formed pass from a judged run (the same shape `receipt --verify` requires, so its `runs` are not all code-only, see [Real-product evidence](#real-product-evidence), and each carries a well-formed step record, see [Step record](#step-record)) and is filed as `<its head>.json`;
|
|
757
757
|
- has a head that is `<head>` (default `HEAD`) or an ancestor of it, with every file changed since that head inside the receipts folder, so the proof still covers the code;
|
|
758
758
|
- was made no later than the push (`--pushed`, an ISO time; default the commit time of `<head>`), compared to the second.
|
|
759
759
|
|
package/docs/releasing.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
# 0.6.
|
|
1
|
+
# 0.6.17 patch release
|
|
2
2
|
|
|
3
|
-
This release
|
|
3
|
+
This release stops `check` and `receipt` refusing a real proof over wording. The step record a journey writes is still required and still checked for shape, but its prose is never compared with the claim's any more: a claim describes the outcome in its own words, and the judge, not a word test, says whether the cited captures show it. Two real proofs were refused before the judge ran — a descriptive claim about a 270 dp scroll-history walk whose words an honest step record never used, and sealed copies of the very files a run wrote, reported as being in no run record.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
A cited file also ties to the run that wrote it by its bytes, not only by its path: a sealed or renamed copy cited outside the evidence root, or at another path inside it, is that run's evidence, and the record's own path for those bytes picks the run when several wrote them. An edit at a recorded path is still reported as `changed after run <run-id>`. Every other refusal of a cited capture stays as it was: no cited files, a file in no run record, all-code-only runs, a run with no step record, an invalid step record, a dirty tree, a non-zero exit, a stale commit and a byte-identical duplicate.
|
|
6
|
+
|
|
7
|
+
Do not publish an unmerged candidate. The existing `release` workflow on main tests and publishes an unpublished package version using npm trusted publishing. After merge, confirm `npm view done-done-axi@0.6.17 version dist.integrity` and smoke-test a clean `npx` install with `done-done --version`. Do not run `npm publish` manually unless the workflow fails and the release is blocked; if so, stop and report.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "done-done-axi",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.17",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Prove your app really works: done-done drives your app the way a user does, a fresh judge checks every capture, and the proof is tied to a commit.",
|
|
6
6
|
"type": "module",
|