toolroll 0.9.91 → 0.9.92
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 +22 -0
- package/dist/approval-authority.d.ts +9 -1
- package/dist/approval-authority.js +15 -2
- package/dist/build-review.js +2 -0
- package/dist/chat-actions.d.ts +11 -3
- package/dist/chat-actions.js +47 -2
- package/dist/chat-decide.js +2 -0
- package/dist/chat-review.d.ts +2 -1
- package/dist/chat-review.js +2 -2
- package/dist/contracts/chat-actions.d.ts +10 -0
- package/dist/contracts/chat-actions.js +4 -0
- package/dist/contracts/console-api.d.ts +1 -0
- package/dist/contracts/console-api.js +3 -0
- package/dist/contracts/route-results.d.ts +121 -0
- package/dist/contracts/route-results.js +71 -0
- package/dist/contracts/route.d.ts +343 -5
- package/dist/contracts/route.js +36 -7
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/dispose.js +6 -0
- package/dist/flow-engine.js +1 -1
- package/dist/flow-pull-request.js +1 -1
- package/dist/lead-doors.js +4 -2
- package/dist/lead-tools.js +10 -2
- package/dist/local-owner.d.ts +1 -1
- package/dist/local-owner.js +11 -0
- package/dist/memory-cli.d.ts +3 -0
- package/dist/memory-cli.js +3 -3
- package/dist/memory-pass.js +5 -1
- package/dist/model-choices.d.ts +7 -3
- package/dist/model-choices.js +26 -11
- package/dist/models-cli.d.ts +205 -0
- package/dist/models-cli.js +143 -4
- package/dist/models-ui.d.ts +57 -0
- package/dist/models-ui.js +44 -1
- package/dist/operate-remote-arguments.js +4 -4
- package/dist/operate-remote.js +1 -1
- package/dist/operate.js +53 -5
- package/dist/phase-routing.d.ts +82 -2
- package/dist/phase-routing.js +132 -18
- package/dist/project-delete.d.ts +4 -0
- package/dist/project-delete.js +12 -4
- package/dist/project-knowledge.d.ts +6 -6
- package/dist/project-memory.d.ts +8 -0
- package/dist/project-memory.js +20 -1
- package/dist/pull-request-flow.js +1 -1
- package/dist/release-auto.js +12 -3
- package/dist/result-actions.d.ts +4 -0
- package/dist/result-actions.js +3 -0
- package/dist/route-hooks.d.ts +54 -0
- package/dist/route-hooks.js +179 -0
- package/dist/route-results.d.ts +70 -0
- package/dist/route-results.js +340 -0
- package/dist/route-rules.d.ts +324 -0
- package/dist/route-rules.js +781 -0
- package/dist/route-savings.d.ts +136 -0
- package/dist/route-savings.js +193 -0
- package/dist/route-store.d.ts +300 -0
- package/dist/route-store.js +598 -0
- package/dist/scope.d.ts +3 -1
- package/dist/scope.js +4 -1
- package/dist/server/render-chat.js +2 -2
- package/dist/server/render-tasks.d.ts +7 -0
- package/dist/server/render-tasks.js +13 -0
- package/dist/server/route-table.js +12 -0
- package/dist/server/settings.d.ts +2 -1
- package/dist/server/settings.js +87 -1
- package/dist/server/task-views.js +6 -0
- package/dist/server/tasks.js +3 -0
- package/dist/settings-proofs.js +2 -0
- package/dist/setup-guide.d.ts +2 -2
- package/dist/spend-ui.d.ts +11 -0
- package/dist/spend-ui.js +49 -2
- package/dist/store.d.ts +38 -2
- package/dist/store.js +320 -35
- package/dist/surface.js +16 -4
- package/dist/task-outcome-cli.js +5 -2
- package/dist/toolroll-update.js +3 -2
- package/package.json +1 -1
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result records (docs/plans/results-routing.md, slice 1): what each finished task family shows about the model that
|
|
3
|
+
* built it, read only from Toolroll's own rows (runs, their cohort snapshots, checks, completions, merges, review
|
|
4
|
+
* findings and revision origins) — never worktree files, park payloads, agent reports or lead tool calls.
|
|
5
|
+
*
|
|
6
|
+
* A policy path (release-auto.ts EVAL_POLICY): the execution-class table, eligibility and the lifecycle decide what
|
|
7
|
+
* counts as a cheaper model's evidence, so a change here needs a person. The only writer is route-store.ts.
|
|
8
|
+
*
|
|
9
|
+
* Read-only in slice 1: nothing here suggests, routes or switches anything.
|
|
10
|
+
*/
|
|
11
|
+
import { protectedChanges } from "./approval-policy.js";
|
|
12
|
+
import { assignmentChecksForRun } from "./assignment.js";
|
|
13
|
+
import { INELIGIBLE_REASONS, REASON_WORDS, selectionSchema, } from "./contracts/route-results.js";
|
|
14
|
+
import { changedFilesOf, COMPLETION_ACTION } from "./result-completion.js";
|
|
15
|
+
import { runAccounting } from "./store.js";
|
|
16
|
+
import { INFRASTRUCTURE_TERMINALS, saveResultRecord } from "./route-store.js";
|
|
17
|
+
// The execution class and the cohort snapshot live with their writer (route-store.ts), which store.ts imports: kept
|
|
18
|
+
// light so the store's module graph doesn't reach the evaluator's readers.
|
|
19
|
+
export { cohortOf, EXEC_CLASS_OF_REASON, execClassOf } from "./route-store.js";
|
|
20
|
+
// ---- the record ---------------------------------------------------------------------------------------------------
|
|
21
|
+
/** How long an accepted result waits before it counts, and how long a result may wait for an answer. */
|
|
22
|
+
export const MATURE_AFTER_MS = 14 * 24 * 60 * 60_000;
|
|
23
|
+
export const UNANSWERED_AFTER_MS = 30 * 24 * 60 * 60_000;
|
|
24
|
+
/** The explicit cutoff for a family's spending and counts: they keep accumulating through later revision and repair
|
|
25
|
+
* runs (a verdict, once final, never moves) until the family is quiet — every version ended and no run open — or
|
|
26
|
+
* this long after its first run, whichever is first. */
|
|
27
|
+
export const ACCUMULATE_FOR_MS = 90 * 24 * 60 * 60_000;
|
|
28
|
+
function cohortsFor(store, runs) {
|
|
29
|
+
const rows = store.handle.prepare("SELECT * FROM run_cohort WHERE run IN (SELECT value FROM json_each(?))").all(JSON.stringify(runs));
|
|
30
|
+
return new Map(rows.map(row => {
|
|
31
|
+
let selection = null;
|
|
32
|
+
try {
|
|
33
|
+
const read = selectionSchema.safeParse(JSON.parse(String(row["selection_json"])));
|
|
34
|
+
selection = read.success ? read.data : null;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
selection = null;
|
|
38
|
+
}
|
|
39
|
+
const size = row["cohort_size"] === "small" || row["cohort_size"] === "medium" ? row["cohort_size"] : null;
|
|
40
|
+
return [Number(row["run"]), { run: Number(row["run"]), repo: row["repo"] == null ? null : String(row["repo"]), cohortSize: size,
|
|
41
|
+
ineligible: row["ineligible"] == null ? null : String(row["ineligible"]), model: row["model"] == null ? null : String(row["model"]),
|
|
42
|
+
provider: String(row["provider"]), selection, startedAt: String(row["started_at"]) }];
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
/** A failed code-writing run's class: its kept class; otherwise a provider terminal or an interruption before the
|
|
46
|
+
* provider started is infrastructure; anything else (a missing write included) is the model's. */
|
|
47
|
+
function classOfRun(run, kept) {
|
|
48
|
+
if (kept === "model" || kept === "infrastructure")
|
|
49
|
+
return kept;
|
|
50
|
+
if (run.terminalClass != null && INFRASTRUCTURE_TERMINALS.has(run.terminalClass))
|
|
51
|
+
return "infrastructure";
|
|
52
|
+
if ((run.outcome === "interrupted" || run.reason === "interrupted") && run.providerStartedAt === null)
|
|
53
|
+
return "infrastructure";
|
|
54
|
+
return "model";
|
|
55
|
+
}
|
|
56
|
+
const FAILED_OUTCOMES = new Set(["failed", "interrupted"]);
|
|
57
|
+
/**
|
|
58
|
+
* A task family's record, evaluated now from its rows (null: no code-writing run, or a family whose lineage can't be
|
|
59
|
+
* proved). The order is the plan's: what can't be attributed to one model and one cohort first, then a live attempt
|
|
60
|
+
* (pending), then every rejection, then what isn't counted, then acceptance and maturity.
|
|
61
|
+
*/
|
|
62
|
+
export function evaluateFamily(store, rootTaskId, evidenceRoot, now) {
|
|
63
|
+
const family = store.taskFamilyOf(rootTaskId, null, true);
|
|
64
|
+
if (family === null || family.problem !== null || family.root.id !== rootTaskId)
|
|
65
|
+
return null;
|
|
66
|
+
const versionOf = new Map();
|
|
67
|
+
const runs = [];
|
|
68
|
+
for (const version of family.versions) {
|
|
69
|
+
for (const run of store.runsFor(version.refId)) {
|
|
70
|
+
if (run.role !== "builder" && run.role !== "repair")
|
|
71
|
+
continue;
|
|
72
|
+
runs.push(run);
|
|
73
|
+
versionOf.set(run.id, version.id);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (runs.length === 0)
|
|
77
|
+
return null;
|
|
78
|
+
runs.sort((a, b) => a.id - b.id);
|
|
79
|
+
const ids = runs.map(run => run.id);
|
|
80
|
+
const cohorts = cohortsFor(store, ids);
|
|
81
|
+
const kept = new Map(store.handle.prepare("SELECT id, exec_class FROM run WHERE id IN (SELECT value FROM json_each(?))").all(JSON.stringify(ids))
|
|
82
|
+
.map(row => [Number(row["id"]), row["exec_class"] == null ? null : String(row["exec_class"])]));
|
|
83
|
+
// Spending, kept apart by kind and never added across kinds.
|
|
84
|
+
let apiMicrousd = 0, subscriptionRuns = 0, subscriptionTokens = 0, unknownRuns = 0, remediationRuns = 0;
|
|
85
|
+
for (const run of runs) {
|
|
86
|
+
if (run.role === "repair" || versionOf.get(run.id) !== family.root.id)
|
|
87
|
+
remediationRuns++;
|
|
88
|
+
if (run.providerStartedAt === null)
|
|
89
|
+
continue;
|
|
90
|
+
const accounting = runAccounting(store, run.id);
|
|
91
|
+
if (accounting === null || accounting.amount.kind === "unknown")
|
|
92
|
+
unknownRuns++;
|
|
93
|
+
else if (accounting.amount.kind === "api")
|
|
94
|
+
apiMicrousd += accounting.amount.microusd;
|
|
95
|
+
else {
|
|
96
|
+
subscriptionRuns++;
|
|
97
|
+
subscriptionTokens += (accounting.usage.in ?? 0) + (accounting.usage.out ?? 0);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
let agentTurns = 0, turnsUnknownRuns = 0;
|
|
101
|
+
for (const run of runs) {
|
|
102
|
+
if (run.providerStartedAt === null)
|
|
103
|
+
continue;
|
|
104
|
+
if (run.agentTurns == null)
|
|
105
|
+
turnsUnknownRuns++;
|
|
106
|
+
else
|
|
107
|
+
agentTurns += run.agentTurns;
|
|
108
|
+
}
|
|
109
|
+
const firstBuilt = runs.find(run => run.outcome === "built");
|
|
110
|
+
const firstCheck = firstBuilt === undefined ? null : store.runCheckFor(firstBuilt.id)?.status ?? null;
|
|
111
|
+
const personRevisions = store.handle.prepare("SELECT COUNT(*) AS n FROM task_ref WHERE id IN (SELECT value FROM json_each(?)) AND revision_origin = 'person'")
|
|
112
|
+
.get(JSON.stringify(family.versions.filter(one => one.id !== family.root.id).map(one => one.refId)))?.["n"];
|
|
113
|
+
const quiet = family.versions.every(one => one.state === "done" || one.state === "cancelled" || one.state === "failed") && runs.every(run => run.outcome !== null);
|
|
114
|
+
let modelFailures = 0, infraFailures = 0;
|
|
115
|
+
for (const run of runs) {
|
|
116
|
+
const failedCheck = run.outcome === "built" && store.runCheckFor(run.id)?.status === "failed";
|
|
117
|
+
if (!FAILED_OUTCOMES.has(run.outcome ?? "") && !failedCheck)
|
|
118
|
+
continue;
|
|
119
|
+
if (failedCheck || classOfRun(run, kept.get(run.id) ?? null) === "model")
|
|
120
|
+
modelFailures++;
|
|
121
|
+
else
|
|
122
|
+
infraFailures++;
|
|
123
|
+
}
|
|
124
|
+
const first = cohorts.get(runs[0].id);
|
|
125
|
+
const attributed = runs.map(run => cohorts.get(run.id)).filter((one) => one !== undefined);
|
|
126
|
+
const value = {
|
|
127
|
+
taskRef: family.root.refId, task: family.root.id, repo: family.root.repo, cohortSize: first?.cohortSize ?? null,
|
|
128
|
+
provider: first?.provider ?? runs[0].provider, model: first?.model ?? runs[0].model, state: "pending", reason: null, resultRun: null,
|
|
129
|
+
firstStartedAt: first?.startedAt ?? runs[0].startedAt, acceptedAt: null, decidedAt: null, modelFailures, infraFailures,
|
|
130
|
+
apiMicrousd, subscriptionRuns, subscriptionTokens, unknownRuns, remediationRuns,
|
|
131
|
+
selections: attributed.map(one => one.selection).filter((one) => one !== null), settled: false,
|
|
132
|
+
personRevisions: Number(personRevisions ?? 0), firstCheckPassed: firstCheck === "passed" ? true : firstCheck === "failed" ? false : null,
|
|
133
|
+
agentTurns, turnsUnknownRuns,
|
|
134
|
+
accumulationEnded: quiet || now.getTime() >= Date.parse(first?.startedAt ?? runs[0].startedAt) + ACCUMULATE_FOR_MS,
|
|
135
|
+
};
|
|
136
|
+
const reject = (reason) => ({ ...value, state: "rejected", reason, decidedAt: now.toISOString(), settled: true });
|
|
137
|
+
const notCounted = (reason, settled) => ({ ...value, state: "not-counted", reason, decidedAt: settled ? now.toISOString() : null, settled });
|
|
138
|
+
// 1. One cohort and one model, or the record can't say anything about a model.
|
|
139
|
+
if (attributed.length !== runs.length)
|
|
140
|
+
return notCounted("before-records", true);
|
|
141
|
+
// A family whose runs disagree (a revision re-sized large, one run under strict) is mixed; one whose runs all agree
|
|
142
|
+
// on not being eligible is not eligible.
|
|
143
|
+
if (new Set(attributed.map(one => `${one.repo ?? ""}\u0000${one.cohortSize ?? "not-eligible"}`)).size > 1)
|
|
144
|
+
return notCounted("mixed-cohort", true);
|
|
145
|
+
if (attributed.some(one => one.cohortSize === null))
|
|
146
|
+
return notCounted("not-eligible", true);
|
|
147
|
+
if (new Set(attributed.map(one => `${one.provider}\u0000${one.model ?? ""}`)).size > 1)
|
|
148
|
+
return notCounted("mixed-models", true);
|
|
149
|
+
for (const run of runs) {
|
|
150
|
+
if (run.providerStartedAt === null || run.outcome === null)
|
|
151
|
+
continue;
|
|
152
|
+
if (run.modelReported == null)
|
|
153
|
+
return notCounted("model-not-reported", true);
|
|
154
|
+
if (run.modelReported !== cohorts.get(run.id).model)
|
|
155
|
+
return notCounted("mixed-models", true);
|
|
156
|
+
}
|
|
157
|
+
// 2. A person who asked for changes, or a HIGH finding, rejected a result already: that stands whatever the family
|
|
158
|
+
// does next (its later runs still add to its spending).
|
|
159
|
+
if (value.personRevisions > 0)
|
|
160
|
+
return reject("person-revision");
|
|
161
|
+
if (highFinding(store, ids))
|
|
162
|
+
return reject("high-finding");
|
|
163
|
+
// 3. Still running: nothing to say yet.
|
|
164
|
+
if (runs.some(run => run.outcome === null))
|
|
165
|
+
return value;
|
|
166
|
+
const current = family.current;
|
|
167
|
+
const result = [...runs].reverse().find(run => versionOf.get(run.id) === current.id && run.role === "builder" && (run.outcome === "built" || run.outcome === "no-change"));
|
|
168
|
+
if (result === undefined) {
|
|
169
|
+
if (current.state === "failed")
|
|
170
|
+
return modelFailures === 0 && infraFailures > 0 ? notCounted("infrastructure-only", true) : reject("task-failed");
|
|
171
|
+
if (current.state === "cancelled")
|
|
172
|
+
return notCounted("cancelled-before-result", true);
|
|
173
|
+
return value;
|
|
174
|
+
}
|
|
175
|
+
value.resultRun = result.id;
|
|
176
|
+
// 4. Rejections, checked first among what a result can be.
|
|
177
|
+
if (modelFailures >= 2)
|
|
178
|
+
return reject("model-failures");
|
|
179
|
+
if (family.versions.some(one => one.state === "failed"))
|
|
180
|
+
return reject("task-failed");
|
|
181
|
+
if (current.state === "cancelled")
|
|
182
|
+
return reject("cancelled-after-result");
|
|
183
|
+
if (store.proofVerdictFor(result.id)?.verdict === "refuted")
|
|
184
|
+
return reject("proof-refuted");
|
|
185
|
+
const checks = evidenceRoot === undefined ? null : assignmentChecksForRun(store, evidenceRoot, result.id, now);
|
|
186
|
+
if (checks?.status === "failed")
|
|
187
|
+
return reject("check-failed");
|
|
188
|
+
// 5. Not counted.
|
|
189
|
+
const answerBy = Date.parse(result.finishedAt ?? result.startedAt) + UNANSWERED_AFTER_MS;
|
|
190
|
+
if (checks === null || checks.status === "unavailable")
|
|
191
|
+
return notCounted("check-unavailable", now.getTime() >= answerBy);
|
|
192
|
+
if (checks.status === "not-run")
|
|
193
|
+
return notCounted("no-check", now.getTime() >= answerBy);
|
|
194
|
+
const rules = value.repo === null ? null : store.approvalRules(value.repo);
|
|
195
|
+
// Post-build: each built run's own diff inventory; one that can't be read whole (or was never saved) is unknown.
|
|
196
|
+
for (const run of runs) {
|
|
197
|
+
if (run.outcome !== "built")
|
|
198
|
+
continue;
|
|
199
|
+
const changed = changedFilesOf(store, run.id, evidenceRoot);
|
|
200
|
+
if (changed === null)
|
|
201
|
+
return notCounted("changed-files-unknown", true);
|
|
202
|
+
if (rules !== null && protectedChanges(rules, changed).length > 0)
|
|
203
|
+
return notCounted("touched-protected", true);
|
|
204
|
+
}
|
|
205
|
+
// 6. Accepted by a person's completion of this result, or by its merge; then mature after 14 days.
|
|
206
|
+
const acceptedAt = acceptanceOf(store, family.root.id, current.id, result.id);
|
|
207
|
+
if (acceptedAt === null)
|
|
208
|
+
return now.getTime() >= answerBy ? notCounted("unanswered", true) : value;
|
|
209
|
+
const matureAt = Date.parse(acceptedAt) + MATURE_AFTER_MS;
|
|
210
|
+
if (now.getTime() < matureAt)
|
|
211
|
+
return { ...value, state: "provisional", acceptedAt };
|
|
212
|
+
return { ...value, state: "mature", acceptedAt, decidedAt: new Date(matureAt).toISOString(), settled: true };
|
|
213
|
+
}
|
|
214
|
+
/** Whether the automatic review graded any of these runs' results HIGH. */
|
|
215
|
+
function highFinding(store, runs) {
|
|
216
|
+
const rows = store.handle.prepare("SELECT findings_json FROM build_review WHERE run IN (SELECT value FROM json_each(?)) AND findings_json IS NOT NULL").all(JSON.stringify(runs));
|
|
217
|
+
return rows.some(row => {
|
|
218
|
+
try {
|
|
219
|
+
const findings = JSON.parse(String(row["findings_json"])).findings;
|
|
220
|
+
return Array.isArray(findings) && findings.some(one => one?.severity === "HIGH");
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return true; /* an unreadable review counts against, never for */
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
/** When a person accepted this result: the earliest person-proved completion of exactly this run (a machine's
|
|
228
|
+
* completion, a lead's, or Toolroll's own `system:landed` is not acceptance), or its merge. */
|
|
229
|
+
function acceptanceOf(store, rootId, currentId, runId) {
|
|
230
|
+
const completed = store.handle.prepare(`SELECT MIN(at) AS at FROM action_ledger WHERE action = ? AND source = 'work' AND task_id IN (?, ?)
|
|
231
|
+
AND run_id = ? AND detail = 'proof person' AND actor LIKE 'operator:%'`).get(COMPLETION_ACTION, rootId, currentId, runId)?.["at"];
|
|
232
|
+
const merged = store.handle.prepare(`SELECT MIN(COALESCE(m.settled_at, m.created_at)) AS at FROM merge_intent m JOIN publication p ON p.id = m.publication
|
|
233
|
+
WHERE p.run = ? AND m.state = 'merged'`).get(runId)?.["at"];
|
|
234
|
+
const times = [completed, merged].filter((one) => typeof one === "string" && Number.isFinite(Date.parse(one)));
|
|
235
|
+
return times.length === 0 ? null : times.sort()[0];
|
|
236
|
+
}
|
|
237
|
+
// ---- reading and re-evaluating ------------------------------------------------------------------------------------
|
|
238
|
+
/** The record in plain words, as the task page and `models results` say it. */
|
|
239
|
+
export function recordWords(record) {
|
|
240
|
+
const model = record.model ?? "this model";
|
|
241
|
+
if (record.state === "pending")
|
|
242
|
+
return record.resultRun === null ? "Pending: no result yet" : "Pending: waiting for the result to be accepted";
|
|
243
|
+
if (record.state === "provisional")
|
|
244
|
+
return `Accepted: counts for ${model} 14 days after acceptance`;
|
|
245
|
+
if (record.state === "mature")
|
|
246
|
+
return `Counts for ${model}: accepted, checks passed`;
|
|
247
|
+
const words = record.reason === null ? "" : REASON_WORDS[record.reason];
|
|
248
|
+
return record.state === "rejected" ? `Counts against ${model}: ${words}` : `Not counted: ${words}`;
|
|
249
|
+
}
|
|
250
|
+
/** A record row as saved, in the shape `models results --json` and the task page read. */
|
|
251
|
+
export function recordView(row, task) {
|
|
252
|
+
const n = (key) => Number(row[key] ?? 0);
|
|
253
|
+
const s = (key) => row[key] == null ? null : String(row[key]);
|
|
254
|
+
let selections = [];
|
|
255
|
+
try {
|
|
256
|
+
selections = JSON.parse(String(row["provenance_json"] ?? "[]")).flatMap(one => { const read = selectionSchema.safeParse(one); return read.success ? [read.data] : []; });
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
selections = [];
|
|
260
|
+
}
|
|
261
|
+
const state = String(row["state"]);
|
|
262
|
+
const reason = s("reason");
|
|
263
|
+
const view = {
|
|
264
|
+
task, repo: s("repo"), size: s("cohort_size"), provider: s("provider"), model: s("model"), state, reason,
|
|
265
|
+
resultRun: row["result_run"] == null ? null : n("result_run"), acceptedAt: s("accepted_at"), decidedAt: s("decided_at"),
|
|
266
|
+
};
|
|
267
|
+
return {
|
|
268
|
+
...view, words: recordWords(view), failures: { model: n("model_failures"), infrastructure: n("infra_failures") },
|
|
269
|
+
spend: { apiMicrousd: n("api_microusd"), subscriptionRuns: n("subscription_runs"), subscriptionTokens: n("subscription_tokens"), unknownRuns: n("unknown_runs"), remediationRuns: n("remediation_runs") },
|
|
270
|
+
selections, personRevisions: n("person_revisions"),
|
|
271
|
+
firstCheckPassed: row["first_check_passed"] == null ? null : Number(row["first_check_passed"]) === 1,
|
|
272
|
+
turns: { known: n("agent_turns"), unknownRuns: n("turns_unknown_runs") }, closed: n("closed") === 1,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
/** The value as a view, for a record evaluated now and not (yet) saved. */
|
|
276
|
+
export function valueView(value) {
|
|
277
|
+
return recordView({ repo: value.repo, cohort_size: value.cohortSize, provider: value.provider, model: value.model, state: value.state, reason: value.reason,
|
|
278
|
+
result_run: value.resultRun, accepted_at: value.acceptedAt, decided_at: value.decidedAt, model_failures: value.modelFailures, infra_failures: value.infraFailures,
|
|
279
|
+
api_microusd: value.apiMicrousd, subscription_runs: value.subscriptionRuns, subscription_tokens: value.subscriptionTokens, unknown_runs: value.unknownRuns,
|
|
280
|
+
remediation_runs: value.remediationRuns, provenance_json: JSON.stringify(value.selections), person_revisions: value.personRevisions,
|
|
281
|
+
first_check_passed: value.firstCheckPassed === null ? null : value.firstCheckPassed ? 1 : 0, agent_turns: value.agentTurns,
|
|
282
|
+
turns_unknown_runs: value.turnsUnknownRuns, closed: value.settled && value.accumulationEnded ? 1 : 0 }, value.task);
|
|
283
|
+
}
|
|
284
|
+
/** The saved records of these projects, newest first (by their family's first run). */
|
|
285
|
+
export function resultRecords(store, repos, limit = 200) {
|
|
286
|
+
return store.handle.prepare(`SELECT r.*, t.external_id AS task FROM route_record r JOIN task_ref t ON t.id = r.task_ref
|
|
287
|
+
WHERE r.repo IN (SELECT value FROM json_each(?)) ORDER BY r.first_started_at DESC, t.external_id DESC LIMIT ?`).all(JSON.stringify(repos), limit)
|
|
288
|
+
.map(row => recordView(row, String(row["task"])));
|
|
289
|
+
}
|
|
290
|
+
/** A task's record: its family root's saved record, or, before the first pass saves one, the record evaluated now. */
|
|
291
|
+
export function resultRecordOf(store, taskId, evidenceRoot, now) {
|
|
292
|
+
const family = store.taskFamilyOf(taskId, null, true);
|
|
293
|
+
if (family === null || family.problem !== null)
|
|
294
|
+
return null;
|
|
295
|
+
const row = store.handle.prepare("SELECT * FROM route_record WHERE task_ref = ?").get(family.root.refId);
|
|
296
|
+
if (row !== undefined)
|
|
297
|
+
return recordView(row, family.root.id);
|
|
298
|
+
const value = evaluateFamily(store, family.root.id, evidenceRoot, now);
|
|
299
|
+
return value === null ? null : valueView(value);
|
|
300
|
+
}
|
|
301
|
+
/** The task page's "Result record" line: the record's words, or null when the task has none (or it can't be read:
|
|
302
|
+
* a record never fails the page). */
|
|
303
|
+
export function resultRecordWordsOf(store, taskId, evidenceRoot, now) {
|
|
304
|
+
try {
|
|
305
|
+
return resultRecordOf(store, taskId, evidenceRoot, now)?.words ?? null;
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/** The families the re-evaluation pass looks at: a root task with a code-writing run that has a cohort snapshot, whose
|
|
312
|
+
* record is missing or not closed (a final verdict still gathers its family's later spending until it closes); the
|
|
313
|
+
* least recently evaluated first. */
|
|
314
|
+
export function familiesToEvaluate(store, repo, limit) {
|
|
315
|
+
return store.handle.prepare(`SELECT root.external_id AS task FROM run_cohort c JOIN task_ref root ON root.id = c.root_ref
|
|
316
|
+
LEFT JOIN route_record rec ON rec.task_ref = c.root_ref
|
|
317
|
+
WHERE c.repo = ? AND (rec.task_ref IS NULL OR rec.closed = 0)
|
|
318
|
+
GROUP BY c.root_ref ORDER BY COALESCE(MIN(rec.evaluated_at), ''), c.root_ref LIMIT ?`).all(repo, limit).map(row => String(row["task"]));
|
|
319
|
+
}
|
|
320
|
+
export { INELIGIBLE_REASONS };
|
|
321
|
+
/** The re-evaluation pass (the reconcile pass of each project, operate.ts): every family with a cohort snapshot whose
|
|
322
|
+
* record is missing or not closed is evaluated again from its rows, oldest evaluation first, at most `limit` a pass,
|
|
323
|
+
* and saved through route-store.ts. Each family is its own transaction, so one unreadable family never holds the others
|
|
324
|
+
* back. */
|
|
325
|
+
export function reevaluateResultRecords(store, repo, evidenceRoot, now, limit = 100) {
|
|
326
|
+
let saved = 0;
|
|
327
|
+
for (const task of familiesToEvaluate(store, repo, limit)) {
|
|
328
|
+
try {
|
|
329
|
+
store.transact(() => {
|
|
330
|
+
const value = evaluateFamily(store, task, evidenceRoot, now);
|
|
331
|
+
if (value !== null) {
|
|
332
|
+
saveResultRecord(store, value, now);
|
|
333
|
+
saved++;
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
catch { /* the next pass tries again */ }
|
|
338
|
+
}
|
|
339
|
+
return saved;
|
|
340
|
+
}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { type NotCountedReason } from "./contracts/route-results.js";
|
|
2
|
+
import { type PhaseRoute } from "./phase-routing.js";
|
|
3
|
+
import { type RuleDoor, type RuleRefusalReason } from "./route-store.js";
|
|
4
|
+
import { type Store } from "./store.js";
|
|
5
|
+
/** The policy version a grant is sealed under: a grant sealed under another version pauses its rule. */
|
|
6
|
+
export declare const ROUTING_POLICY_VERSION = 1;
|
|
7
|
+
/** When to suggest (Suggest and trigger terms): records of the last 90 days, the newest 20 decided. */
|
|
8
|
+
export declare const SUGGEST_TERMS: Readonly<{
|
|
9
|
+
windowDays: 90;
|
|
10
|
+
newest: 20;
|
|
11
|
+
minDecided: 10;
|
|
12
|
+
minMature: 0.9;
|
|
13
|
+
lastClean: 3;
|
|
14
|
+
configuredGap: 0.05;
|
|
15
|
+
maxNotCounted: number;
|
|
16
|
+
}>;
|
|
17
|
+
/** How long a grant lasts, how long Not now keeps a suggestion quiet, and when Needs you says a grant ends soon. */
|
|
18
|
+
export declare const GRANT_DAYS = 90;
|
|
19
|
+
export declare const NOT_NOW_DAYS = 30;
|
|
20
|
+
export declare const EXPIRY_WARNING_DAYS = 7;
|
|
21
|
+
/** A rule's switch-back terms, sealed at acceptance (route_rule.grant_json) and read only from there. */
|
|
22
|
+
export type Grant = {
|
|
23
|
+
policyVersion: number;
|
|
24
|
+
cutoff: string;
|
|
25
|
+
expires: string;
|
|
26
|
+
/** Switch off when more than `max` of the latest `window` decided records are rejected. */
|
|
27
|
+
reject: {
|
|
28
|
+
window: number;
|
|
29
|
+
max: number;
|
|
30
|
+
};
|
|
31
|
+
/** Once `window` decided records exist: switch off when fewer than `min` of the latest `window` are mature. */
|
|
32
|
+
mature: {
|
|
33
|
+
window: number;
|
|
34
|
+
min: number;
|
|
35
|
+
};
|
|
36
|
+
/** Switch off when more than `max` of the latest `window` closed exposures were approved with another model. */
|
|
37
|
+
declined: {
|
|
38
|
+
window: number;
|
|
39
|
+
max: number;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
export declare function grantFor(now: Date): Grant;
|
|
43
|
+
/** The switch-back terms in words, as the review shows them before acceptance and Settings after. */
|
|
44
|
+
export declare function grantWords(grant: Grant): string[];
|
|
45
|
+
export type RuleState = "active" | "trialling" | "paused" | "switched-off" | "turned-off" | "expired" | "dropped" | "finished";
|
|
46
|
+
export type RuleRow = {
|
|
47
|
+
id: number;
|
|
48
|
+
repo: string;
|
|
49
|
+
cohortSize: "small" | "medium";
|
|
50
|
+
kind: "rule" | "trial";
|
|
51
|
+
provider: string;
|
|
52
|
+
model: string;
|
|
53
|
+
configured: {
|
|
54
|
+
provider: string;
|
|
55
|
+
model: string | null;
|
|
56
|
+
};
|
|
57
|
+
state: RuleState;
|
|
58
|
+
generation: number;
|
|
59
|
+
grant: Grant | null;
|
|
60
|
+
prices: Prices | null;
|
|
61
|
+
depends: Record<string, number> | null;
|
|
62
|
+
acceptedBy: string;
|
|
63
|
+
acceptedAt: string;
|
|
64
|
+
endedAt: string | null;
|
|
65
|
+
endedBy: string | null;
|
|
66
|
+
endReason: string | null;
|
|
67
|
+
/** A trial's number of places (slice 3); null for a rule. */
|
|
68
|
+
limit: number | null;
|
|
69
|
+
};
|
|
70
|
+
/** Listed prices, in dollars per million tokens, as snapshotted. */
|
|
71
|
+
export type Prices = {
|
|
72
|
+
model: {
|
|
73
|
+
in: number;
|
|
74
|
+
out: number;
|
|
75
|
+
};
|
|
76
|
+
configured: {
|
|
77
|
+
in: number;
|
|
78
|
+
out: number;
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
export declare function ruleById(store: Store, id: number): RuleRow | null;
|
|
82
|
+
/** A project's rules a person sees: the live ones (active, paused) and, for 30 days, the ones that ended (a Not now
|
|
83
|
+
* offer is not a rule and is left out). */
|
|
84
|
+
export declare function rulesOf(store: Store, repo: string, now: Date): RuleRow[];
|
|
85
|
+
/** Whether a project offers suggestions (its "Suggest cheaper models" switch; a project not saved here reads on). */
|
|
86
|
+
export declare function suggestModelsOn(store: Store, repo: string): boolean;
|
|
87
|
+
/** The configuration generations a rule for this project depends on: the installation's and the project's build and
|
|
88
|
+
* repair rows. */
|
|
89
|
+
export declare function dependsOf(store: Store, repo: string): Record<string, number>;
|
|
90
|
+
export type Evidence = {
|
|
91
|
+
/** The newest decided records counted (at most SUGGEST_TERMS.newest), how many of them were accepted and checked. */
|
|
92
|
+
decided: number;
|
|
93
|
+
mature: number;
|
|
94
|
+
rejected: number;
|
|
95
|
+
/** Records in the window not counted, and their reasons with how many each. */
|
|
96
|
+
notCounted: number;
|
|
97
|
+
notCountedReasons: {
|
|
98
|
+
reason: NotCountedReason;
|
|
99
|
+
count: number;
|
|
100
|
+
}[];
|
|
101
|
+
total: number;
|
|
102
|
+
/** The newest decided tasks, by id, newest first. */
|
|
103
|
+
tasks: string[];
|
|
104
|
+
/** Observed median API dollars a task, over mature records priced in dollars; null below 5 such records. */
|
|
105
|
+
medianMicrousd: number | null;
|
|
106
|
+
};
|
|
107
|
+
/** The model's own name when the catalog knows it ("Claude Haiku 5.5"), else its id. */
|
|
108
|
+
export declare function modelName(store: Store, model: string): string;
|
|
109
|
+
export type Suggestion = {
|
|
110
|
+
/** `<size>:<model>:<offer>` — what the person reviews, and what accept and Not now name. */
|
|
111
|
+
id: string;
|
|
112
|
+
repo: string;
|
|
113
|
+
cohortSize: "small" | "medium";
|
|
114
|
+
provider: "claude";
|
|
115
|
+
model: string;
|
|
116
|
+
name: string;
|
|
117
|
+
configured: {
|
|
118
|
+
provider: string;
|
|
119
|
+
model: string;
|
|
120
|
+
name: string;
|
|
121
|
+
};
|
|
122
|
+
evidence: Evidence;
|
|
123
|
+
configuredEvidence: Evidence;
|
|
124
|
+
prices: Prices;
|
|
125
|
+
depends: Record<string, number>;
|
|
126
|
+
/** Three digests the offer binds: the evidence, the prices, and the configuration generations. */
|
|
127
|
+
offer: {
|
|
128
|
+
evidence: string;
|
|
129
|
+
prices: string;
|
|
130
|
+
depends: string;
|
|
131
|
+
};
|
|
132
|
+
grant: Grant;
|
|
133
|
+
};
|
|
134
|
+
/** What an offer for this cohort and model would say now, whether or not it qualifies: the parts its digests bind,
|
|
135
|
+
* and why it doesn't qualify (null: it does). */
|
|
136
|
+
export declare function offerNow(store: Store, repo: string, size: "small" | "medium", model: string, now: Date): {
|
|
137
|
+
suggestion: Suggestion | null;
|
|
138
|
+
why: string | null;
|
|
139
|
+
parts: Suggestion["offer"] | null;
|
|
140
|
+
};
|
|
141
|
+
/** A suggestion id read back: its cohort, model and the three digest prefixes it was offered under. */
|
|
142
|
+
export declare function readSuggestionId(id: string): {
|
|
143
|
+
size: "small" | "medium";
|
|
144
|
+
model: string;
|
|
145
|
+
evidence: string;
|
|
146
|
+
prices: string;
|
|
147
|
+
depends: string;
|
|
148
|
+
} | null;
|
|
149
|
+
/**
|
|
150
|
+
* The suggestions a project offers now: per size (small, routine), the cheapest Claude model whose records here earn
|
|
151
|
+
* it, unless a rule for that size is live, the project's switch is off, or a person said Not now within 30 days.
|
|
152
|
+
*/
|
|
153
|
+
export declare function suggestionsFor(store: Store, repo: string, now: Date): Suggestion[];
|
|
154
|
+
/** A suggestion in plain words, the evidence first. */
|
|
155
|
+
export declare function suggestionWords(suggestion: Suggestion): string;
|
|
156
|
+
export type RuleRefusal = {
|
|
157
|
+
ok: false;
|
|
158
|
+
reason: RuleRefusalReason | "stale-suggestion" | "price-changed" | "config-changed" | "invalid" | "taken" | "not-found" | "unknown-project";
|
|
159
|
+
problem: string;
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* Accept a suggestion as a rule, in one transaction: the person re-proved; the offer still earned, at the same
|
|
163
|
+
* prices and on the same configuration generations it was offered under; the configuration valid with the rule
|
|
164
|
+
* added (checkScope); and the size's one winner free. A refusal names which: `stale-suggestion`, `price-changed`,
|
|
165
|
+
* `config-changed`, `invalid` or `taken`.
|
|
166
|
+
*/
|
|
167
|
+
export declare function acceptSuggestion(store: Store, input: {
|
|
168
|
+
repo: string;
|
|
169
|
+
id: string;
|
|
170
|
+
}, door: RuleDoor, now: Date): {
|
|
171
|
+
ok: true;
|
|
172
|
+
rule: number;
|
|
173
|
+
suggestion: Suggestion;
|
|
174
|
+
} | RuleRefusal;
|
|
175
|
+
/** Turn a live rule off, or drop a paused one. Tasks already approved keep the model they were approved with. */
|
|
176
|
+
export declare function turnOffRule(store: Store, ruleId: number, door: RuleDoor, now: Date): {
|
|
177
|
+
ok: true;
|
|
178
|
+
rule: RuleRow;
|
|
179
|
+
} | RuleRefusal;
|
|
180
|
+
/** Keep a paused rule: live again on the configuration as it stands, if that configuration can run it. */
|
|
181
|
+
export declare function keepRule(store: Store, ruleId: number, door: RuleDoor, now: Date): {
|
|
182
|
+
ok: true;
|
|
183
|
+
rule: RuleRow;
|
|
184
|
+
} | RuleRefusal;
|
|
185
|
+
/** Not now: the suggestion stays quiet for 30 days. */
|
|
186
|
+
export declare function dismissSuggestion(store: Store, input: {
|
|
187
|
+
repo: string;
|
|
188
|
+
id: string;
|
|
189
|
+
}, door: RuleDoor, now: Date): {
|
|
190
|
+
ok: true;
|
|
191
|
+
} | RuleRefusal;
|
|
192
|
+
/** A project's "Suggest cheaper models" switch: on takes a person's step-up, off also the local owner. */
|
|
193
|
+
export declare function setSuggestModels(store: Store, repo: string, on: boolean, door: RuleDoor): {
|
|
194
|
+
ok: true;
|
|
195
|
+
} | RuleRefusal;
|
|
196
|
+
export type TripResult = {
|
|
197
|
+
trips: false;
|
|
198
|
+
} | {
|
|
199
|
+
trips: true;
|
|
200
|
+
why: string;
|
|
201
|
+
};
|
|
202
|
+
/** Whether a grant's sealed terms trip on these decided records (newest first) and closed exposures (newest first,
|
|
203
|
+
* true = approved with another model). Pure. Pending and provisional records are never passed in. */
|
|
204
|
+
export declare function tripOf(grant: Grant, decided: readonly ("mature" | "rejected")[], exposures: readonly boolean[]): TripResult;
|
|
205
|
+
export type SwitchOffOutcome = "kept" | "switched-off" | "paused" | "expired" | "moved";
|
|
206
|
+
/**
|
|
207
|
+
* One rule's check, as ONE transaction: re-read the rule and its generation; any configuration generation it depends
|
|
208
|
+
* on that moved pauses it (never a switch-off on terms it no longer has); an ended grant ends it, a grant sealed under
|
|
209
|
+
* another policy or by an account that's gone pauses it; then the trigger, recomputed from records with the sealed
|
|
210
|
+
* terms, switches it off by compare-and-swap on its generation — and the configuration is checked without it, or the
|
|
211
|
+
* switch-off rolls back and the rule pauses instead.
|
|
212
|
+
*/
|
|
213
|
+
export declare function switchOffIfTripped(store: Store, ruleId: number, now: Date): {
|
|
214
|
+
outcome: SwitchOffOutcome;
|
|
215
|
+
why: string | null;
|
|
216
|
+
};
|
|
217
|
+
/** Hold a task approved under a rule that switched itself off: nothing starts on it (no strike: nothing ran) until a
|
|
218
|
+
* person approves it again — the hold goes with that yes (store.ts sealScopeApproval). */
|
|
219
|
+
export declare function holdRuleOff(store: Store, taskRef: number, taskId: string, model: string, now: Date): void;
|
|
220
|
+
/**
|
|
221
|
+
* The reconcile pass's routing step (operate.ts, after the records are evaluated again): each live rule checked
|
|
222
|
+
* against its sealed terms; Needs you told about a rule that switched off, paused or ends soon; tasks approved under a
|
|
223
|
+
* switched-off rule held, with their plans re-filed on the configured builder so a person can approve them again; and
|
|
224
|
+
* plans still waiting for approval under a rule that is no longer live re-filed the same way. Each task is its own
|
|
225
|
+
* transaction; one that can't be re-filed now (a live claim) is tried again next pass.
|
|
226
|
+
*/
|
|
227
|
+
export declare function routeRulesPass(store: Store, repo: string, now: Date): {
|
|
228
|
+
switchedOff: number;
|
|
229
|
+
paused: number;
|
|
230
|
+
held: number;
|
|
231
|
+
refiled: number;
|
|
232
|
+
};
|
|
233
|
+
/**
|
|
234
|
+
* The evidence line beside a route whose build leg an earned rule selected (approval sheet, approval step, chat side
|
|
235
|
+
* panel, text card), or null for any other route. The selection is sealed; the counts read "so far", from the
|
|
236
|
+
* records as they stand.
|
|
237
|
+
*/
|
|
238
|
+
export declare function selectionEvidenceWords(store: Store, route: PhaseRoute | null, now: Date): string | null;
|
|
239
|
+
/** The evidence line for a task's proposed (or, once approved, sealed) route. */
|
|
240
|
+
export declare function taskEvidenceWords(store: Store, taskId: string, now: Date): string | null;
|
|
241
|
+
/** A rule in words for Settings and the command line. */
|
|
242
|
+
export declare function ruleWords(store: Store, rule: RuleRow): string;
|
|
243
|
+
/** When to offer a trial (docs/plans/results-routing.md, "Trials"): the configured Claude builder of a cohort has at
|
|
244
|
+
* least 10 decided records here at ≥ 90% accepted with checks passing, and a cheaper Claude model has too few results
|
|
245
|
+
* here to be suggested on its own. A trial tries it on the next 10 eligible tasks, each approved by a person. */
|
|
246
|
+
export declare const TRIAL_TERMS: Readonly<{
|
|
247
|
+
places: 10;
|
|
248
|
+
configuredMinDecided: 10;
|
|
249
|
+
configuredMinMature: 0.9;
|
|
250
|
+
days: 90;
|
|
251
|
+
}>;
|
|
252
|
+
export type TrialOffer = {
|
|
253
|
+
/** `trial:<size>:<model>:<offer>` — what the person starts, and what Not now names. */
|
|
254
|
+
id: string;
|
|
255
|
+
repo: string;
|
|
256
|
+
cohortSize: "small" | "medium";
|
|
257
|
+
provider: "claude";
|
|
258
|
+
model: string;
|
|
259
|
+
name: string;
|
|
260
|
+
configured: {
|
|
261
|
+
provider: string;
|
|
262
|
+
model: string;
|
|
263
|
+
name: string;
|
|
264
|
+
};
|
|
265
|
+
evidence: Evidence;
|
|
266
|
+
configuredEvidence: Evidence;
|
|
267
|
+
prices: Prices;
|
|
268
|
+
depends: Record<string, number>;
|
|
269
|
+
places: number;
|
|
270
|
+
offer: {
|
|
271
|
+
evidence: string;
|
|
272
|
+
prices: string;
|
|
273
|
+
depends: string;
|
|
274
|
+
};
|
|
275
|
+
};
|
|
276
|
+
/** A trial offer's id read back: its cohort, model and the three digest prefixes it was offered under. */
|
|
277
|
+
export declare function readTrialOfferId(id: string): {
|
|
278
|
+
size: "small" | "medium";
|
|
279
|
+
model: string;
|
|
280
|
+
evidence: string;
|
|
281
|
+
prices: string;
|
|
282
|
+
depends: string;
|
|
283
|
+
} | null;
|
|
284
|
+
/** What a trial offer for this cohort and model would say now, whether or not it qualifies: the parts its digests
|
|
285
|
+
* bind, and why it doesn't qualify (null: it does). */
|
|
286
|
+
export declare function trialOfferNow(store: Store, repo: string, size: "small" | "medium", model: string, now: Date): {
|
|
287
|
+
offer: TrialOffer | null;
|
|
288
|
+
why: string | null;
|
|
289
|
+
parts: TrialOffer["offer"] | null;
|
|
290
|
+
};
|
|
291
|
+
/**
|
|
292
|
+
* The trials a project offers now: per size, where nothing is suggested (a suggestion has enough evidence to stand on
|
|
293
|
+
* its own) and no rule or trial is live, the closest cheaper Claude model the catalog lists now (the dearest of those
|
|
294
|
+
* cheaper on both prices, then the newest), unless the project's switch is off or a person said Not now to it.
|
|
295
|
+
*/
|
|
296
|
+
export declare function trialOffersFor(store: Store, repo: string, now: Date): TrialOffer[];
|
|
297
|
+
/** A trial offer in plain words, the evidence first. */
|
|
298
|
+
export declare function trialOfferWords(offer: TrialOffer): string;
|
|
299
|
+
/** A project's trials a person sees: live (trialling, paused) and, for 30 days, the ones that ended. */
|
|
300
|
+
export declare function trialsOf(store: Store, repo: string, now: Date): RuleRow[];
|
|
301
|
+
/**
|
|
302
|
+
* Start a trial, in one transaction: the person re-proved (a step-up); the offer still standing, at the same prices
|
|
303
|
+
* and on the same configuration generations it was offered under, with its eligibility re-checked now (a stale
|
|
304
|
+
* offer refuses); the configuration valid with the trial's builder added (checkScope: repairs resume its session);
|
|
305
|
+
* and the size's one winner free (concurrent starts: one wins). A refusal names which.
|
|
306
|
+
*/
|
|
307
|
+
export declare function startTrial(store: Store, input: {
|
|
308
|
+
repo: string;
|
|
309
|
+
id: string;
|
|
310
|
+
}, door: RuleDoor, now: Date): {
|
|
311
|
+
ok: true;
|
|
312
|
+
trial: number;
|
|
313
|
+
offer: TrialOffer;
|
|
314
|
+
} | RuleRefusal;
|
|
315
|
+
/** Stop a live or paused trial. Plans waiting on it are re-filed on the configured builder; tasks a person already
|
|
316
|
+
* approved on it keep that model. */
|
|
317
|
+
export declare function stopTrial(store: Store, trialId: number, door: RuleDoor, now: Date): {
|
|
318
|
+
ok: true;
|
|
319
|
+
trial: RuleRow;
|
|
320
|
+
} | RuleRefusal;
|
|
321
|
+
/** A trial's progress in words, as Settings, the command line and chat say it. */
|
|
322
|
+
export declare function trialWords(store: Store, trial: RuleRow): string;
|
|
323
|
+
/** The project's live trial for a size, as Settings reads it (null when none). */
|
|
324
|
+
export declare function liveTrialOf(store: Store, repo: string, size: "small" | "medium"): RuleRow | null;
|