@popovych.co/witness 0.14.2 → 0.14.4

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/history.js CHANGED
@@ -17,10 +17,20 @@ export function contentAtSha(root, rel, sha) {
17
17
  }
18
18
  return undefined;
19
19
  }
20
+ // D160 audit note: a batched `git log --name-only` replacement for the per-plan
21
+ // `git log -1` below was written and REVERTED — merge commits print no name list and
22
+ // multi-path pathspecs change history simplification, so the batch disagreed with the
23
+ // per-file shape in both directions (a different pin than `witness diff` resolves — the
24
+ // exact second derivation this row exists to forbid). The per-plan spawn cost on idle
25
+ // `next`/`status` turns is accepted and recorded in Open / deferred.
20
26
  export function baseForSpec(root, canon, specId, excludePlanId) {
21
27
  const candidates = canon.docs.filter((d) => d.meta.type === 'plan' &&
22
28
  d.meta.parent === specId &&
23
29
  typeof d.meta['derives-from'] === 'string' &&
30
+ // D160. A withdrawn plan is not a realization: counting an abandoned plan's pin as
31
+ // the base would report `delta: none` for content nothing ever implemented, and
32
+ // would hide the spec from the plan queue that keys on this same derivation.
33
+ String(d.meta.status) !== 'abandoned' &&
24
34
  d.meta.id !== excludePlanId);
25
35
  if (candidates.length === 0)
26
36
  return { kind: 'empty' };
package/dist/reviewed.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { existsSync } from 'node:fs';
2
+ import { existsSync, lstatSync, readlinkSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { canonicalJson, canonicalSha, planContentSha } from './sha.js';
5
5
  import { changedFiles } from './evidence.js';
6
- import { git } from './gitio.js';
6
+ import { git, tryGit } from './gitio.js';
7
7
  import { effortAbandoned, effortStreams, latestRecap, readStream } from './journal.js';
8
8
  import { findById } from './scan.js';
9
9
  // Row 96a. The reviewed identity is the DIFF the battery read: the base commit plus the
@@ -20,10 +20,39 @@ import { findById } from './scan.js';
20
20
  // Deleted paths carry an explicit marker: `hash-object` throws on a missing file, and
21
21
  // dropping the path silently would make "this file is gone" invisible to the identity.
22
22
  const DELETED_BLOB = '(deleted)';
23
+ // D161 (#27, corrected by the audit's F1/F4). A changed path can be a symlink, a gitlink
24
+ // (submodule) or another non-file — entries `hash-object` fatals on or mis-hashes. Each
25
+ // gets the value git itself would store, so every reviewable change moves the identity
26
+ // and nothing else does:
27
+ // - symlink: the target STRING is git's blob — hash-object would FOLLOW the link and a
28
+ // retarget between byte-identical (or dangling) targets would vanish (F4);
29
+ // - inner repo: its HEAD — but only when the path IS its own repo. A cwd-based rev-parse
30
+ // walks upward and answers with the OUTER head for a plain directory, moving the
31
+ // identity on every commit and re-arming settled gates over identical diffs (F1);
32
+ // - staged gitlink with an uninitialized dir: the staged pointer itself;
33
+ // - anything else: a constant marker, like DELETED_BLOB, so presence still counts.
34
+ function blobOf(runRoot, rel) {
35
+ const abs = join(runRoot, rel);
36
+ const st = lstatSync(abs, { throwIfNoEntry: false });
37
+ if (!st)
38
+ return DELETED_BLOB;
39
+ if (st.isSymbolicLink())
40
+ return `(link)${readlinkSync(abs)}`;
41
+ if (st.isFile())
42
+ return git(runRoot, 'hash-object', '--', rel);
43
+ if (existsSync(join(abs, '.git'))) {
44
+ const inner = tryGit(abs, 'rev-parse', 'HEAD');
45
+ if (inner.ok)
46
+ return `(gitlink)${inner.out}`;
47
+ }
48
+ const staged = tryGit(runRoot, 'ls-files', '--stage', '--', rel);
49
+ if (staged.ok && staged.out.startsWith('160000 ')) {
50
+ return `(gitlink)${staged.out.split(/\s+/)[1]}`;
51
+ }
52
+ return '(non-file)';
53
+ }
23
54
  export function diffReviewedSha(runRoot, base) {
24
- const pairs = changedFiles(runRoot, base).map((rel) => existsSync(join(runRoot, rel))
25
- ? `${rel}\0${git(runRoot, 'hash-object', '--', rel)}`
26
- : `${rel}\0${DELETED_BLOB}`);
55
+ const pairs = changedFiles(runRoot, base).map((rel) => `${rel}\0${blobOf(runRoot, rel)}`);
27
56
  return sha256([base, '\n', pairs.sort().join('\n')]);
28
57
  }
29
58
  // Row 95: implement's identity is the diff AND the plan, because the battery reads both —
@@ -8,6 +8,8 @@ import { findById, loadCanon, plannableParent } from '../scan.js';
8
8
  import { designArtifactCurrent, designPending, designUnseen } from '../design.js';
9
9
  import { effortAbandoned, effortStreams, latestRecap, readStream } from '../journal.js';
10
10
  import { diffReviewedSha, effortOf, effortReviewedSha, effortSpecs, effortWrites, implementReviewedSha, planPairSha } from '../reviewed.js';
11
+ import { baseForSpec } from '../history.js';
12
+ import { canonicalSha } from '../sha.js';
11
13
  import { changedFiles, diffBase, evidenceForDiff, isTestPath } from '../evidence.js';
12
14
  import { SESSION_DEFAULT, stagePin } from '../model.js';
13
15
  import { handoffLine, relayLine, resolveDriver, resolveJudge } from '../harness.js';
@@ -345,7 +347,10 @@ export function readyChoice(canon, ready) {
345
347
  ? {
346
348
  why: flat
347
349
  ? 'the dependency graph does not distinguish these — ranked by ui flag, then by id'
348
- : `${n} of the ${ready.length} ready specs depend on it directly; planning it later means re-planning them`,
350
+ // D160 (#25, cosmetic): the count is direct dependents across all of canon
351
+ // (human-checkable in frontmatter, D121) — not a subset of the ready list,
352
+ // so the sentence must not claim one.
353
+ : `${n} canon doc(s) depend on it directly; planning it later means re-planning them`,
349
354
  judgeFirst: 'which slice matters this week. This ranks the dependency graph, which is all the CLI can see — product priority outranks it',
350
355
  }
351
356
  : {
@@ -623,23 +628,24 @@ export function computeNext(root, ctx, canon, cfg) {
623
628
  const s = String(doc.meta.status);
624
629
  return doc.meta.type === 'plan' ? s === 'done' : s === 'live' || doc.meta.type === 'principles';
625
630
  };
626
- // D157. A spec is queued when the plan gate would accept it as a parent AND someone owes
627
- // it a plan. `approved` is owed by definition (decompose just passed). `live` is owed
628
- // only while a live effort that wrote it owns zero non-abandoned plans — efforts never
629
- // auto-terminate, so without that clause every finished spec would queue forever.
631
+ // D157/D160. A spec is queued when the plan gate would accept it as a parent AND its
632
+ // content has moved past the last plan's pin — the same base derivation `witness diff`
633
+ // prints, because the effort ledger is not a readiness signal: an amendment can be
634
+ // realized by a plan booked under a different effort (issue #25), and a plan count of
635
+ // zero proves nothing either way. No pin at all means never realized, which is owed.
630
636
  // Reaching this rung proves decompose is settled: rung 6 returns first otherwise.
631
- const owedByEffort = (id) => efforts.some((e) => {
632
- const w = effortWrites(root, e.slug);
633
- if (!w.has(id))
634
- return false;
635
- return !plans.some((p) => w.has(String(p.meta.id)) && String(p.meta.status) !== 'abandoned');
636
- });
637
+ const unrealizedDelta = (d) => {
638
+ const base = baseForSpec(root, canon, String(d.meta.id));
639
+ return base.kind === 'empty' || base.sha !== canonicalSha(d.meta, d.body);
640
+ };
637
641
  const planless = canon.docs
638
642
  .filter((d) => d.meta.type === 'spec' && plannableParent(d))
639
- .filter((d) => String(d.meta.status) === 'approved' || owedByEffort(String(d.meta.id)))
640
643
  .filter((d) => !plans.some((p) => String(p.meta.parent) === String(d.meta.id) &&
641
644
  !['done', 'abandoned'].includes(String(p.meta.status))))
642
645
  .filter((d) => (d.meta.depends ?? []).every(ready))
646
+ // last on purpose: the only filter that costs a subprocess — the in-memory ones above
647
+ // discard for free, and all four are pure predicates over an intersection
648
+ .filter(unrealizedDelta)
643
649
  .map((d) => String(d.meta.id)).sort();
644
650
  if (planless.length > 0) {
645
651
  // A spec whose plan write can actually be booked outranks one that needs a new effort
package/dist/verdict.js CHANGED
@@ -103,7 +103,7 @@ function resolveDocPath(segments, body) {
103
103
  function safeRel(rel) {
104
104
  return !isAbsolute(rel) && !normalize(rel).split(/[\\/]/).includes('..');
105
105
  }
106
- function resolveCodeAnchor(anchor, root, canon) {
106
+ function resolveCodeAnchor(anchor, root, files, canon) {
107
107
  if (/[:#]L?\d+$/.test(anchor))
108
108
  return 'line numbers refused — they drift across revisions; use file#symbol';
109
109
  const [file = '', symbol] = anchor.split('#', 2);
@@ -114,8 +114,18 @@ function resolveCodeAnchor(anchor, root, canon) {
114
114
  // from two trees. Code paths keep the reviewed tree, which is the thing under judgment.
115
115
  const isCanon = canon !== undefined && canon.dirs.some((d) => file === d || file.startsWith(`${d}/`));
116
116
  const abs = join(isCanon ? canon.root : root, file);
117
- if (!existsSync(abs) || !statSync(abs).isFile())
118
- return `no file ${file} in the reviewed tree`;
117
+ if (!existsSync(abs) || !statSync(abs).isFile()) {
118
+ // D162 (issue #29). The coverage floor counts every path the diff lists, so every one
119
+ // of them must be anchorable or the floor is unsatisfiable by construction: a submodule
120
+ // pointer is a directory, a deleted file is nothing, a symlink can point at a directory
121
+ // — and each is reviewed content, its hunk in the prompt. The bare path resolves; a
122
+ // symbol still needs a file body to be found in.
123
+ if (!files.includes(file))
124
+ return `no file ${file} in the reviewed tree`;
125
+ if (symbol === undefined)
126
+ return undefined;
127
+ return `${file} is a changed path with no file body in the reviewed tree (a submodule pointer, a deleted file, or a directory) — anchor it by bare path`;
128
+ }
119
129
  if (symbol !== undefined) {
120
130
  const re = new RegExp(`\\b${symbol.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`);
121
131
  if (!re.test(readFileSync(abs, 'utf8')))
@@ -172,6 +182,10 @@ export function resolveAnchor(anchor, reviewed) {
172
182
  return undefined;
173
183
  if (!safeRel(scope))
174
184
  return `omission scope escapes the reviewed tree: ${scope}`;
185
+ // D162: a changed path is reviewed content even when nothing is left on disk (a deleted
186
+ // file's un-migrated export is an omission scoped exactly there).
187
+ if (reviewed.kind === 'tree' && reviewed.files.includes(scope))
188
+ return undefined;
175
189
  return existsSync(join(reviewed.root, scope))
176
190
  ? undefined
177
191
  : `omission scope "${scope}" is no file or directory in the reviewed tree`;
@@ -180,7 +194,7 @@ export function resolveAnchor(anchor, reviewed) {
180
194
  const canon = reviewed.canonRoot !== undefined && reviewed.canonDirs !== undefined
181
195
  ? { root: reviewed.canonRoot, dirs: reviewed.canonDirs }
182
196
  : undefined;
183
- return resolveCodeAnchor(anchor, reviewed.root, canon);
197
+ return resolveCodeAnchor(anchor, reviewed.root, reviewed.files, canon);
184
198
  }
185
199
  const segments = anchor.split(' > ');
186
200
  if (!segments[0].startsWith('#')) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@popovych.co/witness",
3
- "version": "0.14.2",
3
+ "version": "0.14.4",
4
4
  "description": "Evidence-gated spec-driven development pipeline for AI coding agents: brainstorm to PR in six stages, where specs are state, plans are motion, and no transition passes without evidence. Drives from Claude Code or Pi.",
5
5
  "keywords": [
6
6
  "spec-driven-development",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "witness",
3
3
  "description": "Spec-driven development with evidence gates: brainstorm → PR through six stages and five gates. Skills author, the pinned witness CLI writes state and witnesses evidence; gates stop where judgment is owed.",
4
- "version": "0.14.2",
4
+ "version": "0.14.4",
5
5
  "author": {
6
6
  "name": "Kostiantyn Popovych"
7
7
  },
@@ -10,7 +10,7 @@ The CLI decides, you act. If `$ARGUMENTS` contains `--manual`, append `--manual`
10
10
  Resolve the CLI once:
11
11
 
12
12
  ```bash
13
- WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.14.2}"
13
+ WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.14.4}"
14
14
  ```
15
15
 
16
16
  If the repo has no `witness.config.yaml`, run `$WITNESS init` first (one bootstrap commit), then proceed. If init or any verb refuses on a dirty tree, the human commits or cleans it — **never `git commit` yourself, anywhere, at any stage**: the CLI makes its own state commits, and the ship phase makes the sole code commit.
@@ -3,4 +3,4 @@
3
3
  # context). Silent everywhere that isn't a witness repo; failures swallowed:
4
4
  # session start must never hang or die on this.
5
5
  [ -f witness.config.yaml ] || exit 0
6
- ${WITNESS_BIN:-npx -y @popovych.co/witness@0.14.2} 2>/dev/null || true
6
+ ${WITNESS_BIN:-npx -y @popovych.co/witness@0.14.4} 2>/dev/null || true
@@ -15,7 +15,7 @@ witness: ends in a confirmed scope recap, writes no document. See NOTICE.md. -->
15
15
  Resolve the CLI once per session:
16
16
 
17
17
  ```bash
18
- WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.14.2}"
18
+ WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.14.4}"
19
19
  ```
20
20
 
21
21
  - **Render the CLI's decision output verbatim and in full — every line, unmodified.** Never print a command set you remember; never recompose, reformat, summarise or reorder what the CLI emitted. Which decisions are live, how they rank, and what each costs are the CLI's answers, and they change with the round, the bound, the repair grant and the content sha — a remembered set is wrong in more states than it is right.
@@ -10,7 +10,7 @@ description: Slice a witness effort's confirmed recap into vertical spec slices,
10
10
  Resolve the CLI once per session:
11
11
 
12
12
  ```bash
13
- WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.14.2}"
13
+ WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.14.4}"
14
14
  ```
15
15
 
16
16
  - **Render the CLI's decision output verbatim and in full — every line, unmodified.** Never print a command set you remember; never recompose, reformat, summarise or reorder what the CLI emitted. Which decisions are live, how they rank, and what each costs are the CLI's answers, and they change with the round, the bound, the repair grant and the content sha — a remembered set is wrong in more states than it is right.
@@ -16,7 +16,7 @@ No text copied verbatim; the design-critic lens is new material. -->
16
16
  Resolve the CLI once per session:
17
17
 
18
18
  ```bash
19
- WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.14.2}"
19
+ WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.14.4}"
20
20
  ```
21
21
 
22
22
  - **Render the CLI's decision output verbatim and in full — every line, unmodified.** Never print a command set you remember; never recompose, reformat, summarise or reorder what the CLI emitted. Which decisions are live, how they rank, and what each costs are the CLI's answers, and they change with the round, the bound, the repair grant and the content sha — a remembered set is wrong in more states than it is right.
@@ -15,7 +15,7 @@ tests carry the parent spec's tag in their NAME. See NOTICE.md. -->
15
15
  Resolve the CLI once per session:
16
16
 
17
17
  ```bash
18
- WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.14.2}"
18
+ WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.14.4}"
19
19
  ```
20
20
 
21
21
  - **Render the CLI's decision output verbatim and in full — every line, unmodified.** Never print a command set you remember; never recompose, reformat, summarise or reorder what the CLI emitted. Which decisions are live, how they rank, and what each costs are the CLI's answers, and they change with the round, the bound, the repair grant and the content sha — a remembered set is wrong in more states than it is right.
@@ -15,7 +15,7 @@ parent's criteria. See NOTICE.md. -->
15
15
  Resolve the CLI once per session:
16
16
 
17
17
  ```bash
18
- WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.14.2}"
18
+ WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.14.4}"
19
19
  ```
20
20
 
21
21
  - **Render the CLI's decision output verbatim and in full — every line, unmodified.** Never print a command set you remember; never recompose, reformat, summarise or reorder what the CLI emitted. Which decisions are live, how they rank, and what each costs are the CLI's answers, and they change with the round, the bound, the repair grant and the content sha — a remembered set is wrong in more states than it is right.
@@ -15,7 +15,7 @@ model can. See NOTICE.md. -->
15
15
  Resolve the CLI once per session:
16
16
 
17
17
  ```bash
18
- WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.14.2}"
18
+ WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.14.4}"
19
19
  ```
20
20
 
21
21
  - **Render the CLI's decision output verbatim and in full — every line, unmodified.** Never print a command set you remember; never recompose, reformat, summarise or reorder what the CLI emitted. Which decisions are live, how they rank, and what each costs are the CLI's answers, and they change with the round, the bound, the repair grant and the content sha — a remembered set is wrong in more states than it is right.
@@ -51,7 +51,7 @@ Respond with ONLY a JSON object — no prose before or after it (a single ```jso
51
51
  Anchor grammar — every anchor must resolve against the reviewed content, or your whole verdict is rejected as malformed:
52
52
 
53
53
  - Document reviews: a heading path, ` > `-separated — `## Behavior`, or `auth-refresh > ## Behavior` where a first segment without `#` names a reviewed document.
54
- - Code reviews: `path/to/file` or `path/to/file#symbol` (the symbol must occur in that file). Never line numbers — `file.ts:42` rejects the verdict.
54
+ - Code reviews: `path/to/file` or `path/to/file#symbol` (the symbol must occur in that file). A changed path with no file body — a submodule pointer, a deleted file — is anchored by its bare path; the diff hunk is the evidence. Never line numbers — `file.ts:42` rejects the verdict.
55
55
  - Something missing entirely: the finding's `"anchor"` value itself becomes `{ "kind": "omission", "scope": "<doc id, heading path, file, directory, or .>" }` — e.g. a full finding reads `{ "blocking": true, "anchor": { "kind": "omission", "scope": "tests/" }, "claim": "..." }`. Never place `kind`/`scope` as siblings of `blocking`/`claim` — they replace the `anchor` value, not sit beside it. The scope must exist; blocking omissions stop the gate like any other finding.
56
56
 
57
57
  Coverage is proof of reading, required even when `findings` is empty:
@@ -27,7 +27,7 @@ Respond with ONLY a JSON object — no prose before or after it (a single ```jso
27
27
  Anchor grammar — every anchor must resolve against the reviewed content, or your whole verdict is rejected as malformed:
28
28
 
29
29
  - Document reviews: a heading path, ` > `-separated — `## Behavior`, or `auth-refresh > ## Behavior` where a first segment without `#` names a reviewed document. A `## Valid anchors` menu precedes the reviewed content — copy anchors from it verbatim; never paraphrase a heading.
30
- - Code reviews: `path/to/file` or `path/to/file#symbol` (the symbol must occur in that file). Never line numbers — `file.ts:42` rejects the verdict.
30
+ - Code reviews: `path/to/file` or `path/to/file#symbol` (the symbol must occur in that file). A changed path with no file body — a submodule pointer, a deleted file — is anchored by its bare path; the diff hunk is the evidence. Never line numbers — `file.ts:42` rejects the verdict.
31
31
  - Something missing entirely: the finding's `"anchor"` value itself becomes `{ "kind": "omission", "scope": "<doc id, heading path, file, directory, or .>" }` — e.g. a full finding reads `{ "blocking": true, "anchor": { "kind": "omission", "scope": "tests/" }, "claim": "..." }`. Never place `kind`/`scope` as siblings of `blocking`/`claim` — they replace the `anchor` value, not sit beside it. The scope must exist; blocking omissions stop the gate like any other finding.
32
32
 
33
33
  Coverage is proof of reading, required even when `findings` is empty:
@@ -24,7 +24,7 @@ Respond with ONLY a JSON object — no prose before or after it (a single ```jso
24
24
  Anchor grammar — every anchor must resolve against the reviewed content, or your whole verdict is rejected as malformed:
25
25
 
26
26
  - Document reviews: a heading path, ` > `-separated — `## Behavior`, or `auth-refresh > ## Behavior` where a first segment without `#` names a reviewed document. A `## Valid anchors` menu precedes the reviewed content — copy anchors from it verbatim; never paraphrase a heading.
27
- - Code reviews: `path/to/file` or `path/to/file#symbol` (the symbol must occur in that file). Never line numbers — `file.ts:42` rejects the verdict.
27
+ - Code reviews: `path/to/file` or `path/to/file#symbol` (the symbol must occur in that file). A changed path with no file body — a submodule pointer, a deleted file — is anchored by its bare path; the diff hunk is the evidence. Never line numbers — `file.ts:42` rejects the verdict.
28
28
  - Something missing entirely: the finding's `"anchor"` value itself becomes `{ "kind": "omission", "scope": "<doc id, heading path, file, directory, or .>" }` — e.g. a full finding reads `{ "blocking": true, "anchor": { "kind": "omission", "scope": "tests/" }, "claim": "..." }`. Never place `kind`/`scope` as siblings of `blocking`/`claim` — they replace the `anchor` value, not sit beside it. The scope must exist; blocking omissions stop the gate like any other finding.
29
29
 
30
30
  Coverage is proof of reading, required even when `findings` is empty:
@@ -84,7 +84,7 @@ Respond with ONLY a JSON object — no prose before or after it (a single ```jso
84
84
  Anchor grammar — every anchor must resolve against the reviewed content, or your whole verdict is rejected as malformed:
85
85
 
86
86
  - Document reviews: a heading path, ` > `-separated — `## Behavior`, or `auth-refresh > ## Behavior` where a first segment without `#` names a reviewed document.
87
- - Code reviews: `path/to/file` or `path/to/file#symbol` (the symbol must occur in that file). Never line numbers — `file.ts:42` rejects the verdict.
87
+ - Code reviews: `path/to/file` or `path/to/file#symbol` (the symbol must occur in that file). A changed path with no file body — a submodule pointer, a deleted file — is anchored by its bare path; the diff hunk is the evidence. Never line numbers — `file.ts:42` rejects the verdict.
88
88
  - Something missing entirely: the finding's `"anchor"` value itself becomes `{ "kind": "omission", "scope": "<doc id, heading path, file, directory, or .>" }` — e.g. a full finding reads `{ "blocking": true, "anchor": { "kind": "omission", "scope": "tests/" }, "claim": "..." }`. Never place `kind`/`scope` as siblings of `blocking`/`claim` — they replace the `anchor` value, not sit beside it. The scope must exist; blocking omissions stop the gate like any other finding.
89
89
 
90
90
  Coverage is proof of reading, required even when `findings` is empty:
@@ -132,7 +132,7 @@ Respond with ONLY a JSON object — no prose before or after it (a single ```jso
132
132
  Anchor grammar — every anchor must resolve against the reviewed content, or your whole verdict is rejected as malformed:
133
133
 
134
134
  - Document reviews: a heading path, ` > `-separated — `## Behavior`, or `auth-refresh > ## Behavior` where a first segment without `#` names a reviewed document.
135
- - Code reviews: `path/to/file` or `path/to/file#symbol` (the symbol must occur in that file). Never line numbers — `file.ts:42` rejects the verdict.
135
+ - Code reviews: `path/to/file` or `path/to/file#symbol` (the symbol must occur in that file). A changed path with no file body — a submodule pointer, a deleted file — is anchored by its bare path; the diff hunk is the evidence. Never line numbers — `file.ts:42` rejects the verdict.
136
136
  - Something missing entirely: the finding's `"anchor"` value itself becomes `{ "kind": "omission", "scope": "<doc id, heading path, file, directory, or .>" }` — e.g. a full finding reads `{ "blocking": true, "anchor": { "kind": "omission", "scope": "tests/" }, "claim": "..." }`. Never place `kind`/`scope` as siblings of `blocking`/`claim` — they replace the `anchor` value, not sit beside it. The scope must exist; blocking omissions stop the gate like any other finding.
137
137
 
138
138
  Coverage is proof of reading, required even when `findings` is empty:
@@ -33,7 +33,7 @@ Respond with ONLY a JSON object — no prose before or after it (a single ```jso
33
33
  Anchor grammar — every anchor must resolve against the reviewed content, or your whole verdict is rejected as malformed:
34
34
 
35
35
  - Document reviews: a heading path, ` > `-separated — `## Behavior`, or `auth-refresh > ## Behavior` where a first segment without `#` names a reviewed document. A `## Valid anchors` menu precedes the reviewed content — copy anchors from it verbatim; never paraphrase a heading.
36
- - Code reviews: `path/to/file` or `path/to/file#symbol` (the symbol must occur in that file). Never line numbers — `file.ts:42` rejects the verdict.
36
+ - Code reviews: `path/to/file` or `path/to/file#symbol` (the symbol must occur in that file). A changed path with no file body — a submodule pointer, a deleted file — is anchored by its bare path; the diff hunk is the evidence. Never line numbers — `file.ts:42` rejects the verdict.
37
37
  - Something missing entirely: the finding's `"anchor"` value itself becomes `{ "kind": "omission", "scope": "<doc id, heading path, file, directory, or .>" }` — e.g. a full finding reads `{ "blocking": true, "anchor": { "kind": "omission", "scope": "tests/" }, "claim": "..." }`. Never place `kind`/`scope` as siblings of `blocking`/`claim` — they replace the `anchor` value, not sit beside it. The scope must exist; blocking omissions stop the gate like any other finding.
38
38
 
39
39
  Coverage is proof of reading, required even when `findings` is empty:
@@ -100,7 +100,7 @@ Respond with ONLY a JSON object — no prose before or after it (a single ```jso
100
100
  Anchor grammar — every anchor must resolve against the reviewed content, or your whole verdict is rejected as malformed:
101
101
 
102
102
  - Document reviews: a heading path, ` > `-separated — `## Behavior`, or `auth-refresh > ## Behavior` where a first segment without `#` names a reviewed document.
103
- - Code reviews: `path/to/file` or `path/to/file#symbol` (the symbol must occur in that file). Never line numbers — `file.ts:42` rejects the verdict.
103
+ - Code reviews: `path/to/file` or `path/to/file#symbol` (the symbol must occur in that file). A changed path with no file body — a submodule pointer, a deleted file — is anchored by its bare path; the diff hunk is the evidence. Never line numbers — `file.ts:42` rejects the verdict.
104
104
  - Something missing entirely: the finding's `"anchor"` value itself becomes `{ "kind": "omission", "scope": "<doc id, heading path, file, directory, or .>" }` — e.g. a full finding reads `{ "blocking": true, "anchor": { "kind": "omission", "scope": "tests/" }, "claim": "..." }`. Never place `kind`/`scope` as siblings of `blocking`/`claim` — they replace the `anchor` value, not sit beside it. The scope must exist; blocking omissions stop the gate like any other finding.
105
105
 
106
106
  Coverage is proof of reading, required even when `findings` is empty: