@rigour-labs/core 6.6.5 → 6.7.0

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 (154) hide show
  1. package/README.md +16 -92
  2. package/dist/environment.test.js +0 -4
  3. package/dist/gates/agent-team.d.ts +0 -2
  4. package/dist/gates/agent-team.js +0 -8
  5. package/dist/gates/checkpoint.d.ts +0 -1
  6. package/dist/gates/checkpoint.js +0 -1
  7. package/dist/gates/context.d.ts +0 -4
  8. package/dist/gates/context.js +7 -17
  9. package/dist/gates/deep-analysis.d.ts +0 -1
  10. package/dist/gates/deep-runner.js +0 -1
  11. package/dist/gates/frontend-secret-exposure.d.ts +1 -1
  12. package/dist/gates/frontend-secret-exposure.js +9 -3
  13. package/dist/gates/frontend-secret-exposure.test.js +6 -3
  14. package/dist/gates/hallucinated-imports/index.d.ts +0 -2
  15. package/dist/gates/hallucinated-imports/index.js +2 -2
  16. package/dist/gates/hallucinated-imports/js-resolver.js +1 -0
  17. package/dist/gates/hallucinated-imports.test.js +10 -0
  18. package/dist/gates/runner.js +3 -4
  19. package/dist/gates/test-quality-matchers.d.ts +0 -4
  20. package/dist/gates/test-quality-matchers.js +0 -7
  21. package/dist/gates/test-quality.d.ts +0 -2
  22. package/dist/gates/test-quality.js +1 -10
  23. package/dist/hooks/checker.js +1 -1
  24. package/dist/hooks/index.d.ts +2 -3
  25. package/dist/hooks/index.js +1 -2
  26. package/dist/hooks/stop-review.d.ts +6 -9
  27. package/dist/hooks/stop-review.js +5 -10
  28. package/dist/hooks/types.d.ts +0 -14
  29. package/dist/hooks/types.js +1 -15
  30. package/dist/index.d.ts +11 -3
  31. package/dist/index.js +11 -3
  32. package/dist/inference/cloud-provider.d.ts +2 -0
  33. package/dist/inference/cloud-provider.js +16 -6
  34. package/dist/inference/cloud-provider.test.js +21 -0
  35. package/dist/pattern-index/embeddings.d.ts +0 -5
  36. package/dist/pattern-index/embeddings.js +11 -4
  37. package/dist/pattern-index/semantic-runtime.d.ts +10 -0
  38. package/dist/pattern-index/semantic-runtime.js +46 -0
  39. package/dist/pattern-index/semantic-runtime.test.d.ts +1 -0
  40. package/dist/pattern-index/semantic-runtime.test.js +31 -0
  41. package/dist/review/backtest-init.d.ts +8 -0
  42. package/dist/review/backtest-init.js +87 -0
  43. package/dist/review/backtest-init.test.d.ts +1 -0
  44. package/dist/review/backtest-init.test.js +74 -0
  45. package/dist/review/backtest-judges.d.ts +38 -0
  46. package/dist/review/backtest-judges.js +63 -0
  47. package/dist/review/backtest-judges.test.d.ts +1 -0
  48. package/dist/review/backtest-judges.test.js +32 -0
  49. package/dist/review/backtest.d.ts +320 -0
  50. package/dist/review/backtest.js +207 -0
  51. package/dist/review/backtest.test.d.ts +1 -0
  52. package/dist/review/backtest.test.js +129 -0
  53. package/dist/review/baseline.js +2 -1
  54. package/dist/review/dead-code.test.js +15 -0
  55. package/dist/review/fix-scope.d.ts +21 -0
  56. package/dist/review/fix-scope.js +94 -0
  57. package/dist/review/fix-scope.test.d.ts +1 -0
  58. package/dist/review/fix-scope.test.js +104 -0
  59. package/dist/review/lint-overlay.d.ts +21 -0
  60. package/dist/review/lint-overlay.js +89 -0
  61. package/dist/review/orphan-files.js +3 -2
  62. package/dist/review/partial-wiring.js +5 -3
  63. package/dist/review/query-patterns.js +3 -2
  64. package/dist/review/quiet.d.ts +6 -0
  65. package/dist/review/quiet.js +12 -2
  66. package/dist/review/quiet.test.js +11 -0
  67. package/dist/review/review.d.ts +6 -0
  68. package/dist/review/review.js +10 -2
  69. package/dist/review/reviewer/adapters.d.ts +51 -0
  70. package/dist/review/reviewer/adapters.js +181 -0
  71. package/dist/review/reviewer/adapters.test.d.ts +1 -0
  72. package/dist/review/reviewer/adapters.test.js +23 -0
  73. package/dist/review/reviewer/background.d.ts +41 -0
  74. package/dist/review/reviewer/background.js +153 -0
  75. package/dist/review/reviewer/background.test.d.ts +1 -0
  76. package/dist/review/reviewer/background.test.js +118 -0
  77. package/dist/review/reviewer/consensus.d.ts +36 -0
  78. package/dist/review/reviewer/consensus.js +144 -0
  79. package/dist/review/reviewer/context.d.ts +75 -0
  80. package/dist/review/reviewer/context.js +140 -0
  81. package/dist/review/reviewer/context.test.d.ts +1 -0
  82. package/dist/review/reviewer/context.test.js +18 -0
  83. package/dist/review/reviewer/exec.d.ts +17 -0
  84. package/dist/review/reviewer/exec.js +27 -0
  85. package/dist/review/reviewer/exec.test.d.ts +1 -0
  86. package/dist/review/reviewer/exec.test.js +23 -0
  87. package/dist/review/reviewer/inputs.d.ts +39 -0
  88. package/dist/review/reviewer/inputs.js +107 -0
  89. package/dist/review/reviewer/merge-impact.d.ts +2 -0
  90. package/dist/review/reviewer/merge-impact.js +64 -0
  91. package/dist/review/reviewer/panel.d.ts +67 -0
  92. package/dist/review/reviewer/panel.js +101 -0
  93. package/dist/review/reviewer/panel.test.d.ts +1 -0
  94. package/dist/review/reviewer/panel.test.js +110 -0
  95. package/dist/review/reviewer/prompt.d.ts +32 -0
  96. package/dist/review/reviewer/prompt.js +145 -0
  97. package/dist/review/reviewer/settings.d.ts +58 -0
  98. package/dist/review/reviewer/settings.js +152 -0
  99. package/dist/review/reviewer/settings.test.d.ts +1 -0
  100. package/dist/review/reviewer/settings.test.js +57 -0
  101. package/dist/review/reviewer/store.d.ts +49 -0
  102. package/dist/review/reviewer/store.js +99 -0
  103. package/dist/review/reviewer/usage.d.ts +2 -0
  104. package/dist/review/reviewer/usage.js +28 -0
  105. package/dist/review/reviewer/usage.test.d.ts +1 -0
  106. package/dist/review/reviewer/usage.test.js +14 -0
  107. package/dist/review/reviewer/verdict.d.ts +141 -0
  108. package/dist/review/reviewer/verdict.js +166 -0
  109. package/dist/review/reviewer.d.ts +84 -49
  110. package/dist/review/reviewer.js +364 -195
  111. package/dist/review/reviewer.test.js +316 -50
  112. package/dist/review/test-files.d.ts +5 -1
  113. package/dist/review/test-files.js +6 -2
  114. package/dist/review/toolchain.d.ts +5 -2
  115. package/dist/review/toolchain.js +89 -7
  116. package/dist/review/toolchain.test.js +45 -4
  117. package/dist/review/typed/program.d.ts +26 -0
  118. package/dist/review/typed/program.js +90 -0
  119. package/dist/review/typed/redundancy.d.ts +10 -0
  120. package/dist/review/typed/redundancy.js +378 -0
  121. package/dist/review/typed/redundancy.test.d.ts +1 -0
  122. package/dist/review/typed/redundancy.test.js +179 -0
  123. package/dist/review/typed/schema-nullability.d.ts +6 -0
  124. package/dist/review/typed/schema-nullability.js +218 -0
  125. package/dist/review/typed/schema-nullability.test.d.ts +1 -0
  126. package/dist/review/typed/schema-nullability.test.js +63 -0
  127. package/dist/review/unused-exports.js +10 -7
  128. package/dist/semantic/learn/types.d.ts +2 -2
  129. package/dist/services/adaptive-thresholds.d.ts +2 -52
  130. package/dist/services/adaptive-thresholds.js +2 -147
  131. package/dist/services/adaptive-thresholds.test.js +1 -81
  132. package/dist/settings.d.ts +14 -6
  133. package/dist/settings.js +0 -12
  134. package/dist/storage/team-diagnostics.js +1 -1
  135. package/dist/storage/team-schema.d.ts +1 -1
  136. package/dist/storage/team-schema.js +2 -0
  137. package/dist/storage/team-store.js +26 -3
  138. package/dist/storage/team-sync-scope.test.js +35 -2
  139. package/dist/telemetry/telemetry.d.ts +5 -0
  140. package/dist/telemetry/telemetry.js +11 -1
  141. package/dist/templates/paradigms.js +0 -8
  142. package/dist/templates/presets.d.ts +0 -1
  143. package/dist/templates/presets.js +0 -23
  144. package/dist/templates/universal-config.js +16 -46
  145. package/dist/types/index.d.ts +202 -574
  146. package/dist/types/index.js +64 -67
  147. package/dist/types/index.test.d.ts +1 -0
  148. package/dist/types/index.test.js +33 -0
  149. package/dist/utils/command-line.d.ts +8 -0
  150. package/dist/utils/command-line.js +8 -0
  151. package/dist/utils/command-line.test.d.ts +1 -0
  152. package/dist/utils/command-line.test.js +8 -0
  153. package/dist/utils/profile.js +1 -1
  154. package/package.json +6 -7
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Which table columns are NOT NULL, replayed from SQL migrations in file order, so a row type that
3
+ * says `| null` for one can be called out: every guard on it is dead. The migrations may live in
4
+ * another repository (`gates.redundancy.schema_migrations`), read only.
5
+ *
6
+ * Conservative by construction: a column is NOT NULL only when a statement Rigour understands made
7
+ * it so (a column definition, a primary key, `SET NOT NULL`). A table created any other way
8
+ * (`AS SELECT`, `LIKE`, `PARTITION OF`) or altered in a way not followed here is unknown, and an
9
+ * unknown table or column is never reported.
10
+ */
11
+ import fs from 'fs';
12
+ import os from 'os';
13
+ import path from 'path';
14
+ const DEFAULT_SCHEMA = 'public';
15
+ /** The migrations in `dirs` (relative to the repository, absolute, or `~/`), replayed in file-name order. Missing folders are skipped. */
16
+ export function loadSchemaNullability(root, dirs) {
17
+ const files = [];
18
+ for (const dir of dirs) {
19
+ const full = dir.startsWith('~/') ? path.join(os.homedir(), dir.slice(2)) : path.resolve(root, dir);
20
+ let names;
21
+ try {
22
+ names = fs.readdirSync(full).filter(name => name.endsWith('.sql'));
23
+ }
24
+ catch {
25
+ continue;
26
+ }
27
+ files.push(...names.map(name => path.join(full, name)));
28
+ }
29
+ files.sort((a, b) => path.basename(a).localeCompare(path.basename(b)));
30
+ const schema = new Map();
31
+ for (const file of files)
32
+ applyMigration(schema, fs.readFileSync(file, 'utf8'));
33
+ return schema;
34
+ }
35
+ /** Applies one migration's statements to the schema in place. */
36
+ function applyMigration(schema, sql) {
37
+ for (const statement of statements(sql)) {
38
+ const create = /^create\s+(?:(?:global|local)\s+)?(?:(?:temp|temporary|unlogged)\s+)?table\s+(?:if\s+not\s+exists\s+)?([\w."]+)\s*([\s\S]*)$/i.exec(statement);
39
+ if (create) {
40
+ createTable(schema, tableKey(create[1]), create[2]);
41
+ continue;
42
+ }
43
+ const alter = /^alter\s+table\s+(?:if\s+exists\s+)?(?:only\s+)?([\w."]+)\s+([\s\S]*)$/i.exec(statement);
44
+ if (alter) {
45
+ alterTable(schema, tableKey(alter[1]), alter[2]);
46
+ continue;
47
+ }
48
+ const drop = /^drop\s+table\s+(?:if\s+exists\s+)?([\w.",\s]+?)(?:\s+(?:cascade|restrict))?$/i.exec(statement);
49
+ if (drop)
50
+ for (const name of splitTopLevel(drop[1]))
51
+ schema.delete(tableKey(name));
52
+ }
53
+ }
54
+ function createTable(schema, key, rest) {
55
+ const body = /^\(([\s\S]*)\)/.exec(rest.trim());
56
+ if (!body || /^\(\s*like\s/i.test(rest.trim()) || /partition\s+of/i.test(rest)) {
57
+ schema.set(key, null);
58
+ return;
59
+ }
60
+ const columns = new Map();
61
+ for (const part of splitTopLevel(matchingParen(rest.trim()))) {
62
+ const primary = /^(?:constraint\s+[\w"]+\s+)?primary\s+key\s*\(([^)]*)\)/i.exec(part);
63
+ if (primary) {
64
+ for (const col of primary[1].split(','))
65
+ columns.set(ident(col), true);
66
+ continue;
67
+ }
68
+ if (/^(?:constraint|unique|check|foreign\s+key|exclude|like)\b/i.test(part))
69
+ continue;
70
+ const column = columnDefinition(part);
71
+ if (column)
72
+ columns.set(column.name, column.notNull);
73
+ }
74
+ schema.set(key, columns);
75
+ }
76
+ function alterTable(schema, key, actions) {
77
+ const rename = /^rename\s+to\s+([\w."]+)$/i.exec(actions.trim());
78
+ if (rename) {
79
+ const table = schema.get(key);
80
+ schema.delete(key);
81
+ schema.set(`${key.split('.')[0]}.${ident(rename[1].split('.').pop())}`, table ?? null);
82
+ return;
83
+ }
84
+ const columns = schema.get(key);
85
+ if (!columns)
86
+ return; // unknown table: stays unknown
87
+ for (const action of splitTopLevel(actions)) {
88
+ const add = /^add\s+(?:column\s+)?(?:if\s+not\s+exists\s+)?([\s\S]+)$/i.exec(action);
89
+ if (add && !/^(?:constraint|primary|unique|check|foreign)\b/i.test(add[1])) {
90
+ const column = columnDefinition(add[1]);
91
+ if (column)
92
+ columns.set(column.name, column.notNull);
93
+ continue;
94
+ }
95
+ const primary = /^add\s+(?:constraint\s+[\w"]+\s+)?primary\s+key\s*\(([^)]*)\)/i.exec(action);
96
+ if (primary) {
97
+ for (const col of primary[1].split(','))
98
+ columns.set(ident(col), true);
99
+ continue;
100
+ }
101
+ const nullability = /^alter\s+(?:column\s+)?([\w"]+)\s+(set|drop)\s+not\s+null$/i.exec(action);
102
+ if (nullability) {
103
+ columns.set(ident(nullability[1]), nullability[2].toLowerCase() === 'set');
104
+ continue;
105
+ }
106
+ const dropColumn = /^drop\s+(?:column\s+)?(?:if\s+exists\s+)?([\w"]+)/i.exec(action);
107
+ if (dropColumn && !/^drop\s+constraint\b/i.test(action)) {
108
+ columns.delete(ident(dropColumn[1]));
109
+ continue;
110
+ }
111
+ const renameColumn = /^rename\s+(?:column\s+)?([\w"]+)\s+to\s+([\w"]+)$/i.exec(action);
112
+ if (renameColumn && columns.has(ident(renameColumn[1]))) {
113
+ columns.set(ident(renameColumn[2]), columns.get(ident(renameColumn[1])));
114
+ columns.delete(ident(renameColumn[1]));
115
+ }
116
+ }
117
+ }
118
+ function columnDefinition(text) {
119
+ const match = /^("(?:[^"]|"")+"|[a-z_][\w$]*)\s+([\s\S]+)$/i.exec(text.trim());
120
+ if (!match)
121
+ return undefined;
122
+ // Strings and parenthesised expressions (a CHECK, a default, a generated column) say nothing about this column's own constraint.
123
+ let rest = match[2].replace(/'(?:[^']|'')*'/g, "''");
124
+ while (/\([^()]*\)/.test(rest))
125
+ rest = rest.replace(/\([^()]*\)/g, '');
126
+ return { name: ident(match[1]), notNull: /\bnot\s+null\b|\bprimary\s+key\b/i.test(rest) };
127
+ }
128
+ /** `schema.table` with Postgres's folding: unquoted names lower-cased, the schema `public` by default. */
129
+ export function tableKey(name) {
130
+ const parts = name.trim().match(/"(?:[^"]|"")+"|[^.]+/g) ?? [];
131
+ const table = parts.at(-1) ?? '';
132
+ const schema = (parts.length > 1 && parts[0]) || DEFAULT_SCHEMA;
133
+ return `${ident(schema)}.${ident(table)}`;
134
+ }
135
+ function ident(name) {
136
+ const trimmed = name.trim();
137
+ return trimmed.startsWith('"') ? trimmed.slice(1, -1).replace(/""/g, '"') : trimmed.toLowerCase();
138
+ }
139
+ /** The text inside the parenthesis that opens `text`. */
140
+ function matchingParen(text) {
141
+ let depth = 0;
142
+ for (let i = 0; i < text.length; i++) {
143
+ if (text[i] === '(')
144
+ depth++;
145
+ else if (text[i] === ')' && --depth === 0)
146
+ return text.slice(1, i);
147
+ }
148
+ return text.slice(1);
149
+ }
150
+ /** Splits on commas outside parentheses and quotes. */
151
+ function splitTopLevel(text) {
152
+ const parts = [];
153
+ let depth = 0;
154
+ let quote = '';
155
+ let start = 0;
156
+ for (let i = 0; i < text.length; i++) {
157
+ const c = text[i];
158
+ if (quote) {
159
+ if (c === quote)
160
+ quote = '';
161
+ }
162
+ else if (c === "'" || c === '"')
163
+ quote = c;
164
+ else if (c === '(')
165
+ depth++;
166
+ else if (c === ')')
167
+ depth--;
168
+ else if (c === ',' && depth === 0) {
169
+ parts.push(text.slice(start, i).trim());
170
+ start = i + 1;
171
+ }
172
+ }
173
+ parts.push(text.slice(start).trim());
174
+ return parts.filter(Boolean);
175
+ }
176
+ /** The statements of a migration, comments removed, function bodies and strings kept whole. */
177
+ function statements(sql) {
178
+ const out = [];
179
+ let current = '';
180
+ let i = 0;
181
+ while (i < sql.length) {
182
+ const rest = sql.slice(i);
183
+ const dollar = /^\$[\w]*\$/.exec(rest);
184
+ if (rest.startsWith('--')) {
185
+ const end = sql.indexOf('\n', i);
186
+ i = end === -1 ? sql.length : end;
187
+ }
188
+ else if (rest.startsWith('/*')) {
189
+ const end = sql.indexOf('*/', i + 2);
190
+ i = end === -1 ? sql.length : end + 2;
191
+ }
192
+ else if (dollar) {
193
+ const end = sql.indexOf(dollar[0], i + dollar[0].length);
194
+ const stop = end === -1 ? sql.length : end + dollar[0].length;
195
+ current += sql.slice(i, stop);
196
+ i = stop;
197
+ }
198
+ else if (sql[i] === "'" || sql[i] === '"') {
199
+ let j = i + 1;
200
+ while (j < sql.length && !(sql[j] === sql[i] && sql[j + 1] !== sql[i]))
201
+ j += sql[j] === sql[i] ? 2 : 1;
202
+ current += sql.slice(i, j + 1);
203
+ i = j + 1;
204
+ }
205
+ else if (sql[i] === ';') {
206
+ out.push(current.trim().replace(/\s+/g, ' '));
207
+ current = '';
208
+ i++;
209
+ }
210
+ else {
211
+ current += sql[i];
212
+ i++;
213
+ }
214
+ }
215
+ if (current.trim())
216
+ out.push(current.trim().replace(/\s+/g, ' '));
217
+ return out.filter(Boolean);
218
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,63 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { describe, expect, it } from 'vitest';
5
+ import { loadSchemaNullability, tableKey } from './schema-nullability.js';
6
+ /** The migrations as numbered files in a folder of their own, replayed. */
7
+ const replay = (...migrations) => {
8
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'migrations-'));
9
+ try {
10
+ migrations.forEach((sql, i) => fs.writeFileSync(path.join(dir, `${String(i).padStart(3, '0')}_step.sql`), sql));
11
+ return loadSchemaNullability(dir, ['.']);
12
+ }
13
+ finally {
14
+ fs.rmSync(dir, { recursive: true, force: true });
15
+ }
16
+ };
17
+ const columns = (schema, table) => Object.fromEntries(schema.get(tableKey(table)) ?? []);
18
+ describe('NOT NULL columns from migrations', () => {
19
+ it('reads column definitions, primary keys and quoted names, ignoring strings, comments and CHECK text', () => {
20
+ const schema = replay(`
21
+ -- a comment; with a semicolon
22
+ create table if not exists public.study_set (
23
+ id uuid primary key default gen_random_uuid(),
24
+ "OwnerId" uuid not null references auth.users(id),
25
+ title text default 'not null' ,
26
+ archived_at timestamptz check (archived_at is not null or title is null),
27
+ kind text NOT NULL,
28
+ /* block; comment */
29
+ constraint study_set_title_unique unique (title)
30
+ );`);
31
+ expect(columns(schema, 'study_set')).toEqual({ id: true, OwnerId: true, title: false, archived_at: false, kind: true });
32
+ });
33
+ it('follows ALTER TABLE in file order: add, set and drop NOT NULL, rename, drop column, rename table', () => {
34
+ const schema = replay('create table app.card (id bigint, set_id bigint, body text);', `alter table only app.card add column if not exists due_at timestamptz not null default now(),
35
+ alter column set_id set not null, add constraint card_pk primary key (id);`, 'alter table app.card alter column body set not null; alter table app.card alter column body drop not null;', 'alter table app.card rename column due_at to next_due_at; alter table app.card drop column if exists body;', 'alter table app.card rename to flashcard;');
36
+ expect(schema.has(tableKey('app.card'))).toBe(false);
37
+ expect(columns(schema, 'app.flashcard')).toEqual({ id: true, set_id: true, next_due_at: true });
38
+ });
39
+ it('keeps a function body whole and treats tables it cannot read as unknown', () => {
40
+ const schema = replay(`create function touch() returns trigger language plpgsql as $$ begin new.updated_at = now(); return new; end; $$;
41
+ create table copy as select * from other;
42
+ create table part partition of parent for values in (1);
43
+ create table gone (id int not null); drop table if exists gone cascade;`);
44
+ expect(schema.get(tableKey('copy'))).toBeNull();
45
+ expect(schema.get(tableKey('part'))).toBeNull();
46
+ expect(schema.has(tableKey('gone'))).toBe(false);
47
+ });
48
+ it('loads every folder named, in file-name order across them, skipping one that does not exist', () => {
49
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'schema-'));
50
+ try {
51
+ fs.mkdirSync(path.join(root, 'a'));
52
+ fs.mkdirSync(path.join(root, 'b'));
53
+ fs.writeFileSync(path.join(root, 'b', '20240101_create.sql'), 'create table t (id int, note text);');
54
+ fs.writeFileSync(path.join(root, 'a', '20240202_tighten.sql'), 'alter table t alter column note set not null;');
55
+ fs.writeFileSync(path.join(root, 'a', 'README.md'), 'not sql');
56
+ const schema = loadSchemaNullability(root, ['a', path.join(root, 'b'), 'missing']);
57
+ expect(columns(schema, 't')).toEqual({ id: false, note: true });
58
+ }
59
+ finally {
60
+ fs.rmSync(root, { recursive: true, force: true });
61
+ }
62
+ });
63
+ });
@@ -5,16 +5,19 @@
5
5
  * dead exports pile up in agent-written code. Only exports on added lines are checked. Another
6
6
  * file uses an export when it names the symbol as a whole word AND points at the module: an import,
7
7
  * re-export, dynamic import or mock whose specifier ends in the module's name (its folder's, for an
8
- * index file). A same-named word elsewhere, such as an unrelated route parameter, is not a use. What a framework calls by convention
9
- * (SvelteKit and Next.js route modules, hooks, serverless functions) is never reported.
8
+ * index file). A same-named word elsewhere, such as an unrelated route parameter, is not a use, and
9
+ * neither is a test: an export only its tests import is dead in production, and exporting a thing
10
+ * for its test is the habit that keeps it so. What a framework calls by convention (SvelteKit and
11
+ * Next.js route modules, hooks, serverless functions) is never reported.
10
12
  */
11
13
  import { spawnSync } from 'child_process';
12
14
  import fs from 'fs';
13
15
  import path from 'path';
14
16
  import { emitsDeclarations, isPackageEntry } from './package-layout.js';
17
+ import { isTestFile } from './test-files.js';
15
18
  const GIT_TIMEOUT_MS = 10_000;
16
19
  const CODE = /\.(ts|tsx|js|jsx|mjs|svelte)$/;
17
- const SKIPPED = /\.(test|spec)\.|\.d\.ts$|(^|\/)(dist|build|out|coverage|\.next|[\w-]+-dist)\//;
20
+ const SKIPPED = /\.d\.ts$|(^|\/)(dist|build|out|coverage|\.next|[\w-]+-dist)\//;
18
21
  /** What a route or hook module exports for its framework, not for an importer. */
19
22
  const FRAMEWORK_EXPORTS = new Set([
20
23
  'load', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD', 'fallback', 'actions',
@@ -59,7 +62,7 @@ function addedExports(diff) {
59
62
  if (!file || text.startsWith('-'))
60
63
  continue;
61
64
  if (text.startsWith('+')) {
62
- if (CODE.test(file) && !SKIPPED.test(file))
65
+ if (CODE.test(file) && !SKIPPED.test(file) && !isTestFile(file))
63
66
  found.push(...exportsOn(text.slice(1), file, line));
64
67
  line++;
65
68
  }
@@ -109,7 +112,7 @@ export function unusedExportFailures(cwd, diff, config) {
109
112
  return []; // git could not answer: say nothing rather than guess
110
113
  const points = pointsAtCache(cwd);
111
114
  return candidates
112
- .filter(exp => ![...(users.get(exp.name) ?? [])].some(user => !isOwnFile(user, exp) && points(user, exp.file)))
115
+ .filter(exp => ![...(users.get(exp.name) ?? [])].some(user => !isOwnFile(user, exp) && !isTestFile(user) && points(user, exp.file)))
113
116
  .map(unused);
114
117
  }
115
118
  /**
@@ -178,8 +181,8 @@ function filesNaming(cwd, names, excluded) {
178
181
  function unused(exp) {
179
182
  return {
180
183
  id: 'unused-export',
181
- title: 'Unused export',
182
- details: `\`${exp.name}\` is exported but no other file uses it. An export nothing imports is dead code that readers and agents treat as part of the module's contract.`,
184
+ title: `Unused export \`${exp.name}\``,
185
+ details: `\`${exp.name}\` is exported but no other file uses it (a test is not a consumer). An export nothing imports is dead code that readers and agents treat as part of the module's contract.`,
183
186
  severity: 'medium',
184
187
  provenance: 'traditional',
185
188
  files: [exp.file],
@@ -384,7 +384,6 @@ export declare const LearnedRuleSchema: z.ZodObject<{
384
384
  maxHits: number;
385
385
  repoHits: string[];
386
386
  };
387
- version: 1;
388
387
  id: string;
389
388
  severity: "high" | "medium" | "low";
390
389
  source: {
@@ -392,6 +391,7 @@ export declare const LearnedRuleSchema: z.ZodObject<{
392
391
  file: string;
393
392
  commit?: string | undefined;
394
393
  };
394
+ version: 1;
395
395
  pattern: {
396
396
  template: "require-option";
397
397
  callee: {
@@ -427,7 +427,6 @@ export declare const LearnedRuleSchema: z.ZodObject<{
427
427
  maxHits: number;
428
428
  repoHits: string[];
429
429
  };
430
- version: 1;
431
430
  id: string;
432
431
  severity: "high" | "medium" | "low";
433
432
  source: {
@@ -435,6 +434,7 @@ export declare const LearnedRuleSchema: z.ZodObject<{
435
434
  file: string;
436
435
  commit?: string | undefined;
437
436
  };
437
+ version: 1;
438
438
  pattern: {
439
439
  template: "require-option";
440
440
  callee: {
@@ -1,45 +1,8 @@
1
1
  /**
2
- * Adaptive Thresholds Service (v2)
3
- *
4
- * Dynamically adjusts quality gate thresholds based on:
5
- * - Project maturity (age, commit count, file count)
6
- * - Historical failure rates with Z-score anomaly detection
7
- * - Complexity tier (hobby/startup/enterprise)
8
- * - Per-provenance trend analysis (ai-drift, structural, security separate)
9
- *
10
- * v2 upgrades:
11
- * - Z-score replaces naive delta comparison for trend detection
12
- * - Per-provenance failure tracking (AI drift vs structural vs security)
13
- * - Statistical anomaly detection normalizes across project sizes
14
- *
2
+ * Quality history: every gate run's failures by provenance, kept in .rigour/adaptive-history.json, and the
3
+ * trends read from it (Z-score of recent runs against the earlier baseline), overall and per provenance.
15
4
  */
16
- export type ComplexityTier = 'hobby' | 'startup' | 'enterprise';
17
5
  export type QualityTrend = 'improving' | 'stable' | 'degrading';
18
- export interface ProjectMetrics {
19
- fileCount: number;
20
- commitCount?: number;
21
- ageInDays?: number;
22
- testCoverage?: number;
23
- recentFailureRate?: number;
24
- }
25
- export interface AdaptiveConfig {
26
- enabled?: boolean;
27
- base_coverage_threshold?: number;
28
- base_quality_threshold?: number;
29
- auto_detect_tier?: boolean;
30
- forced_tier?: ComplexityTier;
31
- }
32
- export interface ThresholdAdjustments {
33
- tier: ComplexityTier;
34
- trend: QualityTrend;
35
- coverageThreshold: number;
36
- qualityThreshold: number;
37
- securityBlockLevel: 'critical' | 'high' | 'medium' | 'low';
38
- leniencyFactor: number;
39
- reasoning: string[];
40
- /** Per-provenance trend breakdown (new in v5) */
41
- provenanceTrends?: ProvenanceTrends;
42
- }
43
6
  export interface ProvenanceRunData {
44
7
  aiDriftFailures: number;
45
8
  structuralFailures: number;
@@ -88,20 +51,7 @@ export declare function getQualityTrend(cwd: string): QualityTrend;
88
51
  * Falls back gracefully for legacy history data without provenance.
89
52
  */
90
53
  export declare function getProvenanceTrends(cwd: string): ProvenanceTrends;
91
- /**
92
- * Detect project complexity tier based on metrics
93
- */
94
- export declare function detectComplexityTier(metrics: ProjectMetrics): ComplexityTier;
95
- /**
96
- * Calculate adaptive thresholds based on project state.
97
- * v5: Uses Z-score trending and per-provenance analysis.
98
- */
99
- export declare function calculateAdaptiveThresholds(cwd: string, metrics: ProjectMetrics, config?: AdaptiveConfig): ThresholdAdjustments;
100
54
  /**
101
55
  * Clear adaptive history (for testing)
102
56
  */
103
57
  export declare function clearAdaptiveHistory(cwd: string): void;
104
- /**
105
- * Get summary of adaptive thresholds for logging
106
- */
107
- export declare function getAdaptiveSummary(adjustments: ThresholdAdjustments): string;
@@ -1,17 +1,6 @@
1
1
  /**
2
- * Adaptive Thresholds Service (v2)
3
- *
4
- * Dynamically adjusts quality gate thresholds based on:
5
- * - Project maturity (age, commit count, file count)
6
- * - Historical failure rates with Z-score anomaly detection
7
- * - Complexity tier (hobby/startup/enterprise)
8
- * - Per-provenance trend analysis (ai-drift, structural, security separate)
9
- *
10
- * v2 upgrades:
11
- * - Z-score replaces naive delta comparison for trend detection
12
- * - Per-provenance failure tracking (AI drift vs structural vs security)
13
- * - Statistical anomaly detection normalizes across project sizes
14
- *
2
+ * Quality history: every gate run's failures by provenance, kept in .rigour/adaptive-history.json, and the
3
+ * trends read from it (Z-score of recent runs against the earlier baseline), overall and per provenance.
15
4
  */
16
5
  import * as fs from 'fs';
17
6
  import * as path from 'path';
@@ -182,123 +171,6 @@ export function getProvenanceTrends(cwd) {
182
171
  securityZScore: security.z,
183
172
  };
184
173
  }
185
- /**
186
- * Detect project complexity tier based on metrics
187
- */
188
- export function detectComplexityTier(metrics) {
189
- // Enterprise: Large teams, many files, mature codebase
190
- if (metrics.fileCount > 500 || (metrics.commitCount && metrics.commitCount > 1000)) {
191
- return 'enterprise';
192
- }
193
- // Startup: Growing codebase, active development
194
- if (metrics.fileCount > 50 || (metrics.commitCount && metrics.commitCount > 100)) {
195
- return 'startup';
196
- }
197
- // Hobby: Small projects, early stage
198
- return 'hobby';
199
- }
200
- /**
201
- * Calculate adaptive thresholds based on project state.
202
- * v5: Uses Z-score trending and per-provenance analysis.
203
- */
204
- export function calculateAdaptiveThresholds(cwd, metrics, config = {}) {
205
- const reasoning = [];
206
- // Determine tier
207
- const tier = config.forced_tier ??
208
- (config.auto_detect_tier !== false ? detectComplexityTier(metrics) : 'startup');
209
- reasoning.push(`Complexity tier: ${tier} (files: ${metrics.fileCount})`);
210
- // Get overall trend (Z-score based)
211
- const trend = getQualityTrend(cwd);
212
- reasoning.push(`Quality trend: ${trend} (Z-score analysis)`);
213
- // Get per-provenance trends
214
- const provenanceTrends = getProvenanceTrends(cwd);
215
- if (provenanceTrends.aiDrift !== 'stable') {
216
- reasoning.push(`AI drift trend: ${provenanceTrends.aiDrift} (Z=${provenanceTrends.aiDriftZScore})`);
217
- }
218
- if (provenanceTrends.structural !== 'stable') {
219
- reasoning.push(`Structural trend: ${provenanceTrends.structural} (Z=${provenanceTrends.structuralZScore})`);
220
- }
221
- if (provenanceTrends.security !== 'stable') {
222
- reasoning.push(`Security trend: ${provenanceTrends.security} (Z=${provenanceTrends.securityZScore})`);
223
- }
224
- // Base thresholds
225
- let coverageThreshold = config.base_coverage_threshold ?? 80;
226
- let qualityThreshold = config.base_quality_threshold ?? 80;
227
- let securityBlockLevel = 'high';
228
- let leniencyFactor = 0.5;
229
- // Adjust by tier
230
- switch (tier) {
231
- case 'hobby':
232
- coverageThreshold = Math.max(50, coverageThreshold - 30);
233
- qualityThreshold = Math.max(60, qualityThreshold - 20);
234
- securityBlockLevel = 'critical';
235
- leniencyFactor = 0.8;
236
- reasoning.push('Hobby tier: relaxed thresholds, only critical security blocks');
237
- break;
238
- case 'startup':
239
- coverageThreshold = Math.max(60, coverageThreshold - 15);
240
- qualityThreshold = Math.max(70, qualityThreshold - 10);
241
- securityBlockLevel = 'high';
242
- leniencyFactor = 0.5;
243
- reasoning.push('Startup tier: moderate thresholds, high+ security blocks');
244
- break;
245
- case 'enterprise':
246
- coverageThreshold = coverageThreshold;
247
- qualityThreshold = qualityThreshold;
248
- securityBlockLevel = 'medium';
249
- leniencyFactor = 0.2;
250
- reasoning.push('Enterprise tier: strict thresholds, medium+ security blocks');
251
- break;
252
- }
253
- // Adjust by overall trend
254
- if (trend === 'improving') {
255
- coverageThreshold = Math.max(50, coverageThreshold - 5);
256
- qualityThreshold = Math.max(60, qualityThreshold - 5);
257
- leniencyFactor = Math.min(1, leniencyFactor + 0.1);
258
- reasoning.push('Improving trend: bonus threshold relaxation (+5%)');
259
- }
260
- else if (trend === 'degrading') {
261
- coverageThreshold = Math.min(95, coverageThreshold + 5);
262
- qualityThreshold = Math.min(95, qualityThreshold + 5);
263
- leniencyFactor = Math.max(0, leniencyFactor - 0.1);
264
- reasoning.push('Degrading trend: tightened thresholds (-5%)');
265
- }
266
- // v5: Per-provenance adjustments
267
- // If AI drift is degrading but structural is stable, tighten AI-specific gates
268
- if (provenanceTrends.aiDrift === 'degrading' && provenanceTrends.structural !== 'degrading') {
269
- leniencyFactor = Math.max(0, leniencyFactor - 0.15);
270
- reasoning.push('AI drift degrading while structural stable: AI is the problem, tightening AI gates');
271
- }
272
- // If security is degrading, escalate security block level
273
- if (provenanceTrends.security === 'degrading') {
274
- if (securityBlockLevel === 'critical')
275
- securityBlockLevel = 'high';
276
- else if (securityBlockLevel === 'high')
277
- securityBlockLevel = 'medium';
278
- reasoning.push(`Security trend degrading: escalated block level to ${securityBlockLevel}+`);
279
- }
280
- // Recent failure rate adjustment
281
- if (metrics.recentFailureRate !== undefined) {
282
- if (metrics.recentFailureRate > 50) {
283
- leniencyFactor = Math.min(1, leniencyFactor + 0.2);
284
- reasoning.push(`High failure rate (${metrics.recentFailureRate.toFixed(0)}%): increased leniency`);
285
- }
286
- else if (metrics.recentFailureRate < 10) {
287
- leniencyFactor = Math.max(0, leniencyFactor - 0.1);
288
- reasoning.push(`Low failure rate (${metrics.recentFailureRate.toFixed(0)}%): stricter enforcement`);
289
- }
290
- }
291
- return {
292
- tier,
293
- trend,
294
- coverageThreshold: Math.round(coverageThreshold),
295
- qualityThreshold: Math.round(qualityThreshold),
296
- securityBlockLevel,
297
- leniencyFactor: Math.round(leniencyFactor * 100) / 100,
298
- reasoning,
299
- provenanceTrends,
300
- };
301
- }
302
174
  /**
303
175
  * Clear adaptive history (for testing)
304
176
  */
@@ -309,20 +181,3 @@ export function clearAdaptiveHistory(cwd) {
309
181
  fs.unlinkSync(historyPath);
310
182
  }
311
183
  }
312
- /**
313
- * Get summary of adaptive thresholds for logging
314
- */
315
- export function getAdaptiveSummary(adjustments) {
316
- let summary = `[${adjustments.tier.toUpperCase()}] ` +
317
- `Coverage: ${adjustments.coverageThreshold}%, ` +
318
- `Quality: ${adjustments.qualityThreshold}%, ` +
319
- `Security: ${adjustments.securityBlockLevel}+, ` +
320
- `Trend: ${adjustments.trend}`;
321
- if (adjustments.provenanceTrends) {
322
- const pt = adjustments.provenanceTrends;
323
- if (pt.aiDrift !== 'stable' || pt.structural !== 'stable' || pt.security !== 'stable') {
324
- summary += ` | AI:${pt.aiDrift}(Z=${pt.aiDriftZScore}) Struct:${pt.structural}(Z=${pt.structuralZScore}) Sec:${pt.security}(Z=${pt.securityZScore})`;
325
- }
326
- }
327
- return summary;
328
- }