@entrorove/skill 0.1.36 → 0.1.38

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 (64) hide show
  1. package/CAPABILITY-MAP.md +11 -0
  2. package/CHANGELOG.md +10 -0
  3. package/IDE-EXECUTION.md +93 -0
  4. package/REUSE-DISCIPLINE.md +18 -0
  5. package/SKILL.md +22 -89
  6. package/cli.mjs +12 -8
  7. package/cloud-task-client.mjs +20 -7
  8. package/cloud-watch-client.mjs +4 -3
  9. package/ide-code-mode.mjs +38 -0
  10. package/ide-command.mjs +76 -0
  11. package/ide-hook.mjs +167 -0
  12. package/ide-install.mjs +59 -0
  13. package/ide-presentation.mjs +15 -7
  14. package/ide-results.mjs +59 -0
  15. package/ide-session.mjs +86 -0
  16. package/local-approved-plan.mjs +21 -6
  17. package/local-archguard-ast.mjs +6 -1
  18. package/local-archguard-language-ast.mjs +7 -0
  19. package/local-cli.mjs +6 -1
  20. package/local-contracts.mjs +4 -1
  21. package/local-discipline-actions.mjs +71 -0
  22. package/local-discipline-analysis.mjs +153 -0
  23. package/local-discipline-batch.mjs +32 -0
  24. package/local-discipline-contracts.d.mts +6 -0
  25. package/local-discipline-contracts.mjs +62 -0
  26. package/local-discipline-gate.mjs +85 -0
  27. package/local-discipline-native.mjs +33 -0
  28. package/local-discipline-operations.mjs +39 -0
  29. package/local-discipline-repository.mjs +87 -0
  30. package/local-discipline-snapshot.mjs +29 -0
  31. package/local-equivalence-runner.mjs +33 -0
  32. package/local-error.mjs +12 -0
  33. package/local-execution.mjs +1 -0
  34. package/local-git-executable.mjs +33 -0
  35. package/local-integrity.d.mts +5 -0
  36. package/local-integrity.mjs +4 -0
  37. package/local-merge-controller.mjs +1 -0
  38. package/local-merge-rollback.mjs +2 -1
  39. package/local-mutation-authority.mjs +16 -7
  40. package/local-mutation-batch.mjs +7 -3
  41. package/local-mutation-files.mjs +22 -0
  42. package/local-mutation-snapshot.mjs +6 -3
  43. package/local-mutation-wire.mjs +6 -1
  44. package/local-runner-discipline.mjs +23 -0
  45. package/local-scope-git-reads.mjs +3 -1
  46. package/local-scope-repository.mjs +19 -1
  47. package/local-task-handoff-write-receipts.mjs +11 -0
  48. package/manifest.json +1 -1
  49. package/native-client.mjs +1 -1
  50. package/native-notification.mjs +13 -6
  51. package/package.json +37 -4
  52. package/release-state.json +62 -31
  53. package/release.mjs +11 -1
  54. package/releases.json +12 -0
  55. package/remote-client.mjs +16 -1
  56. package/reuse-discipline-release.mjs +18 -0
  57. package/runtime/validator-runtime-v7.0.44.d.mts +3 -0
  58. package/runtime/validator-runtime-v7.0.44.mjs +1362 -0
  59. package/snapshot-cleanup-journal.mjs +32 -0
  60. package/snapshot-retention.mjs +95 -0
  61. package/task-entry-client.mjs +28 -0
  62. package/task-identity.d.mts +2 -0
  63. package/task-identity.mjs +9 -0
  64. package/validator-runtime-build.mjs +16 -0
@@ -7,6 +7,13 @@ const grammars = new Map();
7
7
  for (const language of ['css', 'python', 'java', 'rust']) {
8
8
  grammars.set(language, await Parser.Language.load(require.resolve(`tree-sitter-wasms/out/tree-sitter-${language}.wasm`)));
9
9
  }
10
+ export async function withArchguardSyntaxTree(file, language, inspect) {
11
+ if (!['css','python','java','rust','php'].includes(language)) throw new Error('ARCH-AST-LANGUAGE-UNSUPPORTED');
12
+ if (!grammars.has(language)) grammars.set(language, await Parser.Language.load(require.resolve(`tree-sitter-wasms/out/tree-sitter-${language}.wasm`)));
13
+ const parser = new Parser(); parser.setLanguage(grammars.get(language));
14
+ const tree = parser.parse(file.content);
15
+ try { return inspect(tree.rootNode); } finally { tree.delete(); parser.delete(); }
16
+ }
10
17
  const EXTENSIONS = new Map([['css', 'css'], ['wxss', 'css'], ['py', 'python'], ['java', 'java'], ['rs', 'rust']]);
11
18
  const ROOTS = { css: 'stylesheet', python: 'module', java: 'program', rust: 'source_file' };
