@eviano/tribunal 0.1.2
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/LICENSE +21 -0
- package/README.md +169 -0
- package/dist/analyzers/assertionFreeTest.d.ts +12 -0
- package/dist/analyzers/assertionFreeTest.js +316 -0
- package/dist/analyzers/assertionFreeTest.js.map +1 -0
- package/dist/analyzers/claimReconciliation.d.ts +3 -0
- package/dist/analyzers/claimReconciliation.js +190 -0
- package/dist/analyzers/claimReconciliation.js.map +1 -0
- package/dist/analyzers/defaults.d.ts +8 -0
- package/dist/analyzers/defaults.js +82 -0
- package/dist/analyzers/defaults.js.map +1 -0
- package/dist/analyzers/exports.d.ts +12 -0
- package/dist/analyzers/exports.js +84 -0
- package/dist/analyzers/exports.js.map +1 -0
- package/dist/analyzers/hallucinatedSymbol.d.ts +22 -0
- package/dist/analyzers/hallucinatedSymbol.js +233 -0
- package/dist/analyzers/hallucinatedSymbol.js.map +1 -0
- package/dist/analyzers/index.d.ts +9 -0
- package/dist/analyzers/index.js +9 -0
- package/dist/analyzers/index.js.map +1 -0
- package/dist/claims.d.ts +15 -0
- package/dist/claims.js +32 -0
- package/dist/claims.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +116 -0
- package/dist/cli.js.map +1 -0
- package/dist/diff/gitDiff.d.ts +19 -0
- package/dist/diff/gitDiff.js +64 -0
- package/dist/diff/gitDiff.js.map +1 -0
- package/dist/diff/parseUnifiedDiff.d.ts +9 -0
- package/dist/diff/parseUnifiedDiff.js +63 -0
- package/dist/diff/parseUnifiedDiff.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/report/render.d.ts +10 -0
- package/dist/report/render.js +70 -0
- package/dist/report/render.js.map +1 -0
- package/dist/types.d.ts +71 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { assertionFreeTest } from './assertionFreeTest.js';
|
|
2
|
+
import { directExports } from './exports.js';
|
|
3
|
+
import { collectDefaultParams, keyMatchesArg } from './defaults.js';
|
|
4
|
+
/**
|
|
5
|
+
* `claim-reconciliation` — verifies machine-readable PR claims against what the diff actually did, with
|
|
6
|
+
* deterministic checks only. This is the durable-moat milestone (docs/SPEC.md §5b): the company selling
|
|
7
|
+
* you the agent won't ship the tool that catches its own agent lying.
|
|
8
|
+
*
|
|
9
|
+
* Trust Contract: gate only on CONTRADICTED, and CONTRADICTED must be a syntactic certainty. An unknown
|
|
10
|
+
* claim, a missing base ref, or an un-enumerable module all degrade to UNVERIFIED — never a false red.
|
|
11
|
+
*/
|
|
12
|
+
const SOURCE_EXT_RE = /\.[cm]?[jt]sx?$/i;
|
|
13
|
+
const TEST_FILE_RE = /(^|[./\\])(test|spec)\.[cm]?[jt]sx?$/i;
|
|
14
|
+
const TEST_DIR_RE = /(^|[/\\])(__tests__|tests?)[/\\]/i;
|
|
15
|
+
function isTestPath(path) {
|
|
16
|
+
return TEST_FILE_RE.test(path) || TEST_DIR_RE.test(path);
|
|
17
|
+
}
|
|
18
|
+
/** "added a test" → the diff must add at least one test, ideally one with a detectable assertion. */
|
|
19
|
+
function verifyAddedTest(ctx) {
|
|
20
|
+
// Reuse the assertion-free-test analyzer: it emits one finding per test touched by the diff.
|
|
21
|
+
const testFindings = assertionFreeTest.run(ctx);
|
|
22
|
+
const touched = testFindings.length;
|
|
23
|
+
const asserting = testFindings.filter((f) => f.verdict === 'PASS').length;
|
|
24
|
+
if (asserting > 0) {
|
|
25
|
+
return {
|
|
26
|
+
verdict: 'PASS',
|
|
27
|
+
title: 'Claim confirmed: test added',
|
|
28
|
+
detail: `The diff adds ${asserting} test(s) with at least one assertion.`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (touched === 0) {
|
|
32
|
+
return {
|
|
33
|
+
verdict: 'CONTRADICTED',
|
|
34
|
+
title: "Claimed a test was added, but none is in the diff",
|
|
35
|
+
detail: 'No new test (no it()/test()/Deno.test() block) appears in any changed test file.',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
verdict: 'UNVERIFIED',
|
|
40
|
+
title: 'Test added, but its assertion could not be detected',
|
|
41
|
+
detail: `The diff adds ${touched} test(s), but none has a detectable assertion (see the assertion-free-test findings). Confirm the test is meaningful.`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const EMPTY_EXPORTS = { names: new Set(), hasDefault: false, uncertain: false };
|
|
45
|
+
/** "no public API change" → exported-symbol set is identical between base and head for changed files. */
|
|
46
|
+
function verifyNoPublicApiChange(ctx) {
|
|
47
|
+
if (!ctx.base || !ctx.readBaseFile) {
|
|
48
|
+
return {
|
|
49
|
+
verdict: 'UNVERIFIED',
|
|
50
|
+
title: 'No base ref to compare the public API',
|
|
51
|
+
detail: 'Exported-symbol changes need a base ref to diff against; none was available.',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const changes = [];
|
|
55
|
+
let uncertain = false;
|
|
56
|
+
for (const f of ctx.changedFiles) {
|
|
57
|
+
if (!SOURCE_EXT_RE.test(f.path) || isTestPath(f.path))
|
|
58
|
+
continue;
|
|
59
|
+
const baseContent = ctx.readBaseFile(f.path);
|
|
60
|
+
const headContent = f.status === 'deleted' ? null : ctx.readFile(f.path);
|
|
61
|
+
if (baseContent == null && headContent == null)
|
|
62
|
+
continue;
|
|
63
|
+
const baseExp = baseContent ? directExports(baseContent, f.path) : EMPTY_EXPORTS;
|
|
64
|
+
const headExp = headContent ? directExports(headContent, f.path) : EMPTY_EXPORTS;
|
|
65
|
+
if (baseExp.uncertain || headExp.uncertain) {
|
|
66
|
+
uncertain = true;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const added = [...headExp.names].filter((n) => !baseExp.names.has(n));
|
|
70
|
+
const removed = [...baseExp.names].filter((n) => !headExp.names.has(n));
|
|
71
|
+
if (baseExp.hasDefault !== headExp.hasDefault) {
|
|
72
|
+
(headExp.hasDefault ? added : removed).push('default');
|
|
73
|
+
}
|
|
74
|
+
if (added.length || removed.length) {
|
|
75
|
+
const parts = [];
|
|
76
|
+
if (added.length)
|
|
77
|
+
parts.push(`+${added.join(', +')}`);
|
|
78
|
+
if (removed.length)
|
|
79
|
+
parts.push(`-${removed.join(', -')}`);
|
|
80
|
+
changes.push(`${f.path} (${parts.join('; ')})`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (changes.length) {
|
|
84
|
+
return {
|
|
85
|
+
verdict: 'CONTRADICTED',
|
|
86
|
+
title: 'Claimed no public API change, but exports changed',
|
|
87
|
+
detail: `Exported symbols changed in: ${changes.join(' | ')}.`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (uncertain) {
|
|
91
|
+
return {
|
|
92
|
+
verdict: 'UNVERIFIED',
|
|
93
|
+
title: 'Public API change could not be fully verified',
|
|
94
|
+
detail: 'Some changed modules use `export *` / CJS exports that cannot be enumerated statically.',
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
verdict: 'PASS',
|
|
99
|
+
title: 'Claim confirmed: no public API change',
|
|
100
|
+
detail: 'No exported symbols were added or removed in changed source files.',
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** "no default flip" → no literal default parameter value changed between base and head. */
|
|
104
|
+
function verifyNoDefaultFlip(ctx, claim) {
|
|
105
|
+
if (!ctx.base || !ctx.readBaseFile) {
|
|
106
|
+
return {
|
|
107
|
+
verdict: 'UNVERIFIED',
|
|
108
|
+
title: 'No base ref to compare defaults',
|
|
109
|
+
detail: 'Default-value changes need a base ref to diff against; none was available.',
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const flips = [];
|
|
113
|
+
for (const f of ctx.changedFiles) {
|
|
114
|
+
if (!SOURCE_EXT_RE.test(f.path) || isTestPath(f.path))
|
|
115
|
+
continue;
|
|
116
|
+
const baseContent = ctx.readBaseFile(f.path);
|
|
117
|
+
const headContent = f.status === 'deleted' ? null : ctx.readFile(f.path);
|
|
118
|
+
if (baseContent == null || headContent == null)
|
|
119
|
+
continue;
|
|
120
|
+
const baseDefaults = collectDefaultParams(baseContent, f.path);
|
|
121
|
+
const headDefaults = collectDefaultParams(headContent, f.path);
|
|
122
|
+
for (const [key, headVal] of headDefaults) {
|
|
123
|
+
if (!baseDefaults.has(key))
|
|
124
|
+
continue; // newly-added default is not a flip
|
|
125
|
+
const baseVal = baseDefaults.get(key);
|
|
126
|
+
if (baseVal === headVal)
|
|
127
|
+
continue;
|
|
128
|
+
if (claim.arg && !keyMatchesArg(key, claim.arg))
|
|
129
|
+
continue;
|
|
130
|
+
flips.push(`${f.path} ${key} (${baseVal} → ${headVal})`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (flips.length) {
|
|
134
|
+
return {
|
|
135
|
+
verdict: 'CONTRADICTED',
|
|
136
|
+
title: 'Claimed no default flip, but a default changed',
|
|
137
|
+
detail: `Default values changed: ${flips.join(' | ')}.`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
verdict: 'PASS',
|
|
142
|
+
title: 'Claim confirmed: no default flip',
|
|
143
|
+
detail: claim.arg
|
|
144
|
+
? `No literal default for '${claim.arg}' changed in changed source files.`
|
|
145
|
+
: 'No literal default parameter values changed in changed source files.',
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const verifiers = {
|
|
149
|
+
'added-test': verifyAddedTest,
|
|
150
|
+
'added-tests': verifyAddedTest,
|
|
151
|
+
'added-a-test': verifyAddedTest,
|
|
152
|
+
'no-public-api-change': verifyNoPublicApiChange,
|
|
153
|
+
'no-api-change': verifyNoPublicApiChange,
|
|
154
|
+
'no-public-api-changes': verifyNoPublicApiChange,
|
|
155
|
+
'no-default-flip': verifyNoDefaultFlip,
|
|
156
|
+
'no-default-change': verifyNoDefaultFlip,
|
|
157
|
+
'no-defaults-changed': verifyNoDefaultFlip,
|
|
158
|
+
};
|
|
159
|
+
export const recognizedClaims = Object.keys(verifiers);
|
|
160
|
+
export const claimReconciliation = {
|
|
161
|
+
id: 'claim-reconciliation',
|
|
162
|
+
title: 'Claim reconciliation',
|
|
163
|
+
description: 'Verifies machine-readable PR claims against the diff using deterministic checks (no LLM).',
|
|
164
|
+
kind: 'claim-reconciliation',
|
|
165
|
+
run(ctx) {
|
|
166
|
+
const claims = ctx.claims ?? [];
|
|
167
|
+
const findings = [];
|
|
168
|
+
for (const claim of claims) {
|
|
169
|
+
const verifier = verifiers[claim.key];
|
|
170
|
+
const res = verifier
|
|
171
|
+
? verifier(ctx, claim)
|
|
172
|
+
: {
|
|
173
|
+
verdict: 'UNVERIFIED',
|
|
174
|
+
title: 'Unrecognized claim',
|
|
175
|
+
detail: `No deterministic verifier for claim '${claim.key}'. Recognized: ${recognizedClaims.join(', ')}.`,
|
|
176
|
+
};
|
|
177
|
+
findings.push({
|
|
178
|
+
analyzer: 'claim-reconciliation',
|
|
179
|
+
verdict: res.verdict,
|
|
180
|
+
file: '(PR claim)',
|
|
181
|
+
line: 0,
|
|
182
|
+
title: res.title,
|
|
183
|
+
detail: res.detail,
|
|
184
|
+
claim: claim.raw,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return findings;
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
//# sourceMappingURL=claimReconciliation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claimReconciliation.js","sourceRoot":"","sources":["../../src/analyzers/claimReconciliation.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collect literal default *parameter* values, keyed by a stable name (`fnName:paramName`,
|
|
3
|
+
* `Class.method:paramName`). This is the highest-signal, lowest-false-positive slice of "default flip":
|
|
4
|
+
* `function connect(timeoutMs = 30)` → `connect(timeoutMs = 5000)` is a syntactic, checkable fact.
|
|
5
|
+
*/
|
|
6
|
+
export declare function collectDefaultParams(content: string, fileName: string): Map<string, string>;
|
|
7
|
+
/** Does a `fnName:paramName` key match a user-supplied claim argument (param name or fn name)? */
|
|
8
|
+
export declare function keyMatchesArg(key: string, arg: string): boolean;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
function scriptKindForPath(path) {
|
|
3
|
+
if (/\.tsx$/i.test(path))
|
|
4
|
+
return ts.ScriptKind.TSX;
|
|
5
|
+
if (/\.jsx$/i.test(path))
|
|
6
|
+
return ts.ScriptKind.JSX;
|
|
7
|
+
if (/\.[cm]?ts$/i.test(path))
|
|
8
|
+
return ts.ScriptKind.TS;
|
|
9
|
+
return ts.ScriptKind.JS;
|
|
10
|
+
}
|
|
11
|
+
/** Only compare defaults that are literals — anything computed can't be diffed soundly, so we skip it. */
|
|
12
|
+
function isLiteralLike(n) {
|
|
13
|
+
if (ts.isStringLiteral(n) || ts.isNoSubstitutionTemplateLiteral(n) || ts.isNumericLiteral(n))
|
|
14
|
+
return true;
|
|
15
|
+
if (n.kind === ts.SyntaxKind.TrueKeyword ||
|
|
16
|
+
n.kind === ts.SyntaxKind.FalseKeyword ||
|
|
17
|
+
n.kind === ts.SyntaxKind.NullKeyword) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
if (ts.isIdentifier(n) && n.text === 'undefined')
|
|
21
|
+
return true;
|
|
22
|
+
if (ts.isPrefixUnaryExpression(n) &&
|
|
23
|
+
(n.operator === ts.SyntaxKind.MinusToken || n.operator === ts.SyntaxKind.PlusToken) &&
|
|
24
|
+
ts.isNumericLiteral(n.operand)) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
function isFunctionLike(n) {
|
|
30
|
+
return (ts.isFunctionDeclaration(n) ||
|
|
31
|
+
ts.isMethodDeclaration(n) ||
|
|
32
|
+
ts.isFunctionExpression(n) ||
|
|
33
|
+
ts.isArrowFunction(n) ||
|
|
34
|
+
ts.isConstructorDeclaration(n));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Collect literal default *parameter* values, keyed by a stable name (`fnName:paramName`,
|
|
38
|
+
* `Class.method:paramName`). This is the highest-signal, lowest-false-positive slice of "default flip":
|
|
39
|
+
* `function connect(timeoutMs = 30)` → `connect(timeoutMs = 5000)` is a syntactic, checkable fact.
|
|
40
|
+
*/
|
|
41
|
+
export function collectDefaultParams(content, fileName) {
|
|
42
|
+
const sf = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true, scriptKindForPath(fileName));
|
|
43
|
+
const map = new Map();
|
|
44
|
+
const visit = (node, container) => {
|
|
45
|
+
let name = container;
|
|
46
|
+
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
47
|
+
name = node.name.text;
|
|
48
|
+
}
|
|
49
|
+
else if (ts.isClassDeclaration(node) && node.name) {
|
|
50
|
+
name = node.name.text;
|
|
51
|
+
}
|
|
52
|
+
else if (ts.isMethodDeclaration(node) && ts.isIdentifier(node.name)) {
|
|
53
|
+
name = (container ? `${container}.` : '') + node.name.text;
|
|
54
|
+
}
|
|
55
|
+
else if (ts.isConstructorDeclaration(node)) {
|
|
56
|
+
name = (container ? `${container}.` : '') + 'constructor';
|
|
57
|
+
}
|
|
58
|
+
else if (ts.isVariableDeclaration(node) &&
|
|
59
|
+
ts.isIdentifier(node.name) &&
|
|
60
|
+
node.initializer &&
|
|
61
|
+
(ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) {
|
|
62
|
+
name = node.name.text;
|
|
63
|
+
}
|
|
64
|
+
if (isFunctionLike(node)) {
|
|
65
|
+
const fnName = name ?? '(anonymous)';
|
|
66
|
+
for (const p of node.parameters) {
|
|
67
|
+
if (p.initializer && ts.isIdentifier(p.name) && isLiteralLike(p.initializer)) {
|
|
68
|
+
map.set(`${fnName}:${p.name.text}`, p.initializer.getText(sf));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
ts.forEachChild(node, (child) => visit(child, name));
|
|
73
|
+
};
|
|
74
|
+
visit(sf, undefined);
|
|
75
|
+
return map;
|
|
76
|
+
}
|
|
77
|
+
/** Does a `fnName:paramName` key match a user-supplied claim argument (param name or fn name)? */
|
|
78
|
+
export function keyMatchesArg(key, arg) {
|
|
79
|
+
const [fnPart, paramPart] = key.split(':');
|
|
80
|
+
return paramPart === arg || fnPart === arg || fnPart.endsWith(`.${arg}`);
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=defaults.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"defaults.js","sourceRoot":"","sources":["../../src/analyzers/defaults.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface DirectExports {
|
|
2
|
+
names: Set<string>;
|
|
3
|
+
hasDefault: boolean;
|
|
4
|
+
/** True when exports cannot be enumerated statically (re-exports via `export *`, CJS, `export =`). */
|
|
5
|
+
uncertain: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Collect a module's DIRECTLY-declared exported names from source text (no disk access, no following
|
|
9
|
+
* of `export *`). Any construct whose export set can't be enumerated statically flips `uncertain`,
|
|
10
|
+
* which callers must treat as "cannot decide" rather than "no exports".
|
|
11
|
+
*/
|
|
12
|
+
export declare function directExports(content: string, fileName: string): DirectExports;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
function scriptKindForPath(path) {
|
|
3
|
+
if (/\.tsx$/i.test(path))
|
|
4
|
+
return ts.ScriptKind.TSX;
|
|
5
|
+
if (/\.jsx$/i.test(path))
|
|
6
|
+
return ts.ScriptKind.JSX;
|
|
7
|
+
if (/\.[cm]?ts$/i.test(path))
|
|
8
|
+
return ts.ScriptKind.TS;
|
|
9
|
+
return ts.ScriptKind.JS;
|
|
10
|
+
}
|
|
11
|
+
function addBindingName(name, into) {
|
|
12
|
+
if (ts.isIdentifier(name)) {
|
|
13
|
+
into.add(name.text);
|
|
14
|
+
}
|
|
15
|
+
else if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) {
|
|
16
|
+
for (const el of name.elements) {
|
|
17
|
+
if (ts.isBindingElement(el))
|
|
18
|
+
addBindingName(el.name, into);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Collect a module's DIRECTLY-declared exported names from source text (no disk access, no following
|
|
24
|
+
* of `export *`). Any construct whose export set can't be enumerated statically flips `uncertain`,
|
|
25
|
+
* which callers must treat as "cannot decide" rather than "no exports".
|
|
26
|
+
*/
|
|
27
|
+
export function directExports(content, fileName) {
|
|
28
|
+
const result = { names: new Set(), hasDefault: false, uncertain: false };
|
|
29
|
+
const sf = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true, scriptKindForPath(fileName));
|
|
30
|
+
let sawEsExport = false;
|
|
31
|
+
for (const stmt of sf.statements) {
|
|
32
|
+
const mods = ts.canHaveModifiers(stmt) ? ts.getModifiers(stmt) : undefined;
|
|
33
|
+
const hasExport = mods?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
34
|
+
const hasDefault = mods?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword) ?? false;
|
|
35
|
+
if (hasExport) {
|
|
36
|
+
sawEsExport = true;
|
|
37
|
+
if (hasDefault) {
|
|
38
|
+
result.hasDefault = true;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (ts.isVariableStatement(stmt)) {
|
|
42
|
+
for (const d of stmt.declarationList.declarations)
|
|
43
|
+
addBindingName(d.name, result.names);
|
|
44
|
+
}
|
|
45
|
+
else if ((ts.isFunctionDeclaration(stmt) ||
|
|
46
|
+
ts.isClassDeclaration(stmt) ||
|
|
47
|
+
ts.isInterfaceDeclaration(stmt) ||
|
|
48
|
+
ts.isTypeAliasDeclaration(stmt) ||
|
|
49
|
+
ts.isEnumDeclaration(stmt) ||
|
|
50
|
+
ts.isModuleDeclaration(stmt)) &&
|
|
51
|
+
stmt.name &&
|
|
52
|
+
ts.isIdentifier(stmt.name)) {
|
|
53
|
+
result.names.add(stmt.name.text);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else if (ts.isExportDeclaration(stmt)) {
|
|
57
|
+
sawEsExport = true;
|
|
58
|
+
if (stmt.exportClause) {
|
|
59
|
+
if (ts.isNamedExports(stmt.exportClause)) {
|
|
60
|
+
for (const e of stmt.exportClause.elements)
|
|
61
|
+
result.names.add(e.name.text);
|
|
62
|
+
}
|
|
63
|
+
else if (ts.isNamespaceExport(stmt.exportClause)) {
|
|
64
|
+
result.names.add(stmt.exportClause.name.text);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else if (stmt.moduleSpecifier) {
|
|
68
|
+
result.uncertain = true; // `export * from '...'` — cannot enumerate without resolving
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else if (ts.isExportAssignment(stmt)) {
|
|
72
|
+
sawEsExport = true;
|
|
73
|
+
if (stmt.isExportEquals)
|
|
74
|
+
result.uncertain = true; // `export =` (CJS interop)
|
|
75
|
+
else
|
|
76
|
+
result.hasDefault = true; // `export default`
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (!sawEsExport && (/\.[cm]?jsx?$/i.test(fileName) || /\bmodule\.exports\b|\bexports\./.test(content))) {
|
|
80
|
+
result.uncertain = true;
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=exports.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exports.js","sourceRoot":"","sources":["../../src/analyzers/exports.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import type { Analyzer } from '../types.js';
|
|
3
|
+
interface Resolution {
|
|
4
|
+
options: ts.CompilerOptions;
|
|
5
|
+
host: ts.ModuleResolutionHost;
|
|
6
|
+
}
|
|
7
|
+
/** Resolve the way the target repo's own compiler does — read its tsconfig when present. */
|
|
8
|
+
declare function buildResolution(repoRoot: string): Resolution;
|
|
9
|
+
interface Exports {
|
|
10
|
+
names: Set<string>;
|
|
11
|
+
hasDefault: boolean;
|
|
12
|
+
/** True when exports could not be fully enumerated (CJS, unresolvable `export *`, unreadable file). */
|
|
13
|
+
uncertain: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** Statically collect a module's exported names. Follows local `export *` re-exports, depth-limited. */
|
|
16
|
+
declare function collectExports(file: string, r: Resolution, depth: number, seen: Set<string>): Exports;
|
|
17
|
+
export declare const hallucinatedSymbol: Analyzer;
|
|
18
|
+
export declare const __test__: {
|
|
19
|
+
collectExports: typeof collectExports;
|
|
20
|
+
buildResolution: typeof buildResolution;
|
|
21
|
+
};
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* `hallucinated-symbol` (M1: import subset) — flags imports the PR added that reference a module path
|
|
6
|
+
* or a named export that does not exist. This is the highest-precision slice of "hallucinated symbol":
|
|
7
|
+
* an agent importing `{ parseConifg }` from a module that exports `parseConfig`, or importing from a
|
|
8
|
+
* relative path it never created.
|
|
9
|
+
*
|
|
10
|
+
* Trust Contract (docs/SPEC.md §3):
|
|
11
|
+
* - CONTRADICTED : a syntactic certainty — a RELATIVE import path resolves to no file, or a resolved
|
|
12
|
+
* local module statically has no such named export.
|
|
13
|
+
* - UNVERIFIED : a bare package import that doesn't resolve (uninstalled / maybe slopsquatted — that
|
|
14
|
+
* is Provenance-Lock's job), or a module whose exports cannot be enumerated statically
|
|
15
|
+
* (CJS, `export *` to an unresolvable target).
|
|
16
|
+
* - (no finding) : the path resolves and the named export exists.
|
|
17
|
+
*
|
|
18
|
+
* Default imports are intentionally never flagged: esModuleInterop / CJS synthesize a default, so a
|
|
19
|
+
* missing `export default` is not a certainty.
|
|
20
|
+
*/
|
|
21
|
+
const SOURCE_EXT_RE = /\.[cm]?[jt]sx?$/i;
|
|
22
|
+
const JS_EXT_RE = /\.[cm]?jsx?$/i;
|
|
23
|
+
const MAX_REEXPORT_DEPTH = 4;
|
|
24
|
+
function scriptKindForPath(path) {
|
|
25
|
+
if (/\.tsx$/i.test(path))
|
|
26
|
+
return ts.ScriptKind.TSX;
|
|
27
|
+
if (/\.jsx$/i.test(path))
|
|
28
|
+
return ts.ScriptKind.JSX;
|
|
29
|
+
if (/\.[cm]?ts$/i.test(path))
|
|
30
|
+
return ts.ScriptKind.TS;
|
|
31
|
+
return ts.ScriptKind.JS;
|
|
32
|
+
}
|
|
33
|
+
/** Resolve the way the target repo's own compiler does — read its tsconfig when present. */
|
|
34
|
+
function buildResolution(repoRoot) {
|
|
35
|
+
let options = {
|
|
36
|
+
allowJs: true,
|
|
37
|
+
target: ts.ScriptTarget.Latest,
|
|
38
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
39
|
+
};
|
|
40
|
+
const cfgPath = ts.findConfigFile(repoRoot, ts.sys.fileExists, 'tsconfig.json');
|
|
41
|
+
if (cfgPath) {
|
|
42
|
+
const read = ts.readConfigFile(cfgPath, ts.sys.readFile);
|
|
43
|
+
if (!read.error && read.config) {
|
|
44
|
+
const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, dirname(cfgPath));
|
|
45
|
+
options = { ...parsed.options, allowJs: true };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { options, host: ts.sys };
|
|
49
|
+
}
|
|
50
|
+
function resolveModule(spec, containingFile, r) {
|
|
51
|
+
return ts.resolveModuleName(spec, containingFile, r.options, r.host).resolvedModule?.resolvedFileName;
|
|
52
|
+
}
|
|
53
|
+
function safeRead(file) {
|
|
54
|
+
if (!existsSync(file))
|
|
55
|
+
return null;
|
|
56
|
+
try {
|
|
57
|
+
return readFileSync(file, 'utf8');
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function addBindingName(name, into) {
|
|
64
|
+
if (ts.isIdentifier(name)) {
|
|
65
|
+
into.add(name.text);
|
|
66
|
+
}
|
|
67
|
+
else if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) {
|
|
68
|
+
for (const el of name.elements) {
|
|
69
|
+
if (ts.isBindingElement(el))
|
|
70
|
+
addBindingName(el.name, into);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Statically collect a module's exported names. Follows local `export *` re-exports, depth-limited. */
|
|
75
|
+
function collectExports(file, r, depth, seen) {
|
|
76
|
+
const result = { names: new Set(), hasDefault: false, uncertain: false };
|
|
77
|
+
if (seen.has(file) || depth > MAX_REEXPORT_DEPTH) {
|
|
78
|
+
result.uncertain = true;
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
seen.add(file);
|
|
82
|
+
const content = safeRead(file);
|
|
83
|
+
if (content == null) {
|
|
84
|
+
result.uncertain = true;
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
const sf = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true, scriptKindForPath(file));
|
|
88
|
+
let sawEsExport = false;
|
|
89
|
+
for (const stmt of sf.statements) {
|
|
90
|
+
const mods = ts.canHaveModifiers(stmt) ? ts.getModifiers(stmt) : undefined;
|
|
91
|
+
const hasExport = mods?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
92
|
+
const hasDefault = mods?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword) ?? false;
|
|
93
|
+
if (hasExport) {
|
|
94
|
+
sawEsExport = true;
|
|
95
|
+
if (hasDefault) {
|
|
96
|
+
result.hasDefault = true;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (ts.isVariableStatement(stmt)) {
|
|
100
|
+
for (const d of stmt.declarationList.declarations)
|
|
101
|
+
addBindingName(d.name, result.names);
|
|
102
|
+
}
|
|
103
|
+
else if ((ts.isFunctionDeclaration(stmt) ||
|
|
104
|
+
ts.isClassDeclaration(stmt) ||
|
|
105
|
+
ts.isInterfaceDeclaration(stmt) ||
|
|
106
|
+
ts.isTypeAliasDeclaration(stmt) ||
|
|
107
|
+
ts.isEnumDeclaration(stmt) ||
|
|
108
|
+
ts.isModuleDeclaration(stmt)) &&
|
|
109
|
+
stmt.name &&
|
|
110
|
+
ts.isIdentifier(stmt.name)) {
|
|
111
|
+
result.names.add(stmt.name.text);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else if (ts.isExportDeclaration(stmt)) {
|
|
115
|
+
sawEsExport = true;
|
|
116
|
+
if (stmt.exportClause) {
|
|
117
|
+
if (ts.isNamedExports(stmt.exportClause)) {
|
|
118
|
+
for (const e of stmt.exportClause.elements)
|
|
119
|
+
result.names.add(e.name.text);
|
|
120
|
+
}
|
|
121
|
+
else if (ts.isNamespaceExport(stmt.exportClause)) {
|
|
122
|
+
result.names.add(stmt.exportClause.name.text);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else if (stmt.moduleSpecifier && ts.isStringLiteral(stmt.moduleSpecifier)) {
|
|
126
|
+
// `export * from '...'`
|
|
127
|
+
const target = resolveModule(stmt.moduleSpecifier.text, file, r);
|
|
128
|
+
if (target) {
|
|
129
|
+
const sub = collectExports(target, r, depth + 1, seen);
|
|
130
|
+
for (const n of sub.names)
|
|
131
|
+
result.names.add(n);
|
|
132
|
+
if (sub.uncertain)
|
|
133
|
+
result.uncertain = true;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
result.uncertain = true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else if (ts.isExportAssignment(stmt)) {
|
|
141
|
+
sawEsExport = true;
|
|
142
|
+
if (stmt.isExportEquals)
|
|
143
|
+
result.uncertain = true; // `export =` (CJS interop)
|
|
144
|
+
else
|
|
145
|
+
result.hasDefault = true; // `export default`
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// A JS/CJS module with no ES exports may export dynamically — we cannot be certain.
|
|
149
|
+
if (!sawEsExport && (JS_EXT_RE.test(file) || /\bmodule\.exports\b|\bexports\./.test(content))) {
|
|
150
|
+
result.uncertain = true;
|
|
151
|
+
}
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
function lineOf(sf, pos) {
|
|
155
|
+
return sf.getLineAndCharacterOfPosition(pos).line + 1;
|
|
156
|
+
}
|
|
157
|
+
function overlaps(addedLines, start, end) {
|
|
158
|
+
if (addedLines.size === 0)
|
|
159
|
+
return false;
|
|
160
|
+
for (let l = start; l <= end; l++)
|
|
161
|
+
if (addedLines.has(l))
|
|
162
|
+
return true;
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
function mk(verdict, file, line, endLine, title, detail) {
|
|
166
|
+
return { analyzer: 'hallucinated-symbol', verdict, file, line, endLine, title, detail };
|
|
167
|
+
}
|
|
168
|
+
function checkFile(sf, filePath, addedLines, absFile, r) {
|
|
169
|
+
const findings = [];
|
|
170
|
+
for (const stmt of sf.statements) {
|
|
171
|
+
if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier))
|
|
172
|
+
continue;
|
|
173
|
+
const start = lineOf(sf, stmt.getStart(sf));
|
|
174
|
+
const end = lineOf(sf, stmt.getEnd());
|
|
175
|
+
if (!overlaps(addedLines, start, end))
|
|
176
|
+
continue;
|
|
177
|
+
const spec = stmt.moduleSpecifier.text;
|
|
178
|
+
const isRelative = spec.startsWith('.') || isAbsolute(spec);
|
|
179
|
+
const resolved = resolveModule(spec, absFile, r);
|
|
180
|
+
if (!resolved) {
|
|
181
|
+
if (isRelative) {
|
|
182
|
+
findings.push(mk('CONTRADICTED', filePath, start, end, 'Import path does not exist', `'${spec}' does not resolve to any file from ${filePath}.`));
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
findings.push(mk('UNVERIFIED', filePath, start, end, 'Unresolved package import', `Package '${spec}' could not be resolved (not installed?). Dependency existence is out of scope for this check.`));
|
|
186
|
+
}
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const clause = stmt.importClause;
|
|
190
|
+
if (!clause || !clause.namedBindings || !ts.isNamedImports(clause.namedBindings))
|
|
191
|
+
continue;
|
|
192
|
+
const exp = collectExports(resolved, r, 0, new Set());
|
|
193
|
+
for (const el of clause.namedBindings.elements) {
|
|
194
|
+
const imported = (el.propertyName ?? el.name).text;
|
|
195
|
+
if (imported === 'default')
|
|
196
|
+
continue;
|
|
197
|
+
if (exp.names.has(imported))
|
|
198
|
+
continue;
|
|
199
|
+
if (exp.uncertain) {
|
|
200
|
+
findings.push(mk('UNVERIFIED', filePath, start, end, 'Unverifiable named import', `Could not confirm '${imported}' is exported by '${spec}' (re-exports or non-static exports). Verify manually.`));
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
findings.push(mk('CONTRADICTED', filePath, start, end, 'Import of nonexistent export', `'${spec}' has no export named '${imported}'.`));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return findings;
|
|
208
|
+
}
|
|
209
|
+
export const hallucinatedSymbol = {
|
|
210
|
+
id: 'hallucinated-symbol',
|
|
211
|
+
title: 'Hallucinated imports',
|
|
212
|
+
description: 'Flags imports the PR added that reference a module path or named export that does not exist.',
|
|
213
|
+
kind: 'claim-independent',
|
|
214
|
+
run(ctx) {
|
|
215
|
+
const r = buildResolution(ctx.repoRoot);
|
|
216
|
+
const findings = [];
|
|
217
|
+
for (const f of ctx.changedFiles) {
|
|
218
|
+
if (f.status === 'deleted')
|
|
219
|
+
continue;
|
|
220
|
+
if (!SOURCE_EXT_RE.test(f.path))
|
|
221
|
+
continue;
|
|
222
|
+
const src = ctx.readFile(f.path);
|
|
223
|
+
if (src == null)
|
|
224
|
+
continue;
|
|
225
|
+
const absFile = resolve(ctx.repoRoot, f.path);
|
|
226
|
+
const sf = ts.createSourceFile(absFile, src, ts.ScriptTarget.Latest, true, scriptKindForPath(f.path));
|
|
227
|
+
findings.push(...checkFile(sf, f.path, f.addedLines, absFile, r));
|
|
228
|
+
}
|
|
229
|
+
return findings;
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
export const __test__ = { collectExports, buildResolution };
|
|
233
|
+
//# sourceMappingURL=hallucinatedSymbol.js.map
|