@entrorove/skill 0.1.51 → 0.1.53

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 (51) hide show
  1. package/CAPABILITY-MAP.md +14 -2
  2. package/CHANGELOG.md +12 -0
  3. package/DELIVERY-BOUNDARIES.md +4 -2
  4. package/IDE-EXECUTION.md +25 -3
  5. package/SKILL.md +8 -4
  6. package/cli.mjs +4 -6
  7. package/cloud-mutation-authority.mjs +96 -0
  8. package/connected-cli.mjs +20 -0
  9. package/ide-command.mjs +1 -1
  10. package/ide-hook.mjs +24 -10
  11. package/ide-results.mjs +8 -6
  12. package/ide-session.mjs +44 -1
  13. package/local-approved-plan.mjs +3 -21
  14. package/local-approved-process.mjs +54 -0
  15. package/local-approved-supervisor.mjs +99 -0
  16. package/local-cli.mjs +13 -2
  17. package/local-contracts.mjs +2 -0
  18. package/local-delegated-modify-analysis.mjs +232 -0
  19. package/local-discipline-actions.mjs +2 -0
  20. package/local-discipline-analysis.mjs +11 -5
  21. package/local-discipline-gate.mjs +3 -1
  22. package/local-discipline-repository.mjs +7 -4
  23. package/local-error.mjs +3 -0
  24. package/local-execution.mjs +1 -1
  25. package/local-mutation-authority.mjs +23 -5
  26. package/local-mutation-batch.mjs +1 -0
  27. package/local-mutation-binding.mjs +14 -5
  28. package/local-mutation-consumption.mjs +93 -0
  29. package/local-mutation-pass-store.mjs +6 -4
  30. package/local-mutation-wire.mjs +21 -4
  31. package/local-preserved-mutation-protection.mjs +51 -0
  32. package/local-preserved-mutation-signer.mjs +81 -0
  33. package/local-preserved-mutation-state.mjs +96 -0
  34. package/local-preserved-mutation-wire.mjs +49 -0
  35. package/local-scope-import-expressions.mjs +80 -0
  36. package/local-scope-repository.mjs +32 -21
  37. package/local-typescript-function-reference.mjs +98 -0
  38. package/manifest.json +1 -1
  39. package/mutation-client.mjs +290 -0
  40. package/package.json +22 -3
  41. package/release-state.json +49 -30
  42. package/release.mjs +25 -0
  43. package/releases.json +12 -0
  44. package/remote-client.mjs +2 -1
  45. package/reuse-discipline-release.mjs +3 -0
  46. package/workflow-observation-client.mjs +44 -0
  47. package/workflow-observation-connection.mjs +35 -0
  48. package/workflow-observation-remote.mjs +84 -0
  49. package/workflow-observation-runner.mjs +87 -0
  50. package/workflow-observation-state.mjs +36 -0
  51. package/workflow-observation-wire.mjs +69 -0
@@ -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.53",
5
5
  "endpoint": "https://entrorove.com",
6
6
  "requestSchemaVersion": "entrorove.skill.request/2.0",
