@ngockhoale/ukit 3.0.8 → 3.0.9
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/CHANGELOG.md +14 -1
- package/manifests/documentation.yaml +11 -0
- package/package.json +1 -1
- package/scripts/audit/decision-coverage.mjs +29 -2
- package/scripts/bench/data-foundation.mjs +52 -3
- package/scripts/bench/decision-runtime-baseline.mjs +427 -0
- package/scripts/bench/decision-runtime-metrics.mjs +67 -0
- package/scripts/bench/decision-runtime-variant.mjs +626 -0
- package/scripts/bench/memory-ablation.mjs +495 -0
- package/scripts/bench/memory-baseline.mjs +596 -0
- package/scripts/bench/memory-bench.mjs +661 -0
- package/scripts/bench/memory-canary.mjs +321 -0
- package/scripts/bench/memory-corpus.mjs +354 -0
- package/scripts/bench/memory-gate.mjs +389 -0
- package/scripts/bench/memory-metrics.mjs +179 -0
- package/scripts/bench/parallel-agents.mjs +33 -11
- package/scripts/bench/recorder-overhead.mjs +204 -0
- package/scripts/bench/sqlite-spike.mjs +451 -0
- package/scripts/measure-decision-gateway.mjs +306 -0
- package/scripts/perf/audit-perf.mjs +35 -17
- package/src/bug/triageBug.js +4 -3
- package/src/cli/commands/memory.js +357 -63
- package/src/context/detectProjectContext.js +11 -1
- package/src/core/agentRuntime/adapters.js +254 -0
- package/src/core/agentRuntime/artifacts.js +192 -0
- package/src/core/agentRuntime/completionGate.js +176 -0
- package/src/core/agentRuntime/context.js +149 -0
- package/src/core/agentRuntime/contract.js +247 -0
- package/src/core/agentRuntime/diagnostics.js +244 -0
- package/src/core/agentRuntime/evaluation.js +163 -0
- package/src/core/agentRuntime/eventStore.js +404 -0
- package/src/core/agentRuntime/liveness.js +60 -0
- package/src/core/agentRuntime/planCompiler.js +322 -0
- package/src/core/agentRuntime/promotion.js +53 -0
- package/src/core/agentRuntime/qualityComparison.js +112 -0
- package/src/core/agentRuntime/recovery.js +266 -0
- package/src/core/agentRuntime/resourcePolicy.js +78 -0
- package/src/core/agentRuntime/runtimeSupport.js +237 -0
- package/src/core/agentRuntime/supervisor.js +565 -0
- package/src/core/agentRuntime/vmEngine.js +621 -0
- package/src/core/codeintel/analogy.js +3 -2
- package/src/core/experiments/dynamicWorkflow.js +17 -2
- package/src/core/fileOps.js +21 -3
- package/src/core/memory/deltaOverlays.js +75 -30
- package/src/core/memory/learningCandidates.js +93 -48
- package/src/core/memory/memoryFlags.js +83 -0
- package/src/core/memory/memoryFreshness.js +190 -0
- package/src/core/memory/memoryHit.js +144 -0
- package/src/core/memory/migrate.js +69 -189
- package/src/core/memory/migrateMapping.js +232 -0
- package/src/core/memory/mutateMemory.js +323 -0
- package/src/core/memory/policy.js +96 -0
- package/src/core/memory/projectIdentity.js +266 -0
- package/src/core/memory/recordIndex.js +178 -0
- package/src/core/memory/recordStore.js +133 -20
- package/src/core/memory/records.js +144 -6
- package/src/core/memory/retrieval.js +259 -125
- package/src/core/memory/store.js +16 -5
- package/src/core/memory/storeBackup.js +226 -0
- package/src/core/memory/storeV2.js +63 -26
- package/src/core/memory/storeV2Loader.js +30 -12
- package/src/core/memory/userMemory.js +38 -20
- package/src/core/memory/writeClassification.js +161 -0
- package/src/core/memory/writeGuard.js +129 -0
- package/src/core/observability/adapters/hookTelemetryAdapter.js +90 -0
- package/src/core/observability/analytics/cohorts.js +148 -0
- package/src/core/observability/analytics/storeDigest.js +163 -0
- package/src/core/observability/evaluation/experimentPlan.js +95 -0
- package/src/core/observability/evaluation/findings.js +99 -0
- package/src/core/observability/evaluation/optimizationKnowledge.js +10 -1
- package/src/core/observability/evaluation/perturbation.js +273 -0
- package/src/core/observability/evaluation/replay.js +7 -1
- package/src/core/observability/evaluation/scorecard.js +23 -3
- package/src/core/observability/rollout.js +11 -7
- package/src/core/observability/schema/compatibility.js +135 -0
- package/src/core/observability/schema/registry.js +99 -0
- package/src/core/observability/schema/validate.js +7 -0
- package/src/core/observability/support/import.js +53 -9
- package/src/core/observability/support/paths.js +13 -3
- package/src/core/observability/support/projector.js +148 -12
- package/src/core/output/index.js +12 -2
- package/src/core/runtimeConfig.js +83 -0
- package/src/core/runtimePaths.js +3 -0
- package/src/core/sensitiveValueScanner.js +40 -0
- package/src/core/token/index.js +40 -3
- package/src/decision/client.js +37 -13
- package/src/decision/protocol.js +1 -1
- package/src/decision/registry.js +5 -3
- package/src/decision/runtimeDecide.js +242 -0
- package/src/decision/runtimeFilter.js +150 -0
- package/src/decision/runtimeScheduler.js +239 -0
- package/src/index/buildIndex.js +13 -12
- package/src/index/queryIndex.js +35 -14
- package/src/index/relatedTests.js +50 -8
- package/src/index/resolveContext.js +9 -4
- package/src/manifest/selectItems.js +7 -3
- package/src/render/instructionRenderer.js +17 -5
- package/template_project/.claude/ukit/index/lib/index-core.mjs +94 -39
- package/template_project/.claude/ukit/index/route-task.mjs +121 -19
- package/template_project/.claude/ukit/index/unic-decision.mjs +28 -13
- package/template_project/.claude/ukit/runtime/memory-flags.mjs +51 -0
- package/template_project/.claude/ukit/runtime/memory-freshness.mjs +155 -0
- package/template_project/.claude/ukit/runtime/memory-policy.mjs +286 -0
- package/template_project/.claude/ukit/runtime/output-compression.mjs +3 -0
- package/template_project/.claude/ukit/runtime/reinject-context.mjs +145 -14
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agentRuntime/planCompiler.js — decision-first-runtime G4 (DR-06), IR v1.
|
|
3
|
+
*
|
|
4
|
+
* Pure plan compiler: validates a small DAG IR (frozen opcode set: RUN,
|
|
5
|
+
* WAIT_EVENT, BRANCH, COMPLETE, ESCALATE) and emits an immutable
|
|
6
|
+
* ValidatedPlan { planVersion, nodes, entryNodes, order }.
|
|
7
|
+
*
|
|
8
|
+
* No I/O, no runtimeConfig reads, no imports of vmEngine. The only
|
|
9
|
+
* dependency is the read-only SIDE_EFFECT_CLASSES table from contract.js.
|
|
10
|
+
*
|
|
11
|
+
* SPEC: docs/AI_HANDOFF/SPEC.md §2 G4-FR01/02, §4, §8.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
15
|
+
|
|
16
|
+
import { SIDE_EFFECT_CLASSES } from './contract.js';
|
|
17
|
+
|
|
18
|
+
export const PLAN_OPS = Object.freeze([
|
|
19
|
+
'RUN',
|
|
20
|
+
'WAIT_EVENT',
|
|
21
|
+
'BRANCH',
|
|
22
|
+
'COMPLETE',
|
|
23
|
+
'ESCALATE',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const OP_SET = new Set(PLAN_OPS);
|
|
27
|
+
const SIDE_EFFECT_SET = new Set(SIDE_EFFECT_CLASSES);
|
|
28
|
+
|
|
29
|
+
/** Classes that may auto-retry under bounded attempts (mirrors G1). */
|
|
30
|
+
const AUTO_RETRYABLE = new Set(['pure', 'read_only', 'idempotent_write']);
|
|
31
|
+
|
|
32
|
+
const DEFAULT_MAX_NODES = 8;
|
|
33
|
+
const DEFAULT_MAX_ATTEMPTS = 1;
|
|
34
|
+
const MAX_ATTEMPTS_CEILING = 3;
|
|
35
|
+
const SELECTOR_OPS = new Set(['eq', 'neq', 'in', 'exists']);
|
|
36
|
+
const PATH_TRAVERSAL = /(^|\/)\.\.(\/|$)/;
|
|
37
|
+
|
|
38
|
+
function err(errors, code, nodeId, detail) {
|
|
39
|
+
errors.push({ code, nodeId, detail });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const isNonEmptyString = (v) => typeof v === 'string' && v.length > 0;
|
|
43
|
+
|
|
44
|
+
const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
45
|
+
|
|
46
|
+
const isPositiveFinite = (v) => typeof v === 'number' && Number.isFinite(v) && v > 0;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Canonical serialization for content hashing: keys sorted recursively so
|
|
50
|
+
* identical IR always produces identical bytes.
|
|
51
|
+
*/
|
|
52
|
+
function canonicalize(value) {
|
|
53
|
+
if (Array.isArray(value)) {
|
|
54
|
+
return `[${value.map(canonicalize).join(',')}]`;
|
|
55
|
+
}
|
|
56
|
+
if (isPlainObject(value)) {
|
|
57
|
+
const keys = Object.keys(value).sort();
|
|
58
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalize(value[k])}`).join(',')}}`;
|
|
59
|
+
}
|
|
60
|
+
return JSON.stringify(value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function deepFreeze(value) {
|
|
64
|
+
if (isPlainObject(value) || Array.isArray(value)) {
|
|
65
|
+
for (const key of Object.keys(value)) {
|
|
66
|
+
deepFreeze(value[key]);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return Object.freeze(value);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function validateSelector(selector, nodeId, errors) {
|
|
73
|
+
if (!isPlainObject(selector)) {
|
|
74
|
+
err(errors, 'invalid_selector', nodeId, 'selector must be an object');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (!isNonEmptyString(selector.eventType)) {
|
|
78
|
+
err(errors, 'invalid_selector', nodeId, 'selector.eventType required');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (selector.op !== undefined && !SELECTOR_OPS.has(selector.op)) {
|
|
82
|
+
err(errors, 'invalid_selector', nodeId, `selector.op ${JSON.stringify(selector.op)} unsupported`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (selector.fields !== undefined) {
|
|
86
|
+
if (!Array.isArray(selector.fields) || selector.fields.some((f) => !isNonEmptyString(f))) {
|
|
87
|
+
err(errors, 'invalid_selector', nodeId, 'selector.fields must be non-empty strings');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function validateSpec(spec, nodeId, errors) {
|
|
93
|
+
if (!isPlainObject(spec)) {
|
|
94
|
+
err(errors, 'permission_escalation', nodeId, 'RUN node requires a spec');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const declared = Array.isArray(spec.sideEffects) ? spec.sideEffects : [];
|
|
98
|
+
for (const cls of declared) {
|
|
99
|
+
if (!SIDE_EFFECT_SET.has(cls)) {
|
|
100
|
+
err(errors, 'unknown_side_effect_class', nodeId, `spec.sideEffects ${JSON.stringify(cls)}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Write targets must stay inside declared allowedPaths, if provided.
|
|
104
|
+
const writes = Array.isArray(spec.writes) ? spec.writes : [];
|
|
105
|
+
const allowed = Array.isArray(spec.allowedPaths) ? spec.allowedPaths : null;
|
|
106
|
+
for (const target of writes) {
|
|
107
|
+
if (!isNonEmptyString(target)) {
|
|
108
|
+
err(errors, 'permission_escalation', nodeId, 'spec.writes entries must be paths');
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (PATH_TRAVERSAL.test(target)) {
|
|
112
|
+
err(errors, 'permission_escalation', nodeId, `write ${target} escapes via '..'`);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (allowed && !allowed.some((prefix) => isNonEmptyString(prefix) && target.startsWith(prefix))) {
|
|
116
|
+
err(errors, 'permission_escalation', nodeId, `write ${target} outside allowedPaths`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return declared;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function validateRetryPolicy(retry, nodeId, declaredEffects, errors) {
|
|
123
|
+
if (retry === undefined) return undefined;
|
|
124
|
+
if (!isPlainObject(retry)) {
|
|
125
|
+
err(errors, 'invalid_retry', nodeId, 'retry must be an object');
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const cls = retry.sideEffectClass;
|
|
130
|
+
if (!SIDE_EFFECT_SET.has(cls)) {
|
|
131
|
+
err(errors, 'unknown_side_effect_class', nodeId, `retry.sideEffectClass ${JSON.stringify(cls)}`);
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// A RUN node may not retry a side-effect class its spec never declared.
|
|
136
|
+
if (Array.isArray(declaredEffects) && !declaredEffects.includes(cls)) {
|
|
137
|
+
err(errors, 'permission_escalation', nodeId, `retry class ${cls} not in spec.sideEffects`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const maxAttempts = retry.maxAttempts === undefined ? DEFAULT_MAX_ATTEMPTS : retry.maxAttempts;
|
|
141
|
+
if (!Number.isInteger(maxAttempts) || maxAttempts < 1) {
|
|
142
|
+
err(errors, 'invalid_retry', nodeId, 'maxAttempts must be an integer >= 1');
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
if (maxAttempts > MAX_ATTEMPTS_CEILING) {
|
|
146
|
+
err(errors, 'invalid_retry', nodeId, `maxAttempts ${maxAttempts} exceeds ceiling 3`);
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
if (maxAttempts > 1 && !AUTO_RETRYABLE.has(cls)) {
|
|
150
|
+
err(errors, 'invalid_retry', nodeId, `class ${cls} is not auto-retryable`);
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
if (retry.backoffMs !== undefined
|
|
154
|
+
&& (!Number.isFinite(retry.backoffMs) || retry.backoffMs < 0)) {
|
|
155
|
+
err(errors, 'invalid_retry', nodeId, 'backoffMs must be a finite number >= 0');
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { sideEffectClass: cls, maxAttempts, backoffMs: retry.backoffMs ?? 0 };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* compilePlan(ir, opts) — SPEC §4.
|
|
164
|
+
*
|
|
165
|
+
* @param {object} ir `{ nodes: [...] }`
|
|
166
|
+
* @param {{maxNodes?:number, allowedOps?:string[]}} [opts]
|
|
167
|
+
* @returns {{ok:true, plan:object}|{ok:false, errors:object[]}}
|
|
168
|
+
*/
|
|
169
|
+
export function compilePlan(ir, opts = {}) {
|
|
170
|
+
const errors = [];
|
|
171
|
+
const maxNodes = Number.isInteger(opts.maxNodes) && opts.maxNodes > 0
|
|
172
|
+
? opts.maxNodes
|
|
173
|
+
: DEFAULT_MAX_NODES;
|
|
174
|
+
const allowedOps = Array.isArray(opts.allowedOps)
|
|
175
|
+
? new Set(opts.allowedOps)
|
|
176
|
+
: OP_SET;
|
|
177
|
+
|
|
178
|
+
const rawNodes = Array.isArray(ir?.nodes) ? ir.nodes : [];
|
|
179
|
+
if (rawNodes.length === 0) {
|
|
180
|
+
err(errors, 'empty_plan', undefined, 'ir.nodes must be a non-empty array');
|
|
181
|
+
return { ok: false, errors };
|
|
182
|
+
}
|
|
183
|
+
if (rawNodes.length > maxNodes) {
|
|
184
|
+
err(errors, 'too_many_nodes', undefined, `${rawNodes.length} nodes exceeds maxNodes ${maxNodes}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const ids = new Set();
|
|
188
|
+
for (const node of rawNodes) {
|
|
189
|
+
const id = node?.id;
|
|
190
|
+
if (!isNonEmptyString(id) || ids.has(id)) {
|
|
191
|
+
err(errors, 'invalid_node', node?.id, 'node ids must be unique non-empty strings');
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
ids.add(id);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const compiled = new Map();
|
|
198
|
+
|
|
199
|
+
for (const node of rawNodes) {
|
|
200
|
+
const nodeId = node?.id;
|
|
201
|
+
if (!ids.has(nodeId)) continue;
|
|
202
|
+
|
|
203
|
+
const op = node.op;
|
|
204
|
+
if (!OP_SET.has(op) || !allowedOps.has(op)) {
|
|
205
|
+
err(errors, 'unknown_op', nodeId, `op ${JSON.stringify(op)} not allowed`);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const deps = node.deps === undefined ? [] : node.deps;
|
|
210
|
+
if (!Array.isArray(deps) || deps.some((d) => !isNonEmptyString(d))) {
|
|
211
|
+
err(errors, 'missing_ref', nodeId, 'deps must be an array of node ids');
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
for (const dep of deps) {
|
|
215
|
+
if (!ids.has(dep)) {
|
|
216
|
+
err(errors, 'missing_ref', nodeId, `dep ${dep} not in plan`);
|
|
217
|
+
}
|
|
218
|
+
if (dep === nodeId) {
|
|
219
|
+
err(errors, 'cycle', nodeId, 'self-dependency');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (node.timeoutMs !== undefined && !isPositiveFinite(node.timeoutMs)) {
|
|
224
|
+
err(errors, 'unbounded_timeout', nodeId, 'timeoutMs must be a finite positive number');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let declaredEffects;
|
|
228
|
+
if (op === 'RUN') {
|
|
229
|
+
if (!isPositiveFinite(node.timeoutMs)) {
|
|
230
|
+
err(errors, 'unbounded_timeout', nodeId, 'RUN requires finite timeoutMs');
|
|
231
|
+
}
|
|
232
|
+
declaredEffects = validateSpec(node.spec, nodeId, errors);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (op === 'WAIT_EVENT') {
|
|
236
|
+
if (!isPositiveFinite(node.timeoutMs)) {
|
|
237
|
+
err(errors, 'unbounded_timeout', nodeId, 'WAIT_EVENT requires finite timeoutMs');
|
|
238
|
+
}
|
|
239
|
+
validateSelector(node.selector, nodeId, errors);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let branches;
|
|
243
|
+
if (op === 'BRANCH') {
|
|
244
|
+
validateSelector(node.selector, nodeId, errors);
|
|
245
|
+
branches = node.branches;
|
|
246
|
+
if (!isPlainObject(branches) || Object.keys(branches).length === 0) {
|
|
247
|
+
err(errors, 'missing_ref', nodeId, 'BRANCH requires non-empty branches map');
|
|
248
|
+
} else {
|
|
249
|
+
for (const target of Object.values(branches)) {
|
|
250
|
+
if (!ids.has(target)) {
|
|
251
|
+
err(errors, 'missing_ref', nodeId, `branch target ${JSON.stringify(target)} not in plan`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const retry = validateRetryPolicy(node.retry, nodeId, declaredEffects, errors);
|
|
258
|
+
|
|
259
|
+
const compiledNode = {
|
|
260
|
+
id: nodeId,
|
|
261
|
+
op,
|
|
262
|
+
deps: [...deps],
|
|
263
|
+
...(retry ? { retry } : {}),
|
|
264
|
+
...(node.selector !== undefined ? { selector: deepFreeze({ ...node.selector }) } : {}),
|
|
265
|
+
...(node.timeoutMs !== undefined ? { timeoutMs: node.timeoutMs } : {}),
|
|
266
|
+
...(node.spec !== undefined ? { spec: deepFreeze({ ...node.spec }) } : {}),
|
|
267
|
+
...(branches !== undefined ? { branches: deepFreeze({ ...branches }) } : {}),
|
|
268
|
+
};
|
|
269
|
+
compiled.set(nodeId, compiledNode);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Topological order (Kahn). Deterministic: process in input order.
|
|
273
|
+
const indegree = new Map();
|
|
274
|
+
const dependents = new Map();
|
|
275
|
+
for (const node of compiled.values()) {
|
|
276
|
+
indegree.set(node.id, node.deps.filter((d) => compiled.has(d)).length);
|
|
277
|
+
for (const dep of node.deps) {
|
|
278
|
+
if (!compiled.has(dep)) continue;
|
|
279
|
+
if (!dependents.has(dep)) dependents.set(dep, []);
|
|
280
|
+
dependents.get(dep).push(node.id);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const order = [];
|
|
285
|
+
const queue = rawNodes
|
|
286
|
+
.map((n) => n?.id)
|
|
287
|
+
.filter((id) => compiled.has(id) && indegree.get(id) === 0);
|
|
288
|
+
const entryNodes = [...queue];
|
|
289
|
+
while (queue.length > 0) {
|
|
290
|
+
const id = queue.shift();
|
|
291
|
+
order.push(id);
|
|
292
|
+
for (const next of dependents.get(id) ?? []) {
|
|
293
|
+
indegree.set(next, indegree.get(next) - 1);
|
|
294
|
+
if (indegree.get(next) === 0) queue.push(next);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (order.length < compiled.size) {
|
|
298
|
+
err(errors, 'cycle', undefined, 'dependency cycle detected');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (errors.length > 0) {
|
|
302
|
+
return { ok: false, errors };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const planVersion = createHash('sha256')
|
|
306
|
+
.update(canonicalize(order.map((id) => compiled.get(id))), 'utf8')
|
|
307
|
+
.digest('hex')
|
|
308
|
+
.slice(0, 16);
|
|
309
|
+
|
|
310
|
+
const nodes = new Map();
|
|
311
|
+
for (const id of order) {
|
|
312
|
+
nodes.set(id, deepFreeze(compiled.get(id)));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const plan = Object.freeze({
|
|
316
|
+
planVersion,
|
|
317
|
+
nodes: Object.freeze(nodes),
|
|
318
|
+
entryNodes: Object.freeze(entryNodes),
|
|
319
|
+
order: Object.freeze(order),
|
|
320
|
+
});
|
|
321
|
+
return { ok: true, plan };
|
|
322
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// src/core/agentRuntime/promotion.js
|
|
2
|
+
// Pure caller-wait promotion decision (SPEC §2 G6-FR01/02, §4).
|
|
3
|
+
// selectWaitPolicy(profile, operation, resources) → frozen
|
|
4
|
+
// { mode: 'foreground'|'handle', reason: string }
|
|
5
|
+
// 'handle' is permitted only when the injected frozen DurationProfile records
|
|
6
|
+
// the operation class as long-running (promoteAfterMs: number) AND
|
|
7
|
+
// resources.pressure is not 'critical'. Every other input resolves
|
|
8
|
+
// 'foreground' with a typed reason — never throws, never returns undefined.
|
|
9
|
+
// Promotion changes caller waiting semantics only: this module performs no
|
|
10
|
+
// I/O, reads no config, and never spawns/signals an OS process.
|
|
11
|
+
|
|
12
|
+
const PROFILE_VERSION = 1;
|
|
13
|
+
|
|
14
|
+
function isObj(v) {
|
|
15
|
+
return v != null && typeof v === 'object';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function policy(mode, reason) {
|
|
19
|
+
return Object.freeze({ mode, reason });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function foreground(reason) {
|
|
23
|
+
return policy('foreground', reason);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {object} profile DurationProfile {version:1, classes:{<name>:{promoteAfterMs:number|null}}, fallback:'foreground'}
|
|
28
|
+
* @param {object} operation {class: string}
|
|
29
|
+
* @param {object} resources {pressure: 'normal'|'elevated'|'critical'}
|
|
30
|
+
* @returns {{mode:'foreground'|'handle', reason:string}} frozen
|
|
31
|
+
*/
|
|
32
|
+
export function selectWaitPolicy(profile, operation, resources) {
|
|
33
|
+
if (!isObj(profile) || !isObj(profile.classes)) return foreground('profile:absent');
|
|
34
|
+
if (profile.version !== PROFILE_VERSION) return foreground('profile:version');
|
|
35
|
+
|
|
36
|
+
const cls = isObj(operation) && typeof operation.class === 'string'
|
|
37
|
+
? operation.class
|
|
38
|
+
: null;
|
|
39
|
+
const entry = cls != null ? profile.classes[cls] : undefined;
|
|
40
|
+
if (cls == null || !isObj(entry)) return foreground('class:unknown');
|
|
41
|
+
|
|
42
|
+
const pressure = isObj(resources) && typeof resources.pressure === 'string'
|
|
43
|
+
? resources.pressure
|
|
44
|
+
: 'normal';
|
|
45
|
+
if (pressure === 'critical') return foreground('pressure:critical');
|
|
46
|
+
|
|
47
|
+
const promoteAfterMs = entry.promoteAfterMs;
|
|
48
|
+
if (typeof promoteAfterMs !== 'number' || !Number.isFinite(promoteAfterMs)) {
|
|
49
|
+
return foreground(`class:${cls}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return policy('handle', `class:${cls}:promoteAfterMs:${promoteAfterMs}`);
|
|
53
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Shadow quality-comparison harness (G5 / TASK-004, G5-FR06).
|
|
2
|
+
// Pure replay: re-runs recorded evidence inputs through the completion-evidence
|
|
3
|
+
// gate and compares against the recorded ledger evaluateCompletion() result.
|
|
4
|
+
// No ledger writes, no VM spawns, no model calls — the gate evaluator is
|
|
5
|
+
// injected (defaults to a dynamic import of ./completionGate.js so this module
|
|
6
|
+
// lands safely when TASK-003 ships in the same wave).
|
|
7
|
+
//
|
|
8
|
+
// Ledger verdict semantics (template_project/.claude/ukit/runtime/execution-ledger.mjs):
|
|
9
|
+
// complete:true → released (all evidence satisfied)
|
|
10
|
+
// continue:true → blocked (unfinished work keeps recovering)
|
|
11
|
+
// continue:false (loud release, no complete flag) → released
|
|
12
|
+
// Gate verdict semantics (SPEC §4):
|
|
13
|
+
// PASS → released; MORE_EVIDENCE | FIX_REQUIRED | FAILED → blocked.
|
|
14
|
+
// Comparison kinds:
|
|
15
|
+
// agree — both released or both blocked
|
|
16
|
+
// stricter — ledger released where the gate blocks (counted as
|
|
17
|
+
// falsePassBlocks: a ledger release the gate would have held)
|
|
18
|
+
// looser — gate released where the ledger blocked
|
|
19
|
+
// unverifiable — recorded data insufficient; never guessed.
|
|
20
|
+
|
|
21
|
+
export const VERDICT_KINDS = Object.freeze(['agree', 'stricter', 'looser', 'unverifiable']);
|
|
22
|
+
|
|
23
|
+
const BLOCKING_VERDICTS = new Set(['MORE_EVIDENCE', 'FIX_REQUIRED', 'FAILED']);
|
|
24
|
+
|
|
25
|
+
function gateReleased(verdict) {
|
|
26
|
+
return verdict === 'PASS';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Ledger released = explicit completion OR a loud release without continuation.
|
|
30
|
+
// `continue` must be a boolean; `complete` is optional.
|
|
31
|
+
function ledgerVerdict(ledger) {
|
|
32
|
+
if (ledger == null || typeof ledger !== 'object') return null;
|
|
33
|
+
if (typeof ledger.continue !== 'boolean') return null;
|
|
34
|
+
const released = ledger.complete === true || ledger.continue === false;
|
|
35
|
+
return { complete: ledger.complete === true, continue: ledger.continue, released };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function defaultEvaluator(evidence) {
|
|
39
|
+
const mod = await import('./completionGate.js');
|
|
40
|
+
return mod.evaluateCompletionEvidence(evidence);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function freezeRow(row) {
|
|
44
|
+
return Object.freeze({ ...row, ledger: Object.freeze({ ...row.ledger }) });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function runQualityShadow({ fixtures, evaluator } = {}) {
|
|
48
|
+
const evaluate = typeof evaluator === 'function' ? evaluator : defaultEvaluator;
|
|
49
|
+
const list = Array.isArray(fixtures) ? fixtures : [];
|
|
50
|
+
|
|
51
|
+
const rows = [];
|
|
52
|
+
const summary = { total: 0, agreements: 0, disagreements: 0, falsePassBlocks: 0, unverifiable: 0 };
|
|
53
|
+
|
|
54
|
+
for (const fx of list) {
|
|
55
|
+
const id = fx && fx.id != null ? String(fx.id) : `fixture-${rows.length}`;
|
|
56
|
+
const ledgerV = ledgerVerdict(fx && fx.ledger);
|
|
57
|
+
const evidence = fx && fx.evidence;
|
|
58
|
+
|
|
59
|
+
let verdict = null;
|
|
60
|
+
let evalOk = evidence != null;
|
|
61
|
+
if (evalOk) {
|
|
62
|
+
try {
|
|
63
|
+
const result = await evaluate(evidence);
|
|
64
|
+
verdict = result && typeof result.verdict === 'string' ? result.verdict : null;
|
|
65
|
+
evalOk = verdict === 'PASS' || BLOCKING_VERDICTS.has(verdict);
|
|
66
|
+
} catch {
|
|
67
|
+
evalOk = false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let row;
|
|
72
|
+
if (!evalOk || ledgerV === null) {
|
|
73
|
+
row = freezeRow({
|
|
74
|
+
id,
|
|
75
|
+
gate: 'unverifiable',
|
|
76
|
+
ledger: { complete: ledgerV ? ledgerV.complete : false, continue: ledgerV ? ledgerV.continue : false },
|
|
77
|
+
agreement: false,
|
|
78
|
+
kind: 'unverifiable',
|
|
79
|
+
});
|
|
80
|
+
summary.unverifiable += 1;
|
|
81
|
+
} else {
|
|
82
|
+
const released = gateReleased(verdict);
|
|
83
|
+
let kind;
|
|
84
|
+
let agreement;
|
|
85
|
+
if (released === ledgerV.released) {
|
|
86
|
+
kind = 'agree';
|
|
87
|
+
agreement = true;
|
|
88
|
+
} else {
|
|
89
|
+
kind = ledgerV.released ? 'stricter' : 'looser';
|
|
90
|
+
agreement = false;
|
|
91
|
+
}
|
|
92
|
+
row = freezeRow({
|
|
93
|
+
id,
|
|
94
|
+
gate: verdict,
|
|
95
|
+
ledger: { complete: ledgerV.complete, continue: ledgerV.continue },
|
|
96
|
+
agreement,
|
|
97
|
+
kind,
|
|
98
|
+
});
|
|
99
|
+
if (agreement) {
|
|
100
|
+
summary.agreements += 1;
|
|
101
|
+
} else {
|
|
102
|
+
summary.disagreements += 1;
|
|
103
|
+
if (kind === 'stricter') summary.falsePassBlocks += 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
rows.push(row);
|
|
108
|
+
summary.total += 1;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { rows: Object.freeze(rows), summary: Object.freeze(summary) };
|
|
112
|
+
}
|