diff --git a/loopx/capabilities/explore/README.md b/loopx/capabilities/explore/README.md index da5fa850e2..759a057d22 100644 --- a/loopx/capabilities/explore/README.md +++ b/loopx/capabilities/explore/README.md @@ -31,6 +31,13 @@ attachments also require the same `input_revision`. Use a new finding id per evaluated candidate, and reuse an id only to correct that finding. Missing values are explicit `null`; zero and negative values remain valid measurements. +When an `execution_observation` declares a `result_ref`, each measurement must +cite that same result. This applies to manual findings and writeback attachments, +including shared comparison contexts. Listing both receipts in `evidence_refs` +does not bind a different result to the executed candidate. No result is inferred +when execution has not declared one. These consistency checks do not independently +verify a producer's calculator or turn `confirmed` into an evaluator attestation. + To reuse verified comparison conditions, save these three fields once in `conditions.json`: @@ -1413,3 +1420,35 @@ outcomes. Native research completion retains the graph lock and, when inheriting, the device-configuration lock until its transaction finishes. Inherited planning requires the same exact typed experiment evidence as an explicit Goal mode. + +Historical compatibility: stricter execution/measurement attribution is a new-write +rule. Canonical readers preserve earlier records and their identities, including +an inconsistent original followed by a correction. Current finding views expose +`measurement_error` and omit contradictory quantities from comparison; raw history +retains the producer's original statement. A matching correction becomes usable +without deleting its prior version. Explicit ordered-prefix archival replay keeps +the same audit boundary; ordinary single/batch writes still reject contradictions. +Malformed, unsafe, unknown-field or forged canonical records remain rejected. +Registered-source recovery applies the same structural validation to its selected +missing results, binds execution to the canonical snapshot under lock, and keeps +preview and execution consistent. It does not label that subset an archival prefix. +Completed delivery replays only the original durable ingestion intent, exact +source and recorded time, matching canonical finding and existing Todo link. +That path performs no graph or Todo mutation; missing delivery still requires the +ordinary write/ownership checks, and conflicting attachment retries reject. +CLI attachment parsing preserves structurally valid historical input until the +existing primary owner matches the immutable retry. New primary writes and new +ingestion intents still validate execution/measurement attribution before effects; +parsing is not a new-write eligibility decision or a public bypass. + +历史兼容:执行与测量的一致性约束限制新写入,不能阻断旧记录及其后续更正的读取。 +当前 finding 若有归因矛盾,会返回 `measurement_error` 并排除该数量比较;历史仍保留 +生产者的原始声明。正确更正可恢复当前比较,不能删改旧行。显式按顺序恢复归档前缀也 +保留此审计边界;普通单条和批量写入仍拒绝新矛盾,结构损坏、不安全及伪造记录仍拒绝。 +已登记来源的恢复对选中的缺失结果使用相同结构校验,在锁内绑定 canonical 快照, +保持预览与执行一致;不能把这一子集称为完整归档前缀。已完成交付仅重放原始持久化 +ingestion intent,并精确匹配来源、记录时间、canonical finding 与既有 Todo 关联; +此路径不写图或 Todo。未完成交付仍须遵守普通写入与归属检查,冲突附件重试仍拒绝。 +CLI 附件解析保留结构合法的历史输入,由既有 primary owner 精确匹配不可变重试; +新 primary 写入和新 ingestion intent 仍在产生效果前检查执行与测量归属。 +解析成功不代表新写入合格,也不提供公开的绕过参数。 diff --git a/loopx/capabilities/explore/measurements.py b/loopx/capabilities/explore/measurements.py index 8e7c7f53e1..3490a20873 100644 --- a/loopx/capabilities/explore/measurements.py +++ b/loopx/capabilities/explore/measurements.py @@ -21,10 +21,13 @@ def _normalize(encoded: str) -> str: return json.dumps(result["measurements"], sort_keys=True, allow_nan=False) -def normalize_measurements(value, *, measurement_context=_UNSET): +def normalize_measurements(value, *, measurement_context=_UNSET, execution_observation=_UNSET): validate_public_safe_value(value, path="measurements") request = {"measurements": value} if measurement_context is not _UNSET: validate_public_safe_value(measurement_context, path="measurement_context") request["measurement_context"] = measurement_context + if execution_observation is not _UNSET: + validate_public_safe_value(execution_observation, path="execution_observation") + request["execution_observation"] = execution_observation return json.loads(_normalize(json.dumps(request, sort_keys=True, allow_nan=False))) diff --git a/loopx/capabilities/explore/result_log.py b/loopx/capabilities/explore/result_log.py index 11c6c5a7f5..5400950124 100644 --- a/loopx/capabilities/explore/result_log.py +++ b/loopx/capabilities/explore/result_log.py @@ -352,6 +352,7 @@ def build_explore_finding_event( recorded_at: str | None = None, execution_observation: Mapping[str, Any] | None = None, measurements: Sequence[Mapping[str, Any]] | None = None, + _check_execution_measurements: bool = True, ) -> dict[str, Any]: safe_goal_id = _safe_goal_id(goal_id) safe_title = _compact_text(title, limit=TITLE_LIMIT, field="title") @@ -394,7 +395,10 @@ def build_explore_finding_event( if not node_id: raise ValueError("measurements require an explicit node") - measured = normalize_measurements(measurements) + measured = normalize_measurements( + measurements, **({"execution_observation": event.get("execution_observation")} + if _check_execution_measurements else {}), + ) if any(item["evidence_ref"] not in event.get("evidence_refs", []) for item in measured): raise ValueError("measurement evidence must be included in the finding's evidence_refs") event["measurements"] = measured @@ -456,6 +460,8 @@ def validate_explore_result_event( unknown fields, invalid ids, unsafe text, forged boundary flags, and stale event ids. Persisted text has its own bounded compatibility budget: it is validated and restored before hashing, never clipped to the writer default. + This structural read validation preserves older attribution contradictions; + writers reject new contradictions, and current views exclude their numbers. """ payload = dict(event) @@ -518,6 +524,10 @@ def validate_explore_result_event( supersedes=payload.get("supersedes"), execution_observation=payload.get("execution_observation"), measurements=payload.get("measurements"), + # Canonical history predates the stricter new-write rule. Keep + # original bytes/identities auditable; current views qualify the + # folded result separately, and append rechecks new writes below. + _check_execution_measurements=False, ) if summary: rebuilt["summary"] = summary @@ -527,8 +537,16 @@ def validate_explore_result_event( return rebuilt +def _check_finding_measurement_execution(event: Mapping[str, Any]) -> None: + if "measurements" in event: + from .measurements import normalize_measurements + + normalize_measurements(event["measurements"], execution_observation=event.get("execution_observation")) + + def append_explore_result_event(path: Path, event: Mapping[str, Any]) -> dict[str, Any]: validated = validate_explore_result_event(event) + _check_finding_measurement_execution(validated) log_path = path.expanduser() log_path.parent.mkdir(parents=True, exist_ok=True) with exclusive_file_lock(log_path): @@ -598,6 +616,35 @@ def append_explore_result_events( if require_ordered_prefix and create_only_node_ids: raise ValueError("ordered prefix replay cannot skip existing node revisions") validated = [validate_explore_result_event(event, expected_goal_id=expected_goal_id) for event in events] + # Ordered replay restores an audited historical prefix, not new evidence. + # Its old contradictions remain visible only as archival facts/diagnostics. + if not require_ordered_prefix: + for event in validated: + _check_finding_measurement_execution(event) + return _append_validated_explore_result_events( + path, validated, expected_goal_id=expected_goal_id, + create_only_node_ids=create_only_node_ids, + require_ordered_prefix=require_ordered_prefix, + expected_event_ids=expected_event_ids, + ) + + +def _append_validated_explore_result_events( + path: Path, + validated: Sequence[dict[str, Any]], + *, + expected_goal_id: str, + create_only_node_ids: Sequence[str] = (), + require_ordered_prefix: bool = False, + expected_event_ids: Sequence[str] | None = None, +) -> dict[str, Any]: + """Shared file effect after the owning caller qualifies the events. + + Ordinary writes validate new attribution above. Source reconciliation may + supply structurally validated history only after checking its registered + baseline, missing result ids and exact canonical snapshot. This internal + seam adds no public validation switch or historical-prefix claim. + """ requested_ids = [str(event["event_id"]) for event in validated] if len(requested_ids) != len(set(requested_ids)): raise ValueError("Explore result event batch contains duplicate event ids") @@ -616,6 +663,11 @@ def append_explore_result_events( if current_events[:len(validated)] != validated[:len(current_events)]: raise ValueError("Explore result history differs from the required initial prefix") existing_by_id = {str(row["event_id"]): row for row in current_events} + elif expected_event_ids is not None: + existing_by_id = { + str(row["event_id"]): row for row in + load_explore_result_events_strict(log_path, goal_id=expected_goal_id) + } elif log_path.exists(): for line in log_path.read_text(encoding="utf-8").split("\n"): try: @@ -841,10 +893,19 @@ def _materialized_parent_edges( def _finding_view(event: Mapping[str, Any]) -> dict[str, Any]: + measured: dict[str, Any] = {} + if "measurements" in event: + try: + _check_finding_measurement_execution(event) + except ValueError as error: + # Quarantine contradictory historical quantities in the current + # view, not the append-only history. Runtime failure propagates. + measured["measurement_error"] = str(error) + else: + measured.update(measurements=event["measurements"], recorded_order=event["_recorded_order"]) return { **({"execution_observation": event["execution_observation"]} if "execution_observation" in event else {}), - **({"measurements": event["measurements"], "recorded_order": event["_recorded_order"]} - if "measurements" in event else {}), + **measured, "finding_id": str(event.get("result_id") or ""), "finding": str(event.get("title") or ""), "summary": str(event.get("summary") or ""), diff --git a/loopx/capabilities/explore/result_writeback.py b/loopx/capabilities/explore/result_writeback.py index da47605858..f2beabd9ab 100644 --- a/loopx/capabilities/explore/result_writeback.py +++ b/loopx/capabilities/explore/result_writeback.py @@ -98,7 +98,8 @@ def _evolution_plan(attachment, projection, *, agent_id): }) -def _events(attachment, *, goal_id, agent_id, source_id, recorded_at, existing_node=None): +def _events(attachment, *, goal_id, agent_id, source_id, recorded_at, existing_node=None, + _check_execution_measurements=True): previous = existing_node or {} update = attachment.get("question_update") or {} node = build_explore_node_event( @@ -135,6 +136,7 @@ def _events(attachment, *, goal_id, agent_id, source_id, recorded_at, existing_n tags=["writeback-result"], execution_observation=attachment.get("execution_observation"), measurements=attachment.get("measurements"), + _check_execution_measurements=_check_execution_measurements, recorded_at=recorded_at, ) relations = [build_explore_edge_event( @@ -307,6 +309,11 @@ def deliver_result_attachment( if len(intents) != 1: raise ValueError("Explore result hook did not admit one ingestion intent") intent = intents[0] + # Exact old payloads remain readable; equivalent current shorthand + # still normalizes through the typed owner before comparing intents. + if (attachment != intent["payload"]["attachment"] + and normalize_result_attachment(attachment) != intent["payload"]["attachment"]): + raise ValueError("Explore attachment conflicts with its committed ingestion intent") # Readback-only replay after success needs no new lease or Todo write. existing_events = load_explore_result_events_strict( explore_result_log_path(runtime_root, goal_id), goal_id=goal_id @@ -317,6 +324,12 @@ def deliver_result_attachment( agent_id=agent_id, source_id=intent["idempotency_key"], recorded_at=committed_at, + # A durable terminal intent can describe an old writer's event. + # Structural reconstruction is only for exact canonical readback; + # a missing event still goes through strict preparation below. + _check_execution_measurements=( + "explore.result_writeback" not in dispatch.get("replayed_hooks", []) + ), ) detail = list_goal_todos( registry_path=registry_path, diff --git a/loopx/capabilities/explore/source_history_reconcile.py b/loopx/capabilities/explore/source_history_reconcile.py index 7846c3d264..1475cdc7d1 100644 --- a/loopx/capabilities/explore/source_history_reconcile.py +++ b/loopx/capabilities/explore/source_history_reconcile.py @@ -14,7 +14,7 @@ EVENT_KIND_FINDING, EVENT_KIND_NODE, PUBLIC_BOUNDARY, - append_explore_result_events, + _append_validated_explore_result_events, build_explore_result_projection, load_explore_result_events_strict, ) @@ -168,10 +168,14 @@ def reconcile_explore_source_history( } post_keys = canonical_keys if execute and selected_events: - append_receipt = append_explore_result_events( + # These are existing facts from the validated source, selected only for + # registered missing ids. They are neither new evidence nor an ordered + # archival prefix. Bind this plan to the canonical snapshot under lock. + append_receipt = _append_validated_explore_result_events( canonical_log_path, selected_events, expected_goal_id=goal_id, + expected_event_ids=[str(event["event_id"]) for event in canonical_events], ) post_events = load_explore_result_events_strict(canonical_log_path, goal_id=goal_id) post_event_ids = {str(event.get("event_id") or "") for event in post_events} diff --git a/loopx/control_plane/capabilities/explore_measurements.ts b/loopx/control_plane/capabilities/explore_measurements.ts index 2ed670896e..dc73bbba05 100644 --- a/loopx/control_plane/capabilities/explore_measurements.ts +++ b/loopx/control_plane/capabilities/explore_measurements.ts @@ -4,6 +4,7 @@ import {EffectRuntimeRequestError} from "../effect_runtime_errors.ts"; import {requireJsonObject, type JsonObject} from "../runtime_decode.ts"; import {isExploreEvidenceRef} from "./explore_evidence_ref.ts"; +import {normalizeExploreExecutionObservation} from "./explore_execution_observation.ts"; export interface ExploreMeasurement { metric: string; value: number | null; unit: string; higher_is_better: boolean; @@ -21,7 +22,7 @@ export interface ExploreMeasurementFeedback { improvement: number | null; measured_count: number; unknown_count: number; } -export function normalizeExploreMeasurements(value: unknown, context?: unknown): ExploreMeasurement[] { +export function normalizeExploreMeasurements(value: unknown, context?: unknown, executionObservation?: unknown): ExploreMeasurement[] { if (!Array.isArray(value) || !value.length || value.length > 8) { throw new EffectRuntimeRequestError("measurements requires one to eight explicit measurements"); } @@ -39,10 +40,10 @@ export function normalizeExploreMeasurements(value: unknown, context?: unknown): // Authoring only: persist the existing complete measurement, never a // mutable context reference or inferred comparison membership. return {...shared, ...observation}; - })); + }), undefined, executionObservation); } const seen = new Set(); - return value.map(item => { + const measurements = value.map(item => { const row = requireJsonObject(item, "measurement"); const fields = ["metric", "value", "unit", "higher_is_better", "comparison_scope", "input_revision", "evaluation_ref", "candidate_ref", "evidence_ref"]; @@ -76,6 +77,19 @@ export function normalizeExploreMeasurements(value: unknown, context?: unknown): seen.add(key); return result; }); + const mismatch = executionMeasurementError(measurements, executionObservation); + if (mismatch !== null) throw new EffectRuntimeRequestError(mismatch); + return measurements; +} + +function executionMeasurementError(measurements: readonly ExploreMeasurement[], observation: unknown): string | null { + if (observation == null) return null; + const execution = normalizeExploreExecutionObservation({observation}); + if (measurements.some(item => item.input_revision !== execution.input_revision)) + return "measurements must retain the executed candidate's input revision"; + if (execution.result_ref !== undefined && measurements.some(item => item.evidence_ref !== execution.result_ref)) + return "measurements must cite the executed candidate's result reference"; + return null; } function comparisonKey(node: string, metric: ExploreMeasurement): string { @@ -101,7 +115,11 @@ export function projectExploreMeasurementFeedback(findings: readonly JsonObject[ for (const finding of ordered) { const node = String(finding.node_id ?? ""), id = String(finding.finding_id ?? ""); if (!node || !id) throw new EffectRuntimeRequestError("measurements require their node and finding identities"); - for (const measurement of normalizeExploreMeasurements(finding.measurements)) { + const measurements = normalizeExploreMeasurements(finding.measurements); + // Old canonical history is auditable but is not a quantitative comparison + // source when its declared execution contradicts its measurements. + if (executionMeasurementError(measurements, finding.execution_observation) !== null) continue; + for (const measurement of measurements) { const key = comparisonKey(node, measurement), prior = groups.get(key); const previous = prior?.best ?? null; const point = measurement.value === null ? null : {finding_id: id, @@ -123,5 +141,6 @@ export function projectExploreMeasurementFeedback(findings: readonly JsonObject[ } export function normalizeExploreMeasurementsRequest(params: JsonObject): JsonObject { - return {measurements: normalizeExploreMeasurements(params.measurements, params.measurement_context)}; + return {measurements: normalizeExploreMeasurements(params.measurements, params.measurement_context, + params.execution_observation)}; } diff --git a/loopx/control_plane/capabilities/explore_result_writeback.ts b/loopx/control_plane/capabilities/explore_result_writeback.ts index bf49b8cda2..e9816e4502 100644 --- a/loopx/control_plane/capabilities/explore_result_writeback.ts +++ b/loopx/control_plane/capabilities/explore_result_writeback.ts @@ -193,6 +193,9 @@ function normalizeAttachment(value: unknown, visionPacket?: unknown, linkedScope throw new EffectRuntimeRequestError("measurement_context requires measurements"); } const measurements = Object.hasOwn(row, "measurements") + // Transport also reconstructs already committed inputs before replay is + // identified. New writes bind measurements in their precommit event plan; + // a new ingestion intent repeats that check below, never during replay. ? normalizeExploreMeasurements(row.measurements, row.measurement_context) : null; if (measurements?.some(item => item.input_revision !== text(row.input_revision, "input_revision", 160) || !refs.includes(item.evidence_ref))) { @@ -314,6 +317,9 @@ export function planExploreQuestionEvolution(params: JsonObject): JsonObject { export function produceExploreResultIntent(params: JsonObject): JsonObject { const projection = requireJsonObject(params.projection, "writeback projection"); const attachment = normalizeExploreResultAttachment({attachment: projection.explore_result}); + if (attachment.measurements !== undefined) { + normalizeExploreMeasurements(attachment.measurements, undefined, attachment.execution_observation); + } const receipt = requireJsonObject(params.receipt, "writeback receipt"); const source = text(receipt.event_id, "source receipt", 200); const key = createHash("sha256").update(source).digest("hex"); diff --git a/loopx/control_plane/capabilities/explore_turn_context.ts b/loopx/control_plane/capabilities/explore_turn_context.ts index 5329536e25..5b77bc3a5f 100644 --- a/loopx/control_plane/capabilities/explore_turn_context.ts +++ b/loopx/control_plane/capabilities/explore_turn_context.ts @@ -202,7 +202,7 @@ export function projectExploreTurnContext(params: JsonObject): JsonObject { // Manual quantitative capture uses the same finding owner. Keep its sources // available to the existing exact-result decision path, without requiring a // writeback-origin tag or admitting unrelated unmeasured notes. - const decisionResults = findings.filter(row => row.measurements !== undefined + const decisionResults = findings.filter(row => row.measurements !== undefined || row.measurement_error !== undefined || Array.isArray(row.tags) && row.tags.includes("writeback-result")); // Keep the same detail budget, but do not let unrelated newer results evict // the applicability of evidence linked to the next selected work item. @@ -344,13 +344,13 @@ export function projectExploreTurnContext(params: JsonObject): JsonObject { counts: projection.counts ?? {}, recent_nodes: nodes.slice(0, 3).map(row => compact(row, ["node_id", "title", "status", "blocked_reason"])), // The canonical evidence projection calls the finding's title `finding`. - recent_findings: findings.slice(0, 3).map(row => ({...compact({...row, title: row.finding}, ["finding_id", "node_id", "title", "status"]), + recent_findings: findings.slice(0, 3).map(row => ({...compact({...row, title: row.finding}, ["finding_id", "node_id", "title", "status", "measurement_error"]), ...findingDigest(row, findingSources)})), // Full scoped summaries; progressive reads can raise the page size. // These retain the observation AND applicability; clipping away conditions // could turn a bounded refutation into a blanket route ban. writeback_results: visibleResults - .map(row => ({...compact(row, ["finding_id", "node_id", "finding", "status", "evidence_refs"]), + .map(row => ({...compact(row, ["finding_id", "node_id", "finding", "status", "evidence_refs", "measurement_error"]), summary: String(row.summary ?? "").slice(0, EXPLORE_WRITEBACK_SUMMARY_LIMIT), ...(row.execution_observation ? {execution_observation: row.execution_observation} : {}), ...(row.measurements ? {measurements: row.measurements} : {}), diff --git a/tests/capabilities/test_explore_turn_context.py b/tests/capabilities/test_explore_turn_context.py index 1b9e9bd356..311f86be03 100644 --- a/tests/capabilities/test_explore_turn_context.py +++ b/tests/capabilities/test_explore_turn_context.py @@ -30,7 +30,7 @@ from loopx.todos import add_goal_todo -def registry(tmp_path, *, graph=False, planning=False): +def registry(tmp_path, *, graph=False, planning=False, provider="file"): path = tmp_path / "registry.json" (tmp_path / "goal.md").write_text("# Goal\n\n## Agent Todos\n") path.write_text( @@ -49,7 +49,7 @@ def registry(tmp_path, *, graph=False, planning=False): "registered_agents": ["worker"], "storage_target": { "schema_version": "loopx_new_goal_storage_target_v1", - "provider": "file", + "provider": provider, "handoff_mode": "soft_claim", }, }, diff --git a/tests/control_plane/fixtures/explore_committed_measurement_delivery_v0.json b/tests/control_plane/fixtures/explore_committed_measurement_delivery_v0.json new file mode 100644 index 0000000000..c4efce061d --- /dev/null +++ b/tests/control_plane/fixtures/explore_committed_measurement_delivery_v0.json @@ -0,0 +1,67 @@ +{ + "source_commit": "21367015cce45294dcf99f54a3211d66f5d842c5", + "todo_id": "todo_3a7234404daa4e1c487d83b1", + "turn_instance_id": "turn-legacy", + "payload": { + "explore_result": { + "schema_version": "explore_result_attachment_v0", + "node_id": "question-a", + "question": "Which candidate improved?", + "applicability": "Synthetic fixed-input comparison only.", + "input_revision": "dataset-v1", + "observation": "The receipt was recorded before attribution validation.", + "interpretation": "Keep the original record auditable; contradictory values cannot establish improvement.", + "status": "confirmed", + "evidence_refs": [ + "result:a", + "result:b" + ], + "execution_observation": { + "candidate_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "change_reason": null, + "elapsed_seconds": 1, + "evidence_refs": [ + "result:a", + "result:b" + ], + "executed_method_id": "probe", + "input_revision": "dataset-v1", + "limit_seconds": 10, + "planned_method_id": "probe", + "result_candidate_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "result_ref": "result:a", + "schema_version": "explore_execution_observation_v0", + "search_iterations": null, + "stop_reason": "process_exit:0" + }, + "measurements": [ + { + "candidate_ref": "candidate:a", + "comparison_scope": "validation:budget-v1", + "evaluation_ref": "evaluator:v1", + "evidence_ref": "result:b", + "higher_is_better": true, + "input_revision": "dataset-v1", + "metric": "quality", + "unit": "points", + "value": 8 + } + ] + }, + "generated_at": "2026-01-01T01:00:00Z", + "appended": true, + "dry_run": false, + "settlement_identity": { + "effect_id": "research:worker:turn-legacy" + } + }, + "events_jsonl": "{\"agent_id\": \"worker\", \"boundary\": {\"absolute_paths_recorded\": false, \"credential_values_recorded\": false, \"raw_logs_recorded\": false, \"raw_session_transcript_recorded\": false, \"raw_task_text_recorded\": false, \"raw_trajectory_recorded\": false}, \"event_id\": \"d5a9ab993f5b0fb9\", \"event_kind\": \"node\", \"goal_id\": \"research\", \"node_kind\": \"question\", \"recorded_at\": \"2026-01-01T01:00:00Z\", \"result_id\": \"question-a\", \"schema_version\": \"loopx_explore_result_event_v0\", \"status\": \"open\", \"summary\": \"Synthetic fixed-input comparison only.\", \"title\": \"Which candidate improved?\"}\n{\"agent_id\": \"worker\", \"boundary\": {\"absolute_paths_recorded\": false, \"credential_values_recorded\": false, \"raw_logs_recorded\": false, \"raw_session_transcript_recorded\": false, \"raw_task_text_recorded\": false, \"raw_trajectory_recorded\": false}, \"event_id\": \"74431d3bff696a54\", \"event_kind\": \"finding\", \"evidence_refs\": [\"result:a\", \"result:b\"], \"execution_observation\": {\"candidate_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", \"change_reason\": null, \"elapsed_seconds\": 1, \"evidence_refs\": [\"result:a\", \"result:b\"], \"executed_method_id\": \"probe\", \"input_revision\": \"dataset-v1\", \"limit_seconds\": 10, \"planned_method_id\": \"probe\", \"result_candidate_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", \"result_ref\": \"result:a\", \"schema_version\": \"explore_execution_observation_v0\", \"search_iterations\": null, \"stop_reason\": \"process_exit:0\"}, \"goal_id\": \"research\", \"measurements\": [{\"candidate_ref\": \"candidate:a\", \"comparison_scope\": \"validation:budget-v1\", \"evaluation_ref\": \"evaluator:v1\", \"evidence_ref\": \"result:b\", \"higher_is_better\": true, \"input_revision\": \"dataset-v1\", \"metric\": \"quality\", \"unit\": \"points\", \"value\": 8}], \"node_id\": \"question-a\", \"recorded_at\": \"2026-01-01T01:00:00Z\", \"result_id\": \"finding_98187d071ba473d6cde7c985dc3cc20ce5f0fe946be6340a98403405cf822c2c\", \"schema_version\": \"loopx_explore_result_event_v0\", \"status\": \"confirmed\", \"summary\": \"Input revision: dataset-v1 Applicability: Synthetic fixed-input comparison only. Observation: The receipt was recorded before attribution validation. Interpretation: Keep the original record auditable; contradictory values cannot establish improvement.\", \"tags\": [\"writeback-result\"], \"title\": \"Which candidate improved?\"}\n", + "hook_files": { + "goals/research/post_writeback_hooks/pwh_96ee515e67a60b94c0430f3db38a81f1691f45af6e479bc0c6087e77ff3387b5.json": "{\n \"schema_version\": \"loopx_post_writeback_capability_hook_receipt_v0\",\n \"dispatch_id\": \"pwh_96ee515e67a60b94c0430f3db38a81f1691f45af6e479bc0c6087e77ff3387b5\",\n \"hook_id\": \"explore.result_writeback\",\n \"capability_id\": \"explore\",\n \"source_receipt_id\": \"pwr_67e18f791ccb70c9f0f3e8fa\",\n \"status\": \"intent_recorded\",\n \"intent\": {\n \"schema_version\": \"loopx_capability_intent_v0\",\n \"intent_kind\": \"explore.result_ingestion\",\n \"idempotency_key\": \"explore-result:98187d071ba473d6cde7c985dc3cc20ce5f0fe946be6340a98403405cf822c2c\",\n \"source_receipt_id\": \"pwr_67e18f791ccb70c9f0f3e8fa\",\n \"payload\": {\n \"attachment\": {\n \"schema_version\": \"explore_result_attachment_v0\",\n \"node_id\": \"question-a\",\n \"question\": \"Which candidate improved?\",\n \"applicability\": \"Synthetic fixed-input comparison only.\",\n \"input_revision\": \"dataset-v1\",\n \"observation\": \"The receipt was recorded before attribution validation.\",\n \"interpretation\": \"Keep the original record auditable; contradictory values cannot establish improvement.\",\n \"status\": \"confirmed\",\n \"evidence_refs\": [\n \"result:a\",\n \"result:b\"\n ],\n \"execution_observation\": {\n \"schema_version\": \"explore_execution_observation_v0\",\n \"candidate_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\n \"input_revision\": \"dataset-v1\",\n \"planned_method_id\": \"probe\",\n \"executed_method_id\": \"probe\",\n \"change_reason\": null,\n \"elapsed_seconds\": 1,\n \"limit_seconds\": 10,\n \"search_iterations\": null,\n \"stop_reason\": \"process_exit:0\",\n \"evidence_refs\": [\n \"result:a\",\n \"result:b\"\n ],\n \"result_ref\": \"result:a\",\n \"result_candidate_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"\n },\n \"measurements\": [\n {\n \"metric\": \"quality\",\n \"unit\": \"points\",\n \"value\": 8,\n \"higher_is_better\": true,\n \"comparison_scope\": \"validation:budget-v1\",\n \"input_revision\": \"dataset-v1\",\n \"evaluation_ref\": \"evaluator:v1\",\n \"candidate_ref\": \"candidate:a\",\n \"evidence_ref\": \"result:b\"\n }\n ]\n }\n },\n \"requested_write_scope\": []\n },\n \"error_code\": null,\n \"attempt_count\": 1,\n \"recorded_at\": \"2026-01-01T01:00:00Z\"\n}\n" + }, + "base_replay": { + "ok": true, + "idempotent_replay": true, + "retryable": false + } +} diff --git a/tests/control_plane/fixtures/explore_legacy_cli_writeback_v0.json b/tests/control_plane/fixtures/explore_legacy_cli_writeback_v0.json new file mode 100644 index 0000000000..383f6db0e0 --- /dev/null +++ b/tests/control_plane/fixtures/explore_legacy_cli_writeback_v0.json @@ -0,0 +1,925 @@ +{ + "source_revision": "37cedac966dda06ad6605fc389abab8bc2686777", + "provenance": "Synthetic FileAuthorityStore and complete primary writeback, hook and result events created by the real base CLI. Only the filesystem state address is parameterized. Index fields reuse equal base-writeback values plus the original compact state projection. No model, task trajectory or live Goal data.", + "attachment": { + "schema_version": "explore_result_attachment_v0", + "node_id": "measured-candidate", + "question": "Which candidate improves the bounded case?", + "applicability": "Synthetic fixed input and budget", + "input_revision": "fixture-v1", + "observation": "A legacy measured result was recorded.", + "interpretation": "Retain the original statement for audit; do not infer execution authority.", + "status": "tentative", + "evidence_refs": [ + "result:a", + "result:b" + ], + "execution_observation": { + "schema_version": "explore_execution_observation_v0", + "candidate_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "input_revision": "fixture-v1", + "planned_method_id": "probe", + "executed_method_id": "probe", + "change_reason": null, + "elapsed_seconds": 1, + "limit_seconds": 10, + "search_iterations": null, + "stop_reason": "process_exit:0", + "evidence_refs": [ + "result:a" + ], + "result_ref": "result:a", + "result_candidate_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "measurements": [ + { + "metric": "quality", + "value": 8, + "unit": "points", + "higher_is_better": true, + "comparison_scope": "validation:budget", + "input_revision": "fixture-v1", + "evaluation_ref": "evaluator:v1", + "candidate_ref": "candidate:b", + "evidence_ref": "result:b" + } + ] + }, + "runtime_files": { + "authority/file-v0/authority-store-ebdd5eb7e94ef0c5.json": { + "json": { + "committed": [ + { + "cursor": "1", + "events": [], + "operation_id": "canonical-fixture", + "provider_revision": "file:1:f9c67d2bde7f7d8b39351bc9", + "receipts": [], + "state": { + "kind": "checkpoint", + "projection": { + "goal_id": "settlement-cli-fixture", + "handoff_mode": "soft_claim", + "leases": [], + "partitions": { + "leases": null, + "todos": null + }, + "schema_version": "loopx_coordination_runtime_shadow_projection_v0", + "source_authority": "legacy_markdown_and_task_lease", + "todo_read_model": { + "contract_fields": [ + "index", + "done", + "text", + "schema_version", + "todo_id", + "role", + "status", + "priority", + "title", + "archive_state", + "source_section", + "task_class", + "action_kind", + "task_domain", + "capability_binding_ref", + "task_repository", + "continuation_policy", + "removed_continuation_policy", + "required_write_scopes", + "required_capabilities", + "target_capabilities", + "explore_result_node_refs", + "decision_scope", + "required_decision_scopes", + "decision_outcome", + "decision_scope_outcomes", + "claimed_by", + "created_by", + "last_actor_agent_id", + "bound_agent", + "goal_bound", + "blocks_agent", + "excluded_agents", + "global_gate", + "unblocks_todo_id", + "resume_when", + "resume_monitor_generation", + "resume_condition", + "resume_ready", + "no_followup", + "successor_todo_ids", + "completion_continuation", + "completion_recovery", + "completion_receipt_id", + "replan_obligation_id", + "target_key", + "cadence", + "next_due_at", + "expires_at", + "watch_only", + "last_checked_at", + "result_hash", + "consecutive_no_change", + "material_change", + "material_change_generation", + "max_no_change_before_replan", + "monitor_effect_id", + "note", + "evidence", + "reason", + "completed_at", + "completion_turn_key", + "completion_result", + "updated_at", + "superseded_by", + "completion_validation_required", + "completion_validation_sha256", + "completion_validation_revision", + "completion_validation_revision_history", + "handoff_note" + ], + "records_sha256": "97d288258b609e6e4f61555b9533afaf6a142ccfe517f8fd1b20b09fcb14950f", + "schema_version": "loopx_todo_canonical_read_record_v0", + "todo_count": 1 + }, + "todos": [ + { + "action_kind": "validate", + "archive_state": "active", + "claimed_by": "codex-settlement-cli", + "done": false, + "index": 1, + "priority": "P1", + "role": "agent", + "schema_version": "todo_item_v0", + "source_section": "Agent Todo", + "status": "open", + "task_class": "advancement_task", + "text": "[P1] Validate and settle the selected delivery.", + "title": "Validate and settle the selected delivery.", + "todo_id": "todo_fixture_settlement" + } + ] + } + } + }, + { + "cursor": "2", + "events": [], + "operation_id": "todo-claim:settlement-cli-fixture:todo_fixture_settlement:e1535cd2228d4d6ea91c10926eefd87b", + "provider_revision": "file:2:1e67e94cbc52370f980d67d0", + "receipts": [ + { + "goal_id": "settlement-cli-fixture", + "operation_id": "todo-claim:settlement-cli-fixture:todo_fixture_settlement:e1535cd2228d4d6ea91c10926eefd87b", + "request_sha256": "3f4b8d97ed5ae14b2b45fda314925553b7368f64b2ff0519ebbbc44aa74a9dac", + "result": { + "changed": false, + "claim_operation_id": "todo-claim:settlement-cli-fixture:todo_fixture_settlement:e1535cd2228d4d6ea91c10926eefd87b", + "claimed_by": "codex-settlement-cli", + "execution_managed": true, + "handoff_mode": "soft_claim", + "mutation_authority": { + "actor_agent_id": "codex-settlement-cli", + "command": "claim", + "mode": "single_agent_compatibility", + "registered_agent_count": 1, + "schema_version": "todo_mutation_authority_v0", + "todo_id": "todo_fixture_settlement" + }, + "todo_id": "todo_fixture_settlement" + }, + "schema_version": "loopx_coordination_todo_claim_receipt_v0" + } + ], + "state": { + "delta": { + "operations": [], + "schema_version": "authority_state_delta_v0" + }, + "kind": "delta" + } + }, + { + "cursor": "3", + "events": [ + { + "goal_id": "settlement-cli-fixture", + "mutation_kinds": [ + "todo_upsert" + ], + "mutation_sha256": "f91b740698e06bcffac2b5c5cc7d8ea73d471dac37c9fcafd242265ae1c094ec", + "next_projection_sha256": "a27014ecf1124b4294d71adfae935a3ee7bfdcda730b011cbaad5ec0d4f85bf8", + "operation_id": "todo-update:81ebb9dad7d14ce7afefbb45f09b7e18", + "previous_projection_sha256": "af00a29781233df18bf489f98f895591dd7321b2d6567337b44d74417f03a5eb", + "schema_version": "loopx_coordination_projection_mutation_event_v0", + "targets": [ + "todo:todo_fixture_settlement" + ] + } + ], + "operation_id": "todo-update:81ebb9dad7d14ce7afefbb45f09b7e18", + "provider_revision": "file:3:86e825a267fcd06f5d09785d", + "receipts": [ + { + "changed": true, + "goal_id": "settlement-cli-fixture", + "operation_id": "todo-update:81ebb9dad7d14ce7afefbb45f09b7e18", + "request_sha256": "99101bd347cc493ec2a63d055f00c76e78599fb2ce8a1c600de38629f77a2c4c", + "schema_version": "loopx_coordination_todo_update_receipt_v0", + "todo_id": "todo_fixture_settlement" + } + ], + "state": { + "delta": { + "operations": [ + { + "op": "set", + "path": [ + "todo_read_model", + "records_sha256" + ], + "value": "e205d17cfd80ba60c87cee0a33108a045d6ccc86ab6ffa582c79749cbda48642" + }, + { + "index": 0, + "insert": [ + { + "action_kind": "validate", + "archive_state": "active", + "claimed_by": "codex-settlement-cli", + "done": false, + "explore_result_node_refs": [ + "measured-candidate" + ], + "index": 1, + "priority": "P1", + "role": "agent", + "schema_version": "todo_item_v0", + "source_section": "Agent Todo", + "status": "open", + "task_class": "advancement_task", + "text": "[P1] Validate and settle the selected delivery.", + "title": "Validate and settle the selected delivery.", + "todo_id": "todo_fixture_settlement", + "updated_at": "2026-11-01T04:22:20Z" + } + ], + "op": "splice", + "path": [ + "todos" + ], + "remove": 1 + } + ], + "schema_version": "authority_state_delta_v0" + }, + "kind": "delta" + } + } + ], + "cursor": "3", + "goal_id": "settlement-cli-fixture", + "head": { + "goal_id": "settlement-cli-fixture", + "handoff_mode": "soft_claim", + "leases": [], + "partitions": { + "leases": null, + "todos": null + }, + "schema_version": "loopx_coordination_runtime_shadow_projection_v0", + "source_authority": "legacy_markdown_and_task_lease", + "todo_read_model": { + "contract_fields": [ + "index", + "done", + "text", + "schema_version", + "todo_id", + "role", + "status", + "priority", + "title", + "archive_state", + "source_section", + "task_class", + "action_kind", + "task_domain", + "capability_binding_ref", + "task_repository", + "continuation_policy", + "removed_continuation_policy", + "required_write_scopes", + "required_capabilities", + "target_capabilities", + "explore_result_node_refs", + "decision_scope", + "required_decision_scopes", + "decision_outcome", + "decision_scope_outcomes", + "claimed_by", + "created_by", + "last_actor_agent_id", + "bound_agent", + "goal_bound", + "blocks_agent", + "excluded_agents", + "global_gate", + "unblocks_todo_id", + "resume_when", + "resume_monitor_generation", + "resume_condition", + "resume_ready", + "no_followup", + "successor_todo_ids", + "completion_continuation", + "completion_recovery", + "completion_receipt_id", + "replan_obligation_id", + "target_key", + "cadence", + "next_due_at", + "expires_at", + "watch_only", + "last_checked_at", + "result_hash", + "consecutive_no_change", + "material_change", + "material_change_generation", + "max_no_change_before_replan", + "monitor_effect_id", + "note", + "evidence", + "reason", + "completed_at", + "completion_turn_key", + "completion_result", + "updated_at", + "superseded_by", + "completion_validation_required", + "completion_validation_sha256", + "completion_validation_revision", + "completion_validation_revision_history", + "handoff_note" + ], + "records_sha256": "e205d17cfd80ba60c87cee0a33108a045d6ccc86ab6ffa582c79749cbda48642", + "schema_version": "loopx_todo_canonical_read_record_v0", + "todo_count": 1 + }, + "todos": [ + { + "action_kind": "validate", + "archive_state": "active", + "claimed_by": "codex-settlement-cli", + "done": false, + "explore_result_node_refs": [ + "measured-candidate" + ], + "index": 1, + "priority": "P1", + "role": "agent", + "schema_version": "todo_item_v0", + "source_section": "Agent Todo", + "status": "open", + "task_class": "advancement_task", + "text": "[P1] Validate and settle the selected delivery.", + "title": "Validate and settle the selected delivery.", + "todo_id": "todo_fixture_settlement", + "updated_at": "2026-11-01T04:22:20Z" + } + ] + }, + "provider_revision": "file:3:86e825a267fcd06f5d09785d", + "schema_version": "loopx_file_authority_store_v1", + "store_identity": "file:5065b6be99a348769e985629ab4434b2" + } + }, + "authority/file-v0/store-identity": { + "text": "file:5065b6be99a348769e985629ab4434b2" + }, + "authority-transition/file-v0/legacy-writer-fence-ebdd5eb7e94ef0c5.json": { + "json": { + "expected_shadow_provider_revision": "file:1:f9c67d2bde7f7d8b39351bc9", + "fence_id": "canonical-fixture", + "goal_id": "settlement-cli-fixture", + "schema_version": "loopx_legacy_coordination_writer_fence_v0", + "source_projection_sha256": "af00a29781233df18bf489f98f895591dd7321b2d6567337b44d74417f03a5eb", + "source_version": "canonical-fixture", + "state": "engaged" + } + }, + "goals/settlement-cli-fixture/explore-result-log.jsonl": { + "jsonl": [ + { + "agent_id": "codex-settlement-cli", + "boundary": { + "absolute_paths_recorded": false, + "credential_values_recorded": false, + "raw_logs_recorded": false, + "raw_session_transcript_recorded": false, + "raw_task_text_recorded": false, + "raw_trajectory_recorded": false + }, + "event_id": "e9177bd26c42ebb0", + "event_kind": "node", + "goal_id": "settlement-cli-fixture", + "node_kind": "question", + "recorded_at": "2026-11-01T12:22:19+08:00", + "result_id": "measured-candidate", + "schema_version": "loopx_explore_result_event_v0", + "status": "open", + "summary": "Synthetic fixed input and budget", + "title": "Which candidate improves the bounded case?" + }, + { + "agent_id": "codex-settlement-cli", + "boundary": { + "absolute_paths_recorded": false, + "credential_values_recorded": false, + "raw_logs_recorded": false, + "raw_session_transcript_recorded": false, + "raw_task_text_recorded": false, + "raw_trajectory_recorded": false + }, + "event_id": "4a78fdc40f43455f", + "event_kind": "finding", + "evidence_refs": [ + "result:a", + "result:b" + ], + "execution_observation": { + "candidate_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "change_reason": null, + "elapsed_seconds": 1, + "evidence_refs": [ + "result:a" + ], + "executed_method_id": "probe", + "input_revision": "fixture-v1", + "limit_seconds": 10, + "planned_method_id": "probe", + "result_candidate_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "result_ref": "result:a", + "schema_version": "explore_execution_observation_v0", + "search_iterations": null, + "stop_reason": "process_exit:0" + }, + "goal_id": "settlement-cli-fixture", + "measurements": [ + { + "candidate_ref": "candidate:b", + "comparison_scope": "validation:budget", + "evaluation_ref": "evaluator:v1", + "evidence_ref": "result:b", + "higher_is_better": true, + "input_revision": "fixture-v1", + "metric": "quality", + "unit": "points", + "value": 8 + } + ], + "node_id": "measured-candidate", + "recorded_at": "2026-11-01T12:22:19+08:00", + "result_id": "finding_80c27c98aa8a2b26a2d337158467748b0333b395ada4bbac58ff19613c825048", + "schema_version": "loopx_explore_result_event_v0", + "status": "tentative", + "summary": "Input revision: fixture-v1 Applicability: Synthetic fixed input and budget Observation: A legacy measured result was recorded. Interpretation: Retain the original statement for audit; do not infer execution authority.", + "tags": [ + "writeback-result" + ], + "title": "Which candidate improves the bounded case?" + } + ] + }, + "goals/settlement-cli-fixture/post_writeback_hooks/pwh_1f5ad15373f5da1c466032ad630963681afd7d6f7eb0722cd5798945f5a21fbe.json": { + "json": { + "schema_version": "loopx_post_writeback_capability_hook_receipt_v0", + "dispatch_id": "pwh_1f5ad15373f5da1c466032ad630963681afd7d6f7eb0722cd5798945f5a21fbe", + "hook_id": "explore.result_writeback", + "capability_id": "explore", + "source_receipt_id": "pwr_473c886d7f1df851fc19aad5", + "status": "intent_recorded", + "intent": { + "schema_version": "loopx_capability_intent_v0", + "intent_kind": "explore.result_ingestion", + "idempotency_key": "explore-result:80c27c98aa8a2b26a2d337158467748b0333b395ada4bbac58ff19613c825048", + "source_receipt_id": "pwr_473c886d7f1df851fc19aad5", + "payload": { + "attachment": { + "schema_version": "explore_result_attachment_v0", + "node_id": "measured-candidate", + "question": "Which candidate improves the bounded case?", + "applicability": "Synthetic fixed input and budget", + "input_revision": "fixture-v1", + "observation": "A legacy measured result was recorded.", + "interpretation": "Retain the original statement for audit; do not infer execution authority.", + "status": "tentative", + "evidence_refs": [ + "result:a", + "result:b" + ], + "execution_observation": { + "schema_version": "explore_execution_observation_v0", + "candidate_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "input_revision": "fixture-v1", + "planned_method_id": "probe", + "executed_method_id": "probe", + "change_reason": null, + "elapsed_seconds": 1, + "limit_seconds": 10, + "search_iterations": null, + "stop_reason": "process_exit:0", + "evidence_refs": [ + "result:a" + ], + "result_ref": "result:a", + "result_candidate_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "measurements": [ + { + "metric": "quality", + "unit": "points", + "value": 8, + "higher_is_better": true, + "comparison_scope": "validation:budget", + "input_revision": "fixture-v1", + "evaluation_ref": "evaluator:v1", + "candidate_ref": "candidate:b", + "evidence_ref": "result:b" + } + ] + } + }, + "requested_write_scope": [] + }, + "error_code": null, + "attempt_count": 1, + "recorded_at": "2026-11-01T12:22:19+08:00" + } + }, + "goals/settlement-cli-fixture/rollout-event-log.jsonl": { + "jsonl": [ + { + "agent_id": "codex-settlement-cli", + "boundary": { + "absolute_paths_recorded": false, + "credential_values_recorded": false, + "raw_logs_recorded": false, + "raw_session_transcript_recorded": false, + "raw_task_text_recorded": false, + "raw_trajectory_recorded": false + }, + "details": { + "added": false, + "already_exists": false, + "changed": false, + "command": "todo", + "completion_continuation": null, + "completion_recovery": null, + "decision_outcome": null, + "decision_scope": null, + "mutation_authority": "{'actor_agent_id': 'codex-settlement-cli', 'command': 'claim', 'mode': 'single_agent_compatibility', 'registered_agent_count': 1, 'schema_version': 'todo_mutation_authority_v0', 'todo_id': 'todo_fixture_settlement'}", + "no_followup": false, + "replan_transition": null, + "role": "agent", + "settlement_effect_id": null, + "task_class": null, + "todo_command": "claim" + }, + "event_id": "4f544db230a001c7", + "event_kind": "todo_claim", + "goal_id": "settlement-cli-fixture", + "recorded_at": "2026-11-01T04:22:16.135165Z", + "schema_version": "loopx_rollout_event_v0", + "status": "no_change", + "summary": "todo claim recorded for todo_fixture_settlement", + "todo_id": "todo_fixture_settlement" + }, + { + "agent_id": "codex-settlement-cli", + "boundary": { + "absolute_paths_recorded": false, + "credential_values_recorded": false, + "raw_logs_recorded": false, + "raw_session_transcript_recorded": false, + "raw_task_text_recorded": false, + "raw_trajectory_recorded": false + }, + "details": { + "admission_resume_ready": null, + "admission_resume_when": null, + "appended": false, + "applied_rrule": null, + "closeout_required": true, + "command": "quota", + "delivery_allowed": true, + "delivery_workspace_causality_reason": "todo_write_contract_not_explicit", + "delivery_workspace_causality_schema_version": "delivery_workspace_causality_v0", + "delivery_workspace_causality_source": "selected_todo_contract", + "delivery_workspace_causality_todo_id": "todo_fixture_settlement", + "delivery_workspace_requirement": "unknown", + "interaction_mode": "bounded_delivery", + "must_attempt_work": true, + "ok": true, + "quiet_noop_allowed": false, + "quota_command": "should-run", + "quota_spend_source": "heartbeat", + "replan_obligation_id": null, + "selected_todo_id": "todo_fixture_settlement", + "semantic_replan_obligation_id": null, + "settlement_effect_id": "settlement-cli-fixture:codex-settlement-cli:todo_fixture_settlement:turn-settlement-cli-1", + "should_run": true, + "slots": null, + "source": "run_history", + "stall_observation": "not_applicable", + "successor_todo_ids": null, + "target_key": null, + "todo_id": "todo_fixture_settlement", + "turn_instance_id": "turn-settlement-cli-1" + }, + "event_id": "fbfa67f85d844cff", + "event_kind": "quota_should_run", + "goal_id": "settlement-cli-fixture", + "recorded_at": "2026-11-01T04:22:18.641918Z", + "run_id": "turn-settlement-cli-1", + "schema_version": "loopx_rollout_event_v0", + "status": "normal_run", + "summary": "heartbeat quota receipt committed for turn=turn-settlement-cli-1 stall=not_applicable" + }, + { + "agent_id": "codex-settlement-cli", + "boundary": { + "absolute_paths_recorded": false, + "credential_values_recorded": false, + "raw_logs_recorded": false, + "raw_session_transcript_recorded": false, + "raw_task_text_recorded": false, + "raw_trajectory_recorded": false + }, + "details": { + "replan_obligation_id": null, + "resume_key": "f84400a5e68b1107c22d17a165b7d30f927f144d8bb099606832ca77f4d1ca1d", + "schema_version": "refresh_external_delivery_v0", + "settlement_effect_id": "settlement-cli-fixture:codex-settlement-cli:todo_fixture_settlement:turn-settlement-cli-1", + "state": "paused" + }, + "event_id": "940cc2476f42c47b", + "event_kind": "refresh_external_delivery", + "goal_id": "settlement-cli-fixture", + "recorded_at": "2026-11-01T04:22:19.320822Z", + "run_id": "turn-settlement-cli-1", + "schema_version": "loopx_rollout_event_v0", + "status": "paused", + "summary": "Refresh external delivery preference recorded.", + "todo_id": "todo_fixture_settlement" + }, + { + "agent_id": "codex-settlement-cli", + "boundary": { + "absolute_paths_recorded": false, + "credential_values_recorded": false, + "raw_logs_recorded": false, + "raw_session_transcript_recorded": false, + "raw_task_text_recorded": false, + "raw_trajectory_recorded": false + }, + "classification": "validated_change", + "delivery_outcome": "outcome_progress", + "details": { + "agent_lane": "codex-settlement-cli", + "autonomous_replan_recorded": false, + "command": "refresh-state", + "global_sync_wrote": false, + "progress_scope": "agent_lane", + "replan_obligation_id": null, + "reward_memory_candidate_id": null, + "reward_memory_candidate_status": null, + "reward_memory_reflection_digest": null, + "settlement_effect_id": "settlement-cli-fixture:codex-settlement-cli:todo_fixture_settlement:turn-settlement-cli-1" + }, + "event_id": "fa0319a22957d928", + "event_kind": "refresh_state", + "goal_id": "settlement-cli-fixture", + "recorded_at": "2026-11-01T04:22:19.801108Z", + "run_id": "turn-settlement-cli-1", + "schema_version": "loopx_rollout_event_v0", + "status": "appended", + "summary": "refresh-state appended compact control-plane state with classification=validated_change", + "todo_id": "todo_fixture_settlement" + } + ] + } + }, + "writeback_relative_path": "goals/settlement-cli-fixture/runs/2026-11-01T12-22-19-08-00.json", + "writeback": { + "generated_at": "2026-11-01T12:22:19+08:00", + "goal_id": "settlement-cli-fixture", + "classification": "validated_change", + "recommended_action": "[P1] Validate and settle the selected delivery.", + "recommended_action_source": "settlement_bound_todo", + "health_check": "state_file 1/1; registry_goal 1/1; authority_sources 0", + "state": { + "path": "$STATE_FILE", + "sha256_16": "40dbb5013f59c3e8", + "frontmatter": { + "status": "active-read-only", + "owner_mode": "goal", + "objective": "Settle one standard Codex App delivery.", + "updated_at": "2026-01-01T00:00:00+00:00" + }, + "next_action": [ + "Validate and settle the selected delivery." + ], + "recent_feedback": [], + "progress": [] + }, + "registry_goal": { + "present": true, + "domain": "settlement-cli-fixture", + "status": "active-read-only", + "adapter": { + "kind": "read_only_project_map_v0", + "status": "connected-read-only" + }, + "authority_source_count": 0 + }, + "recommended_action_resolution": { + "schema_version": "refresh_recommendation_v0", + "recommended_action": "[P1] Validate and settle the selected delivery.", + "recommended_action_source": "settlement_bound_todo", + "authority": "settlement", + "settlement_alignment": "exact", + "todo_id": "todo_fixture_settlement", + "selection_binding": "heartbeat_receipt", + "claim_required_before_work": false + }, + "delivery_batch_scale": "implementation", + "delivery_outcome": "outcome_progress", + "delivery_workspace": { + "schema_version": "delivery_workspace_v1", + "workspace_identity": "git:github.com/loopx-project/loopx", + "identity_kind": "git_repository", + "task_repository": "git:github.com/loopx-project/loopx", + "workspace_revision_digest": "a64a1cc8a41cd813a1da746e08427a5adc458beaf686111d68cd30a75e3c2172", + "repository_source": "current_git_origin", + "workspace_kind": "canonical_checkout", + "peer_independent_worktree_required": false + }, + "settlement_identity": { + "schema_version": "quota_settlement_identity_v0", + "effect_id": "settlement-cli-fixture:codex-settlement-cli:todo_fixture_settlement:turn-settlement-cli-1", + "goal_id": "settlement-cli-fixture", + "agent_id": "codex-settlement-cli", + "todo_id": "todo_fixture_settlement", + "turn_instance_id": "turn-settlement-cli-1" + }, + "turn_instance_id": "turn-settlement-cli-1", + "todo_id": "todo_fixture_settlement", + "vision_checkpoint": { + "schema_version": "vision_checkpoint_v0", + "agent_id": "codex-settlement-cli", + "required": true, + "satisfied": false, + "decision": "missing_required", + "triggers": [ + { + "kind": "material_delivery_outcome", + "delivery_outcome": "outcome_progress" + } + ], + "delivery_boundary": "semantic_closeout", + "required_resolution": [ + "write_vision_patch" + ], + "missing_baseline": true + }, + "explore_result": { + "schema_version": "explore_result_attachment_v0", + "node_id": "measured-candidate", + "question": "Which candidate improves the bounded case?", + "applicability": "Synthetic fixed input and budget", + "input_revision": "fixture-v1", + "observation": "A legacy measured result was recorded.", + "interpretation": "Retain the original statement for audit; do not infer execution authority.", + "status": "tentative", + "evidence_refs": [ + "result:a", + "result:b" + ], + "execution_observation": { + "schema_version": "explore_execution_observation_v0", + "candidate_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "input_revision": "fixture-v1", + "planned_method_id": "probe", + "executed_method_id": "probe", + "change_reason": null, + "elapsed_seconds": 1, + "limit_seconds": 10, + "search_iterations": null, + "stop_reason": "process_exit:0", + "evidence_refs": [ + "result:a" + ], + "result_ref": "result:a", + "result_candidate_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "measurements": [ + { + "metric": "quality", + "unit": "points", + "value": 8, + "higher_is_better": true, + "comparison_scope": "validation:budget", + "input_revision": "fixture-v1", + "evaluation_ref": "evaluator:v1", + "candidate_ref": "candidate:b", + "evidence_ref": "result:b" + } + ] + }, + "explore_result_recorded_at": "2026-11-01T12:22:19+08:00", + "progress_scope": "agent_lane", + "agent_id": "codex-settlement-cli", + "agent_lane": "codex-settlement-cli", + "todo_source": { + "source_authority": "file_v0", + "store_identity": "file:5065b6be99a348769e985629ab4434b2", + "provider_revision": "file:2:1e67e94cbc52370f980d67d0", + "cursor": "2" + }, + "delivery_workspace_causality": { + "schema_version": "delivery_workspace_causality_v0", + "todo_id": "todo_fixture_settlement", + "requirement": "unknown", + "source": "selected_todo_contract", + "reason": "todo_write_contract_not_explicit" + }, + "refresh_recovery": { + "schema_version": "refresh_recovery_v0", + "decision": "append", + "reason": "first_writeback", + "vision_request_digest": null, + "mutation_digest": "1451817afe250f88d1509692b01b2486fc7173036d689e83432656f20cfceff6", + "original_generated_at": null + }, + "settlement_workspace_requirement": { + "schema_version": "settlement_workspace_requirement_v0", + "requirement": "unknown", + "reason": "todo_write_contract_not_explicit", + "settlement_binding_kind": "todo", + "source": "selected_todo_contract" + }, + "runtime_projection_route": { + "schema_version": "runtime_projection_route_v0", + "route_id": "9cca71239c99ed5a", + "status": "single_runtime", + "projection_required": false, + "declaration_source": "source_runtime_fallback", + "match_count": 0, + "source_mirror_match_count": 0, + "conflict_count": 0, + "unreadable_target_count": 0, + "source_registry_sha256_16": "5920ce82616ed364", + "source_runtime_sha256_16": "09bf0c30c91ef1e5", + "target_runtime_sha256_16": "09bf0c30c91ef1e5", + "projection_enabled": false, + "projection_marker_field": "shared_runtime_projection" + } + }, + "index_fields": [ + "generated_at", + "goal_id", + "classification", + "recommended_action", + "recommended_action_source", + "health_check", + "state", + "runtime_projection_route", + "recommended_action_resolution", + "delivery_batch_scale", + "delivery_outcome", + "delivery_workspace", + "settlement_identity", + "todo_source", + "refresh_recovery", + "turn_instance_id", + "todo_id", + "vision_checkpoint", + "explore_result", + "explore_result_recorded_at", + "progress_scope", + "agent_id", + "agent_lane" + ], + "index_overrides": { + "state": { + "sha256_16": "40dbb5013f59c3e8", + "frontmatter": { + "updated_at": "2026-01-01T00:00:00+00:00" + } + } + } +} diff --git a/tests/control_plane/fixtures/explore_measurement_history_v0.jsonl b/tests/control_plane/fixtures/explore_measurement_history_v0.jsonl new file mode 100644 index 0000000000..ee4e60ab54 --- /dev/null +++ b/tests/control_plane/fixtures/explore_measurement_history_v0.jsonl @@ -0,0 +1,3 @@ +{"boundary": {"absolute_paths_recorded": false, "credential_values_recorded": false, "raw_logs_recorded": false, "raw_session_transcript_recorded": false, "raw_task_text_recorded": false, "raw_trajectory_recorded": false}, "event_id": "cae05d192b3df6b2", "event_kind": "node", "goal_id": "research", "node_kind": "question", "recorded_at": "2026-01-01T00:00:00Z", "result_id": "question-a", "schema_version": "loopx_explore_result_event_v0", "status": "open", "title": "Which candidate improved?"} +{"boundary": {"absolute_paths_recorded": false, "credential_values_recorded": false, "raw_logs_recorded": false, "raw_session_transcript_recorded": false, "raw_task_text_recorded": false, "raw_trajectory_recorded": false}, "event_id": "5bd7df96eed54e81", "event_kind": "finding", "evidence_refs": ["result:a", "result:b"], "execution_observation": {"candidate_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "change_reason": null, "elapsed_seconds": 1, "evidence_refs": ["result:a", "result:b"], "executed_method_id": "probe", "input_revision": "dataset-v1", "limit_seconds": 10, "planned_method_id": "probe", "result_candidate_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "result_ref": "result:a", "schema_version": "explore_execution_observation_v0", "search_iterations": null, "stop_reason": "process_exit:0"}, "goal_id": "research", "measurements": [{"candidate_ref": "candidate:a", "comparison_scope": "validation:budget-v1", "evaluation_ref": "evaluator:v1", "evidence_ref": "result:b", "higher_is_better": true, "input_revision": "dataset-v1", "metric": "quality", "unit": "points", "value": 8}], "node_id": "question-a", "recorded_at": "2026-01-01T01:00:00Z", "result_id": "measured", "schema_version": "loopx_explore_result_event_v0", "status": "confirmed", "title": "Measured candidate"} +{"boundary": {"absolute_paths_recorded": false, "credential_values_recorded": false, "raw_logs_recorded": false, "raw_session_transcript_recorded": false, "raw_task_text_recorded": false, "raw_trajectory_recorded": false}, "event_id": "56e6425cc30feee1", "event_kind": "finding", "evidence_refs": ["result:a", "result:b"], "execution_observation": {"candidate_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "change_reason": null, "elapsed_seconds": 1, "evidence_refs": ["result:a", "result:b"], "executed_method_id": "probe", "input_revision": "dataset-v1", "limit_seconds": 10, "planned_method_id": "probe", "result_candidate_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "result_ref": "result:a", "schema_version": "explore_execution_observation_v0", "search_iterations": null, "stop_reason": "process_exit:0"}, "goal_id": "research", "measurements": [{"candidate_ref": "candidate:a", "comparison_scope": "validation:budget-v1", "evaluation_ref": "evaluator:v1", "evidence_ref": "result:a", "higher_is_better": true, "input_revision": "dataset-v1", "metric": "quality", "unit": "points", "value": 8}], "node_id": "question-a", "recorded_at": "2026-01-01T02:00:00Z", "result_id": "measured", "schema_version": "loopx_explore_result_event_v0", "status": "confirmed", "title": "Measured candidate"} diff --git a/tests/control_plane/fixtures/explore_registered_measurement_history_v0.jsonl b/tests/control_plane/fixtures/explore_registered_measurement_history_v0.jsonl new file mode 100644 index 0000000000..77a5fd5221 --- /dev/null +++ b/tests/control_plane/fixtures/explore_registered_measurement_history_v0.jsonl @@ -0,0 +1,4 @@ +{"boundary": {"absolute_paths_recorded": false, "credential_values_recorded": false, "raw_logs_recorded": false, "raw_session_transcript_recorded": false, "raw_task_text_recorded": false, "raw_trajectory_recorded": false}, "event_id": "a78028929eef014d", "event_kind": "node", "goal_id": "research", "node_kind": "area", "recorded_at": "2026-01-01T00:00:00Z", "result_id": "question-a", "schema_version": "loopx_explore_result_event_v0", "status": "open", "title": "Which candidate improved?"} +{"boundary": {"absolute_paths_recorded": false, "credential_values_recorded": false, "raw_logs_recorded": false, "raw_session_transcript_recorded": false, "raw_task_text_recorded": false, "raw_trajectory_recorded": false}, "event_id": "72f7487d409a7298", "event_kind": "finding", "evidence_refs": ["result:a", "result:b"], "execution_observation": {"candidate_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "change_reason": null, "elapsed_seconds": 1, "evidence_refs": ["result:a", "result:b"], "executed_method_id": "probe", "input_revision": "dataset-v1", "limit_seconds": 10, "planned_method_id": "probe", "result_candidate_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "result_ref": "result:a", "schema_version": "explore_execution_observation_v0", "search_iterations": null, "stop_reason": "process_exit:0"}, "goal_id": "research", "measurements": [{"candidate_ref": "candidate:a", "comparison_scope": "validation:budget-v1", "evaluation_ref": "evaluator:v1", "evidence_ref": "result:a", "higher_is_better": true, "input_revision": "dataset-v1", "metric": "quality", "unit": "points", "value": 8}], "node_id": "question-a", "recorded_at": "2026-01-01T01:00:00Z", "result_id": "valid", "schema_version": "loopx_explore_result_event_v0", "status": "confirmed", "title": "Measured candidate"} +{"boundary": {"absolute_paths_recorded": false, "credential_values_recorded": false, "raw_logs_recorded": false, "raw_session_transcript_recorded": false, "raw_task_text_recorded": false, "raw_trajectory_recorded": false}, "event_id": "ed0cb94d62b215ae", "event_kind": "finding", "evidence_refs": ["result:a", "result:b"], "execution_observation": {"candidate_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "change_reason": null, "elapsed_seconds": 1, "evidence_refs": ["result:a", "result:b"], "executed_method_id": "probe", "input_revision": "dataset-v1", "limit_seconds": 10, "planned_method_id": "probe", "result_candidate_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "result_ref": "result:a", "schema_version": "explore_execution_observation_v0", "search_iterations": null, "stop_reason": "process_exit:0"}, "goal_id": "research", "measurements": [{"candidate_ref": "candidate:a", "comparison_scope": "validation:budget-v1", "evaluation_ref": "evaluator:v1", "evidence_ref": "result:b", "higher_is_better": true, "input_revision": "dataset-v1", "metric": "quality", "unit": "points", "value": 99}], "node_id": "question-a", "recorded_at": "2026-01-01T02:00:00Z", "result_id": "mixed", "schema_version": "loopx_explore_result_event_v0", "status": "confirmed", "title": "Measured candidate"} +{"boundary": {"absolute_paths_recorded": false, "credential_values_recorded": false, "raw_logs_recorded": false, "raw_session_transcript_recorded": false, "raw_task_text_recorded": false, "raw_trajectory_recorded": false}, "event_id": "130b110e654c9d6f", "event_kind": "finding", "evidence_refs": ["result:a", "result:b"], "execution_observation": {"candidate_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "change_reason": null, "elapsed_seconds": 1, "evidence_refs": ["result:a", "result:b"], "executed_method_id": "probe", "input_revision": "dataset-v1", "limit_seconds": 10, "planned_method_id": "probe", "result_candidate_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "result_ref": "result:a", "schema_version": "explore_execution_observation_v0", "search_iterations": null, "stop_reason": "process_exit:0"}, "goal_id": "research", "measurements": [{"candidate_ref": "candidate:a", "comparison_scope": "validation:budget-v1", "evaluation_ref": "evaluator:v1", "evidence_ref": "result:b", "higher_is_better": true, "input_revision": "dataset-v1", "metric": "quality", "unit": "points", "value": 500}], "node_id": "question-a", "recorded_at": "2026-01-01T03:00:00Z", "result_id": "mixed-later", "schema_version": "loopx_explore_result_event_v0", "status": "confirmed", "title": "Measured candidate"} diff --git a/tests/control_plane/test_explore_legacy_cli_replay.py b/tests/control_plane/test_explore_legacy_cli_replay.py new file mode 100644 index 0000000000..ad68c8e886 --- /dev/null +++ b/tests/control_plane/test_explore_legacy_cli_replay.py @@ -0,0 +1,100 @@ +"""Replay real old-CLI primary/hook receipts through the public entry point.""" +from copy import deepcopy +import hashlib +import json +from pathlib import Path + +import pytest + +from tests.control_plane.test_quota_settlement_cli import ( + AGENT_ID, GOAL_ID, TODO_ID, TURN_ID, _run_cli, _write_fixture, +) + + +FIXTURE = Path(__file__).with_name("fixtures") / "explore_legacy_cli_writeback_v0.json" + + +def _legacy_cli(tmp_path): + original = json.loads(FIXTURE.read_text()) + project, runtime, registry = _write_fixture(tmp_path) + config = json.loads(registry.read_text()) + config["goals"][0]["explore_graph"] = {"enabled": True} + config["goals"][0]["spawn_policy"] = { + "allowed": False, "explore_harness": {"enabled": True}, + } + registry.write_text(json.dumps(config)) + # Restore only synthetic, base-produced persisted records in this disposable + # runtime. Do not invent a primary receipt or mutate an existing real Goal. + for relative, encoded in original["runtime_files"].items(): + target = runtime / relative + target.parent.mkdir(parents=True, exist_ok=True) + if "json" in encoded: + text = json.dumps(encoded["json"]) + "\n" + elif "jsonl" in encoded: + text = "".join(json.dumps(row) + "\n" for row in encoded["jsonl"]) + else: + text = encoded["text"] + target.write_text(text) + primary = deepcopy(original["writeback"]) + assert primary["state"]["path"] == "$STATE_FILE" + primary["state"]["path"] = str(project / config["goals"][0]["state_file"]) + json_path = runtime / original["writeback_relative_path"] + json_path.parent.mkdir(parents=True, exist_ok=True) + json_path.write_text(json.dumps(primary) + "\n") + index = {key: primary[key] for key in original["index_fields"]} + index.update(original["index_overrides"]) + index.update(json_path=str(json_path), markdown_path=str(json_path.with_suffix(".md"))) + (json_path.parent / "index.jsonl").write_text(json.dumps(index) + "\n") + attachment = tmp_path / "attachment.json" + attachment.write_text(json.dumps(original["attachment"])) + command = ( + "refresh-state", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, + "--todo-id", TODO_ID, "--turn-instance-id", TURN_ID, + "--classification", "validated_change", "--delivery-batch-scale", "implementation", + "--delivery-outcome", "outcome_progress", "--no-global-sync", + "--suppress-external-sinks", "--explore-result-json", str(attachment), + ) + return project, runtime, registry, attachment, command + + +def _business_files(runtime): + return { + str(path.relative_to(runtime)): hashlib.sha256(path.read_bytes()).hexdigest() + for path in sorted(runtime.rglob("*")) + if path.is_file() and not path.name.endswith((".lock", "-wal", "-shm")) + } + + +@pytest.mark.parametrize("mutation", [None, "attachment", "missing-graph", "missing-hook"]) +def test_old_successful_command_replays_only_its_completed_delivery(tmp_path, mutation): + project, runtime, registry, attachment, command = _legacy_cli(tmp_path) + if mutation == "attachment": + changed = json.loads(attachment.read_text()) + changed["interpretation"] = "A changed request cannot borrow the old success." + attachment.write_text(json.dumps(changed)) + elif mutation == "missing-graph": + (runtime / "goals" / GOAL_ID / "explore-result-log.jsonl").unlink() + elif mutation == "missing-hook": + for receipt in (runtime / "goals" / GOAL_ID / "post_writeback_hooks").glob("*.json"): + receipt.unlink() + before = _business_files(runtime) + code, replay = _run_cli(registry, runtime, *command, cwd=project) + assert not replay["appended"], replay + if mutation is None: + assert code == 0 and replay["idempotent_replay"], replay + delivery = replay["explore_result_delivery"] + assert delivery["ok"] and delivery["idempotent_replay"] + assert delivery["hook"]["invoked_count"] == 0 + assert _business_files(runtime) == before + code, again = _run_cli(registry, runtime, *command, cwd=project) + assert code == 0 and again["explore_result_delivery"]["idempotent_replay"], again + assert _business_files(runtime) == before + else: + assert code == 1 and not replay["ok"], replay + assert not replay.get("explore_result_delivery", {}).get("ok"), replay + after = _business_files(runtime) + # A missing hook may gain a failed-dispatch receipt. It cannot mint a + # valid intent, append graph evidence, mutate the Todo or recommit primary. + stable_before = {key: value for key, value in before.items() if "/post_writeback_hooks/" not in key} + stable_after = {key: value for key, value in after.items() if "/post_writeback_hooks/" not in key} + assert stable_after == stable_before diff --git a/tests/control_plane/test_explore_measurement_history.py b/tests/control_plane/test_explore_measurement_history.py new file mode 100644 index 0000000000..fa93e70c4e --- /dev/null +++ b/tests/control_plane/test_explore_measurement_history.py @@ -0,0 +1,266 @@ +"""Base-writer history remains auditable after stricter new-write attribution.""" +from __future__ import annotations + +import hashlib +import json +from copy import deepcopy +from pathlib import Path + +import pytest + +from loopx.capabilities.explore.result_log import ( + append_explore_result_event, append_explore_result_events, build_explore_finding_event, + build_explore_result_projection, explore_result_log_path, load_explore_result_events_strict, +) +from loopx.control_plane.effect_runtime import effect_runtime_result +from loopx.presentation.explore_results_api import _finding_rows, _history_rows + +# Written by the real builders/append API at main 21367015cce45294dcf99f54a3211d66f5d842c5. +# Synthetic protocol records only: a node, mixed A/B finding, then A/A correction. +FIXTURE = Path(__file__).with_name("fixtures") / "explore_measurement_history_v0.jsonl" +REGISTERED_FIXTURE = FIXTURE.with_name("explore_registered_measurement_history_v0.jsonl") +DELIVERY_FIXTURE = FIXTURE.with_name("explore_committed_measurement_delivery_v0.json") + + +def test_base_history_and_corrections_remain_readable_without_promoting_mixed_measurements(tmp_path): + original = FIXTURE.read_bytes() + assert hashlib.sha256(original).hexdigest() == "5473f0ef178e732be19dcc5fd6cb7c56de964a080bbe1fd7844c1dd4cd66a940" + events = [json.loads(line) for line in original.splitlines()] + runtime = tmp_path / "runtime" + log = explore_result_log_path(runtime, "research") + log.parent.mkdir(parents=True) + prefix = b"\n".join(original.splitlines()[:2]) + b"\n" + log.write_bytes(prefix) + retained = load_explore_result_events_strict(log, goal_id="research") + assert retained == events[:2] + assert _history_rows(runtime, "research")[1]["measurements"][0]["evidence_ref"] == "result:b" + [current] = _finding_rows(retained, [], goal_id="research") + assert "measurements" not in current + assert "executed candidate's result" in current["measurement_error"] + projection = build_explore_result_projection(retained, goal_id="research") + request = dict(goal_id="research", agent_id="worker", route=["loopx"], graph_enabled=True, + harness_gate={"enabled": True}, projection=projection, plan={}) + turn = effect_runtime_result("explore.turn_context", request)["graph"] + assert not turn.get("measurement_feedback") + assert turn["writeback_results"][0]["measurement_error"] == current["measurement_error"] + # New writes cannot bypass the builder by supplying a legacy canonical row. + for append in ( + lambda: append_explore_result_event(log, events[1]), + lambda: append_explore_result_events(log, [events[1]], expected_goal_id="research"), + ): + with pytest.raises(ValueError, match="executed candidate's result"): + append() + assert log.read_bytes() == prefix + corrected = events[2] + append_explore_result_event(log, corrected) + assert log.read_bytes() == original, "new correction appends without rewriting the old evidence" + assert load_explore_result_events_strict(log, goal_id="research") == events + history = _history_rows(runtime, "research") + assert len(history) == 3 and history[1]["measurements"][0]["evidence_ref"] == "result:b" + [current] = _finding_rows(events, [], goal_id="research") + assert "measurement_error" not in current + assert current["measurements"][0]["evidence_ref"] == "result:a" + request["projection"] = build_explore_result_projection(events, goal_id="research") + feedback = effect_runtime_result("explore.turn_context", request)["graph"]["measurement_feedback"] + assert feedback[0]["best"]["evidence_ref"] == "result:a" + assert feedback[0]["measured_count"] == 1 + # Correcting an already corrected finding remains an ordinary strict write. + fields = {key: corrected[key] for key in ("goal_id", "title", "node_id", "status", "evidence_refs", + "execution_observation", "measurements")} + again = build_explore_finding_event(**fields, finding_id="measured", recorded_at="2026-01-01T03:00:00Z") + append_explore_result_event(log, again) + assert len(load_explore_result_events_strict(log, goal_id="research")) == 4 + assert len(_history_rows(runtime, "research")) == 4 + assert log.read_bytes().startswith(original) + # Explicit archival prefix replay retains old facts and the correction; + # it grants neither execution nor quantitative validity to the old row. + replay = tmp_path / "replayed.jsonl" + restored = append_explore_result_events(replay, events, expected_goal_id="research", require_ordered_prefix=True) + assert restored["appended_event_count"] == 3 + assert replay.read_bytes() == original + repeated = append_explore_result_events(replay, events, expected_goal_id="research", require_ordered_prefix=True) + assert repeated["appended_event_count"] == 0 + assert repeated["reused_event_count"] == 3 + + +def test_historical_read_still_rejects_forged_canonical_records(tmp_path): + event = json.loads(FIXTURE.read_text().splitlines()[1]) + event["event_id"] = "forged" + log = tmp_path / "results.jsonl" + log.write_text(json.dumps(event) + "\n") + with pytest.raises(ValueError, match="canonical"): + load_explore_result_events_strict(log, goal_id="research") + + +def test_registered_history_preview_executes_without_promoting_legacy_measurements(tmp_path): + from loopx.capabilities.explore.source_history_reconcile import reconcile_explore_source_history + + # The real writer at 21367015 produced these bytes, including two old + # contradictions and an independent valid finding. No head writer repairs them. + original = REGISTERED_FIXTURE.read_bytes() + assert hashlib.sha256(original).hexdigest() == "ef4d31cd7a71efb5b6745cea832de21c797dc58ece03eebb79aeb93f1c9e55eb" + events = [json.loads(line) for line in original.splitlines()] + candidate = tmp_path / "candidate.jsonl" + candidate.write_bytes(original) + canonical = tmp_path / "canonical.jsonl" + prefix = original.splitlines(keepends=True)[0] + canonical.write_bytes(prefix) + args = dict(canonical_log_path=canonical, candidate_log_path=candidate, goal_id="research", + registered_result_keys={"research:nodes:question-a", "research:findings:valid", + "research:findings:mixed", "research:findings:mixed-later"}) + preview = reconcile_explore_source_history(**args) + assert preview["status"] == "would_reconcile" + assert preview["projection_reconciliation"]["recovered_lost_history_count"] == 3 + assert canonical.read_bytes() == prefix + receipt = reconcile_explore_source_history(**args, execute=True) + assert receipt["status"] == "reconciled" + assert receipt["writeback"]["appended_event_count"] == 3 + assert receipt["writeback"]["readback_verified"] is True + assert canonical.read_bytes() == candidate.read_bytes() == original + repeated = reconcile_explore_source_history(**args, execute=True) + assert repeated["status"] == "already_reconciled" + assert repeated["writeback"]["performed"] is False + projection = build_explore_result_projection(events, goal_id="research") + assert sum("measurement_error" in row for row in projection["findings"]) == 2 + request = dict(goal_id="research", agent_id="worker", route=["loopx"], graph_enabled=True, + harness_gate={"enabled": True}, projection=projection, plan={}) + feedback = effect_runtime_result("explore.turn_context", request)["graph"]["measurement_feedback"] + assert feedback[0]["measured_count"] == 1 + assert feedback[0]["best"]["value"] == 8 + with pytest.raises(ValueError, match="executed candidate's result"): + append_explore_result_events(canonical, events[1:], expected_goal_id="research") + assert canonical.read_bytes() == original + corrected = {key: events[2][key] for key in ("goal_id", "title", "node_id", "status", + "evidence_refs", "execution_observation")} + correction = build_explore_finding_event(**corrected, finding_id="mixed", + recorded_at="2026-01-01T04:00:00Z", measurements=[{**events[2]["measurements"][0], + "value": 7, "evidence_ref": "result:a"}]) + append_explore_result_event(canonical, correction) + assert canonical.read_bytes().startswith(original) + request["projection"] = build_explore_result_projection( + load_explore_result_events_strict(canonical, goal_id="research"), goal_id="research") + group = effect_runtime_result("explore.turn_context", request)["graph"]["measurement_feedback"][0] + assert (group["measured_count"], group["best"]["value"], group["latest"]["measurement"]["value"], + group["improvement"]) == (2, 8, 7, -1) + + +@pytest.mark.parametrize("damage", ["concurrent-event", "incomplete-tail"]) +def test_registered_history_rechecks_canonical_snapshot_under_lock(tmp_path, monkeypatch, damage): + from loopx.capabilities.explore import source_history_reconcile as owner + + candidate = tmp_path / "candidate.jsonl" + candidate.write_bytes(REGISTERED_FIXTURE.read_bytes()) + canonical = tmp_path / "canonical.jsonl" + prefix = candidate.read_bytes().splitlines(keepends=True)[0] + canonical.write_bytes(prefix) + append = owner._append_validated_explore_result_events + raced = [] + def race(path, events, **kwargs): + if damage == "concurrent-event": + append_explore_result_event(path, build_explore_finding_event(goal_id="research", + title="Concurrent independent result", finding_id="concurrent", + node_id="question-a", recorded_at="2026-01-01T05:00:00Z")) + else: + path.write_bytes(prefix + b'{"incomplete":') + raced.append(path.read_bytes()) + return append(path, events, **kwargs) + monkeypatch.setattr(owner, "_append_validated_explore_result_events", race) + with pytest.raises(ValueError): + owner.reconcile_explore_source_history(canonical_log_path=canonical, + candidate_log_path=candidate, goal_id="research", execute=True, + registered_result_keys={"research:nodes:question-a", "research:findings:valid", + "research:findings:mixed", "research:findings:mixed-later"}) + assert canonical.read_bytes() == raced[0] + assert candidate.read_bytes() == REGISTERED_FIXTURE.read_bytes() + + +def _legacy_delivery(tmp_path, provider): + from tests.capabilities.test_explore_turn_context import registry + from loopx.todos import add_goal_todo, update_goal_todo + + raw = DELIVERY_FIXTURE.read_bytes() + assert hashlib.sha256(raw).hexdigest() == "35566a01d64695661d8f2db38234f43d614894301054a9c277abecf4a940ae13" + legacy = json.loads(raw) + path = registry(tmp_path, graph=True, planning=True, provider=provider) + todo = add_goal_todo(registry_path=path, goal_id="research", role="agent", agent_id="worker", + claimed_by="worker", text="Recover the recorded candidate result", + operation_id="explore-legacy-writeback-fixture") + assert todo["todo_id"] == legacy["todo_id"] + update_goal_todo(registry_path=path, goal_id="research", todo_id=todo["todo_id"], + agent_id="worker", append_explore_result_node_refs=["question-a"]) + root = tmp_path / "runtime" + log = explore_result_log_path(root, "research") + log.parent.mkdir(parents=True, exist_ok=True) + log.write_text(legacy["events_jsonl"], encoding="utf-8") + for relative, content in legacy["hook_files"].items(): + receipt = root / relative + receipt.parent.mkdir(parents=True, exist_ok=True) + receipt.write_text(content, encoding="utf-8") + return legacy, dict(registry_path=path, runtime_root=root, goal_id="research", agent_id="worker", + todo_id=todo["todo_id"], turn_instance_id=legacy["turn_instance_id"]), log + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_base_committed_delivery_replays_read_only(tmp_path, provider, monkeypatch): + from loopx.capabilities.explore import result_writeback + from loopx.todos import list_goal_todos + + legacy, args, log = _legacy_delivery(tmp_path, provider) + before = log.read_bytes() + todo_args = {key: args[key] for key in ("registry_path", "goal_id", "todo_id")} + todo_before = list_goal_todos(**todo_args)["todo"] + hook_before = {relative: (args["runtime_root"] / relative).read_bytes() + for relative in legacy["hook_files"]} + def reject_mutation(**_): + pytest.fail("completed delivery must not mutate the Todo or append graph events") + monkeypatch.setattr(result_writeback, "update_goal_todo", reject_mutation) + monkeypatch.setattr(result_writeback, "append_explore_result_events", reject_mutation) + for supplement in (False, True): + payload = deepcopy(legacy["payload"]) + if supplement: + payload["explore_result_recorded_at"] = payload["generated_at"] + payload["generated_at"] = "2026-01-02T00:00:00Z" + replay = result_writeback.deliver_result_attachment(payload=payload, **args) + assert replay["ok"] and replay["idempotent_replay"] and not replay["retryable"], replay + assert replay["hook"]["invoked_count"] == 0 + assert replay["hook"]["replayed_hooks"] == ["explore.result_writeback"] + assert log.read_bytes() == before + assert list_goal_todos(**todo_args)["todo"] == todo_before + assert {relative: (args["runtime_root"] / relative).read_bytes() + for relative in hook_before} == hook_before + + +@pytest.mark.parametrize("mutation", ["receipt", "source", "time", "attachment", "canonical", "link"]) +def test_legacy_replay_requires_exact_receipt_event_and_todo_link(tmp_path, mutation): + from loopx.capabilities.explore import result_writeback + from loopx.todos import update_goal_todo, list_goal_todos + + legacy, args, log = _legacy_delivery(tmp_path, "file") + payload = deepcopy(legacy["payload"]) + if mutation == "receipt": + for relative in legacy["hook_files"]: + (args["runtime_root"] / relative).unlink() + elif mutation == "source": + payload["settlement_identity"]["effect_id"] = "different-source" + elif mutation == "time": + payload["generated_at"] = "2026-01-02T00:00:00Z" + elif mutation == "attachment": + payload["explore_result"]["interpretation"] = "A conflicting retry cannot rewrite the receipt." + elif mutation == "canonical": + log.write_text(legacy["events_jsonl"].splitlines()[0] + "\n") + else: + config = json.loads(args["registry_path"].read_text()) + config["goals"][0]["coordination"]["registered_agents"].append("other") + args["registry_path"].write_text(json.dumps(config)) + update_goal_todo(registry_path=args["registry_path"], goal_id="research", agent_id="worker", + todo_id=args["todo_id"], explore_result_node_refs=[]) + # An unfinished association must still obey current ownership. + update_goal_todo(registry_path=args["registry_path"], goal_id="research", agent_id="worker", + todo_id=args["todo_id"], claimed_by="other") + before = log.read_bytes() + todo_args = {key: args[key] for key in ("registry_path", "goal_id", "todo_id")} + todo_before = list_goal_todos(**todo_args)["todo"] + result = result_writeback.deliver_result_attachment(payload=payload, **args) + assert not result["ok"] and result["primary_committed"], result + assert log.read_bytes() == before + assert list_goal_todos(**todo_args)["todo"] == todo_before diff --git a/tests/control_plane/test_explore_result_writeback.py b/tests/control_plane/test_explore_result_writeback.py index adc863152c..31c2cd6b77 100644 --- a/tests/control_plane/test_explore_result_writeback.py +++ b/tests/control_plane/test_explore_result_writeback.py @@ -185,7 +185,7 @@ def interrupt(**kwargs): assert second["ok"] and second["graph"]["appended_event_count"] == 0 -@pytest.mark.parametrize("case", ["disabled", "null", "invalid", "blank-status", "conflict", "off-without-result", +@pytest.mark.parametrize("case", ["disabled", "null", "invalid", "blank-status", "conflict", "cross-result", "off-without-result", "path-disabled", "path-missing", "path-no-evidence", "path-overflow"]) def test_real_cli_inline_validation_precedes_primary_commit(tmp_path, case): from canonical_authority_fixture import initialize_canonical_authority @@ -230,6 +230,22 @@ def test_real_cli_inline_validation_precedes_primary_commit(tmp_path, case): packet["explore_result"]["evidence_refs"] = [] if case == "blank-status": packet["explore_result"]["status"] = "" + if case == "cross-result": + attachment = packet["explore_result"] + attachment["evidence_refs"] = ["result:a", "result:b"] + attachment["execution_observation"] = { + "schema_version": "explore_execution_observation_v0", + "candidate_sha256": "a" * 64, "input_revision": attachment["input_revision"], + "planned_method_id": "probe", "executed_method_id": "probe", "change_reason": None, + "elapsed_seconds": 1, "limit_seconds": 10, "search_iterations": None, + "stop_reason": "process_exit:0", "evidence_refs": ["result:a"], + "result_ref": "result:a", "result_candidate_sha256": "a" * 64, + } + attachment["measurements"] = [{ + "metric": "quality", "value": 8, "unit": "points", "higher_is_better": True, + "comparison_scope": "validation:budget", "input_revision": attachment["input_revision"], + "evaluation_ref": "evaluator:v1", "candidate_ref": "candidate:b", "evidence_ref": "result:b", + }] if case.startswith("path-"): packet["explore_result"] = { key: ATTACHMENT[key] for key in @@ -270,6 +286,8 @@ def test_real_cli_inline_validation_precedes_primary_commit(tmp_path, case): else: assert rc == 1 and not result["appended"], result assert (index.read_bytes() if index.exists() else b"") == before + if case == "cross-result": + assert "executed candidate's result reference" in result["error"] assert not explore_result_log_path(runtime, GOAL_ID).exists() diff --git a/tests/control_plane/test_research_execution_loop.py b/tests/control_plane/test_research_execution_loop.py index 9ea048dbd4..cee7f893ca 100644 --- a/tests/control_plane/test_research_execution_loop.py +++ b/tests/control_plane/test_research_execution_loop.py @@ -40,6 +40,41 @@ def test_real_probe_reports_early_exit_not_budget_exhaustion(tmp_path, monkeypat assert (tmp_path / "observation.json.stdout").read_text().strip() == "5" +def test_finding_measurements_keep_the_executed_result_identity(tmp_path, monkeypatch): + from loopx.capabilities.explore.result_log import ( + append_explore_result_event, build_explore_finding_event, + load_explore_result_events_strict, + ) + + observed = probe(tmp_path, monkeypatch) + observed.update(result_ref="result:executed", result_candidate_sha256=observed["candidate_sha256"]) + observed["evidence_refs"].append("result:executed") + observed["evidence_refs"].sort() + measurement = { + "metric": "quality", "value": 8, "unit": "points", "higher_is_better": True, + "comparison_scope": "validation:fixed", "input_revision": observed["input_revision"], + "evaluation_ref": "evaluator:v1", "candidate_ref": "candidate:executed", + "evidence_ref": "result:other", + } + fields = dict( + goal_id="research", title="The exact executed result", finding_id="measured", + node_id="question", status="tentative", execution_observation=observed, + evidence_refs=[*observed["evidence_refs"], "result:other"], + ) + log = tmp_path / "results.jsonl" + with pytest.raises(ValueError, match="executed candidate's result"): + build_explore_finding_event(**fields, measurements=[measurement]) + assert not log.exists(), "inconsistent evidence must fail before publication" + measurement["evidence_ref"] = "result:executed" + with pytest.raises(ValueError, match="executed candidate's input"): + build_explore_finding_event(**fields, measurements=[{**measurement, "input_revision": "other-input"}]) + event = build_explore_finding_event(**fields, measurements=[measurement]) + append_explore_result_event(log, event) + [retained] = load_explore_result_events_strict(log, goal_id="research") + assert retained["measurements"] == [measurement] + assert retained["execution_observation"] == observed + + def test_missing_solver_telemetry_stays_unknown_and_timeout_is_host_observed(tmp_path, monkeypatch): observation = probe(tmp_path, monkeypatch, source="import time; time.sleep(10)", limit_seconds=0.05) assert observation["search_iterations"] is None diff --git a/tests/control_plane_ts/explore_measurements.test.ts b/tests/control_plane_ts/explore_measurements.test.ts index 90551704e7..8259220944 100644 --- a/tests/control_plane_ts/explore_measurements.test.ts +++ b/tests/control_plane_ts/explore_measurements.test.ts @@ -1,7 +1,8 @@ import assert from "node:assert/strict"; import test from "node:test"; -import {normalizeExploreMeasurements, projectExploreMeasurementFeedback} from "../../loopx/control_plane/capabilities/explore_measurements.ts"; -import {normalizeExploreResultAttachment} from "../../loopx/control_plane/capabilities/explore_result_writeback.ts"; +import {readFileSync} from "node:fs"; +import {normalizeExploreMeasurements, normalizeExploreMeasurementsRequest, projectExploreMeasurementFeedback} from "../../loopx/control_plane/capabilities/explore_measurements.ts"; +import {normalizeExploreResultAttachment, produceExploreResultIntent} from "../../loopx/control_plane/capabilities/explore_result_writeback.ts"; import {projectExploreTurnContext} from "../../loopx/control_plane/capabilities/explore_turn_context.ts"; import {parseExploreHistoryPage, projectExploreHistory} from "../../loopx/control_plane/presentation/explore_history.ts"; @@ -11,6 +12,45 @@ const metric = {metric: "quality", value: 8, unit: "points", higher_is_better: t const finding = (id: string, value: number | null, extra = {}) => ({finding_id: id, node_id: "question-a", measurements: [{...metric, value, candidate_ref: `candidate:${id}`, evidence_ref: `result:${id}`, ...extra}]}); +test("attachment transport preserves historical input while new ingestion rejects cross-result measurements", () => { + const execution = {schema_version: "explore_execution_observation_v0", + candidate_sha256: "a".repeat(64), result_candidate_sha256: "a".repeat(64), + input_revision: metric.input_revision, planned_method_id: "probe", executed_method_id: "probe", + change_reason: null, elapsed_seconds: 1, limit_seconds: 10, search_iterations: null, + stop_reason: "process_exit:0", evidence_refs: ["result:executed"], result_ref: "result:executed"}; + const attachment = {schema_version: "explore_result_attachment_v0", question: "Which candidate improved?", + applicability: "Fixed task and budget", input_revision: metric.input_revision, + observation: "A measured result", interpretation: "Investigate the actual candidate", + status: "tentative", evidence_refs: [metric.evidence_ref, "result:executed"], + measurements: [metric], execution_observation: execution}; + // Parsing precedes immutable primary-receipt recovery. It must preserve an + // old successful input; only a new effect qualifies its execution binding. + const normalized = normalizeExploreResultAttachment({attachment}); + assert.deepEqual(normalized.measurements, [metric]); + const intent = (value: unknown) => produceExploreResultIntent({ + projection: {explore_result: value}, receipt: {event_id: "receipt:synthetic"}, + }); + assert.throws(() => intent(normalized), /executed candidate's result/); + assert.throws(() => normalizeExploreMeasurementsRequest({measurements: [metric], + execution_observation: execution}), /executed candidate's result/); + const matching = {...metric, evidence_ref: execution.result_ref}; + assert.deepEqual(normalizeExploreResultAttachment({attachment: {...attachment, + measurements: [matching]}}).measurements, [matching]); + assert.equal(intent({...attachment, measurements: [matching]}).status, "intent"); + assert.deepEqual(normalizeExploreMeasurementsRequest({measurements: [matching], + execution_observation: execution}).measurements, [matching]); + assert.throws(() => normalizeExploreMeasurementsRequest({measurements: [{...matching, + input_revision: "other-input"}], execution_observation: execution}), /executed candidate's input/); + const {comparison_scope, input_revision, evaluation_ref, ...observation} = matching; + assert.throws(() => normalizeExploreMeasurementsRequest({measurements: [{...observation, + evidence_ref: "result:other"}], measurement_context: {comparison_scope, input_revision, evaluation_ref}, + execution_observation: execution}), /executed candidate's result/); + assert.deepEqual(normalizeExploreMeasurements([metric]), [metric], "legacy measurements remain unchanged"); + const {result_ref: _result, result_candidate_sha256: _candidate, ...unfinished} = execution; + assert.deepEqual(normalizeExploreMeasurementsRequest({measurements: [metric], + execution_observation: unfinished}).measurements, [metric], "missing result is not inferred"); +}); + test("explicit comparison context is reused while each result retains its own candidate and receipt", () => { const {comparison_scope, input_revision, evaluation_ref, ...observation} = metric; const context = {comparison_scope, input_revision, evaluation_ref}; @@ -229,3 +269,20 @@ test("measurement feedback pages retain every group and bind manual findings to assert.throws(() => projectExploreTurnContext({...corrected, result_offset: 3, result_revision: snapshot.result_page.revision}), /changed/); }); + + +test("base-produced mixed history remains auditable but only the correction enters comparison", () => { + const events = readFileSync(new URL("../control_plane/fixtures/explore_measurement_history_v0.jsonl", import.meta.url), "utf8") + .trim().split("\n").map(line => JSON.parse(line)); + const before = parseExploreHistoryPage({ok: true, goal_id: "research", items: events.slice(0, 2), total: 2, next_cursor: null}, "research"); + const historical = projectExploreHistory(before.items, "research"); + assert.equal(historical.events[1].measurements?.[0].evidence_ref, "result:b"); + assert.equal(historical.measurementFeedback, undefined, "the contradiction is not a best, latest, unknown or scored point"); + const corrected = parseExploreHistoryPage({ok: true, goal_id: "research", items: events, total: 3, next_cursor: null}, "research"); + const current = projectExploreHistory(corrected.items, "research"); + assert.equal(current.events.length, 3); + assert.equal(current.measurementFeedback?.[0].best?.evidence_ref, "result:a"); + assert.equal(current.measurementFeedback?.[0].measured_count, 1); + const raw = {...finding("legacy", 99), execution_observation: events[1].execution_observation}; + assert.deepEqual(projectExploreMeasurementFeedback([raw]), []); +});