@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 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.51.
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 (ts.isElementAccessExpression(node) && nameOf(node.argumentExpression) === null) return true;
82
- if (ts.isIdentifier(node) && ['eval', 'Function', 'createRequire'].includes(node.text)) return true;
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 !== 1 || !ts.isStringLiteral(node.arguments[0]);
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?$/i.test(file.path)).map(file => ({ path: file.path, exists: true }));
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?$/i.test(relative)) {
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({ path: relative, reason: 'candidate-dynamic-import' });
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.dynamicSources.map(item => ({ path: item.path, reason: 'dynamic-import' }))] });
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 => { const value = moduleLiteral(expression); if (value === null) dynamic = true; else found.add(value); };
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)) dynamic = true;
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
- if (ts.isIdentifier(node)) {
128
- if (dynamicNames.has(node.text)) dynamic = true;
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": "entrorove.skill-manifest/1.0",
3
3
  "name": "entrorove",
4
- "version": "0.1.51",
4
+ "version": "0.1.52",
5
5
  "endpoint": "https://entrorove.com",
6
6
  "requestSchemaVersion": "entrorove.skill.request/2.0",
7
7
  "supportedRequestSchemaVersions": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entrorove/skill",
3
- "version": "0.1.51",
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 --test --test-concurrency=1 test/*.test.mjs",
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",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": "entrorove.release-state/1.0",
3
- "version": "0.1.51",
4
- "digest": "337302626679e547236186da4653bd37c5ed8aec25bd24999920488334426587",
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": "f49e67a2eb2f19f8eb7c4226d481842b38c0f19b495ad0149cbf649baae16bb9",
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": "ffd31e9b2946a9784d4ab59cee5345318d2152816ce995ed5dac11efa4096bc4",
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": "20ded9c21bd71b8779c454f5eba4c7c0a943ef99d94117b9bbef891d2ab6e3de",
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": "cdb1f686304d5d29d1feff1b1ec5b75e4e3dcff1f1da841dfb59d0502ab261e8",
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": "5bdd92729faec9b30f70d57891d0779b3cbff78b8a6a21c98240cdb62f2109d1",
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
  ]);