@entrorove/skill 0.1.23 → 0.1.24

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 (155) hide show
  1. package/AGREEMENT.md +6 -6
  2. package/CHANGELOG.md +5 -0
  3. package/SKILL.md +12 -4
  4. package/archguard-runtime-build.mjs +15 -0
  5. package/blueprint-criterion-runner.mjs +110 -0
  6. package/blueprint-criterion-wire.mjs +78 -0
  7. package/calctool-runtime-build.mjs +59 -0
  8. package/calctool-runtime-entry.mjs +92 -0
  9. package/cli.mjs +17 -82
  10. package/local-approved-plan.mjs +74 -15
  11. package/local-archguard-ast.mjs +371 -0
  12. package/local-archguard-checkpoint.mjs +123 -0
  13. package/local-archguard-components.mjs +177 -0
  14. package/local-archguard-contract.mjs +109 -0
  15. package/local-archguard-contracts.mjs +42 -0
  16. package/local-archguard-evaluate.mjs +120 -0
  17. package/local-archguard-language-ast.mjs +165 -0
  18. package/local-archguard-mutation-fence.mjs +28 -0
  19. package/local-archguard-operations.mjs +25 -0
  20. package/local-archguard-scope.mjs +64 -0
  21. package/local-archguard-signing.mjs +52 -0
  22. package/local-archguard-snapshot.mjs +126 -0
  23. package/local-archguard-state.mjs +200 -0
  24. package/local-archguard.mjs +74 -0
  25. package/local-budget-git-attributes.mjs +49 -0
  26. package/local-budget-git-loader.mjs +61 -0
  27. package/local-budget-git-native-attributes.mjs +81 -0
  28. package/local-budget-git-native-darwin.mjs +104 -0
  29. package/local-budget-git-native-linux.mjs +93 -0
  30. package/local-budget-git-native-reads.mjs +88 -0
  31. package/local-budget-git-native-request.mjs +53 -0
  32. package/local-budget-git-native-runtime.mjs +18 -0
  33. package/local-budget-git-native.c +313 -0
  34. package/local-budget-git-native.mjs +109 -0
  35. package/local-budget-git-outcome.mjs +33 -0
  36. package/local-budget-git-scratch-loader.mjs +158 -0
  37. package/local-budget-git-scratch.mjs +112 -0
  38. package/local-budget-git-system-attributes.mjs +44 -0
  39. package/local-budget-git.mjs +92 -0
  40. package/local-budget-grants.mjs +82 -0
  41. package/local-budget-handoff-completion.mjs +70 -0
  42. package/local-budget-handoff-contracts.mjs +53 -0
  43. package/local-budget-handoff.mjs +229 -0
  44. package/local-budget-lock.mjs +55 -0
  45. package/local-budget-reads.mjs +135 -0
  46. package/local-budget-renewal.mjs +86 -34
  47. package/local-budget-source-bindings.mjs +51 -0
  48. package/local-budget-store.mjs +131 -0
  49. package/local-budget-transition.mjs +46 -0
  50. package/local-budget.mjs +185 -0
  51. package/local-calctool-cli.mjs +22 -0
  52. package/local-calctool-contracts.mjs +42 -0
  53. package/local-calctool-runtime.mjs +10 -0
  54. package/local-calctool-storage.mjs +101 -0
  55. package/local-calctool.mjs +103 -0
  56. package/local-cli.mjs +49 -16
  57. package/local-contract-types.mjs +46 -0
  58. package/local-contracts.mjs +124 -0
  59. package/local-credential.mjs +101 -0
  60. package/local-decimal.mjs +85 -0
  61. package/local-evidence-verification.mjs +199 -0
  62. package/local-execution.mjs +115 -304
  63. package/local-final-gate16-bundle.mjs +21 -0
  64. package/local-final-gate16-checks.mjs +88 -0
  65. package/local-final-gate16-contract.mjs +15 -0
  66. package/local-final-gate16-plan.mjs +66 -0
  67. package/local-final-gate16-proof.mjs +75 -0
  68. package/local-final-gate16-release.mjs +17 -0
  69. package/local-final-gate16-state.mjs +70 -0
  70. package/local-final-gate16-worker.mjs +19 -0
  71. package/local-final-gate16.mjs +137 -0
  72. package/local-formula-ast.mjs +97 -0
  73. package/local-formula-engine.mjs +111 -0
  74. package/local-formula-tests.mjs +61 -0
  75. package/local-formula.mjs +25 -125
  76. package/local-integrity.mjs +22 -41
  77. package/local-invocations.mjs +107 -0
  78. package/local-ledger.mjs +2 -7
  79. package/local-merge-archguard-verification.d.mts +44 -0
  80. package/local-merge-archguard-verification.mjs +240 -0
  81. package/local-merge-archguard.mjs +138 -0
  82. package/local-merge-artifacts.mjs +86 -0
  83. package/local-merge-authority.mjs +89 -0
  84. package/local-merge-contracts.mjs +56 -0
  85. package/local-merge-controller.mjs +170 -0
  86. package/local-merge-head.mjs +56 -0
  87. package/local-merge-journal.mjs +77 -0
  88. package/local-merge-mutation.mjs +97 -0
  89. package/local-merge-rollback.mjs +126 -0
  90. package/local-merge-snapshot-rollback.mjs +26 -0
  91. package/local-merge-storage.mjs +103 -0
  92. package/local-merge-targets.mjs +47 -0
  93. package/local-merge-transaction.mjs +133 -0
  94. package/local-mutation-audit.mjs +34 -0
  95. package/local-mutation-authority.mjs +96 -0
  96. package/local-mutation-batch-receipt.mjs +84 -0
  97. package/local-mutation-batch.mjs +143 -0
  98. package/local-mutation-binding.mjs +86 -0
  99. package/local-mutation-files.mjs +245 -0
  100. package/local-mutation-pass-store.mjs +63 -0
  101. package/local-mutation-snapshot.mjs +200 -0
  102. package/local-mutation-wire.mjs +89 -0
  103. package/local-probe.mjs +133 -11
  104. package/local-scope-candidate.mjs +28 -0
  105. package/local-scope-context.mjs +28 -0
  106. package/local-scope-files.mjs +106 -0
  107. package/local-scope-git-attributes.mjs +200 -0
  108. package/local-scope-git-comparison.mjs +8 -0
  109. package/local-scope-git-history.mjs +72 -0
  110. package/local-scope-git-reads.mjs +66 -0
  111. package/local-scope-git.mjs +124 -0
  112. package/local-scope-repository.mjs +249 -0
  113. package/local-scope-signature.mjs +70 -0
  114. package/local-scope-snapshot.mjs +39 -0
  115. package/local-scope-state.mjs +204 -0
  116. package/local-scope-workspaces.mjs +83 -0
  117. package/local-swarm-authority.mjs +143 -0
  118. package/local-swarm-baseline.mjs +32 -0
  119. package/local-swarm-completion.mjs +102 -0
  120. package/local-swarm-contracts.mjs +47 -0
  121. package/local-swarm-mutation.mjs +142 -0
  122. package/local-swarm-priority.mjs +237 -0
  123. package/local-swarm-state.mjs +102 -0
  124. package/local-swarm-storage.mjs +135 -0
  125. package/local-swarm.mjs +179 -0
  126. package/local-task-handoff-completion-wire.mjs +71 -0
  127. package/local-task-handoff-contracts.mjs +66 -0
  128. package/local-task-handoff-fence.mjs +478 -0
  129. package/local-task-handoff-observation.mjs +38 -0
  130. package/local-task-handoff-overlay-wire.mjs +94 -0
  131. package/local-task-handoff-read-authority.mjs +236 -0
  132. package/local-task-handoff-storage.mjs +73 -0
  133. package/local-task-handoff-write-archive.mjs +80 -0
  134. package/local-task-handoff-write-constraints.mjs +66 -0
  135. package/local-task-handoff-write-receipts.mjs +99 -0
  136. package/local-task-handoff-write-wire.mjs +99 -0
  137. package/local-task-routing.mjs +202 -22
  138. package/manifest.json +1 -1
  139. package/package.json +153 -3
  140. package/release-state.json +153 -17
  141. package/release.mjs +238 -4
  142. package/releases.json +6 -0
  143. package/remote-client.mjs +127 -36
  144. package/remote-transport.mjs +80 -0
  145. package/review-client.mjs +38 -0
  146. package/runtime/archguard-runtime-v7.0.44.mjs +314 -0
  147. package/runtime/calctool-runtime-v7.0.43.mjs +1887 -0
  148. package/runtime/git-native-darwin-v1.mjs +5 -0
  149. package/runtime/git-native-linux-v1.mjs +5 -0
  150. package/task-message-cli.mjs +47 -0
  151. package/task-message-entry.mjs +159 -0
  152. package/workflow-local-client.mjs +35 -0
  153. package/workflow-local-contracts.mjs +58 -0
  154. package/workflow-local-request.mjs +53 -0
  155. package/workflow-local-runner.mjs +350 -0
@@ -0,0 +1,88 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { constants, promises as fs } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { compileFormulaEngine, validateFormulaEngine } from './local-formula-engine.mjs';
5
+ import { formulaFinalGate } from './local-formula-tests.mjs';
6
+
7
+ export const FINAL_GATE16_ROLE_SCHEMA = 'entrorove.calctool-role-result/1.0';
8
+ export const OPS_CONFIG16_SCHEMA = 'entrorove.calctool-ops-config/1.0';
9
+ export const FINAL_GATE16_ROLES = Object.freeze(['audit', 'test', 'ops']);
10
+ export const sha16 = value => createHash('sha256').update(value).digest('hex');
11
+ export function fail16(code, message) { throw Object.assign(new Error(message), { code }); }
12
+ export function exact16(value, fields, label) {
13
+ if (!value || typeof value !== 'object' || Array.isArray(value)
14
+ || Object.keys(value).sort().join('|') !== [...fields].sort().join('|')) fail16('FINAL_GATE16_FIELDS', `${label} requires its exact fields`);
15
+ return value;
16
+ }
17
+ export function relative16(value) {
18
+ if (typeof value !== 'string' || !value || value !== value.normalize('NFC') || path.isAbsolute(value)
19
+ || /^[A-Za-z]:/.test(value) || /[\\\u0000-\u001f\u007f]/.test(value)
20
+ || value.split('/').some(part => !part || part === '.' || part === '..')) fail16('FINAL_GATE16_PATH', 'Expected a normalized relative file path');
21
+ return value;
22
+ }
23
+ export async function readFile16(repositoryRoot, relativePath) {
24
+ if (typeof repositoryRoot !== 'string' || !path.isAbsolute(repositoryRoot)) fail16('FINAL_GATE16_ROOT', 'repositoryRoot must be absolute');
25
+ const root = await fs.realpath(repositoryRoot), target = path.join(root, relative16(relativePath));
26
+ if (await fs.realpath(target) !== target) fail16('FINAL_GATE16_FILE', 'Checker files cannot have symlinked ancestors');
27
+ const handle = await fs.open(target, constants.O_RDONLY | constants.O_NOFOLLOW);
28
+ try {
29
+ const stat = await handle.stat();
30
+ if (!stat.isFile() || stat.size > 1024 * 1024) fail16('FINAL_GATE16_FILE', 'Checker input must be a bounded regular file');
31
+ const bytes = await handle.readFile(), after = await handle.stat(), current = await fs.lstat(target);
32
+ if (stat.dev !== current.dev || stat.ino !== current.ino || stat.size !== after.size || stat.mtimeMs !== after.mtimeMs)
33
+ fail16('FINAL_GATE16_FILE_CHANGED', 'Checker input changed while reading');
34
+ return { path: relativePath, bytes, sha256: sha16(bytes) };
35
+ } finally { await handle.close(); }
36
+ }
37
+ export async function inputs16(input) {
38
+ const [engineFile, opsFile] = await Promise.all([readFile16(input.repositoryRoot, input.enginePath), readFile16(input.repositoryRoot, input.opsConfigPath)]);
39
+ let engine, ops;
40
+ try { engine = JSON.parse(engineFile.bytes); ops = JSON.parse(opsFile.bytes); }
41
+ catch { fail16('FINAL_GATE16_JSON', 'Engine and operations configuration must be valid JSON'); }
42
+ return { engine, ops, engineFile, opsFile };
43
+ }
44
+ async function executable16(config, label) {
45
+ exact16(config, label === 'package manager' ? ['name', 'path', 'sha256'] : ['path', 'sha256'], label);
46
+ if (typeof config.path !== 'string' || !path.isAbsolute(config.path) || !/^[0-9a-f]{64}$/.test(config.sha256 ?? ''))
47
+ fail16('FINAL_GATE16_EXECUTABLE', `${label} requires an absolute executable and SHA256`);
48
+ const resolved = await fs.realpath(config.path), stat = await fs.lstat(config.path);
49
+ if (resolved !== config.path || !stat.isFile() || stat.isSymbolicLink() || !(stat.mode & 0o111) || stat.size > 256 * 1024 * 1024)
50
+ fail16('FINAL_GATE16_EXECUTABLE', `${label} must be a real regular executable`);
51
+ await fs.access(config.path, constants.X_OK);
52
+ if (sha16(await fs.readFile(config.path)) !== config.sha256) fail16('FINAL_GATE16_EXECUTABLE_HASH', `${label} executable bytes changed`);
53
+ return resolved;
54
+ }
55
+ export async function operations16(ops) {
56
+ exact16(ops, ['schemaVersion', 'node', 'packageManager', 'commands'], 'operations configuration');
57
+ if (ops.schemaVersion !== OPS_CONFIG16_SCHEMA) fail16('FINAL_GATE16_OPS_SCHEMA', 'Operations configuration schema is invalid');
58
+ const node = await executable16(ops.node, 'Node'), manager = await executable16(ops.packageManager, 'package manager');
59
+ if (node !== await fs.realpath(process.execPath) || Number(process.versions.node.split('.')[0]) < 18)
60
+ fail16('FINAL_GATE16_NODE', 'The checker must actually run the approved Node 18+ executable');
61
+ if (!['npm', 'pnpm', 'yarn'].includes(ops.packageManager.name)
62
+ || ![ops.packageManager.name, `${ops.packageManager.name}.js`, `${ops.packageManager.name}.cjs`, `${ops.packageManager.name}-cli.js`].includes(path.basename(manager)))
63
+ fail16('FINAL_GATE16_PACKAGE_MANAGER', 'A supported actual package manager executable is required');
64
+ exact16(ops.commands, ['install', 'run'], 'operations commands');
65
+ for (const [name, args] of Object.entries(ops.commands)) if (!Array.isArray(args) || !args.length || args.length > 128
66
+ || args.some(value => typeof value !== 'string' || !value || value.length > 4096 || /[\u0000-\u001f\u007f]/.test(value))
67
+ || args[0] !== name || (name === 'run' && (args.length < 2 || !/^[A-Za-z0-9][A-Za-z0-9:_-]{0,127}$/.test(args[1]))))
68
+ fail16('FINAL_GATE16_COMMAND_CONFIG', 'Install and run commands require explicit bounded argv configuration');
69
+ return { passed: true, findings: [], checks: { nodeExecutable: true, packageManagerExecutable: true,
70
+ commandsConfigured: true, commandsExecuted: false }, actualNodeVersion: process.versions.node };
71
+ }
72
+ export async function role16(role, loaded, gateId) {
73
+ if (!FINAL_GATE16_ROLES.includes(role) || typeof gateId !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(gateId))
74
+ fail16('FINAL_GATE16_ROLE', 'The checker requires its original role and frozen gate ID');
75
+ const findings = validateFormulaEngine(loaded.engine); let program;
76
+ if (!findings.length) program = compileFormulaEngine(loaded.engine);
77
+ let result = { passed: findings.length === 0, findings, checks: { engineValid: findings.length === 0 } };
78
+ if (role === 'test' && program) {
79
+ const tested = formulaFinalGate(loaded.engine);
80
+ result = { passed: tested.computationsPassed, findings: tested.findings, tests: tested.tests,
81
+ checks: { benchmarkSamples: tested.tests.length, passingSamples: tested.tests.filter(value => value.passed).length } };
82
+ }
83
+ if (role === 'ops' && program) result = await operations16(loaded.ops);
84
+ return { schemaVersion: FINAL_GATE16_ROLE_SCHEMA, role, gateId, status: 'reported', ...result,
85
+ engineId: program?.engineId ?? loaded.engine?.engineId ?? null, engineVersion: program?.engineVersion ?? null,
86
+ engineSha256: loaded.engineFile.sha256, opsConfigSha256: loaded.opsFile.sha256,
87
+ engineDigest: program?.engineDigest ?? null, programDigest: program?.programDigest ?? null };
88
+ }
@@ -0,0 +1,15 @@
1
+ const text = { type: 'string', minLength: 1, maxLength: 4096 };
2
+ const identifier = { type: 'string', pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' };
3
+ const relative = { ...text, pattern: '^(?!/)(?![A-Za-z]:)(?!.*[\\u0000-\\u001f\\u007f])(?!.*\\\\)(?!.*(?:^|/)(?:\\.\\.?)(?:/|$)).+' };
4
+ const object = (properties, required = Object.keys(properties)) => ({ type: 'object', properties, required, additionalProperties: false });
5
+ const control = object({ planPath: relative, approvalPath: text, signerConfigPath: { anyOf: [text, { type: 'null' }] } });
6
+ export const FINAL_GATE16_INPUT_SCHEMA = Object.freeze(object({ repositoryRoot: text, home: text,
7
+ enginePath: relative, opsConfigPath: relative, gateId: identifier, flowId: identifier, stepId: identifier,
8
+ plans: object({ audit: control, test: control, ops: control }) }, ['repositoryRoot', 'enginePath', 'opsConfigPath', 'gateId', 'flowId', 'stepId', 'plans']));
9
+ export const FINAL_GATE16_READ_INPUT_SCHEMA = Object.freeze(object({ repositoryRoot: text, home: text,
10
+ gateId: identifier, flowId: identifier, stepId: identifier }, ['repositoryRoot', 'gateId', 'flowId', 'stepId']));
11
+ export const FINAL_GATE16_SCHEMA = 'entrorove.calctool-final-gate/2.0';
12
+ export const FINAL_GATE16_BUNDLE_PREFIX = 'entrorove-final-gate16/';
13
+ export const FINAL_GATE16_WORKER_PATH = FINAL_GATE16_BUNDLE_PREFIX + 'local-final-gate16-worker.mjs';
14
+ export const finalGate16Args = (role, enginePath, opsConfigPath) => [FINAL_GATE16_WORKER_PATH, role, enginePath, opsConfigPath];
15
+ export const finalGate16Tests = role => [{ testId: `calctool-final-gate16:${role}`, path: FINAL_GATE16_WORKER_PATH }];
@@ -0,0 +1,66 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import Ajv from 'ajv';
4
+ import { compileFormulaEngine, formulaCanonical } from './local-formula-engine.mjs';
5
+ import { FINAL_GATE16_INPUT_SCHEMA, finalGate16Args, finalGate16Tests } from './local-final-gate16-contract.mjs';
6
+ import { finalGate16Bundle } from './local-final-gate16-bundle.mjs';
7
+ import { exact16, fail16, inputs16, readFile16, sha16, FINAL_GATE16_ROLES } from './local-final-gate16-checks.mjs';
8
+
9
+ const validate = new Ajv({ strict: true, allErrors: true }).compile(FINAL_GATE16_INPUT_SCHEMA);
10
+ export const digest16 = value => sha16(formulaCanonical(value));
11
+ export function checkInput16(input) {
12
+ if (!validate(input)) fail16('FINAL_GATE16_INPUT', 'Final gate requires its exact repository, identity, source and three plan locators');
13
+ if (input.enginePath === input.opsConfigPath) fail16('FINAL_GATE16_INPUT', 'Engine and operations configuration must be different files');
14
+ return { ...input, home: input.home ?? homedir() };
15
+ }
16
+ export function validateFinalGate16RolePlan(plan, role, binding) {
17
+ const policy = plan?.policy, declaration = plan?.evidencePolicy;
18
+ if (plan?.schemaVersion !== 'validator.execution-plan/1.0' || plan.frozen !== true || !policy)
19
+ fail16('FINAL_GATE16_PLAN', 'Each role requires a frozen independent execution plan');
20
+ exact16(declaration, ['kind', 'runner', 'flowId', 'stepId'], 'role evidence policy');
21
+ if (declaration.kind !== 'test' || !['trusted-runner', 'sandbox'].includes(declaration.runner)
22
+ || declaration.flowId !== binding.flowId || (role === 'audit' && declaration.stepId !== binding.stepId))
23
+ fail16('FINAL_GATE16_ROLE_BINDING', 'Role evidence must retain its original gate context and actual signed workflow step');
24
+ if (formulaCanonical(policy.args) !== formulaCanonical(finalGate16Args(role, binding.enginePath, binding.opsConfigPath))
25
+ || formulaCanonical(plan.tests) !== formulaCanonical(finalGate16Tests(role))
26
+ || formulaCanonical(policy.environment) !== formulaCanonical({ ENTROROVE_FINAL_GATE16_ID: binding.gateId }))
27
+ fail16('FINAL_GATE16_ROLE_COMMAND', 'Each role must execute the fixed checker, tests and frozen gate ID');
28
+ if (policy.executable !== binding.ops.node?.path || policy.executableSha256 !== binding.ops.node?.sha256)
29
+ fail16('FINAL_GATE16_NODE_BINDING', 'Checker executable must be the actual Node configured in the frozen operations file');
30
+ const files = plan.files;
31
+ if (!Array.isArray(files) || !files.length || files.length > 512) fail16('FINAL_GATE16_MANIFEST', 'Role manifest must be nonempty and bounded');
32
+ for (const [index, file] of files.entries()) if (!file || Object.keys(file).sort().join('|') !== 'path|sha256'
33
+ || !/^[0-9a-f]{64}$/.test(file.sha256 ?? '') || typeof file.path !== 'string'
34
+ || (index && files[index - 1].path >= file.path)) fail16('FINAL_GATE16_MANIFEST', 'Role manifest must be sorted and unique');
35
+ for (const file of binding.requiredFiles) if (!files.some(value => value.path === file.path && value.sha256 === file.sha256))
36
+ fail16('FINAL_GATE16_CHECKER_MANIFEST', 'Role manifest omits or changes a fixed checker closure or original input');
37
+ const artifact = sha16(`validator.file-manifest/1.0\n${files.map(file => `${file.path}\0${file.sha256}`).join('\n')}`);
38
+ if (artifact !== plan.artifactSha256) fail16('FINAL_GATE16_MANIFEST', 'Role artifact must use the independent Validator manifest digest');
39
+ return plan;
40
+ }
41
+ export async function prepareFinalGate16(input) {
42
+ input = checkInput16(input);
43
+ const rootStat = await fs.lstat(input.repositoryRoot);
44
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) fail16('FINAL_GATE16_ROOT', 'Repository must be a real directory');
45
+ const loaded = await inputs16(input), program = compileFormulaEngine(loaded.engine), bundle = await finalGate16Bundle();
46
+ const requiredFiles = [{ path: input.enginePath, sha256: loaded.engineFile.sha256 },
47
+ { path: input.opsConfigPath, sha256: loaded.opsFile.sha256 }, ...bundle.map(({ path, sha256 }) => ({ path, sha256 }))];
48
+ if (new Set(requiredFiles.map(file => file.path)).size !== requiredFiles.length)
49
+ fail16('FINAL_GATE16_MANIFEST', 'Engine and operations configuration cannot replace the fixed checker closure');
50
+ const roles = {};
51
+ for (const role of FINAL_GATE16_ROLES) {
52
+ const file = await readFile16(input.repositoryRoot, input.plans[role].planPath); let plan;
53
+ try { plan = JSON.parse(file.bytes); } catch { fail16('FINAL_GATE16_PLAN_JSON', 'Role execution plan must be JSON'); }
54
+ validateFinalGate16RolePlan(plan, role, { ...input, ops: loaded.ops, requiredFiles });
55
+ for (const entry of plan.files) if ((await readFile16(input.repositoryRoot, entry.path)).sha256 !== entry.sha256)
56
+ fail16('FINAL_GATE16_SOURCE_CHANGED', 'A role manifest file differs from its frozen source');
57
+ roles[role] = { plan, planSha256: file.sha256, planBytes: file.bytes.toString('base64') };
58
+ }
59
+ const audit = roles.audit.plan;
60
+ if (new Set(FINAL_GATE16_ROLES.map(role => roles[role].plan.validationRunId)).size !== 3)
61
+ fail16('FINAL_GATE16_ROLE_IDENTITY', 'Each role requires a different signed execution identity');
62
+ for (const role of FINAL_GATE16_ROLES) for (const field of ['files', 'contracts', 'chainId', 'memberId'])
63
+ if (formulaCanonical(roles[role].plan[field]) !== formulaCanonical(audit[field]))
64
+ fail16('FINAL_GATE16_SHARED_BINDING', 'All role plans must retain the same source manifest, contracts, chain and member');
65
+ return { input: structuredClone(input), inputDigest: digest16(input), loaded, program, roles, requiredFiles };
66
+ }
@@ -0,0 +1,75 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { verifyTestEvidence } from './local-evidence-verification.mjs';
3
+ import { compileFormulaEngine, formulaCanonical } from './local-formula-engine.mjs';
4
+ import { FINAL_GATE16_ROLES, fail16, sha16, role16 } from './local-final-gate16-checks.mjs';
5
+ import { digest16, checkInput16, validateFinalGate16RolePlan } from './local-final-gate16-plan.mjs';
6
+ import { finalGate16Bundle } from './local-final-gate16-bundle.mjs';
7
+
8
+ const same16 = (a, b) => formulaCanonical(a) === formulaCanonical(b);
9
+ export function verifyRoleEvidence16(role, execution, prepared, now) {
10
+ const { plan, planSha256 } = prepared.roles[role], item = execution?.evidence?.[0], subject = item?.subject;
11
+ if (!execution || execution.evidence?.length !== 1 || execution.planSha256 !== planSha256 || !subject)
12
+ fail16('FINAL_GATE16_ROLE_RECEIPT', 'Each role requires its original single actual execution evidence');
13
+ for (const field of ['planId', 'validationRunId', 'memberId', 'chainId', 'artifactSha256', 'files', 'tests', 'contracts', 'goldenBaseline'])
14
+ if (!same16(subject[field], plan[field])) fail16('FINAL_GATE16_SIGNED_PLAN', 'Role evidence must retain the complete original frozen plan');
15
+ const command = [plan.policy.executable, ...plan.policy.args].map(value => JSON.stringify(value)).join(' ');
16
+ if (subject.policy?.executionPlanSha256 !== planSha256 || subject.policy?.command !== command
17
+ || subject.policy?.evidenceKind !== 'test' || subject.policy?.evidenceRunner !== plan.evidencePolicy.runner
18
+ || !same16(subject.policy?.workflowBinding, { flowId: prepared.input.flowId, stepId: plan.evidencePolicy.stepId }))
19
+ fail16('FINAL_GATE16_SIGNED_POLICY', 'Role evidence must retain the signed checker command and workflow identity');
20
+ const process = execution.process;
21
+ for (const field of ['exitCode', 'signal', 'timedOut', 'outputLimitExceeded'])
22
+ if (!same16(subject.execution?.[field], process?.[field])) fail16('FINAL_GATE16_PROCESS', 'Actual role process differs from its signed execution');
23
+ if (process.durationMs !== item.durationMs || sha16(process.stdout) !== process.stdoutSha256 || sha16(process.stderr) !== process.stderrSha256)
24
+ fail16('FINAL_GATE16_PROCESS', 'Actual process metadata must retain its original execution evidence');
25
+ return verifyTestEvidence(item, { publicKey: processEnvPin16(), now,
26
+ binding: { flowId: prepared.input.flowId, stepId: plan.evidencePolicy.stepId, artifactSha256: plan.artifactSha256 } });
27
+ }
28
+ export function processEnvPin16() { return process.env.ENTROROVE_VALIDATOR_RECEIPT_PUBLIC_KEY || undefined; }
29
+ export function snapshotPrepared16(prepared) {
30
+ return { input: prepared.input, inputDigest: prepared.inputDigest, roles: prepared.roles,
31
+ engineBytes: prepared.loaded.engineFile.bytes.toString('base64'), opsBytes: prepared.loaded.opsFile.bytes.toString('base64') };
32
+ }
33
+ function originalBytes16(value) {
34
+ if (typeof value !== 'string') fail16('FINAL_GATE16_HISTORY_BYTES', 'Original source bytes must be retained');
35
+ const bytes = Buffer.from(value, 'base64');
36
+ if (bytes.toString('base64') !== value) fail16('FINAL_GATE16_HISTORY_BYTES', 'Original source encoding is invalid');
37
+ return bytes;
38
+ }
39
+ async function assertHistoricalNode16(ops) {
40
+ const executable = await fs.realpath(process.execPath), stat = await fs.stat(executable);
41
+ if (!stat.isFile() || stat.size > 256 * 1024 * 1024 || !(stat.mode & 0o111)
42
+ || ops?.node?.path !== executable || ops.node.sha256 !== sha16(await fs.readFile(executable)))
43
+ fail16('FINAL_GATE16_HISTORY_NODE', 'Historical checker evidence requires the original independently identified host Node executable; changed runtime identity needs trusted review');
44
+ }
45
+ export async function restorePrepared16(record) {
46
+ const saved = record.prepared, input = checkInput16(saved.input);
47
+ if (digest16(input) !== saved.inputDigest) fail16('FINAL_GATE16_HISTORY_INPUT', 'Original gate input binding changed');
48
+ const engineBytes = originalBytes16(saved.engineBytes), opsBytes = originalBytes16(saved.opsBytes);
49
+ const loaded = { engine: JSON.parse(engineBytes), ops: JSON.parse(opsBytes),
50
+ engineFile: { bytes: engineBytes, sha256: sha16(engineBytes) }, opsFile: { bytes: opsBytes, sha256: sha16(opsBytes) } };
51
+ await assertHistoricalNode16(loaded.ops);
52
+ const requiredFiles = [{ path: input.enginePath, sha256: loaded.engineFile.sha256 }, { path: input.opsConfigPath, sha256: loaded.opsFile.sha256 },
53
+ ...(await finalGate16Bundle()).map(({ path, sha256 }) => ({ path, sha256 }))];
54
+ for (const role of FINAL_GATE16_ROLES) {
55
+ const entry = saved.roles[role], bytes = originalBytes16(entry.planBytes);
56
+ if (entry.planSha256 !== sha16(bytes) || !same16(JSON.parse(bytes), entry.plan))
57
+ fail16('FINAL_GATE16_HISTORY_PLAN', 'Original signed role plan bytes changed');
58
+ validateFinalGate16RolePlan(entry.plan, role, { ...input, ops: loaded.ops, requiredFiles });
59
+ }
60
+ const audit = saved.roles.audit.plan;
61
+ if (new Set(FINAL_GATE16_ROLES.map(role => saved.roles[role].plan.validationRunId)).size !== 3)
62
+ fail16('FINAL_GATE16_HISTORY_PLAN', 'Original role identities changed');
63
+ for (const role of FINAL_GATE16_ROLES) for (const field of ['files', 'contracts', 'chainId', 'memberId'])
64
+ if (!same16(saved.roles[role].plan[field], audit[field])) fail16('FINAL_GATE16_HISTORY_PLAN', 'Original shared role binding changed');
65
+ return { input, inputDigest: saved.inputDigest, loaded, program: compileFormulaEngine(loaded.engine), roles: saved.roles, requiredFiles };
66
+ }
67
+ export async function historicalRole16(role, prepared, original) {
68
+ if (role !== 'ops') return role16(role, prepared.loaded, prepared.input.gateId);
69
+ const audit = await role16('audit', prepared.loaded, prepared.input.gateId);
70
+ if (!original?.passed || !same16(original.checks, { nodeExecutable: true, packageManagerExecutable: true, commandsConfigured: true, commandsExecuted: false }))
71
+ fail16('FINAL_GATE16_HISTORY_OPS', 'Original successful operations check changed');
72
+ const result = { ...audit, role: 'ops', passed: true, findings: [], checks: original.checks, actualNodeVersion: original.actualNodeVersion };
73
+ if (!same16(result, original)) fail16('FINAL_GATE16_HISTORY_OPS', 'Original operations source binding changed');
74
+ return result;
75
+ }
@@ -0,0 +1,17 @@
1
+ export const FINAL_GATE16_PACKAGE_FILES = Object.freeze([
2
+ "local-final-gate16.mjs",
3
+ "local-final-gate16-checks.mjs",
4
+ "local-final-gate16-worker.mjs",
5
+ "local-final-gate16-plan.mjs",
6
+ "local-final-gate16-bundle.mjs",
7
+ "local-final-gate16-state.mjs",
8
+ "local-final-gate16-proof.mjs",
9
+ "local-final-gate16-contract.mjs",
10
+ "local-final-gate16-release.mjs"
11
+ ]);
12
+
13
+ export function withFinalGate16ReleaseLabels(labels) {
14
+ const note = { zh: 'Calctool 审计测试运维真实检查与独立最终闸门',
15
+ en: 'Calctool audit, test, operations checks and independent final gate' };
16
+ return { ...labels, ...Object.fromEntries(FINAL_GATE16_PACKAGE_FILES.map(name => [name, note])) };
17
+ }
@@ -0,0 +1,70 @@
1
+ import { constants, promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { digest16 } from './local-final-gate16-plan.mjs';
6
+ import { fail16, sha16 } from './local-final-gate16-checks.mjs';
7
+
8
+ const STATE_SCHEMA = 'entrorove.calctool-final-gate-state/1.0';
9
+ const MAX_BYTES = 16 * 1024 * 1024;
10
+ function privateStat16(stat, directory) {
11
+ if ((directory ? !stat.isDirectory() : !stat.isFile()) || stat.isSymbolicLink()
12
+ || stat.uid !== process.getuid() || (stat.mode & 0o777) !== (directory ? 0o700 : 0o600))
13
+ fail16('FINAL_GATE16_PRIVATE_STATE', 'Gate state must retain its actual private owner and permissions');
14
+ }
15
+ export async function statePath16(input, create = false) {
16
+ if (typeof process.getuid !== 'function') fail16('FINAL_GATE16_POSIX', 'Private gate recovery requires POSIX');
17
+ const home = path.resolve(input.home ?? homedir()), root = path.resolve(input.repositoryRoot);
18
+ if (await fs.realpath(home) !== home || await fs.realpath(root) !== root)
19
+ fail16('FINAL_GATE16_STATE_PATH', 'Gate home and repository cannot have symlinked ancestors');
20
+ let directory = home;
21
+ for (const part of ['.entrorove', 'calctool', 'final-gates', sha16(root), input.flowId]) {
22
+ directory = path.join(directory, part);
23
+ if (create) { try { await fs.mkdir(directory, { mode: 0o700 }); } catch (error) { if (error.code !== 'EEXIST') throw error; } }
24
+ try { privateStat16(await fs.lstat(directory), true); }
25
+ catch (error) { if (!create && error.code === 'ENOENT') return null; throw error; }
26
+ }
27
+ return path.join(directory, `${input.gateId}.json`);
28
+ }
29
+ function encoded16(record) {
30
+ const data = { schemaVersion: STATE_SCHEMA, record, recordDigest: digest16(record) };
31
+ const bytes = Buffer.from(JSON.stringify(data) + '\n');
32
+ if (bytes.length > MAX_BYTES) fail16('FINAL_GATE16_STATE_SIZE', 'Gate recovery state is bounded');
33
+ return bytes;
34
+ }
35
+ export async function readState16(input) {
36
+ const target = await statePath16(input); if (!target) return null;
37
+ let handle;
38
+ try { handle = await fs.open(target, constants.O_RDONLY | constants.O_NOFOLLOW); }
39
+ catch (error) { if (error.code === 'ENOENT') return null; throw error; }
40
+ try {
41
+ const stat = await handle.stat(); privateStat16(stat, false);
42
+ if (stat.size > MAX_BYTES) fail16('FINAL_GATE16_STATE_SIZE', 'Gate recovery state is bounded');
43
+ const bytes = await handle.readFile(), current = await fs.lstat(target);
44
+ if (current.ino !== stat.ino || current.dev !== stat.dev || current.size !== stat.size)
45
+ fail16('FINAL_GATE16_STATE_CHANGED', 'Gate state changed while reading');
46
+ let value; try { value = JSON.parse(bytes); } catch { fail16('FINAL_GATE16_STATE_JSON', 'Gate recovery state is invalid'); }
47
+ if (Object.keys(value).sort().join('|') !== 'record|recordDigest|schemaVersion'
48
+ || value.schemaVersion !== STATE_SCHEMA || value.recordDigest !== digest16(value.record))
49
+ fail16('FINAL_GATE16_STATE_DIGEST', 'Gate recovery state digest does not match');
50
+ return value.record;
51
+ } finally { await handle.close(); }
52
+ }
53
+ export async function createState16(input, record) {
54
+ const target = await statePath16(input, true); let handle;
55
+ try { handle = await fs.open(target, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600); }
56
+ catch (error) { if (error.code === 'EEXIST') return false; throw error; }
57
+ try { await handle.writeFile(encoded16(record)); await handle.sync(); }
58
+ finally { await handle.close(); }
59
+ return true;
60
+ }
61
+ export async function writeState16(input, record) {
62
+ const target = await statePath16(input), temporary = `${target}.${randomUUID()}.tmp`;
63
+ if (!target) fail16('FINAL_GATE16_STATE_MISSING', 'Gate state must exist before execution');
64
+ privateStat16(await fs.lstat(target), false);
65
+ const handle = await fs.open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
66
+ try { await handle.writeFile(encoded16(record)); await handle.sync(); }
67
+ finally { await handle.close(); }
68
+ try { await fs.rename(temporary, target); const directory = await fs.open(path.dirname(target), constants.O_RDONLY); try { await directory.sync(); } finally { await directory.close(); } }
69
+ finally { await fs.rm(temporary, { force: true }); }
70
+ }
@@ -0,0 +1,19 @@
1
+ import { resolve } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { inputs16, role16, fail16 } from './local-final-gate16-checks.mjs';
4
+
5
+ export async function runFinalGate16Worker(args = process.argv.slice(2)) {
6
+ if (args.length !== 3) fail16('FINAL_GATE16_WORKER_ARGS', 'Expected role, engine path and operations configuration path');
7
+ const [role, enginePath, opsConfigPath] = args;
8
+ const loaded = await inputs16({ repositoryRoot: process.cwd(), enginePath, opsConfigPath });
9
+ return role16(role, loaded, process.env.ENTROROVE_FINAL_GATE16_ID);
10
+ }
11
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
12
+ try {
13
+ const result = await runFinalGate16Worker();
14
+ process.stdout.write(JSON.stringify(result) + '\n'); process.exitCode = result.passed ? 0 : 2;
15
+ } catch (error) {
16
+ process.stderr.write(JSON.stringify({ status: 'blocked', code: error.code ?? 'FINAL_GATE16_WORKER', message: error.message }) + '\n');
17
+ process.exitCode = 2;
18
+ }
19
+ }
@@ -0,0 +1,137 @@
1
+ import Ajv from 'ajv';
2
+ import { homedir } from 'node:os';
3
+ import { runApprovedPlan } from './local-approved-plan.mjs';
4
+ import { FINAL_GATE16_READ_INPUT_SCHEMA, FINAL_GATE16_SCHEMA } from './local-final-gate16-contract.mjs';
5
+ import { FINAL_GATE16_ROLES, fail16, role16 } from './local-final-gate16-checks.mjs';
6
+ import { checkInput16, prepareFinalGate16, digest16 } from './local-final-gate16-plan.mjs';
7
+ import { createState16, readState16, writeState16 } from './local-final-gate16-state.mjs';
8
+ import { verifyRoleEvidence16, snapshotPrepared16, restorePrepared16, historicalRole16 } from './local-final-gate16-proof.mjs';
9
+
10
+ const validateRead = new Ajv({ strict: true, allErrors: true }).compile(FINAL_GATE16_READ_INPUT_SCHEMA);
11
+ function response16(input, status, details = {}) {
12
+ return { schemaVersion: FINAL_GATE16_SCHEMA, status, guarantee: 'await-server-for-workflow',
13
+ gateId: input?.gateId ?? null, flowId: input?.flowId ?? null, stepId: input?.stepId ?? null,
14
+ historicalRecovery: false, currentAuthorization: false, ...details };
15
+ }
16
+ function failure16(input, error, details = {}) {
17
+ return response16(input, 'blocked', { code: error.code ?? 'FINAL_GATE16_FAILED', reason: error.message, ...details });
18
+ }
19
+ function matchRecord16(input, record, full) {
20
+ const original = record.prepared?.input;
21
+ for (const field of ['repositoryRoot', 'home', 'flowId', 'stepId', 'gateId'])
22
+ if (input[field] !== original?.[field]) fail16('FINAL_GATE16_RECOVERY_BINDING', 'Gate recovery must retain its original repository, home and workflow identity');
23
+ if (full && digest16(input) !== record.prepared.inputDigest)
24
+ fail16('FINAL_GATE16_RECOVERY_BINDING', 'The same gate ID cannot execute or recover different input');
25
+ }
26
+ function checkHistoricalRoles16(record, prepared) {
27
+ if (!record.roles || Object.keys(record.roles).some(role => !FINAL_GATE16_ROLES.includes(role)))
28
+ fail16('FINAL_GATE16_HISTORY_ROLE', 'Historical roles require their exact original identities');
29
+ for (const role of Object.keys(record.roles)) {
30
+ const original = record.roles[role], response = original.response, execution = original.execution;
31
+ if (!response || Object.keys(response).sort().join('|') !== 'evidence|process|result|status|verifiedAt'
32
+ || !['verified', 'blocked', 'incomplete'].includes(response.status) || original.result?.role !== role
33
+ || original.result?.gateId !== prepared.input.gateId || original.result?.status !== 'reported'
34
+ || digest16(response.process) !== digest16(execution.process) || digest16(response.evidence) !== digest16(execution.evidence)
35
+ || digest16(response.result) !== digest16(original.result))
36
+ fail16('FINAL_GATE16_HISTORY_ROLE', 'Historical role output differs from its original execution');
37
+ const item = execution.evidence[0], time = response.verifiedAt === null ? Date.parse(item?.receipt?.issuedAt) : Date.parse(response.verifiedAt);
38
+ const verified = verifyRoleEvidence16(role, execution, prepared, Number.isSafeInteger(time) ? time : Date.now());
39
+ if (item.receipt && !verified.verified) fail16('FINAL_GATE16_HISTORY_RECEIPT', 'Original role receipt must verify with the configured independent trust pin');
40
+ if (response.status === 'verified' && (!verified.successful || !original.result.passed || time < Date.parse(item.receipt.issuedAt)))
41
+ fail16('FINAL_GATE16_HISTORY_ROLE', 'Historical verified role lacks its original successful evidence');
42
+ }
43
+ }
44
+ async function recovered16(input, record, full) {
45
+ matchRecord16(input, record, full); const prepared = await restorePrepared16(record);
46
+ if (!['pending', 'finished'].includes(record.state)) fail16('FINAL_GATE16_HISTORY_STATE', 'Original gate execution state is invalid');
47
+ if (record.state !== 'finished') return response16(input, 'incomplete', { historicalRecovery: true,
48
+ executionState: 'unknown', reason: 'This gate was already admitted; its interrupted execution must not be repeated', roles: record.roles ?? {} });
49
+ const output = record.output;
50
+ if (output?.schemaVersion !== FINAL_GATE16_SCHEMA || !['verified', 'blocked', 'incomplete'].includes(output.status)
51
+ || output.gateId !== input.gateId || output.flowId !== input.flowId || output.stepId !== input.stepId
52
+ || output.guarantee !== 'await-server-for-workflow') fail16('FINAL_GATE16_HISTORY_RESULT', 'Historical gate identity or status changed');
53
+ checkHistoricalRoles16(record, prepared);
54
+ if (output.status !== 'verified') return response16(input, output.status, { historicalRecovery: true,
55
+ ...Object.fromEntries(['roles', 'verifiedAt', 'engineSha256', 'opsConfigSha256', 'engineDigest', 'programDigest', 'reason', 'code', 'executionState']
56
+ .filter(key => output[key] !== undefined).map(key => [key, output[key]])) });
57
+ const now = Date.parse(output.verifiedAt);
58
+ if (!Number.isSafeInteger(now)) fail16('FINAL_GATE16_HISTORY_TIME', 'The original verification time is required');
59
+ if (!output.roles || Object.keys(output.roles).sort().join('|') !== 'audit|ops|test')
60
+ fail16('FINAL_GATE16_HISTORY_ROLE', 'Verified gate history requires exactly its three original roles');
61
+ for (const role of FINAL_GATE16_ROLES) {
62
+ const original = record.roles[role], issuedAt = Date.parse(original?.execution?.evidence?.[0]?.receipt?.issuedAt);
63
+ if (now < issuedAt || now < Date.parse(original?.execution?.subject?.executedAt))
64
+ fail16('FINAL_GATE16_HISTORY_TIME', 'Historical verification cannot precede original role execution');
65
+ const verified = verifyRoleEvidence16(role, original.execution, prepared, now);
66
+ if (!verified.verified || !verified.successful) fail16('FINAL_GATE16_HISTORY_RECEIPT', 'Original independent role receipt no longer verifies with the configured trust pin');
67
+ const actual = await historicalRole16(role, prepared, original.result);
68
+ if (digest16(actual) !== digest16(original.result) || !actual.passed || original.response.status !== 'verified'
69
+ || now < Date.parse(original.response.verifiedAt) || digest16(output.roles[role]) !== digest16(original.response))
70
+ fail16('FINAL_GATE16_HISTORY_RESULT', 'Historical role conclusion changed');
71
+ }
72
+ return response16(input, 'verified', { historicalRecovery: true, roles: output.roles, verifiedAt: output.verifiedAt,
73
+ engineSha256: prepared.loaded.engineFile.sha256, opsConfigSha256: prepared.loaded.opsFile.sha256,
74
+ engineDigest: prepared.program.engineDigest, programDigest: prepared.program.programDigest });
75
+ }
76
+ export async function readFinalGate16(input) {
77
+ try {
78
+ if (!validateRead(input)) fail16('FINAL_GATE16_READ_INPUT', 'Gate reads require their exact original repository and identity');
79
+ input = { ...input, home: input.home ?? homedir() }; const record = await readState16(input);
80
+ return record ? await recovered16(input, record, false) : response16(input, 'incomplete', { historicalRecovery: true,
81
+ executionState: 'not-started', reason: 'There is no original gate execution to recover' });
82
+ } catch (error) { return failure16(input, error, { historicalRecovery: true }); }
83
+ }
84
+ async function performRole16(role, prepared) {
85
+ const input = prepared.input, control = input.plans[role];
86
+ const execution = await runApprovedPlan({ repositoryRoot: input.repositoryRoot, home: input.home, flowId: input.flowId, ...control });
87
+ let result;
88
+ try { result = await role16(role, prepared.loaded, input.gateId); }
89
+ catch (error) { result = { role, gateId: input.gateId, status: 'reported', passed: false,
90
+ findings: [{ code: error.code ?? 'FINAL_GATE16_ROLE_FAILED', message: error.message }] }; }
91
+ const now = Date.now();
92
+ const verification = verifyRoleEvidence16(role, execution, prepared, now);
93
+ const status = result.passed && verification.verified && verification.successful ? 'verified'
94
+ : execution.process?.exitCode !== 0 || !result.passed || verification.status === 'blocked' ? 'blocked' : 'incomplete';
95
+ const response = { status, result, process: execution.process, evidence: execution.evidence,
96
+ verifiedAt: status === 'verified' ? new Date(now).toISOString() : null };
97
+ return { execution, result, response };
98
+ }
99
+ function finished16(input, prepared, record) {
100
+ const now = Date.now(), roles = Object.fromEntries(FINAL_GATE16_ROLES.map(role => [role, record.roles[role].response]));
101
+ let status = FINAL_GATE16_ROLES.every(role => roles[role].status === 'verified') ? 'verified'
102
+ : FINAL_GATE16_ROLES.some(role => roles[role].status === 'blocked') ? 'blocked' : 'incomplete';
103
+ if (status === 'verified') for (const role of FINAL_GATE16_ROLES) {
104
+ const verified = verifyRoleEvidence16(role, record.roles[role].execution, prepared, now);
105
+ if (!verified.verified || !verified.successful) { status = verified.status === 'blocked' ? 'blocked' : 'incomplete'; roles[role] = { ...roles[role], status, verification: verified }; }
106
+ }
107
+ return response16(input, status, { roles, verifiedAt: status === 'verified' ? new Date(now).toISOString() : null,
108
+ currentAuthorization: status === 'verified', engineSha256: prepared.loaded.engineFile.sha256,
109
+ opsConfigSha256: prepared.loaded.opsFile.sha256, engineDigest: prepared.program.engineDigest,
110
+ programDigest: prepared.program.programDigest });
111
+ }
112
+ export async function runFinalGate16(input) {
113
+ if (validateRead(input)) return readFinalGate16(input);
114
+ let record, admitted = false;
115
+ try {
116
+ input = checkInput16(input); const existing = await readState16(input);
117
+ if (existing) return await recovered16(input, existing, true);
118
+ const prepared = await prepareFinalGate16(input);
119
+ record = { state: 'pending', prepared: snapshotPrepared16(prepared), roles: {}, admittedAt: new Date().toISOString(), activeRole: null };
120
+ admitted = await createState16(input, record);
121
+ if (!admitted) return await recovered16(input, await readState16(input), true);
122
+ for (const role of FINAL_GATE16_ROLES) {
123
+ record.activeRole = role; await writeState16(input, record);
124
+ record.roles[role] = await performRole16(role, prepared); await writeState16(input, record);
125
+ }
126
+ record.output = finished16(input, prepared, record); record.state = 'finished'; record.activeRole = null;
127
+ await writeState16(input, record); return record.output;
128
+ } catch (error) {
129
+ const unknown = Boolean(error.execution && error.execution.effects !== 'none');
130
+ const output = unknown ? response16(input, 'incomplete', { executionState: 'unknown', reason: error.message }) : failure16(input, error);
131
+ if (admitted && record) {
132
+ record.state = unknown ? 'pending' : 'finished'; record.output = output;
133
+ try { await writeState16(input, record); } catch { return response16(input, 'incomplete', { executionState: 'unknown', reason: 'Gate execution cannot safely finish its original recovery record' }); }
134
+ }
135
+ return output;
136
+ }
137
+ }
@@ -0,0 +1,97 @@
1
+ import { Decimal, formulaError } from './local-decimal.mjs';
2
+
3
+ export const FORMULA_OPERATORS = Object.freeze(['add', 'sub', 'mul', 'div', 'safeDivide', 'percentOf', 'round', 'if']);
4
+ const ARITY = { add: [0, 1000], mul: [0, 1000], sub: [2, 2], div: [2, 2], safeDivide: [2, 2], percentOf: [2, 2], round: [1, 1], if: [3, 3] };
5
+ export function formulaKey(value) {
6
+ if (typeof value !== 'string' || !value.trim() || value.length > 128 || /[\u0000-\u001f\u007f]/.test(value)) throw formulaError('FORMULA_KEY_INVALID', 'formula and field keys must be non-empty strings of at most 128 characters');
7
+ return value;
8
+ }
9
+ export function formulaObject(value, field) {
10
+ if (!value || typeof value !== 'object' || Array.isArray(value) || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) throw formulaError('FORMULA_NODE_INVALID', `${field} must be a plain object`);
11
+ }
12
+ export function normalizeFormula(node, budget = { nodes: 0, active: new Set() }, depth = 0) {
13
+ formulaObject(node, 'formula node');
14
+ if (depth > 64 || ++budget.nodes > 10000) throw formulaError('FORMULA_LIMIT', 'formula exceeds its depth or node limit');
15
+ if (budget.active.has(node)) throw formulaError('FORMULA_CYCLE', 'formula AST contains an object cycle');
16
+ const keys = Object.keys(node);
17
+ if (keys.length === 1 && Object.hasOwn(node, 'ref')) return { ref: formulaKey(node.ref) };
18
+ if (keys.length === 1 && Object.hasOwn(node, 'lit')) return { lit: Decimal.from(node.lit).toString() };
19
+ if (!Object.hasOwn(ARITY, node.op)) throw formulaError('UNSUPPORTED_OPERATOR', `Unsupported operator: ${String(node.op)}`);
20
+ if (keys.length !== 2 || !keys.includes('args') || !keys.includes('op')) throw formulaError('FORMULA_NODE_INVALID', 'operator nodes contain only op and args');
21
+ const [minimum, maximum] = ARITY[node.op];
22
+ if (!Array.isArray(node.args) || node.args.length < minimum || node.args.length > maximum || node.args.some((_, i) => !Object.hasOwn(node.args, i))) throw formulaError('FORMULA_ARGS_INVALID', `${node.op} requires ${minimum === maximum ? minimum : `${minimum}..${maximum}`} arguments`);
23
+ budget.active.add(node);
24
+ const args = Array.from(node.args, item => normalizeFormula(item, budget, depth + 1));
25
+ budget.active.delete(node);
26
+ return { op: node.op, args };
27
+ }
28
+ export function formulaRefs(node, output = new Set()) {
29
+ if (Object.hasOwn(node, 'ref')) output.add(node.ref);
30
+ if (node.args) for (const item of node.args) formulaRefs(item, output);
31
+ return [...output].sort();
32
+ }
33
+ function evaluateNode(node, resolve) {
34
+ if (Object.hasOwn(node, 'lit')) return Decimal.from(node.lit);
35
+ if (Object.hasOwn(node, 'ref')) return Decimal.from(resolve(node.ref));
36
+ const [left, right, alternate] = node.args;
37
+ switch (node.op) {
38
+ case 'add': return node.args.reduce((total, item) => total.plus(evaluateNode(item, resolve)), Decimal.from('0'));
39
+ case 'mul': return node.args.reduce((total, item) => total.times(evaluateNode(item, resolve)), Decimal.from('1'));
40
+ case 'sub': return evaluateNode(left, resolve).minus(evaluateNode(right, resolve));
41
+ case 'div': return evaluateNode(left, resolve).div(evaluateNode(right, resolve));
42
+ case 'safeDivide': { const denominator = evaluateNode(right, resolve); return denominator.isZero() ? Decimal.from('0') : evaluateNode(left, resolve).div(denominator); }
43
+ case 'percentOf': return evaluateNode(left, resolve).div(evaluateNode(right, resolve)).times('100');
44
+ case 'round': return evaluateNode(left, resolve).rounded(2);
45
+ case 'if': return evaluateNode(left, resolve).isZero() ? evaluateNode(alternate, resolve) : evaluateNode(right, resolve);
46
+ default: throw formulaError('UNSUPPORTED_OPERATOR', 'unsupported normalized formula operator');
47
+ }
48
+ }
49
+ export function evaluateFormula(node, values = {}) {
50
+ formulaObject(values, 'formula values');
51
+ return evaluateNode(normalizeFormula(node), key => {
52
+ if (!Object.hasOwn(values, key) || values[key] === null || values[key] === undefined || values[key] === '') throw formulaError('MISSING_INPUT', `missing input: ${key}`, { fieldId: key });
53
+ return values[key];
54
+ });
55
+ }
56
+ export function compileFormulaGraph(formulas, fieldKeys) {
57
+ if (!Array.isArray(formulas) || formulas.length > 1000) throw formulaError('FORMULA_GRAPH_INVALID', 'formulas must be an array of at most 1000 definitions');
58
+ if (fieldKeys !== undefined && !Array.isArray(fieldKeys)) throw formulaError('FORMULA_GRAPH_INVALID', 'fieldKeys must be an array');
59
+ const fields = fieldKeys === undefined ? undefined : new Set(fieldKeys.map(formulaKey));
60
+ const definitions = new Map(); const budget = { nodes: 0, active: new Set() };
61
+ for (const item of formulas) {
62
+ formulaObject(item, 'formula definition'); const key = formulaKey(item.key);
63
+ if (definitions.has(key) || fields?.has(key)) throw formulaError('FORMULA_KEY_INVALID', 'formula keys must be unique and separate from input fields');
64
+ definitions.set(key, { key, expression: normalizeFormula(item.expression, budget) });
65
+ }
66
+ const dependencies = Object.fromEntries([...definitions].map(([key, item]) => [key, formulaRefs(item.expression)]));
67
+ const visiting = new Set(); const done = new Set(); const order = [];
68
+ const visit = key => {
69
+ if (done.has(key)) return;
70
+ if (visiting.has(key)) throw formulaError('FORMULA_CYCLE', `formula cycle at ${key}`);
71
+ visiting.add(key);
72
+ for (const ref of dependencies[key]) {
73
+ if (definitions.has(ref)) visit(ref);
74
+ else if (fields && !fields.has(ref)) throw formulaError('FORMULA_REF_UNKNOWN', `unknown formula reference: ${ref}`);
75
+ }
76
+ visiting.delete(key); done.add(key); order.push(key);
77
+ };
78
+ for (const key of [...definitions.keys()].sort()) visit(key);
79
+ return { formulas: [...definitions.values()].sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0), dependencies, order };
80
+ }
81
+ export function evaluateFormulaGraph(formulas, inputs = {}) {
82
+ formulaObject(inputs, 'formula inputs');
83
+ const values = new Map(Object.entries(inputs).map(([key, value]) => [key, Decimal.from(value)]));
84
+ const graph = compileFormulaGraph(formulas);
85
+ const definitions = new Map(graph.formulas.map(item => [item.key, item.expression]));
86
+ const results = new Map(); const visiting = new Set();
87
+ const resolve = key => {
88
+ if (results.has(key)) return results.get(key);
89
+ if (!definitions.has(key)) {
90
+ if (!values.has(key)) throw formulaError('MISSING_INPUT', `missing input: ${key}`, { fieldId: key });
91
+ return values.get(key);
92
+ }
93
+ if (visiting.has(key)) throw formulaError('FORMULA_CYCLE', `formula cycle at ${key}`);
94
+ visiting.add(key); const value = evaluateNode(definitions.get(key), resolve); visiting.delete(key); results.set(key, value); return value;
95
+ };
96
+ return Object.fromEntries(graph.formulas.map(item => [item.key, resolve(item.key).toString()]));
97
+ }