@mrciphersmith/keryx 0.2.74 → 0.2.76

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrciphersmith/keryx",
3
- "version": "0.2.74",
3
+ "version": "0.2.76",
4
4
  "description": "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -52,7 +52,7 @@ unified report sorted by severity. It does not perform any review logic itself.
52
52
  ```
53
53
  Review Orchestrator Progress:
54
54
  - [ ] Step 0: On a PR target, collect external comments — `keryx review comments collect`
55
- - [ ] Step 1: Build Review Context Pack (PR metadata, scope, rules, context_doc summary)
55
+ - [ ] Step 1: Build Review Context Pack — PR metadata AND the PR's own description, scope, rules, context_doc summary, accepted memory, and the cross-repo contracts the diff consumes
56
56
  - [ ] Step 2: Detect review mode (diff mode vs. path mode)
57
57
  - [ ] Step 3: Build the bounded scope with `keryx review scope` — never by hand
58
58
  - [ ] Step 3b: On a deep round, compute scope B with `keryx review blast-radius` — never by browsing — and KEEP the `--json` file; `review ingest --blast-radius <file>` is refused without it
@@ -462,8 +462,8 @@ Required content:
462
462
  - Git/PR metadata: repo, branch, base, head, merge-base, PR number/URL when available.
463
463
  - Scope summary: changed files grouped by domain, high-risk files, generated/ignored files.
464
464
  - Requirements: issue URL, linked task docs, acceptance criteria extracted from `context_doc` when available.
465
- - **The PR's own description**, fetched not assumed: `gh pr view <n> --json title,body`. See below.
466
- - **Cross-repo contracts the diff depends on**, each pinned to the SHA you read it at. See below.
465
+ - **The PR's own description**, fetched not assumed: `gh pr view <n> --json title,body`, into the typed `pr.body`. See below.
466
+ - **Cross-repo contracts the diff depends on**, in the typed `cross_repo`: each pinned to the ref you read it at, and each recording whether the producer has merged. See below.
467
467
  - Rules: matched repository rules and convention docs by path.
468
468
  - **Memory: accepted project memory intersecting the changed paths.** See below — this step is required, not best-effort.
469
469
  - Decisions: why each reviewer was selected or skipped.
@@ -487,8 +487,17 @@ and not a pleasantry because unfiled housekeeping is raised again every round an
487
487
  fixed in none — three consecutive rounds of "still worth rewriting" is the
488
488
  recorded outcome of leaving it out of the findings array.
489
489
 
490
+ **The Stage 1 gate files it.** Handing the body to every reviewer is what makes
491
+ the body available; it is not what makes the comparison happen. A rule addressed
492
+ to everyone is owned by no one, and that is the state this finding sat in for
493
+ three rounds. The gate already holds the stated intent and the diff side by side,
494
+ so the comparison belongs there — see `## Stage 1 Gate — Spec Compliance`.
495
+
490
496
  Same class, same severity: an approach that depends on another repository's change
491
- being deployed first, with no deploy note saying so.
497
+ being deployed first, with no deploy note saying so. Note the boundary — that
498
+ `minor` is about the *description*. When the dependency is real and this change
499
+ can merge first, the blocker is filed against the call site instead; see
500
+ `### A producer that has not merged yet`.
492
501
 
493
502
  ### Cross-repo contracts are read, not assumed
494
503
 
@@ -499,15 +508,21 @@ the SHA:
499
508
  ```yaml
500
509
  cross_repo:
501
510
  - repo: vantage-backend
511
+ state: merged
502
512
  sha: f5219d5d4
513
+ merge_order: independent
503
514
  reason: "DQ report payload: which halves are null vs 0"
515
+ facts_pinned_round: 1
504
516
  facts:
505
517
  - "sqlScore/schemaDriftScore stay null when the half is absent"
506
518
  - "sqlWeightPercentage/schemaDriftWeightPercentage init to 0.0 and are set unconditionally"
507
519
  ```