7
7
  "supportedRequestSchemaVersions": [
@@ -0,0 +1,290 @@
1
+ import path from 'node:path';
2
+ import { canonical, sha256 } from './local-integrity.mjs';
3
+ import { withStateLock } from './local-state-lock.mjs';
4
+ import { mutationId14, mutationPaths14, mutationTarget14, mutationFail14, privateMutationDirectory14,
5
+ readMutationJson14, atomicMutationJson14 } from './local-mutation-files.mjs';
6
+ import { preparePreservedMutationSigner, preservedMutationLocation } from './local-preserved-mutation-signer.mjs';
7
+ import { snapshotPlan14, snapshotVerify14 } from './local-mutation-snapshot.mjs';
8
+ import { attestMutationSnapshot14 } from './local-mutation-binding.mjs';
9
+ import { issueMutationPass14 } from './local-mutation-authority.mjs';
10
+ import { readStoredMutationPass14 } from './local-mutation-pass-store.mjs';
11
+ import { mutationChallenge14, mutationPass14 } from './local-mutation-wire.mjs';
12
+ import { buildInvocation, invokeRemoteResult } from './remote-client.mjs';
13
+ import { parseTransportResponse, requestTransport, trustedOrigin } from './remote-transport.mjs';
14
+ import { acknowledgeAutomaticFeedback, verifyAutomaticFeedback, queryAutomaticFeedback } from './automatic-feedback-client.mjs';
15
+ import { ideClientHeaders } from './ide-presentation.mjs';
16
+ import { appendInvocation, readInvocations } from './local-invocations.mjs';
17
+ import { bootstrapCloudMutationAuthority } from './cloud-mutation-authority.mjs';
18
+ import { attestMutationConsumption14, preservedMutationConsumptionState } from './local-mutation-consumption.mjs';
19
+
20
+ const schema = 'entrorove.mutation-connection-state/1.0';
21
+ const fields = ['taskId', 'flowId', 'stepId', 'leaseId', 'generation', 'runnerId', 'paths'];
22
+ const fail = message => mutationFail14('ENTROROVE_MUTATION_CONNECTION_BINDING', message);
23
+ const unknown = requestId => Object.assign(new Error(`Mutation connection outcome is unknown; recover original requestId=${requestId} without repeating its POST`),
24
+ { outcomeUnknown: true, requestId });
25
+ const notSent = requestId => Object.assign(new Error('Mutation operation was not transmitted; repair its local or discovery prerequisite and reuse the original input'),
26
+ { code: 'ENTROROVE_MUTATION_CONNECTION_NOT_SENT', requestId, knownBeforeTransmission: true });
27
+ const save = (location, value) => atomicMutationJson14(location.file, value, { privateFrom: location.stateRoot, replace: true });
28
+ function inputIdentity(input) {
29
+ if (!input || typeof input !== 'object' || Array.isArray(input)
30
+ || canonical(Object.keys(input).sort()) !== canonical([...fields, ...['previousPassId'].filter(field => Object.hasOwn(input, field))].sort())
31
+ || !Number.isSafeInteger(input.generation) || input.generation < 1) {
32
+ mutationFail14('ENTROROVE_MUTATION_CONNECTION_INPUT', 'Mutation connection requires its exact original task, flow, step, lease, generation, runner and existing paths');
33
+ }
34
+ for (const field of fields.filter(field => !['generation', 'paths'].includes(field))) mutationId14(input[field], field);
35
+ if (Object.hasOwn(input, 'previousPassId')) mutationId14(input.previousPassId, 'previousPassId');
36
+ return { ...input, paths: mutationPaths14(input.paths) };
37
+ }
38
+ async function readConnection(location, identity) {
39
+ let value;
40
+ try { value = await readMutationJson14(location.file); }
41
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
42
+ if (!value) return { schemaVersion: schema, identity };
43
+ if (value.schemaVersion !== schema || canonical(value.identity) !== canonical(identity)
44
+ || value.pending && (value.pending.requestId !== buildInvocation(value.pending.operation, value.pending.input).requestId
45
+ || !['mutation-snapshot-challenge', 'mutation-pass-issue', 'mutation-pass-settle'].includes(value.pending.operation))) {
46
+ fail('Mutation connection journal differs from the original repository, request or physical key');
47
+ }
48
+ return value;
49
+ }
50
+ async function recoverRemote(pending, dependencies) {
51
+ try {
52
+ const token = await dependencies.readCredential(dependencies.home), origin = trustedOrigin(dependencies.apiOrigin());
53
+ const response = await requestTransport(origin, `/api/skill/status/${pending.requestId}`, { headers: { authorization: `Bearer ${token}`, ...ideClientHeaders() } });
54
+ const result = await parseTransportResponse(response, 'original mutation connection status');
55
+ if (!response.ok || result?.output?.requestId !== pending.requestId || ['running', 'pending'].includes(result.output.status)) throw unknown(pending.requestId);
56
+ result.feedback ??= await queryAutomaticFeedback(pending.requestId, dependencies);
57
+ verifyAutomaticFeedback(result.feedback, pending.requestId, pending.requestId);
58
+ result.feedback = await acknowledgeAutomaticFeedback(result.feedback, origin, token, ideClientHeaders());
59
+ await appendInvocation({ kind: 'remote', operation: 'status', requestId: pending.requestId, event: 'status', usage: result.usage ?? [] }, dependencies.home);
60
+ return { response, result };
61
+ } catch { throw unknown(pending.requestId); }
62
+ }
63
+ async function knownNotSent(error, pending, home) {
64
+ if (error?.requestId && error.requestId !== pending.requestId || error?.retryableBeforeTransmission === true) return true;
65
+ try {
66
+ const records = (await readInvocations(home)).filter(value => value.kind === 'remote' && value.requestId === pending.requestId);
67
+ return !records.length || records.at(-1).event === 'not-sent';
68
+ } catch (cause) { return cause.code === 'ENOENT'; }
69
+ }
70
+ async function remote(stage, operation, input, state, location, dependencies) {
71
+ const requestId = buildInvocation(operation, input).requestId;
72
+ let response, result;
73
+ if (state.pending) {
74
+ if (state.pending.stage !== stage || state.pending.operation !== operation || canonical(state.pending.input) !== canonical(input)) {
75
+ fail('Recover the original mutation request before another operation or input');
76
+ }
77
+ ({ response, result } = await recoverRemote(state.pending, dependencies));
78
+ } else {
79
+ state.pending = { stage, operation, input, requestId };
80
+ try { await save(location, state); } catch { throw notSent(requestId); }
81
+ try { ({ response, result } = await invokeRemoteResult({ operation, input }, dependencies)); }
82
+ catch (error) {
83
+ if (await knownNotSent(error, state.pending, dependencies.home)) {
84
+ delete state.pending;
85
+ try { await save(location, state); } catch { throw notSent(requestId); }
86
+ throw notSent(requestId);
87
+ }
88
+ throw unknown(requestId);
89
+ }
90
+ }
91
+ delete state.pending;
92
+ if (!response.ok || !['succeeded', 'reported'].includes(result?.output?.status)) {
93
+ try { await save(location, state); } catch { throw unknown(requestId); }
94
+ throw Object.assign(new Error(result?.output?.message ?? `Mutation connection blocked at ${operation}`),
95
+ { code: result?.output?.code ?? 'ENTROROVE_MUTATION_CONNECTION_BLOCKED', output: result?.output, requestId });
96
+ }
97
+ state[stage] = result.output;
98
+ if (result.feedback) state.feedback = result.feedback;
99
+ try { await save(location, state); } catch { throw unknown(requestId); }
100
+ return result.output;
101
+ }
102
+ function challengeBinding(challenge, identity, mutator, options) {
103
+ mutationChallenge14(challenge, options);
104
+ if ([...fields, 'previousPassId'].some(field => canonical(challenge[field]) !== canonical(identity[field])) || challenge.repositoryDigest !== sha256(identity.repositoryRoot).slice(0, 32)
105
+ || challenge.newFilePaths.length || !challenge.writeDelegation
106
+ || challenge.writeDelegation.mutationKeyId !== mutator.mutationKeyId || challenge.writeDelegation.mutationPublicKey !== mutator.mutationPublicKey) {
107
+ fail('Mutation challenge must retain the original task, lease, physical repository, paths and separate M signer');
108
+ }
109
+ }
110
+ function passBinding(pass, challenge, attestation, options) {
111
+ mutationPass14(pass, options);
112
+ if (pass.challengeId !== challenge.challengeId || pass.challengeDigest !== sha256(canonical(challenge))
113
+ || pass.attestationDigest !== sha256(canonical(attestation)) || pass.snapshotDigest !== attestation.manifest.snapshotDigest
114
+ || [...fields, 'previousPassId'].some(field => canonical(pass[field]) !== canonical(challenge[field]))
115
+ || canonical(pass.writeDelegation) !== canonical(challenge.writeDelegation)) fail('Mutation pass differs from its original signed challenge or actual M snapshot');
116
+ }
117
+ async function snapshot(input, challenge, state, location) {
118
+ if (!state.planned) {
119
+ if (state.planStarted) {
120
+ const recovered = await snapshotVerify14(input);
121
+ state.planned = { snapshotId: recovered.receipt.snapshotId, snapshotDigest: recovered.receipt.snapshotDigest };
122
+ } else {
123
+ state.planStarted = true; await save(location, state);
124
+ try { state.planned = await snapshotPlan14({ ...input, paths: challenge.paths, contractDigest: challenge.scopeContractDigest }); }
125
+ catch (error) { delete state.planStarted; await save(location, state); throw error; }
126
+ }
127
+ await save(location, state);
128
+ }
129
+ const verified = await snapshotVerify14(input);
130
+ if (verified.verified !== true || verified.receipt.snapshotDigest !== state.planned.snapshotDigest
131
+ || canonical(verified.receipt.paths) !== canonical(challenge.paths) || verified.receipt.contractDigest !== challenge.scopeContractDigest) {
132
+ fail('Original mutation snapshot copies or paths failed their actual verification');
133
+ }
134
+ return verified;
135
+ }
136
+ async function authorizeCachedConnection(identity, pass, dependencies) {
137
+ const token = await dependencies.readCredential(dependencies.home), origin = trustedOrigin(dependencies.apiOrigin());
138
+ let response, status;
139
+ try {
140
+ response = await requestTransport(origin, `/api/skill/flows/${identity.flowId}/status`, {
141
+ headers: { authorization: `Bearer ${token}`, ...ideClientHeaders() },
142
+ });
143
+ status = await parseTransportResponse(response, 'original mutation flow status');
144
+ } catch {
145
+ mutationFail14('ENTROROVE_MUTATION_CONNECTION_AUTHORIZATION', 'Current account could not confirm the original active mutation flow');
146
+ }
147
+ if (!response.ok || status?.schemaVersion !== 'entrorove.flow-status/1.0' || status.flow?.flowId !== identity.flowId
148
+ || status.flow.ownerId !== pass.ownerId || status.flow.stepId !== identity.stepId || status.flow.status !== 'running'
149
+ || !['execute', 'merge', 'deliver'].includes(status.workflow?.phase) || status.workflow.terminalAt
150
+ || ['pending', 'declined'].includes(status.workflow.authorization)) {
151
+ mutationFail14('ENTROROVE_MUTATION_CONNECTION_AUTHORIZATION', 'Current account must retain the original owned active flow and business step');
152
+ }
153
+ }
154
+ async function connect(identity, local, mutator, location, dependencies) {
155
+ const state = await readConnection(location, identity);
156
+ const challenged = state.challenged ?? await remote('challenged', 'mutation-snapshot-challenge',
157
+ { ...Object.fromEntries(fields.map(field => [field, identity[field]])),
158
+ ...(identity.previousPassId ? { previousPassId: identity.previousPassId } : {}), mutationPublicKey: mutator.mutationPublicKey }, state, location, dependencies);
159
+ const challenge = challenged.challenge;
160
+ challengeBinding(challenge, identity, mutator, { live: !state.gatePassPath });
161
+ const input = { ...local, snapshotId: challenge.snapshotId };
162
+ if (state.gatePassPath) {
163
+ passBinding(state.issued.pass, challenge, state.attestation, { live: false });
164
+ const stored = await readStoredMutationPass14({ ...input, gatePassPath: state.gatePassPath }, { live: false });
165
+ if (canonical(stored) !== canonical(state.issued.pass)) fail('Installed mutation pass differs from its original connection');
166
+ await authorizeCachedConnection(identity, stored, dependencies);
167
+ const consumption = await preservedMutationConsumptionState({ ...input, gatePassPath: state.gatePassPath });
168
+ if (consumption.complete) return { schemaVersion: 'entrorove.mutation-connection/1.0', status: 'blocked', flowId: identity.flowId,
169
+ code: 'ENTROROVE_MUTATION_SETTLEMENT_REQUIRED', gatePassPath: state.gatePassPath,
170
+ message: 'Original physical writes are consumed; settle their actual M receipt with fresh S evidence before requesting the next restricted pass',
171
+ localAction: { command: 'npx @entrorove/skill mutation-settle <absolute repositoryRoot>',
172
+ input: { taskId: stored.taskId, flowId: stored.flowId, stepId: stored.stepId, gatePassPath: state.gatePassPath } } };
173
+ passBinding(state.issued.pass, challenge, state.attestation);
174
+ return connected(identity, state);
175
+ }
176
+ await snapshot(input, challenge, state, location);
177
+ if (!state.attestation) {
178
+ const observed = await attestMutationSnapshot14({ ...input, challenge, signerConfigPath: mutator.signerConfigPath });
179
+ if (observed.verified !== true || observed.attestation.keyId !== mutator.mutationKeyId) fail('Actual mutation snapshot was not signed by its separate M key');
180
+ state.attestation = observed.attestation; await save(location, state);
181
+ }
182
+ const issued = state.issued ?? await remote('issued', 'mutation-pass-issue',
183
+ { challengeId: challenge.challengeId, attestation: state.attestation }, state, location, dependencies);
184
+ passBinding(issued.pass, challenge, state.attestation);
185
+ await authorizeCachedConnection(identity, issued.pass, dependencies);
186
+ const installed = await issueMutationPass14({ ...input, mutationPass: issued.pass });
187
+ if (canonical(installed.pass) !== canonical(issued.pass) || typeof installed.gatePassPath !== 'string') fail('Mutation gate did not install its exact original server pass');
188
+ state.gatePassPath = installed.gatePassPath; await save(location, state);
189
+ return connected(identity, state);
190
+ }
191
+ function connected(identity, state) {
192
+ return { schemaVersion: 'entrorove.mutation-connection/1.0', status: 'succeeded', taskId: identity.taskId,
193
+ flowId: identity.flowId, stepId: identity.stepId, gatePassPath: state.gatePassPath, pass: state.issued.pass,
194
+ purpose: 'preserved-existing-modify', grantsExecutionAuthority: false, grantsValidatorAuthority: false,
195
+ ...(state.feedback ? { feedback: state.feedback } : {}) };
196
+ }
197
+ export async function connectMutation(repositoryRoot, input, dependencies) {
198
+ const original = inputIdentity(input);
199
+ const local = { repositoryRoot, home: dependencies.home, taskId: original.taskId, flowId: original.flowId, stepId: original.stepId };
200
+ const location = await preservedMutationLocation(local);
201
+ local.repositoryRoot = location.root;
202
+ for (const relative of original.paths) {
203
+ const target = await mutationTarget14(location.root, relative, true);
204
+ if (!target.exists || !target.stat.isFile()) fail('Mutation connection permits only concrete existing files');
205
+ }
206
+ await bootstrapCloudMutationAuthority(dependencies);
207
+ const mutator = await preparePreservedMutationSigner(local);
208
+ const identity = { ...original, repositoryRoot: location.root, apiOrigin: trustedOrigin(dependencies.apiOrigin()), mutationKeyId: mutator.mutationKeyId };
209
+ const directory = path.join(location.directory, 'connections'); await privateMutationDirectory14(directory, { privateFrom: location.stateRoot });
210
+ const file = path.join(directory, `${sha256(canonical(identity))}.json`), journal = { ...location, file };
211
+ return withStateLock(file, () => connect(identity, local, mutator, journal, dependencies));
212
+ }
213
+ function settlementBinding(output, pass, receipt, expectedSettlementId) {
214
+ if (output?.settled !== true || typeof output.settlementId !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(output.settlementId)
215
+ || expectedSettlementId !== undefined && output.settlementId !== expectedSettlementId
216
+ || output.passId !== pass.passId || output.consumptionReceiptDigest !== sha256(canonical(receipt))
217
+ || output.claimsVerifiedExecution !== false || output.claimsCompletion !== false) fail('Mutation settlement must preserve its original actual M receipt without claiming independent validation or completion');
218
+ }
219
+ async function settleMutation(repositoryRoot, input, dependencies) {
220
+ const required = ['flowId', 'stepId', 'gatePassPath'], optional = ['taskId'].filter(field => Object.hasOwn(input ?? {}, field));
221
+ if (!input || canonical(Object.keys(input).sort()) !== canonical([...required, ...optional].sort())
222
+ || !path.isAbsolute(input.gatePassPath ?? '')) fail('Mutation settlement requires the exact original stored pass and task step');
223
+ for (const field of ['flowId', 'stepId', ...optional]) mutationId14(input[field], field);
224
+ await bootstrapCloudMutationAuthority(dependencies);
225
+ const local = { ...input, repositoryRoot, home: dependencies.home }, original = await preservedMutationConsumptionState(local);
226
+ const { pass, binding } = original;
227
+ await authorizeCachedConnection({ flowId: pass.flowId, stepId: pass.stepId }, pass, dependencies);
228
+ const directory = path.join(binding.directory, 'settlements'); await privateMutationDirectory14(directory, { privateFrom: binding.stateRoot });
229
+ const location = { ...binding, file: path.join(directory, `${pass.passId}.json`) };
230
+ const identity = { flowId: pass.flowId, stepId: pass.stepId, passDigest: sha256(canonical(pass)),
231
+ repositoryRoot: binding.root, apiOrigin: trustedOrigin(dependencies.apiOrigin()), mutationKeyId: binding.binding.keyId };
232
+ return withStateLock(location.file, async () => {
233
+ const state = await readConnection(location, identity);
234
+ if (!state.settled && !state.pending && (!state.consumptionReceipt || state.rejection?.effects === 'none'
235
+ && state.rejection.code === 'ENTROROVE_MUTATION_CONSUMPTION_EXPIRED')) {
236
+ const observed = await attestMutationConsumption14(local, { allowExpiredReplay: true, renewExpired: state.rejection?.effects === 'none'
237
+ && state.rejection.code === 'ENTROROVE_MUTATION_CONSUMPTION_EXPIRED' });
238
+ state.consumptionReceipt = observed.consumptionReceipt; delete state.rejection; await save(location, state);
239
+ }
240
+ if (!state.consumptionReceipt) fail('Original mutation settlement receipt is missing; recover its original request without signing another');
241
+ let output = state.settled;
242
+ if (!output) {
243
+ try { output = await remote('settled', 'mutation-pass-settle', { passId: pass.passId, consumptionReceipt: state.consumptionReceipt }, state, location, dependencies); }
244
+ catch (error) {
245
+ if (error.output?.effects === 'none') { state.rejection = { effects: 'none', code: error.output.code }; await save(location, state); }
246
+ throw error;
247
+ }
248
+ }
249
+ settlementBinding(output, pass, state.consumptionReceipt, state.settlementId);
250
+ if (state.settlementId === undefined) { state.settlementId = output.settlementId; await save(location, state); }
251
+ return { schemaVersion: 'entrorove.mutation-settlement/1.0', status: 'reported', settled: true,
252
+ flowId: pass.flowId, taskId: pass.taskId, passId: pass.passId, settlementId: output.settlementId,
253
+ consumptionReceiptDigest: output.consumptionReceiptDigest,
254
+ claimsVerifiedExecution: false, claimsCompletion: false, ...(state.feedback ? { feedback: state.feedback } : {}),
255
+ observationAction: { command: 'npx @entrorove/skill workflow-observe-step <absolute repositoryRoot>', independentValidation: 'pending',
256
+ input: { taskId: pass.taskId, flowId: pass.flowId, stepId: pass.stepId, leaseId: pass.leaseId,
257
+ generation: pass.generation, settlementId: output.settlementId } },
258
+ localAction: { command: 'npx @entrorove/skill mutation-connect <absolute repositoryRoot>',
259
+ input: { ...Object.fromEntries(fields.map(field => [field, pass[field]])), previousPassId: pass.passId } } };
260
+ });
261
+ }
262
+ async function runMutationClient(operation, repositoryRoot, input, dependencies, work) {
263
+ let preflight = true;
264
+ try {
265
+ await dependencies.install();
266
+ if (!await dependencies.agreementAccepted(dependencies.home)) return dependencies.agreementRequired(dependencies.home);
267
+ preflight = false;
268
+ return await work(repositoryRoot, input, dependencies);
269
+ }
270
+ catch (error) {
271
+ const requestId = /^[a-f0-9]{64}$/.test(error?.requestId ?? '') ? error.requestId : undefined;
272
+ if (error?.outcomeUnknown === true && requestId) return {
273
+ schemaVersion: 'entrorove.mutation-connection/1.0', status: 'blocked', flowId: input?.flowId, requestId,
274
+ code: 'ENTROROVE_MUTATION_CONNECTION_OUTCOME_UNKNOWN', outcomeUnknown: true, recoverable: true,
275
+ message: `Original mutation request outcome is unknown; rerun ${operation} with the same input to recover only its original GET, without repeating its POST`,
276
+ nextAction: { operation, requestId, method: 'GET', reuseOriginalInput: true, repeatPost: false },
277
+ };
278
+ const typed = typeof error?.code === 'string' && error.code.startsWith('ENTROROVE_');
279
+ const knownBeforeTransmission = preflight || error?.knownBeforeTransmission === true;
280
+ return { schemaVersion: 'entrorove.mutation-connection/1.0', status: 'blocked', flowId: input?.flowId,
281
+ code: typed ? error.code : 'ENTROROVE_MUTATION_CONNECTION_LOCAL_PREREQUISITE',
282
+ message: typed ? error.message : 'Mutation connection stopped at a local prerequisite; preserve its journal and repair before reusing the original input',
283
+ ...(requestId ? { requestId } : {}), ...(knownBeforeTransmission ? { knownBeforeTransmission: true } : {}),
284
+ recovery: { operation, reuseOriginalInput: true, preserveJournal: true,
285
+ ...(knownBeforeTransmission ? { remoteAttemptedThisRun: false } : {}) },
286
+ ...(error?.output ? { output: error.output } : {}) };
287
+ }
288
+ }
289
+ export const runMutationConnect = (repositoryRoot, input, dependencies) => runMutationClient('mutation-connect', repositoryRoot, input, dependencies, connectMutation);
290
+ export const runMutationSettle = (repositoryRoot, input, dependencies) => runMutationClient('mutation-settle', repositoryRoot, input, dependencies, settleMutation);