arkgate 3.7.0 → 3.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/CHANGELOG.md +113 -1145
  2. package/README.md +59 -19
  3. package/bin/ark-check-runtime.mjs +1598 -0
  4. package/bin/ark-check.mjs +32 -1565
  5. package/bin/ark-layer-match.mjs +9 -4
  6. package/bin/ark-mcp-runtime.mjs +1976 -0
  7. package/bin/ark-mcp.mjs +84 -1495
  8. package/bin/ark-shared.mjs +34 -38
  9. package/bin/ark.mjs +33 -66
  10. package/bin/lib/adapter-contract.mjs +161 -9
  11. package/bin/lib/agent-gates.mjs +1 -0
  12. package/bin/lib/analysis-completeness.mjs +28 -0
  13. package/bin/lib/analysis-engine.mjs +8 -8
  14. package/bin/lib/analysis-policy.mjs +27 -0
  15. package/bin/lib/architecture-scan.mjs +70 -357
  16. package/bin/lib/auto-patch.mjs +76 -8
  17. package/bin/lib/ci-and-commands.mjs +1 -1
  18. package/bin/lib/codex-home.mjs +43 -16
  19. package/bin/lib/design-delta.mjs +4 -0
  20. package/bin/lib/doctor-advisories.mjs +4 -3
  21. package/bin/lib/doctor-plan.mjs +40 -41
  22. package/bin/lib/enforcement-state.mjs +2 -0
  23. package/bin/lib/github-enforcement.mjs +443 -0
  24. package/bin/lib/hook-templates.mjs +12 -148
  25. package/bin/lib/html-report-advisories.mjs +1 -1
  26. package/bin/lib/html-report-depth.mjs +9 -0
  27. package/bin/lib/html-report.mjs +5 -5
  28. package/bin/lib/install-migrate.mjs +83 -79
  29. package/bin/lib/managed-upgrade.mjs +622 -0
  30. package/bin/lib/mcp-adoption.mjs +3 -1
  31. package/bin/lib/parse-health.mjs +6 -5
  32. package/bin/lib/port-proof.mjs +2 -2
  33. package/bin/lib/prepare-change.mjs +68 -38
  34. package/bin/lib/prepare-write.mjs +7 -1
  35. package/bin/lib/resident-doctor-client.mjs +55 -0
  36. package/bin/lib/resident-hook.mjs +247 -0
  37. package/bin/lib/resolved-candidate-facts.mjs +1160 -0
  38. package/bin/lib/scan-files.mjs +19 -6
  39. package/bin/lib/snippet-analysis.mjs +119 -0
  40. package/bin/lib/source-policy.mjs +24 -0
  41. package/bin/lib/typescript-host.mjs +15 -18
  42. package/bin/lib/unavailable-analysis.mjs +76 -0
  43. package/bin/lib/upgrade-command.mjs +115 -0
  44. package/bin/lib/weakest-link.mjs +21 -179
  45. package/bin/lib/write-path-capabilities.mjs +167 -16
  46. package/bin/lib/write-path-detect.mjs +3 -2
  47. package/dist/eslint/index.cjs +3 -3
  48. package/dist/eslint/index.d.ts +4 -1
  49. package/dist/eslint/index.js +3 -3
  50. package/dist/index.cjs +6 -6
  51. package/dist/index.d.ts +1111 -151
  52. package/dist/index.js +7 -7
  53. package/docs/agent-guide.md +106 -62
  54. package/docs/ai-gates.md +97 -16
  55. package/docs/configuration.md +3 -0
  56. package/docs/demos/01-write-gate-self-correction.md +2 -2
  57. package/docs/enthusiast/README.md +10 -10
  58. package/docs/enthusiast/how-to-gallery-starter.md +2 -2
  59. package/docs/enthusiast/reference-commands.md +18 -1
  60. package/docs/enthusiast/tutorial-first-project.md +2 -2
  61. package/docs/package-surface.md +98 -12
  62. package/docs/typescript-support.md +108 -37
  63. package/package.json +32 -4
  64. package/schemas/ark.analysis-result.schema.json +159 -2
  65. package/schemas/ark.design-delta.schema.json +1 -0
  66. package/schemas/ark.enforcement-state.schema.json +84 -0
  67. package/schemas/ark.resolved-candidate-facts.schema.json +1 -0
  68. package/server.json +2 -2
  69. package/templates/skills/ark-explore.md +5 -5
  70. package/templates/skills/ark-fix.md +1 -1
  71. package/templates/skills/ark-runtime.md +15 -8
  72. package/templates/skills/ark-upgrade.md +122 -182
  73. package/bin/lib/ai-velocity.mjs +0 -293
  74. package/bin/lib/graph-cycles.mjs +0 -6
  75. package/bin/lib/safety-diagnostics.mjs +0 -284
  76. package/bin/lib/ts-resolve.mjs +0 -228
  77. package/dist/configTypes-DAPvBqK6.d.cts +0 -61
  78. package/dist/eslint/index.d.cts +0 -146
  79. 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
+ }