508
520
 
509
- Put the facts in `review_context` so every reviewer shares one reading, and so a
510
- later round can tell a contract that moved from a reviewer that misread it.
521
+ Put the facts in `review_context.cross_repo` so every reviewer shares one reading,
522
+ and so a later round can tell a contract that moved from a reviewer that misread
523
+ it. The shape is typed in `review-context.schema.json`: `repo`, `reason`, `facts`
524
+ and `state` are required, `state: merged` requires the `sha`, and `state: open`
525
+ requires the `pr`.
511
526
 
512
527
  The rule that follows for reviewers, and that belongs in the dispatch prompt:
513
528
 
@@ -517,9 +532,49 @@ The rule that follows for reviewers, and that belongs in the dispatch prompt:
517
532
  > producer disproves, and a defect missed because the producer's actual default was
518
533
  > assumed rather than read.
519
534
 
520
- If the other repository is not available to you, say so in `cross_repo` and leave
521
- the dependent findings at `info`. An unavailable producer is a result. Assuming
522
- one is not.
535
+ If the other repository is not available to you, record it as
536
+ `state: unavailable` in `cross_repo` and leave the dependent findings at `info`.
537
+ An unavailable producer is a result. Assuming one is not.
538
+
539
+ A finding whose evidence lives in the producer sets `repo` on the finding itself.
540
+ `file` and `line` alone name a path in the repository under review, so without
541
+ `repo` a cross-repo claim cannot say where its evidence is — and the `info` rule
542
+ above then rests on nothing a later round can check.
543
+
544
+ ### A producer that has not merged yet
545
+
546
+ `state: merged` is the easy case: the contract is on the producer's base branch,
547
+ you pin the SHA, and it stays where you pinned it. A **parallel pull request** —
548
+ the backend change your change was written against, still open — is a different
549
+ object, and pinning it the same way is wrong in three separate ways.
550
+
551
+ **Pin the pull request, not just the commit.** An open branch gets rebased and
552
+ squashed. A SHA read from it today names a commit that will not exist after the
553
+ merge, so `state: open` requires `pr` and the SHA becomes a note about when you
554
+ read it rather than an address a later reader can follow.
555
+
556
+ **Merge order is a `blocker`, not a note.** A missing deploy note in the PR body
557
+ is `minor` — that is a documentation finding and it stays where it is. The
558
+ operational case is not that one: when an entry is `state: open` with
559
+ `merge_order: producer_first`, and this change can merge without the producer,
560
+ **production breaks the moment it does**. That is a `blocker` under the existing
561
+ enumeration, not a new shape — the consumer calls what is not there, or reads a
562
+ default the producer has not changed yet, which is shape 1 or shape 2 in
563
+ `### \`blocker\` — merge-blocking, and nothing else`. File it against the consuming
564
+ call site rather than the description, and write the fix as what it is: a merge
565
+ gate, a flag, or a fallback, never an edit to a diff that may be perfectly
566
+ correct. Scope it exactly — both facts have to be recorded, an open producer AND
567
+ a producer-first order, and no gate already holding the merge. A
568
+ `merge_order: unknown` is a question to resolve, not a blocker to file.
569
+
570
+ **Re-read an open producer every round.** `facts` pinned in round 1 are a
571
+ statement about a branch that is still being written; by round 3 the contract may
572
+ have changed under a finding that still cites it. On each round, re-read every
573
+ entry with `state: open` and set `revalidated_round`. An entry whose
574
+ `revalidated_round` is behind the current round has not been checked this round,
575
+ and findings resting on it drop to `info` until it is. This is the same discipline
576
+ `prior_findings` already imposes on the local diff, applied to the one input that
577
+ is most likely to have moved.
523
578
 
524
579
  ### Memory (required)
525
580
 
@@ -1117,7 +1172,8 @@ local frontend conventions reviewer.
1117
1172
 
1118
1173
  ## Stage 1 Gate — Spec Compliance
