toolroll 0.9.71 → 0.9.73
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 +155 -0
- package/dist/agent-fence.d.ts +10 -2
- package/dist/agent-fence.js +47 -9
- package/dist/approval-authority.d.ts +20 -0
- package/dist/approval-authority.js +67 -0
- package/dist/assignment.js +26 -4
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +28 -28
- package/dist/builder.js +48 -6
- package/dist/chat-actions.js +13 -8
- package/dist/chat-decide.d.ts +47 -1
- package/dist/chat-decide.js +186 -5
- package/dist/check-access.js +2 -2
- package/dist/check-report.d.ts +109 -0
- package/dist/check-report.js +205 -0
- package/dist/claim.js +1 -1
- package/dist/coding-provider.js +4 -2
- package/dist/commit-extract.d.ts +111 -0
- package/dist/commit-extract.js +514 -0
- package/dist/contracts/console-api.d.ts +4 -4
- package/dist/contracts/console-api.js +5 -4
- package/dist/contracts/flow.d.ts +16 -0
- package/dist/contracts/flow.js +3 -0
- package/dist/contracts/release-check.d.ts +335 -0
- package/dist/contracts/release-check.js +159 -0
- package/dist/contracts/release-terms.d.ts +66 -0
- package/dist/contracts/release-terms.js +74 -0
- package/dist/contracts/verification-receipt.d.ts +42 -8
- package/dist/contracts/verification-receipt.js +24 -7
- package/dist/contracts/verification-view.d.ts +1 -0
- package/dist/deploy-gate.d.ts +17 -0
- package/dist/deploy-gate.js +68 -0
- package/dist/dispatch.js +3 -10
- package/dist/dispose.js +4 -1
- package/dist/evidence-pack.d.ts +1 -1
- package/dist/evidence-pack.js +20 -2
- package/dist/evidence.d.ts +1 -0
- package/dist/evidence.js +2 -1
- package/dist/exec.d.ts +1 -1
- package/dist/exec.js +7 -3
- package/dist/flow-engine.js +5 -1
- package/dist/flow-gallery-ui.js +4 -2
- package/dist/flow-gallery.d.ts +3 -0
- package/dist/flow-gallery.js +6 -2
- package/dist/flow-pull-request.d.ts +5 -2
- package/dist/flow-pull-request.js +14 -5
- package/dist/flow-schedule.js +25 -6
- package/dist/flow-triggers.d.ts +8 -0
- package/dist/flow-triggers.js +15 -0
- package/dist/flows-cli.js +13 -3
- package/dist/flows-ui.js +7 -1
- package/dist/guides.js +10 -1
- package/dist/invoke.d.ts +17 -6
- package/dist/invoke.js +48 -18
- package/dist/kits-ui.js +4 -2
- package/dist/kits.d.ts +6 -4
- package/dist/kits.js +9 -11
- package/dist/lead-status.js +5 -2
- package/dist/live-terminal.d.ts +10 -6
- package/dist/live-terminal.js +55 -14
- package/dist/mcp-connect.d.ts +10 -0
- package/dist/mcp-connect.js +2 -0
- package/dist/nightly-check.d.ts +104 -0
- package/dist/nightly-check.js +362 -0
- package/dist/operate.js +39 -2
- package/dist/person-proof.d.ts +2 -0
- package/dist/person-proof.js +14 -0
- package/dist/plan-auto.js +4 -4
- package/dist/planner-source.d.ts +1 -1
- package/dist/project-delete-ui.js +1 -1
- package/dist/project-tools.js +3 -1
- package/dist/proof.js +2 -0
- package/dist/protected-storage.d.ts +67 -0
- package/dist/protected-storage.js +398 -0
- package/dist/publish.js +156 -144
- package/dist/pull-request-flow.d.ts +2 -0
- package/dist/pull-request-flow.js +24 -2
- package/dist/release-adapters.d.ts +16 -1
- package/dist/release-adapters.js +292 -29
- package/dist/release-authority.d.ts +118 -0
- package/dist/release-authority.js +135 -0
- package/dist/release-card.d.ts +76 -0
- package/dist/release-card.js +252 -0
- package/dist/release-carry.d.ts +236 -0
- package/dist/release-carry.js +664 -0
- package/dist/release-cli.d.ts +3 -0
- package/dist/release-cli.js +158 -6
- package/dist/release-coverage.js +3 -0
- package/dist/release-git.d.ts +43 -0
- package/dist/release-git.js +106 -0
- package/dist/release-recheck.d.ts +146 -0
- package/dist/release-recheck.js +657 -0
- package/dist/release-view.d.ts +34 -0
- package/dist/release-view.js +27 -0
- package/dist/release.d.ts +139 -3
- package/dist/release.js +641 -250
- package/dist/remote-main.d.ts +2 -0
- package/dist/remote-main.js +1 -1
- package/dist/result-completion.d.ts +6 -0
- package/dist/result-completion.js +15 -1
- package/dist/result-follow-ups.js +29 -1
- package/dist/sandbox-runtime.d.ts +16 -5
- package/dist/sandbox-runtime.js +19 -6
- package/dist/scope.d.ts +122 -5
- package/dist/scope.js +236 -7
- package/dist/server/flows.js +40 -10
- package/dist/server/render-settings.js +1 -1
- package/dist/server/render-tasks.d.ts +8 -0
- package/dist/server/render-tasks.js +50 -9
- package/dist/server/route-table.js +1 -0
- package/dist/server/settings.js +26 -5
- package/dist/server/task-views.js +4 -0
- package/dist/server/tasks.js +12 -0
- package/dist/setup-guide.d.ts +2 -2
- package/dist/store.d.ts +230 -5
- package/dist/store.js +733 -37
- package/dist/subagent-memory.d.ts +1 -0
- package/dist/subagent-memory.js +1 -1
- package/dist/subagent-tools.d.ts +22 -6
- package/dist/subagent-tools.js +44 -8
- package/dist/subagent-work.d.ts +2 -0
- package/dist/subagent-work.js +5 -2
- package/dist/subagents-ui.js +6 -4
- package/dist/task-outcome-cli.js +7 -3
- package/dist/telegram.d.ts +6 -1
- package/dist/telegram.js +58 -6
- package/dist/toolroll-update.js +2 -2
- package/dist/verification-evidence.d.ts +25 -1
- package/dist/verification-evidence.js +57 -2
- package/dist/work-index.js +3 -6
- package/package.json +1 -1
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The release check's structured results (docs/plans/release-once.md, parts 2 and 3): what the orchestrator
|
|
3
|
+
* (`scripts/release-check.mjs`) reports on its last stdout line, what the plane seals before and after a check runs
|
|
4
|
+
* (the inventory, the execution context, a targeted plan and its obligation snapshot), and the identities every one of
|
|
5
|
+
* them names. Only the plane writes the sealed payloads, from typed values; the report comes from the check itself and
|
|
6
|
+
* is read strictly — anything that doesn't match is no report.
|
|
7
|
+
*
|
|
8
|
+
* Coverage trusts test code (Alex, 2026-10-25): a test runs as the same user as the harness, so these results guard
|
|
9
|
+
* against accidental damage — stray processes, leftover files, output that looks like a report — not a hostile test.
|
|
10
|
+
* Authority decisions never read them.
|
|
11
|
+
*/
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import { type ContractResult } from "./contract.js";
|
|
14
|
+
/** The parts a release check is made of. */
|
|
15
|
+
export declare const CHECK_PARTS: readonly ["typecheck", "build", "unit", "flows", "app", "flows-real", "app-real", "upgrade"];
|
|
16
|
+
export type CheckPart = typeof CHECK_PARTS[number];
|
|
17
|
+
export declare const JOURNEY_SCRIPTS: readonly ["app", "flows"];
|
|
18
|
+
export type JourneyScript = typeof JOURNEY_SCRIPTS[number];
|
|
19
|
+
/** What the orchestrator reports, as the last line of its output: `TOOLROLL_CHECK_REPORT <json>`, at most 256 KiB. */
|
|
20
|
+
export declare const CHECK_REPORT_PREFIX = "TOOLROLL_CHECK_REPORT ";
|
|
21
|
+
export declare const CHECK_REPORT_MAX_BYTES: number;
|
|
22
|
+
export declare const checkReportSchema: z.ZodObject<{
|
|
23
|
+
version: z.ZodLiteral<1>;
|
|
24
|
+
nonce: z.ZodString;
|
|
25
|
+
mode: z.ZodEnum<{
|
|
26
|
+
full: "full";
|
|
27
|
+
targeted: "targeted";
|
|
28
|
+
}>;
|
|
29
|
+
planDigest: z.ZodNullable<z.ZodString>;
|
|
30
|
+
exitCode: z.ZodInt;
|
|
31
|
+
parts: z.ZodArray<z.ZodObject<{
|
|
32
|
+
part: z.ZodEnum<{
|
|
33
|
+
app: "app";
|
|
34
|
+
"app-real": "app-real";
|
|
35
|
+
build: "build";
|
|
36
|
+
flows: "flows";
|
|
37
|
+
"flows-real": "flows-real";
|
|
38
|
+
typecheck: "typecheck";
|
|
39
|
+
unit: "unit";
|
|
40
|
+
upgrade: "upgrade";
|
|
41
|
+
}>;
|
|
42
|
+
selected: z.ZodBoolean;
|
|
43
|
+
ran: z.ZodBoolean;
|
|
44
|
+
code: z.ZodNullable<z.ZodInt>;
|
|
45
|
+
timedOut: z.ZodBoolean;
|
|
46
|
+
}, z.core.$strict>>;
|
|
47
|
+
units: z.ZodNullable<z.ZodObject<{
|
|
48
|
+
reusedFromCI: z.ZodBoolean;
|
|
49
|
+
finished: z.ZodBoolean;
|
|
50
|
+
runErrors: z.ZodInt;
|
|
51
|
+
files: z.ZodArray<z.ZodObject<{
|
|
52
|
+
file: z.ZodString;
|
|
53
|
+
state: z.ZodEnum<{
|
|
54
|
+
failed: "failed";
|
|
55
|
+
passed: "passed";
|
|
56
|
+
skipped: "skipped";
|
|
57
|
+
}>;
|
|
58
|
+
passed: z.ZodInt;
|
|
59
|
+
failed: z.ZodInt;
|
|
60
|
+
skipped: z.ZodInt;
|
|
61
|
+
flaky: z.ZodBoolean;
|
|
62
|
+
}, z.core.$strict>>;
|
|
63
|
+
identities: z.ZodArray<z.ZodObject<{
|
|
64
|
+
file: z.ZodString;
|
|
65
|
+
name: z.ZodString;
|
|
66
|
+
state: z.ZodEnum<{
|
|
67
|
+
failed: "failed";
|
|
68
|
+
passed: "passed";
|
|
69
|
+
skipped: "skipped";
|
|
70
|
+
}>;
|
|
71
|
+
flaky: z.ZodBoolean;
|
|
72
|
+
}, z.core.$strict>>;
|
|
73
|
+
}, z.core.$strict>>;
|
|
74
|
+
journeys: z.ZodArray<z.ZodObject<{
|
|
75
|
+
script: z.ZodEnum<{
|
|
76
|
+
app: "app";
|
|
77
|
+
flows: "flows";
|
|
78
|
+
}>;
|
|
79
|
+
kind: z.ZodEnum<{
|
|
80
|
+
real: "real";
|
|
81
|
+
scripted: "scripted";
|
|
82
|
+
}>;
|
|
83
|
+
finished: z.ZodBoolean;
|
|
84
|
+
groups: z.ZodArray<z.ZodObject<{
|
|
85
|
+
group: z.ZodString;
|
|
86
|
+
state: z.ZodEnum<{
|
|
87
|
+
failed: "failed";
|
|
88
|
+
"not-run": "not-run";
|
|
89
|
+
passed: "passed";
|
|
90
|
+
}>;
|
|
91
|
+
retried: z.ZodBoolean;
|
|
92
|
+
unattributed: z.ZodBoolean;
|
|
93
|
+
journeys: z.ZodArray<z.ZodObject<{
|
|
94
|
+
name: z.ZodString;
|
|
95
|
+
state: z.ZodEnum<{
|
|
96
|
+
failed: "failed";
|
|
97
|
+
passed: "passed";
|
|
98
|
+
skipped: "skipped";
|
|
99
|
+
}>;
|
|
100
|
+
}, z.core.$strict>>;
|
|
101
|
+
}, z.core.$strict>>;
|
|
102
|
+
}, z.core.$strict>>;
|
|
103
|
+
upgrades: z.ZodNullable<z.ZodObject<{
|
|
104
|
+
finished: z.ZodBoolean;
|
|
105
|
+
versions: z.ZodArray<z.ZodObject<{
|
|
106
|
+
version: z.ZodString;
|
|
107
|
+
state: z.ZodEnum<{
|
|
108
|
+
failed: "failed";
|
|
109
|
+
"not-run": "not-run";
|
|
110
|
+
passed: "passed";
|
|
111
|
+
}>;
|
|
112
|
+
}, z.core.$strict>>;
|
|
113
|
+
}, z.core.$strict>>;
|
|
114
|
+
}, z.core.$strict>;
|
|
115
|
+
export type CheckReport = z.infer<typeof checkReportSchema>;
|
|
116
|
+
export declare function readCheckReport(input: unknown): ContractResult<CheckReport>;
|
|
117
|
+
/** What a check must account for, sealed before it runs: the unit files (from the verified tree and Vitest's include
|
|
118
|
+
* globs) and the parts selected. The journey groups and upgrade versions come from the check's own report and are
|
|
119
|
+
* sealed beside these after it ran. */
|
|
120
|
+
export declare const checkInventorySchema: z.ZodObject<{
|
|
121
|
+
version: z.ZodLiteral<1>;
|
|
122
|
+
units: z.ZodArray<z.ZodString>;
|
|
123
|
+
parts: z.ZodArray<z.ZodEnum<{
|
|
124
|
+
app: "app";
|
|
125
|
+
"app-real": "app-real";
|
|
126
|
+
build: "build";
|
|
127
|
+
flows: "flows";
|
|
128
|
+
"flows-real": "flows-real";
|
|
129
|
+
typecheck: "typecheck";
|
|
130
|
+
unit: "unit";
|
|
131
|
+
upgrade: "upgrade";
|
|
132
|
+
}>>;
|
|
133
|
+
}, z.core.$strict>;
|
|
134
|
+
export type CheckInventory = z.infer<typeof checkInventorySchema>;
|
|
135
|
+
/** The sealed inventory artifact: what was sealed before the run, and what the run declared. */
|
|
136
|
+
export declare const sealedInventorySchema: z.ZodObject<{
|
|
137
|
+
version: z.ZodLiteral<1>;
|
|
138
|
+
sealed: z.ZodObject<{
|
|
139
|
+
version: z.ZodLiteral<1>;
|
|
140
|
+
units: z.ZodArray<z.ZodString>;
|
|
141
|
+
parts: z.ZodArray<z.ZodEnum<{
|
|
142
|
+
app: "app";
|
|
143
|
+
"app-real": "app-real";
|
|
144
|
+
build: "build";
|
|
145
|
+
flows: "flows";
|
|
146
|
+
"flows-real": "flows-real";
|
|
147
|
+
typecheck: "typecheck";
|
|
148
|
+
unit: "unit";
|
|
149
|
+
upgrade: "upgrade";
|
|
150
|
+
}>>;
|
|
151
|
+
}, z.core.$strict>;
|
|
152
|
+
declared: z.ZodObject<{
|
|
153
|
+
groups: z.ZodArray<z.ZodObject<{
|
|
154
|
+
script: z.ZodEnum<{
|
|
155
|
+
app: "app";
|
|
156
|
+
flows: "flows";
|
|
157
|
+
}>;
|
|
158
|
+
kind: z.ZodEnum<{
|
|
159
|
+
real: "real";
|
|
160
|
+
scripted: "scripted";
|
|
161
|
+
}>;
|
|
162
|
+
groups: z.ZodArray<z.ZodString>;
|
|
163
|
+
}, z.core.$strict>>;
|
|
164
|
+
versions: z.ZodArray<z.ZodString>;
|
|
165
|
+
}, z.core.$strict>;
|
|
166
|
+
}, z.core.$strict>;
|
|
167
|
+
export type SealedInventory = z.infer<typeof sealedInventorySchema>;
|
|
168
|
+
/** Where a check ran: what has to match for an earlier result to stand beside a later one. */
|
|
169
|
+
export declare const checkContextSchema: z.ZodObject<{
|
|
170
|
+
version: z.ZodLiteral<1>;
|
|
171
|
+
node: z.ZodString;
|
|
172
|
+
platform: z.ZodString;
|
|
173
|
+
arch: z.ZodString;
|
|
174
|
+
lockfile: z.ZodNullable<z.ZodString>;
|
|
175
|
+
harness: z.ZodString;
|
|
176
|
+
grant: z.ZodObject<{
|
|
177
|
+
id: z.ZodInt;
|
|
178
|
+
digest: z.ZodString;
|
|
179
|
+
}, z.core.$strict>;
|
|
180
|
+
access: z.ZodString;
|
|
181
|
+
}, z.core.$strict>;
|
|
182
|
+
export type CheckContext = z.infer<typeof checkContextSchema>;
|
|
183
|
+
/** A targeted check's plan: exactly what it runs, sealed before it runs. */
|
|
184
|
+
export declare const targetedPlanSchema: z.ZodObject<{
|
|
185
|
+
version: z.ZodLiteral<1>;
|
|
186
|
+
base: z.ZodInt;
|
|
187
|
+
units: z.ZodArray<z.ZodString>;
|
|
188
|
+
sanity: z.ZodArray<z.ZodString>;
|
|
189
|
+
groups: z.ZodArray<z.ZodObject<{
|
|
190
|
+
script: z.ZodEnum<{
|
|
191
|
+
app: "app";
|
|
192
|
+
flows: "flows";
|
|
193
|
+
}>;
|
|
194
|
+
kind: z.ZodEnum<{
|
|
195
|
+
real: "real";
|
|
196
|
+
scripted: "scripted";
|
|
197
|
+
}>;
|
|
198
|
+
groups: z.ZodArray<z.ZodString>;
|
|
199
|
+
}, z.core.$strict>>;
|
|
200
|
+
versions: z.ZodArray<z.ZodString>;
|
|
201
|
+
parts: z.ZodArray<z.ZodEnum<{
|
|
202
|
+
app: "app";
|
|
203
|
+
"app-real": "app-real";
|
|
204
|
+
build: "build";
|
|
205
|
+
flows: "flows";
|
|
206
|
+
"flows-real": "flows-real";
|
|
207
|
+
typecheck: "typecheck";
|
|
208
|
+
unit: "unit";
|
|
209
|
+
upgrade: "upgrade";
|
|
210
|
+
}>>;
|
|
211
|
+
required: z.ZodArray<z.ZodString>;
|
|
212
|
+
}, z.core.$strict>;
|
|
213
|
+
export type TargetedPlan = z.infer<typeof targetedPlanSchema>;
|
|
214
|
+
/** The earlier targeted runs of this generation a targeted attempt took into account, sealed with it. */
|
|
215
|
+
export declare const obligationSnapshotSchema: z.ZodObject<{
|
|
216
|
+
version: z.ZodLiteral<1>;
|
|
217
|
+
authority: z.ZodInt;
|
|
218
|
+
base: z.ZodInt;
|
|
219
|
+
earlier: z.ZodArray<z.ZodObject<{
|
|
220
|
+
run: z.ZodInt;
|
|
221
|
+
reportDigest: z.ZodNullable<z.ZodString>;
|
|
222
|
+
failed: z.ZodArray<z.ZodString>;
|
|
223
|
+
}, z.core.$strict>>;
|
|
224
|
+
}, z.core.$strict>;
|
|
225
|
+
export type ObligationSnapshot = z.infer<typeof obligationSnapshotSchema>;
|
|
226
|
+
/** What the plane writes on the check's stdin before closing it. */
|
|
227
|
+
export type CheckHandshake = {
|
|
228
|
+
nonce: string;
|
|
229
|
+
mode: "full" | "targeted";
|
|
230
|
+
plan: TargetedPlan | null;
|
|
231
|
+
watch: string[];
|
|
232
|
+
};
|
|
233
|
+
/** A unit test: its file and full name. */
|
|
234
|
+
export declare const unitIdentity: (file: string, testName: string) => string;
|
|
235
|
+
/** A unit file. */
|
|
236
|
+
export declare const fileIdentity: (file: string) => string;
|
|
237
|
+
/** A journey group of a script and kind. */
|
|
238
|
+
export declare const groupIdentity: (script: string, kind: string, group: string) => string;
|
|
239
|
+
/** One journey in its group. */
|
|
240
|
+
export declare const journeyIdentity: (script: string, kind: string, group: string, journey: string) => string;
|
|
241
|
+
/** An upgrade version. */
|
|
242
|
+
export declare const versionIdentity: (version: string) => string;
|
|
243
|
+
/** A whole part. */
|
|
244
|
+
export declare const partIdentity: (part: string) => string;
|
|
245
|
+
/** One fd-3 result line a harness process writes (test/release-reporter.ts, scripts/check-results.mjs). The
|
|
246
|
+
* orchestrator reads only these shapes; anything else on the pipe is passed over. */
|
|
247
|
+
export declare const checkResultLineSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
248
|
+
kind: z.ZodLiteral<"unit-case">;
|
|
249
|
+
file: z.ZodString;
|
|
250
|
+
name: z.ZodString;
|
|
251
|
+
state: z.ZodEnum<{
|
|
252
|
+
failed: "failed";
|
|
253
|
+
passed: "passed";
|
|
254
|
+
skipped: "skipped";
|
|
255
|
+
}>;
|
|
256
|
+
flaky: z.ZodBoolean;
|
|
257
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
258
|
+
kind: z.ZodLiteral<"unit-file">;
|
|
259
|
+
file: z.ZodString;
|
|
260
|
+
state: z.ZodEnum<{
|
|
261
|
+
failed: "failed";
|
|
262
|
+
passed: "passed";
|
|
263
|
+
skipped: "skipped";
|
|
264
|
+
}>;
|
|
265
|
+
passed: z.ZodInt;
|
|
266
|
+
failed: z.ZodInt;
|
|
267
|
+
skipped: z.ZodInt;
|
|
268
|
+
flaky: z.ZodBoolean;
|
|
269
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
270
|
+
kind: z.ZodLiteral<"unit-end">;
|
|
271
|
+
reason: z.ZodString;
|
|
272
|
+
errors: z.ZodInt;
|
|
273
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
274
|
+
kind: z.ZodLiteral<"journey-inventory">;
|
|
275
|
+
script: z.ZodEnum<{
|
|
276
|
+
app: "app";
|
|
277
|
+
flows: "flows";
|
|
278
|
+
}>;
|
|
279
|
+
journeys: z.ZodString;
|
|
280
|
+
groups: z.ZodArray<z.ZodString>;
|
|
281
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
282
|
+
kind: z.ZodLiteral<"journey">;
|
|
283
|
+
name: z.ZodString;
|
|
284
|
+
state: z.ZodEnum<{
|
|
285
|
+
failed: "failed";
|
|
286
|
+
passed: "passed";
|
|
287
|
+
skipped: "skipped";
|
|
288
|
+
}>;
|
|
289
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
290
|
+
kind: z.ZodLiteral<"journey-end">;
|
|
291
|
+
script: z.ZodOptional<z.ZodEnum<{
|
|
292
|
+
app: "app";
|
|
293
|
+
flows: "flows";
|
|
294
|
+
}>>;
|
|
295
|
+
journeys: z.ZodOptional<z.ZodString>;
|
|
296
|
+
browserCheck: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
297
|
+
unstopped: z.ZodOptional<z.ZodBoolean>;
|
|
298
|
+
catalogue: z.ZodOptional<z.ZodInt>;
|
|
299
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
300
|
+
kind: z.ZodLiteral<"journey-group">;
|
|
301
|
+
script: z.ZodEnum<{
|
|
302
|
+
app: "app";
|
|
303
|
+
flows: "flows";
|
|
304
|
+
}>;
|
|
305
|
+
journeys: z.ZodString;
|
|
306
|
+
group: z.ZodString;
|
|
307
|
+
state: z.ZodEnum<{
|
|
308
|
+
failed: "failed";
|
|
309
|
+
"not-run": "not-run";
|
|
310
|
+
passed: "passed";
|
|
311
|
+
}>;
|
|
312
|
+
retried: z.ZodBoolean;
|
|
313
|
+
unattributed: z.ZodBoolean;
|
|
314
|
+
results: z.ZodArray<z.ZodObject<{
|
|
315
|
+
name: z.ZodString;
|
|
316
|
+
state: z.ZodEnum<{
|
|
317
|
+
failed: "failed";
|
|
318
|
+
passed: "passed";
|
|
319
|
+
skipped: "skipped";
|
|
320
|
+
}>;
|
|
321
|
+
}, z.core.$strict>>;
|
|
322
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
323
|
+
kind: z.ZodLiteral<"upgrade-inventory">;
|
|
324
|
+
versions: z.ZodArray<z.ZodString>;
|
|
325
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
326
|
+
kind: z.ZodLiteral<"upgrade-version">;
|
|
327
|
+
version: z.ZodString;
|
|
328
|
+
state: z.ZodEnum<{
|
|
329
|
+
failed: "failed";
|
|
330
|
+
"not-run": "not-run";
|
|
331
|
+
passed: "passed";
|
|
332
|
+
}>;
|
|
333
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
334
|
+
kind: z.ZodLiteral<"upgrade-end">;
|
|
335
|
+
}, z.core.$strict>], "kind">;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The release check's structured results (docs/plans/release-once.md, parts 2 and 3): what the orchestrator
|
|
3
|
+
* (`scripts/release-check.mjs`) reports on its last stdout line, what the plane seals before and after a check runs
|
|
4
|
+
* (the inventory, the execution context, a targeted plan and its obligation snapshot), and the identities every one of
|
|
5
|
+
* them names. Only the plane writes the sealed payloads, from typed values; the report comes from the check itself and
|
|
6
|
+
* is read strictly — anything that doesn't match is no report.
|
|
7
|
+
*
|
|
8
|
+
* Coverage trusts test code (Alex, 2026-10-25): a test runs as the same user as the harness, so these results guard
|
|
9
|
+
* against accidental damage — stray processes, leftover files, output that looks like a report — not a hostile test.
|
|
10
|
+
* Authority decisions never read them.
|
|
11
|
+
*/
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import { readVersioned, versioned } from "./contract.js";
|
|
14
|
+
/** The parts a release check is made of. */
|
|
15
|
+
export const CHECK_PARTS = ["typecheck", "build", "unit", "flows", "app", "flows-real", "app-real", "upgrade"];
|
|
16
|
+
export const JOURNEY_SCRIPTS = ["app", "flows"];
|
|
17
|
+
const hex64 = z.string().regex(/^[0-9a-f]{64}$/);
|
|
18
|
+
/** A repository path as a tree names it: relative, normalized, no control characters. */
|
|
19
|
+
const repoPath = z.string().min(1).max(512).regex(/^(?!\/)(?!.*(?:^|\/)\.\.?(?:\/|$))[^\u0000-\u001f\\]+$/);
|
|
20
|
+
const name = z.string().min(1).max(2000);
|
|
21
|
+
const groupName = z.string().min(1).max(200).regex(/^[A-Za-z0-9._/-]+$/);
|
|
22
|
+
const versionName = z.string().regex(/^\d+\.\d+\.\d+$/);
|
|
23
|
+
const count = z.int().min(0).max(1_000_000);
|
|
24
|
+
/** What the orchestrator reports, as the last line of its output: `TOOLROLL_CHECK_REPORT <json>`, at most 256 KiB. */
|
|
25
|
+
export const CHECK_REPORT_PREFIX = "TOOLROLL_CHECK_REPORT ";
|
|
26
|
+
export const CHECK_REPORT_MAX_BYTES = 256 * 1024;
|
|
27
|
+
export const checkReportSchema = versioned(1, {
|
|
28
|
+
/** The nonce the plane wrote on the check's stdin: the report is this run's. */
|
|
29
|
+
nonce: hex64,
|
|
30
|
+
mode: z.enum(["full", "targeted"]),
|
|
31
|
+
/** The digest of the targeted plan the orchestrator ran, as it received it; null for a full check. */
|
|
32
|
+
planDigest: hex64.nullable(),
|
|
33
|
+
/** The orchestrator's own exit code. */
|
|
34
|
+
exitCode: z.int().min(0).max(255),
|
|
35
|
+
parts: z.array(z.strictObject({
|
|
36
|
+
part: z.enum(CHECK_PARTS),
|
|
37
|
+
selected: z.boolean(),
|
|
38
|
+
ran: z.boolean(),
|
|
39
|
+
code: z.int().min(0).max(255).nullable(),
|
|
40
|
+
timedOut: z.boolean(),
|
|
41
|
+
})).max(CHECK_PARTS.length),
|
|
42
|
+
/** The unit suite, from the Vitest main process's fd-3 lines: every file's outcome, and the identities that failed,
|
|
43
|
+
* were skipped, passed on a retry or were asked about. Null when the unit part didn't run. */
|
|
44
|
+
units: z.strictObject({
|
|
45
|
+
reusedFromCI: z.boolean(),
|
|
46
|
+
/** Vitest reached the end of its run and said so. */
|
|
47
|
+
finished: z.boolean(),
|
|
48
|
+
/** Errors no test owned (an unhandled rejection, a worker crash). */
|
|
49
|
+
runErrors: count,
|
|
50
|
+
files: z.array(z.strictObject({
|
|
51
|
+
file: repoPath,
|
|
52
|
+
state: z.enum(["passed", "failed", "skipped"]),
|
|
53
|
+
passed: count, failed: count, skipped: count,
|
|
54
|
+
flaky: z.boolean(),
|
|
55
|
+
})).max(5000),
|
|
56
|
+
identities: z.array(z.strictObject({
|
|
57
|
+
file: repoPath,
|
|
58
|
+
name,
|
|
59
|
+
state: z.enum(["passed", "failed", "skipped"]),
|
|
60
|
+
flaky: z.boolean(),
|
|
61
|
+
})).max(5000),
|
|
62
|
+
}).nullable(),
|
|
63
|
+
/** Each journey script's run: the groups it ran and every journey's outcome in each. */
|
|
64
|
+
journeys: z.array(z.strictObject({
|
|
65
|
+
script: z.enum(JOURNEY_SCRIPTS),
|
|
66
|
+
kind: z.enum(["scripted", "real"]),
|
|
67
|
+
finished: z.boolean(),
|
|
68
|
+
groups: z.array(z.strictObject({
|
|
69
|
+
group: groupName,
|
|
70
|
+
state: z.enum(["passed", "failed", "not-run"]),
|
|
71
|
+
/** Passed only on the runner's retry. */
|
|
72
|
+
retried: z.boolean(),
|
|
73
|
+
/** It ended with no result line to read, or only the browser-error check failed. */
|
|
74
|
+
unattributed: z.boolean(),
|
|
75
|
+
journeys: z.array(z.strictObject({ name, state: z.enum(["passed", "failed", "skipped"]) })).max(500),
|
|
76
|
+
})).max(200),
|
|
77
|
+
})).max(4),
|
|
78
|
+
upgrades: z.strictObject({
|
|
79
|
+
finished: z.boolean(),
|
|
80
|
+
versions: z.array(z.strictObject({ version: versionName, state: z.enum(["passed", "failed", "not-run"]) })).max(500),
|
|
81
|
+
}).nullable(),
|
|
82
|
+
});
|
|
83
|
+
export function readCheckReport(input) {
|
|
84
|
+
return readVersioned(checkReportSchema, input);
|
|
85
|
+
}
|
|
86
|
+
/** What a check must account for, sealed before it runs: the unit files (from the verified tree and Vitest's include
|
|
87
|
+
* globs) and the parts selected. The journey groups and upgrade versions come from the check's own report and are
|
|
88
|
+
* sealed beside these after it ran. */
|
|
89
|
+
export const checkInventorySchema = versioned(1, {
|
|
90
|
+
units: z.array(repoPath).max(20_000),
|
|
91
|
+
parts: z.array(z.enum(CHECK_PARTS)).max(CHECK_PARTS.length),
|
|
92
|
+
});
|
|
93
|
+
/** The sealed inventory artifact: what was sealed before the run, and what the run declared. */
|
|
94
|
+
export const sealedInventorySchema = versioned(1, {
|
|
95
|
+
sealed: checkInventorySchema,
|
|
96
|
+
declared: z.strictObject({
|
|
97
|
+
groups: z.array(z.strictObject({ script: z.enum(JOURNEY_SCRIPTS), kind: z.enum(["scripted", "real"]), groups: z.array(groupName).max(200) })).max(4),
|
|
98
|
+
versions: z.array(versionName).max(500),
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
/** Where a check ran: what has to match for an earlier result to stand beside a later one. */
|
|
102
|
+
export const checkContextSchema = versioned(1, {
|
|
103
|
+
node: z.string().max(40),
|
|
104
|
+
platform: z.string().max(40),
|
|
105
|
+
arch: z.string().max(40),
|
|
106
|
+
/** The verified blob id of package-lock.json, or null when the tree has none. */
|
|
107
|
+
lockfile: z.string().max(64).nullable(),
|
|
108
|
+
/** One digest over every harness file's verified blob id. */
|
|
109
|
+
harness: hex64,
|
|
110
|
+
grant: z.strictObject({ id: z.int(), digest: z.string().max(128) }),
|
|
111
|
+
access: z.string().max(40),
|
|
112
|
+
});
|
|
113
|
+
/** A targeted check's plan: exactly what it runs, sealed before it runs. */
|
|
114
|
+
export const targetedPlanSchema = versioned(1, {
|
|
115
|
+
/** The full check A whose sealed evidence every obligation comes from. */
|
|
116
|
+
base: z.int().min(1),
|
|
117
|
+
units: z.array(repoPath).max(5000),
|
|
118
|
+
sanity: z.array(repoPath).max(200),
|
|
119
|
+
groups: z.array(z.strictObject({ script: z.enum(JOURNEY_SCRIPTS), kind: z.enum(["scripted", "real"]), groups: z.array(groupName).max(200) })).max(4),
|
|
120
|
+
versions: z.array(versionName).max(500),
|
|
121
|
+
/** Whole parts to run again (a part-level outcome at A). */
|
|
122
|
+
parts: z.array(z.enum(CHECK_PARTS)).max(CHECK_PARTS.length),
|
|
123
|
+
/** The required identities: each must be reported, and pass. */
|
|
124
|
+
required: z.array(z.string().min(1).max(2600)).max(5000),
|
|
125
|
+
});
|
|
126
|
+
/** The earlier targeted runs of this generation a targeted attempt took into account, sealed with it. */
|
|
127
|
+
export const obligationSnapshotSchema = versioned(1, {
|
|
128
|
+
authority: z.int().min(1),
|
|
129
|
+
base: z.int().min(1),
|
|
130
|
+
earlier: z.array(z.strictObject({ run: z.int().min(1), reportDigest: z.string().max(64).nullable(), failed: z.array(z.string().min(1).max(2600)).max(5000) })).max(200),
|
|
131
|
+
});
|
|
132
|
+
// ------------------------------------------------------------------ identities
|
|
133
|
+
/** A unit test: its file and full name. */
|
|
134
|
+
export const unitIdentity = (file, testName) => `unit:${file}::${testName}`;
|
|
135
|
+
/** A unit file. */
|
|
136
|
+
export const fileIdentity = (file) => `file:${file}`;
|
|
137
|
+
/** A journey group of a script and kind. */
|
|
138
|
+
export const groupIdentity = (script, kind, group) => `group:${script}:${kind}:${group}`;
|
|
139
|
+
/** One journey in its group. */
|
|
140
|
+
export const journeyIdentity = (script, kind, group, journey) => `journey:${script}:${kind}:${group}::${journey}`;
|
|
141
|
+
/** An upgrade version. */
|
|
142
|
+
export const versionIdentity = (version) => `upgrade:${version}`;
|
|
143
|
+
/** A whole part. */
|
|
144
|
+
export const partIdentity = (part) => `part:${part}`;
|
|
145
|
+
/** One fd-3 result line a harness process writes (test/release-reporter.ts, scripts/check-results.mjs). The
|
|
146
|
+
* orchestrator reads only these shapes; anything else on the pipe is passed over. */
|
|
147
|
+
export const checkResultLineSchema = z.discriminatedUnion("kind", [
|
|
148
|
+
z.strictObject({ kind: z.literal("unit-case"), file: repoPath, name, state: z.enum(["passed", "failed", "skipped"]), flaky: z.boolean() }),
|
|
149
|
+
z.strictObject({ kind: z.literal("unit-file"), file: repoPath, state: z.enum(["passed", "failed", "skipped"]), passed: count, failed: count, skipped: count, flaky: z.boolean() }),
|
|
150
|
+
z.strictObject({ kind: z.literal("unit-end"), reason: z.string().max(40), errors: count }),
|
|
151
|
+
z.strictObject({ kind: z.literal("journey-inventory"), script: z.enum(JOURNEY_SCRIPTS), journeys: z.string().max(20), groups: z.array(groupName).max(200) }),
|
|
152
|
+
z.strictObject({ kind: z.literal("journey"), name, state: z.enum(["passed", "failed", "skipped"]) }),
|
|
153
|
+
z.strictObject({ kind: z.literal("journey-end"), script: z.enum(JOURNEY_SCRIPTS).optional(), journeys: z.string().max(20).optional(), browserCheck: z.string().nullable().optional(), unstopped: z.boolean().optional(), catalogue: count.optional() }),
|
|
154
|
+
z.strictObject({ kind: z.literal("journey-group"), script: z.enum(JOURNEY_SCRIPTS), journeys: z.string().max(20), group: groupName, state: z.enum(["passed", "failed", "not-run"]), retried: z.boolean(), unattributed: z.boolean(),
|
|
155
|
+
results: z.array(z.strictObject({ name, state: z.enum(["passed", "failed", "skipped"]) })).max(500) }),
|
|
156
|
+
z.strictObject({ kind: z.literal("upgrade-inventory"), versions: z.array(versionName).max(500) }),
|
|
157
|
+
z.strictObject({ kind: z.literal("upgrade-version"), version: versionName, state: z.enum(["passed", "failed", "not-run"]) }),
|
|
158
|
+
z.strictObject({ kind: z.literal("upgrade-end") }),
|
|
159
|
+
]);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type ContractResult } from "./contract.js";
|
|
3
|
+
/** The carry rules this runtime knows (release-carry.ts CARRY_RULES). A sealed policy naming any other refuses. */
|
|
4
|
+
export declare const CARRY_RULES_VERSION = 1;
|
|
5
|
+
export declare const releasePolicySchema: z.ZodObject<{
|
|
6
|
+
version: z.ZodLiteral<1>;
|
|
7
|
+
rules: z.ZodLiteral<1>;
|
|
8
|
+
carry: z.ZodBoolean;
|
|
9
|
+
protectProject: z.ZodBoolean;
|
|
10
|
+
protectedPaths: z.ZodArray<z.ZodString>;
|
|
11
|
+
limits: z.ZodObject<{
|
|
12
|
+
lines: z.ZodInt;
|
|
13
|
+
files: z.ZodInt;
|
|
14
|
+
churn: z.ZodInt;
|
|
15
|
+
}, z.core.$strict>;
|
|
16
|
+
unsandboxedCarry: z.ZodOptional<z.ZodBoolean>;
|
|
17
|
+
}, z.core.$strict>;
|
|
18
|
+
export type ReleasePolicy = z.infer<typeof releasePolicySchema>;
|
|
19
|
+
export declare const releaseTermsSchema: z.ZodObject<{
|
|
20
|
+
version: z.ZodLiteral<1>;
|
|
21
|
+
repo: z.ZodString;
|
|
22
|
+
packageVersion: z.ZodString;
|
|
23
|
+
branch: z.ZodString;
|
|
24
|
+
pr: z.ZodInt;
|
|
25
|
+
base: z.ZodLiteral<"main">;
|
|
26
|
+
approvedSha: z.ZodString;
|
|
27
|
+
approvedTree: z.ZodString;
|
|
28
|
+
policy: z.ZodObject<{
|
|
29
|
+
version: z.ZodLiteral<1>;
|
|
30
|
+
rules: z.ZodLiteral<1>;
|
|
31
|
+
carry: z.ZodBoolean;
|
|
32
|
+
protectProject: z.ZodBoolean;
|
|
33
|
+
protectedPaths: z.ZodArray<z.ZodString>;
|
|
34
|
+
limits: z.ZodObject<{
|
|
35
|
+
lines: z.ZodInt;
|
|
36
|
+
files: z.ZodInt;
|
|
37
|
+
churn: z.ZodInt;
|
|
38
|
+
}, z.core.$strict>;
|
|
39
|
+
unsandboxedCarry: z.ZodOptional<z.ZodBoolean>;
|
|
40
|
+
}, z.core.$strict>;
|
|
41
|
+
grant: z.ZodObject<{
|
|
42
|
+
id: z.ZodInt;
|
|
43
|
+
digest: z.ZodString;
|
|
44
|
+
}, z.core.$strict>;
|
|
45
|
+
access: z.ZodEnum<{
|
|
46
|
+
sandboxed: "sandboxed";
|
|
47
|
+
"sign-ins": "sign-ins";
|
|
48
|
+
unsandboxed: "unsandboxed";
|
|
49
|
+
}>;
|
|
50
|
+
expiresAt: z.ZodISODateTime;
|
|
51
|
+
}, z.core.$strict>;
|
|
52
|
+
export type ReleaseTerms = z.infer<typeof releaseTermsSchema>;
|
|
53
|
+
export declare function readReleaseTerms(input: unknown): ContractResult<ReleaseTerms>;
|
|
54
|
+
export declare function readReleasePolicy(input: unknown): ContractResult<ReleasePolicy>;
|
|
55
|
+
/** The digest a scope binds for its release terms, domain-separated from every other digest input. */
|
|
56
|
+
export declare function releaseTermsDigest(terms: ReleaseTerms): string;
|
|
57
|
+
/** The digest of an effective policy, as release_authority.policy_digest holds it. */
|
|
58
|
+
export declare function releasePolicyDigest(policy: ReleasePolicy): string;
|
|
59
|
+
/** Strict stored terms: null column → null; anything that isn't exactly terms → a problem. */
|
|
60
|
+
export declare function storedReleaseTerms(raw: unknown): {
|
|
61
|
+
ok: true;
|
|
62
|
+
terms: ReleaseTerms | null;
|
|
63
|
+
} | {
|
|
64
|
+
ok: false;
|
|
65
|
+
problem: string;
|
|
66
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A release scope's terms (docs/plans/release-once.md, part 1): what a person agrees to when they approve a release —
|
|
3
|
+
* the repository (from Toolroll's records), version, head branch, pull request and base, the approved commit and its
|
|
4
|
+
* tree, the effective carry policy, the release check grant, the sealed check access (fenced) and the expiry. Part of
|
|
5
|
+
* the scope digest when present; read strictly, so a row that doesn't match is a stated problem, never authority.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { readVersioned, versioned } from "./contract.js";
|
|
10
|
+
/** The carry rules this runtime knows (release-carry.ts CARRY_RULES). A sealed policy naming any other refuses. */
|
|
11
|
+
export const CARRY_RULES_VERSION = 1;
|
|
12
|
+
export const releasePolicySchema = versioned(1, {
|
|
13
|
+
rules: z.literal(CARRY_RULES_VERSION),
|
|
14
|
+
/** False: every later commit asks again ("Ask for every release commit", or `toolroll release --no-carry`). */
|
|
15
|
+
carry: z.boolean(),
|
|
16
|
+
/** The project's own protection: everything (protectProject) or these paths, which never carry. */
|
|
17
|
+
protectProject: z.boolean(),
|
|
18
|
+
protectedPaths: z.array(z.string().min(1).max(400)).max(200),
|
|
19
|
+
limits: z.strictObject({ lines: z.int().min(0).max(1_000_000), files: z.int().min(0).max(100_000), churn: z.int().min(0).max(1_000_000) }),
|
|
20
|
+
/** "Carry release approvals when checks aren't sandboxed" (a person's project setting, off by default). Absent (a
|
|
21
|
+
* policy sealed before it existed): off. */
|
|
22
|
+
unsandboxedCarry: z.boolean().optional(),
|
|
23
|
+
});
|
|
24
|
+
const sha = z.string().regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/);
|
|
25
|
+
export const releaseTermsSchema = versioned(1, {
|
|
26
|
+
repo: z.string().min(1).max(4096),
|
|
27
|
+
packageVersion: z.string().regex(/^\d+\.\d+\.\d+$/),
|
|
28
|
+
branch: z.string().min(1).max(255).regex(/^[A-Za-z0-9._/-]+$/),
|
|
29
|
+
pr: z.int().min(1),
|
|
30
|
+
base: z.literal("main"),
|
|
31
|
+
approvedSha: sha,
|
|
32
|
+
approvedTree: sha,
|
|
33
|
+
policy: releasePolicySchema,
|
|
34
|
+
grant: z.strictObject({ id: z.int(), digest: z.string().min(1).max(128) }),
|
|
35
|
+
/** The check access the release check runs with at approval: the fence, or (only with unsandboxedCarry sealed in the
|
|
36
|
+
* policy) a wider one. Never wider later: a check that reaches further than this stops the release. */
|
|
37
|
+
access: z.enum(["sandboxed", "sign-ins", "unsandboxed"]),
|
|
38
|
+
expiresAt: z.iso.datetime(),
|
|
39
|
+
});
|
|
40
|
+
export function readReleaseTerms(input) {
|
|
41
|
+
return readVersioned(releaseTermsSchema, input);
|
|
42
|
+
}
|
|
43
|
+
export function readReleasePolicy(input) {
|
|
44
|
+
return readVersioned(releasePolicySchema, input);
|
|
45
|
+
}
|
|
46
|
+
/** The digest a scope binds for its release terms, domain-separated from every other digest input. */
|
|
47
|
+
export function releaseTermsDigest(terms) {
|
|
48
|
+
return createHash("sha256").update(`release-terms:${JSON.stringify(terms)}`).digest("hex");
|
|
49
|
+
}
|
|
50
|
+
/** The digest of an effective policy, as release_authority.policy_digest holds it. */
|
|
51
|
+
export function releasePolicyDigest(policy) {
|
|
52
|
+
return createHash("sha256").update(`release-policy:${JSON.stringify(policy)}`).digest("hex");
|
|
53
|
+
}
|
|
54
|
+
/** Strict stored terms: null column → null; anything that isn't exactly terms → a problem. */
|
|
55
|
+
export function storedReleaseTerms(raw) {
|
|
56
|
+
if (raw === null || raw === undefined)
|
|
57
|
+
return { ok: true, terms: null };
|
|
58
|
+
if (typeof raw !== "string")
|
|
59
|
+
return { ok: false, problem: "the release terms are not stored as text" };
|
|
60
|
+
let parsed;
|
|
61
|
+
try {
|
|
62
|
+
parsed = JSON.parse(raw);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return { ok: false, problem: "the release terms can't be read" };
|
|
66
|
+
}
|
|
67
|
+
const read = readReleaseTerms(parsed);
|
|
68
|
+
if (!read.ok)
|
|
69
|
+
return { ok: false, problem: `the release terms don't match their contract (${read.issues[0]?.line ?? "invalid"})` };
|
|
70
|
+
// Bytes exactly as this code writes them (store.ts serializeReleaseTerms): a reordered or padded row is not what was signed.
|
|
71
|
+
if (JSON.stringify(read.value) !== raw)
|
|
72
|
+
return { ok: false, problem: "the release terms are not stored exactly as written" };
|
|
73
|
+
return { ok: true, terms: read.value };
|
|
74
|
+
}
|