@tea-agent/loop-agent 0.43.0-next.1 → 0.43.0-next.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/application/evaluation/budget.js +19 -1
- package/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi-executor.js +762 -252
- package/dist/executors/pi-executor.js +20 -1
- package/dist/executors/pi-sdk-executor.js +64 -1
- package/dist/executors/shell-executor.js +5 -2
- package/dist/shared/frontend-execution-policy.js +22 -0
- package/dist/task/config-types.js +4 -0
- package/dist/task/source-prepare/ledger-reconciliation.js +2 -2
- package/dist/task/source-prepare/ledger-review.js +6 -9
- package/dist/task/source-prepare/semantic-intake.js +16 -26
- package/dist/task/source-prepare/source-fidelity-pi.js +26 -7
- package/dist/worker/observe/node-transparency.js +81 -72
- package/dist/worker/observe/routes.js +20 -1
- package/dist/worker/observe/static/dag-inspector-humanize.js +3 -0
- package/dist/worker/observe/static/dom.js +20 -1
- package/dist/worker/observe/static/format-pool.d.ts +2 -0
- package/dist/worker/observe/static/format-pool.js +6 -0
- package/dist/worker/observe/static/format.js +7 -0
- package/dist/worker/observe/static/inspect-workspace.js +34 -7
- package/dist/worker/observe/static/inspector-submission.js +32 -0
- package/dist/worker/observe/static/kpi.js +1 -0
- package/dist/worker/observe/static/relations.js +2 -0
- package/dist/worker/observe/static/router.js +13 -0
- package/dist/worker/observe/static/run-processing.js +2 -0
- package/dist/worker/observe/static/shell-chrome.js +36 -3
- package/dist/worker/observe/static/state.js +35 -2
- package/dist/worker/observe/static/styles.css +260 -39
- package/dist/worker/observe/static/task-failure-labels.d.ts +4 -0
- package/dist/worker/observe/static/task-failure-labels.js +67 -0
- package/dist/worker/observe/static/task-history.js +12 -0
- package/dist/worker/observe/static/views/batch.js +6 -13
- package/dist/worker/observe/static/views/dag-graph.js +50 -3
- package/dist/worker/observe/static/views/dag-inspector.js +746 -265
- package/dist/worker/observe/static/views/dag-trajectory.js +3 -0
- package/dist/worker/observe/static/views/dag.d.ts +6 -0
- package/dist/worker/observe/static/views/dag.js +48 -10
- package/dist/worker/observe/static/views/dags.js +2 -0
- package/dist/worker/observe/static/views/dashboard.js +21 -12
- package/dist/worker/observe/static/views/failures.js +21 -11
- package/dist/worker/observe/static/views/feature.js +11 -29
- package/dist/worker/observe/static/views/pool.js +37 -28
- package/dist/worker/observe/static/views/run.js +48 -5
- package/dist/worker/observe/static/views/session-timeline.js +189 -240
- package/dist/worker/observe/static/views/task.js +81 -62
- package/dist/workflows/dag/budget-enforcement.js +53 -3
- package/dist/workflows/dag/frontend-durable-tools.js +193 -0
- package/dist/workflows/dag/frontend-execution-groups.js +24 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +9 -0
- package/dist/workflows/dag/frontend-input-projection.js +76 -0
- package/dist/workflows/dag/frontend-plan-render.js +10 -3
- package/dist/workflows/dag/frontend-recovery-controller.js +7 -7
- package/dist/workflows/dag/frontend-recovery-lineage.js +13 -0
- package/dist/workflows/dag/frontend-recovery-run.js +4 -0
- package/dist/workflows/dag/frontend-review-scopes.js +117 -0
- package/dist/workflows/dag/frontend-session-budget.js +249 -0
- package/dist/workflows/dag/frontend-shadow-dual-write.js +20 -2
- package/dist/workflows/dag/frontend-test-execution-evidence.js +3 -2
- package/dist/workflows/dag/frontend-typed-event-store.js +11 -0
- package/dist/workflows/dag/init-hybrid.js +16 -10
- package/dist/workflows/dag/node-execution.js +32 -155
- package/dist/workflows/dag/prompt.js +4 -0
- package/dist/workflows/dag/rerun-plan.js +7 -1
- package/dist/workflows/dag/runner.js +26 -1
- package/dist/workflows/dag/types.js +6 -0
- package/docs/operations/README.md +1 -0
- package/docs/templates/frontend-design-contract.md +4 -4
- package/docs/templates/frontend-implementation-contract.schema.json +34 -2
- package/docs/templates/frontend-implementation-dag.json +5 -5
- package/package.json +1 -1
- package/skills/frontend-contract/SKILL.md +2 -1
- package/skills/frontend-contract/references/contract-protocol.md +19 -3
- package/skills/frontend-design-review/SKILL.md +12 -11
- package/skills/frontend-plan/SKILL.md +2 -2
- package/skills/frontend-plan/references/decision-contract.md +18 -5
- package/skills/frontend-review/SKILL.md +10 -11
- package/skills/frontend-scout/references/scout-evidence.md +4 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { collectFrontendExecutionGroups } from "./frontend-execution-groups.js";
|
|
1
2
|
/**
|
|
2
3
|
* Deterministic Markdown renderer for a canonical frontend implementation
|
|
3
4
|
* contract. The prewrite gate materializes this as `contracts/frontend-plan.md`
|
|
@@ -17,13 +18,19 @@ function bulletList(items) {
|
|
|
17
18
|
* prose — the renderer must never present it as requirement content. */
|
|
18
19
|
const IMPLIED_OUTCOME_PREFIX = "implied by contract:";
|
|
19
20
|
function requirementsCoverage(contract) {
|
|
20
|
-
const
|
|
21
|
+
const renderRequirement = (requirement, includeOutcome = true) => {
|
|
21
22
|
const targets = requirement.implementationTargets.join(", ");
|
|
22
23
|
const verificationTargets = requirement.verificationTargetIds.join(", ");
|
|
23
|
-
const authoredOutcome = requirement.expectedOutcome.startsWith(IMPLIED_OUTCOME_PREFIX)
|
|
24
|
+
const authoredOutcome = !includeOutcome || requirement.expectedOutcome.startsWith(IMPLIED_OUTCOME_PREFIX)
|
|
24
25
|
? ""
|
|
25
26
|
: ` — ${requirement.expectedOutcome}`;
|
|
26
27
|
return `- ${requirement.id} → ${targets || "(no targets)"} [${verificationTargets || "no VT"}]${authoredOutcome}`;
|
|
28
|
+
};
|
|
29
|
+
const lines = collectFrontendExecutionGroups(contract.requirements).flatMap(group => {
|
|
30
|
+
const members = contract.requirements.filter(r => group.requirementIds.includes(r.id));
|
|
31
|
+
if (group.kind === "unclassified")
|
|
32
|
+
return members.map(r => renderRequirement(r));
|
|
33
|
+
return [`### ${group.id} (${group.kind})`, group.summary, ...members.map(r => renderRequirement(r, false)), "Full independent acceptance conditions remain in contracts/frontend-implementation-contract.json."];
|
|
27
34
|
});
|
|
28
35
|
return lines.length > 0 ? lines.join("\n") : "_(no requirements)_";
|
|
29
36
|
}
|
|
@@ -72,7 +79,7 @@ function sourceFidelityLedger(contract) {
|
|
|
72
79
|
`- ledger: ${binding.ledgerPath}`,
|
|
73
80
|
`- ledgerSha256: ${binding.ledgerSha256 ?? "(missing)"}`,
|
|
74
81
|
`- inputDigest: ${binding.inputDigest ?? "(missing)"}`,
|
|
75
|
-
"- 语义权威:原始 requirement/acceptance 文档与 requirement-ledger.json
|
|
82
|
+
"- 语义权威:原始 requirement/acceptance 文档与 requirement-ledger.json;本计划按执行组展示共享方案;独立验收全文保留在 canonical contract,执行输入按完整义务分批。",
|
|
76
83
|
];
|
|
77
84
|
const boundCount = Object.keys(binding.requirementToFragments ?? {}).length;
|
|
78
85
|
if (boundCount > 0) {
|
|
@@ -96,13 +96,13 @@ export async function runRecoveryOperationController(input) {
|
|
|
96
96
|
};
|
|
97
97
|
}
|
|
98
98
|
// Total budget fold + hard stop (resource protection, fail-closed).
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
ledgers,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
99
|
+
try {
|
|
100
|
+
const ledgers = await readLedgers();
|
|
101
|
+
if (deps.budget)
|
|
102
|
+
budgetAggregate = foldOperationBudgets({ ledgers, budget: deps.budget, now });
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
return { terminalReason: "needs-attention", attempts: attempt, budgetAggregate, fingerprints, detail: `Recovery budget evidence unavailable: ${error instanceof Error ? error.message : String(error)}` };
|
|
106
106
|
}
|
|
107
107
|
if (budgetAggregate?.status === "breached") {
|
|
108
108
|
return {
|
|
@@ -87,6 +87,7 @@ export function foldOperationBudgets(input) {
|
|
|
87
87
|
limits: { ...input.budget.limits },
|
|
88
88
|
consumed: {
|
|
89
89
|
executorCalls: 0,
|
|
90
|
+
...(input.budget.limits.maxProviderRequests !== undefined ? { providerRequests: 0 } : {}),
|
|
90
91
|
wallTimeMs: 0,
|
|
91
92
|
tokens: null,
|
|
92
93
|
repairPasses: 0,
|
|
@@ -100,6 +101,15 @@ export function foldOperationBudgets(input) {
|
|
|
100
101
|
let knownTokenSum = 0;
|
|
101
102
|
const missing = new Set();
|
|
102
103
|
for (const ledger of input.ledgers) {
|
|
104
|
+
if (input.budget.limits.maxProviderRequests !== undefined) {
|
|
105
|
+
if (ledger.consumed.providerRequests === undefined) {
|
|
106
|
+
if (input.budget.mode === "hard")
|
|
107
|
+
throw Error("FRONTEND_PROVIDER_BUDGET_INVALID: missing operation request consumption");
|
|
108
|
+
delete aggregate.consumed.providerRequests;
|
|
109
|
+
}
|
|
110
|
+
else if (aggregate.consumed.providerRequests !== undefined)
|
|
111
|
+
aggregate.consumed.providerRequests += ledger.consumed.providerRequests;
|
|
112
|
+
}
|
|
103
113
|
aggregate.consumed.executorCalls += ledger.consumed.executorCalls;
|
|
104
114
|
aggregate.consumed.wallTimeMs += ledger.consumed.wallTimeMs;
|
|
105
115
|
aggregate.consumed.repairPasses += ledger.consumed.repairPasses;
|
|
@@ -119,6 +129,9 @@ export function foldOperationBudgets(input) {
|
|
|
119
129
|
aggregate.consumed.missingTokenNodeIds = [...missing].sort((a, b) => a.localeCompare(b));
|
|
120
130
|
if (aggregate.mode === "hard") {
|
|
121
131
|
const limits = aggregate.limits;
|
|
132
|
+
pushAggregateBreach(aggregate, "providerRequests", aggregate.consumed.providerRequests ?? 0, limits.maxProviderRequests, now);
|
|
133
|
+
if (aggregate.breaches.length)
|
|
134
|
+
aggregate.status = "breached";
|
|
122
135
|
pushAggregateBreach(aggregate, "executorCalls", aggregate.consumed.executorCalls, limits.maxExecutorCalls, now);
|
|
123
136
|
pushAggregateBreach(aggregate, "wallTimeMs", aggregate.consumed.wallTimeMs, limits.maxWallTimeMs, now);
|
|
124
137
|
pushAggregateBreach(aggregate, "repairPasses", aggregate.consumed.repairPasses, limits.maxRepairPasses, now);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { copyFile, mkdir, readdir, readFile, rm, access } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { sha256OfCanonicalJson } from "../../task/contract/hash.js";
|
|
3
4
|
import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
4
5
|
import { sha256Hex } from "./frontend-implementation-contract.js";
|
|
5
6
|
import { FRONTEND_RECOVERY_IMPORT_MANIFEST_REL_PATH, FRONTEND_RECOVERY_INTENT_REL_DIR, FRONTEND_RECOVERY_STAGING_PREFIX, computeFrontendRecoveryPlan, computeFrontendRecoveryPlanForSource, } from "./frontend-recovery-plan.js";
|
|
@@ -413,6 +414,8 @@ async function materializeChildStaging(input) {
|
|
|
413
414
|
}
|
|
414
415
|
}
|
|
415
416
|
const childState = {
|
|
417
|
+
...(parentState.budget ? { budget: structuredClone(parentState.budget) } : {}),
|
|
418
|
+
...(parentState.budgetLedger ? { budgetLedger: structuredClone(parentState.budgetLedger) } : {}),
|
|
416
419
|
version: 1,
|
|
417
420
|
title: parentSpec.title,
|
|
418
421
|
runId: childRunId,
|
|
@@ -434,6 +437,7 @@ async function materializeChildStaging(input) {
|
|
|
434
437
|
}
|
|
435
438
|
: {}),
|
|
436
439
|
frontendRecoveryState: {
|
|
440
|
+
...(parentState.budgetLedger ? { budgetLineage: { parentLedgerSha256: sha256OfCanonicalJson(parentState.budgetLedger), inheritedAt: createdAt, inheritedConsumed: structuredClone(parentState.budgetLedger.consumed) } } : {}),
|
|
437
441
|
schemaVersion: 1,
|
|
438
442
|
phase: "child-running",
|
|
439
443
|
requestId,
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { readFile, realpath, lstat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { sha256OfCanonicalJson } from "../../task/contract/hash.js";
|
|
5
|
+
import { readCommittedEvents } from "./frontend-typed-event-store.js";
|
|
6
|
+
import { stageTypedEventFact, adoptTypedEventFact } from "./frontend-typed-event-transaction.js";
|
|
7
|
+
const hash = (text) => createHash("sha256").update(text).digest("hex");
|
|
8
|
+
/** The section index is navigation; reconstruct coverage against the canonical source before using it. */
|
|
9
|
+
export async function loadFrontendReviewScopes(runDir, phase, targetBytes) {
|
|
10
|
+
const root = await realpath(runDir);
|
|
11
|
+
const files = new Map();
|
|
12
|
+
async function read(relative, expected) {
|
|
13
|
+
const absolute = path.resolve(root, relative);
|
|
14
|
+
if (!absolute.startsWith(root + path.sep) || (await lstat(absolute)).isSymbolicLink() || !(await realpath(absolute)).startsWith(root + path.sep))
|
|
15
|
+
throw Error("FRONTEND_REVIEW_SCOPE_INVALID: artifact escaped run");
|
|
16
|
+
const text = await readFile(absolute, "utf8");
|
|
17
|
+
const sha256 = hash(text);
|
|
18
|
+
if (expected && sha256 !== expected)
|
|
19
|
+
throw Error(`FRONTEND_REVIEW_SCOPE_DRIFT: ${relative}`);
|
|
20
|
+
files.set(relative, sha256);
|
|
21
|
+
return { id: relative, text, sha256 };
|
|
22
|
+
}
|
|
23
|
+
let indexArtifact;
|
|
24
|
+
try {
|
|
25
|
+
indexArtifact = await read("contracts/frontend-implementation-contract.index.json");
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error.code === "ENOENT")
|
|
29
|
+
return undefined;
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
const index = JSON.parse(indexArtifact.text);
|
|
33
|
+
const canonicalArtifact = await read(index.contract.path, index.contract.sha256);
|
|
34
|
+
const canonical = JSON.parse(canonicalArtifact.text);
|
|
35
|
+
const covered = new Map();
|
|
36
|
+
const sections = [];
|
|
37
|
+
const ids = new Set();
|
|
38
|
+
for (const entry of index.sections) {
|
|
39
|
+
if (ids.has(entry.path))
|
|
40
|
+
throw Error("FRONTEND_REVIEW_SCOPE_INVALID: duplicate section identity");
|
|
41
|
+
ids.add(entry.path);
|
|
42
|
+
const section = await read(entry.path, entry.sha256);
|
|
43
|
+
const payload = JSON.parse(section.text);
|
|
44
|
+
if (payload.contractSha256 !== canonicalArtifact.sha256 || sha256OfCanonicalJson(payload.slices) !== sha256OfCanonicalJson(entry.slices))
|
|
45
|
+
throw Error("FRONTEND_REVIEW_SCOPE_INVALID: unbound section");
|
|
46
|
+
for (const slice of payload.slices) {
|
|
47
|
+
const value = canonical[slice.field];
|
|
48
|
+
const range = Array.isArray(value) ? value.slice(slice.start, slice.end) : value;
|
|
49
|
+
if (sha256OfCanonicalJson(range) !== sha256OfCanonicalJson(payload.data[slice.field]))
|
|
50
|
+
throw Error("FRONTEND_REVIEW_SCOPE_INVALID: section differs from canonical obligation");
|
|
51
|
+
const positions = covered.get(slice.field) ?? new Set();
|
|
52
|
+
if (Array.isArray(value)) {
|
|
53
|
+
if (!Number.isInteger(slice.start) || !Number.isInteger(slice.end) || slice.start < 0 || slice.end < slice.start || slice.end > value.length || slice.total !== value.length)
|
|
54
|
+
throw Error("FRONTEND_REVIEW_SCOPE_INVALID: invalid array coverage");
|
|
55
|
+
for (let i = slice.start; i < slice.end; i++) {
|
|
56
|
+
if (positions.has(i))
|
|
57
|
+
throw Error("FRONTEND_REVIEW_SCOPE_INVALID: overlapping coverage");
|
|
58
|
+
positions.add(i);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
if (positions.size)
|
|
63
|
+
throw Error("FRONTEND_REVIEW_SCOPE_INVALID: duplicate field");
|
|
64
|
+
positions.add(0);
|
|
65
|
+
}
|
|
66
|
+
covered.set(slice.field, positions);
|
|
67
|
+
}
|
|
68
|
+
sections.push(section);
|
|
69
|
+
}
|
|
70
|
+
if (Object.keys(canonical).some(field => !covered.has(field) || covered.get(field).size !== (Array.isArray(canonical[field]) ? canonical[field].length : 1)))
|
|
71
|
+
throw Error("FRONTEND_REVIEW_SCOPE_INVALID: canonical field coverage incomplete");
|
|
72
|
+
const scopes = Buffer.byteLength(canonicalArtifact.text) <= targetBytes ? [canonicalArtifact] : sections;
|
|
73
|
+
if (phase === "review") {
|
|
74
|
+
const context = await read("contracts/frontend-review-context.json");
|
|
75
|
+
const payload = JSON.parse(context.text);
|
|
76
|
+
if (payload.contractRef.sha256 !== canonicalArtifact.sha256 || payload.contractRef.indexSha256 !== indexArtifact.sha256)
|
|
77
|
+
throw Error("FRONTEND_REVIEW_SCOPE_DRIFT: review context contract binding");
|
|
78
|
+
const diff = await read(payload.diff.reviewIndexPath);
|
|
79
|
+
const diffIndex = JSON.parse(diff.text);
|
|
80
|
+
await read(payload.diff.patchPath, payload.diff.patchSha256);
|
|
81
|
+
if (diffIndex.patchSha256 !== payload.diff.patchSha256)
|
|
82
|
+
throw Error("FRONTEND_REVIEW_SCOPE_DRIFT: review diff binding");
|
|
83
|
+
scopes.push(context);
|
|
84
|
+
for (const hunk of diffIndex.hunks)
|
|
85
|
+
scopes.push(await read(hunk.patchPath, hunk.sha256));
|
|
86
|
+
}
|
|
87
|
+
return { scopes, digest: sha256OfCanonicalJson([...files]), validate: async () => { for (const [relative, expected] of files)
|
|
88
|
+
await read(relative, expected); } };
|
|
89
|
+
}
|
|
90
|
+
/** Checkpoints attest explicit model completion of already supplied, hash-bound scopes. */
|
|
91
|
+
export function createFrontendReviewScopeProtocol(input) {
|
|
92
|
+
const kind = input.phase === "design" ? "design-scope-completed" : "review-scope-completed";
|
|
93
|
+
let active = new Set();
|
|
94
|
+
const completedScopeIds = () => new Set(readCommittedEvents(input.getStore(), input.attemptId).filter(r => r.fact.kind === kind).map(r => String(r.fact.id)));
|
|
95
|
+
const receipt = (details) => ({ content: [{ type: "text", text: JSON.stringify(details) }], details });
|
|
96
|
+
return {
|
|
97
|
+
setActiveScope(ids) { active = new Set(ids); },
|
|
98
|
+
completedScopeIds,
|
|
99
|
+
committedFacts: () => readCommittedEvents(input.getStore(), input.attemptId),
|
|
100
|
+
assertComplete() { const missing = input.inventory?.scopes.filter(s => !completedScopeIds().has(s.id)) ?? []; if (missing.length)
|
|
101
|
+
throw Object.assign(Error(`FRONTEND_REVIEW_SCOPE_INCOMPLETE: ${missing.map(s => s.id).join(", ")}`), { code: "FRONTEND_REVIEW_SCOPE_INCOMPLETE" }); },
|
|
102
|
+
customTools: !input.inventory ? [] : [{ name: "complete_review_scope", label: "complete_review_scope", description: "After reviewing the supplied complete scope and saving its findings, checkpoint its exact runtime id. This does not approve the implementation or design.", parameters: { type: "object", properties: { id: { type: "string", minLength: 1 } }, required: ["id"], additionalProperties: false },
|
|
103
|
+
async execute(callId, params) {
|
|
104
|
+
const scope = input.inventory.scopes.find(s => s.id === params.id);
|
|
105
|
+
if (!scope || !active.has(params.id))
|
|
106
|
+
return receipt({ ok: false, code: "FRONTEND_INPUT_SCOPE_VIOLATION", error: "Only the supplied active review scope may be completed" });
|
|
107
|
+
await input.inventory.validate();
|
|
108
|
+
const store = input.getStore();
|
|
109
|
+
const requestId = `${input.attemptId}:${callId}:${randomUUID()}`;
|
|
110
|
+
const fact = { kind, id: scope.id, sha256: scope.sha256 };
|
|
111
|
+
const staged = stageTypedEventFact({ store, requestId, attemptId: input.attemptId, fact });
|
|
112
|
+
const committed = await adoptTypedEventFact({ store, requestId, attemptId: input.attemptId, fact, eventId: staged.eventId, expectedRevision: store.revision });
|
|
113
|
+
return receipt({ ok: true, kind, revision: committed.revision });
|
|
114
|
+
},
|
|
115
|
+
}],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { readFrontendToolProgress } from "./frontend-durable-tools.js";
|
|
5
|
+
import { frontendModelCapabilities } from "../../shared/frontend-execution-policy.js";
|
|
6
|
+
/** Count returned text only after a successful native read. No filesystem reads or raw content retention. */
|
|
7
|
+
export function createFrontendReadObservation() {
|
|
8
|
+
const pending = new Map();
|
|
9
|
+
const finished = new Set();
|
|
10
|
+
const seen = new Set();
|
|
11
|
+
let bytes = 0, duplicates = 0, complete = 0, rejected = 0, unknown = false;
|
|
12
|
+
return {
|
|
13
|
+
observe(raw) {
|
|
14
|
+
const event = record(raw);
|
|
15
|
+
const id = event.toolCallId;
|
|
16
|
+
if (typeof id !== "string")
|
|
17
|
+
return;
|
|
18
|
+
if (event.type === "tool_execution_start" && event.toolName === "read")
|
|
19
|
+
pending.set(id, record(event.args ?? event.input));
|
|
20
|
+
if (event.type !== "tool_execution_end" || !pending.has(id) || finished.has(id))
|
|
21
|
+
return;
|
|
22
|
+
const args = pending.get(id);
|
|
23
|
+
pending.delete(id);
|
|
24
|
+
finished.add(id);
|
|
25
|
+
const result = record(event.result);
|
|
26
|
+
if (event.isError === true || result.isError === true || result.ok === false || result.error || record(result.details).ok === false) {
|
|
27
|
+
rejected++;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const content = result.content;
|
|
31
|
+
if (!Array.isArray(content) || content.some(item => record(item).type !== "text" || typeof record(item).text !== "string") || typeof args.path !== "string") {
|
|
32
|
+
unknown = true;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const text = content.map(item => record(item).text).join("\n");
|
|
36
|
+
const size = Buffer.byteLength(text);
|
|
37
|
+
const key = createHash("sha256").update(JSON.stringify([args.path.replaceAll("\\", "/").replace(/^\.\//, ""), args.offset ?? null, args.limit ?? null, text])).digest("hex");
|
|
38
|
+
bytes += size;
|
|
39
|
+
complete++;
|
|
40
|
+
if (seen.has(key))
|
|
41
|
+
duplicates += size;
|
|
42
|
+
seen.add(key);
|
|
43
|
+
},
|
|
44
|
+
snapshot() { return { successfulReadBytes: unknown ? null : bytes, duplicateReadBytes: unknown ? null : duplicates, completedReads: complete, rejectedReads: rejected, incompleteReads: pending.size }; },
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function classifyFrontendSessionFailure(result) {
|
|
48
|
+
const errorCode = /\b(?:FRONTEND_[A-Z_]+|TOOL_SCHEMA_INVALID|FACT_IDENTITY_CONFLICT|REQUEST_ID_REUSE_CONFLICT|TERMINAL_CONFLICT|[A-Z_]+_INCOMPLETE|[A-Z_]+_EXHAUSTED)\b/.exec(result.stderr)?.[0] ?? null;
|
|
49
|
+
const category = result.failureCategory;
|
|
50
|
+
const failureLayer = result.stopReason === "length" || /context|budget-exhausted/.test(category) ? "capacity"
|
|
51
|
+
: result.ok ? "none"
|
|
52
|
+
: errorCode?.includes("LEDGER") || errorCode === "FRONTEND_COMMIT_FAILED" ? "persistence"
|
|
53
|
+
: errorCode === "TOOL_SCHEMA_INVALID" ? "schema"
|
|
54
|
+
: /binding|source|permission|write-guard|tool-policy/.test(category) ? "governance"
|
|
55
|
+
: /timeout|rate-limit|provider|transport|network|auth/.test(category) ? "provider"
|
|
56
|
+
: /invalid-output|structured-output/.test(category) ? "protocol-or-coverage"
|
|
57
|
+
: "unknown";
|
|
58
|
+
return { failureLayer, errorCode };
|
|
59
|
+
}
|
|
60
|
+
const fields = ["inputTokens", "outputTokens", "reasoningTokens", "cacheReadTokens", "cacheWriteTokens", "totalTokens", "peakRequestContextTokens"];
|
|
61
|
+
const record = (v) => v !== null && typeof v === "object" ? v : {};
|
|
62
|
+
const number = (r, ...keys) => {
|
|
63
|
+
for (const key of keys)
|
|
64
|
+
if (typeof r[key] === "number" && Number.isFinite(r[key]) && r[key] >= 0)
|
|
65
|
+
return r[key];
|
|
66
|
+
return null;
|
|
67
|
+
};
|
|
68
|
+
const sumKnown = (values) => values.length && values.every(v => v !== null) ? values.reduce((s, v) => s + v, 0) : null;
|
|
69
|
+
/** Observation only. Never feed this projection into the SDK execution guard. */
|
|
70
|
+
export function collectFrontendUsage(events) {
|
|
71
|
+
const samples = new Map();
|
|
72
|
+
const aliases = new Map();
|
|
73
|
+
let anonymous = false;
|
|
74
|
+
let observed = false;
|
|
75
|
+
for (const raw of events) {
|
|
76
|
+
const event = record(raw);
|
|
77
|
+
const message = record(event.message);
|
|
78
|
+
if (typeof message.role === "string" && message.role !== "assistant")
|
|
79
|
+
continue;
|
|
80
|
+
const usage = record(event.usage ?? message.usage ?? event.tokenUsage);
|
|
81
|
+
if (!Object.keys(usage).length)
|
|
82
|
+
continue;
|
|
83
|
+
observed = true;
|
|
84
|
+
const ids = [event.responseId, event.messageId, message.responseId, message.id].filter((v) => typeof v === "string" && v.length > 0);
|
|
85
|
+
const roots = [...new Set(ids.map(id => aliases.get(id) ?? id))];
|
|
86
|
+
const key = roots[0];
|
|
87
|
+
if (!key) {
|
|
88
|
+
anonymous = true;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const input = number(usage, "input", "input_tokens", "inputTokens", "prompt_tokens", "promptTokens");
|
|
92
|
+
const output = number(usage, "output", "output_tokens", "outputTokens", "completion_tokens", "completionTokens");
|
|
93
|
+
const cacheRead = number(usage, "cacheRead", "cache_read_input_tokens");
|
|
94
|
+
const cacheWrite = number(usage, "cacheWrite", "cache_creation_input_tokens");
|
|
95
|
+
// Pi's native input excludes cache tokens; OpenAI-compatible input includes them.
|
|
96
|
+
const context = input === null ? null : "input" in usage ? sumKnown([input, cacheRead, cacheWrite]) : input;
|
|
97
|
+
const sample = {
|
|
98
|
+
inputTokens: input, outputTokens: output,
|
|
99
|
+
reasoningTokens: number(usage, "reasoning", "reasoning_tokens", "reasoningTokens") ?? number(record(usage.completion_tokens_details ?? usage.output_tokens_details), "reasoning_tokens"),
|
|
100
|
+
cacheReadTokens: cacheRead, cacheWriteTokens: cacheWrite,
|
|
101
|
+
totalTokens: number(usage, "total_tokens", "totalTokens") ?? sumKnown([context, output]),
|
|
102
|
+
peakRequestContextTokens: context,
|
|
103
|
+
};
|
|
104
|
+
for (const root of roots) {
|
|
105
|
+
const previous = samples.get(root);
|
|
106
|
+
if (previous)
|
|
107
|
+
for (const field of fields)
|
|
108
|
+
sample[field] = previous[field] === null ? sample[field] : sample[field] === null ? previous[field] : Math.max(previous[field], sample[field]);
|
|
109
|
+
samples.delete(root);
|
|
110
|
+
}
|
|
111
|
+
for (const [alias, root] of aliases)
|
|
112
|
+
if (roots.includes(root))
|
|
113
|
+
aliases.set(alias, key);
|
|
114
|
+
for (const id of ids)
|
|
115
|
+
aliases.set(id, key);
|
|
116
|
+
samples.set(key, sample);
|
|
117
|
+
}
|
|
118
|
+
const result = Object.fromEntries(fields.map(field => [field, anonymous ? null : field === "peakRequestContextTokens" ? (samples.size && [...samples.values()].every(s => s[field] !== null) ? Math.max(...[...samples.values()].map(s => s[field])) : null) : sumKnown([...samples.values()].map(s => s[field]))]));
|
|
119
|
+
return { ...result, requestCount: anonymous || !observed ? null : samples.size,
|
|
120
|
+
completeness: !observed ? "unknown" : !anonymous && result.inputTokens !== null && result.outputTokens !== null && result.totalTokens !== null ? "complete" : "partial" };
|
|
121
|
+
}
|
|
122
|
+
function aggregateAttempts(attempts) {
|
|
123
|
+
const usages = attempts.map(a => a.actual ?? collectFrontendUsage([]));
|
|
124
|
+
const totals = Object.fromEntries(fields.map(field => [field, field === "peakRequestContextTokens"
|
|
125
|
+
? usages.every(u => u[field] !== null) ? Math.max(...usages.map(u => u[field])) : null
|
|
126
|
+
: sumKnown(usages.map(u => u[field]))]));
|
|
127
|
+
return { ...totals, requestCount: sumKnown(usages.map(u => u.requestCount)), completeness: usages.every(u => u.completeness === "complete") ? "complete" : usages.every(u => u.completeness === "unknown") ? "unknown" : "partial" };
|
|
128
|
+
}
|
|
129
|
+
/** Captures every transport attempt, including an SDK failure before CLI fallback. */
|
|
130
|
+
export async function observeFrontendAttempt(input, execute) {
|
|
131
|
+
const event = { id: randomUUID(), backend: input.backend, model: input.model, startedAt: new Date().toISOString(), completion: "incomplete" };
|
|
132
|
+
const notify = () => { try {
|
|
133
|
+
input.observer?.(structuredClone(event));
|
|
134
|
+
}
|
|
135
|
+
catch { /* observation cannot change execution */ } };
|
|
136
|
+
notify();
|
|
137
|
+
const result = await execute();
|
|
138
|
+
event.completion = "complete";
|
|
139
|
+
event.actual = result.usageObservation ?? collectFrontendUsage([]);
|
|
140
|
+
event.reads = result.readObservation;
|
|
141
|
+
event.capabilities = result.capabilitiesObservation;
|
|
142
|
+
event.requestEnvelope = result.requestEnvelopeObservation;
|
|
143
|
+
event.terminal = { ok: result.ok, failureCategory: result.failureCategory, stopReason: result.stopReason ?? null, durationMs: result.durationMs };
|
|
144
|
+
notify();
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
const measure = (text) => ({ sha256: createHash("sha256").update(text).digest("hex"), chars: text.length, bytes: Buffer.byteLength(text), estimatedTokens: Math.ceil(Buffer.byteLength(text) / 3) });
|
|
148
|
+
function safeObservation(read, fallback) { try {
|
|
149
|
+
return read();
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return fallback;
|
|
153
|
+
} }
|
|
154
|
+
/** One logical dispatch and its real transport attempts; no raw prompt or tool output is stored. */
|
|
155
|
+
export async function observeFrontendSession(input, execute) {
|
|
156
|
+
const durableBefore = safeObservation(() => input.durableCommittedCount?.(), undefined);
|
|
157
|
+
const before = safeObservation(() => input.committedCount?.(), undefined);
|
|
158
|
+
const toolsBefore = readFrontendToolProgress(input.customTools);
|
|
159
|
+
const receipt = {
|
|
160
|
+
schemaVersion: 1, policyVersion: "frontend-session-observation-v1",
|
|
161
|
+
identity: { sessionId: randomUUID(), phase: input.phase, scopeIds: input.scopeIds ?? [], taskId: input.taskId ?? null, runId: input.runId ?? null, nodeId: input.nodeId ?? null, attempt: input.attempt ?? null, sourceDigest: input.sourceDigest ?? null, contractDigest: input.contractDigest ?? null },
|
|
162
|
+
startedAt: new Date().toISOString(), finishedAt: null, completion: "incomplete",
|
|
163
|
+
planned: { prompt: measure(input.prompt), userMessage: measure(input.userMessage ?? ""), toolSchemas: input.customTools ? safeObservation(() => measure(JSON.stringify(input.customTools.map(t => { const tool = record(t); return { name: tool.name, description: tool.description, parameters: tool.parameters }; }))), null) : null,
|
|
164
|
+
estimatorVersion: "utf8-bytes-div3-v1-estimate", capabilities: { ...frontendModelCapabilities(undefined), model: input.model ?? null }, unmeasuredComponents: ["sdk-system-overhead", "built-in-tools", "history", "tool-results", "attachments"] },
|
|
165
|
+
actual: collectFrontendUsage([]), attempts: [],
|
|
166
|
+
progress: { memoryCommittedDelta: null, durableCommittedDelta: null, failedRecords: null, duplicateRecords: null, durableSubmissions: null, durableReplacements: null, successfulReadBytes: null, duplicateReadBytes: null }, terminal: null,
|
|
167
|
+
};
|
|
168
|
+
let writes = Promise.resolve();
|
|
169
|
+
let abandoned = false;
|
|
170
|
+
const writeObservation = async (file, snapshot) => {
|
|
171
|
+
// Never recreate a run that the runner has already archived.
|
|
172
|
+
await mkdir(path.dirname(file)).catch(error => { if (error.code !== "EEXIST")
|
|
173
|
+
throw error; });
|
|
174
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
175
|
+
try {
|
|
176
|
+
await writeFile(temporary, JSON.stringify(snapshot), { flag: "wx" });
|
|
177
|
+
await rename(temporary, file);
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
await rm(temporary, { force: true }).catch(() => { });
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
const save = () => {
|
|
184
|
+
const snapshot = structuredClone(receipt);
|
|
185
|
+
writes = writes.then(async () => {
|
|
186
|
+
if (abandoned)
|
|
187
|
+
return;
|
|
188
|
+
if (input.sink)
|
|
189
|
+
await input.sink(snapshot);
|
|
190
|
+
else if (input.artifactPath)
|
|
191
|
+
await writeObservation(input.artifactPath, snapshot);
|
|
192
|
+
}).catch(() => { });
|
|
193
|
+
return writes;
|
|
194
|
+
};
|
|
195
|
+
const waitForWrite = async (pending) => {
|
|
196
|
+
let timer;
|
|
197
|
+
try {
|
|
198
|
+
await Promise.race([pending, new Promise(resolve => { timer = setTimeout(() => { abandoned = true; resolve(); }, 100); })]);
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
if (timer)
|
|
202
|
+
clearTimeout(timer);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
await waitForWrite(save());
|
|
206
|
+
const result = await execute(event => {
|
|
207
|
+
const index = receipt.attempts.findIndex(a => a.id === event.id);
|
|
208
|
+
if (index < 0)
|
|
209
|
+
receipt.attempts.push(event);
|
|
210
|
+
else
|
|
211
|
+
receipt.attempts[index] = event;
|
|
212
|
+
void save();
|
|
213
|
+
});
|
|
214
|
+
receipt.finishedAt = new Date().toISOString();
|
|
215
|
+
receipt.completion = "complete";
|
|
216
|
+
receipt.actual = receipt.attempts.length ? aggregateAttempts(receipt.attempts) : result.usageObservation ?? collectFrontendUsage([]);
|
|
217
|
+
receipt.planned.capabilities = receipt.attempts[0]?.capabilities ?? result.capabilitiesObservation ?? receipt.planned.capabilities;
|
|
218
|
+
receipt.progress.memoryCommittedDelta = before !== undefined && input.committedCount ? safeObservation(() => Math.max(0, input.committedCount() - before), null) : null;
|
|
219
|
+
const durableAfter = safeObservation(() => input.durableCommittedCount?.(), undefined);
|
|
220
|
+
receipt.progress.durableCommittedDelta = durableBefore !== undefined && durableAfter !== undefined ? Math.max(0, durableAfter - durableBefore) : null;
|
|
221
|
+
const toolsAfter = readFrontendToolProgress(input.customTools);
|
|
222
|
+
if (toolsBefore && toolsAfter)
|
|
223
|
+
for (const key of ["failedRecords", "duplicateRecords", "durableSubmissions", "durableReplacements"])
|
|
224
|
+
receipt.progress[key] = Math.max(0, toolsAfter[key] - toolsBefore[key]);
|
|
225
|
+
const reads = receipt.attempts.length ? receipt.attempts.map(a => a.reads) : [result.readObservation];
|
|
226
|
+
for (const key of ["successfulReadBytes", "duplicateReadBytes"])
|
|
227
|
+
receipt.progress[key] = sumKnown(reads.map(r => r?.[key] ?? null));
|
|
228
|
+
receipt.terminal = { ok: result.ok, failureCategory: result.failureCategory, stopReason: result.stopReason ?? null, durationMs: Date.parse(receipt.finishedAt) - Date.parse(receipt.startedAt), ...classifyFrontendSessionFailure(result) };
|
|
229
|
+
await waitForWrite(save());
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
export function summarizeFrontendSessions(receipts) {
|
|
233
|
+
const byId = new Map();
|
|
234
|
+
for (const receipt of receipts) {
|
|
235
|
+
const previous = byId.get(receipt.identity.sessionId);
|
|
236
|
+
const rank = (r) => [r.completion === "complete" ? 1 : 0, Date.parse(r.finishedAt ?? r.startedAt), r.attempts.filter(a => a.completion === "complete").length, r.attempts.length];
|
|
237
|
+
const candidateRank = rank(receipt);
|
|
238
|
+
const previousRank = previous ? rank(previous) : [];
|
|
239
|
+
const difference = candidateRank.map((v, i) => v - (previousRank[i] ?? 0)).find(v => v !== 0) ?? 0;
|
|
240
|
+
if (!previous || difference > 0)
|
|
241
|
+
byId.set(receipt.identity.sessionId, receipt);
|
|
242
|
+
}
|
|
243
|
+
const unique = [...byId.values()];
|
|
244
|
+
return { sessions: unique.length, failedSessions: unique.filter(r => r.terminal?.ok === false).length, incompleteSessions: unique.filter(r => r.completion === "incomplete").length,
|
|
245
|
+
failureLayers: Object.fromEntries([...new Set(unique.flatMap(r => r.terminal && r.terminal.failureLayer !== "none" ? [r.terminal.failureLayer] : []))].sort().map(layer => [layer, unique.filter(r => r.terminal?.failureLayer === layer).length])),
|
|
246
|
+
progress: Object.fromEntries(["durableCommittedDelta", "durableSubmissions", "durableReplacements", "failedRecords", "duplicateRecords", "successfulReadBytes", "duplicateReadBytes"].map(key => [key, sumKnown(unique.map(r => r.progress[key]))])),
|
|
247
|
+
totalTokens: sumKnown(unique.flatMap(r => r.attempts.length ? r.attempts.map(a => a.actual?.totalTokens ?? null) : [r.actual.totalTokens])),
|
|
248
|
+
sessionDurationSumMs: sumKnown(unique.map(r => r.terminal?.durationMs ?? null)) };
|
|
249
|
+
}
|
|
@@ -236,6 +236,7 @@ export const PLAN_LEDGER_FACT_KINDS = [
|
|
|
236
236
|
"plan-requirement",
|
|
237
237
|
"plan-verification-target",
|
|
238
238
|
"plan-evidence-gap",
|
|
239
|
+
"mock-endpoint",
|
|
239
240
|
];
|
|
240
241
|
function skeletonTargetFiles(skeleton) {
|
|
241
242
|
if (!isRecord(skeleton))
|
|
@@ -386,8 +387,25 @@ export function assemblePlanPatchFromCommittedFacts(records, contractRequirement
|
|
|
386
387
|
patch.interactions = [...interactionByName.values(), ...unnamedInteractions];
|
|
387
388
|
}
|
|
388
389
|
const mockApi = lastByKind("mock-api");
|
|
390
|
+
if (!mockApi && facts.some(f => f.kind === "mock-endpoint" && f.removed !== true))
|
|
391
|
+
throw Error("MOCK_POLICY_MISSING: endpoint has no policy");
|
|
389
392
|
if (mockApi && isRecord(mockApi.mockApi)) {
|
|
390
|
-
|
|
393
|
+
const endpoints = new Map();
|
|
394
|
+
for (const endpoint of Array.isArray(mockApi.mockApi.endpoints) ? mockApi.mockApi.endpoints : []) {
|
|
395
|
+
if (isRecord(endpoint))
|
|
396
|
+
endpoints.set(`${endpoint.method}:${endpoint.path}`, endpoint);
|
|
397
|
+
}
|
|
398
|
+
for (const fact of facts)
|
|
399
|
+
if (fact.kind === "mock-endpoint" && isRecord(fact.endpoint)) {
|
|
400
|
+
const key = `${fact.endpoint.method}:${fact.endpoint.path}`;
|
|
401
|
+
if (fact.removed === true)
|
|
402
|
+
endpoints.delete(key);
|
|
403
|
+
else
|
|
404
|
+
endpoints.set(key, fact.endpoint);
|
|
405
|
+
}
|
|
406
|
+
if (mockApi.mockApi.strategy === "not-needed" && endpoints.size)
|
|
407
|
+
throw Error("MOCK_POLICY_CONFLICT: withdraw endpoints before declaring Mock/API not needed");
|
|
408
|
+
patch.mockApi = { ...mockApi.mockApi, endpoints: [...endpoints.values()] };
|
|
391
409
|
}
|
|
392
410
|
const deviation = lastByKind("design-deviation");
|
|
393
411
|
if (deviation) {
|
|
@@ -433,7 +451,7 @@ export function assemblePlanPatchFromCommittedFacts(records, contractRequirement
|
|
|
433
451
|
const inherited = contractRequirements.get(id);
|
|
434
452
|
if (!inherited)
|
|
435
453
|
return entry;
|
|
436
|
-
const merged = { ...entry };
|
|
454
|
+
const merged = { ...entry, ...(inherited.text ? { expectedOutcome: inherited.text } : {}), ...(inherited.execution ? { execution: inherited.execution } : {}) };
|
|
437
455
|
// Inherit the contract-declared provenance only when the plan did
|
|
438
456
|
// not already declare it (model override wins; missing inherits).
|
|
439
457
|
if (!Array.isArray(entry.sourceFragmentIds)) {
|
|
@@ -39,9 +39,10 @@ const resultSchema = z.object({
|
|
|
39
39
|
})),
|
|
40
40
|
})),
|
|
41
41
|
});
|
|
42
|
-
|
|
42
|
+
/** Resolve runner paths against its cwd, then use the contract workspace coordinates. */
|
|
43
|
+
export function parseFrontendTestExecutionReport(raw, cwd, workspaceRoot = cwd) {
|
|
43
44
|
return resultSchema.parse(raw).testResults.flatMap((suite) => suite.assertionResults.map((test) => ({
|
|
44
|
-
file: path.relative(
|
|
45
|
+
file: path.relative(workspaceRoot, path.resolve(cwd, suite.name)).replace(/\\/g, "/"),
|
|
45
46
|
title: test.fullName ?? [...(test.ancestorTitles ?? []), test.title].join(" "),
|
|
46
47
|
status: test.status,
|
|
47
48
|
})));
|
|
@@ -128,6 +128,7 @@ export const PLAN_RECORD_FACT_KINDS = [
|
|
|
128
128
|
"plan-evidence-gap",
|
|
129
129
|
"state-registry",
|
|
130
130
|
];
|
|
131
|
+
export const INCREMENTAL_DETAIL_FACT_KINDS = ["mock-endpoint", "review-finding", "design-finding", "contract-scope-completed", "plan-group-coverage", "scout-scope", "review-scope-completed", "design-scope-completed"];
|
|
131
132
|
/**
|
|
132
133
|
* A+B (AC-009): typed issue category shared by review and design change
|
|
133
134
|
* requests. The five-value enum replaces free-form issueCategory strings.
|
|
@@ -159,6 +160,7 @@ const ALL_TYPED_EVENT_FACT_KIND_VALUES = [
|
|
|
159
160
|
...CONTRACT_FACT_KINDS,
|
|
160
161
|
...PLAN_TERMINAL_FACT_KINDS,
|
|
161
162
|
...PLAN_RECORD_FACT_KINDS,
|
|
163
|
+
...INCREMENTAL_DETAIL_FACT_KINDS,
|
|
162
164
|
]),
|
|
163
165
|
];
|
|
164
166
|
export const typedEventFactKindSchema = z.enum(ALL_TYPED_EVENT_FACT_KIND_VALUES);
|
|
@@ -261,6 +263,15 @@ export const typedEventRecordSchema = z
|
|
|
261
263
|
revision: z.number().int().nonnegative(),
|
|
262
264
|
recordedAt: z.string().min(1),
|
|
263
265
|
quarantineReason: z.string().min(1).optional(),
|
|
266
|
+
submission: z.object({
|
|
267
|
+
version: z.literal(1),
|
|
268
|
+
bindingSha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
269
|
+
tool: z.string(),
|
|
270
|
+
callId: z.string(),
|
|
271
|
+
identity: z.string(),
|
|
272
|
+
paramsSha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
273
|
+
receipt: z.record(z.unknown()).optional(),
|
|
274
|
+
}).strict().optional(),
|
|
264
275
|
})
|
|
265
276
|
.strict()
|
|
266
277
|
.superRefine((value, ctx) => {
|