@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.
- package/README.md +16 -92
- package/dist/environment.test.js +0 -4
- package/dist/gates/agent-team.d.ts +0 -2
- package/dist/gates/agent-team.js +0 -8
- package/dist/gates/checkpoint.d.ts +0 -1
- package/dist/gates/checkpoint.js +0 -1
- package/dist/gates/context.d.ts +0 -4
- package/dist/gates/context.js +7 -17
- package/dist/gates/deep-analysis.d.ts +0 -1
- package/dist/gates/deep-runner.js +0 -1
- package/dist/gates/frontend-secret-exposure.d.ts +1 -1
- package/dist/gates/frontend-secret-exposure.js +9 -3
- package/dist/gates/frontend-secret-exposure.test.js +6 -3
- package/dist/gates/hallucinated-imports/index.d.ts +0 -2
- package/dist/gates/hallucinated-imports/index.js +2 -2
- package/dist/gates/hallucinated-imports/js-resolver.js +1 -0
- package/dist/gates/hallucinated-imports.test.js +10 -0
- package/dist/gates/runner.js +3 -4
- package/dist/gates/test-quality-matchers.d.ts +0 -4
- package/dist/gates/test-quality-matchers.js +0 -7
- package/dist/gates/test-quality.d.ts +0 -2
- package/dist/gates/test-quality.js +1 -10
- package/dist/hooks/checker.js +1 -1
- package/dist/hooks/index.d.ts +2 -3
- package/dist/hooks/index.js +1 -2
- package/dist/hooks/stop-review.d.ts +6 -9
- package/dist/hooks/stop-review.js +5 -10
- package/dist/hooks/types.d.ts +0 -14
- package/dist/hooks/types.js +1 -15
- package/dist/index.d.ts +11 -3
- package/dist/index.js +11 -3
- package/dist/inference/cloud-provider.d.ts +2 -0
- package/dist/inference/cloud-provider.js +16 -6
- package/dist/inference/cloud-provider.test.js +21 -0
- package/dist/pattern-index/embeddings.d.ts +0 -5
- package/dist/pattern-index/embeddings.js +11 -4
- package/dist/pattern-index/semantic-runtime.d.ts +10 -0
- package/dist/pattern-index/semantic-runtime.js +46 -0
- package/dist/pattern-index/semantic-runtime.test.d.ts +1 -0
- package/dist/pattern-index/semantic-runtime.test.js +31 -0
- package/dist/review/backtest-init.d.ts +8 -0
- package/dist/review/backtest-init.js +87 -0
- package/dist/review/backtest-init.test.d.ts +1 -0
- package/dist/review/backtest-init.test.js +74 -0
- package/dist/review/backtest-judges.d.ts +38 -0
- package/dist/review/backtest-judges.js +63 -0
- package/dist/review/backtest-judges.test.d.ts +1 -0
- package/dist/review/backtest-judges.test.js +32 -0
- package/dist/review/backtest.d.ts +320 -0
- package/dist/review/backtest.js +207 -0
- package/dist/review/backtest.test.d.ts +1 -0
- package/dist/review/backtest.test.js +129 -0
- package/dist/review/baseline.js +2 -1
- package/dist/review/dead-code.test.js +15 -0
- package/dist/review/fix-scope.d.ts +21 -0
- package/dist/review/fix-scope.js +94 -0
- package/dist/review/fix-scope.test.d.ts +1 -0
- package/dist/review/fix-scope.test.js +104 -0
- package/dist/review/lint-overlay.d.ts +21 -0
- package/dist/review/lint-overlay.js +89 -0
- package/dist/review/orphan-files.js +3 -2
- package/dist/review/partial-wiring.js +5 -3
- package/dist/review/query-patterns.js +3 -2
- package/dist/review/quiet.d.ts +6 -0
- package/dist/review/quiet.js +12 -2
- package/dist/review/quiet.test.js +11 -0
- package/dist/review/review.d.ts +6 -0
- package/dist/review/review.js +10 -2
- package/dist/review/reviewer/adapters.d.ts +51 -0
- package/dist/review/reviewer/adapters.js +181 -0
- package/dist/review/reviewer/adapters.test.d.ts +1 -0
- package/dist/review/reviewer/adapters.test.js +23 -0
- package/dist/review/reviewer/background.d.ts +41 -0
- package/dist/review/reviewer/background.js +153 -0
- package/dist/review/reviewer/background.test.d.ts +1 -0
- package/dist/review/reviewer/background.test.js +118 -0
- package/dist/review/reviewer/consensus.d.ts +36 -0
- package/dist/review/reviewer/consensus.js +144 -0
- package/dist/review/reviewer/context.d.ts +75 -0
- package/dist/review/reviewer/context.js +140 -0
- package/dist/review/reviewer/context.test.d.ts +1 -0
- package/dist/review/reviewer/context.test.js +18 -0
- package/dist/review/reviewer/exec.d.ts +17 -0
- package/dist/review/reviewer/exec.js +27 -0
- package/dist/review/reviewer/exec.test.d.ts +1 -0
- package/dist/review/reviewer/exec.test.js +23 -0
- package/dist/review/reviewer/inputs.d.ts +39 -0
- package/dist/review/reviewer/inputs.js +107 -0
- package/dist/review/reviewer/merge-impact.d.ts +2 -0
- package/dist/review/reviewer/merge-impact.js +64 -0
- package/dist/review/reviewer/panel.d.ts +67 -0
- package/dist/review/reviewer/panel.js +101 -0
- package/dist/review/reviewer/panel.test.d.ts +1 -0
- package/dist/review/reviewer/panel.test.js +110 -0
- package/dist/review/reviewer/prompt.d.ts +32 -0
- package/dist/review/reviewer/prompt.js +145 -0
- package/dist/review/reviewer/settings.d.ts +58 -0
- package/dist/review/reviewer/settings.js +152 -0
- package/dist/review/reviewer/settings.test.d.ts +1 -0
- package/dist/review/reviewer/settings.test.js +57 -0
- package/dist/review/reviewer/store.d.ts +49 -0
- package/dist/review/reviewer/store.js +99 -0
- package/dist/review/reviewer/usage.d.ts +2 -0
- package/dist/review/reviewer/usage.js +28 -0
- package/dist/review/reviewer/usage.test.d.ts +1 -0
- package/dist/review/reviewer/usage.test.js +14 -0
- package/dist/review/reviewer/verdict.d.ts +141 -0
- package/dist/review/reviewer/verdict.js +166 -0
- package/dist/review/reviewer.d.ts +84 -49
- package/dist/review/reviewer.js +364 -195
- package/dist/review/reviewer.test.js +316 -50
- package/dist/review/test-files.d.ts +5 -1
- package/dist/review/test-files.js +6 -2
- package/dist/review/toolchain.d.ts +5 -2
- package/dist/review/toolchain.js +89 -7
- package/dist/review/toolchain.test.js +45 -4
- package/dist/review/typed/program.d.ts +26 -0
- package/dist/review/typed/program.js +90 -0
- package/dist/review/typed/redundancy.d.ts +10 -0
- package/dist/review/typed/redundancy.js +378 -0
- package/dist/review/typed/redundancy.test.d.ts +1 -0
- package/dist/review/typed/redundancy.test.js +179 -0
- package/dist/review/typed/schema-nullability.d.ts +6 -0
- package/dist/review/typed/schema-nullability.js +218 -0
- package/dist/review/typed/schema-nullability.test.d.ts +1 -0
- package/dist/review/typed/schema-nullability.test.js +63 -0
- package/dist/review/unused-exports.js +10 -7
- package/dist/semantic/learn/types.d.ts +2 -2
- package/dist/services/adaptive-thresholds.d.ts +2 -52
- package/dist/services/adaptive-thresholds.js +2 -147
- package/dist/services/adaptive-thresholds.test.js +1 -81
- package/dist/settings.d.ts +14 -6
- package/dist/settings.js +0 -12
- package/dist/storage/team-diagnostics.js +1 -1
- package/dist/storage/team-schema.d.ts +1 -1
- package/dist/storage/team-schema.js +2 -0
- package/dist/storage/team-store.js +26 -3
- package/dist/storage/team-sync-scope.test.js +35 -2
- package/dist/telemetry/telemetry.d.ts +5 -0
- package/dist/telemetry/telemetry.js +11 -1
- package/dist/templates/paradigms.js +0 -8
- package/dist/templates/presets.d.ts +0 -1
- package/dist/templates/presets.js +0 -23
- package/dist/templates/universal-config.js +16 -46
- package/dist/types/index.d.ts +202 -574
- package/dist/types/index.js +64 -67
- package/dist/types/index.test.d.ts +1 -0
- package/dist/types/index.test.js +33 -0
- package/dist/utils/command-line.d.ts +8 -0
- package/dist/utils/command-line.js +8 -0
- package/dist/utils/command-line.test.d.ts +1 -0
- package/dist/utils/command-line.test.js +8 -0
- package/dist/utils/profile.js +1 -1
- 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
|
|
9
|
-
*
|
|
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 = /\.
|
|
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:
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
}
|