arkgate 3.6.1 → 3.8.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/CHANGELOG.md +122 -1145
- package/README.md +59 -19
- package/bin/ark-check-runtime.mjs +1598 -0
- package/bin/ark-check.mjs +32 -1565
- package/bin/ark-layer-match.mjs +2 -1
- package/bin/ark-mcp-runtime.mjs +1976 -0
- package/bin/ark-mcp.mjs +84 -1495
- package/bin/ark-shared.mjs +34 -38
- package/bin/ark.mjs +33 -66
- package/bin/lib/adapter-contract.mjs +161 -9
- package/bin/lib/agent-gates.mjs +1 -0
- package/bin/lib/analysis-completeness.mjs +28 -0
- package/bin/lib/analysis-engine.mjs +8 -8
- package/bin/lib/analysis-policy.mjs +27 -0
- package/bin/lib/architecture-scan.mjs +70 -304
- package/bin/lib/auto-patch.mjs +76 -8
- package/bin/lib/ci-and-commands.mjs +1 -1
- package/bin/lib/codex-home.mjs +43 -16
- package/bin/lib/design-delta.mjs +4 -0
- package/bin/lib/design-smells.mjs +67 -14
- package/bin/lib/doctor-advisories.mjs +23 -7
- package/bin/lib/doctor-plan.mjs +44 -47
- package/bin/lib/enforcement-state.mjs +2 -0
- package/bin/lib/github-enforcement.mjs +443 -0
- package/bin/lib/hook-templates.mjs +12 -148
- package/bin/lib/html-report-advisories.mjs +59 -0
- package/bin/lib/html-report-depth.mjs +9 -0
- package/bin/lib/html-report.mjs +5 -5
- package/bin/lib/install-migrate.mjs +83 -79
- package/bin/lib/managed-upgrade.mjs +622 -0
- package/bin/lib/mcp-adoption.mjs +3 -1
- package/bin/lib/parse-health.mjs +75 -0
- package/bin/lib/port-proof.mjs +2 -2
- package/bin/lib/prepare-change.mjs +68 -38
- package/bin/lib/prepare-write.mjs +7 -1
- package/bin/lib/reshape-decisions.mjs +284 -0
- package/bin/lib/resident-doctor-client.mjs +55 -0
- package/bin/lib/resident-hook.mjs +247 -0
- package/bin/lib/resolved-candidate-facts.mjs +1160 -0
- package/bin/lib/scan-files.mjs +19 -6
- package/bin/lib/snippet-analysis.mjs +119 -0
- package/bin/lib/source-policy.mjs +24 -0
- package/bin/lib/typescript-host.mjs +15 -18
- package/bin/lib/unavailable-analysis.mjs +76 -0
- package/bin/lib/upgrade-command.mjs +115 -0
- package/bin/lib/weakest-link.mjs +21 -179
- package/bin/lib/write-path-capabilities.mjs +167 -16
- package/bin/lib/write-path-detect.mjs +3 -2
- package/dist/eslint/index.cjs +3 -3
- package/dist/eslint/index.d.ts +3 -0
- package/dist/eslint/index.js +3 -3
- package/dist/index.cjs +7 -7
- package/dist/index.d.ts +1073 -141
- package/dist/index.js +7 -7
- package/docs/agent-guide.md +127 -52
- package/docs/ai-gates.md +100 -18
- package/docs/configuration.md +6 -0
- package/docs/demos/01-write-gate-self-correction.md +2 -2
- package/docs/enthusiast/README.md +10 -10
- package/docs/enthusiast/how-to-gallery-starter.md +2 -2
- package/docs/enthusiast/reference-commands.md +18 -1
- package/docs/enthusiast/tutorial-first-project.md +2 -2
- package/docs/package-surface.md +101 -14
- package/docs/typescript-support.md +118 -37
- package/package.json +33 -4
- package/schemas/ark.analysis-result.schema.json +159 -2
- package/schemas/ark.design-delta.schema.json +1 -0
- package/schemas/ark.enforcement-state.schema.json +84 -0
- package/schemas/ark.resolved-candidate-facts.schema.json +1 -0
- package/server.json +2 -2
- package/templates/skills/ark-autopilot.md +12 -0
- package/templates/skills/ark-explore.md +12 -5
- package/templates/skills/ark-fix.md +12 -2
- package/templates/skills/ark-loop.md +14 -1
- package/templates/skills/ark-runtime.md +15 -8
- package/templates/skills/ark-upgrade.md +122 -182
- package/bin/lib/ai-velocity.mjs +0 -293
- package/bin/lib/graph-cycles.mjs +0 -6
- package/bin/lib/safety-diagnostics.mjs +0 -284
- package/bin/lib/ts-resolve.mjs +0 -227
- package/dist/configTypes-DAPvBqK6.d.cts +0 -61
- package/dist/eslint/index.d.cts +0 -146
- package/dist/index.d.cts +0 -986
|
@@ -0,0 +1,1160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single shipped TypeScript/filesystem resolver for ADR 0011 facts.
|
|
3
|
+
*
|
|
4
|
+
* This Tooling adapter owns discovery, nearest-tsconfig lookup, package/symlink
|
|
5
|
+
* resolution, and complete in-memory source overlays. It never classifies a
|
|
6
|
+
* layer or decides a rule; the generated Kernel bundle owns that verdict.
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
import { isScanExcludedRelative } from '../ark-shared.mjs';
|
|
12
|
+
import {
|
|
13
|
+
AMBIENT_CAPABILITY_ENTRIES,
|
|
14
|
+
collectCapabilityUses,
|
|
15
|
+
collectForbiddenCapabilityUses,
|
|
16
|
+
createTrustedResolvedCandidateFacts,
|
|
17
|
+
deterministicHash,
|
|
18
|
+
extractSemanticDependencies,
|
|
19
|
+
looksLikeArkIntent,
|
|
20
|
+
resolvedFactsEvidenceRequirementsHash,
|
|
21
|
+
stableSerialize,
|
|
22
|
+
} from './analysis-engine.mjs';
|
|
23
|
+
import {
|
|
24
|
+
isArkPublishCandidate,
|
|
25
|
+
isPublishCall,
|
|
26
|
+
lineOf,
|
|
27
|
+
namedModuleBindings,
|
|
28
|
+
objectHasProperty,
|
|
29
|
+
publishHasSource,
|
|
30
|
+
publishSourceLiteral,
|
|
31
|
+
sourceFileExportsOnlyTypes,
|
|
32
|
+
sourceFileHasTopLevelSideEffects,
|
|
33
|
+
stringLiteralText,
|
|
34
|
+
typeOnlyExportNames,
|
|
35
|
+
} from './ast-scan.mjs';
|
|
36
|
+
import { provePortProofInject } from './port-proof.mjs';
|
|
37
|
+
import {
|
|
38
|
+
collectGovernedFiles,
|
|
39
|
+
isGovernableSourceFile,
|
|
40
|
+
normalize,
|
|
41
|
+
} from './scan-files.mjs';
|
|
42
|
+
|
|
43
|
+
export const RESOLVED_FACTS_RESOLVER_IDENTITY = 'arkgate-typescript-resolver@1';
|
|
44
|
+
|
|
45
|
+
const IN_MEMORY_STORES = new Set([
|
|
46
|
+
'InMemoryAuditStore',
|
|
47
|
+
'InMemoryOutboxStore',
|
|
48
|
+
'InMemoryReadModelStore',
|
|
49
|
+
'InMemoryWorkflowStore',
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
const IN_MEMORY_DEFAULT_FACTORIES = new Map([
|
|
53
|
+
['createArkKernel', ['outbox', 'auditTrail', 'projections']],
|
|
54
|
+
['createAuditTrail', ['store']],
|
|
55
|
+
['createProjectionRegistry', ['store']],
|
|
56
|
+
['createWorkflowEngine', ['store']],
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
function canonicalProjectPath(value) {
|
|
60
|
+
if (typeof value !== 'string' || value.length === 0 || value.includes('\0')) {
|
|
61
|
+
throw new Error('Every candidate path must be a non-empty project-relative path.');
|
|
62
|
+
}
|
|
63
|
+
const portable = value.replace(/\\/g, '/');
|
|
64
|
+
if (portable.startsWith('/') || /^[A-Za-z]:\//.test(portable)) {
|
|
65
|
+
throw new Error(`Candidate path must be project-relative: ${value}`);
|
|
66
|
+
}
|
|
67
|
+
const normalized = path.posix.normalize(portable);
|
|
68
|
+
if (
|
|
69
|
+
normalized === '.' ||
|
|
70
|
+
normalized === '..' ||
|
|
71
|
+
normalized.startsWith('../') ||
|
|
72
|
+
normalized !== portable
|
|
73
|
+
) {
|
|
74
|
+
throw new Error(`Candidate path must be canonical: ${value}`);
|
|
75
|
+
}
|
|
76
|
+
return normalized;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isInsideRoot(root, target) {
|
|
80
|
+
const relative = path.relative(root, target);
|
|
81
|
+
return (
|
|
82
|
+
relative === '' ||
|
|
83
|
+
(relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative))
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isIncluded(relativePath, include) {
|
|
88
|
+
return (include ?? []).some((entry) => {
|
|
89
|
+
const includeRoot = String(entry)
|
|
90
|
+
.replace(/\\/g, '/')
|
|
91
|
+
.replace(/^\.\//, '')
|
|
92
|
+
.replace(/\/$/, '');
|
|
93
|
+
return (
|
|
94
|
+
includeRoot === '.' ||
|
|
95
|
+
relativePath === includeRoot ||
|
|
96
|
+
relativePath.startsWith(`${includeRoot}/`)
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function observeResolvedInput(observeInput, inputPath, kind) {
|
|
102
|
+
if (typeof inputPath === 'string') observeInput?.(path.resolve(inputPath), kind);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function readPackageName(root, observeInput) {
|
|
106
|
+
const packagePath = path.join(root, 'package.json');
|
|
107
|
+
observeResolvedInput(observeInput, packagePath, 'package');
|
|
108
|
+
try {
|
|
109
|
+
const parsed = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
110
|
+
return typeof parsed?.name === 'string' && parsed.name.length > 0 ? parsed.name : undefined;
|
|
111
|
+
} catch {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function rememberCanonicalAlias(aliases, real, relative, absolute) {
|
|
117
|
+
const current = aliases.get(real);
|
|
118
|
+
if (!current || relative < current.relative) {
|
|
119
|
+
aliases.set(real, { relative, absolute: path.resolve(absolute) });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function rememberDirectoryAlias(aliases, real, relative, absolute) {
|
|
124
|
+
const current = aliases.get(real) ?? new Map();
|
|
125
|
+
if (!current.has(relative)) current.set(relative, { relative, absolute: path.resolve(absolute) });
|
|
126
|
+
aliases.set(real, current);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function potentialRealpath(root, absolute, observeInput) {
|
|
130
|
+
const suffix = [];
|
|
131
|
+
let existing = path.resolve(absolute);
|
|
132
|
+
while (true) {
|
|
133
|
+
observeResolvedInput(observeInput, existing, 'exists');
|
|
134
|
+
if (fs.existsSync(existing)) break;
|
|
135
|
+
const parent = path.dirname(existing);
|
|
136
|
+
if (parent === existing) return undefined;
|
|
137
|
+
suffix.unshift(path.basename(existing));
|
|
138
|
+
existing = parent;
|
|
139
|
+
}
|
|
140
|
+
observeResolvedInput(observeInput, existing, 'realpath');
|
|
141
|
+
const real = path.join(fs.realpathSync(existing), ...suffix);
|
|
142
|
+
if (!isInsideRoot(root, real)) {
|
|
143
|
+
throw new Error(`Refusing candidate overlay through a symlink outside project root.`);
|
|
144
|
+
}
|
|
145
|
+
return path.resolve(real);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Lexical + potential-realpath identities for resolver inputs inside one project root. */
|
|
149
|
+
export function resolvedInputIdentities(root, inputs) {
|
|
150
|
+
const lexicalRoot = path.resolve(root);
|
|
151
|
+
const realRoot = fs.realpathSync(lexicalRoot);
|
|
152
|
+
const identities = new Set();
|
|
153
|
+
for (const input of inputs) {
|
|
154
|
+
if (typeof input !== 'string' || input.length === 0) continue;
|
|
155
|
+
const absolute = path.isAbsolute(input) ? path.resolve(input) : path.resolve(lexicalRoot, input);
|
|
156
|
+
if (!isInsideRoot(lexicalRoot, absolute)) {
|
|
157
|
+
throw new Error(`Resolved-analysis input is outside project root: ${input}`);
|
|
158
|
+
}
|
|
159
|
+
identities.add(`path:${normalize(path.relative(lexicalRoot, absolute))}`);
|
|
160
|
+
const real = potentialRealpath(realRoot, absolute);
|
|
161
|
+
if (real) identities.add(`real:${normalize(path.relative(realRoot, real))}`);
|
|
162
|
+
}
|
|
163
|
+
return identities;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function canonicalOverlayPath(
|
|
167
|
+
root,
|
|
168
|
+
requested,
|
|
169
|
+
fileAliases,
|
|
170
|
+
directoryAliases,
|
|
171
|
+
config,
|
|
172
|
+
observeInput
|
|
173
|
+
) {
|
|
174
|
+
const absolute = path.join(root, ...requested.split('/'));
|
|
175
|
+
const real = potentialRealpath(root, absolute, observeInput);
|
|
176
|
+
if (!real) return requested;
|
|
177
|
+
const fileAlias = fileAliases.get(real);
|
|
178
|
+
if (fileAlias) return fileAlias.relative;
|
|
179
|
+
|
|
180
|
+
const candidates = new Set();
|
|
181
|
+
const rememberCandidate = (relative) => {
|
|
182
|
+
if (isIncluded(relative, config.include) && !isScanExcludedRelative(relative, config)) {
|
|
183
|
+
candidates.add(relative);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
rememberCandidate(requested);
|
|
187
|
+
rememberCandidate(canonicalProjectPath(normalize(path.relative(root, real))));
|
|
188
|
+
const suffix = [path.basename(real)];
|
|
189
|
+
let directory = path.dirname(real);
|
|
190
|
+
while (isInsideRoot(root, directory)) {
|
|
191
|
+
for (const alias of directoryAliases.get(directory)?.values() ?? []) {
|
|
192
|
+
rememberCandidate(canonicalProjectPath([alias.relative, ...suffix].filter(Boolean).join('/')));
|
|
193
|
+
}
|
|
194
|
+
if (directory === root) break;
|
|
195
|
+
suffix.unshift(path.basename(directory));
|
|
196
|
+
directory = path.dirname(directory);
|
|
197
|
+
}
|
|
198
|
+
return [...candidates].sort()[0] ?? requested;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function collectCandidateFiles(root, config, changes, observeInput) {
|
|
202
|
+
const files = new Map();
|
|
203
|
+
const directoryAliases = new Map();
|
|
204
|
+
const expandedAliasDirectories = new Set();
|
|
205
|
+
const discovered = (config.include ?? [])
|
|
206
|
+
.flatMap((entry) =>
|
|
207
|
+
collectGovernedFiles(root, { ...config, include: [entry] }, {
|
|
208
|
+
observeInput,
|
|
209
|
+
onDirectory(absolute, real) {
|
|
210
|
+
let relative = normalize(path.relative(root, absolute));
|
|
211
|
+
if (relative === '.') relative = '';
|
|
212
|
+
rememberDirectoryAlias(directoryAliases, real, relative, absolute);
|
|
213
|
+
},
|
|
214
|
+
})
|
|
215
|
+
)
|
|
216
|
+
.map((absolute) => {
|
|
217
|
+
observeResolvedInput(observeInput, absolute, 'realpath');
|
|
218
|
+
return {
|
|
219
|
+
absolute,
|
|
220
|
+
real: fs.realpathSync(absolute),
|
|
221
|
+
relative: canonicalProjectPath(normalize(path.relative(root, absolute))),
|
|
222
|
+
};
|
|
223
|
+
})
|
|
224
|
+
.sort((left, right) =>
|
|
225
|
+
left.relative < right.relative ? -1 : left.relative > right.relative ? 1 : 0
|
|
226
|
+
);
|
|
227
|
+
const canonicalByRealpath = new Map();
|
|
228
|
+
for (const candidate of discovered) {
|
|
229
|
+
if (!canonicalByRealpath.has(candidate.real)) canonicalByRealpath.set(candidate.real, candidate);
|
|
230
|
+
}
|
|
231
|
+
const fileAliases = new Map();
|
|
232
|
+
for (const candidate of discovered) {
|
|
233
|
+
const canonical = canonicalByRealpath.get(candidate.real);
|
|
234
|
+
rememberCanonicalAlias(
|
|
235
|
+
fileAliases,
|
|
236
|
+
candidate.real,
|
|
237
|
+
canonical.relative,
|
|
238
|
+
canonical.absolute
|
|
239
|
+
);
|
|
240
|
+
let absoluteDirectory = path.dirname(candidate.absolute);
|
|
241
|
+
let relativeDirectory = path.posix.dirname(candidate.relative);
|
|
242
|
+
if (relativeDirectory === '.') relativeDirectory = '';
|
|
243
|
+
while (isInsideRoot(root, absoluteDirectory)) {
|
|
244
|
+
if (expandedAliasDirectories.has(absoluteDirectory)) break;
|
|
245
|
+
expandedAliasDirectories.add(absoluteDirectory);
|
|
246
|
+
observeResolvedInput(observeInput, absoluteDirectory, 'realpath');
|
|
247
|
+
const realDirectory = fs.realpathSync(absoluteDirectory);
|
|
248
|
+
rememberDirectoryAlias(
|
|
249
|
+
directoryAliases,
|
|
250
|
+
realDirectory,
|
|
251
|
+
relativeDirectory,
|
|
252
|
+
absoluteDirectory
|
|
253
|
+
);
|
|
254
|
+
if (absoluteDirectory === root) break;
|
|
255
|
+
absoluteDirectory = path.dirname(absoluteDirectory);
|
|
256
|
+
relativeDirectory = path.posix.dirname(relativeDirectory);
|
|
257
|
+
if (relativeDirectory === '.') relativeDirectory = '';
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
for (const { absolute, real, relative } of canonicalByRealpath.values()) {
|
|
261
|
+
observeResolvedInput(observeInput, absolute, 'source');
|
|
262
|
+
files.set(relative, {
|
|
263
|
+
path: relative,
|
|
264
|
+
absolute: path.resolve(absolute),
|
|
265
|
+
real: path.resolve(real),
|
|
266
|
+
content: fs.readFileSync(absolute, 'utf8'),
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const changed = new Set();
|
|
271
|
+
const canonicalChanges = [];
|
|
272
|
+
for (const change of changes ?? []) {
|
|
273
|
+
const requested = canonicalProjectPath(change?.path);
|
|
274
|
+
const relative = canonicalOverlayPath(
|
|
275
|
+
root,
|
|
276
|
+
requested,
|
|
277
|
+
fileAliases,
|
|
278
|
+
directoryAliases,
|
|
279
|
+
config,
|
|
280
|
+
observeInput
|
|
281
|
+
);
|
|
282
|
+
if (changed.has(relative)) {
|
|
283
|
+
throw new Error(`Atomic candidate overlay contains duplicate path ${relative}.`);
|
|
284
|
+
}
|
|
285
|
+
changed.add(relative);
|
|
286
|
+
if (change?.delete === true && change.content === undefined) {
|
|
287
|
+
files.delete(relative);
|
|
288
|
+
canonicalChanges.push({ path: relative, requestedPath: requested, delete: true });
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (typeof change?.content !== 'string' || change.delete !== undefined) {
|
|
292
|
+
throw new Error(`Candidate overlay for ${relative} requires content or delete: true.`);
|
|
293
|
+
}
|
|
294
|
+
canonicalChanges.push({ path: relative, requestedPath: requested, content: change.content });
|
|
295
|
+
if (!isGovernableSourceFile(path.basename(relative))) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (!isIncluded(relative, config.include) || isScanExcludedRelative(relative, config)) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
files.set(relative, {
|
|
302
|
+
path: relative,
|
|
303
|
+
absolute: path.join(root, ...relative.split('/')),
|
|
304
|
+
content: change.content,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
files: [...files.values()].sort((left, right) =>
|
|
310
|
+
left.path < right.path ? -1 : left.path > right.path ? 1 : 0
|
|
311
|
+
),
|
|
312
|
+
changes: canonicalChanges,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Canonicalize a Tooling overlay without exposing filesystem identity to Kernel. */
|
|
317
|
+
export function canonicalizeCandidateChanges({ root, config, changes = [] }) {
|
|
318
|
+
const canonicalRoot = fs.realpathSync(root);
|
|
319
|
+
return collectCandidateFiles(canonicalRoot, config, changes).changes.map((change) =>
|
|
320
|
+
change.delete === true
|
|
321
|
+
? { path: change.path, delete: true }
|
|
322
|
+
: { path: change.path, content: change.content }
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function tryRealpath(value, observeInput) {
|
|
327
|
+
observeResolvedInput(observeInput, value, 'realpath');
|
|
328
|
+
try {
|
|
329
|
+
return fs.realpathSync(value);
|
|
330
|
+
} catch {
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function createOverlayModuleHost(ts, root, files, changes, observeInput) {
|
|
336
|
+
const sys = ts.sys;
|
|
337
|
+
const byAbsolute = new Map();
|
|
338
|
+
const pathByAbsolute = new Map();
|
|
339
|
+
const deleted = new Set();
|
|
340
|
+
const virtualDirectories = new Set([root]);
|
|
341
|
+
|
|
342
|
+
const rememberVirtualDirectories = (absolute) => {
|
|
343
|
+
let directory = path.dirname(absolute);
|
|
344
|
+
while (isInsideRoot(root, directory)) {
|
|
345
|
+
virtualDirectories.add(path.resolve(directory));
|
|
346
|
+
if (path.resolve(directory) === root) break;
|
|
347
|
+
directory = path.dirname(directory);
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
const remember = (absolute, file) => {
|
|
351
|
+
const key = path.resolve(absolute);
|
|
352
|
+
byAbsolute.set(key, file);
|
|
353
|
+
pathByAbsolute.set(key, file.path);
|
|
354
|
+
rememberVirtualDirectories(key);
|
|
355
|
+
};
|
|
356
|
+
for (const file of files) {
|
|
357
|
+
remember(file.absolute, file);
|
|
358
|
+
const real = file.real ?? tryRealpath(file.absolute, observeInput);
|
|
359
|
+
if (real && isInsideRoot(root, real)) remember(real, file);
|
|
360
|
+
}
|
|
361
|
+
const candidateByPath = new Map(files.map((file) => [file.path, file]));
|
|
362
|
+
for (const change of changes ?? []) {
|
|
363
|
+
const aliases = new Set([change.path, change.requestedPath].filter(Boolean));
|
|
364
|
+
const candidate =
|
|
365
|
+
candidateByPath.get(change.path) ??
|
|
366
|
+
(typeof change.content === 'string'
|
|
367
|
+
? {
|
|
368
|
+
path: change.path,
|
|
369
|
+
absolute: path.join(root, ...change.path.split('/')),
|
|
370
|
+
content: change.content,
|
|
371
|
+
}
|
|
372
|
+
: undefined);
|
|
373
|
+
for (const alias of aliases) {
|
|
374
|
+
const absolute = path.join(root, ...canonicalProjectPath(alias).split('/'));
|
|
375
|
+
if (change?.delete === true) {
|
|
376
|
+
deleted.add(path.resolve(absolute));
|
|
377
|
+
const real = tryRealpath(absolute, observeInput);
|
|
378
|
+
if (real && isInsideRoot(root, real)) deleted.add(path.resolve(real));
|
|
379
|
+
} else if (candidate) {
|
|
380
|
+
remember(absolute, candidate);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const fileExists = (fileName) => {
|
|
386
|
+
const absolute = path.resolve(fileName);
|
|
387
|
+
observeResolvedInput(observeInput, absolute, 'module-file');
|
|
388
|
+
if (deleted.has(absolute)) return false;
|
|
389
|
+
if (byAbsolute.has(absolute)) return true;
|
|
390
|
+
return sys?.fileExists ? sys.fileExists(fileName) : fs.existsSync(fileName);
|
|
391
|
+
};
|
|
392
|
+
const readFile = (fileName) => {
|
|
393
|
+
const absolute = path.resolve(fileName);
|
|
394
|
+
observeResolvedInput(observeInput, absolute, 'module-read');
|
|
395
|
+
if (deleted.has(absolute)) return undefined;
|
|
396
|
+
const candidate = byAbsolute.get(absolute);
|
|
397
|
+
if (candidate) return candidate.content;
|
|
398
|
+
if (sys?.readFile) return sys.readFile(fileName);
|
|
399
|
+
try {
|
|
400
|
+
return fs.readFileSync(fileName, 'utf8');
|
|
401
|
+
} catch {
|
|
402
|
+
return undefined;
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
const directoryExists = (directory) => {
|
|
406
|
+
const absolute = path.resolve(directory);
|
|
407
|
+
observeResolvedInput(observeInput, absolute, 'module-directory');
|
|
408
|
+
if (virtualDirectories.has(absolute)) return true;
|
|
409
|
+
if (sys?.directoryExists) return sys.directoryExists(directory);
|
|
410
|
+
try {
|
|
411
|
+
return fs.statSync(directory).isDirectory();
|
|
412
|
+
} catch {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
const getDirectories = (directory) => {
|
|
417
|
+
const absolute = path.resolve(directory);
|
|
418
|
+
observeResolvedInput(observeInput, absolute, 'module-directory');
|
|
419
|
+
const names = new Set();
|
|
420
|
+
if (sys?.getDirectories) {
|
|
421
|
+
for (const item of sys.getDirectories(directory)) names.add(path.basename(item));
|
|
422
|
+
} else {
|
|
423
|
+
try {
|
|
424
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
425
|
+
if (entry.isDirectory()) names.add(entry.name);
|
|
426
|
+
}
|
|
427
|
+
} catch {
|
|
428
|
+
// A purely virtual directory has no disk entries.
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
for (const virtual of virtualDirectories) {
|
|
432
|
+
if (path.dirname(virtual) === absolute) names.add(path.basename(virtual));
|
|
433
|
+
}
|
|
434
|
+
return [...names].sort();
|
|
435
|
+
};
|
|
436
|
+
const realpath = (fileName) => {
|
|
437
|
+
const absolute = path.resolve(fileName);
|
|
438
|
+
observeResolvedInput(observeInput, absolute, 'module-realpath');
|
|
439
|
+
if (deleted.has(absolute)) return absolute;
|
|
440
|
+
const candidate = byAbsolute.get(absolute);
|
|
441
|
+
if (candidate) {
|
|
442
|
+
const real = candidate.real ?? tryRealpath(candidate.absolute, observeInput);
|
|
443
|
+
return real && isInsideRoot(root, real) ? real : candidate.absolute;
|
|
444
|
+
}
|
|
445
|
+
if (sys?.realpath) return sys.realpath(fileName);
|
|
446
|
+
return tryRealpath(fileName) ?? absolute;
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
fileExists,
|
|
451
|
+
readFile,
|
|
452
|
+
directoryExists,
|
|
453
|
+
getCurrentDirectory: () => root,
|
|
454
|
+
getDirectories,
|
|
455
|
+
realpath,
|
|
456
|
+
useCaseSensitiveFileNames: sys?.useCaseSensitiveFileNames ?? true,
|
|
457
|
+
candidatePath(fileName) {
|
|
458
|
+
const absolute = path.resolve(fileName);
|
|
459
|
+
const direct = pathByAbsolute.get(absolute);
|
|
460
|
+
if (direct) return direct;
|
|
461
|
+
const real = realpath(absolute);
|
|
462
|
+
const candidate = pathByAbsolute.get(path.resolve(real));
|
|
463
|
+
if (candidate) return candidate;
|
|
464
|
+
if (!isInsideRoot(root, real)) return undefined;
|
|
465
|
+
const relative = normalize(path.relative(root, real));
|
|
466
|
+
if (
|
|
467
|
+
relative.split('/').includes('node_modules') ||
|
|
468
|
+
!/\.[cm]?[tj]sx?$/.test(relative) ||
|
|
469
|
+
relative.endsWith('.d.ts')
|
|
470
|
+
) {
|
|
471
|
+
return undefined;
|
|
472
|
+
}
|
|
473
|
+
return canonicalProjectPath(relative);
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function nearestTsconfig(root, fileName, cache, observeInput) {
|
|
479
|
+
const initial = path.dirname(fileName);
|
|
480
|
+
if (cache.has(initial)) return cache.get(initial);
|
|
481
|
+
let directory = initial;
|
|
482
|
+
while (isInsideRoot(root, directory)) {
|
|
483
|
+
const candidate = path.join(directory, 'tsconfig.json');
|
|
484
|
+
observeResolvedInput(observeInput, candidate, 'exists');
|
|
485
|
+
if (fs.existsSync(candidate)) {
|
|
486
|
+
const resolved = path.resolve(candidate);
|
|
487
|
+
cache.set(initial, resolved);
|
|
488
|
+
return resolved;
|
|
489
|
+
}
|
|
490
|
+
if (directory === root) break;
|
|
491
|
+
directory = path.dirname(directory);
|
|
492
|
+
}
|
|
493
|
+
cache.set(initial, undefined);
|
|
494
|
+
return undefined;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function configLabel(root, configPath, externalAnchor) {
|
|
498
|
+
if (isInsideRoot(root, configPath)) return normalize(path.relative(root, configPath));
|
|
499
|
+
if (externalAnchor && isInsideRoot(externalAnchor, configPath)) {
|
|
500
|
+
return `<external-tsconfig>/${normalize(path.relative(externalAnchor, configPath))}`;
|
|
501
|
+
}
|
|
502
|
+
return `<external-config>/${path.basename(configPath)}`;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function configReasonFile(root, configPath) {
|
|
506
|
+
return isInsideRoot(root, configPath)
|
|
507
|
+
? canonicalProjectPath(normalize(path.relative(root, configPath)))
|
|
508
|
+
: undefined;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function portableCompilerValue(root, value, externalAnchor) {
|
|
512
|
+
if (typeof value === 'string') {
|
|
513
|
+
if (!path.isAbsolute(value)) return value.replace(/\\/g, '/');
|
|
514
|
+
const absolute = path.resolve(value);
|
|
515
|
+
if (isInsideRoot(root, absolute)) {
|
|
516
|
+
return `<root>/${normalize(path.relative(root, absolute))}`.replace(/\/$/, '');
|
|
517
|
+
}
|
|
518
|
+
if (externalAnchor && isInsideRoot(externalAnchor, absolute)) {
|
|
519
|
+
return `<external-tsconfig>/${normalize(path.relative(externalAnchor, absolute))}`.replace(
|
|
520
|
+
/\/$/,
|
|
521
|
+
''
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
return `<external>/${path.basename(absolute)}`;
|
|
525
|
+
}
|
|
526
|
+
if (Array.isArray(value)) {
|
|
527
|
+
return value.map((item) => portableCompilerValue(root, item, externalAnchor));
|
|
528
|
+
}
|
|
529
|
+
if (!value || typeof value !== 'object') return value;
|
|
530
|
+
const portable = {};
|
|
531
|
+
for (const [key, item] of Object.entries(value)) {
|
|
532
|
+
if (item !== undefined && typeof item !== 'function') {
|
|
533
|
+
portable[key] = portableCompilerValue(root, item, externalAnchor);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return portable;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function compilerContext(ts, root, tsconfig, candidateFiles, observeInput) {
|
|
540
|
+
const explicitPath = tsconfig
|
|
541
|
+
? path.isAbsolute(tsconfig)
|
|
542
|
+
? path.resolve(tsconfig)
|
|
543
|
+
: path.resolve(root, tsconfig)
|
|
544
|
+
: undefined;
|
|
545
|
+
const externalAnchor =
|
|
546
|
+
explicitPath && !isInsideRoot(root, explicitPath)
|
|
547
|
+
? path.dirname(explicitPath)
|
|
548
|
+
: undefined;
|
|
549
|
+
const configByFile = new Map();
|
|
550
|
+
const nearestConfigByDirectory = new Map();
|
|
551
|
+
if (explicitPath) {
|
|
552
|
+
for (const file of candidateFiles) configByFile.set(path.resolve(file.absolute), explicitPath);
|
|
553
|
+
} else if (!explicitPath) {
|
|
554
|
+
for (const file of candidateFiles) {
|
|
555
|
+
const nearest = nearestTsconfig(
|
|
556
|
+
root,
|
|
557
|
+
file.absolute,
|
|
558
|
+
nearestConfigByDirectory,
|
|
559
|
+
observeInput
|
|
560
|
+
);
|
|
561
|
+
if (nearest) configByFile.set(path.resolve(file.absolute), nearest);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
const configPaths = [...new Set(configByFile.values())].sort((left, right) => {
|
|
565
|
+
const leftLabel = configLabel(root, left, externalAnchor);
|
|
566
|
+
const rightLabel = configLabel(root, right, externalAnchor);
|
|
567
|
+
return leftLabel < rightLabel ? -1 : leftLabel > rightLabel ? 1 : 0;
|
|
568
|
+
});
|
|
569
|
+
const optionsByPath = new Map();
|
|
570
|
+
const configInputsByPath = new Map();
|
|
571
|
+
const reasons = [];
|
|
572
|
+
for (const configPath of configPaths) {
|
|
573
|
+
const configContents = new Map();
|
|
574
|
+
const readConfig = (fileName) => {
|
|
575
|
+
observeResolvedInput(observeInput, fileName, 'tsconfig');
|
|
576
|
+
try {
|
|
577
|
+
const content = fs.readFileSync(fileName, 'utf8');
|
|
578
|
+
if (/\.jsonc?$/i.test(fileName)) configContents.set(path.resolve(fileName), content);
|
|
579
|
+
return content;
|
|
580
|
+
} catch {
|
|
581
|
+
return undefined;
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
const read = ts.readConfigFile(configPath, readConfig);
|
|
585
|
+
const label = configLabel(root, configPath, externalAnchor);
|
|
586
|
+
const reasonFile = configReasonFile(root, configPath);
|
|
587
|
+
if (read.error) {
|
|
588
|
+
reasons.push({
|
|
589
|
+
code: 'TSCONFIG_PARSE_FAILURE',
|
|
590
|
+
...(reasonFile ? { file: reasonFile } : {}),
|
|
591
|
+
message: `TypeScript could not read ${label}.`,
|
|
592
|
+
});
|
|
593
|
+
optionsByPath.set(configPath, {});
|
|
594
|
+
configInputsByPath.set(configPath, configContents);
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
const parsed = ts.parseJsonConfigFileContent(
|
|
598
|
+
read.config,
|
|
599
|
+
{
|
|
600
|
+
useCaseSensitiveFileNames: ts.sys?.useCaseSensitiveFileNames ?? true,
|
|
601
|
+
readDirectory(...args) {
|
|
602
|
+
observeResolvedInput(observeInput, args[0], 'tsconfig-directory');
|
|
603
|
+
return ts.sys?.readDirectory ? ts.sys.readDirectory(...args) : [];
|
|
604
|
+
},
|
|
605
|
+
fileExists(fileName) {
|
|
606
|
+
observeResolvedInput(observeInput, fileName, 'tsconfig-exists');
|
|
607
|
+
return ts.sys?.fileExists ? ts.sys.fileExists(fileName) : fs.existsSync(fileName);
|
|
608
|
+
},
|
|
609
|
+
readFile: readConfig,
|
|
610
|
+
},
|
|
611
|
+
path.dirname(configPath),
|
|
612
|
+
undefined,
|
|
613
|
+
configPath
|
|
614
|
+
);
|
|
615
|
+
const optionErrors = (parsed.errors ?? []).filter(
|
|
616
|
+
(diagnostic) => diagnostic?.code !== 18002 && diagnostic?.code !== 18003
|
|
617
|
+
);
|
|
618
|
+
if (optionErrors.length > 0) {
|
|
619
|
+
reasons.push({
|
|
620
|
+
code: 'TSCONFIG_PARSE_FAILURE',
|
|
621
|
+
...(reasonFile ? { file: reasonFile } : {}),
|
|
622
|
+
message: `${label} has ${optionErrors.length} TypeScript config diagnostic(s).`,
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
optionsByPath.set(configPath, parsed.options ?? {});
|
|
626
|
+
configInputsByPath.set(configPath, configContents);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const optionsFor = (fileName) => {
|
|
630
|
+
const configPath = configByFile.get(path.resolve(fileName));
|
|
631
|
+
return configPath ? optionsByPath.get(configPath) ?? {} : {};
|
|
632
|
+
};
|
|
633
|
+
const configs = configPaths.map((configPath) => ({
|
|
634
|
+
path: configLabel(root, configPath, externalAnchor),
|
|
635
|
+
options: portableCompilerValue(root, optionsByPath.get(configPath) ?? {}, externalAnchor),
|
|
636
|
+
}));
|
|
637
|
+
const configClosure = configPaths.map((configPath) => ({
|
|
638
|
+
top: configLabel(root, configPath, externalAnchor),
|
|
639
|
+
inputs: [...(configInputsByPath.get(configPath) ?? [])]
|
|
640
|
+
.map(([inputPath, content]) => ({
|
|
641
|
+
path: configLabel(root, inputPath, externalAnchor),
|
|
642
|
+
contentHash: deterministicHash(content),
|
|
643
|
+
}))
|
|
644
|
+
.sort((left, right) =>
|
|
645
|
+
left.path < right.path ? -1 : left.path > right.path ? 1 : 0
|
|
646
|
+
),
|
|
647
|
+
}));
|
|
648
|
+
const configInputPaths = [
|
|
649
|
+
...new Set(
|
|
650
|
+
[...configInputsByPath.values()].flatMap((inputs) => [...inputs.keys()])
|
|
651
|
+
),
|
|
652
|
+
]
|
|
653
|
+
.filter((inputPath) => isInsideRoot(root, inputPath))
|
|
654
|
+
.map((inputPath) => canonicalProjectPath(normalize(path.relative(root, inputPath))))
|
|
655
|
+
.sort();
|
|
656
|
+
return {
|
|
657
|
+
optionsFor,
|
|
658
|
+
reasons,
|
|
659
|
+
configInputPaths,
|
|
660
|
+
tsconfigHash: deterministicHash(stableSerialize(configClosure)),
|
|
661
|
+
compilerOptionsHash: deterministicHash(
|
|
662
|
+
stableSerialize(configs.map(({ path: configPath, options }) => ({ path: configPath, options })))
|
|
663
|
+
),
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/** Project-relative TypeScript config closure read by the shipped resolver. */
|
|
668
|
+
export function resolvedCompilerInputPaths({
|
|
669
|
+
root,
|
|
670
|
+
config,
|
|
671
|
+
ts,
|
|
672
|
+
tsconfig,
|
|
673
|
+
changes = [],
|
|
674
|
+
observeInput,
|
|
675
|
+
}) {
|
|
676
|
+
if (!ts?.readConfigFile || !ts?.parseJsonConfigFileContent) return [];
|
|
677
|
+
observeResolvedInput(observeInput, root, 'realpath');
|
|
678
|
+
const canonicalRoot = fs.realpathSync(root);
|
|
679
|
+
const candidate = collectCandidateFiles(canonicalRoot, config, changes, observeInput);
|
|
680
|
+
return compilerContext(ts, canonicalRoot, tsconfig, candidate.files, observeInput)
|
|
681
|
+
.configInputPaths;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function resolveRelativeFallback(specifier, containingFile, host) {
|
|
685
|
+
const base = path.resolve(path.dirname(containingFile), specifier);
|
|
686
|
+
const candidates = [
|
|
687
|
+
base,
|
|
688
|
+
`${base}.ts`,
|
|
689
|
+
`${base}.tsx`,
|
|
690
|
+
`${base}.mts`,
|
|
691
|
+
`${base}.cts`,
|
|
692
|
+
`${base}.js`,
|
|
693
|
+
`${base}.jsx`,
|
|
694
|
+
`${base}.mjs`,
|
|
695
|
+
`${base}.cjs`,
|
|
696
|
+
path.join(base, 'index.ts'),
|
|
697
|
+
path.join(base, 'index.tsx'),
|
|
698
|
+
path.join(base, 'index.mts'),
|
|
699
|
+
path.join(base, 'index.cts'),
|
|
700
|
+
];
|
|
701
|
+
return candidates.find((candidate) => host.fileExists(candidate));
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function resolveDependency(ts, dependency, containingFile, options, host) {
|
|
705
|
+
if (!dependency.specifier) return { resolution: 'dynamic' };
|
|
706
|
+
let resolvedFile;
|
|
707
|
+
let resolverFailed = false;
|
|
708
|
+
try {
|
|
709
|
+
resolvedFile = ts.resolveModuleName(
|
|
710
|
+
dependency.specifier,
|
|
711
|
+
containingFile,
|
|
712
|
+
options,
|
|
713
|
+
host
|
|
714
|
+
).resolvedModule?.resolvedFileName;
|
|
715
|
+
} catch {
|
|
716
|
+
resolverFailed = true;
|
|
717
|
+
resolvedFile = undefined;
|
|
718
|
+
}
|
|
719
|
+
if (!resolvedFile && dependency.specifier.startsWith('.')) {
|
|
720
|
+
resolvedFile = resolveRelativeFallback(dependency.specifier, containingFile, host);
|
|
721
|
+
}
|
|
722
|
+
if (!resolvedFile) return { resolution: 'unresolved', resolverFailed };
|
|
723
|
+
const target = host.candidatePath(resolvedFile);
|
|
724
|
+
const resolution = target
|
|
725
|
+
? { resolution: 'resolved-project', target }
|
|
726
|
+
: { resolution: 'resolved-external' };
|
|
727
|
+
return { ...resolution, resolverFailed };
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function declaredIntent(value, config) {
|
|
731
|
+
if (looksLikeArkIntent(value)) return true;
|
|
732
|
+
return config.layers.some((layer) =>
|
|
733
|
+
(layer.intentPrefixes ?? []).some((prefix) => {
|
|
734
|
+
const normalized = prefix.endsWith('.') ? prefix : `${prefix}.`;
|
|
735
|
+
return value.startsWith(normalized) && value.length > normalized.length;
|
|
736
|
+
})
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function mayContainForbiddenCapability(ts, sourceFile, forbiddenGlobals) {
|
|
741
|
+
// Every symbol-aware match originates in an identifier or static string path segment.
|
|
742
|
+
// Inspect decoded AST text so escaped identifiers still take the full checker path.
|
|
743
|
+
const segments = new Set(forbiddenGlobals.flatMap((entry) => entry.split('.')));
|
|
744
|
+
let found = false;
|
|
745
|
+
const visit = (node) => {
|
|
746
|
+
if (!found &&
|
|
747
|
+
(ts.isIdentifier(node) || ts.isStringLiteralLike(node)) &&
|
|
748
|
+
segments.has(node.text)
|
|
749
|
+
) {
|
|
750
|
+
found = true;
|
|
751
|
+
}
|
|
752
|
+
if (!found) ts.forEachChild(node, visit);
|
|
753
|
+
};
|
|
754
|
+
visit(sourceFile);
|
|
755
|
+
return found;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function collectPolicyFacts(ts, sourceFile, relativePath, config) {
|
|
759
|
+
const publishCalls = [];
|
|
760
|
+
const intentReferences = [];
|
|
761
|
+
const visit = (node) => {
|
|
762
|
+
if (ts.isCallExpression(node) && isPublishCall(ts, node)) {
|
|
763
|
+
const firstArg = node.arguments[0];
|
|
764
|
+
publishCalls.push({
|
|
765
|
+
file: relativePath,
|
|
766
|
+
line: lineOf(sourceFile, node.getStart(sourceFile)),
|
|
767
|
+
...(stringLiteralText(ts, firstArg)
|
|
768
|
+
? { rawIntentName: stringLiteralText(ts, firstArg) }
|
|
769
|
+
: {}),
|
|
770
|
+
objectHasIntent: objectHasProperty(ts, firstArg, 'intent'),
|
|
771
|
+
arkPublishCandidate: isArkPublishCandidate(ts, node),
|
|
772
|
+
hasSource: publishHasSource(ts, node),
|
|
773
|
+
...(publishSourceLiteral(ts, node)
|
|
774
|
+
? { sourceIntent: publishSourceLiteral(ts, node) }
|
|
775
|
+
: {}),
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
if (ts.isStringLiteralLike(node) && declaredIntent(node.text, config)) {
|
|
779
|
+
intentReferences.push({
|
|
780
|
+
file: relativePath,
|
|
781
|
+
line: lineOf(sourceFile, node.getStart(sourceFile)),
|
|
782
|
+
intent: node.text,
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
ts.forEachChild(node, visit);
|
|
786
|
+
};
|
|
787
|
+
visit(sourceFile);
|
|
788
|
+
return { publishCalls, intentReferences };
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function syntaxPropertyName(ts, node) {
|
|
792
|
+
if (ts.isIdentifier(node) || ts.isStringLiteralLike(node)) return node.text;
|
|
793
|
+
return undefined;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function objectLiteralHasProperty(ts, object, name) {
|
|
797
|
+
return Boolean(
|
|
798
|
+
object?.properties?.some((property) => {
|
|
799
|
+
if (ts.isShorthandPropertyAssignment(property)) return property.name.text === name;
|
|
800
|
+
return property.name ? syntaxPropertyName(ts, property.name) === name : false;
|
|
801
|
+
})
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function tsSuppressionPositions(sourceFile, source) {
|
|
806
|
+
const positions = new Set();
|
|
807
|
+
for (const directive of sourceFile.commentDirectives ?? []) {
|
|
808
|
+
const start = directive.range?.pos;
|
|
809
|
+
const end = directive.range?.end;
|
|
810
|
+
if (Number.isInteger(start) && Number.isInteger(end)) {
|
|
811
|
+
const text = source.slice(start, end);
|
|
812
|
+
if (/\@ts-ignore\b/.test(text)) positions.add(start);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
const noCheck = sourceFile.pragmas?.get?.('ts-nocheck');
|
|
816
|
+
for (const entry of Array.isArray(noCheck) ? noCheck : noCheck ? [noCheck] : []) {
|
|
817
|
+
if (Number.isInteger(entry.range?.pos)) positions.add(entry.range.pos);
|
|
818
|
+
}
|
|
819
|
+
return [...positions];
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function collectSafetyUses(ts, sourceFile, relativePath, source, dependencies) {
|
|
823
|
+
const facts = tsSuppressionPositions(sourceFile, source).map((position) => ({
|
|
824
|
+
file: relativePath,
|
|
825
|
+
line: lineOf(sourceFile, position),
|
|
826
|
+
kind: 'ts-suppression',
|
|
827
|
+
}));
|
|
828
|
+
for (const dependency of dependencies) {
|
|
829
|
+
if (!dependency.unresolved) continue;
|
|
830
|
+
facts.push({
|
|
831
|
+
file: relativePath,
|
|
832
|
+
line: dependency.line,
|
|
833
|
+
kind: dependency.kind === 'require' ? 'dynamic-require' : 'dynamic-import',
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const importedFactories = new Map();
|
|
838
|
+
const arkNamespaces = new Set();
|
|
839
|
+
for (const statement of sourceFile.statements) {
|
|
840
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteralLike(statement.moduleSpecifier)) {
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
if (!/^arkgate(?:\/runtime)?$/.test(statement.moduleSpecifier.text)) continue;
|
|
844
|
+
const bindings = statement.importClause?.namedBindings;
|
|
845
|
+
if (bindings && ts.isNamespaceImport(bindings)) arkNamespaces.add(bindings.name.text);
|
|
846
|
+
if (!bindings || !ts.isNamedImports(bindings)) continue;
|
|
847
|
+
for (const element of bindings.elements) {
|
|
848
|
+
const imported = element.propertyName?.text ?? element.name.text;
|
|
849
|
+
const requirements = IN_MEMORY_DEFAULT_FACTORIES.get(imported);
|
|
850
|
+
if (requirements) importedFactories.set(element.name.text, { imported, requirements });
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const visit = (node) => {
|
|
855
|
+
if (
|
|
856
|
+
(ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) &&
|
|
857
|
+
node.type?.kind === ts.SyntaxKind.AnyKeyword
|
|
858
|
+
) {
|
|
859
|
+
facts.push({
|
|
860
|
+
file: relativePath,
|
|
861
|
+
line: lineOf(sourceFile, node.getStart(sourceFile)),
|
|
862
|
+
kind: 'any-cast',
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (ts.isImportDeclaration(node)) {
|
|
867
|
+
const specifier = node.moduleSpecifier;
|
|
868
|
+
const fromArk =
|
|
869
|
+
ts.isStringLiteralLike(specifier) && /^arkgate(?:\/runtime)?$/.test(specifier.text);
|
|
870
|
+
if (fromArk) {
|
|
871
|
+
const elements =
|
|
872
|
+
node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings)
|
|
873
|
+
? node.importClause.namedBindings.elements
|
|
874
|
+
: [];
|
|
875
|
+
for (const element of elements) {
|
|
876
|
+
const imported = element.propertyName?.text ?? element.name.text;
|
|
877
|
+
if (IN_MEMORY_STORES.has(imported)) {
|
|
878
|
+
facts.push({
|
|
879
|
+
file: relativePath,
|
|
880
|
+
line: lineOf(sourceFile, element.getStart(sourceFile)),
|
|
881
|
+
kind: 'in-memory-store',
|
|
882
|
+
symbol: imported,
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if (ts.isCallExpression(node)) {
|
|
890
|
+
let factory;
|
|
891
|
+
if (ts.isIdentifier(node.expression)) {
|
|
892
|
+
factory = importedFactories.get(node.expression.text);
|
|
893
|
+
} else if (
|
|
894
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
895
|
+
ts.isIdentifier(node.expression.expression) &&
|
|
896
|
+
arkNamespaces.has(node.expression.expression.text)
|
|
897
|
+
) {
|
|
898
|
+
const imported = node.expression.name.text;
|
|
899
|
+
const requirements = IN_MEMORY_DEFAULT_FACTORIES.get(imported);
|
|
900
|
+
if (requirements) factory = { imported, requirements };
|
|
901
|
+
}
|
|
902
|
+
if (factory) {
|
|
903
|
+
const options = node.arguments[0];
|
|
904
|
+
const definitelyDefaults =
|
|
905
|
+
!options ||
|
|
906
|
+
(ts.isIdentifier(options) && options.text === 'undefined') ||
|
|
907
|
+
(ts.isObjectLiteralExpression(options) &&
|
|
908
|
+
factory.requirements.some((name) => !objectLiteralHasProperty(ts, options, name)));
|
|
909
|
+
if (definitelyDefaults) {
|
|
910
|
+
facts.push({
|
|
911
|
+
file: relativePath,
|
|
912
|
+
line: lineOf(sourceFile, node.getStart(sourceFile)),
|
|
913
|
+
kind: 'in-memory-store',
|
|
914
|
+
symbol: `${factory.imported} defaults`,
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
ts.forEachChild(node, visit);
|
|
920
|
+
};
|
|
921
|
+
visit(sourceFile);
|
|
922
|
+
return facts;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function unavailableFacts(config, ts, reason) {
|
|
926
|
+
return createTrustedResolvedCandidateFacts({
|
|
927
|
+
schemaVersion: '1.0',
|
|
928
|
+
completeness: 'unavailable',
|
|
929
|
+
completenessReasons: [{ code: 'RESOLVER_UNAVAILABLE', message: reason }],
|
|
930
|
+
resolverIdentity: RESOLVED_FACTS_RESOLVER_IDENTITY,
|
|
931
|
+
compilerIdentity: `typescript@${ts?.version ?? 'unavailable'}`,
|
|
932
|
+
compilerOptionsHash: deterministicHash('unavailable'),
|
|
933
|
+
tsconfigHash: deterministicHash('unavailable'),
|
|
934
|
+
evidenceRequirementsHash: resolvedFactsEvidenceRequirementsHash(config),
|
|
935
|
+
files: [],
|
|
936
|
+
dependencies: [],
|
|
937
|
+
capabilityUses: [],
|
|
938
|
+
ambientUses: [],
|
|
939
|
+
publishCalls: [],
|
|
940
|
+
intentReferences: [],
|
|
941
|
+
safetyUses: [],
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/** Resolve one complete candidate tree (base or virtual overlay) into versioned neutral facts. */
|
|
946
|
+
export function resolveCandidateFacts({
|
|
947
|
+
root,
|
|
948
|
+
config,
|
|
949
|
+
ts,
|
|
950
|
+
tsconfig,
|
|
951
|
+
changes = [],
|
|
952
|
+
observeInput,
|
|
953
|
+
}) {
|
|
954
|
+
if (!ts?.createSourceFile || !ts?.resolveModuleName) {
|
|
955
|
+
return unavailableFacts(config, ts, 'No API-compatible TypeScript resolver is available.');
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
let canonicalRoot;
|
|
959
|
+
let candidateFiles;
|
|
960
|
+
let canonicalChanges;
|
|
961
|
+
let compiler;
|
|
962
|
+
try {
|
|
963
|
+
observeResolvedInput(observeInput, root, 'realpath');
|
|
964
|
+
canonicalRoot = fs.realpathSync(root);
|
|
965
|
+
const candidate = collectCandidateFiles(canonicalRoot, config, changes, observeInput);
|
|
966
|
+
candidateFiles = candidate.files;
|
|
967
|
+
canonicalChanges = candidate.changes;
|
|
968
|
+
compiler = compilerContext(ts, canonicalRoot, tsconfig, candidateFiles, observeInput);
|
|
969
|
+
} catch (error) {
|
|
970
|
+
return unavailableFacts(
|
|
971
|
+
config,
|
|
972
|
+
ts,
|
|
973
|
+
error instanceof Error ? error.message : String(error)
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const host = createOverlayModuleHost(
|
|
978
|
+
ts,
|
|
979
|
+
canonicalRoot,
|
|
980
|
+
candidateFiles,
|
|
981
|
+
canonicalChanges,
|
|
982
|
+
observeInput
|
|
983
|
+
);
|
|
984
|
+
const forbiddenGlobals = [
|
|
985
|
+
...new Set([
|
|
986
|
+
...AMBIENT_CAPABILITY_ENTRIES,
|
|
987
|
+
...config.layers.flatMap((layer) => layer.forbiddenGlobals ?? []),
|
|
988
|
+
]),
|
|
989
|
+
];
|
|
990
|
+
const completenessReasons = [...compiler.reasons];
|
|
991
|
+
const parsed = new Map();
|
|
992
|
+
const files = [];
|
|
993
|
+
const capabilityUses = [];
|
|
994
|
+
const ambientUses = [];
|
|
995
|
+
const publishCalls = [];
|
|
996
|
+
const intentReferences = [];
|
|
997
|
+
const safetyUses = [];
|
|
998
|
+
|
|
999
|
+
for (const candidate of candidateFiles) {
|
|
1000
|
+
const sourceFile = ts.createSourceFile(
|
|
1001
|
+
candidate.absolute,
|
|
1002
|
+
candidate.content,
|
|
1003
|
+
ts.ScriptTarget.Latest,
|
|
1004
|
+
true,
|
|
1005
|
+
ts.getScriptKindFromFileName?.(candidate.absolute)
|
|
1006
|
+
);
|
|
1007
|
+
const parseDiagnosticCount = sourceFile.parseDiagnostics?.length ?? 0;
|
|
1008
|
+
const exportsOnlyTypes = sourceFileExportsOnlyTypes(ts, sourceFile);
|
|
1009
|
+
const typeNames = typeOnlyExportNames(ts, sourceFile);
|
|
1010
|
+
const hasTopLevelSideEffects = sourceFileHasTopLevelSideEffects(ts, sourceFile);
|
|
1011
|
+
const semanticDependencies = extractSemanticDependencies(ts, sourceFile);
|
|
1012
|
+
const resolvedDependencies = semanticDependencies.map(({ node, ...dependency }) => ({
|
|
1013
|
+
...dependency,
|
|
1014
|
+
namedBindings: namedModuleBindings(ts, node),
|
|
1015
|
+
}));
|
|
1016
|
+
let portProofEligible = false;
|
|
1017
|
+
if (/\bimport\s*\{[^}]+\}\s*from\s*['"]\.\.?\//.test(candidate.content)) {
|
|
1018
|
+
try {
|
|
1019
|
+
portProofEligible = Boolean(
|
|
1020
|
+
provePortProofInject(ts, candidate.content, { filePath: candidate.absolute, sourceFile })
|
|
1021
|
+
.eligible
|
|
1022
|
+
);
|
|
1023
|
+
} catch {
|
|
1024
|
+
portProofEligible = false;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
parsed.set(candidate.path, {
|
|
1028
|
+
candidate,
|
|
1029
|
+
exportsOnlyTypes,
|
|
1030
|
+
typeOnlyExportNames: typeNames,
|
|
1031
|
+
hasTopLevelSideEffects,
|
|
1032
|
+
portProofEligible,
|
|
1033
|
+
dependencies: resolvedDependencies,
|
|
1034
|
+
});
|
|
1035
|
+
files.push({
|
|
1036
|
+
path: candidate.path,
|
|
1037
|
+
contentHash: deterministicHash(candidate.content),
|
|
1038
|
+
parseStatus: parseDiagnosticCount === 0 ? 'parsed' : 'invalid',
|
|
1039
|
+
parseDiagnosticCount,
|
|
1040
|
+
exportsOnlyTypes,
|
|
1041
|
+
typeOnlyExportNames: typeNames,
|
|
1042
|
+
hasTopLevelSideEffects,
|
|
1043
|
+
});
|
|
1044
|
+
if (parseDiagnosticCount > 0) {
|
|
1045
|
+
completenessReasons.push({
|
|
1046
|
+
code: 'PARSE_FAILURE',
|
|
1047
|
+
file: candidate.path,
|
|
1048
|
+
message: `${candidate.path} has ${parseDiagnosticCount} TypeScript parse diagnostic(s).`,
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
const forbiddenUses = mayContainForbiddenCapability(ts, sourceFile, forbiddenGlobals)
|
|
1052
|
+
? collectForbiddenCapabilityUses(ts, sourceFile, forbiddenGlobals)
|
|
1053
|
+
: [];
|
|
1054
|
+
capabilityUses.push(
|
|
1055
|
+
...collectCapabilityUses(ts, sourceFile, {
|
|
1056
|
+
dependencies: semanticDependencies,
|
|
1057
|
+
ambientUses: forbiddenUses,
|
|
1058
|
+
}).map((use) => ({
|
|
1059
|
+
file: candidate.path,
|
|
1060
|
+
line: use.line,
|
|
1061
|
+
symbol: use.symbol,
|
|
1062
|
+
capability: use.capability,
|
|
1063
|
+
source: use.source,
|
|
1064
|
+
}))
|
|
1065
|
+
);
|
|
1066
|
+
ambientUses.push(
|
|
1067
|
+
...forbiddenUses.map((use) => ({
|
|
1068
|
+
file: candidate.path,
|
|
1069
|
+
line: use.line,
|
|
1070
|
+
symbol: use.name,
|
|
1071
|
+
}))
|
|
1072
|
+
);
|
|
1073
|
+
const policy = collectPolicyFacts(ts, sourceFile, candidate.path, config);
|
|
1074
|
+
publishCalls.push(...policy.publishCalls);
|
|
1075
|
+
intentReferences.push(...policy.intentReferences);
|
|
1076
|
+
safetyUses.push(
|
|
1077
|
+
...collectSafetyUses(ts, sourceFile, candidate.path, candidate.content, semanticDependencies)
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
const dependencies = [];
|
|
1082
|
+
for (const source of parsed.values()) {
|
|
1083
|
+
for (const dependency of source.dependencies) {
|
|
1084
|
+
const resolved = resolveDependency(
|
|
1085
|
+
ts,
|
|
1086
|
+
dependency,
|
|
1087
|
+
source.candidate.absolute,
|
|
1088
|
+
compiler.optionsFor(source.candidate.absolute),
|
|
1089
|
+
host
|
|
1090
|
+
);
|
|
1091
|
+
if (resolved.resolverFailed) {
|
|
1092
|
+
completenessReasons.push({
|
|
1093
|
+
code: 'MODULE_RESOLUTION_FAILURE',
|
|
1094
|
+
file: source.candidate.path,
|
|
1095
|
+
message:
|
|
1096
|
+
`TypeScript module resolution failed for ${JSON.stringify(dependency.specifier)}` +
|
|
1097
|
+
` at line ${dependency.line}.`,
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
const namedBindings = dependency.namedBindings;
|
|
1101
|
+
const target = resolved.target ? parsed.get(resolved.target) : undefined;
|
|
1102
|
+
const staticEdge = dependency.kind === 'import' || dependency.kind === 'export';
|
|
1103
|
+
const targetTypeNames = new Set(target?.typeOnlyExportNames ?? []);
|
|
1104
|
+
const targetTypeOnlyExports = Boolean(
|
|
1105
|
+
target && staticEdge && target.exportsOnlyTypes && !dependency.typeOnly
|
|
1106
|
+
);
|
|
1107
|
+
const namedBindingsTypeOnly = Boolean(
|
|
1108
|
+
staticEdge &&
|
|
1109
|
+
namedBindings?.length > 0 &&
|
|
1110
|
+
targetTypeNames.size > 0 &&
|
|
1111
|
+
!target?.hasTopLevelSideEffects &&
|
|
1112
|
+
namedBindings.every((name) => targetTypeNames.has(name))
|
|
1113
|
+
);
|
|
1114
|
+
const portProofEligible = Boolean(
|
|
1115
|
+
target &&
|
|
1116
|
+
dependency.kind === 'import' &&
|
|
1117
|
+
!dependency.typeOnly &&
|
|
1118
|
+
!targetTypeOnlyExports &&
|
|
1119
|
+
!namedBindingsTypeOnly &&
|
|
1120
|
+
source.portProofEligible
|
|
1121
|
+
);
|
|
1122
|
+
dependencies.push({
|
|
1123
|
+
from: source.candidate.path,
|
|
1124
|
+
...(dependency.specifier ? { specifier: dependency.specifier } : {}),
|
|
1125
|
+
kind: dependency.kind,
|
|
1126
|
+
typeOnly: dependency.typeOnly,
|
|
1127
|
+
line: dependency.line,
|
|
1128
|
+
resolution: resolved.resolution,
|
|
1129
|
+
...(resolved.target ? { target: resolved.target } : {}),
|
|
1130
|
+
...(namedBindings ? { namedBindings } : {}),
|
|
1131
|
+
...(targetTypeOnlyExports
|
|
1132
|
+
? { targetTypeOnlyExports: true }
|
|
1133
|
+
: {}),
|
|
1134
|
+
...(source.exportsOnlyTypes ? { sourcePureTypeModule: true } : {}),
|
|
1135
|
+
...(namedBindingsTypeOnly ? { namedBindingsTypeOnly: true } : {}),
|
|
1136
|
+
...(portProofEligible ? { portProofEligible: true } : {}),
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
const projectPackageName = readPackageName(canonicalRoot, observeInput);
|
|
1142
|
+
return createTrustedResolvedCandidateFacts({
|
|
1143
|
+
schemaVersion: '1.0',
|
|
1144
|
+
completeness: completenessReasons.length === 0 ? 'complete' : 'partial',
|
|
1145
|
+
completenessReasons,
|
|
1146
|
+
resolverIdentity: RESOLVED_FACTS_RESOLVER_IDENTITY,
|
|
1147
|
+
compilerIdentity: `typescript@${ts.version ?? 'unknown'}`,
|
|
1148
|
+
compilerOptionsHash: compiler.compilerOptionsHash,
|
|
1149
|
+
tsconfigHash: compiler.tsconfigHash,
|
|
1150
|
+
evidenceRequirementsHash: resolvedFactsEvidenceRequirementsHash(config),
|
|
1151
|
+
...(projectPackageName ? { projectPackageName } : {}),
|
|
1152
|
+
files,
|
|
1153
|
+
dependencies,
|
|
1154
|
+
capabilityUses,
|
|
1155
|
+
ambientUses,
|
|
1156
|
+
publishCalls,
|
|
1157
|
+
intentReferences,
|
|
1158
|
+
safetyUses,
|
|
1159
|
+
});
|
|
1160
|
+
}
|