@bitkyc08/opencodex 2.12.0 → 2.13.0
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/README.md +1 -1
- package/gui/dist/assets/index-BHldBl6_.js +76 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +15 -0
- package/src/adapters/google-antigravity-replay.ts +269 -5
- package/src/adapters/openai-chat.ts +25 -18
- package/src/adapters/openai-responses.ts +63 -17
- package/src/bridge.ts +11 -5
- package/src/claude/inbound.ts +24 -3
- package/src/cli/account-api.ts +2 -0
- package/src/cli/account-extended.ts +262 -1
- package/src/cli/account.ts +3 -1
- package/src/cli/dispatch.ts +532 -0
- package/src/cli/doctor.ts +26 -21
- package/src/cli/help.ts +9 -278
- package/src/cli/index.ts +45 -506
- package/src/cli/lab.ts +171 -65
- package/src/cli/registry.ts +384 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/status.ts +9 -2
- package/src/cli/v2.ts +37 -2
- package/src/codex/account-label.ts +14 -1
- package/src/codex/account-store.ts +12 -1
- package/src/codex/admission.ts +2 -1
- package/src/codex/auth-api.ts +3 -2
- package/src/codex/catalog/metadata.ts +6 -4
- package/src/codex/catalog/parsing.ts +31 -7
- package/src/codex/catalog/provider-fetch.ts +2 -1
- package/src/codex/catalog/sync.ts +65 -11
- package/src/codex/features.ts +437 -31
- package/src/codex/history-job.ts +6 -11
- package/src/codex/history-provider.ts +10 -9
- package/src/codex/inject.ts +14 -1
- package/src/codex/native-profile-startup.ts +49 -3
- package/src/codex/native-residue.ts +14 -2
- package/src/codex/paths.ts +75 -1
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/shim.ts +876 -16
- package/src/codex/sync.ts +27 -2
- package/src/config.ts +148 -10
- package/src/generated/compatibility-version.json +2572 -0
- package/src/images/loop.ts +1 -1
- package/src/lab/artifacts/sanitize.ts +3 -3
- package/src/lab/automation/budgets.ts +78 -0
- package/src/lab/automation/config-persistence.ts +256 -0
- package/src/lab/automation/constants.ts +39 -0
- package/src/lab/automation/cooldown.ts +103 -0
- package/src/lab/automation/dispatch.ts +211 -0
- package/src/lab/automation/index.ts +13 -0
- package/src/lab/automation/orchestrator.ts +480 -0
- package/src/lab/automation/persistence.ts +512 -0
- package/src/lab/automation/planner.ts +371 -0
- package/src/lab/automation/policy.ts +136 -0
- package/src/lab/automation/queue.ts +191 -0
- package/src/lab/automation/recovery.ts +24 -0
- package/src/lab/automation/route-context.ts +21 -0
- package/src/lab/automation/run-key.ts +44 -0
- package/src/lab/automation/runs-query.ts +34 -0
- package/src/lab/automation/types.ts +160 -0
- package/src/lab/conformance/types.ts +23 -0
- package/src/lab/events/types.ts +2 -1
- package/src/lab/events/validate.ts +2 -1
- package/src/lab/fabric/constants.ts +40 -0
- package/src/lab/fabric/executor.ts +492 -0
- package/src/lab/fabric/index.ts +80 -0
- package/src/lab/fabric/manifest.ts +222 -0
- package/src/lab/fabric/observe.ts +454 -0
- package/src/lab/fabric/patch.ts +79 -0
- package/src/lab/fabric/producer-child.ts +139 -0
- package/src/lab/fabric/producer-isolate.ts +276 -0
- package/src/lab/fabric/producer-protocol.ts +61 -0
- package/src/lab/fabric/scratch.ts +439 -0
- package/src/lab/fabric/subject.ts +106 -0
- package/src/lab/fabric/types.ts +134 -0
- package/src/lab/fabric/verifier.ts +98 -0
- package/src/lab/index.ts +2 -0
- package/src/lab/ledger/store.ts +173 -0
- package/src/lab/live/executor.ts +29 -4
- package/src/lab/live/transport.ts +3 -1
- package/src/lab/observe/from-conformance.ts +12 -57
- package/src/lab/observe/from-live.ts +9 -1
- package/src/lab/paths.ts +74 -9
- package/src/lab/projection/schema.ts +14 -1
- package/src/lab/projection/verdicts.ts +89 -53
- package/src/lab/projection/verification.ts +83 -3
- package/src/lab/query/catalog.ts +34 -5
- package/src/lab/query/freshness.ts +53 -0
- package/src/lab/query/index.ts +11 -0
- package/src/lab/query/latest-observation.ts +59 -0
- package/src/lab/query/passive-production.ts +159 -0
- package/src/lab/subject/installation-salt.ts +24 -4
- package/src/lab/subject/protocol-subject.ts +80 -0
- package/src/lab/subject/route-subject.ts +2 -1
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -0
- package/src/lib/lab-live-route-production.ts +130 -0
- package/src/lib/local-management-capability.ts +100 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/windows-elevation.ts +8 -1
- package/src/lib/windows-user-principal.ts +62 -2
- package/src/lib/winsw.ts +8 -2
- package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
- package/src/oauth/account-import/index.ts +15 -0
- package/src/oauth/account-import/parser.ts +83 -0
- package/src/oauth/account-import/registry.ts +18 -0
- package/src/oauth/account-import/service.ts +75 -0
- package/src/oauth/account-import/types.ts +91 -0
- package/src/oauth/anthropic.ts +12 -1
- package/src/oauth/callback-server.ts +8 -2
- package/src/oauth/chatgpt.ts +12 -1
- package/src/oauth/google-antigravity.ts +27 -0
- package/src/oauth/health.ts +12 -48
- package/src/oauth/index.ts +122 -0
- package/src/oauth/kimi.ts +16 -2
- package/src/oauth/nous.ts +749 -0
- package/src/oauth/store.ts +56 -3
- package/src/providers/codex-capacity.ts +1 -0
- package/src/providers/registry.ts +161 -16
- package/src/responses/custom-tool-compat.ts +182 -0
- package/src/responses/parser.ts +22 -0
- package/src/responses/reasoning-replay-cache.ts +168 -11
- package/src/responses/schema.ts +1 -1
- package/src/router.ts +5 -20
- package/src/routing/capability.ts +9 -3
- package/src/routing/compatibility/assemble.ts +159 -0
- package/src/routing/compatibility/behavior.ts +224 -0
- package/src/routing/compatibility/catalog.ts +99 -0
- package/src/routing/compatibility/endpoint.ts +52 -0
- package/src/routing/compatibility/policy.ts +181 -0
- package/src/routing/compatibility/reader.ts +110 -0
- package/src/routing/compatibility/subject.ts +191 -0
- package/src/routing/compatibility/types.ts +64 -0
- package/src/routing/compatibility/version.ts +104 -0
- package/src/routing/evaluator.ts +27 -32
- package/src/routing/profile.ts +118 -4
- package/src/routing/trace.ts +82 -28
- package/src/server/auth-cors.ts +7 -0
- package/src/server/background-lifecycle.ts +20 -7
- package/src/server/chat-completions.ts +2 -2
- package/src/server/claude-messages.ts +6 -3
- package/src/server/direct-local-http.ts +343 -0
- package/src/server/index.ts +80 -18
- package/src/server/lifecycle.ts +12 -4
- package/src/server/local-management-read-client.ts +90 -0
- package/src/server/management/agent-settings-routes.ts +27 -3
- package/src/server/management/lab-automation-routes.ts +206 -0
- package/src/server/management/lab-routes.ts +13 -0
- package/src/server/management/logs-usage-routes.ts +1 -0
- package/src/server/management/oauth-account-routes.ts +57 -1
- package/src/server/management/provider-routes.ts +16 -0
- package/src/server/management/routing-profile-routes.ts +12 -24
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +68 -3
- package/src/server/proxy-liveness.ts +3 -2
- package/src/server/request-decompress.ts +9 -3
- package/src/server/request-log.ts +53 -4
- package/src/server/responses/collaboration.ts +93 -17
- package/src/server/responses/compact.ts +3 -0
- package/src/server/responses/core.ts +202 -15
- package/src/server/responses/policy-fallback.ts +1 -0
- package/src/server/responses-custom-tool-repair.ts +280 -0
- package/src/server/responses-terminal-repair.ts +342 -0
- package/src/server/startup-action-control.ts +8 -1
- package/src/service.ts +278 -12
- package/src/types.ts +49 -3
- package/src/usage/expected-prices.ts +30 -0
- package/src/usage/log.ts +30 -4
- package/src/usage/summary.ts +153 -2
- package/src/web-search/loop.ts +1 -1
- package/gui/dist/assets/index-Cw1_mxO8.js +0 -70
- package/src/cli/internal-dispatch.ts +0 -20
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import type { CaseRecord, VerificationRole } from "../conformance/types";
|
|
2
|
+
import { fixtureDigest, scenarioManifestDigest, suiteManifestDigest } from "../digest";
|
|
3
|
+
import {
|
|
4
|
+
FABRIC_EVIDENCE_LAYER,
|
|
5
|
+
FABRIC_LIMITS,
|
|
6
|
+
FABRIC_SCENARIO_ID,
|
|
7
|
+
FABRIC_SCENARIO_VERSION,
|
|
8
|
+
FABRIC_SUITE_ID,
|
|
9
|
+
FABRIC_SUITE_VERSION,
|
|
10
|
+
FABRIC_TASK_CLASS_ID,
|
|
11
|
+
SYNTHETIC_AFTER_UTF8,
|
|
12
|
+
SYNTHETIC_BEFORE_UTF8,
|
|
13
|
+
SYNTHETIC_VALUE_PATH,
|
|
14
|
+
} from "./constants";
|
|
15
|
+
import { verifierManifestDigest } from "./subject";
|
|
16
|
+
|
|
17
|
+
/** In-memory authority for the frozen CL-07 fabric scenario catalogue. */
|
|
18
|
+
export interface FabricCaseAuthority {
|
|
19
|
+
schemaVersion: number;
|
|
20
|
+
sourceCommit: string;
|
|
21
|
+
assertionDslVersion: string;
|
|
22
|
+
evidenceSchemaVersion: string;
|
|
23
|
+
manifestDefaults: {
|
|
24
|
+
version: string;
|
|
25
|
+
suiteVersion: string;
|
|
26
|
+
evidenceLayer: typeof FABRIC_EVIDENCE_LAYER;
|
|
27
|
+
verificationRole: VerificationRole;
|
|
28
|
+
freshness: { maxAgeMs: number | null };
|
|
29
|
+
executionLimits: Record<string, number | null>;
|
|
30
|
+
artifactPolicy: Record<string, unknown>;
|
|
31
|
+
};
|
|
32
|
+
cases: CaseRecord[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const FABRIC_SOURCE_COMMIT = "b66e33ce7207d91014644d99317e456c992a3418";
|
|
36
|
+
const FIXTURE_BYTES_UTF8 = JSON.stringify({
|
|
37
|
+
path: SYNTHETIC_VALUE_PATH,
|
|
38
|
+
before: SYNTHETIC_BEFORE_UTF8,
|
|
39
|
+
after: SYNTHETIC_AFTER_UTF8,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/** Frozen CL-07 fabric scenario authority loaded from compiled constants. */
|
|
43
|
+
export function loadFabricCaseAuthority(): FabricCaseAuthority {
|
|
44
|
+
const fixtureBytes = new TextEncoder().encode(FIXTURE_BYTES_UTF8);
|
|
45
|
+
const digest = fixtureDigest(fixtureBytes);
|
|
46
|
+
return {
|
|
47
|
+
schemaVersion: 1,
|
|
48
|
+
sourceCommit: FABRIC_SOURCE_COMMIT,
|
|
49
|
+
assertionDslVersion: "1.0.0",
|
|
50
|
+
evidenceSchemaVersion: "1.0.0",
|
|
51
|
+
manifestDefaults: {
|
|
52
|
+
version: FABRIC_SCENARIO_VERSION,
|
|
53
|
+
suiteVersion: FABRIC_SUITE_VERSION,
|
|
54
|
+
evidenceLayer: FABRIC_EVIDENCE_LAYER,
|
|
55
|
+
verificationRole: "required",
|
|
56
|
+
freshness: { maxAgeMs: 2_592_000_000 },
|
|
57
|
+
executionLimits: {
|
|
58
|
+
totalTimeoutMs: FABRIC_LIMITS.totalTimeoutMs,
|
|
59
|
+
inactivityTimeoutMs: FABRIC_LIMITS.inactivityTimeoutMs,
|
|
60
|
+
maxInputBytes: FABRIC_LIMITS.maxAggregateIoBytes,
|
|
61
|
+
maxOutputBytes: FABRIC_LIMITS.maxAggregateIoBytes,
|
|
62
|
+
maxArtifactBytes: FABRIC_LIMITS.aggregateArtifactBytes,
|
|
63
|
+
maxRequests: 0,
|
|
64
|
+
maxToolCalls: 0,
|
|
65
|
+
maxOutputTokens: null,
|
|
66
|
+
connectTimeoutMs: null,
|
|
67
|
+
firstByteTimeoutMs: null,
|
|
68
|
+
},
|
|
69
|
+
artifactPolicy: {
|
|
70
|
+
retain: ["verifier_summary", "normalized_tree_diff", "execution_metadata"],
|
|
71
|
+
forbid: ["file_bodies", "prompts", "raw_logs"],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
cases: [{
|
|
75
|
+
id: FABRIC_SCENARIO_ID,
|
|
76
|
+
suite: FABRIC_SUITE_ID,
|
|
77
|
+
capability: FABRIC_TASK_CLASS_ID,
|
|
78
|
+
verificationRole: "required",
|
|
79
|
+
requirements: {
|
|
80
|
+
inboundProtocols: [],
|
|
81
|
+
upstreamProtocols: [],
|
|
82
|
+
surfaces: [],
|
|
83
|
+
requiredClaims: [],
|
|
84
|
+
requiredHarnessFeatures: ["fabric-scratch-v1"],
|
|
85
|
+
platforms: ["*"],
|
|
86
|
+
routePreconditions: ["exact-route-subject"],
|
|
87
|
+
},
|
|
88
|
+
fixture: {
|
|
89
|
+
id: "fabric-core.task.synthetic-patch.fixture",
|
|
90
|
+
role: "adapter_vector",
|
|
91
|
+
mediaType: "application/json",
|
|
92
|
+
bytesUtf8: FIXTURE_BYTES_UTF8,
|
|
93
|
+
digest,
|
|
94
|
+
},
|
|
95
|
+
assertions: [{
|
|
96
|
+
id: "exact-tree-diff-pass",
|
|
97
|
+
operator: "equals",
|
|
98
|
+
selector: "/verifier/passed",
|
|
99
|
+
expected: true,
|
|
100
|
+
required: true,
|
|
101
|
+
}],
|
|
102
|
+
}],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** List fabric scenarios, optionally filtered by suite id. */
|
|
107
|
+
export function discoverFabricScenarios(
|
|
108
|
+
authority: FabricCaseAuthority = loadFabricCaseAuthority(),
|
|
109
|
+
suites?: readonly string[],
|
|
110
|
+
): CaseRecord[] {
|
|
111
|
+
return authority.cases.filter((row) => !suites || suites.includes(row.suite));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Expand a fabric case into the canonical scenario manifest object. */
|
|
115
|
+
export function expandFabricScenario(
|
|
116
|
+
caseRecord: CaseRecord,
|
|
117
|
+
authority: FabricCaseAuthority = loadFabricCaseAuthority(),
|
|
118
|
+
): Record<string, unknown> {
|
|
119
|
+
const defaults = authority.manifestDefaults;
|
|
120
|
+
return {
|
|
121
|
+
schemaVersion: authority.schemaVersion,
|
|
122
|
+
id: caseRecord.id,
|
|
123
|
+
version: defaults.version,
|
|
124
|
+
suite: {
|
|
125
|
+
id: caseRecord.suite,
|
|
126
|
+
version: defaults.suiteVersion,
|
|
127
|
+
evidenceLayer: defaults.evidenceLayer,
|
|
128
|
+
},
|
|
129
|
+
evidenceLayer: defaults.evidenceLayer,
|
|
130
|
+
capability: caseRecord.capability,
|
|
131
|
+
verificationRole: caseRecord.verificationRole ?? defaults.verificationRole,
|
|
132
|
+
requirements: caseRecord.requirements,
|
|
133
|
+
fixtures: [{
|
|
134
|
+
id: caseRecord.fixture.id,
|
|
135
|
+
role: caseRecord.fixture.role,
|
|
136
|
+
mediaType: caseRecord.fixture.mediaType,
|
|
137
|
+
digest: caseRecord.fixture.digest,
|
|
138
|
+
byteLength: Buffer.byteLength(caseRecord.fixture.bytesUtf8, "utf8"),
|
|
139
|
+
syntheticMarker: "ocx-lab-synthetic-v1",
|
|
140
|
+
provenance: {
|
|
141
|
+
kind: "lab_authored",
|
|
142
|
+
authority: "007_cl07_task_effectiveness.md",
|
|
143
|
+
sourceCommit: authority.sourceCommit,
|
|
144
|
+
},
|
|
145
|
+
}],
|
|
146
|
+
executionLimits: defaults.executionLimits,
|
|
147
|
+
assertions: caseRecord.assertions,
|
|
148
|
+
failureRules: [
|
|
149
|
+
{
|
|
150
|
+
id: "verifier-fail",
|
|
151
|
+
match: ["behavioral_failure"],
|
|
152
|
+
classification: "behavioral_failure",
|
|
153
|
+
verdictEffect: "degraded",
|
|
154
|
+
retry: "never",
|
|
155
|
+
expected: false,
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: "sandbox-block",
|
|
159
|
+
match: ["sandbox_violation"],
|
|
160
|
+
classification: "sandbox_violation",
|
|
161
|
+
verdictEffect: "none",
|
|
162
|
+
retry: "bounded",
|
|
163
|
+
expected: false,
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: "timeout-block",
|
|
167
|
+
match: ["timeout", "inactivity_timeout", "budget_exhausted"],
|
|
168
|
+
classification: "timeout",
|
|
169
|
+
verdictEffect: "none",
|
|
170
|
+
retry: "bounded",
|
|
171
|
+
expected: false,
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
artifactPolicy: defaults.artifactPolicy,
|
|
175
|
+
freshness: defaults.freshness,
|
|
176
|
+
verifierManifestDigest: verifierManifestDigest(),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Expand a fabric suite id into the canonical suite manifest object. */
|
|
181
|
+
export function expandFabricSuiteManifest(
|
|
182
|
+
suiteId: string = FABRIC_SUITE_ID,
|
|
183
|
+
authority: FabricCaseAuthority = loadFabricCaseAuthority(),
|
|
184
|
+
): Record<string, unknown> {
|
|
185
|
+
const cases = authority.cases.filter((row) => row.suite === suiteId);
|
|
186
|
+
if (cases.length === 0) throw new Error(`unknown fabric suite ${suiteId}`);
|
|
187
|
+
const defaults = authority.manifestDefaults;
|
|
188
|
+
return {
|
|
189
|
+
schemaVersion: 1,
|
|
190
|
+
id: suiteId,
|
|
191
|
+
version: defaults.suiteVersion,
|
|
192
|
+
evidenceLayer: defaults.evidenceLayer,
|
|
193
|
+
capability: cases[0]!.capability,
|
|
194
|
+
assertionDslVersion: authority.assertionDslVersion,
|
|
195
|
+
evidenceSchemaVersion: authority.evidenceSchemaVersion,
|
|
196
|
+
freshness: defaults.freshness,
|
|
197
|
+
contradictionRule: "newest-required-observation-v1",
|
|
198
|
+
scenarios: cases.map((caseRecord) => ({
|
|
199
|
+
id: caseRecord.id,
|
|
200
|
+
version: defaults.version,
|
|
201
|
+
role: caseRecord.verificationRole ?? defaults.verificationRole,
|
|
202
|
+
manifestDigest: scenarioManifestDigest(expandFabricScenario(caseRecord, authority)),
|
|
203
|
+
})),
|
|
204
|
+
verificationRule: "all-applicable-required-pass-v1",
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Digest of the expanded fabric scenario manifest for a case. */
|
|
209
|
+
export function fabricScenarioManifestDigest(
|
|
210
|
+
caseRecord: CaseRecord = loadFabricCaseAuthority().cases[0]!,
|
|
211
|
+
authority: FabricCaseAuthority = loadFabricCaseAuthority(),
|
|
212
|
+
): string {
|
|
213
|
+
return scenarioManifestDigest(expandFabricScenario(caseRecord, authority));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Digest of the expanded fabric suite manifest. */
|
|
217
|
+
export function fabricSuiteManifestDigest(
|
|
218
|
+
suiteId: string = FABRIC_SUITE_ID,
|
|
219
|
+
authority: FabricCaseAuthority = loadFabricCaseAuthority(),
|
|
220
|
+
): string {
|
|
221
|
+
return suiteManifestDigest(expandFabricSuiteManifest(suiteId, authority));
|
|
222
|
+
}
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import { createArtifactStore, type ArtifactStore } from "../artifacts/store";
|
|
2
|
+
import { sanitizeDiagnostic, truncateUtf8 } from "../artifacts/sanitize";
|
|
3
|
+
import {
|
|
4
|
+
LAB_EVENT_SCHEMA_VERSION,
|
|
5
|
+
LAB_PRODUCER,
|
|
6
|
+
LAB_PRODUCER_VERSION,
|
|
7
|
+
OBSERVATION_LIMIT_NAMES,
|
|
8
|
+
OUTCOMES,
|
|
9
|
+
} from "../constants";
|
|
10
|
+
import { FAILURE_CLASSIFICATIONS } from "../conformance/types";
|
|
11
|
+
import { fixtureDigest, isSha256Hex, jcsStringify } from "../digest";
|
|
12
|
+
import type { ObservationEvent, RouteSubjectV1, TaskSubjectV1 } from "../events/types";
|
|
13
|
+
import { LabValidationError } from "../events/errors";
|
|
14
|
+
import { assignEventId, validateSubject } from "../events/validate";
|
|
15
|
+
import { appendLabEventIfAbsent } from "../ledger/store";
|
|
16
|
+
import { ensureLabDirs } from "../paths";
|
|
17
|
+
import {
|
|
18
|
+
FABRIC_EVIDENCE_LAYER,
|
|
19
|
+
FABRIC_SCENARIO_ID,
|
|
20
|
+
FABRIC_SCENARIO_VERSION,
|
|
21
|
+
FABRIC_SUITE_ID,
|
|
22
|
+
FABRIC_SUITE_VERSION,
|
|
23
|
+
FABRIC_LIMITS,
|
|
24
|
+
FABRIC_VERIFIER_ID,
|
|
25
|
+
} from "./constants";
|
|
26
|
+
import {
|
|
27
|
+
expandFabricScenario,
|
|
28
|
+
expandFabricSuiteManifest,
|
|
29
|
+
fabricScenarioManifestDigest,
|
|
30
|
+
fabricSuiteManifestDigest,
|
|
31
|
+
loadFabricCaseAuthority,
|
|
32
|
+
} from "./manifest";
|
|
33
|
+
import type { FabricLimitsV1, FabricTaskOutcomeV1, FabricTaskRunResult } from "./types";
|
|
34
|
+
import { FabricTaskError } from "./types";
|
|
35
|
+
|
|
36
|
+
/** CL-07 fabric task outcome validation and observation persistence. */
|
|
37
|
+
|
|
38
|
+
/** Options for building or persisting a fabric observation event. */
|
|
39
|
+
export interface PersistFabricOptions {
|
|
40
|
+
configDir?: string;
|
|
41
|
+
recordedAt?: number;
|
|
42
|
+
producerVersion?: string;
|
|
43
|
+
artifactStore?: ArtifactStore;
|
|
44
|
+
attempt?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Result of appending a fabric observation to the compatibility ledger. */
|
|
48
|
+
export interface PersistedFabricObservation {
|
|
49
|
+
event: ObservationEvent;
|
|
50
|
+
ledgerPath: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const OUTCOME_KEYS = new Set([
|
|
54
|
+
"schemaVersion",
|
|
55
|
+
"taskClassId",
|
|
56
|
+
"taskClassVersion",
|
|
57
|
+
"routeSubject",
|
|
58
|
+
"taskSubject",
|
|
59
|
+
"subjectId",
|
|
60
|
+
"taskFixtureDigest",
|
|
61
|
+
"verifierManifestDigest",
|
|
62
|
+
"fabricCompatibilityVersion",
|
|
63
|
+
"sandboxProfileDigest",
|
|
64
|
+
"startedAt",
|
|
65
|
+
"completedAt",
|
|
66
|
+
"limits",
|
|
67
|
+
"usage",
|
|
68
|
+
"outcome",
|
|
69
|
+
"verifier",
|
|
70
|
+
"failure",
|
|
71
|
+
"artifactDigests",
|
|
72
|
+
"sourceRefs",
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
const VERIFIER_KEYS = new Set(["verifierId", "manifestDigest", "passed", "pathSummaries", "reason"]);
|
|
76
|
+
const PATH_SUMMARY_KEYS = new Set(["path", "kind", "beforeDigest", "afterDigest", "reason"]);
|
|
77
|
+
const PATH_SUMMARY_KINDS = new Set(["unchanged", "modified", "added", "deleted", "rejected"]);
|
|
78
|
+
const USAGE_KEYS = ["inputBytes", "outputBytes", "patchOperations", "filesTouched", "artifactBytes", "elapsedMs", "inactiveMs"] as const;
|
|
79
|
+
const LIMIT_KEYS = Object.keys(FABRIC_LIMITS) as Array<keyof FabricLimitsV1>;
|
|
80
|
+
const FAILURE_KEYS = new Set(["class", "code", "retryable", "attribution"]);
|
|
81
|
+
const FAILURE_ATTRIBUTIONS = new Set(["opencodex", "route", "environment", "harness"]);
|
|
82
|
+
|
|
83
|
+
/** Require a non-null plain object or throw a harness-class FabricTaskError. */
|
|
84
|
+
function assertPlainObject(value: unknown, label: string): Record<string, unknown> {
|
|
85
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
86
|
+
throw new FabricTaskError(`malformed producer outcome: ${label}`, "malformed_producer_outcome", "harness");
|
|
87
|
+
}
|
|
88
|
+
return value as Record<string, unknown>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Require a non-empty string field on a producer outcome object. */
|
|
92
|
+
function assertStringField(obj: Record<string, unknown>, key: string): string {
|
|
93
|
+
const value = obj[key];
|
|
94
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
95
|
+
throw new FabricTaskError(`malformed producer outcome: ${key}`, "malformed_producer_outcome", "harness");
|
|
96
|
+
}
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Require an integer field on a producer outcome object. */
|
|
101
|
+
function assertIntegerField(obj: Record<string, unknown>, key: string): number {
|
|
102
|
+
const value = obj[key];
|
|
103
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
104
|
+
throw new FabricTaskError(`malformed producer outcome: ${key}`, "malformed_producer_outcome", "harness");
|
|
105
|
+
}
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Require a non-negative integer field on a producer outcome object. */
|
|
110
|
+
function assertNonNegativeIntegerField(obj: Record<string, unknown>, key: string): number {
|
|
111
|
+
const value = assertIntegerField(obj, key);
|
|
112
|
+
if (value < 0) {
|
|
113
|
+
throw new FabricTaskError(`malformed producer outcome: ${key}`, "malformed_producer_outcome", "harness");
|
|
114
|
+
}
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Map LabValidationError into FabricTaskError for producer outcome parsing. */
|
|
119
|
+
function wrapValidationError(error: unknown): never {
|
|
120
|
+
if (error instanceof LabValidationError) {
|
|
121
|
+
throw new FabricTaskError(error.message, "malformed_producer_outcome", "harness");
|
|
122
|
+
}
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Validate verifier payload shape and reject unknown nested verifier fields. */
|
|
127
|
+
function validateFabricVerifier(raw: Record<string, unknown>): void {
|
|
128
|
+
for (const key of Object.keys(raw)) {
|
|
129
|
+
if (!VERIFIER_KEYS.has(key)) {
|
|
130
|
+
throw new FabricTaskError(`unknown verifier field ${key}`, "malformed_producer_outcome", "harness");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (raw.verifierId !== FABRIC_VERIFIER_ID) {
|
|
134
|
+
throw new FabricTaskError("malformed producer outcome: verifierId", "malformed_producer_outcome", "harness");
|
|
135
|
+
}
|
|
136
|
+
const manifestDigest = assertStringField(raw, "manifestDigest");
|
|
137
|
+
if (!isSha256Hex(manifestDigest)) {
|
|
138
|
+
throw new FabricTaskError("malformed producer outcome: manifestDigest", "malformed_producer_outcome", "harness");
|
|
139
|
+
}
|
|
140
|
+
if (typeof raw.passed !== "boolean") {
|
|
141
|
+
throw new FabricTaskError("malformed producer outcome: passed", "malformed_producer_outcome", "harness");
|
|
142
|
+
}
|
|
143
|
+
if (!Array.isArray(raw.pathSummaries)) {
|
|
144
|
+
throw new FabricTaskError("malformed producer outcome: pathSummaries", "malformed_producer_outcome", "harness");
|
|
145
|
+
}
|
|
146
|
+
for (const row of raw.pathSummaries) {
|
|
147
|
+
const summary = assertPlainObject(row, "pathSummaries[]");
|
|
148
|
+
for (const key of Object.keys(summary)) {
|
|
149
|
+
if (!PATH_SUMMARY_KEYS.has(key)) {
|
|
150
|
+
throw new FabricTaskError(`unknown path summary field ${key}`, "malformed_producer_outcome", "harness");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
assertStringField(summary, "path");
|
|
154
|
+
const kind = assertStringField(summary, "kind");
|
|
155
|
+
if (!PATH_SUMMARY_KINDS.has(kind)) {
|
|
156
|
+
throw new FabricTaskError("malformed producer outcome: path summary kind", "malformed_producer_outcome", "harness");
|
|
157
|
+
}
|
|
158
|
+
if (summary.beforeDigest !== undefined && !isSha256Hex(assertStringField(summary, "beforeDigest"))) {
|
|
159
|
+
throw new FabricTaskError("malformed producer outcome: beforeDigest", "malformed_producer_outcome", "harness");
|
|
160
|
+
}
|
|
161
|
+
if (summary.afterDigest !== undefined && !isSha256Hex(assertStringField(summary, "afterDigest"))) {
|
|
162
|
+
throw new FabricTaskError("malformed producer outcome: afterDigest", "malformed_producer_outcome", "harness");
|
|
163
|
+
}
|
|
164
|
+
if (summary.reason !== undefined && typeof summary.reason !== "string") {
|
|
165
|
+
throw new FabricTaskError("malformed producer outcome: path summary reason", "malformed_producer_outcome", "harness");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (raw.reason !== undefined && typeof raw.reason !== "string") {
|
|
169
|
+
throw new FabricTaskError("malformed producer outcome: reason", "malformed_producer_outcome", "harness");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Validate non-negative usage counters on a producer outcome. */
|
|
174
|
+
function validateFabricUsage(raw: Record<string, unknown>): void {
|
|
175
|
+
for (const key of USAGE_KEYS) {
|
|
176
|
+
assertNonNegativeIntegerField(raw, key);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Validate non-negative limit fields on a producer outcome. */
|
|
181
|
+
function validateFabricLimits(raw: Record<string, unknown>): void {
|
|
182
|
+
for (const key of LIMIT_KEYS) {
|
|
183
|
+
assertNonNegativeIntegerField(raw, key);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Validate failure record shape and known attribution values. */
|
|
188
|
+
function validateFailureRecord(raw: Record<string, unknown>): void {
|
|
189
|
+
for (const key of Object.keys(raw)) {
|
|
190
|
+
if (!FAILURE_KEYS.has(key)) {
|
|
191
|
+
throw new FabricTaskError(`unknown failure field ${key}`, "malformed_producer_outcome", "harness");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const failureClass = assertStringField(raw, "class");
|
|
195
|
+
if (!FAILURE_CLASSIFICATIONS.includes(failureClass as typeof FAILURE_CLASSIFICATIONS[number])) {
|
|
196
|
+
throw new FabricTaskError("malformed producer outcome: failure.class", "malformed_producer_outcome", "harness");
|
|
197
|
+
}
|
|
198
|
+
assertStringField(raw, "code");
|
|
199
|
+
if (typeof raw.retryable !== "boolean") {
|
|
200
|
+
throw new FabricTaskError("malformed producer outcome: failure.retryable", "malformed_producer_outcome", "harness");
|
|
201
|
+
}
|
|
202
|
+
const attribution = assertStringField(raw, "attribution");
|
|
203
|
+
if (!FAILURE_ATTRIBUTIONS.has(attribution)) {
|
|
204
|
+
throw new FabricTaskError("malformed producer outcome: failure.attribution", "malformed_producer_outcome", "harness");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Compare top-level and nested route subjects by canonical JCS representation. */
|
|
209
|
+
function routeSubjectsMatch(top: RouteSubjectV1, nested: RouteSubjectV1): boolean {
|
|
210
|
+
return jcsStringify(top) === jcsStringify(nested);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Build an allowlisted verifier_summary artifact with sanitized diagnostics. */
|
|
214
|
+
function sanitizedVerifierSummary(outcome: FabricTaskOutcomeV1): Record<string, unknown> {
|
|
215
|
+
const verifier = {
|
|
216
|
+
verifierId: outcome.verifier.verifierId,
|
|
217
|
+
manifestDigest: outcome.verifier.manifestDigest,
|
|
218
|
+
passed: outcome.verifier.passed,
|
|
219
|
+
pathSummaries: outcome.verifier.pathSummaries.map((row) => ({
|
|
220
|
+
path: row.path,
|
|
221
|
+
kind: row.kind,
|
|
222
|
+
...(row.beforeDigest ? { beforeDigest: row.beforeDigest } : {}),
|
|
223
|
+
...(row.afterDigest ? { afterDigest: row.afterDigest } : {}),
|
|
224
|
+
...(row.reason ? { reason: truncateUtf8(sanitizeDiagnostic(row.reason), 512) } : {}),
|
|
225
|
+
})),
|
|
226
|
+
...(outcome.verifier.reason
|
|
227
|
+
? { reason: truncateUtf8(sanitizeDiagnostic(outcome.verifier.reason), 512) }
|
|
228
|
+
: {}),
|
|
229
|
+
};
|
|
230
|
+
return {
|
|
231
|
+
scenarioId: FABRIC_SCENARIO_ID,
|
|
232
|
+
passed: outcome.verifier.passed,
|
|
233
|
+
verifier,
|
|
234
|
+
usage: outcome.usage,
|
|
235
|
+
outcome: outcome.outcome,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Parse and deeply validate a fabric producer outcome before persistence. */
|
|
240
|
+
export function assertFabricOutcomeV1(raw: unknown): FabricTaskOutcomeV1 {
|
|
241
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
242
|
+
throw new FabricTaskError("malformed producer outcome", "malformed_producer_outcome", "harness");
|
|
243
|
+
}
|
|
244
|
+
const obj = raw as Record<string, unknown>;
|
|
245
|
+
for (const key of Object.keys(obj)) {
|
|
246
|
+
if (!OUTCOME_KEYS.has(key)) {
|
|
247
|
+
throw new FabricTaskError(`unknown outcome field ${key}`, "malformed_producer_outcome", "harness");
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (obj.schemaVersion !== 1) {
|
|
251
|
+
throw new FabricTaskError("schemaVersion must be 1", "malformed_producer_outcome", "harness");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const taskSubjectObj = assertPlainObject(obj.taskSubject, "taskSubject");
|
|
255
|
+
const routeSubjectObj = assertPlainObject(obj.routeSubject, "routeSubject");
|
|
256
|
+
|
|
257
|
+
let taskSubject!: TaskSubjectV1;
|
|
258
|
+
let routeSubject!: RouteSubjectV1;
|
|
259
|
+
try {
|
|
260
|
+
taskSubject = validateSubject(taskSubjectObj, "task_effectiveness") as TaskSubjectV1;
|
|
261
|
+
routeSubject = validateSubject(routeSubjectObj, "live_route_compatibility") as RouteSubjectV1;
|
|
262
|
+
} catch (error) {
|
|
263
|
+
wrapValidationError(error);
|
|
264
|
+
}
|
|
265
|
+
if (!routeSubjectsMatch(routeSubject, taskSubject.routeSubject)) {
|
|
266
|
+
throw new FabricTaskError("contradictory route subjects", "layer_subject_mismatch", "harness");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
assertStringField(obj, "taskClassId");
|
|
270
|
+
assertStringField(obj, "taskClassVersion");
|
|
271
|
+
const subjectId = assertStringField(obj, "subjectId");
|
|
272
|
+
if (!isSha256Hex(subjectId)) {
|
|
273
|
+
throw new FabricTaskError("malformed producer outcome: subjectId", "malformed_producer_outcome", "harness");
|
|
274
|
+
}
|
|
275
|
+
const taskFixtureDigest = assertStringField(obj, "taskFixtureDigest");
|
|
276
|
+
const verifierManifestDigest = assertStringField(obj, "verifierManifestDigest");
|
|
277
|
+
if (!isSha256Hex(taskFixtureDigest) || !isSha256Hex(verifierManifestDigest)) {
|
|
278
|
+
throw new FabricTaskError("malformed producer outcome: digest field", "malformed_producer_outcome", "harness");
|
|
279
|
+
}
|
|
280
|
+
assertStringField(obj, "fabricCompatibilityVersion");
|
|
281
|
+
const sandboxProfileDigest = assertStringField(obj, "sandboxProfileDigest");
|
|
282
|
+
if (!isSha256Hex(sandboxProfileDigest)) {
|
|
283
|
+
throw new FabricTaskError("malformed producer outcome: sandboxProfileDigest", "malformed_producer_outcome", "harness");
|
|
284
|
+
}
|
|
285
|
+
assertIntegerField(obj, "startedAt");
|
|
286
|
+
assertIntegerField(obj, "completedAt");
|
|
287
|
+
validateFabricLimits(assertPlainObject(obj.limits, "limits"));
|
|
288
|
+
validateFabricUsage(assertPlainObject(obj.usage, "usage"));
|
|
289
|
+
validateFabricVerifier(assertPlainObject(obj.verifier, "verifier"));
|
|
290
|
+
if (!OUTCOMES.includes(obj.outcome as typeof OUTCOMES[number])) {
|
|
291
|
+
throw new FabricTaskError("malformed producer outcome: outcome", "malformed_producer_outcome", "harness");
|
|
292
|
+
}
|
|
293
|
+
if (!Array.isArray(obj.artifactDigests)) {
|
|
294
|
+
throw new FabricTaskError("malformed producer outcome: artifactDigests", "malformed_producer_outcome", "harness");
|
|
295
|
+
}
|
|
296
|
+
for (const digest of obj.artifactDigests) {
|
|
297
|
+
if (typeof digest !== "string" || !isSha256Hex(digest)) {
|
|
298
|
+
throw new FabricTaskError("malformed producer outcome: artifactDigests", "malformed_producer_outcome", "harness");
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (obj.failure !== undefined) {
|
|
302
|
+
validateFailureRecord(assertPlainObject(obj.failure, "failure"));
|
|
303
|
+
}
|
|
304
|
+
if (obj.sourceRefs !== undefined) {
|
|
305
|
+
if (!Array.isArray(obj.sourceRefs) || !obj.sourceRefs.every((row) => typeof row === "string" && row.length > 0)) {
|
|
306
|
+
throw new FabricTaskError("malformed producer outcome: sourceRefs", "malformed_producer_outcome", "harness");
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return raw as FabricTaskOutcomeV1;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Convert a validated fabric outcome into a ledger-ready observation and artifacts. */
|
|
314
|
+
export function observationFromFabricOutcome(
|
|
315
|
+
outcomeRaw: unknown,
|
|
316
|
+
opts: PersistFabricOptions = {},
|
|
317
|
+
): { event: ObservationEvent; artifacts: ReturnType<ArtifactStore["put"]>[] } {
|
|
318
|
+
const outcome = assertFabricOutcomeV1(outcomeRaw);
|
|
319
|
+
const taskSubject = outcome.taskSubject as TaskSubjectV1;
|
|
320
|
+
const routeSubject = outcome.routeSubject as RouteSubjectV1;
|
|
321
|
+
if (taskSubject.subjectKind !== "task") {
|
|
322
|
+
throw new FabricTaskError("task layer requires TaskSubjectV1", "layer_subject_mismatch", "harness");
|
|
323
|
+
}
|
|
324
|
+
if (routeSubject.subjectKind !== "route") {
|
|
325
|
+
throw new FabricTaskError("nested route subject required", "layer_subject_mismatch", "harness");
|
|
326
|
+
}
|
|
327
|
+
if (!Number.isInteger(outcome.startedAt) || !Number.isInteger(outcome.completedAt) || outcome.completedAt < outcome.startedAt) {
|
|
328
|
+
throw new FabricTaskError("invalid execution timestamps", "malformed_producer_outcome", "harness");
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const paths = ensureLabDirs(opts.configDir);
|
|
332
|
+
const ownsStore = !opts.artifactStore;
|
|
333
|
+
const store = opts.artifactStore ?? createArtifactStore(paths.artifactsDir);
|
|
334
|
+
try {
|
|
335
|
+
const authority = loadFabricCaseAuthority();
|
|
336
|
+
const caseRecord = authority.cases.find((row) => row.id === FABRIC_SCENARIO_ID);
|
|
337
|
+
if (!caseRecord) {
|
|
338
|
+
throw new FabricTaskError(`missing fabric scenario ${FABRIC_SCENARIO_ID}`, "harness_failure", "harness");
|
|
339
|
+
}
|
|
340
|
+
const scenarioDigest = fabricScenarioManifestDigest(caseRecord, authority);
|
|
341
|
+
const suiteDigest = fabricSuiteManifestDigest(FABRIC_SUITE_ID, authority);
|
|
342
|
+
const expandedScenario = expandFabricScenario(caseRecord, authority);
|
|
343
|
+
const expandedSuite = expandFabricSuiteManifest(FABRIC_SUITE_ID, authority);
|
|
344
|
+
const fixtureBytes = new TextEncoder().encode(caseRecord.fixture.bytesUtf8);
|
|
345
|
+
const fixtureDigests = [fixtureDigest(fixtureBytes)];
|
|
346
|
+
|
|
347
|
+
const artifacts: ReturnType<ArtifactStore["put"]>[] = [];
|
|
348
|
+
artifacts.push(store.put({
|
|
349
|
+
artifactClass: "fixture",
|
|
350
|
+
payload: fixtureBytes,
|
|
351
|
+
expectedDigest: fixtureDigests[0],
|
|
352
|
+
mediaType: caseRecord.fixture.mediaType,
|
|
353
|
+
}));
|
|
354
|
+
artifacts.push(store.put({
|
|
355
|
+
artifactClass: "scenario_manifest",
|
|
356
|
+
payload: expandedScenario,
|
|
357
|
+
expectedDigest: scenarioDigest,
|
|
358
|
+
}));
|
|
359
|
+
artifacts.push(store.put({
|
|
360
|
+
artifactClass: "suite_manifest",
|
|
361
|
+
payload: expandedSuite,
|
|
362
|
+
expectedDigest: suiteDigest,
|
|
363
|
+
}));
|
|
364
|
+
artifacts.push(store.put({
|
|
365
|
+
artifactClass: "verifier_summary",
|
|
366
|
+
payload: sanitizedVerifierSummary(outcome),
|
|
367
|
+
}));
|
|
368
|
+
|
|
369
|
+
const limits: Record<string, number | null> = {};
|
|
370
|
+
for (const key of OBSERVATION_LIMIT_NAMES) {
|
|
371
|
+
if (key in authority.manifestDefaults.executionLimits) {
|
|
372
|
+
limits[key] = authority.manifestDefaults.executionLimits[key] ?? null;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const recordedAt = opts.recordedAt ?? outcome.completedAt;
|
|
377
|
+
const eventWithoutId = {
|
|
378
|
+
schemaVersion: LAB_EVENT_SCHEMA_VERSION,
|
|
379
|
+
eventKind: "observation" as const,
|
|
380
|
+
recordedAt,
|
|
381
|
+
producer: LAB_PRODUCER,
|
|
382
|
+
producerVersion: opts.producerVersion ?? LAB_PRODUCER_VERSION,
|
|
383
|
+
evidenceLayer: FABRIC_EVIDENCE_LAYER,
|
|
384
|
+
scenarioId: FABRIC_SCENARIO_ID,
|
|
385
|
+
scenarioVersion: FABRIC_SCENARIO_VERSION,
|
|
386
|
+
scenarioManifestDigest: scenarioDigest,
|
|
387
|
+
suiteId: FABRIC_SUITE_ID,
|
|
388
|
+
suiteVersion: FABRIC_SUITE_VERSION,
|
|
389
|
+
suiteManifestDigest: suiteDigest,
|
|
390
|
+
fixtureDigests,
|
|
391
|
+
subject: outcome.taskSubject,
|
|
392
|
+
subjectId: outcome.subjectId,
|
|
393
|
+
startedAt: outcome.startedAt,
|
|
394
|
+
completedAt: outcome.completedAt,
|
|
395
|
+
executionMode: "fabric" as const,
|
|
396
|
+
attempt: opts.attempt ?? 1,
|
|
397
|
+
limits,
|
|
398
|
+
outcome: outcome.outcome,
|
|
399
|
+
assertions: [{
|
|
400
|
+
id: "exact-tree-diff-pass",
|
|
401
|
+
operator: "equals",
|
|
402
|
+
required: true,
|
|
403
|
+
passed: outcome.verifier.passed,
|
|
404
|
+
expectedSummary: "pass",
|
|
405
|
+
observedSummary: truncateUtf8(sanitizeDiagnostic(outcome.verifier.reason ?? (outcome.verifier.passed ? "pass" : "fail")), 512),
|
|
406
|
+
}],
|
|
407
|
+
environment: {
|
|
408
|
+
runtime: {
|
|
409
|
+
platform: process.platform,
|
|
410
|
+
arch: process.arch,
|
|
411
|
+
bunVersion: process.versions.bun ?? Bun.version,
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
artifactRefs: artifacts,
|
|
415
|
+
...(outcome.failure ? { failure: outcome.failure } : {}),
|
|
416
|
+
...(outcome.sourceRefs ? { sourceRefs: [...outcome.sourceRefs] } : {}),
|
|
417
|
+
};
|
|
418
|
+
return { event: assignEventId(eventWithoutId) as ObservationEvent, artifacts };
|
|
419
|
+
} finally {
|
|
420
|
+
if (ownsStore) store.close();
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** Module-private ledger append helper; production evidence must use {@link persistFabricRunResult}. */
|
|
425
|
+
function persistFabricOutcome(
|
|
426
|
+
outcome: FabricTaskOutcomeV1,
|
|
427
|
+
opts: PersistFabricOptions = {},
|
|
428
|
+
): PersistedFabricObservation {
|
|
429
|
+
const paths = ensureLabDirs(opts.configDir);
|
|
430
|
+
const ownsStore = !opts.artifactStore;
|
|
431
|
+
const store = opts.artifactStore ?? createArtifactStore(paths.artifactsDir);
|
|
432
|
+
try {
|
|
433
|
+
const { event } = observationFromFabricOutcome(outcome, { ...opts, artifactStore: store });
|
|
434
|
+
appendLabEventIfAbsent(paths.ledgerPath, event);
|
|
435
|
+
return { event, ledgerPath: paths.ledgerPath };
|
|
436
|
+
} finally {
|
|
437
|
+
if (ownsStore) store.close();
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** Persist production fabric evidence from a trusted-route run result. */
|
|
442
|
+
export function persistFabricRunResult(
|
|
443
|
+
result: FabricTaskRunResult,
|
|
444
|
+
opts: PersistFabricOptions = {},
|
|
445
|
+
): PersistedFabricObservation {
|
|
446
|
+
if (result.executionAuthority !== "trusted_route") {
|
|
447
|
+
throw new FabricTaskError(
|
|
448
|
+
"fabric evidence requires trusted route execution authority",
|
|
449
|
+
"malformed_producer_outcome",
|
|
450
|
+
"harness",
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
return persistFabricOutcome(result.outcome, opts);
|
|
454
|
+
}
|