8 Commits

Author SHA1 Message Date
Croissant Le Doux
fdb0aeac5d Close #29: the v1 rate structure types
Price is a rate function, not a scalar (#22), and #15 bars any model from
evaluating one — so the shapes have to be closed AND computable. This replaces
the src/pricing seam with a composition rather than an enumeration: one base
schedule deciding unit price, plus closed modifiers, plus an optional floor.
Three vendors on one small RFP already produced five cells of the alternative
cross-product, and two of the three are the same object wearing different
decoration.

The scenario vocabulary is closed too, and published with the RFP, so every bid
in a field is priced against identical assumptions. A structure reading a
variable the RFP never declared is rejected at submission, while the vendor can
still fix it.

Abstention is typed. `input_missing` is separated from `shape_unsupported`
because Chesapeake's shape is fully supported and its unit price simply is not
in the document — that is recoverable by asking, and collapsing the two throws
away the only actionable fact. Null is never zero, never the vendor's stated
figure, and never a partial sum.

The derivation is the record. A bare scalar can only assert; a protest asks why
a bid ranked where it did. Lines accumulate as exact rationals in bigint and
round once, half-up. A scenario amendment voids derived costs rather than
recomputing them.

Exceptions carry a closed consequence union. Anacostia's EX-3 says the vendor
cannot bid produce at all if declined — a nullable price delta reads that as "no
cost impact", the inverse of the truth, on the most consequential of the three.

Two defects the corpus check caught that review would not have:

  - The band ladder priced two ways depending on a flag. `to` is inclusive, and
    subtracting those bounds in the marginal path lost the unit between bands —
    one in 21,400, invisible in the total and wrong in the record.

  - The published scenario was quantizing the price before the evaluator saw it.
    Twelve basis-point shares moved the seasonal factor by 3e-5, about $19 on a
    $624,000 line. Weights, not shares: a school calendar publishes instructional
    days, an exact integer, and the ratio is taken last.

  npm run check:pricing

Potomac computes $589,570.00 and not the stated $589,970.00; Chesapeake abstains
naming base.unitPrice; Anacostia is $649,792.00 with the calendar and unpriceable
without it; a 3-of-4-line bid withholds its total.

Migrations generated in two passes so drizzle-kit never needed the interactive
rename prompt. No SQL hand-edited.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 02:39:20 -04:00
Croissant Le Doux
a36956b1e1 corpus: align determinability key with the schema column
The truth files used `determinableFromResponseDocument`; the schema column
and gateEvaluableFromResponse() use `determinableFromResponse`. A silent
name drift means the flag reads as its default (true), and R-B6C — "two bid
copies submitted", which no response document can ever establish — starts
failing bidders on a packaging detail.

Renamed in rfp.truth.json and validate-responses.py, and added the mirror
check to validate-truth.py so the drift cannot recur unnoticed:

  gates 13, evaluable from the response 12, excluded 1
    excluded: R-B6C  Two bid copies submitted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 19:52:32 -04:00
Christian
126e94a14b Apply both corpus findings to the schema
criterion.parent_id — a score commits at any criterion with NO WEIGHTED
CHILDREN. Derived from the rubric, never configured, and neither corpus
document is special-cased: Tarrant's five flat criteria are all leaves;
Friendship's 'Method of Approach' 30 is not scoreable while its 15/10/5
children are. A criterion whose sub-items carry no points stays scoreable
at its own level, because those are requirements mapped through
criterion_requirement rather than children.

requirement.determinable_from_response — false where satisfaction cannot be
established from the response document at all. Gate evaluation excludes
these rather than failing them; treating Friendship's 'two bid copies' as
not_answered disqualifies two of three bidders over a packaging detail.

The scoring rule is mirrored in corpus/validate-truth.py so the corpus and
the code cannot silently disagree. Against Friendship it yields 10 scoreable
criteria of 12, summing to exactly 100.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 18:50:16 -04:00
Christian
19e7ffe92a Add three synthesized Friendship PCS responses with ground truth
Covers all nine stress cases from #25, #15 and #16. The lowest bid is
disqualified on two missing health documents and the highest bid is
strongest on the SFA's own geographic-preference criterion, so a ranking
that cannot represent absence gets the field exactly backwards.

Deliberate traps: a planted $400 arithmetic error in a stated total, an
unverifiable total whose unit price lives only in the external Excel, an
unlabelled exception, a non-priceable exception that is really a conditional
withdrawal, and an answered-but-negative response.

Second schema finding, surfaced by the validator: R-B6C is a mandatory gate
that no response document can establish. Coverage over it is unknowable
rather than absent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 18:39:08 -04:00
Christian
e81da40947 Add Friendship PCS RFP ground truth and a truth-file validator
31 requirements (13 mandatory gates, 5 certification forms, 11 narrative,
1 pricing, 1 compliance schedule), 12 criteria across two levels, and three
labelled known-misses.

The known-misses are drawn from the document rather than invented:
  KM-1  Schedule C holds no prices; content is in an external Excel file,
        and pricing is 40 of 100 points
  KM-2  a mandatory submission stated only inside a parenthetical within a
        scored criterion's description in Section K
  KM-3  'Response to values statement' carries no obligation verb and is
        mandatory only by virtue of the checklist eleven pages earlier

validate-truth.py checks reference integrity and weight sums; these files
are hand-authored, so nothing else checks them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 18:30:36 -04:00
Christian
0b64eda25e Extract Friendship PCS text; confirm rubric and two extraction hazards
52pp, clean text layer, no OCR needed. Rubric matches #22's record and is
nested three deep, with sub-items that are weighted under two parents and
unweighted guidance under two others.

Schedule C (the price sheet) contains only 'Separate attachment to be
completed in Excel' — a ready-made labelled known-miss where pricing is 40%
of the available score.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 18:18:09 -04:00
Christian
e7d68aa6d0 Archive corpus source documents
Seven public-sector solicitations and rubric samples read by #22 from live
URLs and never saved. #22's ninth hazard was that these 403 without warning,
so archival is the irreversible-if-lost part of #30.

All seven retrieved. Two corrections to #22's record: SLC Airport is 142pp
not 55pp, and Tarrant County is an award tabulation rather than a
solicitation — it validates ranking arithmetic but cannot be authored against.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 17:54:38 -04:00
Christian
da74362d00 Initial v1 code layout
The destination of the wayfinder map: directory and module structure, the
domain model as real schema files, permission boundaries expressed as code,
and typed seams where the undecided parts land.

Satisfies the four structural requirements earlier resolutions handed to a
layout that did not exist:

  - a single database entry point (#11) — the pool is not exported
  - a scheduler entry point with actor context and no request (#13)
  - an entitlement assertion helper (#20, #34) — its call sites are the
    paid-feature list
  - an enumerated list of privileged RLS bypasses (#33) — three of them

Seams carry real types and throw with the ticket that owns them, so the
skeleton wires up and fails only where a decision is genuinely missing.

Verified: tsc --noEmit clean; drizzle-kit generate produces RLS on 18 tables
and 6 policies calling the release-level functions.

Resolves #35

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 17:20:47 -04:00