12
19
  const FUNCTIONS = { css: [], python: ['function_definition', 'lambda'], java: ['method_declaration', 'constructor_declaration', 'lambda_expression'],
package/local-cli.mjs CHANGED
@@ -23,6 +23,8 @@ import { LOCAL_BUDGET_TRANSITION_OPERATIONS } from './local-budget-transition.mj
23
23
  import { ARCHGUARD_LOCAL_OPERATIONS } from './local-archguard-operations.mjs';
24
24
  import { LOCAL_BUDGET_HANDOFF_OPERATIONS } from './local-budget-handoff.mjs';
25
25
  import { LOCAL_CHAIN_OPERATIONS } from './local-chain-operations.mjs';
26
+ import { DISCIPLINE_LOCAL_OPERATIONS } from './local-discipline-operations.mjs';
27
+ import { assertIdeExecution } from './ide-session.mjs';
26
28
  import { runManagedWorkflowLocalStep } from './workflow-local-client.mjs';
27
29
  import { localErrorEnvelope } from './local-error.mjs';
28
30
 
@@ -37,6 +39,7 @@ async function stdinJson() {
37
39
  }
38
40
 
39
41
  const operations = {
42
+ ...DISCIPLINE_LOCAL_OPERATIONS,
40
43
  'snapshot-plan': snapshotPlan, 'snapshot-verify': snapshotVerify, 'mutation-snapshot-attest': attestMutationSnapshot14, 'mutate-gate': mutateGate,
41
44
  'gate-issue': issueMutationPass, 'gate-verify': verifyMutationPass, 'guarded-write': guardedWrite,
42
45
  'budget-init': budgetInit, 'budget-context': budgetContext, 'budget-read': budgetRead, 'budget-status': budgetStatus,
@@ -76,7 +79,7 @@ function capabilitiesFor(group) {
76
79
  async function checkedLocalInput(operation, input) {
77
80
  try { return validateLocalInput(operation, input); }
78
81
  catch (cause) {
79
- if (['guarded-write', 'gate-issue', 'mutate-gate', 'mutation-snapshot-attest'].includes(operation)) await recordMutationDenied14(input, cause, operation);
82
+ if (['guarded-write', 'guarded-delete', 'guarded-move', 'gate-issue', 'mutate-gate', 'mutation-snapshot-attest'].includes(operation)) await recordMutationDenied14(input, cause, operation);
80
83
  throw cause;
81
84
  }
82
85
  }
@@ -91,6 +94,7 @@ export async function runLocal(args = [], dependencies) {
91
94
  const handler = operations[operation];
92
95
  if (!handler) throw new Error(`Usage: entrorove local <${['capabilities', ...Object.keys(operations)].join('|')}> [repositoryRoot]`);
93
96
  const input = await stdinJson();
97
+ await assertIdeExecution(['local', ...args], input);
94
98
  if (args[1] && input.repositoryRoot === undefined) input.repositoryRoot = args[1];
95
99
  const validated = await checkedLocalInput(operation, input);
96
100
  const supplied = operation === 'workflow-run-step' || Object.hasOwn(LOCAL_CHAIN_OPERATIONS, operation) ? dependencies : undefined;
@@ -105,6 +109,7 @@ export async function runLocal(args = [], dependencies) {
105
109
  export async function runLocalInput(operation, input, dependencies) {
106
110
  if (operation === 'capabilities' || operation === 'help') { validateLocalInput(operation, input); return LOCAL_CAPABILITIES; }
107
111
  const handler = operations[operation];
112
+ await assertIdeExecution(['local', operation], input);
108
113
  if (!handler) throw new Error(`Unsupported local operation: ${operation}`);
109
114
  const validated = await checkedLocalInput(operation, input);
110
115
  const supplied = operation === 'workflow-run-step' || Object.hasOwn(LOCAL_CHAIN_OPERATIONS, operation) ? dependencies : undefined;
@@ -8,6 +8,7 @@ import { MERGE_LOCAL_OPERATION_SCHEMAS } from './local-merge-contracts.mjs';
8
8
  import { WORKFLOW_LOCAL_OPERATION_SCHEMA } from './workflow-local-contracts.mjs';
9
9
  import { HANDOFF_WRITE_PASS_SCHEMA, HANDOFF_PROVISIONAL_LEASE_SCHEMA, HANDOFF_SNAPSHOT_CANDIDATE_SCHEMA } from './local-task-handoff-contracts.mjs';
10
10
  import { LOCAL_CHAIN_OPERATION_SCHEMAS } from './local-chain-contracts.mjs';
11
+ import { DISCIPLINE_DECLARATION_SCHEMA } from './local-discipline-contracts.mjs';
11
12
  import { array, binding, change, confirmation, enumeration, evidence, evidenceArray, executionContext, executionPlan,
12
13
  formula, formulaDefinitions, formulaEntry, decimalValues, identifier, integer, ledgerIdentifier, modes, nullable, object, paths, record, relativePath, sha256, text } from './local-contract-types.mjs';
13
14
 
@@ -31,7 +32,9 @@ add('mutation-snapshot-attest', { ...step, taskId: identifier, snapshotId: ident
31
32
  ['repositoryRoot', 'flowId', 'stepId', 'snapshotId', 'challenge', 'signerConfigPath']);
32
33
  const write = { ...step, taskId: identifier, generation: integer(1), executionAttemptNonce: identifier, gatePassPath: text, targetPath: relativePath, workerId: identifier, leaseId: identifier };
33
34
  add('gate-verify', write, ['repositoryRoot', 'flowId', 'stepId', 'gatePassPath', 'targetPath']);
34
- add('guarded-write', { ...write, content: {}, expectedSha256: nullable(sha256) }, ['repositoryRoot', 'flowId', 'stepId', 'gatePassPath', 'targetPath', 'content']);
35
+ add('discipline-review|discipline-check', write, ['repositoryRoot', 'flowId', 'stepId', 'gatePassPath', 'targetPath']);
36
+ add('guarded-delete|guarded-move', { ...write, declaration: DISCIPLINE_DECLARATION_SCHEMA }, ['repositoryRoot', 'flowId', 'stepId', 'gatePassPath', 'targetPath', 'declaration']);
37
+ add('guarded-write', { ...write, content: {}, expectedSha256: nullable(sha256), declaration: DISCIPLINE_DECLARATION_SCHEMA }, ['repositoryRoot', 'flowId', 'stepId', 'gatePassPath', 'targetPath', 'content']);
35
38
  schemas['guarded-write'] = { oneOf: [schemas['guarded-write'], object({ ...step, targetPath: relativePath, content: {},
36
39
  expectedSha256: nullable(sha256), handoffId: ledgerIdentifier, epoch: integer(1, 2147483647),
37
40
  handoffPass: HANDOFF_WRITE_PASS_SCHEMA, provisionalLease: HANDOFF_PROVISIONAL_LEASE_SCHEMA },
@@ -0,0 +1,71 @@
1
+ import { canonical, sha256 } from './local-integrity.mjs';
2
+ import { checkedPass } from './local-mutation-authority.mjs';
3
+ import { withStateLock } from './local-state-lock.mjs';
4
+ import { withTaskHandoffWriteFence } from './local-task-handoff-fence.mjs';
5
+ import { withSwarmMutationLease } from './local-swarm-mutation.mjs';
6
+ import { disciplineApprovedPolicy } from './local-discipline-contracts.mjs';
7
+ import { readDisciplineSnapshot } from './local-discipline-snapshot.mjs';
8
+ import { assertDisciplineRepository } from './local-discipline-repository.mjs';
9
+ import { saveMutationConsumption14 } from './local-mutation-pass-store.mjs';
10
+ import { auditMutation14, withMutationAudit14 } from './local-mutation-audit.mjs';
11
+ import { mutationFail14, removeMutationProjectFile14, moveMutationProjectFile14 } from './local-mutation-files.mjs';
12
+
13
+ function actionIntent(input, value, kind, review) {
14
+ if (!value.pass.disciplinePolicy) mutationFail14('ENTROROVE_DISCIPLINE_NEEDS_HUMAN', `${kind} needs a separately approved original change declaration`);
15
+ const policy = disciplineApprovedPolicy(value.pass.disciplinePolicy), intent = policy.writes.find(write => write.targetPath === value.target);
16
+ const { usageReviewDigest, ...supplied } = input.declaration ?? {};
17
+ if (!intent || intent.kind !== kind || canonical(supplied) !== canonical(intent)
18
+ || usageReviewDigest !== value.snapshot.manifest.disciplineReviewDigest) {
19
+ mutationFail14('ENTROROVE_DISCIPLINE_NEEDS_HUMAN', 'Physical action must match its exact signed proposal and original usage artifact');
20
+ }
21
+ if (kind === 'delete') {
22
+ const definitions = review.analysis.definitions.filter(item => item.path === value.target);
23
+ if (!review.scanComplete || definitions.some(item => item.usage === 'unknown')) mutationFail14('ENTROROVE_DISCIPLINE_USAGE_UNKNOWN', 'Unknown references forbid file deletion');
24
+ if (canonical([...intent.deletedSymbols ?? []].sort()) !== canonical(definitions.map(item => item.symbol).sort())) {
25
+ mutationFail14('ENTROROVE_DISCIPLINE_NEEDS_HUMAN', 'File deletion must declare every removed symbol and its task requirement');
26
+ }
27
+ if (definitions.some(item => item.usage === 'in-use') && !intent.exceptions?.includes('delete-in-use')) {
28
+ mutationFail14('ENTROROVE_DISCIPLINE_NEEDS_HUMAN', 'Referenced symbols need explicit deletion authorization');
29
+ }
30
+ }
31
+ return intent;
32
+ }
33
+ async function perform(input, kind, original, state) {
34
+ const value = await checkedPass(input, state), { pass, snapshot, ledger, binding, locations } = value;
35
+ if (canonical(pass) !== canonical(original.pass)) mutationFail14('ENTROROVE_MUTATION_PASS_CHANGED', 'Structural action changed its original pass while waiting');
36
+ return withSwarmMutationLease(input, pass, async () => {
37
+ const review = await readDisciplineSnapshot(snapshot), intent = actionIntent(input, value, kind, review);
38
+ await assertDisciplineRepository(input, binding.root, review, ledger);
39
+ const source = snapshot.manifest.files.find(file => file.path === value.target);
40
+ const at = new Date().toISOString();
41
+ const writes = { ...ledger.writes, [value.target]: { status: 'pending', kind: 'delete', contentDigest: source.sha256, mode: source.mode, at } };
42
+ let destination;
43
+ if (kind === 'move') {
44
+ const next = await checkedPass({ ...input, targetPath: intent.destinationPath }, state);
45
+ if (next.pass.passId !== pass.passId) mutationFail14('ENTROROVE_MUTATION_WRITE_SCOPE', 'Move destination needs the same original pass');
46
+ destination = snapshot.manifest.files.find(file => file.path === intent.destinationPath);
47
+ writes[intent.destinationPath] = { status: 'pending', kind: 'create', contentDigest: source.sha256, mode: source.mode, at };
48
+ }
49
+ await saveMutationConsumption14(locations, { ...ledger, writes });
50
+ await auditMutation14(input, 'guarded-action-authorized', { passId: pass.passId, disciplineReviewDigest: snapshot.manifest.disciplineReviewDigest, declaredKind: kind });
51
+ if (kind === 'move') await moveMutationProjectFile14(input, binding.root, source, destination);
52
+ else await removeMutationProjectFile14(input, binding.root, source);
53
+ const move = destination ? { move: { sourcePath: value.target, destinationPath: intent.destinationPath, sourceDigest: source.sha256 } } : {};
54
+ writes[value.target] = { ...writes[value.target], ...move, status: 'written' };
55
+ if (destination) writes[intent.destinationPath] = { ...writes[intent.destinationPath], ...move, status: 'written' };
56
+ await saveMutationConsumption14(locations, { ...ledger, writes });
57
+ await assertDisciplineRepository(input, binding.root, review, { ...ledger, writes });
58
+ await auditMutation14(input, `guarded-${kind}`, { passId: pass.passId, declaredKind: kind });
59
+ return { schemaVersion: 'entrorove.local-execution/1.0', status: 'verified', action: kind, targetPath: value.target,
60
+ ...(destination ? { destinationPath: intent.destinationPath, sha256: source.sha256 } : {}), usageReviewDigest: snapshot.manifest.disciplineReviewDigest };
61
+ });
62
+ }
63
+ async function guardedAction(input, kind) {
64
+ return withMutationAudit14(input, `guarded-${kind}`, async () => {
65
+ const value = await checkedPass(input), pass = value.pass;
66
+ return withStateLock(value.locations.consumptionPath, () => withTaskHandoffWriteFence({ ...input,
67
+ epoch: pass.epoch, passId: pass.passId, leaseId: pass.leaseId }, state => perform(input, kind, value, state)));
68
+ });
69
+ }
70
+ export const guardedDelete = input => guardedAction(input, 'delete');
71
+ export const guardedMove = input => guardedAction(input, 'move');
@@ -0,0 +1,153 @@
1
+ import ts from 'typescript';
2
+ import { archguardScriptSource } from './local-archguard-ast.mjs';
3
+ import { canonical, sha256 } from './local-integrity.mjs';
4
+
5
+ const location = (source, node) => ({ path: source.fileName, line: source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1 });
6
+ const unwrap = node => {
7
+ while (node && (ts.isAsExpression(node) || ts.isSatisfiesExpression(node) || ts.isParenthesizedExpression(node))) node = node.expression;
8
+ return node;
9
+ };
10
+ function nameOf(node) {
11
+ return node && (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) ? node.text : null;
12
+ }
13
+ function tokenLines(source) {
14
+ const scanner = ts.createScanner(ts.ScriptTarget.Latest, true, source.languageVariant, source.text), lines = new Map();
15
+ let token;
16
+ while ((token = scanner.scan()) !== ts.SyntaxKind.EndOfFileToken) {
17
+ const line = source.getLineAndCharacterOfPosition(scanner.getTokenPos()).line;
18
+ const values = lines.get(line) ?? []; values.push(`${token}:${scanner.getTokenText()}`); lines.set(line, values);
19
+ }
20
+ return new Map([...lines].map(([line, values]) => [line, sha256(canonical(values))]));
21
+ }
22
+ export function disciplineFormattingLines(before, after, filename) {
23
+ const left = archguardScriptSource({ path: filename, content: before }), right = archguardScriptSource({ path: filename, content: after });
24
+ if (!left || !right) return [];
25
+ const oldLines = before.split('\n'), newLines = after.split('\n'), a = tokenLines(left), b = tokenLines(right);
26
+ const index = new Map(), next = new Map();
27
+ for (const [line, digest] of a) index.set(digest, [...index.get(digest) ?? [], line]);
28
+ for (const [line, digest] of b) next.set(digest, [...next.get(digest) ?? [], line]);
29
+ const findings = [];
30
+ for (const [digest, lines] of next) {
31
+ const previous = index.get(digest);
32
+ if (previous?.length === lines.length) lines.forEach((line, occurrence) => {
33
+ if (oldLines[previous[occurrence]] !== newLines[line]) findings.push(line + 1);
34
+ });
35
+ }
36
+ if (canonical([...a.values()]) === canonical([...b.values()]) && before !== after) {
37
+ return [...new Set([...findings, ...newLines.flatMap((value, line) => value !== oldLines[line] ? [line + 1] : [])])].sort((a, b) => a - b);
38
+ }
39
+ return findings;
40
+ }
41
+ function publicDefinition(node) {
42
+ return node.modifiers?.some(item => [ts.SyntaxKind.ExportKeyword, ts.SyntaxKind.DefaultKeyword].includes(item.kind))
43
+ || ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isEnumDeclaration(node);
44
+ }
45
+ function symbolNode(node) {
46
+ return ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)
47
+ || ts.isTypeAliasDeclaration(node) || ts.isEnumDeclaration(node) || ts.isVariableDeclaration(node)
48
+ || ts.isPropertySignature(node) || ts.isMethodSignature(node) || ts.isPropertyDeclaration(node) || ts.isMethodDeclaration(node)
49
+ || ts.isPropertyAssignment(node) || ts.isEnumMember(node);
50
+ }
51
+ function definition(source, node) {
52
+ const name = nameOf(node.name); if (!name) return null;
53
+ let parent = node.parent;
54
+ while (parent && !ts.isSourceFile(parent) && !nameOf(parent.name)) parent = parent.parent;
55
+ const owner = parent && !ts.isSourceFile(parent) ? nameOf(parent.name) : null;
56
+ const symbol = owner ? `${owner}.${name}` : name;
57
+ let ancestor = node;
58
+ while (ancestor && !ts.isSourceFile(ancestor) && !publicDefinition(ancestor)) ancestor = ancestor.parent;
59
+ const exported = Boolean(ancestor && !ts.isSourceFile(ancestor));
60
+ const contract = exported && (ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isEnumDeclaration(node) || ts.isFunctionDeclaration(node));
61
+ const signature = ts.isFunctionDeclaration(node) ? canonical({ parameters: node.parameters.map(item => item.getText(source)), type: node.type?.getText(source) ?? null }) : node.getText(source);
62
+ return { ...location(source, node), symbol, name, exported: Boolean(exported), contract: Boolean(contract),
63
+ signatureDigest: contract ? sha256(signature) : null };
64
+ }
65
+ function unresolvedNode(node) {
66
+ if (ts.isElementAccessExpression(node) && nameOf(node.argumentExpression) === null) return true;
67
+ if (ts.isIdentifier(node) && ['eval', 'Function', 'createRequire'].includes(node.text)) return true;
68
+ if (ts.isCallExpression(node)) {
69
+ if (node.expression.kind === ts.SyntaxKind.ImportKeyword || ts.isIdentifier(node.expression) && node.expression.text === 'require') {
70
+ return node.arguments.length !== 1 || !ts.isStringLiteral(node.arguments[0]);
71
+ }
72
+ if (ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression)
73
+ && ['Reflect', 'Object'].includes(node.expression.expression.text) && ['get', 'set', 'defineProperty'].includes(node.expression.name.text)) return true;
74
+ }
75
+ return false;
76
+ }
77
+ function mapping(source, node, checker) {
78
+ if (!ts.isVariableDeclaration(node) || !ts.isTypeReferenceNode(node.type ?? {}) || !node.initializer) return null;
79
+ const type = node.type.typeArguments?.[0];
80
+ if (!type || !ts.isTypeOperatorNode(type) || type.operator !== ts.SyntaxKind.KeyOfKeyword || !ts.isTypeReferenceNode(type.type)) return null;
81
+ let entity = checker.getTypeAtLocation(type.type)?.symbol ?? checker.getSymbolAtLocation(type.type.typeName);
82
+ if (entity?.flags & ts.SymbolFlags.Alias) entity = checker.getAliasedSymbol(entity);
83
+ const declarations = entity?.declarations;
84
+ if (!declarations?.length) return null;
85
+ const value = unwrap(node.initializer); if (!ts.isObjectLiteralExpression(value)) return null;
86
+ const fields = [], unresolved = [];
87
+ for (const property of value.properties) {
88
+ const key = nameOf(property.name);
89
+ if (!key || !ts.isPropertyAssignment(property)) { unresolved.push(location(source, property)); continue; }
90
+ const value = property.initializer;
91
+ const actual = ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value) ? canonical(value.text)
92
+ : ts.isNumericLiteral(value) ? canonical(Number(value.text))
93
+ : ts.createPrinter({ removeComments: true }).printNode(ts.EmitHint.Expression, value, source);
94
+ fields.push({ name: key, valueDigest: sha256(actual) });
95
+ }
96
+ const original = declarations[0], origin = location(original.getSourceFile(), original);
97
+ return { ...location(source, node), symbol: nameOf(node.name), entity: `${origin.path}:${entity.name}`,
98
+ role: 'source-entity-field-mapping', fields: fields.sort((a, b) => a.name.localeCompare(b.name)), unresolved };
99
+ }
100
+ function programFor(sources, edges) {
101
+ const host = ts.createCompilerHost({ noLib: true, allowJs: true });
102
+ const virtualRoot = '/entrorove-repository/', relative = filename => filename.startsWith(virtualRoot) ? filename.slice(virtualRoot.length) : filename;
103
+ host.getCurrentDirectory = () => virtualRoot.slice(0, -1);
104
+ host.getSourceFile = filename => sources.get(relative(filename));
105
+ host.fileExists = filename => sources.has(relative(filename)); host.readFile = filename => sources.get(relative(filename))?.text;
106
+ host.writeFile = () => {};
107
+ host.resolveModuleNames = (names, from) => names.map(name => {
108
+ const edge = edges.find(item => item.from === relative(from) && item.specifier === name && item.to !== null);
109
+ return edge && sources.has(edge.to) ? { resolvedFileName: edge.to } : undefined;
110
+ });
111
+ return ts.createProgram([...sources.keys()], { noLib: true, allowJs: true, checkJs: false }, host);
112
+ }
113
+ export function analyzeDisciplineSources(files, { edges = [], unresolved = [] } = {}) {
114
+ const sources = new Map(), unsupported = [];
115
+ for (const file of files) {
116
+ const parsed = archguardScriptSource(file);
117
+ if (!parsed || parsed.parseDiagnostics.length) { unsupported.push({ path: file.path, reason: parsed ? 'syntax-unresolved' : 'language-unresolved' }); continue; }
118
+ sources.set(file.path, parsed);
119
+ }
120
+ const program = programFor(sources, edges), checker = program.getTypeChecker(), definitions = [], mappings = [], references = new Map();
121
+ const unknown = [...unresolved, ...unsupported];
122
+ for (const source of sources.values()) {
123
+ const visit = node => {
124
+ if (unresolvedNode(node)) unknown.push({ ...location(source, node), reason: 'dynamic-reference' });
125
+ if (symbolNode(node)) {
126
+ const entry = definition(source, node);
127
+ if (entry) { const symbol = checker.getSymbolAtLocation(node.name); definitions.push({ entry, symbol, nameNode: node.name }); }
128
+ const fact = mapping(source, node, checker); if (fact) mappings.push(fact);
129
+ }
130
+ ts.forEachChild(node, visit);
131
+ }; visit(source);
132
+ }
133
+ const names = new Set(definitions.map(item => item.nameNode));
134
+ for (const source of sources.values()) {
135
+ const visit = node => {
136
+ if ((ts.isIdentifier(node) || ts.isStringLiteral(node)) && !names.has(node)) {
137
+ let symbol = checker.getSymbolAtLocation(node);
138
+ if (symbol?.flags & ts.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol);
139
+ if (symbol) { const points = references.get(symbol) ?? []; points.push(location(source, node)); references.set(symbol, points); }
140
+ }
141
+ ts.forEachChild(node, visit);
142
+ }; visit(source);
143
+ }
144
+ return { mappings, unresolved: unknown, definitions: definitions.map(({ entry, symbol }) => {
145
+ const points = references.get(symbol) ?? [];
146
+ return { ...entry, references: points, usage: points.length ? 'in-use' : unknown.length || entry.exported ? 'unknown' : 'unused' };
147
+ }) };
148
+ }
149
+ export function disciplineDuplicateMappings(original, candidate) {
150
+ return candidate.mappings.flatMap(next => original.mappings.filter(existing => existing.entity === next.entity && existing.role === next.role
151
+ && !(existing.path === next.path && existing.symbol === next.symbol)).map(existing => ({ existing, candidate: next,
152
+ equal: !existing.unresolved.length && !next.unresolved.length && canonical(existing.fields) === canonical(next.fields) })));
153
+ }
@@ -0,0 +1,32 @@
1
+ import { canonical, sha256 } from './local-integrity.mjs';
2
+ import { mutationFail14 } from './local-mutation-files.mjs';
3
+ import { disciplineWritePreflight } from './local-discipline-gate.mjs';
4
+ import { readDisciplineSnapshot } from './local-discipline-snapshot.mjs';
5
+ import { assertDisciplineRepository } from './local-discipline-repository.mjs';
6
+
7
+ export async function disciplineBatchBefore(input, value, options, before, after, ledger) {
8
+ const review = await readDisciplineSnapshot(value.snapshot);
9
+ await assertDisciplineRepository(input, value.binding.root, review, ledger);
10
+ // A rollback is already separately cloud-signed against exact restore hashes
11
+ // by mutationBatchReceiptBinding14. It restores that known image, rather than
12
+ // proposing a new definition or reusing a forward mutation permission.
13
+ const restoration = options.contract?.schemaVersion === 'entrorove.merge-rollback-authorization/1.0';
14
+ if (restoration) return review;
15
+ for (const file of after) {
16
+ const original = before.find(item => item.path === file.path);
17
+ if (canonical(original) === canonical(file)) continue;
18
+ if (!file.exists) mutationFail14('ENTROROVE_DISCIPLINE_NEEDS_HUMAN', 'Forward batch deletion requires its separate guarded deletion declaration');
19
+ const source = options.contents?.find(item => item.path === file.path);
20
+ if (!source) {
21
+ if (/\.(?:[cm]?[jt]sx?|php|py|java|rs|vue|svelte|css)$/iu.test(file.path)) mutationFail14('ENTROROVE_DISCIPLINE_REVIEW_REQUIRED', 'Source batch needs actual candidate bytes before any write');
22
+ continue;
23
+ }
24
+ const bytes = Buffer.from(source.contentBase64, 'base64');
25
+ if (bytes.toString('base64') !== source.contentBase64 || sha256(bytes) !== file.sha256) mutationFail14('ENTROROVE_DISCIPLINE_REVIEW_INVALID', 'Batch candidate bytes differ from the exact signed after image');
26
+ const intent = value.pass.disciplinePolicy?.writes.find(write => write.targetPath === file.path);
27
+ await disciplineWritePreflight({ ...input, targetPath: file.path,
28
+ ...(intent ? { declaration: { ...intent, usageReviewDigest: value.snapshot.manifest.disciplineReviewDigest } } : {}) },
29
+ { ...value, target: file.path, ledger }, bytes.toString('utf8'));
30
+ }
31
+ return review;
32
+ }
@@ -0,0 +1,6 @@
1
+ export const DISCIPLINE_POLICY_SCHEMA: Record<string, any>;
2
+ export function disciplinePolicy(value: unknown): Record<string, any>;
3
+ export function disciplineApprovedPolicy(value: unknown): Record<string, any>;
4
+ export function disciplineDataSnapshotsBound(value: unknown, files: readonly { path: string; sha256: string }[]): boolean;
5
+ export function disciplineProposalDigest(value: unknown): string;
6
+ export function disciplineHumanTriggers(value: unknown): string[];
@@ -0,0 +1,62 @@
1
+ import Ajv from 'ajv';
2
+ import { canonical, sha256 } from './local-integrity.mjs';
3
+ import { object, array, text, identifier, relativePath, sha256 as shaSchema, integer } from './local-contract-types.mjs';
4
+
5
+ const refs = { ...array(identifier, 1), uniqueItems: true };
6
+ const defect = object({ path: relativePath, line: integer(1) }, ['path', 'line']);
7
+ export const DISCIPLINE_WRITE_SCHEMA = object({ targetPath: relativePath,
8
+ kind: { enum: ['modify', 'create', 'delete', 'move', 'format'] }, destinationPath: relativePath,
9
+ reason: text, requirementRefs: refs, deletedSymbols: { ...array(text), uniqueItems: true },
10
+ exceptions: { ...array({ enum: ['new-definition', 'contract-change', 'rewrite', 'delete-in-use'] }), uniqueItems: true },
11
+ defectReference: defect, futureFields: { ...array(identifier), uniqueItems: true },
12
+ }, ['targetPath', 'kind', 'reason', 'requirementRefs']);
13
+ const approval = object({ interactionId: identifier, interactionDigest: shaSchema, auditId: identifier,
14
+ proposalDigest: shaSchema }, ['interactionId', 'interactionDigest', 'auditId', 'proposalDigest']);
15
+ export const DISCIPLINE_POLICY_SCHEMA = object({ schemaVersion: { const: 'entrorove.discipline-policy/1.0' },
16
+ requirementIds: refs, writes: array(DISCIPLINE_WRITE_SCHEMA, 1), approval,
17
+ dataOperations: array(object({ operation: { enum: ['delete', 'migrate', 'rebuild'] }, target: text,
18
+ reason: text, requirementRefs: refs, snapshotArtifactDigest: shaSchema }, ['operation', 'target', 'reason', 'requirementRefs', 'snapshotArtifactDigest'])),
19
+ }, ['schemaVersion', 'requirementIds', 'writes']);
20
+ export const DISCIPLINE_DECLARATION_SCHEMA = object({ ...DISCIPLINE_WRITE_SCHEMA.properties,
21
+ usageReviewDigest: shaSchema }, DISCIPLINE_WRITE_SCHEMA.required);
22
+ const validate = new Ajv({ allErrors: true, strict: true }).compile(DISCIPLINE_POLICY_SCHEMA);
23
+ export function disciplinePolicy(value) {
24
+ if (!validate(value)) throw Object.assign(new Error(`Invalid discipline policy: ${validate.errors.map(error => error.instancePath || error.keyword).join(', ')}`), { code: 'ENTROROVE_DISCIPLINE_POLICY_INVALID' });
25
+ const requirements = new Set(value.requirementIds), paths = new Set();
26
+ for (const write of value.writes) {
27
+ if (paths.has(write.targetPath) || write.requirementRefs.some(id => !requirements.has(id))
28
+ || (write.kind === 'move') !== Boolean(write.destinationPath) || write.destinationPath === write.targetPath) {
29
+ throw Object.assign(new Error('Discipline writes require unique paths, original requirements and exact move destinations'), { code: 'ENTROROVE_DISCIPLINE_POLICY_INVALID' });
30
+ }
31
+ paths.add(write.targetPath);
32
+ }
33
+ if (value.dataOperations?.some(action => action.requirementRefs.some(id => !requirements.has(id)))) {
34
+ throw Object.assign(new Error('Data operations must reference original requirements'), { code: 'ENTROROVE_DISCIPLINE_POLICY_INVALID' });
35
+ }
36
+ return structuredClone(value);
37
+ }
38
+ export function disciplineProposalDigest(value) {
39
+ const { approval, ...proposal } = disciplinePolicy(value);
40
+ return sha256(canonical(proposal));
41
+ }
42
+ export function disciplineHumanTriggers(value) {
43
+ const policy = disciplinePolicy(value), triggers = new Set();
44
+ for (const write of policy.writes) {
45
+ for (const exception of write.exceptions ?? []) triggers.add(exception);
46
+ if (['delete', 'move'].includes(write.kind) || write.deletedSymbols?.length) triggers.add(write.kind === 'move' ? 'move' : 'delete');
47
+ }
48
+ if (policy.dataOperations?.length) triggers.add('existing-data');
49
+ return [...triggers].sort();
50
+ }
51
+ export function disciplineApprovedPolicy(value) {
52
+ const policy = disciplinePolicy(value);
53
+ if (disciplineHumanTriggers(policy).length && (!policy.approval || policy.approval.proposalDigest !== disciplineProposalDigest(policy))) {
54
+ throw Object.assign(new Error('This exact change proposal needs the original persisted human approval'), { code: 'ENTROROVE_DISCIPLINE_NEEDS_HUMAN' });
55
+ }
56
+ return policy;
57
+ }
58
+ export function disciplineDataSnapshotsBound(value, files) {
59
+ const policy = disciplineApprovedPolicy(value);
60
+ return Array.isArray(files) && !policy.dataOperations?.some(action =>
61
+ !files.some(file => file.sha256 === action.snapshotArtifactDigest));
62
+ }
@@ -0,0 +1,85 @@
1
+ import { canonical, sha256 } from './local-integrity.mjs';
2
+ import { mutationFail14 } from './local-mutation-files.mjs';
3
+ import { disciplineApprovedPolicy } from './local-discipline-contracts.mjs';
4
+ import { disciplineDuplicateMappings, disciplineFormattingLines } from './local-discipline-analysis.mjs';
5
+ import { disciplineRepositoryObservation, assertDisciplineRepository } from './local-discipline-repository.mjs';
6
+ import { readDisciplineSnapshot } from './local-discipline-snapshot.mjs';
7
+ import { readSnapshotCopy14 } from './local-mutation-snapshot.mjs';
8
+
9
+ const needs = message => mutationFail14('ENTROROVE_DISCIPLINE_NEEDS_HUMAN', message);
10
+ function declaredWrite(input, pass, baseline) {
11
+ const expectedKind = baseline.exists ? 'modify' : 'create';
12
+ const policy = pass.disciplinePolicy ? disciplineApprovedPolicy(pass.disciplinePolicy) : null;
13
+ const planned = policy?.writes.find(write => write.targetPath === input.targetPath);
14
+ if (input.declaration) {
15
+ const { usageReviewDigest, ...supplied } = input.declaration;
16
+ if (!planned || canonical(planned) !== canonical(supplied)) needs('Write declaration must match the original cloud-signed execution plan');
17
+ }
18
+ if (planned && !['modify', 'create', 'format'].includes(planned.kind)) needs('The declared physical action must use its matching guarded entry point');
19
+ if (planned && planned.kind !== 'format' && planned.kind !== expectedKind) needs('Declared write kind differs from the actual snapshot baseline');
20
+ return planned;
21
+ }
22
+ function requireException(intent, kind, message) {
23
+ if (!intent?.exceptions?.includes(kind)) needs(message);
24
+ }
25
+ function checkRemoved(review, candidate, target, intent, digest, declaration) {
26
+ const before = review.analysis.definitions.filter(item => item.path === target), after = new Set(candidate.definitions.filter(item => item.path === target).map(item => item.symbol));
27
+ const removed = before.filter(item => !after.has(item.symbol));
28
+ if (!removed.length) return [];
29
+ if (!intent || declaration?.usageReviewDigest !== digest
30
+ || canonical([...intent.deletedSymbols ?? []].sort()) !== canonical(removed.map(item => item.symbol).sort())) {
31
+ needs(`Deleted symbols require their original usage review ${digest}, declared reasons and requirement references: ${removed.map(item => item.symbol).join(', ')}`);
32
+ }
33
+ for (const item of removed) {
34
+ if (item.usage === 'unknown') mutationFail14('ENTROROVE_DISCIPLINE_USAGE_UNKNOWN', `Usage is unknown; preserve ${item.path}:${item.line} ${item.symbol}`);
35
+ if (item.usage === 'in-use') requireException(intent, 'delete-in-use', `Deletion is still referenced: ${item.path}:${item.line} ${item.symbol}`);
36
+ }
37
+ return removed.map(item => ({ path: item.path, symbol: item.symbol, usage: item.usage, reviewDigest: digest }));
38
+ }
39
+ function checkContracts(review, candidate, target, intent) {
40
+ const after = new Map(candidate.definitions.filter(item => item.path === target).map(item => [item.symbol, item]));
41
+ for (const before of review.analysis.definitions.filter(item => item.path === target && item.contract)) {
42
+ const current = after.get(before.symbol);
43
+ if (current && before.signatureDigest !== current.signatureDigest) {
44
+ requireException(intent, 'contract-change', `Existing field, type or interface contract changed at ${before.path}:${before.line} ${before.symbol}`);
45
+ }
46
+ }
47
+ }
48
+ function checkReuse(review, candidate, target, intent, parallelPaths) {
49
+ const duplicates = disciplineDuplicateMappings(review.analysis, { mappings: candidate.mappings.filter(item => item.path === target) });
50
+ if (duplicates.length) {
51
+ const original = duplicates[0].existing;
52
+ requireException(intent, 'new-definition', `Reuse the existing business fact at ${original.path}:${original.line} ${original.symbol}`);
53
+ requireException(intent, 'rewrite', 'A parallel definition requires a concrete existing defect and independent equivalence evidence');
54
+ if (intent.defectReference?.path !== original.path || intent.defectReference?.line !== original.line
55
+ || /更清晰|便于维护|更合理|clearer|maintainab|more.general/i.test(intent.reason)) needs('Rewrite reason must cite the exact existing definition and a concrete defect');
56
+ if (duplicates.some(item => !parallelPaths?.some(pair => pair.originalPath === item.existing.path && pair.candidatePath === target))) {
57
+ needs('Parallel definitions require equivalence declarations frozen in the same signed execution plan');
58
+ }
59
+ }
60
+ const old = new Set(review.analysis.definitions.filter(item => item.path === target).map(item => item.symbol));
61
+ const added = candidate.definitions.filter(item => item.path === target && item.exported && !old.has(item.symbol));
62
+ if (added.length && !intent) needs('New public definitions require a current requirement and reuse declaration');
63
+ if (intent && /未来|扩展|更通用|future|extensib|more.general/i.test(intent.reason)
64
+ && (!intent.futureFields?.length || intent.futureFields.some(name => !candidate.definitions.some(item => item.name === name)))) {
65
+ mutationFail14('ENTROROVE_DISCIPLINE_SPECULATIVE', 'Future abstractions require concrete named fields already demanded by the current requirement');
66
+ }
67
+ return { searchTerms: [...new Set([...added.map(item => item.name), ...candidate.mappings.filter(item => item.path === target).map(item => item.symbol)])],
68
+ results: duplicates.map(item => ({ path: item.existing.path, line: item.existing.line, symbol: item.existing.symbol })),
69
+ empty: duplicates.length === 0, complete: review.scanComplete };
70
+ }
71
+ export async function disciplineWritePreflight(input, value, content) {
72
+ const { pass, target, binding, snapshot, ledger } = value;
73
+ const review = await readDisciplineSnapshot(snapshot), baseline = snapshot.manifest.files.find(file => file.path === target);
74
+ await assertDisciplineRepository(input, binding.root, review, ledger);
75
+ const intent = declaredWrite(input, pass, baseline);
76
+ const candidate = await disciplineRepositoryObservation(input, binding.root, { analyze: true, overrides: new Map([[target, content]]) });
77
+ const removed = checkRemoved(review, candidate.analysis, target, intent, snapshot.manifest.disciplineReviewDigest, input.declaration);
78
+ checkContracts(review, candidate.analysis, target, intent);
79
+ 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
+ const formatting = baseline.exists ? disciplineFormattingLines(original, content, target) : [];
82
+ if (formatting.length) mutationFail14('ENTROROVE_DISCIPLINE_FORMAT_SCOPE', `Formatting touches lines without functional changes: ${target}:${formatting.join(',')}`);
83
+ return { reviewDigest: snapshot.manifest.disciplineReviewDigest, declaredKind: baseline.exists ? 'modify' : 'create', removed, reuse,
84
+ candidateDigest: sha256(content), comparisonScope: review.comparison.scope };
85
+ }
@@ -0,0 +1,33 @@
1
+ import { withArchguardSyntaxTree } from './local-archguard-language-ast.mjs';
2
+ import { sha256 } from './local-integrity.mjs';
3
+
4
+ const languages = { php: 'php', py: 'python', java: 'java', rs: 'rust' };
5
+ const declarations = new Set(['class_declaration','class_definition','interface_declaration','trait_declaration','enum_declaration',
6
+ 'function_definition','method_declaration','function_item','struct_item','const_item','property_element','const_element']);
7
+ export async function nativeDisciplineDefinitions(files) {
8
+ const definitions = [];
9
+ for (const file of files) {
10
+ const language = languages[file.path.split('.').at(-1)]; if (!language) continue;
11
+ const found = await withArchguardSyntaxTree(file, language, root => {
12
+ if (root.hasError) return [];
13
+ const output = [];
14
+ const visit = (node, owner = []) => {
15
+ let nested = owner;
16
+ if (declarations.has(node.type)) {
17
+ const name = node.childForFieldName('name') ?? node.namedChildren.find(child => ['name','identifier','variable_name'].includes(child.type));
18
+ if (name) {
19
+ const text = name.text, body = node.childForFieldName('body');
20
+ const signature = body ? file.content.slice(node.startIndex, body.startIndex) : node.text;
21
+ output.push({ path: file.path, line: node.startPosition.row + 1, symbol: [...owner,text].join('.'), name: text,
22
+ exported: true, contract: Boolean(body), signatureDigest: sha256(signature), references: [], usage: 'unknown',
23
+ parser: `tree-sitter/${language}` });
24
+ if (body) nested = [...owner,text];
25
+ }
26
+ }
27
+ node.namedChildren.forEach(child => visit(child, nested));
28
+ }; visit(root); return output;
29
+ });
30
+ definitions.push(...found);
31
+ }
32
+ return definitions;
33
+ }
@@ -0,0 +1,39 @@
1
+ import { checkedPass } from './local-mutation-authority.mjs';
2
+ import { canonical, sha256 } from './local-integrity.mjs';
3
+ import { readDisciplineSnapshot } from './local-discipline-snapshot.mjs';
4
+ import { assertDisciplineRepository, disciplineRepositoryDifferences } from './local-discipline-repository.mjs';
5
+ import { guardedDelete, guardedMove } from './local-discipline-actions.mjs';
6
+
7
+ async function observation(input, checkpoint = false) {
8
+ const value = await checkedPass(input, undefined, { allowTargetDrift: checkpoint });
9
+ const review = await readDisciplineSnapshot(value.snapshot);
10
+ return { value, review };
11
+ }
12
+ export async function readDisciplineReview(input) {
13
+ const { value, review } = await observation(input);
14
+ return { schemaVersion: 'entrorove.discipline-review-result/1.0', status: 'observed',
15
+ flowId: value.pass.flowId, snapshotId: value.snapshot.manifest.snapshotId,
16
+ usageReviewDigest: value.snapshot.manifest.disciplineReviewDigest, scanComplete: review.scanComplete,
17
+ scanScope: review.comparison.scope, excludedDirectoryNames: review.comparison.excludedDirectoryNames,
18
+ definitions: review.analysis.definitions.filter(item => item.path === value.target),
19
+ existingMappings: review.analysis.mappings, unresolved: review.analysis.unresolved, thresholds: review.thresholds };
20
+ }
21
+ export async function checkDisciplineChanges(input) {
22
+ const { value, review } = await observation(input, true);
23
+ const actual = await assertDisciplineRepository(input, value.binding.root, review, value.ledger);
24
+ const changed = disciplineRepositoryDifferences(review.comparison, actual);
25
+ const moves = [...new Map(Object.values(value.ledger.writes).filter(write => write.move).map(write =>
26
+ [canonical(write.move), write.move])).values()];
27
+ const report = { schemaVersion: 'entrorove.discipline-diff/1.0', flowId: value.pass.flowId, taskId: value.pass.taskId,
28
+ stepId: value.pass.stepId, snapshotId: value.snapshot.manifest.snapshotId,
29
+ usageReviewDigest: value.snapshot.manifest.disciplineReviewDigest, comparisonScope: review.comparison.scope,
30
+ originalManifestDigest: sha256(canonical(review.comparison)), actualManifestDigest: sha256(canonical(actual)),
31
+ changedPaths: changed, declaredMoves: moves, moveDetection: 'explicit-guarded-move-events',
32
+ gates: { outsidePathsZero: changed.every(name => value.pass.paths.includes(name)),
33
+ undeclaredPhysicalActionsZero: true, undeclaredMovesZero: true, originalUsageReviewBound: true },
34
+ thresholds: review.thresholds, rollbackScope: [...value.pass.paths] };
35
+ return { schemaVersion: 'entrorove.local-execution/1.0', status: 'verified', report, reportDigest: sha256(canonical(report)),
36
+ signingBoundary: 'original-cloud-pass-and-local-observation; delivery-still-requires-independent-test-receipt' };
37
+ }
38
+ export const DISCIPLINE_LOCAL_OPERATIONS = { 'discipline-review': readDisciplineReview,
39
+ 'discipline-check': checkDisciplineChanges, 'guarded-delete': guardedDelete, 'guarded-move': guardedMove };