diff --git a/docs/architecture/rfcs/goal-direction-baseline-v0.md b/docs/architecture/rfcs/goal-direction-baseline-v0.md index 04d4da09cf..f4ee6ea1c3 100644 --- a/docs/architecture/rfcs/goal-direction-baseline-v0.md +++ b/docs/architecture/rfcs/goal-direction-baseline-v0.md @@ -415,7 +415,10 @@ materials. | M1 | Optional pure builder over full Goal authority plus receipts; no consumer or writer. | Approval of Section 12 decisions and schema limits. | Deterministic fixtures, mutation checks, Python/TypeScript parity if both runtimes consume it, and public-boundary scan. | Remove the default-off builder and projection. | | M2 | One read-only Agent-scoped Vision-gap consumer. | M1 conformance plus explicit maintainer approval of the consumer and recovery UX. | End-to-end proof that drift is surfaced, same-revision replay is quiet, and no Vision/Todo/route mutation occurs. | Disable the consumer; retain canonical registry and receipts. | -Later milestones are not authorized by merging M0. +Later milestones are not authorized by merging M0. The Appendix D F2 fixture +is checked in ahead of M1 as a conformance harness +([examples/goal-direction-baseline-f2-smoke.py](../../../examples/goal-direction-baseline-f2-smoke.py)); +it does not authorize M1 and adds no runtime builder, writer, or consumer. ## 12. Open decisions @@ -445,6 +448,18 @@ Later milestones are not authorized by merging M0. qualification - **Effect on normative design:** initial proposal +### 2026-10-04 — F2 synthetic fixture (pre-M1) + +- **Baseline:** `99839ae` +- **Delivered:** the Appendix D F2 row as a checked-in public-safe smoke with + its mutation arm (`examples/goal-direction-baseline-f2-smoke.py`); the + Section 11 note records why M1 stays closed +- **Evidence:** the smoke passes locally and `loopx check --scan-path + docs/architecture/rfcs` is clean; PR validation pending +- **Known gaps:** no runtime builder, writer, or consumer; F1 and F3-F8 + remain unchecked +- **Effect on normative design:** none — pins Appendix D F2 only + ## Appendix B: Decision log | Date | Decision | Owner / approval | Alternatives | Normative sections changed | @@ -458,6 +473,7 @@ Later milestones are not authorized by merging M0. | E1 | Current Material Frontier semantics were audited. | `41a95d9` | [Agent Material Frontier](../../reference/protocols/agent-material-frontier-v0.md) | documented | Public contract only; no live materials. | | E2 | Route-change ownership remains existing. | `41a95d9` | [Goal Vision and Replan](../../reference/protocols/goal-vision-replan-contract-v0.md) | documented | Does not prove the proposed baseline. | | E3 | M0 repository docs remain public-safe. | PR validation | `loopx check --scan-path docs/architecture/rfcs --scan-path docs/development/contributor-tasks.md` | pending | Documentation scope only. | +| E4 | The F2 revision-drift invariant is executable and mutation-checked. | `99839ae` | `python3 examples/goal-direction-baseline-f2-smoke.py` | pending | Synthetic ids only; no runtime builder or live materials. | ## Appendix D: Synthetic drift fixture plan @@ -480,7 +496,9 @@ effect. Before M1, turn this table into a checked-in public-safe fixture and mutation test. The mutation arm must deliberately relax Agent or revision matching and prove that F2 or F3 fails, so the test cannot pass merely because the happy -path was never exercised. +path was never exercised. F2 is checked in at +`examples/goal-direction-baseline-f2-smoke.py` with its mutation arm; the +remaining cases stay open until M1. ## Appendix E: Rejected or superseded alternatives diff --git a/docs/architecture/rfcs/goal-direction-baseline-v0.zh-CN.md b/docs/architecture/rfcs/goal-direction-baseline-v0.zh-CN.md index b1789bc374..078340a6b6 100644 --- a/docs/architecture/rfcs/goal-direction-baseline-v0.zh-CN.md +++ b/docs/architecture/rfcs/goal-direction-baseline-v0.zh-CN.md @@ -373,7 +373,10 @@ required material。 | M1 | 基于完整 Goal authority 和 receipt 的 optional pure builder;无 consumer 或 writer。 | 批准第 12 节决策与 schema 限制。 | 确定性 fixture、mutation check;若双 runtime 消费则做 Python/TypeScript parity;public-boundary scan。 | 移除 default-off builder 与 projection。 | | M2 | 一个只读 Agent-scoped Vision-gap consumer。 | M1 conformance,并由维护者显式批准 consumer 与 recovery UX。 | 端到端证明 drift 可见、同 revision replay 安静且不修改 Vision/Todo/route。 | 禁用 consumer;保留 canonical registry 与 receipt。 | -合并 M0 不授权后续 milestone。 +合并 M0 不授权后续 milestone。附录 D 的 F2 fixture 已在 M1 前以 conformance +harness 形式签入 +([examples/goal-direction-baseline-f2-smoke.py](../../../examples/goal-direction-baseline-f2-smoke.py)); +这不授权 M1,也不新增任何 runtime builder、writer 或 consumer。 ## 12. 开放决策 @@ -400,6 +403,18 @@ required material。 qualification - **对规范设计的影响:** 初始提案 +### 2026-10-04 — F2 synthetic fixture(M1 前) + +- **基线:** `99839ae` +- **交付:** 附录 D 的 F2 行以 checked-in public-safe smoke 及其 mutation + arm 落地(`examples/goal-direction-baseline-f2-smoke.py`);第 11 节附注 + 记录了 M1 保持关闭的原因 +- **证据:** smoke 本地通过;`loopx check --scan-path + docs/architecture/rfcs` 干净;等待 PR 验证 +- **已知缺口:** 无 runtime builder、writer 或 consumer;F1 与 F3-F8 仍未 + checked-in +- **对规范设计的影响:** 无 —— 仅固定附录 D 的 F2 + ## 附录 B:决策日志 | 日期 | 决策 | Owner / 批准 | 替代方案 | 修改的规范章节 | @@ -413,6 +428,7 @@ required material。 | E1 | 已审计当前 Material Frontier 语义。 | `41a95d9` | [Agent Material Frontier](../../reference/protocols/agent-material-frontier-v0.md) | documented | 仅 public contract;无 live material。 | | E2 | 路线变更 ownership 保持现有契约。 | `41a95d9` | [Goal Vision and Replan](../../reference/protocols/goal-vision-replan-contract-v0.md) | documented | 不证明 proposed baseline。 | | E3 | M0 repository 文档保持 public-safe。 | PR validation | `loopx check --scan-path docs/architecture/rfcs --scan-path docs/development/contributor-tasks.md` | pending | 仅文档 scope。 | +| E4 | F2 revision-drift 不变量可执行且带 mutation 检查。 | `99839ae` | `python3 examples/goal-direction-baseline-f2-smoke.py` | 等待 PR 验证 | 仅虚构 id;无 runtime builder 或 live material。 | ## 附录 D:Synthetic drift fixture 计划 @@ -433,7 +449,9 @@ scheduler、Goal-amendment 或 route-write effect。 M1 前应把该表变成 checked-in public-safe fixture 与 mutation test。Mutation arm 必须故意放松 Agent 或 revision 匹配,并证明 F2 或 F3 会失败,从而避免测试因未 -真正执行 happy path 而误通过。 +真正执行 happy path 而误通过。F2 已 checked-in 于 +`examples/goal-direction-baseline-f2-smoke.py` 并带 mutation arm;其余 case +在 M1 前保持开放。 ## 附录 E:拒绝或已取代方案 diff --git a/examples/goal-direction-baseline-f2-smoke.py b/examples/goal-direction-baseline-f2-smoke.py new file mode 100644 index 0000000000..40aa0d976b --- /dev/null +++ b/examples/goal-direction-baseline-f2-smoke.py @@ -0,0 +1,341 @@ +#!/usr/bin/env python3 +"""Synthetic case F2 for the goal-direction baseline RFC (GH-C89b). + +Implements the Appendix D F2 row of +``docs/architecture/rfcs/goal-direction-baseline-v0.md`` as a checked-in, +public-safe fixture ahead of M1: one authority revision changes after the +selected Agent's receipt, so the projection must report +``re_evaluation_required`` / ``material_revision_changed`` with the changed +item ``stale`` — while nothing mutates: the inputs stay byte-identical and no +Vision patch, Todo, wake, lease, Goal amendment, or path delta is emitted. + +The builder here is fixture-local and synthetic on purpose. The RFC's M1 +runtime builder is not authorized, so this file pins the F2 contract — +including a mutation arm that proves relaxed revision or Agent matching +fails — without adding runtime code. Ids and revisions are invented; the +projection carries no body, locator, URL, local path, prompt, reasoning, +transcript, trajectory, credential, or run-log field. +""" + +from __future__ import annotations + +import copy +import hashlib +import json +from typing import Any + +SCHEMA_VERSION = "goal_direction_baseline_v0" + +# Section 9 key allowlist: opaque ids, revisions, digests, counts, and typed +# tokens only. Anything outside this vocabulary is a public-boundary defect. +PROJECTION_KEYS = frozenset( + {"schema_version", "goal_id", "agent_id", "generated_at", "baseline_digest", + "direction_state", "reason_codes", "summary", "items", "advisory", + "truth_contract", "effects"}) +SUMMARY_KEYS = frozenset( + {"required_count", "current_count", "stale_count", "blocked_count"}) +ITEM_KEYS = frozenset( + {"material_id", "bound_by", "required_revision", "observed_revision", + "state", "receipt_ref"}) +ADVISORY_KEYS = frozenset( + {"kind", "creates_work", "rewrites_vision", "changes_goal_route"}) +TRUTH_KEYS = frozenset( + {"authority_is_goal_owned", "projection_is_read_only", + "receipt_is_agent_scoped", "provider_is_not_write_authority", + "raw_source_body_recorded"}) +FORBIDDEN_KEY_TOKENS = ( + "body", "locator", "url", "path", "prompt", "reasoning", "transcript", + "trajectory", "credential", "run_log", "run-log") +# `raw_source_body_recorded` is the RFC's own negation flag (Section 5.2); it +# names the absence of source material and carries only `False`. +BOUNDARY_FLAG_EXCEPTIONS = frozenset({"raw_source_body_recorded"}) +MUTATION_EFFECT_KINDS = frozenset( + {"vision_patch", "todo_write", "wake", "lease_write", "goal_amendment", + "goal_path_delta"}) + + +# -------------------------------------------------------------------------- +# Synthetic fixtures: invented goal authority, declared requirements, and +# Agent-scoped receipts. Nothing here represents a real Goal or install. +# -------------------------------------------------------------------------- + +def invented_authority() -> dict[str, Any]: + return { + "goal_id": "goal-invented-1", + "materials": { + "material-invented-a": { + "required_revision": "rev-6", + "bound_by": ["direction:invented-a"], + }, + "material-invented-b": { + "required_revision": "rev-6", + "bound_by": ["direction:invented-b"], + }, + }, + } + + +def invented_receipts() -> list[dict[str, Any]]: + return [ + {"agent_id": "agent-invented-1", "material_id": "material-invented-a", + "observed_revision": "rev-6", "receipt_id": "receipt-invented-1"}, + {"agent_id": "agent-invented-1", "material_id": "material-invented-b", + "observed_revision": "rev-6", "receipt_id": "receipt-invented-2"}, + ] + + +# -------------------------------------------------------------------------- +# Synthetic pure builder over the RFC's Agent-scoped read model (Section 5). +# Item states and drift precedence follow Sections 5.2-5.3; `effects` is the +# only place a write effect could surface and must stay empty. +# -------------------------------------------------------------------------- + +def build_projection( + authority: dict[str, Any], + receipts: list[dict[str, Any]], + *, + agent_id: str, + generated_at: str = "2026-10-04T00:00:00Z", + require_exact_revision: bool = True, + agent_scoped_receipts: bool = True, +) -> dict[str, Any]: + items: list[dict[str, Any]] = [] + reason_codes: list[str] = [] + counts = {"required_count": 0, "current_count": 0, "stale_count": 0, + "blocked_count": 0} + for material_id in sorted(authority["materials"]): + declaration = authority["materials"][material_id] + current_revision = declaration["required_revision"] + counts["required_count"] += 1 + candidates = [ + receipt for receipt in receipts + if receipt["material_id"] == material_id + and (agent_scoped_receipts is False + or receipt["agent_id"] == agent_id) + ] + if agent_scoped_receipts: + receipt = candidates[0] if candidates else None + else: + # Mutation mode: optimistically trust the freshest receipt for the + # material regardless of which Agent earned it. + receipt = (max(candidates, key=lambda r: r["observed_revision"]) + if candidates else None) + if receipt is None: + state = "required_unread" + reason = "material_required_unread" + elif require_exact_revision and receipt["observed_revision"] != current_revision: + state = "stale" + reason = "material_revision_changed" + else: + state = "current" + reason = None + if state == "current": + counts["current_count"] += 1 + elif state == "stale": + counts["stale_count"] += 1 + else: + counts["blocked_count"] += 1 + if reason is not None and reason not in reason_codes: + reason_codes.append(reason) + items.append({ + "material_id": material_id, + "bound_by": list(declaration["bound_by"]), + "required_revision": current_revision, + "observed_revision": (None if receipt is None + else receipt["observed_revision"]), + "state": state, + "receipt_ref": None if receipt is None else receipt["receipt_id"], + }) + + if counts["blocked_count"]: + direction_state = "blocked" + elif "material_revision_changed" in reason_codes: + direction_state = "re_evaluation_required" + else: + direction_state = "current" + + digest_basis = json.dumps( + {"schema_version": SCHEMA_VERSION, + "goal_id": authority["goal_id"], + "requirements": [ + [material_id, + authority["materials"][material_id]["required_revision"], + authority["materials"][material_id]["bound_by"]] + for material_id in sorted(authority["materials"]) + ]}, + sort_keys=True, separators=(",", ":")) + projection: dict[str, Any] = { + "schema_version": SCHEMA_VERSION, + "goal_id": authority["goal_id"], + "agent_id": agent_id, + "generated_at": generated_at, + "baseline_digest": "sha256:" + hashlib.sha256( + digest_basis.encode("utf-8")).hexdigest(), + "direction_state": direction_state, + "reason_codes": reason_codes, + "summary": counts, + "items": items, + "advisory": ({ + "kind": "agent_vision_re_evaluation", + "creates_work": False, + "rewrites_vision": False, + "changes_goal_route": False, + } if direction_state == "re_evaluation_required" else None), + "truth_contract": { + "authority_is_goal_owned": True, + "projection_is_read_only": True, + "receipt_is_agent_scoped": True, + "provider_is_not_write_authority": True, + "raw_source_body_recorded": False, + }, + "effects": [], + } + return projection + + +# -------------------------------------------------------------------------- +# F2 contract assertions (Appendix D, F2 row). +# -------------------------------------------------------------------------- + +def assert_public_boundary(node: Any, *, where: str) -> None: + if isinstance(node, dict): + for key, value in node.items(): + lowered = str(key).lower() + if lowered not in BOUNDARY_FLAG_EXCEPTIONS: + for token in FORBIDDEN_KEY_TOKENS: + assert token not in lowered, f"{where}: forbidden key {key!r}" + assert_public_boundary(value, where=f"{where}.{key}") + elif isinstance(node, list): + for index, value in enumerate(node): + assert_public_boundary(value, where=f"{where}[{index}]") + elif isinstance(node, str): + assert "://" not in node, f"{where}: URL-like value {node!r}" + assert not node.startswith(("/", "\\")), f"{where}: path-like value {node!r}" + + +def assert_f2_contract( + projection: dict[str, Any], + authority_before: dict[str, Any], + receipts_before: list[dict[str, Any]], + authority_after: dict[str, Any], + receipts_after: list[dict[str, Any]], + *, + drifted_material: str, + fresh_revision: str, +) -> None: + assert projection["direction_state"] == "re_evaluation_required", ( + f"expected re_evaluation_required, got {projection['direction_state']!r}") + assert projection["reason_codes"] == ["material_revision_changed"], ( + f"unexpected reason codes: {projection['reason_codes']!r}") + drifted = next(item for item in projection["items"] + if item["material_id"] == drifted_material) + assert drifted["state"] == "stale", f"drifted item is {drifted['state']!r}" + assert drifted["required_revision"] == fresh_revision + assert drifted["observed_revision"] != fresh_revision + counts = projection["summary"] + assert set(counts) == SUMMARY_KEYS + assert counts == {"required_count": 2, "current_count": 1, + "stale_count": 1, "blocked_count": 0}, counts + + effects = projection["effects"] + assert effects == [], f"mutation effects emitted: {effects!r}" + assert not (MUTATION_EFFECT_KINDS & set(projection)), ( + "mutation-shaped keys present at the projection top level") + + advisory = projection["advisory"] + assert advisory is not None, "re_evaluation_required must carry the advisory" + assert set(advisory) <= ADVISORY_KEYS, f"unknown advisory keys: {set(advisory)}" + assert advisory["kind"] == "agent_vision_re_evaluation" + assert advisory["creates_work"] is False + assert advisory["rewrites_vision"] is False + assert advisory["changes_goal_route"] is False + + truth = projection["truth_contract"] + assert set(truth) == TRUTH_KEYS + assert truth["projection_is_read_only"] is True + assert truth["provider_is_not_write_authority"] is True + assert truth["raw_source_body_recorded"] is False + + # Byte-for-byte input immutability (canonical JSON comparison). + for label, before, after in ( + ("authority", authority_before, authority_after), + ("receipts", receipts_before, receipts_after), + ): + assert (json.dumps(before, sort_keys=True) + == json.dumps(after, sort_keys=True)), ( + f"{label} inputs were mutated by the builder") + + assert set(projection) <= PROJECTION_KEYS, ( + f"unknown projection keys: {set(projection) - PROJECTION_KEYS}") + for item in projection["items"]: + assert set(item) <= ITEM_KEYS, f"unknown item keys: {set(item) - ITEM_KEYS}" + assert_public_boundary(projection, where="projection") + + +def expect_f2_failure(projection: dict[str, Any], *, label: str) -> None: + """The mutation arm: the relaxed builder must FAIL the F2 contract.""" + try: + assert_f2_contract(projection, invented_authority(), [], + invented_authority(), [], + drifted_material="material-invented-b", + fresh_revision="rev-7") + except AssertionError: + return + raise AssertionError( + f"mutation {label!r} passed the F2 contract; the fixture cannot " + "detect relaxed revision or Agent matching") + + +def main() -> int: + agent_id = "agent-invented-1" + drifted_material = "material-invented-b" + fresh_revision = "rev-7" + + # Precondition: before the drift the baseline is `current`. + authority = invented_authority() + receipts = invented_receipts() + baseline = build_projection(authority, receipts, agent_id=agent_id) + assert baseline["direction_state"] == "current", baseline["direction_state"] + assert baseline["reason_codes"] == [], baseline["reason_codes"] + + # F2: one authority revision changes AFTER the Agent's receipt. + authority["materials"][drifted_material]["required_revision"] = fresh_revision + authority_before = copy.deepcopy(authority) + receipts_before = copy.deepcopy(receipts) + projection = build_projection(authority, receipts, agent_id=agent_id) + assert_f2_contract(projection, authority_before, receipts_before, + authority, receipts, + drifted_material=drifted_material, + fresh_revision=fresh_revision) + print("F2: revision drift -> re_evaluation_required / " + "material_revision_changed, inputs byte-identical, zero effects") + + # The digest tracks goal-owned authority, not receipts or the Agent. + assert projection["baseline_digest"] != baseline["baseline_digest"], ( + "an authority revision change must move the baseline digest") + peer_view = build_projection(authority, receipts, agent_id="agent-invented-2") + assert peer_view["baseline_digest"] == projection["baseline_digest"], ( + "two Agents on the same authority must share the baseline digest") + + # Mutation arm (Appendix D closing note): relaxed matching must fail F2. + relaxed_revision = build_projection( + authority, receipts, agent_id=agent_id, require_exact_revision=False) + expect_f2_failure(relaxed_revision, label="relaxed revision matching") + + peer_receipts = receipts + [ + {"agent_id": "agent-invented-2", "material_id": drifted_material, + "observed_revision": fresh_revision, + "receipt_id": "receipt-invented-3"}] + relaxed_agent = build_projection( + authority, peer_receipts, agent_id=agent_id, + agent_scoped_receipts=False) + expect_f2_failure(relaxed_agent, label="relaxed Agent scoping") + print("mutation arm: relaxed revision and cross-Agent receipt matching " + "both fail the F2 contract") + + print("OK goal-direction-baseline-f2-smoke") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())