@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.
Files changed (105) hide show
  1. package/CHANGELOG.md +14 -1
  2. package/manifests/documentation.yaml +11 -0
  3. package/package.json +1 -1
  4. package/scripts/audit/decision-coverage.mjs +29 -2
  5. package/scripts/bench/data-foundation.mjs +52 -3
  6. package/scripts/bench/decision-runtime-baseline.mjs +427 -0
  7. package/scripts/bench/decision-runtime-metrics.mjs +67 -0
  8. package/scripts/bench/decision-runtime-variant.mjs +626 -0
  9. package/scripts/bench/memory-ablation.mjs +495 -0
  10. package/scripts/bench/memory-baseline.mjs +596 -0
  11. package/scripts/bench/memory-bench.mjs +661 -0
  12. package/scripts/bench/memory-canary.mjs +321 -0
  13. package/scripts/bench/memory-corpus.mjs +354 -0
  14. package/scripts/bench/memory-gate.mjs +389 -0
  15. package/scripts/bench/memory-metrics.mjs +179 -0
  16. package/scripts/bench/parallel-agents.mjs +33 -11
  17. package/scripts/bench/recorder-overhead.mjs +204 -0
  18. package/scripts/bench/sqlite-spike.mjs +451 -0
  19. package/scripts/measure-decision-gateway.mjs +306 -0
  20. package/scripts/perf/audit-perf.mjs +35 -17
  21. package/src/bug/triageBug.js +4 -3
  22. package/src/cli/commands/memory.js +357 -63
  23. package/src/context/detectProjectContext.js +11 -1
  24. package/src/core/agentRuntime/adapters.js +254 -0
  25. package/src/core/agentRuntime/artifacts.js +192 -0
  26. package/src/core/agentRuntime/completionGate.js +176 -0
  27. package/src/core/agentRuntime/context.js +149 -0
  28. package/src/core/agentRuntime/contract.js +247 -0
  29. package/src/core/agentRuntime/diagnostics.js +244 -0
  30. package/src/core/agentRuntime/evaluation.js +163 -0
  31. package/src/core/agentRuntime/eventStore.js +404 -0
  32. package/src/core/agentRuntime/liveness.js +60 -0
  33. package/src/core/agentRuntime/planCompiler.js +322 -0
  34. package/src/core/agentRuntime/promotion.js +53 -0
  35. package/src/core/agentRuntime/qualityComparison.js +112 -0
  36. package/src/core/agentRuntime/recovery.js +266 -0
  37. package/src/core/agentRuntime/resourcePolicy.js +78 -0
  38. package/src/core/agentRuntime/runtimeSupport.js +237 -0
  39. package/src/core/agentRuntime/supervisor.js +565 -0
  40. package/src/core/agentRuntime/vmEngine.js +621 -0
  41. package/src/core/codeintel/analogy.js +3 -2
  42. package/src/core/experiments/dynamicWorkflow.js +17 -2
  43. package/src/core/fileOps.js +21 -3
  44. package/src/core/memory/deltaOverlays.js +75 -30
  45. package/src/core/memory/learningCandidates.js +93 -48
  46. package/src/core/memory/memoryFlags.js +83 -0
  47. package/src/core/memory/memoryFreshness.js +190 -0
  48. package/src/core/memory/memoryHit.js +144 -0
  49. package/src/core/memory/migrate.js +69 -189
  50. package/src/core/memory/migrateMapping.js +232 -0
  51. package/src/core/memory/mutateMemory.js +323 -0
  52. package/src/core/memory/policy.js +96 -0
  53. package/src/core/memory/projectIdentity.js +266 -0
  54. package/src/core/memory/recordIndex.js +178 -0
  55. package/src/core/memory/recordStore.js +133 -20
  56. package/src/core/memory/records.js +144 -6
  57. package/src/core/memory/retrieval.js +259 -125
  58. package/src/core/memory/store.js +16 -5
  59. package/src/core/memory/storeBackup.js +226 -0
  60. package/src/core/memory/storeV2.js +63 -26
  61. package/src/core/memory/storeV2Loader.js +30 -12
  62. package/src/core/memory/userMemory.js +38 -20
  63. package/src/core/memory/writeClassification.js +161 -0
  64. package/src/core/memory/writeGuard.js +129 -0
  65. package/src/core/observability/adapters/hookTelemetryAdapter.js +90 -0
  66. package/src/core/observability/analytics/cohorts.js +148 -0
  67. package/src/core/observability/analytics/storeDigest.js +163 -0
  68. package/src/core/observability/evaluation/experimentPlan.js +95 -0
  69. package/src/core/observability/evaluation/findings.js +99 -0
  70. package/src/core/observability/evaluation/optimizationKnowledge.js +10 -1
  71. package/src/core/observability/evaluation/perturbation.js +273 -0
  72. package/src/core/observability/evaluation/replay.js +7 -1
  73. package/src/core/observability/evaluation/scorecard.js +23 -3
  74. package/src/core/observability/rollout.js +11 -7
  75. package/src/core/observability/schema/compatibility.js +135 -0
  76. package/src/core/observability/schema/registry.js +99 -0
  77. package/src/core/observability/schema/validate.js +7 -0
  78. package/src/core/observability/support/import.js +53 -9
  79. package/src/core/observability/support/paths.js +13 -3
  80. package/src/core/observability/support/projector.js +148 -12
  81. package/src/core/output/index.js +12 -2
  82. package/src/core/runtimeConfig.js +83 -0
  83. package/src/core/runtimePaths.js +3 -0
  84. package/src/core/sensitiveValueScanner.js +40 -0
  85. package/src/core/token/index.js +40 -3
  86. package/src/decision/client.js +37 -13
  87. package/src/decision/protocol.js +1 -1
  88. package/src/decision/registry.js +5 -3
  89. package/src/decision/runtimeDecide.js +242 -0
  90. package/src/decision/runtimeFilter.js +150 -0
  91. package/src/decision/runtimeScheduler.js +239 -0
  92. package/src/index/buildIndex.js +13 -12
  93. package/src/index/queryIndex.js +35 -14
  94. package/src/index/relatedTests.js +50 -8
  95. package/src/index/resolveContext.js +9 -4
  96. package/src/manifest/selectItems.js +7 -3
  97. package/src/render/instructionRenderer.js +17 -5
  98. package/template_project/.claude/ukit/index/lib/index-core.mjs +94 -39
  99. package/template_project/.claude/ukit/index/route-task.mjs +121 -19
  100. package/template_project/.claude/ukit/index/unic-decision.mjs +28 -13
  101. package/template_project/.claude/ukit/runtime/memory-flags.mjs +51 -0
  102. package/template_project/.claude/ukit/runtime/memory-freshness.mjs +155 -0
  103. package/template_project/.claude/ukit/runtime/memory-policy.mjs +286 -0
  104. package/template_project/.claude/ukit/runtime/output-compression.mjs +3 -0
  105. package/template_project/.claude/ukit/runtime/reinject-context.mjs +145 -14
