feat(p20): the requirement that is RIGHT, and a clause number that is not a price
Three seams, one commit: A, B and C touch the same four modules (run.py carries
the debate task, the grounding composition and the announcement; okf.py carries
one reference-number vocabulary read by both A and B), so splitting them into
three commits would have meant hunk-level staging of entangled files. Stated
rather than silently restructured.
A — the declaration answers with the DOCUMENT's own words. Measured: 13
declarations over round 3 and P17b, not one naming a fasit concept, while the
tool answered {"declared": true, ...} by echoing the caller's own arguments. It
now returns the document's title and req_number, read off Bundle.context_files
(so the type: verdict layer can never be named back), plus the sentence saying
what the declaration binds. A path the base carries as no concept answers with
empty strings rather than refusing. The commission's success_criteria now reach
the DEBATE task through mandate.criteria_block, the one renderer, empty when
there are none — which is what keeps every un-commissioned prompt, and the
golden, byte-identical.
B — a clause number is not a price. THE ORDER'S OWN RULE WAS FELLED BY
MEASUREMENT: it asks to refuse a code that IS declared req_number/prosessnr,
and neither of its two known positives is. n500 declares seksjon 10.4.1..10.4.4
but never the bare 10.4; r761 declares 2727 prosessnr and 2753 seksjon, none of
them 1.10.4, which occurs once, as prose ("iht. vegnormal N200 kap. 1.10.4").
The COMPLEMENT fires on both and closes the hole _ground_against_input already
admits in writing -- "it fails OPEN on a coincidental match". Unanchored run +
requirement-shaped code + the base declares a vocabulary + the code is not in
it -> refused, naming the denominator. All five of kontrakt-sorasen's real
process codes ARE declared and pass, which is what keeps the one context set
built on real codes measurable. Replayed over all 24 codes of round 3 + P17b:
exactly the two known positives flip validated -> rejected, 22 unchanged.
C — a parse failure no longer burns the round ledger blind. _fetch_parsed takes
a BUILDER instead of a finished message list, so the retry carries the parse
reason; measured, kontrakt-sorasen-04 spent 11 of 12 rounds re-asking the same
question. And announced_subject names the routed bases instead of saying "the
portfolio" for a two-base commission.
Suite 1807/5 (from 1781, +26, 0 removed), golden demo-transcript.stdout
BYTE-UNCHANGED (shasum -a 1 of the CONTENT = ea8c534773acdbe41ae68f2c55724d69aaf8be4f),
ruff and mypy clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
64723c5d89
commit
c8f0c8f7c4
12 changed files with 1098 additions and 37 deletions
|
|
@ -98,6 +98,7 @@ from portfolio_optimiser.mandate import (
|
|||
MandateRoutingError,
|
||||
announce,
|
||||
candidate_from_approach,
|
||||
criteria_block,
|
||||
load_mandate,
|
||||
route_by_bundle,
|
||||
settle,
|
||||
|
|
@ -1131,6 +1132,10 @@ async def run_project(
|
|||
# gate's share rule needs, and composing them here — where the base is already walked — is what
|
||||
# keeps them from being a second, drifting reconstruction (kø-(p)).
|
||||
bundle_grounding: tuple[str, ...] = ()
|
||||
#: P20/B: the reference numbers the base's documents DECLARE, composed in the SAME walk as the
|
||||
#: documents above, so the gate's vocabulary and the gate's text describe one reading of one
|
||||
#: base. Empty on the road path, which is what keeps the rule unable to fire there.
|
||||
bundle_references: tuple[str, ...] = ()
|
||||
# S2c: a CALLER-OWNED sink for what the debate opens (the ``parse_failures``/``ExplorationTrace``
|
||||
# shape). A returned value would be lost on exactly the run that most needs the evidence — a
|
||||
# budget stop mid-debate raises out of ``debate.run`` and constructs no ``RunResult`` at all.
|
||||
|
|
@ -1151,6 +1156,9 @@ async def run_project(
|
|||
bundle_grounding = tuple(
|
||||
"\n".join([f.name, *f.frontmatter.values(), f.body]) for f in bundle.context_files
|
||||
)
|
||||
bundle_references = tuple(
|
||||
ref for f in bundle.context_files for ref in okf.declared_reference_numbers(f)
|
||||
)
|
||||
# ONE bundle-id rule (Step 10, slackened S7a-3 pkt. 1): the DECLARED id is the identity and
|
||||
# the mount is carried alongside, so a base delivered under a directory name of its own is
|
||||
# opened rather than refused. What is still refused, before a single model call: a base
|
||||
|
|
@ -1272,7 +1280,9 @@ async def run_project(
|
|||
# ``_fetch_parsed`` has returned, so a report from there could only ever speak once an attempt
|
||||
# had been paid for. ONE binding feeding both the report and the gate: two compositions of one
|
||||
# text are free to disagree, which is exactly what a report must not be able to do (kø-(p)).
|
||||
delivered = Grounding(documents=(context, *bundle_grounding))
|
||||
delivered = Grounding(
|
||||
documents=(context, *bundle_grounding), declared_references=bundle_references
|
||||
)
|
||||
offer = grounding_offer(project, baseline, delivered)
|
||||
|
||||
# Trekk B2 (krav 3): configured MCP servers become tools the AGENTS can call during the debate.
|
||||
|
|
@ -1359,8 +1369,16 @@ async def run_project(
|
|||
async with AsyncExitStack() as mcp_stack:
|
||||
for live_tool in live_mcp_tools:
|
||||
await mcp_stack.enter_async_context(live_tool)
|
||||
# P20/A2: the commission's success criteria reach the DEBATE — the prompt where
|
||||
# ``declare_requirement`` is available — and not only the announcement. Composed through
|
||||
# ``mandate.criteria_block``, the ONE renderer (kø-(p)); empty without a commission, so
|
||||
# every un-commissioned run's task message is byte-identical, which is what keeps the
|
||||
# golden transcript unchanged. MEASURED: on the exploration path there is nothing to
|
||||
# carry — ``main()`` passes ``explore()`` no ``success_criteria`` at all, so its
|
||||
# objective IS the prompt and already reaches the task.
|
||||
criteria = criteria_block(mandate.success_criteria) if mandate is not None else ""
|
||||
result = await debate.run(
|
||||
f"Find a cost-saving measure for {project.id}.\nContext:\n{context}"
|
||||
f"Find a cost-saving measure for {project.id}.{criteria}\nContext:\n{context}"
|
||||
)
|
||||
finally:
|
||||
# ``finally``, the ``write_parse_failures`` precedent: any exception leaving the debate —
|
||||
|
|
@ -1610,7 +1628,12 @@ async def run_project(
|
|||
cost_baseline_anchored=baseline is not None,
|
||||
# P19/B2: what the run made of each code it was handed. Derived from the SAME classifier
|
||||
# the gate uses (kø-(p)), off the proposal being stamped — never re-read from anywhere.
|
||||
code_forms=classify_codes([item.code for item in proposal.affected_items]),
|
||||
code_forms=classify_codes(
|
||||
[item.code for item in proposal.affected_items],
|
||||
# P20/B: the THIRD value, ``requirement``, needs the base's own vocabulary. Read off
|
||||
# the SAME ``delivered`` the gate was handed, never a second composition.
|
||||
delivered,
|
||||
),
|
||||
# WHICH corpus was judged, and whether the base named itself or the mount named it for it.
|
||||
# Read off the SAME resolution the run opened the base with (kø-(p)); ``None`` on the road
|
||||
# path, where no knowledge base exists to name.
|
||||
|
|
@ -2290,6 +2313,32 @@ def _write_multibase_summary(
|
|||
)
|
||||
|
||||
|
||||
def announced_subject(project_id: str | None, across_bundle: Sequence[str]) -> str:
|
||||
"""WHO the announcement is about: the project, the routed bases, or the portfolio (P20/C2).
|
||||
|
||||
MEASURED (P17b F5): ``--across-bundle`` takes no ``--project-id`` — each base's project is read
|
||||
from that base's own IR projection — so ``args.project_id or "the portfolio"`` announced a
|
||||
multi-base commission as a portfolio pass, which is a different mode entirely.
|
||||
|
||||
The ids are resolved for NAMING ONLY, and a base that cannot be resolved falls back to its
|
||||
directory name. That is the ``dimension_label`` precedent one line above the call site,
|
||||
verbatim: a configuration that fails to load is left unnamed here and refused a moment later by
|
||||
the dispatch, which stays the single owner of that refusal — announcing must never change which
|
||||
error an operator sees.
|
||||
"""
|
||||
if project_id:
|
||||
return project_id
|
||||
if not across_bundle:
|
||||
return "the portfolio"
|
||||
names = []
|
||||
for raw in across_bundle:
|
||||
try:
|
||||
names.append(okf.reconcile_bundle_id(raw).id)
|
||||
except (okf.BundleIdMismatch, FileNotFoundError, ValueError, OSError):
|
||||
names.append(Path(raw).name)
|
||||
return ", ".join(names)
|
||||
|
||||
|
||||
def resolve_bundle_routing(
|
||||
bundle_dirs: Sequence[str],
|
||||
) -> tuple[tuple[str, str, str], ...]:
|
||||
|
|
@ -4045,7 +4094,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||
print(
|
||||
announce(
|
||||
mandate,
|
||||
project_id=args.project_id or "the portfolio",
|
||||
project_id=announced_subject(args.project_id, args.across_bundle or ()),
|
||||
# From ARGV, never the constants (P16 B2). The announcement is the one thing
|
||||
# printed BEFORE the first paid call, and its whole job is to say what this run
|
||||
# will do; reading the defaults was correct only while main() could not do
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue