@mrciphersmith/keryx 0.3.133 → 0.3.135

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/core.js CHANGED
@@ -1078,6 +1078,12 @@ var init_catalog = __esm(() => {
1078
1078
  "Anchor class_scope on the callers that break, never on the changed line.",
1079
1079
  "Say when the radius could not answer \u2014 a non-code change has an empty one."
1080
1080
  ]),
1081
+ skill("review-degradation", "review", ["full"], "Ask whether the code is worse after a change than on the base branch (behaviour, defaults, speed, structure) and whether that can be shown, over the diff plus every consumer of each changed shared symbol.", [
1082
+ "Read the diff against the description before reading any consumer.",
1083
+ "Trace the consumers the code graph lists for each changed shared export; the set is the graph's, not yours.",
1084
+ "Name one degradation class and one evidence kind per finding; a major or blocker without evidence is lowered to info at ingest.",
1085
+ "Say what was traced when nothing was found, and say when the consumer cap dropped any."
1086
+ ]),
1081
1087
  skill("review-pr-contract", "review", ["recommended", "full"], "Compare what the PR body claims (checklist boxes, stated changes) with the diff, and look for documentation outside the diff that the change made stale.", [
1082
1088
  "Work every checklist box in pr-context.md as a row: quoted box, state, quoted diff line, expectation.",
1083
1089
  "File a finding only where a quoted box or claim disagrees with a quoted diff or document line.",
@@ -9733,6 +9739,7 @@ var init_skill_length_ceilings = __esm(() => {
9733
9739
  "review/review-backend": 361,
9734
9740
  "review/review-clean-code": 538,
9735
9741
  "review/review-core-boundaries": 158,
9742
+ "review/review-degradation": 191,
9736
9743
  "review/review-flow-graph": 183,
9737
9744
  "review/review-frontend": 607,
9738
9745
  "review/review-frontend-conventions": 206,
@@ -30058,6 +30065,7 @@ var IMPACT_TEMPLATE = "A high-risk hunk with no test in the same diff exercising
30058
30065
  // src/review/slice-routing.ts
30059
30066
  var ALWAYS_ROLE = "review-logic";
30060
30067
  var REGRESSION_ROLE = "review-regression";
30068
+ var DEGRADATION_ROLE = "review-degradation";
30061
30069
  var DEEP_ROLES = [...CORE_MANDATORY_REVIEWER_IDS, REGRESSION_ROLE];
30062
30070
  var DOC_FILE = /(\.(md|mdx|rst|txt|adoc)$|(^|\/)docs?\/)/i;
30063
30071
  var STYLE_FILE = /(\.(css|scss|sass|less|styl)$|(^|\/)(styles?|themes?)(\/|\.)|\.(styles?|theme)\.[jt]sx?$)/i;
@@ -30077,6 +30085,7 @@ var SECURITY_PRINCIPLE = new RegExp([
30077
30085
  String.raw`process\.env|\bBun\.env\b|\benv\[`,
30078
30086
  String.raw`\bfetch\(|\bhttps?\.(get|request)|\bnet\.|\bWebSocket\b|\bsocket\b|new URL\(`
30079
30087
  ].join("|"));
30088
+ var SHARED_SYMBOL_CHANGE = /^[+-]\s*export\s+(\*|\{|default\b|declare\b|abstract\b|(async\s+)?(function|class|interface|type|const|let|var|enum)\b)/m;
30080
30089
  var ENTRY_FILE = /(^|\/)(index|mod|main)\.[cm]?[jt]sx?$/;
30081
30090
  var ROLE_RULES = [
30082
30091
  { role: "review-testing-practices", why: "test file", path: (f) => isTestFilePath(f) || BUNDLED_PATH_GATES["review-testing-practices"](f) || /\.stories\.[jt]sx?$/.test(f) },
@@ -30093,6 +30102,7 @@ var ROLE_RULES = [
30093
30102
  { role: "review-architecture", why: "agent instructions (skill, rule, AGENTS/CLAUDE file, prompt)", path: (f) => AGENT_INSTRUCTION_FILE.test(f) },
30094
30103
  { role: "review-architecture", why: "contract or schema", path: (f) => CONTRACT_FILE.test(f) },
30095
30104
  { role: "review-architecture", why: "exported signature or contract type changed", content: /^[+-]\s*export\s+(declare\s+)?(type|interface|abstract\s+class|enum)\b|^[+-]\s*export\s+(async\s+)?function\s+\w+\s*[<(]/m },
30105
+ { role: DEGRADATION_ROLE, why: "shared (exported) symbol changed", content: SHARED_SYMBOL_CHANGE, codeOnly: true },
30096
30106
  { role: "review-security-code", why: "process, argv, git, input-derived path, env or network", content: SECURITY_PRINCIPLE, codeOnly: true },
30097
30107
  { role: "review-security-code", why: "process, filesystem write or trust-sensitive code", content: /\b(exec|execSync|execFile|spawn|spawnSync|Bun\.spawn|child_process|eval|new Function|innerHTML|dangerouslySetInnerHTML|password|passwd|secret|token|credential|api[_-]?key|sanitiz\w*|escape\w*|redact\w*|untrusted|trust\w*|permission\w*|allow-?list|deny-?list|sandbox\w*|cookie|cors|csrf|jwt|writeFile\w*|rm|unlink|rmdir|chmod|symlink\w*|realpath)\b/i, codeOnly: true },
30098
30108
  { role: "review-security-code", why: "auth, crypto or secret path", path: (f) => /\b(auth|login|session|permission|acl|rbac|credential|crypto|cipher|secret|security|trust|sandbox)\w*/i.test(f) && !isTestFilePath(f) && !DOC_FILE.test(f) },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrciphersmith/keryx",
3
- "version": "0.3.133",
3
+ "version": "0.3.135",
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": {
@@ -73,7 +73,6 @@
73
73
  "README.md",
74
74
  "package.json"
75
75
  ],
76
- "dependencies": {},
77
76
  "overrides": {
78
77
  "fast-uri": "^3.1.9",
79
78
  "body-parser": "^2.4.0",
@@ -85,6 +84,7 @@
85
84
  "protobufjs": "7.6.6",
86
85
  "sharp": "0.35.5"
87
86
  },
87
+ "dependencies": {},
88
88
  "optionalDependencies": {
89
89
  "@modelcontextprotocol/sdk": "^1.32.1",
90
90
  "@opentui/core": "^0.4.5",
@@ -455,7 +455,7 @@
455
455
  },
456
456
  "review-core-skills": {
457
457
  "kind": "skill",
458
- "description": "Generic (non-stack-specific) review skills: orchestrator, logic, architecture, security, performance, clean-code, highload, regression, core-boundaries, flow-graph, layout, style, testing-practices, verifier, premise, pr-feedback, and the legacy AI/learned/style review profiles.",
458
+ "description": "Generic review skills: orchestrator, logic, architecture, security, performance, clean-code, highload, regression, degradation, core-boundaries, flow-graph, layout, style, testing-practices, verifier, premise, pr-feedback, and legacy AI/learned/style profiles.",
459
459
  "paths": [
460
460
  "src/gdskills/bundled/skills/review/review-orchestrator/**",
461
461
  "src/gdskills/bundled/skills/review/review-logic/**",
@@ -465,6 +465,7 @@
465
465
  "src/gdskills/bundled/skills/review/review-clean-code/**",
466
466
  "src/gdskills/bundled/skills/review/review-highload/**",
467
467
  "src/gdskills/bundled/skills/review/review-regression/**",
468
+ "src/gdskills/bundled/skills/review/review-degradation/**",
468
469
  "src/gdskills/bundled/skills/review/review-pr-contract/**",
469
470
  "src/gdskills/bundled/skills/review/review-core-boundaries/**",
470
471
  "src/gdskills/bundled/skills/review/review-flow-graph/**",
@@ -0,0 +1,191 @@
1
+ ---
2
+ name: review-degradation
3
+ model_tier: deep
4
+ description: |
5
+ Use when: asking whether the code is WORSE after a change than on the base branch
6
+ (behaviour, defaults, speed or structure) and the answer can be shown. Dispatched
7
+ by review-orchestrator in a deep round at profile `full`, over the diff plus the
8
+ graph-listed importers of the module that holds each shared export the diff changed.
9
+ NOT for: the dependents-only regression check (that is review-regression), and NOT
10
+ for style, naming or architecture of code the change did not touch.
11
+ triggers:
12
+ - "review degradation"
13
+ - "is the code worse than base"
14
+ - "degradation against the base branch"
15
+ metadata:
16
+ author: "MrCipherSmith"
17
+ version: "1.0.0"
18
+ category: "review"
19
+ compatible_harnesses: "cursor,codex,zed,opencode,claude"
20
+ origin: "authored"
21
+ license: "MIT"
22
+ ---
23
+
24
+ # Review: Degradation
25
+
26
+ How this differs from `review-regression`, before anything else:
27
+
28
+ | | `review-regression` | `review-degradation` |
29
+ |---|---|---|
30
+ | Looks at | dependents of the change only | the diff itself plus the importers of each changed shared export's module |
31
+ | Floor | `major`; below it the ingest screen refuses the finding | none: `blocker`, `major`, `minor`, `info` |
32
+ | Set | the blast-radius files, refused in code if outside them | the consumers `keryx gdgraph affected` lists, never chosen by you |
33
+
34
+ `review-regression` is untouched by this reviewer and keeps its own screen. Do not re-file its findings; do not widen yours into its question.
35
+
36
+ Your inputs are the reviewer payload only; session memory and earlier conversation are not inputs. You run no command unless your payload's `allowed_tools` says `run-tests`, except the read-only `git log`, `git blame` and `git show` where your host gives a read-only child git (keryx shell does not: there the payload supplies the history).
37
+
38
+ ## The one question
39
+
40
+ > After this change, is the code worse than on the base branch: in behaviour, in defaults, in speed or in structure, and can that be shown?
41
+
42
+ "Worse" is measured against the base branch, not against an ideal. Code that was
43
+ already imperfect on the base is not a finding; it goes in the **pre-existing** lane.
44
+
45
+ ## Classes
46
+
47
+ Every finding carries exactly one `degradation_class`:
48
+
49
+ | Class | The change made the code worse by | Example shape |
50
+ |---|---|---|
51
+ | `behaviour-delta` | behaving differently from base on an input or event | an Escape key press handled by a nested popup now closes the parent |
52
+ | `default-change` | moving a default or a normalisation | an empty value now falls to a different placeholder in every consumer of a shared component |
53
+ | `diff-vs-description` | the description and the diff disagreeing | the description says shared code is unchanged, the diff edits it |
54
+ | `debt` | adding structure that must be paid back | a second data model beside the existing one, a dead branch, a leftover of the old surface |
55
+ | `performance` | making a path slower | every keystroke rebuilds all rows |
56
+ | `cross-pr` | conflicting with another open change | whichever lands second silently drops the other's fix |
57
+ | `blind-test` | a test that cannot see the regression | it uses the one input that works, or pins a neighbour of the changed symbol |
58
+
59
+ ## Evidence
60
+
61
+ A finding is a claim; evidence is what makes it one. Every non-pre-existing
62
+ minor, major or blocker carries one `evidence_kind`, and `evidence` says what you ran or read:
63
+
64
+ | Kind | What it is |
65
+ |---|---|
66
+ | `probe` | you ran the code, or a script against it, on head and on base, and the outputs differ |
67
+ | `consumer-trace` | you followed a consumer of the changed symbol to its effect, file by file |
68
+ | `merge-trace` | you merged two heads (or traced the merge) and showed what the second drops |
69
+ | `red-test` | you wrote or named a test that fails on the head and passes on the base |
70
+ | `base-diff` | you read the same path on base and on head and quote both |
71
+
72
+ `probe`, `red-test` and `merge-trace` run code, so they need `allowed_tools` to include
73
+ `run-tests`. Without it, `consumer-trace` and `base-diff` are still evidence: a trigger and outcome you
74
+ read in the code is a finding, `major` when a user who reaches it gets a wrong, lost or blocked result.
75
+ A question is for what the code cannot answer (a backend reply, data, timing), not for what you did not run.
76
+
77
+ This is enforced, not requested. `keryx review ingest` lowers a `major` or
78
+ `blocker` of this reviewer that has no `evidence_kind` to `info`, marks it unproven
79
+ and lists it in `scope.md` under `## Degradation evidence screen` with the rule
80
+ `degradation-no-evidence`. A true claim survives, labelled; other reviewers' findings are not touched.
81
+
82
+ ## What you are given
83
+
84
+ - the diff, and the PR description when there is one;
85
+ - the importers of the module that holds each changed shared export (and of a barrel that re-exports it), from the code graph, under one cap shared by all exports, with what it dropped. Trace those first. If the cap dropped consumers, say so in your summary; do not imply a complete sweep.
86
+ Where the graph lists no importers for a changed export (a barrel, a re-export, a file it has no node for), search for the users of the symbol, mark them `search-derived` in `enumeration_method`, and name in the summary the exports the graph could not answer;
87
+ - the flow's frozen acceptance criteria, and the previous round's findings.
88
+
89
+ ## How to look
90
+
91
+ Two passes, in this order. Done when every candidate of the sweep is filed, refuted or placed in a lane with its reason.
92
+
93
+ 1. **Sweep**, no severity floor. Read the diff against the description: anything it calls
94
+ unchanged and the diff edits is a `diff-vs-description` candidate. For each changed shared
95
+ symbol, default, stored field, mode or signature, read its consumers before the symbol: the
96
+ assumption sits at the consumer, and the change moves what the consumer receives. When the
97
+ change swaps a mechanism (a native attribute for a custom one, a reset on load for a rule, sync
98
+ for async), list what the old one did implicitly (styling and focus keyed on it, form submit, an
99
+ event default, a reset of dependent fields) and check the new path still does each. Compare
100
+ behaviour, not text (an event, a default, a fallback, an ordering, how many times a step runs):
101
+ show it on head and on base. Count work per trigger (each keystroke, render, request) and what it
102
+ scales with. For each test the change added or leaned on, ask whether it would fail if the
103
+ regression were real.
104
+ 2. **Refute** every candidate you would file as `blocker`, `major` or suspicion. Try to prove
105
+ yourself wrong with a `file:line` read: does the base do the same on this path, is there a
106
+ guard upstream or downstream, does a comment, commit or sibling give a reason (law 4), does
107
+ the description name it. Put the attempt in `evidence` ("refute: read a.ts:12, no guard").
108
+ What survives keeps its severity; what falls is dropped or goes to pre-existing.
109
+
110
+ ## Output lanes
111
+
112
+ Four lanes, no more:
113
+
114
+ - **Findings**: worse than base, with a class, evidence and `class_scope`.
115
+ - **Suspicions**: `info` with `publication: "question"` when the trigger or the outcome sits
116
+ outside the code (a backend reply, data, timing) and you cannot show it. `problem` ends with
117
+ "If true: <major|minor>, <outcome>"; `fix` is "Check: <the one request, test or command that
118
+ settles it>", which the author can run. Without both it is noise, not a suspicion.
119
+ - **Pre-existing**: `location_class: "pre-existing"`. True, not caused by this change, not blocking.
120
+ - **Verified clean**: in `summary`, one line per thing traced, naming it (the symbol, the consumers, the command).
121
+
122
+ ## Class scope: required for `blocker` and `major`
123
+
124
+ `class_scope` lists **every** site with the shape, and how you enumerated them. For
125
+ this reviewer the sites are usually consumers; the enumeration is the walk over the
126
+ graph's consumer list. "I checked the others" is not a method.
127
+
128
+ ```yaml
129
+ class_scope:
130
+ sites: ["src/screens/a.ts:40", "src/screens/b.ts:12"]
131
+ enumeration_method: "every consumer of the changed component in the payload; 2 of 9 pass no value"
132
+ ```
133
+
134
+ ```markdown
135
+ ### [F-NNN] Title
136
+
137
+ - **Severity**: blocker | major | minor | info
138
+ - **Class**: behaviour-delta | default-change | diff-vs-description | debt | performance | cross-pr | blind-test
139
+ - **Evidence kind**: probe | consumer-trace | merge-trace | red-test | base-diff
140
+ - **File**: path/to/file.ts:line
141
+ - **Problem**: what is worse than on base
142
+ - **Why it matters**: impact
143
+ - **Fix**: concrete suggestion
144
+ ```
145
+
146
+ Severity comes from **Severity (canonical)** in `review-orchestrator/SKILL.md` (`blocker` is its four merge-blocking shapes; `major` and `minor` split on the trigger-and-outcome test); no table here. A `diff-vs-description` is `major` at most, `blocker` only when the change it exposes has a blocker shape itself. When dispatched, follow the `reviewer-input.schema.json` payload and return a `REVIEW_RESULT` object compatible with
147
+ `.metaproject/skills/gdskills/review/review-orchestrator/reviewer-finding.schema.json`, then a short summary; `NEEDS_CONTEXT` rather than a guess. On a FIX slice, also return `prior_dispositions: [{id, disposition}]` for every attached finding id.
148
+
149
+ ### Shared laws (every reviewer)
150
+
151
+ 1. **A claim of runtime harm with no reproducible path is `info`.** If you cannot name the input, call, or condition that reaches the code, you have an observation, not a finding. Report it as `info` and say what would settle it.
152
+ 2. **Never flag the theoretical.** The path you describe must exist in the code under review. Do not report a safe API because it could be misused, or a pattern because it is often wrong elsewhere.
153
+ 3. **One finding per class, not one per occurrence.** When the same shape appears at several sites, report it once and list every site. Ten findings that are one finding hide the other nine problems.
154
+ 4. **A deliberate choice is not a defect until its reason is read and shown wrong.** An explicit override, suppression or exemption, a comment or commit giving a reason, or a sibling doing the same is a decision: read its reason (the comment, `git log -L` or `git blame`, the sibling) before you report it, and report it only when you can show that reason wrong. A project rule is checked against accepted platform practice and its known exceptions before a deviation from it is reported.
155
+
156
+ ---
157
+
158
+ ## Red Flags
159
+
160
+ | Rationalization | Why it is wrong |
161
+ |----------------|-----------------|
162
+ | "My first read is enough for a `major`." | Pass 2 tries to refute it first: the base, a guard, a stated reason. A `major` without a refutation attempt in `evidence` is not ready. |
163
+ | "This code was already messy, and the change sits next to it." | Worse than the base is the question. Pre-existing goes in its own lane, not in findings. |
164
+ | "I will name the consumers I think matter." | The graph's list comes first. A model-chosen set is the sweep that misses the consumer nobody thought of; extra users you find by search are added, labelled `search-derived`. |
165
+ | "I could not run it, so it is a question or a `minor`." | A traced trigger and outcome is a finding without a run, and `major` when a user gets a wrong, lost or blocked result. Ask only what the code cannot answer. |
166
+ | "The new test passes, so the change is covered." | A test that uses the one input that works cannot see the regression. Ask whether it fails when the behaviour is wrong. |
167
+
168
+ ---
169
+
170
+ ## Verification
171
+
172
+ Report done only once all of these hold:
173
+
174
+ - Every finding has one `degradation_class` and one `evidence_kind`, and `evidence` says what was run or read.
175
+ - Every `blocker` and `major` carries `class_scope` whose `enumeration_method` is the walk over the graph's consumer list.
176
+ - Every consumer in the payload was traced, or the summary says which were not and why, and whether the cap dropped any.
177
+ - Every `major`, `blocker` and suspicion names its refutation attempt; every suspicion has "If true" and "Check".
178
+ - The summary has a verified-clean line for each thing traced without a finding, naming what was traced.
179
+
180
+ <!-- keryx:layers-block:start -->
181
+ ## Project layers: graph, tests, wiki, memory
182
+
183
+ Your payload may carry a `layers` block for your files: the files the graph says are affected, symbol neighbours, related tests, the wiki pages that describe the files and the memory entries for their module. It is bounded, and `truncated` names what was cut and the command that fetches the rest. When the diff alone does not settle a question, ask the project. Each command below is read-only and calls no model.
184
+
185
+ - `keryx gdgraph affected <file>`: who depends on the file. Use it before you call a change contained, and whenever a changed export, signature or behaviour may have callers outside your files.
186
+ - `keryx test related <file>`: the tests that cover the file. Use it before you say a behaviour is untested, and to judge whether a test would fail if the code broke.
187
+ - `keryx wiki ask "<q>"`: what the project wiki says a module is for and which rules bind it. Use it when the intent of the code is unclear and the diff does not state it.
188
+ - `keryx memory search "<q>"`: recorded lessons, decisions and known mistakes. Use it before you report a pattern the project may already have decided on, and when a failure looks like one it has seen.
189
+
190
+ In a review package, `keryx gdgraph affected <file> --data-dir <package>/layers/graph` reads the graph built at the PR head; the tracked graph may be from another commit. A finding still rests on the code you read: what a layer says is a lead, not evidence. If a layer is empty or a command fails, say so in the report instead of guessing.
191
+ <!-- keryx:layers-block:end -->
@@ -270,14 +270,12 @@ bound the round.
270
270
  Two channels, and both work:
271
271
 
272
272
  - `--blast-radius <file>` — always correct, and the one to use.
273
- - `.metaproject/reviews/blast-radius.json` (or
274
- `.metaproject/flows/<flow>/reviews/blast-radius.json` for a flow-attached
275
- round) — the handoff slot, read when no flag is passed. The ingest **consumes**
276
- it: it is copied into the package it screened and removed from the slot, so a
277
- later round cannot be screened against a set nobody recomputed.
273
+ - `.metaproject/reviews/blast-radius.json` (or `.metaproject/flows/<flow>/reviews/blast-radius.json` for a flow-attached round) — the handoff slot, read when no flag is passed. The ingest **consumes** it: copied into the package it screened and removed from the slot, so a later round cannot be screened against a set nobody recomputed.
278
274
 
279
275
  Do **not** write the record inside the package directory before the ingest. A round that names no `--review-id` has not allocated that directory yet, and creating it makes the ingest take the next free name instead.
280
276
 
277
+ `review-degradation` (profile `full`, deep tier) asks what the regression screen does not: is the code worse than on the base, and can that be shown? It reads the diff plus the importers of the module that holds each changed shared export (and of a barrel that re-exports it), which `keryx review blast-radius --degradation [--degradation-cap <n>]` prints from the code graph under one cap shared by all exports; the model never picks the set. It has no severity floor and needs no `--blast-radius` record. A `major` or `blocker` of its with no `evidence_kind` is lowered to `info` at ingest and listed in `scope.md` under `## Degradation evidence screen`. A light round does not dispatch it. A delta round dispatches it on a fix slice that changes a source-code file (not a test, a document or a data file) and on every new deep slice; a fix slice's lines cannot tell whether they sit inside an exported function, so the payload settles it.
278
+
281
279
  `--refuted` takes the findings this round **raised and then dismissed**, in the
282
280
  same finding shape, each carrying `disposition: {state, evidence}` with one of
283
281
  the `dismissed-*` states. Without it a package keeps only the survivors of an
@@ -775,6 +773,8 @@ sites**? The orchestrator refuses, in code, a finding that is `outside-set`, bel
775
773
  `--brief` verbatim so no reviewer spends the round on findings that will be refused. `class_scope`
776
774
  names the caller that breaks.
777
775
 
776
+ `review-degradation` is told its consumers by the graph, not by its own search: before each dispatch of it (a deep slice, or a delta round's fix or new deep slice), run `keryx review blast-radius --ref "${BASE_SHA}" --package "${PKG}" --degradation` from the worktree root and hand its stdout to the reviewer verbatim as its payload section. It needs no `--blast-radius` record; without the payload the reviewer has no consumers to read.
777
+
778
778
  Recompute scope B on rounds 2..N only when the changed-file set moved, and always on the final round:
779
779
 
780
780
  ```bash
@@ -131,6 +131,18 @@
131
131
  "enum": ["finding", "question", "omit"],
132
132
  "description": "How an info finding is shown. question renders under Questions. omit stays in the keryx:findings block and is not published as a finding. Absent means finding. Never changes severity."
133
133
  },
134
+ "degradation_class": {
135
+ "title": "Which way the change made the code worse than the base branch",
136
+ "type": "string",
137
+ "enum": ["behaviour-delta", "default-change", "diff-vs-description", "debt", "performance", "cross-pr", "blind-test"],
138
+ "description": "Optional, set by `review-degradation`. Closed list: `behaviour-delta` (the diff behaves differently from base), `default-change` (a default or normalisation moved), `diff-vs-description` (the description and the diff disagree), `debt` (structure the change adds), `performance` (a slower path), `cross-pr` (two changes that conflict on merge), `blind-test` (a test that cannot see the regression)."
139
+ },
140
+ "evidence_kind": {
141
+ "title": "What proves a degradation finding",
142
+ "type": "string",
143
+ "enum": ["probe", "consumer-trace", "merge-trace", "red-test", "base-diff"],
144
+ "description": "Optional. Closed list: `probe` (a run that shows the difference), `consumer-trace` (a consumer of the changed symbol followed to its effect), `merge-trace` (the merged result of two heads), `red-test` (a test that fails on the head), `base-diff` (the same path read on base and on head). A `major` or `blocker` from `review-degradation` without it is downgraded to `info` at ingest, not rejected."
145
+ },
134
146
  "class_scope": {
135
147
  "title": "Every site of this shape, and how the set was derived",
136
148
  "description": "A finding anchored to one file:line is a claim about one site. Repeatedly, a fix repaired that site and left its siblings — one writer of five, one operator instruction of four, six readers of eight — and the next review round found them. Required for blocker and major; optional below, because enumerating the class for every info finding is theatre. Kept identical to src/gdskills/contracts/review-finding.schema.json, which is stricter (additionalProperties: false) and would reject this property if it were declared only here.",
@@ -287,6 +287,29 @@
287
287
  }
288
288
  }
289
289
  },
290
+ "degradation_class": {
291
+ "title": "Which way the change made the code worse than the base branch",
292
+ "type": "string",
293
+ "enum": ["behaviour-delta", "default-change", "diff-vs-description", "debt", "performance", "cross-pr", "blind-test"],
294
+ "description": "Optional, set by `review-degradation`. Closed list: `behaviour-delta` (the diff behaves differently from base), `default-change` (a default or normalisation moved), `diff-vs-description` (the description and the diff disagree), `debt` (structure the change adds), `performance` (a slower path), `cross-pr` (two changes that conflict on merge), `blind-test` (a test that cannot see the regression)."
295
+ },
296
+ "evidence_kind": {
297
+ "title": "What proves a degradation finding",
298
+ "type": "string",
299
+ "enum": ["probe", "consumer-trace", "merge-trace", "red-test", "base-diff"],
300
+ "description": "Optional. Closed list: `probe` (a run that shows the difference), `consumer-trace` (a consumer of the changed symbol followed to its effect), `merge-trace` (the merged result of two heads), `red-test` (a test that fails on the head), `base-diff` (the same path read on base and on head). A `major` or `blocker` from `review-degradation` without it is downgraded to `info` at ingest, not rejected."
301
+ },
302
+ "evidence_screen": {
303
+ "title": "Written by ingest when it downgraded a finding for missing evidence",
304
+ "type": "object",
305
+ "additionalProperties": false,
306
+ "required": ["rule", "original_severity"],
307
+ "description": "A reviewer never writes this. It marks a finding as unproven: the original severity is kept here so the downgrade is visible and reversible.",
308
+ "properties": {
309
+ "rule": { "type": "string", "const": "degradation-no-evidence" },
310
+ "original_severity": { "type": "string", "enum": ["blocker", "major"] }
311
+ }
312
+ },
290
313
  "class_scope": {
291
314
  "title": "Every site of this shape, and how the set was derived",
292
315
  "description": "A finding anchored to one file:line is a claim about one site. Repeatedly, a fix repaired that site and left its siblings — one writer of five, one operator instruction of four, six readers of eight — and the next review round found them. Required for blocker and major; optional below, because enumerating the class for every info finding is theatre.",