@0xcraft/powershot 1.1.4 → 1.2.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 +23 -9
- package/dist/cli/args.js +2 -1
- package/dist/cli/review-command.js +30 -12
- package/dist/cli/session-command.js +1 -0
- package/dist/delegate.js +89 -13
- package/dist/git.js +5 -3
- package/dist/ground.js +19 -12
- package/dist/lang/packs.js +170 -8
- package/dist/langtest.js +1056 -6
- package/dist/plan.js +25 -0
- package/dist/reinvention.js +227 -2
- package/dist/report/sarif.js +2 -2
- package/dist/report/summary.js +12 -4
- package/dist/review.js +5 -13
- package/dist/selftest.js +507 -17
- package/dist/session.js +1 -0
- package/dist/verifiers/dropped-guard.js +188 -84
- package/dist/verifiers/foreign-dropped-guard.js +174 -50
- package/dist/verifiers/foreign-reinvented.js +22 -7
- package/dist/verifiers/foreign-tokens.js +228 -19
- package/dist/verifiers/guard-diff.js +138 -0
- package/dist/verifiers/reinvented.js +7 -5
- package/docs/architecture.md +33 -0
- package/docs/ci.md +9 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -123,8 +123,8 @@ oracle is never counted as a pass in either profile.
|
|
|
123
123
|
| `phantom-dep` | Imports absent from project manifests | Dependency manifests |
|
|
124
124
|
| `phantom-config` | Configuration keys with no declared source | Repository config index |
|
|
125
125
|
| `contract-drift` | Signature changes with callers left behind | Types and references |
|
|
126
|
-
| `reinvented` | New
|
|
127
|
-
| `dropped-guard` |
|
|
126
|
+
| `reinvented` | New cross-file declarations with a token-identical implementation, package, visibility, wrapper, and binding context | Base declaration + scoped token fingerprint |
|
|
127
|
+
| `dropped-guard` | Early-exit guards deleted while every other token in the file and changed source set stays unchanged | Pre/post control-flow AST |
|
|
128
128
|
| `swallowed-error` | Empty or ineffective error handling | AST shape |
|
|
129
129
|
| `vacuous-test` | Tests that do not assert behavior | Test AST |
|
|
130
130
|
| `assertion-drift` | Expectations changed under stable behavior | Pre/post test AST |
|
|
@@ -180,8 +180,8 @@ commands.
|
|
|
180
180
|
flowchart LR
|
|
181
181
|
START["Selected files and checks"] --> ACCOUNT{"Required work accounted for?"}
|
|
182
182
|
ACCOUNT -- "yes" --> DEPTH{"Enriched semantic depth available?"}
|
|
183
|
-
DEPTH -- "yes" --> FULL["full coverage"]
|
|
184
|
-
DEPTH -- "no · portable policy" --> PORTABLE["portable coverage · gaps named"]
|
|
183
|
+
DEPTH -- "yes" --> FULL["full applicable-oracle coverage"]
|
|
184
|
+
DEPTH -- "no · portable policy" --> PORTABLE["portable oracle coverage · gaps named"]
|
|
185
185
|
FULL --> FINDINGS{"Findings?"}
|
|
186
186
|
PORTABLE --> FINDINGS
|
|
187
187
|
FINDINGS -- "no" --> CLEAN["exit 0 · complete and clean"]
|
|
@@ -206,11 +206,11 @@ flowchart LR
|
|
|
206
206
|
| `2` | Command or Git input was invalid |
|
|
207
207
|
| `3` | Review is incomplete; findings may be missing |
|
|
208
208
|
|
|
209
|
-
A clean report names its effective severity threshold, deterministic/model mode,
|
|
210
|
-
|
|
211
|
-
a verdict and keeps the missing work visible. Use
|
|
212
|
-
dispositions, executed and unavailable checks,
|
|
213
|
-
`notLookedAt`.
|
|
209
|
+
A clean report names its effective severity threshold, deterministic/model mode,
|
|
210
|
+
reviewed/changed file ratio, check count, and oracle coverage level. A partial or
|
|
211
|
+
failed review instead says it is not a verdict and keeps the missing work visible. Use
|
|
212
|
+
`--format manifest` to inspect file dispositions, executed and unavailable checks,
|
|
213
|
+
failures, judge units, and `notLookedAt`.
|
|
214
214
|
|
|
215
215
|
## CI integration
|
|
216
216
|
|
|
@@ -298,6 +298,20 @@ psh delegate > /tmp/powershot-brief.md
|
|
|
298
298
|
psh review --verify-only --absorb /tmp/powershot-findings.json
|
|
299
299
|
```
|
|
300
300
|
|
|
301
|
+
Delegation uses the same `SelectionPlan` and target snapshot as a real review. The
|
|
302
|
+
brief names every changed file as selected, policy-waived, or failed, so unsupported
|
|
303
|
+
or oversized files cannot disappear between task creation and absorption. For an
|
|
304
|
+
agent that consumes structured input, request the versioned task directly:
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
psh delegate --format json > /tmp/powershot-task.json
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`powershot.delegate/v1` contains the resolved file dispositions, applicable judges,
|
|
311
|
+
bounded review units, changed-line excerpts, intent, and the exact output contract.
|
|
312
|
+
Creating either form is LLM-free and does not create a session. Deterministic checks
|
|
313
|
+
run when the returned finding array is absorbed by `psh review --verify-only`.
|
|
314
|
+
|
|
301
315
|
## Language coverage
|
|
302
316
|
|
|
303
317
|
| Language | Available oracles |
|
package/dist/cli/args.js
CHANGED
|
@@ -6,7 +6,8 @@ psh — code review for machine-written code
|
|
|
6
6
|
psh review --from main --to feat/x branch range
|
|
7
7
|
psh review --commit <hash> a single commit
|
|
8
8
|
psh scan <path> audit existing files, no git history needed
|
|
9
|
-
psh delegate
|
|
9
|
+
psh delegate [--format text|markdown|json]
|
|
10
|
+
emit a selection-accounted task for your existing agent
|
|
10
11
|
psh session list | view <id> runs that can be resumed, or replayed as a page
|
|
11
12
|
psh session diff <a> <b> what one review found that the other did not
|
|
12
13
|
psh dismiss <id> [--reason "..."] record that a finding is correct, and stop showing it
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { Budget, parseLimits } from '#app/budget.js';
|
|
4
4
|
import { loadConfig, policyChanged } from '#app/config.js';
|
|
5
|
-
import { absorbDelegated, delegateBrief } from '#app/delegate.js';
|
|
5
|
+
import { absorbDelegated, buildDelegateTask, delegateBrief, delegateJson } from '#app/delegate.js';
|
|
6
6
|
import { baseRefOf, checkRange, headSha, repoRoot, shaOf } from '#app/git.js';
|
|
7
7
|
import { JUDGES } from '#app/judges/prompts.js';
|
|
8
8
|
import { RunManifest, coverageProblems, hashOf, writeManifest } from '#app/manifest.js';
|
|
@@ -13,6 +13,7 @@ import { progress, stage } from '#app/report/terminal.js';
|
|
|
13
13
|
import { summarizeRun } from '#app/report/summary.js';
|
|
14
14
|
import { review } from '#app/review.js';
|
|
15
15
|
import { scanPaths } from '#app/scan.js';
|
|
16
|
+
import { SelectionPlan } from '#app/plan.js';
|
|
16
17
|
import { Session } from '#app/session.js';
|
|
17
18
|
import { withTargetTree } from '#app/snapshot.js';
|
|
18
19
|
import { SEVERITIES } from '#app/types.js';
|
|
@@ -33,6 +34,10 @@ export async function runReviewCommand(command, values, positionals) {
|
|
|
33
34
|
process.stderr.write('--format must be one of: ' + REPORT_FORMATS.join(', ') + '\n');
|
|
34
35
|
return 2;
|
|
35
36
|
}
|
|
37
|
+
if (command === 'delegate' && !['text', 'markdown', 'json'].includes(values.format)) {
|
|
38
|
+
process.stderr.write('delegate --format must be one of: text, markdown, json\n');
|
|
39
|
+
return 2;
|
|
40
|
+
}
|
|
36
41
|
if ((values.from && !values.to) || (values.to && !values.from)) {
|
|
37
42
|
process.stderr.write('--from and --to must be given together.\n');
|
|
38
43
|
return 2;
|
|
@@ -142,19 +147,32 @@ export async function runReviewCommand(command, values, positionals) {
|
|
|
142
147
|
if (command === 'delegate') {
|
|
143
148
|
const { buildGround } = await import('#app/ground.js');
|
|
144
149
|
const { collectChanges, statedIntent } = await import('#app/git.js');
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
if (changes.length === 0) {
|
|
150
|
+
const all = collectChanges(root, range);
|
|
151
|
+
if (all.length === 0) {
|
|
148
152
|
process.stderr.write('Nothing to review.\n');
|
|
149
153
|
return 0;
|
|
150
154
|
}
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
155
|
+
const task = await withTargetTree(root, range, async (tree) => {
|
|
156
|
+
const plan = SelectionPlan.build(tree, all, config);
|
|
157
|
+
const selected = plan.keep(all);
|
|
158
|
+
const ground = await buildGround(tree, selected, undefined, all);
|
|
159
|
+
plan.accountForGround(selected, ground);
|
|
160
|
+
return buildDelegateTask(ground, config, plan.items(), {
|
|
161
|
+
intent: statedIntent(root, range),
|
|
162
|
+
maxBundleLines: maxBundle,
|
|
163
|
+
checks: requestedChecks,
|
|
164
|
+
target: { from: values.from, to: values.to, commit: values.commit },
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
const rendered = values.format === 'json' ? delegateJson(task) : delegateBrief(task);
|
|
168
|
+
if (values.output !== undefined) {
|
|
169
|
+
writeFileSync(values.output, rendered);
|
|
170
|
+
process.stderr.write(dim(' written to ' + values.output) + '\n');
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
process.stdout.write(rendered);
|
|
174
|
+
}
|
|
175
|
+
return task.state === 'failed' ? 3 : 0;
|
|
158
176
|
}
|
|
159
177
|
const canceller = new AbortController();
|
|
160
178
|
let interrupted = false;
|
|
@@ -46,6 +46,7 @@ export function runSessionCommand(positionals) {
|
|
|
46
46
|
verifyOnly: session.report.verifyOnly,
|
|
47
47
|
minSeverity: session.report.minSeverity,
|
|
48
48
|
filesReviewed: session.report.filesReviewed,
|
|
49
|
+
filesChanged: session.report.filesChanged,
|
|
49
50
|
deterministicChecks: session.report.deterministicChecks,
|
|
50
51
|
scopeDetails: session.report.scopeDetails,
|
|
51
52
|
}));
|
package/dist/delegate.js
CHANGED
|
@@ -2,44 +2,120 @@ import { bundle, bundleName } from './bundle.js';
|
|
|
2
2
|
import { renderChanges } from './judges/judge.js';
|
|
3
3
|
import { COMMON, JUDGES } from './judges/prompts.js';
|
|
4
4
|
import { enabled } from './config.js';
|
|
5
|
-
|
|
5
|
+
import { stripPath } from './text.js';
|
|
6
|
+
export const DELEGATE_SCHEMA = 'powershot.delegate/v1';
|
|
7
|
+
export function buildDelegateTask(g, cfg, files, opts) {
|
|
6
8
|
const units = bundle(g, opts.maxBundleLines ?? 1200);
|
|
7
9
|
const judges = JUDGES.filter((j) => opts.checks ? opts.checks.includes(j.name) : enabled(cfg.judges, j.name));
|
|
10
|
+
return {
|
|
11
|
+
schema: DELEGATE_SCHEMA,
|
|
12
|
+
state: files.some((file) => file.disposition === 'failed') ? 'failed' : 'complete',
|
|
13
|
+
target: opts.target ?? {},
|
|
14
|
+
intent: opts.intent,
|
|
15
|
+
files: files.map(({ path, disposition, reason, bytes, addedLines, language }) => ({
|
|
16
|
+
path,
|
|
17
|
+
disposition,
|
|
18
|
+
reason,
|
|
19
|
+
bytes,
|
|
20
|
+
addedLines,
|
|
21
|
+
language,
|
|
22
|
+
})),
|
|
23
|
+
judges: judges.map((judge) => {
|
|
24
|
+
const usesIntent = judge.needsIntent === true;
|
|
25
|
+
const applicable = !usesIntent || Boolean(opts.intent);
|
|
26
|
+
return {
|
|
27
|
+
name: judge.name,
|
|
28
|
+
brief: judge.brief,
|
|
29
|
+
applicable,
|
|
30
|
+
usesIntent,
|
|
31
|
+
reason: applicable ? undefined : 'no stated intent available',
|
|
32
|
+
};
|
|
33
|
+
}),
|
|
34
|
+
instructions: {
|
|
35
|
+
summary: 'Perform only the judgement work in this task. PowerShot runs its deterministic checks when these findings are absorbed.',
|
|
36
|
+
groundRules: COMMON,
|
|
37
|
+
output: {
|
|
38
|
+
type: 'json-array',
|
|
39
|
+
empty: [],
|
|
40
|
+
example: [{
|
|
41
|
+
file: 'src/a.ts',
|
|
42
|
+
line: 12,
|
|
43
|
+
severity: 'high',
|
|
44
|
+
confidence: 'firm',
|
|
45
|
+
check: 'plausible-logic',
|
|
46
|
+
title: '…',
|
|
47
|
+
why: '…',
|
|
48
|
+
fix: '…',
|
|
49
|
+
}],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
units: units.map((unit, index) => ({
|
|
53
|
+
id: index + 1,
|
|
54
|
+
name: bundleName(unit, g.root),
|
|
55
|
+
files: unit.files.map((file) => file.path),
|
|
56
|
+
changes: renderChanges(unit.files),
|
|
57
|
+
})),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const cell = (value) => stripPath(value).replace(/\|/g, '\\|');
|
|
61
|
+
export function delegateBrief(task) {
|
|
62
|
+
const scope = task.files.length === 0
|
|
63
|
+
? ['_No changed files._']
|
|
64
|
+
: [
|
|
65
|
+
'| File | Disposition | Language | Added | Reason |',
|
|
66
|
+
'|---|---|---|---:|---|',
|
|
67
|
+
...task.files.map((file) => '| ' + cell(file.path) + ' | ' + file.disposition + ' | ' + file.language + ' | ' + file.addedLines +
|
|
68
|
+
' | ' + cell(file.reason ?? '') + ' |'),
|
|
69
|
+
];
|
|
8
70
|
const out = [
|
|
9
71
|
'# PowerShot review brief',
|
|
10
72
|
'',
|
|
11
|
-
|
|
73
|
+
task.instructions.summary,
|
|
12
74
|
'Act as each judge below over each review unit, then reply with a single JSON array',
|
|
13
75
|
'combining every finding. Add nothing outside the array.',
|
|
14
76
|
'',
|
|
77
|
+
'## Scope',
|
|
78
|
+
'',
|
|
79
|
+
'Task state: **' + task.state + '**.',
|
|
80
|
+
'',
|
|
81
|
+
...scope,
|
|
82
|
+
'',
|
|
15
83
|
'## Output contract',
|
|
16
84
|
'',
|
|
17
85
|
'```json',
|
|
18
|
-
|
|
19
|
-
' "check":"plausible-logic","title":"…","why":"…","fix":"…"}]',
|
|
86
|
+
JSON.stringify(task.instructions.output.example, null, 2),
|
|
20
87
|
'```',
|
|
21
88
|
'',
|
|
22
|
-
'Return `
|
|
89
|
+
'Return `' + JSON.stringify(task.instructions.output.empty) +
|
|
90
|
+
'` when a judge finds nothing. An empty array is a success, not a failure.',
|
|
23
91
|
'',
|
|
24
92
|
'## Ground rules',
|
|
25
93
|
'',
|
|
26
|
-
|
|
94
|
+
task.instructions.groundRules,
|
|
27
95
|
'',
|
|
28
96
|
];
|
|
29
|
-
for (const judge of judges) {
|
|
97
|
+
for (const judge of task.judges) {
|
|
30
98
|
out.push('## Judge: ' + judge.name, '', judge.brief, '');
|
|
31
|
-
if (judge.
|
|
32
|
-
out.push(
|
|
33
|
-
? 'This change states that it does the following:\n\n> ' +
|
|
99
|
+
if (judge.usesIntent) {
|
|
100
|
+
out.push(task.intent
|
|
101
|
+
? 'This change states that it does the following:\n\n> ' + task.intent.split('\n').join('\n> ')
|
|
34
102
|
: '_No stated intent available (no commit in range) — skip this judge._', '');
|
|
35
103
|
}
|
|
36
104
|
}
|
|
37
105
|
out.push('## Review units', '');
|
|
38
|
-
units.
|
|
39
|
-
out.push('
|
|
40
|
-
}
|
|
106
|
+
if (task.units.length === 0) {
|
|
107
|
+
out.push('_No judgement units. Every changed file was excluded or failed preparation; inspect Scope._', '');
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
task.units.forEach((unit) => {
|
|
111
|
+
out.push('### Unit ' + unit.id + ' — ' + cell(unit.name), '', '```', unit.changes, '```', '');
|
|
112
|
+
});
|
|
113
|
+
}
|
|
41
114
|
return out.join('\n');
|
|
42
115
|
}
|
|
116
|
+
export function delegateJson(task) {
|
|
117
|
+
return JSON.stringify(task, null, 2) + '\n';
|
|
118
|
+
}
|
|
43
119
|
export function absorbDelegated(json) {
|
|
44
120
|
let parsed;
|
|
45
121
|
try {
|
package/dist/git.js
CHANGED
|
@@ -89,7 +89,7 @@ function changedPaths(raw) {
|
|
|
89
89
|
}
|
|
90
90
|
const path = fields[i++];
|
|
91
91
|
if (path)
|
|
92
|
-
out.push({ path, beforePath: status === 'A' ? undefined : path });
|
|
92
|
+
out.push({ path, beforePath: status === 'A' ? undefined : path, deleted: status === 'D' });
|
|
93
93
|
}
|
|
94
94
|
return out;
|
|
95
95
|
}
|
|
@@ -195,9 +195,9 @@ export function collectChanges(root, range) {
|
|
|
195
195
|
}
|
|
196
196
|
const files = [];
|
|
197
197
|
const tracked = changedPaths(git(root, [
|
|
198
|
-
'diff', '--name-status', '-z', '--diff-filter=
|
|
198
|
+
'diff', '--name-status', '-z', '--diff-filter=ACDMRT', '--find-renames', ...diffArgs,
|
|
199
199
|
]));
|
|
200
|
-
for (const { path, beforePath } of tracked) {
|
|
200
|
+
for (const { path, beforePath, deleted } of tracked) {
|
|
201
201
|
const pathspecs = beforePath && beforePath !== path
|
|
202
202
|
? [':(literal)' + beforePath, ':(literal)' + path]
|
|
203
203
|
: [':(literal)' + path];
|
|
@@ -207,6 +207,8 @@ export function collectChanges(root, range) {
|
|
|
207
207
|
]);
|
|
208
208
|
files.push({
|
|
209
209
|
path,
|
|
210
|
+
beforePath: beforePath && beforePath !== path ? beforePath : undefined,
|
|
211
|
+
deleted,
|
|
210
212
|
added: addedLinesInPatch(patch),
|
|
211
213
|
before: beforePath === undefined ? undefined : fileAtRef(root, baseRef, beforePath),
|
|
212
214
|
});
|
package/dist/ground.js
CHANGED
|
@@ -4,7 +4,7 @@ import { decode } from './text.js';
|
|
|
4
4
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
5
5
|
import { PACKS, packFor, parseIsolated } from './lang/packs.js';
|
|
6
6
|
import { insideRepo, isSymlink, repoPath } from './fspolicy.js';
|
|
7
|
-
import { createReinventionScopeResolver, typescriptImplementationFingerprint } from './reinvention.js';
|
|
7
|
+
import { createReinventionScopeResolver, exportedDeclarations, typescriptImplementationFingerprint } from './reinvention.js';
|
|
8
8
|
const CODE_EXT = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/;
|
|
9
9
|
const TS_CONFIG = /^tsconfig(?:\..+)?\.json$/i;
|
|
10
10
|
const MISSING_TYPE_PREFIXES = [
|
|
@@ -216,7 +216,7 @@ function makeDepsFor(root) {
|
|
|
216
216
|
* packages, a syntax-only project for unconfigured changes and the base-ref trees,
|
|
217
217
|
* and one deduplicated symbol index over the relevant project closures.
|
|
218
218
|
*/
|
|
219
|
-
export async function buildGround(root, changed, signal) {
|
|
219
|
+
export async function buildGround(root, changed, signal, inventory = changed) {
|
|
220
220
|
root = resolve(root);
|
|
221
221
|
const directoryCache = new Map();
|
|
222
222
|
const projectCache = new Map();
|
|
@@ -253,6 +253,7 @@ export async function buildGround(root, changed, signal) {
|
|
|
253
253
|
}
|
|
254
254
|
}
|
|
255
255
|
const beforeProject = new Project({ useInMemoryFileSystem: true });
|
|
256
|
+
const beforeSources = new Map();
|
|
256
257
|
const files = [];
|
|
257
258
|
for (const c of changed) {
|
|
258
259
|
if (!CODE_EXT.test(c.path))
|
|
@@ -264,7 +265,12 @@ export async function buildGround(root, changed, signal) {
|
|
|
264
265
|
const sf = configured?.project.getSourceFile(abs) ?? syntaxProject.getSourceFile(abs);
|
|
265
266
|
if (!sf)
|
|
266
267
|
continue;
|
|
267
|
-
const
|
|
268
|
+
const beforeKey = c.beforePath ?? c.path;
|
|
269
|
+
let before = c.before === undefined ? undefined : beforeSources.get(beforeKey);
|
|
270
|
+
if (c.before !== undefined && !before) {
|
|
271
|
+
before = beforeProject.createSourceFile(`/before/${beforeKey}`, c.before);
|
|
272
|
+
beforeSources.set(beforeKey, before);
|
|
273
|
+
}
|
|
268
274
|
files.push({
|
|
269
275
|
sf,
|
|
270
276
|
changed: c,
|
|
@@ -289,6 +295,7 @@ export async function buildGround(root, changed, signal) {
|
|
|
289
295
|
configFiles,
|
|
290
296
|
beforeProject,
|
|
291
297
|
changed,
|
|
298
|
+
inventory,
|
|
292
299
|
files,
|
|
293
300
|
symbolIndex: buildSymbolIndex(sourceFiles, root, changed, beforeProject),
|
|
294
301
|
deps: depsFor(join(root, 'x.ts')),
|
|
@@ -465,24 +472,18 @@ function buildSymbolIndex(sourceFiles, root, changed, beforeProject) {
|
|
|
465
472
|
// proof of where the file is
|
|
466
473
|
if (path.includes('/node_modules/') || !insideRepo(root, path))
|
|
467
474
|
continue;
|
|
468
|
-
for (const
|
|
475
|
+
for (const { name, node: decl } of exportedDeclarations(sf)) {
|
|
469
476
|
const key = normalizeName(name);
|
|
470
477
|
// Fingerprint only names the change could have introduced. This keeps index
|
|
471
478
|
// construction proportional to the diff even when the project closure is a
|
|
472
479
|
// very large monorepo.
|
|
473
480
|
if (!relevantNames.has(key))
|
|
474
481
|
continue;
|
|
475
|
-
const decl = decls[0];
|
|
476
|
-
if (!decl)
|
|
477
|
-
continue;
|
|
478
482
|
// only index things that could plausibly be reimplemented
|
|
479
483
|
const kind = decl.getKind();
|
|
480
484
|
if (kind !== SyntaxKind.FunctionDeclaration &&
|
|
481
485
|
kind !== SyntaxKind.VariableDeclaration)
|
|
482
486
|
continue;
|
|
483
|
-
const fingerprint = typescriptImplementationFingerprint(decl);
|
|
484
|
-
if (!fingerprint)
|
|
485
|
-
continue;
|
|
486
487
|
// A barrel alias can be new in this change even when its underlying callable
|
|
487
488
|
// predates it. Index the declaration from its own module, where both its name
|
|
488
489
|
// and base existence can be proved, rather than manufacturing history for the
|
|
@@ -493,9 +494,15 @@ function buildSymbolIndex(sourceFiles, root, changed, beforeProject) {
|
|
|
493
494
|
if (declPath.includes('/node_modules/') || !insideRepo(root, declPath))
|
|
494
495
|
continue;
|
|
495
496
|
const rel = repoPath(root, declPath);
|
|
497
|
+
const fingerprint = typescriptImplementationFingerprint(decl, rel);
|
|
498
|
+
if (!fingerprint)
|
|
499
|
+
continue;
|
|
496
500
|
const change = changes.get(rel);
|
|
497
|
-
const
|
|
498
|
-
const
|
|
501
|
+
const beforePath = change?.beforePath ?? rel;
|
|
502
|
+
const before = change?.before === undefined ? undefined : beforeProject.getSourceFile('/before/' + beforePath);
|
|
503
|
+
const sameScope = scopeFor(beforePath) === scopeFor(rel);
|
|
504
|
+
const existedInBase = change === undefined || (sameScope && before ? exportedDeclarations(before).some((baseDeclaration) => baseDeclaration.name === name &&
|
|
505
|
+
typescriptImplementationFingerprint(baseDeclaration.node, beforePath) === fingerprint) : false);
|
|
499
506
|
const list = index.get(key) ?? [];
|
|
500
507
|
if (list.some((e) => e.file === rel && e.line === decl.getStartLineNumber()))
|
|
501
508
|
continue;
|