@weareikko/code-review 0.0.1 → 0.8.1

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.
Files changed (66) hide show
  1. package/LICENSE +105 -0
  2. package/README.md +221 -0
  3. package/bin/code-review.js +7 -0
  4. package/dist/cli-7RGxyNy6.js +6375 -0
  5. package/dist/cli-7RGxyNy6.js.map +1 -0
  6. package/dist/cli.d.ts +56 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +2 -0
  9. package/dist/config.d.ts +205 -0
  10. package/dist/config.d.ts.map +1 -0
  11. package/dist/diagnostics.d.ts +118 -0
  12. package/dist/diagnostics.d.ts.map +1 -0
  13. package/dist/diff-lines.d.ts +48 -0
  14. package/dist/diff-lines.d.ts.map +1 -0
  15. package/dist/errors.d.ts +69 -0
  16. package/dist/errors.d.ts.map +1 -0
  17. package/dist/fingerprints.d.ts +22 -0
  18. package/dist/fingerprints.d.ts.map +1 -0
  19. package/dist/git.d.ts +50 -0
  20. package/dist/git.d.ts.map +1 -0
  21. package/dist/github.d.ts +119 -0
  22. package/dist/github.d.ts.map +1 -0
  23. package/dist/gitlab-review.d.ts +210 -0
  24. package/dist/gitlab-review.d.ts.map +1 -0
  25. package/dist/gitlab.d.ts +98 -0
  26. package/dist/gitlab.d.ts.map +1 -0
  27. package/dist/logger.d.ts +10 -0
  28. package/dist/logger.d.ts.map +1 -0
  29. package/dist/otel.d.ts +111 -0
  30. package/dist/otel.d.ts.map +1 -0
  31. package/dist/parser.d.ts +27 -0
  32. package/dist/parser.d.ts.map +1 -0
  33. package/dist/payloads.d.ts +30 -0
  34. package/dist/payloads.d.ts.map +1 -0
  35. package/dist/platform.d.ts +71 -0
  36. package/dist/platform.d.ts.map +1 -0
  37. package/dist/platforms/github.d.ts +115 -0
  38. package/dist/platforms/github.d.ts.map +1 -0
  39. package/dist/platforms/gitlab.d.ts +26 -0
  40. package/dist/platforms/gitlab.d.ts.map +1 -0
  41. package/dist/posting.d.ts +108 -0
  42. package/dist/posting.d.ts.map +1 -0
  43. package/dist/prior-threads.d.ts +40 -0
  44. package/dist/prior-threads.d.ts.map +1 -0
  45. package/dist/product.d.ts +17 -0
  46. package/dist/product.d.ts.map +1 -0
  47. package/dist/review.d.ts +18 -0
  48. package/dist/review.d.ts.map +1 -0
  49. package/dist/review.js +2 -0
  50. package/dist/skills.d.ts +92 -0
  51. package/dist/skills.d.ts.map +1 -0
  52. package/dist/skipped-retrieval.d.ts +26 -0
  53. package/dist/skipped-retrieval.d.ts.map +1 -0
  54. package/dist/summary-carryover.d.ts +48 -0
  55. package/dist/summary-carryover.d.ts.map +1 -0
  56. package/dist/triage.d.ts +37 -0
  57. package/dist/triage.d.ts.map +1 -0
  58. package/dist/types.d.ts +95 -0
  59. package/dist/types.d.ts.map +1 -0
  60. package/dist/verify.d.ts +74 -0
  61. package/dist/verify.d.ts.map +1 -0
  62. package/package.json +90 -2
  63. package/skills/code-review/SKILL.md +112 -0
  64. package/skills/code-review/references/javascript-typescript.md +50 -0
  65. package/skills/code-review/references/php.md +46 -0
  66. package/skills/test-integrity/SKILL.md +76 -0
