mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-03 04:07:04 +00:00
Resolve only declared file targets as the plan's constitutional fact
`plan_spec.resolve_constitutional` read every non-URL string in the spec's `affected_resources` list as a file path. On the live install that turned the prose item «Отдельный проект «TSMC — 10-летний инвестиционный анализ»» into "a path under the Ouroboros repository": the plan was marked constitutional and every reviewer of a deck-shaped plan received BIBLE.md plus ARCHITECTURE.md in full, ~470k tokens per cycle. Across the recorded history not one of the 14 constitutional verdicts came from a real file path in that list, and the model never once spelled a target as `file://` (0 of 186 locators). The spec now says the two things separately (owner decisions 8=A, 9=A, 16=A): * `affected_paths` — REQUIRED — is the files the work will CHANGE, and the only list the host resolves. `[]` is a real claim: "this work changes no files". A path under the system repository decides whether or not the file exists yet, because creating `ouroboros/new_module.py` IS self-modification. * `affected_resources` is prose: systems, services, projects, people. Nothing in it reaches the filesystem. * An `evidence` locator is something to LOOK AT. Reading a repository file is not changing it, so it no longer buys the pack on its own (16=A); when the work will also change that file, the file is in `affected_paths` and decides there. System-repo evidence reads are named in the disclosure note, so an author who expected the constitution can see why it did not come. Existence stopped deciding anything, so `_default_exists`/`evidence_exists` are gone with it. A NEW spec submitted in the old mixed form is refused before any dispatch with `PLAN_RESOURCE_FORM_REQUIRED`, quoting the field with a two-line example. The refusal deliberately does NOT carry the `PLAN_SPEC_INVALID` token: that family records a superseding attempt on the way out and would orphan an open wave, so this one writes nothing and, when a wave is open, names its fingerprint and the free `review_disposition` exit. Waves recorded under the old shape keep their identity: `affected_paths` is never defaulted onto stored input, so their `spec_hash` and `plan_fingerprint` are byte-identical (pinned against the three live examples) and they still close at $0. `spec_delta` reports the appearance of the key itself, because "stored before the field existed" and "changes no files" are different claims. No grace mechanism for the 31 open waves — already disclosed to the owner. Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
This commit is contained in:
parent
b8c46e57ed
commit
82de26a2fd
18 changed files with 521 additions and 140 deletions
|
|
@ -784,7 +784,9 @@ def sw1_stub_script(_clone: pathlib.Path) -> dict:
|
|||
"goal": "Survey the repository with two parallel scouts.",
|
||||
"plan": "Schedule two scouts, wait for both, summarize.",
|
||||
"spec": {"deliverables": ["Two scout results summarized."],
|
||||
"acceptance_claims": ["Both scouts completed and were absorbed."]}}},
|
||||
"acceptance_claims": ["Both scouts completed and were absorbed."],
|
||||
# Required on every submitted spec (owner 9=A); a survey changes no file.
|
||||
"affected_paths": []}}},
|
||||
scout("A", "list the top-level directories"),
|
||||
scout("B", "list the test modules under tests/system_e2e"),
|
||||
wait_step,
|
||||
|
|
|
|||
|
|
@ -1750,9 +1750,9 @@ Plan review, task acceptance, commit review, and deep self-review answer differe
|
|||
|
||||
#### Plan construction and review
|
||||
|
||||
`plan_task` reviews an INTENTION before the work starts — the same organ for code, research, deliverables, and actions (BIBLE P3). The obligation is constitutional and the finalization gate is structural (`owner_hurry.force_plan_decision`); the order in which a task asks, explores and plans is the mind's judgment, and no prompt choreographs it. The submitted envelope carries the goal, the plan prose, and a typed domain-neutral SPEC (`in_scope`, `non_goals`, `acceptance_claims`, `invariants`, `decisions` with rejected alternatives, `deferred`, `affected_resources`, `evidence`); `ouroboros/tools/plan_spec.py` validates and normalizes its full operative content without shortening strings or dropping excess items, then mints the ids that are the only valid `breaks` targets (`goal`, `claim_N`, `invariant_N`, `decision_N`, `deferred_N`) — host-minted positionally, because a caller-chosen id could shadow another target and corrupt what a blocking finding `breaks` — and hashes it. Governance documents always come from the system repository; declared targets and evidence resolve against `active_repo_dir_for(ctx)`, and a path escaping the active subject or an unreadable root is a named omission, never a silent gap.
|
||||
`plan_task` reviews an INTENTION before the work starts — the same organ for code, research, deliverables, and actions (BIBLE P3). The obligation is constitutional and the finalization gate is structural (`owner_hurry.force_plan_decision`); the order in which a task asks, explores and plans is the mind's judgment, and no prompt choreographs it. The submitted envelope carries the goal, the plan prose, and a typed domain-neutral SPEC (`in_scope`, `non_goals`, `acceptance_claims`, `invariants`, `decisions` with rejected alternatives, `deferred`, `affected_paths` — REQUIRED, the files the work will CHANGE, `[]` when it changes none — `affected_resources` (the same question in words: systems, services, projects, people, never resolved as paths), `evidence`); a submitted spec without `affected_paths` is refused before any dispatch with the typed `PLAN_RESOURCE_FORM_REQUIRED`, which records nothing and leaves an open wave collectable; `ouroboros/tools/plan_spec.py` validates and normalizes its full operative content without shortening strings or dropping excess items, then mints the ids that are the only valid `breaks` targets (`goal`, `claim_N`, `invariant_N`, `decision_N`, `deferred_N`) — host-minted positionally, because a caller-chosen id could shadow another target and corrupt what a blocking finding `breaks` — and hashes it. Governance documents always come from the system repository; declared targets and evidence resolve against `active_repo_dir_for(ctx)`, and a path escaping the active subject or an unreadable root is a named omission, never a silent gap.
|
||||
|
||||
ONE structural fact tiers the governance pack: `constitutional` is true iff a declared `affected_resources`/`evidence` PATH locator resolves under the Ouroboros system repository (an `evidence` path only if it exists). A constitutional plan carries BIBLE.md and ARCHITECTURE.md in full (an `api_chat` row inline; a retrieving `agent_session` row as mandatory full reads) — the constitutional packet is not tiered, so assembling it without either is a typed failure, never a disclosure — and every other plan carries the runtime heading-derived navigation maps (`context_layout.generate_doc_nav_map`, never a copy) plus resolvable pointers. There is no plan-kind taxonomy, no agent-declared `plan_class`, no planning scouts, and no plan Atlas. The ONE caller-facing strength axis is the envelope's optional `reviewer_effort`: the panel's effort for THIS order, placed on the default rung of each reviewer row's ladder (`reviewer_slot_config.row_effort`: an explicit per-row effort, then a compound Cursor/Agy route slug, then the declaration, then the owner's `OUROBOROS_EFFORT_REVIEW` setting), passed as an ARGUMENT of `plan_review_slots` only, so the commit gate, scope, acceptance and skill review keep reading the untouched rows. Effort is roster identity: a different declaration re-dispatches a paid panel within `OUROBOROS_REVIEW_MAX_CYCLES`, the same one replays free, and a wave records the declaration (`reviewer_effort`) so a collection rebuilds the roster it was dispatched with. Scoping against BIBLE P1 ("Cognitive horizon is part of continuity"): this is the review panel's strength for one order, ordered by the mind under the owner's setting as the default (the owner chose this scoping: the setting is the default and Ouroboros may order stronger or weaker per envelope), never the core's own model, effort or horizon; the disclosed residual is that a cheap panel's closed GREEN is earned authority for that envelope and a later stronger order does not reopen it. The last-execution projection keeps a declared effort apart from the row's saved effort (`declared_effort`).
|
||||
ONE structural fact tiers the governance pack: `constitutional` is true iff a declared `affected_paths` locator resolves under the Ouroboros system repository, whether or not that file exists yet (creating `ouroboros/new_module.py` IS self-modification). Nothing else resolves as a path: `affected_resources` is prose, and an `evidence` locator is something to LOOK AT — reading a repository file is not changing it, so it never buys the pack on its own, and the system-repo reads are named in the disclosure note instead. A constitutional plan carries BIBLE.md and ARCHITECTURE.md in full (an `api_chat` row inline; a retrieving `agent_session` row as mandatory full reads) — the constitutional packet is not tiered, so assembling it without either is a typed failure, never a disclosure — and every other plan carries the runtime heading-derived navigation maps (`context_layout.generate_doc_nav_map`, never a copy) plus resolvable pointers. There is no plan-kind taxonomy, no agent-declared `plan_class`, no planning scouts, and no plan Atlas. The ONE caller-facing strength axis is the envelope's optional `reviewer_effort`: the panel's effort for THIS order, placed on the default rung of each reviewer row's ladder (`reviewer_slot_config.row_effort`: an explicit per-row effort, then a compound Cursor/Agy route slug, then the declaration, then the owner's `OUROBOROS_EFFORT_REVIEW` setting), passed as an ARGUMENT of `plan_review_slots` only, so the commit gate, scope, acceptance and skill review keep reading the untouched rows. Effort is roster identity: a different declaration re-dispatches a paid panel within `OUROBOROS_REVIEW_MAX_CYCLES`, the same one replays free, and a wave records the declaration (`reviewer_effort`) so a collection rebuilds the roster it was dispatched with. Scoping against BIBLE P1 ("Cognitive horizon is part of continuity"): this is the review panel's strength for one order, ordered by the mind under the owner's setting as the default (the owner chose this scoping: the setting is the default and Ouroboros may order stronger or weaker per envelope), never the core's own model, effort or horizon; the disclosed residual is that a cheap panel's closed GREEN is earned authority for that envelope and a later stronger order does not reopen it. The last-execution projection keeps a declared effort apart from the row's saved effort (`declared_effort`).
|
||||
|
||||
Declared evidence is resolved by `ouroboros/tools/plan_evidence.py` against exactly two allowed roots — the active workspace and the system repository — with the shared sensitive-name policy applied to locator and target; every refused, missing, truncated, oversized, binary, or URL locator becomes a typed omission row in the manifest (the host never fetches a URL), and a `need_evidence` locator a reviewer names is attached by the host on the next cycle through the same policy. What that policy cannot attach is a named `[reviewer-requested]` omission row the panel is dispatched with and judges with, never a reason to run no reviewer; the evidence continuation uses a fresh full-packet dispatch only when no exact artifact reference exists, disclosed per slot as a `capability_delta`. An unreadable referenced artifact instead fails closed with `plan_review_exact_artifact_unavailable` and never mints replacement authority. A locator may carry an exact range — `::lines=A-B`, `::bytes=A-B`, `::tail=N`, or `::symbol=Name` for `.py` sources — and a source above the per-item byte bound is attached head-first with the cut named. The manifest hash joins the spec hash and `constitutional` in the wave fingerprint, so changing what the reviewers can see changes the identity of the review. The root exploration log remains outside that identity. Automatic own-room evidence uses the recorded source selected by the author-request identity described below.
|
||||
|
||||
|
|
|
|||
|
|
@ -813,8 +813,9 @@ MUST include these artifacts as **first-class context sections** — not as
|
|||
optional or opportunistic inclusions via touched-file packs.
|
||||
|
||||
Plan review is the one flow whose governance pack is tiered, and by ONE
|
||||
structural fact — whether a declared `affected_resources` target, or an
|
||||
EXISTING `evidence` path, resolves under the Ouroboros system repository —
|
||||
structural fact — whether a declared `affected_paths` target (a file the work
|
||||
will CHANGE) resolves under the Ouroboros system repository; `affected_resources`
|
||||
is prose the host never resolves, and an `evidence` path is a read, not a change —
|
||||
never by prose and never by a plan-kind
|
||||
taxonomy, which is what keeps classification un-gameable. This is a tiering,
|
||||
not an omission: before any work exists the reviewer's subject is the
|
||||
|
|
@ -859,7 +860,7 @@ The context-delivery registry:
|
|||
| Advisory pre-review (`tools/claude_advisory_review.py`) | Two delivery classes: an `api_chat` row runs the bounded NATIVE inspection episode (governance docs reached through its read-only tools); an `agent_session` row receives a resolvable pointer marked MANDATORY FULL READ and the session reads the full doc itself — retrieval is disclosed (native reads are host-observed; vendor-session reads are not) | same two delivery classes | same two delivery classes |
|
||||
| Scope review (`tools/scope_review.py`) | full canonical doc + Atlas accounting; a size terminal or a degradation rung under a cold density cap takes the shared cold-start density rung above (one probe, one rebuild, `density_probe` ladder step) | full canonical doc + Atlas accounting | full canonical doc + Atlas accounting |
|
||||
| Skill review (`skill_review.py`) | full inline (`api_chat`) / mandatory full source-root read (`agent_session`) | full inline (`api_chat`) / mandatory full source-root read (`agent_session`) | full inline (`api_chat`) / mandatory full source-root read (`agent_session`) |
|
||||
| Plan review (`tools/plan_review.py`) | full for a SELF-MODIFICATION plan (structural path fact: a declared `affected_resources` target, or an existing `evidence` path, resolves under the system repo); otherwise a heading-derived navigation map of BIBLE.md generated at runtime (never a copy) | inline, in full, for a self-modification plan; otherwise the lossless navigation map + a resolvable pointer (W3) | named on-demand pointer; a reviewer that needs it returns `need_evidence` (an exact `::lines=A-B` range for one section) and the host attaches what the evidence policy allows on the next cycle, naming every absence |
|
||||
| Plan review (`tools/plan_review.py`) | full for a SELF-MODIFICATION plan (structural path fact: a declared `affected_paths` target resolves under the system repo, whether or not the file exists yet; an `evidence` read of a repo file does not); otherwise a heading-derived navigation map of BIBLE.md generated at runtime (never a copy) | inline, in full, for a self-modification plan; otherwise the lossless navigation map + a resolvable pointer (W3) | named on-demand pointer; a reviewer that needs it returns `need_evidence` (an exact `::lines=A-B` range for one section) and the host attaches what the evidence policy allows on the next cycle, naming every absence |
|
||||
| Deep self-review (`deep_self_review.py`) | Three deliveries on the `deep_review` row. Packed api row: full canonical doc + Atlas accounting; a required set unfit under a cold density cap gets one bounded probe send and one rebuild, and a pack still unfit is the typed `deep_self_review_pack_unfit` refusal asking the owner to switch the row (no automatic fallback). Native inspection episode / agent session: MANDATORY full read at the repository root, named with its size in the task; the native episode's `read_file` receipts carry the delivered line extent and the host merges the repository-root intervals for BIBLE.md afterwards — `read` only on full coverage, else `partial`/`missing`/`unobserved` — disclosed in the report header and as a typed `capability_delta` (host-observed reads); a session's reads are `unobserved`. Memory (up to seven whitelisted files) is INLINED byte-exact on every delivery, each entry's disposition disclosed (task text, `deep_review_memory`, header `memory=n/7`) — never receipt-checked | Packed: full (max) / navigation map (low) + Atlas accounting. Retrieving rows: navigation map (`generate_doc_nav_map`), sections read on demand | Packed: full canonical doc + Atlas accounting. Retrieving rows: navigation map, sections read on demand (CHECKLISTS.md likewise) |
|
||||
|
||||
Skill Review keeps the full stable governance/host prefix for cache-friendly
|
||||
|
|
|
|||
|
|
@ -537,7 +537,8 @@ def plan_review_reminder(decision: Dict[str, Any]) -> str:
|
|||
if status == "legacy_open_requires_resubmission":
|
||||
return (
|
||||
f"{tag} An open plan review from a previous schema cannot be honored. Re-call "
|
||||
"plan_task with your goal, plan and spec to start a fresh review before finalizing."
|
||||
"plan_task with your goal, plan and spec — including affected_paths, the files the "
|
||||
"work will change ([] when none) — to start a fresh review before finalizing."
|
||||
)
|
||||
from ouroboros.review_cycles import review_max_cycles
|
||||
|
||||
|
|
@ -578,13 +579,15 @@ def plan_review_reminder(decision: Dict[str, Any]) -> str:
|
|||
)
|
||||
if outcome == "REVISE_PLAN":
|
||||
return (
|
||||
f"{tag} Blocking plan review requires a revised spec. Change the spec and call "
|
||||
f"{tag} Blocking plan review requires a revised spec. Change the spec — it carries "
|
||||
"affected_paths, the files the work will change ([] when none) — and call "
|
||||
"plan_task again (or reject the blocking findings with a rationale via "
|
||||
"review_disposition). Continue analysis and non-mutating preparation, but do not "
|
||||
"begin the work before the review closes or a real task-wide rail fires."
|
||||
)
|
||||
return (
|
||||
f"{tag} Call plan_task with a concrete goal, plan and spec. If review infrastructure "
|
||||
f"{tag} Call plan_task with a concrete goal, plan and spec, whose affected_paths lists "
|
||||
"the files the work will change ([] when none). If review infrastructure "
|
||||
"is unavailable, continue analysis and non-mutating preparation, but do not begin the "
|
||||
"work before the review closes or a real task-wide rail fires."
|
||||
)
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@ BAND_PATHS = {
|
|||
"ouroboros/tools/commit_gate.py": "Grew INTO the band by the review-wave fix binding the actor reference (delivery class) into the commit review contract fingerprint \u2014 same-module contract identity, splitting it would separate the fingerprint from its gate.",
|
||||
"ouroboros/tools/core.py": "D05 ledger split (rows 311-349): read/list and owner-chat delivery spans moved to core_file_tools/core_artifacts; facade re-enters the band from above (2283 -> 1373) and shrinks further when the residual catalog split lands",
|
||||
"ouroboros/tools/delegate.py": "D07 finisher DEL1 split brought the nanny-verb monolith DOWN from the 1600 hard cap into the band (1600->1263); terminal-evidence family extracted to tools/delegate_terminal_evidence.py, shrink-only direction",
|
||||
"ouroboros/tools/plan_review.py": "Entered the band from 999 lines: the required-affected_paths form (owner 9=A) added the schema field and the PLAN_RESOURCE_FORM_REQUIRED refusal, which must name the task's open wave and the $0 disposition exit \u2014 it belongs beside the one preamble both the paid and dry-run paths share, not in the pure plan_spec companion that owns no task state.",
|
||||
"ouroboros/tools/plan_review_runtime.py": "Entered the band from 986 lines: timeout custody synthesis joined the existing plan-review runtime owner while preserving profile-continuity disclosures and typed health facts during target integration.",
|
||||
"ouroboros/tools/registry_core.py": "F3.1 typed-organ re-split (D04 rows 156/167/170/171/174/175): the tip ToolRegistry class body re-homed whole from the protected registry facade; the guard/dispatch surface already left for its sibling leaves, and the class shrinks further only with the ABI-8 post-release handler conversion.",
|
||||
"ouroboros/tools/review.py": "D06 F2.3a re-entry by extraction: the multi-model fan-out moved to review_multi_model.py (1550->1269); the remaining single-owner review cycle machinery lands in the 1001-1500 band with headroom",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
Owner-approved redesign (2026-08-15, plan §6/§7): the agent submits a SPEC (goal,
|
||||
in_scope, non_goals, acceptance_claims, invariants, decisions, deferred,
|
||||
affected_resources, evidence) plus prose; the host normalizes it (``plan_spec``),
|
||||
affected_paths, affected_resources, evidence) plus prose; the host normalizes it (``plan_spec``),
|
||||
resolves ONE structural fact (``constitutional``), attaches the declared evidence
|
||||
bounded with every omission named, builds the lean packet (``plan_packet``), fans it
|
||||
across the configured reviewer rows through the shared review substrate (api_chat
|
||||
|
|
@ -181,11 +181,21 @@ _SPEC_SCHEMA = {
|
|||
},
|
||||
"description": "Deliberately deferred until the work is underway; ids deferred_1..N — a blocking finding may name any spec id it breaks: goal, claim_N, invariant_N, decision_N or deferred_N itself.",
|
||||
},
|
||||
"affected_paths": {
|
||||
"type": "array", "items": {"type": "string"},
|
||||
"description": (
|
||||
"REQUIRED. The FILES the work will CHANGE — paths relative to the subject "
|
||||
"workspace root, absolute, or file://; send [] when this work changes no files. "
|
||||
"A path under the Ouroboros system repository makes the plan constitutional "
|
||||
"(BIBLE + ARCHITECTURE go to reviewers), whether or not the file exists yet. "
|
||||
"This is the ONLY field read as file paths."
|
||||
),
|
||||
},
|
||||
"affected_resources": {
|
||||
"type": "array", "items": {"type": "string"},
|
||||
"description": (
|
||||
"What the work will CHANGE (paths, systems, services). Paths resolving under "
|
||||
"the Ouroboros system repository make the plan constitutional (BIBLE goes to reviewers)."
|
||||
"What else the work will CHANGE, described in words: systems, services, projects, "
|
||||
"people. Descriptions only — never file paths; nothing here is resolved as a path."
|
||||
),
|
||||
},
|
||||
"evidence": {
|
||||
|
|
@ -193,12 +203,12 @@ _SPEC_SCHEMA = {
|
|||
"description": (
|
||||
"What reviewers should LOOK AT: file paths, task:<id> of a prior task, URLs. "
|
||||
"The host attaches files bounded (never fetching URLs) and names every omission. "
|
||||
"An EXISTING path here that resolves under the Ouroboros system repository also "
|
||||
"makes the plan constitutional (BIBLE + ARCHITECTURE go to reviewers); a path that "
|
||||
"does not exist does not, and the skip is disclosed."
|
||||
"Reading a repository file here is not changing it and does not make the plan "
|
||||
"constitutional; only listing it under affected_paths does."
|
||||
),
|
||||
},
|
||||
},
|
||||
"required": ["affected_paths"],
|
||||
}
|
||||
|
||||
_DISPOSITION_SCHEMA = {
|
||||
|
|
@ -252,7 +262,8 @@ def get_tools():
|
|||
"new; a different reviewer_effort re-dispatches a paid panel). Under blocking enforcement an "
|
||||
"open review holds finalization; under advisory you may proceed with the "
|
||||
"review open and the host discloses it. Declare evidence reviewers need; "
|
||||
"declare affected_resources so a self-modification gets the constitutional pack."
|
||||
"affected_paths is required — the files the work will change ([] when none) — "
|
||||
"and is what gives a self-modification the constitutional pack."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
|
|
@ -407,6 +418,34 @@ def _reviewer_requested_locators(ctx: ToolContext, state_root: pathlib.Path) ->
|
|||
seen.append(loc)
|
||||
return seen, dropped
|
||||
|
||||
def _resource_form_refusal(ctx: ToolContext, state_root: pathlib.Path) -> str:
|
||||
"""The old-form refusal text: name the missing field, show it, and protect an open wave.
|
||||
|
||||
Nothing durable is written, so a task whose previous wave is still open keeps it — and is
|
||||
told how to close it at $0 instead of re-buying a panel it cannot afford."""
|
||||
detail = ""
|
||||
try:
|
||||
_root, task_id = _planning_state_location(ctx)
|
||||
wave = current_plan_review_wave(load_plan_review_state(state_root, task_id)) or {}
|
||||
except (OSError, TimeoutError, ValueError):
|
||||
wave = {}
|
||||
if wave and not wave.get("closed"):
|
||||
detail = (
|
||||
"\nThe open plan-review wave "
|
||||
f"{wave.get('request_fingerprint') or '(fingerprint not recorded)'} is untouched: answer "
|
||||
"it first with review_disposition naming that fingerprint (free, no reviewer call), or "
|
||||
"re-send this spec in the form above."
|
||||
)
|
||||
return (
|
||||
"ERROR: PLAN_RESOURCE_FORM_REQUIRED: spec.affected_paths is required — the FILES this work "
|
||||
"will change, as their own list; send [] when it changes no files. affected_resources is "
|
||||
"now prose (systems, services, projects, people) and is never resolved as a path. "
|
||||
"For example:\n"
|
||||
' "affected_paths": ["ouroboros/tools/plan_review.py"],\n'
|
||||
' "affected_resources": ["the plan-review organ", "the owner\'s review budget"]\n'
|
||||
"No reviewer was called and nothing was recorded." + detail
|
||||
)
|
||||
|
||||
def _prepare_plan_inputs(ctx: ToolContext, request: "_PlanRequest", state_root: pathlib.Path, *, persist: bool = False) -> dict:
|
||||
"""The ONE preamble the paid path and the dry-run seam share: normalize the spec (with the
|
||||
envelope's goal injected), resolve the subject roots, derive `constitutional`, attach the
|
||||
|
|
@ -423,21 +462,29 @@ def _prepare_plan_inputs(ctx: ToolContext, request: "_PlanRequest", state_root:
|
|||
if errors:
|
||||
return {"error": "ERROR: PLAN_SPEC_INVALID: " + "; ".join(errors) + ". No reviewer was called.",
|
||||
"code": "TOOL_ARG_ERROR"}
|
||||
if isinstance(raw_spec, dict) and "affected_paths" not in raw_spec:
|
||||
# Owner 9=A: a spec in the old mixed form is refused BEFORE any paid dispatch, because
|
||||
# `affected_resources` used to be read as a path list — a prose item became "a file under
|
||||
# the Ouroboros repo" and bought every reviewer the whole constitution (~470k tokens/cycle)
|
||||
# for a deck. The refusal carries its OWN code: a message containing `PLAN_SPEC_INVALID`
|
||||
# takes the durable superseding-attempt path below and would orphan an open wave.
|
||||
return {"error": _resource_form_refusal(ctx, state_root), "code": "TOOL_ARG_ERROR"}
|
||||
from ouroboros.review_substrate import review_repo_dirs_for
|
||||
try:
|
||||
system_root, active_root = review_repo_dirs_for(ctx)
|
||||
except ValueError as exc:
|
||||
return {"error": f"ERROR: PLAN_SUBJECT_ROOT_INVALID: {exc}", "code": "TOOL_ERROR"}
|
||||
locators = list(spec["affected_resources"]) + list(spec["evidence"])
|
||||
affected_paths = list(spec.get("affected_paths") or [])
|
||||
locators = affected_paths + list(spec["evidence"])
|
||||
constitutional, constitutional_note = plan_spec.resolve_constitutional(
|
||||
active_root=active_root, system_repo_root=system_root,
|
||||
affected_resources=spec["affected_resources"], evidence=spec["evidence"],
|
||||
affected_paths=affected_paths, evidence=spec["evidence"],
|
||||
payload_roots=_plan_payload_roots(ctx, locators),
|
||||
)
|
||||
reminder = (
|
||||
"REMINDER: affected_resources is empty; if this work will change Ouroboros's own body, "
|
||||
"declare those paths so reviewers receive the constitutional pack (BIBLE)."
|
||||
if active_root == system_root and not spec["affected_resources"] else ""
|
||||
"REMINDER: affected_paths is empty; if this work will change Ouroboros's own body, "
|
||||
"list those files so reviewers receive the constitutional pack (BIBLE)."
|
||||
if active_root == system_root and not affected_paths else ""
|
||||
)
|
||||
declared_evidence = list(spec["evidence"]) # W3: earlier-cycle need_evidence is HOST-attached
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ from hashlib import sha256
|
|||
import json
|
||||
import pathlib
|
||||
import re
|
||||
from typing import Any, Callable, Iterable, Mapping, Optional
|
||||
from typing import Any, Iterable, Mapping, Optional
|
||||
|
||||
from ouroboros.config import adaptive_quorum
|
||||
from ouroboros.contracts.task_contract import normalize_acceptance_claims
|
||||
|
|
@ -57,10 +57,12 @@ AUTHOR_DISPOSITION_SCHEMA = {
|
|||
}
|
||||
DISPOSITION_DECISIONS = ("accept", "reject", "defer")
|
||||
|
||||
_SPEC_STRING_LISTS = ("in_scope", "non_goals", "invariants", "affected_resources", "evidence")
|
||||
_SPEC_STRING_LISTS = (
|
||||
"in_scope", "non_goals", "invariants", "affected_paths", "affected_resources", "evidence",
|
||||
)
|
||||
_SPEC_KEYS = frozenset({
|
||||
"goal", "in_scope", "non_goals", "acceptance_claims", "invariants",
|
||||
"decisions", "deferred", "affected_resources", "evidence",
|
||||
"decisions", "deferred", "affected_paths", "affected_resources", "evidence",
|
||||
})
|
||||
_URL_RE = re.compile(r"^[a-zA-Z][a-zA-Z0-9+.\-]*://")
|
||||
_FILE_SCHEME = "file://"
|
||||
|
|
@ -224,6 +226,9 @@ def normalize_spec(raw: Mapping[str, Any] | None) -> tuple[dict, list[str]]:
|
|||
Invariant ids are positional (``invariant_1..N``) because the schema keeps
|
||||
``invariants: [str]``; ``spec_ids``/``spec_with_ids`` derive them; the id
|
||||
``goal`` is reserved for the intention as a whole (D32).
|
||||
``affected_paths`` (the files the work will CHANGE) appears in the normalized
|
||||
spec only when the input declared it, so a spec stored before the field existed
|
||||
keeps its recorded identity byte for byte.
|
||||
"""
|
||||
errors: list[str] = []
|
||||
omissions: list[str] = []
|
||||
|
|
@ -240,6 +245,13 @@ def normalize_spec(raw: Mapping[str, Any] | None) -> tuple[dict, list[str]]:
|
|||
)
|
||||
spec: dict[str, Any] = {"goal": goal}
|
||||
for key in _SPEC_STRING_LISTS:
|
||||
# `affected_paths` is the ONE key that is never defaulted: a spec RECORDED before the
|
||||
# field existed must normalize to the same bytes as it did then, or its `spec_hash` /
|
||||
# `plan_fingerprint` move and the open wave it belongs to becomes uncollectable. New
|
||||
# submissions always carry the key — `plan_review` refuses the old mixed form before
|
||||
# any paid dispatch, so the absence here only ever means "stored under the old shape".
|
||||
if key == "affected_paths" and key not in raw:
|
||||
continue
|
||||
spec[key] = _string_list(raw.get(key), key, errors, omissions)
|
||||
seen = {GOAL_ID, *(f"invariant_{i}" for i in range(1, len(spec["invariants"]) + 1))}
|
||||
spec["acceptance_claims"] = _normalize_claims(raw.get("acceptance_claims"), errors, omissions, seen)
|
||||
|
|
@ -248,8 +260,8 @@ def normalize_spec(raw: Mapping[str, Any] | None) -> tuple[dict, list[str]]:
|
|||
spec["normalization_omissions"] = omissions
|
||||
ordered = {key: spec[key] for key in (
|
||||
"goal", "in_scope", "non_goals", "acceptance_claims", "invariants", "decisions",
|
||||
"deferred", "affected_resources", "evidence", "normalization_omissions",
|
||||
)}
|
||||
"deferred", "affected_paths", "affected_resources", "evidence", "normalization_omissions",
|
||||
) if key in spec}
|
||||
return ordered, errors
|
||||
|
||||
|
||||
|
|
@ -333,7 +345,9 @@ def spec_delta(prev_spec: Mapping[str, Any] | None, spec: Mapping[str, Any]) ->
|
|||
(added / removed / changed content); plain string lists diff by text; the
|
||||
goal is a changed flag; ``renumbered`` lists elements whose text moved to a
|
||||
different id (positional ids shift when an earlier element is dropped or
|
||||
reordered). ``prev_spec=None`` means everything is added.
|
||||
reordered); ``declared_keys_changed`` reports a spec whose set of declared keys
|
||||
itself moved (a spec stored before ``affected_paths`` existed declares no such
|
||||
key at all). ``prev_spec=None`` means everything is added.
|
||||
"""
|
||||
prev = dict(prev_spec or {})
|
||||
ids: dict[str, list[str]] = {"added": [], "removed": [], "changed": []}
|
||||
|
|
@ -350,12 +364,21 @@ def spec_delta(prev_spec: Mapping[str, Any] | None, spec: Mapping[str, Any]) ->
|
|||
"removed": [t for t in before_l if t not in after_l],
|
||||
}
|
||||
goal_changed = str(prev.get("goal") or "") != str(spec.get("goal") or "")
|
||||
changed = goal_changed or any(ids.values()) or any(
|
||||
# A key that appears or disappears between two normalized specs is a change even when both
|
||||
# sides read as empty: `affected_paths` ABSENT (stored before the field existed) and
|
||||
# `affected_paths: []` ("this work changes no files") are different claims, and the spec
|
||||
# hash already says so — the delta the reviewer reads must not say they are the same.
|
||||
declared_keys_changed = prev_spec is not None and (
|
||||
{key for key in prev if key != "normalization_omissions"}
|
||||
!= {key for key in spec if key != "normalization_omissions"}
|
||||
)
|
||||
changed = goal_changed or declared_keys_changed or any(ids.values()) or any(
|
||||
entry["added"] or entry["removed"] for entry in lists.values()
|
||||
)
|
||||
return {
|
||||
"changed": changed,
|
||||
"goal_changed": goal_changed,
|
||||
"declared_keys_changed": declared_keys_changed,
|
||||
"ids": ids,
|
||||
"lists": lists,
|
||||
"renumbered": _renumbered(prev, spec),
|
||||
|
|
@ -454,35 +477,31 @@ def _under(path: pathlib.Path, root: pathlib.Path) -> bool:
|
|||
return False
|
||||
|
||||
|
||||
def _default_exists(path: pathlib.Path) -> bool:
|
||||
"""Fail CLOSED: only a definite "this path is not there" lets an evidence locator skip
|
||||
the constitutional escalation. An unreadable path (permissions, a transient I/O error)
|
||||
counts as present, because the safe error is carrying the constitution needlessly, never
|
||||
dropping it on a filesystem hiccup."""
|
||||
try:
|
||||
return path.exists()
|
||||
except (OSError, ValueError, RuntimeError):
|
||||
return True
|
||||
|
||||
|
||||
def resolve_constitutional(
|
||||
*,
|
||||
active_root: str | pathlib.Path,
|
||||
system_repo_root: str | pathlib.Path,
|
||||
affected_resources: Iterable[str],
|
||||
affected_paths: Iterable[str],
|
||||
evidence: Iterable[str],
|
||||
payload_roots: Iterable[str | pathlib.Path] = (),
|
||||
evidence_exists: Optional[Callable[[pathlib.Path], bool]] = None,
|
||||
) -> tuple[bool, str]:
|
||||
"""ONE structural fact: does this plan touch Ouroboros's own body?
|
||||
"""ONE structural fact: will this plan CHANGE Ouroboros's own body?
|
||||
|
||||
Port of ``plan_review_runtime.resolve_plan_class`` without the enum. True iff
|
||||
any ``affected_resources`` / ``evidence`` PATH locator (relative resolved
|
||||
against ``active_root``, absolute or ``file://`` as-is) resolves to or under
|
||||
the system repository. The active binding alone does NOT decide (owner
|
||||
decision D29: a plan bound to the system repo that declares no system path
|
||||
is not constitutional). Skill-payload paths under ``payload_roots`` are
|
||||
exempt (data plane, as today). URLs and ``task:``/``chat:`` locators never make it
|
||||
True iff a declared ``affected_paths`` locator (relative resolved against
|
||||
``active_root``, absolute or ``file://`` as-is) resolves to or under the
|
||||
system repository — whether or not the file exists yet, because creating
|
||||
``ouroboros/new_module.py`` IS self-modification. ``affected_resources`` is
|
||||
never read here: treating every non-URL string in it as a path is what turned
|
||||
a Russian sentence about a project into "a file under the repo" and billed the
|
||||
whole constitution to a deck plan (owner decisions 8=A/9=A/16=A).
|
||||
``evidence`` is what to LOOK AT — reading a repository file is not changing it,
|
||||
so an evidence locator never decides on its own (16=A); when the work will also
|
||||
change that file it is in ``affected_paths`` and decides there. System-repo
|
||||
evidence reads are named in the note so the author can see why the constitutional
|
||||
pack was not bought (P1). The active binding alone does NOT decide (owner
|
||||
decision D29: a plan bound to the system repo that declares no system path is
|
||||
not constitutional). Skill-payload paths under ``payload_roots`` are exempt
|
||||
(data plane, as today). URLs and ``task:``/``chat:`` locators never make it
|
||||
true. Returns ``(constitutional, note)`` — the note names the deciding
|
||||
locator for disclosure.
|
||||
"""
|
||||
|
|
@ -498,9 +517,10 @@ def resolve_constitutional(
|
|||
# proposal to drop system-repo-nested payload roots (S-B05) was REJECTED for that
|
||||
# reason; the residual it names requires control of the payload predicate itself.
|
||||
payloads = [pathlib.Path(p).resolve(strict=False) for p in payload_roots]
|
||||
exists = evidence_exists if evidence_exists is not None else _default_exists
|
||||
skipped: list[str] = []
|
||||
for label, locators in (("affected_resources", affected_resources), ("evidence", evidence)):
|
||||
|
||||
def _system_locators(locators: Iterable[str]):
|
||||
"""Lazily yield the declared locators that are PATHS resolving under the system repo,
|
||||
verbatim — the first one is enough to decide, so nothing further is resolved."""
|
||||
for raw in locators or []:
|
||||
locator = str(raw or "").strip()
|
||||
if not _is_path_locator(locator):
|
||||
|
|
@ -509,28 +529,31 @@ def resolve_constitutional(
|
|||
if resolved is None or any(_under(resolved, payload) for payload in payloads):
|
||||
continue
|
||||
if resolved == system or _under(resolved, system):
|
||||
# An `affected_resources` target counts whether or not it exists yet — creating
|
||||
# `ouroboros/new_module.py` IS self-modification. An `evidence` locator is only
|
||||
# something to LOOK AT: a path that does not exist (a typo, a file belonging to
|
||||
# another workspace) must not drag the constitutional pack in behind it — but the
|
||||
# skip is DISCLOSED, never reported as "no locator resolved" (P1).
|
||||
if label == "evidence" and not exists(resolved):
|
||||
skipped.append(locator)
|
||||
continue
|
||||
# The locator is quoted VERBATIM (never ``repr``): the note is disclosure a
|
||||
# reviewer copies back, and ``repr`` doubles every backslash of a Windows path.
|
||||
return True, (
|
||||
f"constitutional: {label} locator '{locator}' resolves under the "
|
||||
"Ouroboros system repository (structural fact)"
|
||||
)
|
||||
if skipped:
|
||||
listed = ", ".join(f"'{item}'" for item in skipped[:5])
|
||||
return False, (
|
||||
"not constitutional: the only system-repo locators declared are EVIDENCE paths that do "
|
||||
f"not exist ({listed}) — declare them under "
|
||||
"affected_resources if the work will change them"
|
||||
yield locator
|
||||
|
||||
for target in _system_locators(affected_paths):
|
||||
# The locator is quoted VERBATIM (never ``repr``): the note is disclosure a
|
||||
# reviewer copies back, and ``repr`` doubles every backslash of a Windows path.
|
||||
return True, (
|
||||
f"constitutional: affected_paths locator '{target}' resolves under the "
|
||||
"Ouroboros system repository (structural fact)"
|
||||
)
|
||||
return False, "not constitutional: no declared locator resolves under the Ouroboros system repository"
|
||||
reads: list[str] = []
|
||||
for read in _system_locators(evidence):
|
||||
reads.append(read)
|
||||
if len(reads) >= 5: # the note names them; a sixth adds nothing a reviewer acts on
|
||||
break
|
||||
if reads:
|
||||
listed = ", ".join(f"'{item}'" for item in reads)
|
||||
return False, (
|
||||
"not constitutional: the system-repo locators declared are EVIDENCE reads "
|
||||
f"({listed}), and reading a repository file is not changing it — list a file "
|
||||
"under affected_paths if the work will change it"
|
||||
)
|
||||
return False, (
|
||||
"not constitutional: no declared affected_paths locator resolves under the "
|
||||
"Ouroboros system repository"
|
||||
)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------- findings
|
||||
|
|
|
|||
|
|
@ -14,7 +14,10 @@ Usage (from anywhere):
|
|||
|
||||
``spec.json`` follows the plan_task spec schema: in_scope, non_goals, acceptance_claims,
|
||||
invariants, decisions[{choice, rejected, why}], deferred[{what, why_safe_to_defer}],
|
||||
affected_resources, evidence (``--evidence`` values are appended to it).
|
||||
affected_paths (REQUIRED — the files the work will CHANGE, ``[]`` when it changes none; this
|
||||
is the only list resolved as paths, and a path under the system repo makes the plan
|
||||
constitutional), affected_resources (the same question in words: systems, services, projects,
|
||||
people), evidence (``--evidence`` values are appended to it).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
|
|||
|
|
@ -253,6 +253,8 @@ S11_GOAL = "Write the w3a plan-review smoke note."
|
|||
S11_SPEC_V1 = {
|
||||
"in_scope": ["w3a plan-review smoke"],
|
||||
"acceptance_claims": ["The plan-review smoke completes with a recorded chronicle."],
|
||||
# Required on every submitted spec (owner 9=A); the smoke note changes no repository file.
|
||||
"affected_paths": [],
|
||||
}
|
||||
S11_SPEC_V2 = {
|
||||
**S11_SPEC_V1,
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ from ouroboros.tools import plan_packet, plan_spec
|
|||
|
||||
|
||||
@pytest.mark.parametrize("field", ["goal", "in_scope", "non_goals", "invariants",
|
||||
"affected_resources", "evidence", "acceptance_claims",
|
||||
"decisions", "deferred"])
|
||||
"affected_paths", "affected_resources", "evidence",
|
||||
"acceptance_claims", "decisions", "deferred"])
|
||||
def test_operative_tail_changes_normalized_identity_and_current_packet(field):
|
||||
prefix = "яё𐍈🚀\n" * 500
|
||||
raw = {"goal": "Full plan", field: prefix + "TAIL_A"}
|
||||
|
|
|
|||
|
|
@ -83,7 +83,8 @@ def test_full_plan_review_disposition_repeat_and_tail_delta(_harness):
|
|||
ctx = _harness.make_ctx()
|
||||
goal = "full chosen goal\n" * 22_000 + "GOAL_TAIL"
|
||||
spec = {"in_scope": ["full requirement\n" * 22_000 + "SCOPE_TAIL_A"],
|
||||
"acceptance_claims": ["full criterion\n" * 22_000 + "CLAIM_TAIL"]}
|
||||
"acceptance_claims": ["full criterion\n" * 22_000 + "CLAIM_TAIL"],
|
||||
"affected_paths": []} # required on every submitted spec (owner 9=A)
|
||||
assert _control(_call(ctx, spec, goal=goal)) == {"outcome": "REVIEW_REQUIRED", "closed": True}
|
||||
state = _state(_harness)
|
||||
fingerprint = state["waves"][-1]["request_fingerprint"]
|
||||
|
|
|
|||
262
tests/test_plan_resource_form.py
Normal file
262
tests/test_plan_resource_form.py
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
"""The plan spec's resource form: `affected_paths` is the ONE list the host resolves.
|
||||
|
||||
Owner decisions 8=A / 9=A / 16=A, taken after a live wave: the old `affected_resources` list
|
||||
was read as "everything that is not a URL is a file path", so the prose item «Отдельный проект
|
||||
«TSMC — 10-летний инвестиционный анализ»» resolved to a path under the Ouroboros repository, the
|
||||
plan was marked constitutional, and every reviewer of a deck-shaped plan received BIBLE.md plus
|
||||
ARCHITECTURE.md in full (~470k tokens per cycle). Across the live history not one of the 14
|
||||
constitutional verdicts came from a real file path in that list.
|
||||
|
||||
What is pinned here: a declared CHANGE target decides (existing or not); prose never resolves
|
||||
and never reaches the filesystem; an evidence READ of a repository file is named but buys
|
||||
nothing; a spec submitted in the old mixed form is refused before any dispatch without writing
|
||||
anything; and a wave recorded under the old shape still closes for free.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
|
||||
from ouroboros.tools import plan_spec
|
||||
from tests.test_plan_review_engine import ( # noqa: F401 — `harness` is the fixture
|
||||
CLEAN,
|
||||
DECK_SPEC,
|
||||
_call,
|
||||
_control,
|
||||
_finding,
|
||||
_state,
|
||||
harness,
|
||||
)
|
||||
|
||||
# Verbatim from the live wave of task 12102247e791ff1e (structural-health sprint evidence,
|
||||
# plan-resource-live-examples.json): the list that made a deck plan constitutional.
|
||||
TSMC_RESOURCES = [
|
||||
"Отдельный проект «TSMC — 10-летний инвестиционный анализ»",
|
||||
"Файлы исследования, моделей и PDF внутри его project workspace",
|
||||
"Финальные пользовательские deliverables",
|
||||
]
|
||||
|
||||
|
||||
# The three plan-review waves recorded on the live install while `affected_resources` was still
|
||||
# resolved as a path list (structural-health sprint evidence, plan-resource-live-examples.json:
|
||||
# tasks 12102247e791ff1e, 0be6fb0690f44bbd, 6596589bb3674aaa). Their resource lists are verbatim;
|
||||
# the plan bodies stay on the owner's machine. The digests were taken from this module BEFORE
|
||||
# `affected_paths` existed — a stored wave must keep its identity or the task it belongs to can
|
||||
# never collect or dispose of it again.
|
||||
STORED_LEGACY_WAVES = (
|
||||
("12102247e791ff1e",
|
||||
["Отдельный проект «TSMC — 10-летний инвестиционный анализ»",
|
||||
"Файлы исследования, моделей и PDF внутри его project workspace",
|
||||
"Финальные пользовательские deliverables"],
|
||||
[],
|
||||
"8832d9db81cbd61dea584c193130e25b3c63fea25b007b7978456f07f92e1f82",
|
||||
"40bc4f76e9053c02066ee0900a6ebde424af70450adfe2e2fdf6ccff1b87d337"),
|
||||
("0be6fb0690f44bbd",
|
||||
["/Users/anton/Ouroboros/projects/TSMC___10-____________________________"],
|
||||
[],
|
||||
"a8aea6bb3adbb7dcb635dbebb7161d238d34168502420b7be4fd4f4c1692f06f",
|
||||
"8e652cd2dbbce9ebfa1ecafe147cea4adf41672551995e83659b2e712d869bcb"),
|
||||
("6596589bb3674aaa",
|
||||
["/Users/anton/Ouroboros/data/skills/external/context-lens",
|
||||
"/Users/anton/Ouroboros/projects/Context_Lens___________________________________"],
|
||||
["data/skills/external/context-lens/SKILL.md"],
|
||||
"2e2764e09bc74383c082bae1eabc44c9acb24c5c8cd4048c4e77187375bbc240",
|
||||
"9acfa5071662c07c1f716c5e15a0fd1648c56b64df96a3e5aae1b9289e91fbff"),
|
||||
)
|
||||
|
||||
|
||||
def test_stored_specs_from_before_affected_paths_keep_their_recorded_identity():
|
||||
"""`affected_paths` is never defaulted onto input that did not declare it: normalizing a
|
||||
spec recorded under the old shape yields the same key list, the same spec hash and the same
|
||||
plan fingerprint it was recorded with."""
|
||||
for task_id, resources, evidence, spec_digest, fingerprint in STORED_LEGACY_WAVES:
|
||||
raw = {"goal": f"stored plan wave {task_id}", "affected_resources": list(resources),
|
||||
"evidence": list(evidence)}
|
||||
spec, errors = plan_spec.normalize_spec(raw)
|
||||
assert errors == []
|
||||
assert "affected_paths" not in spec
|
||||
assert list(spec) == [
|
||||
"goal", "in_scope", "non_goals", "acceptance_claims", "invariants", "decisions",
|
||||
"deferred", "affected_resources", "evidence", "normalization_omissions",
|
||||
]
|
||||
assert plan_spec.spec_hash(spec) == spec_digest
|
||||
assert plan_spec.plan_fingerprint(
|
||||
spec["goal"], "stored prose", spec, "manifest-hash", False) == fingerprint
|
||||
|
||||
|
||||
def test_spec_delta_sees_the_affected_paths_claim_appear_and_then_move():
|
||||
"""Absent ("stored before the field existed") and `[]` ("this work changes no files") are
|
||||
different claims, so the delta the next reviewer reads must not call them the same."""
|
||||
base = {"goal": "g", "affected_resources": ["the uploader"]}
|
||||
legacy, _ = plan_spec.normalize_spec(base)
|
||||
declares_none, _ = plan_spec.normalize_spec({**base, "affected_paths": []})
|
||||
declares_file, _ = plan_spec.normalize_spec({**base, "affected_paths": ["ouroboros/uploader.py"]})
|
||||
|
||||
appeared = plan_spec.spec_delta(legacy, declares_none)
|
||||
assert appeared["changed"] and appeared["declared_keys_changed"]
|
||||
assert appeared["prev_hash"] != appeared["hash"]
|
||||
|
||||
moved = plan_spec.spec_delta(declares_none, declares_file)
|
||||
assert moved["changed"] and moved["declared_keys_changed"] is False
|
||||
assert moved["lists"]["affected_paths"] == {"added": ["ouroboros/uploader.py"], "removed": []}
|
||||
assert plan_spec.spec_delta(declares_file, declares_file)["changed"] is False
|
||||
|
||||
|
||||
def _system_prompt(substrate, index=0):
|
||||
return substrate.calls[index]["request"].messages[0]["content"][0]["text"]
|
||||
|
||||
|
||||
def test_a_declared_new_source_file_is_constitutional_before_it_exists(harness): # noqa: F811
|
||||
"""A file the work will CREATE has no bytes and no parent directory on disk yet; writing
|
||||
`ouroboros/new/deep/module.py` IS self-modification, so the full pack must ride."""
|
||||
target = harness.system / "ouroboros" / "new" / "deep" / "module.py"
|
||||
assert not target.exists() and not target.parent.exists()
|
||||
substrate = harness.install({"s1": CLEAN, "s2": CLEAN, "s3": CLEAN})
|
||||
|
||||
out = _call(harness.make_ctx(), spec={**DECK_SPEC, "affected_paths": [str(target)]})
|
||||
|
||||
wave = _state(harness)["waves"][-1]
|
||||
assert wave["constitutional"] is True
|
||||
assert "affected_paths" in wave["constitutional_note"] and str(target) in wave["constitutional_note"]
|
||||
system_prompt = _system_prompt(substrate)
|
||||
assert "## BIBLE.md (constitution" in system_prompt and "6. Governance" in system_prompt
|
||||
assert "## ARCHITECTURE.md (architecture and data flow" in system_prompt
|
||||
assert "on-demand pointer" not in system_prompt
|
||||
assert "REMINDER" not in out
|
||||
|
||||
|
||||
def test_prose_resources_never_reach_the_filesystem_and_never_buy_the_pack(harness, monkeypatch): # noqa: F811
|
||||
"""The TSMC list, exactly as the live wave sent it: descriptions of a project, its files and
|
||||
its deliverables. With no change target declared the plan is ordinary, the constitutional
|
||||
resolver resolves nothing at all, and the host reminds the agent that the list it CAN use
|
||||
for that is `affected_paths`."""
|
||||
resolved: list[str] = []
|
||||
real = plan_spec._resolve_locator_path
|
||||
monkeypatch.setattr(
|
||||
plan_spec, "_resolve_locator_path",
|
||||
lambda locator, root: (resolved.append(locator), real(locator, root))[1],
|
||||
)
|
||||
substrate = harness.install({"s1": CLEAN, "s2": CLEAN, "s3": CLEAN})
|
||||
spec = {**DECK_SPEC, "affected_paths": [], "affected_resources": list(TSMC_RESOURCES)}
|
||||
|
||||
# `active_workspace=False` binds the task to the system repo itself: the one situation where
|
||||
# an empty change-target list is worth a reminder (D29 keeps the binding from deciding).
|
||||
out = _call(harness.make_ctx(active_workspace=False), spec=spec)
|
||||
|
||||
assert resolved == []
|
||||
wave = _state(harness)["waves"][-1]
|
||||
assert wave["constitutional"] is False
|
||||
assert wave["constitutional_note"] == (
|
||||
"not constitutional: no declared affected_paths locator resolves under the "
|
||||
"Ouroboros system repository"
|
||||
)
|
||||
assert "REMINDER: affected_paths is empty" in out
|
||||
system_prompt = _system_prompt(substrate)
|
||||
assert "## BIBLE.md (constitution" not in system_prompt and "on-demand pointer" in system_prompt
|
||||
|
||||
|
||||
def test_reading_a_repository_file_is_not_changing_it(harness): # noqa: F811
|
||||
"""Owner 16=A. The same locator, twice: as evidence alone it is named in the note and the
|
||||
pack stays a pointer; listed as a change target too, it escalates."""
|
||||
bible = str(harness.system / "BIBLE.md")
|
||||
substrate = harness.install({"s1": CLEAN, "s2": CLEAN, "s3": CLEAN})
|
||||
|
||||
_call(harness.make_ctx(), spec={**DECK_SPEC, "affected_paths": [], "evidence": [bible]})
|
||||
|
||||
read_only = _state(harness)["waves"][-1]
|
||||
assert read_only["constitutional"] is False
|
||||
assert "EVIDENCE reads" in read_only["constitutional_note"] and bible in read_only["constitutional_note"]
|
||||
assert "## BIBLE.md (constitution" not in _system_prompt(substrate)
|
||||
|
||||
substrate = harness.install({"s1": CLEAN, "s2": CLEAN, "s3": CLEAN})
|
||||
_call(harness.make_ctx(task_id="task-2"),
|
||||
spec={**DECK_SPEC, "affected_paths": [bible], "evidence": [bible]})
|
||||
|
||||
changing = _state(harness, "task-2")["waves"][-1]
|
||||
assert changing["constitutional"] is True
|
||||
assert "affected_paths" in changing["constitutional_note"]
|
||||
assert "## BIBLE.md (constitution" in _system_prompt(substrate)
|
||||
|
||||
|
||||
def test_a_legacy_form_submission_is_refused_and_records_nothing(harness): # noqa: F811
|
||||
"""Owner 9=A: the old mixed form is refused BEFORE any dispatch. The refusal quotes the
|
||||
field with an example, and — because its code is not the `PLAN_SPEC_INVALID` family that
|
||||
supersedes the current attempt — the task's recorded plan state does not move a byte."""
|
||||
substrate = harness.install({"s1": CLEAN, "s2": CLEAN, "s3": CLEAN})
|
||||
ctx = harness.make_ctx()
|
||||
assert _control(_call(ctx))["closed"] is True
|
||||
before = json.dumps(_state(harness), sort_keys=True, ensure_ascii=False)
|
||||
|
||||
legacy = {key: value for key, value in DECK_SPEC.items() if key != "affected_paths"}
|
||||
legacy["affected_resources"] = list(TSMC_RESOURCES)
|
||||
out = _call(ctx, spec=legacy)
|
||||
|
||||
assert "PLAN_RESOURCE_FORM_REQUIRED" in out and "spec.affected_paths is required" in out
|
||||
assert '"affected_paths": ["ouroboros/tools/plan_review.py"]' in out
|
||||
assert '"affected_resources": ["the plan-review organ"' in out
|
||||
assert len(substrate.calls) == 1 # the refused submission called no reviewer
|
||||
assert json.dumps(_state(harness), sort_keys=True, ensure_ascii=False) == before
|
||||
|
||||
|
||||
def test_a_legacy_form_submission_over_an_open_wave_points_at_the_free_exit(harness): # noqa: F811
|
||||
"""The expensive mistake this prevents: re-submitting into a refusal while an OPEN wave is
|
||||
still the live obligation. The refusal names that wave and the $0 way to answer it."""
|
||||
ask = json.dumps([_finding("f1", "need_evidence", breaks="goal", summary="who signs off?")])
|
||||
harness.install({"s1": ask, "s2": CLEAN, "s3": CLEAN})
|
||||
ctx = harness.make_ctx()
|
||||
assert _control(_call(ctx)) == {"outcome": "REVIEW_REQUIRED", "closed": False}
|
||||
open_fingerprint = _state(harness)["waves"][-1]["request_fingerprint"]
|
||||
before = json.dumps(_state(harness), sort_keys=True, ensure_ascii=False)
|
||||
|
||||
legacy = {key: value for key, value in DECK_SPEC.items() if key != "affected_paths"}
|
||||
out = _call(ctx, spec=legacy)
|
||||
|
||||
assert "PLAN_RESOURCE_FORM_REQUIRED" in out
|
||||
assert open_fingerprint in out and "review_disposition" in out
|
||||
assert json.dumps(_state(harness), sort_keys=True, ensure_ascii=False) == before
|
||||
|
||||
|
||||
def test_a_wave_stored_before_affected_paths_still_closes_at_zero_cost(harness): # noqa: F811
|
||||
"""No grace mechanism was built for the waves already open (owner, disclosed) — they must
|
||||
simply stay answerable. A wave recorded under the old spec shape closes through
|
||||
`review_disposition` with no reviewer call, and its recorded identity does not move."""
|
||||
from ouroboros import task_results
|
||||
from ouroboros.tools import plan_review_artifacts as artifacts
|
||||
from ouroboros.tools.plan_review import _apply_disposition
|
||||
|
||||
legacy_spec, errors = plan_spec.normalize_spec({
|
||||
"goal": "Deliver the TSMC analysis",
|
||||
"affected_resources": list(TSMC_RESOURCES),
|
||||
})
|
||||
assert errors == [] and "affected_paths" not in legacy_spec
|
||||
stored_hash = plan_spec.spec_hash(legacy_spec)
|
||||
fingerprint = "c" * 64
|
||||
wave = {
|
||||
"schema_version": 2, "cycle_index": 1, "request_fingerprint": fingerprint,
|
||||
"goal": legacy_spec["goal"], "spec": legacy_spec, "spec_hash": stored_hash,
|
||||
"aggregate": "REVIEW_REQUIRED", "closed": False, "paid": True, "dispositions": [],
|
||||
"findings": [{"finding_id": "s1:f1", "id": "f1", "class": "need_evidence", "breaks": "goal",
|
||||
"locator": "", "summary": "who signs this off?", "recommendation": ""}],
|
||||
}
|
||||
artifacts.record_exact_wave(
|
||||
harness.drive, "task-1", wave, copy.deepcopy(wave), need_evidence_seen=[], page_size=32,
|
||||
)
|
||||
task_results.record_plan_review_attempt(
|
||||
harness.drive, "task-1", fingerprint=fingerprint, status="open",
|
||||
)
|
||||
paid_before = int(_state(harness).get("cycles_paid") or 0)
|
||||
substrate = harness.install({"s1": CLEAN, "s2": CLEAN, "s3": CLEAN})
|
||||
|
||||
out = _apply_disposition(harness.make_ctx(), {"review_fingerprint": fingerprint, "items": [
|
||||
{"finding_id": "s1:f1", "decision": "accept", "rationale": "the owner signs it off"},
|
||||
]})
|
||||
|
||||
assert _control(out) == {"outcome": "REVIEW_REQUIRED", "closed": True}
|
||||
assert substrate.calls == []
|
||||
state = _state(harness)
|
||||
assert int(state.get("cycles_paid") or 0) == paid_before
|
||||
closed = state["waves"][-1]
|
||||
assert closed["request_fingerprint"] == fingerprint and closed["closed"] is True
|
||||
assert closed["spec_hash"] == stored_hash and closed["aggregate"] == "REVIEW_REQUIRED"
|
||||
assert "affected_paths" not in closed["spec"]
|
||||
|
|
@ -355,7 +355,7 @@ def test_malformed_reviewer_slots_block_plan_review_before_any_dispatch(tmp_path
|
|||
patch.object(pr, "_run_plan_review_slots",
|
||||
side_effect=AssertionError("no reviewer dispatch")),
|
||||
):
|
||||
result = pr._handle_plan_task(ctx, plan="P", goal="G", spec={"in_scope": ["x"]})
|
||||
result = pr._handle_plan_task(ctx, plan="P", goal="G", spec={"in_scope": ["x"], "affected_paths": []})
|
||||
|
||||
assert "Invalid reviewer-slot configuration blocks plan review" in result
|
||||
assert "not valid JSON" in result
|
||||
|
|
@ -410,7 +410,7 @@ def test_expired_explicit_deadline_skips_before_any_reviewer(monkeypatch, tmp_pa
|
|||
monkeypatch.setattr(pr, "_plan_review_slots",
|
||||
lambda: (_ for _ in ()).throw(AssertionError("expired deadline must skip")))
|
||||
out = asyncio.run(pr._run_plan_review_async(
|
||||
ctx, pr._PlanRequest(goal="G", plan="P", spec={"in_scope": ["x"]}),
|
||||
ctx, pr._PlanRequest(goal="G", plan="P", spec={"in_scope": ["x"], "affected_paths": []}),
|
||||
))
|
||||
|
||||
assert out.startswith("PLAN_TASK_SKIPPED_DEADLINE: the task deadline has expired")
|
||||
|
|
@ -437,7 +437,7 @@ def test_expired_deadline_replays_a_recorded_wave_but_never_pays(monkeypatch, tm
|
|||
monkeypatch.setattr(pr, "_plan_review_slots",
|
||||
lambda: (_ for _ in ()).throw(AssertionError("no panel under a dead deadline")))
|
||||
out = asyncio.run(pr._run_plan_review_async(
|
||||
ctx, pr._PlanRequest(goal="G", plan="P", spec={"in_scope": ["x"]}),
|
||||
ctx, pr._PlanRequest(goal="G", plan="P", spec={"in_scope": ["x"], "affected_paths": []}),
|
||||
))
|
||||
assert out.startswith("PLAN_TASK_SKIPPED_DEADLINE:")
|
||||
attempt = load_plan_review_state(tmp_path, ctx.task_id)["current_attempt"]
|
||||
|
|
@ -512,8 +512,10 @@ class TestPlanReviewToolRegistration(unittest.TestCase):
|
|||
spec = params["spec"]["properties"]
|
||||
self.assertEqual(set(spec), {
|
||||
"in_scope", "non_goals", "acceptance_claims", "invariants", "decisions",
|
||||
"deferred", "affected_resources", "evidence",
|
||||
"deferred", "affected_paths", "affected_resources", "evidence",
|
||||
})
|
||||
# The ONE list the host resolves as file paths, and the one a submitted spec must carry.
|
||||
self.assertEqual(params["spec"]["required"], ["affected_paths"])
|
||||
disposition = params["review_disposition"]
|
||||
self.assertEqual(disposition["required"], ["review_fingerprint", "items"])
|
||||
decision = disposition["properties"]["items"]["items"]["properties"]["decision"]
|
||||
|
|
|
|||
|
|
@ -134,7 +134,10 @@ DECK_SPEC = {
|
|||
"invariants": ["deliver by Friday", "no confidential numbers"],
|
||||
"decisions": [{"choice": "one chart per slide", "rejected": ["tables"], "why": "audience"}],
|
||||
"deferred": [{"what": "color palette", "why_safe_to_defer": "cosmetic"}],
|
||||
"affected_resources": [],
|
||||
# `affected_paths` is REQUIRED on every submitted spec (owner 9=A): a deck changes no
|
||||
# repository file, and `[]` is how a plan says exactly that.
|
||||
"affected_paths": [],
|
||||
"affected_resources": ["the Q3 board deck"],
|
||||
"evidence": [],
|
||||
}
|
||||
|
||||
|
|
@ -669,13 +672,14 @@ def test_evidence_omissions_reach_the_packet_and_the_wave(harness):
|
|||
assert manifest["attached"][0]["sha256"] and "text" not in manifest["attached"][0]
|
||||
|
||||
|
||||
def test_constitutional_from_affected_resources_and_reminder_on_system_binding(harness):
|
||||
def test_constitutional_from_affected_paths_and_reminder_on_system_binding(harness):
|
||||
sub = harness.install({"s1": CLEAN, "s2": CLEAN, "s3": CLEAN})
|
||||
# (a) workspace binding, an absolute path into the system repo declared as affected
|
||||
spec = {**DECK_SPEC, "affected_resources": [str(harness.system / "ouroboros" / "loop.py")]}
|
||||
# (a) workspace binding, an absolute path into the system repo declared as a CHANGE target
|
||||
# (owner 8=A: `affected_resources` is prose now and buys nothing, so the trigger is here)
|
||||
spec = {**DECK_SPEC, "affected_paths": [str(harness.system / "ouroboros" / "loop.py")]}
|
||||
out = _call(harness.make_ctx(), spec=spec)
|
||||
wave = _state(harness)["waves"][-1]
|
||||
assert wave["constitutional"] is True and "affected_resources" in wave["constitutional_note"]
|
||||
assert wave["constitutional"] is True and "affected_paths" in wave["constitutional_note"]
|
||||
system_prompt = sub.calls[0]["request"].messages[0]["content"][0]["text"]
|
||||
assert "## BIBLE.md" in system_prompt and "Principle 3: Immune Integrity" in system_prompt
|
||||
# W3: a self-modification plan carries ARCHITECTURE.md inline, in full — not a map, not a pointer
|
||||
|
|
@ -688,7 +692,7 @@ def test_constitutional_from_affected_resources_and_reminder_on_system_binding(h
|
|||
out2 = _call(ctx)
|
||||
wave2 = _state(harness, "task-2")["waves"][-1]
|
||||
assert wave2["constitutional"] is False
|
||||
assert "REMINDER: affected_resources is empty" in out2
|
||||
assert "REMINDER: affected_paths is empty" in out2
|
||||
system_prompt2 = sub.calls[1]["request"].messages[0]["content"][0]["text"]
|
||||
assert "Principle 3: Immune Integrity\n\nreview." not in system_prompt2
|
||||
assert "on-demand pointer" in system_prompt2
|
||||
|
|
@ -861,8 +865,16 @@ def test_retired_swarm_keys_are_dropped_on_settings_load(tmp_path, monkeypatch):
|
|||
|
||||
|
||||
def test_ratchet_module_sizes():
|
||||
"""The engine entered the 1001-1500 band with the required-`affected_paths` form gate; the
|
||||
exact debt is owned by ouroboros/size_ratchet_manifest.py, so the pin here is the band plus
|
||||
the rule that a banded module must carry a rationale there."""
|
||||
from ouroboros.size_ratchet_manifest import BAND_PATHS
|
||||
|
||||
repo = pathlib.Path(pr.__file__).resolve().parents[2]
|
||||
assert len((repo / "ouroboros" / "tools" / "plan_review.py").read_text(encoding="utf-8").splitlines()) < 1000
|
||||
lines = len((repo / "ouroboros" / "tools" / "plan_review.py").read_text(encoding="utf-8").splitlines())
|
||||
assert lines <= 1500
|
||||
if lines > 1000:
|
||||
assert (BAND_PATHS.get("ouroboros/tools/plan_review.py") or "").strip()
|
||||
assert len((repo / "ouroboros" / "config.py").read_text(encoding="utf-8").splitlines()) <= 1600
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@ def test_run_plan_review_script_runs_the_engine_on_the_new_envelope(monkeypatch,
|
|||
spec_path.write_text(json.dumps({
|
||||
"in_scope": ["the accepted phase"], "acceptance_claims": ["tests green"],
|
||||
"invariants": ["no new settings"],
|
||||
# An operator envelope carries the same required form as the agent's (owner 9=A).
|
||||
"affected_paths": [],
|
||||
}), encoding="utf-8")
|
||||
evidence_file = workspace / "notes.txt"
|
||||
evidence_file.write_text("inspect the existing SSOT", encoding="utf-8")
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ def test_constitutional_packet_without_architecture_is_a_typed_failure(harness):
|
|||
agent sees, never a reviewer wave that silently lacks the document."""
|
||||
sub = harness.install({"s1": CLEAN, "s2": CLEAN, "s3": CLEAN})
|
||||
(harness.system / "docs" / "ARCHITECTURE.md").unlink()
|
||||
spec = {**DECK_SPEC, "affected_resources": [str(harness.system / "ouroboros" / "loop.py")]}
|
||||
spec = {**DECK_SPEC, "affected_paths": [str(harness.system / "ouroboros" / "loop.py")]}
|
||||
out = _call(harness.make_ctx(), spec=spec)
|
||||
assert "ARCHITECTURE.md" in out and "W3" in out
|
||||
assert sub.calls == [] # no reviewer was called
|
||||
|
|
@ -372,7 +372,7 @@ def test_session_task_is_the_compact_form_with_governance_by_mandatory_retrieval
|
|||
resolvable locators, never ~500k chars inline; api rows still get them inline."""
|
||||
harness.state["slots"] = _slots(("api1", "m/a"), ("sess1", "cursor=grok", "session"), ("api2", "m/b"))
|
||||
sub = harness.install({"api1": CLEAN, "sess1": CLEAN, "api2": CLEAN})
|
||||
spec = {**DECK_SPEC, "affected_resources": [str(harness.system / "ouroboros" / "loop.py")]}
|
||||
spec = {**DECK_SPEC, "affected_paths": [str(harness.system / "ouroboros" / "loop.py")]}
|
||||
_call(harness.make_ctx(), spec=spec)
|
||||
request = sub.calls[0]["request"]
|
||||
api_system = request.messages[0]["content"][0]["text"]
|
||||
|
|
@ -383,9 +383,12 @@ def test_session_task_is_the_compact_form_with_governance_by_mandatory_retrieval
|
|||
assert "slots and quorum." not in task and "Principle 3: Immune Integrity\n\nreview." not in task
|
||||
assert "Plan Review Checklist" in task and "REDACTED snapshot" in task
|
||||
# the governance documents are the ONE raw-read exception, even when the agent ALSO declared
|
||||
# BIBLE.md as evidence (declaring it makes the plan constitutional): no contradictory orders
|
||||
# BIBLE.md as evidence beside changing it (the change target is what makes the plan
|
||||
# constitutional — owner 16=A): no contradictory orders
|
||||
sub = harness.install({"api1": CLEAN, "sess1": CLEAN, "api2": CLEAN})
|
||||
_call(harness.make_ctx(task_id="task-2"), spec={**DECK_SPEC, "evidence": [str(harness.system / "BIBLE.md")]})
|
||||
_call(harness.make_ctx(task_id="task-2"), spec={**DECK_SPEC,
|
||||
"affected_paths": [str(harness.system / "BIBLE.md")],
|
||||
"evidence": [str(harness.system / "BIBLE.md")]})
|
||||
task2 = sub.calls[0]["request"].session_task
|
||||
assert "MANDATORY FULL READS" in task2 and "even if the agent also declared them as evidence" in task2
|
||||
assert f"### {harness.system / 'BIBLE.md'}" in task2 # the redacted snapshot is still there too
|
||||
|
|
@ -416,7 +419,8 @@ def test_state_stays_persistable_at_the_worst_case_request_bounds(tmp_path):
|
|||
"decisions": [{"choice": wide, "why": wide,
|
||||
"rejected": [wide] * 8} for _ in range(n_items)],
|
||||
"deferred": [{"what": wide, "why_safe_to_defer": wide} for _ in range(n_items)],
|
||||
"affected_resources": [f"{wide[:-4]}a{i:03d}" for i in range(n_items)],
|
||||
"affected_paths": [f"{wide[:-4]}a{i:03d}" for i in range(n_items)],
|
||||
"affected_resources": [f"{wide[:-4]}r{i:03d}" for i in range(n_items)],
|
||||
"evidence": [f"{wide[:-4]}e{i:03d}" for i in range(n_items)],
|
||||
}
|
||||
normalized, errors = plan_spec.normalize_spec(spec)
|
||||
|
|
@ -689,13 +693,17 @@ def test_both_reviewer_routes_learn_the_range_selectors(harness):
|
|||
assert "::lines=A-B" in request.session_task
|
||||
|
||||
|
||||
def test_the_plan_spec_schema_discloses_both_halves_of_the_constitutional_trigger():
|
||||
"""The trigger reads `affected_resources` AND an existing `evidence` path; the schema the
|
||||
agent sees says so, including that a non-existent path does not count."""
|
||||
def test_the_plan_spec_schema_names_affected_paths_as_the_only_resolved_list():
|
||||
"""The trigger reads ONE list — `affected_paths` — and the schema the agent sees says so:
|
||||
required, files only, `[]` when none, and the other two lists explicitly not resolved."""
|
||||
assert pr._SPEC_SCHEMA["required"] == ["affected_paths"]
|
||||
props = pr._SPEC_SCHEMA["properties"]
|
||||
assert "system repository" in props["affected_resources"]["description"]
|
||||
paths = props["affected_paths"]["description"]
|
||||
assert "REQUIRED" in paths and "system repository" in paths and "[]" in paths
|
||||
resources = props["affected_resources"]["description"]
|
||||
assert "never file paths" in resources and "system repository" not in resources
|
||||
evidence = props["evidence"]["description"]
|
||||
assert "system repository" in evidence and "EXISTING" in evidence
|
||||
assert "does not make the plan" in evidence and "affected_paths" in evidence
|
||||
|
||||
|
||||
# ------------------------------------------------------------- in-flight honesty (P1-4)
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@ CODE_SPEC = {
|
|||
"invariants": ["no new dependencies", "public API unchanged"],
|
||||
"decisions": [{"choice": "exponential backoff", "rejected": ["fixed delay"], "why": "bursty upstream"}],
|
||||
"deferred": [{"what": "metrics emission", "why_safe_to_defer": "observability only"}, "log wording"],
|
||||
"affected_resources": ["ouroboros/uploader.py"],
|
||||
"affected_paths": ["ouroboros/uploader.py"],
|
||||
"affected_resources": ["the upload pipeline", "the storage provider"],
|
||||
"evidence": ["ouroboros/uploader.py", "https://example.com/spec", "task:abc123"],
|
||||
}
|
||||
|
||||
|
|
@ -40,7 +41,8 @@ DECK_SPEC = {
|
|||
"invariants": ["deadline Friday 17:00", "no confidential customer names"],
|
||||
"decisions": [{"choice": "one message per slide", "rejected": ["dense slides"], "why": "board attention"}],
|
||||
"deferred": ["colour palette"],
|
||||
"affected_resources": [],
|
||||
"affected_paths": [],
|
||||
"affected_resources": ["the board deck"],
|
||||
"evidence": [],
|
||||
}
|
||||
|
||||
|
|
@ -62,7 +64,7 @@ def test_normalize_spec_mints_ids_and_keeps_schema():
|
|||
assert spec["normalization_omissions"] == []
|
||||
assert list(spec) == [
|
||||
"goal", "in_scope", "non_goals", "acceptance_claims", "invariants", "decisions",
|
||||
"deferred", "affected_resources", "evidence", "normalization_omissions",
|
||||
"deferred", "affected_paths", "affected_resources", "evidence", "normalization_omissions",
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -184,11 +186,11 @@ def test_constitutional_active_binding_alone_does_not_decide(tmp_path):
|
|||
system = tmp_path / "repo"
|
||||
system.mkdir()
|
||||
ok, note = plan_spec.resolve_constitutional(
|
||||
active_root=system, system_repo_root=system, affected_resources=[], evidence=[],
|
||||
active_root=system, system_repo_root=system, affected_paths=[], evidence=[],
|
||||
)
|
||||
assert ok is False and note.startswith("not constitutional")
|
||||
ok, _ = plan_spec.resolve_constitutional(
|
||||
active_root=system, system_repo_root=system, affected_resources=["ouroboros/loop.py"], evidence=[],
|
||||
active_root=system, system_repo_root=system, affected_paths=["ouroboros/loop.py"], evidence=[],
|
||||
)
|
||||
assert ok is True # a relative path under the (system) active root still decides
|
||||
|
||||
|
|
@ -200,56 +202,63 @@ def test_constitutional_declared_paths_relative_absolute_and_file_scheme(tmp_pat
|
|||
workspace.mkdir()
|
||||
ok, note = plan_spec.resolve_constitutional(
|
||||
active_root=workspace, system_repo_root=system,
|
||||
affected_resources=["../repo/ouroboros/loop.py"], evidence=[],
|
||||
affected_paths=["../repo/ouroboros/loop.py"], evidence=[],
|
||||
)
|
||||
assert ok and "affected_resources" in note
|
||||
# An EVIDENCE locator counts only when it actually exists: a plan that merely wants to
|
||||
# LOOK at a repo file is constitutional, a typo pointing at nothing is not (E2E finding).
|
||||
assert ok and "affected_paths" in note
|
||||
# Owner 16=A: an EVIDENCE locator is something to LOOK AT, and reading a repo file is not
|
||||
# changing it — existing or not, it never buys the constitutional pack on its own. Existence
|
||||
# therefore stopped deciding anything, and the check went with it.
|
||||
(system / "BIBLE.md").write_text("# constitution\n", encoding="utf-8")
|
||||
ok, note = plan_spec.resolve_constitutional(
|
||||
active_root=workspace, system_repo_root=system,
|
||||
affected_resources=["src/app.py"], evidence=[str(system / "BIBLE.md")],
|
||||
affected_paths=["src/app.py"], evidence=[str(system / "BIBLE.md")],
|
||||
)
|
||||
assert ok and "evidence" in note
|
||||
assert ok is False and "EVIDENCE reads" in note and str(system / "BIBLE.md") in note
|
||||
ok, _ = plan_spec.resolve_constitutional(
|
||||
active_root=workspace, system_repo_root=system,
|
||||
affected_resources=["src/app.py"], evidence=[str(system / "NO_SUCH_FILE.md")],
|
||||
affected_paths=["src/app.py"], evidence=[str(system / "NO_SUCH_FILE.md")],
|
||||
)
|
||||
assert ok is False
|
||||
# ...while a TARGET counts whether or not it exists yet — creating a module is self-modification.
|
||||
ok, _ = plan_spec.resolve_constitutional(
|
||||
active_root=workspace, system_repo_root=system,
|
||||
affected_resources=[str(system / "ouroboros" / "brand_new_module.py")], evidence=[],
|
||||
affected_paths=[str(system / "ouroboros" / "brand_new_module.py")], evidence=[],
|
||||
)
|
||||
assert ok is True
|
||||
ok, _ = plan_spec.resolve_constitutional(
|
||||
active_root=workspace, system_repo_root=system,
|
||||
affected_resources=["src/app.py"], evidence=["docs/x.md"],
|
||||
affected_paths=["src/app.py"], evidence=["docs/x.md"],
|
||||
)
|
||||
assert ok is False
|
||||
# B-06: a system-repo path dressed as file:// is still a path.
|
||||
ok, note = plan_spec.resolve_constitutional(
|
||||
active_root=workspace, system_repo_root=system,
|
||||
affected_resources=[f"file://{system}/ouroboros/loop.py"], evidence=[],
|
||||
affected_paths=[f"file://{system}/ouroboros/loop.py"], evidence=[],
|
||||
)
|
||||
assert ok is True and "file://" in note
|
||||
|
||||
|
||||
def test_constitutional_evidence_selector_classifies_the_source_path(tmp_path):
|
||||
def test_constitutional_selector_classifies_the_source_path(tmp_path):
|
||||
"""A `::lines=A-B` suffix names a RANGE of one file: the path in front of it decides, on
|
||||
either list — as a change target it escalates, as an evidence read it is only named."""
|
||||
system = tmp_path / "repo"
|
||||
workspace = tmp_path / "workspace"
|
||||
workspace.mkdir()
|
||||
evidence = _write(system / "docs" / "ARCHITECTURE.md", "# Architecture\n")
|
||||
source = _write(system / "docs" / "ARCHITECTURE.md", "# Architecture\n")
|
||||
|
||||
ok, note = plan_spec.resolve_constitutional(
|
||||
active_root=workspace,
|
||||
system_repo_root=system,
|
||||
affected_resources=[],
|
||||
evidence=[f"{evidence}::lines=1-1"],
|
||||
active_root=workspace, system_repo_root=system,
|
||||
affected_paths=[f"{source}::lines=1-1"], evidence=[],
|
||||
)
|
||||
|
||||
assert ok is True
|
||||
assert f"{evidence}::lines=1-1" in note
|
||||
assert f"{source}::lines=1-1" in note
|
||||
|
||||
ok, note = plan_spec.resolve_constitutional(
|
||||
active_root=workspace, system_repo_root=system,
|
||||
affected_paths=[], evidence=[f"{source}::lines=1-1"],
|
||||
)
|
||||
assert ok is False
|
||||
assert f"{source}::lines=1-1" in note and "EVIDENCE reads" in note
|
||||
|
||||
|
||||
def test_constitutional_payload_exempt_url_task_never_and_empty_false(tmp_path):
|
||||
|
|
@ -261,7 +270,7 @@ def test_constitutional_payload_exempt_url_task_never_and_empty_false(tmp_path):
|
|||
(system / "x").mkdir(parents=True)
|
||||
ok, _ = plan_spec.resolve_constitutional(
|
||||
active_root=workspace, system_repo_root=system,
|
||||
affected_resources=[str(payload / "SKILL.md")], evidence=[], payload_roots=[payload],
|
||||
affected_paths=[str(payload / "SKILL.md")], evidence=[], payload_roots=[payload],
|
||||
)
|
||||
assert ok is False
|
||||
# A payload root that resolves INSIDE the system repo still exempts: the roots come
|
||||
|
|
@ -271,22 +280,22 @@ def test_constitutional_payload_exempt_url_task_never_and_empty_false(tmp_path):
|
|||
nested = system / "data" / "skills" / "native" / "demo"
|
||||
ok, _ = plan_spec.resolve_constitutional(
|
||||
active_root=workspace, system_repo_root=system,
|
||||
affected_resources=[str(nested / "SKILL.md")], evidence=[], payload_roots=[nested],
|
||||
affected_paths=[str(nested / "SKILL.md")], evidence=[], payload_roots=[nested],
|
||||
)
|
||||
assert ok is False
|
||||
# A system path OUTSIDE every payload root is still constitutional.
|
||||
ok, _ = plan_spec.resolve_constitutional(
|
||||
active_root=workspace, system_repo_root=system,
|
||||
affected_resources=[str(system / "ouroboros" / "loop.py")], evidence=[], payload_roots=[nested],
|
||||
affected_paths=[str(system / "ouroboros" / "loop.py")], evidence=[], payload_roots=[nested],
|
||||
)
|
||||
assert ok is True
|
||||
ok, _ = plan_spec.resolve_constitutional(
|
||||
active_root=workspace, system_repo_root=system, affected_resources=[],
|
||||
active_root=workspace, system_repo_root=system, affected_paths=[],
|
||||
evidence=["https://github.com/razzant/ouroboros/blob/main/ouroboros/loop.py", "task:xyz"],
|
||||
)
|
||||
assert ok is False
|
||||
ok, note = plan_spec.resolve_constitutional(
|
||||
active_root=workspace, system_repo_root=system, affected_resources=[], evidence=[],
|
||||
active_root=workspace, system_repo_root=system, affected_paths=[], evidence=[],
|
||||
)
|
||||
assert ok is False and note.startswith("not constitutional")
|
||||
|
||||
|
|
@ -399,7 +408,7 @@ def test_resolve_evidence_and_constitutional_never_raise_on_hostile_locators(tmp
|
|||
assert manifest["attached"] == []
|
||||
for locator in (long, "loop1", "\x00bad"):
|
||||
ok, _ = plan_spec.resolve_constitutional(
|
||||
active_root=root, system_repo_root=tmp_path / "repo", affected_resources=[locator], evidence=[],
|
||||
active_root=root, system_repo_root=tmp_path / "repo", affected_paths=[locator], evidence=[],
|
||||
)
|
||||
assert ok is False
|
||||
# A fifo / device is a non-regular file: refused, never opened (would block forever).
|
||||
|
|
@ -869,11 +878,14 @@ def test_capped_reviewer_requests_are_rendered_even_when_the_agent_declared_noth
|
|||
|
||||
def test_domain_independence_deck_spec_without_paths(tmp_path):
|
||||
spec, errors = plan_spec.normalize_spec(DECK_SPEC)
|
||||
assert errors == [] and spec["affected_resources"] == [] and spec["evidence"] == []
|
||||
# A deck changes no file: `affected_paths` is the empty claim, while the prose list still
|
||||
# says what the work touches — and neither one is resolved into a file read.
|
||||
assert errors == [] and spec["affected_paths"] == [] and spec["evidence"] == []
|
||||
assert spec["affected_resources"] == ["the board deck"]
|
||||
assert plan_spec.spec_ids(spec) == frozenset({"goal", "claim_1", "claim_2", "invariant_1", "invariant_2", "decision_1", "deferred_1"})
|
||||
assert len(plan_spec.spec_hash(spec)) == 64
|
||||
ok, _ = plan_spec.resolve_constitutional(
|
||||
active_root=tmp_path / "ws", system_repo_root=tmp_path / "repo", affected_resources=spec["affected_resources"], evidence=spec["evidence"],
|
||||
active_root=tmp_path / "ws", system_repo_root=tmp_path / "repo", affected_paths=spec["affected_paths"], evidence=spec["evidence"],
|
||||
)
|
||||
assert ok is False
|
||||
manifest = plan_evidence.resolve_evidence(spec["evidence"], active_root=tmp_path, allowed_roots=[tmp_path])
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"""Structural constitutional escalation for ``plan_task`` (formerly v6.61.0 plan_class
|
||||
escalation): the ONE path fact ``constitutional`` — declared ``affected_resources`` /
|
||||
``evidence`` locators resolving under the Ouroboros system repository (owner D29:
|
||||
escalation): the ONE path fact ``constitutional`` — declared ``affected_paths`` locators
|
||||
(the files the work will CHANGE) resolving under the Ouroboros system repository (owner D29:
|
||||
the active binding alone never decides) — with the skill-payload exemption the
|
||||
retired ``resolve_plan_class`` applied. Kept from the original file: the
|
||||
path-escalation and payload-exemption cases, ported to ``plan_spec.resolve_constitutional``
|
||||
|
|
@ -34,7 +34,7 @@ def _resolve(ctx, affected, evidence=()):
|
|||
active = ctx.active_repo_dir()
|
||||
return resolve_constitutional(
|
||||
active_root=active, system_repo_root=system,
|
||||
affected_resources=list(affected), evidence=list(evidence),
|
||||
affected_paths=list(affected), evidence=list(evidence),
|
||||
payload_roots=plan_payload_roots(ctx, list(affected) + list(evidence)),
|
||||
)
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ def _resolve(ctx, affected, evidence=()):
|
|||
def test_system_repo_paths_make_the_plan_constitutional(tmp_path):
|
||||
ctx = _ctx(tmp_path) # active workspace IS the system repo
|
||||
ok, note = _resolve(ctx, ["ouroboros/loop.py"])
|
||||
assert ok and "affected_resources" in note
|
||||
assert ok and "affected_paths" in note
|
||||
# D29: the binding alone never decides — nothing declared, not constitutional.
|
||||
ok, note = _resolve(ctx, [])
|
||||
assert ok is False and note.startswith("not constitutional")
|
||||
|
|
@ -56,15 +56,15 @@ def test_external_workspace_paths_stay_non_constitutional_unless_absolute_into_s
|
|||
# An ABSOLUTE path back into the system repo escalates even from a workspace.
|
||||
abs_sys = str(tmp_path / "sys" / "ouroboros" / "config.py")
|
||||
ok, note = _resolve(ctx, ["src/app.py", abs_sys])
|
||||
assert ok and "affected_resources" in note
|
||||
# A declared EVIDENCE locator under the system repo escalates too — when it exists.
|
||||
assert ok and "affected_paths" in note
|
||||
# Owner 16=A: a declared EVIDENCE locator under the system repo is a READ, not a change —
|
||||
# it never escalates on its own, existing or not, and the host says which reads it saw
|
||||
# instead of reporting "no locator resolved".
|
||||
(tmp_path / "sys" / "BIBLE.md").write_text("# constitution\n", encoding="utf-8")
|
||||
ok, note = _resolve(ctx, ["src/app.py"], evidence=[str(tmp_path / "sys" / "BIBLE.md")])
|
||||
assert ok and "evidence" in note
|
||||
# A system-repo evidence path that does NOT exist (a typo) must not drag the constitutional
|
||||
# pack in, and the host says exactly that instead of "no locator resolved".
|
||||
assert ok is False and "EVIDENCE reads" in note
|
||||
ok, note = _resolve(ctx, ["src/app.py"], evidence=[str(tmp_path / "sys" / "NOPE.md")])
|
||||
assert ok is False and "do not exist" in note
|
||||
assert ok is False and "EVIDENCE reads" in note
|
||||
|
||||
|
||||
# --- skill-payload exemption (data plane, never self-modification by itself) ---------
|
||||
|
|
@ -100,7 +100,7 @@ def test_native_bucket_is_not_exempt(tmp_path):
|
|||
# Native skills are repo-seeded territory — the payload predicate does not admit them.
|
||||
ctx = _ctx(tmp_path)
|
||||
ok, note = _resolve(ctx, ["data/skills/native/x/plugin.py"])
|
||||
assert ok and "affected_resources" in note
|
||||
assert ok and "affected_paths" in note
|
||||
|
||||
|
||||
def test_drive_resolution_failure_skips_the_exemption(tmp_path):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue