@markstanden/opencode-test-container 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +263 -2
- package/container/Containerfile +24 -0
- package/container/README.md +29 -0
- package/dist/adapters/opencode/events.d.ts +90 -0
- package/dist/adapters/opencode/events.js +213 -0
- package/dist/adapters/opencode/runtime.d.ts +87 -0
- package/dist/adapters/opencode/runtime.js +136 -0
- package/dist/adapters/podman/container.d.ts +62 -0
- package/dist/adapters/podman/container.js +119 -0
- package/dist/adapters/podman/image.d.ts +50 -0
- package/dist/adapters/podman/image.js +124 -0
- package/dist/adapters/process.d.ts +38 -0
- package/dist/adapters/process.js +71 -0
- package/dist/campaign/assess.d.ts +45 -0
- package/dist/campaign/assess.js +208 -0
- package/dist/campaign/close.d.ts +17 -0
- package/dist/campaign/close.js +112 -0
- package/dist/campaign/evidence-guard.d.ts +24 -0
- package/dist/campaign/evidence-guard.js +49 -0
- package/dist/campaign/freeze.d.ts +94 -0
- package/dist/campaign/freeze.js +297 -0
- package/dist/campaign/inventory.d.ts +62 -0
- package/dist/campaign/inventory.js +192 -0
- package/dist/campaign/judge.d.ts +209 -0
- package/dist/campaign/judge.js +1285 -0
- package/dist/campaign/ledger.d.ts +64 -0
- package/dist/campaign/ledger.js +149 -0
- package/dist/campaign/prepare.d.ts +129 -0
- package/dist/campaign/prepare.js +483 -0
- package/dist/campaign/primary.d.ts +96 -0
- package/dist/campaign/primary.js +189 -0
- package/dist/campaign/run.d.ts +151 -0
- package/dist/campaign/run.js +562 -0
- package/dist/campaign/status.d.ts +76 -0
- package/dist/campaign/status.js +237 -0
- package/dist/cli/otc.d.ts +51 -0
- package/dist/cli/otc.js +435 -0
- package/dist/contracts/canonical.d.ts +21 -0
- package/dist/contracts/canonical.js +44 -0
- package/dist/contracts/types.d.ts +261 -0
- package/dist/contracts/types.js +9 -0
- package/dist/contracts/validate.d.ts +72 -0
- package/dist/contracts/validate.js +1021 -0
- package/dist/contracts/version.d.ts +44 -0
- package/dist/contracts/version.js +147 -0
- package/dist/evaluators/docs/analysis/accounting.d.ts +140 -0
- package/dist/evaluators/docs/analysis/accounting.js +237 -0
- package/dist/evaluators/docs/analysis/blinding.d.ts +40 -0
- package/dist/evaluators/docs/analysis/blinding.js +80 -0
- package/dist/evaluators/docs/analysis/bounds.d.ts +40 -0
- package/dist/evaluators/docs/analysis/bounds.js +31 -0
- package/dist/evaluators/docs/analysis/budget.d.ts +137 -0
- package/dist/evaluators/docs/analysis/budget.js +305 -0
- package/dist/evaluators/docs/analysis/closure.d.ts +175 -0
- package/dist/evaluators/docs/analysis/closure.js +251 -0
- package/dist/evaluators/docs/analysis/decision.d.ts +59 -0
- package/dist/evaluators/docs/analysis/decision.js +161 -0
- package/dist/evaluators/docs/analysis/freeze.d.ts +201 -0
- package/dist/evaluators/docs/analysis/freeze.js +395 -0
- package/dist/evaluators/docs/analysis/guardrails.d.ts +51 -0
- package/dist/evaluators/docs/analysis/guardrails.js +194 -0
- package/dist/evaluators/docs/analysis/journal.d.ts +150 -0
- package/dist/evaluators/docs/analysis/journal.js +475 -0
- package/dist/evaluators/docs/analysis/ledger.d.ts +78 -0
- package/dist/evaluators/docs/analysis/ledger.js +145 -0
- package/dist/evaluators/docs/analysis/measures.d.ts +302 -0
- package/dist/evaluators/docs/analysis/measures.js +1069 -0
- package/dist/evaluators/docs/analysis/model.d.ts +236 -0
- package/dist/evaluators/docs/analysis/model.js +740 -0
- package/dist/evaluators/docs/analysis/pairing.d.ts +68 -0
- package/dist/evaluators/docs/analysis/pairing.js +95 -0
- package/dist/evaluators/docs/analysis/records.d.ts +229 -0
- package/dist/evaluators/docs/analysis/records.js +935 -0
- package/dist/evaluators/docs/analysis/recovery.d.ts +225 -0
- package/dist/evaluators/docs/analysis/recovery.js +507 -0
- package/dist/evaluators/docs/analysis/render.d.ts +235 -0
- package/dist/evaluators/docs/analysis/render.js +566 -0
- package/dist/evaluators/docs/analysis/replay.d.ts +15 -0
- package/dist/evaluators/docs/analysis/replay.js +1272 -0
- package/dist/evaluators/docs/analysis/screening/inventory.d.ts +136 -0
- package/dist/evaluators/docs/analysis/screening/inventory.js +307 -0
- package/dist/evaluators/docs/analysis/screening/noteVersion.d.ts +15 -0
- package/dist/evaluators/docs/analysis/screening/noteVersion.js +46 -0
- package/dist/evaluators/docs/analysis/screening/reconcile.d.ts +38 -0
- package/dist/evaluators/docs/analysis/screening/reconcile.js +40 -0
- package/dist/evaluators/docs/analysis/screening/rules.d.ts +163 -0
- package/dist/evaluators/docs/analysis/screening/rules.js +330 -0
- package/dist/evaluators/docs/analysis/screening/scan.d.ts +72 -0
- package/dist/evaluators/docs/analysis/screening/scan.js +362 -0
- package/dist/evaluators/docs/analysis/selector.d.ts +68 -0
- package/dist/evaluators/docs/analysis/selector.js +123 -0
- package/dist/evaluators/docs/assess.d.ts +129 -0
- package/dist/evaluators/docs/assess.js +433 -0
- package/dist/evaluators/docs/campaign.d.ts +133 -0
- package/dist/evaluators/docs/campaign.js +222 -0
- package/dist/evaluators/docs/index.d.ts +25 -0
- package/dist/evaluators/docs/index.js +25 -0
- package/dist/evaluators/docs/journal.d.ts +150 -0
- package/dist/evaluators/docs/journal.js +475 -0
- package/dist/evaluators/docs/scoring.d.ts +367 -0
- package/dist/evaluators/docs/scoring.js +662 -0
- package/dist/evaluators/skills/assess.d.ts +94 -0
- package/dist/evaluators/skills/assess.js +207 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +29 -0
- package/dist/legacy/prod014.d.ts +86 -0
- package/dist/legacy/prod014.js +209 -0
- package/dist/reporting/report.d.ts +79 -0
- package/dist/reporting/report.js +242 -0
- package/docs/architecture.md +106 -0
- package/docs/capability-matrix.md +84 -0
- package/docs/offline-rehearsal.md +75 -0
- package/docs/parity-record.md +81 -0
- package/docs/pr-description.md +103 -0
- package/docs/release-0.1.0.md +52 -0
- package/docs/releasing.md +82 -0
- package/docs/schema.md +97 -0
- package/examples/skills/README.md +13 -0
- package/examples/skills/campaign.json +73 -0
- package/examples/skills/skills/otel-helper/SKILL.md +10 -0
- package/examples/skills/suite.json +29 -0
- package/package.json +45 -5
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interval arithmetic for trial utility bounds.
|
|
3
|
+
*
|
|
4
|
+
* A missing component is reported as a closed interval, never as zero and
|
|
5
|
+
* never as a confidence interval. Arm bounds come from assessed totals plus
|
|
6
|
+
* the summed missing components; with independent arms the candidate-minus-
|
|
7
|
+
* control interval is `candidate.low − control.high .. candidate.high −
|
|
8
|
+
* control.low`.
|
|
9
|
+
*/
|
|
10
|
+
/** A closed numeric interval. */
|
|
11
|
+
export interface Interval {
|
|
12
|
+
/** The lowest value the quantity can take. */
|
|
13
|
+
low: number;
|
|
14
|
+
/** The highest value the quantity can take. */
|
|
15
|
+
high: number;
|
|
16
|
+
}
|
|
17
|
+
/** The accounting totals an interval can be read from. */
|
|
18
|
+
export interface BoundableTotals {
|
|
19
|
+
/** Sum of assessed trial utilities. */
|
|
20
|
+
utilityTotal: number;
|
|
21
|
+
/** Full-population lower bound. */
|
|
22
|
+
utilityLow: number;
|
|
23
|
+
/** Full-population upper bound. */
|
|
24
|
+
utilityHigh: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Read the interval an accounting record already carries.
|
|
28
|
+
* @param counts The accounting totals and their derived bounds.
|
|
29
|
+
* @returns The closed interval.
|
|
30
|
+
*/
|
|
31
|
+
export declare function intervalFromCounts(counts: BoundableTotals): Interval;
|
|
32
|
+
/**
|
|
33
|
+
* Compute a candidate-minus-control interval for independent bounds.
|
|
34
|
+
* The worst case pairs the candidate's low with the control's high and the
|
|
35
|
+
* candidate's high with the control's low.
|
|
36
|
+
* @param candidate The candidate arm's interval.
|
|
37
|
+
* @param control The control arm's interval.
|
|
38
|
+
* @returns The difference interval.
|
|
39
|
+
*/
|
|
40
|
+
export declare function differenceInterval(candidate: Interval, control: Interval): Interval;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interval arithmetic for trial utility bounds.
|
|
3
|
+
*
|
|
4
|
+
* A missing component is reported as a closed interval, never as zero and
|
|
5
|
+
* never as a confidence interval. Arm bounds come from assessed totals plus
|
|
6
|
+
* the summed missing components; with independent arms the candidate-minus-
|
|
7
|
+
* control interval is `candidate.low − control.high .. candidate.high −
|
|
8
|
+
* control.low`.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Read the interval an accounting record already carries.
|
|
12
|
+
* @param counts The accounting totals and their derived bounds.
|
|
13
|
+
* @returns The closed interval.
|
|
14
|
+
*/
|
|
15
|
+
export function intervalFromCounts(counts) {
|
|
16
|
+
return { low: counts.utilityLow, high: counts.utilityHigh };
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Compute a candidate-minus-control interval for independent bounds.
|
|
20
|
+
* The worst case pairs the candidate's low with the control's high and the
|
|
21
|
+
* candidate's high with the control's low.
|
|
22
|
+
* @param candidate The candidate arm's interval.
|
|
23
|
+
* @param control The control arm's interval.
|
|
24
|
+
* @returns The difference interval.
|
|
25
|
+
*/
|
|
26
|
+
export function differenceInterval(candidate, control) {
|
|
27
|
+
return {
|
|
28
|
+
low: candidate.low - control.high,
|
|
29
|
+
high: candidate.high - control.low,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable, exclusive spend claims for a campaign.
|
|
3
|
+
*
|
|
4
|
+
* A claim is a small file created with `wx` in a per-kind directory. Because
|
|
5
|
+
* the filesystem decides the winner, two invocations racing for the last
|
|
6
|
+
* allowance cannot both succeed, and no lock or clock is consulted.
|
|
7
|
+
*
|
|
8
|
+
* A claim counts against its ceiling from the moment it is created. That is
|
|
9
|
+
* deliberate: a crash between reserving and spawning leaves a durable
|
|
10
|
+
* reservation that still consumes budget, so recovery can never silently
|
|
11
|
+
* exceed the authorised number of calls.
|
|
12
|
+
*/
|
|
13
|
+
/** The spend kinds with independent ceilings. */
|
|
14
|
+
export type BudgetKind = "trial" | "judge" | "recovery";
|
|
15
|
+
/** The lifecycle of one durable claim. */
|
|
16
|
+
export type ClaimState = "reserved" | "spawned" | "closed";
|
|
17
|
+
/** One durable claim record. */
|
|
18
|
+
export interface BudgetClaim {
|
|
19
|
+
/** The spend kind. */
|
|
20
|
+
kind: BudgetKind;
|
|
21
|
+
/** The one-based claim index within the kind. */
|
|
22
|
+
index: number;
|
|
23
|
+
/** The claim file path. */
|
|
24
|
+
path: string;
|
|
25
|
+
/** The current lifecycle state. */
|
|
26
|
+
state: ClaimState;
|
|
27
|
+
/** When the claim was created. */
|
|
28
|
+
at: string | null;
|
|
29
|
+
/** The trial stamp or analysis selection the claim names. */
|
|
30
|
+
stamp: string | null;
|
|
31
|
+
/** The attempt id the claim names. */
|
|
32
|
+
attemptId: string | null;
|
|
33
|
+
/** A free-form note written by the caller. */
|
|
34
|
+
note: string | null;
|
|
35
|
+
/** The logical attempt this claim owns, when one was named. */
|
|
36
|
+
key: string | null;
|
|
37
|
+
}
|
|
38
|
+
/** The outcome of claiming one budget slot. */
|
|
39
|
+
export type ClaimResult = {
|
|
40
|
+
ok: true;
|
|
41
|
+
claim: BudgetClaim;
|
|
42
|
+
} | {
|
|
43
|
+
ok: false;
|
|
44
|
+
reason: string;
|
|
45
|
+
used: number;
|
|
46
|
+
ceiling: number;
|
|
47
|
+
};
|
|
48
|
+
/** A claim's caller-supplied identity. */
|
|
49
|
+
export interface ClaimIdentity {
|
|
50
|
+
/** The trial stamp or analysis selection the claim names. */
|
|
51
|
+
stamp?: string;
|
|
52
|
+
/** The attempt id the claim names. */
|
|
53
|
+
attemptId?: string;
|
|
54
|
+
/** A free-form note. */
|
|
55
|
+
note?: string;
|
|
56
|
+
/** The logical attempt this claim owns; duplicate keys are refused. */
|
|
57
|
+
key?: string;
|
|
58
|
+
/** The creation timestamp, injected for determinism in tests. */
|
|
59
|
+
at?: string;
|
|
60
|
+
}
|
|
61
|
+
/** The claim filename for one index. */
|
|
62
|
+
export declare function claimFileName(kind: BudgetKind, index: number): string;
|
|
63
|
+
/** The key lock filename for one logical attempt. */
|
|
64
|
+
export declare function keyLockName(kind: BudgetKind, key: string): string;
|
|
65
|
+
/** The directory holding one kind's claims. */
|
|
66
|
+
export declare function claimDir(root: string, kind: BudgetKind): string;
|
|
67
|
+
/**
|
|
68
|
+
* Read every claim of one kind, in index order. A file that matches the claim
|
|
69
|
+
* name but cannot be parsed is returned as a `reserved` claim so it still
|
|
70
|
+
* consumes budget: corrupt state fails closed.
|
|
71
|
+
* @param root The campaign evidence root.
|
|
72
|
+
* @param kind The spend kind.
|
|
73
|
+
* @returns The claims, including unreadable ones as reserved.
|
|
74
|
+
*/
|
|
75
|
+
export declare function readClaims(root: string, kind: BudgetKind): BudgetClaim[];
|
|
76
|
+
/**
|
|
77
|
+
* Claim the next free slot of one kind, or refuse at the ceiling.
|
|
78
|
+
*
|
|
79
|
+
* The claim is written with `wx`, so the create is the arbitration point.
|
|
80
|
+
* A refused claim writes nothing and returns the observed usage.
|
|
81
|
+
* @param root The campaign evidence root.
|
|
82
|
+
* @param kind The spend kind.
|
|
83
|
+
* @param ceiling The authorised calls of this kind.
|
|
84
|
+
* @param identity The caller's identity fields.
|
|
85
|
+
* @returns The created claim, or the refusal.
|
|
86
|
+
*/
|
|
87
|
+
export declare function claimBudget(root: string, kind: BudgetKind, ceiling: number, identity?: ClaimIdentity): ClaimResult;
|
|
88
|
+
/**
|
|
89
|
+
* Release a reservation that provably dispatched nothing.
|
|
90
|
+
*
|
|
91
|
+
* Only a `reserved` claim may be released: a spawned claim's call may have
|
|
92
|
+
* begun and must keep consuming the ceiling.
|
|
93
|
+
* @param claim The claim to release.
|
|
94
|
+
* @returns Whether the claim and its logical lock were removed.
|
|
95
|
+
*/
|
|
96
|
+
export declare function releaseClaim(claim: BudgetClaim): boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Advance one claim's durable state.
|
|
99
|
+
* @param claim The claim to update.
|
|
100
|
+
* @param state The new lifecycle state.
|
|
101
|
+
* @param patch Extra identity fields to record.
|
|
102
|
+
* @param patch.note The new note.
|
|
103
|
+
* @param patch.stamp The stamp to record.
|
|
104
|
+
* @param patch.attemptId The attempt id to record.
|
|
105
|
+
* @returns The updated claim.
|
|
106
|
+
*/
|
|
107
|
+
export declare function advanceClaim(claim: BudgetClaim, state: ClaimState, patch?: {
|
|
108
|
+
note?: string;
|
|
109
|
+
stamp?: string;
|
|
110
|
+
attemptId?: string;
|
|
111
|
+
}): BudgetClaim;
|
|
112
|
+
/** Per-state claim counts. */
|
|
113
|
+
export interface ClaimUsage {
|
|
114
|
+
/** Total claims, which is the spend committed against the ceiling. */
|
|
115
|
+
claimed: number;
|
|
116
|
+
/** Claims whose call has not yet been observed to start. */
|
|
117
|
+
reserved: number;
|
|
118
|
+
/** Claims whose call started and has not closed. */
|
|
119
|
+
spawned: number;
|
|
120
|
+
/** Claims whose call reached a terminal record. */
|
|
121
|
+
closed: number;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Count one kind's claims by lifecycle state.
|
|
125
|
+
* @param root The campaign evidence root.
|
|
126
|
+
* @param kind The spend kind.
|
|
127
|
+
* @returns The counts.
|
|
128
|
+
*/
|
|
129
|
+
export declare function claimUsage(root: string, kind: BudgetKind): ClaimUsage;
|
|
130
|
+
/**
|
|
131
|
+
* Classify a claim that has not closed, for a restart decision. A reservation
|
|
132
|
+
* that never spawned is `unresolved`; a spawned call with no terminal record
|
|
133
|
+
* is `unknown-outcome`. Both refuse re-entry and preserve the original claim.
|
|
134
|
+
* @param claim The claim.
|
|
135
|
+
* @returns The restart classification, or null when the claim is closed.
|
|
136
|
+
*/
|
|
137
|
+
export declare function restartDisposition(claim: BudgetClaim): "unresolved" | "unknown-outcome" | null;
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable, exclusive spend claims for a campaign.
|
|
3
|
+
*
|
|
4
|
+
* A claim is a small file created with `wx` in a per-kind directory. Because
|
|
5
|
+
* the filesystem decides the winner, two invocations racing for the last
|
|
6
|
+
* allowance cannot both succeed, and no lock or clock is consulted.
|
|
7
|
+
*
|
|
8
|
+
* A claim counts against its ceiling from the moment it is created. That is
|
|
9
|
+
* deliberate: a crash between reserving and spawning leaves a durable
|
|
10
|
+
* reservation that still consumes budget, so recovery can never silently
|
|
11
|
+
* exceed the authorised number of calls.
|
|
12
|
+
*/
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import { mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
|
+
/** Whether a value is a plain JSON object. */
|
|
17
|
+
function isRecord(value) {
|
|
18
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
/** The claim filename for one index. */
|
|
21
|
+
export function claimFileName(kind, index) {
|
|
22
|
+
return `${kind}-${String(index).padStart(4, "0")}.claim`;
|
|
23
|
+
}
|
|
24
|
+
/** The key lock filename for one logical attempt. */
|
|
25
|
+
export function keyLockName(kind, key) {
|
|
26
|
+
return `${kind}-key-${createHash("sha256").update(key, "utf8").digest("hex").slice(0, 16)}.lock`;
|
|
27
|
+
}
|
|
28
|
+
/** The directory holding one kind's claims. */
|
|
29
|
+
export function claimDir(root, kind) {
|
|
30
|
+
return join(root, kind);
|
|
31
|
+
}
|
|
32
|
+
/** A recognised claim state, or null. */
|
|
33
|
+
function claimStateOf(value) {
|
|
34
|
+
if (value === "reserved" || value === "spawned" || value === "closed") {
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
/** A string field, or null. */
|
|
40
|
+
function stringOrNull(value) {
|
|
41
|
+
return typeof value === "string" ? value : null;
|
|
42
|
+
}
|
|
43
|
+
/** Parse one claim file, or null when its bytes are unusable. */
|
|
44
|
+
function claimOf(kind, index, path, text) {
|
|
45
|
+
let value;
|
|
46
|
+
try {
|
|
47
|
+
value = JSON.parse(text);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
if (!isRecord(value))
|
|
53
|
+
return null;
|
|
54
|
+
const state = claimStateOf(value.state);
|
|
55
|
+
if (state === null)
|
|
56
|
+
return null;
|
|
57
|
+
return {
|
|
58
|
+
kind,
|
|
59
|
+
index,
|
|
60
|
+
path,
|
|
61
|
+
state,
|
|
62
|
+
at: stringOrNull(value.at),
|
|
63
|
+
stamp: stringOrNull(value.stamp),
|
|
64
|
+
attemptId: stringOrNull(value.attemptId),
|
|
65
|
+
note: stringOrNull(value.note),
|
|
66
|
+
key: stringOrNull(value.key),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Read every claim of one kind, in index order. A file that matches the claim
|
|
71
|
+
* name but cannot be parsed is returned as a `reserved` claim so it still
|
|
72
|
+
* consumes budget: corrupt state fails closed.
|
|
73
|
+
* @param root The campaign evidence root.
|
|
74
|
+
* @param kind The spend kind.
|
|
75
|
+
* @returns The claims, including unreadable ones as reserved.
|
|
76
|
+
*/
|
|
77
|
+
export function readClaims(root, kind) {
|
|
78
|
+
const dir = claimDir(root, kind);
|
|
79
|
+
let names;
|
|
80
|
+
try {
|
|
81
|
+
names = readdirSync(dir);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
const pattern = new RegExp(`^${kind}-(\\d{4})\\.claim$`);
|
|
87
|
+
const claims = [];
|
|
88
|
+
for (const name of names.sort()) {
|
|
89
|
+
const match = pattern.exec(name);
|
|
90
|
+
if (match === null)
|
|
91
|
+
continue;
|
|
92
|
+
const index = Number(match[1]);
|
|
93
|
+
const path = join(dir, name);
|
|
94
|
+
const text = readFileSync(path, "utf8");
|
|
95
|
+
claims.push(claimOf(kind, index, path, text) ?? {
|
|
96
|
+
kind,
|
|
97
|
+
index,
|
|
98
|
+
path,
|
|
99
|
+
state: "reserved",
|
|
100
|
+
at: null,
|
|
101
|
+
stamp: null,
|
|
102
|
+
attemptId: null,
|
|
103
|
+
note: "unreadable claim treated as reserved",
|
|
104
|
+
key: null,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return claims;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Claim the next free slot of one kind, or refuse at the ceiling.
|
|
111
|
+
*
|
|
112
|
+
* The claim is written with `wx`, so the create is the arbitration point.
|
|
113
|
+
* A refused claim writes nothing and returns the observed usage.
|
|
114
|
+
* @param root The campaign evidence root.
|
|
115
|
+
* @param kind The spend kind.
|
|
116
|
+
* @param ceiling The authorised calls of this kind.
|
|
117
|
+
* @param identity The caller's identity fields.
|
|
118
|
+
* @returns The created claim, or the refusal.
|
|
119
|
+
*/
|
|
120
|
+
export function claimBudget(root, kind, ceiling, identity = {}) {
|
|
121
|
+
if (!Number.isInteger(ceiling) || ceiling < 1) {
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
reason: `invalid ${kind} ceiling ${String(ceiling)}`,
|
|
125
|
+
used: 0,
|
|
126
|
+
ceiling,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const dir = claimDir(root, kind);
|
|
130
|
+
mkdirSync(dir, { recursive: true });
|
|
131
|
+
const key = identity.key ?? null;
|
|
132
|
+
if (key !== null && !acquireKeyLock(dir, kind, key)) {
|
|
133
|
+
return {
|
|
134
|
+
ok: false,
|
|
135
|
+
reason: `${kind} logical attempt ${key} already owns a reservation`,
|
|
136
|
+
used: readClaims(root, kind).length,
|
|
137
|
+
ceiling,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
for (let index = 1; index <= ceiling; index += 1) {
|
|
141
|
+
const claim = createClaim(dir, kind, index, identity);
|
|
142
|
+
if (claim !== undefined)
|
|
143
|
+
return { ok: true, claim };
|
|
144
|
+
}
|
|
145
|
+
if (key !== null)
|
|
146
|
+
releaseKeyLock(dir, kind, key);
|
|
147
|
+
return {
|
|
148
|
+
ok: false,
|
|
149
|
+
reason: `${kind} ceiling reached: ${String(ceiling)} authorised call(s)`,
|
|
150
|
+
used: ceiling,
|
|
151
|
+
ceiling,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Try to create one claim file exclusively.
|
|
156
|
+
* @param dir The kind's claim directory.
|
|
157
|
+
* @param kind The spend kind.
|
|
158
|
+
* @param index The claim index.
|
|
159
|
+
* @param identity The caller's identity fields.
|
|
160
|
+
* @returns The claim, or undefined when that slot is already taken.
|
|
161
|
+
*/
|
|
162
|
+
function createClaim(dir, kind, index, identity) {
|
|
163
|
+
const path = join(dir, claimFileName(kind, index));
|
|
164
|
+
const claim = {
|
|
165
|
+
kind,
|
|
166
|
+
index,
|
|
167
|
+
path,
|
|
168
|
+
state: "reserved",
|
|
169
|
+
at: identity.at ?? null,
|
|
170
|
+
stamp: identity.stamp ?? null,
|
|
171
|
+
attemptId: identity.attemptId ?? null,
|
|
172
|
+
note: identity.note ?? null,
|
|
173
|
+
key: identity.key ?? null,
|
|
174
|
+
};
|
|
175
|
+
try {
|
|
176
|
+
writeFileSync(path, claimBytes(claim, new Date().toISOString()), {
|
|
177
|
+
encoding: "utf8",
|
|
178
|
+
flag: "wx",
|
|
179
|
+
});
|
|
180
|
+
return { ...claim, at: identity.at ?? new Date().toISOString() };
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
if (error.code !== "EEXIST")
|
|
184
|
+
throw error;
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Serialise one claim record.
|
|
190
|
+
* @param claim The claim.
|
|
191
|
+
* @param at The creation timestamp.
|
|
192
|
+
* @returns The JSON bytes with a trailing newline.
|
|
193
|
+
*/
|
|
194
|
+
function claimBytes(claim, at) {
|
|
195
|
+
return `${JSON.stringify({
|
|
196
|
+
kind: claim.kind,
|
|
197
|
+
index: claim.index,
|
|
198
|
+
state: claim.state,
|
|
199
|
+
at: claim.at ?? at,
|
|
200
|
+
stamp: claim.stamp,
|
|
201
|
+
attemptId: claim.attemptId,
|
|
202
|
+
note: claim.note,
|
|
203
|
+
key: claim.key,
|
|
204
|
+
}, null, 2)}\n`;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Acquire the exclusive lock for one logical attempt.
|
|
208
|
+
* @param dir The kind's claim directory.
|
|
209
|
+
* @param kind The spend kind.
|
|
210
|
+
* @param key The logical attempt key.
|
|
211
|
+
* @returns True when this caller now owns the reservation.
|
|
212
|
+
*/
|
|
213
|
+
function acquireKeyLock(dir, kind, key) {
|
|
214
|
+
try {
|
|
215
|
+
writeFileSync(join(dir, keyLockName(kind, key)), `${key}\n`, {
|
|
216
|
+
encoding: "utf8",
|
|
217
|
+
flag: "wx",
|
|
218
|
+
});
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
if (error.code === "EEXIST")
|
|
223
|
+
return false;
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/** Remove one logical attempt's lock. */
|
|
228
|
+
function releaseKeyLock(dir, kind, key) {
|
|
229
|
+
try {
|
|
230
|
+
unlinkSync(join(dir, keyLockName(kind, key)));
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
/* already released */
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Release a reservation that provably dispatched nothing.
|
|
238
|
+
*
|
|
239
|
+
* Only a `reserved` claim may be released: a spawned claim's call may have
|
|
240
|
+
* begun and must keep consuming the ceiling.
|
|
241
|
+
* @param claim The claim to release.
|
|
242
|
+
* @returns Whether the claim and its logical lock were removed.
|
|
243
|
+
*/
|
|
244
|
+
export function releaseClaim(claim) {
|
|
245
|
+
if (claim.state !== "reserved")
|
|
246
|
+
return false;
|
|
247
|
+
try {
|
|
248
|
+
unlinkSync(claim.path);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
if (claim.key !== null) {
|
|
254
|
+
releaseKeyLock(dirname(claim.path), claim.kind, claim.key);
|
|
255
|
+
}
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Advance one claim's durable state.
|
|
260
|
+
* @param claim The claim to update.
|
|
261
|
+
* @param state The new lifecycle state.
|
|
262
|
+
* @param patch Extra identity fields to record.
|
|
263
|
+
* @param patch.note The new note.
|
|
264
|
+
* @param patch.stamp The stamp to record.
|
|
265
|
+
* @param patch.attemptId The attempt id to record.
|
|
266
|
+
* @returns The updated claim.
|
|
267
|
+
*/
|
|
268
|
+
export function advanceClaim(claim, state, patch = {}) {
|
|
269
|
+
const updated = {
|
|
270
|
+
...claim,
|
|
271
|
+
state,
|
|
272
|
+
stamp: patch.stamp ?? claim.stamp,
|
|
273
|
+
attemptId: patch.attemptId ?? claim.attemptId,
|
|
274
|
+
note: patch.note ?? claim.note,
|
|
275
|
+
};
|
|
276
|
+
writeFileSync(updated.path, claimBytes(updated, new Date().toISOString()), "utf8");
|
|
277
|
+
return updated;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Count one kind's claims by lifecycle state.
|
|
281
|
+
* @param root The campaign evidence root.
|
|
282
|
+
* @param kind The spend kind.
|
|
283
|
+
* @returns The counts.
|
|
284
|
+
*/
|
|
285
|
+
export function claimUsage(root, kind) {
|
|
286
|
+
const claims = readClaims(root, kind);
|
|
287
|
+
return {
|
|
288
|
+
claimed: claims.length,
|
|
289
|
+
reserved: claims.filter((claim) => claim.state === "reserved").length,
|
|
290
|
+
spawned: claims.filter((claim) => claim.state === "spawned").length,
|
|
291
|
+
closed: claims.filter((claim) => claim.state === "closed").length,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Classify a claim that has not closed, for a restart decision. A reservation
|
|
296
|
+
* that never spawned is `unresolved`; a spawned call with no terminal record
|
|
297
|
+
* is `unknown-outcome`. Both refuse re-entry and preserve the original claim.
|
|
298
|
+
* @param claim The claim.
|
|
299
|
+
* @returns The restart classification, or null when the claim is closed.
|
|
300
|
+
*/
|
|
301
|
+
export function restartDisposition(claim) {
|
|
302
|
+
if (claim.state === "closed")
|
|
303
|
+
return null;
|
|
304
|
+
return claim.state === "reserved" ? "unresolved" : "unknown-outcome";
|
|
305
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Campaign accounting and closure, derived from durable evidence.
|
|
3
|
+
*
|
|
4
|
+
* The report never trusts a hard-coded checkpoint. Counts come from the
|
|
5
|
+
* frozen schedule, the launch claims, the recorded trial directories and each
|
|
6
|
+
* attempt's own token/duration record. A stopped partial campaign closes with
|
|
7
|
+
* the real scope it ran, and a closed campaign cannot be reopened by either
|
|
8
|
+
* the runner or the recovery path.
|
|
9
|
+
*/
|
|
10
|
+
import type { BudgetClaim } from "./budget.ts";
|
|
11
|
+
/** What one scheduled slot's evidence proves. */
|
|
12
|
+
export interface SlotObservation {
|
|
13
|
+
/** The declared slot number. */
|
|
14
|
+
slot: number;
|
|
15
|
+
/** The pair id. */
|
|
16
|
+
pairId: string | null;
|
|
17
|
+
/** The trial stamp. */
|
|
18
|
+
stamp: string;
|
|
19
|
+
/** The declared arm. */
|
|
20
|
+
arm: string;
|
|
21
|
+
/** The declared scenario. */
|
|
22
|
+
scenario: string;
|
|
23
|
+
/** Whether a trial launch attempt exists. */
|
|
24
|
+
launched: boolean;
|
|
25
|
+
/** Whether the launch reached a recorded result. */
|
|
26
|
+
recorded: boolean;
|
|
27
|
+
/** Recorded trial steps, or null when no trail was readable. */
|
|
28
|
+
trialSteps: number | null;
|
|
29
|
+
/** Recorded trial token fields; null means unavailable, never zero. */
|
|
30
|
+
trialTokens: TokenFields | null;
|
|
31
|
+
/** Recorded trial duration in milliseconds, or null. */
|
|
32
|
+
trialDurationMs: number | null;
|
|
33
|
+
/** Judge attempts recorded for this slot. */
|
|
34
|
+
judgeAttempts: readonly JudgeAttempt[];
|
|
35
|
+
/** The published selection for this slot, or null when unrecorded. */
|
|
36
|
+
selectedAttemptId?: string | null;
|
|
37
|
+
}
|
|
38
|
+
/** One token field, kept explicit so a known zero differs from unknown. */
|
|
39
|
+
export type TokenFields = {
|
|
40
|
+
/** Input-token subtotal, or null when unrecorded on any observation. */
|
|
41
|
+
input: number | null;
|
|
42
|
+
/** Output-token subtotal, or null when unrecorded on any observation. */
|
|
43
|
+
output: number | null;
|
|
44
|
+
/** Reasoning-token subtotal, or null when unrecorded on any observation. */
|
|
45
|
+
reasoning: number | null;
|
|
46
|
+
/** Cache-read-token subtotal, or null when unrecorded on any observation. */
|
|
47
|
+
cacheRead: number | null;
|
|
48
|
+
};
|
|
49
|
+
/** What one judge attempt proves. */
|
|
50
|
+
export interface JudgeAttempt {
|
|
51
|
+
/** The attempt id, e.g. `attempt-002`. */
|
|
52
|
+
attemptId: string;
|
|
53
|
+
/** Whether the attempt spawned a model call. */
|
|
54
|
+
spawned: boolean;
|
|
55
|
+
/** Whether the call reached inference (at least one step). */
|
|
56
|
+
reachedInference: boolean;
|
|
57
|
+
/** Recorded steps, or null when no trail was readable. */
|
|
58
|
+
steps: number | null;
|
|
59
|
+
/** Recorded token fields, or null when unavailable. */
|
|
60
|
+
tokens: TokenFields | null;
|
|
61
|
+
/** The recorded terminal status. */
|
|
62
|
+
status: "judged" | "invalid-verdict" | "judge-error" | "started" | "unknown";
|
|
63
|
+
/** Recorded duration, or null. */
|
|
64
|
+
durationMs: number | null;
|
|
65
|
+
/** The packet identity the attempt was judged from, or null. */
|
|
66
|
+
packetSha256?: string | null;
|
|
67
|
+
/** The machine-readable failure class, when the attempt is unusable. */
|
|
68
|
+
failureClass?: string | null;
|
|
69
|
+
/** Whether the capture proves a complete final response, or null. */
|
|
70
|
+
complete?: boolean | null;
|
|
71
|
+
}
|
|
72
|
+
/** The derivation inputs. */
|
|
73
|
+
export interface ClosureInput {
|
|
74
|
+
/** The frozen schedule size. */
|
|
75
|
+
scheduledSlots: number;
|
|
76
|
+
/** Every slot's observation, in schedule order. */
|
|
77
|
+
observations: readonly SlotObservation[];
|
|
78
|
+
/** The durable trial claims, when the runner kept them. */
|
|
79
|
+
trialClaims?: readonly BudgetClaim[];
|
|
80
|
+
/** The durable judge claims, when the runner kept them. */
|
|
81
|
+
judgeClaims?: readonly BudgetClaim[];
|
|
82
|
+
/** The authorised ceilings. */
|
|
83
|
+
ceilings: {
|
|
84
|
+
trial: number;
|
|
85
|
+
judge: number;
|
|
86
|
+
};
|
|
87
|
+
/** Whether the attempt ledger records a terminal close. */
|
|
88
|
+
closed: boolean;
|
|
89
|
+
}
|
|
90
|
+
/** One model's accounting. */
|
|
91
|
+
export interface ModelAccount {
|
|
92
|
+
/** Distinct scheduled slots with a spawn. */
|
|
93
|
+
attempts: number;
|
|
94
|
+
/** Attempts that reached inference. */
|
|
95
|
+
inferred: number;
|
|
96
|
+
/** Summed steps, or null when no trail was readable. */
|
|
97
|
+
steps: number | null;
|
|
98
|
+
/** Per-field totals; null where no attempt reported the field. */
|
|
99
|
+
tokens: TokenFields;
|
|
100
|
+
/** Steps that reported each field, for coverage. */
|
|
101
|
+
tokenCoverage: {
|
|
102
|
+
input: number;
|
|
103
|
+
output: number;
|
|
104
|
+
reasoning: number;
|
|
105
|
+
cacheRead: number;
|
|
106
|
+
};
|
|
107
|
+
/** Summed duration, or null when no attempt reported one. */
|
|
108
|
+
durationMs: number | null;
|
|
109
|
+
}
|
|
110
|
+
/** The closure report. */
|
|
111
|
+
export interface ClosureReport {
|
|
112
|
+
/** Whether the campaign was already closed. */
|
|
113
|
+
state: "open" | "closed";
|
|
114
|
+
/** The scheduled slot count. */
|
|
115
|
+
scheduledSlots: number;
|
|
116
|
+
/** Distinct slots with any evidence. */
|
|
117
|
+
observedSlots: number;
|
|
118
|
+
/** Slots with no evidence at all. */
|
|
119
|
+
unrunSlots: number[];
|
|
120
|
+
/** Distinct slots with a trial spawn. */
|
|
121
|
+
trialLaunches: number;
|
|
122
|
+
/** Distinct slots with a recorded result. */
|
|
123
|
+
recordedTrials: number;
|
|
124
|
+
/** The trial accounting. */
|
|
125
|
+
trial: ModelAccount;
|
|
126
|
+
/** The judge accounting, across every attempt. */
|
|
127
|
+
judge: ModelAccount & {
|
|
128
|
+
attempts: number;
|
|
129
|
+
usableJudgements: number;
|
|
130
|
+
unusableJudgements: number;
|
|
131
|
+
/** Recorded first attempts, one per judged slot at most. */
|
|
132
|
+
initialAttempts: number;
|
|
133
|
+
/** Recorded recovery attempts beside a first attempt. */
|
|
134
|
+
recoveryAttempts: number;
|
|
135
|
+
};
|
|
136
|
+
/** The checkpoint the campaign actually stopped at, or null. */
|
|
137
|
+
stoppedAt: string | null;
|
|
138
|
+
/** Every reconciliation disagreement. */
|
|
139
|
+
problems: string[];
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Charge a claimed-but-unspawned attempt explicitly. A reservation consumes
|
|
143
|
+
* budget and appears as an unresolved attempt, never as a launch.
|
|
144
|
+
* @param claims The durable claims.
|
|
145
|
+
* @returns The count of claims that never spawned.
|
|
146
|
+
*/
|
|
147
|
+
export declare function unresolvedReservations(claims: readonly BudgetClaim[] | undefined): BudgetClaim[];
|
|
148
|
+
/**
|
|
149
|
+
* Derive the campaign accounts from durable observations.
|
|
150
|
+
* @param input The schedule, slot observations, claims and ceilings.
|
|
151
|
+
* @returns The closure report.
|
|
152
|
+
*/
|
|
153
|
+
export declare function deriveClosure(input: ClosureInput): ClosureReport;
|
|
154
|
+
/** The closure decision. */
|
|
155
|
+
export type ClosureOutcome = {
|
|
156
|
+
ok: true;
|
|
157
|
+
event: {
|
|
158
|
+
event: "campaign-closed";
|
|
159
|
+
stoppedAt: string | null;
|
|
160
|
+
scope: number;
|
|
161
|
+
};
|
|
162
|
+
} | {
|
|
163
|
+
ok: false;
|
|
164
|
+
problems: string[];
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* Decide whether a campaign may close now, and with what scope.
|
|
168
|
+
*
|
|
169
|
+
* A campaign with no evidence is refused rather than closed empty. A stopped
|
|
170
|
+
* partial campaign closes with the slots that actually ran; the schedule's
|
|
171
|
+
* unrun slots are preserved as scope, not invented results.
|
|
172
|
+
* @param report The derived closure report.
|
|
173
|
+
* @returns The close event, or every reason closure is refused.
|
|
174
|
+
*/
|
|
175
|
+
export declare function closeCampaign(report: ClosureReport): ClosureOutcome;
|