1119
1174
 
1120
- **Run this FIRST, before dispatching quality reviewers, when an `issue_url` or task doc is provided.**
1175
+ **Run this FIRST, before dispatching quality reviewers, whenever the change has a
1176
+ stated intent — an `issue_url`, a task doc, or a PR body.**
1121
1177
 
1122
1178
  1. Fetch issue or task requirements.
1123
1179
  2. Map changed files and functions to acceptance criteria.
@@ -1125,6 +1181,27 @@ local frontend conventions reviewer.
1125
1181
  4. If there are unimplemented criteria: emit them as `blocker` findings in the final report and note them in `## Blockers`.
1126
1182
  5. Continue dispatching the remaining reviewers regardless (spec gaps + quality issues both belong in the report).
1127
1183
 
1184
+ ### With no issue and no task doc, the PR body is the spec
1185
+
1186
+ A pull request with nothing linked is the common case, not the exception, and the
1187
+ gate used to skip entirely on it — which meant the only written statement of what
1188
+ the change was for went unchecked precisely when it was the only one there. When
1189
+ `issue_url` and `context_doc` are both absent, read `pr.body` as the spec and run
1190
+ steps 2-4 against it. Nothing at all to read — no issue, no task doc, an empty
1191
+ body — is itself the finding: `minor`, that the change states no intent.
1192
+
1193
+ ### This gate owns the description-vs-diff comparison
1194
+
1195
+ The `minor` for a description that promises an approach the diff does not take
1196
+ (see the Context Pack) is **filed here**. It was previously stated as a rule for
1197
+ "every reviewer" and owned by none, which is how it stayed a remark instead of a
1198
+ finding across three rounds. A domain reviewer reviews its domain; the one step
1199
+ that already holds intent and diff side by side is this one.
1200
+
1201
+ Run it on every round, not only the first. The drift the finding catches is
1202
+ created BY the rounds: the code moves to answer findings, the body does not, and
1203
+ whoever reads the merge commit a year later reads the body.
1204
+
1128
1205
  ---
1129
1206
 
1130
1207
  ## Dispatching Reviewers
@@ -1328,6 +1405,18 @@ Everything else is at most `major`. "This will definitely cause problems later"
1328
1405
  is not one of the four. Neither is "this violates the architecture", "this fails
1329
1406
  the linter", or "this is how the last outage started".
1330
1407
 
1408
+ **An unshipped dependency is shape 1 or shape 2, and the list stays closed.**
1409
+ When a producer is recorded `state: open` with `merge_order: producer_first` and
1410
+ nothing stops this change merging first, ask the enumeration's own question —
1411
+ what happens at runtime. The consumer calls what is not there (shape 1) or reads
1412
+ the default the producer has not changed yet (shape 2). That is why it is a
1413
+ `blocker` without a fifth shape being invented for it. Two things follow from
1414
+ its being an ordering fault rather than a coding one: file it against the
1415
+ consuming call site, and say in the fix that the remedy is a merge gate, a flag,
1416
+ or a fallback — never a change to the diff, which may be entirely correct. A
1417
+ missing deploy note with no recorded producer state behind it is not this; that
1418
+ is the `minor` in the Context Pack. See `### A producer that has not merged yet`.
1419
+
1331
1420
  ### `major` / `minor` / `info` — the boundary test
1332
1421
 
1333
1422
  Ask one question, and ask it of the **finding**, not of the code:
@@ -1459,6 +1548,7 @@ All findings from all sub-reviewers must be normalized to this format before con
1459
1548
 
1460
1549
  - **Severity**: blocker | major | minor | info
1461
1550
  - **File**: path/to/file.ts:line
1551
+ - **Repo**: producer repository — only when the evidence is not in the repository under review
1462
1552
  - **Problem**: what is wrong
1463
1553
  - **Why it matters**: impact on correctness / safety / maintainability / UX
1464
1554
  - **Fix**: concrete suggestion
@@ -23,6 +23,58 @@
23
23
  "object",
24
24
  "null"
25
25
  ],
26
+ "description": "The pull request under review, when there is one. `body` is the author's own statement of what the change does, and it is checkable against the diff — it is typed here rather than left to convention because the Context Pack prose told reviewers to read `pr.body` while the schema declared no such field, so nothing carried it and nothing could tell a body that was never fetched from one that was empty.",
27
+ "properties": {
28
+ "number": {
29
+ "type": [
30
+ "integer",
31
+ "null"
32
+ ]
33
+ },
34
+ "url": {
35
+ "type": [
36
+ "string",
37
+ "null"
38
+ ]
39
+ },
40
+ "title": {
41
+ "type": [
42
+ "string",
43
+ "null"
44
+ ]
45
+ },
46
+ "body": {
47
+ "type": [
48
+ "string",
49
+ "null"
50
+ ],
51
+ "description": "Fetched, not assumed: `gh pr view <n> --json title,body`. Null means the fetch did not happen or returned nothing; an empty string means the author wrote no description, which is a different fact and a reportable one."
52
+ },
53
+ "state": {
54
+ "type": [
55
+ "string",
56
+ "null"
57
+ ],
58
+ "enum": [
59
+ "open",
60
+ "merged",
61
+ "closed",
62
+ null
63
+ ]
64
+ },
65
+ "base": {
66
+ "type": [
67
+ "string",
68
+ "null"
69
+ ]
70
+ },
71
+ "head": {
72
+ "type": [
73
+ "string",
74
+ "null"
75
+ ]
76
+ }
77
+ },
26
78
  "additionalProperties": true
27
79
  },
28
80
  "scope": {
@@ -71,6 +123,129 @@
71
123
  "type": "string"
72
124
  }
73
125
  },
126
+ "cross_repo": {
127
+ "type": "array",
128
+ "description": "Contracts owned by another service that this diff consumes — a payload shape, a status enum, a timeout, a permission, a default. Each entry is one reading of the producer, shared by every reviewer, so that a later round can tell a contract that moved from a reviewer that misread it. An entry is a RECORD OF READING: an unavailable producer is a result and belongs here as `state: unavailable`, whereas an assumed one belongs nowhere.",
129
+ "items": {
130
+ "type": "object",
131
+ "required": [
132
+ "repo",
133
+ "reason",
134
+ "facts",
135
+ "state"
136
+ ],
137
+ "properties": {
138
+ "repo": {
139
+ "type": "string",
140
+ "minLength": 1
141
+ },
142
+ "reason": {
143
+ "type": "string",
144
+ "minLength": 1,
145
+ "description": "What this diff needs from the producer, in one line."
146
+ },
147
+ "facts": {
148
+ "type": "array",
149
+ "minItems": 1,
150
+ "items": {
151
+ "type": "string"
152
+ },
153
+ "description": "What the producer actually says, read at the pinned ref. A claim about another service's behaviour with no `file:line` behind it at a pinned ref is `info`, not `major`, however confident — both failure modes are recorded: a finding the producer disproves, and a defect missed because the producer's real default was assumed rather than read."
154
+ },
155
+ "state": {
156
+ "type": "string",
157
+ "enum": [
158
+ "merged",
159
+ "open",
160
+ "unavailable"
161
+ ],
162
+ "description": "Where the contract lives right now. `merged` is on the producer's own base branch and is pinned by `sha`. `open` is a parallel pull request that has not landed: its branch will be rebased or squashed, so a sha pinned today names a commit that will not exist after the merge, and the contract itself can still change. `unavailable` means the producer could not be read."
163
+ },
164
+ "sha": {
165
+ "type": [
166
+ "string",
167
+ "null"
168
+ ],
169
+ "description": "The commit the facts were read at. Required when `state` is `merged`."
170
+ },
171
+ "pr": {
172
+ "type": [
173
+ "string",
174
+ "null"
175
+ ],
176
+ "description": "The producer's pull request, when the contract is not merged yet."
177
+ },
178
+ "branch": {
179
+ "type": [
180
+ "string",
181
+ "null"
182
+ ]
183
+ },
184
+ "merge_order": {
185
+ "type": "string",
186
+ "enum": [
187
+ "producer_first",
188
+ "consumer_first",
189
+ "independent",
190
+ "unknown"
191
+ ],
192
+ "default": "unknown",
193
+ "description": "Which side has to ship first for production to stay correct. `producer_first` on a `state: open` entry is the case where merging THIS change first breaks production, and it is a blocker rather than the `minor` filed for a missing deploy note."
194
+ },
195
+ "facts_pinned_round": {
196
+ "type": [
197
+ "integer",
198
+ "null"
199
+ ],
200
+ "description": "The review round the facts were read in."
201
+ },
202
+ "revalidated_round": {
203
+ "type": [
204
+ "integer",
205
+ "null"
206
+ ],
207
+ "description": "The last round the producer was re-read. An entry with `state: open` that was pinned in round 1 and never revalidated is stale by round 3: a parallel pull request is precisely the thing that moves between rounds."
208
+ }
209
+ },
210
+ "allOf": [
211
+ {
212
+ "if": {
213
+ "properties": {
214
+ "state": {
215
+ "const": "merged"
216
+ }
217
+ },
218
+ "required": [
219
+ "state"
220
+ ]
221
+ },
222
+ "then": {
223
+ "required": [
224
+ "sha"
225
+ ]
226
+ }
227
+ },
228
+ {
229
+ "if": {
230
+ "properties": {
231
+ "state": {
232
+ "const": "open"
233
+ }
234
+ },
235
+ "required": [
236
+ "state"
237
+ ]
238
+ },
239
+ "then": {
240
+ "required": [
241
+ "pr"
242
+ ]
243
+ }
244
+ }
245
+ ],
246
+ "additionalProperties": true
247
+ }
248
+ },
74
249
  "memory": {
75
250
  "type": "object",
76
251
  "required": [
@@ -60,6 +60,13 @@
60
60
  "info"
61
61
  ]
62
62
  },
63
+ "repo": {
64
+ "type": [
65
+ "string",
66
+ "null"
67
+ ],
68
+ "description": "Which repository `file` and `line` are in. Null, the default, means the repository under review. A cross-repo claim names the producer here and pins the ref it was read at in `evidence`; without this field a finding about another service's behaviour could not say where its evidence lived, so the rule that such a claim is `info` unless it has a `file:line` at a pinned ref could not be checked by anything but the reviewer's word. Kept identical to src/gdskills/contracts/review-finding.schema.json, which is stricter (additionalProperties: false) and would reject a finding carrying `repo` if the property were declared only here."
69
+ },
63
70
  "file": {
64
71
  "type": [
65
72
  "string",
@@ -95,6 +95,10 @@
95
95
  }
96
96
  },
97
97
  "severity": { "type": "string", "enum": ["blocker", "major", "minor", "info"] },
98
+ "repo": {
99
+ "type": ["string", "null"],
100
+ "description": "Which repository `file` and `line` are in. Null, the default, means the repository under review. A cross-repo claim names the producer here and pins the ref it was read at in `evidence`; without this field a finding about another service's behaviour could not say where its evidence lived, so the rule that such a claim is `info` unless it has a `file:line` at a pinned ref could not be checked by anything but the reviewer's word. Declared in BOTH this schema and skills/review/review-orchestrator/reviewer-finding.schema.json: this one is additionalProperties:false, so a finding carrying `repo` is rejected here if only the reviewer-side schema knows about it."
101
+ },
98
102
  "file": { "type": ["string", "null"] },
99
103
  "line": { "type": ["integer", "null"], "minimum": 1 },
100
104
  "symbol": { "type": ["string", "null"] },