Skip to content

feat(challenge): delete the obligation whitelist, keep the seven facts that earn their place (closes #830) - #831

Merged
atomchung merged 3 commits into
mainfrom
claude/issue-830-obligation-deletion
Aug 20, 2026
Merged

feat(challenge): delete the obligation whitelist, keep the seven facts that earn their place (closes #830)#831
atomchung merged 3 commits into
mainfrom
claude/issue-830-obligation-deletion

Conversation

@atomchung

Copy link
Copy Markdown
Owner

The flip

The product was too verbose because the engine handed every answer a whitelist:
roughly fifteen must_state obligations, an unchecked list, and pre-written
disclosure sentences — while the expression layer explicitly disclaimed volume
("expression governs where an owed fact is said, never whether it is
owed"), and no rule anywhere claimed it. An obligation list has one cheapest
discharge: a sentence per item. The wall of tables the owner could not finish
reading was obligation discharge, not judgment.

Exhibit A from the owner's audit: basis.state_version, a sha256 content hash,
sat on the list of facts a human answer owes.

This flips the structure from whitelist obligations ("state these fifteen")
to blacklist prohibitions plus decision relevance ("never misstate or invent
— machine-gated, zero words; what gets said is chosen by what this decision
needs"). Every integrity gate is untouched, because they cost no reading.

Keeps and deletes

build_challenge returns three lists where it returned one.

must_state — kept, with the reason each keep is a keep (now stated in
references/trade-consequence.md):

Kept Why it may not be dropped
Stance + the deciding reason (case_required) The product's point
Every non-empty rule_effects entry Silence about a line the user wrote is help breaking it (owner-live incident 2026-08-02)
The one or two consequence numbers that would flip this decision That is the answer, not a disclosure
A falsifier on any directional recommendation The one element the owner praised in the #827 blind A/B
"Recorded a consideration, not executed", when a canonical write happened The user cannot see the difference; the difference is real money
Source + as-of on a public fact, compact and inline C2
One warning line when pricing is degraded Cost weights can invert which position is the largest

Deleted, and what replaced each:

Deleted Now
basis.state_version in the answer machine_state — never rendered, in any register
The basis four-piece recital every answer Two facts (as_of, stale_days); staleness is said only when it could change the decision
The concentration family every answer (top3 / ai_pct / max_sector_pct / the triggers) may_state — surfaces when it is the deciding fact or touches the user's own cap
Cash balance and weight every answer may_state — when cash is the question or a floor is crossed
Enumerating unchecked Nothing. A material gap earns one end-block line
A standalone counter-case section The falsifier line (counter_case: when_material stays in the payload)
Basis and caliber narrated in body and footer One compact end block; disclosures_display retargets there

The deletion is of obligations, never of data. Every number is still
computed, anchored and citable, and the user can ask — which is what makes not
saying a number this decision does not turn on different from hiding it.

The two silences that could hurt stayed machine-enforced. rule_effects and
required_coverage are byte-identical: a rule the user wrote is still named
and still refused-for-dropping. Only the engine's own default threshold became a
may-state. source and completeness leave must_state but still force a
basis citation through required_coverage, which basis.as_of /
basis.stale_days pay.

The one grammar rule (D7)

docs/expression-contract.md gains D7 — a fact lives on exactly one floor,
and §2's wording changes so volume distribution is expression's business while
"whether a fact is owed" stays upstream.

Floor What lives there
Opening body The facts that decide this call
Parenthetical beside its number A truth-critical denominator, unit, or pricing set (D2's enumeration)
One compact end block Sources, caliber, material gaps — one line each, non-narrative
Not rendered Machine anchors: hashes, state versions, validator detail

A fact on two floors is a bug. Completeness lives in the data layer.

The reading-budget rule proposed as V10 in the issue is not adopted: a length
cap is what #827 had just deleted, and re-adding one prices the symptom instead
of removing the cause. It stays available as a backstop if deletion alone is not
enough.

Before / after (fictional issuers, per the tests/agent convention)

Same three-way comparison, same engine facts. Before, ~2,300 zh characters
opening on the concentration sermon both arms of the #827 A/B independently
wrote. After (tests/agent/expression-witnesses.json,
deletion_first_three_way_comparison, ~280):

**三個裡我會選 GRDC 加 15 股。**決定性理由:三案對組合的影響都在一個百分點內——誰都不改變你的集中度——真正有差的只有事件風險:WDGT 六天後出財報、預期已拉滿(公司財報行事曆,2026-08-14),這時把最大倉再加大,是三案裡波動最大的;FABR 18 股只佔 1.2%,公司再好這個大小也改變不了結果。GRDC 下次財報在十月底,中間乾淨,上季主業 +82%(公司財報,2026-07-30)撐著。

反面就一條:前三大會從 51.3% 升到 51.9%(GRDC 本來就是第二大)——嫌集中的話這是三案共同的問題,答案是減碼不是選誰。
會讓我改口:你本來就想賭財報超預期——那 WDGT 反而是最直接的表達,排序整個反過來。

(帳本 8/14、價格 8/14 收盤;三案動用 $4.4K/$5.1K/$4.9K;估值未評)

The stance opens it. The deciding reason is event risk, not a standing
concentration paragraph. The counter-side is one line, and the falsifier is
another. Book date, price session and the unchecked valuation gap are one end
block of one line each — never narrated twice. The two other approved templates
(recording a selection, and a no-book single-name decision) ship as witnesses
beside it.

The same shape on the repository's own worked English example is now the third
rendering in trade-consequence.md's "One payload, three renderings".

Fail-closed evidence

A green suite must not read as a governed output, so the witnesses cut both ways.

  • A rendered machine anchor fails deterministically, twice.
    tools/ux_receipt.py refuses the delivery evidence when a declared
    machine_state value appears in the presented answer — refused rather than
    counted, because there is no acceptable number of content hashes to show
    someone mid-decision. tests/agent/check_expression.py gains E-6, which
    fails the answer text alone, by shape rather than vocabulary. TA-001's
    renders_the_book_hash proves it never reaches a model.
  • The keep survived. test_the_users_own_rule_survives_the_deletion_with_its_gate_intact
    plus the existing test_a_case_silent_about_a_collided_rule_is_refused_on_the_production_path:
    an answer recommending while dropping a touched user rule still fails.
  • Bloat witness. TA-001's discharges_every_available_fact states every
    reading the payload offers — the whole may_state family, all five unchecked
    dimensions, the basis — and reaches its stance in the last sentence. It is
    deterministically eligible and factually complete, and it fails only
    decision_focus. That is the point: no deterministic gate here can fail
    bloat, which is why the witness exists.
  • Deletion witness. deletion_first_compact is the same call under the new
    floor: it never mentions concentration or cash and clears every production
    gate. Before this change it would have failed the delivery check for omitting
    those digits.
  • Floor-vs-inventory invariant.
    test_the_owed_floor_stayed_smaller_than_the_whole_inventory fails if a
    family quietly moves back onto the floor, or if the split becomes a deletion
    of the data instead.

Compatibility

Additive and migration-free. The block is emitted, never stored, and never
enters _evaluation_id's seed, so there is no payload at rest to migrate.
may_state / machine_state are new keys, required in the schema because the
block is computed fresh per call and an optional-but-always-present key is a
declaration nothing means. must_state's topic enum narrows to match TOPICS
because tests/test_evaluation_challenge.py holds those two as an ordered
equality. ux_receipt stays silent on a challenge block that carries no
machine_state key — absence is "nothing declared", never a clean result.

skills/fomo-kernel/SKILL.md comes out 33 bytes shorter than before, which
the 16 KiB always-loaded budget required and which is the right outcome for a
change whose subject is deletion. The candidate-persistence sentence is left
verbatim; #829 owns it.

Validation

python3.12 tests/run_all.py --group product   → PASS: all 48 suites
python3.12 tests/run_all.py --group qa-eval   → PASS: all 11 suites

Suite-level counts on the directly affected files: test_evaluation_challenge
41/41 (was 34), test_consider 207/207, test_expression_contract 14/14 (was
13), test_interaction_trajectory 153/153 (was 150), test_trade_answer_judge
29/29, check_expression and check_voice witnesses PASS.

🤖 Generated with Claude Code

test added 3 commits August 20, 2026 17:18
…machine-only

One `consider` call handed every answer roughly fifteen `must_state` facts,
and an obligation list has one cheapest discharge: a sentence per item. The
owner audit of that inventory asked, of each entry, what its must-have reason
was, and found `basis.state_version` -- a content hash -- on the list of facts
a human answer owes.

`build_challenge` now returns three lists instead of one. `must_state` keeps
what had a reason: which position this trade moves, which of the user's own
rules it touches, which limitations change what a number refers to, and which
day the book and the market were read at. `may_state` carries the two families
that were recited every call and decided almost none -- the concentration
readings and the cash line -- computed, addressed, and owed by default on no
call. `machine_state` carries the hash, which is never rendered in any
register.

Nothing left the data layer. Every number is still computed, still anchorable,
still citable; `rule_effects` and `required_coverage` are untouched, so the
two silences that could help a user break their own rule stay machine-enforced
rather than moving to judgment. The basis recital falls from five facts to
two: the day the book was true and how old that makes it. `source` and
`completeness` stay on the frozen row and still force a citation through
`required_coverage`, which `basis.as_of`/`basis.stale_days` pay.

Delivery gains a fail-closed half. `ux_receipt` refuses evidence whose
presented answer renders a declared machine anchor -- refused rather than
counted, because there is no acceptable number of content hashes to show
someone mid-decision -- and stays silent on a block pasted from a build that
predates the key. `check_expression` gains E-6, which fails a rendered
machine anchor by shape on the answer text alone.
Section 2 said expression governs where an owed fact is said and never
whether it is owed, and that was read as a disclaimer of volume. Nothing else
claimed it: no rule anywhere governed how much an answer said, so every owed
fact landed in body prose as its own sentence and a fifteen-item obligation
list became a fifteen-sentence answer while D1-D6 stayed satisfied.

D7 is the missing rule. A fact lives on exactly one floor -- deciding facts
open the body, a truth-critical qualifier stays beside its number, sources and
caliber and material gaps collect into one end block one line each, machine
anchors are not rendered at all -- and a fact on two floors is a bug. It
cannot license dropping an owed fact: a fact whose floor is the end block is
still said, and the route's own obligation list is still the authority on
which facts those are.

SKILL.md's answer shape is rewritten to the reader's question chain rather
than the payload's field order, and comes out 33 bytes shorter than before --
which the 16 KiB always-loaded budget required, and which is the right shape
for a change whose subject is deletion. Two duplications went with it: the
process-leakage sentence folds into D7's never-rendered floor, and the
forecast-uncertainty rule stops being stated twice one paragraph apart. The
candidate-persistence sentence is left verbatim; #829 owns it.
`references/trade-consequence.md` carried the obligation list and no argument
for it, which is how a list nobody could defend grew to fifteen items. It now
carries both tables: what was kept with the reason each keep is a keep, and
what was deleted with what replaced it. The deletion is of obligations and
never of data -- every number is still computed, anchored and citable, and the
user can ask -- which is what makes not saying a number this decision does not
turn on different from hiding it.

"State" is redefined, because the old reading was the mechanism: stated = the
fact appears with its correct anchor, and an inline number, a table cell, and a
line in the end block all qualify. Fifteen obligations were never fifteen
sentences; nothing had said so.

The route synthesis gains the reader's question chain -- what you asked, the
answer, why, what would overturn it, what to do -- in place of reading the
payload's dependency order aloud, and the answer slots become opening body /
falsifier / one end block. A third rendering joins the worked example: the same
frozen payload under the new floor, three paragraphs where the whitelist-era
one has three and the flat one has a wall, with the concentration sermon gone
and the counter-case turned into the falsifier that replaced it.

Witnesses, in three layers. The three owner-approved zh-TW acceptance
templates join `expression-witnesses.json` as positive scenes. TA-001 gains
`discharges_every_available_fact` (every reading the payload offers, stance in
the last sentence -- eligible, complete, and failing only `decision_focus`,
which is the point: no deterministic gate here can fail bloat),
`deletion_first_compact` (the same call under the new floor, passing every
production gate while never mentioning concentration or cash -- before this
change it would have failed the delivery check for omitting those digits), and
`renders_the_book_hash` (refused before any model call). The QA check-file
example stops teaching the recital shape it was written under.
@atomchung

Copy link
Copy Markdown
Owner Author

Implementer findings recorded for follow-up (none block this merge)

  1. may_state_total on the UX receipt — after the split, the number the owner's comprehension verdict wants is "how much was available and not said". Touches the evaluation_presented allowed-field set, verify, qa/receipts.py VERDICT_KEYS, references/ux-receipt.md, docs/qa-runbook.md — a coherent change of its own.
  2. Bloat has no deterministic gatedischarges_every_available_fact is factually complete and only the model judge's decision_focus axis catches it. If bloat should be caught rather than witnessed, that is a fifth judge axis or the demoted reading-budget rule from [design·M1] Owner-live: answers exceed the reading budget — verbosity is the next usefulness bottleneck after #827 #830's first design — an explicit owner call, deliberately not made here.
  3. TA-001's two pre-[design·M1] Owner-live: answers exceed the reading budget — verbosity is the next usefulness bottleneck after #827 #830 "all axes pass" witnesses still recite the concentration family and the unchecked list — written under the whitelist, still labelled passing, now inconsistent with the shipped answer shape without being wrong. Decide whether to re-cut them.
  4. check_voice.py classify_failure is English-keyword-only — no zh-TW answer can be classified by the voice oracle, which is why the acceptance templates live in the expression witnesses instead.

Owner picks which of these become issues.

@atomchung
atomchung merged commit 98a3c00 into main Aug 20, 2026
5 checks passed
@atomchung

Copy link
Copy Markdown
Owner Author

Post-merge live measurement (sanitized): deletion landed, length did not move

Re-ran the same four frozen scenes against merged main 98a3c00, same model and inputs as the #827 A/B. Two findings:

Composition: fixed. No basis recital, no rendered machine anchor, disclosures collapsed to one end block, the concentration family appears only when it carries the argument (present in the cash-deployment scene where it IS the thesis; fully silent in the no-book scene), answers open on stance + deciding reason, and the cash-deployment conversation's product-side total dropped ~49% (faster convergence, fewer rounds).

Length: unchanged. First-answer character counts sit within ±5% of the pre-trim arm in all four scenes (~1.5k–2.7k zh chars vs the ~300-char acceptance templates, which pass as witnesses but do not attract the live skill). The obligation whitelist was worth only a few hundred chars per answer; the model refills the space with discretionary elaboration. This is direct evidence for the open owner call in this PR's follow-up comment (item 2): bloat has no deterministic gate, and prose norms alone do not converge length. If the owner wants the template-scale answers, the demoted reading-budget rule (first-screen self-sufficiency + delete-test per block + a deterministic oracle) is the remaining lever.

Also: #829 reproduced exactly as scoped — the discovery scene again persisted its own unselected recommendation twice (-size and budget-size rows, both open, self-disclosed in-answer), while the selection scene stayed exact-1 (4 ephemeral probes all appended: 0, one canonical row for the user-selected candidate, verified in the trace).

atomchung added a commit that referenced this pull request Aug 22, 2026
… layer, per-surface derivation additive-only (closes #832) (#833)

* docs(expression): add the answer pyramid as the mother law of how this product speaks

#830 deleted the obligation whitelist and its post-merge rerun (PR #831) moved
first-answer length by less than 5% in all four frozen scenes. Deleting
obligations vacates space; nothing positive said what an answer *is*, so the
space refilled with discretionary elaboration. Meanwhile the answer-first
principle existed five times, written five different ways, which is drift by
construction.

expression-contract.md gains section 3, the one statement of the shape every
user-visible answer takes: one-sentence answer on top, an increment-gated
middle (delete a block; if the decision does not change, delete it), the rest
of the computed inventory behind a single offer, one caliber block at the end,
and one paragraph of voice. Four named bans are encoded with the slugs the
exemplar corpus references them by. Derivation by a surface is additive-only
and an empty derivation is the default.

V, D and C are frozen for shape and length: no new ID for "answers are too
long" or "lead with X", because a sixth phrasing with an ID on it is still a
sixth. V1 keeps its ID as the failure class its fixtures and cross-host
rulings cite, marks its own definition superseded, and routes its shape half
to the mother chapter. V10 stays unallocated.

No character-count cap: #543's ceiling was deleted by #827 and stays deleted.
Length is the shape's consequence, not its rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(surfaces): replace six local answer-first phrasings with one derivation each

Every surface that stated the answer shape in its own words now derives it
from the mother chapter and keeps only what it genuinely adds:

- the review card is the document incarnation — keynote = top floor, the three
  middle blocks = the middle, the Block-1 footnote = the end block. Its
  structure does not change; the derivation is recorded so it stops reading as
  an independent statement of answer-first.
- `consider` adds exactly two parameters: which fact wins the top sentence
  (lead selection) and which blocks the middle floor may hold (answer slots).
  Its reader's-question-chain section, the sixth phrasing the audit found,
  becomes that derivation.
- no recorded book adds two: with no book the top sentence is a
  research-backed baseline, and the strategy-class map is a middle-floor block
  set.
- the weekly market read adds one: its optional question comes after the
  complete brief.
- freeform answers add nothing, and say so. An empty derivation is valid and
  is the expected case; text-first is a latency default, never a shape.
- SKILL.md keeps the shape inline because it is always loaded, and is labelled
  as the mother chapter's projection rather than a second wording. It lands at
  8,512 bytes, below its previous 8,522, and the always-loaded pair stays
  inside its budget.

A funding shortfall now outranks every other lead candidate on `consider`
(#778): a negative post-trade cash balance is not a portfolio consequence, it
says the trade cannot be done out of the recorded book. The two numbers are
support; the decision they imply is the answer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(expression): make the exemplars the spec, and wire the oracle to them

The binding statement of the answer pyramid is the corpus, not the prose.
`tests/agent/expression-witnesses.json` becomes schema 2: 14 canonical
exemplars across the four conversational surfaces (three to five each, gated),
each declaring the one-sentence answer it leads with and the increment every
one of its blocks adds. The owner-approved acceptance templates from #830 are
three of them; every issuer is fictional (WDGT, GRDC, FABR, ACME) and nothing
is derived from a user record.

Two new assertions, both honest about their half:

- E-7 fails a scene whose declared core is not in its opening block. Its
  negative witness is a complete, anchored, obligation-discharging answer to
  the same call template 1 answers, which never says which candidate to buy.
- E-8 fails a scene whose declared blocks are not all present in order, or
  which declares no increment for a block, or the same increment twice. Its
  negative witnesses are the manufactured all-in-one-name simulation (a block
  with no increment) and a closing summary that is the opening judgment in a
  second form.

The two bans nothing mechanical reaches — a system default explained as
insight, and a hedging couplet — get `counter` scenes that must PASS every
assertion. The coverage boundary is asserted rather than promised: the day an
oracle can catch one of them, that scene is what says so.

#778's delivered answer joins as a second E-7 witness: the two cash numbers
stated, the funding decision never, the opening spent on the boundary and
hedging.

`tests/test_expression_contract.py` gains the grep-checkable acceptance —
zero retired answer-first phrasings remain, every surface declares a
derivation, every named ban is defined in the mother chapter, the registry
freeze is recorded in both the contract and the maintainer route, and no
character-count cap came back.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(no-book): pin what the retired phrasing protected, not the phrasing

`test_research_priors.py` asserted the literal sentence "lead with the bounded
value already supported", which is one of the six answer-first phrasings #832
replaces with a derivation. Restoring the sentence would defeat the change;
dropping the assertion would lose a real protection (#597/#598: the user sees
the bounded value before any intake question).

So it pins the protection harder instead. The route's own block order must run
baseline -> strategy-class map -> question, in that order — the question being
last is now asserted rather than implied — and the section must declare itself
a derivation of the shape rather than a second statement of it. The numeric
question-cap regression still reddens.

Recorded on the #832 mirrored-surfaces row, since a test that pins prose by
literal is exactly the kind of reader a shape change has to carry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: test <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant