opencode-feature-factory 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +877 -0
- package/assets/agent/backend-builder.md +73 -0
- package/assets/agent/codebase-researcher.md +56 -0
- package/assets/agent/design-interpreter.md +37 -0
- package/assets/agent/frontend-builder.md +74 -0
- package/assets/agent/implementation-validator.md +73 -0
- package/assets/agent/security-reviewer.md +60 -0
- package/assets/agent/spec-writer.md +94 -0
- package/assets/agent/story-reader.md +38 -0
- package/assets/agent/story-writer.md +39 -0
- package/assets/agent/test-verifier.md +73 -0
- package/assets/agent/work-decomposer.md +102 -0
- package/assets/agent/work-reviewer.md +77 -0
- package/assets/command/feature.md +68 -0
- package/assets/skills/feature/SCHEMA.md +990 -0
- package/assets/skills/feature/SKILL.md +620 -0
- package/dist/tui.js +3641 -0
- package/package.json +65 -0
- package/src/cleanup-sweep-command.js +75 -0
- package/src/cleanup-sweep-eligibility.js +581 -0
- package/src/cleanup-sweep-output.js +139 -0
- package/src/cleanup-sweep-report.js +548 -0
- package/src/cleanup-sweep.js +546 -0
- package/src/cli-output.js +251 -0
- package/src/cli.js +1152 -0
- package/src/config.js +231 -0
- package/src/cost-attribution.js +327 -0
- package/src/cost-report.js +185 -0
- package/src/detached-log-supervisor.js +178 -0
- package/src/doctor.js +598 -0
- package/src/env-snapshot.js +211 -0
- package/src/factory-diagnostics.js +429 -0
- package/src/factory-paths.js +40 -0
- package/src/factory.js +4769 -0
- package/src/feature-command-payload.js +378 -0
- package/src/git.js +110 -0
- package/src/github.js +252 -0
- package/src/hardening/atomic-write.js +954 -0
- package/src/hardening/line-output.js +139 -0
- package/src/hardening/output-policy.js +365 -0
- package/src/hardening/process-verification.js +542 -0
- package/src/hardening/sensitive-data.js +341 -0
- package/src/hardening/terminal-encoding.js +224 -0
- package/src/plugin.js +246 -0
- package/src/post-pr-ci.js +754 -0
- package/src/process-evidence.js +1139 -0
- package/src/refs.js +144 -0
- package/src/run-state.js +2411 -0
- package/src/steering-conflicts.js +77 -0
- package/src/telemetry.js +419 -0
- package/src/tui-data.js +499 -0
- package/src/tui-rendering.js +148 -0
- package/src/utils.js +35 -0
- package/src/validate.js +1655 -0
- package/src/worktrees.js +56 -0
package/dist/tui.js
ADDED
|
@@ -0,0 +1,3641 @@
|
|
|
1
|
+
// src/tui.jsx
|
|
2
|
+
import { createMemo, createSignal, For, Show } from "solid-js";
|
|
3
|
+
|
|
4
|
+
// src/tui-data.js
|
|
5
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync, statSync as statSync2 } from "node:fs";
|
|
6
|
+
import { dirname as dirname4, join as join6, resolve as resolve5 } from "node:path";
|
|
7
|
+
|
|
8
|
+
// src/cost-attribution.js
|
|
9
|
+
var COST_ATTRIBUTION_SCHEMA_VERSION = 1;
|
|
10
|
+
var MAX_COST_ATTRIBUTION_ENTRIES = 1e3;
|
|
11
|
+
var COST_ATTRIBUTION_STATUSES = Object.freeze(["available", "partial", "unavailable"]);
|
|
12
|
+
var COST_CURRENCY_PATTERN = /^[A-Z]{3,12}$/u;
|
|
13
|
+
var USAGE_NUMERIC_FIELDS = Object.freeze([
|
|
14
|
+
"input_tokens",
|
|
15
|
+
"output_tokens",
|
|
16
|
+
"total_tokens",
|
|
17
|
+
"cache_creation_input_tokens",
|
|
18
|
+
"cache_read_input_tokens",
|
|
19
|
+
"reasoning_tokens"
|
|
20
|
+
]);
|
|
21
|
+
var COST_NUMERIC_FIELDS = Object.freeze([
|
|
22
|
+
"cost_total",
|
|
23
|
+
"cost_input",
|
|
24
|
+
"cost_output",
|
|
25
|
+
"cost_cache_creation",
|
|
26
|
+
"cost_cache_read"
|
|
27
|
+
]);
|
|
28
|
+
var OPTIONAL_STRING_FIELDS = Object.freeze(["step", "slice_id", "source", "operation", "provider", "model", "request_id", "cost_currency"]);
|
|
29
|
+
var NUMERIC_FIELDS = Object.freeze([...USAGE_NUMERIC_FIELDS, ...COST_NUMERIC_FIELDS]);
|
|
30
|
+
var STATUS_SET = new Set(COST_ATTRIBUTION_STATUSES);
|
|
31
|
+
var TERMINAL_CONTROL_PATTERN = /[\u0000-\u001F\u007F-\u009F]/gu;
|
|
32
|
+
function publicCostAttributionSummary(runOrAttribution) {
|
|
33
|
+
const attribution = runOrAttribution?.cost_attribution || runOrAttribution;
|
|
34
|
+
const totals = isRecord(attribution?.totals) ? attribution.totals : rollupEntries(Array.isArray(attribution?.entries) ? attribution.entries : []);
|
|
35
|
+
const status = safeCostStatus(attribution?.status) || safeCostStatus(totals.status) || "unavailable";
|
|
36
|
+
const summary = {
|
|
37
|
+
schema_version: COST_ATTRIBUTION_SCHEMA_VERSION,
|
|
38
|
+
updated_at: attribution?.updated_at === void 0 || attribution?.updated_at === null ? null : sanitizePublicCostText(attribution.updated_at),
|
|
39
|
+
status,
|
|
40
|
+
entry_count: totals.entry_count || 0,
|
|
41
|
+
agent_count: isRecord(attribution?.by_agent) ? Object.keys(attribution.by_agent).length : 0,
|
|
42
|
+
slice_count: isRecord(attribution?.by_slice) ? Object.keys(attribution.by_slice).length : 0,
|
|
43
|
+
mixed_currency: totals.mixed_currency === true,
|
|
44
|
+
missing: Array.isArray(totals.missing) ? totals.missing.map((item) => sanitizePublicCostText(item)).filter(Boolean) : []
|
|
45
|
+
};
|
|
46
|
+
for (const field of USAGE_NUMERIC_FIELDS) if (totals[field] !== void 0) summary[field] = totals[field];
|
|
47
|
+
if (totals.cost_total !== void 0) summary.cost_total = totals.cost_total;
|
|
48
|
+
const currency = safeCostCurrency(totals.cost_currency);
|
|
49
|
+
if (currency) summary.cost_currency = currency;
|
|
50
|
+
return summary;
|
|
51
|
+
}
|
|
52
|
+
function isSafeCostCurrency(value) {
|
|
53
|
+
return safeCostCurrency(value) !== null;
|
|
54
|
+
}
|
|
55
|
+
function hasTerminalControl(value) {
|
|
56
|
+
TERMINAL_CONTROL_PATTERN.lastIndex = 0;
|
|
57
|
+
return typeof value === "string" && TERMINAL_CONTROL_PATTERN.test(value);
|
|
58
|
+
}
|
|
59
|
+
function sanitizePublicCostText(value) {
|
|
60
|
+
return String(value).replace(/[\t\r\n]+/gu, " ").replace(TERMINAL_CONTROL_PATTERN, "").replace(/\s+/gu, " ").trim();
|
|
61
|
+
}
|
|
62
|
+
function rollupEntries(entries) {
|
|
63
|
+
const rollup = {
|
|
64
|
+
status: "unavailable",
|
|
65
|
+
entry_count: entries.length,
|
|
66
|
+
request_count: entries.length
|
|
67
|
+
};
|
|
68
|
+
const missing = /* @__PURE__ */ new Set();
|
|
69
|
+
const costCurrencies = /* @__PURE__ */ new Set();
|
|
70
|
+
const totalCurrencies = /* @__PURE__ */ new Set();
|
|
71
|
+
let availableEntries = 0;
|
|
72
|
+
let partial = entries.length === 0;
|
|
73
|
+
for (const entry of entries) {
|
|
74
|
+
if (entry.status === "available") availableEntries += 1;
|
|
75
|
+
if (entry.status !== "available") partial = true;
|
|
76
|
+
for (const item of Array.isArray(entry.missing) ? entry.missing : []) missing.add(item);
|
|
77
|
+
for (const field of NUMERIC_FIELDS) {
|
|
78
|
+
if (entry[field] === void 0 || entry[field] === null) continue;
|
|
79
|
+
const aggregate = (rollup[field] ?? 0) + entry[field];
|
|
80
|
+
if (!Number.isFinite(aggregate)) throw new Error(`cost attribution aggregate overflow for ${field}`);
|
|
81
|
+
rollup[field] = aggregate;
|
|
82
|
+
if (COST_NUMERIC_FIELDS.includes(field)) {
|
|
83
|
+
const currency = safeCostCurrency(entry.cost_currency);
|
|
84
|
+
if (currency) {
|
|
85
|
+
costCurrencies.add(currency);
|
|
86
|
+
if (field === "cost_total") totalCurrencies.add(currency);
|
|
87
|
+
} else {
|
|
88
|
+
missing.add("cost_currency");
|
|
89
|
+
partial = true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (totalCurrencies.size === 1) rollup.cost_currency = [...totalCurrencies][0];
|
|
95
|
+
const hasMixedCurrency = costCurrencies.size > 1;
|
|
96
|
+
if (hasMixedCurrency) {
|
|
97
|
+
delete rollup.cost_total;
|
|
98
|
+
delete rollup.cost_currency;
|
|
99
|
+
missing.add("mixed_currency");
|
|
100
|
+
partial = true;
|
|
101
|
+
}
|
|
102
|
+
if (availableEntries === entries.length && entries.length > 0 && !partial) rollup.status = "available";
|
|
103
|
+
else if (entries.some((entry) => entry.status === "available" || entry.status === "partial")) rollup.status = "partial";
|
|
104
|
+
else rollup.status = "unavailable";
|
|
105
|
+
rollup.mixed_currency = hasMixedCurrency;
|
|
106
|
+
rollup.missing = entries.length === 0 ? ["entries"] : [...missing].sort();
|
|
107
|
+
return canonicalRollup(rollup);
|
|
108
|
+
}
|
|
109
|
+
function canonicalRollup(rollup) {
|
|
110
|
+
const ordered = {
|
|
111
|
+
status: rollup.status,
|
|
112
|
+
entry_count: rollup.entry_count,
|
|
113
|
+
request_count: rollup.request_count
|
|
114
|
+
};
|
|
115
|
+
for (const field of NUMERIC_FIELDS) {
|
|
116
|
+
if (Object.prototype.hasOwnProperty.call(rollup, field)) ordered[field] = rollup[field];
|
|
117
|
+
}
|
|
118
|
+
if (Object.prototype.hasOwnProperty.call(rollup, "cost_currency")) ordered.cost_currency = rollup.cost_currency;
|
|
119
|
+
ordered.mixed_currency = rollup.mixed_currency;
|
|
120
|
+
ordered.missing = rollup.missing;
|
|
121
|
+
return ordered;
|
|
122
|
+
}
|
|
123
|
+
function safeCostCurrency(value) {
|
|
124
|
+
const text = nonEmptyString(value);
|
|
125
|
+
return text && COST_CURRENCY_PATTERN.test(text) && !hasTerminalControl(text) ? text : null;
|
|
126
|
+
}
|
|
127
|
+
function safeCostStatus(value) {
|
|
128
|
+
return typeof value === "string" && STATUS_SET.has(value) ? value : null;
|
|
129
|
+
}
|
|
130
|
+
function nonEmptyString(value) {
|
|
131
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
132
|
+
}
|
|
133
|
+
function isRecord(value) {
|
|
134
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/factory-diagnostics.js
|
|
138
|
+
import { existsSync as existsSync2, readFileSync, statSync } from "node:fs";
|
|
139
|
+
import { dirname as dirname3, isAbsolute as isAbsolute3, join as join5, resolve as resolve4 } from "node:path";
|
|
140
|
+
|
|
141
|
+
// src/validate.js
|
|
142
|
+
import { join as join4, relative as relative2, resolve as resolve3, sep as sep3 } from "node:path";
|
|
143
|
+
|
|
144
|
+
// src/env-snapshot.js
|
|
145
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
146
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
147
|
+
|
|
148
|
+
// src/utils.js
|
|
149
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
150
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
151
|
+
function requireNonEmptyString(value, label) {
|
|
152
|
+
if (typeof value !== "string" || value.trim() === "") throw new Error(`${label} must be a non-empty string`);
|
|
153
|
+
return value.trim();
|
|
154
|
+
}
|
|
155
|
+
function timestamp(value, label = "timestamp") {
|
|
156
|
+
if (value === void 0 || value === null) return (/* @__PURE__ */ new Date()).toISOString();
|
|
157
|
+
const parsed = typeof value === "number" ? value : value instanceof Date ? value.getTime() : Date.parse(value);
|
|
158
|
+
if (!Number.isFinite(parsed)) throw new Error(`invalid ${label}`);
|
|
159
|
+
return new Date(parsed).toISOString();
|
|
160
|
+
}
|
|
161
|
+
function physicalPath(path, label = "path", options = {}) {
|
|
162
|
+
const resolved = resolve(requireNonEmptyString(path, label));
|
|
163
|
+
if (!existsSync(resolved)) {
|
|
164
|
+
if (options.mustExist) throw new Error(`${label} is unresolvable: ${resolved}`);
|
|
165
|
+
return resolved;
|
|
166
|
+
}
|
|
167
|
+
return realpathSync.native(resolved);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/git.js
|
|
171
|
+
var DEFAULT_GIT_MAX_BUFFER = 1024 * 1024;
|
|
172
|
+
var MAX_GIT_MAX_BUFFER = 8 * 1024 * 1024;
|
|
173
|
+
|
|
174
|
+
// src/plugin.js
|
|
175
|
+
import { dirname, join } from "node:path";
|
|
176
|
+
import { fileURLToPath } from "node:url";
|
|
177
|
+
|
|
178
|
+
// src/config.js
|
|
179
|
+
import { parse, printParseErrorCode } from "jsonc-parser";
|
|
180
|
+
var POST_PR_CI_DEFAULTS = Object.freeze({
|
|
181
|
+
enabled: false,
|
|
182
|
+
wait_ms: 36e5,
|
|
183
|
+
initial_poll_ms: 3e4,
|
|
184
|
+
max_poll_ms: 12e4,
|
|
185
|
+
check_start_grace_ms: 3e5,
|
|
186
|
+
max_transient_errors: 12
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// src/plugin.js
|
|
190
|
+
var root = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
191
|
+
var assets = join(root, "assets");
|
|
192
|
+
|
|
193
|
+
// src/env-snapshot.js
|
|
194
|
+
var root2 = dirname2(dirname2(fileURLToPath2(import.meta.url)));
|
|
195
|
+
var SENSITIVE_ENV_KEY_PATTERN = /(?:secret|token|password|passwd|pwd|api[_-]?key|private[_-]?key|credential|authorization|auth[_-]?header|access[_-]?key|bearer|cookie)/iu;
|
|
196
|
+
var SENSITIVE_ENV_VALUE_PATTERN = /(?:secret|token|password|passwd|api[_-]?key|private[_-]?key)/iu;
|
|
197
|
+
var TOKEN_SHAPED_ENV_VALUE_PATTERNS = [
|
|
198
|
+
/\bgh[pousr]_[A-Za-z0-9_]{20,}\b/u,
|
|
199
|
+
/\bgithub_pat_[A-Za-z0-9_]{20,}\b/u,
|
|
200
|
+
/\bhc_[A-Za-z0-9][A-Za-z0-9_-]{10,}\b/iu,
|
|
201
|
+
/\bsk-proj[-_][A-Za-z0-9_-]{20,}\b/u,
|
|
202
|
+
/\bsk-[A-Za-z0-9_-]{20,}\b/u,
|
|
203
|
+
/\bxox[abp][_-][A-Za-z0-9-]{10,}(?:-[A-Za-z0-9-]{10,})*\b/u,
|
|
204
|
+
/\bglpat-[A-Za-z0-9_-]{20,}\b/u,
|
|
205
|
+
/\bBearer\s+[A-Za-z0-9._~+/=-]{20,}\b/iu,
|
|
206
|
+
/\beyJ[A-Za-z0-9_-]{7,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/u,
|
|
207
|
+
/\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/u,
|
|
208
|
+
/(?:^|[^A-Za-z0-9-])(?:[A-Fa-f0-9]{32,})(?=$|[^A-Za-z0-9-])/u,
|
|
209
|
+
/(?:https?|ssh|git|ftp):\/\/[^/\s:@]+:[^/\s@]+@/iu
|
|
210
|
+
];
|
|
211
|
+
var HIGH_ENTROPY_SINGLE_TOKEN_MIN_LENGTH = 32;
|
|
212
|
+
var HIGH_ENTROPY_MIN_SHANNON = 3.5;
|
|
213
|
+
var SAFE_HIGH_ENTROPY_ENV_KEY_PATTERN = /^(?:OTEL_EXPORTER_OTLP(?:_(?:TRACES|METRICS|LOGS))?_(?:ENDPOINT|HEADERS|PROTOCOL|TIMEOUT|COMPRESSION|INSECURE|CERTIFICATE)|FEATURE_FACTORY_OTEL_ENABLED)$/u;
|
|
214
|
+
var REDACTED_ENV_VALUE = "[redacted]";
|
|
215
|
+
function isSensitiveEnvKey(key) {
|
|
216
|
+
const string = String(key ?? "");
|
|
217
|
+
return SENSITIVE_ENV_KEY_PATTERN.test(string) || isSecretShapedEnvKey(string);
|
|
218
|
+
}
|
|
219
|
+
function isSensitiveEnvValue(value) {
|
|
220
|
+
if (typeof value !== "string") return false;
|
|
221
|
+
const trimmed = value.trim();
|
|
222
|
+
if (!trimmed) return false;
|
|
223
|
+
return SENSITIVE_ENV_VALUE_PATTERN.test(trimmed) || TOKEN_SHAPED_ENV_VALUE_PATTERNS.some((pattern) => pattern.test(trimmed)) || credentialBearingUrl(trimmed) || highEntropySingleToken(trimmed);
|
|
224
|
+
}
|
|
225
|
+
function isSecretShapedEnvKey(key) {
|
|
226
|
+
const trimmed = String(key ?? "").trim();
|
|
227
|
+
if (!trimmed) return false;
|
|
228
|
+
return TOKEN_SHAPED_ENV_VALUE_PATTERNS.some((pattern) => pattern.test(trimmed)) || credentialBearingUrl(trimmed) || highEntropySecretKey(trimmed);
|
|
229
|
+
}
|
|
230
|
+
function credentialBearingUrl(value) {
|
|
231
|
+
try {
|
|
232
|
+
const parsed = new URL(value);
|
|
233
|
+
return Boolean(parsed.username || parsed.password);
|
|
234
|
+
} catch {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function highEntropySingleToken(value) {
|
|
239
|
+
if (value.length < HIGH_ENTROPY_SINGLE_TOKEN_MIN_LENGTH) return false;
|
|
240
|
+
if (/\s/u.test(value)) return false;
|
|
241
|
+
if (!/^[A-Za-z0-9._~+/=-]+$/u.test(value)) return false;
|
|
242
|
+
return shannonEntropy(value) >= HIGH_ENTROPY_MIN_SHANNON;
|
|
243
|
+
}
|
|
244
|
+
function highEntropySecretKey(value) {
|
|
245
|
+
if (SAFE_HIGH_ENTROPY_ENV_KEY_PATTERN.test(value)) return false;
|
|
246
|
+
return highEntropySingleToken(value);
|
|
247
|
+
}
|
|
248
|
+
function shannonEntropy(value) {
|
|
249
|
+
const counts = /* @__PURE__ */ new Map();
|
|
250
|
+
for (const char of value) counts.set(char, (counts.get(char) || 0) + 1);
|
|
251
|
+
return [...counts.values()].reduce((entropy, count) => {
|
|
252
|
+
const probability = count / value.length;
|
|
253
|
+
return entropy - probability * Math.log2(probability);
|
|
254
|
+
}, 0);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// src/process-evidence.js
|
|
258
|
+
import { basename, isAbsolute as isAbsolute2, join as join3, normalize, resolve as resolve2, sep as sep2 } from "node:path";
|
|
259
|
+
|
|
260
|
+
// src/hardening/atomic-write.js
|
|
261
|
+
var ERROR_MESSAGES = Object.freeze({
|
|
262
|
+
INVALID_ROOT: "protected file root is invalid",
|
|
263
|
+
UNTRUSTED_ROOT: "protected file root is not trusted",
|
|
264
|
+
INVALID_PATH: "protected relative path is invalid",
|
|
265
|
+
INVALID_OPTIONS: "protected file options are invalid",
|
|
266
|
+
PARENT_MISSING: "protected file parent is missing",
|
|
267
|
+
PARENT_TYPE: "protected file parent has an unsafe type",
|
|
268
|
+
PARENT_CHANGED: "protected file parent changed during the operation",
|
|
269
|
+
TARGET_TYPE: "protected file target has an unsafe type",
|
|
270
|
+
TARGET_CHANGED: "protected file target changed during the operation",
|
|
271
|
+
TARGET_EXISTS: "protected file target already exists",
|
|
272
|
+
TEMP_COLLISION: "protected temporary file name collisions exceeded the limit",
|
|
273
|
+
TEMP_OPEN_FAILED: "protected temporary file could not be opened",
|
|
274
|
+
TEMP_TYPE: "protected temporary file has an unsafe type",
|
|
275
|
+
TEMP_CHANGED: "protected temporary file changed during the operation",
|
|
276
|
+
WRITE_FAILED: "protected file write failed",
|
|
277
|
+
CLOSE_FAILED: "protected file close failed",
|
|
278
|
+
COMMIT_FAILED: "protected file commit failed",
|
|
279
|
+
CROSS_DEVICE: "protected file commit crossed filesystem devices",
|
|
280
|
+
LINK_UNSUPPORTED: "protected create-only publication is unsupported",
|
|
281
|
+
CLEANUP_INDETERMINATE: "protected temporary file cleanup is indeterminate",
|
|
282
|
+
DIRECTORY_CREATE_FAILED: "protected directory could not be created",
|
|
283
|
+
APPEND_UNSUPPORTED: "protected no-follow append is unsupported",
|
|
284
|
+
APPEND_OPEN_FAILED: "protected append file could not be opened"
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// src/hardening/process-verification.js
|
|
288
|
+
function normalizeLegacyBooleanLiveness(value) {
|
|
289
|
+
if (value === true) return "live";
|
|
290
|
+
if (value === false) return "absent";
|
|
291
|
+
return "indeterminate";
|
|
292
|
+
}
|
|
293
|
+
function probeLegacyBooleanLiveness(callback, ...args) {
|
|
294
|
+
if (typeof callback !== "function") return "indeterminate";
|
|
295
|
+
try {
|
|
296
|
+
return normalizeLegacyBooleanLiveness(callback(...args));
|
|
297
|
+
} catch {
|
|
298
|
+
return "indeterminate";
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function publicLivenessBoolean(status) {
|
|
302
|
+
if (status === "live") return true;
|
|
303
|
+
if (status === "absent") return false;
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
var DEFAULT_COMMAND_MAX_BUFFER = 1024 * 1024;
|
|
307
|
+
var DARWIN_WEEKDAYS = Object.freeze(["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]);
|
|
308
|
+
var DARWIN_MONTHS = Object.freeze(["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]);
|
|
309
|
+
var PROCESS_VERIFICATION_CODES = Object.freeze({
|
|
310
|
+
INVALID_PID: "INVALID_PID",
|
|
311
|
+
INVALID_OPTIONS: "INVALID_OPTIONS",
|
|
312
|
+
LIVENESS_LIVE: "LIVENESS_LIVE",
|
|
313
|
+
LIVENESS_ABSENT: "LIVENESS_ABSENT",
|
|
314
|
+
LIVENESS_PERMISSION_DENIED: "LIVENESS_PERMISSION_DENIED",
|
|
315
|
+
LIVENESS_FAILED: "LIVENESS_FAILED",
|
|
316
|
+
LIVENESS_RESULT_MALFORMED: "LIVENESS_RESULT_MALFORMED",
|
|
317
|
+
PLATFORM_UNSUPPORTED: "PLATFORM_UNSUPPORTED",
|
|
318
|
+
METADATA_UNAVAILABLE: "METADATA_UNAVAILABLE",
|
|
319
|
+
METADATA_MALFORMED: "METADATA_MALFORMED",
|
|
320
|
+
INSPECTION_TIMEOUT: "INSPECTION_TIMEOUT",
|
|
321
|
+
IDENTITY_LIVE: "IDENTITY_LIVE",
|
|
322
|
+
IDENTITY_INVALID: "IDENTITY_INVALID",
|
|
323
|
+
IDENTITY_MATCH: "IDENTITY_MATCH",
|
|
324
|
+
IDENTITY_MISMATCH: "IDENTITY_MISMATCH",
|
|
325
|
+
SIGNAL_INVALID: "SIGNAL_INVALID",
|
|
326
|
+
SIGNAL_NOT_AUTHORIZED: "SIGNAL_NOT_AUTHORIZED",
|
|
327
|
+
SIGNAL_SENT: "SIGNAL_SENT",
|
|
328
|
+
SIGNAL_FAILED: "SIGNAL_FAILED",
|
|
329
|
+
POST_SIGNAL_NOT_CHECKED: "POST_SIGNAL_NOT_CHECKED",
|
|
330
|
+
POST_SIGNAL_ABSENT: "POST_SIGNAL_ABSENT",
|
|
331
|
+
POST_SIGNAL_LIVE: "POST_SIGNAL_LIVE",
|
|
332
|
+
POST_SIGNAL_MISMATCH: "POST_SIGNAL_MISMATCH",
|
|
333
|
+
POST_SIGNAL_INDETERMINATE: "POST_SIGNAL_INDETERMINATE"
|
|
334
|
+
});
|
|
335
|
+
var REASONS = Object.freeze({
|
|
336
|
+
INVALID_PID: "process pid must be a positive integer",
|
|
337
|
+
INVALID_OPTIONS: "process verification options are invalid",
|
|
338
|
+
LIVENESS_LIVE: "process is live",
|
|
339
|
+
LIVENESS_ABSENT: "process is absent",
|
|
340
|
+
LIVENESS_PERMISSION_DENIED: "process liveness is indeterminate because permission was denied",
|
|
341
|
+
LIVENESS_FAILED: "process liveness could not be determined",
|
|
342
|
+
LIVENESS_RESULT_MALFORMED: "process liveness probe returned a malformed result",
|
|
343
|
+
PLATFORM_UNSUPPORTED: "process identity inspection is unsupported on this platform",
|
|
344
|
+
METADATA_UNAVAILABLE: "process identity metadata could not be inspected",
|
|
345
|
+
METADATA_MALFORMED: "process identity metadata is malformed",
|
|
346
|
+
INSPECTION_TIMEOUT: "process identity inspection timed out",
|
|
347
|
+
IDENTITY_LIVE: "process identity is live",
|
|
348
|
+
IDENTITY_INVALID: "expected process identity is invalid",
|
|
349
|
+
IDENTITY_MATCH: "process identity matches expected identity",
|
|
350
|
+
IDENTITY_MISMATCH: "process identity does not match expected identity",
|
|
351
|
+
SIGNAL_INVALID: "only SIGTERM may be sent by verified process signaling",
|
|
352
|
+
SIGNAL_NOT_AUTHORIZED: "process signal was not authorized by identity verification",
|
|
353
|
+
SIGNAL_SENT: "SIGTERM was sent to the verified process",
|
|
354
|
+
SIGNAL_FAILED: "SIGTERM could not be sent to the verified process",
|
|
355
|
+
POST_SIGNAL_NOT_CHECKED: "process state was not checked after SIGTERM",
|
|
356
|
+
POST_SIGNAL_ABSENT: "process absence was confirmed after SIGTERM",
|
|
357
|
+
POST_SIGNAL_LIVE: "the matching process remains live after SIGTERM",
|
|
358
|
+
POST_SIGNAL_MISMATCH: "a different process identity was observed after SIGTERM",
|
|
359
|
+
POST_SIGNAL_INDETERMINATE: "process state is indeterminate after SIGTERM"
|
|
360
|
+
});
|
|
361
|
+
function probeProcessLiveness(pid, options = {}) {
|
|
362
|
+
if (!positivePid(pid)) return result("indeterminate", "INVALID_PID", pid);
|
|
363
|
+
const injectedProbe = firstFunction(options.livenessProbe, options.livenessProbeFn, options.processLivenessProbe);
|
|
364
|
+
if (injectedProbe) {
|
|
365
|
+
try {
|
|
366
|
+
return normalizeLivenessResult(injectedProbe(pid), pid);
|
|
367
|
+
} catch (error) {
|
|
368
|
+
return livenessErrorResult(error, pid);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
process.kill(pid, 0);
|
|
373
|
+
return result("live", "LIVENESS_LIVE", pid);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
return livenessErrorResult(error, pid);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function normalizeLivenessResult(value, pid) {
|
|
379
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
380
|
+
return result("indeterminate", "LIVENESS_RESULT_MALFORMED", pid);
|
|
381
|
+
}
|
|
382
|
+
if (value.status === "live") return result("live", "LIVENESS_LIVE", pid);
|
|
383
|
+
if (value.status === "absent") return result("absent", "LIVENESS_ABSENT", pid);
|
|
384
|
+
if (value.status === "indeterminate") return result("indeterminate", "LIVENESS_FAILED", pid);
|
|
385
|
+
return result("indeterminate", "LIVENESS_RESULT_MALFORMED", pid);
|
|
386
|
+
}
|
|
387
|
+
function livenessErrorResult(error, pid) {
|
|
388
|
+
if (error?.code === "ESRCH") return result("absent", "LIVENESS_ABSENT", pid);
|
|
389
|
+
if (error?.code === "EPERM" || error?.code === "EACCES") {
|
|
390
|
+
return result("indeterminate", "LIVENESS_PERMISSION_DENIED", pid);
|
|
391
|
+
}
|
|
392
|
+
return result("indeterminate", "LIVENESS_FAILED", pid);
|
|
393
|
+
}
|
|
394
|
+
function positivePid(value) {
|
|
395
|
+
return Number.isSafeInteger(value) && value > 0;
|
|
396
|
+
}
|
|
397
|
+
function firstFunction(...values) {
|
|
398
|
+
return values.find((value) => typeof value === "function") || null;
|
|
399
|
+
}
|
|
400
|
+
function result(status, code, pid, extra = {}) {
|
|
401
|
+
return { status, code: PROCESS_VERIFICATION_CODES[code], reason: REASONS[code], pid, ...extra };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// src/process-evidence.js
|
|
405
|
+
var PROCESS_EVIDENCE_FILE = "process.json";
|
|
406
|
+
var PROCESS_EVIDENCE_KIND = "opencode-process";
|
|
407
|
+
var PROCESS_EVIDENCE_SCHEMA_VERSION = 1;
|
|
408
|
+
var LAUNCH_CLAIM_DIR = "process-launch.lock";
|
|
409
|
+
var LAUNCH_CLAIM_FILE = "owner.json";
|
|
410
|
+
var LAUNCH_CLAIM_REF = `${LAUNCH_CLAIM_DIR}/${LAUNCH_CLAIM_FILE}`;
|
|
411
|
+
var LAUNCH_CLAIM_PHASES = Object.freeze(["foreground-live", "predecessor-active", "predecessor-released", "spawning"]);
|
|
412
|
+
var LAUNCH_KINDS = Object.freeze(["approval-handoff", "resume-foreground", "resume-detached", "start-resume-foreground", "start-resume-detached"]);
|
|
413
|
+
var PROCESS_STATES = /* @__PURE__ */ new Set(["running", "cancelled", "failed-closed", "exited"]);
|
|
414
|
+
var LAUNCH_PHASE_SET = new Set(LAUNCH_CLAIM_PHASES);
|
|
415
|
+
var LAUNCH_KIND_SET = new Set(LAUNCH_KINDS);
|
|
416
|
+
function processEvidenceProcessesDir(runDir) {
|
|
417
|
+
return join3(resolve2(runDir), "processes");
|
|
418
|
+
}
|
|
419
|
+
function validateProcessEvidence(evidence, opts = {}) {
|
|
420
|
+
const errors = [];
|
|
421
|
+
if (!plainObject(evidence)) return invalid("process evidence must be a JSON object");
|
|
422
|
+
if (evidence.schema_version !== PROCESS_EVIDENCE_SCHEMA_VERSION) errors.push("schema_version must be 1");
|
|
423
|
+
if (evidence.kind !== PROCESS_EVIDENCE_KIND) errors.push("kind must be opencode-process");
|
|
424
|
+
if (!nonEmptyString2(evidence.run_id)) errors.push("run_id must be a non-empty string");
|
|
425
|
+
if (nonEmptyString2(opts.runId) && evidence.run_id !== opts.runId) errors.push("run_id must match requested run");
|
|
426
|
+
if (!nonEmptyString2(evidence.execution_id)) errors.push("execution_id must be a non-empty string");
|
|
427
|
+
if (!positivePid2(evidence.pid)) errors.push("pid must be a positive integer");
|
|
428
|
+
if (!validTimestamp(evidence.started_at)) errors.push("started_at must be an ISO timestamp");
|
|
429
|
+
if (!validTimestamp(evidence.updated_at)) errors.push("updated_at must be an ISO timestamp");
|
|
430
|
+
if (!PROCESS_STATES.has(evidence.state)) errors.push("state must be one of running, cancelled, failed-closed, exited");
|
|
431
|
+
if (!nonEmptyString2(evidence.cwd) || !isAbsolute2(evidence.cwd)) errors.push("cwd must be an absolute path");
|
|
432
|
+
if (!plainObject(evidence.identity)) {
|
|
433
|
+
errors.push("identity must be an object");
|
|
434
|
+
} else {
|
|
435
|
+
if (!nonEmptyString2(evidence.identity.inspector)) errors.push("identity.inspector must be a non-empty string");
|
|
436
|
+
if (!nonEmptyString2(evidence.identity.start_marker)) errors.push("identity.start_marker must be a non-empty string");
|
|
437
|
+
else if (String(evidence.identity.start_marker).startsWith("unverified:")) errors.push("identity.start_marker must be verifiable process evidence");
|
|
438
|
+
if (!nonEmptyString2(evidence.identity.command_name)) errors.push("identity.command_name must be a non-empty string");
|
|
439
|
+
}
|
|
440
|
+
if (!validProcessLogRef(evidence.log_ref)) errors.push("log_ref must stay under processes/");
|
|
441
|
+
if (!(evidence.cancel === null || plainObject(evidence.cancel))) errors.push("cancel must be null or an object");
|
|
442
|
+
if (errors.length) return invalid(`invalid process evidence: ${errors.join("; ")}`);
|
|
443
|
+
return { ok: true, reason: null, evidence };
|
|
444
|
+
}
|
|
445
|
+
function validProcessLogRef(ref) {
|
|
446
|
+
if (!nonEmptyString2(ref) || isAbsolute2(ref) || ref.includes("\\") || ref.includes("\0")) return false;
|
|
447
|
+
const normalized = normalize(ref).replaceAll(sep2, "/");
|
|
448
|
+
if (normalized === "." || normalized.startsWith("../") || normalized === "..") return false;
|
|
449
|
+
if (!normalized.startsWith("processes/")) return false;
|
|
450
|
+
return basename(normalized).length > 0 && normalized !== "processes";
|
|
451
|
+
}
|
|
452
|
+
function positivePid2(value) {
|
|
453
|
+
return Number.isInteger(value) && value > 0;
|
|
454
|
+
}
|
|
455
|
+
function plainObject(value) {
|
|
456
|
+
return value && typeof value === "object" && !Array.isArray(value);
|
|
457
|
+
}
|
|
458
|
+
function nonEmptyString2(value) {
|
|
459
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
460
|
+
}
|
|
461
|
+
function validTimestamp(value) {
|
|
462
|
+
return nonEmptyString2(value) && Number.isFinite(Date.parse(value));
|
|
463
|
+
}
|
|
464
|
+
function invalid(reason) {
|
|
465
|
+
return { ok: false, reason, evidence: null };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// src/refs.js
|
|
469
|
+
import { createHash } from "node:crypto";
|
|
470
|
+
var DURABLE_ROOTS = Object.freeze(["artifacts", "evidence", "reviews", "gates", "steering"]);
|
|
471
|
+
function hashValue(value) {
|
|
472
|
+
return `sha256:${createHash("sha256").update(canonicalJson(value), "utf8").digest("hex")}`;
|
|
473
|
+
}
|
|
474
|
+
function canonicalJson(value) {
|
|
475
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
476
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
477
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// src/validate.js
|
|
481
|
+
var TERMINAL_RUN_STATUSES = Object.freeze(["completed", "blocked", "partial", "needs-human"]);
|
|
482
|
+
var HEARTBEAT_PHASES = Object.freeze([
|
|
483
|
+
"spec-review",
|
|
484
|
+
"decomposition-review",
|
|
485
|
+
"builder-wave",
|
|
486
|
+
"slice-review",
|
|
487
|
+
"test-verifier",
|
|
488
|
+
"test-rerun",
|
|
489
|
+
"test-review",
|
|
490
|
+
"implementation-validator",
|
|
491
|
+
"security-reviewer",
|
|
492
|
+
"remediation",
|
|
493
|
+
"post-pr-observation",
|
|
494
|
+
"post-pr-remediation",
|
|
495
|
+
"post-pr-revalidation"
|
|
496
|
+
]);
|
|
497
|
+
var HEARTBEAT_PROTECTED_GATES = Object.freeze(["story", "brief", "pre_pr"]);
|
|
498
|
+
var MAX_SLICE_DEPENDENCY_WAVES = 3;
|
|
499
|
+
var RUN_STATUSES = /* @__PURE__ */ new Set(["running", ...TERMINAL_RUN_STATUSES]);
|
|
500
|
+
var TERMINAL_STATUSES = new Set(TERMINAL_RUN_STATUSES);
|
|
501
|
+
var RUN_MODES = /* @__PURE__ */ new Set(["interactive", "headless", "autonomous"]);
|
|
502
|
+
var PR_MODES = /* @__PURE__ */ new Set(["draft", "ready"]);
|
|
503
|
+
var GATE_STATUSES = /* @__PURE__ */ new Set(["pending", "approved", "changes_requested", "stopped"]);
|
|
504
|
+
var APPROVAL_SOURCES = /* @__PURE__ */ new Set(["human", "external-driver", "autonomous", "override"]);
|
|
505
|
+
var SAFE_GATE_NAME_PATTERN = /^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?$/u;
|
|
506
|
+
var SLICE_STATUSES = /* @__PURE__ */ new Set(["pending", "running", "review", "merged", "blocked"]);
|
|
507
|
+
var STEP_STATUSES = /* @__PURE__ */ new Set(["running", "accepted", "rejected", "blocked"]);
|
|
508
|
+
var VALIDATOR_VERDICTS = /* @__PURE__ */ new Set(["GO", "GO-WITH-NITS", "NO-GO"]);
|
|
509
|
+
var PASSING_VALIDATOR_VERDICTS = /* @__PURE__ */ new Set(["GO", "GO-WITH-NITS"]);
|
|
510
|
+
var SECURITY_VERDICTS = /* @__PURE__ */ new Set(["PASS", "BLOCK"]);
|
|
511
|
+
var PASSING_SECURITY_VERDICTS = /* @__PURE__ */ new Set(["PASS"]);
|
|
512
|
+
var CONTINUATION_KINDS = /* @__PURE__ */ new Set(["blocked-run-continuation"]);
|
|
513
|
+
var BLOCKED_CONTINUATION_PARENT_STATUSES = /* @__PURE__ */ new Set(["blocked"]);
|
|
514
|
+
var HASH_PATTERN = /^sha256:[0-9a-f]{64}$/u;
|
|
515
|
+
var HANDOFF_RECEIPT_KIND = "interactive-approval-handoff";
|
|
516
|
+
var DEBUG_SNAPSHOT_KEYS = /* @__PURE__ */ new Set(["created_with", "last_resumed_with", "resume_count"]);
|
|
517
|
+
var DEBUG_SNAPSHOT_EVENT_KEYS = /* @__PURE__ */ new Set(["collected_at", "event", "diagnostic_only", "env"]);
|
|
518
|
+
var COST_ATTRIBUTION_STATUS_SET = new Set(COST_ATTRIBUTION_STATUSES);
|
|
519
|
+
var COST_ATTRIBUTION_ENTRY_OPTIONAL_STRINGS = /* @__PURE__ */ new Set(["step", "slice_id", "source", "operation", "provider", "model", "request_id", "cost_currency"]);
|
|
520
|
+
var COST_ATTRIBUTION_NUMERIC_FIELDS = /* @__PURE__ */ new Set([...USAGE_NUMERIC_FIELDS, ...COST_NUMERIC_FIELDS]);
|
|
521
|
+
var POST_PR_PHASES = Object.freeze(["disabled", "awaiting-pr", "observing", "failure-recording", "remediation-planned", "remediation-running", "changes-observed", "committed", "revalidating", "validated", "push-pending", "remote-confirmed", "succeeded", "blocked", "needs-human"]);
|
|
522
|
+
var POST_PR_TERMINAL_REASONS = Object.freeze({
|
|
523
|
+
completed: ["post-pr-ci-green", "post-pr-draft-ci-green", "post-pr-external-merge"],
|
|
524
|
+
blocked: ["post-pr-retry-exhausted", "post-pr-observation-timeout", "post-pr-observer-infrastructure", "post-pr-pr-closed"],
|
|
525
|
+
"needs-human": ["post-pr-review-changes-requested", "post-pr-owner-ambiguous", "post-pr-account-switch-failed", "post-pr-head-mismatch", "post-pr-dispatch-start-unknown", "post-pr-path-lane-violation", "post-pr-remote-head-diverged", "post-pr-metadata-unsafe", "post-pr-push-failed", "post-pr-panel-attribution-unsafe"]
|
|
526
|
+
});
|
|
527
|
+
var POST_PR_PHASE_SET = new Set(POST_PR_PHASES);
|
|
528
|
+
var POST_PR_ACTIVE_PHASES = new Set(POST_PR_PHASES.filter((phase) => !["disabled", "awaiting-pr", "succeeded", "blocked", "needs-human"].includes(phase)));
|
|
529
|
+
var FULL_GIT_SHA_PATTERN = /^[0-9a-f]{40}$/u;
|
|
530
|
+
var ValidationError = class extends Error {
|
|
531
|
+
constructor(errors) {
|
|
532
|
+
const safeErrors = errors.map((item) => ({
|
|
533
|
+
...item,
|
|
534
|
+
path: safeValidationText(item.path),
|
|
535
|
+
message: safeValidationText(item.message)
|
|
536
|
+
}));
|
|
537
|
+
super(safeErrors.map((item) => `${item.path}: ${item.message}`).join("; "));
|
|
538
|
+
this.name = "ValidationError";
|
|
539
|
+
this.errors = safeErrors;
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
function validateRun(run) {
|
|
543
|
+
const errors = [];
|
|
544
|
+
if (!isRecord2(run)) return fail([{ path: "run", message: "must be an object" }]);
|
|
545
|
+
requiredString(errors, run, "run_id", "run.run_id");
|
|
546
|
+
optionalNumber(errors, run, "schema_version", "run.schema_version");
|
|
547
|
+
optionalEnum(errors, run, "mode", RUN_MODES, "run.mode");
|
|
548
|
+
requiredEnum(errors, run, "status", RUN_STATUSES, "run.status");
|
|
549
|
+
optionalString(errors, run, "created_at", "run.created_at");
|
|
550
|
+
optionalString(errors, run, "updated_at", "run.updated_at");
|
|
551
|
+
optionalString(errors, run, "heartbeat_at", "run.heartbeat_at");
|
|
552
|
+
optionalString(errors, run, "base_ref", "run.base_ref");
|
|
553
|
+
optionalString(errors, run, "base_commit", "run.base_commit");
|
|
554
|
+
optionalString(errors, run, "branch", "run.branch");
|
|
555
|
+
optionalString(errors, run, "worktree", "run.worktree");
|
|
556
|
+
optionalNonEmptyString(errors, run, "github_account", "run.github_account");
|
|
557
|
+
optionalEnum(errors, run, "pr_mode", PR_MODES, "run.pr_mode");
|
|
558
|
+
optionalString(errors, run, "pr_url", "run.pr_url");
|
|
559
|
+
optionalInteger(errors, run, "max_parallel_slices", "run.max_parallel_slices");
|
|
560
|
+
optionalInteger(errors, run, "max_retries", "run.max_retries");
|
|
561
|
+
optionalNonEmptyString(errors, run, "review_tier", "run.review_tier");
|
|
562
|
+
validateDebugSnapshot(errors, run.debug_snapshot, "run.debug_snapshot");
|
|
563
|
+
validateContinuation(errors, run, "run.continuation");
|
|
564
|
+
validateSteering(errors, run.steering, "run.steering");
|
|
565
|
+
validatePostPr(errors, run, "run.post_pr");
|
|
566
|
+
validateGateMap(errors, run.gates, "run.gates");
|
|
567
|
+
validateRunSlices(errors, run.slices, "run.slices");
|
|
568
|
+
validateCostAttribution(errors, run.cost_attribution, "run.cost_attribution", run);
|
|
569
|
+
validateSteps(errors, run.steps, "run.steps");
|
|
570
|
+
validateVerdict(errors, run.validator, "run.validator", VALIDATOR_VERDICTS);
|
|
571
|
+
validateVerdict(errors, run.security_review, "run.security_review", SECURITY_VERDICTS);
|
|
572
|
+
validateTerminalResult(errors, run, "run.terminal_result");
|
|
573
|
+
if (errors.length) fail(errors);
|
|
574
|
+
return run;
|
|
575
|
+
}
|
|
576
|
+
function validateSlicesPlan(plan, { enforceDependencyDepth = true } = {}) {
|
|
577
|
+
const errors = [];
|
|
578
|
+
if (!isRecord2(plan)) return fail([{ path: "plan", message: "must be an object" }]);
|
|
579
|
+
if (!Array.isArray(plan.slices)) errors.push({ path: "plan.slices", message: "must be an array" });
|
|
580
|
+
else validatePlannedSlices(errors, plan.slices, "plan.slices", { enforceDependencyDepth });
|
|
581
|
+
if (errors.length) fail(errors);
|
|
582
|
+
return plan;
|
|
583
|
+
}
|
|
584
|
+
function runSlicesMatchPlan(run, plan) {
|
|
585
|
+
const runGraph = normalizeSliceGraph(run?.slices);
|
|
586
|
+
const planGraph = normalizeSliceGraph(plan?.slices);
|
|
587
|
+
if (!runGraph || !planGraph) return false;
|
|
588
|
+
if (runGraph.size === 0 || runGraph.size !== planGraph.size) return false;
|
|
589
|
+
for (const [id, deps] of runGraph) {
|
|
590
|
+
const planDeps = planGraph.get(id);
|
|
591
|
+
if (!planDeps || planDeps.length !== deps.length || planDeps.some((dep, index) => dep !== deps[index])) return false;
|
|
592
|
+
}
|
|
593
|
+
return true;
|
|
594
|
+
}
|
|
595
|
+
function normalizeSliceGraph(slices) {
|
|
596
|
+
if (!Array.isArray(slices)) return null;
|
|
597
|
+
const graph = /* @__PURE__ */ new Map();
|
|
598
|
+
for (const slice of slices) {
|
|
599
|
+
if (!isRecord2(slice) || !stringValue(slice.id)) return null;
|
|
600
|
+
const id = String(slice.id).trim();
|
|
601
|
+
if (graph.has(id)) return null;
|
|
602
|
+
const deps = Array.isArray(slice.depends_on) ? [...new Set(slice.depends_on.filter(stringValue).map((dep) => String(dep).trim()))].sort() : [];
|
|
603
|
+
graph.set(id, deps);
|
|
604
|
+
}
|
|
605
|
+
return graph;
|
|
606
|
+
}
|
|
607
|
+
function validateHeartbeatState(heartbeat) {
|
|
608
|
+
const errors = [];
|
|
609
|
+
if (!isRecord2(heartbeat)) return fail([{ path: "heartbeat", message: "must be an object" }]);
|
|
610
|
+
requiredInteger(errors, heartbeat, "schema_version", "heartbeat.schema_version");
|
|
611
|
+
requiredString(errors, heartbeat, "run_id", "heartbeat.run_id");
|
|
612
|
+
requiredString(errors, heartbeat, "phase", "heartbeat.phase");
|
|
613
|
+
optionalNullableInteger(errors, heartbeat, "pid", "heartbeat.pid");
|
|
614
|
+
requiredInteger(errors, heartbeat, "interval_ms", "heartbeat.interval_ms");
|
|
615
|
+
requiredString(errors, heartbeat, "last_tick_at", "heartbeat.last_tick_at");
|
|
616
|
+
if (errors.length) fail(errors);
|
|
617
|
+
return heartbeat;
|
|
618
|
+
}
|
|
619
|
+
function validateFactoryLock(factoryLock) {
|
|
620
|
+
const errors = [];
|
|
621
|
+
if (!isRecord2(factoryLock)) return fail([{ path: "factory_lock", message: "must be an object" }]);
|
|
622
|
+
requiredInteger(errors, factoryLock, "schema_version", "factory_lock.schema_version");
|
|
623
|
+
requiredString(errors, factoryLock, "run_id", "factory_lock.run_id");
|
|
624
|
+
optionalString(errors, factoryLock, "session_owner", "factory_lock.session_owner");
|
|
625
|
+
optionalString(errors, factoryLock, "updated_at", "factory_lock.updated_at");
|
|
626
|
+
if (errors.length) fail(errors);
|
|
627
|
+
return factoryLock;
|
|
628
|
+
}
|
|
629
|
+
function validateProcessSidecar(processSidecar, options = {}) {
|
|
630
|
+
const validation = validateProcessEvidence(processSidecar, { runDir: options.runDir, runId: options.runId });
|
|
631
|
+
if (!validation.ok) fail([{ path: "process", message: validation.reason }]);
|
|
632
|
+
const errors = [];
|
|
633
|
+
validateProcessLogRefContainment(errors, validation.evidence.log_ref, "process.log_ref", options.runDir);
|
|
634
|
+
if (errors.length) fail(errors);
|
|
635
|
+
return validation.evidence;
|
|
636
|
+
}
|
|
637
|
+
function pendingProtectedGate(run) {
|
|
638
|
+
if (!isRecord2(run) || !isRecord2(run.gates)) return null;
|
|
639
|
+
for (const gateName of HEARTBEAT_PROTECTED_GATES) if (isPendingGate(run.gates[gateName])) return gateName;
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
function validateGateMap(errors, gates, path) {
|
|
643
|
+
if (gates === void 0 || gates === null) return;
|
|
644
|
+
if (!isRecord2(gates)) {
|
|
645
|
+
errors.push({ path, message: "must be an object" });
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
for (const [name, gate] of Object.entries(gates)) {
|
|
649
|
+
validateGateName(errors, name, `${path}.${name}`);
|
|
650
|
+
validateGate(errors, gate, `${path}.${name}`, name);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
function validateDebugSnapshot(errors, snapshotRoot, path) {
|
|
654
|
+
if (snapshotRoot === void 0 || snapshotRoot === null) return;
|
|
655
|
+
if (!isRecord2(snapshotRoot)) {
|
|
656
|
+
errors.push({ path, message: "must be an object" });
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
for (const key of Object.keys(snapshotRoot)) if (!DEBUG_SNAPSHOT_KEYS.has(key)) errors.push({ path: `${path}.${key}`, message: "is not allowed" });
|
|
660
|
+
validateDebugSnapshotEvent(errors, snapshotRoot.created_with, `${path}.created_with`);
|
|
661
|
+
if (snapshotRoot.last_resumed_with !== void 0 && snapshotRoot.last_resumed_with !== null) validateDebugSnapshotEvent(errors, snapshotRoot.last_resumed_with, `${path}.last_resumed_with`);
|
|
662
|
+
requiredInteger(errors, snapshotRoot, "resume_count", `${path}.resume_count`);
|
|
663
|
+
}
|
|
664
|
+
function validateDebugSnapshotEvent(errors, snapshot, path) {
|
|
665
|
+
if (!isRecord2(snapshot)) {
|
|
666
|
+
errors.push({ path, message: "must be an object" });
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
for (const key of Object.keys(snapshot)) if (!DEBUG_SNAPSHOT_EVENT_KEYS.has(key)) errors.push({ path: `${path}.${key}`, message: "is not allowed" });
|
|
670
|
+
requiredString(errors, snapshot, "collected_at", `${path}.collected_at`);
|
|
671
|
+
requiredString(errors, snapshot, "event", `${path}.event`);
|
|
672
|
+
if (snapshot.diagnostic_only !== true) errors.push({ path: `${path}.diagnostic_only`, message: "must equal true" });
|
|
673
|
+
const payload = snapshot.env;
|
|
674
|
+
if (!isRecord2(payload)) {
|
|
675
|
+
errors.push({ path: `${path}.env`, message: "must be an object" });
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
validateRedactedEnv(errors, payload, `${path}.env`);
|
|
679
|
+
}
|
|
680
|
+
function validateContinuation(errors, run, path) {
|
|
681
|
+
const continuation = run.continuation;
|
|
682
|
+
if (continuation === void 0 || continuation === null) return;
|
|
683
|
+
if (!isRecord2(continuation)) {
|
|
684
|
+
errors.push({ path, message: "must be an object" });
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
requiredInteger(errors, continuation, "schema_version", `${path}.schema_version`);
|
|
688
|
+
if (Number.isInteger(continuation.schema_version) && continuation.schema_version !== 1) errors.push({ path: `${path}.schema_version`, message: "must equal 1" });
|
|
689
|
+
requiredEnum(errors, continuation, "kind", CONTINUATION_KINDS, `${path}.kind`);
|
|
690
|
+
requiredString(errors, continuation, "created_at", `${path}.created_at`);
|
|
691
|
+
requiredString(errors, continuation, "operator_summary", `${path}.operator_summary`);
|
|
692
|
+
validateContinuationParent(errors, continuation.parent, `${path}.parent`);
|
|
693
|
+
validateContinuationReview(errors, continuation.review, `${path}.review`);
|
|
694
|
+
validateContinuationTarget(errors, run, continuation.target, `${path}.target`);
|
|
695
|
+
validateContinuationRefHashArray(errors, continuation.parent_artifacts, `${path}.parent_artifacts`);
|
|
696
|
+
validateContinuationRefHashArray(errors, continuation.parent_evidence, `${path}.parent_evidence`);
|
|
697
|
+
validateContinuationRefHashArray(errors, continuation.parent_reviews, `${path}.parent_reviews`);
|
|
698
|
+
validateContinuationSelectedReview(errors, continuation, path);
|
|
699
|
+
validateContinuationPlanningReuse(errors, continuation.planning_reuse, `${path}.planning_reuse`);
|
|
700
|
+
validateContinuationPostPr(errors, continuation.post_pr, `${path}.post_pr`);
|
|
701
|
+
}
|
|
702
|
+
function validateContinuationPlanningReuse(errors, reuse, path) {
|
|
703
|
+
if (reuse === void 0 || reuse === null) return;
|
|
704
|
+
if (!isRecord2(reuse)) {
|
|
705
|
+
errors.push({ path, message: "must be an object" });
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
if (typeof reuse.eligible !== "boolean") errors.push({ path: `${path}.eligible`, message: "must be a boolean" });
|
|
709
|
+
if (reuse.eligible === true) {
|
|
710
|
+
requiredString(errors, reuse, "spec_review_ref", `${path}.spec_review_ref`);
|
|
711
|
+
requiredHash(errors, reuse, "spec_review_hash", `${path}.spec_review_hash`);
|
|
712
|
+
requiredString(errors, reuse, "spec_artifact_ref", `${path}.spec_artifact_ref`);
|
|
713
|
+
requiredHash(errors, reuse, "spec_artifact_hash", `${path}.spec_artifact_hash`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
function validateContinuationPostPr(errors, value, path) {
|
|
717
|
+
if (value === void 0 || value === null) return;
|
|
718
|
+
if (!isRecord2(value)) {
|
|
719
|
+
errors.push({ path, message: "must be an object" });
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
allowedKeys(errors, value, /* @__PURE__ */ new Set(["pr_url", "repository", "pr_number", "head_sha", "disposition", "policy", "post_pr_hash", "evidence_ref", "evidence_hash", "continuation_review_ref", "continuation_review_hash"]), path);
|
|
723
|
+
requiredString(errors, value, "pr_url", `${path}.pr_url`);
|
|
724
|
+
requiredString(errors, value, "repository", `${path}.repository`);
|
|
725
|
+
boundedInteger(errors, value, "pr_number", 1, Number.MAX_SAFE_INTEGER, `${path}.pr_number`);
|
|
726
|
+
requiredFullGitSha(errors, value, "head_sha", `${path}.head_sha`);
|
|
727
|
+
requiredEnum(errors, value, "disposition", /* @__PURE__ */ new Set(["leave-unchanged"]), `${path}.disposition`);
|
|
728
|
+
validatePostPrPolicy(errors, value.policy, `${path}.policy`);
|
|
729
|
+
for (const key of ["post_pr_hash", "evidence_hash", "continuation_review_hash"]) requiredHash(errors, value, key, `${path}.${key}`);
|
|
730
|
+
for (const key of ["evidence_ref", "continuation_review_ref"]) requiredString(errors, value, key, `${path}.${key}`);
|
|
731
|
+
}
|
|
732
|
+
function validatePostPr(errors, run, path) {
|
|
733
|
+
const value = run.post_pr;
|
|
734
|
+
if (value === void 0 || value === null) return;
|
|
735
|
+
if (!isRecord2(value)) {
|
|
736
|
+
errors.push({ path, message: "must be an object" });
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
allowedKeys(errors, value, /* @__PURE__ */ new Set(["schema_version", "policy", "phase", "attempt", "observation", "remediation", "evidence_refs", "continuation_review", "terminal_fact"]), path);
|
|
740
|
+
requiredInteger(errors, value, "schema_version", `${path}.schema_version`);
|
|
741
|
+
if (value.schema_version !== 1) errors.push({ path: `${path}.schema_version`, message: "must equal 1" });
|
|
742
|
+
validatePostPrPolicy(errors, value.policy, `${path}.policy`);
|
|
743
|
+
requiredEnum(errors, value, "phase", POST_PR_PHASE_SET, `${path}.phase`);
|
|
744
|
+
requiredInteger(errors, value, "attempt", `${path}.attempt`);
|
|
745
|
+
if (Number.isInteger(value.attempt) && value.attempt < 0) errors.push({ path: `${path}.attempt`, message: "must be non-negative" });
|
|
746
|
+
if (Number.isInteger(value.attempt) && Number.isInteger(run.max_retries) && value.attempt > run.max_retries) errors.push({ path: `${path}.attempt`, message: "must not exceed run.max_retries" });
|
|
747
|
+
validatePostPrObservation(errors, value.observation, `${path}.observation`);
|
|
748
|
+
validatePostPrRemediation(errors, value.remediation, `${path}.remediation`);
|
|
749
|
+
validatePostPrRefHashArray(errors, value.evidence_refs, `${path}.evidence_refs`);
|
|
750
|
+
validatePostPrRefHash(errors, value.continuation_review, `${path}.continuation_review`, { optional: true });
|
|
751
|
+
validatePostPrTerminalFact(errors, run, value, `${path}.terminal_fact`);
|
|
752
|
+
const enabled = value.policy?.enabled === true;
|
|
753
|
+
if (!enabled && value.phase !== "disabled") errors.push({ path: `${path}.phase`, message: "disabled policy requires disabled phase" });
|
|
754
|
+
if (enabled && value.phase === "disabled") errors.push({ path: `${path}.phase`, message: "enabled policy cannot use disabled phase" });
|
|
755
|
+
if (enabled && POST_PR_ACTIVE_PHASES.has(value.phase)) {
|
|
756
|
+
if (run.status !== "running") errors.push({ path: "run.status", message: `must be running while post-PR phase is ${value.phase}` });
|
|
757
|
+
if (!stringValue(run.pr_url)) errors.push({ path: "run.pr_url", message: `is required while post-PR phase is ${value.phase}` });
|
|
758
|
+
if (run.terminal_result !== void 0 && run.terminal_result !== null) errors.push({ path: "run.terminal_result", message: "must be null during active post-PR state" });
|
|
759
|
+
}
|
|
760
|
+
if (["observing", "remote-confirmed", "succeeded"].includes(value.phase) && !isRecord2(value.observation)) errors.push({ path: `${path}.observation`, message: `is required for ${value.phase}` });
|
|
761
|
+
if (isRecord2(value.observation) && isRecord2(value.policy)) {
|
|
762
|
+
if (value.observation.current_interval_ms < value.policy.initial_poll_ms || value.observation.current_interval_ms > value.policy.max_poll_ms) errors.push({ path: `${path}.observation.current_interval_ms`, message: "must stay within persisted poll policy" });
|
|
763
|
+
if (value.observation.consecutive_transient_errors > value.policy.max_transient_errors) errors.push({ path: `${path}.observation.consecutive_transient_errors`, message: "must not exceed persisted transient error budget" });
|
|
764
|
+
}
|
|
765
|
+
if (["failure-recording", "remediation-planned", "remediation-running", "changes-observed", "committed", "revalidating", "validated", "push-pending", "remote-confirmed"].includes(value.phase) && !isRecord2(value.remediation)) errors.push({ path: `${path}.remediation`, message: `is required for ${value.phase}` });
|
|
766
|
+
if (isRecord2(value.remediation) && value.remediation.attempt !== value.attempt) errors.push({ path: `${path}.remediation.attempt`, message: "must equal run.post_pr.attempt" });
|
|
767
|
+
if (isRecord2(value.remediation)) {
|
|
768
|
+
const expectedStage = (/* @__PURE__ */ new Map([["remediation-planned", "planned"], ["remediation-running", "running"], ["changes-observed", "changes-observed"], ["committed", "committed"], ["revalidating", "revalidating"], ["validated", "validated"], ["push-pending", "push-pending"], ["remote-confirmed", "remote-confirmed"]])).get(value.phase);
|
|
769
|
+
if (expectedStage && value.remediation.stage !== expectedStage) errors.push({ path: `${path}.remediation.stage`, message: `must be ${expectedStage} while phase is ${value.phase}` });
|
|
770
|
+
const boundFailure = Array.isArray(value.evidence_refs) && value.evidence_refs.some((item) => item?.ref === value.remediation.failure_evidence_ref && item?.hash === value.remediation.failure_evidence_hash);
|
|
771
|
+
if (!boundFailure) errors.push({ path: `${path}.evidence_refs`, message: "must bind the current failure evidence ref/hash" });
|
|
772
|
+
}
|
|
773
|
+
if (isRecord2(value.continuation_review) && !(value.phase === "blocked" && run.terminal_result?.reason === "post-pr-retry-exhausted")) errors.push({ path: `${path}.continuation_review`, message: "is allowed only for retry exhaustion" });
|
|
774
|
+
validatePostPrTerminalConsistency(errors, run, value, path);
|
|
775
|
+
}
|
|
776
|
+
function validatePostPrTerminalFact(errors, run, postPr, path) {
|
|
777
|
+
const fact = postPr.terminal_fact;
|
|
778
|
+
const reason = run.terminal_result?.reason;
|
|
779
|
+
const expectedKinds = /* @__PURE__ */ new Map([
|
|
780
|
+
["post-pr-account-switch-failed", "account-switch-failed"],
|
|
781
|
+
["post-pr-dispatch-start-unknown", "dispatch-start-unknown"],
|
|
782
|
+
["post-pr-path-lane-violation", "path-lane-violation"],
|
|
783
|
+
["post-pr-remote-head-diverged", "remote-head-diverged"],
|
|
784
|
+
["post-pr-push-failed", "push-failed"],
|
|
785
|
+
["post-pr-panel-attribution-unsafe", "panel-attribution-unsafe"]
|
|
786
|
+
]);
|
|
787
|
+
const expectedKind = expectedKinds.get(reason) || (reason === "post-pr-metadata-unsafe" && fact?.kind === "panel-runner-result-malformed" ? "panel-runner-result-malformed" : null);
|
|
788
|
+
if (fact === void 0 || fact === null) {
|
|
789
|
+
if (expectedKind) errors.push({ path, message: `is required for ${reason}` });
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
if (!isRecord2(fact)) {
|
|
793
|
+
errors.push({ path, message: "must be an object or null" });
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
if (!expectedKind) {
|
|
797
|
+
errors.push({ path, message: "is allowed only for fact-bound post-PR terminal reasons" });
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
requiredInteger(errors, fact, "schema_version", `${path}.schema_version`);
|
|
801
|
+
if (fact.schema_version !== 1) errors.push({ path: `${path}.schema_version`, message: "must equal 1" });
|
|
802
|
+
requiredEnum(errors, fact, "kind", /* @__PURE__ */ new Set([expectedKind]), `${path}.kind`);
|
|
803
|
+
requiredString(errors, fact, "observed_at", `${path}.observed_at`);
|
|
804
|
+
if (!Number.isFinite(Date.parse(fact.observed_at || ""))) errors.push({ path: `${path}.observed_at`, message: "must be an ISO timestamp" });
|
|
805
|
+
const remediation = postPr.remediation;
|
|
806
|
+
if (expectedKind === "account-switch-failed") {
|
|
807
|
+
const pushPhase = fact.operation !== "gh-auth-switch";
|
|
808
|
+
allowedKeys(errors, fact, pushPhase ? /* @__PURE__ */ new Set(["schema_version", "kind", "observed_at", "attempt", "operation", "error_class", "exit_code", "classification", "error_count", "error_limit", "expected_remote_sha", "candidate_head_sha", "next_retry_at"]) : /* @__PURE__ */ new Set(["schema_version", "kind", "observed_at", "operation", "github_account", "error_class", "exit_code"]), path);
|
|
809
|
+
requiredEnum(errors, fact, "operation", pushPhase ? /* @__PURE__ */ new Set(["remote-head", "fast-forward-push", "remote-confirmation"]) : /* @__PURE__ */ new Set(["gh-auth-switch"]), `${path}.operation`);
|
|
810
|
+
requiredEnum(errors, fact, "error_class", /* @__PURE__ */ new Set(["account-auth", "permission", "not-found", "protocol", "command"]), `${path}.error_class`);
|
|
811
|
+
if (fact.exit_code !== null) boundedInteger(errors, fact, "exit_code", 0, 255, `${path}.exit_code`);
|
|
812
|
+
if (pushPhase) {
|
|
813
|
+
boundedInteger(errors, fact, "attempt", 1, Number.MAX_SAFE_INTEGER, `${path}.attempt`);
|
|
814
|
+
requiredEnum(errors, fact, "classification", /* @__PURE__ */ new Set(["permanent"]), `${path}.classification`);
|
|
815
|
+
boundedInteger(errors, fact, "error_count", 1, Number.MAX_SAFE_INTEGER, `${path}.error_count`);
|
|
816
|
+
boundedInteger(errors, fact, "error_limit", 1, Number.MAX_SAFE_INTEGER, `${path}.error_limit`);
|
|
817
|
+
for (const key of ["expected_remote_sha", "candidate_head_sha"]) requiredFullGitSha(errors, fact, key, `${path}.${key}`);
|
|
818
|
+
if (fact.next_retry_at !== null || fact.operation !== remediation?.push?.last_error?.operation) errors.push({ path, message: "must bind the persisted push account failure exactly" });
|
|
819
|
+
} else {
|
|
820
|
+
requiredString(errors, fact, "github_account", `${path}.github_account`);
|
|
821
|
+
if (fact.github_account !== run.github_account) errors.push({ path: `${path}.github_account`, message: "must match run.github_account" });
|
|
822
|
+
if (fact.error_class !== postPr.observation?.last_error?.class || fact.exit_code !== (postPr.observation?.last_error?.exit_code ?? null) || fact.observed_at !== postPr.observation?.last_error?.occurred_at) errors.push({ path, message: "must bind the persisted account-switch error exactly" });
|
|
823
|
+
}
|
|
824
|
+
} else if (expectedKind === "dispatch-start-unknown") {
|
|
825
|
+
allowedKeys(errors, fact, /* @__PURE__ */ new Set(["schema_version", "kind", "observed_at", "attempt", "activity", "dispatch_id", "dispatch_started_at", "candidate_head_sha", "outcome"]), path);
|
|
826
|
+
boundedInteger(errors, fact, "attempt", 1, Number.MAX_SAFE_INTEGER, `${path}.attempt`);
|
|
827
|
+
requiredString(errors, fact, "dispatch_id", `${path}.dispatch_id`);
|
|
828
|
+
requiredString(errors, fact, "dispatch_started_at", `${path}.dispatch_started_at`);
|
|
829
|
+
if (fact.activity !== void 0) requiredEnum(errors, fact, "activity", /* @__PURE__ */ new Set(["remediation", "canonical", "validator", "security"]), `${path}.activity`);
|
|
830
|
+
if (fact.candidate_head_sha !== void 0 && fact.candidate_head_sha !== null) requiredFullGitSha(errors, fact, "candidate_head_sha", `${path}.candidate_head_sha`);
|
|
831
|
+
requiredEnum(errors, fact, "outcome", /* @__PURE__ */ new Set(["return-unknown"]), `${path}.outcome`);
|
|
832
|
+
const dispatch = fact.activity && fact.activity !== "remediation" ? remediation?.revalidation?.jobs?.[fact.activity] : remediation?.dispatch;
|
|
833
|
+
const dispatchId = fact.activity && fact.activity !== "remediation" ? dispatch?.dispatch_id : dispatch?.id;
|
|
834
|
+
if (fact.attempt !== postPr.attempt || fact.dispatch_id !== dispatchId || fact.dispatch_started_at !== dispatch?.started_at || dispatch?.status !== "running") errors.push({ path, message: "must bind the running dispatch identity exactly" });
|
|
835
|
+
} else if (expectedKind === "path-lane-violation") {
|
|
836
|
+
allowedKeys(errors, fact, /* @__PURE__ */ new Set(["schema_version", "kind", "observed_at", "attempt", "lane", "source", "violation", "path_b64url", "changes_hash"]), path);
|
|
837
|
+
boundedInteger(errors, fact, "attempt", 1, Number.MAX_SAFE_INTEGER, `${path}.attempt`);
|
|
838
|
+
requiredEnum(errors, fact, "lane", /* @__PURE__ */ new Set(["slice", "test"]), `${path}.lane`);
|
|
839
|
+
requiredEnum(errors, fact, "source", /* @__PURE__ */ new Set(["remediation-diff"]), `${path}.source`);
|
|
840
|
+
requiredEnum(errors, fact, "violation", /* @__PURE__ */ new Set(["outside-lane", "unsafe-change-kind", "symlink-escape"]), `${path}.violation`);
|
|
841
|
+
requiredString(errors, fact, "path_b64url", `${path}.path_b64url`);
|
|
842
|
+
if (stringValue(fact.path_b64url) && !/^[A-Za-z0-9_-]+$/u.test(fact.path_b64url)) errors.push({ path: `${path}.path_b64url`, message: "must be canonical base64url" });
|
|
843
|
+
const decodedPath = decodeCanonicalBase64url(fact.path_b64url);
|
|
844
|
+
if (stringValue(fact.path_b64url) && decodedPath === null) errors.push({ path: `${path}.path_b64url`, message: "must encode valid UTF-8 path bytes canonically" });
|
|
845
|
+
requiredHash(errors, fact, "changes_hash", `${path}.changes_hash`);
|
|
846
|
+
if (fact.attempt !== postPr.attempt || fact.lane !== remediation?.lane || fact.changes_hash !== hashValueForValidation(remediation?.changes)) errors.push({ path, message: "must bind the remediation lane and changed paths exactly" });
|
|
847
|
+
if (decodedPath !== null && !remediation?.changes?.paths?.includes(decodedPath)) errors.push({ path: `${path}.path_b64url`, message: "must identify a persisted changed path" });
|
|
848
|
+
} else if (expectedKind === "remote-head-diverged") {
|
|
849
|
+
allowedKeys(errors, fact, /* @__PURE__ */ new Set(["schema_version", "kind", "observed_at", "attempt", "expected_remote_sha", "candidate_head_sha", "observed_remote_sha"]), path);
|
|
850
|
+
boundedInteger(errors, fact, "attempt", 1, Number.MAX_SAFE_INTEGER, `${path}.attempt`);
|
|
851
|
+
for (const key of ["expected_remote_sha", "candidate_head_sha", "observed_remote_sha"]) requiredFullGitSha(errors, fact, key, `${path}.${key}`);
|
|
852
|
+
if (fact.attempt !== postPr.attempt || fact.expected_remote_sha !== remediation?.push?.remote_before_sha || fact.candidate_head_sha !== remediation?.candidate_head_sha) errors.push({ path, message: "must bind the push-pending remote and candidate heads exactly" });
|
|
853
|
+
if (fact.observed_remote_sha === fact.expected_remote_sha || fact.observed_remote_sha === fact.candidate_head_sha) errors.push({ path: `${path}.observed_remote_sha`, message: "must differ from both expected remote and candidate heads" });
|
|
854
|
+
} else if (expectedKind === "panel-runner-result-malformed") {
|
|
855
|
+
allowedKeys(errors, fact, /* @__PURE__ */ new Set(["schema_version", "kind", "observed_at", "attempt", "activity", "dispatch_id", "candidate_head_sha", "issue"]), path);
|
|
856
|
+
boundedInteger(errors, fact, "attempt", 1, Number.MAX_SAFE_INTEGER, `${path}.attempt`);
|
|
857
|
+
requiredEnum(errors, fact, "activity", /* @__PURE__ */ new Set(["validator", "security"]), `${path}.activity`);
|
|
858
|
+
requiredString(errors, fact, "dispatch_id", `${path}.dispatch_id`);
|
|
859
|
+
requiredFullGitSha(errors, fact, "candidate_head_sha", `${path}.candidate_head_sha`);
|
|
860
|
+
requiredEnum(errors, fact, "issue", /* @__PURE__ */ new Set(["non-object", "missing-verdict", "unexpected-result-keys", "invalid-verdict"]), `${path}.issue`);
|
|
861
|
+
const job = remediation?.revalidation?.jobs?.[fact.activity];
|
|
862
|
+
if (fact.attempt !== postPr.attempt || fact.candidate_head_sha !== remediation?.candidate_head_sha || fact.dispatch_id !== job?.dispatch_id || job?.status !== "running") errors.push({ path, message: "must bind the running panel job exactly" });
|
|
863
|
+
} else if (expectedKind === "push-failed") {
|
|
864
|
+
allowedKeys(errors, fact, /* @__PURE__ */ new Set(["schema_version", "kind", "observed_at", "attempt", "operation", "error_class", "exit_code", "classification", "error_count", "error_limit", "expected_remote_sha", "candidate_head_sha", "next_retry_at"]), path);
|
|
865
|
+
boundedInteger(errors, fact, "attempt", 1, Number.MAX_SAFE_INTEGER, `${path}.attempt`);
|
|
866
|
+
requiredEnum(errors, fact, "operation", /* @__PURE__ */ new Set(["remote-head", "fast-forward-push", "remote-confirmation"]), `${path}.operation`);
|
|
867
|
+
requiredEnum(errors, fact, "error_class", /* @__PURE__ */ new Set(["timeout", "network", "rate-limit", "server", "account-auth", "permission", "not-found", "protocol", "command", "non-fast-forward"]), `${path}.error_class`);
|
|
868
|
+
if (fact.exit_code !== null) boundedInteger(errors, fact, "exit_code", 0, 255, `${path}.exit_code`);
|
|
869
|
+
requiredEnum(errors, fact, "classification", /* @__PURE__ */ new Set(["permanent", "exhausted"]), `${path}.classification`);
|
|
870
|
+
boundedInteger(errors, fact, "error_count", 1, Number.MAX_SAFE_INTEGER, `${path}.error_count`);
|
|
871
|
+
boundedInteger(errors, fact, "error_limit", 1, Number.MAX_SAFE_INTEGER, `${path}.error_limit`);
|
|
872
|
+
for (const key of ["expected_remote_sha", "candidate_head_sha"]) requiredFullGitSha(errors, fact, key, `${path}.${key}`);
|
|
873
|
+
if (fact.next_retry_at !== null) errors.push({ path: `${path}.next_retry_at`, message: "must be null for terminal push failures" });
|
|
874
|
+
if (fact.attempt !== postPr.attempt || fact.error_count !== remediation?.push?.consecutive_transient_errors || fact.error_limit !== postPr.policy?.max_transient_errors || fact.operation !== remediation?.push?.last_error?.operation) errors.push({ path, message: "must bind the persisted push failure exactly" });
|
|
875
|
+
} else if (expectedKind === "panel-attribution-unsafe") {
|
|
876
|
+
allowedKeys(errors, fact, /* @__PURE__ */ new Set(["schema_version", "kind", "observed_at", "attempt", "candidate_head_sha", "panel", "category", "affected_paths_hash"]), path);
|
|
877
|
+
boundedInteger(errors, fact, "attempt", 1, Number.MAX_SAFE_INTEGER, `${path}.attempt`);
|
|
878
|
+
requiredFullGitSha(errors, fact, "candidate_head_sha", `${path}.candidate_head_sha`);
|
|
879
|
+
requiredEnum(errors, fact, "panel", /* @__PURE__ */ new Set(["validator", "security", "combined"]), `${path}.panel`);
|
|
880
|
+
requiredEnum(errors, fact, "category", /* @__PURE__ */ new Set(["missing-paths", "invalid-paths", "empty-paths", "mixed-owner", "unowned-path", "owner-conflict", "security-block-without-slice-owner"]), `${path}.category`);
|
|
881
|
+
requiredBareSha256(errors, fact, "affected_paths_hash", `${path}.affected_paths_hash`);
|
|
882
|
+
if (fact.attempt !== postPr.attempt || fact.candidate_head_sha !== remediation?.candidate_head_sha) errors.push({ path, message: "must bind the current panel candidate exactly" });
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
function hashValueForValidation(value) {
|
|
886
|
+
return value === void 0 ? null : hashValue(value);
|
|
887
|
+
}
|
|
888
|
+
function requiredBareSha256(errors, object, key, path) {
|
|
889
|
+
if (typeof object?.[key] !== "string" || !/^[0-9a-f]{64}$/u.test(object[key])) errors.push({ path, message: "must be a bare lowercase SHA-256 digest" });
|
|
890
|
+
}
|
|
891
|
+
function decodeCanonicalBase64url(value) {
|
|
892
|
+
if (!stringValue(value) || !/^[A-Za-z0-9_-]+$/u.test(value)) return null;
|
|
893
|
+
const bytes = Buffer.from(value, "base64url");
|
|
894
|
+
const decoded = bytes.toString("utf8");
|
|
895
|
+
return Buffer.from(decoded, "utf8").toString("base64url") === value ? decoded : null;
|
|
896
|
+
}
|
|
897
|
+
function validatePostPrPolicy(errors, policy, path) {
|
|
898
|
+
if (!isRecord2(policy)) {
|
|
899
|
+
errors.push({ path, message: "must be an object" });
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
allowedKeys(errors, policy, /* @__PURE__ */ new Set(["enabled", "wait_ms", "initial_poll_ms", "max_poll_ms", "check_start_grace_ms", "max_transient_errors", "review"]), path);
|
|
903
|
+
requiredBoolean(errors, policy, "enabled", `${path}.enabled`);
|
|
904
|
+
boundedInteger(errors, policy, "wait_ms", 18e5, 864e5, `${path}.wait_ms`);
|
|
905
|
+
boundedInteger(errors, policy, "initial_poll_ms", 15e3, 3e5, `${path}.initial_poll_ms`);
|
|
906
|
+
boundedInteger(errors, policy, "max_poll_ms", 15e3, 6e5, `${path}.max_poll_ms`);
|
|
907
|
+
boundedInteger(errors, policy, "check_start_grace_ms", 6e4, 9e5, `${path}.check_start_grace_ms`);
|
|
908
|
+
boundedInteger(errors, policy, "max_transient_errors", 1, 50, `${path}.max_transient_errors`);
|
|
909
|
+
if (Number.isInteger(policy.initial_poll_ms) && Number.isInteger(policy.max_poll_ms) && policy.max_poll_ms < policy.initial_poll_ms) errors.push({ path: `${path}.max_poll_ms`, message: "must be greater than or equal to initial_poll_ms" });
|
|
910
|
+
const review = policy.review;
|
|
911
|
+
if (!isRecord2(review)) errors.push({ path: `${path}.review`, message: "must be an object" });
|
|
912
|
+
else {
|
|
913
|
+
allowedKeys(errors, review, /* @__PURE__ */ new Set(["required", "reviewer_login", "source"]), `${path}.review`);
|
|
914
|
+
requiredBoolean(errors, review, "required", `${path}.review.required`);
|
|
915
|
+
requiredEnum(errors, review, "source", /* @__PURE__ */ new Set(["driver", "none"]), `${path}.review.source`);
|
|
916
|
+
if (review.required === true) {
|
|
917
|
+
requiredString(errors, review, "reviewer_login", `${path}.review.reviewer_login`);
|
|
918
|
+
if (stringValue(review.reviewer_login) && !/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/u.test(review.reviewer_login)) errors.push({ path: `${path}.review.reviewer_login`, message: "must be a valid GitHub login" });
|
|
919
|
+
if (review.source !== "driver") errors.push({ path: `${path}.review.source`, message: "required review must use driver source" });
|
|
920
|
+
} else if (review.reviewer_login !== null) errors.push({ path: `${path}.review.reviewer_login`, message: "must be null when review is not required" });
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
function validatePostPrObservation(errors, observation, path) {
|
|
924
|
+
if (observation === void 0 || observation === null) return;
|
|
925
|
+
if (!isRecord2(observation)) {
|
|
926
|
+
errors.push({ path, message: "must be an object or null" });
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
allowedKeys(errors, observation, /* @__PURE__ */ new Set(["epoch", "expected_head_sha", "started_at", "deadline_at", "next_poll_at", "poll_count", "unchanged_count", "current_interval_ms", "consecutive_transient_errors", "last_observed_at", "last_fingerprint", "last_check_verdict", "last_review_verdict", "last_verdict", "last_error", "review_request", "snapshot"]), path);
|
|
930
|
+
boundedInteger(errors, observation, "epoch", 1, Number.MAX_SAFE_INTEGER, `${path}.epoch`);
|
|
931
|
+
requiredFullGitSha(errors, observation, "expected_head_sha", `${path}.expected_head_sha`);
|
|
932
|
+
for (const key of ["started_at", "deadline_at", "next_poll_at"]) requiredString(errors, observation, key, `${path}.${key}`);
|
|
933
|
+
for (const key of ["poll_count", "unchanged_count", "consecutive_transient_errors"]) boundedInteger(errors, observation, key, 0, Number.MAX_SAFE_INTEGER, `${path}.${key}`);
|
|
934
|
+
boundedInteger(errors, observation, "current_interval_ms", 1, 6e5, `${path}.current_interval_ms`);
|
|
935
|
+
for (const key of ["last_observed_at", "last_fingerprint"]) optionalNullableString(errors, observation, key, `${path}.${key}`);
|
|
936
|
+
requiredEnum(errors, observation, "last_check_verdict", /* @__PURE__ */ new Set(["not_started", "not_applicable", "pending", "pass", "red", "indeterminate"]), `${path}.last_check_verdict`);
|
|
937
|
+
requiredEnum(errors, observation, "last_review_verdict", /* @__PURE__ */ new Set(["not_required", "pending", "pass", "red", "indeterminate", "deferred"]), `${path}.last_review_verdict`);
|
|
938
|
+
requiredEnum(errors, observation, "last_verdict", /* @__PURE__ */ new Set(["pending", "green", "red", "external-merge", "closed", "head-mismatch", "infrastructure"]), `${path}.last_verdict`);
|
|
939
|
+
validatePostPrLastError(errors, observation.last_error, `${path}.last_error`);
|
|
940
|
+
validatePostPrReviewRequest(errors, observation.review_request, `${path}.review_request`);
|
|
941
|
+
if (observation.snapshot !== void 0 && observation.snapshot !== null) validatePostPrSanitizedSnapshot(errors, observation.snapshot, `${path}.snapshot`);
|
|
942
|
+
const started = Date.parse(observation.started_at || "");
|
|
943
|
+
const deadline = Date.parse(observation.deadline_at || "");
|
|
944
|
+
const nextPoll = Date.parse(observation.next_poll_at || "");
|
|
945
|
+
if (Number.isFinite(started) && Number.isFinite(deadline) && deadline <= started) errors.push({ path: `${path}.deadline_at`, message: "must be after started_at" });
|
|
946
|
+
if (Number.isFinite(deadline) && Number.isFinite(nextPoll) && nextPoll > deadline) errors.push({ path: `${path}.next_poll_at`, message: "must not exceed deadline_at" });
|
|
947
|
+
}
|
|
948
|
+
function validatePostPrLastError(errors, value, path) {
|
|
949
|
+
if (value === void 0 || value === null) return;
|
|
950
|
+
if (!isRecord2(value)) {
|
|
951
|
+
errors.push({ path, message: "must be an object or null" });
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
allowedKeys(errors, value, /* @__PURE__ */ new Set(["class", "exit_code", "occurred_at", "next_retry_at"]), path);
|
|
955
|
+
requiredEnum(errors, value, "class", /* @__PURE__ */ new Set(["timeout", "network", "rate-limit", "server", "account-auth", "permission", "not-found", "protocol", "command"]), `${path}.class`);
|
|
956
|
+
if (value.exit_code !== void 0) optionalNullableInteger(errors, value, "exit_code", `${path}.exit_code`);
|
|
957
|
+
requiredString(errors, value, "occurred_at", `${path}.occurred_at`);
|
|
958
|
+
optionalNullableString(errors, value, "next_retry_at", `${path}.next_retry_at`);
|
|
959
|
+
}
|
|
960
|
+
function validatePostPrReviewRequest(errors, value, path) {
|
|
961
|
+
if (value === void 0 || value === null) return;
|
|
962
|
+
if (!isRecord2(value)) {
|
|
963
|
+
errors.push({ path, message: "must be an object or null" });
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
allowedKeys(errors, value, /* @__PURE__ */ new Set(["status", "attempts", "requested_at"]), path);
|
|
967
|
+
requiredEnum(errors, value, "status", /* @__PURE__ */ new Set(["pending", "requested"]), `${path}.status`);
|
|
968
|
+
boundedInteger(errors, value, "attempts", 0, Number.MAX_SAFE_INTEGER, `${path}.attempts`);
|
|
969
|
+
optionalNullableString(errors, value, "requested_at", `${path}.requested_at`);
|
|
970
|
+
if (value.status === "requested" && !stringValue(value.requested_at)) errors.push({ path: `${path}.requested_at`, message: "is required after reviewer request" });
|
|
971
|
+
}
|
|
972
|
+
function validatePostPrSanitizedSnapshot(errors, value, path) {
|
|
973
|
+
if (Array.isArray(value)) {
|
|
974
|
+
for (const [index, item] of value.entries()) validatePostPrSanitizedSnapshot(errors, item, `${path}[${index}]`);
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
if (!isRecord2(value)) return;
|
|
978
|
+
for (const [key, item] of Object.entries(value)) {
|
|
979
|
+
if (/^(?:raw|body|stdout|stderr|headers?|token|credentials?)$/iu.test(key)) errors.push({ path: `${path}.${key}`, message: "untrusted raw or sensitive data is not allowed" });
|
|
980
|
+
validatePostPrSanitizedSnapshot(errors, item, `${path}.${key}`);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
function validatePostPrRemediation(errors, remediation, path) {
|
|
984
|
+
if (remediation === void 0 || remediation === null) return;
|
|
985
|
+
if (!isRecord2(remediation)) {
|
|
986
|
+
errors.push({ path, message: "must be an object or null" });
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
allowedKeys(errors, remediation, /* @__PURE__ */ new Set(["schema_version", "attempt", "reason_code", "failure_fingerprint", "failed_head_sha", "failure_evidence_ref", "failure_evidence_hash", "owner", "route", "lane", "stage", "baseline_head_sha", "dispatch", "changes", "candidate_head_sha", "remediation_evidence_ref", "remediation_evidence_hash", "revalidation", "push"]), path);
|
|
990
|
+
requiredInteger(errors, remediation, "schema_version", `${path}.schema_version`);
|
|
991
|
+
if (remediation.schema_version !== 1) errors.push({ path: `${path}.schema_version`, message: "must equal 1" });
|
|
992
|
+
boundedInteger(errors, remediation, "attempt", 1, Number.MAX_SAFE_INTEGER, `${path}.attempt`);
|
|
993
|
+
requiredEnum(errors, remediation, "reason_code", /* @__PURE__ */ new Set(["check-red", "local-red"]), `${path}.reason_code`);
|
|
994
|
+
requiredHash(errors, remediation, "failure_fingerprint", `${path}.failure_fingerprint`);
|
|
995
|
+
requiredFullGitSha(errors, remediation, "failed_head_sha", `${path}.failed_head_sha`);
|
|
996
|
+
requiredString(errors, remediation, "failure_evidence_ref", `${path}.failure_evidence_ref`);
|
|
997
|
+
requiredHash(errors, remediation, "failure_evidence_hash", `${path}.failure_evidence_hash`);
|
|
998
|
+
validatePostPrOwner(errors, remediation.owner, `${path}.owner`, remediation.route, remediation.lane);
|
|
999
|
+
requiredEnum(errors, remediation, "route", /* @__PURE__ */ new Set(["backend-builder", "frontend-builder", "test-verifier"]), `${path}.route`);
|
|
1000
|
+
requiredEnum(errors, remediation, "lane", /* @__PURE__ */ new Set(["slice", "test"]), `${path}.lane`);
|
|
1001
|
+
requiredEnum(errors, remediation, "stage", /* @__PURE__ */ new Set(["planned", "running", "changes-observed", "committed", "revalidating", "validated", "push-pending", "remote-confirmed"]), `${path}.stage`);
|
|
1002
|
+
requiredFullGitSha(errors, remediation, "baseline_head_sha", `${path}.baseline_head_sha`);
|
|
1003
|
+
validatePostPrDispatch(errors, remediation.dispatch, `${path}.dispatch`, remediation);
|
|
1004
|
+
validatePostPrChanges(errors, remediation.changes, `${path}.changes`);
|
|
1005
|
+
optionalNullableFullGitSha(errors, remediation, "candidate_head_sha", `${path}.candidate_head_sha`);
|
|
1006
|
+
optionalNullableString(errors, remediation, "remediation_evidence_ref", `${path}.remediation_evidence_ref`);
|
|
1007
|
+
optionalNullableHash(errors, remediation, "remediation_evidence_hash", `${path}.remediation_evidence_hash`);
|
|
1008
|
+
if (remediation.remediation_evidence_ref === null !== (remediation.remediation_evidence_hash === null)) errors.push({ path, message: "remediation evidence ref/hash must be set together" });
|
|
1009
|
+
validatePostPrRevalidation(errors, remediation.revalidation, `${path}.revalidation`);
|
|
1010
|
+
validatePostPrPush(errors, remediation.push, `${path}.push`);
|
|
1011
|
+
if (stringValue(remediation.candidate_head_sha) && remediation.candidate_head_sha === remediation.failed_head_sha) errors.push({ path: `${path}.candidate_head_sha`, message: "must differ from failed_head_sha" });
|
|
1012
|
+
if (["validated", "push-pending", "remote-confirmed"].includes(remediation.stage)) validatePostPrValidated(errors, remediation, path);
|
|
1013
|
+
if (remediation.stage === "push-pending" && remediation.push?.local_head_sha !== remediation.candidate_head_sha) errors.push({ path: `${path}.push.local_head_sha`, message: "must equal candidate_head_sha while push is pending" });
|
|
1014
|
+
if (remediation.stage === "remote-confirmed" && (remediation.push?.status !== "confirmed" || remediation.push?.remote_after_sha !== remediation.candidate_head_sha)) errors.push({ path: `${path}.push`, message: "remote-confirmed requires confirmed remote_after_sha equal to candidate_head_sha" });
|
|
1015
|
+
}
|
|
1016
|
+
function validatePostPrOwner(errors, owner, path, route, lane) {
|
|
1017
|
+
if (!isRecord2(owner)) {
|
|
1018
|
+
errors.push({ path, message: "must be an object" });
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
allowedKeys(errors, owner, /* @__PURE__ */ new Set(["kind", "slice_id", "stack", "path_b64url", "method"]), path);
|
|
1022
|
+
requiredEnum(errors, owner, "kind", /* @__PURE__ */ new Set(["slice", "integration"]), `${path}.kind`);
|
|
1023
|
+
requiredString(errors, owner, "method", `${path}.method`);
|
|
1024
|
+
if (owner.kind === "slice") {
|
|
1025
|
+
requiredString(errors, owner, "slice_id", `${path}.slice_id`);
|
|
1026
|
+
requiredEnum(errors, owner, "stack", /* @__PURE__ */ new Set(["backend", "frontend"]), `${path}.stack`);
|
|
1027
|
+
if (lane !== "slice" || route !== `${owner.stack}-builder`) errors.push({ path, message: "slice owner requires matching slice lane and builder route" });
|
|
1028
|
+
} else if (lane !== "test" || route !== "test-verifier") errors.push({ path, message: "integration owner requires test lane and test-verifier route" });
|
|
1029
|
+
}
|
|
1030
|
+
function validatePostPrDispatch(errors, dispatch, path, remediation) {
|
|
1031
|
+
if (!isRecord2(dispatch)) {
|
|
1032
|
+
errors.push({ path, message: "must be an object" });
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
allowedKeys(errors, dispatch, /* @__PURE__ */ new Set(["id", "status", "role", "subject", "started_at", "returned_at"]), path);
|
|
1036
|
+
requiredString(errors, dispatch, "id", `${path}.id`);
|
|
1037
|
+
requiredEnum(errors, dispatch, "status", /* @__PURE__ */ new Set(["planned", "running", "returned"]), `${path}.status`);
|
|
1038
|
+
requiredString(errors, dispatch, "role", `${path}.role`);
|
|
1039
|
+
requiredString(errors, dispatch, "subject", `${path}.subject`);
|
|
1040
|
+
optionalNullableString(errors, dispatch, "started_at", `${path}.started_at`);
|
|
1041
|
+
optionalNullableString(errors, dispatch, "returned_at", `${path}.returned_at`);
|
|
1042
|
+
if (stringValue(remediation.route) && dispatch.role !== remediation.route) errors.push({ path: `${path}.role`, message: "must match remediation route" });
|
|
1043
|
+
}
|
|
1044
|
+
function validatePostPrChanges(errors, changes, path) {
|
|
1045
|
+
if (!isRecord2(changes)) {
|
|
1046
|
+
errors.push({ path, message: "must be an object" });
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
allowedKeys(errors, changes, /* @__PURE__ */ new Set(["paths", "entries", "tree_hash"]), path);
|
|
1050
|
+
validateStringArray(errors, changes.paths, `${path}.paths`, { required: true });
|
|
1051
|
+
if (changes.entries !== void 0) {
|
|
1052
|
+
if (!Array.isArray(changes.entries)) errors.push({ path: `${path}.entries`, message: "must be an array" });
|
|
1053
|
+
else changes.entries.forEach((entry, index) => validatePostPrChangeEntry(errors, entry, `${path}.entries[${index}]`));
|
|
1054
|
+
}
|
|
1055
|
+
optionalNullableHash(errors, changes, "tree_hash", `${path}.tree_hash`);
|
|
1056
|
+
}
|
|
1057
|
+
function validatePostPrChangeEntry(errors, entry, path) {
|
|
1058
|
+
if (!isRecord2(entry)) {
|
|
1059
|
+
errors.push({ path, message: "must be an object" });
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
allowedKeys(errors, entry, /* @__PURE__ */ new Set(["source", "status", "index_status", "worktree_status", "path", "previous_path", "old_mode", "new_mode"]), path);
|
|
1063
|
+
requiredEnum(errors, entry, "source", /* @__PURE__ */ new Set(["worktree", "commit"]), `${path}.source`);
|
|
1064
|
+
requiredEnum(errors, entry, "status", /* @__PURE__ */ new Set(["modified", "added", "untracked", "deleted", "renamed", "copied"]), `${path}.status`);
|
|
1065
|
+
requiredString(errors, entry, "path", `${path}.path`);
|
|
1066
|
+
for (const key of ["index_status", "worktree_status", "previous_path", "old_mode", "new_mode"]) if (entry[key] !== void 0 && entry[key] !== null && typeof entry[key] !== "string") errors.push({ path: `${path}.${key}`, message: "must be a string or null" });
|
|
1067
|
+
}
|
|
1068
|
+
function validatePostPrRevalidation(errors, value, path) {
|
|
1069
|
+
if (!isRecord2(value)) {
|
|
1070
|
+
errors.push({ path, message: "must be an object" });
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
const refs = ["canonical_evidence", "validator_review", "security_review"];
|
|
1074
|
+
allowedKeys(errors, value, new Set(refs.flatMap((name) => [`${name}_ref`, `${name}_hash`]).concat(["canonical_verdict", "validator_verdict", "security_verdict", "jobs"])), path);
|
|
1075
|
+
for (const name of refs) {
|
|
1076
|
+
optionalNullableString(errors, value, `${name}_ref`, `${path}.${name}_ref`);
|
|
1077
|
+
optionalNullableHash(errors, value, `${name}_hash`, `${path}.${name}_hash`);
|
|
1078
|
+
if (value[`${name}_ref`] === null !== (value[`${name}_hash`] === null)) errors.push({ path, message: `${name} ref/hash must be set together` });
|
|
1079
|
+
}
|
|
1080
|
+
optionalNullableEnum(errors, value, "canonical_verdict", /* @__PURE__ */ new Set(["pass", "fail"]), `${path}.canonical_verdict`);
|
|
1081
|
+
optionalNullableEnum(errors, value, "validator_verdict", VALIDATOR_VERDICTS, `${path}.validator_verdict`);
|
|
1082
|
+
optionalNullableEnum(errors, value, "security_verdict", SECURITY_VERDICTS, `${path}.security_verdict`);
|
|
1083
|
+
if (value.jobs !== void 0) validatePostPrJobs(errors, value.jobs, `${path}.jobs`);
|
|
1084
|
+
}
|
|
1085
|
+
function validatePostPrJobs(errors, jobs, path) {
|
|
1086
|
+
if (!isRecord2(jobs)) {
|
|
1087
|
+
errors.push({ path, message: "must be an object" });
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
allowedKeys(errors, jobs, /* @__PURE__ */ new Set(["canonical", "validator", "security"]), path);
|
|
1091
|
+
for (const activity of ["canonical", "validator", "security"]) if (jobs[activity] !== void 0) validatePostPrJob(errors, jobs[activity], `${path}.${activity}`, activity);
|
|
1092
|
+
}
|
|
1093
|
+
function validatePostPrJob(errors, job, path, activity) {
|
|
1094
|
+
if (!isRecord2(job)) {
|
|
1095
|
+
errors.push({ path, message: "must be an object" });
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
allowedKeys(errors, job, /* @__PURE__ */ new Set(["dispatch_id", "status", "action_token", "steering_generation", "started_at", "returned_at", "result_ref", "result_hash", "verdict", "transient_error_count", "next_retry_at", "last_error"]), path);
|
|
1099
|
+
requiredString(errors, job, "dispatch_id", `${path}.dispatch_id`);
|
|
1100
|
+
requiredEnum(errors, job, "status", /* @__PURE__ */ new Set(["planned", "running", "retry-wait", "bound"]), `${path}.status`);
|
|
1101
|
+
for (const key of ["action_token", "started_at", "returned_at", "result_ref", "result_hash", "verdict", "next_retry_at", "last_error"]) if (job[key] !== null && job[key] !== void 0 && typeof job[key] !== "string") errors.push({ path: `${path}.${key}`, message: "must be a string or null" });
|
|
1102
|
+
if (job.steering_generation !== null && job.steering_generation !== void 0 && (!Number.isInteger(job.steering_generation) || job.steering_generation < 0)) errors.push({ path: `${path}.steering_generation`, message: "must be a non-negative integer or null" });
|
|
1103
|
+
boundedInteger(errors, job, "transient_error_count", 0, Number.MAX_SAFE_INTEGER, `${path}.transient_error_count`);
|
|
1104
|
+
if (job.status === "running" && (!stringValue(job.action_token) || !stringValue(job.started_at))) errors.push({ path, message: "running job requires action token and start time" });
|
|
1105
|
+
if (job.status === "bound" && (!stringValue(job.returned_at) || !stringValue(job.result_ref) || !stringValue(job.result_hash) || !stringValue(job.verdict))) errors.push({ path, message: "bound job requires return/ref/hash/verdict" });
|
|
1106
|
+
const vocabulary = activity === "canonical" ? /* @__PURE__ */ new Set(["pass", "red"]) : activity === "validator" ? VALIDATOR_VERDICTS : SECURITY_VERDICTS;
|
|
1107
|
+
if (stringValue(job.verdict) && !vocabulary.has(job.verdict)) errors.push({ path: `${path}.verdict`, message: "is outside the activity verdict vocabulary" });
|
|
1108
|
+
}
|
|
1109
|
+
function validatePostPrPush(errors, push, path) {
|
|
1110
|
+
if (!isRecord2(push)) {
|
|
1111
|
+
errors.push({ path, message: "must be an object" });
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
allowedKeys(errors, push, /* @__PURE__ */ new Set(["status", "remote_before_sha", "local_head_sha", "remote_after_sha", "consecutive_transient_errors", "next_retry_at", "pushed_at", "last_error"]), path);
|
|
1115
|
+
requiredEnum(errors, push, "status", /* @__PURE__ */ new Set(["not-ready", "pending", "confirmed"]), `${path}.status`);
|
|
1116
|
+
for (const key of ["remote_before_sha", "local_head_sha", "remote_after_sha"]) optionalNullableFullGitSha(errors, push, key, `${path}.${key}`);
|
|
1117
|
+
boundedInteger(errors, push, "consecutive_transient_errors", 0, Number.MAX_SAFE_INTEGER, `${path}.consecutive_transient_errors`);
|
|
1118
|
+
optionalNullableString(errors, push, "next_retry_at", `${path}.next_retry_at`);
|
|
1119
|
+
optionalNullableString(errors, push, "pushed_at", `${path}.pushed_at`);
|
|
1120
|
+
if (push.last_error !== void 0 && push.last_error !== null) {
|
|
1121
|
+
const error = push.last_error;
|
|
1122
|
+
if (!isRecord2(error)) errors.push({ path: `${path}.last_error`, message: "must be an object or null" });
|
|
1123
|
+
else {
|
|
1124
|
+
allowedKeys(errors, error, /* @__PURE__ */ new Set(["operation", "observed_at", "error_class", "exit_code", "classification", "error_count", "error_limit", "expected_remote_sha", "candidate_head_sha", "next_retry_at"]), `${path}.last_error`);
|
|
1125
|
+
requiredEnum(errors, error, "operation", /* @__PURE__ */ new Set(["remote-head", "fast-forward-push", "remote-confirmation"]), `${path}.last_error.operation`);
|
|
1126
|
+
requiredEnum(errors, error, "classification", /* @__PURE__ */ new Set(["transient", "permanent", "exhausted"]), `${path}.last_error.classification`);
|
|
1127
|
+
requiredString(errors, error, "observed_at", `${path}.last_error.observed_at`);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
function validatePostPrValidated(errors, remediation, path) {
|
|
1132
|
+
const value = remediation.revalidation;
|
|
1133
|
+
if (value?.canonical_verdict !== "pass") errors.push({ path: `${path}.revalidation.canonical_verdict`, message: "must be pass when validated" });
|
|
1134
|
+
if (!PASSING_VALIDATOR_VERDICTS.has(value?.validator_verdict)) errors.push({ path: `${path}.revalidation.validator_verdict`, message: "must be GO or GO-WITH-NITS when validated" });
|
|
1135
|
+
if (!PASSING_SECURITY_VERDICTS.has(value?.security_verdict)) errors.push({ path: `${path}.revalidation.security_verdict`, message: "must be PASS when validated" });
|
|
1136
|
+
for (const key of ["canonical_evidence_ref", "canonical_evidence_hash", "validator_review_ref", "validator_review_hash", "security_review_ref", "security_review_hash"]) if (!stringValue(value?.[key])) errors.push({ path: `${path}.revalidation.${key}`, message: "is required when validated" });
|
|
1137
|
+
if (!stringValue(remediation.candidate_head_sha)) errors.push({ path: `${path}.candidate_head_sha`, message: "is required when validated" });
|
|
1138
|
+
}
|
|
1139
|
+
function validatePostPrTerminalConsistency(errors, run, postPr, path) {
|
|
1140
|
+
const expected = postPr.phase === "succeeded" ? "completed" : postPr.phase === "blocked" ? "blocked" : postPr.phase === "needs-human" ? "needs-human" : null;
|
|
1141
|
+
if (!expected) return;
|
|
1142
|
+
if (run.status !== expected) errors.push({ path: "run.status", message: `must be ${expected} when post-PR phase is ${postPr.phase}` });
|
|
1143
|
+
const reason = run.terminal_result?.reason;
|
|
1144
|
+
if (!POST_PR_TERMINAL_REASONS[expected]?.includes(reason)) errors.push({ path: "run.terminal_result.reason", message: `must be a closed post-PR ${expected} reason` });
|
|
1145
|
+
}
|
|
1146
|
+
function validatePostPrRefHashArray(errors, items, path) {
|
|
1147
|
+
if (!Array.isArray(items)) {
|
|
1148
|
+
errors.push({ path, message: "must be an array" });
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
const refs = /* @__PURE__ */ new Set();
|
|
1152
|
+
for (const [index, item] of items.entries()) {
|
|
1153
|
+
validatePostPrRefHash(errors, item, `${path}[${index}]`);
|
|
1154
|
+
if (stringValue(item?.ref) && refs.has(item.ref)) errors.push({ path: `${path}[${index}].ref`, message: "must be unique" });
|
|
1155
|
+
if (stringValue(item?.ref)) refs.add(item.ref);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
function validatePostPrRefHash(errors, value, path, { optional = false } = {}) {
|
|
1159
|
+
if (value === void 0 || value === null) {
|
|
1160
|
+
if (!optional) errors.push({ path, message: "must be an object" });
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
if (!isRecord2(value)) {
|
|
1164
|
+
errors.push({ path, message: "must be an object" });
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
allowedKeys(errors, value, /* @__PURE__ */ new Set(["ref", "hash"]), path);
|
|
1168
|
+
requiredString(errors, value, "ref", `${path}.ref`);
|
|
1169
|
+
requiredHash(errors, value, "hash", `${path}.hash`);
|
|
1170
|
+
}
|
|
1171
|
+
function validateSteering(errors, steering, path) {
|
|
1172
|
+
if (steering === void 0 || steering === null) return;
|
|
1173
|
+
if (!isRecord2(steering)) {
|
|
1174
|
+
errors.push({ path, message: "must be an object" });
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
requiredInteger(errors, steering, "schema_version", `${path}.schema_version`);
|
|
1178
|
+
if (Number.isInteger(steering.schema_version) && steering.schema_version !== 1) errors.push({ path: `${path}.schema_version`, message: "must equal 1" });
|
|
1179
|
+
optionalInteger(errors, steering, "generation", `${path}.generation`);
|
|
1180
|
+
if (Number.isInteger(steering.generation) && steering.generation < 0) errors.push({ path: `${path}.generation`, message: "must be non-negative" });
|
|
1181
|
+
if (steering.pending !== void 0 && steering.pending !== null) validateSteeringEntry(errors, steering.pending, `${path}.pending`, { pending: true });
|
|
1182
|
+
if (steering.pending !== void 0 && steering.pending !== null && !isRecord2(steering.pending)) errors.push({ path: `${path}.pending`, message: "must be an object or null" });
|
|
1183
|
+
if (steering.uncheckpointed !== void 0 && steering.uncheckpointed !== null) validateSteeringEntry(errors, steering.uncheckpointed, `${path}.uncheckpointed`, { consumed: true });
|
|
1184
|
+
if (steering.uncheckpointed !== void 0 && steering.uncheckpointed !== null && !isRecord2(steering.uncheckpointed)) errors.push({ path: `${path}.uncheckpointed`, message: "must be an object or null" });
|
|
1185
|
+
validateSteeringBoundary(errors, steering.boundary, `${path}.boundary`, { fence: false });
|
|
1186
|
+
validateSteeringAction(errors, steering.action_claim, `${path}.action_claim`, { claim: true });
|
|
1187
|
+
validateSteeringAction(errors, steering.last_action, `${path}.last_action`, { resolved: true });
|
|
1188
|
+
validateSteeringBoundary(errors, steering.pr_fence, `${path}.pr_fence`, { fence: true });
|
|
1189
|
+
if (steering.pending !== void 0 && steering.pending !== null && steering.uncheckpointed !== void 0 && steering.uncheckpointed !== null) {
|
|
1190
|
+
errors.push({ path, message: "cannot have both pending and uncheckpointed steering" });
|
|
1191
|
+
}
|
|
1192
|
+
if (steering.boundary !== void 0 && steering.boundary !== null && (steering.pending !== void 0 && steering.pending !== null || steering.uncheckpointed !== void 0 && steering.uncheckpointed !== null)) {
|
|
1193
|
+
errors.push({ path, message: "boundary cannot coexist with pending or uncheckpointed steering" });
|
|
1194
|
+
}
|
|
1195
|
+
if (steering.action_claim !== void 0 && steering.action_claim !== null && (steering.pending !== void 0 && steering.pending !== null || steering.uncheckpointed !== void 0 && steering.uncheckpointed !== null || steering.boundary !== void 0 && steering.boundary !== null)) {
|
|
1196
|
+
errors.push({ path, message: "action claim cannot coexist with pending, uncheckpointed, or boundary steering state" });
|
|
1197
|
+
}
|
|
1198
|
+
if (steering.pr_fence !== void 0 && steering.pr_fence !== null && (steering.pending !== void 0 && steering.pending !== null || steering.uncheckpointed !== void 0 && steering.uncheckpointed !== null || steering.boundary !== void 0 && steering.boundary !== null || steering.action_claim !== void 0 && steering.action_claim !== null)) {
|
|
1199
|
+
errors.push({ path, message: "pre-PR fence cannot coexist with pending, uncheckpointed, boundary, or action claim steering state" });
|
|
1200
|
+
}
|
|
1201
|
+
if (steering.history === void 0 || steering.history === null) return;
|
|
1202
|
+
if (!Array.isArray(steering.history)) {
|
|
1203
|
+
errors.push({ path: `${path}.history`, message: "must be an array" });
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
for (const [index, entry] of steering.history.entries()) validateSteeringEntry(errors, entry, `${path}.history[${index}]`, { history: true });
|
|
1207
|
+
}
|
|
1208
|
+
function validateSteeringEntry(errors, entry, path, options = {}) {
|
|
1209
|
+
if (!isRecord2(entry)) {
|
|
1210
|
+
errors.push({ path, message: "must be an object" });
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
if (options.history) requiredEnum(errors, entry, "event", /* @__PURE__ */ new Set(["queued", "consumed", "acknowledged"]), `${path}.event`);
|
|
1214
|
+
requiredString(errors, entry, "id", `${path}.id`);
|
|
1215
|
+
requiredString(errors, entry, "ref", `${path}.ref`);
|
|
1216
|
+
if (stringValue(entry.ref) && (options.consumed || entry.event === "consumed" || entry.event === "acknowledged") && !/^steering\/consumed-[^/]+\.json$/u.test(entry.ref)) errors.push({ path: `${path}.ref`, message: "must name a consumed steering file" });
|
|
1217
|
+
requiredHash(errors, entry, "hash", `${path}.hash`);
|
|
1218
|
+
requiredInteger(errors, entry, "message_chars", `${path}.message_chars`);
|
|
1219
|
+
requiredString(errors, entry, "created_at", `${path}.created_at`);
|
|
1220
|
+
if (entry.event === "consumed") {
|
|
1221
|
+
requiredString(errors, entry, "source_ref", `${path}.source_ref`);
|
|
1222
|
+
requiredString(errors, entry, "consumed_at", `${path}.consumed_at`);
|
|
1223
|
+
}
|
|
1224
|
+
if (options.consumed || entry.event === "acknowledged") requiredString(errors, entry, "consumed_at", `${path}.consumed_at`);
|
|
1225
|
+
if (entry.event === "acknowledged") {
|
|
1226
|
+
requiredString(errors, entry, "acknowledged_at", `${path}.acknowledged_at`);
|
|
1227
|
+
requiredEnum(errors, entry, "outcome", /* @__PURE__ */ new Set(["applied-prospectively"]), `${path}.outcome`);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
function validateSteeringBoundary(errors, boundary, path, options = {}) {
|
|
1231
|
+
if (boundary === void 0 || boundary === null) return;
|
|
1232
|
+
if (!isRecord2(boundary)) {
|
|
1233
|
+
errors.push({ path, message: "must be an object or null" });
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
if (!options.fence) requiredEnum(errors, boundary, "kind", /* @__PURE__ */ new Set(["gate", "dispatch", "remediation", "terminal", "post-pr-observe", "post-pr-push"]), `${path}.kind`);
|
|
1237
|
+
requiredString(errors, boundary, "token", `${path}.token`);
|
|
1238
|
+
if (stringValue(boundary.token) && !/^[A-Za-z0-9_-]{8,128}$/u.test(boundary.token)) errors.push({ path: `${path}.token`, message: "must use 8-128 safe characters" });
|
|
1239
|
+
requiredInteger(errors, boundary, "generation", `${path}.generation`);
|
|
1240
|
+
if (Number.isInteger(boundary.generation) && boundary.generation < 0) errors.push({ path: `${path}.generation`, message: "must be non-negative" });
|
|
1241
|
+
requiredHash(errors, boundary, "state_hash", `${path}.state_hash`);
|
|
1242
|
+
requiredString(errors, boundary, "created_at", `${path}.created_at`);
|
|
1243
|
+
}
|
|
1244
|
+
function validateSteeringAction(errors, action, path, options = {}) {
|
|
1245
|
+
if (action === void 0 || action === null) return;
|
|
1246
|
+
if (!isRecord2(action)) {
|
|
1247
|
+
errors.push({ path, message: "must be an object or null" });
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
requiredEnum(errors, action, "kind", /* @__PURE__ */ new Set(["dispatch", "remediation", "terminal", "post-pr-observe", "post-pr-push"]), `${path}.kind`);
|
|
1251
|
+
requiredString(errors, action, "token", `${path}.token`);
|
|
1252
|
+
if (stringValue(action.token) && !/^[A-Za-z0-9_-]{8,128}$/u.test(action.token)) errors.push({ path: `${path}.token`, message: "must use 8-128 safe characters" });
|
|
1253
|
+
requiredInteger(errors, action, "generation", `${path}.generation`);
|
|
1254
|
+
if (Number.isInteger(action.generation) && action.generation < 0) errors.push({ path: `${path}.generation`, message: "must be non-negative" });
|
|
1255
|
+
requiredString(errors, action, "claimed_at", `${path}.claimed_at`);
|
|
1256
|
+
if (options.resolved) {
|
|
1257
|
+
requiredEnum(errors, action, "outcome", /* @__PURE__ */ new Set(["started", "aborted", "closed"]), `${path}.outcome`);
|
|
1258
|
+
requiredString(errors, action, "resolved_at", `${path}.resolved_at`);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
function validateContinuationParent(errors, parent, path) {
|
|
1262
|
+
if (!isRecord2(parent)) {
|
|
1263
|
+
errors.push({ path, message: "must be an object" });
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
requiredString(errors, parent, "run_id", `${path}.run_id`);
|
|
1267
|
+
requiredEnum(errors, parent, "status", BLOCKED_CONTINUATION_PARENT_STATUSES, `${path}.status`);
|
|
1268
|
+
requiredString(errors, parent, "run_ref", `${path}.run_ref`);
|
|
1269
|
+
requiredHash(errors, parent, "run_hash", `${path}.run_hash`);
|
|
1270
|
+
requiredString(errors, parent, "branch", `${path}.branch`);
|
|
1271
|
+
requiredString(errors, parent, "commit", `${path}.commit`);
|
|
1272
|
+
requiredString(errors, parent, "worktree", `${path}.worktree`);
|
|
1273
|
+
}
|
|
1274
|
+
function validateContinuationReview(errors, review, path) {
|
|
1275
|
+
if (!isRecord2(review)) {
|
|
1276
|
+
errors.push({ path, message: "must be an object" });
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
requiredString(errors, review, "ref", `${path}.ref`);
|
|
1280
|
+
requiredString(errors, review, "kind", `${path}.kind`);
|
|
1281
|
+
optionalString(errors, review, "source", `${path}.source`);
|
|
1282
|
+
requiredHash(errors, review, "hash", `${path}.hash`);
|
|
1283
|
+
requiredString(errors, review, "subject", `${path}.subject`);
|
|
1284
|
+
optionalString(errors, review, "verdict", `${path}.verdict`);
|
|
1285
|
+
optionalString(errors, review, "summary", `${path}.summary`);
|
|
1286
|
+
validateStringArray(errors, review.required_fixes, `${path}.required_fixes`, { required: false });
|
|
1287
|
+
if (!stringValue(review.summary) && !hasNonEmptyStringItem(review.required_fixes)) {
|
|
1288
|
+
errors.push({ path, message: "requires summary or required_fixes" });
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
function validateContinuationTarget(errors, run, target, path) {
|
|
1292
|
+
if (!isRecord2(target)) {
|
|
1293
|
+
errors.push({ path, message: "must be an object" });
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
requiredString(errors, target, "run_id", `${path}.run_id`);
|
|
1297
|
+
requiredString(errors, target, "branch", `${path}.branch`);
|
|
1298
|
+
requiredString(errors, target, "worktree", `${path}.worktree`);
|
|
1299
|
+
requiredString(errors, target, "base_ref", `${path}.base_ref`);
|
|
1300
|
+
requiredString(errors, target, "base_commit", `${path}.base_commit`);
|
|
1301
|
+
if (stringValue(target.run_id) && stringValue(run.run_id) && target.run_id !== run.run_id) errors.push({ path: `${path}.run_id`, message: "must match run.run_id" });
|
|
1302
|
+
if (stringValue(target.branch) && stringValue(run.branch) && target.branch !== run.branch) errors.push({ path: `${path}.branch`, message: "must match run.branch" });
|
|
1303
|
+
if (stringValue(target.worktree) && stringValue(run.worktree) && target.worktree !== run.worktree) errors.push({ path: `${path}.worktree`, message: "must match run.worktree" });
|
|
1304
|
+
}
|
|
1305
|
+
function validateContinuationRefHashArray(errors, items, path) {
|
|
1306
|
+
if (!Array.isArray(items)) {
|
|
1307
|
+
errors.push({ path, message: "must be an array" });
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
for (const [index, item] of items.entries()) {
|
|
1311
|
+
const itemPath = `${path}[${index}]`;
|
|
1312
|
+
if (!isRecord2(item)) {
|
|
1313
|
+
errors.push({ path: itemPath, message: "must be an object" });
|
|
1314
|
+
continue;
|
|
1315
|
+
}
|
|
1316
|
+
requiredString(errors, item, "kind", `${itemPath}.kind`);
|
|
1317
|
+
requiredString(errors, item, "ref", `${itemPath}.ref`);
|
|
1318
|
+
requiredHash(errors, item, "hash", `${itemPath}.hash`);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
function validateContinuationSelectedReview(errors, continuation, path) {
|
|
1322
|
+
const review = continuation.review;
|
|
1323
|
+
if (!isRecord2(review) || !Array.isArray(continuation.parent_reviews)) return;
|
|
1324
|
+
if (!stringValue(review.ref) || typeof review.hash !== "string" || !HASH_PATTERN.test(review.hash)) return;
|
|
1325
|
+
const match = continuation.parent_reviews.find((item) => isRecord2(item) && item.ref === review.ref);
|
|
1326
|
+
if (!match) {
|
|
1327
|
+
errors.push({ path: `${path}.parent_reviews`, message: "must include selected review ref" });
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
if (match.hash !== review.hash) {
|
|
1331
|
+
errors.push({ path: `${path}.parent_reviews`, message: "selected review hash must match review.hash" });
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
function validateRedactedEnv(errors, value, path) {
|
|
1335
|
+
if (Array.isArray(value)) {
|
|
1336
|
+
for (const [index, item] of value.entries()) validateRedactedEnv(errors, item, `${path}[${index}]`);
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
if (typeof value === "string") {
|
|
1340
|
+
if (value !== REDACTED_ENV_VALUE && isSensitiveEnvValue(value)) errors.push({ path, message: "must be redacted in debug snapshot" });
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
if (!isRecord2(value)) return;
|
|
1344
|
+
for (const [key, item] of Object.entries(value)) {
|
|
1345
|
+
const itemPath = `${path}.${key}`;
|
|
1346
|
+
if (isSensitiveEnvKey(key) && item !== REDACTED_ENV_VALUE) errors.push({ path: itemPath, message: "is not allowed in debug snapshot" });
|
|
1347
|
+
validateRedactedEnv(errors, item, itemPath);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
function validateGate(errors, gate, path, gateName) {
|
|
1351
|
+
if (!isRecord2(gate)) {
|
|
1352
|
+
errors.push({ path, message: "must be an object" });
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
requiredEnum(errors, gate, "status", GATE_STATUSES, `${path}.status`);
|
|
1356
|
+
optionalString(errors, gate, "artifact", `${path}.artifact`);
|
|
1357
|
+
optionalString(errors, gate, "question_ref", `${path}.question_ref`);
|
|
1358
|
+
optionalString(errors, gate, "answer_ref", `${path}.answer_ref`);
|
|
1359
|
+
optionalString(errors, gate, "answered_at", `${path}.answered_at`);
|
|
1360
|
+
optionalString(errors, gate, "answer", `${path}.answer`);
|
|
1361
|
+
optionalString(errors, gate, "decision_note", `${path}.decision_note`);
|
|
1362
|
+
optionalEnum(errors, gate, "approval_source", APPROVAL_SOURCES, `${path}.approval_source`);
|
|
1363
|
+
validatePendingSnapshot(errors, gate.pending_snapshot, `${path}.pending_snapshot`);
|
|
1364
|
+
validateGateHandoffReceipt(errors, gate.handoff_receipt, `${path}.handoff_receipt`, gateName);
|
|
1365
|
+
}
|
|
1366
|
+
function validateGateHandoffReceipt(errors, receipt, path, gateName) {
|
|
1367
|
+
if (receipt === void 0 || receipt === null) return;
|
|
1368
|
+
if (!isRecord2(receipt)) {
|
|
1369
|
+
errors.push({ path, message: "must be an object" });
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
requiredInteger(errors, receipt, "schema_version", `${path}.schema_version`);
|
|
1373
|
+
if (Number.isInteger(receipt.schema_version) && receipt.schema_version !== 1) errors.push({ path: `${path}.schema_version`, message: "must equal 1" });
|
|
1374
|
+
requiredString(errors, receipt, "kind", `${path}.kind`);
|
|
1375
|
+
if (stringValue(receipt.kind) && receipt.kind !== HANDOFF_RECEIPT_KIND) errors.push({ path: `${path}.kind`, message: `must equal ${HANDOFF_RECEIPT_KIND}` });
|
|
1376
|
+
requiredString(errors, receipt, "gate", `${path}.gate`);
|
|
1377
|
+
if (stringValue(receipt.gate) && receipt.gate !== gateName) errors.push({ path: `${path}.gate`, message: "must match its gate key" });
|
|
1378
|
+
requiredHash(errors, receipt, "approval_fingerprint", `${path}.approval_fingerprint`);
|
|
1379
|
+
requiredHash(errors, receipt, "pending_snapshot_hash", `${path}.pending_snapshot_hash`);
|
|
1380
|
+
requiredHash(errors, receipt, "answer_hash", `${path}.answer_hash`);
|
|
1381
|
+
requiredInteger(errors, receipt, "steering_generation", `${path}.steering_generation`);
|
|
1382
|
+
if (Number.isInteger(receipt.steering_generation) && receipt.steering_generation < 0) errors.push({ path: `${path}.steering_generation`, message: "must be non-negative" });
|
|
1383
|
+
requiredString(errors, receipt, "accepted_at", `${path}.accepted_at`);
|
|
1384
|
+
}
|
|
1385
|
+
function validatePendingSnapshot(errors, pendingSnapshot, path) {
|
|
1386
|
+
if (pendingSnapshot === void 0 || pendingSnapshot === null) return;
|
|
1387
|
+
if (!isRecord2(pendingSnapshot)) {
|
|
1388
|
+
errors.push({ path, message: "must be an object" });
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
requiredString(errors, pendingSnapshot, "question_ref", `${path}.question_ref`);
|
|
1392
|
+
requiredHash(errors, pendingSnapshot, "question_hash", `${path}.question_hash`);
|
|
1393
|
+
requiredString(errors, pendingSnapshot, "artifact_ref", `${path}.artifact_ref`);
|
|
1394
|
+
requiredHash(errors, pendingSnapshot, "artifact_hash", `${path}.artifact_hash`);
|
|
1395
|
+
optionalString(errors, pendingSnapshot, "answer_ref", `${path}.answer_ref`);
|
|
1396
|
+
optionalHash(errors, pendingSnapshot, "answer_hash", `${path}.answer_hash`);
|
|
1397
|
+
requiredString(errors, pendingSnapshot, "created_at", `${path}.created_at`);
|
|
1398
|
+
}
|
|
1399
|
+
function isPendingGate(gate) {
|
|
1400
|
+
return isRecord2(gate) && gate.status === "pending";
|
|
1401
|
+
}
|
|
1402
|
+
function validateRunSlices(errors, slices, path) {
|
|
1403
|
+
if (slices === void 0 || slices === null) return;
|
|
1404
|
+
if (!Array.isArray(slices)) {
|
|
1405
|
+
errors.push({ path, message: "must be an array" });
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
const ids = validateSliceIDs(errors, slices, path);
|
|
1409
|
+
for (const [index, slice] of slices.entries()) validateRunSlice(errors, slice, `${path}[${index}]`, ids);
|
|
1410
|
+
}
|
|
1411
|
+
function validateRunSlice(errors, slice, path, ids) {
|
|
1412
|
+
if (!isRecord2(slice)) {
|
|
1413
|
+
errors.push({ path, message: "must be an object" });
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
requiredTerminalSafeString(errors, slice, "id", `${path}.id`);
|
|
1417
|
+
optionalString(errors, slice, "stack", `${path}.stack`);
|
|
1418
|
+
validateStringArray(errors, slice.depends_on, `${path}.depends_on`, { required: false, values: ids });
|
|
1419
|
+
optionalEnum(errors, slice, "status", SLICE_STATUSES, `${path}.status`);
|
|
1420
|
+
optionalString(errors, slice, "branch", `${path}.branch`);
|
|
1421
|
+
optionalString(errors, slice, "worktree", `${path}.worktree`);
|
|
1422
|
+
optionalInteger(errors, slice, "attempts", `${path}.attempts`);
|
|
1423
|
+
optionalString(errors, slice, "evidence_ref", `${path}.evidence_ref`);
|
|
1424
|
+
optionalString(errors, slice, "review_ref", `${path}.review_ref`);
|
|
1425
|
+
optionalString(errors, slice, "merge_commit", `${path}.merge_commit`);
|
|
1426
|
+
optionalString(errors, slice, "blocked_reason", `${path}.blocked_reason`);
|
|
1427
|
+
}
|
|
1428
|
+
function validatePlannedSlices(errors, slices, path, { enforceDependencyDepth }) {
|
|
1429
|
+
const ids = validateSliceIDs(errors, slices, path);
|
|
1430
|
+
for (const [index, slice] of slices.entries()) {
|
|
1431
|
+
if (!isRecord2(slice)) {
|
|
1432
|
+
errors.push({ path: `${path}[${index}]`, message: "must be an object" });
|
|
1433
|
+
continue;
|
|
1434
|
+
}
|
|
1435
|
+
requiredTerminalSafeString(errors, slice, "id", `${path}[${index}].id`);
|
|
1436
|
+
requiredString(errors, slice, "stack", `${path}[${index}].stack`);
|
|
1437
|
+
validateStringArray(errors, slice.paths, `${path}[${index}].paths`, { required: true, nonEmpty: true });
|
|
1438
|
+
validateStringArray(errors, slice.depends_on, `${path}[${index}].depends_on`, { required: true, values: ids });
|
|
1439
|
+
validateStringArray(errors, slice.acceptance, `${path}[${index}].acceptance`, { required: true, nonEmpty: true });
|
|
1440
|
+
validateStringArray(errors, slice.test_plan, `${path}[${index}].test_plan`, { required: true, nonEmpty: true });
|
|
1441
|
+
}
|
|
1442
|
+
const acyclic = validateAcyclic(errors, slices, ids, path);
|
|
1443
|
+
if (enforceDependencyDepth && errors.length === 0 && acyclic) validateDependencyDepth(errors, slices, path);
|
|
1444
|
+
}
|
|
1445
|
+
function validateSliceIDs(errors, slices, path) {
|
|
1446
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1447
|
+
for (const [index, slice] of slices.entries()) {
|
|
1448
|
+
if (!isRecord2(slice) || typeof slice.id !== "string" || !slice.id.trim()) continue;
|
|
1449
|
+
if (ids.has(slice.id)) errors.push({ path: `${path}[${index}].id`, message: `duplicate id '${safeValidationIdentifier(slice.id)}'` });
|
|
1450
|
+
ids.add(slice.id);
|
|
1451
|
+
}
|
|
1452
|
+
return ids;
|
|
1453
|
+
}
|
|
1454
|
+
function validateAcyclic(errors, slices, ids, path) {
|
|
1455
|
+
const graph = /* @__PURE__ */ new Map();
|
|
1456
|
+
for (const slice of slices) if (isRecord2(slice) && typeof slice.id === "string" && ids.has(slice.id)) graph.set(slice.id, Array.isArray(slice.depends_on) ? slice.depends_on.filter((id) => ids.has(id)) : []);
|
|
1457
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
1458
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1459
|
+
let acyclic = true;
|
|
1460
|
+
const visit = (id, chain) => {
|
|
1461
|
+
if (visited.has(id)) return;
|
|
1462
|
+
if (visiting.has(id)) {
|
|
1463
|
+
errors.push({ path, message: `dependency cycle: ${[...chain, id].join(" -> ")}` });
|
|
1464
|
+
acyclic = false;
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
visiting.add(id);
|
|
1468
|
+
for (const dep of graph.get(id) || []) visit(dep, [...chain, id]);
|
|
1469
|
+
visiting.delete(id);
|
|
1470
|
+
visited.add(id);
|
|
1471
|
+
};
|
|
1472
|
+
for (const id of graph.keys()) visit(id, []);
|
|
1473
|
+
return acyclic;
|
|
1474
|
+
}
|
|
1475
|
+
function validateDependencyDepth(errors, slices, path) {
|
|
1476
|
+
const byId = new Map(slices.map((slice, index) => [slice.id, { slice, index }]));
|
|
1477
|
+
const memo = /* @__PURE__ */ new Map();
|
|
1478
|
+
const longestPath = (id) => {
|
|
1479
|
+
if (memo.has(id)) return memo.get(id);
|
|
1480
|
+
const dependencies = byId.get(id).slice.depends_on;
|
|
1481
|
+
let result2 = { depth: 1, ids: [id] };
|
|
1482
|
+
for (const dependency of dependencies) {
|
|
1483
|
+
const parent = longestPath(dependency);
|
|
1484
|
+
if (parent.depth + 1 > result2.depth) result2 = { depth: parent.depth + 1, ids: [...parent.ids, id] };
|
|
1485
|
+
}
|
|
1486
|
+
memo.set(id, result2);
|
|
1487
|
+
return result2;
|
|
1488
|
+
};
|
|
1489
|
+
for (const [id, { index }] of byId) {
|
|
1490
|
+
const result2 = longestPath(id);
|
|
1491
|
+
if (result2.depth > MAX_SLICE_DEPENDENCY_WAVES) {
|
|
1492
|
+
errors.push({
|
|
1493
|
+
path: `${path}[${index}].depends_on`,
|
|
1494
|
+
message: `dependency depth ${result2.depth} exceeds maximum ${MAX_SLICE_DEPENDENCY_WAVES} waves: ${result2.ids.join(" -> ")}`
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
function validateSteps(errors, steps, path) {
|
|
1500
|
+
if (steps === void 0 || steps === null) return;
|
|
1501
|
+
if (!Array.isArray(steps)) {
|
|
1502
|
+
errors.push({ path, message: "must be an array" });
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
for (const [index, step] of steps.entries()) {
|
|
1506
|
+
if (!isRecord2(step)) {
|
|
1507
|
+
errors.push({ path: `${path}[${index}]`, message: "must be an object" });
|
|
1508
|
+
continue;
|
|
1509
|
+
}
|
|
1510
|
+
requiredTerminalSafeString(errors, step, "agent", `${path}[${index}].agent`);
|
|
1511
|
+
requiredEnum(errors, step, "status", STEP_STATUSES, `${path}[${index}].status`);
|
|
1512
|
+
optionalInteger(errors, step, "attempts", `${path}[${index}].attempts`);
|
|
1513
|
+
optionalString(errors, step, "artifact_ref", `${path}[${index}].artifact_ref`);
|
|
1514
|
+
optionalString(errors, step, "review_ref", `${path}[${index}].review_ref`);
|
|
1515
|
+
optionalString(errors, step, "evidence_ref", `${path}[${index}].evidence_ref`);
|
|
1516
|
+
validateStepAcceptance(errors, step.acceptance, `${path}[${index}].acceptance`);
|
|
1517
|
+
validateStepInheritedAcceptance(errors, step.inherited_acceptance, `${path}[${index}].inherited_acceptance`);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
function validateStepAcceptance(errors, acceptance, path) {
|
|
1521
|
+
if (acceptance === void 0 || acceptance === null) return;
|
|
1522
|
+
if (!isRecord2(acceptance)) {
|
|
1523
|
+
errors.push({ path, message: "must be an object" });
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
requiredString(errors, acceptance, "artifact_ref", `${path}.artifact_ref`);
|
|
1527
|
+
requiredHash(errors, acceptance, "artifact_hash", `${path}.artifact_hash`);
|
|
1528
|
+
optionalString(errors, acceptance, "review_ref", `${path}.review_ref`);
|
|
1529
|
+
if (acceptance.review_ref !== void 0 && acceptance.review_ref !== null) requiredHash(errors, acceptance, "review_hash", `${path}.review_hash`);
|
|
1530
|
+
}
|
|
1531
|
+
function validateStepInheritedAcceptance(errors, inherited, path) {
|
|
1532
|
+
if (inherited === void 0 || inherited === null) return;
|
|
1533
|
+
if (!isRecord2(inherited)) {
|
|
1534
|
+
errors.push({ path, message: "must be an object" });
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
requiredString(errors, inherited, "from_run_id", `${path}.from_run_id`);
|
|
1538
|
+
requiredString(errors, inherited, "parent_spec_review_ref", `${path}.parent_spec_review_ref`);
|
|
1539
|
+
requiredHash(errors, inherited, "artifact_hash", `${path}.artifact_hash`);
|
|
1540
|
+
requiredHash(errors, inherited, "review_hash", `${path}.review_hash`);
|
|
1541
|
+
}
|
|
1542
|
+
function validateVerdict(errors, value, path, allowed) {
|
|
1543
|
+
if (value === void 0 || value === null) return;
|
|
1544
|
+
if (!isRecord2(value)) {
|
|
1545
|
+
errors.push({ path, message: "must be an object" });
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
optionalEnum(errors, value, "verdict", allowed, `${path}.verdict`);
|
|
1549
|
+
optionalString(errors, value, "report", `${path}.report`);
|
|
1550
|
+
optionalString(errors, value, "review_ref", `${path}.review_ref`);
|
|
1551
|
+
optionalInteger(errors, value, "loops", `${path}.loops`);
|
|
1552
|
+
}
|
|
1553
|
+
function validateTerminalResult(errors, run, path) {
|
|
1554
|
+
const terminal = TERMINAL_STATUSES.has(run.status);
|
|
1555
|
+
if (!terminal && (run.terminal_result === void 0 || run.terminal_result === null)) return;
|
|
1556
|
+
if (terminal && !isRecord2(run.terminal_result)) {
|
|
1557
|
+
errors.push({ path, message: "must be present when run.status is terminal" });
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
if (!isRecord2(run.terminal_result)) {
|
|
1561
|
+
errors.push({ path, message: "must be an object or null" });
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
requiredEnum(errors, run.terminal_result, "status", TERMINAL_STATUSES, `${path}.status`);
|
|
1565
|
+
requiredString(errors, run.terminal_result, "run_id", `${path}.run_id`);
|
|
1566
|
+
optionalString(errors, run.terminal_result, "pr_url", `${path}.pr_url`);
|
|
1567
|
+
optionalString(errors, run.terminal_result, "reason", `${path}.reason`);
|
|
1568
|
+
optionalString(errors, run.terminal_result, "summary", `${path}.summary`);
|
|
1569
|
+
validateStringMap(errors, run.terminal_result.artifacts, `${path}.artifacts`);
|
|
1570
|
+
if (run.terminal_result.status && run.terminal_result.status !== run.status) errors.push({ path: `${path}.status`, message: `must match run.status '${run.status}'` });
|
|
1571
|
+
if (run.terminal_result.run_id && run.terminal_result.run_id !== run.run_id) errors.push({ path: `${path}.run_id`, message: "must match run.run_id" });
|
|
1572
|
+
if (["blocked", "partial", "needs-human"].includes(run.status) && !stringValue(run.terminal_result.reason)) errors.push({ path: `${path}.reason`, message: `is required for ${run.status}` });
|
|
1573
|
+
}
|
|
1574
|
+
function validateCostAttribution(errors, attribution, path, run) {
|
|
1575
|
+
if (attribution === void 0 || attribution === null) return;
|
|
1576
|
+
if (!isRecord2(attribution)) {
|
|
1577
|
+
errors.push({ path, message: "must be an object" });
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
requiredInteger(errors, attribution, "schema_version", `${path}.schema_version`);
|
|
1581
|
+
if (Number.isInteger(attribution.schema_version) && attribution.schema_version !== COST_ATTRIBUTION_SCHEMA_VERSION) errors.push({ path: `${path}.schema_version`, message: `must equal ${COST_ATTRIBUTION_SCHEMA_VERSION}` });
|
|
1582
|
+
requiredString(errors, attribution, "updated_at", `${path}.updated_at`);
|
|
1583
|
+
requiredEnum(errors, attribution, "status", COST_ATTRIBUTION_STATUS_SET, `${path}.status`);
|
|
1584
|
+
validateCostAttributionRollup(errors, attribution.totals, `${path}.totals`, { required: true });
|
|
1585
|
+
validateCostAttributionRollupMap(errors, attribution.by_agent, `${path}.by_agent`, { required: true });
|
|
1586
|
+
validateCostAttributionRollupMap(errors, attribution.by_slice, `${path}.by_slice`, { required: true, knownKeys: knownRunSliceIds(run) });
|
|
1587
|
+
const knownSlices = knownRunSliceIds(run);
|
|
1588
|
+
const runId = stringValue(run?.run_id) ? run.run_id : null;
|
|
1589
|
+
if (!Array.isArray(attribution.entries)) {
|
|
1590
|
+
errors.push({ path: `${path}.entries`, message: "must be an array" });
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
if (attribution.entries.length > MAX_COST_ATTRIBUTION_ENTRIES) errors.push({ path: `${path}.entries`, message: `must have at most ${MAX_COST_ATTRIBUTION_ENTRIES} entries` });
|
|
1594
|
+
for (const [index, entry] of attribution.entries.entries()) validateCostAttributionEntry(errors, entry, `${path}.entries[${index}]`, knownSlices, runId);
|
|
1595
|
+
}
|
|
1596
|
+
function validateCostAttributionEntry(errors, entry, path, knownSlices, runId) {
|
|
1597
|
+
if (!isRecord2(entry)) {
|
|
1598
|
+
errors.push({ path, message: "must be an object" });
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
requiredString(errors, entry, "id", `${path}.id`);
|
|
1602
|
+
requiredString(errors, entry, "recorded_at", `${path}.recorded_at`);
|
|
1603
|
+
requiredString(errors, entry, "run_id", `${path}.run_id`);
|
|
1604
|
+
requiredString(errors, entry, "agent", `${path}.agent`);
|
|
1605
|
+
requiredEnum(errors, entry, "status", COST_ATTRIBUTION_STATUS_SET, `${path}.status`);
|
|
1606
|
+
validateStringArray(errors, entry.missing, `${path}.missing`, { required: true, noControlChars: true });
|
|
1607
|
+
for (const field of COST_ATTRIBUTION_ENTRY_OPTIONAL_STRINGS) optionalString(errors, entry, field, `${path}.${field}`);
|
|
1608
|
+
optionalCostCurrency(errors, entry, "cost_currency", `${path}.cost_currency`);
|
|
1609
|
+
validateCostAttributionNumbers(errors, entry, path);
|
|
1610
|
+
if (hasCostNumber(entry) && !stringValue(entry.cost_currency)) errors.push({ path: `${path}.cost_currency`, message: "is required when cost fields are present" });
|
|
1611
|
+
if (runId && stringValue(entry.run_id) && entry.run_id !== runId) errors.push({ path: `${path}.run_id`, message: "must match run.run_id" });
|
|
1612
|
+
validateCostAttributionAvailability(errors, entry, path);
|
|
1613
|
+
if ((entry.status === "partial" || entry.status === "unavailable") && Array.isArray(entry.missing) && !hasNonEmptyStringItem(entry.missing)) errors.push({ path: `${path}.missing`, message: `is required when status is ${entry.status}` });
|
|
1614
|
+
if (stringValue(entry.slice_id) && knownSlices && !knownSlices.has(entry.slice_id)) errors.push({ path: `${path}.slice_id`, message: `unknown slice '${entry.slice_id}'` });
|
|
1615
|
+
}
|
|
1616
|
+
function validateCostAttributionAvailability(errors, entry, path) {
|
|
1617
|
+
const missing = costAttributionAvailabilityMissing(entry);
|
|
1618
|
+
const hasUsage = hasUsageNumber(entry);
|
|
1619
|
+
const hasCost = hasCostNumber(entry);
|
|
1620
|
+
if (entry.status === "available" && Array.isArray(entry.missing) && entry.missing.length > 0) {
|
|
1621
|
+
errors.push({ path: `${path}.missing`, message: "must be empty when status is available" });
|
|
1622
|
+
}
|
|
1623
|
+
if (entry.status === "available" && missing.length > 0) {
|
|
1624
|
+
errors.push({ path: `${path}.status`, message: "available requires provider, model, usage, cost_total, and cost_currency" });
|
|
1625
|
+
}
|
|
1626
|
+
if (entry.status === "unavailable" && (hasUsage || hasCost)) errors.push({ path: `${path}.status`, message: "must be partial or available when usage or cost fields are present" });
|
|
1627
|
+
}
|
|
1628
|
+
function costAttributionAvailabilityMissing(entry) {
|
|
1629
|
+
const missing = [];
|
|
1630
|
+
if (!stringValue(entry.provider)) missing.push("provider");
|
|
1631
|
+
if (!stringValue(entry.model)) missing.push("model");
|
|
1632
|
+
if (!hasUsageNumber(entry)) missing.push("usage");
|
|
1633
|
+
if (entry.cost_total === void 0 || entry.cost_total === null) missing.push("cost_total");
|
|
1634
|
+
if (!stringValue(entry.cost_currency)) missing.push("cost_currency");
|
|
1635
|
+
return missing;
|
|
1636
|
+
}
|
|
1637
|
+
function validateCostAttributionRollupMap(errors, map, path, options = {}) {
|
|
1638
|
+
if (!isRecord2(map)) {
|
|
1639
|
+
if (options.required) errors.push({ path, message: "must be an object" });
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
for (const [key, rollup] of Object.entries(map)) {
|
|
1643
|
+
if (!stringValue(key)) errors.push({ path: `${path}.${key}`, message: "must be keyed by non-empty strings" });
|
|
1644
|
+
if (options.knownKeys && !options.knownKeys.has(key)) errors.push({ path: `${path}.${key}`, message: `unknown slice '${key}'` });
|
|
1645
|
+
validateCostAttributionRollup(errors, rollup, `${path}.${key}`, { required: true });
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
function validateCostAttributionRollup(errors, rollup, path, options = {}) {
|
|
1649
|
+
if (!isRecord2(rollup)) {
|
|
1650
|
+
if (options.required) errors.push({ path, message: "must be an object" });
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
requiredEnum(errors, rollup, "status", COST_ATTRIBUTION_STATUS_SET, `${path}.status`);
|
|
1654
|
+
requiredInteger(errors, rollup, "entry_count", `${path}.entry_count`);
|
|
1655
|
+
optionalInteger(errors, rollup, "request_count", `${path}.request_count`);
|
|
1656
|
+
validateStringArray(errors, rollup.missing, `${path}.missing`, { required: true, noControlChars: true });
|
|
1657
|
+
optionalBoolean(errors, rollup, "mixed_currency", `${path}.mixed_currency`);
|
|
1658
|
+
optionalString(errors, rollup, "cost_currency", `${path}.cost_currency`);
|
|
1659
|
+
optionalCostCurrency(errors, rollup, "cost_currency", `${path}.cost_currency`);
|
|
1660
|
+
validateCostAttributionNumbers(errors, rollup, path);
|
|
1661
|
+
if (rollup.mixed_currency === true && rollup.cost_total !== void 0 && rollup.cost_total !== null) errors.push({ path: `${path}.cost_total`, message: "must be omitted when mixed_currency is true" });
|
|
1662
|
+
if (hasCostNumber(rollup) && rollup.mixed_currency !== true && rollup.cost_total !== void 0 && !stringValue(rollup.cost_currency)) errors.push({ path: `${path}.cost_currency`, message: "is required when cost_total is present" });
|
|
1663
|
+
}
|
|
1664
|
+
function validateCostAttributionNumbers(errors, value, path) {
|
|
1665
|
+
for (const field of COST_ATTRIBUTION_NUMERIC_FIELDS) {
|
|
1666
|
+
if (value[field] === void 0 || value[field] === null) continue;
|
|
1667
|
+
if (typeof value[field] !== "number" || !Number.isFinite(value[field]) || value[field] < 0) errors.push({ path: `${path}.${field}`, message: "must be a finite non-negative number" });
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
function knownRunSliceIds(run) {
|
|
1671
|
+
if (!Array.isArray(run?.slices)) return null;
|
|
1672
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1673
|
+
for (const slice of run.slices) if (stringValue(slice?.id)) ids.add(slice.id);
|
|
1674
|
+
return ids;
|
|
1675
|
+
}
|
|
1676
|
+
function hasCostNumber(value) {
|
|
1677
|
+
return COST_NUMERIC_FIELDS.some((field) => value[field] !== void 0 && value[field] !== null);
|
|
1678
|
+
}
|
|
1679
|
+
function hasUsageNumber(value) {
|
|
1680
|
+
return USAGE_NUMERIC_FIELDS.some((field) => value[field] !== void 0 && value[field] !== null);
|
|
1681
|
+
}
|
|
1682
|
+
function validateGateName(errors, name, path) {
|
|
1683
|
+
if (!SAFE_GATE_NAME_PATTERN.test(name)) errors.push({ path, message: "must match safe gate name pattern [a-z0-9][a-z0-9_-]*[a-z0-9]" });
|
|
1684
|
+
}
|
|
1685
|
+
function validateStringMap(errors, value, path) {
|
|
1686
|
+
if (value === void 0 || value === null) return;
|
|
1687
|
+
if (!isRecord2(value)) {
|
|
1688
|
+
errors.push({ path, message: "must be an object" });
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
for (const [key, item] of Object.entries(value)) if (typeof item !== "string") errors.push({ path: `${path}.${key}`, message: "must be a string" });
|
|
1692
|
+
}
|
|
1693
|
+
function validateProcessLogRefContainment(errors, ref, path, runDir) {
|
|
1694
|
+
if (!stringValue(ref) || !stringValue(runDir)) return;
|
|
1695
|
+
const processesDir = resolve3(processEvidenceProcessesDir(runDir));
|
|
1696
|
+
const resolvedLog = resolve3(runDir, ref);
|
|
1697
|
+
const relativeLog = relative2(processesDir, resolvedLog);
|
|
1698
|
+
if (relativeLog === "" || relativeLog === ".." || relativeLog.startsWith(`..${sep3}`)) {
|
|
1699
|
+
errors.push({ path, message: "must stay under run processes directory" });
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
function validateStringArray(errors, value, path, options = {}) {
|
|
1703
|
+
if (value === void 0 || value === null) {
|
|
1704
|
+
if (options.required) errors.push({ path, message: "must be an array" });
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
if (!Array.isArray(value)) {
|
|
1708
|
+
errors.push({ path, message: "must be an array" });
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
if (options.nonEmpty && value.length === 0) errors.push({ path, message: "must not be empty" });
|
|
1712
|
+
for (const [index, item] of value.entries()) {
|
|
1713
|
+
if (!stringValue(item)) errors.push({ path: `${path}[${index}]`, message: "must be a non-empty string" });
|
|
1714
|
+
else if (options.noControlChars && hasTerminalControl(item)) errors.push({ path: `${path}[${index}]`, message: "must not contain control characters" });
|
|
1715
|
+
else if (options.values && !options.values.has(item)) errors.push({ path: `${path}[${index}]`, message: `unknown dependency '${item}'` });
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
function hasNonEmptyStringItem(value) {
|
|
1719
|
+
return Array.isArray(value) && value.some((item) => stringValue(item));
|
|
1720
|
+
}
|
|
1721
|
+
function requiredString(errors, obj, key, path) {
|
|
1722
|
+
if (!stringValue(obj[key])) errors.push({ path, message: "must be a non-empty string" });
|
|
1723
|
+
}
|
|
1724
|
+
function requiredTerminalSafeString(errors, obj, key, path) {
|
|
1725
|
+
requiredString(errors, obj, key, path);
|
|
1726
|
+
if (typeof obj[key] === "string" && hasTerminalControl(obj[key])) errors.push({ path, message: "must not contain control characters" });
|
|
1727
|
+
}
|
|
1728
|
+
function optionalString(errors, obj, key, path) {
|
|
1729
|
+
if (obj[key] === void 0 || obj[key] === null) return;
|
|
1730
|
+
if (typeof obj[key] !== "string") errors.push({ path, message: "must be a string or null" });
|
|
1731
|
+
}
|
|
1732
|
+
function optionalCostCurrency(errors, obj, key, path) {
|
|
1733
|
+
if (obj[key] === void 0 || obj[key] === null) return;
|
|
1734
|
+
if (typeof obj[key] === "string" && !isSafeCostCurrency(obj[key])) errors.push({ path, message: "must be an uppercase currency code (3-12 letters) with no control characters" });
|
|
1735
|
+
}
|
|
1736
|
+
function optionalNonEmptyString(errors, obj, key, path) {
|
|
1737
|
+
if (obj[key] === void 0 || obj[key] === null) return;
|
|
1738
|
+
if (!stringValue(obj[key])) errors.push({ path, message: "must be a non-empty string" });
|
|
1739
|
+
}
|
|
1740
|
+
function requiredEnum(errors, obj, key, values, path) {
|
|
1741
|
+
if (typeof obj[key] !== "string" || !values.has(obj[key])) errors.push({ path, message: `must be one of ${[...values].join(", ")}` });
|
|
1742
|
+
}
|
|
1743
|
+
function optionalEnum(errors, obj, key, values, path) {
|
|
1744
|
+
if (obj[key] === void 0 || obj[key] === null) return;
|
|
1745
|
+
requiredEnum(errors, obj, key, values, path);
|
|
1746
|
+
}
|
|
1747
|
+
function optionalNumber(errors, obj, key, path) {
|
|
1748
|
+
if (obj[key] === void 0 || obj[key] === null) return;
|
|
1749
|
+
if (typeof obj[key] !== "number" || !Number.isFinite(obj[key])) errors.push({ path, message: "must be a number" });
|
|
1750
|
+
}
|
|
1751
|
+
function optionalInteger(errors, obj, key, path) {
|
|
1752
|
+
if (obj[key] === void 0 || obj[key] === null) return;
|
|
1753
|
+
if (!Number.isInteger(obj[key]) || obj[key] < 0) errors.push({ path, message: "must be a non-negative integer" });
|
|
1754
|
+
}
|
|
1755
|
+
function optionalBoolean(errors, obj, key, path) {
|
|
1756
|
+
if (obj[key] === void 0 || obj[key] === null) return;
|
|
1757
|
+
if (typeof obj[key] !== "boolean") errors.push({ path, message: "must be a boolean" });
|
|
1758
|
+
}
|
|
1759
|
+
function requiredBoolean(errors, obj, key, path) {
|
|
1760
|
+
if (typeof obj[key] !== "boolean") errors.push({ path, message: "must be a boolean" });
|
|
1761
|
+
}
|
|
1762
|
+
function boundedInteger(errors, obj, key, min, max, path) {
|
|
1763
|
+
if (!Number.isInteger(obj?.[key]) || obj[key] < min || obj[key] > max) errors.push({ path, message: `must be an integer from ${min} to ${max}` });
|
|
1764
|
+
}
|
|
1765
|
+
function optionalNullableString(errors, obj, key, path) {
|
|
1766
|
+
if (obj[key] === void 0 || obj[key] === null) return;
|
|
1767
|
+
if (!stringValue(obj[key])) errors.push({ path, message: "must be a non-empty string or null" });
|
|
1768
|
+
}
|
|
1769
|
+
function optionalNullableEnum(errors, obj, key, values, path) {
|
|
1770
|
+
if (obj[key] === void 0 || obj[key] === null) return;
|
|
1771
|
+
requiredEnum(errors, obj, key, values, path);
|
|
1772
|
+
}
|
|
1773
|
+
function optionalNullableHash(errors, obj, key, path) {
|
|
1774
|
+
if (obj[key] === void 0 || obj[key] === null) return;
|
|
1775
|
+
requiredHash(errors, obj, key, path);
|
|
1776
|
+
}
|
|
1777
|
+
function requiredFullGitSha(errors, obj, key, path) {
|
|
1778
|
+
if (typeof obj[key] !== "string" || !FULL_GIT_SHA_PATTERN.test(obj[key])) errors.push({ path, message: "must be a full 40-character lowercase git SHA" });
|
|
1779
|
+
}
|
|
1780
|
+
function optionalNullableFullGitSha(errors, obj, key, path) {
|
|
1781
|
+
if (obj[key] === void 0 || obj[key] === null) return;
|
|
1782
|
+
requiredFullGitSha(errors, obj, key, path);
|
|
1783
|
+
}
|
|
1784
|
+
function allowedKeys(errors, value, allowed, path) {
|
|
1785
|
+
for (const key of Object.keys(value)) if (!allowed.has(key)) errors.push({ path: `${path}.${key}`, message: "is not allowed" });
|
|
1786
|
+
}
|
|
1787
|
+
function requiredInteger(errors, obj, key, path) {
|
|
1788
|
+
if (!Number.isInteger(obj[key]) || obj[key] < 0) errors.push({ path, message: "must be a non-negative integer" });
|
|
1789
|
+
}
|
|
1790
|
+
function optionalNullableInteger(errors, obj, key, path) {
|
|
1791
|
+
if (obj[key] === null) return;
|
|
1792
|
+
requiredInteger(errors, obj, key, path);
|
|
1793
|
+
}
|
|
1794
|
+
function requiredHash(errors, obj, key, path) {
|
|
1795
|
+
if (typeof obj[key] !== "string" || !HASH_PATTERN.test(obj[key])) errors.push({ path, message: "must be a sha256 hash" });
|
|
1796
|
+
}
|
|
1797
|
+
function optionalHash(errors, obj, key, path) {
|
|
1798
|
+
if (obj[key] === void 0 || obj[key] === null) return;
|
|
1799
|
+
requiredHash(errors, obj, key, path);
|
|
1800
|
+
}
|
|
1801
|
+
function stringValue(value) {
|
|
1802
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
1803
|
+
}
|
|
1804
|
+
function safeValidationIdentifier(value) {
|
|
1805
|
+
return safeValidationText(value) || "<control characters removed>";
|
|
1806
|
+
}
|
|
1807
|
+
function safeValidationText(value) {
|
|
1808
|
+
if (!hasTerminalControl(value)) return value;
|
|
1809
|
+
return sanitizePublicCostText(value);
|
|
1810
|
+
}
|
|
1811
|
+
function fail(errors) {
|
|
1812
|
+
throw new ValidationError(errors);
|
|
1813
|
+
}
|
|
1814
|
+
function isRecord2(value) {
|
|
1815
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
// src/run-state.js
|
|
1819
|
+
var HEARTBEAT_STEP_IN_FLIGHT_STATUSES = /* @__PURE__ */ new Set(["running"]);
|
|
1820
|
+
var HEARTBEAT_SLICE_IN_FLIGHT_STATUSES = /* @__PURE__ */ new Set(["running", "review"]);
|
|
1821
|
+
var POST_PR_HEARTBEAT_PHASES = /* @__PURE__ */ new Set(["observing", "remediation-running", "revalidating"]);
|
|
1822
|
+
var POST_PR_TERMINAL_PHASE = Object.freeze({ completed: "succeeded", blocked: "blocked", "needs-human": "needs-human" });
|
|
1823
|
+
function hasInFlightHeartbeatWork(run) {
|
|
1824
|
+
if (Array.isArray(run.steps) && run.steps.some((step) => HEARTBEAT_STEP_IN_FLIGHT_STATUSES.has(step?.status))) return true;
|
|
1825
|
+
if (Array.isArray(run.slices) && run.slices.some((slice) => HEARTBEAT_SLICE_IN_FLIGHT_STATUSES.has(slice?.status))) return true;
|
|
1826
|
+
if (run?.status === "running" && run?.post_pr?.policy?.enabled === true && POST_PR_HEARTBEAT_PHASES.has(run.post_pr.phase)) return true;
|
|
1827
|
+
return false;
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
// src/hardening/sensitive-data.js
|
|
1831
|
+
var BASELINE_SENSITIVE_KEY_PATTERN = /(?:secret|token|password|passwd|pwd|api[_-]?key|private[_-]?key|credential|authorization|auth[_-]?header|access[_-]?key|bearer|cookie)/iu;
|
|
1832
|
+
var BASELINE_SENSITIVE_VALUE_PATTERN = /(?:secret|token|password|passwd|api[_-]?key|private[_-]?key)/iu;
|
|
1833
|
+
var FLATTENED_BASIC_HEADER_PATTERN = /(^|[\r\n]|[^A-Za-z0-9_-])((?:Proxy-)?Authorization[ \t]*[:=][ \t]*Basic[ \t]+)([A-Za-z0-9._~+/-]+={0,})(?![A-Za-z0-9._~+/=-])/iu;
|
|
1834
|
+
var FLATTENED_BASIC_HEADER_GLOBAL_PATTERN = /(^|[\r\n]|[^A-Za-z0-9_-])((?:Proxy-)?Authorization[ \t]*[:=][ \t]*Basic[ \t]+)([A-Za-z0-9._~+/-]+={0,})(?![A-Za-z0-9._~+/=-])/giu;
|
|
1835
|
+
var TOKEN_SHAPED_VALUE_PATTERNS = Object.freeze([
|
|
1836
|
+
/\bgh[pousr]_[A-Za-z0-9_]{20,}\b/iu,
|
|
1837
|
+
/\bgithub_pat_[A-Za-z0-9_]{20,}\b/iu,
|
|
1838
|
+
/\bhc_[A-Za-z0-9][A-Za-z0-9_-]{10,}\b/iu,
|
|
1839
|
+
/\bsk-proj[-_][A-Za-z0-9_-]{20,}\b/iu,
|
|
1840
|
+
/\bsk-[A-Za-z0-9_-]{20,}\b/iu,
|
|
1841
|
+
/\bxox[abp][_-][A-Za-z0-9-]{10,}(?:-[A-Za-z0-9-]{10,})*\b/iu,
|
|
1842
|
+
/\bglpat-[A-Za-z0-9_-]{20,}\b/iu,
|
|
1843
|
+
/\bBearer\s+[A-Za-z0-9._~+/=-]{20,}\b/iu,
|
|
1844
|
+
/\beyJ[A-Za-z0-9_-]{7,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/iu,
|
|
1845
|
+
/\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/iu,
|
|
1846
|
+
/(?:^|[^A-Za-z0-9-])(?:[A-Fa-f0-9]{32,})(?=$|[^A-Za-z0-9-])/u,
|
|
1847
|
+
/(?:https?|ssh|git|ftp):\/\/[^/\s:@]+:[^/\s@]+@/iu
|
|
1848
|
+
]);
|
|
1849
|
+
var ENDPOINT_PROVIDER_PREFIX_PATTERN = /(?:hc[a-z0-9_-]*|gh[pousr]|github_pat|sk(?:-proj)?|xox[abp]|glpat)[_-][A-Za-z0-9_-]{10,}/iu;
|
|
1850
|
+
var ENDPOINT_BARE_HEX_PATTERN = /^[A-Fa-f0-9]{32,}$/u;
|
|
1851
|
+
var ENDPOINT_SECRET_ASSIGNMENT_PATTERN = /(?:(?:key|token|secret|password|authorization|credential|access|team|api[_-]?key)\s*[=:]|[=:]\s*(?:key|token|secret|password|authorization|credential|access|team|api[_-]?key))/iu;
|
|
1852
|
+
var ENDPOINT_LONG_TOKEN_PATTERN = /^[A-Za-z0-9._~+/-]{24,}$/u;
|
|
1853
|
+
var HIGH_ENTROPY_SINGLE_TOKEN_MIN_LENGTH2 = 32;
|
|
1854
|
+
var HIGH_ENTROPY_MIN_SHANNON2 = 3.5;
|
|
1855
|
+
var SAFE_HIGH_ENTROPY_KEY_PATTERN = /^(?:OTEL_EXPORTER_OTLP(?:_(?:TRACES|METRICS|LOGS))?_(?:ENDPOINT|HEADERS|PROTOCOL|TIMEOUT|COMPRESSION|INSECURE|CERTIFICATE)|FEATURE_FACTORY_OTEL_ENABLED)$/u;
|
|
1856
|
+
var ARRAY_INDEX_MAX = 2 ** 32 - 2;
|
|
1857
|
+
var REDACTED_VALUE = "[redacted]";
|
|
1858
|
+
var REDACTED_KEY = "[redacted-key]";
|
|
1859
|
+
var SCRUB_MARKERS = Object.freeze({
|
|
1860
|
+
circular: "[circular]",
|
|
1861
|
+
repeated: "[repeated]",
|
|
1862
|
+
truncated: "[truncated]",
|
|
1863
|
+
unsupported: "[unsupported]",
|
|
1864
|
+
unavailable: "[unavailable]"
|
|
1865
|
+
});
|
|
1866
|
+
function isSensitiveKey(value, { mode = "baseline" } = {}) {
|
|
1867
|
+
assertMode(mode);
|
|
1868
|
+
if (typeof value !== "string") return false;
|
|
1869
|
+
return BASELINE_SENSITIVE_KEY_PATTERN.test(value) || isSecretShapedKey(value, { mode });
|
|
1870
|
+
}
|
|
1871
|
+
function isSecretShapedKey(value, { mode = "baseline" } = {}) {
|
|
1872
|
+
assertMode(mode);
|
|
1873
|
+
if (typeof value !== "string") return false;
|
|
1874
|
+
const trimmed = value.trim();
|
|
1875
|
+
if (!trimmed || SAFE_HIGH_ENTROPY_KEY_PATTERN.test(trimmed)) return false;
|
|
1876
|
+
return tokenShapedValue(trimmed) || credentialBearingUrl2(trimmed) || highEntropySingleToken2(trimmed) || mode === "endpoint" && endpointValueLooksSensitive(trimmed);
|
|
1877
|
+
}
|
|
1878
|
+
function isSensitiveValue(value, { mode = "baseline" } = {}) {
|
|
1879
|
+
assertMode(mode);
|
|
1880
|
+
if (typeof value !== "string") return false;
|
|
1881
|
+
const trimmed = value.trim();
|
|
1882
|
+
if (!trimmed) return false;
|
|
1883
|
+
return FLATTENED_BASIC_HEADER_PATTERN.test(value) || BASELINE_SENSITIVE_VALUE_PATTERN.test(trimmed) || tokenShapedValue(trimmed) || credentialBearingUrl2(trimmed) || highEntropySingleToken2(trimmed) || mode === "endpoint" && endpointValueLooksSensitive(trimmed);
|
|
1884
|
+
}
|
|
1885
|
+
function scrubSensitiveString(value, { mode = "baseline" } = {}) {
|
|
1886
|
+
assertMode(mode);
|
|
1887
|
+
if (typeof value !== "string") return SCRUB_MARKERS.unsupported;
|
|
1888
|
+
const flattenedCredentialsRedacted = value.replace(
|
|
1889
|
+
FLATTENED_BASIC_HEADER_GLOBAL_PATTERN,
|
|
1890
|
+
`$1$2${REDACTED_VALUE}`
|
|
1891
|
+
);
|
|
1892
|
+
return isSensitiveValue(flattenedCredentialsRedacted, { mode }) ? REDACTED_VALUE : flattenedCredentialsRedacted;
|
|
1893
|
+
}
|
|
1894
|
+
function scrubSensitiveData(value, {
|
|
1895
|
+
mode = "baseline",
|
|
1896
|
+
keyMode = "omit",
|
|
1897
|
+
maxDepth = 32,
|
|
1898
|
+
maxNodes = 1e4,
|
|
1899
|
+
maxKeys = 1e4,
|
|
1900
|
+
maxArrayLength = 1e4,
|
|
1901
|
+
maxStringLength = 65536
|
|
1902
|
+
} = {}) {
|
|
1903
|
+
assertMode(mode);
|
|
1904
|
+
if (keyMode !== "omit" && keyMode !== "redact") throw new TypeError("invalid sensitive-data key mode");
|
|
1905
|
+
assertBound("maxDepth", maxDepth);
|
|
1906
|
+
assertBound("maxNodes", maxNodes);
|
|
1907
|
+
assertBound("maxKeys", maxKeys);
|
|
1908
|
+
assertBound("maxArrayLength", maxArrayLength, 2 ** 32 - 1, 1);
|
|
1909
|
+
assertBound("maxStringLength", maxStringLength);
|
|
1910
|
+
const state = {
|
|
1911
|
+
mode,
|
|
1912
|
+
keyMode,
|
|
1913
|
+
maxDepth,
|
|
1914
|
+
maxNodes,
|
|
1915
|
+
maxKeys,
|
|
1916
|
+
maxArrayLength,
|
|
1917
|
+
maxStringLength,
|
|
1918
|
+
nodes: 0,
|
|
1919
|
+
keys: 0,
|
|
1920
|
+
active: /* @__PURE__ */ new WeakSet(),
|
|
1921
|
+
completed: /* @__PURE__ */ new WeakSet()
|
|
1922
|
+
};
|
|
1923
|
+
return scrubValue(value, 0, state).value;
|
|
1924
|
+
}
|
|
1925
|
+
function scrubValue(value, depth, state) {
|
|
1926
|
+
if (state.nodes >= state.maxNodes) return { value: SCRUB_MARKERS.truncated, stop: true };
|
|
1927
|
+
state.nodes += 1;
|
|
1928
|
+
if (value === null || typeof value === "boolean") return { value, stop: false };
|
|
1929
|
+
if (typeof value === "number") {
|
|
1930
|
+
return { value: Number.isFinite(value) && !Object.is(value, -0) ? value : SCRUB_MARKERS.unsupported, stop: false };
|
|
1931
|
+
}
|
|
1932
|
+
if (typeof value === "string") {
|
|
1933
|
+
if (value.length > state.maxStringLength) return { value: REDACTED_VALUE, stop: false };
|
|
1934
|
+
return { value: scrubSensitiveString(value, { mode: state.mode }), stop: false };
|
|
1935
|
+
}
|
|
1936
|
+
if (typeof value !== "object") return { value: SCRUB_MARKERS.unsupported, stop: false };
|
|
1937
|
+
let array;
|
|
1938
|
+
let prototype;
|
|
1939
|
+
try {
|
|
1940
|
+
array = Array.isArray(value);
|
|
1941
|
+
prototype = Object.getPrototypeOf(value);
|
|
1942
|
+
} catch {
|
|
1943
|
+
return { value: SCRUB_MARKERS.unavailable, stop: false };
|
|
1944
|
+
}
|
|
1945
|
+
if (array && prototype !== Array.prototype || !array && prototype !== Object.prototype && prototype !== null) {
|
|
1946
|
+
return { value: SCRUB_MARKERS.unsupported, stop: false };
|
|
1947
|
+
}
|
|
1948
|
+
if (depth >= state.maxDepth) return { value: SCRUB_MARKERS.truncated, stop: false };
|
|
1949
|
+
if (state.active.has(value)) return { value: SCRUB_MARKERS.circular, stop: false };
|
|
1950
|
+
if (state.completed.has(value)) return { value: SCRUB_MARKERS.repeated, stop: false };
|
|
1951
|
+
state.active.add(value);
|
|
1952
|
+
let result2;
|
|
1953
|
+
try {
|
|
1954
|
+
result2 = array ? scrubArray(value, depth, state) : scrubObject(value, depth, state);
|
|
1955
|
+
} catch {
|
|
1956
|
+
result2 = { value: SCRUB_MARKERS.unavailable, stop: false };
|
|
1957
|
+
}
|
|
1958
|
+
state.active.delete(value);
|
|
1959
|
+
state.completed.add(value);
|
|
1960
|
+
return result2;
|
|
1961
|
+
}
|
|
1962
|
+
function scrubObject(source, depth, state) {
|
|
1963
|
+
let keys;
|
|
1964
|
+
try {
|
|
1965
|
+
keys = Reflect.ownKeys(source);
|
|
1966
|
+
} catch {
|
|
1967
|
+
return { value: SCRUB_MARKERS.unavailable, stop: false };
|
|
1968
|
+
}
|
|
1969
|
+
const output = /* @__PURE__ */ Object.create(null);
|
|
1970
|
+
return projectKeys(source, output, keys, depth, state, () => true);
|
|
1971
|
+
}
|
|
1972
|
+
function scrubArray(source, depth, state) {
|
|
1973
|
+
let lengthDescriptor;
|
|
1974
|
+
let keys;
|
|
1975
|
+
try {
|
|
1976
|
+
lengthDescriptor = Reflect.getOwnPropertyDescriptor(source, "length");
|
|
1977
|
+
keys = Reflect.ownKeys(source);
|
|
1978
|
+
} catch {
|
|
1979
|
+
return { value: SCRUB_MARKERS.unavailable, stop: false };
|
|
1980
|
+
}
|
|
1981
|
+
if (!lengthDescriptor || !("value" in lengthDescriptor) || !Number.isInteger(lengthDescriptor.value) || lengthDescriptor.value < 0 || lengthDescriptor.value > 2 ** 32 - 1) {
|
|
1982
|
+
return { value: SCRUB_MARKERS.unavailable, stop: false };
|
|
1983
|
+
}
|
|
1984
|
+
const sourceLength = lengthDescriptor.value;
|
|
1985
|
+
const outputLength = Math.min(sourceLength, state.maxArrayLength);
|
|
1986
|
+
const output = new Array(outputLength);
|
|
1987
|
+
const overCap = sourceLength > state.maxArrayLength;
|
|
1988
|
+
const forcedTruncationIndex = overCap && outputLength > 0 ? outputLength - 1 : -1;
|
|
1989
|
+
if (forcedTruncationIndex >= 0) defineData(output, String(forcedTruncationIndex), SCRUB_MARKERS.truncated);
|
|
1990
|
+
return projectKeys(source, output, keys, depth, state, (key) => {
|
|
1991
|
+
if (key === "length") return false;
|
|
1992
|
+
const index = arrayIndex(key);
|
|
1993
|
+
if (index === null) return true;
|
|
1994
|
+
if (index >= outputLength) return false;
|
|
1995
|
+
return index !== forcedTruncationIndex;
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
function projectKeys(source, output, keys, depth, state, include) {
|
|
1999
|
+
const prepared = prepareKeys(source, keys, state, include);
|
|
2000
|
+
const allocated = new Set(prepared.reserved);
|
|
2001
|
+
for (const item of prepared.items) {
|
|
2002
|
+
const { key, sensitive, descriptor } = item;
|
|
2003
|
+
if (state.keys >= state.maxKeys) {
|
|
2004
|
+
addTruncationProperty(output, allocated);
|
|
2005
|
+
return { value: output, stop: true };
|
|
2006
|
+
}
|
|
2007
|
+
state.keys += 1;
|
|
2008
|
+
if (item.unavailable) {
|
|
2009
|
+
if (state.keyMode === "omit" && sensitive) continue;
|
|
2010
|
+
const outputKey = sensitive ? allocateName(REDACTED_KEY, allocated) : key;
|
|
2011
|
+
defineData(output, outputKey, SCRUB_MARKERS.unavailable);
|
|
2012
|
+
continue;
|
|
2013
|
+
}
|
|
2014
|
+
if (sensitive) {
|
|
2015
|
+
if (state.keyMode === "redact") defineData(output, allocateName(REDACTED_KEY, allocated), REDACTED_VALUE);
|
|
2016
|
+
continue;
|
|
2017
|
+
}
|
|
2018
|
+
if (!("value" in descriptor)) {
|
|
2019
|
+
defineData(output, key, SCRUB_MARKERS.unavailable);
|
|
2020
|
+
continue;
|
|
2021
|
+
}
|
|
2022
|
+
const scrubbed = scrubValue(descriptor.value, depth + 1, state);
|
|
2023
|
+
defineData(output, key, scrubbed.value);
|
|
2024
|
+
if (scrubbed.stop) return { value: output, stop: true };
|
|
2025
|
+
}
|
|
2026
|
+
if (prepared.truncated) {
|
|
2027
|
+
addTruncationProperty(output, allocated);
|
|
2028
|
+
return { value: output, stop: true };
|
|
2029
|
+
}
|
|
2030
|
+
return { value: output, stop: false };
|
|
2031
|
+
}
|
|
2032
|
+
function prepareKeys(source, keys, state, include) {
|
|
2033
|
+
const items = [];
|
|
2034
|
+
const reserved = /* @__PURE__ */ new Set();
|
|
2035
|
+
const remaining = state.maxKeys - state.keys;
|
|
2036
|
+
for (const key of keys) {
|
|
2037
|
+
if (typeof key !== "string" || !include(key)) continue;
|
|
2038
|
+
const sensitive = key.length > state.maxStringLength || isSensitiveKey(key, { mode: state.mode });
|
|
2039
|
+
let descriptor;
|
|
2040
|
+
try {
|
|
2041
|
+
descriptor = Reflect.getOwnPropertyDescriptor(source, key);
|
|
2042
|
+
} catch {
|
|
2043
|
+
descriptor = null;
|
|
2044
|
+
}
|
|
2045
|
+
if (descriptor && descriptor.enumerable !== true) continue;
|
|
2046
|
+
if (items.length >= remaining) return { items, reserved, truncated: true };
|
|
2047
|
+
items.push({ key, sensitive, descriptor, unavailable: descriptor === null || descriptor === void 0 });
|
|
2048
|
+
if (!sensitive) reserved.add(key);
|
|
2049
|
+
}
|
|
2050
|
+
return { items, reserved, truncated: false };
|
|
2051
|
+
}
|
|
2052
|
+
function addTruncationProperty(output, allocated) {
|
|
2053
|
+
defineData(output, allocateName(SCRUB_MARKERS.truncated, allocated), SCRUB_MARKERS.truncated);
|
|
2054
|
+
}
|
|
2055
|
+
function allocateName(base, allocated) {
|
|
2056
|
+
if (!allocated.has(base)) {
|
|
2057
|
+
allocated.add(base);
|
|
2058
|
+
return base;
|
|
2059
|
+
}
|
|
2060
|
+
for (let suffix = 2; ; suffix += 1) {
|
|
2061
|
+
const candidate = `${base}#${suffix}`;
|
|
2062
|
+
if (!allocated.has(candidate)) {
|
|
2063
|
+
allocated.add(candidate);
|
|
2064
|
+
return candidate;
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
function defineData(target, key, value) {
|
|
2069
|
+
Object.defineProperty(target, key, {
|
|
2070
|
+
value,
|
|
2071
|
+
enumerable: true,
|
|
2072
|
+
configurable: true,
|
|
2073
|
+
writable: true
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
function arrayIndex(key) {
|
|
2077
|
+
if (key === "") return null;
|
|
2078
|
+
const number = Number(key);
|
|
2079
|
+
if (!Number.isInteger(number) || number < 0 || number > ARRAY_INDEX_MAX || String(number) !== key) return null;
|
|
2080
|
+
return number;
|
|
2081
|
+
}
|
|
2082
|
+
function endpointValueLooksSensitive(value) {
|
|
2083
|
+
return ENDPOINT_PROVIDER_PREFIX_PATTERN.test(value) || ENDPOINT_BARE_HEX_PATTERN.test(value) || ENDPOINT_SECRET_ASSIGNMENT_PATTERN.test(value) || ENDPOINT_LONG_TOKEN_PATTERN.test(value) && mixedTokenChars(value);
|
|
2084
|
+
}
|
|
2085
|
+
function tokenShapedValue(value) {
|
|
2086
|
+
return TOKEN_SHAPED_VALUE_PATTERNS.some((pattern) => pattern.test(value));
|
|
2087
|
+
}
|
|
2088
|
+
function credentialBearingUrl2(value) {
|
|
2089
|
+
try {
|
|
2090
|
+
const parsed = new URL(value);
|
|
2091
|
+
return Boolean(parsed.username || parsed.password);
|
|
2092
|
+
} catch {
|
|
2093
|
+
return false;
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
function highEntropySingleToken2(value) {
|
|
2097
|
+
if (value.length < HIGH_ENTROPY_SINGLE_TOKEN_MIN_LENGTH2 || /\s/u.test(value)) return false;
|
|
2098
|
+
if (!/^[A-Za-z0-9._~+/=-]+$/u.test(value)) return false;
|
|
2099
|
+
return shannonEntropy2(value) >= HIGH_ENTROPY_MIN_SHANNON2;
|
|
2100
|
+
}
|
|
2101
|
+
function shannonEntropy2(value) {
|
|
2102
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2103
|
+
for (const character of value) counts.set(character, (counts.get(character) || 0) + 1);
|
|
2104
|
+
let entropy = 0;
|
|
2105
|
+
for (const count of counts.values()) {
|
|
2106
|
+
const probability = count / value.length;
|
|
2107
|
+
entropy -= probability * Math.log2(probability);
|
|
2108
|
+
}
|
|
2109
|
+
return entropy;
|
|
2110
|
+
}
|
|
2111
|
+
function mixedTokenChars(value) {
|
|
2112
|
+
return /[A-Z]/u.test(value) && /[a-z]/u.test(value) && /[0-9]/u.test(value);
|
|
2113
|
+
}
|
|
2114
|
+
function assertMode(mode) {
|
|
2115
|
+
if (mode !== "baseline" && mode !== "endpoint") throw new TypeError("invalid sensitive-data mode");
|
|
2116
|
+
}
|
|
2117
|
+
function assertBound(name, value, maximum = Number.MAX_SAFE_INTEGER, minimum = 0) {
|
|
2118
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
2119
|
+
throw new TypeError(`invalid sensitive-data ${name}`);
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
// src/hardening/terminal-encoding.js
|
|
2124
|
+
var ASCII_IDENTITY_PROFILE = "ascii-identity";
|
|
2125
|
+
var UNICODE_PROSE_PROFILE = "unicode-prose";
|
|
2126
|
+
var SAFE_JSON_MESSAGE = "Value is not valid safe JSON data.";
|
|
2127
|
+
var UNICODE_PROSE_SCALAR = /^[\p{L}\p{M}\p{N}\p{P}\p{S}]$/u;
|
|
2128
|
+
var DEFAULT_IGNORABLE_SCALAR = /^\p{Default_Ignorable_Code_Point}$/u;
|
|
2129
|
+
var SafeJsonError = class extends Error {
|
|
2130
|
+
constructor() {
|
|
2131
|
+
super(SAFE_JSON_MESSAGE);
|
|
2132
|
+
this.name = "SafeJsonError";
|
|
2133
|
+
}
|
|
2134
|
+
};
|
|
2135
|
+
function encodeTerminalText(value, options = void 0) {
|
|
2136
|
+
return encodeTerminalValue(value, readProfile(options, UNICODE_PROSE_PROFILE), false);
|
|
2137
|
+
}
|
|
2138
|
+
function serializeTerminalJson(value, options = void 0) {
|
|
2139
|
+
try {
|
|
2140
|
+
const space = readJsonSpace(options);
|
|
2141
|
+
const clone = cloneJsonValue(value, /* @__PURE__ */ new WeakSet());
|
|
2142
|
+
return escapeJsonTransport(JSON.stringify(clone, null, space));
|
|
2143
|
+
} catch {
|
|
2144
|
+
throw new SafeJsonError();
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
function readProfile(options, fallback) {
|
|
2148
|
+
try {
|
|
2149
|
+
if (options === void 0) return fallback;
|
|
2150
|
+
if (options === null || typeof options !== "object" && typeof options !== "function") {
|
|
2151
|
+
throw new TypeError();
|
|
2152
|
+
}
|
|
2153
|
+
const profile = options.profile === void 0 ? fallback : options.profile;
|
|
2154
|
+
if (profile !== ASCII_IDENTITY_PROFILE && profile !== UNICODE_PROSE_PROFILE) {
|
|
2155
|
+
throw new TypeError();
|
|
2156
|
+
}
|
|
2157
|
+
return profile;
|
|
2158
|
+
} catch {
|
|
2159
|
+
throw new TypeError("Invalid terminal encoding options.");
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
function encodeTerminalValue(value, profile, quoteIsReserved) {
|
|
2163
|
+
let text;
|
|
2164
|
+
try {
|
|
2165
|
+
text = String(value);
|
|
2166
|
+
} catch {
|
|
2167
|
+
throw new TypeError("Terminal text encoding failed.");
|
|
2168
|
+
}
|
|
2169
|
+
let encoded = "";
|
|
2170
|
+
for (let index = 0; index < text.length; ) {
|
|
2171
|
+
const first = text.charCodeAt(index);
|
|
2172
|
+
if (first === 92) {
|
|
2173
|
+
encoded += "\\\\";
|
|
2174
|
+
index += 1;
|
|
2175
|
+
continue;
|
|
2176
|
+
}
|
|
2177
|
+
if (quoteIsReserved && first === 34) {
|
|
2178
|
+
encoded += '\\"';
|
|
2179
|
+
index += 1;
|
|
2180
|
+
continue;
|
|
2181
|
+
}
|
|
2182
|
+
if (profile === ASCII_IDENTITY_PROFILE) {
|
|
2183
|
+
encoded += first >= 32 && first <= 126 ? text[index] : unicodeEscape(first);
|
|
2184
|
+
index += 1;
|
|
2185
|
+
continue;
|
|
2186
|
+
}
|
|
2187
|
+
const second = text.charCodeAt(index + 1);
|
|
2188
|
+
const isPair = first >= 55296 && first <= 56319 && second >= 56320 && second <= 57343;
|
|
2189
|
+
if (isPair) {
|
|
2190
|
+
const scalar2 = text.slice(index, index + 2);
|
|
2191
|
+
encoded += isSafeUnicodeProseScalar(scalar2) ? scalar2 : `${unicodeEscape(first)}${unicodeEscape(second)}`;
|
|
2192
|
+
index += 2;
|
|
2193
|
+
continue;
|
|
2194
|
+
}
|
|
2195
|
+
const scalar = text[index];
|
|
2196
|
+
encoded += isSafeUnicodeProseScalar(scalar) ? scalar : unicodeEscape(first);
|
|
2197
|
+
index += 1;
|
|
2198
|
+
}
|
|
2199
|
+
return encoded;
|
|
2200
|
+
}
|
|
2201
|
+
function isSafeUnicodeProseScalar(scalar) {
|
|
2202
|
+
if (scalar === " ") return true;
|
|
2203
|
+
return UNICODE_PROSE_SCALAR.test(scalar) && !DEFAULT_IGNORABLE_SCALAR.test(scalar);
|
|
2204
|
+
}
|
|
2205
|
+
function unicodeEscape(codeUnit) {
|
|
2206
|
+
return `\\u${codeUnit.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
2207
|
+
}
|
|
2208
|
+
function readJsonSpace(options) {
|
|
2209
|
+
if (options === void 0) return 0;
|
|
2210
|
+
if (options === null || typeof options !== "object" && typeof options !== "function") failJson();
|
|
2211
|
+
const space = options.space === void 0 ? 0 : options.space;
|
|
2212
|
+
if (space !== 0 && space !== 2) failJson();
|
|
2213
|
+
return space;
|
|
2214
|
+
}
|
|
2215
|
+
function cloneJsonValue(value, seen) {
|
|
2216
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
2217
|
+
if (typeof value === "number") {
|
|
2218
|
+
if (!Number.isFinite(value) || Object.is(value, -0)) failJson();
|
|
2219
|
+
return value;
|
|
2220
|
+
}
|
|
2221
|
+
if (typeof value !== "object") failJson();
|
|
2222
|
+
if (seen.has(value)) failJson();
|
|
2223
|
+
seen.add(value);
|
|
2224
|
+
if (Array.isArray(value)) return cloneJsonArray(value, seen);
|
|
2225
|
+
return cloneJsonObject(value, seen);
|
|
2226
|
+
}
|
|
2227
|
+
function cloneJsonArray(value, seen) {
|
|
2228
|
+
if (Object.getPrototypeOf(value) !== Array.prototype) failJson();
|
|
2229
|
+
const keys = Reflect.ownKeys(value);
|
|
2230
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
|
|
2231
|
+
if (!isDataDescriptor(lengthDescriptor) || lengthDescriptor.enumerable || !Number.isInteger(lengthDescriptor.value) || lengthDescriptor.value < 0 || lengthDescriptor.value > 4294967295 || keys.length !== lengthDescriptor.value + 1) {
|
|
2232
|
+
failJson();
|
|
2233
|
+
}
|
|
2234
|
+
const clone = [];
|
|
2235
|
+
Object.setPrototypeOf(clone, null);
|
|
2236
|
+
for (const key of keys) {
|
|
2237
|
+
if (key === "length") continue;
|
|
2238
|
+
if (typeof key !== "string" || !isArrayIndex(key, lengthDescriptor.value)) failJson();
|
|
2239
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
2240
|
+
if (!isDataDescriptor(descriptor) || !descriptor.enumerable) failJson();
|
|
2241
|
+
defineJsonProperty(clone, key, cloneJsonValue(descriptor.value, seen));
|
|
2242
|
+
}
|
|
2243
|
+
return clone;
|
|
2244
|
+
}
|
|
2245
|
+
function cloneJsonObject(value, seen) {
|
|
2246
|
+
const prototype = Object.getPrototypeOf(value);
|
|
2247
|
+
if (prototype !== Object.prototype && prototype !== null) failJson();
|
|
2248
|
+
const clone = /* @__PURE__ */ Object.create(null);
|
|
2249
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
2250
|
+
if (typeof key !== "string" || key === "toJSON") failJson();
|
|
2251
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
2252
|
+
if (!isDataDescriptor(descriptor) || !descriptor.enumerable) failJson();
|
|
2253
|
+
defineJsonProperty(clone, key, cloneJsonValue(descriptor.value, seen));
|
|
2254
|
+
}
|
|
2255
|
+
return clone;
|
|
2256
|
+
}
|
|
2257
|
+
function isDataDescriptor(descriptor) {
|
|
2258
|
+
return descriptor !== void 0 && Object.hasOwn(descriptor, "value");
|
|
2259
|
+
}
|
|
2260
|
+
function isArrayIndex(key, length) {
|
|
2261
|
+
if (key.length === 0) return false;
|
|
2262
|
+
const index = Number(key);
|
|
2263
|
+
return Number.isInteger(index) && index >= 0 && index < length && String(index) === key;
|
|
2264
|
+
}
|
|
2265
|
+
function defineJsonProperty(target, key, value) {
|
|
2266
|
+
Object.defineProperty(target, key, {
|
|
2267
|
+
value,
|
|
2268
|
+
enumerable: true,
|
|
2269
|
+
configurable: true,
|
|
2270
|
+
writable: true
|
|
2271
|
+
});
|
|
2272
|
+
}
|
|
2273
|
+
function escapeJsonTransport(serialized) {
|
|
2274
|
+
let safe = "";
|
|
2275
|
+
for (let index = 0; index < serialized.length; ) {
|
|
2276
|
+
const first = serialized.charCodeAt(index);
|
|
2277
|
+
if (first <= 126) {
|
|
2278
|
+
safe += serialized[index];
|
|
2279
|
+
index += 1;
|
|
2280
|
+
continue;
|
|
2281
|
+
}
|
|
2282
|
+
const second = serialized.charCodeAt(index + 1);
|
|
2283
|
+
const isPair = first >= 55296 && first <= 56319 && second >= 56320 && second <= 57343;
|
|
2284
|
+
if (isPair) {
|
|
2285
|
+
const scalar = serialized.slice(index, index + 2);
|
|
2286
|
+
safe += isSafeUnicodeProseScalar(scalar) ? scalar : `${unicodeEscape(first)}${unicodeEscape(second)}`;
|
|
2287
|
+
index += 2;
|
|
2288
|
+
continue;
|
|
2289
|
+
}
|
|
2290
|
+
safe += isSafeUnicodeProseScalar(serialized[index]) ? serialized[index] : unicodeEscape(first);
|
|
2291
|
+
index += 1;
|
|
2292
|
+
}
|
|
2293
|
+
return safe;
|
|
2294
|
+
}
|
|
2295
|
+
function failJson() {
|
|
2296
|
+
throw new Error();
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
// src/hardening/output-policy.js
|
|
2300
|
+
var SAFE_OUTPUT_FALLBACK = "Output rendering failed safely.";
|
|
2301
|
+
var DIAGNOSTIC_FREEFORM_FIELDS = Object.freeze([
|
|
2302
|
+
"error",
|
|
2303
|
+
"message",
|
|
2304
|
+
"reason",
|
|
2305
|
+
"detail",
|
|
2306
|
+
"summary",
|
|
2307
|
+
"action"
|
|
2308
|
+
]);
|
|
2309
|
+
var OUTPUT_POLICY_ERROR_CODE = "ERR_OUTPUT_POLICY";
|
|
2310
|
+
var segmentBrand = /* @__PURE__ */ new WeakSet();
|
|
2311
|
+
var TRUSTED_FRAMING_TEXT = Object.freeze({
|
|
2312
|
+
EMPTY: "",
|
|
2313
|
+
SPACE: " ",
|
|
2314
|
+
COLON_SPACE: ": ",
|
|
2315
|
+
COMMA_SPACE: ", ",
|
|
2316
|
+
DASH_SEPARATOR: " - ",
|
|
2317
|
+
SLASH: "/",
|
|
2318
|
+
EQUALS: "=",
|
|
2319
|
+
TAB: " ",
|
|
2320
|
+
LINE_FEED: "\n",
|
|
2321
|
+
OPEN_PAREN: "(",
|
|
2322
|
+
CLOSE_PAREN: ")",
|
|
2323
|
+
OPEN_BRACKET: "[",
|
|
2324
|
+
CLOSE_BRACKET: "]",
|
|
2325
|
+
ERROR_PREFIX: "error: ",
|
|
2326
|
+
WARNING_PREFIX: "warning: "
|
|
2327
|
+
});
|
|
2328
|
+
var TRUSTED_SEGMENTS = createTrustedSegments();
|
|
2329
|
+
var OutputPolicyError = class extends Error {
|
|
2330
|
+
constructor(stage = "output") {
|
|
2331
|
+
super(SAFE_OUTPUT_FALLBACK);
|
|
2332
|
+
this.name = "OutputPolicyError";
|
|
2333
|
+
this.code = OUTPUT_POLICY_ERROR_CODE;
|
|
2334
|
+
this.stage = safeStage(stage);
|
|
2335
|
+
}
|
|
2336
|
+
};
|
|
2337
|
+
function identitySegment(value) {
|
|
2338
|
+
return makeSegment("identity", value);
|
|
2339
|
+
}
|
|
2340
|
+
function freeformSegment(value) {
|
|
2341
|
+
return makeSegment("freeform", value);
|
|
2342
|
+
}
|
|
2343
|
+
function isOutputSegment(value) {
|
|
2344
|
+
return (typeof value === "object" || typeof value === "function") && value !== null && segmentBrand.has(value);
|
|
2345
|
+
}
|
|
2346
|
+
function renderTerminalSegments(segments) {
|
|
2347
|
+
try {
|
|
2348
|
+
return normalizeSegments(segments).map(renderSegment).join("");
|
|
2349
|
+
} catch (error) {
|
|
2350
|
+
if (error instanceof OutputPolicyError) throw error;
|
|
2351
|
+
fail2("render");
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
function renderTerminalSegmentsOrFallback(segments) {
|
|
2355
|
+
try {
|
|
2356
|
+
return renderTerminalSegments(segments);
|
|
2357
|
+
} catch {
|
|
2358
|
+
return SAFE_OUTPUT_FALLBACK;
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
function projectFreeformData(value) {
|
|
2362
|
+
try {
|
|
2363
|
+
return scrubSensitiveData(value, { mode: "baseline" });
|
|
2364
|
+
} catch {
|
|
2365
|
+
fail2("projection");
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
function projectDiagnosticData(value, options = void 0) {
|
|
2369
|
+
try {
|
|
2370
|
+
const policy = readDiagnosticPolicy(options);
|
|
2371
|
+
return projectDiagnosticValue(value, policy, [], 0, createDiagnosticState(policy)).value;
|
|
2372
|
+
} catch (error) {
|
|
2373
|
+
if (error instanceof OutputPolicyError) throw error;
|
|
2374
|
+
fail2("projection");
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
function makeSegment(kind, value) {
|
|
2378
|
+
const segment = Object.freeze({ kind, value });
|
|
2379
|
+
segmentBrand.add(segment);
|
|
2380
|
+
return segment;
|
|
2381
|
+
}
|
|
2382
|
+
function createTrustedSegments() {
|
|
2383
|
+
const output = /* @__PURE__ */ Object.create(null);
|
|
2384
|
+
for (const name of Object.keys(TRUSTED_FRAMING_TEXT)) {
|
|
2385
|
+
defineData2(output, name, makeSegment("trusted", TRUSTED_FRAMING_TEXT[name]));
|
|
2386
|
+
}
|
|
2387
|
+
return Object.freeze(output);
|
|
2388
|
+
}
|
|
2389
|
+
function renderSegment(segment) {
|
|
2390
|
+
if (segment.kind === "trusted") return segment.value;
|
|
2391
|
+
if (segment.kind === "identity") return encodeTerminalText(segment.value, { profile: "ascii-identity" });
|
|
2392
|
+
const scrubbed = projectFreeformData(segment.value);
|
|
2393
|
+
if (scrubbed !== null && typeof scrubbed === "object") return serializeTerminalJson(scrubbed);
|
|
2394
|
+
return encodeTerminalText(scrubbed, { profile: "unicode-prose" });
|
|
2395
|
+
}
|
|
2396
|
+
function normalizeSegments(segments) {
|
|
2397
|
+
try {
|
|
2398
|
+
if (!Array.isArray(segments) || Object.getPrototypeOf(segments) !== Array.prototype) fail2("segment");
|
|
2399
|
+
const keys = Reflect.ownKeys(segments);
|
|
2400
|
+
const lengthDescriptor = Reflect.getOwnPropertyDescriptor(segments, "length");
|
|
2401
|
+
if (!isDataDescriptor2(lengthDescriptor) || !Number.isSafeInteger(lengthDescriptor.value) || lengthDescriptor.value < 0 || keys.length !== lengthDescriptor.value + 1) fail2("segment");
|
|
2402
|
+
const normalized = [];
|
|
2403
|
+
for (let index = 0; index < lengthDescriptor.value; index += 1) {
|
|
2404
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(segments, String(index));
|
|
2405
|
+
if (!isDataDescriptor2(descriptor) || descriptor.enumerable !== true || !isOutputSegment(descriptor.value)) fail2("segment");
|
|
2406
|
+
normalized.push(descriptor.value);
|
|
2407
|
+
}
|
|
2408
|
+
return Object.freeze(normalized);
|
|
2409
|
+
} catch (error) {
|
|
2410
|
+
if (error instanceof OutputPolicyError) throw error;
|
|
2411
|
+
fail2("segment");
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
function readDiagnosticPolicy(options) {
|
|
2415
|
+
if (options === void 0) return defaultDiagnosticPolicy();
|
|
2416
|
+
if (options === null || typeof options !== "object" && typeof options !== "function") fail2("projection");
|
|
2417
|
+
const fields = options.freeformFields === void 0 ? DIAGNOSTIC_FREEFORM_FIELDS : options.freeformFields;
|
|
2418
|
+
if (!Array.isArray(fields) || fields.some((field) => typeof field !== "string")) fail2("projection");
|
|
2419
|
+
const paths = options.validatedIdentityPaths === void 0 ? [] : options.validatedIdentityPaths;
|
|
2420
|
+
if (!Array.isArray(paths)) fail2("projection");
|
|
2421
|
+
return {
|
|
2422
|
+
freeformFields: new Set(fields),
|
|
2423
|
+
validatedIdentityPaths: paths.map(readIdentityPath),
|
|
2424
|
+
maxDepth: readDiagnosticBound(options, "maxDepth", 32),
|
|
2425
|
+
maxNodes: readDiagnosticBound(options, "maxNodes", 1e4),
|
|
2426
|
+
maxKeys: readDiagnosticBound(options, "maxKeys", 1e4),
|
|
2427
|
+
maxArrayLength: readDiagnosticBound(options, "maxArrayLength", 1e4, 2 ** 32 - 1, 1),
|
|
2428
|
+
maxStringLength: readDiagnosticBound(options, "maxStringLength", 65536)
|
|
2429
|
+
};
|
|
2430
|
+
}
|
|
2431
|
+
function defaultDiagnosticPolicy() {
|
|
2432
|
+
return {
|
|
2433
|
+
freeformFields: new Set(DIAGNOSTIC_FREEFORM_FIELDS),
|
|
2434
|
+
validatedIdentityPaths: [],
|
|
2435
|
+
maxDepth: 32,
|
|
2436
|
+
maxNodes: 1e4,
|
|
2437
|
+
maxKeys: 1e4,
|
|
2438
|
+
maxArrayLength: 1e4,
|
|
2439
|
+
maxStringLength: 65536
|
|
2440
|
+
};
|
|
2441
|
+
}
|
|
2442
|
+
function readDiagnosticBound(options, name, fallback, maximum = Number.MAX_SAFE_INTEGER, minimum = 0) {
|
|
2443
|
+
const value = options[name] === void 0 ? fallback : options[name];
|
|
2444
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) fail2("projection");
|
|
2445
|
+
return value;
|
|
2446
|
+
}
|
|
2447
|
+
function readIdentityPath(path) {
|
|
2448
|
+
if (!Array.isArray(path) || path.length === 0) fail2("projection");
|
|
2449
|
+
const normalized = [];
|
|
2450
|
+
for (let index = 0; index < path.length; index += 1) {
|
|
2451
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(path, String(index));
|
|
2452
|
+
if (!isDataDescriptor2(descriptor) || typeof descriptor.value !== "string" || descriptor.value.length === 0) fail2("projection");
|
|
2453
|
+
normalized.push(descriptor.value);
|
|
2454
|
+
}
|
|
2455
|
+
if (Reflect.ownKeys(path).length !== path.length + 1) fail2("projection");
|
|
2456
|
+
return Object.freeze(normalized);
|
|
2457
|
+
}
|
|
2458
|
+
function createDiagnosticState(policy) {
|
|
2459
|
+
return { nodes: 0, keys: 0, active: /* @__PURE__ */ new WeakSet(), completed: /* @__PURE__ */ new WeakSet(), ...policy };
|
|
2460
|
+
}
|
|
2461
|
+
function projectDiagnosticValue(value, policy, path, depth, state) {
|
|
2462
|
+
if (state.nodes >= state.maxNodes) return { value: SCRUB_MARKERS.truncated, stop: true };
|
|
2463
|
+
state.nodes += 1;
|
|
2464
|
+
if (isValidatedIdentityPath(policy.validatedIdentityPaths, path)) {
|
|
2465
|
+
return { value: validatedIdentityScalar(value), stop: false };
|
|
2466
|
+
}
|
|
2467
|
+
if (value === null || typeof value === "boolean") return { value, stop: false };
|
|
2468
|
+
if (typeof value === "string") {
|
|
2469
|
+
return { value: value.length > state.maxStringLength ? REDACTED_VALUE : scrubSensitiveString(value, { mode: "baseline" }), stop: false };
|
|
2470
|
+
}
|
|
2471
|
+
if (typeof value === "number") {
|
|
2472
|
+
return { value: Number.isFinite(value) && !Object.is(value, -0) ? value : SCRUB_MARKERS.unsupported, stop: false };
|
|
2473
|
+
}
|
|
2474
|
+
if (typeof value !== "object") return { value: SCRUB_MARKERS.unsupported, stop: false };
|
|
2475
|
+
let array;
|
|
2476
|
+
let prototype;
|
|
2477
|
+
try {
|
|
2478
|
+
array = Array.isArray(value);
|
|
2479
|
+
prototype = Object.getPrototypeOf(value);
|
|
2480
|
+
} catch {
|
|
2481
|
+
return { value: SCRUB_MARKERS.unavailable, stop: false };
|
|
2482
|
+
}
|
|
2483
|
+
if (array && prototype !== Array.prototype || !array && prototype !== Object.prototype && prototype !== null) {
|
|
2484
|
+
return { value: SCRUB_MARKERS.unsupported, stop: false };
|
|
2485
|
+
}
|
|
2486
|
+
if (depth >= state.maxDepth) return { value: SCRUB_MARKERS.truncated, stop: false };
|
|
2487
|
+
if (state.active.has(value)) return { value: SCRUB_MARKERS.circular, stop: false };
|
|
2488
|
+
if (state.completed.has(value)) return { value: SCRUB_MARKERS.repeated, stop: false };
|
|
2489
|
+
state.active.add(value);
|
|
2490
|
+
let result2;
|
|
2491
|
+
try {
|
|
2492
|
+
result2 = array ? projectDiagnosticArray(value, policy, path, depth, state) : projectDiagnosticObject(value, policy, path, depth, state);
|
|
2493
|
+
} catch (error) {
|
|
2494
|
+
if (error instanceof OutputPolicyError) throw error;
|
|
2495
|
+
result2 = { value: SCRUB_MARKERS.unavailable, stop: false };
|
|
2496
|
+
}
|
|
2497
|
+
state.active.delete(value);
|
|
2498
|
+
state.completed.add(value);
|
|
2499
|
+
return result2;
|
|
2500
|
+
}
|
|
2501
|
+
function projectDiagnosticArray(value, policy, path, depth, state) {
|
|
2502
|
+
let keys;
|
|
2503
|
+
let lengthDescriptor;
|
|
2504
|
+
try {
|
|
2505
|
+
keys = Reflect.ownKeys(value);
|
|
2506
|
+
lengthDescriptor = Reflect.getOwnPropertyDescriptor(value, "length");
|
|
2507
|
+
} catch {
|
|
2508
|
+
return { value: SCRUB_MARKERS.unavailable, stop: false };
|
|
2509
|
+
}
|
|
2510
|
+
if (!isDataDescriptor2(lengthDescriptor) || !Number.isInteger(lengthDescriptor.value) || lengthDescriptor.value < 0 || lengthDescriptor.value > 2 ** 32 - 1 || keys.length !== lengthDescriptor.value + 1) fail2("projection");
|
|
2511
|
+
const sourceLength = lengthDescriptor.value;
|
|
2512
|
+
const outputLength = Math.min(sourceLength, state.maxArrayLength);
|
|
2513
|
+
const forcedTruncationIndex = sourceLength > state.maxArrayLength ? outputLength - 1 : -1;
|
|
2514
|
+
const output = [];
|
|
2515
|
+
for (let index = 0; index < outputLength; index += 1) {
|
|
2516
|
+
if (index === forcedTruncationIndex) {
|
|
2517
|
+
defineData2(output, String(index), SCRUB_MARKERS.truncated);
|
|
2518
|
+
return { value: output, stop: false };
|
|
2519
|
+
}
|
|
2520
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(value, String(index));
|
|
2521
|
+
if (!isDataDescriptor2(descriptor) || descriptor.enumerable !== true) fail2("projection");
|
|
2522
|
+
if (state.keys >= state.maxKeys) {
|
|
2523
|
+
defineData2(output, String(index), SCRUB_MARKERS.truncated);
|
|
2524
|
+
return { value: output, stop: true };
|
|
2525
|
+
}
|
|
2526
|
+
state.keys += 1;
|
|
2527
|
+
const projected = projectDiagnosticValue(
|
|
2528
|
+
descriptor.value,
|
|
2529
|
+
policy,
|
|
2530
|
+
[...path, String(index)],
|
|
2531
|
+
depth + 1,
|
|
2532
|
+
state
|
|
2533
|
+
);
|
|
2534
|
+
defineData2(output, String(index), projected.value);
|
|
2535
|
+
if (projected.stop) return { value: output, stop: true };
|
|
2536
|
+
}
|
|
2537
|
+
return { value: output, stop: false };
|
|
2538
|
+
}
|
|
2539
|
+
function projectDiagnosticObject(value, policy, path, depth, state) {
|
|
2540
|
+
let keys;
|
|
2541
|
+
try {
|
|
2542
|
+
keys = Reflect.ownKeys(value);
|
|
2543
|
+
} catch {
|
|
2544
|
+
return { value: SCRUB_MARKERS.unavailable, stop: false };
|
|
2545
|
+
}
|
|
2546
|
+
const output = /* @__PURE__ */ Object.create(null);
|
|
2547
|
+
const allocated = /* @__PURE__ */ new Set();
|
|
2548
|
+
for (const key of keys) {
|
|
2549
|
+
if (typeof key !== "string") continue;
|
|
2550
|
+
const classified = key.length > state.maxStringLength || isSensitiveKey(key, { mode: "baseline" });
|
|
2551
|
+
let descriptor;
|
|
2552
|
+
try {
|
|
2553
|
+
descriptor = Reflect.getOwnPropertyDescriptor(value, key);
|
|
2554
|
+
} catch {
|
|
2555
|
+
descriptor = void 0;
|
|
2556
|
+
}
|
|
2557
|
+
if (descriptor && descriptor.enumerable !== true) continue;
|
|
2558
|
+
if (state.keys >= state.maxKeys) {
|
|
2559
|
+
addDiagnosticTruncation(output, allocated);
|
|
2560
|
+
return { value: output, stop: true };
|
|
2561
|
+
}
|
|
2562
|
+
state.keys += 1;
|
|
2563
|
+
if (classified) continue;
|
|
2564
|
+
if (key === "toJSON") fail2("projection");
|
|
2565
|
+
allocated.add(key);
|
|
2566
|
+
if (!isDataDescriptor2(descriptor)) {
|
|
2567
|
+
defineData2(output, key, SCRUB_MARKERS.unavailable);
|
|
2568
|
+
continue;
|
|
2569
|
+
}
|
|
2570
|
+
const projected = projectDiagnosticValue(
|
|
2571
|
+
descriptor.value,
|
|
2572
|
+
policy,
|
|
2573
|
+
[...path, key],
|
|
2574
|
+
depth + 1,
|
|
2575
|
+
state
|
|
2576
|
+
);
|
|
2577
|
+
defineData2(output, key, projected.value);
|
|
2578
|
+
if (projected.stop) return { value: output, stop: true };
|
|
2579
|
+
}
|
|
2580
|
+
return { value: output, stop: false };
|
|
2581
|
+
}
|
|
2582
|
+
function addDiagnosticTruncation(output, allocated) {
|
|
2583
|
+
let key = SCRUB_MARKERS.truncated;
|
|
2584
|
+
for (let suffix = 2; allocated.has(key); suffix += 1) key = `${SCRUB_MARKERS.truncated}#${suffix}`;
|
|
2585
|
+
defineData2(output, key, SCRUB_MARKERS.truncated);
|
|
2586
|
+
}
|
|
2587
|
+
function validatedIdentityScalar(value) {
|
|
2588
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
2589
|
+
if (typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0)) return value;
|
|
2590
|
+
fail2("projection");
|
|
2591
|
+
}
|
|
2592
|
+
function isValidatedIdentityPath(patterns, path) {
|
|
2593
|
+
return patterns.some((pattern) => pattern.length === path.length && pathMatches(pattern, path));
|
|
2594
|
+
}
|
|
2595
|
+
function pathMatches(pattern, path) {
|
|
2596
|
+
return pattern.every((part, index) => part === "*" || part === path[index]);
|
|
2597
|
+
}
|
|
2598
|
+
function isDataDescriptor2(descriptor) {
|
|
2599
|
+
return descriptor !== void 0 && Object.hasOwn(descriptor, "value");
|
|
2600
|
+
}
|
|
2601
|
+
function defineData2(target, key, value) {
|
|
2602
|
+
Object.defineProperty(target, key, { value, enumerable: true, configurable: true, writable: true });
|
|
2603
|
+
}
|
|
2604
|
+
function safeStage(stage) {
|
|
2605
|
+
return ["segment", "render", "projection", "error", "output"].includes(stage) ? stage : "output";
|
|
2606
|
+
}
|
|
2607
|
+
function fail2(stage) {
|
|
2608
|
+
throw new OutputPolicyError(stage);
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
// src/factory-diagnostics.js
|
|
2612
|
+
var DIAGNOSTIC_SCHEMA_VERSION = 1;
|
|
2613
|
+
var HEARTBEAT_FILE = "heartbeat.json";
|
|
2614
|
+
var DEFAULT_HEARTBEAT_INTERVAL_MS = 3e4;
|
|
2615
|
+
var MIN_STALE_HEARTBEAT_MS = 12e4;
|
|
2616
|
+
var CONDITIONS = Object.freeze([
|
|
2617
|
+
Object.freeze({ condition: "invalid-run-state", classification: "invalid", severity: "critical", status: "error", message: "Factory run state is invalid or cannot be parsed.", action: "Treat the run as untrusted until run.json and required sidecars validate." }),
|
|
2618
|
+
Object.freeze({ condition: "missing-worktree", classification: "blocked", severity: "error", status: "error", message: "A recorded worktree path is missing or inaccessible.", action: "Restore the worktree or complete cleanup/recovery from durable state." }),
|
|
2619
|
+
Object.freeze({ condition: "missing-heartbeat-process", classification: "recoverable", severity: "warning", status: "warning", message: "The heartbeat helper process recorded in heartbeat.json is not alive.", action: "Inspect the run log and validate durable state before resuming; do not infer run failure from PID liveness alone." }),
|
|
2620
|
+
Object.freeze({ condition: "stale-heartbeat", classification: "recoverable", severity: "warning", status: "warning", message: "Heartbeat has not advanced within the stale threshold.", action: "Inspect the run log and validate durable state before resuming; do not restart blindly." }),
|
|
2621
|
+
Object.freeze({ condition: "protected-gate", classification: "needs-human", severity: "warning", status: "warning", message: "Run is waiting at a protected human gate.", action: "Answer the pending protected gate or stop the run; heartbeat liveness alarms are suppressed while waiting." }),
|
|
2622
|
+
Object.freeze({ condition: "terminal-run", classification: "terminal", severity: "info", status: "ok", message: "Run is terminal.", action: "Read terminal_result for the durable outcome." })
|
|
2623
|
+
]);
|
|
2624
|
+
var DIAGNOSTIC_CONDITIONS = Object.freeze(CONDITIONS.map((item) => item.condition));
|
|
2625
|
+
var DIAGNOSTIC_CLASSIFICATIONS = Object.freeze(["healthy", "recoverable", "blocked", "needs-human", "terminal", "invalid"]);
|
|
2626
|
+
var DIAGNOSTIC_STATUSES = Object.freeze(["ok", "warning", "error"]);
|
|
2627
|
+
var DIAGNOSTIC_SEVERITIES = Object.freeze(["info", "warning", "error", "critical"]);
|
|
2628
|
+
var CONDITION_BY_NAME = new Map(CONDITIONS.map((item, index) => [item.condition, { ...item, index }]));
|
|
2629
|
+
var TERMINAL_OVERRIDES = Object.freeze({
|
|
2630
|
+
completed: Object.freeze({ classification: "terminal", severity: "info", status: "ok" }),
|
|
2631
|
+
partial: Object.freeze({ classification: "terminal", severity: "info", status: "ok" }),
|
|
2632
|
+
blocked: Object.freeze({ classification: "blocked", severity: "error", status: "error" }),
|
|
2633
|
+
"needs-human": Object.freeze({ classification: "needs-human", severity: "warning", status: "warning" })
|
|
2634
|
+
});
|
|
2635
|
+
var TERMINAL_STATUSES2 = new Set(TERMINAL_RUN_STATUSES);
|
|
2636
|
+
var DIAGNOSTIC_IDENTITY_FIELDS = Object.freeze({
|
|
2637
|
+
status: new Set(DIAGNOSTIC_STATUSES),
|
|
2638
|
+
severity: new Set(DIAGNOSTIC_SEVERITIES),
|
|
2639
|
+
classification: new Set(DIAGNOSTIC_CLASSIFICATIONS),
|
|
2640
|
+
condition: new Set(DIAGNOSTIC_CONDITIONS)
|
|
2641
|
+
});
|
|
2642
|
+
function diagnoseRunFile(runFile, options = {}) {
|
|
2643
|
+
const runDir = options.runDir || dirname3(runFile);
|
|
2644
|
+
const checkedAt = timestamp(options.now);
|
|
2645
|
+
let raw;
|
|
2646
|
+
try {
|
|
2647
|
+
raw = readFileSync(runFile, "utf8");
|
|
2648
|
+
} catch (error) {
|
|
2649
|
+
return diagnosticEnvelope([
|
|
2650
|
+
diagnosticItem("invalid-run-state", {
|
|
2651
|
+
checkedAt,
|
|
2652
|
+
authoritative: false,
|
|
2653
|
+
message: `Factory run state is unavailable: ${error.message}`,
|
|
2654
|
+
evidence: { source: "run.json", run_dir: runDir, run_path: runFile, error: error.message }
|
|
2655
|
+
})
|
|
2656
|
+
], { checkedAt, authoritative: false });
|
|
2657
|
+
}
|
|
2658
|
+
try {
|
|
2659
|
+
return diagnoseRunObject(JSON.parse(raw), { ...options, runDir, runFile, checkedAt });
|
|
2660
|
+
} catch (error) {
|
|
2661
|
+
return diagnosticEnvelope([
|
|
2662
|
+
diagnosticItem("invalid-run-state", {
|
|
2663
|
+
checkedAt,
|
|
2664
|
+
authoritative: false,
|
|
2665
|
+
message: `Factory run state is invalid: ${error.message}`,
|
|
2666
|
+
evidence: { source: "run.json", run_dir: runDir, run_path: runFile, error: error.message }
|
|
2667
|
+
})
|
|
2668
|
+
], { checkedAt, authoritative: false });
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
function diagnoseRunObject(input, options = {}) {
|
|
2672
|
+
const checkedAt = options.checkedAt || timestamp(options.now);
|
|
2673
|
+
const runDir = options.runDir || null;
|
|
2674
|
+
const runFile = options.runFile || (runDir ? join5(runDir, "run.json") : null);
|
|
2675
|
+
const items = [];
|
|
2676
|
+
let run;
|
|
2677
|
+
try {
|
|
2678
|
+
run = validateRun(input);
|
|
2679
|
+
} catch (error) {
|
|
2680
|
+
return diagnosticEnvelope([
|
|
2681
|
+
diagnosticItem("invalid-run-state", {
|
|
2682
|
+
checkedAt,
|
|
2683
|
+
authoritative: false,
|
|
2684
|
+
message: `Factory run state is invalid: ${error.message}`,
|
|
2685
|
+
evidence: { source: "run.json", run_dir: runDir, run_path: runFile, error: error.message }
|
|
2686
|
+
})
|
|
2687
|
+
], { checkedAt, authoritative: false });
|
|
2688
|
+
}
|
|
2689
|
+
const invalidSidecar = runDir ? inspectSidecars(runDir, checkedAt, run) : null;
|
|
2690
|
+
if (invalidSidecar) {
|
|
2691
|
+
return diagnosticEnvelope([invalidSidecar], { checkedAt, authoritative: false });
|
|
2692
|
+
}
|
|
2693
|
+
const terminal = TERMINAL_STATUSES2.has(run.status);
|
|
2694
|
+
const authoritative = Boolean(runDir);
|
|
2695
|
+
const protectedGate = pendingProtectedGate(run);
|
|
2696
|
+
if (terminal) {
|
|
2697
|
+
items.push(terminalRunItem(run, { checkedAt, runDir, runFile, authoritative }));
|
|
2698
|
+
return diagnosticEnvelope(items, { checkedAt, authoritative });
|
|
2699
|
+
}
|
|
2700
|
+
if (protectedGate) {
|
|
2701
|
+
items.push(diagnosticItem("protected-gate", {
|
|
2702
|
+
checkedAt,
|
|
2703
|
+
authoritative,
|
|
2704
|
+
message: `Run is waiting at protected gate '${protectedGate}'.`,
|
|
2705
|
+
evidence: { source: "run.json", run_dir: runDir, run_path: runFile, gate: protectedGate }
|
|
2706
|
+
}));
|
|
2707
|
+
} else if (hasInFlightHeartbeatWork(run)) {
|
|
2708
|
+
const heartbeat = inspectHeartbeat(run, { ...options, checkedAt, runDir });
|
|
2709
|
+
if (heartbeat.invalid) {
|
|
2710
|
+
items.push(diagnosticItem("invalid-run-state", {
|
|
2711
|
+
checkedAt,
|
|
2712
|
+
authoritative: false,
|
|
2713
|
+
message: `Heartbeat state is invalid: ${heartbeat.error}`,
|
|
2714
|
+
evidence: { source: "heartbeat.json", run_dir: runDir, heartbeat_path: heartbeat.path, error: heartbeat.error }
|
|
2715
|
+
}));
|
|
2716
|
+
} else {
|
|
2717
|
+
if (heartbeat.missingProcess) items.push(heartbeat.missingProcess);
|
|
2718
|
+
if (heartbeat.stale) items.push(heartbeat.stale);
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
const missingWorktree = inspectWorktree(run, { ...options, checkedAt, runDir, authoritative });
|
|
2722
|
+
if (missingWorktree) items.push(missingWorktree);
|
|
2723
|
+
return diagnosticEnvelope(items, { checkedAt, authoritative });
|
|
2724
|
+
}
|
|
2725
|
+
function diagnosticEnvelope(items = [], options = {}) {
|
|
2726
|
+
const checkedAt = options.checkedAt || timestamp(options.now);
|
|
2727
|
+
const normalized = items.map((item) => normalizeDiagnosticItem(item, checkedAt));
|
|
2728
|
+
const aggregate = aggregateDiagnostics(normalized);
|
|
2729
|
+
const envelope = {
|
|
2730
|
+
schema_version: DIAGNOSTIC_SCHEMA_VERSION,
|
|
2731
|
+
checked_at: checkedAt,
|
|
2732
|
+
authoritative: Boolean(options.authoritative) && aggregate.classification !== "invalid",
|
|
2733
|
+
status: aggregate.status,
|
|
2734
|
+
severity: aggregate.severity,
|
|
2735
|
+
classification: aggregate.classification,
|
|
2736
|
+
summary: aggregate.summary,
|
|
2737
|
+
items: normalized
|
|
2738
|
+
};
|
|
2739
|
+
return plainDiagnosticProjection(projectDiagnosticData(envelope, {
|
|
2740
|
+
validatedIdentityPaths: validatedDiagnosticIdentityPaths(envelope)
|
|
2741
|
+
}));
|
|
2742
|
+
}
|
|
2743
|
+
function aggregateDiagnostics(items = []) {
|
|
2744
|
+
if (!items.length) {
|
|
2745
|
+
return { classification: "healthy", status: "ok", severity: "info", summary: "No diagnostics", primary: null };
|
|
2746
|
+
}
|
|
2747
|
+
const primary = items.map((item, index) => ({ item, index })).sort((left, right) => conditionOrder(left.item.condition) - conditionOrder(right.item.condition) || left.index - right.index)[0].item;
|
|
2748
|
+
return {
|
|
2749
|
+
classification: primary.classification,
|
|
2750
|
+
status: primary.status,
|
|
2751
|
+
severity: primary.severity,
|
|
2752
|
+
summary: primary.message,
|
|
2753
|
+
primary
|
|
2754
|
+
};
|
|
2755
|
+
}
|
|
2756
|
+
function diagnosticItem(condition, options = {}) {
|
|
2757
|
+
const definition = condition === "terminal-run" ? terminalDefinition(options.terminalStatus) : CONDITION_BY_NAME.get(condition);
|
|
2758
|
+
if (!definition) throw new Error(`unknown diagnostic condition: ${condition}`);
|
|
2759
|
+
const checkedAt = options.checkedAt || timestamp(options.now);
|
|
2760
|
+
return normalizeDiagnosticItem({
|
|
2761
|
+
condition,
|
|
2762
|
+
classification: definition.classification,
|
|
2763
|
+
severity: definition.severity,
|
|
2764
|
+
status: definition.status,
|
|
2765
|
+
message: options.message || definition.message,
|
|
2766
|
+
action: options.action || definition.action || terminalAction(options.terminalStatus),
|
|
2767
|
+
authoritative: Boolean(options.authoritative),
|
|
2768
|
+
checked_at: checkedAt,
|
|
2769
|
+
evidence: options.evidence || {}
|
|
2770
|
+
}, checkedAt);
|
|
2771
|
+
}
|
|
2772
|
+
function normalizeDiagnosticItem(item, checkedAt) {
|
|
2773
|
+
return {
|
|
2774
|
+
condition: item.condition,
|
|
2775
|
+
classification: item.classification,
|
|
2776
|
+
severity: item.severity,
|
|
2777
|
+
status: item.status,
|
|
2778
|
+
message: item.message,
|
|
2779
|
+
action: item.action,
|
|
2780
|
+
authoritative: Boolean(item.authoritative),
|
|
2781
|
+
checked_at: item.checked_at || checkedAt,
|
|
2782
|
+
evidence: item.evidence && typeof item.evidence === "object" && !Array.isArray(item.evidence) ? item.evidence : {}
|
|
2783
|
+
};
|
|
2784
|
+
}
|
|
2785
|
+
function terminalRunItem(run, options) {
|
|
2786
|
+
return diagnosticItem("terminal-run", {
|
|
2787
|
+
...options,
|
|
2788
|
+
terminalStatus: run.status,
|
|
2789
|
+
message: terminalMessage(run.status),
|
|
2790
|
+
evidence: { source: "run.json", run_dir: options.runDir, run_path: options.runFile, run_status: run.status }
|
|
2791
|
+
});
|
|
2792
|
+
}
|
|
2793
|
+
function inspectSidecars(runDir, checkedAt, run) {
|
|
2794
|
+
const checks = [
|
|
2795
|
+
{ path: join5(runDir, HEARTBEAT_FILE), source: HEARTBEAT_FILE, validator: validateHeartbeatState },
|
|
2796
|
+
{ path: join5(runDir, "factory.lock"), source: "factory.lock", validator: validateFactoryLock },
|
|
2797
|
+
{ path: join5(runDir, PROCESS_EVIDENCE_FILE), source: PROCESS_EVIDENCE_FILE, validator: (value) => validateProcessSidecar(value, { runDir, runId: run.run_id }), failClosed: true },
|
|
2798
|
+
{ path: join5(runDir, "plan", "slices.json"), source: "plan/slices.json", validator: (value) => validateSlicesPlan(value, { enforceDependencyDepth: !runSlicesMatchPlan(run, value) }) }
|
|
2799
|
+
];
|
|
2800
|
+
for (const check of checks) {
|
|
2801
|
+
if (!existsSync2(check.path)) continue;
|
|
2802
|
+
let parsed = null;
|
|
2803
|
+
try {
|
|
2804
|
+
parsed = JSON.parse(readFileSync(check.path, "utf8"));
|
|
2805
|
+
const sidecar = check.validator(parsed);
|
|
2806
|
+
if ((check.source === HEARTBEAT_FILE || check.source === "factory.lock") && sidecar.run_id !== run.run_id) {
|
|
2807
|
+
return diagnosticItem("invalid-run-state", {
|
|
2808
|
+
checkedAt,
|
|
2809
|
+
authoritative: false,
|
|
2810
|
+
message: `Factory sidecar state contradicts run.json: ${check.source}.run_id does not match run.run_id.`,
|
|
2811
|
+
evidence: {
|
|
2812
|
+
source: check.source,
|
|
2813
|
+
run_dir: runDir,
|
|
2814
|
+
path: check.path,
|
|
2815
|
+
error: `${check.source}.run_id does not match run.run_id`,
|
|
2816
|
+
run_id: run.run_id,
|
|
2817
|
+
sidecar_run_id: sidecar.run_id
|
|
2818
|
+
}
|
|
2819
|
+
});
|
|
2820
|
+
}
|
|
2821
|
+
} catch (error) {
|
|
2822
|
+
const failClosed = Boolean(check.failClosed);
|
|
2823
|
+
return diagnosticItem("invalid-run-state", {
|
|
2824
|
+
checkedAt,
|
|
2825
|
+
authoritative: false,
|
|
2826
|
+
message: failClosed ? `Factory process sidecar state is invalid; cancellation must fail closed: ${error.message}` : `Factory sidecar state is invalid: ${error.message}`,
|
|
2827
|
+
action: failClosed ? "Treat process evidence as untrusted; do not signal any process until the sidecar is repaired or manually verified." : void 0,
|
|
2828
|
+
evidence: {
|
|
2829
|
+
source: check.source,
|
|
2830
|
+
run_dir: runDir,
|
|
2831
|
+
path: check.path,
|
|
2832
|
+
error: error.message,
|
|
2833
|
+
...failClosed ? { fail_closed: true, sidecar_run_id: parsed?.run_id || null } : {}
|
|
2834
|
+
}
|
|
2835
|
+
});
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
return null;
|
|
2839
|
+
}
|
|
2840
|
+
function inspectHeartbeat(run, options) {
|
|
2841
|
+
const checkedAt = options.checkedAt;
|
|
2842
|
+
const runDir = options.runDir;
|
|
2843
|
+
const heartbeatPath = runDir ? join5(runDir, HEARTBEAT_FILE) : null;
|
|
2844
|
+
const nowMs = Date.parse(checkedAt);
|
|
2845
|
+
let heartbeat = null;
|
|
2846
|
+
let source = "run.json";
|
|
2847
|
+
if (heartbeatPath && existsSync2(heartbeatPath)) {
|
|
2848
|
+
try {
|
|
2849
|
+
heartbeat = validateHeartbeatState(JSON.parse(readFileSync(heartbeatPath, "utf8")));
|
|
2850
|
+
if (heartbeat.run_id !== run.run_id) {
|
|
2851
|
+
return { invalid: true, error: "heartbeat.run_id does not match run.run_id", path: heartbeatPath };
|
|
2852
|
+
}
|
|
2853
|
+
source = "heartbeat.json";
|
|
2854
|
+
} catch (error) {
|
|
2855
|
+
return { invalid: true, error: error.message, path: heartbeatPath };
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
const lastTickAt = heartbeat?.last_tick_at || run.heartbeat_at || null;
|
|
2859
|
+
const intervalMs = positiveInteger(heartbeat?.interval_ms) || DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
2860
|
+
const staleMs = Math.max(2 * intervalMs, MIN_STALE_HEARTBEAT_MS);
|
|
2861
|
+
const lastTickMs = Date.parse(lastTickAt || "");
|
|
2862
|
+
const ageMs = Number.isFinite(nowMs) && Number.isFinite(lastTickMs) ? Math.max(0, nowMs - lastTickMs) : null;
|
|
2863
|
+
const evidence = {
|
|
2864
|
+
source,
|
|
2865
|
+
liveness_only: true,
|
|
2866
|
+
run_dir: runDir,
|
|
2867
|
+
heartbeat_path: heartbeatPath,
|
|
2868
|
+
heartbeat_phase: heartbeat?.phase || null,
|
|
2869
|
+
pid: heartbeat?.pid || null,
|
|
2870
|
+
process_alive: null,
|
|
2871
|
+
last_tick_at: lastTickAt,
|
|
2872
|
+
interval_ms: intervalMs,
|
|
2873
|
+
stale_after: Number.isFinite(lastTickMs) ? new Date(lastTickMs + staleMs).toISOString() : null,
|
|
2874
|
+
age_ms: ageMs
|
|
2875
|
+
};
|
|
2876
|
+
const result2 = {};
|
|
2877
|
+
if (heartbeat) {
|
|
2878
|
+
const liveness = processLiveness(heartbeat.pid, options);
|
|
2879
|
+
evidence.process_alive = publicLivenessBoolean(liveness);
|
|
2880
|
+
if (liveness === "absent") result2.missingProcess = diagnosticItem("missing-heartbeat-process", {
|
|
2881
|
+
checkedAt,
|
|
2882
|
+
authoritative: false,
|
|
2883
|
+
evidence: { ...evidence }
|
|
2884
|
+
});
|
|
2885
|
+
}
|
|
2886
|
+
if (ageMs !== null && ageMs > staleMs) {
|
|
2887
|
+
result2.stale = diagnosticItem("stale-heartbeat", {
|
|
2888
|
+
checkedAt,
|
|
2889
|
+
authoritative: false,
|
|
2890
|
+
evidence
|
|
2891
|
+
});
|
|
2892
|
+
}
|
|
2893
|
+
return result2;
|
|
2894
|
+
}
|
|
2895
|
+
function inspectWorktree(run, options) {
|
|
2896
|
+
if (!stringValue2(run.worktree)) return null;
|
|
2897
|
+
const repoRoot = options.repoRoot || options.cwd || process.cwd();
|
|
2898
|
+
const worktree = isAbsolute3(run.worktree) ? run.worktree : resolve4(repoRoot, run.worktree);
|
|
2899
|
+
try {
|
|
2900
|
+
const physical = realpath(options, worktree);
|
|
2901
|
+
if (!statSync(physical).isDirectory()) throw new Error(`worktree is not a directory: ${worktree}`);
|
|
2902
|
+
return null;
|
|
2903
|
+
} catch (error) {
|
|
2904
|
+
return diagnosticItem("missing-worktree", {
|
|
2905
|
+
checkedAt: options.checkedAt,
|
|
2906
|
+
authoritative: options.authoritative,
|
|
2907
|
+
evidence: {
|
|
2908
|
+
source: "filesystem",
|
|
2909
|
+
run_dir: options.runDir,
|
|
2910
|
+
worktree,
|
|
2911
|
+
error: error.message
|
|
2912
|
+
}
|
|
2913
|
+
});
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
function processLiveness(pid, options = {}) {
|
|
2917
|
+
if (typeof options.processAliveFn === "function") {
|
|
2918
|
+
return probeLegacyBooleanLiveness(options.processAliveFn, pid);
|
|
2919
|
+
}
|
|
2920
|
+
return probeProcessLiveness(pid, options).status;
|
|
2921
|
+
}
|
|
2922
|
+
function validatedDiagnosticIdentityPaths(envelope) {
|
|
2923
|
+
const paths = [];
|
|
2924
|
+
for (const field of ["status", "severity", "classification"]) {
|
|
2925
|
+
if (DIAGNOSTIC_IDENTITY_FIELDS[field].has(envelope[field])) paths.push([field]);
|
|
2926
|
+
}
|
|
2927
|
+
for (let index = 0; index < envelope.items.length; index += 1) {
|
|
2928
|
+
const item = envelope.items[index];
|
|
2929
|
+
for (const field of ["condition", "status", "severity", "classification"]) {
|
|
2930
|
+
if (DIAGNOSTIC_IDENTITY_FIELDS[field].has(item[field])) paths.push(["items", String(index), field]);
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
return paths;
|
|
2934
|
+
}
|
|
2935
|
+
function plainDiagnosticProjection(value) {
|
|
2936
|
+
if (Array.isArray(value)) return value.map(plainDiagnosticProjection);
|
|
2937
|
+
if (value && typeof value === "object") {
|
|
2938
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, plainDiagnosticProjection(item)]));
|
|
2939
|
+
}
|
|
2940
|
+
return value;
|
|
2941
|
+
}
|
|
2942
|
+
function realpath(options, path) {
|
|
2943
|
+
if (typeof options.realpathFn === "function") return options.realpathFn(path);
|
|
2944
|
+
return physicalPath(path, "worktree", { mustExist: true });
|
|
2945
|
+
}
|
|
2946
|
+
function terminalDefinition(status) {
|
|
2947
|
+
return { ...CONDITION_BY_NAME.get("terminal-run"), ...TERMINAL_OVERRIDES[status] || TERMINAL_OVERRIDES.completed };
|
|
2948
|
+
}
|
|
2949
|
+
function terminalMessage(status) {
|
|
2950
|
+
if (status === "completed") return "Run completed.";
|
|
2951
|
+
if (status === "partial") return "Run ended with a partial result.";
|
|
2952
|
+
if (status === "blocked") return "Run ended blocked.";
|
|
2953
|
+
if (status === "needs-human") return "Run is terminal and needs human attention.";
|
|
2954
|
+
return "Run is terminal.";
|
|
2955
|
+
}
|
|
2956
|
+
function terminalAction(status) {
|
|
2957
|
+
if (status === "completed" || status === "partial") return "No liveness action is required.";
|
|
2958
|
+
if (status === "blocked") return "Inspect the terminal result and resolve the blocker before attempting follow-up work.";
|
|
2959
|
+
return "Inspect the terminal result and provide the required human input.";
|
|
2960
|
+
}
|
|
2961
|
+
function conditionOrder(condition) {
|
|
2962
|
+
return CONDITION_BY_NAME.get(condition)?.index ?? Number.MAX_SAFE_INTEGER;
|
|
2963
|
+
}
|
|
2964
|
+
function positiveInteger(value) {
|
|
2965
|
+
return Number.isInteger(value) && value > 0 ? value : null;
|
|
2966
|
+
}
|
|
2967
|
+
function stringValue2(value) {
|
|
2968
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
// src/tui-data.js
|
|
2972
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "coverage", ".cache", ".next"]);
|
|
2973
|
+
var MAX_SCAN_DIRS = 2e3;
|
|
2974
|
+
var MAX_DIAGNOSTIC_SUMMARY = 180;
|
|
2975
|
+
var ROOT_CACHE_TTL_MS = 3e4;
|
|
2976
|
+
var FAIL_CLOSED_CONDITIONS = /* @__PURE__ */ new Set(["invalid-run-state"]);
|
|
2977
|
+
var DIAGNOSTIC_IDENTITIES = Object.freeze({
|
|
2978
|
+
condition: new Set(DIAGNOSTIC_CONDITIONS),
|
|
2979
|
+
classification: new Set(DIAGNOSTIC_CLASSIFICATIONS),
|
|
2980
|
+
severity: new Set(DIAGNOSTIC_SEVERITIES),
|
|
2981
|
+
status: new Set(DIAGNOSTIC_STATUSES)
|
|
2982
|
+
});
|
|
2983
|
+
var rootCache = /* @__PURE__ */ new Map();
|
|
2984
|
+
function factoryRoots(api, options = {}) {
|
|
2985
|
+
const starts = tuiStartPaths(api);
|
|
2986
|
+
const cacheKey = starts.map((start) => resolve5(start)).join("\0");
|
|
2987
|
+
const now = Date.now();
|
|
2988
|
+
const cached = rootCache.get(cacheKey);
|
|
2989
|
+
if (!options.noCache && !options.notes && cached && cached.expiresAt > now && cached.roots.some((root3) => existsSync3(root3))) return cached.roots;
|
|
2990
|
+
const roots = /* @__PURE__ */ new Set();
|
|
2991
|
+
for (const start of starts) {
|
|
2992
|
+
for (const root3 of findFactoryRoots(start, options)) roots.add(root3);
|
|
2993
|
+
}
|
|
2994
|
+
const sorted = [...roots].sort();
|
|
2995
|
+
if (!options.noCache) rootCache.set(cacheKey, { expiresAt: now + ROOT_CACHE_TTL_MS, roots: sorted });
|
|
2996
|
+
return sorted;
|
|
2997
|
+
}
|
|
2998
|
+
function tuiStartPaths(api) {
|
|
2999
|
+
const path = api?.state?.path;
|
|
3000
|
+
const starts = [path?.worktree, path?.directory].filter(isUsablePath);
|
|
3001
|
+
if (starts.length) return [...new Set(starts)];
|
|
3002
|
+
const cwd = typeof process.cwd === "function" ? process.cwd() : null;
|
|
3003
|
+
return isUsablePath(cwd) ? [cwd] : [];
|
|
3004
|
+
}
|
|
3005
|
+
function isUsablePath(value) {
|
|
3006
|
+
if (typeof value !== "string" || value.trim().length === 0) return false;
|
|
3007
|
+
const resolved = resolve5(value);
|
|
3008
|
+
return dirname4(resolved) !== resolved;
|
|
3009
|
+
}
|
|
3010
|
+
function readRuns(roots, options = {}) {
|
|
3011
|
+
return roots.flatMap((root3) => readRootRuns(root3, options)).sort(compareRunRows);
|
|
3012
|
+
}
|
|
3013
|
+
function runHasNonOkDiagnostic(run) {
|
|
3014
|
+
return Boolean(run?.diagnostic_status && run.diagnostic_status !== "ok");
|
|
3015
|
+
}
|
|
3016
|
+
function selectVisibleRuns(runs) {
|
|
3017
|
+
const rows = Array.isArray(runs) ? runs : [];
|
|
3018
|
+
const list = rows.filter((run) => run?.status !== "completed" || runHasNonOkDiagnostic(run));
|
|
3019
|
+
const latestCompleted = rows.find((run) => run?.status === "completed");
|
|
3020
|
+
if (latestCompleted && !list.some((run) => run.run_id === latestCompleted.run_id)) return [...list, latestCompleted];
|
|
3021
|
+
return list;
|
|
3022
|
+
}
|
|
3023
|
+
function findFactoryRoots(start, options = {}) {
|
|
3024
|
+
if (!isUsablePath(start)) return [];
|
|
3025
|
+
const dir = resolve5(start);
|
|
3026
|
+
const roots = /* @__PURE__ */ new Set();
|
|
3027
|
+
const nearest = findNearestFactoryRoot(dir);
|
|
3028
|
+
if (nearest) roots.add(nearest);
|
|
3029
|
+
for (const root3 of findNestedFactoryRoots(dir, options)) roots.add(root3);
|
|
3030
|
+
if (!roots.size) roots.add(join6(dir, ".opencode", "factory"));
|
|
3031
|
+
return [...roots];
|
|
3032
|
+
}
|
|
3033
|
+
function findNearestFactoryRoot(start) {
|
|
3034
|
+
let dir = resolve5(start);
|
|
3035
|
+
while (true) {
|
|
3036
|
+
const candidate = join6(dir, ".opencode", "factory");
|
|
3037
|
+
if (existsSync3(candidate)) return candidate;
|
|
3038
|
+
const parent = dirname4(dir);
|
|
3039
|
+
if (parent === dir) return null;
|
|
3040
|
+
dir = parent;
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
function findNestedFactoryRoots(start, options = {}) {
|
|
3044
|
+
const roots = [];
|
|
3045
|
+
const queue = [resolve5(start)];
|
|
3046
|
+
const maxScanDirs = Number.isInteger(options.maxScanDirs) && options.maxScanDirs > 0 ? options.maxScanDirs : MAX_SCAN_DIRS;
|
|
3047
|
+
let scanned = 0;
|
|
3048
|
+
while (queue.length && scanned < maxScanDirs) {
|
|
3049
|
+
const dir = queue.shift();
|
|
3050
|
+
scanned += 1;
|
|
3051
|
+
const candidate = join6(dir, ".opencode", "factory");
|
|
3052
|
+
if (existsSync3(candidate)) roots.push(candidate);
|
|
3053
|
+
for (const child of safeReadDir(dir)) {
|
|
3054
|
+
if (SKIP_DIRS.has(child.name)) continue;
|
|
3055
|
+
const path = join6(dir, child.name);
|
|
3056
|
+
if (child.isDirectory()) queue.push(path);
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
if (queue.length && Array.isArray(options.notes)) {
|
|
3060
|
+
options.notes.push({ type: "scan-truncated", start: resolve5(start), scanned, remaining: queue.length, max_scan_dirs: maxScanDirs });
|
|
3061
|
+
}
|
|
3062
|
+
return roots;
|
|
3063
|
+
}
|
|
3064
|
+
function compareRunRows(a, b) {
|
|
3065
|
+
return Number(isInvalidRow(b)) - Number(isInvalidRow(a)) || String(b.updated_at || "").localeCompare(String(a.updated_at || "")) || String(b.run_id || "").localeCompare(String(a.run_id || ""));
|
|
3066
|
+
}
|
|
3067
|
+
function isInvalidRow(row) {
|
|
3068
|
+
return row?.status === "invalid" || row?.diagnostic_classification === "invalid";
|
|
3069
|
+
}
|
|
3070
|
+
function safeReadDir(dir) {
|
|
3071
|
+
try {
|
|
3072
|
+
return readdirSync(dir, { withFileTypes: true });
|
|
3073
|
+
} catch {
|
|
3074
|
+
return [];
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
function readRootRuns(root3, options = {}) {
|
|
3078
|
+
if (!root3 || !existsSync3(root3)) return [];
|
|
3079
|
+
const repoRoot = dirname4(dirname4(root3));
|
|
3080
|
+
return readdirSync(root3).flatMap((runID) => {
|
|
3081
|
+
const file = join6(root3, runID, "run.json");
|
|
3082
|
+
if (!existsSync3(file) || !statSync2(file).isFile()) return [];
|
|
3083
|
+
try {
|
|
3084
|
+
const run = JSON.parse(readFileSync2(file, "utf8"));
|
|
3085
|
+
const diagnostics = options.diagnostics === false ? healthyDiagnostics() : safeDiagnoseRunObject(run, file, { repoRoot });
|
|
3086
|
+
if (shouldUseFallbackRow(diagnostics)) return [fallbackRun(runID, file, diagnostics)];
|
|
3087
|
+
return [summarize(run, runID, file, diagnostics)];
|
|
3088
|
+
} catch (error) {
|
|
3089
|
+
const diagnostics = options.diagnostics === false ? parseErrorDiagnostics(file, error) : safeDiagnoseRunFile(file, { repoRoot });
|
|
3090
|
+
return [fallbackRun(runID, file, diagnostics)];
|
|
3091
|
+
}
|
|
3092
|
+
});
|
|
3093
|
+
}
|
|
3094
|
+
function parseErrorDiagnostics(file, error) {
|
|
3095
|
+
const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3096
|
+
return diagnosticEnvelope([
|
|
3097
|
+
diagnosticItem("invalid-run-state", {
|
|
3098
|
+
checkedAt,
|
|
3099
|
+
authoritative: false,
|
|
3100
|
+
message: `Factory run JSON could not be parsed: ${error.message}`,
|
|
3101
|
+
evidence: { source: "tui-data", run_path: file, error: error.message }
|
|
3102
|
+
})
|
|
3103
|
+
], { checkedAt, authoritative: false });
|
|
3104
|
+
}
|
|
3105
|
+
function summarize(run, fallbackID, file, diagnostics = healthyDiagnostics()) {
|
|
3106
|
+
return {
|
|
3107
|
+
run_id: projectFreeformText(run.run_id || fallbackID),
|
|
3108
|
+
status: projectFreeformText(run.status || "unknown"),
|
|
3109
|
+
mode: projectOptionalFreeformText(run.mode),
|
|
3110
|
+
gate: projectOptionalFreeformText(pendingGate(run)),
|
|
3111
|
+
branch: projectOptionalFreeformText(run.branch),
|
|
3112
|
+
pr_url: projectOptionalFreeformText(run.pr_url),
|
|
3113
|
+
review_tier: projectOptionalFreeformText(stringOrNull(run.review_tier)),
|
|
3114
|
+
review_tier_source: null,
|
|
3115
|
+
updated_at: projectOptionalFreeformText(run.updated_at),
|
|
3116
|
+
current: projectOptionalFreeformText(currentSummary(run)),
|
|
3117
|
+
steering: steeringSummary(run),
|
|
3118
|
+
cost: costSummary(run.cost_attribution),
|
|
3119
|
+
slices: sliceSummary(run),
|
|
3120
|
+
panel: projectOptionalFreeformText(panelSummary(run)),
|
|
3121
|
+
terminal_reason: projectOptionalFreeformText(run.terminal_result?.reason),
|
|
3122
|
+
file: projectFreeformText(file),
|
|
3123
|
+
run_dir: projectFreeformText(dirname4(file)),
|
|
3124
|
+
...diagnosticSummary(diagnostics)
|
|
3125
|
+
};
|
|
3126
|
+
}
|
|
3127
|
+
function fallbackRun(fallbackID, file, diagnostics) {
|
|
3128
|
+
return {
|
|
3129
|
+
run_id: projectFreeformText(fallbackID),
|
|
3130
|
+
status: "invalid",
|
|
3131
|
+
mode: null,
|
|
3132
|
+
gate: null,
|
|
3133
|
+
branch: null,
|
|
3134
|
+
pr_url: null,
|
|
3135
|
+
review_tier: null,
|
|
3136
|
+
review_tier_source: null,
|
|
3137
|
+
updated_at: null,
|
|
3138
|
+
current: null,
|
|
3139
|
+
steering: null,
|
|
3140
|
+
cost: null,
|
|
3141
|
+
slices: null,
|
|
3142
|
+
panel: null,
|
|
3143
|
+
terminal_reason: null,
|
|
3144
|
+
file: projectFreeformText(file),
|
|
3145
|
+
run_dir: projectFreeformText(dirname4(file)),
|
|
3146
|
+
...diagnosticSummary(diagnostics)
|
|
3147
|
+
};
|
|
3148
|
+
}
|
|
3149
|
+
function shouldUseFallbackRow(diagnostics) {
|
|
3150
|
+
return Array.isArray(diagnostics?.items) && diagnostics.items.some((item) => FAIL_CLOSED_CONDITIONS.has(item?.condition));
|
|
3151
|
+
}
|
|
3152
|
+
function safeDiagnoseRunFile(file, options) {
|
|
3153
|
+
try {
|
|
3154
|
+
return diagnoseRunFile(file, options);
|
|
3155
|
+
} catch (error) {
|
|
3156
|
+
const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3157
|
+
return diagnosticEnvelope([
|
|
3158
|
+
diagnosticItem("invalid-run-state", {
|
|
3159
|
+
checkedAt,
|
|
3160
|
+
authoritative: false,
|
|
3161
|
+
message: `Factory diagnostics failed: ${error.message}`,
|
|
3162
|
+
evidence: { source: "tui-data", run_path: file, error: error.message }
|
|
3163
|
+
})
|
|
3164
|
+
], { checkedAt, authoritative: false });
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
function safeDiagnoseRunObject(run, file, options) {
|
|
3168
|
+
try {
|
|
3169
|
+
return diagnoseRunObject(run, { ...options, runDir: dirname4(file), runFile: file });
|
|
3170
|
+
} catch (error) {
|
|
3171
|
+
const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3172
|
+
return diagnosticEnvelope([
|
|
3173
|
+
diagnosticItem("invalid-run-state", {
|
|
3174
|
+
checkedAt,
|
|
3175
|
+
authoritative: false,
|
|
3176
|
+
message: `Factory diagnostics failed: ${error.message}`,
|
|
3177
|
+
evidence: { source: "tui-data", run_path: file, error: error.message }
|
|
3178
|
+
})
|
|
3179
|
+
], { checkedAt, authoritative: false });
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
function diagnosticSummary(diagnostics) {
|
|
3183
|
+
const envelope = projectTuiDiagnosticData(diagnostics || healthyDiagnostics());
|
|
3184
|
+
return {
|
|
3185
|
+
diagnostics: envelope,
|
|
3186
|
+
diagnostic_status: stringOrDefault(envelope.status, "ok"),
|
|
3187
|
+
diagnostic_severity: stringOrDefault(envelope.severity, "info"),
|
|
3188
|
+
diagnostic_classification: stringOrDefault(envelope.classification, "healthy"),
|
|
3189
|
+
diagnostic_summary: truncateDiagnosticSummary(stringOrDefault(envelope.summary, "No diagnostics"))
|
|
3190
|
+
};
|
|
3191
|
+
}
|
|
3192
|
+
function projectTuiDiagnosticData(diagnostics) {
|
|
3193
|
+
return terminalSafeProjection(plainProjection(projectDiagnosticData(diagnostics, {
|
|
3194
|
+
validatedIdentityPaths: validatedDiagnosticIdentityPaths2(diagnostics)
|
|
3195
|
+
})));
|
|
3196
|
+
}
|
|
3197
|
+
function healthyDiagnostics() {
|
|
3198
|
+
return diagnosticEnvelope([], { authoritative: true });
|
|
3199
|
+
}
|
|
3200
|
+
function truncateDiagnosticSummary(value) {
|
|
3201
|
+
const text = String(value || "");
|
|
3202
|
+
if (text.length <= MAX_DIAGNOSTIC_SUMMARY) return text;
|
|
3203
|
+
return `${text.slice(0, MAX_DIAGNOSTIC_SUMMARY - 3)}...`;
|
|
3204
|
+
}
|
|
3205
|
+
function pendingGate(run) {
|
|
3206
|
+
for (const [name, gate] of Object.entries(run.gates || {})) {
|
|
3207
|
+
if (gate?.status === "pending") return name;
|
|
3208
|
+
}
|
|
3209
|
+
return null;
|
|
3210
|
+
}
|
|
3211
|
+
function sliceSummary(run) {
|
|
3212
|
+
const slices = Array.isArray(run.slices) ? run.slices : [];
|
|
3213
|
+
if (!slices.length) return null;
|
|
3214
|
+
return {
|
|
3215
|
+
merged: slices.filter((item) => item?.status === "merged").length,
|
|
3216
|
+
blocked: slices.filter((item) => item?.status === "blocked").length,
|
|
3217
|
+
total: slices.length
|
|
3218
|
+
};
|
|
3219
|
+
}
|
|
3220
|
+
function currentSummary(run) {
|
|
3221
|
+
const activeSlice = firstByStatus(run.slices, ["running", "review"]);
|
|
3222
|
+
if (activeSlice) return summarizeWorkItem(activeSlice.id, activeSlice.status, activeSlice.attempts);
|
|
3223
|
+
const activeStep = firstByStatus(run.steps, ["running", "review"]);
|
|
3224
|
+
if (activeStep) return summarizeWorkItem(activeStep.agent, activeStep.status, activeStep.attempts);
|
|
3225
|
+
const blockedSlice = firstByStatus(run.slices, ["blocked"]);
|
|
3226
|
+
if (blockedSlice) return summarizeWorkItem(blockedSlice.id, blockedSlice.status, blockedSlice.attempts);
|
|
3227
|
+
const step = firstByStatus(run.steps, ["blocked", "pending"]);
|
|
3228
|
+
if (step) return summarizeWorkItem(step.agent, step.status, step.attempts);
|
|
3229
|
+
const panel = inferredPrePrPanelSummary(run);
|
|
3230
|
+
if (panel) return panel;
|
|
3231
|
+
return null;
|
|
3232
|
+
}
|
|
3233
|
+
function steeringSummary(run) {
|
|
3234
|
+
const steering = run?.steering;
|
|
3235
|
+
if (!steering || typeof steering !== "object" || Array.isArray(steering)) return { pending: null, uncheckpointed: null, consumed_count: 0, latest_consumed: null, boundary: null, action_claim: null, last_action: null, pr_fence: null };
|
|
3236
|
+
const pending = steering.pending && typeof steering.pending === "object" && !Array.isArray(steering.pending) ? {
|
|
3237
|
+
id: projectOptionalFreeformText(stringOrNull(steering.pending.id)),
|
|
3238
|
+
ref: projectOptionalFreeformText(stringOrNull(steering.pending.ref)),
|
|
3239
|
+
hash: projectOptionalFreeformText(stringOrNull(steering.pending.hash)),
|
|
3240
|
+
message_chars: Number.isInteger(steering.pending.message_chars) ? steering.pending.message_chars : null,
|
|
3241
|
+
created_at: projectOptionalFreeformText(stringOrNull(steering.pending.created_at))
|
|
3242
|
+
} : null;
|
|
3243
|
+
const consumed = Array.isArray(steering.history) ? steering.history.filter((item) => item?.event === "consumed") : [];
|
|
3244
|
+
const latest = consumed[consumed.length - 1];
|
|
3245
|
+
const uncheckpointed = steering.uncheckpointed && typeof steering.uncheckpointed === "object" && !Array.isArray(steering.uncheckpointed) ? {
|
|
3246
|
+
id: projectOptionalFreeformText(stringOrNull(steering.uncheckpointed.id)),
|
|
3247
|
+
ref: projectOptionalFreeformText(stringOrNull(steering.uncheckpointed.ref)),
|
|
3248
|
+
hash: projectOptionalFreeformText(stringOrNull(steering.uncheckpointed.hash)),
|
|
3249
|
+
message_chars: Number.isInteger(steering.uncheckpointed.message_chars) ? steering.uncheckpointed.message_chars : null,
|
|
3250
|
+
created_at: projectOptionalFreeformText(stringOrNull(steering.uncheckpointed.created_at)),
|
|
3251
|
+
consumed_at: projectOptionalFreeformText(stringOrNull(steering.uncheckpointed.consumed_at))
|
|
3252
|
+
} : null;
|
|
3253
|
+
return {
|
|
3254
|
+
pending,
|
|
3255
|
+
uncheckpointed,
|
|
3256
|
+
consumed_count: consumed.length,
|
|
3257
|
+
latest_consumed: latest ? {
|
|
3258
|
+
ref: projectOptionalFreeformText(stringOrNull(latest.ref)),
|
|
3259
|
+
consumed_at: projectOptionalFreeformText(stringOrNull(latest.consumed_at))
|
|
3260
|
+
} : null,
|
|
3261
|
+
boundary: steering.boundary && typeof steering.boundary === "object" ? {
|
|
3262
|
+
kind: projectOptionalFreeformText(stringOrNull(steering.boundary.kind)),
|
|
3263
|
+
token: projectOptionalFreeformText(stringOrNull(steering.boundary.token)),
|
|
3264
|
+
generation: Number.isInteger(steering.boundary.generation) ? steering.boundary.generation : null,
|
|
3265
|
+
created_at: projectOptionalFreeformText(stringOrNull(steering.boundary.created_at))
|
|
3266
|
+
} : null,
|
|
3267
|
+
action_claim: steeringActionSummary(steering.action_claim),
|
|
3268
|
+
last_action: steeringActionSummary(steering.last_action),
|
|
3269
|
+
pr_fence: steering.pr_fence && typeof steering.pr_fence === "object" ? {
|
|
3270
|
+
token: projectOptionalFreeformText(stringOrNull(steering.pr_fence.token)),
|
|
3271
|
+
generation: Number.isInteger(steering.pr_fence.generation) ? steering.pr_fence.generation : null,
|
|
3272
|
+
created_at: projectOptionalFreeformText(stringOrNull(steering.pr_fence.created_at))
|
|
3273
|
+
} : null
|
|
3274
|
+
};
|
|
3275
|
+
}
|
|
3276
|
+
function steeringActionSummary(action) {
|
|
3277
|
+
if (!action || typeof action !== "object" || Array.isArray(action)) return null;
|
|
3278
|
+
return {
|
|
3279
|
+
kind: projectOptionalFreeformText(stringOrNull(action.kind)),
|
|
3280
|
+
token: projectOptionalFreeformText(stringOrNull(action.token)),
|
|
3281
|
+
generation: Number.isInteger(action.generation) ? action.generation : null,
|
|
3282
|
+
claimed_at: projectOptionalFreeformText(stringOrNull(action.claimed_at)),
|
|
3283
|
+
outcome: projectOptionalFreeformText(stringOrNull(action.outcome)),
|
|
3284
|
+
resolved_at: projectOptionalFreeformText(stringOrNull(action.resolved_at))
|
|
3285
|
+
};
|
|
3286
|
+
}
|
|
3287
|
+
function costSummary(costAttribution) {
|
|
3288
|
+
if (!costAttribution || typeof costAttribution !== "object" || Array.isArray(costAttribution)) return null;
|
|
3289
|
+
const summary = projectPublicStrings(publicCostAttributionSummary(costAttribution));
|
|
3290
|
+
return { ...summary, label: formatProjectedCostSummary(summary) };
|
|
3291
|
+
}
|
|
3292
|
+
function inferredPrePrPanelSummary(run) {
|
|
3293
|
+
if (run?.status !== "running" || pendingGate(run)) return null;
|
|
3294
|
+
const testAccepted = Array.isArray(run.steps) && run.steps.some((step) => step?.agent === "test-verifier" && step?.status === "accepted");
|
|
3295
|
+
if (!testAccepted) return null;
|
|
3296
|
+
const validatorVerdict = stringOrNull(run.validator?.verdict);
|
|
3297
|
+
const securityVerdict = stringOrNull(run.security_review?.verdict);
|
|
3298
|
+
if (!validatorVerdict && !securityVerdict) return "pre-PR panel running";
|
|
3299
|
+
if (validatorVerdict === "NO-GO" || securityVerdict === "BLOCK") return "panel remediation running";
|
|
3300
|
+
if (!validatorVerdict) return "implementation-validator running";
|
|
3301
|
+
if (!securityVerdict) return "security-reviewer running";
|
|
3302
|
+
if (!run.pr_url) return "PR pending";
|
|
3303
|
+
return null;
|
|
3304
|
+
}
|
|
3305
|
+
function firstByStatus(items, statuses) {
|
|
3306
|
+
if (!Array.isArray(items)) return null;
|
|
3307
|
+
for (const status of statuses) {
|
|
3308
|
+
const item = items.find((candidate) => candidate?.status === status);
|
|
3309
|
+
if (item) return item;
|
|
3310
|
+
}
|
|
3311
|
+
return null;
|
|
3312
|
+
}
|
|
3313
|
+
function summarizeWorkItem(name, status, attempts) {
|
|
3314
|
+
const label = projectOptionalFreeformData(stringOrNull(name));
|
|
3315
|
+
if (!label || !status) return null;
|
|
3316
|
+
const normalizedStatus = projectFreeformData(String(status));
|
|
3317
|
+
const attempt = Number.isInteger(attempts) && attempts > 0 ? ` a${attempts}` : "";
|
|
3318
|
+
return `${label} ${normalizedStatus}${attempt}`;
|
|
3319
|
+
}
|
|
3320
|
+
function panelSummary(run) {
|
|
3321
|
+
const validator = projectOptionalFreeformData(run.validator?.verdict);
|
|
3322
|
+
const security = projectOptionalFreeformData(run.security_review?.verdict);
|
|
3323
|
+
if (!validator && !security) return null;
|
|
3324
|
+
return [validator, security].filter(Boolean).join(" / ");
|
|
3325
|
+
}
|
|
3326
|
+
function validatedDiagnosticIdentityPaths2(value) {
|
|
3327
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return [];
|
|
3328
|
+
const paths = [];
|
|
3329
|
+
for (const field of ["status", "severity", "classification"]) {
|
|
3330
|
+
if (DIAGNOSTIC_IDENTITIES[field].has(value[field])) paths.push([field]);
|
|
3331
|
+
}
|
|
3332
|
+
if (!Array.isArray(value.items)) return paths;
|
|
3333
|
+
for (let index = 0; index < value.items.length; index += 1) {
|
|
3334
|
+
const item = value.items[index];
|
|
3335
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
|
3336
|
+
for (const field of ["condition", "status", "severity", "classification"]) {
|
|
3337
|
+
if (DIAGNOSTIC_IDENTITIES[field].has(item[field])) paths.push(["items", String(index), field]);
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
return paths;
|
|
3341
|
+
}
|
|
3342
|
+
function plainProjection(value) {
|
|
3343
|
+
if (Array.isArray(value)) return value.map(plainProjection);
|
|
3344
|
+
if (value && typeof value === "object") {
|
|
3345
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, plainProjection(item)]));
|
|
3346
|
+
}
|
|
3347
|
+
return value;
|
|
3348
|
+
}
|
|
3349
|
+
function terminalSafeProjection(value) {
|
|
3350
|
+
if (typeof value === "string") return projectFreeformText(value);
|
|
3351
|
+
if (Array.isArray(value)) return value.map(terminalSafeProjection);
|
|
3352
|
+
if (value && typeof value === "object") {
|
|
3353
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, terminalSafeProjection(item)]));
|
|
3354
|
+
}
|
|
3355
|
+
return value;
|
|
3356
|
+
}
|
|
3357
|
+
function projectPublicStrings(value) {
|
|
3358
|
+
if (typeof value === "string") return projectFreeformText(value);
|
|
3359
|
+
if (Array.isArray(value)) return value.map(projectPublicStrings);
|
|
3360
|
+
if (value && typeof value === "object") {
|
|
3361
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, projectPublicStrings(item)]));
|
|
3362
|
+
}
|
|
3363
|
+
return value;
|
|
3364
|
+
}
|
|
3365
|
+
function projectFreeformText(value) {
|
|
3366
|
+
return renderTerminalSegmentsOrFallback([freeformSegment(String(value))]);
|
|
3367
|
+
}
|
|
3368
|
+
function projectOptionalFreeformText(value) {
|
|
3369
|
+
return value === null || value === void 0 ? null : projectFreeformText(value);
|
|
3370
|
+
}
|
|
3371
|
+
function projectOptionalFreeformData(value) {
|
|
3372
|
+
return value === null || value === void 0 ? null : projectFreeformData(String(value));
|
|
3373
|
+
}
|
|
3374
|
+
function formatProjectedCostSummary(summary) {
|
|
3375
|
+
const parts = [`cost ${summary.status}`, `${summary.entry_count} ${summary.entry_count === 1 ? "entry" : "entries"}`];
|
|
3376
|
+
if (summary.total_tokens !== void 0) parts.push(`${summary.total_tokens} tokens`);
|
|
3377
|
+
else if (summary.input_tokens !== void 0 || summary.output_tokens !== void 0) {
|
|
3378
|
+
parts.push(`${summary.input_tokens ?? "?"}/${summary.output_tokens ?? "?"} tokens`);
|
|
3379
|
+
}
|
|
3380
|
+
if (summary.mixed_currency) parts.push("mixed currency");
|
|
3381
|
+
else if (summary.cost_total !== void 0) parts.push(`${formatCost(summary.cost_total)} ${summary.cost_currency || ""}`.trim());
|
|
3382
|
+
if (summary.missing.length > 0) parts.push(`missing ${summary.missing.join(",")}`);
|
|
3383
|
+
return parts.join(" \xB7 ");
|
|
3384
|
+
}
|
|
3385
|
+
function formatCost(value) {
|
|
3386
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(6).replace(/0+$/u, "").replace(/\.$/u, "");
|
|
3387
|
+
}
|
|
3388
|
+
function stringOrNull(value) {
|
|
3389
|
+
return typeof value === "string" ? value : null;
|
|
3390
|
+
}
|
|
3391
|
+
function stringOrDefault(value, fallback) {
|
|
3392
|
+
return typeof value === "string" ? value : fallback;
|
|
3393
|
+
}
|
|
3394
|
+
|
|
3395
|
+
// src/tui-rendering.js
|
|
3396
|
+
var RUN_STATUSES2 = /* @__PURE__ */ new Set(["running", "completed", "blocked", "partial", "needs-human", "invalid"]);
|
|
3397
|
+
var RUN_MODES2 = /* @__PURE__ */ new Set(["interactive", "headless", "autonomous"]);
|
|
3398
|
+
var DIAGNOSTIC_CLASSIFICATION_SET = new Set(DIAGNOSTIC_CLASSIFICATIONS);
|
|
3399
|
+
var DIAGNOSTIC_STATUS_SET = new Set(DIAGNOSTIC_STATUSES);
|
|
3400
|
+
var COST_STATUSES = /* @__PURE__ */ new Set(["available", "partial", "unavailable"]);
|
|
3401
|
+
var COST_CURRENCY_PATTERN2 = /^[A-Z]{3,12}$/u;
|
|
3402
|
+
function renderFreeformText(value, max) {
|
|
3403
|
+
return truncate(renderTerminalSegmentsOrFallback([freeformSegment(value)]), max);
|
|
3404
|
+
}
|
|
3405
|
+
function renderRunStatus(value) {
|
|
3406
|
+
return renderValidatedIdentity(value, RUN_STATUSES2);
|
|
3407
|
+
}
|
|
3408
|
+
function renderRunMode(value) {
|
|
3409
|
+
return renderValidatedIdentity(value, RUN_MODES2);
|
|
3410
|
+
}
|
|
3411
|
+
function renderDiagnosticLine(run, max = 42) {
|
|
3412
|
+
const segments = [];
|
|
3413
|
+
if (run?.diagnostic_classification) {
|
|
3414
|
+
segments.push(validatedIdentitySegment(run.diagnostic_classification, DIAGNOSTIC_CLASSIFICATION_SET));
|
|
3415
|
+
segments.push(TRUSTED_SEGMENTS.COLON_SPACE);
|
|
3416
|
+
}
|
|
3417
|
+
segments.push(freeformSegment(run?.diagnostic_summary || "Diagnostics require attention"));
|
|
3418
|
+
return truncate(renderTerminalSegmentsOrFallback(segments), max);
|
|
3419
|
+
}
|
|
3420
|
+
function renderRunTextFields(run) {
|
|
3421
|
+
const status = renderRunStatus(run?.status);
|
|
3422
|
+
const mode = run?.mode ? renderRunMode(run.mode) : null;
|
|
3423
|
+
const steering = renderSteeringLine(run?.steering);
|
|
3424
|
+
const slices = renderSliceLine(run?.slices);
|
|
3425
|
+
const diagnostic = renderDiagnosticLine(run);
|
|
3426
|
+
return {
|
|
3427
|
+
run_id: renderFreeformText(run?.run_id, 31),
|
|
3428
|
+
status_line: mode ? `${status} | ${mode}` : status,
|
|
3429
|
+
gate_line: run?.gate ? `gate: ${renderFreeformText(run.gate)}` : null,
|
|
3430
|
+
current_line: run?.current ? `current: ${renderFreeformText(run.current, 34)}` : null,
|
|
3431
|
+
steering_line: steering,
|
|
3432
|
+
slices_line: slices,
|
|
3433
|
+
cost_line: renderCostLine(run?.cost),
|
|
3434
|
+
panel_line: run?.panel ? `panel: ${renderFreeformText(run.panel)}` : null,
|
|
3435
|
+
pr_line: run?.pr_url ? `PR: ${renderFreeformText(run.pr_url, 34)}` : null,
|
|
3436
|
+
terminal_reason_line: run?.terminal_reason ? `reason: ${renderFreeformText(run.terminal_reason, 30)}` : null,
|
|
3437
|
+
diagnostic_line: `diagnostic: ${diagnostic}`,
|
|
3438
|
+
branch_line: run?.branch ? `branch: ${renderFreeformText(run.branch, 30)}` : null
|
|
3439
|
+
};
|
|
3440
|
+
}
|
|
3441
|
+
function renderHiddenRunsLine(value) {
|
|
3442
|
+
const count = Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
3443
|
+
return `+ ${renderTerminalSegmentsOrFallback([identitySegment(count)])} more runs`;
|
|
3444
|
+
}
|
|
3445
|
+
function renderSteeringLine(steering) {
|
|
3446
|
+
if (!steering || typeof steering !== "object") return null;
|
|
3447
|
+
if (steering.pending) {
|
|
3448
|
+
return `steering pending: ${renderFreeformText(steering.pending.ref || "pending", 34)}`;
|
|
3449
|
+
}
|
|
3450
|
+
if (steering.latest_consumed) {
|
|
3451
|
+
const count = Number.isSafeInteger(steering.consumed_count) && steering.consumed_count >= 0 ? steering.consumed_count : 0;
|
|
3452
|
+
const renderedCount = renderTerminalSegmentsOrFallback([identitySegment(count)]);
|
|
3453
|
+
const ref = renderFreeformText(steering.latest_consumed.ref || "consumed", 24);
|
|
3454
|
+
return `steering consumed: ${renderedCount} latest ${ref}`;
|
|
3455
|
+
}
|
|
3456
|
+
return null;
|
|
3457
|
+
}
|
|
3458
|
+
function renderSliceLine(slices) {
|
|
3459
|
+
if (!slices || typeof slices !== "object") return null;
|
|
3460
|
+
const merged = nonNegativeInteger(slices.merged);
|
|
3461
|
+
const total = nonNegativeInteger(slices.total);
|
|
3462
|
+
const blocked = nonNegativeInteger(slices.blocked);
|
|
3463
|
+
const base = `slices: ${renderNumber(merged)}/${renderNumber(total)}`;
|
|
3464
|
+
return blocked > 0 ? `${base} | blocked ${renderNumber(blocked)}` : base;
|
|
3465
|
+
}
|
|
3466
|
+
function renderNumber(value) {
|
|
3467
|
+
return renderTerminalSegmentsOrFallback([identitySegment(value)]);
|
|
3468
|
+
}
|
|
3469
|
+
function nonNegativeInteger(value) {
|
|
3470
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
3471
|
+
}
|
|
3472
|
+
function renderCostLine(cost) {
|
|
3473
|
+
if (!cost || typeof cost !== "object" || !COST_STATUSES.has(cost.status)) return null;
|
|
3474
|
+
const entryCount = nonNegativeInteger(cost.entry_count);
|
|
3475
|
+
const parts = [`cost ${cost.status}`, `${entryCount} ${entryCount === 1 ? "entry" : "entries"}`];
|
|
3476
|
+
if (Number.isFinite(cost.total_tokens) && cost.total_tokens >= 0) parts.push(`${cost.total_tokens} tokens`);
|
|
3477
|
+
else if (Number.isFinite(cost.input_tokens) && cost.input_tokens >= 0 || Number.isFinite(cost.output_tokens) && cost.output_tokens >= 0) {
|
|
3478
|
+
parts.push(`${validCostNumber(cost.input_tokens)}/${validCostNumber(cost.output_tokens)} tokens`);
|
|
3479
|
+
}
|
|
3480
|
+
if (cost.mixed_currency === true) parts.push("mixed currency");
|
|
3481
|
+
else if (Number.isFinite(cost.cost_total) && cost.cost_total >= 0) {
|
|
3482
|
+
const currency = typeof cost.cost_currency === "string" && COST_CURRENCY_PATTERN2.test(cost.cost_currency) ? cost.cost_currency : "";
|
|
3483
|
+
parts.push(`${formatCost2(cost.cost_total)} ${currency}`.trim());
|
|
3484
|
+
}
|
|
3485
|
+
if (Array.isArray(cost.missing) && cost.missing.length > 0) {
|
|
3486
|
+
parts.push(`missing ${cost.missing.map((item) => renderFreeformText(item)).join(",")}`);
|
|
3487
|
+
}
|
|
3488
|
+
return truncate(parts.join(" \xB7 "), 42);
|
|
3489
|
+
}
|
|
3490
|
+
function validCostNumber(value) {
|
|
3491
|
+
return Number.isFinite(value) && value >= 0 ? value : "?";
|
|
3492
|
+
}
|
|
3493
|
+
function formatCost2(value) {
|
|
3494
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(6).replace(/0+$/u, "").replace(/\.$/u, "");
|
|
3495
|
+
}
|
|
3496
|
+
function renderValidatedIdentity(value, allowed) {
|
|
3497
|
+
return renderTerminalSegmentsOrFallback([validatedIdentitySegment(value, allowed)]);
|
|
3498
|
+
}
|
|
3499
|
+
function validatedIdentitySegment(value, allowed) {
|
|
3500
|
+
return typeof value === "string" && allowed.has(value) ? identitySegment(value) : freeformSegment(value ?? "unknown");
|
|
3501
|
+
}
|
|
3502
|
+
function truncate(value, max) {
|
|
3503
|
+
if (!Number.isSafeInteger(max) || max < 0 || value.length <= max) return value;
|
|
3504
|
+
return `${value.slice(0, Math.max(0, max - 3))}...`;
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
// src/tui.jsx
|
|
3508
|
+
import { jsx, jsxs } from "@opentui/solid/jsx-runtime";
|
|
3509
|
+
var REFRESH_INTERVAL_MS = 5e3;
|
|
3510
|
+
var MAX_VISIBLE_RUNS = 3;
|
|
3511
|
+
var DEFAULT_THEME = {
|
|
3512
|
+
text: "white",
|
|
3513
|
+
textMuted: "gray",
|
|
3514
|
+
success: "green",
|
|
3515
|
+
error: "red",
|
|
3516
|
+
warning: "yellow",
|
|
3517
|
+
info: "cyan"
|
|
3518
|
+
};
|
|
3519
|
+
var runStore = null;
|
|
3520
|
+
function currentTheme(api) {
|
|
3521
|
+
const theme = api?.theme?.current;
|
|
3522
|
+
return theme && typeof theme === "object" ? { ...DEFAULT_THEME, ...theme } : DEFAULT_THEME;
|
|
3523
|
+
}
|
|
3524
|
+
function statusColor(theme, status) {
|
|
3525
|
+
if (status === "completed") return theme.success;
|
|
3526
|
+
if (status === "blocked" || status === "invalid") return theme.error;
|
|
3527
|
+
if (status === "needs-human" || status === "partial") return theme.warning;
|
|
3528
|
+
if (status === "running") return theme.info;
|
|
3529
|
+
return theme.textMuted;
|
|
3530
|
+
}
|
|
3531
|
+
function diagnosticColor(theme, status) {
|
|
3532
|
+
if (status === "error") return theme.error;
|
|
3533
|
+
if (status === "warning") return theme.warning;
|
|
3534
|
+
return theme.textMuted;
|
|
3535
|
+
}
|
|
3536
|
+
function scanRuns(api) {
|
|
3537
|
+
return readRuns(factoryRoots(api));
|
|
3538
|
+
}
|
|
3539
|
+
function runSnapshot(runs) {
|
|
3540
|
+
return JSON.stringify(runs.map((run) => ({
|
|
3541
|
+
run_id: run.run_id,
|
|
3542
|
+
status: run.status,
|
|
3543
|
+
mode: run.mode,
|
|
3544
|
+
gate: run.gate,
|
|
3545
|
+
current: run.current,
|
|
3546
|
+
steering: run.steering,
|
|
3547
|
+
cost: run.cost,
|
|
3548
|
+
slices: run.slices,
|
|
3549
|
+
panel: run.panel,
|
|
3550
|
+
pr_url: run.pr_url,
|
|
3551
|
+
terminal_reason: run.terminal_reason,
|
|
3552
|
+
branch: run.branch,
|
|
3553
|
+
diagnostic_status: run.diagnostic_status,
|
|
3554
|
+
diagnostic_classification: run.diagnostic_classification,
|
|
3555
|
+
diagnostic_summary: run.diagnostic_summary,
|
|
3556
|
+
updated_at: run.updated_at
|
|
3557
|
+
})));
|
|
3558
|
+
}
|
|
3559
|
+
function sharedRunStore(api) {
|
|
3560
|
+
if (!runStore) {
|
|
3561
|
+
const initialRuns = scanRuns(api);
|
|
3562
|
+
const [runs, setRuns] = createSignal(initialRuns, { equals: false });
|
|
3563
|
+
const [version, setVersion] = createSignal(1);
|
|
3564
|
+
runStore = { api, runs, setRuns, version, setVersion, snapshot: runSnapshot(initialRuns), refreshing: false };
|
|
3565
|
+
setInterval(() => refreshRunStore(), REFRESH_INTERVAL_MS);
|
|
3566
|
+
return runStore;
|
|
3567
|
+
}
|
|
3568
|
+
runStore.api = api;
|
|
3569
|
+
refreshRunStore();
|
|
3570
|
+
return runStore;
|
|
3571
|
+
}
|
|
3572
|
+
function refreshRunStore() {
|
|
3573
|
+
if (!runStore || runStore.refreshing) return;
|
|
3574
|
+
runStore.refreshing = true;
|
|
3575
|
+
let nextRuns;
|
|
3576
|
+
let nextSnapshot;
|
|
3577
|
+
try {
|
|
3578
|
+
nextRuns = scanRuns(runStore.api);
|
|
3579
|
+
nextSnapshot = runSnapshot(nextRuns);
|
|
3580
|
+
} finally {
|
|
3581
|
+
runStore.refreshing = false;
|
|
3582
|
+
}
|
|
3583
|
+
if (nextSnapshot === runStore.snapshot) return;
|
|
3584
|
+
runStore.snapshot = nextSnapshot;
|
|
3585
|
+
runStore.setRuns(nextRuns);
|
|
3586
|
+
runStore.setVersion((value) => value + 1);
|
|
3587
|
+
}
|
|
3588
|
+
function View(props) {
|
|
3589
|
+
const store = sharedRunStore(props.api);
|
|
3590
|
+
const runs = store.runs;
|
|
3591
|
+
const version = store.version;
|
|
3592
|
+
const theme = () => currentTheme(props.api);
|
|
3593
|
+
const visible = createMemo(() => runs().length > 0);
|
|
3594
|
+
const shown = createMemo(() => selectVisibleRuns(runs()));
|
|
3595
|
+
const visibleRuns = createMemo(() => shown().slice(0, MAX_VISIBLE_RUNS));
|
|
3596
|
+
const hiddenCount = createMemo(() => Math.max(0, runs().length - visibleRuns().length));
|
|
3597
|
+
return /* @__PURE__ */ jsxs("box", { flexDirection: "column", flexGrow: 1, flexShrink: 1, minHeight: 0, overflow: "hidden", children: [
|
|
3598
|
+
/* @__PURE__ */ jsx("text", { fg: theme().text, children: /* @__PURE__ */ jsx("b", { children: "Feature Factory" }) }),
|
|
3599
|
+
/* @__PURE__ */ jsx(Show, { when: visible(), fallback: /* @__PURE__ */ jsx("text", { fg: theme().textMuted, children: "No factory runs yet" }), children: /* @__PURE__ */ jsx(Show, { keyed: true, when: version(), children: () => /* @__PURE__ */ jsxs("box", { flexDirection: "column", flexGrow: 1, flexShrink: 1, minHeight: 0, overflow: "hidden", children: [
|
|
3600
|
+
/* @__PURE__ */ jsx(For, { each: visibleRuns(), children: (run) => {
|
|
3601
|
+
const rendered = renderRunTextFields(run);
|
|
3602
|
+
return /* @__PURE__ */ jsxs("box", { paddingTop: 1, children: [
|
|
3603
|
+
/* @__PURE__ */ jsxs("box", { flexDirection: "row", gap: 1, children: [
|
|
3604
|
+
/* @__PURE__ */ jsx("text", { fg: statusColor(theme(), run.status), children: "*" }),
|
|
3605
|
+
/* @__PURE__ */ jsx("text", { fg: theme().text, wrapMode: "none", children: rendered.run_id })
|
|
3606
|
+
] }),
|
|
3607
|
+
/* @__PURE__ */ jsx("text", { fg: theme().textMuted, children: rendered.status_line }),
|
|
3608
|
+
/* @__PURE__ */ jsx(Show, { when: run.gate, children: /* @__PURE__ */ jsx("text", { fg: theme().warning, children: rendered.gate_line }) }),
|
|
3609
|
+
/* @__PURE__ */ jsx(Show, { when: run.current, children: /* @__PURE__ */ jsx("text", { fg: theme().textMuted, children: rendered.current_line }) }),
|
|
3610
|
+
/* @__PURE__ */ jsx(Show, { when: rendered.steering_line, children: /* @__PURE__ */ jsx("text", { fg: theme().warning, children: rendered.steering_line }) }),
|
|
3611
|
+
/* @__PURE__ */ jsx(Show, { when: rendered.slices_line, children: /* @__PURE__ */ jsx("text", { fg: theme().textMuted, children: rendered.slices_line }) }),
|
|
3612
|
+
/* @__PURE__ */ jsx(Show, { when: rendered.cost_line, children: /* @__PURE__ */ jsx("text", { fg: theme().textMuted, children: rendered.cost_line }) }),
|
|
3613
|
+
/* @__PURE__ */ jsx(Show, { when: run.panel, children: /* @__PURE__ */ jsx("text", { fg: theme().textMuted, children: rendered.panel_line }) }),
|
|
3614
|
+
/* @__PURE__ */ jsx(Show, { when: run.pr_url, children: /* @__PURE__ */ jsx("text", { fg: theme().success, children: rendered.pr_line }) }),
|
|
3615
|
+
/* @__PURE__ */ jsx(Show, { when: run.terminal_reason, children: /* @__PURE__ */ jsx("text", { fg: theme().warning, children: rendered.terminal_reason_line }) }),
|
|
3616
|
+
/* @__PURE__ */ jsx(Show, { when: runHasNonOkDiagnostic(run), children: /* @__PURE__ */ jsx("text", { fg: diagnosticColor(theme(), run.diagnostic_status), children: rendered.diagnostic_line }) }),
|
|
3617
|
+
/* @__PURE__ */ jsx(Show, { when: run.branch, children: /* @__PURE__ */ jsx("text", { fg: theme().textMuted, children: rendered.branch_line }) })
|
|
3618
|
+
] });
|
|
3619
|
+
} }),
|
|
3620
|
+
/* @__PURE__ */ jsx(Show, { when: hiddenCount() > 0, children: /* @__PURE__ */ jsx("text", { fg: theme().textMuted, children: renderHiddenRunsLine(hiddenCount()) }) })
|
|
3621
|
+
] }) }) })
|
|
3622
|
+
] });
|
|
3623
|
+
}
|
|
3624
|
+
var plugin = {
|
|
3625
|
+
id: "opencode-feature-factory",
|
|
3626
|
+
async tui(api) {
|
|
3627
|
+
if (typeof api?.slots?.register !== "function") return;
|
|
3628
|
+
api.slots.register({
|
|
3629
|
+
order: 450,
|
|
3630
|
+
slots: {
|
|
3631
|
+
sidebar_content(_ctx, props) {
|
|
3632
|
+
return /* @__PURE__ */ jsx(View, { api, session_id: props?.session_id });
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
});
|
|
3636
|
+
}
|
|
3637
|
+
};
|
|
3638
|
+
var tui_default = plugin;
|
|
3639
|
+
export {
|
|
3640
|
+
tui_default as default
|
|
3641
|
+
};
|