feat(okf): derive a cost baseline from a priced schedule, or refuse
MAJOR-4 (misjonsreview v2 section 7), owner B = the consumer. Neither existing projection into ir.CostBaseline can serve an ingested tender corpus: cost-baseline.json is hand-written per project and baseline_from_project belongs to the road domain, so a K2-shaped bundle could be navigated and never anchored. okf.derive_cost_baseline reads the numbers already in the bundle. The premise was MEASURED before anything was built on it, and the order's two pointers named two different forms. examples/*/expected-bundle/ carry pipe tables, but every one of them is csv- or sql-sourced via render.render_table -- the xlsx path never reaches render_table at all. Measured with pandoc 3.10.2 under the producer's own writer and arguments: extract._extract_office converts, and inbox.py hands that text to render_inbox_concept untouched, so an xlsx-sourced concept file carries a pandoc SIMPLE table whose dash rule defines the column spans. A pipe-only reader would have been inert on exactly the corpus this exists for. Both forms are read, by two scanners over one role mapping and one number grammar. No judgement anywhere: the header vocabulary and the number grammar are closed, and every ambiguity refuses -- no candidate table, more than one, two columns claiming one role, two rows sharing a cost code, a row that prices nothing. A partly-priced schedule refuses in full, because a half-derived baseline anchors some codes while cost_baseline_anchored reports True. Wired behind --derive-cost-baseline and never silently: one resolution in run.py's bundle arm serves both the full run and the dry run, and the refusal propagates rather than degrading to the file loader. The two fixtures are pandoc's output verbatim, not hand-typed. The unpriced one is K2's actual pre-award shape, and the columns survive as blanks -- so the table IS a candidate and the refusal is the sharp one. Load-bearing MEASURED: 18 mutations all red against the WHOLE suite, green control 1230 passed / 5 skipped (from 1208/5, superset, 0 removed), golden demo-transcript.stdout byte-unchanged (ea8c534773acdbe41ae68f2c55724d69aaf8be4f). M17 is the one that matters for arm (b): dropping the positivity guard makes the mutant raise pydantic ValidationError, which IS a ValueError but is NOT the named class -- so pytest.raises(ValueError) would have stayed green against exactly the mutation the arm exists to catch. Verified directly, not argued. Honesty limits stated in the invariant row: NS 3451 section rows are not classified (K2 itself was not available to measure), the stamp records that a run was anchored and never which projection anchored it, the hosted surface is deliberately untouched, and no live K2 file was read. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
9e44afeefb
commit
0add73531b
11 changed files with 870 additions and 7 deletions
28
README.md
28
README.md
|
|
@ -421,7 +421,9 @@ when the seam is detached, so the loop cannot silently degrade into theater.
|
|||
[Walk the whole chain offline](#walk-the-whole-chain-offline); mutually exclusive with
|
||||
`--live-dry-run`, which stops before the first model call rather than answering it), and
|
||||
`--explore "<prompt>" --explore-config <file>` (opt-in: run an exploration first and let it
|
||||
shape the mandate this run evaluates — see below).
|
||||
shape the mandate this run evaluates — see below), and `--derive-cost-baseline` (opt-in:
|
||||
anchor the validator on a priced schedule already inside `--bundle-dir` instead of a
|
||||
hand-written `cost-baseline.json` — see below).
|
||||
- **Portfolio** — `--portfolio`, plus optional `--goals`, `--ledger`, `--dimension-config`,
|
||||
`--semantic-retrieval`; it stops early and prints a `goal reached: …` line when the
|
||||
accumulated ledger meets a goal.
|
||||
|
|
@ -509,6 +511,30 @@ when the seam is detached, so the loop cannot silently degrade into theater.
|
|||
`--checkpoint-dir` are refused together (two doors onto one review), as are `--resume` and
|
||||
`--explore` (two sources of one exploration).
|
||||
|
||||
- **Deriving the cost baseline from the knowledge base** — `--derive-cost-baseline` (opt-in,
|
||||
requires `--bundle-dir`). The deterministic validator anchors a proposal's `affected_items` to
|
||||
the project's actual cost lines. Normally those come from a hand-written `cost-baseline.json` in
|
||||
the bundle, or — on the road reference path — from the project's own `cost_items`. Neither is
|
||||
available when the bundle was *ingested* from tender documents, so this third route reads the
|
||||
priced schedule that is already in the base: one markdown table whose header names a cost code, a
|
||||
quantity and a unit price. Both table forms the producer emits are read — the pipe tables its
|
||||
csv/sql connectors render, and the pandoc simple tables an `.xlsx` becomes.
|
||||
|
||||
**It refuses rather than guesses, and that is the point.** A pre-award schedule has its price
|
||||
column empty, and completing it with a zero or an invented number would hand the gate a baseline
|
||||
nobody wrote. So the run stops — by name — when no table names all three columns, when more than
|
||||
one does, when two columns claim the same role, when two rows share a cost code, or when any row
|
||||
prices nothing. A partly-priced schedule is refused in full: a half-derived baseline would anchor
|
||||
some codes while the run still reported itself anchored. The header vocabulary and the number
|
||||
grammar are both closed (plain dot-decimal numbers; a thousands separator or a comma decimal is
|
||||
refused, not interpreted).
|
||||
|
||||
```bash
|
||||
uv run python -m portfolio_optimiser.run PROSJEKT-1 --docs-dir <bundle> --bundle-dir <bundle> \
|
||||
--derive-cost-baseline
|
||||
```
|
||||
|
||||
|
||||
`--explore` is refused together with `--mandate` — they are two sources of one mandate, and
|
||||
merging would silently overwrite what you wrote. To seed an exploration with a domain expert's
|
||||
own hypotheses, use `explore(..., seed_approaches=[Approach(...)])`; seeds are always preserved
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue