@entrorove/skill 0.1.52 → 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 (45) hide show
  1. package/CAPABILITY-MAP.md +14 -2
  2. package/CHANGELOG.md +7 -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-gate.mjs +3 -1
  21. package/local-error.mjs +3 -0
  22. package/local-execution.mjs +1 -1
  23. package/local-mutation-authority.mjs +23 -5
  24. package/local-mutation-batch.mjs +1 -0
  25. package/local-mutation-binding.mjs +14 -5
  26. package/local-mutation-consumption.mjs +93 -0
  27. package/local-mutation-pass-store.mjs +6 -4
  28. package/local-mutation-wire.mjs +21 -4
  29. package/local-preserved-mutation-protection.mjs +51 -0
  30. package/local-preserved-mutation-signer.mjs +81 -0
  31. package/local-preserved-mutation-state.mjs +96 -0
  32. package/local-preserved-mutation-wire.mjs +49 -0
  33. package/manifest.json +1 -1
  34. package/mutation-client.mjs +290 -0
  35. package/package.json +19 -2
  36. package/release-state.json +43 -26
  37. package/release.mjs +25 -0
  38. package/releases.json +6 -0
  39. package/remote-client.mjs +2 -1
  40. package/workflow-observation-client.mjs +44 -0
  41. package/workflow-observation-connection.mjs +35 -0
  42. package/workflow-observation-remote.mjs +84 -0
  43. package/workflow-observation-runner.mjs +87 -0
  44. package/workflow-observation-state.mjs +36 -0
  45. package/workflow-observation-wire.mjs +69 -0
@@ -0,0 +1,54 @@
1
+ import { fork } from 'node:child_process';
2
+ import { performance } from 'node:perf_hooks';
3
+
4
+ const SUPERVISOR = new URL('./local-approved-supervisor.mjs', import.meta.url);
5
+ const MESSAGE_SCHEMA = 'entrorove.approved-process-supervision/1.0';
6
+ const RUNNER_SCHEMA = 'entrorove.validator-local-runner/1.0';
7
+
8
+ function launchInput(authority) {
9
+ const now = Date.now(), policy = authority.policy;
10
+ const leaseDeadline = authority.executionDeadlineAt === undefined ? Infinity : Date.parse(authority.executionDeadlineAt);
11
+ const startDeadlineAt = authority.startDeadlineAt === undefined ? null : Date.parse(authority.startDeadlineAt);
12
+ if ((!Number.isFinite(leaseDeadline) && leaseDeadline !== Infinity) || leaseDeadline <= now
13
+ || (startDeadlineAt !== null && (!Number.isFinite(startDeadlineAt) || startDeadlineAt <= now))) {
14
+ throw Object.assign(new Error('workflow execution lease expired before process launch'), { code: 'ENTROROVE_WORKFLOW_EXPIRED' });
15
+ }
16
+ return { schemaVersion: MESSAGE_SCHEMA, root: authority.root, executable: policy.executable, args: policy.args,
17
+ environment: policy.environment, timeoutMs: policy.timeoutMs, maxOutputBytes: policy.maxOutputBytes,
18
+ objectPlan: Boolean(authority.objectPlan), deadlineAt: Math.min(now + policy.timeoutMs, leaseDeadline), startDeadlineAt,
19
+ leaseDeadline: leaseDeadline <= now + policy.timeoutMs };
20
+ }
21
+
22
+ function supervisionError(code, message, effects = 'unknown') {
23
+ return Object.assign(new Error(message), { code,
24
+ execution: { schemaVersion: RUNNER_SCHEMA, status: 'failed', evidence: [], effects } });
25
+ }
26
+
27
+ export async function runApprovedProcess(authority) {
28
+ const started = performance.now(), input = launchInput(authority);
29
+ return new Promise((resolve, reject) => {
30
+ let finished = false;
31
+ const supervisor = fork(SUPERVISOR, [], { execPath: process.execPath, execArgv: [], env: {}, detached: true,
32
+ stdio: ['ignore', 'ignore', 'ignore', 'ipc'], serialization: 'json' });
33
+ const finish = (error, result) => {
34
+ if (finished) return; finished = true;
35
+ if (supervisor.connected) supervisor.disconnect();
36
+ if (error) reject(error); else resolve({ ...result, durationMs: Math.max(0, Math.round(performance.now() - started)) });
37
+ };
38
+ supervisor.on('message', message => {
39
+ if (message?.schemaVersion !== MESSAGE_SCHEMA || message.type !== 'result') return;
40
+ if (message.error) {
41
+ const error = Object.assign(new Error(message.error.message), { code: message.error.code });
42
+ if (message.process) error.execution = { schemaVersion: RUNNER_SCHEMA, status: 'failed', process: message.process,
43
+ evidence: [], effects: message.process.started === true ? 'unknown' : 'none' };
44
+ finish(error); return;
45
+ }
46
+ finish(null, message.process);
47
+ });
48
+ supervisor.once('error', () => finish(supervisionError('ENTROROVE_PROCESS_SUPERVISOR', 'Approved process supervision could not start', 'none')));
49
+ supervisor.once('close', () => { if (!finished) finish(supervisionError('ENTROROVE_PROCESS_SUPERVISOR', 'Approved process supervision ended without a result')); });
50
+ supervisor.send(input, error => {
51
+ if (error) finish(supervisionError('ENTROROVE_PROCESS_SUPERVISOR', 'Approved process supervision could not receive its frozen command', 'none'));
52
+ });
53
+ });
54
+ }
@@ -0,0 +1,99 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import path from 'node:path';
4
+ import { performance } from 'node:perf_hooks';
5
+
6
+ const MESSAGE_SCHEMA = 'entrorove.approved-process-supervision/1.0';
7
+ const RUNNER_SCHEMA = 'entrorove.validator-local-runner/1.0';
8
+ const FIELDS = ['schemaVersion', 'root', 'executable', 'args', 'environment', 'timeoutMs', 'maxOutputBytes', 'objectPlan', 'deadlineAt', 'startDeadlineAt', 'leaseDeadline'];
9
+ const ENV = /^[A-Za-z_][A-Za-z0-9_]*$/;
10
+
11
+ function validInput(value) {
12
+ return value && typeof value === 'object' && !Array.isArray(value)
13
+ && Object.keys(value).sort().join('|') === [...FIELDS].sort().join('|') && value.schemaVersion === MESSAGE_SCHEMA
14
+ && typeof value.root === 'string' && path.isAbsolute(value.root) && !value.root.includes('\0')
15
+ && typeof value.executable === 'string' && path.isAbsolute(value.executable) && !value.executable.includes('\0')
16
+ && Array.isArray(value.args) && value.args.length <= 128
17
+ && value.args.every(item => typeof item === 'string' && !item.includes('\0') && item.length <= 4096)
18
+ && value.environment && typeof value.environment === 'object' && !Array.isArray(value.environment)
19
+ && Object.keys(value.environment).length <= 64
20
+ && Object.entries(value.environment).every(([key, item]) => ENV.test(key) && typeof item === 'string' && !item.includes('\0') && item.length <= 16_384)
21
+ && Number.isSafeInteger(value.timeoutMs) && value.timeoutMs > 0 && value.timeoutMs <= 300_000
22
+ && Number.isSafeInteger(value.maxOutputBytes) && value.maxOutputBytes > 0 && value.maxOutputBytes <= 1_048_576
23
+ && typeof value.objectPlan === 'boolean' && typeof value.leaseDeadline === 'boolean'
24
+ && (value.startDeadlineAt === null || Number.isSafeInteger(value.startDeadlineAt))
25
+ && Number.isSafeInteger(value.deadlineAt) && value.deadlineAt <= Date.now() + value.timeoutMs;
26
+ }
27
+
28
+ function resultMessage(processResult, error) {
29
+ return { schemaVersion: MESSAGE_SCHEMA, type: 'result', process: processResult,
30
+ ...(error ? { error: { code: error.code ?? 'ENTROROVE_PROCESS_SUPERVISOR', message: String(error.message).slice(0, 1024) } } : {}) };
31
+ }
32
+
33
+ function deliver(value) {
34
+ if (!process.connected) return;
35
+ process.send(value, () => { if (process.connected) process.disconnect(); });
36
+ }
37
+
38
+ function assertLaunchable(input) {
39
+ if (!process.connected || input.deadlineAt <= Date.now() || input.startDeadlineAt !== null && input.startDeadlineAt <= Date.now()) {
40
+ throw Object.assign(new Error('workflow execution lease expired before process launch'), { code: 'ENTROROVE_WORKFLOW_EXPIRED' });
41
+ }
42
+ }
43
+
44
+ function supervise(input) {
45
+ const started = performance.now(), stdout = [], stderr = [], stdoutHash = createHash('sha256'), stderrHash = createHash('sha256');
46
+ let bytes = 0, dropped = 0, timedOut = false, terminating = false, spawned = false, closed = false;
47
+ let processStartedAt = null, spawnError = null, killTimer;
48
+ assertLaunchable(input);
49
+ const child = spawn(input.executable, input.args, { cwd: input.root, env: { ...input.environment },
50
+ shell: false, detached: true, stdio: ['ignore', 'pipe', 'pipe'] });
51
+ const signalGroup = signal => { if (!closed && child.pid) { try { process.kill(-child.pid, signal); } catch {} } };
52
+ const terminate = () => {
53
+ if (terminating || closed) return; terminating = true; signalGroup('SIGTERM');
54
+ killTimer = setTimeout(() => signalGroup('SIGKILL'), 250);
55
+ };
56
+ const consume = (target, hash, chunk) => {
57
+ const data = Buffer.from(chunk); hash.update(data);
58
+ const keep = Math.max(0, Math.min(data.length, input.maxOutputBytes - bytes));
59
+ if (keep) target.push(data.subarray(0, keep)); bytes += keep; dropped += data.length - keep;
60
+ if (dropped) terminate();
61
+ };
62
+ const timer = setTimeout(() => { timedOut = true; terminate(); }, Math.max(1, input.deadlineAt - Date.now()));
63
+ process.once('disconnect', terminate); process.once('SIGTERM', terminate); process.once('SIGINT', terminate);
64
+ child.stdout.on('data', chunk => consume(stdout, stdoutHash, chunk)); child.stderr.on('data', chunk => consume(stderr, stderrHash, chunk));
65
+ child.once('spawn', () => { spawned = true; processStartedAt = new Date().toISOString(); });
66
+ child.once('error', error => { spawnError = error; terminate(); });
67
+ child.once('exit', () => signalGroup('SIGKILL'));
68
+ child.once('close', (exitCode, signal) => {
69
+ closed = true; clearTimeout(timer); clearTimeout(killTimer);
70
+ process.removeListener('disconnect', terminate); process.removeListener('SIGTERM', terminate); process.removeListener('SIGINT', terminate);
71
+ const processResult = { schemaVersion: RUNNER_SCHEMA,
72
+ status: exitCode === 0 && signal === null && !timedOut && !dropped && !spawnError ? 'succeeded' : 'failed',
73
+ exitCode, signal, timedOut, outputLimitExceeded: dropped > 0, durationMs: Math.max(0, Math.round(performance.now() - started)),
74
+ stdout: Buffer.concat(stdout).toString('utf8'), stderr: Buffer.concat(stderr).toString('utf8'),
75
+ stdoutSha256: stdoutHash.digest('hex'), stderrSha256: stderrHash.digest('hex'), capturedBytes: bytes, droppedBytes: dropped,
76
+ ...(input.objectPlan ? { started: spawned, startedAt: processStartedAt, finishedAt: new Date().toISOString(),
77
+ executionDeadlineExceeded: timedOut && input.leaseDeadline } : {}) };
78
+ deliver(resultMessage(processResult, spawnError));
79
+ });
80
+ if (!process.connected) terminate();
81
+ }
82
+
83
+ function main() {
84
+ if (process.argv.length !== 2 || typeof process.send !== 'function' || !process.connected) { process.exitCode = 2; return; }
85
+ const startup = setTimeout(() => { if (process.connected) process.disconnect(); }, 5_000);
86
+ const disconnect = () => clearTimeout(startup); process.once('disconnect', disconnect);
87
+ process.once('message', input => {
88
+ clearTimeout(startup); process.removeListener('disconnect', disconnect);
89
+ if (!process.connected) return;
90
+ if (!validInput(input)) {
91
+ deliver(resultMessage(null, { code: 'ENTROROVE_PROCESS_SUPERVISOR', message: 'Approved process supervision requires one bounded frozen command' })); return;
92
+ }
93
+ try { supervise(input); }
94
+ catch (error) { deliver(resultMessage(null, error.code === 'ENTROROVE_WORKFLOW_EXPIRED' ? error
95
+ : { code: 'ENTROROVE_PROCESS_SUPERVISOR', message: 'Approved process supervision could not launch its frozen command' })); }
96
+ });
97
+ }
98
+
99
+ main();
package/local-cli.mjs CHANGED
@@ -28,6 +28,10 @@ import { DISCIPLINE_LOCAL_OPERATIONS } from './local-discipline-operations.mjs';
28
28
  import { assertIdeExecution, settleBreakGlassSession } from './ide-session.mjs';
29
29
  import { runManagedWorkflowLocalStep } from './workflow-local-client.mjs';
30
30
  import { localErrorEnvelope } from './local-error.mjs';
31
+ import { preparePreservedMutationSigner } from './local-preserved-mutation-signer.mjs';
32
+ import { hydrateCloudMutationAuthority } from './cloud-mutation-authority.mjs';
33
+ import { attestMutationConsumption14 } from './local-mutation-consumption.mjs';
34
+ import { apiUrl } from './remote-client.mjs';
31
35
 
32
36
  async function stdinJson() {
33
37
  let raw = '';
@@ -40,6 +44,8 @@ async function stdinJson() {
40
44
  }
41
45
 
42
46
  const operations = {
47
+ 'mutation-consumption-attest': attestMutationConsumption14,
48
+ 'mutation-delegate-prepare': preparePreservedMutationSigner,
43
49
  ...DISCIPLINE_LOCAL_OPERATIONS,
44
50
  'snapshot-plan': snapshotPlan, 'snapshot-verify': snapshotVerify, 'mutation-snapshot-attest': attestMutationSnapshot14, 'mutate-gate': mutateGate,
45
51
  'gate-issue': issueMutationPass, 'gate-verify': verifyMutationPass, 'guarded-write': guardedWrite,
@@ -79,7 +85,11 @@ function capabilitiesFor(group) {
79
85
  }
80
86
 
81
87
  async function checkedLocalInput(operation, input) {
82
- try { return validateLocalInput(operation, input); }
88
+ try {
89
+ const validated = validateLocalInput(operation, input);
90
+ if (['mutation-snapshot-attest', 'mutation-consumption-attest', 'workflow-run-step', 'mutate-gate', 'gate-issue', 'gate-verify', 'guarded-write', 'guarded-delete', 'guarded-move', 'discipline-review', 'discipline-check'].includes(operation)) await hydrateCloudMutationAuthority(input.home, new URL(apiUrl).origin);
91
+ return validated;
92
+ }
83
93
  catch (cause) {
84
94
  if (['guarded-write', 'guarded-delete', 'guarded-move', 'gate-issue', 'mutate-gate', 'mutation-snapshot-attest'].includes(operation)) await recordMutationDenied14(input, cause, operation);
85
95
  throw cause;
@@ -94,9 +104,10 @@ export async function runLocal(args = [], dependencies) {
94
104
  process.stdout.write(`${JSON.stringify(capabilitiesFor(args[1]))}\n`); return;
95
105
  }
96
106
  const handler = operations[operation];
97
- if (!handler) throw new Error(`Usage: entrorove local <${['capabilities', ...Object.keys(operations)].join('|')}> [repositoryRoot]`);
107
+ if (!handler && process.stdin.isTTY) throw new Error(`Unsupported local operation: ${operation}`);
98
108
  const input = await stdinJson();
99
109
  await assertIdeExecution(['local', ...args], input);
110
+ if (!handler) throw new Error(`Usage: entrorove local <${['capabilities', ...Object.keys(operations)].join('|')}> [repositoryRoot]`);
100
111
  if (args[1] && input.repositoryRoot === undefined) input.repositoryRoot = args[1];
101
112
  const validated = await checkedLocalInput(operation, input);
102
113
  const supplied = operation === 'workflow-run-step' || Object.hasOwn(LOCAL_CHAIN_OPERATIONS, operation) ? dependencies : undefined;
@@ -29,6 +29,8 @@ add('flow-cancel|flow-abandon', flowLifecycle, ['repositoryRoot', 'flowId']);
29
29
  add('snapshot-plan', { ...step, paths, snapshotId: identifier, contractDigest: nullable(sha256), handoffCandidate: HANDOFF_SNAPSHOT_CANDIDATE_SCHEMA }, ['repositoryRoot', 'flowId', 'stepId', 'paths']);
30
30
  add('snapshot-verify', { ...step, snapshotId: identifier }, ['repositoryRoot', 'flowId', 'stepId', 'snapshotId']);
31
31
  const gate = { ...step, taskId: identifier, mutationPass: record };
32
+ add('mutation-delegate-prepare', { ...step, taskId: identifier }, ['repositoryRoot', 'home', 'flowId']);
33
+ add('mutation-consumption-attest', { ...step, taskId: identifier, gatePassPath: text }, ['repositoryRoot', 'flowId', 'stepId', 'gatePassPath']);
32
34
  add('mutate-gate|gate-issue', gate, ['repositoryRoot', 'flowId', 'stepId', 'mutationPass']);
33
35
  add('mutation-snapshot-attest', { ...step, taskId: identifier, snapshotId: identifier, challenge: record, signerConfigPath: text },
34
36
  ['repositoryRoot', 'flowId', 'stepId', 'snapshotId', 'challenge', 'signerConfigPath']);
@@ -0,0 +1,232 @@
1
+ import ts from 'typescript';
2
+ import { parse as parseVue, parseCache } from '@vue/compiler-sfc';
3
+ import { canonical } from './local-integrity.mjs';
4
+ import { createScopeImportResolver } from './local-scope-import-expressions.mjs';
5
+ import { isDynamicFunctionReference, isTypeOnlyReference } from './local-typescript-function-reference.mjs';
6
+
7
+ const fail = () => { throw Object.assign(new Error('Preserved modification changed an existing definition, public contract, dependency, or unsupported source boundary'),
8
+ { code: 'ENTROROVE_MUTATION_DELEGATION_CHANGE' }); };
9
+ const printer = ts.createPrinter({ removeComments: true });
10
+ const print = node => node ? printer.printNode(ts.EmitHint.Unspecified, node, node.getSourceFile()) : null;
11
+ function unwrap(node) {
12
+ while (node && (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)
13
+ || ts.isSatisfiesExpression(node) || ts.isNonNullExpression(node))) node = node.expression;
14
+ return node;
15
+ }
16
+ function names(node) {
17
+ if (!node) return [];
18
+ if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node) || ts.isPrivateIdentifier(node)) return [node.text];
19
+ if (ts.isObjectBindingPattern(node) || ts.isArrayBindingPattern(node)) return node.elements.flatMap(item => ts.isBindingElement(item) ? names(item.name) : []);
20
+ return [print(node)];
21
+ }
22
+ function owner(node) {
23
+ if (node.name) return names(node.name).join(',');
24
+ if (node.parent?.name && (ts.isVariableDeclaration(node.parent) || ts.isPropertyAssignment(node.parent))) return names(node.parent.name).join(',');
25
+ return '<anonymous>';
26
+ }
27
+ function scope(node) {
28
+ const parts = [];
29
+ for (let parent = node.parent; parent && !ts.isSourceFile(parent); parent = parent.parent) {
30
+ if (ts.isFunctionLike(parent) || ts.isClassLike(parent) || ts.isModuleDeclaration(parent)) parts.unshift(`${parent.kind}:${owner(parent)}`);
31
+ }
32
+ return parts.join('/');
33
+ }
34
+ const modifiers = node => node.modifiers?.map(value => value.kind) ?? [];
35
+ function signature(node) {
36
+ return { kind: node.kind, name: node.name ? print(node.name) : null, modifiers: modifiers(node), optional: Boolean(node.questionToken),
37
+ generator: Boolean(node.asteriskToken), types: node.typeParameters?.map(print) ?? [], type: print(node.type),
38
+ parameters: node.parameters?.map(print) ?? [] };
39
+ }
40
+ function exported(node, exportNames) {
41
+ if (names(node.name).some(name => exportNames.has(name))) return true;
42
+ for (let current = node; current && !ts.isSourceFile(current); current = current.parent) {
43
+ if (current !== node && ts.isFunctionLike(current)) return false;
44
+ if (current.modifiers?.some(value => [ts.SyntaxKind.ExportKeyword, ts.SyntaxKind.DefaultKeyword].includes(value.kind))
45
+ || ts.isExportAssignment(current)) return true;
46
+ }
47
+ return false;
48
+ }
49
+ function exportedNames(source) {
50
+ const result = new Set();
51
+ for (const node of source.statements) {
52
+ if (ts.isExportDeclaration(node) && !node.moduleSpecifier && node.exportClause && ts.isNamedExports(node.exportClause)) {
53
+ for (const item of node.exportClause.elements) result.add((item.propertyName ?? item.name).text);
54
+ }
55
+ if (ts.isExportAssignment(node) && ts.isIdentifier(unwrap(node.expression))) result.add(unwrap(node.expression).text);
56
+ }
57
+ const common = node => {
58
+ if (commonJs(node)) {
59
+ const value = unwrap(node.right);
60
+ if (ts.isIdentifier(value)) result.add(value.text);
61
+ if (ts.isObjectLiteralExpression(value)) for (const property of value.properties) {
62
+ if (ts.isShorthandPropertyAssignment(property)) result.add(property.name.text);
63
+ if (ts.isPropertyAssignment(property) && ts.isIdentifier(unwrap(property.initializer))) result.add(unwrap(property.initializer).text);
64
+ }
65
+ }
66
+ ts.forEachChild(node, common);
67
+ };
68
+ common(source);
69
+ return result;
70
+ }
71
+ function isDefinition(node) {
72
+ return ts.isVariableDeclaration(node) || ts.isParameter(node) || ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)
73
+ || ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isEnumDeclaration(node) || ts.isEnumMember(node)
74
+ || ts.isPropertySignature(node) || ts.isMethodSignature(node) || ts.isPropertyDeclaration(node) || ts.isMethodDeclaration(node)
75
+ || ts.isPropertyAssignment(node) || ts.isShorthandPropertyAssignment(node) || ts.isGetAccessorDeclaration(node)
76
+ || ts.isSetAccessorDeclaration(node) || ts.isModuleDeclaration(node) || ts.isImportSpecifier(node) || ts.isImportClause(node)
77
+ || ts.isNamespaceImport(node) || ts.isImportEqualsDeclaration(node) || ts.isTypeParameterDeclaration(node);
78
+ }
79
+ const increment = (map, key) => map.set(key, (map.get(key) ?? 0) + 1);
80
+ function variableContract(node) {
81
+ const value = unwrap(node.initializer);
82
+ return { ...signature(node), binding: node.parent.flags & (ts.NodeFlags.Const | ts.NodeFlags.Let),
83
+ callable: value && ts.isFunctionLike(value) ? signature(value) : null,
84
+ alias: value && (ts.isIdentifier(value) || ts.isPropertyAccessExpression(value) || ts.isElementAccessExpression(value)) ? print(value) : null };
85
+ }
86
+ function isDependency(node) {
87
+ return ts.isImportDeclaration(node) || ts.isExportDeclaration(node) || ts.isImportEqualsDeclaration(node) || ts.isImportTypeNode(node)
88
+ || ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword
89
+ || ts.isIdentifier(node.expression) && node.expression.text === 'require');
90
+ }
91
+ function dependency(node, source, resolve) {
92
+ const call = ts.isCallExpression(node);
93
+ return { syntax: print(node), alternatives: call ? resolve(node.arguments[0]) : null,
94
+ comments: source.text.slice(node.getStart(source), node.end).match(/\/\*[\s\S]*?\*\//g) ?? [] };
95
+ }
96
+ function commonJs(node) {
97
+ return ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken
98
+ && /^(?:module\.exports|exports)(?:\.[A-Za-z_$][\w$]*|\[[\s\S]+\])?$/.test(print(node.left) ?? '');
99
+ }
100
+ function sourceChecker(source) {
101
+ const filename = `/${source.fileName.replace(/^\/+/, '')}`;
102
+ const matches = value => `/${value.replace(/^\/+/, '')}` === filename;
103
+ return ts.createProgram([filename], { noLib: true, noResolve: true, allowJs: true }, {
104
+ getSourceFile: value => matches(value) ? source : undefined, getDefaultLibFileName: () => '/none.d.ts', writeFile: () => {},
105
+ getCurrentDirectory: () => '/', getDirectories: () => [], fileExists: matches,
106
+ readFile: value => matches(value) ? source.text : undefined, getCanonicalFileName: value => value,
107
+ useCaseSensitiveFileNames: () => true, getNewLine: () => '\n',
108
+ }).getTypeChecker();
109
+ }
110
+ function exportedObject(value) {
111
+ return value.properties.map(property => {
112
+ const initializer = ts.isPropertyAssignment(property) ? unwrap(property.initializer) : property;
113
+ return { name: print(property.name), kind: property.kind,
114
+ alias: ts.isIdentifier(initializer) || ts.isPropertyAccessExpression(initializer) ? print(initializer) : null,
115
+ callable: ts.isFunctionLike(initializer) ? signature(initializer) : null };
116
+ });
117
+ }
118
+ function unknownReference(node, context) {
119
+ if (isTypeOnlyReference(node)) return false;
120
+ if (ts.isIdentifier(node) && (['eval', 'createRequire'].includes(node.text) || isDynamicFunctionReference(node, context))) return true;
121
+ if (ts.isIdentifier(node) && node.text === 'require' && !(ts.isCallExpression(node.parent) && node.parent.expression === node)
122
+ && !(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node)) return true;
123
+ if (ts.isElementAccessExpression(node) && (!ts.isStringLiteralLike(node.argumentExpression)
124
+ || ['Function', 'eval', 'require', 'createRequire'].includes(node.argumentExpression.text))) return true;
125
+ return ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression)
126
+ && ['Object', 'Reflect'].includes(node.expression.expression.text) && ['get', 'set', 'defineProperty'].includes(node.expression.name.text);
127
+ }
128
+ function scriptProjection(part) {
129
+ const kind = /\.[jt]sx$/i.test(part.path) ? ts.ScriptKind.TSX : /\.[cm]?js$/i.test(part.path) ? ts.ScriptKind.JS : ts.ScriptKind.TS;
130
+ const source = ts.createSourceFile(part.path, part.content, ts.ScriptTarget.Latest, true, kind);
131
+ if (source.parseDiagnostics.length) fail();
132
+ const result = { dependencies: [], definitions: new Map(), signatures: new Map(), public: [], unknown: new Map() };
133
+ const exports = exportedNames(source), resolve = createScopeImportResolver(source);
134
+ let visited = 0, checker;
135
+ const inferred = node => {
136
+ checker ??= sourceChecker(source);
137
+ return checker.typeToString(checker.getTypeAtLocation(node), node, ts.TypeFormatFlags.NoTruncation);
138
+ };
139
+ const visit = node => {
140
+ if (++visited > 200_000) fail();
141
+ const key = `${scope(node)}/${node.kind}:${names(node.name).join(',')}`;
142
+ if (isDependency(node)) result.dependencies.push(dependency(node, source, resolve));
143
+ if (isDefinition(node)) for (const name of names(node.name)) increment(result.definitions, `${scope(node)}/${node.kind}:${name}`);
144
+ if (ts.isClassExpression(node)) increment(result.definitions, `${scope(node)}/${node.kind}:${owner(node)}`);
145
+ if (ts.isFunctionLike(node)) increment(result.signatures, canonical({ scope: scope(node), signature: signature(node) }));
146
+ if (ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isEnumDeclaration(node)) increment(result.signatures, canonical({ key, contract: print(node) }));
147
+ if (exported(node, exports) && isDefinition(node)) {
148
+ const contract = ts.isVariableDeclaration(node) ? variableContract(node) : signature(node);
149
+ result.public.push({ key, contract });
150
+ if (ts.isFunctionLike(node) || ts.isVariableDeclaration(node) && (contract.callable || contract.alias)) {
151
+ result.public.push({ key, inferred: inferred(node) });
152
+ }
153
+ if (ts.isPropertyAssignment(node) && ['props', 'emits'].includes(names(node.name)[0])) result.public.push({ key, metadata: print(node.initializer) });
154
+ if (ts.isClassDeclaration(node)) result.public.push({ key, heritage: node.heritageClauses?.map(print) ?? [] });
155
+ }
156
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)
157
+ && ['defineProps', 'defineEmits', 'defineSlots', 'defineModel', 'withDefaults'].includes(node.expression.text)) {
158
+ result.public.push({ macro: print(node) });
159
+ }
160
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'defineExpose') {
161
+ result.public.push({ expose: node.arguments.map(value => ts.isObjectLiteralExpression(value) ? exportedObject(value) : print(value)) });
162
+ }
163
+ if (ts.isExportAssignment(node) && ![ts.SyntaxKind.ObjectLiteralExpression, ts.SyntaxKind.CallExpression,
164
+ ts.SyntaxKind.FunctionExpression, ts.SyntaxKind.ClassExpression, ts.SyntaxKind.ArrowFunction].includes(unwrap(node.expression).kind)) {
165
+ result.public.push({ defaultAlias: print(node.expression), equals: node.isExportEquals });
166
+ }
167
+ if (commonJs(node)) result.public.push({ commonJs: print(node.left), contract: ts.isFunctionLike(unwrap(node.right))
168
+ ? { signature: signature(unwrap(node.right)), inferred: inferred(unwrap(node.right)) }
169
+ : ts.isObjectLiteralExpression(unwrap(node.right)) ? exportedObject(unwrap(node.right)) : print(node.right) });
170
+ if (unknownReference(node, part.context)) increment(result.unknown, print(node));
171
+ ts.forEachChild(node, visit);
172
+ };
173
+ visit(source);
174
+ return result;
175
+ }
176
+ function templateParts(root, path) {
177
+ const parts = [], seen = new WeakSet();
178
+ const walk = node => {
179
+ if (!node || typeof node !== 'object' || seen.has(node)) return;
180
+ seen.add(node);
181
+ if (node.type === 7 && ['on', 'for'].includes(node.name) && node.exp?.content) {
182
+ parts.push({ path: `${path}.ts`, content: node.name === 'for' ? `for (${node.exp.content}) {}` : node.exp.content });
183
+ seen.add(node.exp);
184
+ }
185
+ if (node.type === 4 && !node.isStatic && node.content) parts.push({ path: `${path}.ts`, content: `void (${node.content});` });
186
+ for (const [key, child] of Object.entries(node)) if (!['loc', 'ast', 'parent', 'codegenNode'].includes(key)) {
187
+ if (Array.isArray(child)) child.forEach(walk); else walk(child);
188
+ }
189
+ };
190
+ walk(root);
191
+ return parts;
192
+ }
193
+ function parts(path, content) {
194
+ if (!/\.vue$/i.test(path)) {
195
+ if (!/\.[cm]?[jt]sx?$/i.test(path)) fail();
196
+ return { metadata: [], scripts: [{ path, content }] };
197
+ }
198
+ const parsed = parseVue(content, { filename: path, sourceMap: false, templateParseOptions: { expressionPlugins: ['typescript'] } });
199
+ parseCache.clear();
200
+ if (parsed.errors.length) fail();
201
+ const value = parsed.descriptor, scripts = [];
202
+ const metadata = [value.template?.attrs ?? null, value.styles.map(block => ({ attrs: block.attrs, content: block.content })),
203
+ value.customBlocks.map(block => ({ type: block.type, attrs: block.attrs, content: block.content }))];
204
+ if (value.template?.lang) fail();
205
+ for (const [index, block] of [value.script, value.scriptSetup].entries()) {
206
+ metadata.push(block?.attrs ?? null);
207
+ if (!block) continue;
208
+ const extension = { javascript: 'js', typescript: 'ts' }[block.lang] ?? block.lang ?? 'js';
209
+ if (!['js', 'ts', 'jsx', 'tsx'].includes(extension)) fail();
210
+ scripts.push({ path: `${path}.${index}.${extension}`, content: block.src ? `import ${JSON.stringify(block.src)};` : block.content,
211
+ context: { vueComponent: true, vueSetup: block === value.scriptSetup } });
212
+ }
213
+ scripts.push(...templateParts(value.template?.ast, path));
214
+ return { metadata, scripts };
215
+ }
216
+ function retained(before, after) {
217
+ for (const [key, count] of before) if ((after.get(key) ?? 0) < count) fail();
218
+ }
219
+ export function assertDelegatedSourceModification(input) {
220
+ const { path, before, after } = input ?? {};
221
+ if (typeof path !== 'string' || typeof before !== 'string' || typeof after !== 'string'
222
+ || Buffer.byteLength(before) > 1_048_576 || Buffer.byteLength(after) > 1_048_576) fail();
223
+ try {
224
+ const original = parts(path, before), candidate = parts(path, after);
225
+ if (canonical(original.metadata) !== canonical(candidate.metadata) || original.scripts.length !== candidate.scripts.length) fail();
226
+ for (let index = 0; index < original.scripts.length; index++) {
227
+ const a = scriptProjection(original.scripts[index]), b = scriptProjection(candidate.scripts[index]);
228
+ if (canonical(a.dependencies) !== canonical(b.dependencies) || canonical(a.public) !== canonical(b.public)) fail();
229
+ retained(a.definitions, b.definitions); retained(a.signatures, b.signatures); retained(b.unknown, a.unknown);
230
+ }
231
+ } catch { fail(); }
232
+ }
@@ -11,6 +11,7 @@ import { auditMutation14, withMutationAudit14 } from './local-mutation-audit.mjs
11
11
  import { mutationFail14, removeMutationProjectFile14, moveMutationProjectFile14 } from './local-mutation-files.mjs';
12
12
 
13
13
  function actionIntent(input, value, kind, review) {
14
+ if (value.pass.writeDelegation) mutationFail14('ENTROROVE_MUTATION_DELEGATION_CHANGE', 'Preserved mutation forbids structural file actions');
14
15
  if (!value.pass.disciplinePolicy) mutationFail14('ENTROROVE_DISCIPLINE_NEEDS_HUMAN', `${kind} needs a separately approved original change declaration`);
15
16
  const policy = disciplineApprovedPolicy(value.pass.disciplinePolicy), intent = policy.writes.find(write => write.targetPath === value.target);
16
17
  const { usageReviewDigest, ...supplied } = input.declaration ?? {};
@@ -63,6 +64,7 @@ async function perform(input, kind, original, state) {
63
64
  async function guardedAction(input, kind) {
64
65
  return withMutationAudit14(input, `guarded-${kind}`, async () => {
65
66
  const value = await checkedPass(input), pass = value.pass;
67
+ if (pass.writeDelegation) mutationFail14('ENTROROVE_MUTATION_DELEGATION_CHANGE', 'Preserved mutation forbids structural file actions');
66
68
  return withStateLock(value.locations.consumptionPath, () => withTaskHandoffWriteFence({ ...input,
67
69
  epoch: pass.epoch, passId: pass.passId, leaseId: pass.leaseId }, state => perform(input, kind, value, state)));
68
70
  });
@@ -5,6 +5,7 @@ import { disciplineDuplicateMappings, disciplineFormattingLines } from './local-
5
5
  import { disciplineRepositoryObservation, assertDisciplineRepository } from './local-discipline-repository.mjs';
6
6
  import { readDisciplineSnapshot } from './local-discipline-snapshot.mjs';
7
7
  import { readSnapshotCopy14 } from './local-mutation-snapshot.mjs';
8
+ import { assertDelegatedSourceModification } from './local-delegated-modify-analysis.mjs';
8
9
 
9
10
  const needs = message => mutationFail14('ENTROROVE_DISCIPLINE_NEEDS_HUMAN', message);
10
11
  function declaredWrite(input, pass, baseline) {
@@ -73,11 +74,12 @@ export async function disciplineWritePreflight(input, value, content) {
73
74
  const review = await readDisciplineSnapshot(snapshot), baseline = snapshot.manifest.files.find(file => file.path === target);
74
75
  await assertDisciplineRepository(input, binding.root, review, ledger);
75
76
  const intent = declaredWrite(input, pass, baseline);
77
+ const original = baseline.exists ? (await readSnapshotCopy14(input, snapshot.snapshotRoot, snapshot.manifest, target)).content.toString('utf8') : '';
78
+ if (pass.writeDelegation) assertDelegatedSourceModification({ path: target, before: original, after: content });
76
79
  const candidate = await disciplineRepositoryObservation(input, binding.root, { analyze: true, overrides: new Map([[target, content]]) });
77
80
  const removed = checkRemoved(review, candidate.analysis, target, intent, snapshot.manifest.disciplineReviewDigest, input.declaration);
78
81
  checkContracts(review, candidate.analysis, target, intent);
79
82
  const reuse = checkReuse(review, candidate.analysis, target, intent, pass.parallelPaths);
80
- const original = baseline.exists ? (await readSnapshotCopy14(input, snapshot.snapshotRoot, snapshot.manifest, target)).content.toString('utf8') : '';
81
83
  const formatting = baseline.exists ? disciplineFormattingLines(original, content, target) : [];
82
84
  if (formatting.length) mutationFail14('ENTROROVE_DISCIPLINE_FORMAT_SCOPE', `Formatting touches lines without functional changes: ${target}:${formatting.join(',')}`);
83
85
  return { reviewDigest: snapshot.manifest.disciplineReviewDigest, declaredKind: baseline.exists ? 'modify' : 'create', removed, reuse,
package/local-error.mjs CHANGED
@@ -13,6 +13,8 @@ export const LOCAL_ERROR_CODE_CATALOG = Object.freeze({
13
13
  ENTROROVE_DISCIPLINE_USAGE_UNKNOWN: Object.freeze({ disposition: 'needs-human', stability: 'stable' }),
14
14
  ENTROROVE_DISCIPLINE_REVIEW_REQUIRED: Object.freeze({ disposition: 'needs-human', stability: 'stable' }),
15
15
  ENTROROVE_DISCIPLINE_DATA_SNAPSHOT_REQUIRED: Object.freeze({ disposition: 'needs-human', stability: 'stable' }),
16
+ ENTROROVE_MUTATION_DELEGATION_CHANGE: Object.freeze({ disposition: 'terminal', stability: 'stable' }),
17
+ ENTROROVE_MUTATION_DELEGATION_INVALID: Object.freeze({ disposition: 'terminal', stability: 'stable' }),
16
18
  ENTROROVE_IDE_GUARD_REQUIRED: Object.freeze({ disposition: 'needs-human', stability: 'stable' }),
17
19
  ENTROROVE_IDE_GUARD_UNSUPPORTED: Object.freeze({ disposition: 'terminal', stability: 'stable' }),
18
20
  });
@@ -25,6 +27,7 @@ const BLOCKED_CODES = new Set([
25
27
  'ENTROROVE_GIT_UNAVAILABLE',
26
28
  'ENTROROVE_MUTATE_NOT_ACCEPTED', 'ENTROROVE_MUTATION_AUTHORITY_REQUIRED',
27
29
  'ENTROROVE_MUTATION_PASS_INVALID', 'ENTROROVE_MUTATION_PASS_EXPIRED',
30
+ 'ENTROROVE_MUTATION_DELEGATION_CHANGE', 'ENTROROVE_MUTATION_DELEGATION_INVALID',
28
31
  'ENTROROVE_BUDGET_LEASE_EXPIRED', 'ENTROROVE_BUDGET_AUTHORITY_REQUIRED',
29
32
  'ENTROROVE_SWARM_AUTHORITY_INVALID', 'ENTROROVE_SWARM_CLOUD_AUTHORITY_REQUIRED',
30
33
  'ENTROROVE_HANDOFF_FENCE', 'ENTROROVE_HANDOFF_WRITE_REVOKED',
@@ -277,7 +277,7 @@ export async function finalGate(input) {
277
277
  }
278
278
 
279
279
  const LOCAL_OPERATION_GROUPS = Object.freeze(['core', 'budget', 'scope', 'merge', 'chain', 'handoff', 'archguard', 'calctool', 'swarm', 'validator', 'formula', 'evidence']);
280
- const PASS_REQUIRED_OPERATIONS = new Set(['mutation-snapshot-attest', 'mutate-gate', 'gate-issue', 'gate-verify', 'guarded-write',
280
+ const PASS_REQUIRED_OPERATIONS = new Set(['mutation-snapshot-attest', 'mutation-consumption-attest', 'mutate-gate', 'gate-issue', 'gate-verify', 'guarded-write',
281
281
  'discipline-review', 'discipline-check', 'guarded-delete', 'guarded-move',
282
282
  'merge-verified', 'rollback', 'archguard-contract-create', 'archguard-contract-update', 'archguard-checkpoint', 'archguard-scope-request',
283
283
  'budget-handoff', 'budget-transition', 'spawn-bind', 'peer-complete', 'complete']);
@@ -14,6 +14,8 @@ import { withSwarmMutationLease } from './local-swarm-mutation.mjs';
14
14
  import { disciplineWritePreflight } from './local-discipline-gate.mjs';
15
15
  import { readDisciplineSnapshot } from './local-discipline-snapshot.mjs';
16
16
  import { assertDisciplineRepository } from './local-discipline-repository.mjs';
17
+ import { withPreservedMutationFence } from './local-preserved-mutation-state.mjs';
18
+ import { assertPreservedMutationProtection } from './local-preserved-mutation-protection.mjs';
17
19
  export { beginMutationBatch14, finishMutationBatch14, failMutationBatch14 } from './local-mutation-batch.mjs';
18
20
 
19
21
  const SCHEMA = 'entrorove.local-execution/1.0';
@@ -25,7 +27,8 @@ export async function mutateGate14(input) {
25
27
  return withMutationAudit14(input, 'mutate-gate', async () => {
26
28
  const pass = suppliedPass(input), binding = await mutationOriginalBinding14(input, pass);
27
29
  const snapshot = await mutationSnapshotBinding14(input, pass, { allSources: true });
28
- assertTaskHandoffSnapshotBinding(binding.state, pass.snapshotReceipt);
30
+ if (!pass.writeDelegation) assertTaskHandoffSnapshotBinding(binding.state, pass.snapshotReceipt);
31
+ else await assertPreservedMutationProtection(input, binding.root, pass);
29
32
  return { schemaVersion: SCHEMA, status: 'verified', allowed: true, snapshotId: snapshot.manifest.snapshotId,
30
33
  receiptDigest: pass.snapshotReceiptDigest, paths: [...pass.paths], coordinationRequired: false };
31
34
  });
@@ -33,6 +36,18 @@ export async function mutateGate14(input) {
33
36
  export async function issueMutationPass14(input) {
34
37
  return withMutationAudit14(input, 'mutation-pass-issue', async () => {
35
38
  const pass = suppliedPass(input);
39
+ if (pass.writeDelegation) {
40
+ const binding = await mutationOriginalBinding14(input, pass);
41
+ await mutationSnapshotBinding14(input, pass, { allSources: true });
42
+ await assertPreservedMutationProtection(input, binding.root, pass);
43
+ mutationLifetime14(pass);
44
+ const locations = await installMutationPass14(input, binding, pass);
45
+ await withPreservedMutationFence(input, pass, async () => {
46
+ await assertPreservedMutationProtection(input, binding.root, pass);
47
+ await auditMutation14(input, 'mutation-pass-issued', { passId: pass.passId, leaseId: pass.leaseId, generation: pass.generation });
48
+ }, { issue: true });
49
+ return { schemaVersion: SCHEMA, gatePassPath: locations.gatePassPath, pass };
50
+ }
36
51
  return withTaskHandoffIssueFence(input, async state => {
37
52
  const binding = await mutationOriginalBinding14(input, pass, state);
38
53
  await mutationSnapshotBinding14(input, pass, { allSources: true });
@@ -50,7 +65,8 @@ export async function checkedPass(input, state, { allowTargetDrift = false } = {
50
65
  if (!pass.paths.includes(target)) mutationFail14('ENTROROVE_MUTATION_WRITE_SCOPE', 'Guarded writing is outside the cloud pass path set');
51
66
  const binding = await mutationOriginalBinding14(input, pass, state);
52
67
  const snapshot = await mutationSnapshotBinding14(input, pass, { targetPath: target, allowTargetDrift });
53
- assertTaskHandoffSnapshotBinding(binding.state, pass.snapshotReceipt);
68
+ if (!pass.writeDelegation) assertTaskHandoffSnapshotBinding(binding.state, pass.snapshotReceipt);
69
+ else await assertPreservedMutationProtection(input, binding.root, pass);
54
70
  const locations = await mutationPassLocations14(input, binding, pass);
55
71
  const ledger = await readMutationConsumption14(pass, locations);
56
72
  const consumedWrite = ledger.writes[target];
@@ -92,9 +108,10 @@ async function performWrite(input, state, value) {
92
108
  disciplineReviewDigest: discipline.reviewDigest, declaredKind: discipline.declaredKind });
93
109
  mutationLifetime14(pass);
94
110
  const written = await writeMutationProjectFile14(input, binding.root, target, content, baseline);
95
- const completed = { ...ledger, writes: { ...ledger.writes, [target]: { ...entry, status: 'written' } } };
111
+ const completed = { ...ledger, writes: { ...ledger.writes, [target]: { ...entry, status: 'written', writtenAt: new Date().toISOString() } } };
96
112
  await saveMutationConsumption14(locations, completed);
97
113
  await assertDisciplineRepository(input, binding.root, await readDisciplineSnapshot(snapshot), completed);
114
+ if (pass.writeDelegation) await assertPreservedMutationProtection(input, binding.root, pass);
98
115
  await auditMutation14(input, 'guarded-write', { passId: pass.passId, sha256: written.sha256 });
99
116
  const stateRoot = await mutationStateRoot14(input);
100
117
  await privateMutationDirectory14(path.join(stateRoot, 'ledgers', pass.repositoryDigest), { privateFrom: stateRoot });
@@ -106,7 +123,8 @@ async function performWrite(input, state, value) {
106
123
  export async function guardedWrite14(input) {
107
124
  return withMutationAudit14(input, 'guarded-write', async () => {
108
125
  const value = await checkedPass(input), pass = value.pass;
109
- return withStateLock(value.locations.consumptionPath, () => withTaskHandoffWriteFence({ ...input,
110
- epoch: pass.epoch, passId: pass.passId, leaseId: pass.leaseId }, state => performWrite(input, state, value)));
126
+ return withStateLock(value.locations.consumptionPath, () => pass.writeDelegation
127
+ ? withPreservedMutationFence(input, pass, state => performWrite(input, state, value))
128
+ : withTaskHandoffWriteFence({ ...input, epoch: pass.epoch, passId: pass.passId, leaseId: pass.leaseId }, state => performWrite(input, state, value)));
111
129
  });
112
130
  }
@@ -41,6 +41,7 @@ function tokenState(token, permitted) {
41
41
  }
42
42
  async function authority(input, supplied) {
43
43
  const pass = await readStoredMutationPass14(input);
44
+ if (pass.writeDelegation) mutationFail14('ENTROROVE_MUTATION_DELEGATION_CHANGE', 'Preserved mutation requires individual existing-file modification');
44
45
  for (const value of [supplied, input.mutationPass]) if (value !== undefined && canonical(mutationPass14(value)) !== canonical(pass)) {
45
46
  mutationFail14('ENTROROVE_MUTATION_PASS_CHANGED', 'Batch cannot replace its stored original signed pass');
46
47
  }