@entrorove/skill 0.1.24 → 0.1.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CAPABILITY-MAP.md +6 -0
- package/CHANGELOG.md +5 -0
- package/SKILL.md +7 -1
- package/calctool-runtime-entry.mjs +2 -2
- package/cli.mjs +18 -1
- package/local-archguard-checkpoint.mjs +1 -1
- package/local-archguard-snapshot.mjs +12 -12
- package/local-calctool-cli.mjs +3 -1
- package/local-calctool-runtime.mjs +1 -1
- package/local-calctool.mjs +1 -1
- package/local-chain-bindings.mjs +93 -0
- package/local-chain-broker.mjs +61 -0
- package/local-chain-contracts.mjs +21 -0
- package/local-chain-controller.mjs +321 -0
- package/local-chain-human.mjs +48 -0
- package/local-chain-lock.mjs +54 -0
- package/local-chain-operations.mjs +7 -0
- package/local-chain-plan.mjs +232 -0
- package/local-chain-release-labels.mjs +12 -0
- package/local-chain-results.mjs +75 -0
- package/local-cli.mjs +23 -15
- package/local-contract-types.mjs +1 -1
- package/local-contracts.mjs +2 -0
- package/local-error.mjs +23 -0
- package/local-execution.mjs +1 -1
- package/local-final-gate16-bundle.mjs +2 -2
- package/local-formula-ast.mjs +23 -14
- package/local-formula-engine.mjs +17 -3
- package/local-formula.mjs +1 -1
- package/local-invocations.mjs +35 -0
- package/local-ledger.mjs +13 -2
- package/local-mutation-authority.mjs +13 -6
- package/local-mutation-binding.mjs +5 -2
- package/local-mutation-files.mjs +29 -4
- package/local-task-handoff-observation.mjs +4 -1
- package/manifest.json +1 -1
- package/package.json +12 -1
- package/release-state.json +38 -28
- package/release.mjs +5 -1
- package/releases.json +6 -0
- package/runtime/calctool-runtime-v7.0.43.mjs +40 -17
package/local-invocations.mjs
CHANGED
|
@@ -9,6 +9,7 @@ const UUID = /^[a-f0-9-]{36}$/;
|
|
|
9
9
|
const ID = /^[a-z][a-z0-9-]{0,63}$/;
|
|
10
10
|
const COUNTS = ['inputTokens', 'outputTokens', 'durationMs'];
|
|
11
11
|
const events = new Set(['started', 'completed', 'rejected', 'outcome-unknown', 'not-sent', 'status']);
|
|
12
|
+
const readOnlyOperations = new Set(['ledger-query', 'chain-ledger-query']);
|
|
12
13
|
|
|
13
14
|
function receipt(value, requestId) {
|
|
14
15
|
if (!value || !UUID.test(value.usageId) || value.requestId !== requestId
|
|
@@ -44,6 +45,22 @@ async function ledgerFile(home) {
|
|
|
44
45
|
return path.join(directory, 'invocations.jsonl');
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
async function finalGateStateExists(input) {
|
|
49
|
+
if (!input || typeof input.home !== 'string' || typeof input.repositoryRoot !== 'string' || typeof input.gateId !== 'string') return false;
|
|
50
|
+
try {
|
|
51
|
+
const root = await fs.realpath(input.repositoryRoot);
|
|
52
|
+
const repositoryDigest = createHash('sha256').update(root).digest('hex');
|
|
53
|
+
const base = path.join(await fs.realpath(input.home), '.entrorove', 'calctool', 'final-gates', repositoryDigest);
|
|
54
|
+
for (const flow of await fs.readdir(base, { withFileTypes: true })) {
|
|
55
|
+
if (!flow.isDirectory() || flow.isSymbolicLink()) continue;
|
|
56
|
+
const target = path.join(base, flow.name, `${input.gateId}.json`);
|
|
57
|
+
try { const stat = await fs.lstat(target); if (stat.isFile() && !stat.isSymbolicLink()) return true; }
|
|
58
|
+
catch (error) { if (error?.code !== 'ENOENT') throw error; }
|
|
59
|
+
}
|
|
60
|
+
} catch (error) { if (!['ENOENT', 'ENOTDIR'].includes(error?.code)) throw error; }
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
47
64
|
export async function appendInvocation(value, home = os.homedir()) {
|
|
48
65
|
if (!value || !SHA.test(value.requestId) || !ID.test(value.operation) || !events.has(value.event)
|
|
49
66
|
|| !['remote', 'local'].includes(value.kind)) throw new Error('Invalid invocation ledger metadata');
|
|
@@ -77,6 +94,24 @@ function canonical(value) {
|
|
|
77
94
|
}
|
|
78
95
|
|
|
79
96
|
export async function withLocalInvocation(operation, input, work) {
|
|
97
|
+
// A final-gate recovery carries only the immutable identity fields and must
|
|
98
|
+
// remain a read-only GET-style lookup. A fresh gate includes plans and keeps
|
|
99
|
+
// the normal invocation audit record.
|
|
100
|
+
if (readOnlyOperations.has(operation)) return work();
|
|
101
|
+
if (operation === 'final-gate' && input) {
|
|
102
|
+
if (!Object.hasOwn(input, 'plans')) return work();
|
|
103
|
+
try {
|
|
104
|
+
const { statePath16 } = await import('./local-final-gate16-state.mjs');
|
|
105
|
+
const target = await statePath16(input);
|
|
106
|
+
if (target) {
|
|
107
|
+
try { await fs.access(target); return work(); }
|
|
108
|
+
catch (error) { if (error?.code !== 'ENOENT') throw error; }
|
|
109
|
+
}
|
|
110
|
+
if (await finalGateStateExists(input)) return work();
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
80
115
|
const requestId = createHash('sha256').update(operation + canonical(input)).digest('hex');
|
|
81
116
|
const metadata = { kind: 'local', operation, requestId };
|
|
82
117
|
await appendInvocation({ ...metadata, event: 'started' }, input.home);
|
package/local-ledger.mjs
CHANGED
|
@@ -19,9 +19,20 @@ async function repository(value) { if (typeof value !== 'string' || !path.isAbso
|
|
|
19
19
|
function repoDigest(root) { return sha(root).slice(0, 32); }
|
|
20
20
|
async function privateDir(directory) { await fs.mkdir(directory, { recursive: true, mode: 0o700 }); const stat = await fs.lstat(directory); if (!stat.isDirectory() || stat.isSymbolicLink()) fail('ENTROROVE_STATE_INVALID', 'state directory must be a real directory'); if (process.platform !== 'win32') await fs.chmod(directory, 0o700); }
|
|
21
21
|
async function atomic(file, value) { await privateDir(path.dirname(file)); const tmp = `${file}.${randomUUID()}.tmp`; await fs.writeFile(tmp, `${canonical(value)}\n`, { flag: 'wx', mode: 0o600 }); await fs.rename(tmp, file); if (process.platform !== 'win32') await fs.chmod(file, 0o600); }
|
|
22
|
-
async function read(file) { await privateDir(path.dirname(file)); const stat = await fs.lstat(file); if (!stat.isFile() || stat.isSymbolicLink()) fail('ENTROROVE_STATE_INVALID', 'ledger state must be a regular file'); return JSON.parse(await fs.readFile(file, 'utf8')); }
|
|
22
|
+
async function read(file, ensureDirectory = true) { if (ensureDirectory) await privateDir(path.dirname(file)); const stat = await fs.lstat(file); if (!stat.isFile() || stat.isSymbolicLink()) fail('ENTROROVE_STATE_INVALID', 'ledger state must be a regular file'); return JSON.parse(await fs.readFile(file, 'utf8')); }
|
|
23
23
|
function ledgerPath(home, root) { return path.join(rootFor(home), 'ledgers', repoDigest(root), 'state.json'); }
|
|
24
24
|
async function loadLedger(input, create = false) { const root = await repository(input.repositoryRoot); const file = ledgerPath(input.home, root); try { const state = await read(file); if (state.schemaVersion !== LEDGER_SCHEMA || state.repositoryDigest !== repoDigest(root)) fail('ENTROROVE_LEDGER_INVALID', 'ledger identity mismatch'); return { root, file, state }; } catch (error) { if (!create || error.code !== 'ENOENT') throw error; const state = { schemaVersion: LEDGER_SCHEMA, repositoryDigest: repoDigest(root), sequence: 0, head: null, activeBranch: null, branches: [], records: [] }; await atomic(file, state); return { root, file, state }; } }
|
|
25
|
+
async function readLedgerForQuery(input) {
|
|
26
|
+
const root = await repository(input.repositoryRoot); const file = ledgerPath(input.home, root);
|
|
27
|
+
try {
|
|
28
|
+
const state = await read(file, false);
|
|
29
|
+
if (state.schemaVersion !== LEDGER_SCHEMA || state.repositoryDigest !== repoDigest(root)) fail('ENTROROVE_LEDGER_INVALID', 'ledger identity mismatch');
|
|
30
|
+
return { root, file, state };
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error.code !== 'ENOENT') throw error;
|
|
33
|
+
return { root, file, state: { schemaVersion: LEDGER_SCHEMA, repositoryDigest: repoDigest(root), sequence: 0, head: null, activeBranch: null, branches: [], records: [] } };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
25
36
|
async function append(input, event, data = {}) { const root = await repository(input.repositoryRoot); const file = ledgerPath(input.home, root); const previous = ledgerLocks.get(file) ?? Promise.resolve(); let release; const current = new Promise(resolve => { release = resolve; }); const queued = previous.then(() => current); ledgerLocks.set(file, queued); await previous; try { return await withStateLock(file, async () => { const authority = await loadLedger(input, true); const at = new Date().toISOString(); const unsigned = { sequence: authority.state.sequence + 1, event, at, ...data }; const record = { ...unsigned, previousDigest: authority.state.head, digest: sha(canonical(unsigned) + (authority.state.head ?? '')) }; const state = { ...authority.state, sequence: record.sequence, head: record.digest, records: [...authority.state.records, record] }; await atomic(authority.file, state); return { authority, state, record }; }); } finally { release(); if (ledgerLocks.get(file) === queued) ledgerLocks.delete(file); } }
|
|
26
37
|
export async function appendLedgerRecord(input, event, data = {}) { if (typeof event !== 'string' || !event.trim()) fail('ENTROROVE_LEDGER_EVENT', 'ledger event is required'); return (await append(input, event, data)).record; }
|
|
27
38
|
export async function branchCreate(input) { const name = id(input.name ?? input.branchName, 'branchName'); const fromRef = typeof input.fromRef === 'string' && input.fromRef ? input.fromRef : 'working-tree'; const authority = await loadLedger(input, true); if (authority.state.branches.some(item => item.name === name)) fail('ENTROROVE_BRANCH_EXISTS', `virtual branch already exists: ${name}`); const branch = { branchId: `branch-${randomUUID()}`, name, fromRef, description: input.description ?? '', virtual: true, createdAt: new Date().toISOString(), snapshotId: input.snapshotId ?? null }; const state = { ...authority.state, branches: [...authority.state.branches, branch], activeBranch: authority.state.activeBranch ?? name }; await atomic(authority.file, state); await append(input, 'branch-create', { name, fromRef, virtual: true }); return { schemaVersion: LEDGER_SCHEMA, status: 'verified', branch, forbidGitBranch: true, forbidWorktree: true }; }
|
|
@@ -36,7 +47,7 @@ export async function preflight(input) {
|
|
|
36
47
|
const reasons = []; if (!archguard.verified) reasons.push(archguard.reason); if (!evidence) reasons.push('trusted signed evidence bound to this flow, snapshot, and contract with exitCode 0 is required');
|
|
37
48
|
return { schemaVersion: LEDGER_SCHEMA, status: archguard.verified && evidence ? 'verified' : 'blocked', source: 'entrorove-ledger', checks: { archguard: archguard.verified, evidence }, reasons, virtualOnly: true, keyId: archguard.keyId ?? null };
|
|
38
49
|
}
|
|
39
|
-
export async function ledgerQuery(input) { const authority = await
|
|
50
|
+
export async function ledgerQuery(input) { const authority = await readLedgerForQuery(input); const limit = Number.isSafeInteger(input.limit) ? Math.min(500, Math.max(1, input.limit)) : 100; return { schemaVersion: LEDGER_SCHEMA, status: 'verified', repositoryDigest: repoDigest(authority.root), records: authority.state.records.slice(-limit), head: authority.state.head, sequence: authority.state.sequence }; }
|
|
40
51
|
|
|
41
52
|
function validatePlan(plan) { if (!plan || typeof plan !== 'object' || Array.isArray(plan) || plan.schemaVersion !== 'entrorove.execution-plan/1.0' || !ID.test(plan.chainId) || !Array.isArray(plan.steps) || !plan.steps.length) fail('ENTROROVE_CHAIN_PLAN_INVALID', 'chain plan must declare a schema, chainId, and steps'); const seen = new Set(); for (const [index, step] of plan.steps.entries()) { if (!step || typeof step !== 'object' || !ID.test(step.stepId) || seen.has(step.stepId) || !['command', 'skill', 'coordinator', 'decision'].includes(step.kind)) fail('ENTROROVE_CHAIN_STEP_INVALID', `step ${index} is invalid`); seen.add(step.stepId); } return plan; }
|
|
42
53
|
async function chainAuthority(input, create = false) { const root = await repository(input.repositoryRoot); const chainId = id(input.chainId ?? input.plan?.chainId, 'chainId'); const file = path.join(rootFor(input.home), 'chains', repoDigest(root), chainId, 'state.json'); try { const state = await read(file); if (state.schemaVersion !== CHAIN_SCHEMA || state.chainId !== chainId || state.repositoryDigest !== repoDigest(root)) fail('ENTROROVE_CHAIN_INVALID', 'chain state identity mismatch'); return { root, chainId, file, state }; } catch (error) { if (!create || error.code !== 'ENOENT') throw error; return { root, chainId, file, state: null }; } }
|
|
@@ -4,7 +4,7 @@ import { appendLedgerRecord } from './local-ledger.mjs';
|
|
|
4
4
|
import { withStateLock } from './local-state-lock.mjs';
|
|
5
5
|
import { withTaskHandoffIssueFence, withTaskHandoffWriteFence, assertTaskHandoffSnapshotBinding } from './local-task-handoff-fence.mjs';
|
|
6
6
|
import { mutationFail14, mutationPath14, mutationStateRoot14,
|
|
7
|
-
privateMutationDirectory14, writeMutationProjectFile14 } from './local-mutation-files.mjs';
|
|
7
|
+
mutationRead14, privateMutationDirectory14, writeMutationProjectFile14 } from './local-mutation-files.mjs';
|
|
8
8
|
import { mutationPass14, mutationLifetime14 } from './local-mutation-wire.mjs';
|
|
9
9
|
import { mutationOriginalBinding14, mutationSnapshotBinding14 } from './local-mutation-binding.mjs';
|
|
10
10
|
import { installMutationPass14, mutationPassLocations14, readMutationConsumption14,
|
|
@@ -42,20 +42,27 @@ export async function issueMutationPass14(input) {
|
|
|
42
42
|
});
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
|
-
async function checkedPass(input, state) {
|
|
45
|
+
async function checkedPass(input, state, { allowTargetDrift = false } = {}) {
|
|
46
46
|
const pass = await readStoredMutationPass14(input), target = mutationPath14(input.targetPath, 'targetPath');
|
|
47
47
|
if (!pass.paths.includes(target)) mutationFail14('ENTROROVE_MUTATION_WRITE_SCOPE', 'Guarded writing is outside the cloud pass path set');
|
|
48
48
|
const binding = await mutationOriginalBinding14(input, pass, state);
|
|
49
|
-
const snapshot = await mutationSnapshotBinding14(input, pass, { targetPath: target });
|
|
49
|
+
const snapshot = await mutationSnapshotBinding14(input, pass, { targetPath: target, allowTargetDrift });
|
|
50
50
|
assertTaskHandoffSnapshotBinding(binding.state, pass.snapshotReceipt);
|
|
51
51
|
const locations = await mutationPassLocations14(input, binding, pass);
|
|
52
52
|
const ledger = await readMutationConsumption14(pass, locations);
|
|
53
|
-
|
|
53
|
+
const consumedWrite = ledger.writes[target];
|
|
54
|
+
if (allowTargetDrift) {
|
|
55
|
+
if (consumedWrite?.status !== 'written') mutationFail14('ENTROROVE_MUTATION_PASS_CONSUMED', 'Checkpoint requires a completed guarded write');
|
|
56
|
+
const current = await mutationRead14(input, binding.root, target);
|
|
57
|
+
if (current.sha256 !== consumedWrite.contentDigest) mutationFail14('ENTROROVE_MUTATION_WRITE_CHANGED', 'Checkpoint target differs from its completed guarded write');
|
|
58
|
+
} else if (consumedWrite) {
|
|
59
|
+
mutationFail14('ENTROROVE_MUTATION_PASS_CONSUMED', 'Mutation pass was already consumed or has an unresolved physical write');
|
|
60
|
+
}
|
|
54
61
|
return { pass, target, binding, snapshot, locations, ledger };
|
|
55
62
|
}
|
|
56
|
-
export async function verifyMutationPass14(input) {
|
|
63
|
+
export async function verifyMutationPass14(input, { allowTargetDrift = false } = {}) {
|
|
57
64
|
return withMutationAudit14(input, 'mutation-pass-verify', async () => {
|
|
58
|
-
const value = await checkedPass(input);
|
|
65
|
+
const value = await checkedPass(input, undefined, { allowTargetDrift });
|
|
59
66
|
return { schemaVersion: SCHEMA, verified: true, pass: value.pass, targetPath: value.target };
|
|
60
67
|
});
|
|
61
68
|
}
|
|
@@ -39,7 +39,7 @@ export async function mutationOriginalBinding14(input, authority, state) {
|
|
|
39
39
|
}
|
|
40
40
|
return { root, home, context: ctx, state: current };
|
|
41
41
|
}
|
|
42
|
-
export async function mutationSnapshotBinding14(input, pass, { allSources = false, targetPath } = {}) {
|
|
42
|
+
export async function mutationSnapshotBinding14(input, pass, { allSources = false, targetPath, allowTargetDrift = false } = {}) {
|
|
43
43
|
mutationPass14(pass);
|
|
44
44
|
const root = await mutationRoot14(input.repositoryRoot);
|
|
45
45
|
const snapshot = await snapshotAuthority14(input, root, pass.snapshotReceipt);
|
|
@@ -53,7 +53,10 @@ export async function mutationSnapshotBinding14(input, pass, { allSources = fals
|
|
|
53
53
|
if (snapshot.manifest.files.some(file => !file.exists && !pass.newFilePaths.includes(file.path))) {
|
|
54
54
|
mutationFail14('ENTROROVE_MUTATION_NEW_FILE_SCOPE', 'Missing source is outside the original planned new-file set');
|
|
55
55
|
}
|
|
56
|
-
|
|
56
|
+
// ArchGuard validates the changed target after a guarded write. The original
|
|
57
|
+
// snapshot still binds the pass, while its target source is expected to drift
|
|
58
|
+
// before checkpoint evaluation. Ordinary mutation paths keep the hash check.
|
|
59
|
+
if (allSources || targetPath && !allowTargetDrift) await verifySnapshotSources14(input, root, snapshot.manifest,
|
|
57
60
|
{ paths: allSources ? snapshot.manifest.paths : [targetPath] });
|
|
58
61
|
return { ...snapshot, root };
|
|
59
62
|
}
|
package/local-mutation-files.mjs
CHANGED
|
@@ -63,6 +63,31 @@ async function inspectPath(target, { allowMissing = false, final = 'file', code
|
|
|
63
63
|
else return { target: resolved, exists: true, stat, ancestors };
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
+
async function canonicalDirectoryPath(value, { allowMissing = false, code }) {
|
|
67
|
+
const resolved = absolute(value, 'filesystem path');
|
|
68
|
+
const base = path.parse(resolved).root;
|
|
69
|
+
const parts = path.relative(base, resolved).split(path.sep).filter(Boolean);
|
|
70
|
+
let current = base;
|
|
71
|
+
for (const [index, part] of parts.entries()) {
|
|
72
|
+
const candidate = path.join(current, part);
|
|
73
|
+
let stat;
|
|
74
|
+
try { stat = await fs.lstat(candidate); } catch (error) {
|
|
75
|
+
if (error.code === 'ENOENT' && allowMissing) return path.join(current, ...parts.slice(index));
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
if (stat.isSymbolicLink()) {
|
|
79
|
+
const linked = await fs.realpath(candidate);
|
|
80
|
+
const systemAlias = process.platform === 'darwin' && current === base
|
|
81
|
+
&& ['/var', '/tmp', '/etc'].includes(candidate) && linked === path.join('/private', candidate);
|
|
82
|
+
if (!systemAlias) mutationFail14(code, 'mutation paths refuse symlink, non-directory ancestors, and invalid file types');
|
|
83
|
+
current = linked;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (!stat.isDirectory()) mutationFail14(code, 'mutation paths refuse symlink, non-directory ancestors, and invalid file types');
|
|
87
|
+
current = candidate;
|
|
88
|
+
}
|
|
89
|
+
return current;
|
|
90
|
+
}
|
|
66
91
|
async function assertAncestors(item, code = 'ENTROROVE_STATE_CHANGED') {
|
|
67
92
|
for (const ancestor of item.ancestors) {
|
|
68
93
|
const stat = await fs.lstat(ancestor.path);
|
|
@@ -72,12 +97,12 @@ async function assertAncestors(item, code = 'ENTROROVE_STATE_CHANGED') {
|
|
|
72
97
|
}
|
|
73
98
|
}
|
|
74
99
|
export async function mutationRoot14(value) {
|
|
75
|
-
const
|
|
76
|
-
await inspectPath(
|
|
77
|
-
return
|
|
100
|
+
const canonicalRoot = await canonicalDirectoryPath(value, { code: 'ENTROROVE_ROOT_INVALID' });
|
|
101
|
+
await inspectPath(canonicalRoot, { final: 'directory', code: 'ENTROROVE_ROOT_INVALID' });
|
|
102
|
+
return canonicalRoot;
|
|
78
103
|
}
|
|
79
104
|
export async function mutationHome14(value = os.homedir()) {
|
|
80
|
-
const home =
|
|
105
|
+
const home = await canonicalDirectoryPath(value, { allowMissing: true, code: 'ENTROROVE_HOME_INVALID' });
|
|
81
106
|
await inspectPath(home, { allowMissing: true, final: 'directory', code: 'ENTROROVE_HOME_INVALID' });
|
|
82
107
|
return home;
|
|
83
108
|
}
|
|
@@ -9,7 +9,10 @@ function fail(message) { throw Object.assign(new Error(message), { code: 'ENTROR
|
|
|
9
9
|
export async function loadTaskHandoffSigner(ctx, file, keyId) {
|
|
10
10
|
async function control(target) {
|
|
11
11
|
if (typeof target !== 'string' || !path.isAbsolute(target)) fail('runner signer controls are unavailable; an external absolute configuration is required');
|
|
12
|
-
|
|
12
|
+
let actual;
|
|
13
|
+
try { actual = await fs.realpath(target); }
|
|
14
|
+
catch { fail('runner signer controls are unavailable; an external absolute configuration is required'); }
|
|
15
|
+
const relative = path.relative(ctx.root, actual);
|
|
13
16
|
if (actual !== path.resolve(target) || relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative))) fail('runner signer controls must be external real files');
|
|
14
17
|
const value = await regular(target, 16 * 1024);
|
|
15
18
|
if ((process.platform !== 'win32' && (value.stat.mode & 0o077)) || (typeof process.getuid === 'function' && value.stat.uid !== process.getuid())) fail('runner signer controls must be owned private files');
|
package/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@entrorove/skill",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.25",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"entrorove": "cli.mjs"
|
|
@@ -37,6 +37,16 @@
|
|
|
37
37
|
"local-evidence-verification.mjs",
|
|
38
38
|
"local-formula.mjs",
|
|
39
39
|
"local-ledger.mjs",
|
|
40
|
+
"local-chain-bindings.mjs",
|
|
41
|
+
"local-chain-broker.mjs",
|
|
42
|
+
"local-chain-contracts.mjs",
|
|
43
|
+
"local-chain-controller.mjs",
|
|
44
|
+
"local-chain-human.mjs",
|
|
45
|
+
"local-chain-lock.mjs",
|
|
46
|
+
"local-chain-operations.mjs",
|
|
47
|
+
"local-chain-plan.mjs",
|
|
48
|
+
"local-chain-release-labels.mjs",
|
|
49
|
+
"local-chain-results.mjs",
|
|
40
50
|
"local-approved-plan.mjs",
|
|
41
51
|
"workflow-local-runner.mjs",
|
|
42
52
|
"workflow-local-client.mjs",
|
|
@@ -133,6 +143,7 @@
|
|
|
133
143
|
"local-formula-tests.mjs",
|
|
134
144
|
"local-calctool.mjs",
|
|
135
145
|
"local-calctool-cli.mjs",
|
|
146
|
+
"local-error.mjs",
|
|
136
147
|
"local-calctool-contracts.mjs",
|
|
137
148
|
"local-calctool-runtime.mjs",
|
|
138
149
|
"local-calctool-storage.mjs",
|
package/release-state.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "entrorove.release-state/1.0",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"digest": "
|
|
3
|
+
"version": "0.1.25",
|
|
4
|
+
"digest": "51ba0446dc6d2004bdf31195d4cf62bdd522f3c0a76de1c35344df32ae130046",
|
|
5
5
|
"files": {
|
|
6
6
|
"local-final-gate16.mjs": "b30caab08441449ad556a6e9a3cefbf91190f0b3f7370f46d2a21156375aa8e3",
|
|
7
7
|
"local-final-gate16-checks.mjs": "1dd30e9666d458d8b12ca6e4975f6f0ac8450c3ea9cbecc7401ae037f66b589a",
|
|
8
8
|
"local-final-gate16-worker.mjs": "344d64b6c1c5552b96960e0d8514a876a3dbfa9762ab9eb749038e8351727121",
|
|
9
9
|
"local-final-gate16-plan.mjs": "9d34c51fa239376e552abc312409a7f3258519efe0b29d88a5054680cdda483c",
|
|
10
|
-
"local-final-gate16-bundle.mjs": "
|
|
10
|
+
"local-final-gate16-bundle.mjs": "08cff47892199a0b1c08196e151770075ee09a9036e9aea4906fdb619edf7499",
|
|
11
11
|
"local-final-gate16-state.mjs": "a8cf6ac28183fb2274772774d52418a82343485271f78b4d304d0dae7c8a642d",
|
|
12
12
|
"local-final-gate16-proof.mjs": "5f7d5be29b1ce5fbb1a05fd2204969215ec5d0297ad84dcb7b29abb3ef285f56",
|
|
13
13
|
"local-final-gate16-contract.mjs": "cffa9b0d89eab360ce6c27c73400b8b377adbdc32813b244f754423d10061fe0",
|
|
@@ -36,24 +36,34 @@
|
|
|
36
36
|
"local-swarm-state.mjs": "ff84c5f35ef3e4a0ad9887d145e404f5786ae0dc92496116699d031b98e90c6a",
|
|
37
37
|
"local-swarm-storage.mjs": "4b506481913fe22769f47a14a024a2cc4c0f1e986bb52ef606d14c7844226086",
|
|
38
38
|
"local-swarm.mjs": "94b5adde0239cc21bada288886482934d409f4383d31076025437ed621ef43b6",
|
|
39
|
-
"cli.mjs": "
|
|
39
|
+
"cli.mjs": "c85289b1cbe0ea469a57e67edb1fc475dc9c626c2beae00f5e0954c00757481d",
|
|
40
40
|
"review-client.mjs": "3491eef68ed533174b31db3c53febb6519b8f088f71a9964644714a9a4c03c77",
|
|
41
41
|
"version.mjs": "1f392147d72c40af4b03b307ea607075fb815af5174fa07e254b95fe12dbf3e7",
|
|
42
|
-
"SKILL.md": "
|
|
43
|
-
"CAPABILITY-MAP.md": "
|
|
42
|
+
"SKILL.md": "f6a140c4d0dd79b6985d1cd085c831a26c64119a4107e35d25c3ac5a566ed797",
|
|
43
|
+
"CAPABILITY-MAP.md": "1a58ac506efe894c0ba224605a758cbc8a1c8f7b9ed05ce355e0c9a3ce43c61f",
|
|
44
44
|
"AGREEMENT.md": "f93a68e92c457200dd024610e65f74e478b8f60860f951da818b70561f1b9bf8",
|
|
45
|
-
"package.json": "
|
|
45
|
+
"package.json": "a60c18189e680be7c8547c5331ceee74f64115dc571e8e069e76570a5fcede0b",
|
|
46
46
|
"manifest.json": "ab06e307795bc9da5add926752b6c4dbdaf18625090261a035aa356a679c504f",
|
|
47
|
-
"release.mjs": "
|
|
48
|
-
"local-cli.mjs": "
|
|
47
|
+
"release.mjs": "b6882c24084129a0853330ec707b46fac69c25d533cdcc23b95b23cdefda27e9",
|
|
48
|
+
"local-cli.mjs": "ecc85bc5f36a5663fef004e59525374dc7883d3889f5a0cfe219ae5e84b2d3ea",
|
|
49
49
|
"local-state-lock.mjs": "1dbf64264642a0851faf043e07ee9f66d2ad0508e9f799b0626246e42fae893e",
|
|
50
|
-
"local-execution.mjs": "
|
|
50
|
+
"local-execution.mjs": "97dfad5e889e679e9cc34434005342526502795abba21078fa40673788073153",
|
|
51
51
|
"local-integrity.mjs": "4f325376e536999c3b00245f66aac9ef1ae762c33012b363c231622008f72805",
|
|
52
52
|
"local-evidence-verification.mjs": "9b7dea31777505272f63616fd62af7603338f703bb1285fc565d6451715c7ce0",
|
|
53
|
-
"local-formula.mjs": "
|
|
54
|
-
"local-ledger.mjs": "
|
|
53
|
+
"local-formula.mjs": "706d5ec0cb2caf95a547531a81ca746fe246d4c53d902e03595a3b360e8c85af",
|
|
54
|
+
"local-ledger.mjs": "2009fb68aadcd7292b72f1d0d99f378d03644be1c5b4b4150d9f6a0b3f7c15ce",
|
|
55
55
|
"blueprint-criterion-wire.mjs": "fb6b7dcce0b97a699e3e8968372767a3fddd83fedc750998573f789177243072",
|
|
56
56
|
"blueprint-criterion-runner.mjs": "761c0e670b4b20c6bed53e5ca5d0178759505c3977ea2ed1b7223383c534bc4c",
|
|
57
|
+
"local-chain-bindings.mjs": "b8eff999ccd17b53c614a3c9f5376da78fcaa8632bb3e07bc1adcceda2e75f6c",
|
|
58
|
+
"local-chain-broker.mjs": "0f43ae3d5e713a344d94c3f67891b0d5f6ed0b70a8d237a54d06c0fa36e4c6d8",
|
|
59
|
+
"local-chain-contracts.mjs": "d4d2be4e5c6980f2dda8ff598c7333a75e0bbf2f656aa7fe327af7c9fe73da07",
|
|
60
|
+
"local-chain-controller.mjs": "dd80d8001a11d8433b615a017a34c30dd130ff5cd9c5b58731314a8f93c9571b",
|
|
61
|
+
"local-chain-human.mjs": "9d5dbaef13ebbbe0cfdd1146cd9c48cefcca30f33ccc0b1566e67cbd730d3499",
|
|
62
|
+
"local-chain-lock.mjs": "e0bc9dccc3c152007f4d67eff0b48de40516ce991b6868f513bc88b53a9b41b2",
|
|
63
|
+
"local-chain-operations.mjs": "78ba550f374b3b7fd03abfc88ae773e1189795d498929a13089fbf50a5611d9f",
|
|
64
|
+
"local-chain-plan.mjs": "e45a9170f569c80f7e5532bbc109dbae3c24b696815316e503cb3779135be648",
|
|
65
|
+
"local-chain-release-labels.mjs": "7ce5350d1062d5236f57f10e7e81713c1101463d5e7f252604ef0c2a30840bfe",
|
|
66
|
+
"local-chain-results.mjs": "0455f72cad574cfef87c2c6c0928d6432bba7e5dbaff521a2b62a9575299ca10",
|
|
57
67
|
"local-approved-plan.mjs": "bb31bf0f826ce22d1e57c6d9cda5314cbbb1fc04e6c0778eb319045ab23a3193",
|
|
58
68
|
"local-budget-renewal.mjs": "6b436358066fe33b2f6d871fc90d8e7c551edd6255efd73c0d255d0fd35d5ef8",
|
|
59
69
|
"local-probe.mjs": "9b948db1de1e34c7605c4c454cd4aced66d2b57d2cf4471adbb80bcc76dee635",
|
|
@@ -101,14 +111,14 @@
|
|
|
101
111
|
"remote-client.mjs": "dabbcc7bd0fd16ba029163f407edb85e914df83cd5914221eb2ce0d061ca8f28",
|
|
102
112
|
"remote-transport.mjs": "5e5a8c0b490371f7f5a052cffbeee6202d4cf713dff1c43b6d910fe944f8429c",
|
|
103
113
|
"local-credential.mjs": "2a302a7f67b5ea8dc73453fc96ab33db349b59ca11fa9456e48ca1d9669d877c",
|
|
104
|
-
"local-invocations.mjs": "
|
|
114
|
+
"local-invocations.mjs": "f6ac4c41c1a5ec34de358c4cca210ca8e403b3a913150a43bb55ec9be55d9998",
|
|
105
115
|
"local-task-handoff-fence.mjs": "5532a359ee29b885f7ce0cf7178f76dba0fc9245bdd92ef460982275baf0d557",
|
|
106
116
|
"local-task-handoff-storage.mjs": "f4d5046d54658e8092e035b31fdaeaed6fdbef55a6953827417c857f2e9e6638",
|
|
107
117
|
"local-task-handoff-overlay-wire.mjs": "4fbe3f9d1ed8b6ddf839e9525cdc52c930a22e9d4f4ad562c6f193f621ee6527",
|
|
108
118
|
"local-task-handoff-completion-wire.mjs": "2f3708b162d2d7524dde8c276bb0ad2328c17eefa597e50f30a2cd2ca1fa7fba",
|
|
109
119
|
"local-task-handoff-contracts.mjs": "c03094185fd05308c8a968a719b121e04e2b86201cc0408f3656e7f81f1840ad",
|
|
110
120
|
"local-task-handoff-write-constraints.mjs": "19cba9330dddd170d1cb8707405574f888292acea2dc3f48bc71f6b3c61065cc",
|
|
111
|
-
"local-task-handoff-observation.mjs": "
|
|
121
|
+
"local-task-handoff-observation.mjs": "d17c75dba647a1ce01681feeb9109c2f51927309255452166a2c2f7758c5ab50",
|
|
112
122
|
"local-task-handoff-write-wire.mjs": "7b49b66de8ecc33c1c92e9dd50118528972f00e1c7748cb3a230a58eefa9ff3f",
|
|
113
123
|
"local-task-handoff-read-authority.mjs": "7d2991cfac4562039232566d25441aa98106ce27bc9b7bcf6df4ff28d738e167",
|
|
114
124
|
"local-task-handoff-write-archive.mjs": "3668f39d8ab5664ed717994cf1b4bcc7ea23e146a0e06291f851d30337faeb77",
|
|
@@ -119,10 +129,10 @@
|
|
|
119
129
|
"workflow-local-request.mjs": "e8cce489ecadc756e27bf8686556a830e9e789df03a56e034f7b741e6455d3ae",
|
|
120
130
|
"task-message-entry.mjs": "31d10056cb905237cd5649e8eb067009ff1612ec5dc1061e0373209fbc0b646a",
|
|
121
131
|
"task-message-cli.mjs": "3ca65e96c28129b3bd5cac962d106e0859a1f17df1f6a66ee543c6efc22f918d",
|
|
122
|
-
"local-contracts.mjs": "
|
|
123
|
-
"local-contract-types.mjs": "
|
|
132
|
+
"local-contracts.mjs": "43715cd73460ccea734501a3dd5667184da0f6a5b0189e038319a439ba52fc76",
|
|
133
|
+
"local-contract-types.mjs": "5d244237fc69052b2fa83098022761bae929e8b75a0683883b620711bc132050",
|
|
124
134
|
"local-archguard-ast.mjs": "cefd835312a623e83bf48e4d8e1f76ac6a8adc56c35843705c16967d16bec4e6",
|
|
125
|
-
"local-archguard-checkpoint.mjs": "
|
|
135
|
+
"local-archguard-checkpoint.mjs": "1ee3a40561940a62583382036d4049321627fe9e41e0f31a973ef40bc86fcb55",
|
|
126
136
|
"local-archguard-components.mjs": "a8fb3d0fd787f9160905013fd333cc0b9e8cf624cce797461f36f4ab31d170ad",
|
|
127
137
|
"local-archguard-contract.mjs": "cfc397a1ce171ab84f8bf1db152e6d8d280b7ca041da56a7a4cd62b02c2e2e89",
|
|
128
138
|
"local-archguard-contracts.mjs": "29e9b86f14dc086aa0b791c874d6efc50d849f806b3f2b8725481114f01ba8ba",
|
|
@@ -132,31 +142,31 @@
|
|
|
132
142
|
"local-archguard-operations.mjs": "1a35268b857b86eab54dd4efa78fc54bf08b39dfab4fa980c7dd63b8959206b0",
|
|
133
143
|
"local-archguard-scope.mjs": "afbd23a79c175f198892ddb2026ecac219a1650ee6115763736d9b1ee5a07ee0",
|
|
134
144
|
"local-archguard-signing.mjs": "7de0fe4b8f6f5a92d043ad8976054faaa76b7b37556ed582e5e012c6f0facc87",
|
|
135
|
-
"local-archguard-snapshot.mjs": "
|
|
145
|
+
"local-archguard-snapshot.mjs": "81b456915232b8e87d9b663912bf95062753a0bd0e38a096cbd80c2ef03683f9",
|
|
136
146
|
"local-archguard-state.mjs": "1d3c74c4ec222646e82120be4aa6a00c52d2867fb8c20323420d6ab0bf2cd46c",
|
|
137
147
|
"local-archguard.mjs": "4c8ec5d569d728b15161eb88e49371c77def7d1fe511aa16694581a2c1d59433",
|
|
138
148
|
"archguard-runtime-build.mjs": "45bf31764763973bd8de32b3e16d194162407e7cb74802df3ab363f0fd30e866",
|
|
139
149
|
"runtime/archguard-runtime-v7.0.44.mjs": "836f20b270139371bef43e3934fa7232371e42d46371c513db2a5e3c5622b215",
|
|
140
150
|
"local-decimal.mjs": "93580b0cbdac2e881942658c1cd10394b432219f8657afaaaf4cb1c35c91d644",
|
|
141
|
-
"local-formula-ast.mjs": "
|
|
142
|
-
"local-formula-engine.mjs": "
|
|
151
|
+
"local-formula-ast.mjs": "768ec4aa88a127588035d5b94aa2721df00e3e0576dc7e341baaead44795f314",
|
|
152
|
+
"local-formula-engine.mjs": "cfc44f03bb8dc4797f9f9e859a64a2dd5e4dd316db45624c03f7f8fad3202ede",
|
|
143
153
|
"local-formula-tests.mjs": "d6d2809572c46955e6b556023ed6a12362c8acd0ad0d5be3b1b2dd64439215c0",
|
|
144
|
-
"local-calctool.mjs": "
|
|
145
|
-
"local-calctool-cli.mjs": "
|
|
154
|
+
"local-calctool.mjs": "9398e642250b38b2831ec58e490fbbef761455ce9ae49af033baa8c3ba8d778e",
|
|
155
|
+
"local-calctool-cli.mjs": "a6b46e2aa9a91dfb73fc0dc415b2281a0343e22e317ff1bfcf58cadbb8093122",
|
|
146
156
|
"local-calctool-contracts.mjs": "d8fccf662b2dd7aefd5cc6770b2473014af7a28855074fcf294937ca5a49eae8",
|
|
147
|
-
"local-calctool-runtime.mjs": "
|
|
157
|
+
"local-calctool-runtime.mjs": "9070b6900d1782bf77357fea1cc522bf796898d56929f96d7cb935b3036ac82f",
|
|
148
158
|
"local-calctool-storage.mjs": "4c3f352847aeb368dea1247759a1055beeab25534adf25b6e7323196d76fd684",
|
|
149
|
-
"local-mutation-files.mjs": "
|
|
159
|
+
"local-mutation-files.mjs": "b99a1987e0884cee1f0f04552300fbf61853ea2547d74c77b77c71fc3ebb89ad",
|
|
150
160
|
"local-mutation-snapshot.mjs": "a2f5f1df1fa0ecb40bdd4776e319c5244508ed04a2aa4493aebcec926fc61148",
|
|
151
161
|
"local-mutation-audit.mjs": "5a30ee3446f632e032ffcfe46bcd97d6dfc0f099a76ea5185c3f9a9118c4f6cb",
|
|
152
162
|
"local-mutation-wire.mjs": "dc79992422254d876bec94c621407bede94c9f43c95367a2c9f16a2a64b15576",
|
|
153
|
-
"local-mutation-binding.mjs": "
|
|
163
|
+
"local-mutation-binding.mjs": "a1756642dd758701b476d332a48799b4af56fdd8f67aba6d46ca9bb2341b6327",
|
|
154
164
|
"local-mutation-pass-store.mjs": "8d6b57c6d1ea3df15a794e6a4cd39497f4412a5f911b9a30b0437498f51f92c0",
|
|
155
|
-
"local-mutation-authority.mjs": "
|
|
165
|
+
"local-mutation-authority.mjs": "28561b5b672838a46fa13b0044b5d9302630d073aa5893a7e0990e6567336499",
|
|
156
166
|
"local-mutation-batch.mjs": "80293d8111203687e2527cd23b771447cc44ff58c0b0155c368bcbfa05aa3bb8",
|
|
157
167
|
"local-mutation-batch-receipt.mjs": "b0e6a8522867255d899e60b66677885933f6a1c4c771cbb52f84dbbbdcf5e5c2",
|
|
158
|
-
"calctool-runtime-entry.mjs": "
|
|
168
|
+
"calctool-runtime-entry.mjs": "98d2bc8df38d16131fc56db0a3d4d260531ba4df412baa5ddc4b8b5c5ca99fe9",
|
|
159
169
|
"calctool-runtime-build.mjs": "3c5a9bd40bc1abe02e3b6e307809eea4eaaf198e8f0db6f5d6b2583dd68a59d2",
|
|
160
|
-
"runtime/calctool-runtime-v7.0.43.mjs": "
|
|
170
|
+
"runtime/calctool-runtime-v7.0.43.mjs": "5473427c350411f62c16ac5239fa0db7b96e7b2c19458f9046768dcb4ada87d1"
|
|
161
171
|
}
|
|
162
172
|
}
|
package/release.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { createHash } from 'node:crypto'
|
|
|
3
3
|
import { readFile, writeFile, rename, rm } from 'node:fs/promises'
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import { chainReleaseLabels } from './local-chain-release-labels.mjs'
|
|
6
7
|
|
|
7
8
|
const directory = path.dirname(fileURLToPath(import.meta.url))
|
|
8
9
|
const swarmSourceFiles = Object.freeze([
|
|
@@ -38,6 +39,9 @@ const sourceFiles = [
|
|
|
38
39
|
'package.json', 'manifest.json', 'release.mjs',
|
|
39
40
|
'local-cli.mjs', 'local-state-lock.mjs', 'local-execution.mjs', 'local-integrity.mjs', 'local-evidence-verification.mjs', 'local-formula.mjs', 'local-ledger.mjs',
|
|
40
41
|
'blueprint-criterion-wire.mjs', 'blueprint-criterion-runner.mjs',
|
|
42
|
+
'local-chain-bindings.mjs', 'local-chain-broker.mjs', 'local-chain-contracts.mjs', 'local-chain-controller.mjs',
|
|
43
|
+
'local-chain-human.mjs', 'local-chain-lock.mjs', 'local-chain-operations.mjs', 'local-chain-plan.mjs',
|
|
44
|
+
'local-chain-release-labels.mjs', 'local-chain-results.mjs',
|
|
41
45
|
'local-approved-plan.mjs', 'local-budget-renewal.mjs', 'local-probe.mjs',
|
|
42
46
|
'local-budget-grants.mjs', 'local-budget-handoff.mjs', 'local-budget-handoff-contracts.mjs', 'local-budget-lock.mjs',
|
|
43
47
|
'local-budget-handoff-completion.mjs',
|
|
@@ -227,7 +231,7 @@ function releaseNote(changed) {
|
|
|
227
231
|
if (suppliedZh && suppliedEn) return {
|
|
228
232
|
zh: suppliedZh.replaceAll(/\s+/g, ' '), en: suppliedEn.replaceAll(/\s+/g, ' '),
|
|
229
233
|
}
|
|
230
|
-
const labels = withFinalGate16ReleaseLabels({ ...calctoolReleaseLabels, ...mutationReleaseLabels, ...swarmReleaseLabels, ...nativeReadReleaseLabels, ...feedbackReleaseLabels, ...invocationReleaseLabels,
|
|
234
|
+
const labels = withFinalGate16ReleaseLabels({ ...calctoolReleaseLabels, ...mutationReleaseLabels, ...swarmReleaseLabels, ...nativeReadReleaseLabels, ...feedbackReleaseLabels, ...invocationReleaseLabels, ...chainReleaseLabels,
|
|
231
235
|
'blueprint-criterion-wire.mjs': { zh: '蓝图验收标准执行授权', en: 'Blueprint criterion execution authority' },
|
|
232
236
|
'blueprint-criterion-runner.mjs': { zh: '蓝图验收标准实际执行器', en: 'Blueprint criterion process runner' },
|
|
233
237
|
'local-archguard-ast.mjs': { zh: 'Archguard 架构合同与执行守卫', en: 'Archguard architecture contract and execution guard' },
|
package/releases.json
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.1.25",
|
|
4
|
+
"date": "2026-10-07",
|
|
5
|
+
"zh": "原链本地执行与固定Git计费收口,补齐macOS路径和发布夹具",
|
|
6
|
+
"en": "Close original-chain local execution and fixed Git accounting, with macOS path and release fixture coverage"
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.1.24",
|
|
4
10
|
"date": "2026-10-07",
|
|
@@ -1528,7 +1528,8 @@ class Decimal {
|
|
|
1528
1528
|
const Decimal28 = Decimal;
|
|
1529
1529
|
|
|
1530
1530
|
const FORMULA_OPERATORS = Object.freeze(['add', 'sub', 'mul', 'div', 'safeDivide', 'percentOf', 'round', 'if']);
|
|
1531
|
-
const
|
|
1531
|
+
const FORMULA_ROUNDING_POLICY = Object.freeze({ decimalPlaces: 2, mode: 'half-away-from-zero' });
|
|
1532
|
+
const ARITY = { add: [1, 1000], mul: [1, 1000], sub: [2, 2], div: [2, 2], safeDivide: [2, 2], percentOf: [2, 2], round: [1, 1], if: [3, 3] };
|
|
1532
1533
|
function formulaKey(value) {
|
|
1533
1534
|
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');
|
|
1534
1535
|
return value;
|
|
@@ -1557,28 +1558,35 @@ function formulaRefs(node, output = new Set()) {
|
|
|
1557
1558
|
if (node.args) for (const item of node.args) formulaRefs(item, output);
|
|
1558
1559
|
return [...output].sort();
|
|
1559
1560
|
}
|
|
1560
|
-
function
|
|
1561
|
+
function roundPlaces(policy) {
|
|
1562
|
+
const value = policy?.decimalPlaces;
|
|
1563
|
+
if (!Number.isInteger(value) || value < 0 || value > 28) throw formulaError('ROUND_POLICY_INVALID', 'rounding policy decimalPlaces must be an integer from 0 to 28');
|
|
1564
|
+
if (policy.mode !== 'half-away-from-zero') throw formulaError('ROUND_POLICY_INVALID', 'rounding policy mode must be half-away-from-zero');
|
|
1565
|
+
return value;
|
|
1566
|
+
}
|
|
1567
|
+
function evaluateNode(node, resolve, policy) {
|
|
1561
1568
|
if (Object.hasOwn(node, 'lit')) return Decimal.from(node.lit);
|
|
1562
1569
|
if (Object.hasOwn(node, 'ref')) return Decimal.from(resolve(node.ref));
|
|
1563
1570
|
const [left, right, alternate] = node.args;
|
|
1564
1571
|
switch (node.op) {
|
|
1565
|
-
case 'add': return node.args.reduce((total, item) => total.plus(evaluateNode(item, resolve)), Decimal.from('0'));
|
|
1566
|
-
case 'mul': return node.args.reduce((total, item) => total.times(evaluateNode(item, resolve)), Decimal.from('1'));
|
|
1567
|
-
case 'sub': return evaluateNode(left, resolve).minus(evaluateNode(right, resolve));
|
|
1568
|
-
case 'div': return evaluateNode(left, resolve).div(evaluateNode(right, resolve));
|
|
1569
|
-
case 'safeDivide': { const denominator = evaluateNode(right, resolve);
|
|
1570
|
-
case 'percentOf': return evaluateNode(left, resolve).div(evaluateNode(right, resolve)).times('100');
|
|
1571
|
-
case 'round': return evaluateNode(left, resolve).rounded(
|
|
1572
|
-
case 'if': return evaluateNode(left, resolve).isZero() ? evaluateNode(alternate, resolve) : evaluateNode(right, resolve);
|
|
1572
|
+
case 'add': return node.args.reduce((total, item) => total.plus(evaluateNode(item, resolve, policy)), Decimal.from('0'));
|
|
1573
|
+
case 'mul': return node.args.reduce((total, item) => total.times(evaluateNode(item, resolve, policy)), Decimal.from('1'));
|
|
1574
|
+
case 'sub': return evaluateNode(left, resolve, policy).minus(evaluateNode(right, resolve, policy));
|
|
1575
|
+
case 'div': return evaluateNode(left, resolve, policy).div(evaluateNode(right, resolve, policy));
|
|
1576
|
+
case 'safeDivide': { const denominator = evaluateNode(right, resolve, policy); if (denominator.isZero()) throw formulaError('DIV_ZERO', 'division by zero', { nodeId: 'safeDivide' }); return evaluateNode(left, resolve, policy).div(denominator); }
|
|
1577
|
+
case 'percentOf': return evaluateNode(left, resolve, policy).div(evaluateNode(right, resolve, policy)).times('100');
|
|
1578
|
+
case 'round': return evaluateNode(left, resolve, policy).rounded(roundPlaces(policy));
|
|
1579
|
+
case 'if': return evaluateNode(left, resolve, policy).isZero() ? evaluateNode(alternate, resolve, policy) : evaluateNode(right, resolve, policy);
|
|
1573
1580
|
default: throw formulaError('UNSUPPORTED_OPERATOR', 'unsupported normalized formula operator');
|
|
1574
1581
|
}
|
|
1575
1582
|
}
|
|
1576
|
-
function evaluateFormula(node, values = {}) {
|
|
1583
|
+
function evaluateFormula(node, values = {}, options = {}) {
|
|
1577
1584
|
formulaObject(values, 'formula values');
|
|
1585
|
+
const policy = options.roundingPolicy ?? FORMULA_ROUNDING_POLICY;
|
|
1578
1586
|
return evaluateNode(normalizeFormula(node), key => {
|
|
1579
1587
|
if (!Object.hasOwn(values, key) || values[key] === null || values[key] === undefined || values[key] === '') throw formulaError('MISSING_INPUT', `missing input: ${key}`, { fieldId: key });
|
|
1580
1588
|
return values[key];
|
|
1581
|
-
});
|
|
1589
|
+
}, policy);
|
|
1582
1590
|
}
|
|
1583
1591
|
function compileFormulaGraph(formulas, fieldKeys) {
|
|
1584
1592
|
if (!Array.isArray(formulas) || formulas.length > 1000) throw formulaError('FORMULA_GRAPH_INVALID', 'formulas must be an array of at most 1000 definitions');
|
|
@@ -1605,8 +1613,9 @@ function compileFormulaGraph(formulas, fieldKeys) {
|
|
|
1605
1613
|
for (const key of [...definitions.keys()].sort()) visit(key);
|
|
1606
1614
|
return { formulas: [...definitions.values()].sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0), dependencies, order };
|
|
1607
1615
|
}
|
|
1608
|
-
function evaluateFormulaGraph(formulas, inputs = {}) {
|
|
1616
|
+
function evaluateFormulaGraph(formulas, inputs = {}, options = {}) {
|
|
1609
1617
|
formulaObject(inputs, 'formula inputs');
|
|
1618
|
+
const policy = options.roundingPolicy ?? FORMULA_ROUNDING_POLICY;
|
|
1610
1619
|
const values = new Map(Object.entries(inputs).map(([key, value]) => [key, Decimal.from(value)]));
|
|
1611
1620
|
const graph = compileFormulaGraph(formulas);
|
|
1612
1621
|
const definitions = new Map(graph.formulas.map(item => [item.key, item.expression]));
|
|
@@ -1618,13 +1627,14 @@ function evaluateFormulaGraph(formulas, inputs = {}) {
|
|
|
1618
1627
|
return values.get(key);
|
|
1619
1628
|
}
|
|
1620
1629
|
if (visiting.has(key)) throw formulaError('FORMULA_CYCLE', `formula cycle at ${key}`);
|
|
1621
|
-
visiting.add(key); const value = evaluateNode(definitions.get(key), resolve); visiting.delete(key); results.set(key, value); return value;
|
|
1630
|
+
visiting.add(key); const value = evaluateNode(definitions.get(key), resolve, policy); visiting.delete(key); results.set(key, value); return value;
|
|
1622
1631
|
};
|
|
1623
1632
|
return Object.fromEntries(graph.formulas.map(item => [item.key, resolve(item.key).toString()]));
|
|
1624
1633
|
}
|
|
1625
1634
|
|
|
1626
1635
|
const FORMULA_SCHEMA = 'entrorove.formula-engine/1.0';
|
|
1627
1636
|
const FORMULA_RUNTIME_VERSION = 'decimal28-ast/1.0';
|
|
1637
|
+
{ FORMULA_ROUNDING_POLICY };
|
|
1628
1638
|
const TYPES = ['number', 'integer', 'money', 'percent', 'text', 'date', 'enum'];
|
|
1629
1639
|
const NUMERIC = TYPES.slice(0, 4);
|
|
1630
1640
|
const ID = /^[a-z0-9][a-z0-9._-]{0,127}$/;
|
|
@@ -1642,6 +1652,14 @@ function cloned(value) { return JSON.parse(formulaCanonical(value)); }
|
|
|
1642
1652
|
function requiredText(value, field) {
|
|
1643
1653
|
if (typeof value !== 'string' || !value.trim()) throw formulaError('ENGINE_REQUIRED_FIELD', `${field} is required`);
|
|
1644
1654
|
}
|
|
1655
|
+
function normalizeRoundingPolicy(value) {
|
|
1656
|
+
const policy = value ?? FORMULA_ROUNDING_POLICY;
|
|
1657
|
+
formulaObject(policy, 'roundingPolicy');
|
|
1658
|
+
if (Object.keys(policy).sort().join(',') !== 'decimalPlaces,mode'
|
|
1659
|
+
|| !Number.isInteger(policy.decimalPlaces) || policy.decimalPlaces < 0 || policy.decimalPlaces > 28
|
|
1660
|
+
|| policy.mode !== 'half-away-from-zero') throw formulaError('ROUND_POLICY_INVALID', 'roundingPolicy must declare decimalPlaces 0..28 and half-away-from-zero mode');
|
|
1661
|
+
return { decimalPlaces: policy.decimalPlaces, mode: policy.mode };
|
|
1662
|
+
}
|
|
1645
1663
|
function normalizeEngine(engine) {
|
|
1646
1664
|
formulaObject(engine, 'engine');
|
|
1647
1665
|
if (engine.schemaVersion !== 'engine.spec/1') throw formulaError('ENGINE_SCHEMA_VERSION', 'Expected engine.spec/1');
|
|
@@ -1662,12 +1680,14 @@ function normalizeEngine(engine) {
|
|
|
1662
1680
|
if (NUMERIC.includes(field.type) && typeof field.unit !== 'string') throw formulaError('FIELD_UNIT_MISSING', 'numeric fields require an explicit unit');
|
|
1663
1681
|
if (field.required !== undefined && typeof field.required !== 'boolean') throw formulaError('FIELD_REQUIRED_INVALID', 'field required must be boolean');
|
|
1664
1682
|
}
|
|
1683
|
+
const roundingPolicy = normalizeRoundingPolicy(engine.roundingPolicy);
|
|
1665
1684
|
const graph = compileFormulaGraph(engine.formulas, [...keys]);
|
|
1666
1685
|
for (const item of graph.formulas) for (const ref of graph.dependencies[item.key]) {
|
|
1667
1686
|
const field = engine.fields.find(entry => entry.key === ref);
|
|
1668
1687
|
if (field && !NUMERIC.includes(field.type)) throw formulaError('FORMULA_REF_TYPE', `formula reference ${ref} must be numeric`);
|
|
1669
1688
|
}
|
|
1670
1689
|
const normalized = cloned(engine);
|
|
1690
|
+
normalized.roundingPolicy = roundingPolicy;
|
|
1671
1691
|
normalized.formulas = engine.formulas.map(item => ({ ...cloned(item), expression: graph.formulas.find(entry => entry.key === item.key).expression }));
|
|
1672
1692
|
return { engine: normalized, graph };
|
|
1673
1693
|
}
|
|
@@ -1683,7 +1703,8 @@ function buildFormulaEngine(requirements) {
|
|
|
1683
1703
|
semanticVersion: requirements.semanticVersion ?? '1.0.0', status: 'draft', compatibilityProfile: 'legacy-compatible',
|
|
1684
1704
|
decimalPolicy: requirements.decimalPolicy ?? 'decimal-string', defaultLocale: requirements.defaultLocale ?? 'zh-CN',
|
|
1685
1705
|
category: requirements.category ?? 'general', inputMethod: requirements.inputMethod ?? 'manual', output: requirements.output ?? ['metric-cards'],
|
|
1686
|
-
constraints: requirements.constraints ?? '',
|
|
1706
|
+
constraints: requirements.constraints ?? '', roundingPolicy: requirements.roundingPolicy ?? FORMULA_ROUNDING_POLICY,
|
|
1707
|
+
fields: requirements.inputs, formulas: requirements.formulas,
|
|
1687
1708
|
rules: requirements.rules ?? [], views: requirements.views ?? [], importProfiles: requirements.importProfiles ?? [],
|
|
1688
1709
|
reports: requirements.reports ?? [], testSuites: requirements.testSuites ?? [], acceptance: requirements.acceptance ?? '',
|
|
1689
1710
|
};
|
|
@@ -1692,6 +1713,7 @@ function buildFormulaEngine(requirements) {
|
|
|
1692
1713
|
function compileFormulaEngine(engine) {
|
|
1693
1714
|
const normalized = normalizeEngine(engine);
|
|
1694
1715
|
const program = { schemaVersion: FORMULA_SCHEMA, runtimeVersion: FORMULA_RUNTIME_VERSION, decimalPolicy: DECIMAL_POLICY,
|
|
1716
|
+
roundingPolicy: normalized.engine.roundingPolicy,
|
|
1695
1717
|
engineId: normalized.engine.engineId, engineVersion: normalized.engine.semanticVersion,
|
|
1696
1718
|
engineDigest: formulaDigest(normalized.engine), engine: normalized.engine,
|
|
1697
1719
|
formulas: normalized.graph.formulas, dependencies: normalized.graph.dependencies, order: normalized.graph.order };
|
|
@@ -1719,7 +1741,8 @@ function evaluateFormulaProgram(program, inputs = {}, expected = {}) {
|
|
|
1719
1741
|
} else if (typeof value !== 'string') throw formulaError('ENGINE_INPUT_TYPE', `${key} must be text`);
|
|
1720
1742
|
}
|
|
1721
1743
|
for (const field of fields.values()) if (field.required && (!Object.hasOwn(inputs, field.key) || inputs[field.key] === '' || inputs[field.key] === null)) throw formulaError('MISSING_INPUT', `missing input: ${field.key}`, { fieldId: field.key });
|
|
1722
|
-
return evaluateFormulaGraph(verified.formulas, Object.fromEntries(Object.entries(inputs).filter(([key]) => NUMERIC.includes(fields.get(key).type)))
|
|
1744
|
+
return evaluateFormulaGraph(verified.formulas, Object.fromEntries(Object.entries(inputs).filter(([key]) => NUMERIC.includes(fields.get(key).type))),
|
|
1745
|
+
{ roundingPolicy: verified.roundingPolicy });
|
|
1723
1746
|
}
|
|
1724
1747
|
function formulaImpact(program, changedKeys) {
|
|
1725
1748
|
const verified = verifyFormulaProgram(program);
|
|
@@ -1818,7 +1841,7 @@ function createCalctoolEntrypoint(core) {
|
|
|
1818
1841
|
const output = await core.run(request, options);
|
|
1819
1842
|
return { ...output, capabilities: { ...output.capabilities, operations: CLOUD_OPERATIONS, localRunnerOperations: LOCAL_OPERATIONS,
|
|
1820
1843
|
formulaOperators: FORMULA_OPERATORS, engineSchema: 'engine.spec/1', formulaRuntime: 'decimal28-ast/1.0',
|
|
1821
|
-
decimalPolicy: { precision: 28, rounding: 'half-away-from-zero', representation: 'decimal-string' } },
|
|
1844
|
+
decimalPolicy: { precision: 28, rounding: 'half-away-from-zero', representation: 'decimal-string' }, roundingPolicy: FORMULA_ROUNDING_POLICY },
|
|
1822
1845
|
localRunner: { command: 'entrorove local calctool-<operation>', requiredFor: LOCAL_OPERATIONS } };
|
|
1823
1846
|
}
|
|
1824
1847
|
if (request.operation === 'validate') {
|