feat(consider): an answer names the market session its numbers came from, and an unpriced one names none (closes #618) - #643
Conversation
…rom, and an unpriced one names none (closes #618) Before #611 a `consider` weight was a share of cost, or of whatever the last review froze, so the same premise re-asked returned the same weights. It is now a share of the current close — which is the point, and which made every number a function of a market day the answer never stated. A user who asked twice could see that the numbers moved with no way to tell whether the market moved or their own book did. The basis now freezes `price_observations` beside `valuation_basis`, where the was-this-priced fact already lives: `by_ticker`, each instrument's own observation session, and `as_of`, the newest of them. Per instrument rather than one frame date, because a frame-level date cannot say which session a given instrument's number came from and one fresh close then makes every stale one look same-day (#583 §2); `price_feed.parse` already stores each row's `observed_date`, so nothing is derived twice. The summary is `max(by_ticker)` and not the envelope's declared `as_of`, which is an upper bound the parser enforces on its rows rather than an observation — a supplied envelope may declare today over yesterday's closes, and copying that forward would put the summary ahead of every number it summarizes. Scoped to the same `priced_universe` the recovery kit is built over, so an envelope carrying a close for an instrument no number here uses does not date it. An unpriced run grows nothing: no null, no placeholder, no today's date. `_price_observation_record` returns None and the key is simply absent. The obligation moves through the existing mechanism in both directions, per #479 Wave B's rule that what the agent is told it owes and what a case is refused for dropping are one list. `build_challenge` states it under a new `price_basis` topic — the frame session always, plus a per-instrument entry only where that instrument is off the frame, which is the case a single date would have hidden — and `required_coverage` enforces it, so a `--agent-case` that never cites the price day is refused before anything is stored. That widening exposed a latent defect and carries its repair. `basis.price_observations` is the first required path ever to sit under another, and plain prefix matching let one price-day citation discharge the `basis` staleness obligation too: a case that never mentioned the stale record was accepted, whole suite green. `_paid_path` is the fix — one claim pays the narrowest obligation it matches — and it is a no-op wherever the required paths are disjoint, which is every case that existed before this. A priced call's `evaluation_id` moves, correctly: the id seeds on the frozen answer and the answer genuinely differs. A context-free unpriced call's does not, and the literal digest `test_a_context_free_evaluation_id_is_exactly_what_it_was_before_context_existed` pins is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e-observation-date
Review — verified independently, mergingMerged the just-landed #546 in and ran the combination with the exit code read directly (not through a pipe — see your own note below): The identity invariant, checked by running both treesSame offline call on Byte-identical, and the unpriced row grows no key at all — not a "price_observations": {"as_of": "2026-07-30",
"by_ticker": {"AMD": "2026-07-30", "MRVL": "2026-07-30", "NVDA": "2026-07-30"}}
must_state: {"anchor": "basis.price_observations.as_of", "topic": "price_basis", "value": "2026-07-30"}
required_coverage: {"path": "basis.price_observations", "owes": "price_basis", "key": "price_observed"}The bundled
|
|
Correction to my own note above, before it sends anyone to build something that exists: So the trap is narrower than I made it: it is |
A user who asks the same question twice can now tell whether the numbers moved because the market moved or because their book changed.
closes #618. Basemain@6723db1(#629/PR #641, its stated precondition).origin/mainhad not moved at push time, so the tree tested below is the merged combination.What the row carries now
basis.price_observations, frozen besidevaluation_basiswhere the was-this-priced fact already lives:Absent — not null, not a placeholder, not today's — whenever
valuation_basisisunpriced.review._price_observation_recordbuilds it from the parsed feed's ownobserved_date, scoped to the samepriced_universethe #629 recovery kit is built over. Both the supplied--priceslane and the resolved lane land on onefeed, so it is stamped once incmd_considerrather than in each of_consider_rows' two book lanes.Per-ticker vs frame, and why
as_ofis not the envelope'sPer-ticker, with a frame summary — the issue's preferred option, taken. A frame-level date cannot say which session a given instrument's number came from, which is #583 §2 exactly: one fresh instrument made every stale one look same-day.
price_feed.parsealready storesobserved_dateper row, so nothing is derived a second time.The summary is
max(by_ticker)and notfeed["as_of"]. That was the one real design finding here. The envelope's declaredas_ofis an upper bound the parser enforces on its rows, not an observation: a supplied envelope may legitimately declare today while every close in it is yesterday's, and copying it forward would put the summary ahead of every number it summarizes — the same confusion at frame level that the per-ticker map exists to prevent.market_data.to_price_feed_envelopealready takesmax(row date)for its ownas_offor this reason; this is that definition applied to the subset that reached the answer.must_statefollows the same shape: the frame session always, plus a per-instrument entry only where that instrument is off the frame. On a same-day frame that is one sentence, not one per holding; on a mixed frame the exceptions are exactly the instruments the summary does not describe. A dotted ticker (2330.TW) keeps its date and loses its anchor like every other unaddressable fact, so the ticker rides indetail— otherwise the answer would carry a market session with no instrument attached.Staleness: checked, and nothing added
The issue asked whether a stale quote needs more than a date. It does not, and I added nothing. The reasons already in the tree:
market_data.to_price_feed_envelopedrops an instrument whose close predates a split (#611) and records asymbol_unpricedgap;trade_recap.price_observationsrefuses a close more than ten days off the frame's end, so it never becomes a price at all; andprice_feed.basis_conflictsrefuses the whole call when the supplied closes and the book's share counts are on different split bases. What survives all three is a quote a few sessions old — which a date describes completely. A second staleness concept here would be a threshold nobody has set, on the surfacebasis.stale_daysalready establishes the "disclose, never gate" precedent for.required_coverage, and the latent defect that came with itThe fact is in
required_coverage, per the issue's acceptance and #479 Wave B's rule that the two lists are one. Justification for the entry being its own rather than folded intobasis: thebasisentry is satisfiable by anybasis.*citation, so folding it in would let a claim aboutbasis.sourcedischarge the price-day obligation — an obligation nothing enforces, the exact failure the two-directional test exists to catch.Making it a separate entry surfaced a defect that was already there:
answer_provenance._paid_pathis the repair — one claim pays the narrowest obligation it matches — and it is a byte-for-byte no-op wherever the required paths are disjoint, which is every case that existed before this PR (consequence.disclosures.<i>andrule_collisions.<id>never nest). Mutation D below is the proof it is load-bearing. This is the one thing outside the issue's three-change scope, and it is not optional: without it, the acceptance criterion "dropping it from an--agent-caseis refused, both directions" cannot hold.The two CLI observations
Same fictional two-position root, same premise, isolated
--root,TR_OFFLINE=1.Offline / unpriced — no date appears anywhere on the row:
With a
--pricesenvelope — the date is on the row and inmust_state. The envelope also pricesCCC, which the book does not hold; it is correctly not dated.evaluation_id: which moved, which did not
eval-57eba3f7905fb86beval-57eba3f7905fb86b--priceseval-c2f80a5ba7b666a3eval-10be32e0f6cc35c9_evaluation_id(_PINNED_*)eval-a8d8c02f625ce105eval-a8d8c02f625ce105_PINNED_BASISisunpricedand gains no key;_evaluation_idsynthesizes nothing.Mutation table
Backed up and restored with
cp,__pycache__cleared between runs, exit codes read rather than output grepped. All three engine files verified byte-identical to their backups afterwards._price_basis_entriesreturns[]— the field never reachesmust_statetest_a_priced_answer_owes_the_market_session_it_was_valued_at,..._an_instrument_off_the_frame_date_is_named...,..._a_dotted_ticker_off_the_frame_date...,test_every_topic_appears_and_in_the_declared_order,test_every_required_coverage_path_is_reachable_from_must_state,test_a_case_covering_exactly_what_the_challenge_asked_for_is_accepted,test_dropping_any_single_entry_the_challenge_named_is_refused; plustest_consider::..._the_priced_answer_owes_the_session_and_names_the_instrument_off_itand..._a_case_silent_about_the_price_day_is_refused..._price_observation_recorddefaults an unpriced run to today's date over the requested universetest_considertests, headed bytest_an_offline_answer_carries_no_price_day_at_all(an offline consider grew a price observation: {'as_of': '2026-07-31', ...}); the rest through_check_evaluation_shape's no-fake-date assertionrequired_coveragestops emitting theprice_basisentry — stated, never enforcedtest_consider::..._a_case_silent_about_the_price_day_is_refused_on_the_production_path(expected failure, got exit 0— the refusal stopped firing),..._the_priced_answer_owes_the_session..., and 2 challenge tests_paid_pathtakes the broadest matching path — i.e. the prefix matching that existed before this PRtest_a_price_day_citation_does_not_also_pay_the_staleness_obligation,test_a_case_covering_exactly_what_the_challenge_asked_for_is_accepted, andtest_consider::..._a_case_silent_about_the_price_day...No mutation stayed green.
Test gate
python3 tests/run_all.py, real exit code 0 (captured directly — piping totailmasks it, which is how an earlier red run in this session first read as green).Notes for review
tools/ux_receipt.pygets a comment only, no behaviour change:price_basisis deliberately outsideNUMERIC_FACT_TOPICS, for the same reasonbasisis. Its values are dates and the reference's own example sentence states one as "priced at Tuesday's closes", so a literal-string gate would be at war with the contract it enforces. Whether the session reached the user stays thecomprehensionverdict's call.card_renderer.py,skills/fomo-kernel/copy/*.json(PR fix(card): pre-commitment preview falsely implies a standing rule exists on a user's first-ever review #546 in flight), and anything about how prices are retrieved ([architecture·implementation] One market-data resolver — minimum Yahoo calls, one fact bundle for prepare and consider #605/feat(market-data): one resolver, one dated bundle, and a consider that can see today's prices (refs #605) (closes #584) #611).price_snapshotand its own honesty ledger.valuation_basisreadspricedwhenever anylast_pxexists, even if none of those closes touched this book. Such a run would carry noprice_observations— the field records what was observed, and inventing one is what this issue forbids. That looseness predates this PR and belongs tovaluation_basis, not here; the shape check asserts the direction that has no edge case (unpriced⇒ absent).🤖 Generated with Claude Code