@@ -0,0 +1,37 @@
1
+ import type { ReviewComment } from './types.js';
2
+ /**
3
+ * A Find "angle" — one lens a finder is specialised to. `full` depth runs one
4
+ * finder per angle (concurrently) so each can go deep in its lane; Triage then
5
+ * merges and deduplicates their findings.
6
+ */
7
+ export interface ReviewAngle {
8
+ key: string;
9
+ directive: string;
10
+ }
11
+ export declare const REVIEW_ANGLES: readonly ReviewAngle[];
12
+ /**
13
+ * A finding paired with the pool member that authored it. The author model is
14
+ * internal pipeline metadata used to pick a cross-family verifier (a model other
15
+ * than the one that raised the finding). It MUST NOT leak into posted comments,
16
+ * fingerprints, or the summary — only `comment` is ever surfaced.
17
+ */
18
+ export interface AuthoredFinding {
19
+ comment: ReviewComment;
20
+ authorModel: string;
21
+ }
22
+ /**
23
+ * Merge findings from multiple angle finders into a deduplicated set.
24
+ *
25
+ * Two findings are considered the same when they share a file, sit within a few
26
+ * lines of each other, and have either an identical or sufficiently similar
27
+ * normalised subject — so heterogeneous models that phrase the same defect
28
+ * differently collapse to one finding. The higher-severity copy wins (ties
29
+ * broken by higher confidence), carrying its own author model forward, so a
30
+ * finding one angle rates CRITICAL is not masked by another angle's WARN.
31
+ *
32
+ * Deterministic by construction: inputs are sorted by a stable key before
33
+ * clustering, so the same set of findings always yields the same merged output
34
+ * regardless of the order angles complete in.
35
+ */
36
+ export declare function triageFindings(groups: AuthoredFinding[][]): AuthoredFinding[];
37
+ //# sourceMappingURL=triage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"triage.d.ts","sourceRoot":"","sources":["../src/triage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAc,aAAa,EAAY,MAAM,YAAY,CAAC;AAEtE;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,aAAa,EAAE,SAAS,WAAW,EAgB/C,CAAC;AAKF;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,aAAa,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;CACrB;AA+GD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,eAAe,EAAE,EAAE,GAAG,eAAe,EAAE,CAqC7E"}
@@ -0,0 +1,95 @@
1
+ export type Severity = 'info' | 'warn' | 'critical';
2
+ export type GitLabReviewSeverity = 'INFO' | 'WARN' | 'CRITICAL';
3
+ export type Confidence = 'high' | 'medium' | 'low';
4
+ export type Side = 'RIGHT' | 'LEFT';
5
+ export type ThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
6
+ /**
7
+ * How many stages of the review pipeline run.
8
+ * - `single`: one Find pass; the model's output is used verbatim (legacy behaviour).
9
+ * - `verify`: Find → Verify → Synthesize; each severe finding is re-checked by a
10
+ * separate adversarial agent before it survives into the posted review.
11
+ * - `full`: multi-angle Find (several finders, each a different lens) → Triage
12
+ * (dedup) → Verify → Synthesize.
13
+ */
14
+ export type ReviewDepth = 'single' | 'verify' | 'full';
15
+ export declare const REVIEW_DEPTHS: readonly ReviewDepth[];
16
+ export declare const THINKING_LEVELS: readonly ThinkingLevel[];
17
+ export interface ReviewComment {
18
+ file: string;
19
+ line: number;
20
+ side: Side;
21
+ severity: Severity;
22
+ /**
23
+ * The reviewer's certainty that the finding is a real defect, separate from
24
+ * its impact (encoded in `severity`). Defaults to 'high' when absent so
25
+ * legacy reviewer outputs continue to parse unchanged.
26
+ */
27
+ confidence: Confidence;
28
+ body: string;
29
+ }
30
+ /**
31
+ * A file dropped from the reviewed diff because the cumulative diff exceeded the
32
+ * char budget (distinct from quiet noise skips like lockfiles). `chars` is the
33
+ * size of that file's diff section.
34
+ */
35
+ export interface SizeSkippedFile {
36
+ path: string;
37
+ chars: number;
38
+ /** Added/removed lines in this file's dropped diff — feeds the coverage ratio. */
39
+ changedLines: number;
40
+ }
41
+ export interface DiffRefs {
42
+ base_sha: string;
43
+ start_sha: string;
44
+ head_sha: string;
45
+ }
46
+ export interface Fingerprints {
47
+ primary: string;
48
+ secondary: string;
49
+ }
50
+ /**
51
+ * A parsed reviewer finding paired with its dedup fingerprints and the
52
+ * platform-specific posting payload. `payload` is generic (defaulting to
53
+ * `unknown`) so the seam stays platform-agnostic: only the platform that built
54
+ * a payload reads it back. GitLab builds a {@link GitLabDiscussionPayload}.
55
+ */
56
+ export interface GeneratedComment<Payload = unknown> {
57
+ comment: ReviewComment;
58
+ fingerprints: Fingerprints;
59
+ duplicate: boolean;
60
+ payload: Payload;
61
+ }
62
+ export interface GitLabDiscussionPayload {
63
+ body: string;
64
+ position: {
65
+ position_type: 'text';
66
+ base_sha: string;
67
+ start_sha: string;
68
+ head_sha: string;
69
+ old_path: string;
70
+ new_path: string;
71
+ old_line?: number;
72
+ new_line?: number;
73
+ };
74
+ }
75
+ export declare function toGitLabReviewSeverity(severity: Severity): GitLabReviewSeverity;
76
+ export declare function normalizeSeverity(value: unknown): Severity;
77
+ /**
78
+ * Normalize a raw confidence value from reviewer JSON into the strict enum.
79
+ * Defaults to 'high' for absent / unrecognised values: a missing field is
80
+ * assumed to come from a pre-confidence reviewer output, and the reviewer
81
+ * historically only emitted findings it considered provable, which maps to
82
+ * high confidence.
83
+ */
84
+ export declare function normalizeConfidence(value: unknown): Confidence;
85
+ /**
86
+ * Split a `"provider/modelId"` model string on the FIRST slash. Multi-slash
87
+ * model IDs (e.g. `openrouter/anthropic/claude-3`) keep everything after the
88
+ * first slash as the model ID. When the string has no slash, `provider` is
89
+ * `undefined` and `modelId` is the whole string (or `undefined` when empty).
90
+ */
91
+ export declare function splitModel(model: string): {
92
+ provider: string | undefined;
93
+ modelId: string | undefined;
94
+ };
95
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;AACpD,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;AAChE,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;AACnD,MAAM,MAAM,IAAI,GAAG,OAAO,GAAG,MAAM,CAAC;AACpC,MAAM,MAAM,aAAa,GAAG,KAAK,GAAG,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC;AAEpF;;;;;;;GAOG;AACH,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEvD,eAAO,MAAM,aAAa,EAAE,SAAS,WAAW,EAAiC,CAAC;AAElF,eAAO,MAAM,eAAe,EAAE,SAAS,aAAa,EAOnD,CAAC;AAEF,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,IAAI,CAAC;IACX,QAAQ,EAAE,QAAQ,CAAC;IACnB;;;;OAIG;IACH,UAAU,EAAE,UAAU,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,kFAAkF;IAClF,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB,CAAC,OAAO,GAAG,OAAO;IACjD,OAAO,EAAE,aAAa,CAAC;IACvB,YAAY,EAAE,YAAY,CAAC;IAC3B,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE;QACR,aAAa,EAAE,MAAM,CAAC;QACtB,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,QAAQ,GAAG,oBAAoB,CAE/E;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,CAO1D;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,UAAU,CAO9D;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG;IACzC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;CAC7B,CAIA"}
@@ -0,0 +1,74 @@
1
+ import type { ReviewComment, Severity } from './types.js';
2
+ /**
3
+ * The Verify stage hands each severe finding to a separate, adversarial agent
4
+ * whose job is to refute it. The agent returns one of three decisions, which
5
+ * the Synthesize stage applies deterministically:
6
+ *
7
+ * - `keep`: the finding is proven at its stated severity — survives unchanged.
8
+ * - `downgrade`: a real concern, but the stated severity overstates the
9
+ * demonstrable impact — severity steps down one tier (and the Conventional
10
+ * Comment header is relabelled to match).
11
+ * - `drop`: not a real defect — removed from the review.
12
+ *
13
+ * Unknown / unparseable verifier output defaults to `keep` so a flaky verifier
14
+ * never silently deletes a finding (precision is the goal, but not at the cost
15
+ * of dropping findings we failed to actually evaluate).
16
+ */
17
+ export type VerifyDecision = 'keep' | 'downgrade' | 'drop';
18
+ export interface Verdict {
19
+ decision: VerifyDecision;
20
+ reason: string;
21
+ }
22
+ export interface AuditEntry {
23
+ file: string;
24
+ line: number;
25
+ action: 'dropped' | 'downgraded';
26
+ fromSeverity: Severity;
27
+ toSeverity?: Severity;
28
+ reason: string;
29
+ }
30
+ export interface SynthesisResult {
31
+ comments: ReviewComment[];
32
+ audit: AuditEntry[];
33
+ }
34
+ export declare function buildVerifySystemPrompt(diff: string, commitLog?: string): string;
35
+ export declare function buildVerifyUserPrompt(comment: ReviewComment): string;
36
+ export declare function parseVerdict(text: string): Verdict;
37
+ export declare function stepDownSeverity(severity: Severity): Severity;
38
+ /**
39
+ * Rewrite the leading Conventional Comment header of a comment body so its
40
+ * label/decoration matches a new severity. Leaves the body untouched when the
41
+ * first line is not a recognizable header (the severity field still changes —
42
+ * the header is cosmetic).
43
+ */
44
+ export declare function relabelBodyHeader(body: string, severity: Severity): string;
45
+ /**
46
+ * Apply per-finding verdicts to the Find stage's comments. Comments without a
47
+ * verdict (e.g. INFO findings that were never verified) pass through unchanged.
48
+ * Returns the surviving comments plus an audit trail of every drop/downgrade.
49
+ */
50
+ export declare function applyVerdicts(comments: ReviewComment[], verdicts: Map<number, Verdict>): SynthesisResult;
51
+ /**
52
+ * Rebuild the review summary from the comments that survived Verify, preserving
53
+ * the Find stage's prose overview and its own Notes (the context it applied,
54
+ * e.g. an ADR/commit that suppressed a finding), and regenerating the risk line
55
+ * and issues block from the surviving set.
56
+ *
57
+ * Verify's own drop/downgrade decisions are deliberately NOT surfaced here: a
58
+ * finding the verifier refuted is a non-issue the developer never saw, so
59
+ * echoing "Verify removed a WARN at x:y — not a real defect" only re-injects the
60
+ * noise the verifier just removed. The drop/downgrade counts stay in the run log
61
+ * for operators; see `result.audit` at the call site.
62
+ *
63
+ * Deterministic by design: the production Synthesize stage may instead write the
64
+ * summary with an LLM, but the skateboard keeps it pure and testable so the
65
+ * variable under test (Verify's decisions) is isolated from model variance.
66
+ */
67
+ export declare function rebuildSummary(originalSummary: string | null, kept: ReviewComment[]): string;
68
+ /**
69
+ * Build the canonical `{ summary, comments }` JSON the Synthesize stage writes
70
+ * to the review file. Shapes match what `parseReviewMarkdownWithWarnings`
71
+ * consumes, so the parser, payload builder, and posting path are untouched.
72
+ */
73
+ export declare function synthesizeReviewJson(originalSummary: string | null, result: SynthesisResult): string;
74
+ //# sourceMappingURL=verify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE1D;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,WAAW,GAAG,MAAM,CAAC;AAE3D,MAAM,WAAW,OAAO;IACtB,QAAQ,EAAE,cAAc,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,GAAG,YAAY,CAAC;IACjC,YAAY,EAAE,QAAQ,CAAC;IACvB,UAAU,CAAC,EAAE,QAAQ,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,KAAK,EAAE,UAAU,EAAE,CAAC;CACrB;AAID,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAgChF;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAWpE;AAID,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CA0BlD;AAID,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,GAAG,QAAQ,CAI7D;AAWD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAS1E;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,aAAa,EAAE,EACzB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,eAAe,CA0CjB;AAmDD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,cAAc,CAAC,eAAe,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,EAAE,aAAa,EAAE,GAAG,MAAM,CAkB5F;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,eAAe,EAAE,MAAM,GAAG,IAAI,EAC9B,MAAM,EAAE,eAAe,GACtB,MAAM,CASR"}
package/package.json CHANGED
@@ -1,8 +1,96 @@
1
1
  {
2
2
  "name": "@weareikko/code-review",
3
- "version": "0.0.1",
3
+ "version": "0.8.1",
4
+ "description": "Agent-driven code review for GitLab merge requests and GitHub pull requests, with deduplicated inline comments and summary notes.",
5
+ "keywords": [
6
+ "ci",
7
+ "code-review",
8
+ "github",
9
+ "gitlab",
10
+ "merge-request",
11
+ "pull-request"
12
+ ],
13
+ "homepage": "https://github.com/weareikko/code-review#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/weareikko/code-review/issues"
16
+ },
17
+ "license": "FSL-1.1-ALv2",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+ssh://git@github.com/weareikko/code-review.git"
21
+ },
22
+ "bin": {
23
+ "code-review": "./bin/code-review.js"
24
+ },
25
+ "files": [
26
+ "bin/",
27
+ "dist/",
28
+ "skills/",
29
+ "LICENSE",
30
+ "README.md"
31
+ ],
32
+ "type": "module",
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/review.d.ts",
36
+ "default": "./dist/review.js"
37
+ },
38
+ "./cli": {
39
+ "types": "./dist/cli.d.ts",
40
+ "default": "./dist/cli.js"
41
+ }
42
+ },
4
43
  "publishConfig": {
5
44
  "access": "public"
45
+ },
46
+ "scripts": {
47
+ "build": "vite build && tsgo --emitDeclarationOnly",
48
+ "test": "vitest run",
49
+ "test:ci": "vitest run --coverage",
50
+ "test:evals": "vitest run --config vite.eval.config.ts",
51
+ "snapshot:mr": "node scripts/snapshot-mr.mjs",
52
+ "typecheck": "tsgo --noEmit",
53
+ "typecheck:tests": "tsgo -p tsconfig.test.json --noEmit",
54
+ "lint": "oxlint",
55
+ "lint:fix": "oxlint --fix",
56
+ "lint:types": "oxlint --type-aware",
57
+ "format": "oxfmt --write .",
58
+ "format:check": "oxfmt --check .",
59
+ "check": "npm run lint && npm run format:check && npm run typecheck && npm run typecheck:tests && npm test",
60
+ "prepublishOnly": "npm run check && npm run build && npm pack --dry-run",
61
+ "prepare": "husky"
62
+ },
63
+ "dependencies": {
64
+ "@earendil-works/pi-agent-core": "0.75.3",
65
+ "@earendil-works/pi-ai": "0.75.3",
66
+ "@earendil-works/pi-coding-agent": "0.75.3",
67
+ "@opentelemetry/api": "^1.9.1",
68
+ "@opentelemetry/api-logs": "^0.218.0",
69
+ "@opentelemetry/resources": "^2.7.1",
70
+ "@opentelemetry/sdk-node": "^0.218.0",
71
+ "@opentelemetry/semantic-conventions": "^1.41.1",
72
+ "jsonrepair": "^3.14.0",
73
+ "yaml": "^2.9.0"
74
+ },
75
+ "devDependencies": {
76
+ "@types/node": "^24.12.4",
77
+ "@typescript/native-preview": "7.0.0-dev.20260304.1",
78
+ "@vitest/coverage-v8": "^4.1.8",
79
+ "dotenv": "^17.4.2",
80
+ "husky": "9.1.7",
81
+ "lint-staged": "17.0.4",
82
+ "memfs": "^4.57.6",
83
+ "oxfmt": "0.36.0",
84
+ "oxlint": "1.51.0",
85
+ "oxlint-tsgolint": "^0.23.0",
86
+ "vite": "^8.0.16",
87
+ "vitest": "^4.1.8",
88
+ "vitest-evals": "^0.11.0"
89
+ },
90
+ "lint-staged": {
91
+ "*.{js,ts,json,md,yml,yaml}": "oxfmt --write"
92
+ },
93
+ "engines": {
94
+ "node": ">=24"
6
95
  }
