@ngockhoale/ukit 3.0.8 → 3.0.10

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 +18 -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,254 @@
1
+ /**
2
+ * agentRuntime/adapters.js — decision-first-runtime G5 (DR-07), semantic
3
+ * output adapters (SPEC §2 G5-FR01/02/03, §4).
4
+ *
5
+ * `adaptOutput` converts raw tool output (async-iterable stream or chunk
6
+ * array) into contract-valid `SemanticEvent[]` (`eventType:
7
+ * 'tool.output.summary'`) whose `safePayload` stays under
8
+ * `MAX_SAFE_PAYLOAD_BYTES`. Raw output never enters an event payload:
9
+ * the full bytes are reachable only through `artifactStore` refs.
10
+ *
11
+ * Profiles (`TOOL_PROFILES`):
12
+ * - `generic` — exit code + byte count + bounded tail digest (any tool).
13
+ * - `vitest` — test files/tests passed/failed counts, bounded failed
14
+ * test names, duration.
15
+ *
16
+ * No fabricated PASS: when pass/fail cannot be established the summary
17
+ * emits `status: 'indeterminate'` — never `passed`. A vitest profile that
18
+ * cannot parse its input degrades to the generic event with a
19
+ * `parseFallback` marker (`parseStatus: 'parse_fallback'`).
20
+ *
21
+ * Secret-shaped lines (`SECRET=...`, `*_KEY=...`, `*_TOKEN=...`,
22
+ * `PASSWORD=...`, `AUTHORIZATION: ...`) are redacted before any payload
23
+ * or digest is built.
24
+ *
25
+ * Pure module: no imports of context/completionGate/qualityComparison.
26
+ * `now` and `artifactStore` are injectable for tests.
27
+ *
28
+ * `artifactStore` protocol (injected): `put(buffer)` →
29
+ * `Promise<string | { path: string }>`; the returned ref is recorded on
30
+ * `event.artifactRefs` and in the result `artifactRefs` list.
31
+ */
32
+
33
+ import crypto from 'node:crypto';
34
+
35
+ import {
36
+ validateSemanticEvent,
37
+ CONTRACT_VERSION,
38
+ MAX_SAFE_PAYLOAD_BYTES,
39
+ } from './contract.js';
40
+
41
+ export const TOOL_PROFILES = Object.freeze(['generic', 'vitest']);
42
+
43
+ const PRODUCER_VERSION = '3.0.8';
44
+ const PRIVACY_CLASS = 'internal';
45
+ const EVENT_TYPE = 'tool.output.summary';
46
+
47
+ /** Bounded tail digest: last 1 KiB of redacted text, line-sliced. */
48
+ const TAIL_DIGEST_BYTES = 1024;
49
+ /** Cap on failed test names surfaced in safePayload. */
50
+ const MAX_FAILED_NAMES = 20;
51
+ const MAX_FAILED_NAME_LEN = 200;
52
+
53
+ const SECRET_LINE_RE =
54
+ /^(?:.*\b)?(?:SECRET|PASSWORD|TOKEN|API_KEY|PRIVATE_KEY|ACCESS_KEY|AUTH(?:ORIZATION)?|CREDENTIAL)[\w.-]*\s*[:=][^\n]*$/i;
55
+ const SECRET_INLINE_RE =
56
+ /\b(?:SECRET|PASSWORD|TOKEN|API_KEY|PRIVATE_KEY|ACCESS_KEY|AUTHORIZATION|CREDENTIAL)[\w.-]*\s*[:=]\s*\S+/gi;
57
+
58
+ function redactSecrets(text) {
59
+ return text
60
+ .split('\n')
61
+ .map((line) =>
62
+ SECRET_LINE_RE.test(line.trim()) ? '[REDACTED]' : line,
63
+ )
64
+ .join('\n')
65
+ .replace(SECRET_INLINE_RE, '[REDACTED]');
66
+ }
67
+
68
+ async function collectInput({ stream, chunks }) {
69
+ const parts = [];
70
+ const source = chunks ?? stream;
71
+ if (source == null) return Buffer.alloc(0);
72
+ if (Array.isArray(source)) {
73
+ for (const c of source) {
74
+ parts.push(Buffer.isBuffer(c) ? c : Buffer.from(String(c)));
75
+ }
76
+ return Buffer.concat(parts);
77
+ }
78
+ for await (const c of source) {
79
+ parts.push(Buffer.isBuffer(c) ? c : Buffer.from(String(c)));
80
+ }
81
+ return Buffer.concat(parts);
82
+ }
83
+
84
+ function makeEvent(operationId, seq, safePayload, artifactRefs, now) {
85
+ return {
86
+ eventId: `${operationId}-output-${seq}`,
87
+ operationId,
88
+ seq,
89
+ eventType: EVENT_TYPE,
90
+ observedAt: now(),
91
+ producerVersion: PRODUCER_VERSION,
92
+ contractVersion: CONTRACT_VERSION,
93
+ privacyClass: PRIVACY_CLASS,
94
+ artifactRefs,
95
+ safePayload,
96
+ };
97
+ }
98
+
99
+ function tailDigest(text) {
100
+ const buf = Buffer.from(text, 'utf8');
101
+ const tail = buf.length > TAIL_DIGEST_BYTES ? buf.subarray(buf.length - TAIL_DIGEST_BYTES) : buf;
102
+ const str = tail.toString('utf8').replace(/\n{3,}/g, '\n\n').trim();
103
+ // Drop a partial first line.
104
+ const idx = str.indexOf('\n');
105
+ return idx >= 0 ? str.slice(idx + 1) : str;
106
+ }
107
+
108
+ function genericStatus(exitCode) {
109
+ if (!Number.isInteger(exitCode)) return 'indeterminate';
110
+ return exitCode === 0 ? 'passed' : 'failed';
111
+ }
112
+
113
+ function genericPayload({ raw, redacted, exitCode, parseFallback, profile }) {
114
+ return {
115
+ profile,
116
+ status: genericStatus(exitCode),
117
+ exitCode: Number.isInteger(exitCode) ? exitCode : null,
118
+ byteCount: raw.length,
119
+ sha256: crypto.createHash('sha256').update(raw).digest('hex'),
120
+ tailDigest: tailDigest(redacted),
121
+ ...(parseFallback ? { parseFallback: true } : {}),
122
+ };
123
+ }
124
+
125
+ const RE_FILES =
126
+ /Test Files\s+(?:(\d+)\s+failed\s*\|)?\s*(\d+)\s+passed(?:\s*\(\d+\))?/;
127
+ const RE_TESTS =
128
+ /Tests\s+(?:(\d+)\s+failed\s*\|)?\s*(\d+)\s+passed(?:\s*\(\d+\))?/;
129
+ const RE_DURATION = /Duration\s+([\d.]+)\s*(ms|s)/;
130
+ const RE_FAIL_NAME = /^\s*FAIL\s+(.+?)\s*$/gm;
131
+
132
+ function parseVitest(redacted) {
133
+ const files = RE_FILES.exec(redacted);
134
+ const tests = RE_TESTS.exec(redacted);
135
+ if (!files || !tests) return null;
136
+
137
+ const filesFailed = files[1] ? Number(files[1]) : 0;
138
+ const filesPassed = Number(files[2]);
139
+ const testsFailed = tests[1] ? Number(tests[1]) : 0;
140
+ const testsPassed = Number(tests[2]);
141
+
142
+ const dur = RE_DURATION.exec(redacted);
143
+ const durationMs = dur
144
+ ? Math.round(Number(dur[1]) * (dur[2] === 's' ? 1000 : 1))
145
+ : null;
146
+
147
+ const failedNames = [];
148
+ for (const m of redacted.matchAll(RE_FAIL_NAME)) {
149
+ if (failedNames.length >= MAX_FAILED_NAMES) break;
150
+ const name = m[1].trim().slice(0, MAX_FAILED_NAME_LEN);
151
+ if (name) failedNames.push(name);
152
+ }
153
+
154
+ return {
155
+ profile: 'vitest',
156
+ status: testsFailed > 0 || filesFailed > 0 ? 'failed' : 'passed',
157
+ filesPassed,
158
+ filesFailed,
159
+ testsPassed,
160
+ testsFailed,
161
+ failedNames,
162
+ durationMs,
163
+ };
164
+ }
165
+
166
+ async function storeArtifact(artifactStore, buf) {
167
+ if (!artifactStore || typeof artifactStore.put !== 'function') return [];
168
+ try {
169
+ const ref = await artifactStore.put(buf);
170
+ const out = typeof ref === 'string' ? ref : ref?.path;
171
+ return out ? [out] : [];
172
+ } catch {
173
+ // Artifact persistence failure must not fabricate or drop the verdict —
174
+ // emit the bounded event without a ref.
175
+ return [];
176
+ }
177
+ }
178
+
179
+ /**
180
+ * @param {object} args
181
+ * @param {AsyncIterable<Buffer|string>} [args.stream] async-iterable output.
182
+ * @param {Array<Buffer|string>} [args.chunks] buffered output chunks.
183
+ * @param {string} [args.toolProfile] 'generic'|'vitest' (unknown → generic).
184
+ * @param {string} args.operationId
185
+ * @param {number} [args.exitCode] child exit code when known.
186
+ * @param {() => string} [args.now] injectable clock (ISO string).
187
+ * @param {{put: (buf: Buffer) => Promise<string|{path:string}>}} [args.artifactStore]
188
+ * @returns {Promise<{events: object[], artifactRefs: string[],
189
+ * parseStatus: 'parsed'|'generic'|'parse_fallback'}>}
190
+ */
191
+ export async function adaptOutput({
192
+ stream,
193
+ chunks,
194
+ toolProfile = 'generic',
195
+ operationId,
196
+ exitCode,
197
+ now = () => new Date().toISOString(),
198
+ artifactStore,
199
+ } = {}) {
200
+ const raw = await collectInput({ stream, chunks });
201
+ const redacted = redactSecrets(raw.toString('utf8'));
202
+ const artifactRefs = await storeArtifact(artifactStore, raw);
203
+
204
+ const profile = TOOL_PROFILES.includes(toolProfile) ? toolProfile : 'generic';
205
+
206
+ let safePayload;
207
+ let parseStatus;
208
+ if (profile === 'vitest') {
209
+ const parsed = parseVitest(redacted);
210
+ if (parsed) {
211
+ safePayload = parsed;
212
+ parseStatus = 'parsed';
213
+ } else {
214
+ safePayload = genericPayload({
215
+ raw,
216
+ redacted,
217
+ exitCode,
218
+ parseFallback: true,
219
+ profile: 'generic',
220
+ });
221
+ parseStatus = 'parse_fallback';
222
+ }
223
+ } else {
224
+ safePayload = genericPayload({
225
+ raw,
226
+ redacted,
227
+ exitCode,
228
+ parseFallback: false,
229
+ profile: 'generic',
230
+ });
231
+ parseStatus = 'generic';
232
+ }
233
+
234
+ const event = makeEvent(
235
+ String(operationId ?? 'unknown'),
236
+ 0,
237
+ safePayload,
238
+ artifactRefs,
239
+ now,
240
+ );
241
+
242
+ // Defensive: never emit an unvalidated envelope.
243
+ const v = validateSemanticEvent(event);
244
+ if (!v.ok) {
245
+ event.safePayload = {
246
+ profile: 'generic',
247
+ status: 'indeterminate',
248
+ byteCount: raw.length,
249
+ shrinkReason: v.code,
250
+ };
251
+ }
252
+
253
+ return { events: [event], artifactRefs, parseStatus };
254
+ }
@@ -0,0 +1,192 @@
1
+ /**
2
+ * agentRuntime/artifacts.js — decision-first-runtime G2 bounded output
3
+ * artifact writer (SPEC §2 G2-FR02/G2-FR09, §4).
4
+ *
5
+ * Streams child stdout/stderr into bounded artifact files under `dir`:
6
+ * per-stream and total byte caps (over-cap input is dropped, the file is
7
+ * marked `truncated:true`), SHA-256 over the bytes actually written, and
8
+ * typed `ArtifactError` on write failure (ENOSPC/EIO) — never an unhandled
9
+ * rejection. Pure I/O module: no policy, no transitions.
10
+ *
11
+ * Interface (frozen this cycle):
12
+ * createArtifactWriter(dir, limits, deps?) -> writer
13
+ * writer.open(operationId) -> { stdout, stderr, close() }
14
+ * close() -> Promise<ArtifactRef[]>
15
+ * ref: { path, sha256, bytes, truncated, stream, error? }
16
+ *
17
+ * `deps` exists for fault injection in tests: { openImpl, writeImpl,
18
+ * fsyncImpl, closeImpl, mkdirImpl } default to fs.promises equivalents.
19
+ */
20
+
21
+ import { promises as fs } from 'node:fs';
22
+ import { Writable } from 'node:stream';
23
+ import crypto from 'node:crypto';
24
+ import path from 'node:path';
25
+
26
+ export class ArtifactError extends Error {
27
+ constructor(code, message, cause) {
28
+ super(message);
29
+ this.name = 'ArtifactError';
30
+ this.code = code;
31
+ if (cause !== undefined) this.cause = cause;
32
+ }
33
+ }
34
+
35
+ const DEFAULT_LIMITS = Object.freeze({
36
+ maxOutputBytes: 4 * 1024 * 1024,
37
+ maxStderrBytes: 4 * 1024 * 1024,
38
+ maxTotalBytes: Infinity,
39
+ });
40
+
41
+ const STREAMS = ['stdout', 'stderr'];
42
+ const PER_STREAM_LIMIT = { stdout: 'maxOutputBytes', stderr: 'maxStderrBytes' };
43
+
44
+ function toArtifactError(err) {
45
+ if (err instanceof ArtifactError) return err;
46
+ const code = err && err.code === 'ENOSPC' ? 'enospc' : 'artifact_write_failed';
47
+ return new ArtifactError(
48
+ code,
49
+ `artifact write failed: ${err?.message ?? err}`,
50
+ err,
51
+ );
52
+ }
53
+
54
+ export function createArtifactWriter(dir, limits = {}, deps = {}) {
55
+ const lim = { ...DEFAULT_LIMITS, ...limits };
56
+ const d = {
57
+ mkdirImpl: deps.mkdirImpl ?? ((p) => fs.mkdir(p, { recursive: true })),
58
+ openImpl: deps.openImpl ?? ((p) => fs.open(p, 'w')),
59
+ writeImpl: deps.writeImpl ?? ((h, b) => h.write(b)),
60
+ fsyncImpl: deps.fsyncImpl ?? ((h) => h.fsync()),
61
+ closeImpl: deps.closeImpl ?? ((h) => h.close()),
62
+ };
63
+
64
+ function open(operationId) {
65
+
66
+ // Shared total-bytes budget across both streams.
67
+ let totalBytes = 0;
68
+
69
+ const streams = {};
70
+ for (const stream of STREAMS) {
71
+ const filePath = path.join(dir, `${operationId}.${stream}.log`);
72
+ const cap = lim[PER_STREAM_LIMIT[stream]];
73
+ const state = {
74
+ stream,
75
+ path: filePath,
76
+ handle: null,
77
+ handleReady: null,
78
+ hash: crypto.createHash('sha256'),
79
+ bytes: 0,
80
+ truncated: false,
81
+ error: null,
82
+ finished: null,
83
+ };
84
+
85
+ state.handleReady = (async () => {
86
+ try {
87
+ await d.mkdirImpl(dir);
88
+ state.handle = await d.openImpl(filePath);
89
+ } catch (err) {
90
+ throw toArtifactError(err);
91
+ }
92
+ })();
93
+ // Mark handled so a failed open never surfaces as an unhandled
94
+ // rejection before write()/close() await it; awaiters still reject.
95
+ state.handleReady.catch(() => {});
96
+
97
+ const writable = new Writable({
98
+ async write(chunk, _enc, cb) {
99
+ try {
100
+ await state.handleReady;
101
+ const perStreamRoom = Math.max(0, cap - state.bytes);
102
+ const totalRoom = Math.max(0, lim.maxTotalBytes - totalBytes);
103
+ const room = Math.min(perStreamRoom, totalRoom);
104
+ if (room <= 0) {
105
+ state.truncated = true;
106
+ cb();
107
+ return;
108
+ }
109
+ const slice =
110
+ chunk.length > room ? chunk.subarray(0, room) : chunk;
111
+ if (chunk.length > room) state.truncated = true;
112
+ try {
113
+ await d.writeImpl(state.handle, slice);
114
+ } catch (err) {
115
+ throw toArtifactError(err);
116
+ }
117
+ state.hash.update(slice);
118
+ state.bytes += slice.length;
119
+ totalBytes += slice.length;
120
+ cb();
121
+ } catch (err) {
122
+ cb(err instanceof ArtifactError ? err : toArtifactError(err));
123
+ }
124
+ },
125
+ });
126
+
127
+ // Keep a record of the first stream error without leaving an unhandled
128
+ // 'error' event (Writable emits asynchronously). Consumers attaching
129
+ // their own listener (pipeline) still receive it.
130
+ state.finished = new Promise((resolve) => {
131
+ writable.on('error', (err) => {
132
+ if (!state.error) {
133
+ state.error =
134
+ err instanceof ArtifactError ? err : toArtifactError(err);
135
+ }
136
+ resolve();
137
+ });
138
+ writable.on('finish', () => resolve());
139
+ });
140
+
141
+ streams[stream] = { writable, state };
142
+ }
143
+
144
+ async function close() {
145
+ const refs = [];
146
+ for (const stream of STREAMS) {
147
+ const { writable, state } = streams[stream];
148
+ if (!writable.destroyed && !writable.writableEnded) {
149
+ writable.end();
150
+ }
151
+ await state.finished;
152
+ // Whatever was written must be durable before refs are returned —
153
+ // fsync + close even when a write error was recorded (partial file).
154
+ try {
155
+ await state.handleReady;
156
+ if (state.handle) {
157
+ try {
158
+ await d.fsyncImpl(state.handle);
159
+ } catch (err) {
160
+ if (!state.error) state.error = toArtifactError(err);
161
+ }
162
+ try {
163
+ await d.closeImpl(state.handle);
164
+ } catch (err) {
165
+ if (!state.error) state.error = toArtifactError(err);
166
+ }
167
+ }
168
+ } catch (err) {
169
+ // open itself failed — nothing to fsync; record the typed error.
170
+ if (!state.error) {
171
+ state.error =
172
+ err instanceof ArtifactError ? err : toArtifactError(err);
173
+ }
174
+ }
175
+ const ref = {
176
+ path: state.path,
177
+ sha256: state.hash.digest('hex'),
178
+ bytes: state.bytes,
179
+ truncated: state.truncated,
180
+ stream: state.stream,
181
+ };
182
+ if (state.error) ref.error = state.error;
183
+ refs.push(ref);
184
+ }
185
+ return refs;
186
+ }
187
+
188
+ return { stdout: streams.stdout.writable, stderr: streams.stderr.writable, close };
189
+ }
190
+
191
+ return { open };
192
+ }
@@ -0,0 +1,176 @@
1
+ // Deterministic completion-evidence evaluator (G5 / TASK-003).
2
+ // Pure module: no I/O, no runtimeConfig reads, no adapter/context imports.
3
+ // Maps requirements × checks × operationRefs (+ optional G4 vmStatus,
4
+ // optional modelJudgment) to a frozen CompletionVerdict with precedence
5
+ // FAILED > FIX_REQUIRED > MORE_EVIDENCE > PASS.
6
+ //
7
+ // The installed execution-ledger.mjs remains the completion authority —
8
+ // its evaluateCompletion result shape may be fed here as a fixture, but it
9
+ // is never imported and never edited. A model-review layer may only ask for
10
+ // more evidence: `modelJudgment` is recorded on the verdict and can
11
+ // *downgrade* a PASS to MORE_EVIDENCE, but never converts FAIL→PASS.
12
+
13
+ import { isTerminal } from './contract.js';
14
+
15
+ export const COMPLETION_VERDICTS = Object.freeze([
16
+ 'PASS',
17
+ 'MORE_EVIDENCE',
18
+ 'FIX_REQUIRED',
19
+ 'FAILED',
20
+ ]);
21
+
22
+ // vm terminal states that mean the run itself failed — not merely unproven.
23
+ const VM_FAILED_STATES = new Set(['failed', 'cancelled']);
24
+ // vm states that mean evidence is unusable until recovery completes.
25
+ const VM_FIX_STATES = new Set(['recovery_required']);
26
+
27
+ const PASS_STATUSES = new Set(['passed', 'pass', 'ok', 'success']);
28
+ const FAIL_STATUSES = new Set(['failed', 'fail', 'error']);
29
+ const WARN_STATUSES = new Set(['warning', 'warn', 'skipped', 'indeterminate']);
30
+
31
+ function normStatus(status) {
32
+ const s = typeof status === 'string' ? status.toLowerCase() : '';
33
+ if (PASS_STATUSES.has(s)) return 'passed';
34
+ if (FAIL_STATUSES.has(s)) return 'failed';
35
+ if (WARN_STATUSES.has(s)) return 'warning';
36
+ return 'unproven';
37
+ }
38
+
39
+ // A check proves a requirement when ids match: check.requirementId → req.id,
40
+ // or req.checkId → check.checkId. Either direction is accepted so callers may
41
+ // key evidence from whichever side they own.
42
+ function checkFor(req, checks) {
43
+ return checks.find(
44
+ (c) =>
45
+ c != null
46
+ && typeof c === 'object'
47
+ && ((typeof c.requirementId === 'string' && c.requirementId === req.id)
48
+ || (typeof req.checkId === 'string' && c.checkId === req.checkId)),
49
+ );
50
+ }
51
+
52
+ // Evidence is stale when it was produced against a different patch/head than
53
+ // the operation now carries. Stale ≠ missing: the work must be re-verified,
54
+ // which is rework (FIX_REQUIRED), not just more evidence.
55
+ function isStale(check, operationRefs) {
56
+ if (!check || !operationRefs || typeof operationRefs !== 'object') return false;
57
+ if (operationRefs.patchId != null && check.patchId != null
58
+ && check.patchId !== operationRefs.patchId) return true;
59
+ if (operationRefs.head != null && check.head != null
60
+ && check.head !== operationRefs.head) return true;
61
+ return false;
62
+ }
63
+
64
+ export function evaluateCompletionEvidence({
65
+ requirements,
66
+ checks,
67
+ operationRefs,
68
+ vmStatus,
69
+ modelJudgment,
70
+ now,
71
+ } = {}) {
72
+ const nowMs = typeof now === 'number' ? now : Date.now();
73
+ const reqs = Array.isArray(requirements) ? requirements : [];
74
+ const checkList = Array.isArray(checks) ? checks : [];
75
+ const refs = operationRefs != null && typeof operationRefs === 'object'
76
+ ? operationRefs
77
+ : null;
78
+
79
+ const satisfied = [];
80
+ const missing = [];
81
+ const unprovenRequirements = [];
82
+ const failedChecks = [];
83
+ const staleEvidence = [];
84
+ const warnings = [];
85
+
86
+ for (const req of reqs) {
87
+ const reqId = req != null && req.id != null ? String(req.id) : 'unknown';
88
+ const check = checkFor(req, checkList);
89
+
90
+ if (!check) {
91
+ missing.push(reqId);
92
+ unprovenRequirements.push(reqId);
93
+ continue;
94
+ }
95
+
96
+ const status = normStatus(check.status);
97
+ const stale = isStale(check, refs);
98
+
99
+ if (stale) {
100
+ staleEvidence.push(Object.freeze({
101
+ checkId: check.checkId != null ? String(check.checkId) : reqId,
102
+ evidenceRef: check.evidenceRef != null ? String(check.evidenceRef) : null,
103
+ patchId: check.patchId != null ? check.patchId : null,
104
+ }));
105
+ unprovenRequirements.push(reqId);
106
+ continue;
107
+ }
108
+
109
+ if (status === 'failed') {
110
+ failedChecks.push(Object.freeze({
111
+ checkId: check.checkId != null ? String(check.checkId) : reqId,
112
+ evidenceRef: check.evidenceRef != null ? String(check.evidenceRef) : null,
113
+ }));
114
+ continue;
115
+ }
116
+
117
+ if (status === 'warning') {
118
+ warnings.push(reqId);
119
+ unprovenRequirements.push(reqId);
120
+ continue;
121
+ }
122
+
123
+ if (status === 'passed') {
124
+ satisfied.push(reqId);
125
+ continue;
126
+ }
127
+
128
+ unprovenRequirements.push(reqId);
129
+ }
130
+
131
+ // vm state never permits PASS: terminal failure → FAILED, recovery →
132
+ // rework, any other non-terminal state → the operation is still in flight
133
+ // so evidence cannot yet prove completion.
134
+ const vmFailed = typeof vmStatus === 'string' && VM_FAILED_STATES.has(vmStatus);
135
+ const vmRecovery = typeof vmStatus === 'string' && VM_FIX_STATES.has(vmStatus);
136
+ const vmInFlight = typeof vmStatus === 'string'
137
+ && !vmFailed && !vmRecovery && !isTerminal(vmStatus) && vmStatus !== 'completed';
138
+
139
+ // Non-zero exit status = failed verification run → rework required.
140
+ const exitFailed = refs != null
141
+ && typeof refs.exitStatus === 'number'
142
+ && refs.exitStatus !== 0;
143
+
144
+ let verdict = 'PASS';
145
+ if (missing.length > 0 || unprovenRequirements.length > 0 || vmInFlight) {
146
+ verdict = 'MORE_EVIDENCE';
147
+ }
148
+ if (staleEvidence.length > 0 || exitFailed || vmRecovery) {
149
+ verdict = 'FIX_REQUIRED';
150
+ }
151
+ if (failedChecks.length > 0 || vmFailed) {
152
+ verdict = 'FAILED';
153
+ }
154
+
155
+ // modelJudgment may only request more evidence, never promote.
156
+ const judgment = modelJudgment != null
157
+ ? (typeof modelJudgment === 'string' ? modelJudgment : String(modelJudgment))
158
+ : null;
159
+ if (verdict === 'PASS' && judgment != null && judgment !== 'pass') {
160
+ verdict = 'MORE_EVIDENCE';
161
+ }
162
+
163
+ return Object.freeze({
164
+ verdict,
165
+ satisfied: Object.freeze(satisfied),
166
+ missing: Object.freeze(missing),
167
+ failedChecks: Object.freeze(failedChecks),
168
+ unprovenRequirements: Object.freeze(unprovenRequirements),
169
+ staleEvidence: Object.freeze(staleEvidence),
170
+ warnings: Object.freeze(warnings),
171
+ patchId: refs != null && refs.patchId != null ? refs.patchId : undefined,
172
+ vmStatus: typeof vmStatus === 'string' ? vmStatus : undefined,
173
+ modelJudgment: judgment != null ? judgment : undefined,
174
+ evaluatedAt: nowMs,
175
+ });
176
+ }