loopforge 3.9.0 → 3.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/audit.d.ts +13 -1
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +18 -3
- package/dist/audit.js.map +1 -1
- package/dist/committed-round.d.ts +29 -32
- package/dist/committed-round.d.ts.map +1 -1
- package/dist/committed-round.js +164 -157
- package/dist/committed-round.js.map +1 -1
- package/dist/contract-items.d.ts +3 -2
- package/dist/contract-items.d.ts.map +1 -1
- package/dist/contract-items.js +18 -7
- package/dist/contract-items.js.map +1 -1
- package/dist/enforcement-gate.d.ts.map +1 -1
- package/dist/enforcement-gate.js +13 -61
- package/dist/enforcement-gate.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +16 -9
- package/dist/engine.js.map +1 -1
- package/dist/evidence-claims.d.ts +6 -5
- package/dist/evidence-claims.d.ts.map +1 -1
- package/dist/evidence-claims.js +21 -20
- package/dist/evidence-claims.js.map +1 -1
- package/dist/evidence-provider.d.ts +73 -42
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +437 -138
- package/dist/evidence-provider.js.map +1 -1
- package/dist/explain.d.ts +18 -3
- package/dist/explain.d.ts.map +1 -1
- package/dist/explain.js +26 -5
- package/dist/explain.js.map +1 -1
- package/dist/field-accounting.d.ts +29 -0
- package/dist/field-accounting.d.ts.map +1 -0
- package/dist/field-accounting.js +132 -0
- package/dist/field-accounting.js.map +1 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/loop-compiler.d.ts +5 -11
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +29 -86
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-store.d.ts +3 -0
- package/dist/loop-store.d.ts.map +1 -1
- package/dist/loop-store.js +21 -11
- package/dist/loop-store.js.map +1 -1
- package/dist/mcp/round-lifecycle.d.ts +26 -2
- package/dist/mcp/round-lifecycle.d.ts.map +1 -1
- package/dist/mcp/round-lifecycle.js +193 -25
- package/dist/mcp/round-lifecycle.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +17 -14
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +15 -1
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +34 -1
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/submission-boundary.d.ts +5 -2
- package/dist/mcp/submission-boundary.d.ts.map +1 -1
- package/dist/mcp/submission-boundary.js +38 -6
- package/dist/mcp/submission-boundary.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +73 -39
- package/dist/mcp/tools.js.map +1 -1
- package/dist/policy-metrics.d.ts +14 -0
- package/dist/policy-metrics.d.ts.map +1 -1
- package/dist/policy-metrics.js +53 -1
- package/dist/policy-metrics.js.map +1 -1
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +63 -11
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/protocol.d.ts +182 -11
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +4 -1
- package/dist/protocol.js.map +1 -1
- package/dist/replay.d.ts +3 -0
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +46 -7
- package/dist/replay.js.map +1 -1
- package/dist/round-contract.d.ts +5 -4
- package/dist/round-contract.d.ts.map +1 -1
- package/dist/round-contract.js +4 -4
- package/dist/round-contract.js.map +1 -1
- package/dist/round-coordinator.d.ts +2 -1
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +17 -9
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +4 -1
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +2 -0
- package/dist/round-driver.js.map +1 -1
- package/dist/round-facts.d.ts +3 -2
- package/dist/round-facts.d.ts.map +1 -1
- package/dist/round-facts.js +4 -4
- package/dist/round-facts.js.map +1 -1
- package/dist/round-transaction.d.ts +21 -10
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +81 -19
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +28 -10
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +522 -124
- package/dist/self-eval.js.map +1 -1
- package/dist/submission-diagnostics.d.ts +12 -0
- package/dist/submission-diagnostics.d.ts.map +1 -0
- package/dist/submission-diagnostics.js +12 -0
- package/dist/submission-diagnostics.js.map +1 -0
- package/dist/submission-guidance.d.ts +119 -0
- package/dist/submission-guidance.d.ts.map +1 -0
- package/dist/submission-guidance.js +766 -0
- package/dist/submission-guidance.js.map +1 -0
- package/dist/submission-receipt.d.ts +48 -0
- package/dist/submission-receipt.d.ts.map +1 -0
- package/dist/submission-receipt.js +336 -0
- package/dist/submission-receipt.js.map +1 -0
- package/dist/verification-gate.d.ts +10 -6
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +95 -87
- package/dist/verification-gate.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -1
- package/skills/perception/SKILL.md +66 -27
package/dist/self-eval.js
CHANGED
|
@@ -2,17 +2,82 @@
|
|
|
2
2
|
* The MCP boundary validates required fields before these helpers run.
|
|
3
3
|
*/
|
|
4
4
|
import { makeExecutionReport, makeSelfEvaluation, } from "./protocol.js";
|
|
5
|
+
import { retiredFieldDetail, submittedFieldPaths, unknownExecutionReportEntryFields, unknownTopLevelFields, } from "./submission-guidance.js";
|
|
6
|
+
import { DIAG_ALL_ENTRIES_DROPPED, DIAG_EMPTY_VALUE, } from "./submission-diagnostics.js";
|
|
7
|
+
const DIAGNOSTICS_LIMIT = 100;
|
|
8
|
+
class DiagnosticsCollector {
|
|
9
|
+
entries = [];
|
|
10
|
+
omitted = 0;
|
|
11
|
+
push(path, code, handling, detail) {
|
|
12
|
+
if (this.entries.length >= DIAGNOSTICS_LIMIT) {
|
|
13
|
+
this.omitted++;
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
this.entries.push({ path, code, handling, detail });
|
|
17
|
+
}
|
|
18
|
+
finish() {
|
|
19
|
+
if (this.omitted > 0) {
|
|
20
|
+
this.entries.push({
|
|
21
|
+
path: "diagnostics",
|
|
22
|
+
code: "diagnostics_truncated",
|
|
23
|
+
handling: "truncated",
|
|
24
|
+
detail: `${this.omitted} more diagnostic(s) omitted`,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return this.entries;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
5
30
|
// ── Raw parsing helpers ───────────────────────────────────────────────────
|
|
6
|
-
function boundedString(value, maxChars) {
|
|
7
|
-
|
|
31
|
+
function boundedString(value, maxChars, sink, path = "") {
|
|
32
|
+
if (value === undefined)
|
|
33
|
+
return undefined;
|
|
34
|
+
if (value === null) {
|
|
35
|
+
sink?.push(path, "invalid_value", "dropped", "expected a string; null was dropped");
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
if (typeof value !== "string") {
|
|
39
|
+
sink?.push(path, "invalid_value", "dropped", `expected a string; the value was dropped`);
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
if (value.length > maxChars) {
|
|
43
|
+
sink?.push(path, "truncated", "truncated", `longer than ${maxChars} chars; truncated to the limit`);
|
|
44
|
+
return value.slice(0, maxChars);
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
8
47
|
}
|
|
9
|
-
function boundedStringArray(value, maxItems, maxChars) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
48
|
+
function boundedStringArray(value, maxItems, maxChars, sink, path = "") {
|
|
49
|
+
if (value === undefined || value === null)
|
|
50
|
+
return [];
|
|
51
|
+
if (!Array.isArray(value)) {
|
|
52
|
+
sink?.push(path, "not_array", "dropped", `expected an array of strings; the value was dropped`);
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
const strings = value.filter((item) => typeof item === "string");
|
|
56
|
+
const invalid = value.length - strings.length;
|
|
57
|
+
if (invalid > 0) {
|
|
58
|
+
if (strings.length === 0) {
|
|
59
|
+
sink?.push(path, DIAG_ALL_ENTRIES_DROPPED, "dropped", `all ${invalid} entr${invalid === 1 ? "y was" : "ies were"} invalid; the value was dropped`);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
sink?.push(path, "invalid_entry", "dropped", `${invalid} non-string entr${invalid === 1 ? "y" : "ies"} dropped`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
let truncated = 0;
|
|
66
|
+
const bounded = strings.map((item) => {
|
|
67
|
+
if (item.length > maxChars) {
|
|
68
|
+
truncated++;
|
|
69
|
+
return item.slice(0, maxChars);
|
|
70
|
+
}
|
|
71
|
+
return item;
|
|
72
|
+
});
|
|
73
|
+
if (truncated > 0) {
|
|
74
|
+
sink?.push(path, "truncated", "truncated", `${truncated} entr${truncated === 1 ? "y was" : "ies were"} longer than ${maxChars} chars`);
|
|
75
|
+
}
|
|
76
|
+
if (bounded.length > maxItems) {
|
|
77
|
+
sink?.push(path, "too_many", "truncated", `${bounded.length - maxItems} entr${bounded.length - maxItems === 1 ? "y" : "ies"} over the ${maxItems}-item cap were dropped`);
|
|
78
|
+
return bounded.slice(0, maxItems);
|
|
79
|
+
}
|
|
80
|
+
return bounded;
|
|
16
81
|
}
|
|
17
82
|
/** v3.8.1: the emerged-subtask intake bound — ONE constant, because the
|
|
18
83
|
* sub-goal ids are derived by ORDINAL over this list. The strict submission
|
|
@@ -24,55 +89,105 @@ function boundedStringArray(value, maxItems, maxChars) {
|
|
|
24
89
|
export const EMERGED_LIMITS = { items: 50, chars: 500 };
|
|
25
90
|
/** THE emerged-list normalization. Both the boundary and the committed
|
|
26
91
|
* evaluation call this, so the two sets cannot disagree. */
|
|
27
|
-
export function boundedEmergedSubtasks(value) {
|
|
28
|
-
return boundedStringArray(value, EMERGED_LIMITS.items, EMERGED_LIMITS.chars);
|
|
92
|
+
export function boundedEmergedSubtasks(value, sink) {
|
|
93
|
+
return boundedStringArray(value, EMERGED_LIMITS.items, EMERGED_LIMITS.chars, sink, "emerged_subtasks");
|
|
29
94
|
}
|
|
30
|
-
function nonNegativeInteger(value) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
95
|
+
function nonNegativeInteger(value, sink, path = "") {
|
|
96
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
97
|
+
if (value !== undefined && value !== null) {
|
|
98
|
+
sink?.push(path, "invalid_value", "normalized", "expected a finite number; defaulted to 0");
|
|
99
|
+
}
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
const normalized = Math.max(0, Math.trunc(value));
|
|
103
|
+
if (normalized !== value) {
|
|
104
|
+
sink?.push(path, "normalized", "normalized", `expected a non-negative integer; normalized to ${normalized}`);
|
|
105
|
+
}
|
|
106
|
+
return normalized;
|
|
34
107
|
}
|
|
35
108
|
/** v2.12: Parse retroactive claims — {round ≥ 1, claim non-empty}, capped at
|
|
36
109
|
* 20 entries. Invalid entries are silently dropped (lenient parsing). */
|
|
37
|
-
function parseRetroactiveClaims(raw) {
|
|
38
|
-
if (
|
|
110
|
+
function parseRetroactiveClaims(raw, sink) {
|
|
111
|
+
if (raw === undefined || raw === null)
|
|
39
112
|
return [];
|
|
113
|
+
if (!Array.isArray(raw)) {
|
|
114
|
+
sink?.push("retroactiveClaims", "not_array", "dropped", "expected an array of { round, claim }; the value was dropped");
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
40
117
|
const claims = [];
|
|
118
|
+
let invalid = 0;
|
|
41
119
|
for (const item of raw) {
|
|
42
120
|
if (claims.length >= 20)
|
|
43
121
|
break;
|
|
44
|
-
if (typeof item !== "object" || item === null || Array.isArray(item))
|
|
122
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) {
|
|
123
|
+
invalid++;
|
|
45
124
|
continue;
|
|
125
|
+
}
|
|
46
126
|
const record = item;
|
|
47
127
|
if (typeof record.round !== "number" || !Number.isInteger(record.round) ||
|
|
48
128
|
record.round < 1 || typeof record.claim !== "string" ||
|
|
49
129
|
record.claim.trim().length === 0) {
|
|
130
|
+
invalid++;
|
|
50
131
|
continue;
|
|
51
132
|
}
|
|
52
|
-
|
|
133
|
+
const claim = record.claim.length > 500 ? record.claim.slice(0, 500) : record.claim;
|
|
134
|
+
if (claim !== record.claim) {
|
|
135
|
+
sink?.push("retroactiveClaims", "truncated", "truncated", "a claim was longer than 500 chars and was truncated");
|
|
136
|
+
}
|
|
137
|
+
claims.push({ round: record.round, claim });
|
|
138
|
+
}
|
|
139
|
+
if (invalid > 0) {
|
|
140
|
+
sink?.push("retroactiveClaims", claims.length === 0 ? DIAG_ALL_ENTRIES_DROPPED : "invalid_entry", "dropped", claims.length === 0
|
|
141
|
+
? `all ${invalid} entries were malformed; the value was dropped`
|
|
142
|
+
: `${invalid} malformed entr${invalid === 1 ? "y" : "ies"} dropped`);
|
|
143
|
+
}
|
|
144
|
+
if (raw.length > 20) {
|
|
145
|
+
sink?.push("retroactiveClaims", "too_many", "truncated", `only the first 20 of ${raw.length} entries are kept`);
|
|
53
146
|
}
|
|
54
147
|
return claims;
|
|
55
148
|
}
|
|
56
149
|
/** Parse ExecutionReport from a raw JSON object. */
|
|
57
|
-
export function parseExecutionReport(raw) {
|
|
58
|
-
if (
|
|
150
|
+
export function parseExecutionReport(raw, sink) {
|
|
151
|
+
if (raw === undefined || raw === null)
|
|
152
|
+
return undefined;
|
|
153
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
154
|
+
sink?.push("execution_report", "invalid_entry", "dropped", "expected an object; the value was dropped");
|
|
59
155
|
return undefined;
|
|
156
|
+
}
|
|
157
|
+
for (const key of unknownExecutionReportEntryFields(raw)) {
|
|
158
|
+
sink?.push(`execution_report.${key}`, "unknown_field", "dropped", "not a declared execution_report field; it was dropped");
|
|
159
|
+
}
|
|
60
160
|
const testResults = raw.tests_reported && typeof raw.tests_reported === "object" &&
|
|
61
161
|
!Array.isArray(raw.tests_reported)
|
|
62
162
|
? raw.tests_reported
|
|
63
163
|
: undefined;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
164
|
+
let tests = null;
|
|
165
|
+
if (raw.tests_reported !== undefined && raw.tests_reported !== null) {
|
|
166
|
+
if (!testResults) {
|
|
167
|
+
sink?.push("execution_report.tests_reported", "invalid_entry", "dropped", "expected { passed, failed, skipped }; the value was dropped");
|
|
168
|
+
}
|
|
169
|
+
else if (typeof testResults.passed !== "number" ||
|
|
170
|
+
!Number.isFinite(testResults.passed)) {
|
|
171
|
+
sink?.push("execution_report.tests_reported", "invalid_value", "dropped", "`passed` must be a finite number; the whole object was dropped");
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
for (const key of Object.keys(testResults)) {
|
|
175
|
+
if (key !== "passed" && key !== "failed" && key !== "skipped") {
|
|
176
|
+
sink?.push(`execution_report.tests_reported.${key}`, "unknown_field", "dropped", "not a declared test-count field; it was dropped");
|
|
177
|
+
}
|
|
72
178
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
179
|
+
tests = {
|
|
180
|
+
passed: nonNegativeInteger(testResults.passed, sink, "execution_report.tests_reported.passed"),
|
|
181
|
+
failed: nonNegativeInteger(testResults.failed, sink, "execution_report.tests_reported.failed"),
|
|
182
|
+
skipped: nonNegativeInteger(testResults.skipped, sink, "execution_report.tests_reported.skipped"),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return makeExecutionReport({
|
|
187
|
+
files_changed: boundedStringArray(raw.files_changed, 200, 500, sink, "execution_report.files_changed"),
|
|
188
|
+
tests_reported: tests,
|
|
189
|
+
criterion_claims: parseCriterionClaims(raw.criterion_claims, sink),
|
|
190
|
+
contract_item_claims: parseContractItemClaims(raw.contract_item_claims, sink),
|
|
76
191
|
});
|
|
77
192
|
}
|
|
78
193
|
/** v3.8: The criterion ids the agent claims met this round. */
|
|
@@ -90,63 +205,131 @@ export function claimedRemainingCriteria(report) {
|
|
|
90
205
|
/** v3.8: LENIENT parse of advisory criterion claims — unknown or malformed
|
|
91
206
|
* entries are dropped, never a rejection (the contract item layer is the
|
|
92
207
|
* strict one). */
|
|
93
|
-
function parseCriterionClaims(raw) {
|
|
94
|
-
if (
|
|
208
|
+
function parseCriterionClaims(raw, sink) {
|
|
209
|
+
if (raw === undefined || raw === null)
|
|
210
|
+
return [];
|
|
211
|
+
if (!Array.isArray(raw)) {
|
|
212
|
+
sink?.push("execution_report.criterion_claims", "not_array", "dropped", "expected an array of claims; the value was dropped");
|
|
95
213
|
return [];
|
|
214
|
+
}
|
|
96
215
|
const claims = [];
|
|
97
|
-
|
|
216
|
+
let invalid = 0;
|
|
217
|
+
for (let index = 0; index < raw.length; index++) {
|
|
98
218
|
if (claims.length >= 100)
|
|
99
219
|
break;
|
|
100
|
-
|
|
220
|
+
const item = raw[index];
|
|
221
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) {
|
|
222
|
+
invalid++;
|
|
101
223
|
continue;
|
|
224
|
+
}
|
|
102
225
|
const entry = item;
|
|
103
|
-
if (typeof entry.criterion_id !== "string" ||
|
|
104
|
-
|
|
105
|
-
|
|
226
|
+
if (typeof entry.criterion_id !== "string" ||
|
|
227
|
+
entry.criterion_id.trim().length === 0 ||
|
|
228
|
+
(entry.outcome !== "met" && entry.outcome !== "remaining")) {
|
|
229
|
+
invalid++;
|
|
106
230
|
continue;
|
|
231
|
+
}
|
|
232
|
+
const trimmed = entry.criterion_id.trim();
|
|
233
|
+
if (trimmed !== entry.criterion_id) {
|
|
234
|
+
sink?.push(`execution_report.criterion_claims[${index}].criterion_id`, "normalized", "normalized", "leading/trailing whitespace was trimmed");
|
|
235
|
+
}
|
|
236
|
+
if (trimmed.length > 500) {
|
|
237
|
+
sink?.push(`execution_report.criterion_claims[${index}].criterion_id`, "truncated", "truncated", "longer than 500 chars; truncated to the limit");
|
|
238
|
+
}
|
|
107
239
|
claims.push({
|
|
108
|
-
criterion_id:
|
|
240
|
+
criterion_id: trimmed.slice(0, 500),
|
|
109
241
|
outcome: entry.outcome,
|
|
110
242
|
});
|
|
111
243
|
}
|
|
244
|
+
if (invalid > 0) {
|
|
245
|
+
sink?.push("execution_report.criterion_claims", claims.length === 0 ? DIAG_ALL_ENTRIES_DROPPED : "invalid_entry", "dropped", claims.length === 0
|
|
246
|
+
? `all ${invalid} claims were malformed; the value was dropped (advisory)`
|
|
247
|
+
: `${invalid} malformed claim${invalid === 1 ? "" : "s"} dropped (advisory)`);
|
|
248
|
+
}
|
|
249
|
+
if (raw.length > 100) {
|
|
250
|
+
sink?.push("execution_report.criterion_claims", "too_many", "truncated", "only the first 100 claims are kept");
|
|
251
|
+
}
|
|
112
252
|
return claims;
|
|
113
253
|
}
|
|
114
254
|
/** v3.8: LENIENT parse of contract item claims. Referential validity is the
|
|
115
255
|
* strict pre-advance boundary in validateContractShape — this parser only
|
|
116
256
|
* normalizes what reaches the read model. */
|
|
117
|
-
function parseContractItemClaims(raw) {
|
|
118
|
-
if (
|
|
257
|
+
function parseContractItemClaims(raw, sink) {
|
|
258
|
+
if (raw === undefined || raw === null)
|
|
259
|
+
return [];
|
|
260
|
+
if (!Array.isArray(raw)) {
|
|
261
|
+
sink?.push("execution_report.contract_item_claims", "not_array", "dropped", "expected an array of claims; the value was dropped");
|
|
119
262
|
return [];
|
|
263
|
+
}
|
|
120
264
|
const claims = [];
|
|
121
|
-
|
|
265
|
+
let invalid = 0;
|
|
266
|
+
for (let index = 0; index < raw.length; index++) {
|
|
122
267
|
if (claims.length >= 20)
|
|
123
268
|
break;
|
|
124
|
-
|
|
269
|
+
const item = raw[index];
|
|
270
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) {
|
|
271
|
+
invalid++;
|
|
125
272
|
continue;
|
|
273
|
+
}
|
|
126
274
|
const entry = item;
|
|
127
|
-
if (typeof entry.item_id !== "string"
|
|
128
|
-
|
|
129
|
-
|
|
275
|
+
if (typeof entry.item_id !== "string" ||
|
|
276
|
+
(entry.outcome !== "met" && entry.outcome !== "remaining")) {
|
|
277
|
+
invalid++;
|
|
130
278
|
continue;
|
|
279
|
+
}
|
|
280
|
+
const trimmed = entry.item_id.trim();
|
|
281
|
+
if (trimmed !== entry.item_id) {
|
|
282
|
+
sink?.push(`execution_report.contract_item_claims[${index}].item_id`, "normalized", "normalized", "leading/trailing whitespace was trimmed");
|
|
283
|
+
}
|
|
284
|
+
if (trimmed.length > 64) {
|
|
285
|
+
sink?.push(`execution_report.contract_item_claims[${index}].item_id`, "truncated", "truncated", "longer than 64 chars; truncated to the limit");
|
|
286
|
+
}
|
|
131
287
|
claims.push({
|
|
132
|
-
item_id:
|
|
288
|
+
item_id: trimmed.slice(0, 64),
|
|
133
289
|
outcome: entry.outcome,
|
|
134
290
|
});
|
|
135
291
|
}
|
|
292
|
+
if (invalid > 0) {
|
|
293
|
+
sink?.push("execution_report.contract_item_claims", claims.length === 0 ? DIAG_ALL_ENTRIES_DROPPED : "invalid_entry", "dropped", claims.length === 0
|
|
294
|
+
? `all ${invalid} claims were malformed; the value was dropped`
|
|
295
|
+
: `${invalid} malformed claim${invalid === 1 ? "" : "s"} dropped`);
|
|
296
|
+
}
|
|
297
|
+
if (raw.length > 20) {
|
|
298
|
+
sink?.push("execution_report.contract_item_claims", "too_many", "truncated", "only the first 20 claims are kept");
|
|
299
|
+
}
|
|
136
300
|
return claims;
|
|
137
301
|
}
|
|
138
302
|
/** Parse CriterionRevision[] from a raw JSON array. */
|
|
139
|
-
export function parseCriterionRevisions(raw) {
|
|
140
|
-
if (
|
|
303
|
+
export function parseCriterionRevisions(raw, sink) {
|
|
304
|
+
if (raw === undefined || raw === null)
|
|
141
305
|
return [];
|
|
142
|
-
|
|
143
|
-
|
|
306
|
+
if (!Array.isArray(raw)) {
|
|
307
|
+
sink?.push("revised_success_criteria", "not_array", "dropped", "expected an array of { old, new }; the value was dropped");
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
310
|
+
const valid = raw.filter((v) => typeof v === "object" && v !== null &&
|
|
144
311
|
typeof v.old === "string" &&
|
|
145
|
-
typeof v.new === "string")
|
|
146
|
-
|
|
147
|
-
|
|
312
|
+
typeof v.new === "string");
|
|
313
|
+
const invalid = raw.length - valid.length;
|
|
314
|
+
if (invalid > 0) {
|
|
315
|
+
sink?.push("revised_success_criteria", valid.length === 0 ? DIAG_ALL_ENTRIES_DROPPED : "invalid_entry", "dropped", valid.length === 0
|
|
316
|
+
? `all ${invalid} entries lacked string old/new; the value was dropped`
|
|
317
|
+
: `${invalid} entr${invalid === 1 ? "y" : "ies"} without string old/new dropped`);
|
|
318
|
+
}
|
|
319
|
+
if (valid.length > 20) {
|
|
320
|
+
sink?.push("revised_success_criteria", "too_many", "truncated", "only the first 20 revisions are kept");
|
|
321
|
+
}
|
|
322
|
+
return valid.slice(0, 20).map((v, index) => {
|
|
148
323
|
const r = v;
|
|
149
|
-
|
|
324
|
+
for (const key of ["old", "new"]) {
|
|
325
|
+
if (r[key].length > 500) {
|
|
326
|
+
sink?.push(`revised_success_criteria[${index}].${key}`, "truncated", "truncated", "longer than 500 chars; truncated to the limit");
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
old: r.old.slice(0, 500),
|
|
331
|
+
new: r.new.slice(0, 500),
|
|
332
|
+
};
|
|
150
333
|
});
|
|
151
334
|
}
|
|
152
335
|
/** Parse SubGoalUpdate[] from a raw JSON array.
|
|
@@ -154,24 +337,55 @@ export function parseCriterionRevisions(raw) {
|
|
|
154
337
|
* (20 entries, id ≤ 64, note ≤ 300). Referential validity — unknown IDs
|
|
155
338
|
* and illegal migrations — is NOT checked here: that needs the committed
|
|
156
339
|
* sub-goal set and runs as a pre-advance validation (evaluation_invalid). */
|
|
157
|
-
export function parseSubGoalUpdates(raw) {
|
|
158
|
-
if (
|
|
340
|
+
export function parseSubGoalUpdates(raw, sink) {
|
|
341
|
+
if (raw === undefined || raw === null)
|
|
342
|
+
return [];
|
|
343
|
+
if (!Array.isArray(raw)) {
|
|
344
|
+
sink?.push("subgoal_updates", "not_array", "dropped", "expected an array of { id, status }; the value was dropped");
|
|
159
345
|
return [];
|
|
160
|
-
|
|
161
|
-
|
|
346
|
+
}
|
|
347
|
+
const valid = raw.filter((v) => typeof v === "object" && v !== null &&
|
|
162
348
|
typeof v.id === "string" &&
|
|
163
349
|
(v.status === "in_progress" ||
|
|
164
350
|
v.status === "done" ||
|
|
165
351
|
v.status === "blocked" ||
|
|
166
|
-
v.status === "canceled"))
|
|
167
|
-
|
|
168
|
-
|
|
352
|
+
v.status === "canceled"));
|
|
353
|
+
const invalid = raw.length - valid.length;
|
|
354
|
+
if (invalid > 0) {
|
|
355
|
+
sink?.push("subgoal_updates", valid.length === 0 ? DIAG_ALL_ENTRIES_DROPPED : "invalid_entry", "dropped", valid.length === 0
|
|
356
|
+
? `all ${invalid} transitions were malformed; the value was dropped`
|
|
357
|
+
: `${invalid} malformed transition${invalid === 1 ? "" : "s"} dropped`);
|
|
358
|
+
}
|
|
359
|
+
if (valid.length > 20) {
|
|
360
|
+
sink?.push("subgoal_updates", "too_many", "truncated", "only the first 20 transitions are kept");
|
|
361
|
+
}
|
|
362
|
+
return valid.slice(0, 20).map((u, index) => {
|
|
363
|
+
const rawId = u.id;
|
|
364
|
+
const trimmedId = rawId.trim();
|
|
365
|
+
if (trimmedId !== rawId) {
|
|
366
|
+
sink?.push(`subgoal_updates[${index}].id`, "normalized", "normalized", "leading/trailing whitespace was trimmed");
|
|
367
|
+
}
|
|
368
|
+
if (trimmedId.length > 64) {
|
|
369
|
+
sink?.push(`subgoal_updates[${index}].id`, "truncated", "truncated", "longer than 64 chars; truncated to the limit");
|
|
370
|
+
}
|
|
169
371
|
const update = {
|
|
170
|
-
id:
|
|
372
|
+
id: trimmedId.slice(0, 64),
|
|
171
373
|
status: u.status,
|
|
172
374
|
};
|
|
173
|
-
if (
|
|
174
|
-
|
|
375
|
+
if (u.note !== undefined) {
|
|
376
|
+
if (typeof u.note !== "string") {
|
|
377
|
+
sink?.push(`subgoal_updates[${index}].note`, "invalid_value", "dropped", "expected a string; the note was dropped");
|
|
378
|
+
}
|
|
379
|
+
else if (u.note.trim().length === 0) {
|
|
380
|
+
sink?.push(`subgoal_updates[${index}].note`, DIAG_EMPTY_VALUE, "dropped", "empty note was dropped");
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
const note = u.note.trim();
|
|
384
|
+
if (note.length > 300) {
|
|
385
|
+
sink?.push(`subgoal_updates[${index}].note`, "truncated", "truncated", "a note was longer than 300 chars and was truncated");
|
|
386
|
+
}
|
|
387
|
+
update.note = note.slice(0, 300);
|
|
388
|
+
}
|
|
175
389
|
}
|
|
176
390
|
return update;
|
|
177
391
|
});
|
|
@@ -181,31 +395,59 @@ export function parseSubGoalUpdates(raw) {
|
|
|
181
395
|
* is dropped (not derived, not defaulted) — the array stays informational:
|
|
182
396
|
* absent/empty arrays are accepted unchanged, so a round without any
|
|
183
397
|
* delegation can never be rejected over worker fields. */
|
|
184
|
-
export function parseWorkerResults(raw) {
|
|
185
|
-
if (
|
|
398
|
+
export function parseWorkerResults(raw, sink) {
|
|
399
|
+
if (raw === undefined || raw === null)
|
|
186
400
|
return [];
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
401
|
+
if (!Array.isArray(raw)) {
|
|
402
|
+
sink?.push("worker_results", "not_array", "dropped", "expected an array of worker reports; the value was dropped");
|
|
403
|
+
return [];
|
|
404
|
+
}
|
|
405
|
+
const valid = raw.filter((v) => {
|
|
406
|
+
if (typeof v !== "object" || v === null || Array.isArray(v))
|
|
190
407
|
return false;
|
|
191
408
|
const w = v;
|
|
192
409
|
return typeof w.agentId === "string" &&
|
|
193
410
|
typeof w.subTask === "string" &&
|
|
194
411
|
typeof w.resultSummary === "string" &&
|
|
195
412
|
(w.outcome === "success" || w.outcome === "partial" || w.outcome === "failed");
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
|
|
413
|
+
});
|
|
414
|
+
const invalid = raw.length - valid.length;
|
|
415
|
+
if (invalid > 0) {
|
|
416
|
+
sink?.push("worker_results", valid.length === 0 ? DIAG_ALL_ENTRIES_DROPPED : "invalid_entry", "dropped", valid.length === 0
|
|
417
|
+
? `all ${invalid} reports were malformed; the value was dropped`
|
|
418
|
+
: `${invalid} report${invalid === 1 ? "" : "s"} missing a string agentId/subTask/resultSummary or a valid outcome dropped`);
|
|
419
|
+
}
|
|
420
|
+
if (valid.length > 20) {
|
|
421
|
+
sink?.push("worker_results", "too_many", "truncated", "only the first 20 worker reports are kept");
|
|
422
|
+
}
|
|
423
|
+
return valid.slice(0, 20).map((v, index) => {
|
|
199
424
|
const w = v;
|
|
425
|
+
for (const key of Object.keys(w)) {
|
|
426
|
+
if (!["agentId", "subAgentType", "subTask", "resultSummary", "outcome",
|
|
427
|
+
"discoveredConstraints"].includes(key)) {
|
|
428
|
+
sink?.push(`worker_results[${index}].${key}`, "unknown_field", "dropped", "not a declared worker-result field; it was dropped");
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (w.subAgentType !== undefined && typeof w.subAgentType !== "string") {
|
|
432
|
+
sink?.push(`worker_results[${index}].subAgentType`, "invalid_value", "normalized", "expected a string; defaulted to general-purpose");
|
|
433
|
+
}
|
|
434
|
+
for (const [key, cap] of [
|
|
435
|
+
["agentId", 200], ["subAgentType", 100], ["subTask", 500], ["resultSummary", 1000],
|
|
436
|
+
]) {
|
|
437
|
+
const value = w[key];
|
|
438
|
+
if (typeof value === "string" && value.length > cap) {
|
|
439
|
+
sink?.push(`worker_results[${index}].${key}`, "truncated", "truncated", `longer than ${cap} chars; truncated to the limit`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
200
442
|
return {
|
|
201
|
-
agentId: w.agentId.slice(0, 200),
|
|
443
|
+
agentId: (w.agentId.slice(0, 200)),
|
|
202
444
|
subAgentType: typeof w.subAgentType === "string"
|
|
203
445
|
? w.subAgentType.slice(0, 100)
|
|
204
446
|
: "general-purpose",
|
|
205
|
-
subTask: w.subTask.slice(0, 500),
|
|
206
|
-
resultSummary: w.resultSummary.slice(0, 1000),
|
|
447
|
+
subTask: (w.subTask.slice(0, 500)),
|
|
448
|
+
resultSummary: (w.resultSummary.slice(0, 1000)),
|
|
207
449
|
outcome: w.outcome,
|
|
208
|
-
discoveredConstraints: boundedStringArray(w.discoveredConstraints, 50, 500),
|
|
450
|
+
discoveredConstraints: boundedStringArray(w.discoveredConstraints, 50, 500, sink, `worker_results[${index}].discoveredConstraints`),
|
|
209
451
|
};
|
|
210
452
|
});
|
|
211
453
|
}
|
|
@@ -620,31 +862,86 @@ export function validateContractShape(raw, context) {
|
|
|
620
862
|
return errors;
|
|
621
863
|
}
|
|
622
864
|
/** Build a SelfEvaluation from a parsed JSON object.
|
|
623
|
-
* Lenient parsing: missing optional fields get sensible defaults.
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
const
|
|
629
|
-
const
|
|
865
|
+
* Lenient parsing: missing optional fields get sensible defaults. When a
|
|
866
|
+
* diagnostic sink is supplied, every drop/truncation/normalization is
|
|
867
|
+
* reported; without one (the engine's historical payload parse) the values
|
|
868
|
+
* are normalized as before. */
|
|
869
|
+
export function buildSelfEvaluation(raw, sink) {
|
|
870
|
+
const executionReport = parseExecutionReport(raw.execution_report, sink);
|
|
871
|
+
const retractedConstraints = boundedStringArray(raw.retracted_constraints, 50, 500, sink, "retracted_constraints");
|
|
872
|
+
const revisedCriteria = parseCriterionRevisions(raw.revised_success_criteria, sink);
|
|
873
|
+
const wrongAssumptions = boundedStringArray(raw.wrong_assumptions, 50, 500, sink, "wrong_assumptions");
|
|
874
|
+
const workerResults = parseWorkerResults(raw.worker_results, sink);
|
|
875
|
+
if (raw.success !== undefined && typeof raw.success !== "boolean") {
|
|
876
|
+
sink?.push("success", "invalid_value", "dropped", "expected a boolean; normalized to false (core validation rejects it)");
|
|
877
|
+
}
|
|
878
|
+
if (raw.output_summary !== undefined && typeof raw.output_summary !== "string") {
|
|
879
|
+
sink?.push("output_summary", "invalid_value", "dropped", "expected a string; normalized to the empty string (core validation rejects it)");
|
|
880
|
+
}
|
|
881
|
+
let constraintViolations = [];
|
|
882
|
+
if (raw.constraint_violations !== undefined) {
|
|
883
|
+
if (!Array.isArray(raw.constraint_violations)) {
|
|
884
|
+
sink?.push("constraint_violations", "not_array", "dropped", "expected an array of strings; normalized to [] (core validation rejects it)");
|
|
885
|
+
}
|
|
886
|
+
else {
|
|
887
|
+
constraintViolations = raw.constraint_violations
|
|
888
|
+
.filter((v) => typeof v === "string");
|
|
889
|
+
const invalid = raw.constraint_violations.length - constraintViolations.length;
|
|
890
|
+
if (invalid > 0) {
|
|
891
|
+
sink?.push("constraint_violations", constraintViolations.length === 0 ? DIAG_ALL_ENTRIES_DROPPED : "invalid_entry", "dropped", constraintViolations.length === 0
|
|
892
|
+
? `all ${invalid} entries were non-strings; the value was dropped`
|
|
893
|
+
: `${invalid} non-string entr${invalid === 1 ? "y" : "ies"} dropped`);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
if (raw.should_continue !== undefined && typeof raw.should_continue !== "boolean") {
|
|
898
|
+
sink?.push("should_continue", "invalid_value", "dropped", "expected a boolean; normalized to true (core validation rejects it)");
|
|
899
|
+
}
|
|
900
|
+
if (raw.stop_reason !== undefined &&
|
|
901
|
+
raw.stop_reason !== "gave_up" && raw.stop_reason !== "blocked" &&
|
|
902
|
+
raw.stop_reason !== "needs_human_input") {
|
|
903
|
+
sink?.push("stop_reason", "invalid_enum", "dropped", "expected gave_up | blocked | needs_human_input; the value was dropped");
|
|
904
|
+
}
|
|
905
|
+
if (raw.outcome !== undefined &&
|
|
906
|
+
raw.outcome !== "success" && raw.outcome !== "partial" &&
|
|
907
|
+
raw.outcome !== "failed" && raw.outcome !== "blocked") {
|
|
908
|
+
sink?.push("outcome", "invalid_enum", "dropped", "expected success | partial | failed | blocked; the value was dropped");
|
|
909
|
+
}
|
|
910
|
+
if (raw.compression_checkpoint !== undefined &&
|
|
911
|
+
typeof raw.compression_checkpoint !== "boolean") {
|
|
912
|
+
sink?.push("compression_checkpoint", "invalid_value", "dropped", "expected a boolean; normalized to false");
|
|
913
|
+
}
|
|
914
|
+
if (raw.blocker !== undefined) {
|
|
915
|
+
if (typeof raw.blocker !== "string") {
|
|
916
|
+
sink?.push("blocker", "invalid_value", "dropped", "expected a string; the value was dropped");
|
|
917
|
+
}
|
|
918
|
+
else if (raw.blocker.trim().length === 0) {
|
|
919
|
+
sink?.push("blocker", DIAG_EMPTY_VALUE, "dropped", "empty blocker was dropped");
|
|
920
|
+
}
|
|
921
|
+
else if (raw.blocker.length > 500) {
|
|
922
|
+
sink?.push("blocker", "truncated", "truncated", "longer than 500 chars; truncated to the limit");
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
const noChangeReason = boundedString(raw.no_change_reason, 200, sink, "no_change_reason");
|
|
926
|
+
if (raw.no_change_reason === "") {
|
|
927
|
+
sink?.push("no_change_reason", DIAG_EMPTY_VALUE, "dropped", "empty reason was dropped");
|
|
928
|
+
}
|
|
630
929
|
return makeSelfEvaluation({
|
|
631
930
|
success: typeof raw.success === "boolean" ? raw.success : false,
|
|
632
931
|
output_summary: typeof raw.output_summary === "string" ? raw.output_summary : "",
|
|
633
|
-
constraint_violations:
|
|
634
|
-
? raw.constraint_violations.filter((v) => typeof v === "string")
|
|
635
|
-
: [],
|
|
932
|
+
constraint_violations: constraintViolations,
|
|
636
933
|
should_continue: typeof raw.should_continue === "boolean" ? raw.should_continue : true,
|
|
637
|
-
discovered_constraints: boundedStringArray(raw.discovered_constraints, 50, 500),
|
|
638
|
-
objective_refinement: boundedString(raw.objective_refinement, 1000) ?? "",
|
|
639
|
-
emerged_subtasks: boundedEmergedSubtasks(raw.emerged_subtasks),
|
|
934
|
+
discovered_constraints: boundedStringArray(raw.discovered_constraints, 50, 500, sink, "discovered_constraints"),
|
|
935
|
+
objective_refinement: boundedString(raw.objective_refinement, 1000, sink, "objective_refinement") ?? "",
|
|
936
|
+
emerged_subtasks: boundedEmergedSubtasks(raw.emerged_subtasks, sink),
|
|
640
937
|
execution_report: executionReport,
|
|
641
938
|
retracted_constraints: retractedConstraints,
|
|
642
939
|
revised_success_criteria: revisedCriteria,
|
|
643
940
|
wrong_assumptions: wrongAssumptions,
|
|
644
941
|
worker_results: workerResults,
|
|
645
942
|
compression_checkpoint: typeof raw.compression_checkpoint === "boolean" ? raw.compression_checkpoint : false,
|
|
646
|
-
checkpoint_label: boundedString(raw.checkpoint_label, 200) ?? "",
|
|
647
|
-
subgoal_updates: parseSubGoalUpdates(raw.subgoal_updates),
|
|
943
|
+
checkpoint_label: boundedString(raw.checkpoint_label, 200, sink, "checkpoint_label") ?? "",
|
|
944
|
+
subgoal_updates: parseSubGoalUpdates(raw.subgoal_updates, sink),
|
|
648
945
|
stop_reason: raw.stop_reason === "gave_up" || raw.stop_reason === "blocked" || raw.stop_reason === "needs_human_input"
|
|
649
946
|
? raw.stop_reason
|
|
650
947
|
: undefined,
|
|
@@ -655,15 +952,27 @@ export function buildSelfEvaluation(raw) {
|
|
|
655
952
|
blocker: typeof raw.blocker === "string" && raw.blocker.trim().length > 0
|
|
656
953
|
? raw.blocker.slice(0, 500)
|
|
657
954
|
: undefined,
|
|
658
|
-
gate_ids: boundedStringArray(raw.gate_ids, 20, 64),
|
|
659
|
-
retroactiveClaims: parseRetroactiveClaims(raw.retroactiveClaims),
|
|
660
|
-
no_change_reason:
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
prompt_requests: parsePromptRequests(raw.prompt_requests),
|
|
664
|
-
round_contract: parseRoundContract(raw.round_contract),
|
|
955
|
+
gate_ids: boundedStringArray(raw.gate_ids, 20, 64, sink, "gate_ids"),
|
|
956
|
+
retroactiveClaims: parseRetroactiveClaims(raw.retroactiveClaims, sink),
|
|
957
|
+
no_change_reason: noChangeReason || undefined,
|
|
958
|
+
prompt_requests: parsePromptRequests(raw.prompt_requests, sink),
|
|
959
|
+
round_contract: parseRoundContract(raw.round_contract, sink),
|
|
665
960
|
});
|
|
666
961
|
}
|
|
962
|
+
/** Normalize a raw submission and report every lossy decision. Unknown and
|
|
963
|
+
* retired fields are diagnosed and dropped; they are never silently accepted. */
|
|
964
|
+
export function normalizeSelfEvaluation(raw) {
|
|
965
|
+
const collector = new DiagnosticsCollector();
|
|
966
|
+
for (const key of unknownTopLevelFields(raw)) {
|
|
967
|
+
collector.push(key, "unknown_field", "dropped", retiredFieldDetail(key) ?? "not a declared evaluation field; it was dropped");
|
|
968
|
+
}
|
|
969
|
+
const evaluation = buildSelfEvaluation(raw, collector);
|
|
970
|
+
return {
|
|
971
|
+
evaluation,
|
|
972
|
+
diagnostics: collector.finish(),
|
|
973
|
+
submittedFields: submittedFieldPaths(raw),
|
|
974
|
+
};
|
|
975
|
+
}
|
|
667
976
|
/** v3.8: The ONE set of contract-declaration limits. `validateContractShape`
|
|
668
977
|
* (the strict boundary) rejects a declaration over these; `parseRoundContract`
|
|
669
978
|
* (the lenient path, reached only after the strict boundary passed) truncates
|
|
@@ -680,35 +989,110 @@ export const CONTRACT_LIMITS = {
|
|
|
680
989
|
* capped, non-string array entries are dropped. Item ORDER and COUNT are
|
|
681
990
|
* preserved — the derived rci- ids depend on them. Returns undefined only
|
|
682
991
|
* when the raw value is absent or not an object. */
|
|
683
|
-
export function parseRoundContract(raw) {
|
|
684
|
-
if (
|
|
992
|
+
export function parseRoundContract(raw, sink) {
|
|
993
|
+
if (raw === undefined)
|
|
994
|
+
return undefined;
|
|
995
|
+
if (raw === null) {
|
|
996
|
+
sink?.push("round_contract", "invalid_value", "dropped", "expected a contract object; null was dropped");
|
|
685
997
|
return undefined;
|
|
998
|
+
}
|
|
999
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
1000
|
+
sink?.push("round_contract", "invalid_entry", "dropped", "expected a contract object; the value was dropped");
|
|
1001
|
+
return undefined;
|
|
1002
|
+
}
|
|
686
1003
|
const obj = raw;
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
1004
|
+
for (const key of Object.keys(obj)) {
|
|
1005
|
+
if (!["work_item", "scope", "items"].includes(key)) {
|
|
1006
|
+
sink?.push(`round_contract.${key}`, "unknown_field", "dropped", "not a declared contract field; it was dropped");
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
const capStrings = (value, max, cap, path) => {
|
|
1010
|
+
if (value === undefined)
|
|
1011
|
+
return [];
|
|
1012
|
+
if (!Array.isArray(value)) {
|
|
1013
|
+
sink?.push(path, "invalid_entry", "dropped", "expected an array of strings; the value was dropped");
|
|
1014
|
+
return [];
|
|
1015
|
+
}
|
|
1016
|
+
const strings = value.filter((v) => typeof v === "string");
|
|
1017
|
+
const invalid = value.length - strings.length;
|
|
1018
|
+
if (invalid > 0) {
|
|
1019
|
+
sink?.push(`${path}[]`, strings.length === 0 ? DIAG_ALL_ENTRIES_DROPPED : "invalid_entry", "dropped", strings.length === 0
|
|
1020
|
+
? `all ${invalid} entries were invalid; the value was dropped`
|
|
1021
|
+
: `${invalid} non-string entr${invalid === 1 ? "y" : "ies"} dropped`);
|
|
1022
|
+
}
|
|
1023
|
+
const trimmed = strings.map((v) => v.trim()).filter(Boolean);
|
|
1024
|
+
const empty = strings.length - trimmed.length;
|
|
1025
|
+
if (empty > 0) {
|
|
1026
|
+
sink?.push(`${path}[]`, DIAG_EMPTY_VALUE, "dropped", `${empty} empty entr${empty === 1 ? "y was" : "ies were"} dropped`);
|
|
1027
|
+
}
|
|
1028
|
+
const bounded = trimmed.map((v) => v.slice(0, max));
|
|
1029
|
+
if (bounded.some((v, index) => v !== trimmed[index])) {
|
|
1030
|
+
sink?.push(`${path}[]`, "truncated", "truncated", `a contract string was longer than ${max} chars`);
|
|
1031
|
+
}
|
|
1032
|
+
if (bounded.length > cap) {
|
|
1033
|
+
sink?.push(path, "too_many", "truncated", `only the first ${cap} entries are kept`);
|
|
1034
|
+
}
|
|
1035
|
+
return bounded.slice(0, cap);
|
|
1036
|
+
};
|
|
1037
|
+
const rawItems = obj.items;
|
|
1038
|
+
if (rawItems === undefined) {
|
|
1039
|
+
sink?.push("round_contract", "invalid_value", "dropped", "expected an items array; the proposal was dropped");
|
|
1040
|
+
return undefined;
|
|
1041
|
+
}
|
|
1042
|
+
if (!Array.isArray(rawItems)) {
|
|
1043
|
+
sink?.push("round_contract", "invalid_value", "dropped", "items must be an array; the proposal was dropped");
|
|
1044
|
+
return undefined;
|
|
1045
|
+
}
|
|
1046
|
+
const objectItems = rawItems.filter((item) => typeof item === "object" && item !== null && !Array.isArray(item));
|
|
1047
|
+
const nonObject = rawItems.length - objectItems.length;
|
|
1048
|
+
if (nonObject > 0) {
|
|
1049
|
+
sink?.push("round_contract.items", "invalid_entry", "dropped", `${nonObject} non-object item${nonObject === 1 ? "" : "s"} dropped`);
|
|
1050
|
+
}
|
|
1051
|
+
const items = objectItems
|
|
1052
|
+
.slice(0, CONTRACT_LIMITS.items)
|
|
1053
|
+
.map((item, index) => {
|
|
1054
|
+
for (const key of Object.keys(item)) {
|
|
1055
|
+
if (!["description", "criterion_refs", "subgoal_refs", "verify_with"].includes(key)) {
|
|
1056
|
+
sink?.push(`round_contract.items[${index}].${key}`, "unknown_field", "dropped", "not a declared contract-item field; it was dropped");
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
if (item.description !== undefined && typeof item.description !== "string") {
|
|
1060
|
+
sink?.push(`round_contract.items[${index}].description`, "invalid_value", "dropped", "expected a string; the description was dropped");
|
|
1061
|
+
}
|
|
1062
|
+
if (typeof item.description === "string" &&
|
|
1063
|
+
item.description.trim().length > 200) {
|
|
1064
|
+
sink?.push(`round_contract.items[${index}].description`, "truncated", "truncated", "longer than 200 chars; truncated to the limit");
|
|
1065
|
+
}
|
|
1066
|
+
return {
|
|
699
1067
|
description: typeof item.description === "string"
|
|
700
1068
|
? item.description.trim().slice(0, 200)
|
|
701
1069
|
: "",
|
|
702
|
-
criterion_refs: capStrings(item.criterion_refs, 200, CONTRACT_LIMITS.criterionRefs),
|
|
703
|
-
subgoal_refs: capStrings(item.subgoal_refs, 64, CONTRACT_LIMITS.subgoalRefs),
|
|
704
|
-
verify_with: capStrings(item.verify_with, 200, CONTRACT_LIMITS.verifyWith),
|
|
705
|
-
}
|
|
706
|
-
|
|
1070
|
+
criterion_refs: capStrings(item.criterion_refs, 200, CONTRACT_LIMITS.criterionRefs, `round_contract.items[${index}].criterion_refs`),
|
|
1071
|
+
subgoal_refs: capStrings(item.subgoal_refs, 64, CONTRACT_LIMITS.subgoalRefs, `round_contract.items[${index}].subgoal_refs`),
|
|
1072
|
+
verify_with: capStrings(item.verify_with, 200, CONTRACT_LIMITS.verifyWith, `round_contract.items[${index}].verify_with`),
|
|
1073
|
+
};
|
|
1074
|
+
});
|
|
1075
|
+
if (rawItems.length > CONTRACT_LIMITS.items) {
|
|
1076
|
+
sink?.push("round_contract.items", "too_many", "truncated", `only the first ${CONTRACT_LIMITS.items} items are kept`);
|
|
1077
|
+
}
|
|
1078
|
+
if (items.length === 0) {
|
|
1079
|
+
sink?.push("round_contract", DIAG_ALL_ENTRIES_DROPPED, "dropped", "no valid items; the proposal was dropped");
|
|
1080
|
+
return undefined;
|
|
1081
|
+
}
|
|
1082
|
+
if (obj.work_item !== undefined && typeof obj.work_item !== "string") {
|
|
1083
|
+
sink?.push("round_contract.work_item", "invalid_value", "dropped", "expected a string; the work_item was dropped");
|
|
1084
|
+
}
|
|
1085
|
+
else if (typeof obj.work_item === "string" && obj.work_item.trim().length === 0) {
|
|
1086
|
+
sink?.push("round_contract.work_item", DIAG_EMPTY_VALUE, "dropped", "empty work_item was dropped");
|
|
1087
|
+
}
|
|
1088
|
+
else if (typeof obj.work_item === "string" && obj.work_item.trim().length > 200) {
|
|
1089
|
+
sink?.push("round_contract.work_item", "truncated", "truncated", "longer than 200 chars; truncated to the limit");
|
|
1090
|
+
}
|
|
707
1091
|
return {
|
|
708
1092
|
work_item: typeof obj.work_item === "string" && obj.work_item.trim().length > 0
|
|
709
1093
|
? obj.work_item.trim().slice(0, 200)
|
|
710
1094
|
: undefined,
|
|
711
|
-
scope: capStrings(obj.scope, 500, CONTRACT_LIMITS.scope),
|
|
1095
|
+
scope: capStrings(obj.scope, 500, CONTRACT_LIMITS.scope, "round_contract.scope"),
|
|
712
1096
|
items,
|
|
713
1097
|
};
|
|
714
1098
|
}
|
|
@@ -720,15 +1104,29 @@ export function parseRoundContract(raw) {
|
|
|
720
1104
|
* lifecycle assembles prompt_requests into the request, but every
|
|
721
1105
|
* compile path funnels through engine.invokeLoopCompile, whose
|
|
722
1106
|
* field-by-field reconstruction must preserve them (v3.3.1). */
|
|
723
|
-
export function parsePromptRequests(raw) {
|
|
724
|
-
if (
|
|
1107
|
+
export function parsePromptRequests(raw, sink) {
|
|
1108
|
+
if (raw === undefined)
|
|
725
1109
|
return undefined;
|
|
1110
|
+
if (raw === null) {
|
|
1111
|
+
sink?.push("prompt_requests", "invalid_value", "dropped", "expected an object with emphasize/confusion_points; null was dropped");
|
|
1112
|
+
return undefined;
|
|
1113
|
+
}
|
|
1114
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
1115
|
+
sink?.push("prompt_requests", "invalid_entry", "dropped", "expected an object with emphasize/confusion_points; the value was dropped");
|
|
1116
|
+
return undefined;
|
|
1117
|
+
}
|
|
726
1118
|
const obj = raw;
|
|
727
|
-
const
|
|
728
|
-
|
|
1119
|
+
for (const key of Object.keys(obj)) {
|
|
1120
|
+
if (key !== "emphasize" && key !== "confusion_points") {
|
|
1121
|
+
sink?.push(`prompt_requests.${key}`, "unknown_field", "dropped", "not a declared prompt-request field; it was dropped");
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
const emphasize = boundedStringArray(obj.emphasize, 5, 500, sink, "prompt_requests.emphasize");
|
|
1125
|
+
const confusionPoints = boundedStringArray(obj.confusion_points, 3, 200, sink, "prompt_requests.confusion_points");
|
|
729
1126
|
// Return undefined when all fields are empty — semantically equivalent
|
|
730
1127
|
// to "not set", avoids carrying an empty object through the pipeline.
|
|
731
1128
|
if (emphasize.length === 0 && confusionPoints.length === 0) {
|
|
1129
|
+
sink?.push("prompt_requests", DIAG_EMPTY_VALUE, "dropped", "emphasize and confusion_points are both empty; treated as not set");
|
|
732
1130
|
return undefined;
|
|
733
1131
|
}
|
|
734
1132
|
return { emphasize, confusion_points: confusionPoints };
|