From 0c48b32d71f141ac3fe1ba5bdc2239ccf13080e1 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Sat, 7 Nov 2026 21:36:06 +0800 Subject: [PATCH] fix(stride): expose the existing review citation limit Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../goal-vision-replan-contract-v0.md | 5 +++++ .../control_plane/stride_decision_schema.json | 1 + .../work_items/replan_context.ts | 6 +++--- .../control_plane/work_items/stride_policy.ts | 8 +++++--- .../test_replan_progress_native_cli.py | 1 + tests/control_plane_ts/stride_policy.test.ts | 19 +++++++++++++++++++ 6 files changed, 34 insertions(+), 6 deletions(-) diff --git a/docs/reference/protocols/goal-vision-replan-contract-v0.md b/docs/reference/protocols/goal-vision-replan-contract-v0.md index 72ed12e408..9517a546d1 100644 --- a/docs/reference/protocols/goal-vision-replan-contract-v0.md +++ b/docs/reference/protocols/goal-vision-replan-contract-v0.md @@ -640,6 +640,11 @@ exact `replan-decision-*` id and separately records: - `goal_gain`: improved, flat, regressed or unknown; - delivered evidence references and the bounded lesson for the next experiment. +The authoring contract exposes `review_reference_limit` (3). It applies separately +to `previous_review.evidence_refs` and `explore_review.considered_refs`. + +编写契约的 `review_reference_limit`(3)分别限制上述两个引用列表。 + A claim needs evidence references; unavailable evidence remains unknown. These are explicit Agent assessments, not host certification of scientific truth. The next review retains the assessment with the original decision and subsequent diff --git a/loopx/control_plane/stride_decision_schema.json b/loopx/control_plane/stride_decision_schema.json index a7aaa3b16b..537b6c268a 100644 --- a/loopx/control_plane/stride_decision_schema.json +++ b/loopx/control_plane/stride_decision_schema.json @@ -7,6 +7,7 @@ "probe_field_limits": {"scope": 120, "prediction": 120, "adoption_criterion": 120, "cost_bound": 100, "minimum_evidence": 120}, "axis_reason_limit": 96, "review_text_limit": 120, + "review_reference_limit": 3, "adoption_options": ["executed", "partially_executed", "not_executed", "unknown"], "information_options": ["new_evidence", "repeated_evidence", "unknown"], "gain_options": ["improved", "flat", "regressed", "unknown"], diff --git a/loopx/control_plane/work_items/replan_context.ts b/loopx/control_plane/work_items/replan_context.ts index fbac5ab5e6..7e63597d82 100644 --- a/loopx/control_plane/work_items/replan_context.ts +++ b/loopx/control_plane/work_items/replan_context.ts @@ -8,7 +8,7 @@ import {requireJsonObject, requireNonEmptyString} from "../runtime_decode.ts"; import {FRESH_PATH_DISPOSITIONS, requiredSemanticOutcomes} from "./replan_semantics.ts"; import {readReplanSnapshot} from "./replan_history_snapshot.ts"; import {normalizeGoalPathDelta} from "../goals/vision_checkpoint.ts"; -import {normalizeRecordedStrideDecision, normalizeStrideStrategy, projectStrideExploreBasis} from "./stride_policy.ts"; +import {normalizeRecordedStrideDecision, normalizeStrideStrategy, projectStrideExploreBasis, STRIDE_REVIEW_REFERENCE_LIMIT} from "./stride_policy.ts"; import {PROGRESS_RESULT_CLASSES} from "./replan_history.ts"; import {MATERIAL_DELIVERY_OUTCOMES} from "./delivery_outcome.ts"; @@ -294,8 +294,8 @@ function reviewEvidence(rows: JsonObject[], request: JsonObject): JsonObject { throw new EffectRuntimeRequestError("previous decision changed; refresh replan context before reviewing its outcome"); } const refs = request.evidence_refs; - if (!Array.isArray(refs) || refs.length > 3 || new Set(refs).size !== refs.length) { - throw new EffectRuntimeRequestError("previous review accepts at most three distinct evidence references"); + if (!Array.isArray(refs) || refs.length > STRIDE_REVIEW_REFERENCE_LIMIT || new Set(refs).size !== refs.length) { + throw new EffectRuntimeRequestError(`previous review accepts at most ${STRIDE_REVIEW_REFERENCE_LIMIT} distinct evidence references`); } for (const ref of refs) identifier(ref, "review evidence reference"); const source = objectOrNull(previous!._source_facts)!; diff --git a/loopx/control_plane/work_items/stride_policy.ts b/loopx/control_plane/work_items/stride_policy.ts index 2e6cc2662e..94697734d4 100644 --- a/loopx/control_plane/work_items/stride_policy.ts +++ b/loopx/control_plane/work_items/stride_policy.ts @@ -39,6 +39,7 @@ export const STRIDE_AXIS_NAMES = decisionSchema.axis_names; export const STRIDE_PLAN_LIMIT = decisionSchema.axis_plan_limit; export const STRIDE_REASON_LIMIT = decisionSchema.axis_reason_limit; export const STRIDE_REVIEW_LIMIT = decisionSchema.review_text_limit; +export const STRIDE_REVIEW_REFERENCE_LIMIT = decisionSchema.review_reference_limit; // Persisted v1 records retain their original meaning; never offered for new reviews. export const RECORDED_STRIDE_SCHEMA = "stride_decision_v1"; const recordedAdjustments = ["grow", "hold", "shrink"] as const; @@ -52,9 +53,9 @@ function exactFields(value: JsonObject, fields: string[], label: string): void { if (Object.keys(value).some(key => !fields.includes(key))) throw new EffectRuntimeRequestError(`${label} contains unsupported fields`); } function refs(value: unknown, label: string): string[] { - if (!Array.isArray(value) || value.length > 3 || value.some(ref => typeof ref !== "string" || + if (!Array.isArray(value) || value.length > STRIDE_REVIEW_REFERENCE_LIMIT || value.some(ref => typeof ref !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(ref)) || new Set(value).size !== value.length) { - throw new EffectRuntimeRequestError(`${label} requires at most three distinct public-safe evidence references`); + throw new EffectRuntimeRequestError(`${label} requires at most ${STRIDE_REVIEW_REFERENCE_LIMIT} distinct public-safe evidence references`); } return value as string[]; } @@ -389,6 +390,7 @@ export function strideControlContract(context: JsonObject, obligationId: unknown axis_reason_limit: decisionSchema.axis_reason_limit, probe_field_limits: STRIDE_PROBE_LIMITS, decision_value_review: {...decisionValueReview}, review_text_limit: decisionSchema.review_text_limit, + review_reference_limit: STRIDE_REVIEW_REFERENCE_LIMIT, previous_decision_ref: previous, ...(follow ? {decision_followthrough: follow} : {}), previous_review_options: {adoption: decisionSchema.adoption_options, information_gain: decisionSchema.information_options, goal_gain: decisionSchema.gain_options}, @@ -406,6 +408,6 @@ export function strideControlContract(context: JsonObject, obligationId: unknown required_on_periodic_review: true, rule: "Enabled periodic and observed-stall reviews require a current evidence-linked Vision path and stride decision before any settlement, including progress/successor/blocker/terminal exits. " + "Each axis needs plan/reason: grow useful completed scope on a supported route; switch names reference method and changed mechanism; verify resolves a grow-or-switch question. Apply these plans to the actual Todo scope and acceptance using the existing Todo authoring/update commands, then record the verified IDs in todo_delta. Retain unfinished useful work; do not create successor microtasks merely to count progress. On grow, consolidate fragmented setup, implementation, integration and decisive validation into a coherent accepted deliverable or complete interpretable experiment, including engineering/maintenance. The next admitted work must execute that scope, method and validation plan; changing review frequency or repeating the proposal is not adoption. Keep the configured review interval and preserve dependencies, acceptance, risk, authority and per-Turn settlement boundaries. " + - "Review the exact previous decision's adoption/information/goal gain and delivered Explore sources in their review fields. Missing/omitted/unread/unprojected evidence stays unknown, not exhausted. Guidance adds no fields/gates; the example is syntax, not an action recommendation. Claims certify no authority, execution or utility. Other legal outcomes cannot bypass a due review.", + "Review the exact previous decision's adoption/information/goal gain and delivered Explore sources in their review fields. Apply review_reference_limit separately to previous_review.evidence_refs and explore_review.considered_refs. Missing/omitted/unread/unprojected evidence stays unknown, not exhausted. Guidance adds no fields/gates; the example is syntax, not an action recommendation. Claims certify no authority, execution or utility. Other legal outcomes cannot bypass a due review.", }; } diff --git a/tests/control_plane/test_replan_progress_native_cli.py b/tests/control_plane/test_replan_progress_native_cli.py index 6faa860cec..d7d580f187 100644 --- a/tests/control_plane/test_replan_progress_native_cli.py +++ b/tests/control_plane/test_replan_progress_native_cli.py @@ -347,6 +347,7 @@ def spend(turn: str, *, todo: str | None = TODO, obligation: str | None = None) assert fixture["index"].read_bytes() == original_index control = admitted["replan_action_packet"]["stride_control"] + assert control["review_reference_limit"] == 3 decision = {**control["minimal_example"], "basis": "unexplained_gap", "action": "verify", "axes": { "work_slice": {"plan": "Run and compare three complete assignment methods.", diff --git a/tests/control_plane_ts/stride_policy.test.ts b/tests/control_plane_ts/stride_policy.test.ts index ae30a9dca4..f0413f82f9 100644 --- a/tests/control_plane_ts/stride_policy.test.ts +++ b/tests/control_plane_ts/stride_policy.test.ts @@ -94,6 +94,25 @@ test("Explore review keeps bare lowercase digest revisions and unavailable null } }); +test("each authored review list advertises and enforces its own three-reference bound", () => { + const control = strideControlContract({stride_strategy: "evidence_conditioned_v1"})!; + assert.equal(control.review_reference_limit, 3); + const evidence = ["evidence:first", "evidence:second", "evidence:third"]; + const review = {reference_decision_ref: "replan-decision-" + "b".repeat(24), adoption: "unknown", + information_gain: "unknown", goal_gain: "unknown", lesson: "Interpret three observations together.", + evidence_refs: evidence}; + const explore = {revision: "a".repeat(64), disposition: "used", considered_refs: evidence, reason: "Compared three findings."}; + const decision = {...proposal, previous_review: review, explore_review: explore}; + assert.deepEqual(normalizeStrideDecision(decision)[0]!.previous_review, review); + for (const extra of ["evidence:fourth", "evidence:first"]) { + const refs = extra === "evidence:first" ? [extra, extra] : [...evidence, extra]; + assert.throws(() => normalizeStrideDecision({...decision, previous_review: {...review, evidence_refs: refs}}), + /previous_review\.evidence_refs requires at most 3 distinct/); + assert.throws(() => normalizeStrideDecision({...decision, explore_review: {...explore, considered_refs: refs}}), + /explore_review\.considered_refs requires at most 3 distinct/); + } +}); + test("concrete independent plans retain action choice and fit the shared vision budget", () => { for (const basis of ["unexplained_gap", "uncertain_evidence", "supported_progress"]) { const decision = normalizeStrideDecision({...proposal, basis})[0]!;