loopforge 3.9.2 → 4.0.7
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 +236 -112
- package/dist/audit.d.ts +73 -15
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +131 -61
- package/dist/audit.js.map +1 -1
- package/dist/canonical-state.d.ts +152 -40
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +382 -206
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +41 -12
- package/dist/cli.js.map +1 -1
- package/dist/cognitive-governance.d.ts +0 -2
- package/dist/cognitive-governance.d.ts.map +1 -1
- package/dist/cognitive-governance.js +0 -4
- package/dist/cognitive-governance.js.map +1 -1
- package/dist/committed-round.d.ts +105 -141
- package/dist/committed-round.d.ts.map +1 -1
- package/dist/committed-round.js +249 -372
- package/dist/committed-round.js.map +1 -1
- package/dist/constraint-lifecycle.d.ts +73 -0
- package/dist/constraint-lifecycle.d.ts.map +1 -0
- package/dist/constraint-lifecycle.js +174 -0
- package/dist/constraint-lifecycle.js.map +1 -0
- package/dist/context-boundary.d.ts +83 -0
- package/dist/context-boundary.d.ts.map +1 -0
- package/dist/context-boundary.js +101 -0
- package/dist/context-boundary.js.map +1 -0
- package/dist/contract-items.d.ts +12 -8
- package/dist/contract-items.d.ts.map +1 -1
- package/dist/contract-items.js +31 -26
- package/dist/contract-items.js.map +1 -1
- package/dist/contract-requirement.d.ts +47 -0
- package/dist/contract-requirement.d.ts.map +1 -0
- package/dist/contract-requirement.js +51 -0
- package/dist/contract-requirement.js.map +1 -0
- package/dist/criterion-status.d.ts +35 -0
- package/dist/criterion-status.d.ts.map +1 -0
- package/dist/criterion-status.js +89 -0
- package/dist/criterion-status.js.map +1 -0
- package/dist/enforcement-gate.d.ts +32 -67
- package/dist/enforcement-gate.d.ts.map +1 -1
- package/dist/enforcement-gate.js +203 -439
- package/dist/enforcement-gate.js.map +1 -1
- package/dist/engine.d.ts +9 -103
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +28 -887
- package/dist/engine.js.map +1 -1
- package/dist/errors.d.ts +41 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +69 -0
- package/dist/errors.js.map +1 -0
- package/dist/evidence-claims.d.ts +33 -31
- package/dist/evidence-claims.d.ts.map +1 -1
- package/dist/evidence-claims.js +58 -72
- package/dist/evidence-claims.js.map +1 -1
- package/dist/evidence-posture-view.d.ts +19 -0
- package/dist/evidence-posture-view.d.ts.map +1 -0
- package/dist/evidence-posture-view.js +116 -0
- package/dist/evidence-posture-view.js.map +1 -0
- package/dist/evidence-posture.d.ts +73 -0
- package/dist/evidence-posture.d.ts.map +1 -0
- package/dist/evidence-posture.js +281 -0
- package/dist/evidence-posture.js.map +1 -0
- package/dist/evidence-provider.d.ts +55 -37
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +182 -332
- package/dist/evidence-provider.js.map +1 -1
- package/dist/explain.d.ts +167 -27
- package/dist/explain.d.ts.map +1 -1
- package/dist/explain.js +373 -58
- package/dist/explain.js.map +1 -1
- package/dist/field-accounting.d.ts.map +1 -1
- package/dist/field-accounting.js +3 -22
- package/dist/field-accounting.js.map +1 -1
- package/dist/generate-schema.js +13 -8
- package/dist/generate-schema.js.map +1 -1
- package/dist/git-boundary.d.ts +128 -0
- package/dist/git-boundary.d.ts.map +1 -0
- package/dist/git-boundary.js +625 -0
- package/dist/git-boundary.js.map +1 -0
- package/dist/git-facts.d.ts +22 -0
- package/dist/git-facts.d.ts.map +1 -0
- package/dist/git-facts.js +29 -0
- package/dist/git-facts.js.map +1 -0
- package/dist/git-motion.d.ts +37 -0
- package/dist/git-motion.d.ts.map +1 -0
- package/dist/git-motion.js +59 -0
- package/dist/git-motion.js.map +1 -0
- package/dist/history.d.ts +44 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +77 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts +19 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -4
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +16 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +80 -14
- package/dist/init.js.map +1 -1
- package/dist/loop-compiler.d.ts +51 -19
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +423 -507
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-store.d.ts +6 -5
- package/dist/loop-store.d.ts.map +1 -1
- package/dist/loop-store.js +153 -35
- package/dist/loop-store.js.map +1 -1
- package/dist/mcp/round-lifecycle.d.ts +285 -81
- package/dist/mcp/round-lifecycle.d.ts.map +1 -1
- package/dist/mcp/round-lifecycle.js +1629 -574
- package/dist/mcp/round-lifecycle.js.map +1 -1
- package/dist/mcp/server.d.ts +5 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +61 -19
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +81 -39
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +963 -271
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/submission-boundary.d.ts +23 -31
- package/dist/mcp/submission-boundary.d.ts.map +1 -1
- package/dist/mcp/submission-boundary.js +242 -82
- package/dist/mcp/submission-boundary.js.map +1 -1
- package/dist/mcp/submission-handle.d.ts +66 -0
- package/dist/mcp/submission-handle.d.ts.map +1 -0
- package/dist/mcp/submission-handle.js +173 -0
- package/dist/mcp/submission-handle.js.map +1 -0
- package/dist/mcp/tools.d.ts +17 -7
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +763 -244
- package/dist/mcp/tools.js.map +1 -1
- package/dist/open-question.d.ts +7 -0
- package/dist/open-question.d.ts.map +1 -0
- package/dist/open-question.js +14 -0
- package/dist/open-question.js.map +1 -0
- package/dist/policy-metrics.d.ts +31 -21
- package/dist/policy-metrics.d.ts.map +1 -1
- package/dist/policy-metrics.js +117 -64
- package/dist/policy-metrics.js.map +1 -1
- package/dist/policy.d.ts +15 -40
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +13 -13
- package/dist/policy.js.map +1 -1
- package/dist/progress.d.ts +45 -0
- package/dist/progress.d.ts.map +1 -0
- package/dist/progress.js +136 -0
- package/dist/progress.js.map +1 -0
- package/dist/prompt-assembler.d.ts +28 -9
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +592 -603
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/prompt-coverage.d.ts +39 -0
- package/dist/prompt-coverage.d.ts.map +1 -0
- package/dist/prompt-coverage.js +80 -0
- package/dist/prompt-coverage.js.map +1 -0
- package/dist/prompt-freshness.d.ts +25 -0
- package/dist/prompt-freshness.d.ts.map +1 -0
- package/dist/prompt-freshness.js +88 -0
- package/dist/prompt-freshness.js.map +1 -0
- package/dist/prompt-policy.d.ts +14 -4
- package/dist/prompt-policy.d.ts.map +1 -1
- package/dist/prompt-policy.js +29 -10
- package/dist/prompt-policy.js.map +1 -1
- package/dist/protocol.d.ts +875 -419
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +73 -99
- package/dist/protocol.js.map +1 -1
- package/dist/recovery-guidance.d.ts +13 -0
- package/dist/recovery-guidance.d.ts.map +1 -0
- package/dist/recovery-guidance.js +15 -0
- package/dist/recovery-guidance.js.map +1 -0
- package/dist/recovery.d.ts +68 -0
- package/dist/recovery.d.ts.map +1 -0
- package/dist/recovery.js +234 -0
- package/dist/recovery.js.map +1 -0
- package/dist/replay.d.ts +42 -8
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +311 -84
- package/dist/replay.js.map +1 -1
- package/dist/result-facts.d.ts +45 -0
- package/dist/result-facts.d.ts.map +1 -0
- package/dist/result-facts.js +86 -0
- package/dist/result-facts.js.map +1 -0
- package/dist/round-contract.d.ts +38 -8
- package/dist/round-contract.d.ts.map +1 -1
- package/dist/round-contract.js +71 -31
- package/dist/round-contract.js.map +1 -1
- package/dist/round-coordinator.d.ts +45 -91
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +254 -271
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +56 -16
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +225 -39
- package/dist/round-driver.js.map +1 -1
- package/dist/round-facts.d.ts +33 -52
- package/dist/round-facts.d.ts.map +1 -1
- package/dist/round-facts.js +54 -157
- package/dist/round-facts.js.map +1 -1
- package/dist/round-guidance.d.ts +56 -0
- package/dist/round-guidance.d.ts.map +1 -0
- package/dist/round-guidance.js +161 -0
- package/dist/round-guidance.js.map +1 -0
- package/dist/round-outcome.d.ts +21 -0
- package/dist/round-outcome.d.ts.map +1 -0
- package/dist/round-outcome.js +31 -0
- package/dist/round-outcome.js.map +1 -0
- package/dist/round-transaction.d.ts +168 -21
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +856 -39
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +40 -60
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +455 -384
- package/dist/self-eval.js.map +1 -1
- package/dist/storage.d.ts +16 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +54 -4
- package/dist/storage.js.map +1 -1
- package/dist/subgoal-state.d.ts +2 -3
- package/dist/subgoal-state.d.ts.map +1 -1
- package/dist/subgoal-state.js +2 -3
- package/dist/subgoal-state.js.map +1 -1
- package/dist/submission-guidance.d.ts +43 -21
- package/dist/submission-guidance.d.ts.map +1 -1
- package/dist/submission-guidance.js +368 -262
- package/dist/submission-guidance.js.map +1 -1
- package/dist/submission-receipt.d.ts +16 -8
- package/dist/submission-receipt.d.ts.map +1 -1
- package/dist/submission-receipt.js +146 -51
- package/dist/submission-receipt.js.map +1 -1
- package/dist/token-utils.d.ts +47 -7
- package/dist/token-utils.d.ts.map +1 -1
- package/dist/token-utils.js +66 -15
- package/dist/token-utils.js.map +1 -1
- package/dist/transaction-identity.d.ts +9 -0
- package/dist/transaction-identity.d.ts.map +1 -0
- package/dist/transaction-identity.js +69 -0
- package/dist/transaction-identity.js.map +1 -0
- package/dist/verification-actions.d.ts +13 -0
- package/dist/verification-actions.d.ts.map +1 -0
- package/dist/verification-actions.js +44 -0
- package/dist/verification-actions.js.map +1 -0
- package/dist/verification-gate.d.ts +26 -46
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +218 -427
- package/dist/verification-gate.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/working-memory.d.ts +187 -0
- package/dist/working-memory.d.ts.map +1 -0
- package/dist/working-memory.js +767 -0
- package/dist/working-memory.js.map +1 -0
- package/dist/workspace-checkpoint.d.ts +88 -0
- package/dist/workspace-checkpoint.d.ts.map +1 -0
- package/dist/workspace-checkpoint.js +326 -0
- package/dist/workspace-checkpoint.js.map +1 -0
- package/dist/workspace-restore.d.ts +68 -0
- package/dist/workspace-restore.d.ts.map +1 -0
- package/dist/workspace-restore.js +604 -0
- package/dist/workspace-restore.js.map +1 -0
- package/dist/workspace.d.ts +0 -9
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +0 -17
- package/dist/workspace.js.map +1 -1
- package/loop_policy.json +5 -19
- package/package.json +2 -2
- package/skills/forge/SKILL.md +212 -0
- package/skills/forge/references/evidence-and-contracts.md +204 -0
- package/skills/forge/references/recovery-and-views.md +188 -0
- package/dist/cognitive-facts.d.ts +0 -30
- package/dist/cognitive-facts.d.ts.map +0 -1
- package/dist/cognitive-facts.js +0 -113
- package/dist/cognitive-facts.js.map +0 -1
- package/dist/loop-extras-parser.d.ts +0 -81
- package/dist/loop-extras-parser.d.ts.map +0 -1
- package/dist/loop-extras-parser.js +0 -213
- package/dist/loop-extras-parser.js.map +0 -1
- package/dist/loop-projection.d.ts +0 -6
- package/dist/loop-projection.d.ts.map +0 -1
- package/dist/loop-projection.js +0 -28
- package/dist/loop-projection.js.map +0 -1
- package/skills/perception/SKILL.md +0 -370
package/dist/self-eval.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
import { makeExecutionReport, makeSelfEvaluation, } from "./protocol.js";
|
|
5
5
|
import { retiredFieldDetail, submittedFieldPaths, unknownExecutionReportEntryFields, unknownTopLevelFields, } from "./submission-guidance.js";
|
|
6
6
|
import { DIAG_ALL_ENTRIES_DROPPED, DIAG_EMPTY_VALUE, } from "./submission-diagnostics.js";
|
|
7
|
-
import { criterionRefMatches } from "./token-utils.js";
|
|
7
|
+
import { canonicalContractItemText, criterionRefMatches, isCriterionId, } from "./token-utils.js";
|
|
8
|
+
import { isOpenQuestionId } from "./open-question.js";
|
|
8
9
|
const DIAGNOSTICS_LIMIT = 100;
|
|
9
10
|
class DiagnosticsCollector {
|
|
10
11
|
entries = [];
|
|
@@ -93,19 +94,6 @@ export const EMERGED_LIMITS = { items: 50, chars: 500 };
|
|
|
93
94
|
export function boundedEmergedSubtasks(value, sink) {
|
|
94
95
|
return boundedStringArray(value, EMERGED_LIMITS.items, EMERGED_LIMITS.chars, sink, "emerged_subtasks");
|
|
95
96
|
}
|
|
96
|
-
function nonNegativeInteger(value, sink, path = "") {
|
|
97
|
-
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
98
|
-
if (value !== undefined && value !== null) {
|
|
99
|
-
sink?.push(path, "invalid_value", "normalized", "expected a finite number; defaulted to 0");
|
|
100
|
-
}
|
|
101
|
-
return 0;
|
|
102
|
-
}
|
|
103
|
-
const normalized = Math.max(0, Math.trunc(value));
|
|
104
|
-
if (normalized !== value) {
|
|
105
|
-
sink?.push(path, "normalized", "normalized", `expected a non-negative integer; normalized to ${normalized}`);
|
|
106
|
-
}
|
|
107
|
-
return normalized;
|
|
108
|
-
}
|
|
109
97
|
/** v2.12: Parse retroactive claims — {round ≥ 1, claim non-empty}, capped at
|
|
110
98
|
* 20 entries. Invalid entries are silently dropped (lenient parsing). */
|
|
111
99
|
function parseRetroactiveClaims(raw, sink) {
|
|
@@ -156,61 +144,44 @@ export function parseExecutionReport(raw, sink, knownCriterionRefs) {
|
|
|
156
144
|
return undefined;
|
|
157
145
|
}
|
|
158
146
|
for (const key of unknownExecutionReportEntryFields(raw)) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const testResults = raw.tests_reported && typeof raw.tests_reported === "object" &&
|
|
162
|
-
!Array.isArray(raw.tests_reported)
|
|
163
|
-
? raw.tests_reported
|
|
164
|
-
: undefined;
|
|
165
|
-
let tests = null;
|
|
166
|
-
if (raw.tests_reported !== undefined && raw.tests_reported !== null) {
|
|
167
|
-
if (!testResults) {
|
|
168
|
-
sink?.push("execution_report.tests_reported", "invalid_entry", "dropped", "expected { passed, failed, skipped }; the value was dropped");
|
|
169
|
-
}
|
|
170
|
-
else if (typeof testResults.passed !== "number" ||
|
|
171
|
-
!Number.isFinite(testResults.passed)) {
|
|
172
|
-
sink?.push("execution_report.tests_reported", "invalid_value", "dropped", "`passed` must be a finite number; the whole object was dropped");
|
|
173
|
-
}
|
|
174
|
-
else {
|
|
175
|
-
for (const key of Object.keys(testResults)) {
|
|
176
|
-
if (key !== "passed" && key !== "failed" && key !== "skipped") {
|
|
177
|
-
sink?.push(`execution_report.tests_reported.${key}`, "unknown_field", "dropped", "not a declared test-count field; it was dropped");
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
tests = {
|
|
181
|
-
passed: nonNegativeInteger(testResults.passed, sink, "execution_report.tests_reported.passed"),
|
|
182
|
-
failed: nonNegativeInteger(testResults.failed, sink, "execution_report.tests_reported.failed"),
|
|
183
|
-
skipped: nonNegativeInteger(testResults.skipped, sink, "execution_report.tests_reported.skipped"),
|
|
184
|
-
};
|
|
185
|
-
}
|
|
147
|
+
const path = `execution_report.${key}`;
|
|
148
|
+
sink?.push(path, "unknown_field", "dropped", retiredFieldDetail(path) ?? "not a declared execution_report field; it was dropped");
|
|
186
149
|
}
|
|
187
150
|
return makeExecutionReport({
|
|
188
151
|
files_changed: boundedStringArray(raw.files_changed, 200, 500, sink, "execution_report.files_changed"),
|
|
189
|
-
|
|
190
|
-
criterion_claims: parseCriterionClaims(raw.criterion_claims, sink, knownCriterionRefs),
|
|
191
|
-
contract_item_claims: parseContractItemClaims(raw.contract_item_claims, sink),
|
|
152
|
+
claims: parseExecutionClaims(raw.claims, sink, knownCriterionRefs),
|
|
192
153
|
});
|
|
193
154
|
}
|
|
194
|
-
|
|
155
|
+
const CLAIM_ID_RE = /^(cr|rci)-[a-f0-9]{8}$/;
|
|
156
|
+
function isCriterionClaimId(id) {
|
|
157
|
+
return id.startsWith("cr-");
|
|
158
|
+
}
|
|
159
|
+
/** v4.0.6: the claimed `cr-` criterion ids with the given outcome. */
|
|
160
|
+
function claimedCriteria(report, outcome) {
|
|
161
|
+
return (report?.claims ?? [])
|
|
162
|
+
.filter((claim) => claim.outcome === outcome && isCriterionClaimId(claim.id))
|
|
163
|
+
.map((claim) => claim.id);
|
|
164
|
+
}
|
|
165
|
+
/** v4.0.6: The criterion ids the agent claims met this round. */
|
|
195
166
|
export function claimedMetCriteria(report) {
|
|
196
|
-
return (report
|
|
197
|
-
.filter((claim) => claim.outcome === "met")
|
|
198
|
-
.map((claim) => claim.criterion_id);
|
|
167
|
+
return claimedCriteria(report, "met");
|
|
199
168
|
}
|
|
200
|
-
/**
|
|
169
|
+
/** v4.0.6: The criterion ids the agent declares still outstanding. */
|
|
201
170
|
export function claimedRemainingCriteria(report) {
|
|
202
|
-
return (report
|
|
203
|
-
.filter((claim) => claim.outcome === "remaining")
|
|
204
|
-
.map((claim) => claim.criterion_id);
|
|
171
|
+
return claimedCriteria(report, "remaining");
|
|
205
172
|
}
|
|
206
|
-
/**
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
|
|
173
|
+
/** v4.0.6: LENIENT parse of the ONE claims channel.
|
|
174
|
+
*
|
|
175
|
+
* `cr-` claims are advisory: unknown/malformed entries are dropped with a
|
|
176
|
+
* diagnostic, never a rejection. `rci-` claims are STRICT at the submission
|
|
177
|
+
* boundary (`validateContractShape` checks format, uniqueness and the ACTIVE
|
|
178
|
+
* item set); this parser only normalizes what reaches the read model and
|
|
179
|
+
* drops structurally malformed entries on library paths. */
|
|
180
|
+
function parseExecutionClaims(raw, sink, knownCriterionRefs) {
|
|
210
181
|
if (raw === undefined || raw === null)
|
|
211
182
|
return [];
|
|
212
183
|
if (!Array.isArray(raw)) {
|
|
213
|
-
sink?.push("execution_report.
|
|
184
|
+
sink?.push("execution_report.claims", "not_array", "dropped", "expected an array of { id, outcome } claims; the value was dropped");
|
|
214
185
|
return [];
|
|
215
186
|
}
|
|
216
187
|
const claims = [];
|
|
@@ -225,29 +196,30 @@ function parseCriterionClaims(raw, sink, knownCriterionRefs) {
|
|
|
225
196
|
continue;
|
|
226
197
|
}
|
|
227
198
|
const entry = item;
|
|
228
|
-
if (typeof entry.
|
|
229
|
-
entry.criterion_id.trim().length === 0 ||
|
|
199
|
+
if (typeof entry.id !== "string" ||
|
|
230
200
|
(entry.outcome !== "met" && entry.outcome !== "remaining")) {
|
|
231
201
|
invalid++;
|
|
232
202
|
continue;
|
|
233
203
|
}
|
|
234
|
-
const trimmed = entry.
|
|
235
|
-
if (trimmed !== entry.
|
|
236
|
-
sink?.push(`execution_report.
|
|
204
|
+
const trimmed = entry.id.trim();
|
|
205
|
+
if (trimmed !== entry.id) {
|
|
206
|
+
sink?.push(`execution_report.claims[${index}].id`, "normalized", "normalized", "leading/trailing whitespace was trimmed");
|
|
237
207
|
}
|
|
238
|
-
if (trimmed.length >
|
|
239
|
-
sink?.push(`execution_report.
|
|
208
|
+
if (trimmed.length > 64) {
|
|
209
|
+
sink?.push(`execution_report.claims[${index}].id`, "truncated", "truncated", "longer than 64 chars; truncated to the limit");
|
|
240
210
|
}
|
|
241
|
-
const
|
|
242
|
-
if (
|
|
243
|
-
|
|
211
|
+
const id = trimmed.slice(0, 64);
|
|
212
|
+
if (!CLAIM_ID_RE.test(id)) {
|
|
213
|
+
invalid++;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (isCriterionClaimId(id) && knownCriterionRefs !== undefined &&
|
|
217
|
+
knownCriterionRefs !== null &&
|
|
218
|
+
!knownCriterionRefs.some((criterion) => criterionRefMatches(id, criterion))) {
|
|
244
219
|
unknown++;
|
|
245
220
|
continue;
|
|
246
221
|
}
|
|
247
|
-
claims.push({
|
|
248
|
-
criterion_id: criterionId,
|
|
249
|
-
outcome: entry.outcome,
|
|
250
|
-
});
|
|
222
|
+
claims.push({ id, outcome: entry.outcome });
|
|
251
223
|
}
|
|
252
224
|
if (invalid > 0 || unknown > 0) {
|
|
253
225
|
if (claims.length === 0) {
|
|
@@ -256,104 +228,24 @@ function parseCriterionClaims(raw, sink, knownCriterionRefs) {
|
|
|
256
228
|
parts.push(`${invalid} malformed`);
|
|
257
229
|
if (unknown > 0)
|
|
258
230
|
parts.push(`${unknown} unknown criterion`);
|
|
259
|
-
sink?.push("execution_report.
|
|
231
|
+
sink?.push("execution_report.claims", DIAG_ALL_ENTRIES_DROPPED, "dropped", `all ${invalid + unknown} claims were dropped (${parts.join(", ")}; ` +
|
|
232
|
+
"malformed/unreferenced claims never reject a round)");
|
|
260
233
|
}
|
|
261
234
|
else {
|
|
262
235
|
if (invalid > 0) {
|
|
263
|
-
sink?.push("execution_report.
|
|
236
|
+
sink?.push("execution_report.claims", "invalid_entry", "dropped", `${invalid} malformed claim${invalid === 1 ? "" : "s"} dropped`);
|
|
264
237
|
}
|
|
265
238
|
if (unknown > 0) {
|
|
266
|
-
sink?.push("execution_report.
|
|
239
|
+
sink?.push("execution_report.claims", "unknown_id", "dropped", `${unknown} cr- claim${unknown === 1 ? "" : "s"} cited a criterion the ` +
|
|
267
240
|
"prompt did not render; dropped (advisory)");
|
|
268
241
|
}
|
|
269
242
|
}
|
|
270
243
|
}
|
|
271
244
|
if (raw.length > 100) {
|
|
272
|
-
sink?.push("execution_report.
|
|
273
|
-
}
|
|
274
|
-
return claims;
|
|
275
|
-
}
|
|
276
|
-
/** v3.8: LENIENT parse of contract item claims. Referential validity is the
|
|
277
|
-
* strict pre-advance boundary in validateContractShape — this parser only
|
|
278
|
-
* normalizes what reaches the read model. */
|
|
279
|
-
function parseContractItemClaims(raw, sink) {
|
|
280
|
-
if (raw === undefined || raw === null)
|
|
281
|
-
return [];
|
|
282
|
-
if (!Array.isArray(raw)) {
|
|
283
|
-
sink?.push("execution_report.contract_item_claims", "not_array", "dropped", "expected an array of claims; the value was dropped");
|
|
284
|
-
return [];
|
|
285
|
-
}
|
|
286
|
-
const claims = [];
|
|
287
|
-
let invalid = 0;
|
|
288
|
-
for (let index = 0; index < raw.length; index++) {
|
|
289
|
-
if (claims.length >= 20)
|
|
290
|
-
break;
|
|
291
|
-
const item = raw[index];
|
|
292
|
-
if (typeof item !== "object" || item === null || Array.isArray(item)) {
|
|
293
|
-
invalid++;
|
|
294
|
-
continue;
|
|
295
|
-
}
|
|
296
|
-
const entry = item;
|
|
297
|
-
if (typeof entry.item_id !== "string" ||
|
|
298
|
-
(entry.outcome !== "met" && entry.outcome !== "remaining")) {
|
|
299
|
-
invalid++;
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
const trimmed = entry.item_id.trim();
|
|
303
|
-
if (trimmed !== entry.item_id) {
|
|
304
|
-
sink?.push(`execution_report.contract_item_claims[${index}].item_id`, "normalized", "normalized", "leading/trailing whitespace was trimmed");
|
|
305
|
-
}
|
|
306
|
-
if (trimmed.length > 64) {
|
|
307
|
-
sink?.push(`execution_report.contract_item_claims[${index}].item_id`, "truncated", "truncated", "longer than 64 chars; truncated to the limit");
|
|
308
|
-
}
|
|
309
|
-
claims.push({
|
|
310
|
-
item_id: trimmed.slice(0, 64),
|
|
311
|
-
outcome: entry.outcome,
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
if (invalid > 0) {
|
|
315
|
-
sink?.push("execution_report.contract_item_claims", claims.length === 0 ? DIAG_ALL_ENTRIES_DROPPED : "invalid_entry", "dropped", claims.length === 0
|
|
316
|
-
? `all ${invalid} claims were malformed; the value was dropped`
|
|
317
|
-
: `${invalid} malformed claim${invalid === 1 ? "" : "s"} dropped`);
|
|
318
|
-
}
|
|
319
|
-
if (raw.length > 20) {
|
|
320
|
-
sink?.push("execution_report.contract_item_claims", "too_many", "truncated", "only the first 20 claims are kept");
|
|
245
|
+
sink?.push("execution_report.claims", "too_many", "truncated", "only the first 100 claims are kept");
|
|
321
246
|
}
|
|
322
247
|
return claims;
|
|
323
248
|
}
|
|
324
|
-
/** Parse CriterionRevision[] from a raw JSON array. */
|
|
325
|
-
export function parseCriterionRevisions(raw, sink) {
|
|
326
|
-
if (raw === undefined || raw === null)
|
|
327
|
-
return [];
|
|
328
|
-
if (!Array.isArray(raw)) {
|
|
329
|
-
sink?.push("revised_success_criteria", "not_array", "dropped", "expected an array of { old, new }; the value was dropped");
|
|
330
|
-
return [];
|
|
331
|
-
}
|
|
332
|
-
const valid = raw.filter((v) => typeof v === "object" && v !== null &&
|
|
333
|
-
typeof v.old === "string" &&
|
|
334
|
-
typeof v.new === "string");
|
|
335
|
-
const invalid = raw.length - valid.length;
|
|
336
|
-
if (invalid > 0) {
|
|
337
|
-
sink?.push("revised_success_criteria", valid.length === 0 ? DIAG_ALL_ENTRIES_DROPPED : "invalid_entry", "dropped", valid.length === 0
|
|
338
|
-
? `all ${invalid} entries lacked string old/new; the value was dropped`
|
|
339
|
-
: `${invalid} entr${invalid === 1 ? "y" : "ies"} without string old/new dropped`);
|
|
340
|
-
}
|
|
341
|
-
if (valid.length > 20) {
|
|
342
|
-
sink?.push("revised_success_criteria", "too_many", "truncated", "only the first 20 revisions are kept");
|
|
343
|
-
}
|
|
344
|
-
return valid.slice(0, 20).map((v, index) => {
|
|
345
|
-
const r = v;
|
|
346
|
-
for (const key of ["old", "new"]) {
|
|
347
|
-
if (r[key].length > 500) {
|
|
348
|
-
sink?.push(`revised_success_criteria[${index}].${key}`, "truncated", "truncated", "longer than 500 chars; truncated to the limit");
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
return {
|
|
352
|
-
old: r.old.slice(0, 500),
|
|
353
|
-
new: r.new.slice(0, 500),
|
|
354
|
-
};
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
249
|
/** Parse SubGoalUpdate[] from a raw JSON array.
|
|
358
250
|
* v3.7.1: lenient per-entry filtering (id/status shape) with fixed caps
|
|
359
251
|
* (20 entries, id ≤ 64, note ≤ 300). Referential validity — unknown IDs
|
|
@@ -445,16 +337,12 @@ export function parseWorkerResults(raw, sink) {
|
|
|
445
337
|
return valid.slice(0, 20).map((v, index) => {
|
|
446
338
|
const w = v;
|
|
447
339
|
for (const key of Object.keys(w)) {
|
|
448
|
-
if (!["agentId", "
|
|
449
|
-
"discoveredConstraints"].includes(key)) {
|
|
340
|
+
if (!["agentId", "subTask", "resultSummary", "outcome"].includes(key)) {
|
|
450
341
|
sink?.push(`worker_results[${index}].${key}`, "unknown_field", "dropped", "not a declared worker-result field; it was dropped");
|
|
451
342
|
}
|
|
452
343
|
}
|
|
453
|
-
if (w.subAgentType !== undefined && typeof w.subAgentType !== "string") {
|
|
454
|
-
sink?.push(`worker_results[${index}].subAgentType`, "invalid_value", "normalized", "expected a string; defaulted to general-purpose");
|
|
455
|
-
}
|
|
456
344
|
for (const [key, cap] of [
|
|
457
|
-
["agentId", 200], ["
|
|
345
|
+
["agentId", 200], ["subTask", 500], ["resultSummary", 1000],
|
|
458
346
|
]) {
|
|
459
347
|
const value = w[key];
|
|
460
348
|
if (typeof value === "string" && value.length > cap) {
|
|
@@ -463,56 +351,34 @@ export function parseWorkerResults(raw, sink) {
|
|
|
463
351
|
}
|
|
464
352
|
return {
|
|
465
353
|
agentId: (w.agentId.slice(0, 200)),
|
|
466
|
-
subAgentType: typeof w.subAgentType === "string"
|
|
467
|
-
? w.subAgentType.slice(0, 100)
|
|
468
|
-
: "general-purpose",
|
|
469
354
|
subTask: (w.subTask.slice(0, 500)),
|
|
470
355
|
resultSummary: (w.resultSummary.slice(0, 1000)),
|
|
471
356
|
outcome: w.outcome,
|
|
472
|
-
discoveredConstraints: boundedStringArray(w.discoveredConstraints, 50, 500, sink, `worker_results[${index}].discoveredConstraints`),
|
|
473
357
|
};
|
|
474
358
|
});
|
|
475
359
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
export
|
|
481
|
-
|
|
482
|
-
selfEval.outcome === "failed" || selfEval.outcome === "blocked") {
|
|
483
|
-
return selfEval.outcome;
|
|
484
|
-
}
|
|
485
|
-
return selfEval.success ? "success" : "failed";
|
|
486
|
-
}
|
|
487
|
-
/** v2.12: Whether the effective outcome is a success claim. Shared by the
|
|
488
|
-
* verification gate (success-class checks) and enforcement gate (R3). */
|
|
489
|
-
export function effectiveSuccess(selfEval) {
|
|
490
|
-
return effectiveOutcome(selfEval) === "success";
|
|
491
|
-
}
|
|
492
|
-
/** v3.8.3: The ONE definition of "this round declares itself blocked".
|
|
493
|
-
*
|
|
494
|
-
* A declared `outcome: "blocked"` and a `stop_reason` of `blocked` or
|
|
495
|
-
* `needs_human_input` say the same thing — the agent is stopping because it
|
|
496
|
-
* cannot continue, not because it finished. The stop decision
|
|
497
|
-
* (round-coordinator) and the scope-drift waiver (enforcement gate) both read
|
|
498
|
-
* this function, so "declared blocked" cannot mean two different things at
|
|
499
|
-
* two different boundaries. `effectiveOutcome` alone is NOT this predicate:
|
|
500
|
-
* it reads `outcome` only. */
|
|
501
|
-
export function declaresBlocked(selfEval) {
|
|
502
|
-
return selfEval.outcome === "blocked" ||
|
|
503
|
-
selfEval.stop_reason === "blocked" ||
|
|
504
|
-
selfEval.stop_reason === "needs_human_input";
|
|
505
|
-
}
|
|
360
|
+
// v3.9.7: the outcome predicates live in round-outcome.ts so contract
|
|
361
|
+
// closure, the walker, the verification gate and the coordinator share one
|
|
362
|
+
// definition. Re-exported for the existing public surface.
|
|
363
|
+
export { effectiveOutcome, effectiveSuccess, declaresBlocked, } from "./round-outcome.js";
|
|
364
|
+
export const OBJECTIVE_REFINEMENT_MAX = 1000;
|
|
365
|
+
export const BLOCKER_MAX = 500;
|
|
506
366
|
export function validateCoreSelfEvaluation(raw) {
|
|
507
367
|
const missing = [];
|
|
508
368
|
const invalid = [];
|
|
509
|
-
const required = ["
|
|
369
|
+
const required = ["outcome", "output_summary", "constraint_violations", "should_continue"];
|
|
510
370
|
for (const field of required) {
|
|
511
371
|
if (!(field in raw))
|
|
512
372
|
missing.push(field);
|
|
513
373
|
}
|
|
514
|
-
|
|
515
|
-
|
|
374
|
+
const outcome = raw.outcome;
|
|
375
|
+
if ("outcome" in raw &&
|
|
376
|
+
(outcome !== "success" && outcome !== "partial" &&
|
|
377
|
+
outcome !== "failed" && outcome !== "blocked")) {
|
|
378
|
+
invalid.push({
|
|
379
|
+
field: "outcome",
|
|
380
|
+
expected: "success | partial | failed | blocked",
|
|
381
|
+
});
|
|
516
382
|
}
|
|
517
383
|
if ("output_summary" in raw && typeof raw.output_summary !== "string") {
|
|
518
384
|
invalid.push({ field: "output_summary", expected: "string" });
|
|
@@ -525,13 +391,56 @@ export function validateCoreSelfEvaluation(raw) {
|
|
|
525
391
|
if ("should_continue" in raw && typeof raw.should_continue !== "boolean") {
|
|
526
392
|
invalid.push({ field: "should_continue", expected: "boolean" });
|
|
527
393
|
}
|
|
394
|
+
// v4.0.6: strict outcome/blocker combinations. A non-blocked blocker is NOT
|
|
395
|
+
// rejected — it is dropped with a diagnostic by the parser.
|
|
396
|
+
if (outcome === "success" && raw.should_continue === true) {
|
|
397
|
+
invalid.push({
|
|
398
|
+
field: "outcome",
|
|
399
|
+
expected: "outcome=success requires should_continue=false",
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
if (outcome === "blocked") {
|
|
403
|
+
if (raw.should_continue === true) {
|
|
404
|
+
invalid.push({
|
|
405
|
+
field: "outcome",
|
|
406
|
+
expected: "outcome=blocked requires should_continue=false",
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
if (typeof raw.blocker !== "string" || raw.blocker.trim().length === 0) {
|
|
410
|
+
invalid.push({
|
|
411
|
+
field: "blocker",
|
|
412
|
+
expected: "a non-empty blocker string is required when outcome=blocked",
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
else if (raw.blocker.length > BLOCKER_MAX) {
|
|
416
|
+
invalid.push({
|
|
417
|
+
field: "blocker",
|
|
418
|
+
expected: `a string of at most ${BLOCKER_MAX} chars when outcome=blocked`,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
if (raw.objective_refinement !== undefined) {
|
|
423
|
+
const refinement = raw.objective_refinement;
|
|
424
|
+
if (typeof refinement !== "string" || refinement.trim().length === 0) {
|
|
425
|
+
invalid.push({
|
|
426
|
+
field: "objective_refinement",
|
|
427
|
+
expected: "a non-empty string",
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
else if (refinement.length > OBJECTIVE_REFINEMENT_MAX) {
|
|
431
|
+
invalid.push({
|
|
432
|
+
field: "objective_refinement",
|
|
433
|
+
expected: `a string of at most ${OBJECTIVE_REFINEMENT_MAX} chars`,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
528
437
|
return { missing, invalid };
|
|
529
438
|
}
|
|
530
439
|
/** v3.7.1: Strict structural checks for the sub-goal protocol fields
|
|
531
440
|
* (subgoal_updates / emerged_subtasks). Runs in the same pre-advance
|
|
532
441
|
* evaluation_invalid boundary as the four core fields: an error here is a
|
|
533
442
|
* payload defect, never a work-quality rejection — no session state, no
|
|
534
|
-
* gates, no rejection counters, retry with the same
|
|
443
|
+
* gates, no rejection counters, retry with the same submissionHandle.
|
|
535
444
|
*
|
|
536
445
|
* Shape-only (no vault): entry shape, id pattern, status enum, caps, and
|
|
537
446
|
* the sg-XXXXXXXX creation prohibition on emerged_subtasks. Referential
|
|
@@ -606,47 +515,51 @@ export function validateSubGoalUpdatesShape(raw) {
|
|
|
606
515
|
});
|
|
607
516
|
return errors;
|
|
608
517
|
}
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
});
|
|
623
|
-
return;
|
|
518
|
+
const CONTRACT_ID_RE = /^rc-[a-f0-9]{8}$/;
|
|
519
|
+
export const CONTRACT_SUPERSEDE_REASON_MAX = 500;
|
|
520
|
+
function declaresBlockedRaw(raw) {
|
|
521
|
+
return raw.outcome === "blocked";
|
|
522
|
+
}
|
|
523
|
+
/** v4.0.6: LENIENT parser for the `supersedes` modifier inside a committed
|
|
524
|
+
* Round Contract proposal. Historical/externally written data produces a
|
|
525
|
+
* diagnostic and is dropped; it never closes a contract and never throws.
|
|
526
|
+
* The strict submission boundary is the only commit check. */
|
|
527
|
+
export function parseContractSupersede(raw, sink, path = "round_contract.supersedes") {
|
|
528
|
+
if (raw === undefined)
|
|
529
|
+
return undefined;
|
|
530
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
531
|
+
sink?.push(path, "invalid_entry", "dropped", "expected a { contract_id, reason? } object; the value was dropped");
|
|
532
|
+
return undefined;
|
|
624
533
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
});
|
|
534
|
+
const obj = raw;
|
|
535
|
+
for (const key of Object.keys(obj)) {
|
|
536
|
+
if (key !== "contract_id" && key !== "reason") {
|
|
537
|
+
sink?.push(`${path}.${key}`, "unknown_field", "dropped", "not a declared supersedes field; it was dropped");
|
|
538
|
+
}
|
|
631
539
|
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
540
|
+
if (typeof obj.contract_id !== "string" ||
|
|
541
|
+
!CONTRACT_ID_RE.test(obj.contract_id.trim())) {
|
|
542
|
+
sink?.push(path, "invalid_id", "dropped", "contract_id must be an rc-XXXXXXXX id; the redirect was dropped");
|
|
543
|
+
return undefined;
|
|
544
|
+
}
|
|
545
|
+
const contractId = obj.contract_id.trim();
|
|
546
|
+
let reason;
|
|
547
|
+
if (obj.reason !== undefined) {
|
|
548
|
+
if (typeof obj.reason !== "string") {
|
|
549
|
+
sink?.push(`${path}.reason`, "invalid_value", "dropped", "expected a string; the reason was dropped");
|
|
640
550
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
field: "round_contract",
|
|
645
|
-
reason: "scope_outside_workspace",
|
|
646
|
-
detail: `scope entry ${index} "${entry.trim()}" ${detail}`,
|
|
647
|
-
});
|
|
551
|
+
else if (obj.reason.length > CONTRACT_SUPERSEDE_REASON_MAX) {
|
|
552
|
+
sink?.push(`${path}.reason`, "truncated", "truncated", `longer than ${CONTRACT_SUPERSEDE_REASON_MAX} chars; truncated to the limit`);
|
|
553
|
+
reason = obj.reason.slice(0, CONTRACT_SUPERSEDE_REASON_MAX);
|
|
648
554
|
}
|
|
649
|
-
|
|
555
|
+
else {
|
|
556
|
+
reason = obj.reason;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return {
|
|
560
|
+
contract_id: contractId,
|
|
561
|
+
...(reason !== undefined ? { reason } : {}),
|
|
562
|
+
};
|
|
650
563
|
}
|
|
651
564
|
function validateContractItem(item, index, context, errors) {
|
|
652
565
|
if (typeof item !== "object" || item === null || Array.isArray(item)) {
|
|
@@ -658,6 +571,15 @@ function validateContractItem(item, index, context, errors) {
|
|
|
658
571
|
return;
|
|
659
572
|
}
|
|
660
573
|
const entry = item;
|
|
574
|
+
for (const key of Object.keys(entry)) {
|
|
575
|
+
if (!["description", "criterion_refs", "verify_with"].includes(key)) {
|
|
576
|
+
errors.push({
|
|
577
|
+
field: "round_contract",
|
|
578
|
+
reason: "unknown_field",
|
|
579
|
+
detail: `item ${index} has unknown field "${key}"`,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
}
|
|
661
583
|
if (typeof entry.description !== "string" || entry.description.trim().length === 0) {
|
|
662
584
|
errors.push({
|
|
663
585
|
field: "round_contract",
|
|
@@ -665,6 +587,14 @@ function validateContractItem(item, index, context, errors) {
|
|
|
665
587
|
detail: `item ${index} needs a non-empty description`,
|
|
666
588
|
});
|
|
667
589
|
}
|
|
590
|
+
else if (entry.description.trim().length > CONTRACT_LIMITS.itemDescription) {
|
|
591
|
+
errors.push({
|
|
592
|
+
field: "round_contract",
|
|
593
|
+
reason: "item_description_too_long",
|
|
594
|
+
detail: `item ${index} description may be at most ` +
|
|
595
|
+
`${CONTRACT_LIMITS.itemDescription} chars`,
|
|
596
|
+
});
|
|
597
|
+
}
|
|
668
598
|
const verifyWith = entry.verify_with;
|
|
669
599
|
if (!Array.isArray(verifyWith) || verifyWith.length === 0) {
|
|
670
600
|
errors.push({
|
|
@@ -711,62 +641,155 @@ function validateContractItem(item, index, context, errors) {
|
|
|
711
641
|
`${CONTRACT_LIMITS.criterionRefs} ids (got ${criterionRefs.length})`,
|
|
712
642
|
});
|
|
713
643
|
}
|
|
644
|
+
const seen = new Set();
|
|
714
645
|
criterionRefs.forEach((ref, refIndex) => {
|
|
715
|
-
if (typeof ref !== "string" || ref.trim()
|
|
646
|
+
if (typeof ref !== "string" || !isCriterionId(ref.trim())) {
|
|
716
647
|
errors.push({
|
|
717
648
|
field: "round_contract",
|
|
718
649
|
reason: "invalid_criterion_ref",
|
|
719
|
-
detail: `item ${index} criterion_refs entry ${refIndex} must be a
|
|
650
|
+
detail: `item ${index} criterion_refs entry ${refIndex} must be a cr-XXXXXXXX id`,
|
|
720
651
|
});
|
|
721
|
-
|
|
722
|
-
});
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
const subgoalRefs = entry.subgoal_refs;
|
|
726
|
-
if (subgoalRefs !== undefined) {
|
|
727
|
-
if (!Array.isArray(subgoalRefs)) {
|
|
728
|
-
errors.push({
|
|
729
|
-
field: "round_contract",
|
|
730
|
-
reason: "invalid_subgoal_refs",
|
|
731
|
-
detail: `item ${index} subgoal_refs must be an array of sg-XXXXXXXX ids`,
|
|
732
|
-
});
|
|
733
|
-
}
|
|
734
|
-
else {
|
|
735
|
-
if (subgoalRefs.length > CONTRACT_LIMITS.subgoalRefs) {
|
|
736
|
-
errors.push({
|
|
737
|
-
field: "round_contract",
|
|
738
|
-
reason: "too_many_subgoal_refs",
|
|
739
|
-
detail: `item ${index} subgoal_refs may list at most ` +
|
|
740
|
-
`${CONTRACT_LIMITS.subgoalRefs} ids (got ${subgoalRefs.length})`,
|
|
741
|
-
});
|
|
742
|
-
}
|
|
743
|
-
const seen = new Set();
|
|
744
|
-
for (const ref of subgoalRefs) {
|
|
745
|
-
if (typeof ref !== "string" || !SUBGOAL_ID_RE.test(ref.trim())) {
|
|
746
|
-
errors.push({
|
|
747
|
-
field: "round_contract",
|
|
748
|
-
reason: "invalid_subgoal_ref",
|
|
749
|
-
detail: `item ${index} subgoal_refs entry "${String(ref)}" must be an sg-XXXXXXXX id`,
|
|
750
|
-
});
|
|
751
|
-
continue;
|
|
652
|
+
return;
|
|
752
653
|
}
|
|
753
654
|
const id = ref.trim();
|
|
754
655
|
if (seen.has(id)) {
|
|
755
656
|
errors.push({
|
|
756
657
|
field: "round_contract",
|
|
757
|
-
reason: "
|
|
758
|
-
detail: `item ${index}
|
|
658
|
+
reason: "duplicate_criterion_ref",
|
|
659
|
+
detail: `item ${index} criterion_refs repeats ${id}`,
|
|
759
660
|
});
|
|
760
661
|
}
|
|
761
662
|
seen.add(id);
|
|
762
|
-
if (context.
|
|
663
|
+
if (context.knownCriterionIds && !context.knownCriterionIds.has(id)) {
|
|
763
664
|
errors.push({
|
|
764
665
|
field: "round_contract",
|
|
765
|
-
reason: "
|
|
766
|
-
detail: `item ${index} references ${id}, which is not
|
|
666
|
+
reason: "unknown_criterion_ref",
|
|
667
|
+
detail: `item ${index} references ${id}, which is not an active criterion of this loop`,
|
|
767
668
|
});
|
|
768
669
|
}
|
|
769
|
-
}
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
/** v3.9.4: the normalized content identity of a RAW proposal item. Malformed
|
|
675
|
+
* items return null (their own shape errors already fire); valid items use
|
|
676
|
+
* the same canonical text as `deriveContractItemIds`. */
|
|
677
|
+
function rawContractItemIdentity(item) {
|
|
678
|
+
if (typeof item !== "object" || item === null || Array.isArray(item))
|
|
679
|
+
return null;
|
|
680
|
+
const entry = item;
|
|
681
|
+
const strings = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
682
|
+
return canonicalContractItemText({
|
|
683
|
+
description: typeof entry.description === "string" ? entry.description : "",
|
|
684
|
+
criterion_refs: strings(entry.criterion_refs),
|
|
685
|
+
verify_with: strings(entry.verify_with),
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
/** v4.0.6: strict `round_contract.supersedes` boundary. Every failure is
|
|
689
|
+
* `contract_invalid`; the same submissionHandle may retry with a corrected
|
|
690
|
+
* payload. The rules migrated from the former standalone
|
|
691
|
+
* `contract_transition` field:
|
|
692
|
+
* - the target must exist and equal the ACTIVE contract id;
|
|
693
|
+
* - the successor (this same proposal) must carry different acceptance items;
|
|
694
|
+
* - a redirect cannot be combined with a blocked outcome or a stop.
|
|
695
|
+
* `scope`/`work_item` differences alone are not a new slice. */
|
|
696
|
+
function validateContractSupersede(raw, proposal, context, errors) {
|
|
697
|
+
const value = proposal.supersedes;
|
|
698
|
+
if (value === undefined)
|
|
699
|
+
return;
|
|
700
|
+
const field = "round_contract.supersedes";
|
|
701
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
702
|
+
errors.push({
|
|
703
|
+
field,
|
|
704
|
+
reason: "not_object",
|
|
705
|
+
detail: "round_contract.supersedes must be a { contract_id, reason? } object",
|
|
706
|
+
});
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
const obj = value;
|
|
710
|
+
for (const key of Object.keys(obj)) {
|
|
711
|
+
if (key !== "contract_id" && key !== "reason") {
|
|
712
|
+
errors.push({
|
|
713
|
+
field,
|
|
714
|
+
reason: "unknown_field",
|
|
715
|
+
detail: `round_contract.supersedes has unknown field "${key}"`,
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
const contractId = typeof obj.contract_id === "string"
|
|
720
|
+
? obj.contract_id.trim()
|
|
721
|
+
: "";
|
|
722
|
+
if (!CONTRACT_ID_RE.test(contractId)) {
|
|
723
|
+
errors.push({
|
|
724
|
+
field,
|
|
725
|
+
reason: "invalid_contract_id",
|
|
726
|
+
detail: "round_contract.supersedes.contract_id must be an rc-XXXXXXXX id",
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
if (obj.reason !== undefined &&
|
|
730
|
+
(typeof obj.reason !== "string" ||
|
|
731
|
+
obj.reason.length > CONTRACT_SUPERSEDE_REASON_MAX)) {
|
|
732
|
+
errors.push({
|
|
733
|
+
field,
|
|
734
|
+
reason: "invalid_reason",
|
|
735
|
+
detail: `round_contract.supersedes.reason must be a string of at most ` +
|
|
736
|
+
`${CONTRACT_SUPERSEDE_REASON_MAX} chars`,
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
if (declaresBlockedRaw(raw)) {
|
|
740
|
+
errors.push({
|
|
741
|
+
field,
|
|
742
|
+
reason: "supersede_conflicts_with_blocked",
|
|
743
|
+
detail: "a contract redirect cannot be combined with a blocked outcome",
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
if (raw.should_continue === false) {
|
|
747
|
+
errors.push({
|
|
748
|
+
field,
|
|
749
|
+
reason: "supersede_on_stop",
|
|
750
|
+
detail: "a contract redirect activates a successor next round; it cannot be submitted with should_continue=false",
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
if (context.observed === false)
|
|
754
|
+
return;
|
|
755
|
+
if (context.activeContractId === undefined)
|
|
756
|
+
return;
|
|
757
|
+
if (context.activeContractId === null) {
|
|
758
|
+
errors.push({
|
|
759
|
+
field,
|
|
760
|
+
reason: "no_active_contract",
|
|
761
|
+
detail: "there is no active contract to supersede",
|
|
762
|
+
});
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
if (contractId !== context.activeContractId) {
|
|
766
|
+
errors.push({
|
|
767
|
+
field,
|
|
768
|
+
reason: "supersede_target_mismatch",
|
|
769
|
+
detail: `round_contract.supersedes.contract_id must equal the active contract ` +
|
|
770
|
+
`(${context.activeContractId})`,
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
// "Substantive change" means the ACCEPTANCE CONTENT changed. A new
|
|
774
|
+
// work_item/scope label alone is not a new slice.
|
|
775
|
+
const activeIdentities = context.activeItemIdentities;
|
|
776
|
+
const rawItems = proposal.items;
|
|
777
|
+
if (activeIdentities && Array.isArray(rawItems)) {
|
|
778
|
+
const identities = new Set();
|
|
779
|
+
for (const item of rawItems) {
|
|
780
|
+
const identity = rawContractItemIdentity(item);
|
|
781
|
+
if (identity !== null)
|
|
782
|
+
identities.add(identity);
|
|
783
|
+
}
|
|
784
|
+
if (identities.size > 0 &&
|
|
785
|
+
identities.size === activeIdentities.size &&
|
|
786
|
+
[...identities].every((identity) => activeIdentities.has(identity))) {
|
|
787
|
+
errors.push({
|
|
788
|
+
field,
|
|
789
|
+
reason: "supersede_without_change",
|
|
790
|
+
detail: "the successor's acceptance items are identical to the active " +
|
|
791
|
+
"contract's; changing only work_item/scope is not a new slice",
|
|
792
|
+
});
|
|
770
793
|
}
|
|
771
794
|
}
|
|
772
795
|
}
|
|
@@ -774,13 +797,14 @@ function validateContractItem(item, index, context, errors) {
|
|
|
774
797
|
* the contract item claims — the same pre-advance contract as
|
|
775
798
|
* subgoal_updates: an error here is a payload defect, never a work-quality
|
|
776
799
|
* rejection (no session state, no gates, no rejection counters, retry with
|
|
777
|
-
* the same
|
|
800
|
+
* the same submissionHandle as `contract_invalid`).
|
|
778
801
|
*
|
|
779
802
|
* Declaration strictness: items must exist and stay within CONTRACT_LIMITS;
|
|
780
803
|
* every item must bind at least one configured, enabled, after-capable
|
|
781
|
-
* command;
|
|
782
|
-
* sub-goal references must
|
|
783
|
-
*
|
|
804
|
+
* command; criterion and sub-goal references must be well-formed, and
|
|
805
|
+
* sub-goal references must name a sub-goal that actually exists. `scope` is
|
|
806
|
+
* deliberately NOT part of this boundary (v3.9.9): it is optional,
|
|
807
|
+
* report-only intent and is normalized leniently. The limits are enforced HERE rather than
|
|
784
808
|
* by silently truncating in the lenient parser — a declaration is a
|
|
785
809
|
* boundary, not a suggestion.
|
|
786
810
|
*
|
|
@@ -799,7 +823,47 @@ export function validateContractShape(raw, context) {
|
|
|
799
823
|
}
|
|
800
824
|
else {
|
|
801
825
|
const obj = contract;
|
|
802
|
-
|
|
826
|
+
for (const key of Object.keys(obj)) {
|
|
827
|
+
if (!["work_item", "scope", "supersedes", "items"].includes(key)) {
|
|
828
|
+
errors.push({
|
|
829
|
+
field: "round_contract",
|
|
830
|
+
reason: "unknown_field",
|
|
831
|
+
detail: `round_contract has unknown field "${key}"`,
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
if (obj.work_item === undefined) {
|
|
836
|
+
errors.push({
|
|
837
|
+
field: "round_contract",
|
|
838
|
+
reason: "missing_work_item",
|
|
839
|
+
detail: "a contract must name the deliverable in work_item",
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
else if (typeof obj.work_item !== "string") {
|
|
843
|
+
errors.push({
|
|
844
|
+
field: "round_contract",
|
|
845
|
+
reason: "invalid_work_item",
|
|
846
|
+
detail: "work_item must be a string",
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
else if (obj.work_item.trim().length === 0) {
|
|
850
|
+
errors.push({
|
|
851
|
+
field: "round_contract",
|
|
852
|
+
reason: "empty_work_item",
|
|
853
|
+
detail: "work_item must not be empty",
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
else if (obj.work_item.trim().length > CONTRACT_LIMITS.workItem) {
|
|
857
|
+
errors.push({
|
|
858
|
+
field: "round_contract",
|
|
859
|
+
reason: "work_item_too_long",
|
|
860
|
+
detail: `work_item may be at most ${CONTRACT_LIMITS.workItem} chars`,
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
// v3.9.9: `scope` is optional, report-only intent. It is normalized
|
|
864
|
+
// leniently by parseRoundContract (trim, cap, drop non-strings) and no
|
|
865
|
+
// longer participates in this strict boundary — no required key, no
|
|
866
|
+
// workspace containment, no whole-contract rejection.
|
|
803
867
|
const items = obj.items;
|
|
804
868
|
if (!Array.isArray(items) || items.length === 0) {
|
|
805
869
|
errors.push({
|
|
@@ -816,21 +880,42 @@ export function validateContractShape(raw, context) {
|
|
|
816
880
|
});
|
|
817
881
|
}
|
|
818
882
|
else {
|
|
883
|
+
const seenItems = new Map();
|
|
819
884
|
items.forEach((item, index) => {
|
|
820
885
|
validateContractItem(item, index, context, errors);
|
|
886
|
+
// v3.9.4: two identical normalized items would derive two rci- ids
|
|
887
|
+
// for one semantic object. Reject the restatement instead of
|
|
888
|
+
// letting a citation become ambiguous.
|
|
889
|
+
const identity = rawContractItemIdentity(item);
|
|
890
|
+
if (identity === null)
|
|
891
|
+
return;
|
|
892
|
+
const first = seenItems.get(identity);
|
|
893
|
+
if (first !== undefined) {
|
|
894
|
+
errors.push({
|
|
895
|
+
field: "round_contract",
|
|
896
|
+
reason: "duplicate_contract_item",
|
|
897
|
+
detail: `item ${index} duplicates item ${first} by normalized content`,
|
|
898
|
+
});
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
seenItems.set(identity, index);
|
|
821
902
|
});
|
|
822
903
|
}
|
|
904
|
+
validateContractSupersede(raw, obj, context, errors);
|
|
823
905
|
}
|
|
824
906
|
}
|
|
907
|
+
// v4.0.6: `execution_report.claims` is the ONE claims channel. Only `rci-`
|
|
908
|
+
// entries are STRICT here (format, uniqueness, ACTIVE item membership);
|
|
909
|
+
// `cr-` entries stay advisory and are normalized by the lenient parser.
|
|
825
910
|
const report = raw.execution_report;
|
|
826
911
|
if (typeof report === "object" && report !== null && !Array.isArray(report)) {
|
|
827
|
-
const claims = report.
|
|
912
|
+
const claims = report.claims;
|
|
828
913
|
if (claims !== undefined) {
|
|
829
914
|
if (!Array.isArray(claims)) {
|
|
830
915
|
errors.push({
|
|
831
|
-
field: "execution_report.
|
|
916
|
+
field: "execution_report.claims",
|
|
832
917
|
reason: "not_array",
|
|
833
|
-
detail: "
|
|
918
|
+
detail: "execution_report.claims must be an array of { id, outcome } entries",
|
|
834
919
|
});
|
|
835
920
|
}
|
|
836
921
|
else {
|
|
@@ -838,41 +923,44 @@ export function validateContractShape(raw, context) {
|
|
|
838
923
|
claims.forEach((claim, index) => {
|
|
839
924
|
if (typeof claim !== "object" || claim === null || Array.isArray(claim)) {
|
|
840
925
|
errors.push({
|
|
841
|
-
field: "execution_report.
|
|
926
|
+
field: "execution_report.claims",
|
|
842
927
|
reason: "invalid_entry",
|
|
843
928
|
detail: `entry ${index} is not an object`,
|
|
844
929
|
});
|
|
845
930
|
return;
|
|
846
931
|
}
|
|
847
932
|
const entry = claim;
|
|
848
|
-
const
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
reason: "invalid_item_id",
|
|
853
|
-
detail: `entry ${index} must reference an rci-XXXXXXXX contract item id`,
|
|
854
|
-
});
|
|
855
|
-
}
|
|
856
|
-
else {
|
|
857
|
-
if (seen.has(id)) {
|
|
933
|
+
const rawId = typeof entry.id === "string" ? entry.id.trim() : "";
|
|
934
|
+
const strictItem = rawId.startsWith("rci-");
|
|
935
|
+
if (strictItem) {
|
|
936
|
+
if (!/^rci-[a-f0-9]{8}$/.test(rawId)) {
|
|
858
937
|
errors.push({
|
|
859
|
-
field: "execution_report.
|
|
860
|
-
reason: "
|
|
861
|
-
detail: `entry ${index}
|
|
938
|
+
field: "execution_report.claims",
|
|
939
|
+
reason: "invalid_item_id",
|
|
940
|
+
detail: `entry ${index} must reference an rci-XXXXXXXX contract item id`,
|
|
862
941
|
});
|
|
863
942
|
}
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
943
|
+
else {
|
|
944
|
+
if (seen.has(rawId)) {
|
|
945
|
+
errors.push({
|
|
946
|
+
field: "execution_report.claims",
|
|
947
|
+
reason: "duplicate_item_id",
|
|
948
|
+
detail: `entry ${index} repeats item id ${rawId}`,
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
seen.add(rawId);
|
|
952
|
+
if (context.activeItemIds && !context.activeItemIds.has(rawId)) {
|
|
953
|
+
errors.push({
|
|
954
|
+
field: "execution_report.claims",
|
|
955
|
+
reason: "unknown_item_id",
|
|
956
|
+
detail: `entry ${index} references ${rawId}, which is not an item of the ACTIVE contract`,
|
|
957
|
+
});
|
|
958
|
+
}
|
|
871
959
|
}
|
|
872
960
|
}
|
|
873
961
|
if (entry.outcome !== "met" && entry.outcome !== "remaining") {
|
|
874
962
|
errors.push({
|
|
875
|
-
field: "execution_report.
|
|
963
|
+
field: "execution_report.claims",
|
|
876
964
|
reason: "invalid_outcome",
|
|
877
965
|
detail: `entry ${index} outcome must be "met" or "remaining"`,
|
|
878
966
|
});
|
|
@@ -891,12 +979,18 @@ export function validateContractShape(raw, context) {
|
|
|
891
979
|
export function buildSelfEvaluation(raw, sink, facts) {
|
|
892
980
|
const executionReport = parseExecutionReport(raw.execution_report, sink, facts?.knownCriterionRefs);
|
|
893
981
|
const retractedConstraints = boundedStringArray(raw.retracted_constraints, 50, 500, sink, "retracted_constraints");
|
|
894
|
-
const revisedCriteria = parseCriterionRevisions(raw.revised_success_criteria, sink);
|
|
895
982
|
const wrongAssumptions = boundedStringArray(raw.wrong_assumptions, 50, 500, sink, "wrong_assumptions");
|
|
983
|
+
// v4.0.2: strictly report-only. The window/caps are the field's contract,
|
|
984
|
+
// not policy: at most 3 entries, 300 chars each.
|
|
985
|
+
const openQuestions = boundedStringArray(raw.open_questions, 3, 300, sink, "open_questions");
|
|
986
|
+
const resolvedOpenQuestionIds = boundedStringArray(raw.resolved_open_question_ids, 20, 80, sink, "resolved_open_question_ids")
|
|
987
|
+
.filter((value) => {
|
|
988
|
+
if (isOpenQuestionId(value))
|
|
989
|
+
return true;
|
|
990
|
+
sink?.push("resolved_open_question_ids", "invalid_value", "dropped", `invalid open-question id: ${value}`);
|
|
991
|
+
return false;
|
|
992
|
+
});
|
|
896
993
|
const workerResults = parseWorkerResults(raw.worker_results, sink);
|
|
897
|
-
if (raw.success !== undefined && typeof raw.success !== "boolean") {
|
|
898
|
-
sink?.push("success", "invalid_value", "dropped", "expected a boolean; normalized to false (core validation rejects it)");
|
|
899
|
-
}
|
|
900
994
|
if (raw.output_summary !== undefined && typeof raw.output_summary !== "string") {
|
|
901
995
|
sink?.push("output_summary", "invalid_value", "dropped", "expected a string; normalized to the empty string (core validation rejects it)");
|
|
902
996
|
}
|
|
@@ -919,29 +1013,38 @@ export function buildSelfEvaluation(raw, sink, facts) {
|
|
|
919
1013
|
if (raw.should_continue !== undefined && typeof raw.should_continue !== "boolean") {
|
|
920
1014
|
sink?.push("should_continue", "invalid_value", "dropped", "expected a boolean; normalized to true (core validation rejects it)");
|
|
921
1015
|
}
|
|
922
|
-
if (raw.stop_reason !== undefined &&
|
|
923
|
-
raw.stop_reason !== "gave_up" && raw.stop_reason !== "blocked" &&
|
|
924
|
-
raw.stop_reason !== "needs_human_input") {
|
|
925
|
-
sink?.push("stop_reason", "invalid_enum", "dropped", "expected gave_up | blocked | needs_human_input; the value was dropped");
|
|
926
|
-
}
|
|
927
1016
|
if (raw.outcome !== undefined &&
|
|
928
1017
|
raw.outcome !== "success" && raw.outcome !== "partial" &&
|
|
929
1018
|
raw.outcome !== "failed" && raw.outcome !== "blocked") {
|
|
930
|
-
sink?.push("outcome", "invalid_enum", "dropped", "expected success | partial | failed | blocked; the value was dropped");
|
|
1019
|
+
sink?.push("outcome", "invalid_enum", "dropped", "expected success | partial | failed | blocked; the value was dropped (core validation rejects it)");
|
|
1020
|
+
}
|
|
1021
|
+
if (raw.objective_refinement !== undefined) {
|
|
1022
|
+
if (typeof raw.objective_refinement !== "string") {
|
|
1023
|
+
sink?.push("objective_refinement", "invalid_value", "dropped", "expected a string; the value was dropped (core validation rejects it)");
|
|
1024
|
+
}
|
|
1025
|
+
else if (raw.objective_refinement.trim().length === 0) {
|
|
1026
|
+
sink?.push("objective_refinement", DIAG_EMPTY_VALUE, "dropped", "empty objective_refinement was dropped (core validation rejects it)");
|
|
1027
|
+
}
|
|
1028
|
+
else if (raw.objective_refinement.length > OBJECTIVE_REFINEMENT_MAX) {
|
|
1029
|
+
sink?.push("objective_refinement", "truncated", "truncated", `longer than ${OBJECTIVE_REFINEMENT_MAX} chars; truncated to the limit (core validation rejects it)`);
|
|
1030
|
+
}
|
|
931
1031
|
}
|
|
932
|
-
if (raw.
|
|
933
|
-
typeof raw.
|
|
934
|
-
sink?.push("
|
|
1032
|
+
if (raw.subtask_boundary !== undefined &&
|
|
1033
|
+
typeof raw.subtask_boundary !== "boolean") {
|
|
1034
|
+
sink?.push("subtask_boundary", "invalid_value", "dropped", "expected a boolean; normalized to false");
|
|
935
1035
|
}
|
|
936
1036
|
if (raw.blocker !== undefined) {
|
|
937
|
-
if (
|
|
938
|
-
sink?.push("blocker", "
|
|
1037
|
+
if (raw.outcome !== "blocked") {
|
|
1038
|
+
sink?.push("blocker", "not_applicable", "dropped", `blocker is meaningful only when outcome=blocked (outcome=${typeof raw.outcome === "string" ? raw.outcome : "missing"}); the value was dropped`);
|
|
1039
|
+
}
|
|
1040
|
+
else if (typeof raw.blocker !== "string") {
|
|
1041
|
+
sink?.push("blocker", "invalid_value", "dropped", "expected a string; the value was dropped (core validation rejects it)");
|
|
939
1042
|
}
|
|
940
1043
|
else if (raw.blocker.trim().length === 0) {
|
|
941
|
-
sink?.push("blocker", DIAG_EMPTY_VALUE, "dropped", "empty blocker was dropped");
|
|
1044
|
+
sink?.push("blocker", DIAG_EMPTY_VALUE, "dropped", "empty blocker was dropped (core validation rejects it)");
|
|
942
1045
|
}
|
|
943
|
-
else if (raw.blocker.length >
|
|
944
|
-
sink?.push("blocker", "truncated", "truncated",
|
|
1046
|
+
else if (raw.blocker.length > BLOCKER_MAX) {
|
|
1047
|
+
sink?.push("blocker", "truncated", "truncated", `longer than ${BLOCKER_MAX} chars; truncated to the limit (core validation rejects it)`);
|
|
945
1048
|
}
|
|
946
1049
|
}
|
|
947
1050
|
const noChangeReason = boundedString(raw.no_change_reason, 200, sink, "no_change_reason");
|
|
@@ -949,35 +1052,35 @@ export function buildSelfEvaluation(raw, sink, facts) {
|
|
|
949
1052
|
sink?.push("no_change_reason", DIAG_EMPTY_VALUE, "dropped", "empty reason was dropped");
|
|
950
1053
|
}
|
|
951
1054
|
return makeSelfEvaluation({
|
|
952
|
-
|
|
1055
|
+
outcome: raw.outcome === "success" || raw.outcome === "partial" ||
|
|
1056
|
+
raw.outcome === "failed" || raw.outcome === "blocked"
|
|
1057
|
+
? raw.outcome
|
|
1058
|
+
: "failed",
|
|
953
1059
|
output_summary: typeof raw.output_summary === "string" ? raw.output_summary : "",
|
|
954
1060
|
constraint_violations: constraintViolations,
|
|
955
1061
|
should_continue: typeof raw.should_continue === "boolean" ? raw.should_continue : true,
|
|
956
|
-
discovered_constraints: boundedStringArray(raw.discovered_constraints, 50, 500, sink, "discovered_constraints"),
|
|
957
|
-
objective_refinement: boundedString(raw.objective_refinement, 1000, sink, "objective_refinement") ?? "",
|
|
1062
|
+
discovered_constraints: boundedStringArray(raw.discovered_constraints, facts?.maxDiscoveredConstraints ?? 50, 500, sink, "discovered_constraints"),
|
|
958
1063
|
emerged_subtasks: boundedEmergedSubtasks(raw.emerged_subtasks, sink),
|
|
959
1064
|
execution_report: executionReport,
|
|
960
1065
|
retracted_constraints: retractedConstraints,
|
|
961
|
-
revised_success_criteria: revisedCriteria,
|
|
962
1066
|
wrong_assumptions: wrongAssumptions,
|
|
1067
|
+
open_questions: openQuestions,
|
|
1068
|
+
resolved_open_question_ids: resolvedOpenQuestionIds,
|
|
963
1069
|
worker_results: workerResults,
|
|
964
|
-
|
|
965
|
-
|
|
1070
|
+
subtask_boundary: typeof raw.subtask_boundary === "boolean" ? raw.subtask_boundary : false,
|
|
1071
|
+
subtask_boundary_label: boundedString(raw.subtask_boundary_label, 200, sink, "subtask_boundary_label") ?? "",
|
|
966
1072
|
subgoal_updates: parseSubGoalUpdates(raw.subgoal_updates, sink),
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
outcome: raw.outcome === "success" || raw.outcome === "partial" ||
|
|
971
|
-
raw.outcome === "failed" || raw.outcome === "blocked"
|
|
972
|
-
? raw.outcome
|
|
1073
|
+
objective_refinement: typeof raw.objective_refinement === "string" &&
|
|
1074
|
+
raw.objective_refinement.trim().length > 0
|
|
1075
|
+
? raw.objective_refinement.trim().slice(0, OBJECTIVE_REFINEMENT_MAX)
|
|
973
1076
|
: undefined,
|
|
974
|
-
blocker:
|
|
975
|
-
|
|
1077
|
+
blocker: raw.outcome === "blocked" &&
|
|
1078
|
+
typeof raw.blocker === "string" && raw.blocker.trim().length > 0
|
|
1079
|
+
? raw.blocker.slice(0, BLOCKER_MAX)
|
|
976
1080
|
: undefined,
|
|
977
1081
|
gate_ids: boundedStringArray(raw.gate_ids, 20, 64, sink, "gate_ids"),
|
|
978
1082
|
retroactiveClaims: parseRetroactiveClaims(raw.retroactiveClaims, sink),
|
|
979
1083
|
no_change_reason: noChangeReason || undefined,
|
|
980
|
-
prompt_requests: parsePromptRequests(raw.prompt_requests, sink),
|
|
981
1084
|
round_contract: parseRoundContract(raw.round_contract, sink),
|
|
982
1085
|
});
|
|
983
1086
|
}
|
|
@@ -1000,10 +1103,11 @@ export function normalizeSelfEvaluation(raw, facts) {
|
|
|
1000
1103
|
* (the lenient path, reached only after the strict boundary passed) truncates
|
|
1001
1104
|
* to the same numbers. One source, so the two can never drift. */
|
|
1002
1105
|
export const CONTRACT_LIMITS = {
|
|
1106
|
+
workItem: 200,
|
|
1107
|
+
itemDescription: 200,
|
|
1003
1108
|
items: 20,
|
|
1004
1109
|
scope: 50,
|
|
1005
1110
|
criterionRefs: 20,
|
|
1006
|
-
subgoalRefs: 20,
|
|
1007
1111
|
verifyWith: 20,
|
|
1008
1112
|
};
|
|
1009
1113
|
/** Parse a Round Contract PROPOSAL from raw JSON input. Lenient on shapes
|
|
@@ -1024,7 +1128,7 @@ export function parseRoundContract(raw, sink) {
|
|
|
1024
1128
|
}
|
|
1025
1129
|
const obj = raw;
|
|
1026
1130
|
for (const key of Object.keys(obj)) {
|
|
1027
|
-
if (!["work_item", "scope", "items"].includes(key)) {
|
|
1131
|
+
if (!["work_item", "scope", "supersedes", "items"].includes(key)) {
|
|
1028
1132
|
sink?.push(`round_contract.${key}`, "unknown_field", "dropped", "not a declared contract field; it was dropped");
|
|
1029
1133
|
}
|
|
1030
1134
|
}
|
|
@@ -1074,7 +1178,7 @@ export function parseRoundContract(raw, sink) {
|
|
|
1074
1178
|
.slice(0, CONTRACT_LIMITS.items)
|
|
1075
1179
|
.map((item, index) => {
|
|
1076
1180
|
for (const key of Object.keys(item)) {
|
|
1077
|
-
if (!["description", "criterion_refs", "
|
|
1181
|
+
if (!["description", "criterion_refs", "verify_with"].includes(key)) {
|
|
1078
1182
|
sink?.push(`round_contract.items[${index}].${key}`, "unknown_field", "dropped", "not a declared contract-item field; it was dropped");
|
|
1079
1183
|
}
|
|
1080
1184
|
}
|
|
@@ -1082,15 +1186,14 @@ export function parseRoundContract(raw, sink) {
|
|
|
1082
1186
|
sink?.push(`round_contract.items[${index}].description`, "invalid_value", "dropped", "expected a string; the description was dropped");
|
|
1083
1187
|
}
|
|
1084
1188
|
if (typeof item.description === "string" &&
|
|
1085
|
-
item.description.trim().length >
|
|
1086
|
-
sink?.push(`round_contract.items[${index}].description`, "truncated", "truncated",
|
|
1189
|
+
item.description.trim().length > CONTRACT_LIMITS.itemDescription) {
|
|
1190
|
+
sink?.push(`round_contract.items[${index}].description`, "truncated", "truncated", `longer than ${CONTRACT_LIMITS.itemDescription} chars; truncated to the limit`);
|
|
1087
1191
|
}
|
|
1088
1192
|
return {
|
|
1089
1193
|
description: typeof item.description === "string"
|
|
1090
|
-
? item.description.trim().slice(0,
|
|
1194
|
+
? item.description.trim().slice(0, CONTRACT_LIMITS.itemDescription)
|
|
1091
1195
|
: "",
|
|
1092
1196
|
criterion_refs: capStrings(item.criterion_refs, 200, CONTRACT_LIMITS.criterionRefs, `round_contract.items[${index}].criterion_refs`),
|
|
1093
|
-
subgoal_refs: capStrings(item.subgoal_refs, 64, CONTRACT_LIMITS.subgoalRefs, `round_contract.items[${index}].subgoal_refs`),
|
|
1094
1197
|
verify_with: capStrings(item.verify_with, 200, CONTRACT_LIMITS.verifyWith, `round_contract.items[${index}].verify_with`),
|
|
1095
1198
|
};
|
|
1096
1199
|
});
|
|
@@ -1107,52 +1210,20 @@ export function parseRoundContract(raw, sink) {
|
|
|
1107
1210
|
else if (typeof obj.work_item === "string" && obj.work_item.trim().length === 0) {
|
|
1108
1211
|
sink?.push("round_contract.work_item", DIAG_EMPTY_VALUE, "dropped", "empty work_item was dropped");
|
|
1109
1212
|
}
|
|
1110
|
-
else if (typeof obj.work_item === "string" &&
|
|
1111
|
-
|
|
1213
|
+
else if (typeof obj.work_item === "string" &&
|
|
1214
|
+
obj.work_item.trim().length > CONTRACT_LIMITS.workItem) {
|
|
1215
|
+
sink?.push("round_contract.work_item", "truncated", "truncated", `longer than ${CONTRACT_LIMITS.workItem} chars; truncated to the limit`);
|
|
1112
1216
|
}
|
|
1217
|
+
const supersedes = parseContractSupersede(obj.supersedes, sink);
|
|
1113
1218
|
return {
|
|
1114
1219
|
work_item: typeof obj.work_item === "string" && obj.work_item.trim().length > 0
|
|
1115
|
-
? obj.work_item.trim().slice(0,
|
|
1220
|
+
? obj.work_item.trim().slice(0, CONTRACT_LIMITS.workItem)
|
|
1116
1221
|
: undefined,
|
|
1117
1222
|
scope: capStrings(obj.scope, 500, CONTRACT_LIMITS.scope, "round_contract.scope"),
|
|
1223
|
+
...(supersedes !== undefined ? { supersedes } : {}),
|
|
1118
1224
|
items,
|
|
1119
1225
|
};
|
|
1120
1226
|
}
|
|
1121
|
-
/** Parse a PromptRequests object from raw JSON input.
|
|
1122
|
-
* Lenient: missing or invalid fields get sensible defaults.
|
|
1123
|
-
* Returns undefined if the raw value is absent or not an object,
|
|
1124
|
-
* so makeSelfEvaluation can distinguish "not set" from "empty".
|
|
1125
|
-
* Exported for the engine's last_round_result rebuild — the MCP
|
|
1126
|
-
* lifecycle assembles prompt_requests into the request, but every
|
|
1127
|
-
* compile path funnels through engine.invokeLoopCompile, whose
|
|
1128
|
-
* field-by-field reconstruction must preserve them (v3.3.1). */
|
|
1129
|
-
export function parsePromptRequests(raw, sink) {
|
|
1130
|
-
if (raw === undefined)
|
|
1131
|
-
return undefined;
|
|
1132
|
-
if (raw === null) {
|
|
1133
|
-
sink?.push("prompt_requests", "invalid_value", "dropped", "expected an object with emphasize/confusion_points; null was dropped");
|
|
1134
|
-
return undefined;
|
|
1135
|
-
}
|
|
1136
|
-
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
1137
|
-
sink?.push("prompt_requests", "invalid_entry", "dropped", "expected an object with emphasize/confusion_points; the value was dropped");
|
|
1138
|
-
return undefined;
|
|
1139
|
-
}
|
|
1140
|
-
const obj = raw;
|
|
1141
|
-
for (const key of Object.keys(obj)) {
|
|
1142
|
-
if (key !== "emphasize" && key !== "confusion_points") {
|
|
1143
|
-
sink?.push(`prompt_requests.${key}`, "unknown_field", "dropped", "not a declared prompt-request field; it was dropped");
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
const emphasize = boundedStringArray(obj.emphasize, 5, 500, sink, "prompt_requests.emphasize");
|
|
1147
|
-
const confusionPoints = boundedStringArray(obj.confusion_points, 3, 200, sink, "prompt_requests.confusion_points");
|
|
1148
|
-
// Return undefined when all fields are empty — semantically equivalent
|
|
1149
|
-
// to "not set", avoids carrying an empty object through the pipeline.
|
|
1150
|
-
if (emphasize.length === 0 && confusionPoints.length === 0) {
|
|
1151
|
-
sink?.push("prompt_requests", DIAG_EMPTY_VALUE, "dropped", "emphasize and confusion_points are both empty; treated as not set");
|
|
1152
|
-
return undefined;
|
|
1153
|
-
}
|
|
1154
|
-
return { emphasize, confusion_points: confusionPoints };
|
|
1155
|
-
}
|
|
1156
1227
|
// Free-text inference is intentionally absent. Invalid core fields are
|
|
1157
1228
|
// rejected before round advancement and may be resubmitted with the same
|
|
1158
1229
|
// roundId without changing session, gate, metric, or vault state.
|