@rigour-labs/core 6.6.6 → 6.7.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.
- 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.test.js +2 -2
- 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 +1 -2
- package/dist/hooks/index.js +0 -1
- package/dist/hooks/types.d.ts +0 -14
- package/dist/hooks/types.js +1 -15
- package/dist/index.d.ts +6 -1
- package/dist/index.js +6 -1
- 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-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 +9 -4
- package/dist/review/backtest.js +19 -9
- package/dist/review/backtest.test.js +2 -2
- package/dist/review/baseline.js +2 -1
- package/dist/review/dead-code.test.js +7 -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.js +1 -1
- 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/review.js +2 -1
- package/dist/review/reviewer/adapters.d.ts +23 -7
- package/dist/review/reviewer/adapters.js +32 -7
- 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 +8 -1
- package/dist/review/reviewer/background.js +34 -9
- package/dist/review/reviewer/background.test.js +16 -2
- 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.js +6 -2
- 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 +2 -0
- package/dist/review/reviewer/inputs.js +5 -1
- 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 +11 -0
- package/dist/review/reviewer/prompt.js +26 -4
- 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 +25 -2
- package/dist/review/reviewer/store.js +43 -1
- 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 +15 -1
- package/dist/review/reviewer/verdict.js +17 -7
- package/dist/review/reviewer.d.ts +45 -2
- package/dist/review/reviewer.js +186 -25
- package/dist/review/reviewer.test.js +127 -12
- package/dist/review/test-files.d.ts +5 -1
- package/dist/review/test-files.js +6 -2
- package/dist/review/toolchain.js +10 -3
- package/dist/review/toolchain.test.js +6 -3
- package/dist/review/typed/program.d.ts +0 -1
- package/dist/review/typed/program.js +3 -3
- package/dist/review/typed/redundancy.js +43 -4
- package/dist/review/typed/redundancy.test.js +58 -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 +5 -6
- 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 +8 -45
- package/dist/types/index.d.ts +108 -566
- package/dist/types/index.js +41 -66
- 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
|
@@ -73,12 +73,15 @@ describe('runToolchain', () => {
|
|
|
73
73
|
expect(calls()).toContain('prettier --check --ignore-unknown src/a.ts src/bad.ts');
|
|
74
74
|
expect(calls()).not.toContain('gone.ts');
|
|
75
75
|
});
|
|
76
|
-
it('quiets ESLint 9 about ignored files, and
|
|
76
|
+
it('quiets ESLint 9 about ignored files, and runs a tool set under commands: as that command, whose failure fails the push', async () => {
|
|
77
77
|
tool('eslint', "if (args[0] === '--version') { console.log('v9.1.0'); process.exit(0); }");
|
|
78
|
-
|
|
78
|
+
tool('prettier', 'process.exit(0);');
|
|
79
|
+
const results = await runToolchain(dir, ['src/a.ts'], config({ format: 'node -e "process.exit(4)"', test: 'node -e "process.exit(0)"' }));
|
|
79
80
|
expect(results.find(r => r.tool === 'lint')).toMatchObject({ status: 'pass' });
|
|
80
81
|
expect(calls()).toContain('eslint --max-warnings=0 --no-warn-ignored src/a.ts');
|
|
81
|
-
expect(results.find(r => r.tool === 'format')).toMatchObject({ status: '
|
|
82
|
+
expect(results.find(r => r.tool === 'format')).toMatchObject({ status: 'fail', command: expect.stringContaining('node -e') });
|
|
83
|
+
expect(calls()).not.toContain('prettier'); // the team's command replaces the detected tool
|
|
84
|
+
expect(results.find(r => r.tool === 'test')).toMatchObject({ status: 'pass' });
|
|
82
85
|
});
|
|
83
86
|
it("uses the project's own check script for types, and never hands a tool Rigour's own files", async () => {
|
|
84
87
|
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ scripts: { check: 'node -e "process.exit(3)"' } }));
|
|
@@ -16,7 +16,6 @@ export interface TypedProgram {
|
|
|
16
16
|
textFiles: TextFile[];
|
|
17
17
|
inProgram: Set<string>;
|
|
18
18
|
}
|
|
19
|
-
export declare const TEST_FILE: RegExp;
|
|
20
19
|
/** The program, `skip` when this is not a TypeScript project, or `error` when it is one that cannot be loaded. */
|
|
21
20
|
export declare function loadProgram(root: string): {
|
|
22
21
|
program: TypedProgram;
|
|
@@ -12,7 +12,7 @@ import { createRequire } from 'module';
|
|
|
12
12
|
import path from 'path';
|
|
13
13
|
import ownTs from 'typescript';
|
|
14
14
|
import { installedBin } from '../../utils/installed-bin.js';
|
|
15
|
-
|
|
15
|
+
import { isTestFile } from '../test-files.js';
|
|
16
16
|
const GIT_TIMEOUT_MS = 10_000;
|
|
17
17
|
/** The program, `skip` when this is not a TypeScript project, or `error` when it is one that cannot be loaded. */
|
|
18
18
|
export function loadProgram(root) {
|
|
@@ -32,7 +32,7 @@ export function loadProgram(root) {
|
|
|
32
32
|
return {
|
|
33
33
|
program: {
|
|
34
34
|
ts, program, checker: program.getTypeChecker(), root, rel,
|
|
35
|
-
appSources: sources.filter(sf => !
|
|
35
|
+
appSources: sources.filter(sf => !isTestFile(rel(sf))),
|
|
36
36
|
textFiles: textFiles(root, inProgram),
|
|
37
37
|
inProgram,
|
|
38
38
|
},
|
|
@@ -77,7 +77,7 @@ function textFiles(root, inProgram) {
|
|
|
77
77
|
return [];
|
|
78
78
|
const files = [];
|
|
79
79
|
for (const file of listed.stdout.split('\n')) {
|
|
80
|
-
if (!file || inProgram.has(file) || file.endsWith('.d.ts') ||
|
|
80
|
+
if (!file || inProgram.has(file) || file.endsWith('.d.ts') || isTestFile(file))
|
|
81
81
|
continue;
|
|
82
82
|
try {
|
|
83
83
|
files.push({ file, text: fs.readFileSync(path.join(root, file), 'utf8') });
|
|
@@ -9,17 +9,22 @@
|
|
|
9
9
|
* write-only-property a property the hosts set and nothing reads. When the value only
|
|
10
10
|
* leaves through serialisation to a callee the program does not declare
|
|
11
11
|
* (a wire payload), that is a hint for the reviewer, not a block.
|
|
12
|
+
* Plus a note, not yet proven on merged pull requests so it never blocks:
|
|
13
|
+
* nullable-not-null-column the row type of `.from('t')` says `| null` for a column the
|
|
14
|
+
* migrations make NOT NULL (`schema_migrations`, schema-nullability.ts).
|
|
12
15
|
* Plus a hint: a function that scans a collection, called once per item of another (nested-scan).
|
|
13
16
|
* Scoped to lines the change touched and members it declared, or whose every host it wrote; a
|
|
14
17
|
* pre-existing member with one touched writer is the team's backlog, not this change's.
|
|
15
18
|
*/
|
|
16
19
|
import micromatch from 'micromatch';
|
|
17
20
|
import { emitsDeclarations } from '../package-layout.js';
|
|
18
|
-
import {
|
|
21
|
+
import { isTestFile } from '../test-files.js';
|
|
22
|
+
import { loadProgram } from './program.js';
|
|
23
|
+
import { loadSchemaNullability, tableKey } from './schema-nullability.js';
|
|
19
24
|
export const TYPED_CHECKS = 'typed-checks-unavailable';
|
|
20
25
|
/** The typed checks on a change: nothing when it touches no TypeScript or the project has no tsconfig. */
|
|
21
26
|
export function typedChecks(cwd, changedLines, config) {
|
|
22
|
-
if (!config.gates.redundancy?.enabled || !Object.keys(changedLines).some(file => /\.[cm]?ts$/.test(file) && !
|
|
27
|
+
if (!config.gates.redundancy?.enabled || !Object.keys(changedLines).some(file => /\.[cm]?ts$/.test(file) && !isTestFile(file)))
|
|
23
28
|
return { failures: [], hints: [] };
|
|
24
29
|
const loaded = loadProgram(cwd);
|
|
25
30
|
if ('skip' in loaded)
|
|
@@ -35,7 +40,7 @@ function redundancyFailures(typed, changedLines, config) {
|
|
|
35
40
|
if (!settings?.enabled)
|
|
36
41
|
return { failures: [], hints: [] };
|
|
37
42
|
const { ts, checker, rel } = typed;
|
|
38
|
-
const changed = Object.keys(changedLines).filter(file => /\.[cm]?ts$/.test(file) && !file.endsWith('.d.ts') && !
|
|
43
|
+
const changed = Object.keys(changedLines).filter(file => /\.[cm]?ts$/.test(file) && !file.endsWith('.d.ts') && !isTestFile(file));
|
|
39
44
|
const failures = [];
|
|
40
45
|
const hints = [];
|
|
41
46
|
const line = (node) => node.getSourceFile().getLineAndCharacterOfPosition(node.getStart()).line + 1;
|
|
@@ -83,13 +88,47 @@ function redundancyFailures(typed, changedLines, config) {
|
|
|
83
88
|
return undefined;
|
|
84
89
|
};
|
|
85
90
|
const isNullable = (t) => t.isUnion() && t.types.some(m => !!(m.flags & ts.TypeFlags.Null));
|
|
86
|
-
//
|
|
91
|
+
// nullable-not-null-column: read once, and only when a changed file queries a table.
|
|
92
|
+
let schema;
|
|
93
|
+
const reportedDecls = new Set();
|
|
94
|
+
const notNullColumns = (chain, outer, filtered) => {
|
|
95
|
+
const from = chain.find(c => c.name === 'from' && strArg(c.call, 0));
|
|
96
|
+
if (!from)
|
|
97
|
+
return;
|
|
98
|
+
const schemaCall = chain.find(c => c.name === 'schema' && strArg(c.call, 0));
|
|
99
|
+
const table = `${schemaCall ? `${strArg(schemaCall.call, 0)}.` : ''}${strArg(from.call, 0)}`;
|
|
100
|
+
schema ??= loadSchemaNullability(typed.root, settings.schema_migrations);
|
|
101
|
+
const columns = schema.get(tableKey(table));
|
|
102
|
+
const row = columns && rowTypeOf(chain, outer);
|
|
103
|
+
if (!columns || !row)
|
|
104
|
+
return;
|
|
105
|
+
const select = chain.find(c => c.name === 'select');
|
|
106
|
+
const selected = select ? strArg(select.call, 0) ?? '' : '';
|
|
107
|
+
// The query's own line the change touched: a call's start is the start of the whole chain, so its method name.
|
|
108
|
+
const queryLine = chain.map(c => c.call.expression.name).find(touched);
|
|
109
|
+
for (const prop of checker.getPropertiesOfType(row)) {
|
|
110
|
+
const decl = prop.declarations?.[0];
|
|
111
|
+
const name = prop.name;
|
|
112
|
+
if (!decl || !ts.isPropertySignature(decl) || !decl.type || reportedDecls.has(decl) || filtered.has(name))
|
|
113
|
+
continue;
|
|
114
|
+
// An alias (`name:other_column`) maps another column onto this property.
|
|
115
|
+
if (columns.get(name) !== true || new RegExp(`(^|[\\s,(])${name.replace(/[$]/g, '\\$')}\\s*:`).test(selected) || !isNullable(checker.getTypeFromTypeNode(decl.type)))
|
|
116
|
+
continue;
|
|
117
|
+
const anchor = touched(decl) ? decl : queryLine;
|
|
118
|
+
if (!anchor)
|
|
119
|
+
continue;
|
|
120
|
+
reportedDecls.add(decl);
|
|
121
|
+
report('nullable-not-null-column', 'Nullable type for a NOT NULL column', anchor, `\`${name}\` is declared nullable at ${at(decl)} but \`${table}.${name}\` is NOT NULL in the migrations, so every guard on it is dead.`, 'Narrow the row type and delete the guards on it. If the migrations Rigour read are not the ones this code runs against, point gates.redundancy.schema_migrations at them.');
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
// duplicate-null-filter, nullable-filtered-column and nullable-not-null-column
|
|
87
125
|
for (const sf of changedSources) {
|
|
88
126
|
walk(sf, node => {
|
|
89
127
|
if (!ts.isCallExpression(node) || !outermost(node))
|
|
90
128
|
return;
|
|
91
129
|
const chain = chainOf(node);
|
|
92
130
|
const nots = chain.filter(c => c.name === 'not' && strArg(c.call, 1) === 'is' && isNullLiteral(c.call.arguments[2]) && strArg(c.call, 0));
|
|
131
|
+
notNullColumns(chain, node, new Set(nots.map(n => strArg(n.call, 0))));
|
|
93
132
|
if (nots.length === 0 || !spanTouched(node))
|
|
94
133
|
return;
|
|
95
134
|
for (const n of nots) {
|
|
@@ -119,3 +119,61 @@ export async function scan(since: string): Promise<Row[]> {
|
|
|
119
119
|
expect(broken.typedError).toMatch(/\.generated\/tsconfig\.json.*install the dependencies/s);
|
|
120
120
|
}, 60_000);
|
|
121
121
|
});
|
|
122
|
+
describe('a nullable row type for a NOT NULL column, from migrations in another repository', () => {
|
|
123
|
+
const SUPABASE = `export class Query<T> {
|
|
124
|
+
select(_columns: string): Query<T> { return this; }
|
|
125
|
+
returns<R>(): Promise<R> { return Promise.resolve([] as unknown as R); }
|
|
126
|
+
}
|
|
127
|
+
export const db = {
|
|
128
|
+
schema(_name: string) { return db; },
|
|
129
|
+
from(_table: string): Query<unknown> { return new Query(); },
|
|
130
|
+
};
|
|
131
|
+
`;
|
|
132
|
+
let migrations;
|
|
133
|
+
beforeEach(() => {
|
|
134
|
+
migrations = fs.mkdtempSync(path.join(os.tmpdir(), 'other-repo-migrations-'));
|
|
135
|
+
fs.writeFileSync(path.join(migrations, '20240101_sets.sql'), 'create table study_set (id uuid primary key, owner_id uuid not null, title text, archived_at timestamptz not null);');
|
|
136
|
+
write('src/sb.ts', SUPABASE);
|
|
137
|
+
git('add', '-A');
|
|
138
|
+
git('commit', '-qm', 'client');
|
|
139
|
+
});
|
|
140
|
+
afterEach(() => { fs.rmSync(migrations, { recursive: true, force: true }); });
|
|
141
|
+
it('notes the nullable member the change declared, never blocking, and skips nullable columns and aliases', async () => {
|
|
142
|
+
write('src/sets.ts', `import { db } from './sb';
|
|
143
|
+
export interface SetRow { id: string; owner_id: string | null; title: string | null; done: string | null }
|
|
144
|
+
export async function sets(): Promise<SetRow[]> {
|
|
145
|
+
return db.from('study_set').select('id, owner_id, title, done:archived_at').returns<SetRow[]>();
|
|
146
|
+
}
|
|
147
|
+
`);
|
|
148
|
+
git('add', '-A');
|
|
149
|
+
git('commit', '-qm', 'sets');
|
|
150
|
+
const result = await review(config({ schema_migrations: [migrations] }));
|
|
151
|
+
expect(result.findings.filter(f => f.id === 'nullable-not-null-column')).toEqual([]);
|
|
152
|
+
const notes = result.advisory.filter(f => f.id === 'nullable-not-null-column');
|
|
153
|
+
expect(notes.map(f => [f.files?.[0], f.line])).toEqual([['src/sets.ts', 2]]);
|
|
154
|
+
expect(notes[0].details).toContain('`owner_id` is declared nullable at src/sets.ts:2 but `study_set.owner_id` is NOT NULL');
|
|
155
|
+
});
|
|
156
|
+
it('anchors on the new query when the row type is older, and reads the schema the query names', async () => {
|
|
157
|
+
fs.writeFileSync(path.join(migrations, '20240202_app.sql'), 'create table app.study_set (id uuid, owner_id uuid);');
|
|
158
|
+
git('checkout', '-q', 'main'); // the row type is older than the branch
|
|
159
|
+
write('src/rows.ts', 'export interface SetRow { id: string; owner_id: string | null }\n');
|
|
160
|
+
git('add', '-A');
|
|
161
|
+
git('commit', '-qm', 'rows');
|
|
162
|
+
git('checkout', '-q', 'feature');
|
|
163
|
+
git('merge', '-q', '--no-edit', 'main');
|
|
164
|
+
write('src/sets.ts', `import { db } from './sb';
|
|
165
|
+
import type { SetRow } from './rows';
|
|
166
|
+
export async function publicSets(): Promise<SetRow[]> {
|
|
167
|
+
return db
|
|
168
|
+
.from('study_set').select('*').returns<SetRow[]>();
|
|
169
|
+
}
|
|
170
|
+
export async function appSets(): Promise<SetRow[]> {
|
|
171
|
+
return db.schema('app').from('study_set').select('*').returns<SetRow[]>();
|
|
172
|
+
}
|
|
173
|
+
`);
|
|
174
|
+
git('add', '-A');
|
|
175
|
+
git('commit', '-qm', 'sets');
|
|
176
|
+
const result = await review(config({ schema_migrations: [migrations] }));
|
|
177
|
+
expect(result.advisory.filter(f => f.id === 'nullable-not-null-column').map(f => [f.files?.[0], f.line])).toEqual([['src/sets.ts', 5]]);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** `schema.table` → column → NOT NULL. A table mapped to `null` is one whose columns cannot be known. */
|
|
2
|
+
export type SchemaNullability = Map<string, Map<string, boolean> | null>;
|
|
3
|
+
/** The migrations in `dirs` (relative to the repository, absolute, or `~/`), replayed in file-name order. Missing folders are skipped. */
|
|
4
|
+
export declare function loadSchemaNullability(root: string, dirs: string[]): SchemaNullability;
|
|
5
|
+
/** `schema.table` with Postgres's folding: unquoted names lower-cased, the schema `public` by default. */
|
|
6
|
+
export declare function tableKey(name: string): string;
|
|
@@ -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
|
+
});
|
|
@@ -14,11 +14,10 @@ import { spawnSync } from 'child_process';
|
|
|
14
14
|
import fs from 'fs';
|
|
15
15
|
import path from 'path';
|
|
16
16
|
import { emitsDeclarations, isPackageEntry } from './package-layout.js';
|
|
17
|
+
import { isTestFile } from './test-files.js';
|
|
17
18
|
const GIT_TIMEOUT_MS = 10_000;
|
|
18
19
|
const CODE = /\.(ts|tsx|js|jsx|mjs|svelte)$/;
|
|
19
|
-
const SKIPPED = /\.
|
|
20
|
-
/** A test is not a consumer: production code must use the export. */
|
|
21
|
-
const TEST = /\.(test|spec|e2e)\.[cm]?[jt]sx?$|(^|\/)(tests?|e2e|__tests__|__mocks__)\//;
|
|
20
|
+
const SKIPPED = /\.d\.ts$|(^|\/)(dist|build|out|coverage|\.next|[\w-]+-dist)\//;
|
|
22
21
|
/** What a route or hook module exports for its framework, not for an importer. */
|
|
23
22
|
const FRAMEWORK_EXPORTS = new Set([
|
|
24
23
|
'load', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD', 'fallback', 'actions',
|
|
@@ -63,7 +62,7 @@ function addedExports(diff) {
|
|
|
63
62
|
if (!file || text.startsWith('-'))
|
|
64
63
|
continue;
|
|
65
64
|
if (text.startsWith('+')) {
|
|
66
|
-
if (CODE.test(file) && !SKIPPED.test(file))
|
|
65
|
+
if (CODE.test(file) && !SKIPPED.test(file) && !isTestFile(file))
|
|
67
66
|
found.push(...exportsOn(text.slice(1), file, line));
|
|
68
67
|
line++;
|
|
69
68
|
}
|
|
@@ -113,7 +112,7 @@ export function unusedExportFailures(cwd, diff, config) {
|
|
|
113
112
|
return []; // git could not answer: say nothing rather than guess
|
|
114
113
|
const points = pointsAtCache(cwd);
|
|
115
114
|
return candidates
|
|
116
|
-
.filter(exp => ![...(users.get(exp.name) ?? [])].some(user => !isOwnFile(user, exp) && !
|
|
115
|
+
.filter(exp => ![...(users.get(exp.name) ?? [])].some(user => !isOwnFile(user, exp) && !isTestFile(user) && points(user, exp.file)))
|
|
117
116
|
.map(unused);
|
|
118
117
|
}
|
|
119
118
|
/**
|
|
@@ -182,7 +181,7 @@ function filesNaming(cwd, names, excluded) {
|
|
|
182
181
|
function unused(exp) {
|
|
183
182
|
return {
|
|
184
183
|
id: 'unused-export',
|
|
185
|
-
title:
|
|
184
|
+
title: `Unused export \`${exp.name}\``,
|
|
186
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.`,
|
|
187
186
|
severity: 'medium',
|
|
188
187
|
provenance: 'traditional',
|
|
@@ -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;
|