@neurcode-ai/cli 0.14.0 → 0.15.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 +60 -8
- package/dist/api-client.d.ts +284 -0
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +111 -0
- package/dist/api-client.js.map +1 -1
- package/dist/commands/activate.d.ts +82 -0
- package/dist/commands/activate.d.ts.map +1 -0
- package/dist/commands/activate.js +551 -0
- package/dist/commands/activate.js.map +1 -0
- package/dist/commands/admission.d.ts +67 -0
- package/dist/commands/admission.d.ts.map +1 -0
- package/dist/commands/admission.js +350 -0
- package/dist/commands/admission.js.map +1 -0
- package/dist/commands/agent.d.ts +3 -0
- package/dist/commands/agent.d.ts.map +1 -0
- package/dist/commands/agent.js +2045 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/demo.d.ts +3 -0
- package/dist/commands/demo.d.ts.map +1 -0
- package/dist/commands/demo.js +102 -0
- package/dist/commands/demo.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +58 -44
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/login.d.ts +1 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +44 -22
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/profile.d.ts +14 -0
- package/dist/commands/profile.d.ts.map +1 -0
- package/dist/commands/profile.js +118 -0
- package/dist/commands/profile.js.map +1 -0
- package/dist/commands/quickstart.d.ts +2 -2
- package/dist/commands/quickstart.d.ts.map +1 -1
- package/dist/commands/quickstart.js +31 -30
- package/dist/commands/quickstart.js.map +1 -1
- package/dist/commands/remediate-export.d.ts +6 -1
- package/dist/commands/remediate-export.d.ts.map +1 -1
- package/dist/commands/remediate-export.js +359 -7
- package/dist/commands/remediate-export.js.map +1 -1
- package/dist/commands/replay.d.ts.map +1 -1
- package/dist/commands/replay.js +84 -0
- package/dist/commands/replay.js.map +1 -1
- package/dist/commands/run.d.ts +3 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +98 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/runtime-adapter.d.ts +8 -0
- package/dist/commands/runtime-adapter.d.ts.map +1 -0
- package/dist/commands/runtime-adapter.js +375 -0
- package/dist/commands/runtime-adapter.js.map +1 -0
- package/dist/commands/runtime-doctor.d.ts +6 -0
- package/dist/commands/runtime-doctor.d.ts.map +1 -0
- package/dist/commands/runtime-doctor.js +478 -0
- package/dist/commands/runtime-doctor.js.map +1 -0
- package/dist/commands/runtime-report.d.ts +13 -0
- package/dist/commands/runtime-report.d.ts.map +1 -0
- package/dist/commands/runtime-report.js +81 -0
- package/dist/commands/runtime-report.js.map +1 -0
- package/dist/commands/runtime-sync.d.ts +17 -0
- package/dist/commands/runtime-sync.d.ts.map +1 -0
- package/dist/commands/runtime-sync.js +656 -0
- package/dist/commands/runtime-sync.js.map +1 -0
- package/dist/commands/runtime.d.ts +16 -0
- package/dist/commands/runtime.d.ts.map +1 -0
- package/dist/commands/runtime.js +380 -0
- package/dist/commands/runtime.js.map +1 -0
- package/dist/commands/session-hook.d.ts +35 -0
- package/dist/commands/session-hook.d.ts.map +1 -0
- package/dist/commands/session-hook.js +1297 -0
- package/dist/commands/session-hook.js.map +1 -0
- package/dist/commands/session.d.ts +91 -0
- package/dist/commands/session.d.ts.map +1 -1
- package/dist/commands/session.js +1226 -0
- package/dist/commands/session.js.map +1 -1
- package/dist/commands/whoami.d.ts +7 -4
- package/dist/commands/whoami.d.ts.map +1 -1
- package/dist/commands/whoami.js +59 -34
- package/dist/commands/whoami.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +24 -5
- package/dist/config.js.map +1 -1
- package/dist/daemon/routes.d.ts.map +1 -1
- package/dist/daemon/routes.js +8 -0
- package/dist/daemon/routes.js.map +1 -1
- package/dist/daemon/server.d.ts.map +1 -1
- package/dist/daemon/server.js +88 -0
- package/dist/daemon/server.js.map +1 -1
- package/dist/governance/impact-analysis.d.ts +27 -0
- package/dist/governance/impact-analysis.d.ts.map +1 -0
- package/dist/governance/impact-analysis.js +274 -0
- package/dist/governance/impact-analysis.js.map +1 -0
- package/dist/index.js +472 -29
- package/dist/index.js.map +1 -1
- package/dist/intent-engine/matcher.d.ts.map +1 -1
- package/dist/intent-engine/matcher.js +3 -12
- package/dist/intent-engine/matcher.js.map +1 -1
- package/dist/utils/admission-artifact.d.ts +59 -0
- package/dist/utils/admission-artifact.d.ts.map +1 -0
- package/dist/utils/admission-artifact.js +410 -0
- package/dist/utils/admission-artifact.js.map +1 -0
- package/dist/utils/agent-adapter-setup.d.ts +80 -0
- package/dist/utils/agent-adapter-setup.d.ts.map +1 -0
- package/dist/utils/agent-adapter-setup.js +577 -0
- package/dist/utils/agent-adapter-setup.js.map +1 -0
- package/dist/utils/agent-guard-supervisor.d.ts +75 -0
- package/dist/utils/agent-guard-supervisor.d.ts.map +1 -0
- package/dist/utils/agent-guard-supervisor.js +388 -0
- package/dist/utils/agent-guard-supervisor.js.map +1 -0
- package/dist/utils/agent-guard.d.ts +92 -0
- package/dist/utils/agent-guard.d.ts.map +1 -0
- package/dist/utils/agent-guard.js +326 -0
- package/dist/utils/agent-guard.js.map +1 -0
- package/dist/utils/agent-session-launcher.d.ts +89 -0
- package/dist/utils/agent-session-launcher.d.ts.map +1 -0
- package/dist/utils/agent-session-launcher.js +308 -0
- package/dist/utils/agent-session-launcher.js.map +1 -0
- package/dist/utils/bash-command-analysis.d.ts +19 -0
- package/dist/utils/bash-command-analysis.d.ts.map +1 -0
- package/dist/utils/bash-command-analysis.js +295 -0
- package/dist/utils/bash-command-analysis.js.map +1 -0
- package/dist/utils/consequence-nudges.d.ts +30 -0
- package/dist/utils/consequence-nudges.d.ts.map +1 -0
- package/dist/utils/consequence-nudges.js +313 -0
- package/dist/utils/consequence-nudges.js.map +1 -0
- package/dist/utils/drift-intelligence.d.ts.map +1 -1
- package/dist/utils/drift-intelligence.js +29 -7
- package/dist/utils/drift-intelligence.js.map +1 -1
- package/dist/utils/git-coverage.d.ts +57 -0
- package/dist/utils/git-coverage.d.ts.map +1 -0
- package/dist/utils/git-coverage.js +302 -0
- package/dist/utils/git-coverage.js.map +1 -0
- package/dist/utils/gitignore.d.ts.map +1 -1
- package/dist/utils/gitignore.js +2 -1
- package/dist/utils/gitignore.js.map +1 -1
- package/dist/utils/governed-intent.d.ts +10 -0
- package/dist/utils/governed-intent.d.ts.map +1 -0
- package/dist/utils/governed-intent.js +108 -0
- package/dist/utils/governed-intent.js.map +1 -0
- package/dist/utils/hook-heartbeat.d.ts +55 -0
- package/dist/utils/hook-heartbeat.d.ts.map +1 -0
- package/dist/utils/hook-heartbeat.js +116 -0
- package/dist/utils/hook-heartbeat.js.map +1 -0
- package/dist/utils/intent-continuity.d.ts +21 -0
- package/dist/utils/intent-continuity.d.ts.map +1 -0
- package/dist/utils/intent-continuity.js +192 -0
- package/dist/utils/intent-continuity.js.map +1 -0
- package/dist/utils/messages.d.ts +1 -1
- package/dist/utils/messages.d.ts.map +1 -1
- package/dist/utils/messages.js +24 -21
- package/dist/utils/messages.js.map +1 -1
- package/dist/utils/runtime-companion.d.ts +137 -0
- package/dist/utils/runtime-companion.d.ts.map +1 -0
- package/dist/utils/runtime-companion.js +231 -0
- package/dist/utils/runtime-companion.js.map +1 -0
- package/dist/utils/runtime-connection.d.ts +46 -0
- package/dist/utils/runtime-connection.d.ts.map +1 -0
- package/dist/utils/runtime-connection.js +148 -0
- package/dist/utils/runtime-connection.js.map +1 -0
- package/dist/utils/runtime-evidence.d.ts +68 -0
- package/dist/utils/runtime-evidence.d.ts.map +1 -0
- package/dist/utils/runtime-evidence.js +248 -0
- package/dist/utils/runtime-evidence.js.map +1 -0
- package/dist/utils/runtime-live.d.ts +33 -0
- package/dist/utils/runtime-live.d.ts.map +1 -0
- package/dist/utils/runtime-live.js +361 -0
- package/dist/utils/runtime-live.js.map +1 -0
- package/dist/utils/runtime-outbox.d.ts +76 -0
- package/dist/utils/runtime-outbox.d.ts.map +1 -0
- package/dist/utils/runtime-outbox.js +410 -0
- package/dist/utils/runtime-outbox.js.map +1 -0
- package/dist/utils/runtime-receipt.d.ts +50 -0
- package/dist/utils/runtime-receipt.d.ts.map +1 -0
- package/dist/utils/runtime-receipt.js +223 -0
- package/dist/utils/runtime-receipt.js.map +1 -0
- package/dist/utils/state.d.ts +21 -0
- package/dist/utils/state.d.ts.map +1 -1
- package/dist/utils/state.js +30 -0
- package/dist/utils/state.js.map +1 -1
- package/dist/utils/structural-understanding.d.ts +334 -0
- package/dist/utils/structural-understanding.d.ts.map +1 -0
- package/dist/utils/structural-understanding.js +2316 -0
- package/dist/utils/structural-understanding.js.map +1 -0
- package/dist/utils/v0-governance.d.ts +197 -0
- package/dist/utils/v0-governance.d.ts.map +1 -0
- package/dist/utils/v0-governance.js +904 -0
- package/dist/utils/v0-governance.js.map +1 -0
- package/package.json +5 -5
|
@@ -0,0 +1,2316 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.CONSEQUENCE_UNDERSTANDING_SCHEMA_VERSION = exports.STRUCTURAL_UNDERSTANDING_SCHEMA_VERSION = void 0;
|
|
37
|
+
exports.structuralUnderstandingPath = structuralUnderstandingPath;
|
|
38
|
+
exports.buildStructuralUnderstanding = buildStructuralUnderstanding;
|
|
39
|
+
exports.writeStructuralUnderstanding = writeStructuralUnderstanding;
|
|
40
|
+
exports.readStructuralUnderstanding = readStructuralUnderstanding;
|
|
41
|
+
const crypto_1 = require("crypto");
|
|
42
|
+
const fs_1 = require("fs");
|
|
43
|
+
const path_1 = require("path");
|
|
44
|
+
const ts = __importStar(require("typescript"));
|
|
45
|
+
const governance_runtime_1 = require("@neurcode-ai/governance-runtime");
|
|
46
|
+
const import_edge_extractor_1 = require("./import-edge-extractor");
|
|
47
|
+
exports.STRUCTURAL_UNDERSTANDING_SCHEMA_VERSION = 'neurcode.structural-understanding.v1';
|
|
48
|
+
exports.CONSEQUENCE_UNDERSTANDING_SCHEMA_VERSION = 'neurcode.consequence-understanding.v1';
|
|
49
|
+
const TS_LIKE = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/i;
|
|
50
|
+
const SKIP_DIR = /^(node_modules|\.git|dist|build|out|coverage|\.next|vendor)$/i;
|
|
51
|
+
const EFFECT_REGISTRY = [
|
|
52
|
+
{
|
|
53
|
+
category: 'filesystem-write',
|
|
54
|
+
callees: new Set([
|
|
55
|
+
'writeFile',
|
|
56
|
+
'writeFileSync',
|
|
57
|
+
'appendFile',
|
|
58
|
+
'appendFileSync',
|
|
59
|
+
'mkdir',
|
|
60
|
+
'mkdirSync',
|
|
61
|
+
'rename',
|
|
62
|
+
'renameSync',
|
|
63
|
+
'rm',
|
|
64
|
+
'rmSync',
|
|
65
|
+
'unlink',
|
|
66
|
+
'unlinkSync',
|
|
67
|
+
]),
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
category: 'session-evidence-write',
|
|
71
|
+
callees: new Set(['writeAIChangeRecord', 'writeStructuralUnderstanding', 'appendEvent', 'finishSession']),
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
category: 'network-send',
|
|
75
|
+
callees: new Set(['fetch', 'sendBeacon']),
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
const GENERATED_FINGERPRINTS = [
|
|
79
|
+
{ needle: 'webpackBootstrap', reasonCode: 'webpack_bootstrap' },
|
|
80
|
+
{ needle: '__webpack_modules__', reasonCode: 'webpack_modules' },
|
|
81
|
+
{ needle: 'sourceMappingURL=', reasonCode: 'source_map_marker' },
|
|
82
|
+
{ needle: '@generated', reasonCode: 'generated_marker' },
|
|
83
|
+
{ needle: 'Code generated by', reasonCode: 'generated_marker' },
|
|
84
|
+
];
|
|
85
|
+
function stableStringify(value) {
|
|
86
|
+
if (Array.isArray(value))
|
|
87
|
+
return `[${value.map(stableStringify).join(',')}]`;
|
|
88
|
+
if (value && typeof value === 'object') {
|
|
89
|
+
return `{${Object.entries(value)
|
|
90
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
91
|
+
.map(([key, item]) => `${JSON.stringify(key)}:${stableStringify(item)}`)
|
|
92
|
+
.join(',')}}`;
|
|
93
|
+
}
|
|
94
|
+
return JSON.stringify(value);
|
|
95
|
+
}
|
|
96
|
+
function hash(value, length = 24) {
|
|
97
|
+
return (0, crypto_1.createHash)('sha256').update(typeof value === 'string' ? value : stableStringify(value)).digest('hex').slice(0, length);
|
|
98
|
+
}
|
|
99
|
+
function repoRel(projectRoot, absPath) {
|
|
100
|
+
return (0, path_1.relative)(projectRoot, absPath).split(path_1.sep).join('/');
|
|
101
|
+
}
|
|
102
|
+
function normalizeRepoPath(value) {
|
|
103
|
+
return value.replace(/\\/g, '/').replace(/^\.\//, '').trim();
|
|
104
|
+
}
|
|
105
|
+
function isTestFile(file) {
|
|
106
|
+
const normalized = normalizeRepoPath(file).toLowerCase();
|
|
107
|
+
return (normalized.includes('/__tests__/') ||
|
|
108
|
+
normalized.includes('/test/') ||
|
|
109
|
+
normalized.includes('/tests/') ||
|
|
110
|
+
/\.(test|spec)\.[tj]sx?$/.test(normalized));
|
|
111
|
+
}
|
|
112
|
+
function uniqueSorted(values) {
|
|
113
|
+
return Array.from(new Set(Array.from(values, (value) => String(value ?? '').trim()).filter(Boolean)))
|
|
114
|
+
.sort((a, b) => a.localeCompare(b));
|
|
115
|
+
}
|
|
116
|
+
function emptyDigest(generatedAt, changedSymbolCount = 0, referenceCount = 0, testReferenceCount = 0) {
|
|
117
|
+
return {
|
|
118
|
+
schemaVersion: 'neurcode.structural-digest.v1',
|
|
119
|
+
generatedAt,
|
|
120
|
+
provenance: 'deterministic-ranking',
|
|
121
|
+
modelUsed: false,
|
|
122
|
+
topSymbols: [],
|
|
123
|
+
topConsequences: [],
|
|
124
|
+
topReferences: [],
|
|
125
|
+
hidden: {
|
|
126
|
+
references: referenceCount,
|
|
127
|
+
testReferences: testReferenceCount,
|
|
128
|
+
lowSignalReferences: referenceCount,
|
|
129
|
+
sameFileReferences: 0,
|
|
130
|
+
},
|
|
131
|
+
summary: {
|
|
132
|
+
changedSymbolCount,
|
|
133
|
+
referenceCount,
|
|
134
|
+
topConsequenceCount: 0,
|
|
135
|
+
topReferenceCount: 0,
|
|
136
|
+
crossPackageReferenceCount: 0,
|
|
137
|
+
nonTestReferenceCount: Math.max(0, referenceCount - testReferenceCount),
|
|
138
|
+
testReferenceCount,
|
|
139
|
+
},
|
|
140
|
+
limitations: [
|
|
141
|
+
'Digest ranking is deterministic and facts-only; it is not an LLM judgment.',
|
|
142
|
+
'No source text, diff hunks, patch content, or shell command bodies are stored.',
|
|
143
|
+
],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function artifactHashInput(artifact) {
|
|
147
|
+
const maybeConsequence = artifact['consequenceUnderstanding'];
|
|
148
|
+
return {
|
|
149
|
+
...artifact,
|
|
150
|
+
generatedAt: null,
|
|
151
|
+
artifactHash: null,
|
|
152
|
+
digest: artifact.digest ? { ...artifact.digest, generatedAt: null } : artifact.digest,
|
|
153
|
+
consequenceUnderstanding: maybeConsequence && typeof maybeConsequence === 'object'
|
|
154
|
+
? { ...maybeConsequence, generatedAt: null, artifactHash: null }
|
|
155
|
+
: maybeConsequence,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function consequenceHashInput(artifact) {
|
|
159
|
+
return { ...artifact, generatedAt: null, artifactHash: null };
|
|
160
|
+
}
|
|
161
|
+
function emptyConsequenceUnderstanding(generatedAt, reason = null) {
|
|
162
|
+
const core = {
|
|
163
|
+
schemaVersion: exports.CONSEQUENCE_UNDERSTANDING_SCHEMA_VERSION,
|
|
164
|
+
generatedAt,
|
|
165
|
+
analyzed: reason === null,
|
|
166
|
+
reason,
|
|
167
|
+
confidence: reason === null ? 'deterministic-static' : 'not-analyzed',
|
|
168
|
+
modelUsed: false,
|
|
169
|
+
effectDeltas: [],
|
|
170
|
+
contractDeltas: [],
|
|
171
|
+
inheritorProjections: [],
|
|
172
|
+
topFindings: [],
|
|
173
|
+
topImpacts: [],
|
|
174
|
+
summary: {
|
|
175
|
+
effectDeltaCount: 0,
|
|
176
|
+
contractDeltaCount: 0,
|
|
177
|
+
inheritorProjectionCount: 0,
|
|
178
|
+
topFindingCount: 0,
|
|
179
|
+
topImpactCount: 0,
|
|
180
|
+
escapingConsequenceCount: 0,
|
|
181
|
+
highestExternalConsumerCount: 0,
|
|
182
|
+
headline: '0 symbols changed; 0 escaping TypeScript-static consequences found (effects checked: allowlisted filesystem/network/session evidence; SQL, dependency injection, dynamic dispatch, route wiring, and cross-language behavior not analyzed).',
|
|
183
|
+
},
|
|
184
|
+
limitations: [
|
|
185
|
+
'Known-effect detection is allowlist-bounded; effects via unrecognized wrappers, dynamic dispatch, or dependency injection are not claimed.',
|
|
186
|
+
'Contract detection is TypeScript/JavaScript static analysis only.',
|
|
187
|
+
'No LLM or probabilistic judgment is used in this artifact.',
|
|
188
|
+
'No source text, diff hunks, patch content, or shell command bodies are stored.',
|
|
189
|
+
],
|
|
190
|
+
factProvenance: {
|
|
191
|
+
effectDeltas: 'effect-registry',
|
|
192
|
+
contractDeltas: 'typescript-checker',
|
|
193
|
+
inheritorProjections: 'inheritance-projection',
|
|
194
|
+
topFindings: 'deterministic-ranking',
|
|
195
|
+
topImpacts: 'deterministic-impact-grouping',
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
return { ...core, artifactHash: hash(consequenceHashInput(core)) };
|
|
199
|
+
}
|
|
200
|
+
function none(projectRoot, diffFiles, reason, generatedAt, suppressedArtifacts = []) {
|
|
201
|
+
const core = {
|
|
202
|
+
schemaVersion: exports.STRUCTURAL_UNDERSTANDING_SCHEMA_VERSION,
|
|
203
|
+
generatedAt,
|
|
204
|
+
repoRootHash: hash(projectRoot),
|
|
205
|
+
privacy: privacyBlock(),
|
|
206
|
+
analysis: {
|
|
207
|
+
language: 'typescript',
|
|
208
|
+
analyzed: false,
|
|
209
|
+
reason,
|
|
210
|
+
confidence: 'not-analyzed',
|
|
211
|
+
filesAnalyzed: 0,
|
|
212
|
+
changedFileCount: diffFiles.length,
|
|
213
|
+
changedSymbolCount: 0,
|
|
214
|
+
referenceCount: 0,
|
|
215
|
+
testReferenceCount: 0,
|
|
216
|
+
},
|
|
217
|
+
changedFiles: changedFileFacts(diffFiles),
|
|
218
|
+
changedSymbols: [],
|
|
219
|
+
references: [],
|
|
220
|
+
importEdgesChanged: importEdgeFacts(diffFiles, suppressedArtifacts),
|
|
221
|
+
suppressedArtifacts,
|
|
222
|
+
testReferences: [],
|
|
223
|
+
digest: emptyDigest(generatedAt),
|
|
224
|
+
consequenceUnderstanding: emptyConsequenceUnderstanding(generatedAt, reason),
|
|
225
|
+
planAlignment: null,
|
|
226
|
+
boundaryImpact: [],
|
|
227
|
+
limitations: commonLimitations(),
|
|
228
|
+
factProvenance: provenanceMap(),
|
|
229
|
+
};
|
|
230
|
+
return { ...core, artifactHash: hash(artifactHashInput(core)) };
|
|
231
|
+
}
|
|
232
|
+
function privacyBlock() {
|
|
233
|
+
return {
|
|
234
|
+
sourceUploaded: false,
|
|
235
|
+
sourceStored: false,
|
|
236
|
+
diffStored: false,
|
|
237
|
+
modelUsed: false,
|
|
238
|
+
factsOnly: true,
|
|
239
|
+
outputContains: ['repo-relative paths', 'symbol names', 'line numbers', 'import targets', 'owner tokens', 'hashes'],
|
|
240
|
+
outputOmits: ['source text', 'diff hunks', 'patch content', 'shell command bodies', 'model judgments'],
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function commonLimitations() {
|
|
244
|
+
return [
|
|
245
|
+
'TypeScript/JavaScript static analysis only.',
|
|
246
|
+
'Dynamic dispatch, reflection, dependency injection, runtime wiring, and generated code may be under-counted.',
|
|
247
|
+
'Test references mean a test file statically references a changed symbol; this does not claim behavioral coverage.',
|
|
248
|
+
'No LLM or probabilistic judgment is used in this artifact.',
|
|
249
|
+
];
|
|
250
|
+
}
|
|
251
|
+
function provenanceMap() {
|
|
252
|
+
return {
|
|
253
|
+
changedFiles: 'git-diff',
|
|
254
|
+
changedSymbols: 'typescript-compiler',
|
|
255
|
+
references: 'typescript-compiler',
|
|
256
|
+
importEdgesChanged: 'git-diff',
|
|
257
|
+
testReferences: 'typescript-compiler',
|
|
258
|
+
suppressedArtifacts: 'generated-artifact-heuristic',
|
|
259
|
+
digest: 'deterministic-ranking',
|
|
260
|
+
consequenceUnderstanding: 'typescript-checker',
|
|
261
|
+
planAlignment: 'session-plan',
|
|
262
|
+
boundaryImpact: 'codeowners-profile',
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
function changedFileFacts(diffFiles) {
|
|
266
|
+
return diffFiles.map((file) => ({
|
|
267
|
+
path: normalizeRepoPath(file.path),
|
|
268
|
+
changeType: file.changeType,
|
|
269
|
+
addedLines: file.addedLines || 0,
|
|
270
|
+
removedLines: file.removedLines || 0,
|
|
271
|
+
provenance: 'git-diff',
|
|
272
|
+
})).sort((a, b) => a.path.localeCompare(b.path));
|
|
273
|
+
}
|
|
274
|
+
function importEdgeFacts(diffFiles, suppressedArtifacts = []) {
|
|
275
|
+
const suppressed = new Set(suppressedArtifacts.map((item) => item.path));
|
|
276
|
+
return (0, import_edge_extractor_1.extractImportEdgesFromDiff)(diffFiles.filter((file) => !suppressed.has(normalizeRepoPath(file.path))))
|
|
277
|
+
.map((edge) => ({
|
|
278
|
+
sourceFile: normalizeRepoPath(edge.sourceFile),
|
|
279
|
+
sourceLine: edge.sourceLine,
|
|
280
|
+
importTarget: edge.importTarget,
|
|
281
|
+
importKind: edge.importKind,
|
|
282
|
+
language: edge.language,
|
|
283
|
+
provenance: 'git-diff',
|
|
284
|
+
}))
|
|
285
|
+
.sort((a, b) => a.sourceFile.localeCompare(b.sourceFile) ||
|
|
286
|
+
a.sourceLine - b.sourceLine ||
|
|
287
|
+
a.importTarget.localeCompare(b.importTarget));
|
|
288
|
+
}
|
|
289
|
+
function loadNeurcodeIgnorePatterns(projectRoot) {
|
|
290
|
+
const path = (0, path_1.join)(projectRoot, '.neurcodeignore');
|
|
291
|
+
if (!(0, fs_1.existsSync)(path))
|
|
292
|
+
return [];
|
|
293
|
+
try {
|
|
294
|
+
return (0, fs_1.readFileSync)(path, 'utf8')
|
|
295
|
+
.split(/\r?\n/)
|
|
296
|
+
.map((line) => line.trim())
|
|
297
|
+
.filter((line) => line.length > 0 && !line.startsWith('#'))
|
|
298
|
+
.map((line) => normalizeRepoPath(line.replace(/^\//, '')))
|
|
299
|
+
.map((line) => line.endsWith('/') ? `${line}**` : line);
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
return [];
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function classifyGeneratedArtifact(projectRoot, file, ignorePatterns) {
|
|
306
|
+
const normalized = normalizeRepoPath(file);
|
|
307
|
+
if (ignorePatterns.some((pattern) => matchesSimpleGlob(normalized, pattern) || normalized === pattern)) {
|
|
308
|
+
return { path: normalized, reasonCode: 'neurcodeignore', provenance: 'generated-artifact-heuristic' };
|
|
309
|
+
}
|
|
310
|
+
const segments = normalized.split('/');
|
|
311
|
+
if (segments.includes('dist'))
|
|
312
|
+
return { path: normalized, reasonCode: 'dist_directory', provenance: 'generated-artifact-heuristic' };
|
|
313
|
+
if (segments.includes('build'))
|
|
314
|
+
return { path: normalized, reasonCode: 'build_directory', provenance: 'generated-artifact-heuristic' };
|
|
315
|
+
if (segments.includes('out'))
|
|
316
|
+
return { path: normalized, reasonCode: 'out_directory', provenance: 'generated-artifact-heuristic' };
|
|
317
|
+
if (/\.min\.js$/i.test(normalized))
|
|
318
|
+
return { path: normalized, reasonCode: 'minified_javascript', provenance: 'generated-artifact-heuristic' };
|
|
319
|
+
if (/\.bundle\.js$/i.test(normalized))
|
|
320
|
+
return { path: normalized, reasonCode: 'bundled_javascript', provenance: 'generated-artifact-heuristic' };
|
|
321
|
+
const abs = (0, path_1.join)(projectRoot, normalized);
|
|
322
|
+
if (!(0, fs_1.existsSync)(abs))
|
|
323
|
+
return null;
|
|
324
|
+
let firstLines;
|
|
325
|
+
try {
|
|
326
|
+
firstLines = (0, fs_1.readFileSync)(abs, 'utf8').split(/\r?\n/, 4).join('\n');
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
for (const fingerprint of GENERATED_FINGERPRINTS) {
|
|
332
|
+
if (firstLines.includes(fingerprint.needle)) {
|
|
333
|
+
return { path: normalized, reasonCode: fingerprint.reasonCode, provenance: 'generated-artifact-heuristic' };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
function classifyGeneratedArtifacts(projectRoot, changedFiles) {
|
|
339
|
+
const ignorePatterns = loadNeurcodeIgnorePatterns(projectRoot);
|
|
340
|
+
const byPath = new Map();
|
|
341
|
+
for (const file of changedFiles) {
|
|
342
|
+
const item = classifyGeneratedArtifact(projectRoot, file, ignorePatterns);
|
|
343
|
+
if (item)
|
|
344
|
+
byPath.set(item.path, item);
|
|
345
|
+
}
|
|
346
|
+
return [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
347
|
+
}
|
|
348
|
+
function scopeRoots(projectRoot, changedRepoRelFiles) {
|
|
349
|
+
const roots = new Set();
|
|
350
|
+
for (const file of changedRepoRelFiles) {
|
|
351
|
+
const parts = file.split('/');
|
|
352
|
+
const pkgIdx = parts.indexOf('packages');
|
|
353
|
+
const servicesIdx = parts.indexOf('services');
|
|
354
|
+
const webIdx = parts.indexOf('web');
|
|
355
|
+
const appsIdx = parts.indexOf('apps');
|
|
356
|
+
const srcIdx = parts.indexOf('src');
|
|
357
|
+
let depth;
|
|
358
|
+
if (pkgIdx >= 0 && parts[pkgIdx + 1] !== undefined)
|
|
359
|
+
depth = pkgIdx + 2;
|
|
360
|
+
else if (servicesIdx >= 0 && parts[servicesIdx + 1] !== undefined)
|
|
361
|
+
depth = servicesIdx + 2;
|
|
362
|
+
else if (webIdx >= 0 && parts[webIdx + 1] !== undefined)
|
|
363
|
+
depth = webIdx + 2;
|
|
364
|
+
else if (appsIdx >= 0 && parts[appsIdx + 1] !== undefined)
|
|
365
|
+
depth = appsIdx + 2;
|
|
366
|
+
else if (srcIdx >= 0)
|
|
367
|
+
depth = srcIdx + 1;
|
|
368
|
+
else
|
|
369
|
+
depth = Math.max(1, parts.length - 1);
|
|
370
|
+
roots.add((0, path_1.join)(projectRoot, parts.slice(0, depth).join('/')));
|
|
371
|
+
}
|
|
372
|
+
return [...roots].filter((root) => (0, fs_1.existsSync)(root));
|
|
373
|
+
}
|
|
374
|
+
function gatherSourceFiles(dir, out, cap) {
|
|
375
|
+
if (out.length > cap)
|
|
376
|
+
return;
|
|
377
|
+
let names;
|
|
378
|
+
try {
|
|
379
|
+
names = (0, fs_1.readdirSync)(dir);
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
for (const name of names) {
|
|
385
|
+
if (out.length > cap)
|
|
386
|
+
return;
|
|
387
|
+
const path = (0, path_1.join)(dir, name);
|
|
388
|
+
let stat;
|
|
389
|
+
try {
|
|
390
|
+
stat = (0, fs_1.statSync)(path);
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (stat.isDirectory()) {
|
|
396
|
+
if (!SKIP_DIR.test(name))
|
|
397
|
+
gatherSourceFiles(path, out, cap);
|
|
398
|
+
}
|
|
399
|
+
else if (TS_LIKE.test(name) && !/\.d\.ts$/i.test(name)) {
|
|
400
|
+
out.push(path);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
/** Discover workspace packages and their internal dependency edges from package.json. */
|
|
405
|
+
function loadWorkspacePackages(projectRoot) {
|
|
406
|
+
const groups = ['packages', 'services', 'web', 'apps'];
|
|
407
|
+
const packages = [];
|
|
408
|
+
const names = new Set();
|
|
409
|
+
for (const group of groups) {
|
|
410
|
+
let entries;
|
|
411
|
+
try {
|
|
412
|
+
entries = (0, fs_1.readdirSync)((0, path_1.join)(projectRoot, group));
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
for (const entry of entries) {
|
|
418
|
+
const dirRel = `${group}/${entry}`;
|
|
419
|
+
const absDir = (0, path_1.join)(projectRoot, group, entry);
|
|
420
|
+
const pjPath = (0, path_1.join)(absDir, 'package.json');
|
|
421
|
+
if (!(0, fs_1.existsSync)(pjPath))
|
|
422
|
+
continue;
|
|
423
|
+
let pj;
|
|
424
|
+
try {
|
|
425
|
+
pj = JSON.parse((0, fs_1.readFileSync)(pjPath, 'utf8'));
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
const name = typeof pj.name === 'string' ? pj.name : null;
|
|
431
|
+
if (!name)
|
|
432
|
+
continue;
|
|
433
|
+
const srcEntryRel = ['src/index.ts', 'src/index.tsx', 'src/index.js']
|
|
434
|
+
.map((file) => `${dirRel}/${file}`)
|
|
435
|
+
.find((file) => (0, fs_1.existsSync)((0, path_1.join)(projectRoot, file))) ?? null;
|
|
436
|
+
const deps = new Set();
|
|
437
|
+
for (const key of ['dependencies', 'devDependencies', 'peerDependencies']) {
|
|
438
|
+
const block = pj[key];
|
|
439
|
+
if (block && typeof block === 'object') {
|
|
440
|
+
for (const dep of Object.keys(block))
|
|
441
|
+
deps.add(dep);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
packages.push({ name, dirRel, absDir, srcEntryRel, deps });
|
|
445
|
+
names.add(name);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
// Retain only workspace-internal dependency edges.
|
|
449
|
+
for (const pkg of packages) {
|
|
450
|
+
pkg.deps = new Set([...pkg.deps].filter((dep) => names.has(dep)));
|
|
451
|
+
}
|
|
452
|
+
return packages;
|
|
453
|
+
}
|
|
454
|
+
/** Which workspace packages contain the changed files (most specific dir wins). */
|
|
455
|
+
function changedWorkspacePackages(workspace, changedFiles) {
|
|
456
|
+
const byName = new Map();
|
|
457
|
+
for (const file of changedFiles) {
|
|
458
|
+
let best = null;
|
|
459
|
+
for (const pkg of workspace) {
|
|
460
|
+
if (file === pkg.dirRel || file.startsWith(`${pkg.dirRel}/`)) {
|
|
461
|
+
if (!best || pkg.dirRel.length > best.dirRel.length)
|
|
462
|
+
best = pkg;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (best)
|
|
466
|
+
byName.set(best.name, best);
|
|
467
|
+
}
|
|
468
|
+
return [...byName.values()];
|
|
469
|
+
}
|
|
470
|
+
/** Packages that (transitively) depend on any changed package — the reverse-dep closure. */
|
|
471
|
+
function reverseDepConsumers(workspace, changedNames) {
|
|
472
|
+
const consumers = new Set();
|
|
473
|
+
let added = true;
|
|
474
|
+
while (added) {
|
|
475
|
+
added = false;
|
|
476
|
+
for (const pkg of workspace) {
|
|
477
|
+
if (changedNames.has(pkg.name) || consumers.has(pkg.name))
|
|
478
|
+
continue;
|
|
479
|
+
for (const dep of pkg.deps) {
|
|
480
|
+
if (changedNames.has(dep) || consumers.has(dep)) {
|
|
481
|
+
consumers.add(pkg.name);
|
|
482
|
+
added = true;
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return workspace.filter((pkg) => consumers.has(pkg.name));
|
|
489
|
+
}
|
|
490
|
+
/** Source files in a consumer package that directly import a changed package by name. */
|
|
491
|
+
function gatherDirectImporterFiles(consumer, changedPackageNames, out, cap) {
|
|
492
|
+
const srcDir = (0, path_1.join)(consumer.absDir, 'src');
|
|
493
|
+
const base = (0, fs_1.existsSync)(srcDir) ? srcDir : consumer.absDir;
|
|
494
|
+
const candidates = [];
|
|
495
|
+
gatherSourceFiles(base, candidates, cap);
|
|
496
|
+
for (const file of candidates) {
|
|
497
|
+
if (out.length > cap)
|
|
498
|
+
return;
|
|
499
|
+
let text;
|
|
500
|
+
try {
|
|
501
|
+
text = (0, fs_1.readFileSync)(file, 'utf8');
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
if (changedPackageNames.some((name) => text.includes(`'${name}'`) ||
|
|
507
|
+
text.includes(`"${name}"`) ||
|
|
508
|
+
text.includes(`'${name}/`) ||
|
|
509
|
+
text.includes(`"${name}/`))) {
|
|
510
|
+
out.push(file);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
/** Map workspace package imports to their source entry so refs resolve to src, not dist/*.d.ts. */
|
|
515
|
+
function buildWorkspacePaths(workspace) {
|
|
516
|
+
const paths = {};
|
|
517
|
+
for (const pkg of workspace) {
|
|
518
|
+
if (!pkg.srcEntryRel)
|
|
519
|
+
continue;
|
|
520
|
+
paths[pkg.name] = [pkg.srcEntryRel];
|
|
521
|
+
paths[`${pkg.name}/*`] = [`${pkg.dirRel}/src/*`];
|
|
522
|
+
}
|
|
523
|
+
return paths;
|
|
524
|
+
}
|
|
525
|
+
function resolveModuleRepoFile(projectRoot, containingFile, moduleSpecifier, compilerOptions) {
|
|
526
|
+
const resolved = ts.resolveModuleName(moduleSpecifier, containingFile.fileName, compilerOptions, ts.sys).resolvedModule?.resolvedFileName;
|
|
527
|
+
if (!resolved || /\.d\.ts$/i.test(resolved))
|
|
528
|
+
return null;
|
|
529
|
+
const normalized = normalizeRepoPath(repoRel(projectRoot, resolved));
|
|
530
|
+
return normalized.startsWith('..') ? null : normalized;
|
|
531
|
+
}
|
|
532
|
+
function hasDefaultExportModifier(node) {
|
|
533
|
+
return Boolean(ts.canHaveModifiers(node) && ts.getModifiers(node)?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword));
|
|
534
|
+
}
|
|
535
|
+
function buildImportExportTargetGraph(input) {
|
|
536
|
+
const sourceFilesByRel = new Map();
|
|
537
|
+
for (const sourceFile of input.program.getSourceFiles()) {
|
|
538
|
+
if (sourceFile.isDeclarationFile || sourceFile.fileName.includes('node_modules'))
|
|
539
|
+
continue;
|
|
540
|
+
sourceFilesByRel.set(repoRel(input.projectRoot, sourceFile.fileName), sourceFile);
|
|
541
|
+
}
|
|
542
|
+
const targetByFileAndName = new Map();
|
|
543
|
+
const localExportsByFile = new Map();
|
|
544
|
+
for (const target of input.targets) {
|
|
545
|
+
targetByFileAndName.set(`${target.file}\0${target.name}`, target);
|
|
546
|
+
if (!target.declaration || !isExportedDeclaration(target.declaration))
|
|
547
|
+
continue;
|
|
548
|
+
const bucket = localExportsByFile.get(target.file) ?? new Map();
|
|
549
|
+
bucket.set(target.name, target);
|
|
550
|
+
if (hasDefaultExportModifier(target.declaration))
|
|
551
|
+
bucket.set('default', target);
|
|
552
|
+
localExportsByFile.set(target.file, bucket);
|
|
553
|
+
}
|
|
554
|
+
const exportTargetsByFile = new Map();
|
|
555
|
+
const resolveExportsForFile = (file, visiting = new Set()) => {
|
|
556
|
+
const cached = exportTargetsByFile.get(file);
|
|
557
|
+
if (cached)
|
|
558
|
+
return cached;
|
|
559
|
+
if (visiting.has(file))
|
|
560
|
+
return new Map();
|
|
561
|
+
visiting.add(file);
|
|
562
|
+
const out = new Map(localExportsByFile.get(file) ?? []);
|
|
563
|
+
const sourceFile = sourceFilesByRel.get(file);
|
|
564
|
+
if (sourceFile) {
|
|
565
|
+
for (const statement of sourceFile.statements) {
|
|
566
|
+
if (!ts.isExportDeclaration(statement))
|
|
567
|
+
continue;
|
|
568
|
+
const moduleFile = statement.moduleSpecifier && ts.isStringLiteralLike(statement.moduleSpecifier)
|
|
569
|
+
? resolveModuleRepoFile(input.projectRoot, sourceFile, statement.moduleSpecifier.text, input.compilerOptions)
|
|
570
|
+
: file;
|
|
571
|
+
const moduleExports = moduleFile ? resolveExportsForFile(moduleFile, new Set(visiting)) : out;
|
|
572
|
+
if (statement.exportClause && ts.isNamedExports(statement.exportClause)) {
|
|
573
|
+
for (const specifier of statement.exportClause.elements) {
|
|
574
|
+
const importedName = (specifier.propertyName ?? specifier.name).text;
|
|
575
|
+
const exportedName = specifier.name.text;
|
|
576
|
+
const target = moduleExports.get(importedName) ?? targetByFileAndName.get(`${moduleFile ?? file}\0${importedName}`);
|
|
577
|
+
if (target)
|
|
578
|
+
out.set(exportedName, target);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
else if (!statement.exportClause && moduleFile) {
|
|
582
|
+
for (const [name, target] of moduleExports) {
|
|
583
|
+
if (name !== 'default')
|
|
584
|
+
out.set(name, target);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
exportTargetsByFile.set(file, out);
|
|
590
|
+
visiting.delete(file);
|
|
591
|
+
return out;
|
|
592
|
+
};
|
|
593
|
+
for (const file of sourceFilesByRel.keys())
|
|
594
|
+
resolveExportsForFile(file);
|
|
595
|
+
const importsByFile = new Map();
|
|
596
|
+
const namespaceImportsByFile = new Map();
|
|
597
|
+
for (const [file, sourceFile] of sourceFilesByRel) {
|
|
598
|
+
const imports = new Map();
|
|
599
|
+
const namespaces = new Map();
|
|
600
|
+
for (const statement of sourceFile.statements) {
|
|
601
|
+
if (!ts.isImportDeclaration(statement) || !statement.importClause)
|
|
602
|
+
continue;
|
|
603
|
+
if (!ts.isStringLiteralLike(statement.moduleSpecifier))
|
|
604
|
+
continue;
|
|
605
|
+
const moduleFile = resolveModuleRepoFile(input.projectRoot, sourceFile, statement.moduleSpecifier.text, input.compilerOptions);
|
|
606
|
+
if (!moduleFile)
|
|
607
|
+
continue;
|
|
608
|
+
const moduleExports = resolveExportsForFile(moduleFile);
|
|
609
|
+
const importClause = statement.importClause;
|
|
610
|
+
if (importClause.name) {
|
|
611
|
+
const target = moduleExports.get('default') ?? moduleExports.get(importClause.name.text);
|
|
612
|
+
if (target)
|
|
613
|
+
imports.set(importClause.name.text, target);
|
|
614
|
+
}
|
|
615
|
+
const namedBindings = importClause.namedBindings;
|
|
616
|
+
if (namedBindings && ts.isNamedImports(namedBindings)) {
|
|
617
|
+
for (const specifier of namedBindings.elements) {
|
|
618
|
+
const importedName = (specifier.propertyName ?? specifier.name).text;
|
|
619
|
+
const target = moduleExports.get(importedName);
|
|
620
|
+
if (target)
|
|
621
|
+
imports.set(specifier.name.text, target);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
else if (namedBindings && ts.isNamespaceImport(namedBindings)) {
|
|
625
|
+
namespaces.set(namedBindings.name.text, moduleExports);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if (imports.size > 0)
|
|
629
|
+
importsByFile.set(file, imports);
|
|
630
|
+
if (namespaces.size > 0)
|
|
631
|
+
namespaceImportsByFile.set(file, namespaces);
|
|
632
|
+
}
|
|
633
|
+
return { importsByFile, namespaceImportsByFile, exportTargetsByFile };
|
|
634
|
+
}
|
|
635
|
+
function importExportGraphTargetForIdentifier(node, sourceRel, graph) {
|
|
636
|
+
if (ts.isPropertyAccessExpression(node.parent) &&
|
|
637
|
+
node.parent.name === node &&
|
|
638
|
+
ts.isIdentifier(node.parent.expression)) {
|
|
639
|
+
const namespaces = graph.namespaceImportsByFile.get(sourceRel);
|
|
640
|
+
const namespaceExports = namespaces?.get(node.parent.expression.text);
|
|
641
|
+
const target = namespaceExports?.get(node.text);
|
|
642
|
+
if (target)
|
|
643
|
+
return target;
|
|
644
|
+
}
|
|
645
|
+
return graph.importsByFile.get(sourceRel)?.get(node.text);
|
|
646
|
+
}
|
|
647
|
+
function normalizedHunkLines(hunk) {
|
|
648
|
+
const out = [];
|
|
649
|
+
let oldCursor = hunk.oldStart ?? 1;
|
|
650
|
+
let newCursor = hunk.newStart ?? 1;
|
|
651
|
+
for (const line of (hunk.lines ?? [])) {
|
|
652
|
+
if (line.type === 'added') {
|
|
653
|
+
const newLine = line.lineNumber ?? newCursor;
|
|
654
|
+
out.push({ type: line.type, content: line.content ?? '', oldLine: null, newLine });
|
|
655
|
+
newCursor += 1;
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
if (line.type === 'removed') {
|
|
659
|
+
const oldLine = line.lineNumber ?? oldCursor;
|
|
660
|
+
out.push({ type: line.type, content: line.content ?? '', oldLine, newLine: null });
|
|
661
|
+
oldCursor += 1;
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
const explicitLine = typeof line.lineNumber === 'number' ? line.lineNumber : null;
|
|
665
|
+
out.push({
|
|
666
|
+
type: line.type,
|
|
667
|
+
content: line.content ?? '',
|
|
668
|
+
oldLine: explicitLine ?? oldCursor,
|
|
669
|
+
newLine: explicitLine ?? newCursor,
|
|
670
|
+
});
|
|
671
|
+
oldCursor += 1;
|
|
672
|
+
newCursor += 1;
|
|
673
|
+
}
|
|
674
|
+
return out;
|
|
675
|
+
}
|
|
676
|
+
function changedNewLines(file) {
|
|
677
|
+
const out = [];
|
|
678
|
+
for (const hunk of file.hunks ?? []) {
|
|
679
|
+
for (const line of normalizedHunkLines(hunk)) {
|
|
680
|
+
if (line.type === 'added' && line.newLine !== null)
|
|
681
|
+
out.push(line.newLine);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return uniqueSorted(out.map(String)).map(Number).filter((value) => Number.isFinite(value));
|
|
685
|
+
}
|
|
686
|
+
function addedLineSet(file) {
|
|
687
|
+
return new Set(changedNewLines(file));
|
|
688
|
+
}
|
|
689
|
+
function rightmostIdentifierText(node) {
|
|
690
|
+
if (ts.isIdentifier(node))
|
|
691
|
+
return node.text;
|
|
692
|
+
if (ts.isPropertyAccessExpression(node))
|
|
693
|
+
return rightmostIdentifierText(node.name);
|
|
694
|
+
if (ts.isElementAccessExpression(node) && ts.isStringLiteralLike(node.argumentExpression)) {
|
|
695
|
+
return node.argumentExpression.text;
|
|
696
|
+
}
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
function effectCategoryForCalleeName(name) {
|
|
700
|
+
if (!name)
|
|
701
|
+
return null;
|
|
702
|
+
for (const entry of EFFECT_REGISTRY) {
|
|
703
|
+
if (entry.callees.has(name))
|
|
704
|
+
return entry.category;
|
|
705
|
+
}
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
function effectMatchesFromLineContent(content) {
|
|
709
|
+
const out = [];
|
|
710
|
+
for (const entry of EFFECT_REGISTRY) {
|
|
711
|
+
for (const callee of entry.callees) {
|
|
712
|
+
const escaped = callee.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
713
|
+
if (new RegExp(`(?:\\b|\\.)${escaped}\\s*\\(`).test(content)) {
|
|
714
|
+
out.push({ calleeName: callee, effectCategory: entry.category });
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return out;
|
|
719
|
+
}
|
|
720
|
+
function diffFileByPath(diffFiles) {
|
|
721
|
+
return new Map(diffFiles.map((file) => [normalizeRepoPath(file.path), file]));
|
|
722
|
+
}
|
|
723
|
+
function oldFileTextFromDiff(projectRoot, diffFile) {
|
|
724
|
+
const path = normalizeRepoPath(diffFile.path);
|
|
725
|
+
const abs = (0, path_1.join)(projectRoot, path);
|
|
726
|
+
if (!(0, fs_1.existsSync)(abs))
|
|
727
|
+
return null;
|
|
728
|
+
let currentLines;
|
|
729
|
+
try {
|
|
730
|
+
currentLines = (0, fs_1.readFileSync)(abs, 'utf8').split(/\r?\n/);
|
|
731
|
+
}
|
|
732
|
+
catch {
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
735
|
+
const hunks = [...(diffFile.hunks ?? [])].sort((a, b) => (b.newStart ?? 1) - (a.newStart ?? 1));
|
|
736
|
+
for (const hunk of hunks) {
|
|
737
|
+
const newStart = Math.max(1, hunk.newStart ?? 1);
|
|
738
|
+
const newLines = Math.max(0, hunk.newLines ?? 0);
|
|
739
|
+
const oldLines = (hunk.lines ?? [])
|
|
740
|
+
.filter((line) => line.type === 'context' || line.type === 'removed')
|
|
741
|
+
.map((line) => line.content ?? '');
|
|
742
|
+
currentLines.splice(newStart - 1, newLines, ...oldLines);
|
|
743
|
+
}
|
|
744
|
+
return currentLines.join('\n');
|
|
745
|
+
}
|
|
746
|
+
function isExportedDeclaration(node) {
|
|
747
|
+
const hasExportModifier = (candidate) => Boolean(ts.canHaveModifiers(candidate) && ts.getModifiers(candidate)?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword));
|
|
748
|
+
if (hasExportModifier(node))
|
|
749
|
+
return true;
|
|
750
|
+
if (ts.isVariableDeclaration(node)) {
|
|
751
|
+
const statement = node.parent?.parent;
|
|
752
|
+
return Boolean(statement && hasExportModifier(statement));
|
|
753
|
+
}
|
|
754
|
+
return false;
|
|
755
|
+
}
|
|
756
|
+
function declarationContractShape(node) {
|
|
757
|
+
const name = declarationName(node);
|
|
758
|
+
const kind = declarationKind(node);
|
|
759
|
+
if (!name || !kind)
|
|
760
|
+
return null;
|
|
761
|
+
const sourceFile = node.getSourceFile();
|
|
762
|
+
const paramTypes = [];
|
|
763
|
+
let returnType = null;
|
|
764
|
+
if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) {
|
|
765
|
+
for (const param of node.parameters) {
|
|
766
|
+
paramTypes.push(param.type ? param.type.getText(sourceFile) : 'implicit');
|
|
767
|
+
}
|
|
768
|
+
returnType = node.type ? node.type.getText(sourceFile) : null;
|
|
769
|
+
}
|
|
770
|
+
else if (ts.isVariableDeclaration(node) && node.initializer && ts.isArrowFunction(node.initializer)) {
|
|
771
|
+
for (const param of node.initializer.parameters) {
|
|
772
|
+
paramTypes.push(param.type ? param.type.getText(sourceFile) : 'implicit');
|
|
773
|
+
}
|
|
774
|
+
returnType = node.initializer.type ? node.initializer.type.getText(sourceFile) : null;
|
|
775
|
+
}
|
|
776
|
+
const members = [];
|
|
777
|
+
const pushMember = (memberName) => {
|
|
778
|
+
if (!memberName)
|
|
779
|
+
return;
|
|
780
|
+
if (ts.isIdentifier(memberName) || ts.isStringLiteral(memberName) || ts.isNumericLiteral(memberName)) {
|
|
781
|
+
members.push(memberName.text);
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
if (ts.isInterfaceDeclaration(node) || ts.isClassDeclaration(node)) {
|
|
785
|
+
for (const member of node.members)
|
|
786
|
+
pushMember(member.name);
|
|
787
|
+
}
|
|
788
|
+
else if (ts.isTypeAliasDeclaration(node) && ts.isTypeLiteralNode(node.type)) {
|
|
789
|
+
for (const member of node.type.members)
|
|
790
|
+
pushMember(member.name);
|
|
791
|
+
}
|
|
792
|
+
return {
|
|
793
|
+
name: name.text,
|
|
794
|
+
kind,
|
|
795
|
+
exported: isExportedDeclaration(node),
|
|
796
|
+
paramTypes,
|
|
797
|
+
returnType,
|
|
798
|
+
members: uniqueSorted(members),
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
function contractShapesFromSourceText(text, fileName) {
|
|
802
|
+
const sourceFile = ts.createSourceFile(fileName, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
803
|
+
const shapes = new Map();
|
|
804
|
+
const visit = (node) => {
|
|
805
|
+
const shape = declarationContractShape(node);
|
|
806
|
+
if (shape)
|
|
807
|
+
shapes.set(`${shape.kind}#${shape.name}`, shape);
|
|
808
|
+
ts.forEachChild(node, visit);
|
|
809
|
+
};
|
|
810
|
+
ts.forEachChild(sourceFile, visit);
|
|
811
|
+
return shapes;
|
|
812
|
+
}
|
|
813
|
+
function contractShapeFromTarget(target) {
|
|
814
|
+
return target.declaration ? declarationContractShape(target.declaration) : null;
|
|
815
|
+
}
|
|
816
|
+
function compareContractShapes(target, current, previous) {
|
|
817
|
+
if (!current.exported && !previous?.exported)
|
|
818
|
+
return null;
|
|
819
|
+
const changes = [];
|
|
820
|
+
const push = (change, memberName = null) => {
|
|
821
|
+
changes.push({ change, memberName, line: target.lineStart });
|
|
822
|
+
};
|
|
823
|
+
if (current.exported && !previous?.exported)
|
|
824
|
+
push('export_added');
|
|
825
|
+
if (!current.exported && previous?.exported)
|
|
826
|
+
push('export_removed');
|
|
827
|
+
if (previous && previous.kind !== current.kind)
|
|
828
|
+
push('kind_changed');
|
|
829
|
+
if (previous) {
|
|
830
|
+
if (current.paramTypes.length > previous.paramTypes.length)
|
|
831
|
+
push('param_added');
|
|
832
|
+
if (current.paramTypes.length < previous.paramTypes.length)
|
|
833
|
+
push('param_removed');
|
|
834
|
+
if (current.paramTypes.length === previous.paramTypes.length &&
|
|
835
|
+
current.paramTypes.some((type, index) => type !== previous.paramTypes[index])) {
|
|
836
|
+
push('param_type_changed');
|
|
837
|
+
}
|
|
838
|
+
if (current.returnType !== previous.returnType)
|
|
839
|
+
push('return_type_changed');
|
|
840
|
+
const currentMembers = new Set(current.members);
|
|
841
|
+
const previousMembers = new Set(previous.members);
|
|
842
|
+
for (const member of current.members) {
|
|
843
|
+
if (!previousMembers.has(member))
|
|
844
|
+
push('member_added', member);
|
|
845
|
+
}
|
|
846
|
+
for (const member of previous.members) {
|
|
847
|
+
if (!currentMembers.has(member))
|
|
848
|
+
push('member_removed', member);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
const uniqueChanges = new Map();
|
|
852
|
+
for (const change of changes) {
|
|
853
|
+
uniqueChanges.set(`${change.change}:${change.memberName ?? ''}`, change);
|
|
854
|
+
}
|
|
855
|
+
const rows = [...uniqueChanges.values()].sort((a, b) => a.change.localeCompare(b.change) ||
|
|
856
|
+
String(a.memberName ?? '').localeCompare(String(b.memberName ?? '')));
|
|
857
|
+
if (rows.length === 0)
|
|
858
|
+
return null;
|
|
859
|
+
return {
|
|
860
|
+
symbol: target.name,
|
|
861
|
+
file: target.file,
|
|
862
|
+
kind: target.kind,
|
|
863
|
+
line: target.lineStart,
|
|
864
|
+
changes: rows,
|
|
865
|
+
consumers: [],
|
|
866
|
+
externalConsumers: [],
|
|
867
|
+
provenance: 'typescript-checker',
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
function declarationKind(node) {
|
|
871
|
+
if (ts.isFunctionDeclaration(node))
|
|
872
|
+
return 'function';
|
|
873
|
+
if (ts.isClassDeclaration(node))
|
|
874
|
+
return 'class';
|
|
875
|
+
if (ts.isInterfaceDeclaration(node))
|
|
876
|
+
return 'interface';
|
|
877
|
+
if (ts.isTypeAliasDeclaration(node))
|
|
878
|
+
return 'type';
|
|
879
|
+
if (ts.isEnumDeclaration(node))
|
|
880
|
+
return 'enum';
|
|
881
|
+
if (ts.isMethodDeclaration(node))
|
|
882
|
+
return 'method';
|
|
883
|
+
if (ts.isVariableDeclaration(node) &&
|
|
884
|
+
ts.isVariableDeclarationList(node.parent) &&
|
|
885
|
+
ts.isVariableStatement(node.parent.parent) &&
|
|
886
|
+
ts.isSourceFile(node.parent.parent.parent)) {
|
|
887
|
+
return 'const';
|
|
888
|
+
}
|
|
889
|
+
return null;
|
|
890
|
+
}
|
|
891
|
+
function declarationName(node) {
|
|
892
|
+
if ((ts.isFunctionDeclaration(node) ||
|
|
893
|
+
ts.isClassDeclaration(node) ||
|
|
894
|
+
ts.isInterfaceDeclaration(node) ||
|
|
895
|
+
ts.isTypeAliasDeclaration(node) ||
|
|
896
|
+
ts.isEnumDeclaration(node)) &&
|
|
897
|
+
node.name) {
|
|
898
|
+
return node.name;
|
|
899
|
+
}
|
|
900
|
+
if ((ts.isMethodDeclaration(node) || ts.isVariableDeclaration(node)) && ts.isIdentifier(node.name)) {
|
|
901
|
+
return node.name;
|
|
902
|
+
}
|
|
903
|
+
return undefined;
|
|
904
|
+
}
|
|
905
|
+
function collectDeclarations(sourceFile, checker, projectRoot) {
|
|
906
|
+
const out = [];
|
|
907
|
+
const file = repoRel(projectRoot, sourceFile.fileName);
|
|
908
|
+
const visit = (node) => {
|
|
909
|
+
const kind = declarationKind(node);
|
|
910
|
+
const name = declarationName(node);
|
|
911
|
+
if (kind && name) {
|
|
912
|
+
const start = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
|
913
|
+
const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line + 1;
|
|
914
|
+
out.push({
|
|
915
|
+
name: name.text,
|
|
916
|
+
kind,
|
|
917
|
+
file,
|
|
918
|
+
startLine: start,
|
|
919
|
+
endLine: end,
|
|
920
|
+
node,
|
|
921
|
+
symbol: checker.getSymbolAtLocation(name),
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
ts.forEachChild(node, visit);
|
|
925
|
+
};
|
|
926
|
+
ts.forEachChild(sourceFile, visit);
|
|
927
|
+
return out;
|
|
928
|
+
}
|
|
929
|
+
function smallestContaining(declarations, line) {
|
|
930
|
+
return declarations
|
|
931
|
+
.filter((decl) => line >= decl.startLine && line <= decl.endLine)
|
|
932
|
+
.sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine))[0] ?? null;
|
|
933
|
+
}
|
|
934
|
+
function resolveAlias(checker, symbol) {
|
|
935
|
+
if (symbol && symbol.flags & ts.SymbolFlags.Alias) {
|
|
936
|
+
try {
|
|
937
|
+
return checker.getAliasedSymbol(symbol);
|
|
938
|
+
}
|
|
939
|
+
catch {
|
|
940
|
+
return symbol;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
return symbol;
|
|
944
|
+
}
|
|
945
|
+
function symbolKey(file, kind, name) {
|
|
946
|
+
return `${file}::${kind}::${name}`;
|
|
947
|
+
}
|
|
948
|
+
function matchesSimpleGlob(file, pattern) {
|
|
949
|
+
const normalizedFile = normalizeRepoPath(file);
|
|
950
|
+
const normalizedPattern = normalizeRepoPath(pattern);
|
|
951
|
+
if (!normalizedPattern)
|
|
952
|
+
return false;
|
|
953
|
+
if (normalizedPattern === normalizedFile)
|
|
954
|
+
return true;
|
|
955
|
+
if (normalizedPattern.endsWith('/**')) {
|
|
956
|
+
const prefix = normalizedPattern.slice(0, -3);
|
|
957
|
+
return normalizedFile === prefix || normalizedFile.startsWith(`${prefix}/`);
|
|
958
|
+
}
|
|
959
|
+
if (normalizedPattern.endsWith('/*')) {
|
|
960
|
+
const prefix = normalizedPattern.slice(0, -2);
|
|
961
|
+
if (!normalizedFile.startsWith(`${prefix}/`))
|
|
962
|
+
return false;
|
|
963
|
+
return normalizedFile.slice(prefix.length + 1).indexOf('/') === -1;
|
|
964
|
+
}
|
|
965
|
+
return false;
|
|
966
|
+
}
|
|
967
|
+
function buildPlanAlignment(session, changedFiles, changedSymbols) {
|
|
968
|
+
const plan = session?.contract.agentPlan ?? null;
|
|
969
|
+
const intent = session?.contract.intentContract ?? null;
|
|
970
|
+
if (!plan && !intent)
|
|
971
|
+
return null;
|
|
972
|
+
const plannedFiles = uniqueSorted([...(plan?.expectedFiles ?? []), ...(intent?.target.pathTokens ?? [])].map(normalizeRepoPath));
|
|
973
|
+
const plannedGlobs = uniqueSorted([
|
|
974
|
+
...(plan?.expectedGlobs ?? []),
|
|
975
|
+
...(intent?.target.expectedPathGlobs ?? []),
|
|
976
|
+
...plannedFiles,
|
|
977
|
+
].map(normalizeRepoPath));
|
|
978
|
+
const plannedFilesTouched = changedFiles.filter((file) => plannedGlobs.some((pattern) => matchesSimpleGlob(file, pattern) || pattern === file));
|
|
979
|
+
const unplannedFilesTouched = changedFiles.filter((file) => !plannedFilesTouched.includes(file));
|
|
980
|
+
const planText = [
|
|
981
|
+
plan?.summary,
|
|
982
|
+
...(plan?.steps ?? []),
|
|
983
|
+
...(plan?.constraints ?? []),
|
|
984
|
+
...(plan?.risks ?? []),
|
|
985
|
+
session?.contract.goal,
|
|
986
|
+
].filter(Boolean).join('\n');
|
|
987
|
+
const changedSymbolsMentionedInPlan = changedSymbols
|
|
988
|
+
.filter((symbol) => new RegExp(`\\b${symbol.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(planText))
|
|
989
|
+
.map((symbol) => `${symbol.file}#${symbol.name}`);
|
|
990
|
+
return {
|
|
991
|
+
plannedFiles,
|
|
992
|
+
plannedGlobs,
|
|
993
|
+
plannedFilesTouched,
|
|
994
|
+
unplannedFilesTouched,
|
|
995
|
+
changedSymbolsMentionedInPlan: uniqueSorted(changedSymbolsMentionedInPlan),
|
|
996
|
+
provenance: 'session-plan',
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
function buildBoundaryImpact(profile, changedFiles) {
|
|
1000
|
+
if (!profile)
|
|
1001
|
+
return [];
|
|
1002
|
+
return changedFiles.map((file) => {
|
|
1003
|
+
const approvalRequiredGlobs = profile.approvalRequiredPaths.filter((glob) => matchesSimpleGlob(file, glob));
|
|
1004
|
+
return {
|
|
1005
|
+
file,
|
|
1006
|
+
owners: (0, governance_runtime_1.ownersForPath)(file, profile.ownershipBoundaries),
|
|
1007
|
+
approvalRequired: approvalRequiredGlobs.length > 0,
|
|
1008
|
+
approvalRequiredGlobs,
|
|
1009
|
+
provenance: 'codeowners-profile',
|
|
1010
|
+
};
|
|
1011
|
+
}).filter((item) => item.owners.length > 0 || item.approvalRequired)
|
|
1012
|
+
.sort((a, b) => a.file.localeCompare(b.file));
|
|
1013
|
+
}
|
|
1014
|
+
function packageForFile(workspace, file) {
|
|
1015
|
+
let best = null;
|
|
1016
|
+
for (const pkg of workspace) {
|
|
1017
|
+
if (file === pkg.dirRel || file.startsWith(`${pkg.dirRel}/`)) {
|
|
1018
|
+
if (!best || pkg.dirRel.length > best.dirRel.length)
|
|
1019
|
+
best = pkg;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
return best;
|
|
1023
|
+
}
|
|
1024
|
+
function isRegistrationOrConfigReference(ref) {
|
|
1025
|
+
const file = normalizeRepoPath(ref.referencingFile).toLowerCase();
|
|
1026
|
+
const symbol = (ref.referencingSymbol ?? '').toLowerCase();
|
|
1027
|
+
return (/(^|\/)(index|app|main|routes?|setup|config|registry|register)\.[tj]sx?$/.test(file) ||
|
|
1028
|
+
symbol.includes('register') ||
|
|
1029
|
+
symbol.includes('route'));
|
|
1030
|
+
}
|
|
1031
|
+
function ownersForReference(profile, file) {
|
|
1032
|
+
return profile ? (0, governance_runtime_1.ownersForPath)(file, profile.ownershipBoundaries) : [];
|
|
1033
|
+
}
|
|
1034
|
+
function approvalRequiredForReference(profile, file) {
|
|
1035
|
+
return profile ? profile.approvalRequiredPaths.some((glob) => matchesSimpleGlob(file, glob)) : false;
|
|
1036
|
+
}
|
|
1037
|
+
function sensitiveForReference(profile, file) {
|
|
1038
|
+
return profile ? profile.sensitiveBoundaries.some((boundary) => matchesSimpleGlob(file, boundary.glob)) : false;
|
|
1039
|
+
}
|
|
1040
|
+
function runtimeGovernanceConsumer(consumer) {
|
|
1041
|
+
const haystack = [
|
|
1042
|
+
consumer.file,
|
|
1043
|
+
consumer.symbol ?? '',
|
|
1044
|
+
consumer.kind ?? '',
|
|
1045
|
+
].join(' ').toLowerCase();
|
|
1046
|
+
return /\b(runtime|governance|approval|approv|session|evidence|ledger|hook|control-plane|controlplane|audit|receipt|policy)\b/.test(haystack);
|
|
1047
|
+
}
|
|
1048
|
+
function profileFromSession(session) {
|
|
1049
|
+
if (!session)
|
|
1050
|
+
return null;
|
|
1051
|
+
return {
|
|
1052
|
+
ownershipBoundaries: session.contract.ownershipRules ?? [],
|
|
1053
|
+
approvalRequiredPaths: session.contract.approvalRequiredGlobs ?? [],
|
|
1054
|
+
sensitiveBoundaries: (session.contract.sensitiveGlobs ?? []).map((glob) => ({
|
|
1055
|
+
glob,
|
|
1056
|
+
reason: 'session-contract',
|
|
1057
|
+
})),
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
function plannedFileMatches(planAlignment, file) {
|
|
1061
|
+
if (!planAlignment)
|
|
1062
|
+
return false;
|
|
1063
|
+
return planAlignment.plannedGlobs.some((glob) => matchesSimpleGlob(file, glob) || normalizeRepoPath(glob) === file);
|
|
1064
|
+
}
|
|
1065
|
+
function rankReference(ref, context) {
|
|
1066
|
+
const targetPkg = packageForFile(context.workspace, ref.targetFile);
|
|
1067
|
+
const referencingPkg = packageForFile(context.workspace, ref.referencingFile);
|
|
1068
|
+
const targetPackageName = targetPkg?.name ?? null;
|
|
1069
|
+
const referencingPackageName = referencingPkg?.name ?? null;
|
|
1070
|
+
const crossPackage = Boolean(targetPackageName && referencingPackageName && targetPackageName !== referencingPackageName);
|
|
1071
|
+
const outsideChangedPackage = Boolean(referencingPackageName && !context.changedPackageNames.has(referencingPackageName));
|
|
1072
|
+
const outsideChangedFile = ref.targetFile !== ref.referencingFile;
|
|
1073
|
+
const owners = ownersForReference(context.profile, ref.referencingFile);
|
|
1074
|
+
const approvalRequired = approvalRequiredForReference(context.profile, ref.referencingFile);
|
|
1075
|
+
const inChangedFiles = context.changedFileSet.has(ref.referencingFile);
|
|
1076
|
+
const registrationOrConfig = isRegistrationOrConfigReference(ref);
|
|
1077
|
+
const importOnly = ref.referencingSymbol === null;
|
|
1078
|
+
const planUnmentioned = context.planAlignment ? !plannedFileMatches(context.planAlignment, ref.referencingFile) : false;
|
|
1079
|
+
const reasonCodes = [];
|
|
1080
|
+
let score = 0;
|
|
1081
|
+
if (crossPackage) {
|
|
1082
|
+
reasonCodes.push('cross_package_reference');
|
|
1083
|
+
score += 40;
|
|
1084
|
+
}
|
|
1085
|
+
if (outsideChangedPackage) {
|
|
1086
|
+
reasonCodes.push('outside_changed_package');
|
|
1087
|
+
score += 20;
|
|
1088
|
+
}
|
|
1089
|
+
if (outsideChangedFile) {
|
|
1090
|
+
reasonCodes.push('outside_changed_file');
|
|
1091
|
+
score += 15;
|
|
1092
|
+
}
|
|
1093
|
+
if (!ref.isTestFile) {
|
|
1094
|
+
reasonCodes.push('non_test_reference');
|
|
1095
|
+
score += 28;
|
|
1096
|
+
}
|
|
1097
|
+
else {
|
|
1098
|
+
reasonCodes.push('test_reference');
|
|
1099
|
+
score -= 22;
|
|
1100
|
+
}
|
|
1101
|
+
if (owners.length > 0) {
|
|
1102
|
+
reasonCodes.push('owned_path');
|
|
1103
|
+
score += 18;
|
|
1104
|
+
}
|
|
1105
|
+
if (approvalRequired) {
|
|
1106
|
+
reasonCodes.push('approval_required_path');
|
|
1107
|
+
score += 35;
|
|
1108
|
+
}
|
|
1109
|
+
if (!inChangedFiles) {
|
|
1110
|
+
reasonCodes.push('not_in_changed_files');
|
|
1111
|
+
score += 12;
|
|
1112
|
+
}
|
|
1113
|
+
if (planUnmentioned) {
|
|
1114
|
+
reasonCodes.push('plan_unmentioned_file');
|
|
1115
|
+
score += 10;
|
|
1116
|
+
}
|
|
1117
|
+
if (!outsideChangedFile) {
|
|
1118
|
+
reasonCodes.push('same_file_reference');
|
|
1119
|
+
score -= 25;
|
|
1120
|
+
}
|
|
1121
|
+
if (registrationOrConfig) {
|
|
1122
|
+
reasonCodes.push('registration_or_config_surface');
|
|
1123
|
+
score -= 12;
|
|
1124
|
+
}
|
|
1125
|
+
if (importOnly) {
|
|
1126
|
+
reasonCodes.push('import_or_declaration_reference');
|
|
1127
|
+
score -= 25;
|
|
1128
|
+
}
|
|
1129
|
+
return {
|
|
1130
|
+
rank: 0,
|
|
1131
|
+
score,
|
|
1132
|
+
targetFile: ref.targetFile,
|
|
1133
|
+
targetSymbol: ref.targetSymbol,
|
|
1134
|
+
targetKind: ref.targetKind,
|
|
1135
|
+
referencingFile: ref.referencingFile,
|
|
1136
|
+
referencingSymbol: ref.referencingSymbol,
|
|
1137
|
+
referencingKind: ref.referencingKind,
|
|
1138
|
+
line: ref.line,
|
|
1139
|
+
isTestFile: ref.isTestFile,
|
|
1140
|
+
owners,
|
|
1141
|
+
approvalRequired,
|
|
1142
|
+
reasonCodes: uniqueSorted(reasonCodes),
|
|
1143
|
+
provenance: 'deterministic-ranking',
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
function combineReasonCodes(items) {
|
|
1147
|
+
return uniqueSorted(items.flatMap((item) => item.reasonCodes));
|
|
1148
|
+
}
|
|
1149
|
+
function buildConsequences(rankedReferences) {
|
|
1150
|
+
const byKey = new Map();
|
|
1151
|
+
for (const ref of rankedReferences) {
|
|
1152
|
+
const key = `${ref.targetFile}#${ref.targetKind}#${ref.targetSymbol}<-${ref.referencingFile}`;
|
|
1153
|
+
const bucket = byKey.get(key) ?? [];
|
|
1154
|
+
bucket.push(ref);
|
|
1155
|
+
byKey.set(key, bucket);
|
|
1156
|
+
}
|
|
1157
|
+
return [...byKey.values()].map((items) => {
|
|
1158
|
+
const first = items[0];
|
|
1159
|
+
const nonTest = items.filter((item) => !item.isTestFile);
|
|
1160
|
+
const tests = items.filter((item) => item.isTestFile);
|
|
1161
|
+
const importOnly = items.filter((item) => item.reasonCodes.includes('import_or_declaration_reference'));
|
|
1162
|
+
const owners = uniqueSorted(items.flatMap((item) => item.owners));
|
|
1163
|
+
const reasonCodes = combineReasonCodes(items);
|
|
1164
|
+
const maxScore = Math.max(...items.map((item) => item.score));
|
|
1165
|
+
const score = maxScore +
|
|
1166
|
+
Math.min(30, items.length * 4) +
|
|
1167
|
+
nonTest.length * 6 -
|
|
1168
|
+
tests.length * 2 -
|
|
1169
|
+
importOnly.length * 3;
|
|
1170
|
+
return {
|
|
1171
|
+
rank: 0,
|
|
1172
|
+
score,
|
|
1173
|
+
targetFile: first.targetFile,
|
|
1174
|
+
targetSymbol: first.targetSymbol,
|
|
1175
|
+
targetKind: first.targetKind,
|
|
1176
|
+
referencingFile: first.referencingFile,
|
|
1177
|
+
referenceCount: items.length,
|
|
1178
|
+
nonTestReferenceCount: nonTest.length,
|
|
1179
|
+
testReferenceCount: tests.length,
|
|
1180
|
+
representativeLines: Array.from(new Set(items.map((item) => item.line)))
|
|
1181
|
+
.sort((a, b) => a - b)
|
|
1182
|
+
.slice(0, 3),
|
|
1183
|
+
representativeSymbols: uniqueSorted(items.map((item) => item.referencingSymbol)).slice(0, 3),
|
|
1184
|
+
owners,
|
|
1185
|
+
approvalRequired: items.some((item) => item.approvalRequired),
|
|
1186
|
+
reasonCodes,
|
|
1187
|
+
provenance: 'deterministic-ranking',
|
|
1188
|
+
};
|
|
1189
|
+
}).sort((a, b) => b.score - a.score ||
|
|
1190
|
+
b.nonTestReferenceCount - a.nonTestReferenceCount ||
|
|
1191
|
+
b.referenceCount - a.referenceCount ||
|
|
1192
|
+
a.targetFile.localeCompare(b.targetFile) ||
|
|
1193
|
+
a.targetSymbol.localeCompare(b.targetSymbol) ||
|
|
1194
|
+
a.referencingFile.localeCompare(b.referencingFile)).map((item, index) => ({ ...item, rank: index + 1 }));
|
|
1195
|
+
}
|
|
1196
|
+
function buildStructuralDigest(input) {
|
|
1197
|
+
const changedFileSet = new Set(input.changedFiles);
|
|
1198
|
+
const changedPackageNameSet = new Set(input.changedPackageNames);
|
|
1199
|
+
const rankedReferences = input.references
|
|
1200
|
+
.map((ref) => rankReference(ref, {
|
|
1201
|
+
workspace: input.workspace,
|
|
1202
|
+
changedFileSet,
|
|
1203
|
+
changedPackageNames: changedPackageNameSet,
|
|
1204
|
+
profile: input.profile,
|
|
1205
|
+
planAlignment: input.planAlignment,
|
|
1206
|
+
}))
|
|
1207
|
+
.sort((a, b) => b.score - a.score ||
|
|
1208
|
+
a.targetFile.localeCompare(b.targetFile) ||
|
|
1209
|
+
a.targetSymbol.localeCompare(b.targetSymbol) ||
|
|
1210
|
+
a.referencingFile.localeCompare(b.referencingFile) ||
|
|
1211
|
+
a.line - b.line)
|
|
1212
|
+
.map((ref, index) => ({ ...ref, rank: index + 1 }));
|
|
1213
|
+
const consequences = buildConsequences(rankedReferences);
|
|
1214
|
+
const topConsequences = consequences.slice(0, 6);
|
|
1215
|
+
const topReferences = rankedReferences.slice(0, 8);
|
|
1216
|
+
const symbolRows = input.changedSymbols.map((symbol) => {
|
|
1217
|
+
const refs = input.references.filter((ref) => ref.targetFile === symbol.file &&
|
|
1218
|
+
ref.targetKind === symbol.kind &&
|
|
1219
|
+
ref.targetSymbol === symbol.name);
|
|
1220
|
+
const nonTest = refs.filter((ref) => !ref.isTestFile);
|
|
1221
|
+
const tests = refs.filter((ref) => ref.isTestFile);
|
|
1222
|
+
const targetPkg = packageForFile(input.workspace, symbol.file);
|
|
1223
|
+
const crossPackageCount = refs.filter((ref) => {
|
|
1224
|
+
const referencingPkg = packageForFile(input.workspace, ref.referencingFile);
|
|
1225
|
+
return Boolean(targetPkg?.name && referencingPkg?.name && targetPkg.name !== referencingPkg.name);
|
|
1226
|
+
}).length;
|
|
1227
|
+
const reasonCodes = [];
|
|
1228
|
+
let score = refs.length + nonTest.length * 3 + crossPackageCount * 5 - tests.length;
|
|
1229
|
+
if (crossPackageCount > 0) {
|
|
1230
|
+
reasonCodes.push('cross_package_reference');
|
|
1231
|
+
score += 12;
|
|
1232
|
+
}
|
|
1233
|
+
if (nonTest.length > 0)
|
|
1234
|
+
reasonCodes.push('non_test_reference');
|
|
1235
|
+
if (tests.length > 0)
|
|
1236
|
+
reasonCodes.push('test_reference');
|
|
1237
|
+
const owners = ownersForReference(input.profile, symbol.file);
|
|
1238
|
+
if (owners.length > 0) {
|
|
1239
|
+
reasonCodes.push('owned_path');
|
|
1240
|
+
score += 4;
|
|
1241
|
+
}
|
|
1242
|
+
if (approvalRequiredForReference(input.profile, symbol.file)) {
|
|
1243
|
+
reasonCodes.push('approval_required_path');
|
|
1244
|
+
score += 10;
|
|
1245
|
+
}
|
|
1246
|
+
return {
|
|
1247
|
+
rank: 0,
|
|
1248
|
+
score,
|
|
1249
|
+
file: symbol.file,
|
|
1250
|
+
name: symbol.name,
|
|
1251
|
+
kind: symbol.kind,
|
|
1252
|
+
referenceCount: refs.length,
|
|
1253
|
+
nonTestReferenceCount: nonTest.length,
|
|
1254
|
+
testReferenceCount: tests.length,
|
|
1255
|
+
crossPackageReferenceCount: crossPackageCount,
|
|
1256
|
+
reasonCodes: uniqueSorted(reasonCodes),
|
|
1257
|
+
provenance: 'deterministic-ranking',
|
|
1258
|
+
};
|
|
1259
|
+
}).sort((a, b) => b.score - a.score ||
|
|
1260
|
+
b.crossPackageReferenceCount - a.crossPackageReferenceCount ||
|
|
1261
|
+
b.nonTestReferenceCount - a.nonTestReferenceCount ||
|
|
1262
|
+
a.file.localeCompare(b.file) ||
|
|
1263
|
+
a.name.localeCompare(b.name)).map((symbol, index) => ({ ...symbol, rank: index + 1 }));
|
|
1264
|
+
const crossPackageReferenceCount = rankedReferences.filter((ref) => ref.reasonCodes.includes('cross_package_reference')).length;
|
|
1265
|
+
const nonTestReferenceCount = rankedReferences.filter((ref) => !ref.isTestFile).length;
|
|
1266
|
+
const sameFileReferences = rankedReferences.filter((ref) => ref.reasonCodes.includes('same_file_reference')).length;
|
|
1267
|
+
const lowSignalReferences = rankedReferences.filter((ref) => ref.score < 35 ||
|
|
1268
|
+
ref.reasonCodes.includes('test_reference') ||
|
|
1269
|
+
ref.reasonCodes.includes('same_file_reference')).length;
|
|
1270
|
+
return {
|
|
1271
|
+
schemaVersion: 'neurcode.structural-digest.v1',
|
|
1272
|
+
generatedAt: input.generatedAt,
|
|
1273
|
+
provenance: 'deterministic-ranking',
|
|
1274
|
+
modelUsed: false,
|
|
1275
|
+
topSymbols: symbolRows.slice(0, 6),
|
|
1276
|
+
topConsequences,
|
|
1277
|
+
topReferences,
|
|
1278
|
+
hidden: {
|
|
1279
|
+
references: Math.max(0, input.references.length - topReferences.length),
|
|
1280
|
+
testReferences: input.testReferences.length,
|
|
1281
|
+
lowSignalReferences,
|
|
1282
|
+
sameFileReferences,
|
|
1283
|
+
},
|
|
1284
|
+
summary: {
|
|
1285
|
+
changedSymbolCount: input.changedSymbols.length,
|
|
1286
|
+
referenceCount: input.references.length,
|
|
1287
|
+
topConsequenceCount: topConsequences.length,
|
|
1288
|
+
topReferenceCount: topReferences.length,
|
|
1289
|
+
crossPackageReferenceCount,
|
|
1290
|
+
nonTestReferenceCount,
|
|
1291
|
+
testReferenceCount: input.testReferences.length,
|
|
1292
|
+
},
|
|
1293
|
+
limitations: [
|
|
1294
|
+
'Digest ranking is deterministic and facts-only; it is not an LLM judgment.',
|
|
1295
|
+
'Ranking reflects static reference shape, ownership metadata, path surfaces, and plan/path alignment only.',
|
|
1296
|
+
'A high-ranked reference is a place to inspect; it is not a claim that behavior is broken.',
|
|
1297
|
+
'No source text, diff hunks, patch content, or shell command bodies are stored.',
|
|
1298
|
+
],
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
function inferRemovedEffectTargets(targets, hunk) {
|
|
1302
|
+
const touchedLines = new Set();
|
|
1303
|
+
for (const line of normalizedHunkLines(hunk)) {
|
|
1304
|
+
if (line.type !== 'context' && line.type !== 'added')
|
|
1305
|
+
continue;
|
|
1306
|
+
if (line.newLine !== null)
|
|
1307
|
+
touchedLines.add(line.newLine);
|
|
1308
|
+
}
|
|
1309
|
+
return targets.filter((target) => target.lineStart !== null &&
|
|
1310
|
+
target.lineEnd !== null &&
|
|
1311
|
+
[...touchedLines].some((line) => line >= target.lineStart && line <= target.lineEnd));
|
|
1312
|
+
}
|
|
1313
|
+
function externalNonTestConsumers(consumers) {
|
|
1314
|
+
return consumers.filter((consumer) => !consumer.isTestFile &&
|
|
1315
|
+
!consumer.inChangedFile &&
|
|
1316
|
+
!isPackageEntrypointFile(consumer.file));
|
|
1317
|
+
}
|
|
1318
|
+
function isPackageEntrypointFile(file) {
|
|
1319
|
+
return /(^|\/)index\.(cjs|cts|js|jsx|mjs|mts|ts|tsx)$/.test(file);
|
|
1320
|
+
}
|
|
1321
|
+
function consumerReferencesForTarget(target, references) {
|
|
1322
|
+
const byKey = new Map();
|
|
1323
|
+
for (const ref of references) {
|
|
1324
|
+
if (ref.targetFile !== target.file || ref.targetKind !== target.kind || ref.targetSymbol !== target.symbol)
|
|
1325
|
+
continue;
|
|
1326
|
+
const row = {
|
|
1327
|
+
file: ref.referencingFile,
|
|
1328
|
+
symbol: ref.referencingSymbol,
|
|
1329
|
+
kind: ref.referencingKind,
|
|
1330
|
+
line: ref.line,
|
|
1331
|
+
isTestFile: ref.isTestFile,
|
|
1332
|
+
inChangedFile: ref.inChangedFile,
|
|
1333
|
+
provenance: 'typescript-compiler',
|
|
1334
|
+
};
|
|
1335
|
+
const key = row.file;
|
|
1336
|
+
const existing = byKey.get(key);
|
|
1337
|
+
if (!existing ||
|
|
1338
|
+
(existing.symbol === null && row.symbol !== null) ||
|
|
1339
|
+
(existing.symbol === row.symbol && row.line < existing.line)) {
|
|
1340
|
+
byKey.set(key, row);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
return [...byKey.values()].sort((a, b) => Number(a.isTestFile) - Number(b.isTestFile) ||
|
|
1344
|
+
Number(a.inChangedFile) - Number(b.inChangedFile) ||
|
|
1345
|
+
a.file.localeCompare(b.file) ||
|
|
1346
|
+
a.line - b.line);
|
|
1347
|
+
}
|
|
1348
|
+
function buildEffectDeltas(input) {
|
|
1349
|
+
const byFile = diffFileByPath(input.diffFiles);
|
|
1350
|
+
const targetByKey = new Map(input.targets.map((target) => [symbolKey(target.file, target.kind, target.name), target]));
|
|
1351
|
+
const added = new Map();
|
|
1352
|
+
const removed = new Map();
|
|
1353
|
+
const addCount = (map, target, effectCategory, calleeName, line) => {
|
|
1354
|
+
const key = `${symbolKey(target.file, target.kind, target.name)}:${effectCategory}:${calleeName}`;
|
|
1355
|
+
const current = map.get(key) ?? { target, effectCategory, calleeName, count: 0, lines: [] };
|
|
1356
|
+
current.count += 1;
|
|
1357
|
+
if (line !== null)
|
|
1358
|
+
current.lines.push(line);
|
|
1359
|
+
map.set(key, current);
|
|
1360
|
+
};
|
|
1361
|
+
for (const target of input.targets) {
|
|
1362
|
+
const diffFile = byFile.get(target.file);
|
|
1363
|
+
if (!diffFile || !target.declaration)
|
|
1364
|
+
continue;
|
|
1365
|
+
const addedLines = addedLineSet(diffFile);
|
|
1366
|
+
const sourceFile = target.declaration.getSourceFile();
|
|
1367
|
+
const visit = (node) => {
|
|
1368
|
+
if (ts.isCallExpression(node)) {
|
|
1369
|
+
const line = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
|
1370
|
+
if (addedLines.has(line)) {
|
|
1371
|
+
const calleeName = rightmostIdentifierText(node.expression);
|
|
1372
|
+
const effectCategory = effectCategoryForCalleeName(calleeName);
|
|
1373
|
+
if (calleeName && effectCategory)
|
|
1374
|
+
addCount(added, target, effectCategory, calleeName, line);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
ts.forEachChild(node, visit);
|
|
1378
|
+
};
|
|
1379
|
+
ts.forEachChild(target.declaration, visit);
|
|
1380
|
+
}
|
|
1381
|
+
const targetsByFile = new Map();
|
|
1382
|
+
for (const target of input.targets) {
|
|
1383
|
+
const bucket = targetsByFile.get(target.file) ?? [];
|
|
1384
|
+
bucket.push(target);
|
|
1385
|
+
targetsByFile.set(target.file, bucket);
|
|
1386
|
+
}
|
|
1387
|
+
for (const diffFile of input.diffFiles) {
|
|
1388
|
+
const file = normalizeRepoPath(diffFile.path);
|
|
1389
|
+
const fileTargets = targetsByFile.get(file) ?? [];
|
|
1390
|
+
if (fileTargets.length === 0)
|
|
1391
|
+
continue;
|
|
1392
|
+
for (const hunk of diffFile.hunks ?? []) {
|
|
1393
|
+
const removedTargets = inferRemovedEffectTargets(fileTargets, hunk);
|
|
1394
|
+
if (removedTargets.length === 0)
|
|
1395
|
+
continue;
|
|
1396
|
+
for (const line of normalizedHunkLines(hunk)) {
|
|
1397
|
+
if (line.type !== 'removed')
|
|
1398
|
+
continue;
|
|
1399
|
+
for (const match of effectMatchesFromLineContent(line.content)) {
|
|
1400
|
+
for (const target of removedTargets)
|
|
1401
|
+
addCount(removed, target, match.effectCategory, match.calleeName, line.oldLine ?? target.lineStart);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
const keys = uniqueSorted([...added.keys(), ...removed.keys()]);
|
|
1407
|
+
const out = [];
|
|
1408
|
+
for (const key of keys) {
|
|
1409
|
+
const add = added.get(key);
|
|
1410
|
+
const remove = removed.get(key);
|
|
1411
|
+
const addCountValue = add?.count ?? 0;
|
|
1412
|
+
const removeCountValue = remove?.count ?? 0;
|
|
1413
|
+
if (addCountValue === removeCountValue)
|
|
1414
|
+
continue;
|
|
1415
|
+
const row = add ?? remove;
|
|
1416
|
+
if (!row)
|
|
1417
|
+
continue;
|
|
1418
|
+
const baseRow = {
|
|
1419
|
+
symbol: row.target.name,
|
|
1420
|
+
file: row.target.file,
|
|
1421
|
+
kind: row.target.kind,
|
|
1422
|
+
line: row.lines.length ? Math.min(...row.lines) : row.target.lineStart,
|
|
1423
|
+
effectCategory: row.effectCategory,
|
|
1424
|
+
direction: addCountValue > removeCountValue ? 'added' : 'removed',
|
|
1425
|
+
calleeName: row.calleeName,
|
|
1426
|
+
count: Math.abs(addCountValue - removeCountValue),
|
|
1427
|
+
provenance: 'effect-registry',
|
|
1428
|
+
};
|
|
1429
|
+
const consumers = consumerReferencesForTarget(baseRow, input.references);
|
|
1430
|
+
out.push({
|
|
1431
|
+
...baseRow,
|
|
1432
|
+
consumers,
|
|
1433
|
+
externalConsumers: externalNonTestConsumers(consumers),
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
return out.sort((a, b) => a.file.localeCompare(b.file) ||
|
|
1437
|
+
a.symbol.localeCompare(b.symbol) ||
|
|
1438
|
+
a.effectCategory.localeCompare(b.effectCategory) ||
|
|
1439
|
+
a.calleeName.localeCompare(b.calleeName));
|
|
1440
|
+
}
|
|
1441
|
+
function buildContractDeltas(projectRoot, diffFiles, targets) {
|
|
1442
|
+
const byPath = diffFileByPath(diffFiles);
|
|
1443
|
+
const oldShapesByFile = new Map();
|
|
1444
|
+
const out = [];
|
|
1445
|
+
for (const target of targets) {
|
|
1446
|
+
const current = contractShapeFromTarget(target);
|
|
1447
|
+
if (!current)
|
|
1448
|
+
continue;
|
|
1449
|
+
const diffFile = byPath.get(target.file);
|
|
1450
|
+
if (!diffFile)
|
|
1451
|
+
continue;
|
|
1452
|
+
let oldShapes = oldShapesByFile.get(target.file);
|
|
1453
|
+
if (!oldShapes) {
|
|
1454
|
+
const oldText = oldFileTextFromDiff(projectRoot, diffFile);
|
|
1455
|
+
oldShapes = oldText ? contractShapesFromSourceText(oldText, target.file) : new Map();
|
|
1456
|
+
oldShapesByFile.set(target.file, oldShapes);
|
|
1457
|
+
}
|
|
1458
|
+
const previous = oldShapes.get(`${target.kind}#${target.name}`) ?? oldShapes.get(`${current.kind}#${current.name}`) ?? null;
|
|
1459
|
+
const delta = compareContractShapes(target, current, previous);
|
|
1460
|
+
if (delta)
|
|
1461
|
+
out.push(delta);
|
|
1462
|
+
}
|
|
1463
|
+
return out.sort((a, b) => a.file.localeCompare(b.file) || a.symbol.localeCompare(b.symbol));
|
|
1464
|
+
}
|
|
1465
|
+
function attachContractConsumers(contractDeltas, references) {
|
|
1466
|
+
return contractDeltas.map((delta) => {
|
|
1467
|
+
const consumers = consumerReferencesForTarget(delta, references);
|
|
1468
|
+
return {
|
|
1469
|
+
...delta,
|
|
1470
|
+
consumers,
|
|
1471
|
+
externalConsumers: externalNonTestConsumers(consumers),
|
|
1472
|
+
};
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
function effectSeverityScore(effect) {
|
|
1476
|
+
const external = effect.externalConsumers.length;
|
|
1477
|
+
const nonTest = effect.consumers.filter((consumer) => !consumer.isTestFile).length;
|
|
1478
|
+
const base = effect.direction === 'added' ? 24 : 18;
|
|
1479
|
+
let score = base;
|
|
1480
|
+
if (effect.effectCategory === 'session-evidence-write')
|
|
1481
|
+
score += 45;
|
|
1482
|
+
else if (effect.effectCategory === 'filesystem-write')
|
|
1483
|
+
score += 38;
|
|
1484
|
+
else if (effect.effectCategory === 'network-send')
|
|
1485
|
+
score += 42;
|
|
1486
|
+
else
|
|
1487
|
+
score += 30;
|
|
1488
|
+
score += Math.min(90, external * 28);
|
|
1489
|
+
score += Math.min(18, nonTest * 3);
|
|
1490
|
+
if (external === 0)
|
|
1491
|
+
score -= 65;
|
|
1492
|
+
if (isTestFile(effect.file))
|
|
1493
|
+
score -= 70;
|
|
1494
|
+
if (external === 0 && effect.consumers.some((consumer) => consumer.isTestFile))
|
|
1495
|
+
score -= 15;
|
|
1496
|
+
return score;
|
|
1497
|
+
}
|
|
1498
|
+
function contractSeverityScore(delta) {
|
|
1499
|
+
const changes = new Set(delta.changes.map((change) => change.change));
|
|
1500
|
+
let score = 20;
|
|
1501
|
+
if (changes.has('param_removed') || changes.has('return_type_changed') || changes.has('kind_changed'))
|
|
1502
|
+
score += 55;
|
|
1503
|
+
if (changes.has('member_removed') || changes.has('param_type_changed'))
|
|
1504
|
+
score += 45;
|
|
1505
|
+
if (changes.has('param_added') || changes.has('member_added'))
|
|
1506
|
+
score += 28;
|
|
1507
|
+
if (changes.has('export_removed'))
|
|
1508
|
+
score += 65;
|
|
1509
|
+
if (changes.has('export_added'))
|
|
1510
|
+
score -= 15;
|
|
1511
|
+
const nonTest = delta.consumers.filter((consumer) => !consumer.isTestFile);
|
|
1512
|
+
const outsideChanged = contractEscapingConsumers(delta);
|
|
1513
|
+
score += Math.min(60, outsideChanged.length * 16);
|
|
1514
|
+
score += Math.min(20, nonTest.length * 4);
|
|
1515
|
+
if (outsideChanged.length === 0)
|
|
1516
|
+
score -= 42;
|
|
1517
|
+
if (nonTest.length === 0 && delta.consumers.length > 0)
|
|
1518
|
+
score -= 20;
|
|
1519
|
+
return score;
|
|
1520
|
+
}
|
|
1521
|
+
function contractHasEscapingShape(delta) {
|
|
1522
|
+
const changes = new Set(delta.changes.map((change) => change.change));
|
|
1523
|
+
return (changes.has('export_removed') ||
|
|
1524
|
+
changes.has('param_removed') ||
|
|
1525
|
+
changes.has('param_type_changed') ||
|
|
1526
|
+
changes.has('return_type_changed') ||
|
|
1527
|
+
changes.has('kind_changed') ||
|
|
1528
|
+
changes.has('member_removed'));
|
|
1529
|
+
}
|
|
1530
|
+
function contractEscapingConsumers(delta) {
|
|
1531
|
+
return contractHasEscapingShape(delta) ? delta.externalConsumers : [];
|
|
1532
|
+
}
|
|
1533
|
+
function effectReasonCodes(effect) {
|
|
1534
|
+
const out = [];
|
|
1535
|
+
out.push(effect.direction === 'added' ? 'effect_added' : 'effect_removed');
|
|
1536
|
+
if (effect.effectCategory === 'filesystem-write')
|
|
1537
|
+
out.push('filesystem_write');
|
|
1538
|
+
if (effect.effectCategory === 'session-evidence-write')
|
|
1539
|
+
out.push('session_evidence_write');
|
|
1540
|
+
if (effect.effectCategory === 'network-send')
|
|
1541
|
+
out.push('network_send');
|
|
1542
|
+
if (effect.consumers.length > 0)
|
|
1543
|
+
out.push('has_consumers');
|
|
1544
|
+
if (effect.externalConsumers.length > 0)
|
|
1545
|
+
out.push('external_consumers');
|
|
1546
|
+
else
|
|
1547
|
+
out.push('changed_file_only');
|
|
1548
|
+
if (isTestFile(effect.file))
|
|
1549
|
+
out.push('test_file_effect');
|
|
1550
|
+
return out;
|
|
1551
|
+
}
|
|
1552
|
+
function contractReasonCodes(delta) {
|
|
1553
|
+
const out = ['contract_changed'];
|
|
1554
|
+
if (contractHasEscapingShape(delta)) {
|
|
1555
|
+
out.push('breaking_contract_shape');
|
|
1556
|
+
}
|
|
1557
|
+
if (delta.consumers.length > 0)
|
|
1558
|
+
out.push('has_consumers');
|
|
1559
|
+
if (contractEscapingConsumers(delta).length > 0)
|
|
1560
|
+
out.push('external_consumers');
|
|
1561
|
+
else
|
|
1562
|
+
out.push('changed_file_only');
|
|
1563
|
+
if (delta.consumers.some((consumer) => !consumer.isTestFile))
|
|
1564
|
+
out.push('non_test_consumers');
|
|
1565
|
+
else if (delta.consumers.length > 0)
|
|
1566
|
+
out.push('test_only_consumers');
|
|
1567
|
+
return out;
|
|
1568
|
+
}
|
|
1569
|
+
function consumerSummaryReasonCodes(summary) {
|
|
1570
|
+
const out = [];
|
|
1571
|
+
if (summary.reachableProductionConsumerCount > 0)
|
|
1572
|
+
out.push('reachable_production_consumers');
|
|
1573
|
+
if (summary.changedProductionConsumerCount > 0)
|
|
1574
|
+
out.push('changed_production_consumers');
|
|
1575
|
+
if (summary.sensitiveConsumerCount > 0)
|
|
1576
|
+
out.push('sensitive_consumers');
|
|
1577
|
+
if (summary.approvalRequiredConsumerCount > 0)
|
|
1578
|
+
out.push('approval_required_consumers');
|
|
1579
|
+
if (summary.runtimeGovernanceConsumerCount > 0)
|
|
1580
|
+
out.push('runtime_governance_consumers');
|
|
1581
|
+
if (summary.highFanout)
|
|
1582
|
+
out.push('high_fanout');
|
|
1583
|
+
if (summary.architectureRelevant)
|
|
1584
|
+
out.push('architecture_relevant');
|
|
1585
|
+
return out;
|
|
1586
|
+
}
|
|
1587
|
+
function orderedConsequenceReasonCodes(values) {
|
|
1588
|
+
return Array.from(new Set(values));
|
|
1589
|
+
}
|
|
1590
|
+
function buildConsumerSummary(input) {
|
|
1591
|
+
const production = input.consumers.filter((consumer) => !consumer.isTestFile);
|
|
1592
|
+
const tests = input.consumers.filter((consumer) => consumer.isTestFile);
|
|
1593
|
+
const reachableProduction = production.filter((consumer) => consumer.file !== input.targetFile &&
|
|
1594
|
+
!isPackageEntrypointFile(consumer.file));
|
|
1595
|
+
const externalProduction = input.externalConsumers.filter((consumer) => !consumer.isTestFile);
|
|
1596
|
+
const changedProduction = reachableProduction.filter((consumer) => consumer.inChangedFile);
|
|
1597
|
+
const sensitive = input.consumers.filter((consumer) => sensitiveForReference(input.profile, consumer.file));
|
|
1598
|
+
const approvalRequired = input.consumers.filter((consumer) => approvalRequiredForReference(input.profile, consumer.file));
|
|
1599
|
+
const runtimeGovernance = input.consumers.filter(runtimeGovernanceConsumer);
|
|
1600
|
+
const highFanout = externalProduction.length >= 3 || reachableProduction.length >= 5 || production.length >= 5;
|
|
1601
|
+
const architectureRelevant = highFanout ||
|
|
1602
|
+
reachableProduction.length >= 2 ||
|
|
1603
|
+
sensitive.length > 0 ||
|
|
1604
|
+
approvalRequired.length > 0 ||
|
|
1605
|
+
runtimeGovernance.length > 0;
|
|
1606
|
+
return {
|
|
1607
|
+
productionConsumerCount: production.length,
|
|
1608
|
+
testConsumerCount: tests.length,
|
|
1609
|
+
reachableProductionConsumerCount: reachableProduction.length,
|
|
1610
|
+
externalProductionConsumerCount: externalProduction.length,
|
|
1611
|
+
changedProductionConsumerCount: changedProduction.length,
|
|
1612
|
+
sensitiveConsumerCount: sensitive.length,
|
|
1613
|
+
approvalRequiredConsumerCount: approvalRequired.length,
|
|
1614
|
+
runtimeGovernanceConsumerCount: runtimeGovernance.length,
|
|
1615
|
+
productionFiles: uniqueSorted(reachableProduction.map((consumer) => consumer.file)).slice(0, 12),
|
|
1616
|
+
changedProductionFiles: uniqueSorted(changedProduction.map((consumer) => consumer.file)).slice(0, 12),
|
|
1617
|
+
testFiles: uniqueSorted(tests.map((consumer) => consumer.file)).slice(0, 12),
|
|
1618
|
+
sensitiveFiles: uniqueSorted(sensitive.map((consumer) => consumer.file)).slice(0, 12),
|
|
1619
|
+
approvalRequiredFiles: uniqueSorted(approvalRequired.map((consumer) => consumer.file)).slice(0, 12),
|
|
1620
|
+
runtimeGovernanceFiles: uniqueSorted(runtimeGovernance.map((consumer) => consumer.file)).slice(0, 12),
|
|
1621
|
+
highFanout,
|
|
1622
|
+
architectureRelevant,
|
|
1623
|
+
provenance: 'typescript-compiler+governance-profile+path-classifier',
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
function externalConsumerSummary(consumers, max = 3) {
|
|
1627
|
+
const files = uniqueSorted(consumers.map((consumer) => consumer.file));
|
|
1628
|
+
if (files.length === 0)
|
|
1629
|
+
return '';
|
|
1630
|
+
const shown = files.slice(0, max).join(', ');
|
|
1631
|
+
const hidden = files.length > max ? `; +${files.length - max} more external caller file(s)` : '';
|
|
1632
|
+
return `${shown}${hidden}`;
|
|
1633
|
+
}
|
|
1634
|
+
function summarizeEffect(effect) {
|
|
1635
|
+
const base = `${effect.file}#${effect.symbol} ${effect.direction} ${effect.effectCategory} (${effect.calleeName}${effect.line ? ` @ line ${effect.line}` : ''})`;
|
|
1636
|
+
if (effect.externalConsumers.length === 0) {
|
|
1637
|
+
return `${base}; no non-test callers outside this change`;
|
|
1638
|
+
}
|
|
1639
|
+
const callers = externalConsumerSummary(effect.externalConsumers);
|
|
1640
|
+
return `${base}; reached by ${effect.externalConsumers.length} external non-test caller${effect.externalConsumers.length === 1 ? '' : 's'}: ${callers}`;
|
|
1641
|
+
}
|
|
1642
|
+
function summarizeContract(delta) {
|
|
1643
|
+
const changes = delta.changes.slice(0, 4).map((change) => change.memberName ? `${change.change}:${change.memberName}` : change.change).join(', ');
|
|
1644
|
+
const escapingConsumers = contractEscapingConsumers(delta);
|
|
1645
|
+
if (escapingConsumers.length === 0) {
|
|
1646
|
+
if (delta.externalConsumers.length > 0 && !contractHasEscapingShape(delta)) {
|
|
1647
|
+
return `${delta.file}#${delta.symbol} contract changed: ${changes}; additive/non-breaking external consumers are not counted as escaping consequences`;
|
|
1648
|
+
}
|
|
1649
|
+
return `${delta.file}#${delta.symbol} contract changed: ${changes}; no non-test consumers outside this change`;
|
|
1650
|
+
}
|
|
1651
|
+
return `${delta.file}#${delta.symbol} contract changed: ${changes}; reached by ${escapingConsumers.length} external non-test consumer${escapingConsumers.length === 1 ? '' : 's'}: ${externalConsumerSummary(escapingConsumers)}`;
|
|
1652
|
+
}
|
|
1653
|
+
function buildTopFindings(input) {
|
|
1654
|
+
const rows = [];
|
|
1655
|
+
for (const effect of input.effectDeltas) {
|
|
1656
|
+
const consumerSummary = buildConsumerSummary({
|
|
1657
|
+
targetFile: effect.file,
|
|
1658
|
+
consumers: effect.consumers,
|
|
1659
|
+
externalConsumers: effect.externalConsumers,
|
|
1660
|
+
profile: input.profile,
|
|
1661
|
+
});
|
|
1662
|
+
rows.push({
|
|
1663
|
+
rank: 0,
|
|
1664
|
+
score: effectSeverityScore(effect),
|
|
1665
|
+
findingType: 'effect-delta',
|
|
1666
|
+
file: effect.file,
|
|
1667
|
+
symbol: effect.symbol,
|
|
1668
|
+
summary: summarizeEffect(effect),
|
|
1669
|
+
consumerCount: effect.consumers.length,
|
|
1670
|
+
nonTestConsumerCount: effect.consumers.filter((consumer) => !consumer.isTestFile).length,
|
|
1671
|
+
testConsumerCount: effect.consumers.filter((consumer) => consumer.isTestFile).length,
|
|
1672
|
+
externalConsumerCount: effect.externalConsumers.length,
|
|
1673
|
+
externalConsumerFiles: uniqueSorted(effect.externalConsumers.map((consumer) => consumer.file)),
|
|
1674
|
+
consumerSummary,
|
|
1675
|
+
reasonCodes: orderedConsequenceReasonCodes([
|
|
1676
|
+
...effectReasonCodes(effect),
|
|
1677
|
+
...consumerSummaryReasonCodes(consumerSummary),
|
|
1678
|
+
]),
|
|
1679
|
+
provenance: 'deterministic-ranking',
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
for (const delta of input.contractDeltas) {
|
|
1683
|
+
const tests = delta.consumers.filter((consumer) => consumer.isTestFile);
|
|
1684
|
+
const escapingConsumers = contractEscapingConsumers(delta);
|
|
1685
|
+
const consumerSummary = buildConsumerSummary({
|
|
1686
|
+
targetFile: delta.file,
|
|
1687
|
+
consumers: delta.consumers,
|
|
1688
|
+
externalConsumers: escapingConsumers,
|
|
1689
|
+
profile: input.profile,
|
|
1690
|
+
});
|
|
1691
|
+
rows.push({
|
|
1692
|
+
rank: 0,
|
|
1693
|
+
score: contractSeverityScore(delta),
|
|
1694
|
+
findingType: 'contract-delta',
|
|
1695
|
+
file: delta.file,
|
|
1696
|
+
symbol: delta.symbol,
|
|
1697
|
+
summary: summarizeContract(delta),
|
|
1698
|
+
consumerCount: delta.consumers.length,
|
|
1699
|
+
nonTestConsumerCount: delta.consumers.filter((consumer) => !consumer.isTestFile).length,
|
|
1700
|
+
testConsumerCount: tests.length,
|
|
1701
|
+
externalConsumerCount: escapingConsumers.length,
|
|
1702
|
+
externalConsumerFiles: uniqueSorted(escapingConsumers.map((consumer) => consumer.file)),
|
|
1703
|
+
consumerSummary,
|
|
1704
|
+
reasonCodes: orderedConsequenceReasonCodes([
|
|
1705
|
+
...contractReasonCodes(delta),
|
|
1706
|
+
...consumerSummaryReasonCodes(consumerSummary),
|
|
1707
|
+
]),
|
|
1708
|
+
provenance: 'deterministic-ranking',
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
for (const projection of input.inheritorProjections) {
|
|
1712
|
+
const consumer = {
|
|
1713
|
+
file: projection.inheritorFile,
|
|
1714
|
+
symbol: projection.inheritorSymbol,
|
|
1715
|
+
kind: null,
|
|
1716
|
+
line: 0,
|
|
1717
|
+
isTestFile: projection.isTestFile,
|
|
1718
|
+
inChangedFile: false,
|
|
1719
|
+
provenance: 'typescript-compiler',
|
|
1720
|
+
};
|
|
1721
|
+
const consumerSummary = buildConsumerSummary({
|
|
1722
|
+
targetFile: projection.baseFile,
|
|
1723
|
+
consumers: [consumer],
|
|
1724
|
+
externalConsumers: projection.isTestFile ? [] : [consumer],
|
|
1725
|
+
profile: input.profile,
|
|
1726
|
+
});
|
|
1727
|
+
const projectionReasonCodes = projection.isTestFile
|
|
1728
|
+
? ['inheritor_affected', 'test_only_consumers']
|
|
1729
|
+
: ['inheritor_affected', 'non_test_consumers'];
|
|
1730
|
+
rows.push({
|
|
1731
|
+
rank: 0,
|
|
1732
|
+
score: projection.isTestFile ? 15 : 45,
|
|
1733
|
+
findingType: 'inheritor-projection',
|
|
1734
|
+
file: projection.baseFile,
|
|
1735
|
+
symbol: projection.baseSymbol,
|
|
1736
|
+
summary: `${projection.baseFile}#${projection.baseSymbol} ${projection.relationship} -> ${projection.inheritorFile}#${projection.inheritorSymbol}`,
|
|
1737
|
+
consumerCount: 1,
|
|
1738
|
+
nonTestConsumerCount: projection.isTestFile ? 0 : 1,
|
|
1739
|
+
testConsumerCount: projection.isTestFile ? 1 : 0,
|
|
1740
|
+
externalConsumerCount: projection.isTestFile ? 0 : 1,
|
|
1741
|
+
externalConsumerFiles: projection.isTestFile ? [] : [projection.inheritorFile],
|
|
1742
|
+
consumerSummary,
|
|
1743
|
+
reasonCodes: orderedConsequenceReasonCodes([
|
|
1744
|
+
...projectionReasonCodes,
|
|
1745
|
+
...consumerSummaryReasonCodes(consumerSummary),
|
|
1746
|
+
]),
|
|
1747
|
+
provenance: 'deterministic-ranking',
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
return rows.sort((a, b) => b.score - a.score ||
|
|
1751
|
+
b.nonTestConsumerCount - a.nonTestConsumerCount ||
|
|
1752
|
+
a.file.localeCompare(b.file) ||
|
|
1753
|
+
a.symbol.localeCompare(b.symbol) ||
|
|
1754
|
+
a.summary.localeCompare(b.summary)).map((row, index) => ({ ...row, rank: index + 1 }));
|
|
1755
|
+
}
|
|
1756
|
+
function impactSummary(input) {
|
|
1757
|
+
const parts = [];
|
|
1758
|
+
if (input.productionFiles.length > 0) {
|
|
1759
|
+
parts.push(`${input.productionFiles.length} production consumer file${input.productionFiles.length === 1 ? '' : 's'}`);
|
|
1760
|
+
}
|
|
1761
|
+
if (input.changedProductionFiles.length > 0) {
|
|
1762
|
+
parts.push(`${input.changedProductionFiles.length} consumer file${input.changedProductionFiles.length === 1 ? '' : 's'} also changed`);
|
|
1763
|
+
}
|
|
1764
|
+
if (input.testFiles.length > 0) {
|
|
1765
|
+
parts.push(`${input.testFiles.length} test consumer file${input.testFiles.length === 1 ? '' : 's'}`);
|
|
1766
|
+
}
|
|
1767
|
+
if (input.sensitiveFiles.length > 0) {
|
|
1768
|
+
parts.push(`${input.sensitiveFiles.length} sensitive consumer${input.sensitiveFiles.length === 1 ? '' : 's'}`);
|
|
1769
|
+
}
|
|
1770
|
+
if (input.approvalRequiredFiles.length > 0) {
|
|
1771
|
+
parts.push(`${input.approvalRequiredFiles.length} approval-required consumer${input.approvalRequiredFiles.length === 1 ? '' : 's'}`);
|
|
1772
|
+
}
|
|
1773
|
+
if (input.runtimeGovernanceFiles.length > 0) {
|
|
1774
|
+
parts.push(`${input.runtimeGovernanceFiles.length} runtime/governance consumer${input.runtimeGovernanceFiles.length === 1 ? '' : 's'}`);
|
|
1775
|
+
}
|
|
1776
|
+
const typeText = input.findingTypes.join('+');
|
|
1777
|
+
return `${input.file}#${input.symbol} has ${parts.length > 0 ? parts.join(', ') : 'no external consumer files'} (${typeText})`;
|
|
1778
|
+
}
|
|
1779
|
+
function buildTopImpacts(topFindings) {
|
|
1780
|
+
const bySymbol = new Map();
|
|
1781
|
+
for (const finding of topFindings) {
|
|
1782
|
+
const key = `${finding.file}\0${finding.symbol}`;
|
|
1783
|
+
const bucket = bySymbol.get(key) ?? [];
|
|
1784
|
+
bucket.push(finding);
|
|
1785
|
+
bySymbol.set(key, bucket);
|
|
1786
|
+
}
|
|
1787
|
+
const groups = [...bySymbol.values()].map((findings) => {
|
|
1788
|
+
const first = findings
|
|
1789
|
+
.slice()
|
|
1790
|
+
.sort((a, b) => a.file.localeCompare(b.file) ||
|
|
1791
|
+
a.symbol.localeCompare(b.symbol) ||
|
|
1792
|
+
a.rank - b.rank)[0];
|
|
1793
|
+
const findingTypes = uniqueSorted(findings.map((finding) => finding.findingType));
|
|
1794
|
+
const productionFiles = uniqueSorted(findings.flatMap((finding) => finding.consumerSummary.productionFiles));
|
|
1795
|
+
const changedProductionFiles = uniqueSorted(findings.flatMap((finding) => finding.consumerSummary.changedProductionFiles));
|
|
1796
|
+
const testFiles = uniqueSorted(findings.flatMap((finding) => finding.consumerSummary.testFiles));
|
|
1797
|
+
const sensitiveFiles = uniqueSorted(findings.flatMap((finding) => finding.consumerSummary.sensitiveFiles));
|
|
1798
|
+
const approvalRequiredFiles = uniqueSorted(findings.flatMap((finding) => finding.consumerSummary.approvalRequiredFiles));
|
|
1799
|
+
const runtimeGovernanceFiles = uniqueSorted(findings.flatMap((finding) => finding.consumerSummary.runtimeGovernanceFiles));
|
|
1800
|
+
const reasonCodes = orderedConsequenceReasonCodes(findings.flatMap((finding) => finding.reasonCodes));
|
|
1801
|
+
const reachableProductionConsumerCount = productionFiles.length;
|
|
1802
|
+
const externalProductionConsumerCount = Math.max(0, ...findings.map((finding) => finding.consumerSummary.externalProductionConsumerCount));
|
|
1803
|
+
const changedProductionConsumerCount = changedProductionFiles.length;
|
|
1804
|
+
const sensitiveConsumerCount = sensitiveFiles.length;
|
|
1805
|
+
const approvalRequiredConsumerCount = approvalRequiredFiles.length;
|
|
1806
|
+
const runtimeGovernanceConsumerCount = runtimeGovernanceFiles.length;
|
|
1807
|
+
const productionConsumerCount = Math.max(productionFiles.length, ...findings.map((finding) => finding.consumerSummary.productionConsumerCount));
|
|
1808
|
+
const testConsumerCount = Math.max(testFiles.length, ...findings.map((finding) => finding.consumerSummary.testConsumerCount));
|
|
1809
|
+
const highFanout = findings.some((finding) => finding.consumerSummary.highFanout) || productionFiles.length >= 5 || externalProductionConsumerCount >= 3;
|
|
1810
|
+
const architectureRelevant = highFanout ||
|
|
1811
|
+
sensitiveConsumerCount > 0 ||
|
|
1812
|
+
approvalRequiredConsumerCount > 0 ||
|
|
1813
|
+
runtimeGovernanceConsumerCount > 0 ||
|
|
1814
|
+
findings.some((finding) => finding.consumerSummary.architectureRelevant);
|
|
1815
|
+
const baseScore = Math.max(...findings.map((finding) => finding.score));
|
|
1816
|
+
const score = baseScore +
|
|
1817
|
+
Math.min(60, externalProductionConsumerCount * 12) +
|
|
1818
|
+
Math.min(40, sensitiveConsumerCount * 14) +
|
|
1819
|
+
Math.min(40, approvalRequiredConsumerCount * 16) +
|
|
1820
|
+
Math.min(40, runtimeGovernanceConsumerCount * 14) +
|
|
1821
|
+
(highFanout ? 18 : 0) +
|
|
1822
|
+
(architectureRelevant ? 10 : 0) +
|
|
1823
|
+
(findingTypes.length > 1 ? 8 : 0);
|
|
1824
|
+
return {
|
|
1825
|
+
rank: 0,
|
|
1826
|
+
score,
|
|
1827
|
+
file: first.file,
|
|
1828
|
+
symbol: first.symbol,
|
|
1829
|
+
summary: impactSummary({
|
|
1830
|
+
file: first.file,
|
|
1831
|
+
symbol: first.symbol,
|
|
1832
|
+
findingTypes,
|
|
1833
|
+
productionFiles,
|
|
1834
|
+
changedProductionFiles,
|
|
1835
|
+
testFiles,
|
|
1836
|
+
sensitiveFiles,
|
|
1837
|
+
approvalRequiredFiles,
|
|
1838
|
+
runtimeGovernanceFiles,
|
|
1839
|
+
}),
|
|
1840
|
+
findingTypes,
|
|
1841
|
+
findingRanks: findings.map((finding) => finding.rank).sort((a, b) => a - b),
|
|
1842
|
+
findingCount: findings.length,
|
|
1843
|
+
productionConsumerCount,
|
|
1844
|
+
testConsumerCount,
|
|
1845
|
+
reachableProductionConsumerCount,
|
|
1846
|
+
externalProductionConsumerCount,
|
|
1847
|
+
changedProductionConsumerCount,
|
|
1848
|
+
sensitiveConsumerCount,
|
|
1849
|
+
approvalRequiredConsumerCount,
|
|
1850
|
+
runtimeGovernanceConsumerCount,
|
|
1851
|
+
productionFiles: productionFiles.slice(0, 12),
|
|
1852
|
+
changedProductionFiles: changedProductionFiles.slice(0, 12),
|
|
1853
|
+
testFiles: testFiles.slice(0, 12),
|
|
1854
|
+
sensitiveFiles: sensitiveFiles.slice(0, 12),
|
|
1855
|
+
approvalRequiredFiles: approvalRequiredFiles.slice(0, 12),
|
|
1856
|
+
runtimeGovernanceFiles: runtimeGovernanceFiles.slice(0, 12),
|
|
1857
|
+
highFanout,
|
|
1858
|
+
architectureRelevant,
|
|
1859
|
+
reasonCodes,
|
|
1860
|
+
provenance: 'deterministic-impact-grouping',
|
|
1861
|
+
};
|
|
1862
|
+
});
|
|
1863
|
+
return groups.sort((a, b) => b.score - a.score ||
|
|
1864
|
+
b.externalProductionConsumerCount - a.externalProductionConsumerCount ||
|
|
1865
|
+
b.runtimeGovernanceConsumerCount - a.runtimeGovernanceConsumerCount ||
|
|
1866
|
+
b.sensitiveConsumerCount - a.sensitiveConsumerCount ||
|
|
1867
|
+
b.approvalRequiredConsumerCount - a.approvalRequiredConsumerCount ||
|
|
1868
|
+
a.file.localeCompare(b.file) ||
|
|
1869
|
+
a.symbol.localeCompare(b.symbol)).map((group, index) => ({ ...group, rank: index + 1 }));
|
|
1870
|
+
}
|
|
1871
|
+
function consequenceHeadline(changedSymbolCount, topFindings) {
|
|
1872
|
+
const escaping = topFindings.filter((finding) => finding.externalConsumerCount > 0);
|
|
1873
|
+
const highest = escaping
|
|
1874
|
+
.sort((a, b) => b.externalConsumerCount - a.externalConsumerCount ||
|
|
1875
|
+
b.score - a.score ||
|
|
1876
|
+
a.file.localeCompare(b.file) ||
|
|
1877
|
+
a.symbol.localeCompare(b.symbol))[0] ?? null;
|
|
1878
|
+
if (!highest) {
|
|
1879
|
+
return {
|
|
1880
|
+
escapingConsequenceCount: 0,
|
|
1881
|
+
highestExternalConsumerCount: 0,
|
|
1882
|
+
headline: `${changedSymbolCount} symbols changed; 0 escaping TypeScript-static consequences found (effects checked: allowlisted filesystem/network/session evidence; SQL, dependency injection, dynamic dispatch, route wiring, and cross-language behavior not analyzed).`,
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
return {
|
|
1886
|
+
escapingConsequenceCount: escaping.length,
|
|
1887
|
+
highestExternalConsumerCount: highest.externalConsumerCount,
|
|
1888
|
+
headline: `${changedSymbolCount} symbols changed; ${escaping.length} consequence${escaping.length === 1 ? '' : 's'} ${escaping.length === 1 ? 'reaches' : 'reach'} non-test callers outside this change; highest-fanout: ${highest.file}#${highest.symbol} (${highest.externalConsumerCount} external caller${highest.externalConsumerCount === 1 ? '' : 's'}).`,
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
function heritageRelationship(token) {
|
|
1892
|
+
if (token.token === ts.SyntaxKind.ExtendsKeyword)
|
|
1893
|
+
return 'extends';
|
|
1894
|
+
return 'implements';
|
|
1895
|
+
}
|
|
1896
|
+
function buildInheritorProjections(input) {
|
|
1897
|
+
const deltaKeys = new Set(input.contractDeltas.map((delta) => symbolKey(delta.file, delta.kind, delta.symbol)));
|
|
1898
|
+
const targetByResolvedSymbol = new Map();
|
|
1899
|
+
for (const target of input.targets) {
|
|
1900
|
+
if (!deltaKeys.has(symbolKey(target.file, target.kind, target.name)))
|
|
1901
|
+
continue;
|
|
1902
|
+
if (target.kind !== 'interface' && target.kind !== 'class' && target.kind !== 'type')
|
|
1903
|
+
continue;
|
|
1904
|
+
if (target.symbol) {
|
|
1905
|
+
targetByResolvedSymbol.set(target.symbol, target);
|
|
1906
|
+
const aliased = resolveAlias(input.checker, target.symbol);
|
|
1907
|
+
if (aliased)
|
|
1908
|
+
targetByResolvedSymbol.set(aliased, target);
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
if (targetByResolvedSymbol.size === 0)
|
|
1912
|
+
return [];
|
|
1913
|
+
const byKey = new Map();
|
|
1914
|
+
for (const sourceFile of input.program.getSourceFiles()) {
|
|
1915
|
+
if (sourceFile.isDeclarationFile || sourceFile.fileName.includes('node_modules'))
|
|
1916
|
+
continue;
|
|
1917
|
+
const file = repoRel(input.projectRoot, sourceFile.fileName);
|
|
1918
|
+
if (input.changedFileSet.has(file))
|
|
1919
|
+
continue;
|
|
1920
|
+
const visit = (node) => {
|
|
1921
|
+
if ((ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) && node.name && node.heritageClauses) {
|
|
1922
|
+
for (const clause of node.heritageClauses) {
|
|
1923
|
+
for (const type of clause.types) {
|
|
1924
|
+
const symbol = resolveAlias(input.checker, input.checker.getSymbolAtLocation(type.expression));
|
|
1925
|
+
const target = symbol ? targetByResolvedSymbol.get(symbol) : undefined;
|
|
1926
|
+
if (!target)
|
|
1927
|
+
continue;
|
|
1928
|
+
const relationship = ts.isInterfaceDeclaration(node) && clause.token === ts.SyntaxKind.ExtendsKeyword
|
|
1929
|
+
? 'type-extends'
|
|
1930
|
+
: heritageRelationship(clause);
|
|
1931
|
+
const row = {
|
|
1932
|
+
baseSymbol: target.name,
|
|
1933
|
+
baseFile: target.file,
|
|
1934
|
+
inheritorSymbol: node.name.text,
|
|
1935
|
+
inheritorFile: file,
|
|
1936
|
+
relationship,
|
|
1937
|
+
isTestFile: isTestFile(file),
|
|
1938
|
+
provenance: 'inheritance-projection',
|
|
1939
|
+
};
|
|
1940
|
+
byKey.set(`${row.baseFile}#${row.baseSymbol}<-${row.inheritorFile}#${row.inheritorSymbol}`, row);
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
ts.forEachChild(node, visit);
|
|
1945
|
+
};
|
|
1946
|
+
ts.forEachChild(sourceFile, visit);
|
|
1947
|
+
}
|
|
1948
|
+
return [...byKey.values()].sort((a, b) => a.baseFile.localeCompare(b.baseFile) ||
|
|
1949
|
+
a.baseSymbol.localeCompare(b.baseSymbol) ||
|
|
1950
|
+
a.inheritorFile.localeCompare(b.inheritorFile) ||
|
|
1951
|
+
a.inheritorSymbol.localeCompare(b.inheritorSymbol));
|
|
1952
|
+
}
|
|
1953
|
+
function buildConsequenceUnderstanding(input) {
|
|
1954
|
+
if (input.targets.length === 0) {
|
|
1955
|
+
return emptyConsequenceUnderstanding(input.generatedAt, 'no changed symbols available for consequence analysis');
|
|
1956
|
+
}
|
|
1957
|
+
if (input.suppressedArtifacts.length > 0 && input.targets.length === 0) {
|
|
1958
|
+
return emptyConsequenceUnderstanding(input.generatedAt, 'no analyzable symbols after generated-artifact suppression');
|
|
1959
|
+
}
|
|
1960
|
+
const effectDeltas = buildEffectDeltas({
|
|
1961
|
+
diffFiles: input.diffFiles,
|
|
1962
|
+
targets: input.targets,
|
|
1963
|
+
references: input.references,
|
|
1964
|
+
});
|
|
1965
|
+
const contractDeltas = attachContractConsumers(buildContractDeltas(input.projectRoot, input.diffFiles, input.targets), input.references);
|
|
1966
|
+
const inheritorProjections = buildInheritorProjections({
|
|
1967
|
+
program: input.program,
|
|
1968
|
+
checker: input.checker,
|
|
1969
|
+
projectRoot: input.projectRoot,
|
|
1970
|
+
targets: input.targets,
|
|
1971
|
+
contractDeltas,
|
|
1972
|
+
changedFileSet: input.changedFileSet,
|
|
1973
|
+
});
|
|
1974
|
+
const topFindings = buildTopFindings({
|
|
1975
|
+
effectDeltas,
|
|
1976
|
+
contractDeltas,
|
|
1977
|
+
inheritorProjections,
|
|
1978
|
+
profile: input.profile,
|
|
1979
|
+
});
|
|
1980
|
+
const topImpacts = buildTopImpacts(topFindings);
|
|
1981
|
+
const headline = consequenceHeadline(input.targets.length, topFindings);
|
|
1982
|
+
const core = {
|
|
1983
|
+
schemaVersion: exports.CONSEQUENCE_UNDERSTANDING_SCHEMA_VERSION,
|
|
1984
|
+
generatedAt: input.generatedAt,
|
|
1985
|
+
analyzed: true,
|
|
1986
|
+
reason: null,
|
|
1987
|
+
confidence: 'deterministic-static',
|
|
1988
|
+
modelUsed: false,
|
|
1989
|
+
effectDeltas,
|
|
1990
|
+
contractDeltas,
|
|
1991
|
+
inheritorProjections,
|
|
1992
|
+
topFindings,
|
|
1993
|
+
topImpacts,
|
|
1994
|
+
summary: {
|
|
1995
|
+
effectDeltaCount: effectDeltas.length,
|
|
1996
|
+
contractDeltaCount: contractDeltas.length,
|
|
1997
|
+
inheritorProjectionCount: inheritorProjections.length,
|
|
1998
|
+
topFindingCount: topFindings.length,
|
|
1999
|
+
topImpactCount: topImpacts.length,
|
|
2000
|
+
escapingConsequenceCount: headline.escapingConsequenceCount,
|
|
2001
|
+
highestExternalConsumerCount: headline.highestExternalConsumerCount,
|
|
2002
|
+
headline: headline.headline,
|
|
2003
|
+
},
|
|
2004
|
+
limitations: [
|
|
2005
|
+
'Known-effect detection is allowlist-bounded; effects via unrecognized wrappers, dynamic dispatch, or dependency injection are not claimed.',
|
|
2006
|
+
'Contract detection is TypeScript/JavaScript static analysis only.',
|
|
2007
|
+
'Contract consumers are static symbol references; HTTP, serialization, and dynamic runtime consumers are not proven.',
|
|
2008
|
+
'Inheritor projection only follows static TypeScript extends/implements relationships outside changed files.',
|
|
2009
|
+
'Generated artifacts are skipped entirely and reported through suppressedArtifacts.',
|
|
2010
|
+
'No LLM or probabilistic judgment is used in this artifact.',
|
|
2011
|
+
'No source text, diff hunks, patch content, or shell command bodies are stored.',
|
|
2012
|
+
],
|
|
2013
|
+
factProvenance: {
|
|
2014
|
+
effectDeltas: 'effect-registry',
|
|
2015
|
+
contractDeltas: 'typescript-checker',
|
|
2016
|
+
inheritorProjections: 'inheritance-projection',
|
|
2017
|
+
topFindings: 'deterministic-ranking',
|
|
2018
|
+
topImpacts: 'deterministic-impact-grouping',
|
|
2019
|
+
},
|
|
2020
|
+
};
|
|
2021
|
+
return { ...core, artifactHash: hash(consequenceHashInput(core)) };
|
|
2022
|
+
}
|
|
2023
|
+
function structuralUnderstandingPath(projectRoot, sessionId) {
|
|
2024
|
+
return (0, path_1.join)(projectRoot, '.neurcode', 'understanding', `${sessionId}.json`);
|
|
2025
|
+
}
|
|
2026
|
+
function buildStructuralUnderstanding(projectRoot, diffFiles, options = {}) {
|
|
2027
|
+
const generatedAt = options.generatedAt ?? new Date().toISOString();
|
|
2028
|
+
const changedFiles = diffFiles.map((file) => normalizeRepoPath(file.path));
|
|
2029
|
+
const profile = options.profile ?? profileFromSession(options.session);
|
|
2030
|
+
const suppressedArtifacts = classifyGeneratedArtifacts(projectRoot, changedFiles);
|
|
2031
|
+
const suppressedArtifactPaths = new Set(suppressedArtifacts.map((item) => item.path));
|
|
2032
|
+
const tsChanged = changedFiles.filter((file) => TS_LIKE.test(file) &&
|
|
2033
|
+
!/\.d\.ts$/i.test(file) &&
|
|
2034
|
+
!suppressedArtifactPaths.has(file));
|
|
2035
|
+
if (tsChanged.length === 0) {
|
|
2036
|
+
const reason = suppressedArtifacts.length > 0
|
|
2037
|
+
? 'no analyzable TypeScript/JavaScript files after generated-artifact suppression'
|
|
2038
|
+
: 'no TypeScript/JavaScript files in change set';
|
|
2039
|
+
const artifact = none(projectRoot, diffFiles, reason, generatedAt, suppressedArtifacts);
|
|
2040
|
+
artifact.planAlignment = buildPlanAlignment(options.session ?? null, changedFiles, []);
|
|
2041
|
+
artifact.boundaryImpact = buildBoundaryImpact(profile, changedFiles);
|
|
2042
|
+
artifact.artifactHash = hash(artifactHashInput(artifact));
|
|
2043
|
+
return artifact;
|
|
2044
|
+
}
|
|
2045
|
+
const maxFiles = options.maxProgramFiles ?? 6000;
|
|
2046
|
+
const timeBudgetMs = options.timeBudgetMs ?? 12000;
|
|
2047
|
+
const started = Date.now();
|
|
2048
|
+
const roots = scopeRoots(projectRoot, tsChanged);
|
|
2049
|
+
if (roots.length === 0)
|
|
2050
|
+
return none(projectRoot, diffFiles, 'no resolvable TypeScript scope roots', generatedAt, suppressedArtifacts);
|
|
2051
|
+
const sourceFiles = new Set();
|
|
2052
|
+
for (const root of roots) {
|
|
2053
|
+
const gathered = [];
|
|
2054
|
+
gatherSourceFiles(root, gathered, maxFiles + 1);
|
|
2055
|
+
for (const file of gathered)
|
|
2056
|
+
sourceFiles.add(file);
|
|
2057
|
+
if (sourceFiles.size > maxFiles)
|
|
2058
|
+
return none(projectRoot, diffFiles, `program too large (>${maxFiles} files)`, generatedAt, suppressedArtifacts);
|
|
2059
|
+
}
|
|
2060
|
+
for (const file of tsChanged) {
|
|
2061
|
+
const abs = (0, path_1.join)(projectRoot, file);
|
|
2062
|
+
if ((0, fs_1.existsSync)(abs) && (0, fs_1.statSync)(abs).isFile())
|
|
2063
|
+
sourceFiles.add(abs);
|
|
2064
|
+
}
|
|
2065
|
+
// ── Cross-package recall ──────────────────────────────────────────────────────
|
|
2066
|
+
// A program scoped to the changed package alone can never see callers that live
|
|
2067
|
+
// in OTHER workspace packages. Add the direct importer files of every package that
|
|
2068
|
+
// (transitively) depends on the changed package(s), and map workspace imports to
|
|
2069
|
+
// source so those callers resolve to the same declaration we changed — not the
|
|
2070
|
+
// built .d.ts (which would be a different symbol and silently match nothing).
|
|
2071
|
+
const workspace = loadWorkspacePackages(projectRoot);
|
|
2072
|
+
const changedPackages = changedWorkspacePackages(workspace, tsChanged);
|
|
2073
|
+
const changedPackageNames = changedPackages.map((pkg) => pkg.name);
|
|
2074
|
+
const reverseDepConsumerNames = new Set(changedPackageNames.length
|
|
2075
|
+
? reverseDepConsumers(workspace, new Set(changedPackageNames)).map((pkg) => pkg.name)
|
|
2076
|
+
: []);
|
|
2077
|
+
const consumerPackages = [];
|
|
2078
|
+
let directImporterFilesAdded = 0;
|
|
2079
|
+
for (const consumer of changedPackageNames.length
|
|
2080
|
+
? workspace.filter((pkg) => !changedPackageNames.includes(pkg.name))
|
|
2081
|
+
: []) {
|
|
2082
|
+
const before = sourceFiles.size;
|
|
2083
|
+
const gathered = [];
|
|
2084
|
+
gatherDirectImporterFiles(consumer, changedPackageNames, gathered, maxFiles + 1);
|
|
2085
|
+
for (const file of gathered)
|
|
2086
|
+
sourceFiles.add(file);
|
|
2087
|
+
const added = sourceFiles.size - before;
|
|
2088
|
+
directImporterFilesAdded += added;
|
|
2089
|
+
if (added > 0 || reverseDepConsumerNames.has(consumer.name))
|
|
2090
|
+
consumerPackages.push(consumer);
|
|
2091
|
+
if (sourceFiles.size > maxFiles)
|
|
2092
|
+
break;
|
|
2093
|
+
}
|
|
2094
|
+
const workspacePaths = buildWorkspacePaths(workspace);
|
|
2095
|
+
const crossPackageResolved = changedPackageNames.length > 0 && Object.keys(workspacePaths).length > 0;
|
|
2096
|
+
const files = [...sourceFiles];
|
|
2097
|
+
if (files.length === 0)
|
|
2098
|
+
return none(projectRoot, diffFiles, 'no TypeScript source files gathered', generatedAt, suppressedArtifacts);
|
|
2099
|
+
const compilerOptions = {
|
|
2100
|
+
target: ts.ScriptTarget.ES2020,
|
|
2101
|
+
module: ts.ModuleKind.CommonJS,
|
|
2102
|
+
moduleResolution: ts.ModuleResolutionKind.Node10,
|
|
2103
|
+
allowJs: true,
|
|
2104
|
+
noEmit: true,
|
|
2105
|
+
skipLibCheck: true,
|
|
2106
|
+
noResolve: false,
|
|
2107
|
+
...(crossPackageResolved ? { baseUrl: projectRoot, paths: workspacePaths } : {}),
|
|
2108
|
+
};
|
|
2109
|
+
const program = ts.createProgram(files, compilerOptions);
|
|
2110
|
+
const checker = program.getTypeChecker();
|
|
2111
|
+
if (Date.now() - started > timeBudgetMs)
|
|
2112
|
+
return none(projectRoot, diffFiles, 'time budget exceeded during program build', generatedAt);
|
|
2113
|
+
const declarationsByFile = new Map();
|
|
2114
|
+
for (const sourceFile of program.getSourceFiles()) {
|
|
2115
|
+
if (sourceFile.isDeclarationFile || sourceFile.fileName.includes('node_modules'))
|
|
2116
|
+
continue;
|
|
2117
|
+
declarationsByFile.set(repoRel(projectRoot, sourceFile.fileName), collectDeclarations(sourceFile, checker, projectRoot));
|
|
2118
|
+
}
|
|
2119
|
+
const targets = new Map();
|
|
2120
|
+
const targetBySymbol = new Map();
|
|
2121
|
+
for (const diffFile of diffFiles) {
|
|
2122
|
+
const file = normalizeRepoPath(diffFile.path);
|
|
2123
|
+
if (suppressedArtifactPaths.has(file))
|
|
2124
|
+
continue;
|
|
2125
|
+
if (!TS_LIKE.test(file) || /\.d\.ts$/i.test(file))
|
|
2126
|
+
continue;
|
|
2127
|
+
const declarations = declarationsByFile.get(file) ?? [];
|
|
2128
|
+
for (const line of changedNewLines(diffFile)) {
|
|
2129
|
+
const decl = smallestContaining(declarations, line);
|
|
2130
|
+
if (!decl)
|
|
2131
|
+
continue;
|
|
2132
|
+
const key = symbolKey(decl.file, decl.kind, decl.name);
|
|
2133
|
+
const existing = targets.get(key);
|
|
2134
|
+
const action = diffFile.changeType === 'add' ? 'add' : 'modify';
|
|
2135
|
+
const candidate = {
|
|
2136
|
+
name: decl.name,
|
|
2137
|
+
kind: decl.kind,
|
|
2138
|
+
file: decl.file,
|
|
2139
|
+
lineStart: decl.startLine,
|
|
2140
|
+
lineEnd: decl.endLine,
|
|
2141
|
+
action,
|
|
2142
|
+
symbol: decl.symbol,
|
|
2143
|
+
declaration: decl.node,
|
|
2144
|
+
};
|
|
2145
|
+
if (!existing || existing.action !== 'add')
|
|
2146
|
+
targets.set(key, candidate);
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
for (const target of targets.values()) {
|
|
2150
|
+
if (target.symbol) {
|
|
2151
|
+
targetBySymbol.set(target.symbol, target);
|
|
2152
|
+
const aliased = resolveAlias(checker, target.symbol);
|
|
2153
|
+
if (aliased)
|
|
2154
|
+
targetBySymbol.set(aliased, target);
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
const changedSymbols = [...targets.values()]
|
|
2158
|
+
.map((target) => ({
|
|
2159
|
+
name: target.name,
|
|
2160
|
+
kind: target.kind,
|
|
2161
|
+
file: target.file,
|
|
2162
|
+
action: target.action,
|
|
2163
|
+
lineStart: target.lineStart,
|
|
2164
|
+
lineEnd: target.lineEnd,
|
|
2165
|
+
provenance: 'typescript-compiler',
|
|
2166
|
+
}))
|
|
2167
|
+
.sort((a, b) => a.file.localeCompare(b.file) || a.lineStart - b.lineStart || a.name.localeCompare(b.name));
|
|
2168
|
+
const referenceMap = new Map();
|
|
2169
|
+
const changedFileSet = new Set(tsChanged);
|
|
2170
|
+
const importExportGraph = buildImportExportTargetGraph({
|
|
2171
|
+
program,
|
|
2172
|
+
projectRoot,
|
|
2173
|
+
compilerOptions,
|
|
2174
|
+
targets: [...targets.values()],
|
|
2175
|
+
});
|
|
2176
|
+
let budgetExceeded = false;
|
|
2177
|
+
for (const sourceFile of program.getSourceFiles()) {
|
|
2178
|
+
if (sourceFile.isDeclarationFile || sourceFile.fileName.includes('node_modules'))
|
|
2179
|
+
continue;
|
|
2180
|
+
if (Date.now() - started > timeBudgetMs) {
|
|
2181
|
+
budgetExceeded = true;
|
|
2182
|
+
break;
|
|
2183
|
+
}
|
|
2184
|
+
const sourceRel = repoRel(projectRoot, sourceFile.fileName);
|
|
2185
|
+
const sourceDeclarations = declarationsByFile.get(sourceRel) ?? [];
|
|
2186
|
+
const visit = (node) => {
|
|
2187
|
+
if (ts.isIdentifier(node)) {
|
|
2188
|
+
const symbol = resolveAlias(checker, checker.getSymbolAtLocation(node));
|
|
2189
|
+
const checkerTarget = symbol ? targetBySymbol.get(symbol) : undefined;
|
|
2190
|
+
const graphTarget = importExportGraphTargetForIdentifier(node, sourceRel, importExportGraph);
|
|
2191
|
+
const target = checkerTarget ?? graphTarget;
|
|
2192
|
+
if (target && node !== target.declaration && node.parent !== target.declaration) {
|
|
2193
|
+
const line = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
|
2194
|
+
const enclosing = smallestContaining(sourceDeclarations, line);
|
|
2195
|
+
const ref = {
|
|
2196
|
+
targetSymbol: target.name,
|
|
2197
|
+
targetKind: target.kind,
|
|
2198
|
+
targetFile: target.file,
|
|
2199
|
+
referencingFile: sourceRel,
|
|
2200
|
+
referencingSymbol: enclosing?.name ?? null,
|
|
2201
|
+
referencingKind: enclosing?.kind ?? null,
|
|
2202
|
+
line,
|
|
2203
|
+
isTestFile: isTestFile(sourceRel),
|
|
2204
|
+
inChangedFile: changedFileSet.has(sourceRel),
|
|
2205
|
+
provenance: checkerTarget ? 'typescript-compiler' : 'import-export-graph',
|
|
2206
|
+
};
|
|
2207
|
+
referenceMap.set(`${ref.targetFile}#${ref.targetKind}#${ref.targetSymbol}:${ref.referencingFile}:${ref.line}:${ref.referencingSymbol ?? ''}`, ref);
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
ts.forEachChild(node, visit);
|
|
2211
|
+
};
|
|
2212
|
+
ts.forEachChild(sourceFile, visit);
|
|
2213
|
+
}
|
|
2214
|
+
const references = [...referenceMap.values()]
|
|
2215
|
+
.filter((ref) => !(ref.inChangedFile && ref.referencingFile === ref.targetFile))
|
|
2216
|
+
.sort((a, b) => a.targetFile.localeCompare(b.targetFile) ||
|
|
2217
|
+
a.targetSymbol.localeCompare(b.targetSymbol) ||
|
|
2218
|
+
a.referencingFile.localeCompare(b.referencingFile) ||
|
|
2219
|
+
a.line - b.line);
|
|
2220
|
+
const testReferences = references.filter((ref) => ref.isTestFile);
|
|
2221
|
+
const planAlignment = buildPlanAlignment(options.session ?? null, changedFiles, changedSymbols);
|
|
2222
|
+
const boundaryImpact = buildBoundaryImpact(profile, changedFiles);
|
|
2223
|
+
const digest = buildStructuralDigest({
|
|
2224
|
+
generatedAt,
|
|
2225
|
+
changedSymbols,
|
|
2226
|
+
references,
|
|
2227
|
+
testReferences,
|
|
2228
|
+
workspace,
|
|
2229
|
+
changedPackageNames,
|
|
2230
|
+
changedFiles,
|
|
2231
|
+
profile,
|
|
2232
|
+
planAlignment,
|
|
2233
|
+
});
|
|
2234
|
+
const consequenceUnderstanding = buildConsequenceUnderstanding({
|
|
2235
|
+
projectRoot,
|
|
2236
|
+
generatedAt,
|
|
2237
|
+
diffFiles,
|
|
2238
|
+
targets: [...targets.values()],
|
|
2239
|
+
references,
|
|
2240
|
+
program,
|
|
2241
|
+
checker,
|
|
2242
|
+
changedFileSet,
|
|
2243
|
+
suppressedArtifacts,
|
|
2244
|
+
profile,
|
|
2245
|
+
});
|
|
2246
|
+
const core = {
|
|
2247
|
+
schemaVersion: exports.STRUCTURAL_UNDERSTANDING_SCHEMA_VERSION,
|
|
2248
|
+
generatedAt,
|
|
2249
|
+
repoRootHash: hash(projectRoot),
|
|
2250
|
+
privacy: privacyBlock(),
|
|
2251
|
+
analysis: {
|
|
2252
|
+
language: 'typescript',
|
|
2253
|
+
analyzed: true,
|
|
2254
|
+
reason: null,
|
|
2255
|
+
confidence: 'deterministic-static',
|
|
2256
|
+
filesAnalyzed: files.length,
|
|
2257
|
+
changedFileCount: changedFiles.length,
|
|
2258
|
+
changedSymbolCount: changedSymbols.length,
|
|
2259
|
+
referenceCount: references.length,
|
|
2260
|
+
testReferenceCount: testReferences.length,
|
|
2261
|
+
crossPackage: {
|
|
2262
|
+
changedPackages: changedPackageNames,
|
|
2263
|
+
consumerPackagesScanned: consumerPackages.map((pkg) => pkg.name),
|
|
2264
|
+
directImporterFilesAdded,
|
|
2265
|
+
resolvedToSource: crossPackageResolved,
|
|
2266
|
+
},
|
|
2267
|
+
},
|
|
2268
|
+
changedFiles: changedFileFacts(diffFiles),
|
|
2269
|
+
changedSymbols,
|
|
2270
|
+
references,
|
|
2271
|
+
importEdgesChanged: importEdgeFacts(diffFiles, suppressedArtifacts),
|
|
2272
|
+
suppressedArtifacts,
|
|
2273
|
+
testReferences,
|
|
2274
|
+
digest,
|
|
2275
|
+
consequenceUnderstanding,
|
|
2276
|
+
planAlignment,
|
|
2277
|
+
boundaryImpact,
|
|
2278
|
+
limitations: [
|
|
2279
|
+
...commonLimitations(),
|
|
2280
|
+
...(changedPackageNames.length
|
|
2281
|
+
? [
|
|
2282
|
+
`Cross-package references resolved for direct importers of [${changedPackageNames.join(', ')}] across ${consumerPackages.length} consumer package(s). Transitive re-export chains and consumers that do not import the changed package directly may be under-counted.`,
|
|
2283
|
+
]
|
|
2284
|
+
: []),
|
|
2285
|
+
...(budgetExceeded ? ['Analysis stopped at the time budget; reference recall may be partial.'] : []),
|
|
2286
|
+
...(suppressedArtifacts.length
|
|
2287
|
+
? [
|
|
2288
|
+
`${suppressedArtifacts.length} generated artifact(s) were excluded from symbol/reference analysis. Suppression is path/fingerprint based and visible in suppressedArtifacts.`,
|
|
2289
|
+
]
|
|
2290
|
+
: []),
|
|
2291
|
+
],
|
|
2292
|
+
factProvenance: provenanceMap(),
|
|
2293
|
+
};
|
|
2294
|
+
return {
|
|
2295
|
+
...core,
|
|
2296
|
+
artifactHash: hash(artifactHashInput(core)),
|
|
2297
|
+
};
|
|
2298
|
+
}
|
|
2299
|
+
function writeStructuralUnderstanding(projectRoot, sessionId, artifact) {
|
|
2300
|
+
const path = structuralUnderstandingPath(projectRoot, sessionId);
|
|
2301
|
+
(0, fs_1.mkdirSync)((0, path_1.join)(projectRoot, '.neurcode', 'understanding'), { recursive: true });
|
|
2302
|
+
(0, fs_1.writeFileSync)(path, JSON.stringify(artifact, null, 2) + '\n', 'utf8');
|
|
2303
|
+
return path;
|
|
2304
|
+
}
|
|
2305
|
+
function readStructuralUnderstanding(projectRoot, sessionId) {
|
|
2306
|
+
const path = structuralUnderstandingPath(projectRoot, sessionId);
|
|
2307
|
+
if (!(0, fs_1.existsSync)(path))
|
|
2308
|
+
return null;
|
|
2309
|
+
try {
|
|
2310
|
+
return JSON.parse((0, fs_1.readFileSync)(path, 'utf8'));
|
|
2311
|
+
}
|
|
2312
|
+
catch {
|
|
2313
|
+
return null;
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
//# sourceMappingURL=structural-understanding.js.map
|