7
96
  }
8
-
@@ -0,0 +1,112 @@
1
+ ---
2
+ name: code-review
3
+ description: Finds real correctness bugs in code changes, and carries a baseline of Fowler design smells surfaced as non-blocking judgment calls. Use for adversarial code review, bug hunts, regression review, PR correctness checks, logic errors, data loss, race conditions, state bugs, interface contract breaks, error handling bugs, edge cases, and maintainability smells (duplicated code, feature envy, primitive obsession, data clumps, shotgun surgery). Excludes pure formatting, naming bikeshedding, AppSec, and anything a linter already enforces.
4
+ ---
5
+
6
+ You are an adversarial code reviewer. Your primary job is to find real, demonstrable bugs in the diff — report nothing in that category unless the failure is concrete and reproducible from the code itself. On top of that, you carry the **code smell baseline** below: a fixed set of design smells you may surface as non-blocking, judgment-call suggestions. Correctness comes first; smells never outrank or crowd out a real bug.
7
+
8
+ ## References
9
+
10
+ Load only the references relevant to the languages in the diff:
11
+
12
+ | Reference | Read When |
13
+ | ------------------------------------- | ---------------------------------------------------------------------- |
14
+ | `references/javascript-typescript.md` | Reviewing JavaScript, TypeScript, Node.js, React, Vue, or browser code |
15
+ | `references/php.md` | Reviewing PHP or Laravel code |
16
+
17
+ ## Finding Criteria
18
+
19
+ Report a finding only when you can prove all of these:
20
+
21
+ - The changed code is reachable in production or via a published interface.
22
+ - A specific input, state, or execution path triggers the failure.
23
+ - The surrounding code, tests, schema, or public contract defines what should happen.
24
+ - The changed behavior violates that contract and produces a concrete symptom.
25
+ - The impact is observable: wrong result, crash, data loss, corrupted state, missed side effect, or broken build.
26
+
27
+ No proof, no finding. Suspicion is not a result.
28
+
29
+ ## Commit Context
30
+
31
+ The `<commits>` section lists the commit messages for this MR (oldest first). Read them before analysing the diff — they tell you _why_ the code changed, not just _what_ changed.
32
+
33
+ When a commit message provides an explicit, specific justification for a pattern that would otherwise look like a bug, treat that justification as authoritative:
34
+
35
+ - **Suppress the finding** if the commit cites a concrete artefact: an ADR number, an incident or post-mortem reference, a named team sign-off, or an explicit architectural decision with stated rationale (e.g. "errors are handled at the SDK boundary", "this is intentional fire-and-forget per ADR-042").
36
+ - **Still report the finding** if the commit justification is vague ("this is fine", "works for us") or does not directly address the specific code pattern.
37
+ - **Reference the commit rationale** in your summary when you suppress a finding — note what the commit says so reviewers can verify it holds.
38
+
39
+ Do not use commit messages to suppress findings about data loss, incorrect billing, broken auth, or critical-path crashes — those require explicit confirmation in the code or tests, not just a commit message.
40
+
41
+ ## Investigation Process
42
+
43
+ 1. Read the changed code and enough surrounding context to understand the intended behavior.
44
+ 2. Identify the contract: caller expectations, public types, schemas, validation, docs, tests.
45
+ 3. Construct adversarial cases: null/undefined, empty collections, zero, false, empty string, duplicates, boundary values, concurrent calls, partial failures, reordered events.
46
+ 4. Trace data and state across imports, wrappers, validators, serializers, database writes, caches, and dependent call sites.
47
+ 5. Compare old and new behavior when the diff changes a condition, default, type, schema, query, or error path.
48
+ 6. Check whether types, schemas, framework guarantees, or caller guards already exclude the failure.
49
+ 7. Report only defects that survive this verification.
50
+
51
+ ## What To Report
52
+
53
+ | Category | Report When |
54
+ | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
55
+ | Logic and conditions | Branches are inverted, unreachable, too broad, too narrow, or collapse distinct cases such as `0`, `false`, `""`, `null`, and missing values. |
56
+ | Data contracts | Runtime values no longer match schemas, public types, API responses, persistence shapes, or caller assumptions. |
57
+ | State and mutation | Shared objects, caches, global state, refs, or maps are mutated in a way that leaks across callers or corrupts later work. |
58
+ | Async and ordering | Promises not awaited, race conditions in a reachable path, cleanup in wrong order, or unhandled rejections. |
59
+ | Error handling | Real failures swallowed, converted to success, retried unsafely, or leaving partial state that callers treat as complete. |
60
+ | Edge cases | Empty, first, last, duplicate, boundary, overflow, or timezone cases producing wrong behavior. |
61
+ | Build and workflow | Changed imports, exports, generated artifacts, or CI config that fails deterministically or reports false success. |
62
+
63
+ ## Code Smell Baseline
64
+
65
+ Beyond correctness, carry the smell baseline below — a fixed set of Fowler smells (_Refactoring_, ch. 3) that applies to the changed code even when the repo documents no standards. Two rules bind it:
66
+
67
+ - **The repo overrides.** A documented repo standard — project conventions, an in-file comment, or a decision cited in `<commits>` — always wins. Where it endorses something the baseline would flag, suppress the smell.
68
+ - **Always a judgement call.** Each smell is a labelled heuristic ("possible Feature Envy"), never a hard violation. Surface it as a suggestion, never as a blocker (see **Severity**). Flag only smells the diff **introduces or worsens** and that are visible in the changed lines — do not audit pre-existing code the diff leaves alone.
69
+
70
+ | Smell | What it is → how to fix |
71
+ | ---------------------- | ----------------------------------------------------------------------------------------- |
72
+ | Mysterious Name | function/variable/type whose name doesn't reveal intent → rename it |
73
+ | Duplicated Code | the same logic appears in multiple hunks or files → extract and call from both |
74
+ | Feature Envy | a method reaches into another object's data more than its own → move it to that data |
75
+ | Data Clumps | the same fields or params travel together repeatedly → bundle them into one type |
76
+ | Primitive Obsession | a primitive stands in for a domain concept → give the concept its own type |
77
+ | Repeated Switches | the same switch / if-cascade recurs across the diff → use polymorphism or a shared map |
78
+ | Shotgun Surgery | one conceptual change forces scattered edits across many files → gather it into one place |
79
+ | Divergent Change | one file is edited for unrelated reasons → split so each module changes for one reason |
80
+ | Speculative Generality | abstraction added for needs that don't exist yet → delete; inline until a real need lands |
81
+ | Message Chains | long `a.b().c().d()` navigation → hide the walk behind one method |
82
+ | Middle Man | a class/module that mostly just delegates onward → call the real target directly |
83
+ | Refused Bequest | a subtype ignores or overrides most of what it inherits → prefer composition |
84
+
85
+ Deliberately out of scope: formatting and whitespace, naming bikeshedding a linter would catch, and the vaguer Fowler entries (Comments, Long Function, Loops) — they are too subjective or already tooled. Never restate a smell the bug categories above already cover.
86
+
87
+ ## Severity
88
+
89
+ Map to the project's existing tiers:
90
+
91
+ - **CRITICAL**: data loss or corruption, critical-path crashes, broken production deploys, incorrect billing or permissions, deadlocks.
92
+ - **WARN**: reproducible wrong results, recoverable crashes, missed side effects, meaningful edge cases in a shipped path.
93
+ - **INFO**: narrow bug with limited blast radius, confusing state that can cause user-visible mistakes.
94
+
95
+ Use the lower severity when impact depends on unproven preconditions.
96
+
97
+ **Code Smell Baseline findings** map separately: default **INFO** (a `suggestion (non-blocking)` or `nitpick`). Rise to **WARN** only when the smell is concrete and carries real maintenance cost visible in the diff — e.g. logic duplicated across hunks that will predictably drift, or a switch repeated in several places. **Never CRITICAL** — a smell is never blocking. When unsure between INFO and WARN, pick INFO; when unsure whether a smell is real, stay silent.
98
+
99
+ ## What Not To Report
100
+
101
+ - Security vulnerabilities — route to a dedicated security skill.
102
+ - Pure formatting, whitespace, import ordering, or naming bikeshedding — anything a linter or formatter already enforces.
103
+ - Maintainability or refactor advice that is not one of the **Code Smell Baseline** smells, or that targets pre-existing code the diff does not touch.
104
+ - Large-scale architecture or design-layering rewrites beyond the smell baseline.
105
+ - Performance unless the change causes a reachable timeout, hang, or resource exhaustion.
106
+ - Missing tests unless the changed test now asserts the wrong behavior or hides a real regression.
107
+ - Existing bugs untouched by the diff.
108
+ - Typos, misspelled identifiers, or wrong characters unless you quote the offending token verbatim from the diff and it appears there character-for-character. If your "corrected" spelling already matches the code, the typo does not exist — do not report it.
109
+ - Hypothetical failures requiring unrealistic inputs or impossible call order.
110
+ - A failure that an existing guard, early return, default, optional chain, or type already prevents. Re-read the function entry and adjacent lines before claiming a crash or unguarded access — e.g. do NOT report "crashes when `this.input` is missing" when the method opens with `if (!this.input) return;`. If a guard on a nearby line excludes the failure path, the finding does not stand.
111
+ - Framework, language, or dependency behavior that already guarantees the suspected case is safe.
112
+ - Patterns that a commit message in `<commits>` explicitly justifies with a concrete artefact (ADR, incident reference, named sign-off, or stated architectural rationale) — see **Commit Context** above.
@@ -0,0 +1,50 @@
1
+ # JavaScript / TypeScript Correctness Reference
2
+
3
+ Use this reference when reviewing JavaScript, TypeScript, Node.js, React, or browser code changes. Load only when materially useful — do not apply patterns mechanically.
4
+
5
+ ## Async and Promises
6
+
7
+ - **Missing await**: a `Promise` returned from an async function and not awaited silently discards the result and swallows thrown errors. Check call sites, event handlers, and middleware chains.
8
+ - **Unhandled rejection**: `Promise.all` rejects on the first failure; `Promise.allSettled` continues. Mixing them incorrectly drops results or hides errors.
9
+ - **Race condition in sequential reads**: reading a value before an async write completes produces stale data. Look for patterns where `.then()` chains or `await` steps are interleaved across shared mutable state.
10
+ - **`async` inside `forEach`**: `Array.prototype.forEach` does not await async callbacks. Use `for...of` or `Promise.all(arr.map(...))` when ordering or completion matters.
11
+
12
+ ## Type and Value Coercion
13
+
14
+ - **Falsy conflation**: `0`, `""`, `false`, `null`, `undefined`, and `NaN` are all falsy. A guard like `if (!value)` incorrectly rejects legitimate values of `0` or `""`.
15
+ - **Loose equality**: `== null` matches both `null` and `undefined`; `=== null` does not. Changing from one to the other silently changes which values are accepted.
16
+ - **`typeof null === "object"`**: a classic trap. Distinguish `null` from objects explicitly when branching on type.
17
+ - **Optional chaining short-circuit**: `a?.b.c` evaluates `b.c` only when `a` is not null/undefined; `a?.b?.c` is needed when `b` may also be absent.
18
+
19
+ ## Arrays and Collections
20
+
21
+ - **Mutating while iterating**: modifying an array inside a `forEach`, `for...of`, or `reduce` that reads the same array produces unpredictable results.
22
+ - **`Array.sort` is in-place**: `arr.sort(fn)` mutates the original. Callers expecting the original to be unchanged receive a sorted array.
23
+ - **Sparse array traps**: `Array(n)` creates a sparse array; `.map()` skips holes. Use `Array.from({ length: n })` for filled iteration.
24
+ - **`Set`/`Map` equality**: objects are compared by reference in `Set` and `Map`. Adding a structurally identical object creates a duplicate entry.
25
+
26
+ ## Objects and Spread
27
+
28
+ - **Shallow copy**: `{ ...obj }` copies only top-level keys. Nested objects share the same reference; mutations propagate to both copies.
29
+ - **Prototype pollution**: merging untrusted input with `Object.assign` or spread can overwrite `__proto__`, `constructor`, or `toString`.
30
+ - **Key enumeration order**: `Object.keys` order is insertion order for string keys in modern engines but not guaranteed for numeric-like keys. Code relying on a specific order may break.
31
+
32
+ ## Node.js Specifics
33
+
34
+ - **Synchronous blocking in async context**: `fs.readFileSync`, `execSync`, and similar block the event loop. Replacing an async path with a sync one in a request handler stalls concurrent requests.
35
+ - **`process.env` values are strings**: comparing `process.env.PORT === 8080` is always false; use `Number(process.env.PORT)`.
36
+ - **`require` caching**: modules are cached after first load. Mutating a required module's exports affects all other consumers in the same process.
37
+
38
+ ## React / UI
39
+
40
+ - **Stale closure**: an event handler or `useEffect` callback closed over a state variable captures its value at render time. Reading it after an update returns the old value; use the functional updater form or a ref.
41
+ - **Missing dependency in `useEffect`**: omitting a variable from the dependency array means the effect does not re-run when that variable changes, silently using stale data.
42
+ - **Mutating state directly**: `state.items.push(x)` does not trigger a re-render; always return a new reference.
43
+ - **Key reuse across list types**: reusing the same `key` for different components in the same list causes React to reuse DOM nodes incorrectly.
44
+
45
+ ## TypeScript-Specific
46
+
47
+ - **Non-null assertion `!`**: `value!` removes null/undefined from the type but adds no runtime check. If `value` is actually null at runtime, the assertion causes a downstream crash that TypeScript cannot catch.
48
+ - **`as` cast silences the compiler**: a cast `x as Foo` forces the type without checking. The bug becomes a runtime failure instead of a compile error.
49
+ - **`any` propagation**: a value typed as `any` disables type checking for all downstream operations. A change that widens a type to `any` can hide contract violations.
50
+ - **Discriminated union exhaustiveness**: removing a `case` or `default` from a switch on a discriminated union silently ignores valid values if `noImplicitReturns` is not enabled.
@@ -0,0 +1,46 @@
1
+ # PHP / Laravel Correctness Reference
2
+
3
+ Use this reference when reviewing PHP or Laravel code changes. Load only when materially useful — do not apply patterns mechanically.
4
+
5
+ ## Type System and Coercion
6
+
7
+ - **Loose comparison**: `==` in PHP coerces types before comparing. `0 == "foo"` is true in PHP 7 (false in PHP 8), `0 == ""` is true in PHP 7, `"1" == true` is true in any version. Use `===` when the type matters.
8
+ - **`empty()` conflation**: `empty($x)` returns true for `0`, `""`, `"0"`, `[]`, `null`, `false`, and unset variables. Using it as a null check silently rejects legitimate zero and empty-string values.
9
+ - **`isset()` vs `array_key_exists()`**: `isset($arr['key'])` returns false when the key exists with a `null` value; `array_key_exists` does not. Use the latter when distinguishing missing from null.
10
+ - **Implicit int/string coercion in array keys**: PHP coerces numeric strings to integers when used as array keys. `$arr["1"]` and `$arr[1]` access the same slot.
11
+
12
+ ## Null Handling
13
+
14
+ - **Nullsafe operator short-circuit**: `$a?->b->c` only short-circuits the first dereference; `$a?->b?->c` is needed when `b` may also be null.
15
+ - **Return type narrowing**: adding `?` to a return type is a widening change; removing `?` is a narrowing one. Callers that handled `null` now receive an unexpected non-null, or callers that didn't will receive null they cannot handle.
16
+ - **`null` coalescence with side effects**: `$x ?? someCall()` only calls `someCall()` when `$x` is null. If the call had necessary side effects, removing the null path skips them.
17
+
18
+ ## Laravel-Specific
19
+
20
+ - **Eloquent `firstOrFail` vs `first`**: swapping one for the other changes whether a missing record throws a `ModelNotFoundException` or returns `null`. Callers expecting an exception now receive null; callers expecting null now get an exception.
21
+ - **Mass assignment without `$fillable`**: adding a new field to a form request without adding it to `$fillable` silently ignores it. Adding it to `$fillable` without a form request validation rule may expose it to arbitrary input.
22
+ - **Query builder vs Eloquent**: a raw query builder call bypasses model observers, global scopes, and casting. Switching from Eloquent to query builder removes those guarantees silently.
23
+ - **Eager loading N+1**: adding a relationship access inside a loop on a collection loaded without `with()` produces N additional queries. Check whether the relationship is loaded before the loop.
24
+ - **`->get()` vs `->all()`**: on a Collection, `->all()` returns the underlying array; on a query builder, calling `->all()` does not exist. Mixing these causes a fatal error or unexpected return type.
25
+ - **`Cache::remember` race**: two concurrent requests can both miss the cache and execute the callback simultaneously. For expensive or write-sensitive operations, consider atomic locking.
26
+ - **Queued job serialization**: Eloquent models in a queued job are serialized by ID and re-fetched on the worker. If the model is deleted before the job runs, the job fails with a `ModelNotFoundException`.
27
+ - **`withTrashed` scope leakage**: applying `withTrashed()` to a query builder instance affects all subsequent chained calls on that builder, including unrelated filters added later.
28
+
29
+ ## Error Handling
30
+
31
+ - **Swallowed exceptions**: an empty `catch` block or a `catch` that only logs silently continues execution after a failure, leaving downstream code to operate on invalid state.
32
+ - **`@` error suppression operator**: `@function()` suppresses PHP errors and warnings, hiding real failures. Removing it can expose previously silent errors; adding it hides new ones.
33
+ - **Exception hierarchy**: catching `\Exception` does not catch `\Error` (including `TypeError`, `ParseError`). Catching `\Throwable` catches both; not catching `\Throwable` leaves fatal errors unhandled.
34
+
35
+ ## Database and Transactions
36
+
37
+ - **Non-atomic multi-step write**: two separate queries that must succeed together (e.g., deducting balance and inserting a record) without a transaction can leave data in a partial state on failure.
38
+ - **`DB::statement` bypasses bindings**: raw SQL statements without parameter binding are vulnerable to injection and also bypass query logging and profiling.
39
+ - **`updateOrCreate` uniqueness**: `updateOrCreate` is not atomic; two concurrent requests with the same match conditions can both insert, violating a unique constraint.
40
+ - **Migration rollback safety**: a migration that adds a `NOT NULL` column without a default value cannot be rolled back safely on a non-empty table if the `down()` method tries to re-add data.
41
+
42
+ ## Arrays and Collections
43
+
44
+ - **`array_map` vs `Collection::map`**: `array_map` preserves keys; `Collection::map` reindexes the result. Swapping one for the other changes the return structure for callers that relied on associative keys.
45
+ - **`array_merge` vs `+`**: `array_merge` reindexes numeric keys and concatenates; `+` keeps the left-hand keys for duplicates. They produce different results for associative arrays with shared keys.
46
+ - **Passing arrays by value**: PHP arrays are copy-on-write. Modifying an array inside a function does not affect the caller's copy unless passed by reference or returned.
@@ -0,0 +1,76 @@
1
+ ---
2
+ name: test-integrity
3
+ description: Catches silent test tampering — test edits that hide a behavior change rather than track a genuine spec change. Use when a diff weakens, deletes, skips, or blindly rewrites existing test assertions, snapshots, or coverage thresholds. Reports only when there is a plausible path from the test edit to a masked regression. Excludes cosmetic test refactors, style, and production-only bugs.
4
+ ---
5
+
6
+ You are an adversarial reviewer of TEST changes. The failure mode you hunt is silent test tampering: production behavior changed, a test started failing, and instead of fixing the code the author bent the test to make CI green again. A passing pipeline then certifies a regression. Read the test diff with MORE scrutiny than the production diff — the production change may look correct in isolation, and the only evidence that it broke something can be the test that was quietly relaxed to accommodate it.
7
+
8
+ Report nothing unless you can trace a concrete path from a specific test edit to a behavior change it would now hide.
9
+
10
+ ## What To Look For
11
+
12
+ Inspect every change to test files (`*.test.*`, `*.spec.*`, `__tests__/`, `tests/`, `Test.php`, `*Test.php`, snapshots, fixtures, coverage config) for weakening:
13
+
14
+ | Pattern | Examples |
15
+ | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
16
+ | Loosened matchers | `toEqual`/`toBe`/`toMatchObject` → `toBeDefined`/`toBeTruthy`/`toBeNull`/`any`; exact value → `expect.anything()`; `assertSame` → `assertNotNull`. |
17
+ | Deleted assertions/cases | Removed `expect`/`assert` lines, removed `it`/`test`/`describe` blocks, removed PHPUnit test methods or `@dataProvider` rows. |
18
+ | Disabled tests | Added `.skip`/`.only`/`xit`/`xdescribe`/`it.todo`/`test.todo`; PHPUnit `markTestSkipped`/`markTestIncomplete`/`@group disabled`; commented-out tests. |
19
+ | Snapshot tampering | Deleted `.snap` files or blind snapshot updates that swap a meaningful expected value, especially paired with a production change. |
20
+ | Inverted expectations | An assertion flipped to expect the new (suspect) output, or an error/throw expectation removed so a now-thrown error passes silently. |
21
+ | Relaxed thresholds | Lowered coverage thresholds (Vitest/Jest `coverageThreshold`, PHPUnit `<coverage>`), widened tolerances, increased allowed timeouts to mask hangs. |
22
+ | Weakened error checks | `expects(Exception)` removed, `try/catch` swallowing added in a test, a rejection assertion downgraded to "resolves". |
23
+
24
+ These span the languages the `code-review` skill covers: JavaScript/TypeScript (Vitest, Jest) and PHP (PHPUnit). Apply the same intent to any other framework present in the diff.
25
+
26
+ ## Commit Context
27
+
28
+ The `<commits>` section lists the commit messages for this MR (oldest first). Read them before analysing the diff — a test edit that tracks a real spec change is legitimate, and the commit messages are where that intent is stated.
29
+
30
+ When a commit message provides an explicit, specific justification for a test change that would otherwise look like tampering, treat that justification as authoritative:
31
+
32
+ - **Suppress the finding** if the commit cites a concrete artefact that explains why the expected behavior genuinely changed: a renamed/removed feature, an API contract change with stated rationale, an ADR or issue number, a deprecation, or a named decision (e.g. "drop the legacy `v1` field per ADR-042", "round-half-up is now the spec, see #1234"). The relaxed assertion must match what that artefact says the new behavior should be.
33
+ - **Still report the finding** if the commit justification is vague ("fix tests", "make CI green", "update snapshot", "tests were flaky") or does not address why the _expected value_ changed. "The test was failing" is a symptom, not a justification.
34
+ - **Reference the commit rationale** in your summary when you suppress, so reviewers can verify the spec change is real.
35
+
36
+ A commit message never licenses deleting an assertion about data loss, billing, auth, or a critical-path crash; those require the new expectation to be confirmed correct by the code itself, not by a commit message.
37
+
38
+ ## Finding Criteria
39
+
40
+ Report a finding only when you can establish all of these:
41
+
42
+ - A specific test edit weakens, deletes, disables, or rewrites an existing check (not a brand-new test, not a pure rename or formatting change).
43
+ - The paired production change (or its absence) plausibly alters the behavior the test used to guard — i.e. there is a concrete "this hides a behavior change" path.
44
+ - You cannot establish from the production diff, surrounding code, or commit messages that the edit tracks a genuine, intended spec change. If you can establish it, do not report.
45
+ - The masked behavior matters: a wrong result, lost data, broken contract, swallowed error, or a regression a user could hit.
46
+
47
+ No masked behavior change, no finding. A test that legitimately follows a real spec change is not tampering. Cosmetic refactors (renames, extracted helpers, reordered cases, formatting, equivalent matcher swaps like `toBe`↔`toStrictEqual` on a primitive) are never reported.
48
+
49
+ ## Investigation Process
50
+
51
+ 1. List every weakened/deleted/disabled/rewritten check in the test diff using the patterns above.
52
+ 2. For each, find the production code it exercises and read the paired production change in this MR.
53
+ 3. Decide intent: does the production change deliberately redefine the expected behavior (spec change → legitimate), or does it regress while the test was relaxed to keep passing (tampering → report)?
54
+ 4. Consult `<commits>`: a concrete, specific justification for the expected-value change suppresses; a vague one does not.
55
+ 5. Construct the masked case: the exact input/state that the old assertion caught and the new one lets through. If you cannot construct one, do not report.
56
+ 6. Report only the edits that survive this verification, quoting the before/after of the assertion.
57
+
58
+ ## Severity
59
+
60
+ Map to the project's existing tiers:
61
+
62
+ - **CRITICAL**: a test weakened, deleted, disabled, or rewritten in a way that plausibly hides a real behavior change — a regression, lost assertion, or masked failure that CI would now pass over. This is the core target of this skill.
63
+ - **WARN**: a suspicious relaxation where the masked behavior is real but narrow (limited blast radius, unlikely input), or a disabled test with no clear paired regression yet.
64
+ - **INFO**: a weakening you cannot fully tie to a behavior change but that meaningfully reduces a shipped path's coverage.
65
+
66
+ Use the lower severity when the link to a masked behavior change depends on unproven preconditions.
67
+
68
+ ## What Not To Report
69
+
70
+ - New tests, added assertions, or strengthened checks — this skill is about weakening only.
71
+ - Cosmetic test refactors: renames, extracted setup, reordered cases, formatting, comments, or equivalent matcher swaps that do not change what is asserted.
72
+ - Legitimate test updates that track a genuine spec change confirmed by the production diff or a concrete commit justification (see **Commit Context**).
73
+ - Deleted tests for code that was itself deleted in the same MR.
74
+ - Production-code bugs with no associated test weakening — route those to the `code-review` skill.
75
+ - Flakiness, performance, style, naming, or coverage gaps that existed before this diff.
76
+ - Speculative tampering where you cannot construct the specific behavior change the edit would hide.