@entrorove/skill 0.1.51 → 0.1.52
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 +5 -0
- package/SKILL.md +1 -1
- package/local-discipline-analysis.mjs +11 -5
- package/local-discipline-repository.mjs +7 -4
- package/local-scope-import-expressions.mjs +80 -0
- package/local-scope-repository.mjs +32 -21
- package/local-typescript-function-reference.mjs +98 -0
- package/manifest.json +1 -1
- package/package.json +4 -2
- package/release-state.json +9 -7
- package/releases.json +6 -0
- package/reuse-discipline-release.mjs +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.52 — 2026-10-17
|
|
4
|
+
|
|
5
|
+
- 修复 Vue/TypeScript 的 Function 类型和真实 Vue 回调 prop 被误判为动态依赖;解析不可变 const 的有限语言模块导入,记录实际依赖与哈希。补齐 Vue 候选依赖检查;未知运行时导入报告文件、行列和原因。测试采用隔离用户目录,越界、动态执行、未知删除与原任务写入校验继续保留。
|
|
6
|
+
- Fixed Vue/TypeScript Function types and genuine Vue callback props being misclassified as dynamic dependencies. Resolve finite immutable const-based language imports with actual dependency hashes and inspect Vue candidate dependencies. Unbounded runtime imports report file, line, column and reason. Tests use isolated user directories while path, dynamic-execution, unknown-deletion and original-task mutation checks remain enforced.
|
|
7
|
+
|
|
3
8
|
## 0.1.51 — 2026-10-16
|
|
4
9
|
|
|
5
10
|
- 修复入口阶段将自身生成的带引号 capabilities/unlock 命令误判为原生 Bash;正常 TypeScript 候选探测遇到普通文件祖先时继续回退,真实访问仍严格校验。路径拒绝会报告请求路径、阻断祖先、解析地址和原因。
|
package/SKILL.md
CHANGED
|
@@ -5,7 +5,7 @@ description: Execute an engineering task through Entrorove, preserving its origi
|
|
|
5
5
|
|
|
6
6
|
# entrorove · 熵巡
|
|
7
7
|
|
|
8
|
-
Version: 0.1.
|
|
8
|
+
Version: 0.1.52.
|
|
9
9
|
|
|
10
10
|
Entrorove applies to the whole task once selected. Reading this document or calling an entry once does not execute the task. On every later turn, including after compaction, continue the original task and its required next operation before dependent work. Keep the original goal, scope, account, flow, cloud task and failure records. Never describe planning, a reported step or model output as verified delivery.
|
|
11
11
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
2
|
import { archguardScriptSource } from './local-archguard-ast.mjs';
|
|
3
3
|
import { canonical, sha256 } from './local-integrity.mjs';
|
|
4
|
+
import { isTypeOnlyReference, isDynamicFunctionReference } from './local-typescript-function-reference.mjs';
|
|
5
|
+
import { createScopeImportResolver } from './local-scope-import-expressions.mjs';
|
|
4
6
|
|
|
5
7
|
export const disciplineAnalysisLimits = Object.freeze({ fileBytes: 1_048_576, sourceBytes: 8_388_608, files: 512, nodes: 200_000 });
|
|
6
8
|
function analysisLimit(file, reason, limit, required) {
|
|
@@ -77,12 +79,15 @@ function definition(source, node) {
|
|
|
77
79
|
return { ...location(source, node), symbol, name, exported: Boolean(exported), contract: Boolean(contract),
|
|
78
80
|
signatureDigest: contract ? sha256(signature) : null };
|
|
79
81
|
}
|
|
80
|
-
function unresolvedNode(node) {
|
|
81
|
-
if (
|
|
82
|
-
if (ts.
|
|
82
|
+
function unresolvedNode(node, resolve) {
|
|
83
|
+
if (isTypeOnlyReference(node)) return false;
|
|
84
|
+
if (ts.isElementAccessExpression(node) && (nameOf(node.argumentExpression) === null
|
|
85
|
+
|| ['eval', 'Function', 'createRequire', 'require'].includes(nameOf(node.argumentExpression)))) return true;
|
|
86
|
+
if (ts.isIdentifier(node) && (['eval', 'createRequire'].includes(node.text) || isDynamicFunctionReference(node))) return true;
|
|
83
87
|
if (ts.isCallExpression(node)) {
|
|
84
88
|
if (node.expression.kind === ts.SyntaxKind.ImportKeyword || ts.isIdentifier(node.expression) && node.expression.text === 'require') {
|
|
85
|
-
return node.arguments.length
|
|
89
|
+
return !node.arguments.length || node.arguments.length > (node.expression.kind === ts.SyntaxKind.ImportKeyword ? 2 : 1)
|
|
90
|
+
|| resolve(node.arguments[0]) === null;
|
|
86
91
|
}
|
|
87
92
|
if (ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression)
|
|
88
93
|
&& ['Reflect', 'Object'].includes(node.expression.expression.text) && ['get', 'set', 'defineProperty'].includes(node.expression.name.text)) return true;
|
|
@@ -148,8 +153,9 @@ export function analyzeDisciplineSources(files, { edges = [], unresolved = [], r
|
|
|
148
153
|
const program = programFor(sources, edges), checker = program.getTypeChecker(), definitions = [], mappings = [], references = new Map();
|
|
149
154
|
const unknown = [...unresolved, ...unsupported];
|
|
150
155
|
for (const source of sources.values()) {
|
|
156
|
+
const resolve = createScopeImportResolver(source, checker);
|
|
151
157
|
const visit = node => {
|
|
152
|
-
if (unresolvedNode(node)) unknown.push({ ...location(source, node), reason: 'dynamic-reference' });
|
|
158
|
+
if (unresolvedNode(node, resolve)) unknown.push({ ...location(source, node), reason: 'dynamic-reference' });
|
|
153
159
|
if (symbolNode(node)) {
|
|
154
160
|
const entry = definition(source, node);
|
|
155
161
|
if (entry) { const symbol = checker.getSymbolAtLocation(node.name); definitions.push({ entry, symbol, nameNode: node.name }); }
|
|
@@ -90,25 +90,28 @@ export async function disciplineRepositoryObservation(input, root, { analyze = f
|
|
|
90
90
|
if (!analyze) return { comparison };
|
|
91
91
|
// The existing resolver supplies actual aliases, package exports and import edges.
|
|
92
92
|
// Unsupported/dynamic references stay unknown; they are never converted to unused.
|
|
93
|
-
const roots = sources.filter(file => paths.includes(file.path) && /\.[cm]?[jt]sx
|
|
93
|
+
const roots = sources.filter(file => paths.includes(file.path) && /\.(?:[cm]?[jt]sx?|vue)$/i.test(file.path)).map(file => ({ path: file.path, exists: true }));
|
|
94
94
|
let graph;
|
|
95
95
|
try { graph = roots.length ? await importGraph(root, roots, input, disciplineAnalysisLimits) : { edges: [], dynamicSources: [], complete: true }; }
|
|
96
96
|
catch (error) {
|
|
97
97
|
if (!['ENTROROVE_IMPORT_UNRESOLVED', 'ENTROROVE_IMPORT_INVALID', 'ENTROROVE_IMPORT_UNSUPPORTED', 'ENTROROVE_SCOPE_LIMIT', 'ENTROROVE_BUDGET_SCOPE'].includes(error.code)) throw error;
|
|
98
98
|
graph = { edges: [], dynamicSources: [], complete: false }; unresolved.push({ reason: error.code, message: error.message });
|
|
99
99
|
}
|
|
100
|
-
for (const [relative, content] of overrides) if (semanticSource(relative) && /\.[cm]?[jt]sx
|
|
100
|
+
for (const [relative, content] of overrides) if (semanticSource(relative) && /\.(?:[cm]?[jt]sx?|vue)$/i.test(relative)) {
|
|
101
101
|
try {
|
|
102
102
|
const candidate = await scopeCandidateImports(root, { path: relative, content }, input);
|
|
103
103
|
graph.edges = [...graph.edges.filter(edge => edge.from !== relative), ...candidate.edges];
|
|
104
|
-
if (candidate.dynamic) unresolved.push(
|
|
104
|
+
if (candidate.dynamic) unresolved.push(...(candidate.unresolvedImports?.length ? candidate.unresolvedImports
|
|
105
|
+
: [{ path: relative, reason: 'candidate-dynamic-import' }]));
|
|
105
106
|
} catch (error) {
|
|
106
107
|
if (!['ENTROROVE_IMPORT_UNRESOLVED', 'ENTROROVE_IMPORT_INVALID', 'ENTROROVE_IMPORT_UNSUPPORTED', 'ENTROROVE_BUDGET_SCOPE'].includes(error.code)) throw error;
|
|
107
108
|
unresolved.push({ path: relative, reason: error.code });
|
|
108
109
|
}
|
|
109
110
|
}
|
|
110
111
|
const analysis = analyzeDisciplineSources(sources, { edges: graph.edges, requiredPaths: [...required],
|
|
111
|
-
unresolved: [...unresolved, ...graph.
|
|
112
|
+
unresolved: [...unresolved, ...(graph.unresolvedImports ?? []),
|
|
113
|
+
...graph.dynamicSources.filter(item => !graph.unresolvedImports?.some(site => site.path === item.path))
|
|
114
|
+
.map(item => ({ path: item.path, reason: 'dynamic-import' }))] });
|
|
112
115
|
analysis.definitions.push(...await nativeDisciplineDefinitions(sources));
|
|
113
116
|
return { comparison, analysis, scanComplete: graph.complete && analysis.unresolved.length === 0 };
|
|
114
117
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
|
|
4
|
+
const limits = Object.freeze({ alternatives: 64, nodes: 512, depth: 32 });
|
|
5
|
+
function unwrap(node) {
|
|
6
|
+
while (node && (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isSatisfiesExpression(node)
|
|
7
|
+
|| ts.isTypeAssertionExpression(node) || ts.isNonNullExpression(node))) node = node.expression;
|
|
8
|
+
return node;
|
|
9
|
+
}
|
|
10
|
+
function sourceChecker(source) {
|
|
11
|
+
const name = path.posix.resolve('/', source.fileName.replaceAll('\\', '/'));
|
|
12
|
+
const matches = value => path.posix.resolve('/', value.replaceAll('\\', '/')) === name;
|
|
13
|
+
const host = { getSourceFile: filename => matches(filename) ? source : undefined,
|
|
14
|
+
getDefaultLibFileName: () => '/entrorove-no-library.d.ts', writeFile: () => {},
|
|
15
|
+
getCurrentDirectory: () => '/', getDirectories: () => [], fileExists: matches,
|
|
16
|
+
readFile: filename => matches(filename) ? source.text : undefined,
|
|
17
|
+
getCanonicalFileName: filename => filename, useCaseSensitiveFileNames: () => true, getNewLine: () => '\n' };
|
|
18
|
+
return ts.createProgram([name], { noLib: true, noResolve: true, allowJs: true }, host).getTypeChecker();
|
|
19
|
+
}
|
|
20
|
+
function product(left, right) {
|
|
21
|
+
if (!left || !right || left.length * right.length > limits.alternatives) return null;
|
|
22
|
+
return [...new Set(left.flatMap(prefix => right.map(suffix => prefix + suffix)))];
|
|
23
|
+
}
|
|
24
|
+
/** Resolve literal alternatives without executing source, trusting type assertions, or reading other files. */
|
|
25
|
+
export function createScopeImportResolver(source, suppliedChecker) {
|
|
26
|
+
let checker = suppliedChecker;
|
|
27
|
+
const resolve = expression => {
|
|
28
|
+
let nodes = 0;
|
|
29
|
+
const values = (raw, seen = new Set(), depth = 0) => {
|
|
30
|
+
const node = unwrap(raw);
|
|
31
|
+
if (!node || depth > limits.depth || ++nodes > limits.nodes) return null;
|
|
32
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return [node.text];
|
|
33
|
+
if (ts.isConditionalExpression(node)) {
|
|
34
|
+
const left = values(node.whenTrue, seen, depth + 1), right = values(node.whenFalse, seen, depth + 1);
|
|
35
|
+
return left && right && left.length + right.length <= limits.alternatives ? [...new Set([...left, ...right])] : null;
|
|
36
|
+
}
|
|
37
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
38
|
+
return product(values(node.left, seen, depth + 1), values(node.right, seen, depth + 1));
|
|
39
|
+
}
|
|
40
|
+
if (ts.isTemplateExpression(node)) {
|
|
41
|
+
let result = [node.head.text];
|
|
42
|
+
for (const span of node.templateSpans) {
|
|
43
|
+
result = product(result, values(span.expression, seen, depth + 1));
|
|
44
|
+
if (!result) return null;
|
|
45
|
+
result = result.map(value => value + span.literal.text);
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
if (!ts.isIdentifier(node)) return null;
|
|
50
|
+
checker ??= sourceChecker(source);
|
|
51
|
+
const declarations = checker.getSymbolAtLocation(node)?.declarations;
|
|
52
|
+
if (declarations?.length !== 1) return null;
|
|
53
|
+
const declaration = declarations[0];
|
|
54
|
+
if (seen.has(declaration) || declaration.getSourceFile() !== source) return null;
|
|
55
|
+
const next = new Set([...seen, declaration]);
|
|
56
|
+
if (ts.isVariableDeclaration(declaration) && ts.isVariableDeclarationList(declaration.parent)
|
|
57
|
+
&& declaration.parent.flags & ts.NodeFlags.Const && declaration.initializer
|
|
58
|
+
&& !(ts.getCombinedModifierFlags(declaration) & ts.ModifierFlags.Ambient)
|
|
59
|
+
&& !source.isDeclarationFile) return values(declaration.initializer, next, depth + 1);
|
|
60
|
+
return null;
|
|
61
|
+
};
|
|
62
|
+
const alternatives = values(expression);
|
|
63
|
+
if (alternatives?.some(value => !value || /[\u0000-\u001f\u007f]/.test(value))) {
|
|
64
|
+
throw Object.assign(new Error('module literal is empty or contains control characters'), { code: 'ENTROROVE_IMPORT_INVALID' });
|
|
65
|
+
}
|
|
66
|
+
return alternatives?.sort() ?? null;
|
|
67
|
+
};
|
|
68
|
+
return resolve;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function unresolvedImportSite(source, expression) {
|
|
72
|
+
const node = unwrap(expression), position = source.getLineAndCharacterOfPosition((node ?? source).getStart(source));
|
|
73
|
+
const kind = node ? ts.SyntaxKind[node.kind] : 'MissingArgument';
|
|
74
|
+
const reason = node && ts.isIdentifier(node) ? 'import-binding-not-finite-constant'
|
|
75
|
+
: node && ts.isTemplateExpression(node) ? 'import-template-has-unbounded-substitution'
|
|
76
|
+
: node && ts.isBinaryExpression(node) ? 'import-concatenation-has-unbounded-operand'
|
|
77
|
+
: 'import-expression-not-statically-finite';
|
|
78
|
+
return { line: position.line + 1, column: position.character + 1, kind, reason,
|
|
79
|
+
message: `Cannot verify ${kind} import: only immutable literal alternatives (at most ${limits.alternatives}) are statically bounded; runtime values and mutable bindings remain unresolved` };
|
|
80
|
+
}
|
|
@@ -13,6 +13,8 @@ import { observeBudgetScopeGit, scopeGitBudgetEnforced } from './local-scope-git
|
|
|
13
13
|
import { scopeGitMetadata } from './local-scope-git-reads.mjs';
|
|
14
14
|
import { scopeTsconfigImports } from './local-scope-config-imports.mjs';
|
|
15
15
|
import { scopeGitExecutable } from './local-git-executable.mjs';
|
|
16
|
+
import { createScopeImportResolver, unresolvedImportSite } from './local-scope-import-expressions.mjs';
|
|
17
|
+
import { isTypeOnlyReference, isDynamicFunctionReference } from './local-typescript-function-reference.mjs';
|
|
16
18
|
|
|
17
19
|
const execute = promisify(execFile);
|
|
18
20
|
async function git(root, args, allowed = [], input = { repositoryRoot: root }) {
|
|
@@ -94,11 +96,6 @@ function staticString(expression) {
|
|
|
94
96
|
if (!expression || (!ts.isStringLiteral(expression) && !ts.isNoSubstitutionTemplateLiteral(expression))) return null;
|
|
95
97
|
return expression.text;
|
|
96
98
|
}
|
|
97
|
-
function moduleLiteral(expression) {
|
|
98
|
-
const value = staticString(expression);
|
|
99
|
-
if (value !== null && (!value || /[\u0000-\u001f\u007f]/.test(value))) scopeFail('ENTROROVE_IMPORT_INVALID', 'module literal is empty or contains control characters');
|
|
100
|
-
return value;
|
|
101
|
-
}
|
|
102
99
|
function globalObject(expression) {
|
|
103
100
|
while (expression && (ts.isParenthesizedExpression(expression) || ts.isAsExpression(expression)
|
|
104
101
|
|| ts.isTypeAssertionExpression(expression) || ts.isNonNullExpression(expression) || ts.isSatisfiesExpression(expression))) expression = expression.expression;
|
|
@@ -106,12 +103,16 @@ function globalObject(expression) {
|
|
|
106
103
|
}
|
|
107
104
|
// The same pinned TypeScript parser as the cloud scope analyzer visits actual
|
|
108
105
|
// expressions inside templates and JSX; literal bodies never become code.
|
|
109
|
-
function scriptImports(source, filename) {
|
|
106
|
+
function scriptImports(source, filename, context = {}) {
|
|
110
107
|
const parsed = ts.createSourceFile(filename, source, ts.ScriptTarget.Latest, true,
|
|
111
108
|
scriptKinds.get(path.extname(filename).toLowerCase()) ?? ts.ScriptKind.TS);
|
|
112
109
|
if (parsed.parseDiagnostics.length) scopeFail('ENTROROVE_IMPORT_INVALID', 'source has invalid or unterminated syntax');
|
|
113
|
-
const found = new Set(); let dynamic = false;
|
|
114
|
-
const add = expression => {
|
|
110
|
+
const found = new Set(), unresolvedImports = [], resolve = createScopeImportResolver(parsed); let dynamic = false;
|
|
111
|
+
const add = expression => {
|
|
112
|
+
const values = resolve(expression);
|
|
113
|
+
if (values === null) { dynamic = true; unresolvedImports.push(unresolvedImportSite(parsed, expression)); }
|
|
114
|
+
else for (const value of values) found.add(value);
|
|
115
|
+
};
|
|
115
116
|
const visit = node => {
|
|
116
117
|
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) { if (node.moduleSpecifier) add(node.moduleSpecifier); }
|
|
117
118
|
else if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference)) add(node.moduleReference.expression);
|
|
@@ -120,24 +121,29 @@ function scriptImports(source, filename) {
|
|
|
120
121
|
const importer = node.expression.kind === ts.SyntaxKind.ImportKeyword;
|
|
121
122
|
const requireCall = ts.isIdentifier(node.expression) && node.expression.text === 'require';
|
|
122
123
|
if (importer || requireCall) {
|
|
123
|
-
if (!node.arguments.length || node.arguments.length > (importer ? 2 : 1))
|
|
124
|
+
if (!node.arguments.length || node.arguments.length > (importer ? 2 : 1)) {
|
|
125
|
+
dynamic = true;
|
|
126
|
+
unresolvedImports.push({ ...unresolvedImportSite(parsed, node), reason: 'import-argument-list-not-supported',
|
|
127
|
+
message: 'Import inspection requires one module argument and at most one import-attributes argument' });
|
|
128
|
+
}
|
|
124
129
|
else add(node.arguments[0]);
|
|
125
130
|
}
|
|
126
131
|
}
|
|
127
|
-
|
|
128
|
-
|
|
132
|
+
const runtimeReference = !isTypeOnlyReference(node);
|
|
133
|
+
if (runtimeReference && ts.isIdentifier(node)) {
|
|
134
|
+
if (dynamicNames.has(node.text) && (node.text !== 'Function' || isDynamicFunctionReference(node, context))) dynamic = true;
|
|
129
135
|
if (node.text === 'require' && !(ts.isCallExpression(node.parent) && node.parent.expression === node)
|
|
130
136
|
&& !(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node)
|
|
131
137
|
&& !(ts.isMethodDeclaration(node.parent) && node.parent.name === node)) dynamic = true;
|
|
132
138
|
}
|
|
133
|
-
if (ts.isElementAccessExpression(node)) {
|
|
139
|
+
if (runtimeReference && ts.isElementAccessExpression(node)) {
|
|
134
140
|
const member = staticString(node.argumentExpression);
|
|
135
141
|
if (dynamicNames.has(member) || member === 'require' || member === null && globalObject(node.expression)) dynamic = true;
|
|
136
142
|
}
|
|
137
|
-
if (ts.isPropertyAccessExpression(node) && node.name.text === 'require' && globalObject(node.expression)) dynamic = true;
|
|
143
|
+
if (runtimeReference && ts.isPropertyAccessExpression(node) && node.name.text === 'require' && globalObject(node.expression)) dynamic = true;
|
|
138
144
|
ts.forEachChild(node, visit);
|
|
139
145
|
};
|
|
140
|
-
visit(parsed); return { specifiers: [...found].sort(), dynamic };
|
|
146
|
+
visit(parsed); return { specifiers: [...found].sort(), dynamic, unresolvedImports };
|
|
141
147
|
}
|
|
142
148
|
function vueScripts(source, filename) {
|
|
143
149
|
const { descriptor, errors } = parseVue(source, { filename, sourceMap: false,
|
|
@@ -152,7 +158,8 @@ function vueScripts(source, filename) {
|
|
|
152
158
|
const language = block.lang ?? 'js';
|
|
153
159
|
if (!['js', 'javascript', 'ts', 'typescript', 'jsx', 'tsx'].includes(language)) scopeFail('ENTROROVE_IMPORT_UNSUPPORTED', 'component script language is unsupported');
|
|
154
160
|
const extension = { javascript: 'js', typescript: 'ts' }[language] ?? language;
|
|
155
|
-
scripts.push({ source: block.src ? `import ${JSON.stringify(block.src)};` : block.content, filename: `${filename}.${extension}
|
|
161
|
+
scripts.push({ source: block.src ? `import ${JSON.stringify(block.src)};` : block.content, filename: `${filename}.${extension}`,
|
|
162
|
+
lineOffset: block.loc.start.line - 1, vueComponent: true, vueSetup: block === descriptor.scriptSetup });
|
|
156
163
|
}
|
|
157
164
|
const seen = new WeakSet();
|
|
158
165
|
const walk = node => {
|
|
@@ -172,10 +179,11 @@ function vueScripts(source, filename) {
|
|
|
172
179
|
function importSpecifiers(content, filename) {
|
|
173
180
|
try {
|
|
174
181
|
const source = content.toString('utf8'), parts = path.extname(filename).toLowerCase() === '.vue' ? vueScripts(source, filename) : [{ source, filename }];
|
|
175
|
-
const specifiers = new Set(); let dynamic = false;
|
|
176
|
-
for (const part of parts) { const parsed = scriptImports(part.source, part.filename); dynamic ||= parsed.dynamic;
|
|
182
|
+
const specifiers = new Set(), unresolvedImports = []; let dynamic = false;
|
|
183
|
+
for (const part of parts) { const parsed = scriptImports(part.source, part.filename, part); dynamic ||= parsed.dynamic;
|
|
184
|
+
unresolvedImports.push(...parsed.unresolvedImports.map(site => ({ ...site, line: site.line + (part.lineOffset ?? 0) })));
|
|
177
185
|
for (const specifier of parsed.specifiers) specifiers.add(specifier); }
|
|
178
|
-
return { specifiers: [...specifiers].sort(), dynamic };
|
|
186
|
+
return { specifiers: [...specifiers].sort(), dynamic, unresolvedImports };
|
|
179
187
|
} catch (error) {
|
|
180
188
|
if (typeof error?.code === 'string' && error.code.startsWith('ENTROROVE_')) throw error;
|
|
181
189
|
if (error instanceof RangeError) scopeFail('ENTROROVE_SCOPE_LIMIT', 'dependency syntax exceeds the supported parser or traversal bound');
|
|
@@ -250,7 +258,7 @@ async function resolveLocalImport(root, relative, required = true) {
|
|
|
250
258
|
export async function importGraph(root, originals, input = { repositoryRoot: root }, limits = {}) {
|
|
251
259
|
const pending = originals.filter(file => file.exists !== false).map(file => file.path); const visited = new Map(); const edges = []; let dynamic = false;
|
|
252
260
|
let sourceBytes = 0;
|
|
253
|
-
const dynamicSources = [];
|
|
261
|
+
const dynamicSources = [], unresolvedImports = [];
|
|
254
262
|
const workspace = await workspaceIndex(root, input);
|
|
255
263
|
const metadata = new Map(workspace.metadata.map(item => [item.path, item]));
|
|
256
264
|
while (pending.length) {
|
|
@@ -273,6 +281,7 @@ export async function importGraph(root, originals, input = { repositoryRoot: roo
|
|
|
273
281
|
for (const observed of aliases.observations) metadata.set(observed.path, observed);
|
|
274
282
|
const declarations = declarationModules(read.content, relative);
|
|
275
283
|
const imports = importSpecifiers(read.content, relative); dynamic ||= imports.dynamic;
|
|
284
|
+
unresolvedImports.push(...imports.unresolvedImports.map(site => ({ path: relative, ...site })));
|
|
276
285
|
if (imports.dynamic) dynamicSources.push({ path: relative, sha256: read.sha256 });
|
|
277
286
|
for (const specifier of imports.specifiers.sort()) {
|
|
278
287
|
const target = await resolveImport(root, relative, specifier, workspace, input, aliases, configPath, declarations);
|
|
@@ -286,7 +295,8 @@ export async function importGraph(root, originals, input = { repositoryRoot: roo
|
|
|
286
295
|
const workspaceMetadata = [...metadata.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
287
296
|
await stableScope(root, workspaceMetadata, input);
|
|
288
297
|
return { nodes, edges: edges.sort((a, b) => `${a.from}:${a.specifier}`.localeCompare(`${b.from}:${b.specifier}`)),
|
|
289
|
-
dynamic, complete: !dynamic, dynamicSources: dynamicSources.sort((a, b) => a.path.localeCompare(b.path)), workspaceMetadata
|
|
298
|
+
dynamic, complete: !dynamic, dynamicSources: dynamicSources.sort((a, b) => a.path.localeCompare(b.path)), workspaceMetadata,
|
|
299
|
+
...(unresolvedImports.length ? { unresolvedImports } : {}) };
|
|
290
300
|
}
|
|
291
301
|
export async function scopeCandidateImports(root, file, input = { repositoryRoot: root }) {
|
|
292
302
|
const parsed = importSpecifiers(Buffer.from(file.content), file.path);
|
|
@@ -299,7 +309,8 @@ export async function scopeCandidateImports(root, file, input = { repositoryRoot
|
|
|
299
309
|
const target = await resolveImport(root, file.path, specifier, workspace, input, aliases, context?.configPath, declarations);
|
|
300
310
|
edges.push({ from: file.path, to: target, external: target === null, specifier });
|
|
301
311
|
}
|
|
302
|
-
return { edges, dynamic: parsed.dynamic
|
|
312
|
+
return { edges, dynamic: parsed.dynamic,
|
|
313
|
+
...(parsed.unresolvedImports.length ? { unresolvedImports: parsed.unresolvedImports.map(site => ({ path: file.path, ...site })) } : {}) };
|
|
303
314
|
}
|
|
304
315
|
export async function collectScopeFacts(root, requested, baselineRef, knownFiles = [], input = { repositoryRoot: root }) {
|
|
305
316
|
const selected = await scopeFiles(root, requested, knownFiles); const files = [];
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
|
|
3
|
+
const bindings = new WeakMap();
|
|
4
|
+
const propertyName = node => node && (ts.isIdentifier(node) || ts.isStringLiteral(node)) ? node.text : null;
|
|
5
|
+
const wrapper = node => ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)
|
|
6
|
+
|| ts.isNonNullExpression(node) || ts.isSatisfiesExpression(node);
|
|
7
|
+
const unwrap = node => {
|
|
8
|
+
while (node && wrapper(node)) node = node.expression;
|
|
9
|
+
return node;
|
|
10
|
+
};
|
|
11
|
+
export function isTypeOnlyReference(node) {
|
|
12
|
+
for (let current = node; current?.parent; current = current.parent) {
|
|
13
|
+
const parent = current.parent;
|
|
14
|
+
if (ts.isExpressionWithTypeArguments(parent) && ts.isHeritageClause(parent.parent)) {
|
|
15
|
+
return parent.parent.token !== ts.SyntaxKind.ExtendsKeyword
|
|
16
|
+
|| !(ts.isClassDeclaration(parent.parent.parent) || ts.isClassExpression(parent.parent.parent));
|
|
17
|
+
}
|
|
18
|
+
if (ts.isTypeNode(parent) || ts.isInterfaceDeclaration(parent) || ts.isTypeAliasDeclaration(parent)
|
|
19
|
+
|| ts.isTypeParameterDeclaration(parent)) return true;
|
|
20
|
+
if ((ts.isImportClause(parent) || ts.isImportSpecifier(parent)) && parent.isTypeOnly) return true;
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
function bindingNames(node) {
|
|
25
|
+
if (ts.isIdentifier(node)) return [node.text];
|
|
26
|
+
if (ts.isObjectBindingPattern(node) || ts.isArrayBindingPattern(node)) {
|
|
27
|
+
return node.elements.flatMap(element => ts.isBindingElement(element) ? bindingNames(element.name) : []);
|
|
28
|
+
}
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
function sourceBindings(source) {
|
|
32
|
+
if (bindings.has(source)) return bindings.get(source);
|
|
33
|
+
const found = new Map();
|
|
34
|
+
const add = (name, api = null) => found.set(name, [...found.get(name) ?? [], api]);
|
|
35
|
+
const visit = node => {
|
|
36
|
+
if (ts.isImportSpecifier(node) || ts.isNamespaceImport(node)) {
|
|
37
|
+
let declaration = node.parent;
|
|
38
|
+
while (declaration && !ts.isImportDeclaration(declaration)) declaration = declaration.parent;
|
|
39
|
+
const vue = declaration && ts.isStringLiteral(declaration.moduleSpecifier) && declaration.moduleSpecifier.text === 'vue'
|
|
40
|
+
&& !declaration.importClause?.isTypeOnly && !node.isTypeOnly;
|
|
41
|
+
add(node.name.text, vue ? ts.isNamespaceImport(node) ? '*' : (node.propertyName ?? node.name).text : null);
|
|
42
|
+
} else if (ts.isImportClause(node) && node.name) add(node.name.text);
|
|
43
|
+
else if (ts.isVariableDeclaration(node) || ts.isParameter(node)) bindingNames(node.name).forEach(name => add(name));
|
|
44
|
+
else if ((ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isClassDeclaration(node)
|
|
45
|
+
|| ts.isClassExpression(node) || ts.isImportEqualsDeclaration(node) || ts.isEnumDeclaration(node)) && node.name) add(node.name.text);
|
|
46
|
+
ts.forEachChild(node, visit);
|
|
47
|
+
};
|
|
48
|
+
visit(source); bindings.set(source, found); return found;
|
|
49
|
+
}
|
|
50
|
+
function vueCall(node, api, context) {
|
|
51
|
+
if (!ts.isCallExpression(node) || node.arguments.length !== 1) return false;
|
|
52
|
+
const expression = unwrap(node.expression), found = sourceBindings(node.getSourceFile());
|
|
53
|
+
if (ts.isIdentifier(expression)) {
|
|
54
|
+
const declarations = found.get(expression.text) ?? [];
|
|
55
|
+
return declarations.length === 1 && declarations[0] === api
|
|
56
|
+
|| api === 'defineProps' && context.vueSetup === true && expression.text === api && !declarations.length;
|
|
57
|
+
}
|
|
58
|
+
return ts.isPropertyAccessExpression(expression) && expression.name.text === api && ts.isIdentifier(expression.expression)
|
|
59
|
+
&& found.get(expression.expression.text)?.length === 1 && found.get(expression.expression.text)[0] === '*';
|
|
60
|
+
}
|
|
61
|
+
function transparentParent(node) {
|
|
62
|
+
while (node.parent && wrapper(node.parent) && node.parent.expression === node) node = node.parent;
|
|
63
|
+
return node;
|
|
64
|
+
}
|
|
65
|
+
function vuePropsContainer(node, context) {
|
|
66
|
+
const value = transparentParent(node), parent = value.parent;
|
|
67
|
+
if (parent && vueCall(parent, 'defineProps', context) && parent.arguments[0] === value) return true;
|
|
68
|
+
if (!ts.isPropertyAssignment(parent) || propertyName(parent.name) !== 'props' || parent.initializer !== value) return false;
|
|
69
|
+
const component = transparentParent(parent.parent), owner = component.parent;
|
|
70
|
+
return context.vueComponent === true && ts.isExportAssignment(owner) && owner.expression === component
|
|
71
|
+
|| owner && vueCall(owner, 'defineComponent', context) && owner.arguments[0] === component;
|
|
72
|
+
}
|
|
73
|
+
function vueFunctionMetadata(node, context) {
|
|
74
|
+
let value = transparentParent(node);
|
|
75
|
+
if (ts.isArrayLiteralExpression(value.parent)) value = transparentParent(value.parent);
|
|
76
|
+
const field = value.parent;
|
|
77
|
+
if (!ts.isPropertyAssignment(field) || field.initializer !== value) return false;
|
|
78
|
+
let props = field.parent;
|
|
79
|
+
if (ts.isObjectLiteralExpression(props) && vuePropsContainer(props, context)) return true;
|
|
80
|
+
if (propertyName(field.name) === 'type') {
|
|
81
|
+
const descriptor = transparentParent(props), property = descriptor.parent;
|
|
82
|
+
if (!ts.isPropertyAssignment(property) || property.initializer !== descriptor) return false;
|
|
83
|
+
props = property.parent;
|
|
84
|
+
}
|
|
85
|
+
return ts.isObjectLiteralExpression(props) && vuePropsContainer(props, context);
|
|
86
|
+
}
|
|
87
|
+
function declarationName(node) {
|
|
88
|
+
const parent = node.parent;
|
|
89
|
+
return parent?.name === node && (ts.isPropertyAssignment(parent) || ts.isPropertySignature(parent)
|
|
90
|
+
|| ts.isMethodDeclaration(parent) || ts.isMethodSignature(parent) || ts.isPropertyDeclaration(parent)
|
|
91
|
+
|| ts.isFunctionDeclaration(parent) || ts.isClassDeclaration(parent) || ts.isVariableDeclaration(parent)
|
|
92
|
+
|| ts.isParameter(parent) || ts.isEnumDeclaration(parent) || ts.isEnumMember(parent)
|
|
93
|
+
|| ts.isImportSpecifier(parent) || ts.isImportClause(parent));
|
|
94
|
+
}
|
|
95
|
+
export function isDynamicFunctionReference(node, context = {}) {
|
|
96
|
+
if (!ts.isIdentifier(node) || node.text !== 'Function') return false;
|
|
97
|
+
return !isTypeOnlyReference(node) && !declarationName(node) && !vueFunctionMetadata(node, context);
|
|
98
|
+
}
|
package/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@entrorove/skill",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.52",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"entrorove": "cli.mjs"
|
|
@@ -132,6 +132,7 @@
|
|
|
132
132
|
"local-scope-candidate.mjs",
|
|
133
133
|
"local-scope-context.mjs",
|
|
134
134
|
"local-scope-files.mjs",
|
|
135
|
+
"local-scope-import-expressions.mjs",
|
|
135
136
|
"local-scope-repository.mjs",
|
|
136
137
|
"local-scope-signature.mjs",
|
|
137
138
|
"local-scope-config-imports.mjs",
|
|
@@ -212,6 +213,7 @@
|
|
|
212
213
|
"ide-queue.mjs",
|
|
213
214
|
"local-discipline-actions.mjs",
|
|
214
215
|
"local-discipline-analysis.mjs",
|
|
216
|
+
"local-typescript-function-reference.mjs",
|
|
215
217
|
"local-discipline-batch.mjs",
|
|
216
218
|
"local-discipline-contracts.mjs",
|
|
217
219
|
"local-discipline-gate.mjs",
|
|
@@ -233,7 +235,7 @@
|
|
|
233
235
|
"reuse-discipline-release.mjs"
|
|
234
236
|
],
|
|
235
237
|
"scripts": {
|
|
236
|
-
"test": "node
|
|
238
|
+
"test": "node test/run.mjs",
|
|
237
239
|
"prepublishOnly": "node release.mjs",
|
|
238
240
|
"prepare:archguard": "node archguard-runtime-build.mjs",
|
|
239
241
|
"check:archguard": "node archguard-runtime-build.mjs --check",
|
package/release-state.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "entrorove.release-state/1.0",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"digest": "
|
|
3
|
+
"version": "0.1.52",
|
|
4
|
+
"digest": "81fb53f113036a9ffdaa64ac99f3f9c0cc4180c623997ee009d4bc63e58a9ab8",
|
|
5
5
|
"files": {
|
|
6
6
|
"ide-code-mode.mjs": "f3f53357ba09727b3b597851976a24de93287c0faca5052448277d0904bb61f7",
|
|
7
7
|
"ide-command.mjs": "86472c2f89c17e13a6fb0e41794add983400aac3e626891dbb3812e34320e229",
|
|
@@ -12,14 +12,16 @@
|
|
|
12
12
|
"ide-results.mjs": "dfe0d3f773c65a9fc21056538d7005012c982ed4509bc0410d7f62813d0863fd",
|
|
13
13
|
"ide-queue.mjs": "806e716c775fd0079417027b135977c8516f47dddb0bc5ca19e32a355dab0e9e",
|
|
14
14
|
"local-discipline-actions.mjs": "2379c08d0f49c44068f8239db7e514d75885f4606b1b88a7a36e4ff19167caca",
|
|
15
|
-
"local-discipline-analysis.mjs": "
|
|
15
|
+
"local-discipline-analysis.mjs": "e887abdd1c6d6ba59ef7323e9035528f0491e5458ca8159985ec26e1ca67a4af",
|
|
16
16
|
"local-discipline-batch.mjs": "2cc90dbae55efdee0c445da399c0a9acafa92e915344c25208ebca554fc4c9e9",
|
|
17
|
+
"local-typescript-function-reference.mjs": "af682b479fe0d69a5ecd62232406bb8078408ec8710733cfed9ae6b059762ba5",
|
|
18
|
+
"local-scope-import-expressions.mjs": "6623dea5501c4dc24e12c99e0776d65650bbe752c85c9509d022ba1f61bd5111",
|
|
17
19
|
"local-discipline-contracts.mjs": "6cf8a3d132fdb9074eb1441600afe7aa199df33e88a71a660d9db534c14f19e4",
|
|
18
20
|
"local-discipline-contracts.d.mts": "defc684c78dc88afa1c54340ab4a01ae36db7aa11257cbe1f5cd323f608c78c4",
|
|
19
21
|
"local-discipline-gate.mjs": "04b1ba092d7f4342a0e77cfbeb44d9634dbce9fec0c79e6a1e4b11a662d904ac",
|
|
20
22
|
"local-discipline-native.mjs": "8b99b5ce951cab3c1426428ad807650c61c0ffb2c346d747b73b99057b382049",
|
|
21
23
|
"local-discipline-operations.mjs": "9fcb7b8889b7718d42e1d37936485d8198d5017d405e0cdbbabdbbd12fcc9d48",
|
|
22
|
-
"local-discipline-repository.mjs": "
|
|
24
|
+
"local-discipline-repository.mjs": "b6392e02ecae333dfdc8df41f203fd69bed035d88b15b16d112ab1897b3c297c",
|
|
23
25
|
"local-discipline-snapshot.mjs": "8fd35b2cfbece8f64579f144f589a66dec9b6022d50fe931c4c1f886cb83134d",
|
|
24
26
|
"local-runner-discipline.mjs": "86f451de2f1e12a24f5596a29828891c1eba13cd4714ed0c18a77d66cecef934",
|
|
25
27
|
"local-integrity.d.mts": "21357f306234bd841c0d3d90bf2d09f7663f63746f897b9f38190428ae8198b3",
|
|
@@ -31,7 +33,7 @@
|
|
|
31
33
|
"snapshot-cleanup-journal.mjs": "76a9684cc95b4eb076e1298d78a9dff37948b00d9607b8f546c599072d3ed331",
|
|
32
34
|
"IDE-EXECUTION.md": "2cb952bb919261e9e420c9d83659eba013e5b1b175d80da03b50b660edfe4f31",
|
|
33
35
|
"REUSE-DISCIPLINE.md": "5b214cf313e7f715f52d54e28093a2d038f0f196ca64b15d1c49587b9d8eecf8",
|
|
34
|
-
"reuse-discipline-release.mjs": "
|
|
36
|
+
"reuse-discipline-release.mjs": "2935898d1aa807fb982a78d7ab5cb1abe05e3c8fc945dc0909da8098872aa1ac",
|
|
35
37
|
"local-final-gate16.mjs": "083aec470670e6cdb7ad2ab27b7a51b53a9ec3a8b4121cb9cbfa17d9940892c2",
|
|
36
38
|
"local-final-gate16-checks.mjs": "1dd30e9666d458d8b12ca6e4975f6f0ac8450c3ea9cbecc7401ae037f66b589a",
|
|
37
39
|
"local-final-gate16-worker.mjs": "344d64b6c1c5552b96960e0d8514a876a3dbfa9762ab9eb749038e8351727121",
|
|
@@ -81,7 +83,7 @@
|
|
|
81
83
|
"CAPABILITY-MAP.md": "a0f0468a098725ced52a96dd8e22bfc7d1ce7681a8446ebf7dae3696c76c3385",
|
|
82
84
|
"AGREEMENT.md": "f93a68e92c457200dd024610e65f74e478b8f60860f951da818b70561f1b9bf8",
|
|
83
85
|
"DELIVERY-BOUNDARIES.md": "d1bb2895e23fc14bf1c76707bd1f6f03b39695501a78a0b7289dc37b099889dc",
|
|
84
|
-
"package.json": "
|
|
86
|
+
"package.json": "d385aa543bff1f373a1fb8464c712c4ea04e1b2938a8603ddecca903161cbac3",
|
|
85
87
|
"manifest.json": "ab06e307795bc9da5add926752b6c4dbdaf18625090261a035aa356a679c504f",
|
|
86
88
|
"release.mjs": "e206cb6cabbb02542317bce283f7ba674621e67765a3eacef5741f2d6b1c58b2",
|
|
87
89
|
"local-cli.mjs": "11461e4cf4f6a8612ff275bf8296ec57ff22281156cb2c72834d3c764ba422a1",
|
|
@@ -122,7 +124,7 @@
|
|
|
122
124
|
"local-scope-candidate.mjs": "8f0bd9606e70d4a0ebacdd3cd600ec3afed85e2bb3e1760aa1144cf55d4e028a",
|
|
123
125
|
"local-scope-context.mjs": "79753abfc6c76089d17f003a1be537dfaea10d6731c542f7fc53f2a9996ed164",
|
|
124
126
|
"local-scope-files.mjs": "4c14a5e226dd5518aeb37f199b1aa896943a66d41e5a1757a3b3fd8a79446d76",
|
|
125
|
-
"local-scope-repository.mjs": "
|
|
127
|
+
"local-scope-repository.mjs": "d9d4c81c3f401c38c33b8dd7bdf8ebaf4658d1afd1cce408a7acd37f8836a6df",
|
|
126
128
|
"local-scope-git.mjs": "53bbbb6c4795db711b5bc164591a6ad79a561427da046d6bbe997dae6eb82829",
|
|
127
129
|
"local-scope-git-reads.mjs": "5d8a2636bab955cb325cb693db7423e69512355887ae5b5af3b8cb615f5ddad0",
|
|
128
130
|
"local-git-executable.mjs": "9391d716529025b1de3ee006df658a9d765b6ef29ad24ef9b7436829318e651e",
|
package/releases.json
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.1.52",
|
|
4
|
+
"date": "2026-10-17",
|
|
5
|
+
"zh": "修复 Vue/TypeScript 的 Function 类型和真实 Vue 回调 prop 被误判为动态依赖;解析不可变 const 的有限语言模块导入,记录实际依赖与哈希。补齐 Vue 候选依赖检查;未知运行时导入报告文件、行列和原因。测试采用隔离用户目录,越界、动态执行、未知删除与原任务写入校验继续保留。",
|
|
6
|
+
"en": "Fixed Vue/TypeScript Function types and genuine Vue callback props being misclassified as dynamic dependencies. Resolve finite immutable const-based language imports with actual dependency hashes and inspect Vue candidate dependencies. Unbounded runtime imports report file, line, column and reason. Tests use isolated user directories while path, dynamic-execution, unknown-deletion and original-task mutation checks remain enforced."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.1.51",
|
|
4
10
|
"date": "2026-10-16",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const ide = ['ide-code-mode.mjs', 'ide-command.mjs', 'ide-hook.mjs', 'ide-install.mjs', 'ide-session.mjs', 'break-glass.mjs', 'ide-results.mjs', 'ide-queue.mjs'];
|
|
2
2
|
const discipline = ['local-discipline-actions.mjs', 'local-discipline-analysis.mjs', 'local-discipline-batch.mjs',
|
|
3
|
+
'local-typescript-function-reference.mjs', 'local-scope-import-expressions.mjs',
|
|
3
4
|
'local-discipline-contracts.mjs', 'local-discipline-contracts.d.mts', 'local-discipline-gate.mjs',
|
|
4
5
|
'local-discipline-native.mjs', 'local-discipline-operations.mjs', 'local-discipline-repository.mjs',
|
|
5
6
|
'local-discipline-snapshot.mjs', 'local-runner-discipline.mjs', 'local-integrity.d.mts'];
|
|
@@ -15,4 +16,6 @@ export const reuseDisciplineReleaseLabels = Object.fromEntries([
|
|
|
15
16
|
...cleanup.map(file => [file, { zh: '验收后快照自动清理', en: 'verified snapshot cleanup' }]),
|
|
16
17
|
...documentation.map(file => [file, { zh: 'IDE 执行与复用纪律文档', en: 'IDE execution and reuse discipline documentation' }]),
|
|
17
18
|
['reuse-discipline-release.mjs', { zh: '复用纪律版本追踪', en: 'reuse discipline release tracking' }],
|
|
19
|
+
['local-typescript-function-reference.mjs', { zh: 'TypeScript 类型与 Vue 属性构造器引用分析', en: 'TypeScript type and Vue prop constructor references' }],
|
|
20
|
+
['local-scope-import-expressions.mjs', { zh: '不可变有限动态导入与逐站点未核实诊断', en: 'immutable finite imports and precise unresolved import diagnostics' }],
|
|
18
21
|
]);
|