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 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; a copy elsewhere, by its bytes.
864
- const claims = records.filter((r) => r.files.some((f) => f.sha256 === hash && (inRoot === null || f.path === inRoot)));
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 covering the claim. Runs from before step records warn.
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()], claim.outcome);
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, rec.claim);
1131
- return steps.fault ? `receipt for ${short} has no step record covering its claim: ${steps.fault}` : null;
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, rec.claim).warning;
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,files,headRefOid,headRefName,baseRefOid');
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.stderr.trim().split('\n').pop()?.slice(0, 160) || '?'}`, head });
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 attrs = nwo && [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);
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
- let size = (pr.files ?? []).reduce((n, f) => n + (isGenerated(f.path ?? '') ? 0 : f.additions ?? 0), 0);
130
- if (size >= SIZE_LIMIT) {
131
- const d = run('pr', 'diff', src);
132
- if (!d.status) {
133
- let net = netLines(d.stdout, isGenerated);
134
- if (nwo && rules.upstream_catch_up && total(net) >= SIZE_LIMIT)
135
- net = without(net, upstreamDelta(run, nwo, head, pr.baseRefOid ?? '', isGenerated));
136
- if (nwo && rules.moved_from && total(net) >= SIZE_LIMIT)
137
- net = without(net, movedLines(run, nwo, body, (pr.files ?? []).map((f) => f.path ?? ''), isGenerated));
138
- size = total(net);
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
- // Words that carry no claim: the claim's own scaffolding, not what a user does or sees.
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
- export function stepsFault(runs, outcome) {
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
- if (bad)
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 whose step record covers the claim: ${reasons.join('; ')}`, warning: null };
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` refuses a capture whose run has no step record that covers the claim ([Step record](reference.md#step-record)).
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, and an API failure keeps the full count.
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 reads the claim against the record, with no model: at least half of the claim's words (four letters or more, without scaffolding such as `user`, `can`, `now`, `works`) must appear in the record's `start`, `steps` and `result`, matching word beginnings (`attach`, `attaches`, `attached`), and at least one of them in `result`. This runs after the code-only rule, which stays the first filter, and with it:
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) whose step record covers the claim, naming each run and the words its record never mentions. A log from a wrapper script whose record says `ran the unit suite` cannot satisfy a claim about attaching to a pane; a real CLI transcript whose record names the command and its output still satisfies a claim about that command. 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` apply the same rule to the receipt's claim with no config or evidence: a receipt whose runs have no covering record fails.
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 that no cited run's step record covers ([Step record](#step-record)).
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 covering the claim ([Step record](#step-record); 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>`.
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 carry a step record covering its claim, see [Step record](#step-record)) and is filed as `<its head>.json`;
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.15 patch release
1
+ # 0.6.17 patch release
2
2
 
3
- This release carries the audited README to the npm page: the hero GIF opens on the finished screen, the four lower terminal pictures sit one per row at one width, and the journey and Firstmate commands wrap so no line is clipped in the README column.
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
- 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.15 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.
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.15",
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",