@@ -0,0 +1,242 @@
1
+ // src/decision/runtimeDecide.js — pure (no I/O, no config of its own).
2
+ // C65 G3-FR03/FR04 — registered semantic decisions + bounded fallback.
3
+ //
4
+ // createRuntimeDecider({ client, registry?, confidenceFloor?, now? }) produces
5
+ // the `decideFn` runtimeScheduler expects: one ambiguous classified item →
6
+ // exactly one client.requestBatch → TypedDecision, or a typed conservative
7
+ // Fallback on every failure mode. The gateway is contacted ONLY for items
8
+ // classified 'model_decision' with a registered decisionKey — known signals
9
+ // never reach the model.
10
+ //
11
+ // Fallback contract (SPEC §4, frozen):
12
+ // { decidedBy:'fallback', code:
13
+ // 'timeout'|'invalid'|'unavailable'|'low_confidence'|'abstained',
14
+ // action: <conservative per decisionKey> }
15
+ // Fallback NEVER auto-approves a retry or an escalation-to-action and never
16
+ // throws.
17
+
18
+ import {
19
+ createDecisionRegistry,
20
+ DECISION_REGISTRY,
21
+ } from './registry.js';
22
+
23
+ const DEFAULT_CONFIDENCE_FLOOR = 0.6;
24
+
25
+ /**
26
+ * Registered runtime.*.v1 semantic decisions (G3-FR03). Each entry conforms
27
+ * to validateEntry/DECISION_KEY_PATTERN; `instruction`/`candidates` ride
28
+ * along as entry metadata so the decider can build the wire question without
29
+ * a second source of truth. All ship at rolloutStage 'off'.
30
+ */
31
+ export const RUNTIME_DECISIONS = Object.freeze([
32
+ {
33
+ decisionKey: 'runtime.stall_action.v1',
34
+ schemaVersion: 1,
35
+ family: 'runtime',
36
+ owner: 'runtimeScheduler',
37
+ kind: 'choice',
38
+ description: 'Action for an ambiguous stalled/timeout observation without explicit spec policy.',
39
+ candidatePolicy: 'conservative-stall-actions',
40
+ hardConstraints: ['no-auto-kill'],
41
+ probabilityPolicy: 'raw-label',
42
+ fallbackPolicy: 'frozen-conservative-fallback',
43
+ telemetryClass: 'decision',
44
+ rolloutStage: 'off',
45
+ cacheSensitivity: 'none',
46
+ instruction:
47
+ 'A runtime operation reported a stalled or timeout observation with no explicit spec policy. Choose the next action.',
48
+ candidates: ['continue_observe', 'request_nudge', 'escalate'],
49
+ },
50
+ {
51
+ decisionKey: 'runtime.wake_needed.v1',
52
+ schemaVersion: 1,
53
+ family: 'runtime',
54
+ owner: 'runtimeScheduler',
55
+ kind: 'choice',
56
+ description: 'Whether an ambiguous runtime event needs to wake a host/human, from an unparseable or unknown payload.',
57
+ candidatePolicy: 'conservative-wake-actions',
58
+ hardConstraints: ['no-silent-drop'],
59
+ probabilityPolicy: 'raw-label',
60
+ fallbackPolicy: 'frozen-conservative-fallback',
61
+ telemetryClass: 'decision',
62
+ rolloutStage: 'off',
63
+ cacheSensitivity: 'none',
64
+ instruction:
65
+ 'A runtime event arrived with an unparseable or unrecognized payload. Decide whether to keep observing or escalate.',
66
+ candidates: ['continue_observe', 'escalate'],
67
+ },
68
+ {
69
+ decisionKey: 'runtime.safe_retry.v1',
70
+ schemaVersion: 1,
71
+ family: 'runtime',
72
+ owner: 'runtimeScheduler',
73
+ kind: 'choice',
74
+ description: 'Whether an ambiguous failure-adjacent event is safe to retry within registered bounds.',
75
+ candidatePolicy: 'conservative-retry-actions',
76
+ hardConstraints: ['no-auto-retry-approval'],
77
+ probabilityPolicy: 'raw-label',
78
+ fallbackPolicy: 'frozen-conservative-fallback',
79
+ telemetryClass: 'decision',
80
+ rolloutStage: 'off',
81
+ cacheSensitivity: 'none',
82
+ instruction:
83
+ 'A runtime event may warrant a retry but no deterministic policy covers it. Decide conservatively.',
84
+ candidates: ['retry', 'no_retry', 'escalate'],
85
+ },
86
+ ]);
87
+
88
+ // Decision→fallback action map (frozen, SPEC §4). Fallbacks are conservative:
89
+ // a stall keeps observing (never auto-kills), an unknown retry does not retry,
90
+ // an ambiguous wake escalates to a human boundary.
91
+ const FALLBACK_ACTIONS = Object.freeze({
92
+ 'runtime.stall_action.v1': 'continue_observe',
93
+ 'runtime.wake_needed.v1': 'escalate',
94
+ 'runtime.safe_retry.v1': 'no_retry',
95
+ });
96
+
97
+ // Batch-level statuses → fallback codes. 'abstained' stays distinct so
98
+ // shadow-agreement telemetry can separate model-refusal from transport
99
+ // failure; everything else folds into the nearest conservative class.
100
+ const STATUS_CODES = Object.freeze({
101
+ timeout: 'timeout',
102
+ invalid: 'invalid',
103
+ abstained: 'abstained',
104
+ unavailable: 'unavailable',
105
+ unsupported: 'unavailable',
106
+ 'blocked-sensitive': 'invalid',
107
+ });
108
+
109
+ function isPlainObject(value) {
110
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
111
+ }
112
+
113
+ function fallback(code, action) {
114
+ return { decidedBy: 'fallback', code, action };
115
+ }
116
+
117
+ function fallbackActionFor(decisionKey) {
118
+ return FALLBACK_ACTIONS[decisionKey] ?? 'escalate';
119
+ }
120
+
121
+ function resolveFloor(confidenceFloor, decisionKey) {
122
+ if (isPlainObject(confidenceFloor)) {
123
+ const perKey = confidenceFloor[decisionKey];
124
+ return Number.isFinite(perKey) ? perKey : DEFAULT_CONFIDENCE_FLOOR;
125
+ }
126
+ return Number.isFinite(confidenceFloor) ? confidenceFloor : DEFAULT_CONFIDENCE_FLOOR;
127
+ }
128
+
129
+ // Minimal state packet — only the fields the model needs; the full event
130
+ // payload stays out of the request (budget + sensitive-value gates live in
131
+ // the client).
132
+ function buildStatePacket(item) {
133
+ return {
134
+ stateVersion: 1,
135
+ decisionKey: item.decisionKey ?? null,
136
+ reason: item.reason ?? null,
137
+ eventType: item.event?.eventType ?? item.eventType ?? null,
138
+ };
139
+ }
140
+
141
+ function buildQuestion(item, entry) {
142
+ return {
143
+ decisionKey: entry.decisionKey,
144
+ schemaVersion: entry.schemaVersion ?? 1,
145
+ kind: entry.kind,
146
+ instruction:
147
+ typeof entry.instruction === 'string' && entry.instruction.length > 0
148
+ ? entry.instruction
149
+ : `Decide ${entry.decisionKey} for the described runtime event.`,
150
+ candidates: Array.isArray(entry.candidates) ? entry.candidates : [],
151
+ };
152
+ }
153
+
154
+ /**
155
+ * @param {{client: {requestBatch: Function}, registry?: object,
156
+ * confidenceFloor?: number|object, now?: Function}} options
157
+ * registry: defaults to DECISION_REGISTRY composed with RUNTIME_DECISIONS —
158
+ * the shipped catalog is frozen, so composition copies, never mutates.
159
+ * @returns {{decide: (item: object) => Promise<object>}}
160
+ */
161
+ export function createRuntimeDecider({
162
+ client,
163
+ registry,
164
+ confidenceFloor,
165
+ now = () => Date.now(),
166
+ } = {}) {
167
+ const resolvedRegistry =
168
+ registry ?? createDecisionRegistry([...DECISION_REGISTRY, ...RUNTIME_DECISIONS]);
169
+ void now; // injectable for parity with client/scheduler; no clock use yet.
170
+
171
+ async function decide(item) {
172
+ if (!isPlainObject(item)) {
173
+ return fallback('invalid', 'escalate');
174
+ }
175
+ // Only 'model_decision' items reach the gateway — known local signals are
176
+ // decided upstream by runtimeFilter and pass through untouched.
177
+ if (item.action !== 'model_decision') {
178
+ return item;
179
+ }
180
+ const decisionKey = item.decisionKey;
181
+ const conservative = fallbackActionFor(decisionKey);
182
+ const entry =
183
+ typeof decisionKey === 'string' ? resolvedRegistry.getDecision(decisionKey) : null;
184
+ if (!entry) {
185
+ // Unregistered/unknown key — never send an unregistered question to the
186
+ // gateway; conservative fallback instead.
187
+ return fallback('invalid', conservative);
188
+ }
189
+
190
+ let result;
191
+ try {
192
+ result = await client.requestBatch({
193
+ batchId: item.batchId ?? null,
194
+ boundary: 'runtime',
195
+ statePacket: buildStatePacket(item),
196
+ questions: [buildQuestion(item, entry)],
197
+ });
198
+ } catch {
199
+ // A throwing client never propagates — SPEC §5: decideFn throws →
200
+ // 'invalid', scheduler stays alive.
201
+ return fallback('invalid', conservative);
202
+ }
203
+
204
+ const statusCode = STATUS_CODES[result?.status];
205
+ if (statusCode !== undefined) {
206
+ return fallback(statusCode, conservative);
207
+ }
208
+
209
+ const answer = Array.isArray(result?.answers)
210
+ ? result.answers.find(
211
+ (a) => a?.decisionKey === decisionKey && a?.validationStatus === 'valid',
212
+ )
213
+ : null;
214
+ if (!answer) {
215
+ return fallback('invalid', conservative);
216
+ }
217
+ // Missing confidence is treated as low confidence — never auto-approve.
218
+ const confidence = Number.isFinite(answer.confidence) ? answer.confidence : 0;
219
+ if (confidence < resolveFloor(confidenceFloor, decisionKey)) {
220
+ return fallback('low_confidence', conservative);
221
+ }
222
+
223
+ return {
224
+ decidedBy: 'model',
225
+ decisionKey,
226
+ action: answer.value,
227
+ value: answer.value,
228
+ kind: answer.kind ?? entry.kind,
229
+ confidence: answer.confidence,
230
+ probabilities: answer.probabilities ?? null,
231
+ calibration: answer.calibration ?? 'unknown',
232
+ checkpoint: result?.checkpoint ?? null,
233
+ batchId: result?.batchId ?? null,
234
+ requestFingerprint: result?.requestFingerprint ?? null,
235
+ latencyClass: result?.latencyClass ?? null,
236
+ circuitState: result?.circuitState ?? null,
237
+ finishReason: result?.finishReason ?? null,
238
+ };
239
+ }
240
+
241
+ return { decide };
242
+ }
@@ -0,0 +1,150 @@
1
+ // src/decision/runtimeFilter.js — pure (no I/O, no config, no gateway).
2
+ // C65 G3-FR01 — deterministic pre-filter for G1/G2 SemanticEvents.
3
+ // Known signals are decided locally and must NEVER produce a model call
4
+ // downstream; only genuinely ambiguous items return action 'model_decision'
5
+ // with a registered `runtime.*.v1` decisionKey.
6
+ //
7
+ // Interface (SPEC §4, frozen):
8
+ // classifyRuntimeEvent(event, policy?) -> {
9
+ // action: 'local_transition'|'local_observation'|'local_action'
10
+ // |'model_decision'|'escalate'|'ignore',
11
+ // priority: 'critical'|'decision'|'progress',
12
+ // coalesceKey?: string, // only for coalescible progress
13
+ // decisionKey?: string, // only when action='model_decision'
14
+ // reason: string }
15
+ //
16
+ // Consumes: SemanticEvent { eventType, safePayload, operationId, seq, ... }
17
+ // from src/core/agentRuntime/contract.js; liveness class vocabulary from
18
+ // src/core/agentRuntime/liveness.js.
19
+ // Consumed by: runtimeScheduler.submit (TASK-002), runtimeDecide (TASK-003).
20
+
21
+ import { TERMINAL_STATES } from '../core/agentRuntime/contract.js';
22
+ import { LIVENESS_CLASSES } from '../core/agentRuntime/liveness.js';
23
+
24
+ const TERMINAL_SET = new Set(TERMINAL_STATES);
25
+ const LIVENESS_SET = new Set(LIVENESS_CLASSES);
26
+
27
+ // Observation classes that represent redundant progress — safe to coalesce
28
+ // by key, latest wins (SPEC §5).
29
+ const COALESCIBLE_CLASSES = new Set(['live_progressing', 'silent_live']);
30
+
31
+ // Observation classes where a stall decision may be needed: with an explicit
32
+ // spec/policy stallAction the action is deterministic; without one the item
33
+ // is ambiguous and goes to the model via runtime.stall_action.v1.
34
+ const STALL_CLASSES = new Set(['stalled', 'timeout']);
35
+
36
+ // Ambiguous items that need a semantic decision: which decision key each
37
+ // maps to. Keys must be registered in the decision registry (TASK-003).
38
+ const DECISION_KEYS = Object.freeze({
39
+ stall: 'runtime.stall_action.v1',
40
+ ambiguous: 'runtime.wake_needed.v1',
41
+ });
42
+
43
+ // Signals that a human/authorization boundary must resolve — never dropped,
44
+ // never coalesced, never auto-acted-on.
45
+ const ESCALATION_EVENT_TYPE = /permission|input[_-]?required|authoriz/i;
46
+ const ESCALATION_CLASS = /permission|input[_-]?required|authoriz|approval/i;
47
+
48
+ function result(action, priority, reason, extra) {
49
+ return { action, priority, reason, ...extra };
50
+ }
51
+
52
+ const ignore = (reason) => result('ignore', 'progress', reason);
53
+
54
+ /**
55
+ * Classify one runtime SemanticEvent into a local action, a model-decision
56
+ * candidate, an escalation, or ignore. Pure: no I/O, no config import.
57
+ *
58
+ * @param {object} event G1 SemanticEvent envelope
59
+ * @param {object} [policy] { stallAction?: string } — explicit spec policy
60
+ * @returns {{action:string,priority:string,coalesceKey?:string,
61
+ * decisionKey?:string,reason:string}}
62
+ */
63
+ export function classifyRuntimeEvent(event, policy) {
64
+ if (event === null || typeof event !== 'object' || Array.isArray(event)) {
65
+ return ignore('malformed_event');
66
+ }
67
+ const eventType = event.eventType;
68
+ if (typeof eventType !== 'string' || eventType.length === 0) {
69
+ return ignore('malformed_event');
70
+ }
71
+
72
+ // Permission / user-input / authorization signals escalate deterministically,
73
+ // regardless of the channel they arrive on.
74
+ if (ESCALATION_EVENT_TYPE.test(eventType)) {
75
+ return result('escalate', 'critical', `escalation_signal:${eventType}`);
76
+ }
77
+
78
+ const safePayload = event.safePayload;
79
+ const operationId =
80
+ typeof event.operationId === 'string' && event.operationId.length > 0
81
+ ? event.operationId
82
+ : 'unknown';
83
+
84
+ switch (eventType) {
85
+ case 'operation.transition': {
86
+ if (typeof safePayload !== 'object' || safePayload === null) {
87
+ return result('model_decision', 'decision', 'unparseable_payload', {
88
+ decisionKey: DECISION_KEYS.ambiguous,
89
+ });
90
+ }
91
+ const to = safePayload.to;
92
+ if (TERMINAL_SET.has(to)) {
93
+ return result('local_transition', 'critical', `terminal_transition:${to}`);
94
+ }
95
+ // Non-terminal transition (queued→running, recovery_required, …):
96
+ // known signal, decided locally; never reaches the model.
97
+ return result('local_transition', 'progress', `transition:${to ?? 'unknown'}`);
98
+ }
99
+
100
+ case 'operation.observation': {
101
+ if (typeof safePayload !== 'object' || safePayload === null) {
102
+ return result('model_decision', 'decision', 'unparseable_payload', {
103
+ decisionKey: DECISION_KEYS.ambiguous,
104
+ });
105
+ }
106
+ const cls = safePayload.class;
107
+
108
+ if (typeof cls === 'string' && ESCALATION_CLASS.test(cls)) {
109
+ return result('escalate', 'critical', `escalation_signal:${cls}`);
110
+ }
111
+
112
+ if (COALESCIBLE_CLASSES.has(cls)) {
113
+ return result('local_observation', 'progress', `observation:${cls}`, {
114
+ coalesceKey: `obs:${operationId}:${cls}`,
115
+ });
116
+ }
117
+
118
+ if (STALL_CLASSES.has(cls)) {
119
+ const stallAction =
120
+ policy && typeof policy === 'object' && typeof policy.stallAction === 'string'
121
+ ? policy.stallAction
122
+ : null;
123
+ if (stallAction) {
124
+ return result('local_action', 'critical', `policy_stall_action:${stallAction}:${cls}`);
125
+ }
126
+ return result('model_decision', 'decision', `ambiguous_stall:${cls}`, {
127
+ decisionKey: DECISION_KEYS.stall,
128
+ });
129
+ }
130
+
131
+ if (LIVENESS_SET.has(cls)) {
132
+ // 'exited' — the only remaining liveness class. Terminal-adjacent
133
+ // sample signal: critical so it is never dropped/starved by progress.
134
+ return result('local_observation', 'critical', `observation:${cls}`);
135
+ }
136
+
137
+ // Unrecognized observation class — ambiguous.
138
+ return result('model_decision', 'decision', `unknown_observation_class:${cls ?? 'none'}`, {
139
+ decisionKey: DECISION_KEYS.ambiguous,
140
+ });
141
+ }
142
+
143
+ default:
144
+ // Valid envelope shape but a vocabulary the filter does not own —
145
+ // ambiguous, candidate for a semantic decision.
146
+ return result('model_decision', 'decision', `unknown_event_type:${eventType}`, {
147
+ decisionKey: DECISION_KEYS.ambiguous,
148
+ });
149
+ }
150
+ }
@@ -0,0 +1,239 @@
1
+ /**
2
+ * runtimeScheduler.js (G3-FR02)
3
+ *
4
+ * Bounded 3-tier queue for classified runtime items (SPEC §4):
5
+ * - critical : terminal/failure/cancel/permission — emitted directly via
6
+ * emitFn at flush; never dropped, never coalesced, never
7
+ * counted against maxQueue, always drained first.
8
+ * - decision : ambiguous items awaiting decideFn.
9
+ * - progress : redundant progress — coalesced by coalesceKey while an
10
+ * entry is pending (latest wins); eligible for drain after
11
+ * coalesceWindowMs from first arrival.
12
+ *
13
+ * Serial drain (maxInFlight = 1 — the gateway is a single local model).
14
+ * decideFn failures convert to typed Fallback objects emitted via emitFn;
15
+ * a decideFn that never resolves yields { code:'timeout' }.
16
+ *
17
+ * Pure module: no I/O, no config. All dependencies injected via opts.
18
+ */
19
+
20
+ import { resolveConfigStage } from '../core/runtimeConfig.js';
21
+ import { selectResourcePolicy } from '../core/agentRuntime/resourcePolicy.js';
22
+
23
+ // G6 (DR-09): `decisionRuntime.resourcePolicy.stage` — absent/malformed →
24
+ // 'off'; 'shadow' consults selectResourcePolicy on submit and records a
25
+ // bounded receipt WITHOUT changing enqueue behavior. A policy throw or
26
+ // malformed decision deterministically falls back to the prior path.
27
+ const RESOURCE_FLAG = 'decisionRuntime.resourcePolicy.stage';
28
+ const RESOURCE_RECEIPT_CAP = 128;
29
+
30
+ // priority → eventClass for the resource consult. Scheduler-internal,
31
+ // fail-open: unknown priorities consult as 'progress' (the only coalescible
32
+ // class); malformed input still resolves emit downstream.
33
+ const EVENT_CLASS_BY_PRIORITY = Object.freeze({
34
+ critical: 'terminal',
35
+ progress: 'progress',
36
+ decision: 'progress',
37
+ });
38
+
39
+ const DEFAULT_COALESCE_WINDOW_MS = 250;
40
+ const DEFAULT_MAX_QUEUE = 256;
41
+ const DEFAULT_DECISION_TIMEOUT_MS = 5000;
42
+
43
+ function fallback(code, item) {
44
+ return { decidedBy: 'fallback', code, action: item?.action ?? 'ignore' };
45
+ }
46
+
47
+ export function createScheduler(opts = {}) {
48
+ const decideFn = opts.decideFn ?? (async () => fallback('unavailable'));
49
+ const emitFn = opts.emitFn ?? (() => {});
50
+ const now = opts.now ?? (() => Date.now());
51
+ const coalesceWindowMs = opts.coalesceWindowMs ?? DEFAULT_COALESCE_WINDOW_MS;
52
+ const maxQueue = opts.maxQueue ?? DEFAULT_MAX_QUEUE;
53
+ const decisionTimeoutMs = opts.decisionTimeoutMs ?? DEFAULT_DECISION_TIMEOUT_MS;
54
+ // maxInFlight accepted for forward-compat; drain is serial regardless.
55
+ void (opts.maxInFlight ?? 1);
56
+
57
+ const resourceStage = () => resolveConfigStage(opts.config ?? null, RESOURCE_FLAG);
58
+ const resourcePolicyFn = typeof opts.resourcePolicyFn === 'function'
59
+ ? opts.resourcePolicyFn
60
+ : selectResourcePolicy;
61
+ const resourcesFn = typeof opts.resourcesFn === 'function'
62
+ ? opts.resourcesFn
63
+ : () => ({});
64
+ const resourceReceipts = [];
65
+
66
+ function recordResourceReceipt(entry) {
67
+ if (resourceReceipts.length >= RESOURCE_RECEIPT_CAP) resourceReceipts.shift();
68
+ resourceReceipts.push(Object.freeze(entry));
69
+ }
70
+
71
+ /**
72
+ * Shadow consult: evaluate the resource policy for this submit and record
73
+ * a bounded receipt. Never changes enqueue behavior; never throws.
74
+ */
75
+ function consultResourcePolicy(item) {
76
+ let decision;
77
+ let error;
78
+ try {
79
+ const resources = resourcesFn() ?? {};
80
+ decision = resourcePolicyFn({
81
+ cpuPressure: typeof resources.cpuPressure === 'string' ? resources.cpuPressure : 'normal',
82
+ eventClass: EVENT_CLASS_BY_PRIORITY[item?.priority] ?? 'progress',
83
+ queueDepth: boundedSize(),
84
+ });
85
+ } catch (err) {
86
+ error = String(err?.message ?? err);
87
+ }
88
+ const valid = decision !== null && typeof decision === 'object'
89
+ && (decision.action === 'emit' || decision.action === 'coalesce' || decision.action === 'defer');
90
+ recordResourceReceipt({
91
+ priority: item?.priority ?? null,
92
+ stage: resourceStage(),
93
+ decision: valid ? { action: decision.action, reason: decision.reason } : null,
94
+ error: error ?? null,
95
+ });
96
+ }
97
+
98
+ const critical = [];
99
+ const decision = [];
100
+ const progressPending = new Map(); // coalesceKey -> { item, firstArrival }
101
+ const timers = new Set();
102
+ let closed = false;
103
+ let closedWith = 0;
104
+ const counters = { submitted: 0, coalesced: 0, dropped: 0, deferred: 0, emitted: 0 };
105
+
106
+ function boundedSize() {
107
+ return decision.length + progressPending.size;
108
+ }
109
+
110
+ function submit(item) {
111
+ if (closed || !item || typeof item !== 'object') {
112
+ return { accepted: false, code: closed ? 'closed' : 'invalid' };
113
+ }
114
+ // G6 resource consult — shadow evaluates+records a bounded receipt;
115
+ // enqueue below is always the prior path (throw/malformed → prior
116
+ // behavior deterministically).
117
+ if (resourceStage() !== 'off') {
118
+ consultResourcePolicy(item);
119
+ }
120
+ if (item.priority === 'critical') {
121
+ critical.push(item);
122
+ counters.submitted++;
123
+ return { accepted: true };
124
+ }
125
+ if (item.priority === 'progress' && item.coalesceKey) {
126
+ const existing = progressPending.get(item.coalesceKey);
127
+ if (existing) {
128
+ existing.item = item; // latest wins
129
+ counters.coalesced++;
130
+ counters.submitted++;
131
+ return { accepted: true };
132
+ }
133
+ if (boundedSize() >= maxQueue) {
134
+ counters.dropped++;
135
+ return { accepted: false, code: 'queue_full' };
136
+ }
137
+ progressPending.set(item.coalesceKey, { item, firstArrival: now() });
138
+ counters.submitted++;
139
+ return { accepted: true };
140
+ }
141
+ // decision tier (or progress without a key — cannot coalesce)
142
+ if (boundedSize() >= maxQueue) {
143
+ counters.dropped++;
144
+ return { accepted: false, code: 'queue_full' };
145
+ }
146
+ decision.push(item);
147
+ counters.submitted++;
148
+ return { accepted: true };
149
+ }
150
+
151
+ function scheduleTimeout(ms) {
152
+ let handle;
153
+ const p = new Promise((resolve) => {
154
+ handle = setTimeout(() => {
155
+ timers.delete(handle);
156
+ resolve('timeout');
157
+ }, ms);
158
+ if (typeof handle.unref === 'function') handle.unref();
159
+ });
160
+ timers.add(handle);
161
+ return { promise: p, cancel: () => { clearTimeout(handle); timers.delete(handle); } };
162
+ }
163
+
164
+ async function decide(item) {
165
+ const t = scheduleTimeout(decisionTimeoutMs);
166
+ try {
167
+ const result = await Promise.race([Promise.resolve().then(() => decideFn(item)), t.promise]);
168
+ t.cancel();
169
+ if (result === 'timeout') return fallback('timeout', item);
170
+ if (result && result.decidedBy === 'fallback') return result;
171
+ if (result === undefined || result === null) return fallback('invalid', item);
172
+ return result;
173
+ } catch {
174
+ t.cancel();
175
+ return fallback('invalid', item);
176
+ }
177
+ }
178
+
179
+ async function flush() {
180
+ const report = { processed: 0, emitted: 0, coalesced: 0, dropped: 0, deferred: 0 };
181
+ if (closed) return report;
182
+
183
+ while (critical.length) {
184
+ const item = critical.shift();
185
+ emitFn(item);
186
+ counters.emitted++;
187
+ report.processed++;
188
+ report.emitted++;
189
+ }
190
+ while (decision.length) {
191
+ const item = decision.shift();
192
+ emitFn(await decide(item));
193
+ counters.emitted++;
194
+ report.processed++;
195
+ report.emitted++;
196
+ }
197
+ const t = now();
198
+ for (const [key, entry] of progressPending) {
199
+ if (t - entry.firstArrival < coalesceWindowMs) {
200
+ counters.deferred++;
201
+ report.deferred++;
202
+ continue;
203
+ }
204
+ progressPending.delete(key);
205
+ emitFn(await decide(entry.item));
206
+ counters.emitted++;
207
+ report.processed++;
208
+ report.emitted++;
209
+ }
210
+ return report;
211
+ }
212
+
213
+ function stats() {
214
+ return {
215
+ ...counters,
216
+ pending: critical.length + decision.length + progressPending.size,
217
+ closedWith,
218
+ closed,
219
+ };
220
+ }
221
+
222
+ async function close() {
223
+ if (closed) return;
224
+ closed = true;
225
+ closedWith = critical.length + decision.length + progressPending.size;
226
+ critical.length = 0;
227
+ decision.length = 0;
228
+ progressPending.clear();
229
+ for (const h of timers) clearTimeout(h);
230
+ timers.clear();
231
+ }
232
+
233
+ /** Bounded frozen receipts of shadow-stage resource-policy consults (G6). */
234
+ function resourcePolicyReceipts() {
235
+ return [...resourceReceipts];
236
+ }
237
+
238
+ return { submit, flush, stats, close, resourcePolicyReceipts };
239
+ }
@@ -255,9 +255,9 @@ export async function buildCodeIndex({ rootDir = process.cwd(), changedFiles = n
255
255
  && Array.isArray(previousCallsArtifact.items);
256
256
 
257
257
  if (canReuseParsedArtifacts) {
258
- const previousSymbolsByPath = groupBy(previousSymbolsArtifact.items ?? [], (item) => item.filePath);
259
- const previousImportsByPath = groupBy(previousImportsArtifact.items ?? [], (item) => item.from);
260
- const previousCallsByPath = groupBy(previousCallsArtifact.items ?? [], (item) => item.filePath);
258
+ const previousSymbolsByPath = groupBy(previousSymbolsArtifact.items ?? [], (item) => item?.filePath);
259
+ const previousImportsByPath = groupBy(previousImportsArtifact.items ?? [], (item) => item?.from);
260
+ const previousCallsByPath = groupBy(previousCallsArtifact.items ?? [], (item) => item?.filePath);
261
261
 
262
262
  for (const filePath of reusableCodeFiles) {
263
263
  symbols.push(...(previousSymbolsByPath.get(filePath) ?? []));
@@ -891,9 +891,9 @@ async function tryIncrementalIndexUpdate({ absoluteRoot, indexDir, changedPaths,
891
891
  }
892
892
  const styleFilePaths = styleEntries.filter(Boolean).map((entry) => normalizeRelative(absoluteRoot, entry.absolutePath));
893
893
 
894
- const previousSymbolsByPath = groupBy(symbolsArtifact.items ?? [], (item) => item.filePath);
895
- const previousImportsByPath = groupBy(importsArtifact.items ?? [], (item) => item.from);
896
- const previousCallsByPath = groupBy(callsArtifact.items ?? [], (item) => item.filePath);
894
+ const previousSymbolsByPath = groupBy(symbolsArtifact.items ?? [], (item) => item?.filePath);
895
+ const previousImportsByPath = groupBy(importsArtifact.items ?? [], (item) => item?.from);
896
+ const previousCallsByPath = groupBy(callsArtifact.items ?? [], (item) => item?.filePath);
897
897
 
898
898
  const parsedSet = new Set(parsedFiles);
899
899
  const codeFiles = mergedFileRecords.filter((record) => CODE_EXTENSIONS.has(record.ext));
@@ -2385,8 +2385,8 @@ const ARCHETYPE_RULES = [
2385
2385
  { pattern: /(?:^|\/)(?:models?|schemas?)\//, archetype: 'schema' },
2386
2386
  ];
2387
2387
 
2388
- function classifyArchetypes(fileRecords, symbols) {
2389
- const symbolNamesByPath = groupBy(symbols, (s) => s.filePath);
2388
+ export function classifyArchetypes(fileRecords, symbols) {
2389
+ const symbolNamesByPath = groupBy(symbols, (s) => s?.filePath);
2390
2390
 
2391
2391
  return fileRecords
2392
2392
  .filter((f) => CODE_EXTENSIONS.has(f.ext))
@@ -2409,12 +2409,13 @@ function classifyArchetypes(fileRecords, symbols) {
2409
2409
 
2410
2410
  if (!archetype) {
2411
2411
  const fileSymbols = symbolNamesByPath.get(file.filePath) ?? [];
2412
- const hasDefaultExport = fileSymbols.some((s) => s.name === 'default');
2412
+ const hasDefaultExport = fileSymbols.some((s) => s?.name === 'default');
2413
2413
  const hasComponentSignal = file.ext === '.vue'
2414
2414
  || fileSymbols.some((s) =>
2415
- s.type === 'component-name'
2416
- || s.name.toLowerCase().includes('component')
2417
- || s.name.toLowerCase().includes('page'),
2415
+ s?.type === 'component-name'
2416
+ || (typeof s?.name === 'string'
2417
+ && (s.name.toLowerCase().includes('component')
2418
+ || s.name.toLowerCase().includes('page'))),
2418
2419
  );
2419
2420
 
2420
2421
  if ((hasDefaultExport || file.ext === '.vue') && hasComponentSignal) {