21 KiB
Preflight: state_change.unary_delta Implementation Map
Status: Design complete; implementation remains future work
Date: 2026-06-21
Controlling authorization:
docs/sessions/unary-state-change-delta-authorization-2026-06-21.md
Target future PR: feat(kernel): introduce diagnostic unary-delta proposal seam
1. Preflight result
The implementation is feasible without a broad story parser, but only after
splitting state_change.transition into the narrower
state_change.unary_delta family.
The current substrate already carries the scalar, object, and exact
quantity_entity edge needed by the two seed positives. It does not carry an
exact typed action cue. The minimum necessary type addition is therefore one
family-local cue record, not a universal event IR.
No implementation is present in this documentation PR.
2. Current relevant code map
2.1 Descriptive family gate
generate/foundational_families.py
- contains exactly two current registry entries;
- marks
binding.quantity_entityimplemented/authorized; - keeps broad
state_change.transitionproposed andimplementation_authorized=False; - is descriptive metadata only and is not runtime dispatch.
Future action: add a third exact entry for state_change.unary_delta. Do not
authorize or repurpose state_change.transition.
2.2 Construction proposal catalog
generate/construction_affordances.py
ConstructionProposal.statusis structurally restricted to"proposed";- proposals cannot store missing roles or active hazards;
- all catalog entries are diagnostic-only and serving-disallowed;
propose_construction(family_id, evidence_spans)has hypothesis-only inputs;make_proposal()rejects all current catalog families and must remain a loud legacy fence.
Future action: add _UNARY_DELTA_FAMILY, catalog it, and include it in the
proposal-first set. Do not change proposal schema or make_proposal().
2.3 ProblemFrame types
generate/problem_frame.py
- carries exact mentions, bindings, bound relations, proposals, and quantity-kind dispositions;
- has no event/action cue record;
- has no unary-delta-specific state;
QuantityKindDispositionis family-local to quantity/entity and is not available for state-change confusers after #853.
Future action: add one family-local GroundedUnaryDeltaCue and a
ProblemFrame.unary_delta_cues tuple with a matching builder method. Do not
widen MentionKind to a generic event/action universe.
2.4 Frame builder
generate/problem_frame_builder.py::build_problem_frame
Current order:
scalars / units / hazards / process-frame candidates
→ proposal-first construction proposals
→ mentions / bindings / quantity disposition / bound relations / target
→ initial ProblemFrame
→ ContractAssessment dispatch
Relevant existing facts:
Ana gained 3 marbles.produces oneconsumptionprocess-frame candidate, one exact scalar mention, one exact object mention, and one exactquantity_entitybinding; it produces no proposal or bound relation.The jar lost 2 cookies.has the same topology._quantity_entity_proposals()intentionally refuses any active process frame, so the state-change positive has no quantity-family proposal._quantity_kind_dispositions()now requires exactly onebinding.quantity_entityproposal. This is the #853 hardening boundary and must not be widened._TRANSFER_REseparately constructs atransferrelation for a narrowgavesurface. It must not be reused as unary state change.
Future action: add one cue-proposal helper before mention binding, then publish the exact cue and unary relation after mentions/bindings exist.
2.5 Contract assessment
generate/problem_frame_contracts.py
assess_contracts()dispatches current catalog contracts only from an existing family proposal;- dedicated assessment functions hold family-specific closure logic;
ContractAssessmentis the sole runnable/refused authority;- output ordering is deterministic by
candidate_organ.
Future action: register unary_delta_transition, implement
assess_unary_delta(frame), and dispatch only when the exact proposal exists.
2.6 Process-frame candidates
generate/process_frames.py
- the broad
consumptioncandidate frame includes bothgain*andlose*; - its own
not_licenseddeclarations forbid deciding gain/loss without exact surface evidence and forbid computing remaining quantities; - it is candidate context, not a grounded event role or contract.
Future action: allow the existing consumption candidate to coexist, but do
not treat it as cue, proposal, relation, or closure authority.
2.7 Legacy semantic-state derivation path
generate/derivation/state/*
- already models
set,gain, andlosstransitions for accumulation; - parses raw clauses independently of
ProblemFrame; - stores numeric values as float-backed
SemanticQuantityrather than exactGroundedScalarfractions; - permits loose pronoun continuation;
- replays to
GroundedDerivationcandidates.
Future action: none. The new diagnostic family must not import, call, wrap,
compare against, or emit SemanticLedger, StateTransition, or
GroundedDerivation.
2.8 Callers and consumers
The import/call-site sweep found:
build_problem_frame()andassess_contracts()are consumed by focused tests plusscripts/gsm8k_problem_frame_adequacy.pyandscripts/gsm8k_substrate_morphology.py;- no
calibration/module directly consumes this seam; - no serving module directly imports
problem_frame_contracts; - current proposal-first tests enforce proposal ordering, exact spans, diagnostic posture, and proposal-gated dispatch.
The future PR must preserve these callers and must not add a serving import.
3. Required type addition
Add only this family-local shape in generate/problem_frame.py:
UnaryDeltaDirection = Literal["increase", "decrease"]
UnaryDeltaActionKind = Literal["gain", "loss"]
@dataclass(frozen=True, slots=True)
class GroundedUnaryDeltaCue:
cue_id: str
surface: str
action_kind: UnaryDeltaActionKind
direction: UnaryDeltaDirection
span: SourceSpan
Construction obligations:
surfacemust equalspan.text;gainedmust map only toaction_kind="gain",direction="increase";lostmust map only toaction_kind="loss",direction="decrease";- no other cue value is constructible;
- the frame carries cues as
tuple[GroundedUnaryDeltaCue, ...]in stable source order.
This type is intentionally not:
- a generic
EventMention; - a verb parse tree;
- an actor/source/target model;
- a temporal graph;
- a
SemanticLedgertransition; - an arithmetic operator.
Do not add a new SubstrateFact variant or widen MentionKind in the first PR.
4. Closed cue inventory and recognition
The future builder owns one immutable mapping:
gained → (gain, increase)
lost → (loss, decrease)
Recognition rules:
- enumerate exact lexical-boundary occurrences using the existing lexical boundary helper style;
- zero hits: no proposal;
- more than one hit, including repeated identical cues: no proposal;
- one hit: use only its exact
SourceSpanto create the proposal; - do not capture surrounding grammar or synthesize an event chunk;
- block proposal creation when an explicit competing surface is already visible: percent, rate/per/each, comparison, transfer, transaction, containment, list coordination, or multiple event cues;
- the only allowed process-frame set is empty or exactly
{"consumption"}; the two authorized cues currently produceconsumption.
The proposal helper must be a closed lexical recognizer, not a broad regex with verb, subject, object, source, target, and clause capture groups.
5. Required frame publication path
The future build order is fixed:
1. Existing scalar/unit/hazard/process-frame extraction
2. Detect exactly one authorized cue span
3. ConstructionProposal(status="proposed") from cue span only
4. Existing mention and MentionBinding extraction
5. GroundedUnaryDeltaCue from the proposal-backed exact cue
6. BoundRelation(relation_type="unary_delta") from cue + existing binding
7. ProblemFrame publication
8. Proposal-gated ContractAssessment
The unary relation uses existing BoundRelation / BoundRole:
| Bound role | Target |
|---|---|
action_cue |
GroundedUnaryDeltaCue.cue_id, kind unary_delta_cue |
delta_quantity |
selected quantity GroundedMention.mention_id |
changed_object |
selected object GroundedMention.mention_id |
direction is an explicit field of the exact cue. The construction catalog
still declares direction as a required semantic obligation; assessment checks
the cue's typed disposition.
Relation publication requirements:
- exactly one
quantity_entitybinding; - its source resolves to exactly one source-grounded scalar mention;
- its target resolves to exactly one object mention;
- cue precedes quantity, which precedes object;
- no
.,?, or!boundary lies between the three spans; - relation evidence is the ordered tuple of exact cue, quantity, and object spans;
- no actor, owner, source, target, before, after, container, or temporal role is synthesized.
If any requirement fails, publish no bound relation. The proposal may still be assessed and refused from the missing evidence.
6. Construction catalog addition
The future _UNARY_DELTA_FAMILY must declare:
family_id: state_change.unary_delta
relation_type: unary_delta
candidate_organ: unary_delta_transition
required roles: action_cue, changed_object, delta_quantity, direction,
local_binding_relation, local_event_containment,
provenance_span
optional roles: actor
diagnostic_only: True
serving_allowed: False
unit, source, target, before_quantity, after_quantity, container,
and temporal_marker stay absent from the first catalog signature. Listing
them as optional would imply the first relation can lawfully carry those
states; it cannot. They remain future-family roles, not optional SCT-1 data.
7. Contract assessment path
Add:
def assess_unary_delta(frame: ProblemFrame) -> ContractAssessment:
...
Assessment order:
- require exactly one
state_change.unary_deltaproposal; - require exactly one
GroundedUnaryDeltaCuecontained by proposal evidence; - require exactly one
BoundRelation(relation_type="unary_delta"); - require exactly one action, delta, and changed-object role target;
- prove delta mention → scalar provenance;
- prove the exact
quantity_entityedge joins the selected mentions; - prove typed cue/action/direction consistency;
- prove local ordering and sentence containment;
- prove every proposal, cue, role, binding, and relation span is an exact
problem_textslice; - reject unit/object collisions and all active competing contexts;
- report
runnable=Trueonly if both blocker and hazard sets are empty.
The function does not calculate a signed number. It does not inspect a question target. It does not produce a target value. Its meaning is only “this local unary-delta construction is role-complete.”
8. Refusal code placement
missing_bindings
unary_delta_proposal_requiredaction_cue_unboundaction_cue_ambiguousdelta_quantity_unbounddelta_quantity_ambiguouschanged_object_unboundchanged_object_ambiguouslocal_binding_relation_unboundlocal_binding_relation_ambiguouslocal_event_containment_unprovendelta_direction_unboundunit_object_conflictprovenance_span_inexact
unresolved_hazards
multiple_actor_surfacedelta_direction_ambiguoussource_target_unlicensedpronoun_antecedent_unresolvedcross_sentence_eventimplicit_total_unlicensedcomparative_contextrate_contextpercent_contextpassive_voice_unsupportedtemporal_context_ambiguouslist_context_ambiguoustransfer_unlicensedmultiple_state_change_cueschained_state_changesevent_assertion_unlicensed
The implementation may use a strict subset on naturally unreachable no-proposal paths, but direct assessment of a deliberately malformed frame must remain fail-closed and legible.
9. Relationship to current quantity disposition
Do not reuse or widen QuantityKindDisposition in SCT-1.
Reason: #853 made that disposition valid only when an exact
binding.quantity_entity proposal exists. State-change text deliberately has a
process-frame context, so the quantity family does not propose. Making its
disposition generic again would recreate the authority leak #853 removed.
SCT-1 instead closes only a family-specific count-valued delta relation from its own proposal plus the exact existing scalar/object binding. Measurement support is deferred.
10. Required future code files
The bounded implementation is expected to touch only:
generate/foundational_families.pygenerate/construction_affordances.pygenerate/problem_frame.pygenerate/problem_frame_builder.pygenerate/problem_frame_contracts.py- focused tests listed below
Do not touch:
generate/derivation/*generate/math_candidate_graph.pyevals/*or reports- runtime/serving paths
- teaching/proposals, packs, policy, identity, recall, Vault, field, algebra,
sealed artifacts, or
report.json
11. Likely test files
Extend:
tests/test_foundational_families.pytests/test_construction_affordances.pytests/test_construction_proposal_seam.pytests/test_problem_frame_builder.pytests/test_problem_frame_contracts.pytests/test_quantity_entity_proposal.pytests/test_proportional_decrease_proposal.pytests/test_percent_partition_proposal.py
Add:
tests/test_unary_delta_proposal.py
No eval fixture or report file is required.
12. Exact future tests
Type and catalog
- only
gainedandlostconstructGroundedUnaryDeltaCue; - cue action kind/direction pairs cannot be mismatched;
- catalog entry is diagnostic-only and serving-disallowed;
- broad
state_change.transitionstays implementation-unauthorized; state_change.unary_deltais a distinct foundational entry;ConstructionProposalremains proposed-only.
Ordering and authority
- proposal is published before cue binding/relation assessment;
- proposal-free frame does not dispatch
unary_delta_transition; - positive proposal remains
status="proposed"after runnable assessment; - legacy
make_proposal()is never used; - direct malformed-frame assessment refuses with stable codes.
Exact evidence
- every proposal/cue/relation/assessment span equals its source slice;
- synthetic proposal span refuses;
- synthetic cue span refuses;
- mismatched relation role target refuses;
- cue/quantity/object crossing a sentence boundary refuses;
- repeated build produces identical cue, proposal, relation, and assessment.
Positive fixtures
Ana gained 3 marbles.is diagnostically runnable;The jar lost 2 cookies.is diagnostically runnable;The team gained 4 points.is diagnostically runnable;- no positive produces an answer, target, serving flag, or derivation candidate.
Negative/confuser fixtures
- every table entry in authorization §17 has the specified no-proposal or refusal disposition;
Ana gained apples.refusesdelta_quantity_unbound;Ana gained 3.refuseschanged_object_unbound;3 apples were gained by Ana.refuses passive/order topology;Ana gained 3 pounds.refuses unit/object conflict;Ana gained 3 apples and lost 2.has no proposal;Tom gave Ana 3 apples.still has no quantity disposition and no unary proposal;- plain
There are 12 apples.keeps its quantity proposal and gains no unary assessment; - current proportional-decrease and percent-partition positives remain unchanged.
Structural firewall
- an AST/import guard proves the new ProblemFrame/catalog/contract path does
not import
generate.derivation.state,generate.derivation.accumulate,generate.derivation.pool, orgenerate.derivation.verify; - no new raw-prose parser appears under
generate/derivation/; - the existing no-new-legacy guard remains green.
13. Manual probe plan
For every seed/confuser, print only:
process frame names
proposal family IDs + exact evidence spans
unary cue records
mention IDs/kinds/spans
quantity_entity bindings
unary bound roles + spans
ContractAssessment organ/runnable/blockers/hazards
Required assertions:
- positives: exactly one unary proposal, cue, relation, and assessment;
- no-proposal confusers: no unary cue authority, relation, or assessment;
- proposed/refused cases: proposal remains proposed and blockers are exact;
- no case produces a quantity-kind disposition unless the independent quantity-family proposal exists;
- no case touches or compares against legacy semantic-ledger output.
14. Validation plan
Run targeted tests first:
uv run python -m pytest -q \
tests/test_foundational_families.py \
tests/test_construction_affordances.py \
tests/test_construction_proposal_seam.py \
tests/test_problem_frame_builder.py \
tests/test_problem_frame_contracts.py \
tests/test_quantity_entity_proposal.py \
tests/test_proportional_decrease_proposal.py \
tests/test_percent_partition_proposal.py \
tests/test_unary_delta_proposal.py \
tests/test_kernel_no_new_legacy_derivation_surfaces.py
uv run python -m core.cli test --suite smoke -q
uv run python -m compileall -q generate tests
uv run ruff check \
generate/foundational_families.py \
generate/construction_affordances.py \
generate/problem_frame.py \
generate/problem_frame_builder.py \
generate/problem_frame_contracts.py \
tests/test_foundational_families.py \
tests/test_construction_affordances.py \
tests/test_construction_proposal_seam.py \
tests/test_problem_frame_builder.py \
tests/test_problem_frame_contracts.py \
tests/test_quantity_entity_proposal.py \
tests/test_proportional_decrease_proposal.py \
tests/test_percent_partition_proposal.py \
tests/test_unary_delta_proposal.py
git diff --check
Run the canonical train and holdout serving probes without writing reports. Both must retain:
wrong_ids == []
The PR must state any correct/refused count movement observed live, but it may not update governed report artifacts.
15. Implementation stop conditions
Stop immediately if implementation requires:
- actor, owner, source, target, pronoun, or temporal resolution;
- more than the exact
gained/lostcue inventory; - a generic action/event parser;
- widening quantity-entity proposal or disposition generation;
- reusing
transferas unary delta; - importing or invoking the semantic-state derivation ledger;
- synthetic spans or nearest-mention selection;
- question-target or arithmetic logic;
- serving, report, eval fixture, teaching, memory, policy, identity, recall, Vault, field, or algebra changes;
- a non-empty wrong-ID list.
If exact local topology cannot make both seed positives runnable under these constraints, return to design. Do not widen the parser to make fixtures pass.
16. Copy-paste future implementation brief
PR title:
feat(kernel): introduce diagnostic unary-delta proposal seamDependency: Begin only after
docs(kernel): authorize unary state-change delta slicemerges. Create a fresh worktree from currentorigin/main; runsource scripts/agent_startup.sh; prove the authorization and preflight documents are in the base.Implement only
state_change.unary_delta, not broadstate_change.transition. Add a family-local frozenGroundedUnaryDeltaCuewith the closed exact cue mappinggained → gain/increase,lost → loss/decrease. CreateConstructionProposal(status="proposed", diagnostic_only=True, serving_allowed=False)from the exact cue span before mention/relation binding. Reuse the existing exactGroundedScalar,GroundedMention, andMentionBinding(binding_type="quantity_entity")to publish oneBoundRelation(relation_type="unary_delta")with action cue, delta quantity, and changed object roles. Add proposal-gatedassess_unary_delta(frame)as the sole runnable/refused authority.Keep the first slice count-only, single-cue, single-scalar, single-object, and single-sentence. Do not assert actor/ownership, source/target, before/after values, containment, transfer, arithmetic, answers, or serving. Do not widen
_quantity_entity_proposals()or_quantity_kind_dispositions(). Do not import or callgenerate.derivation.state. Use the exact refusal taxonomy, tests, manual probes, validation commands, and stop conditions in this preflight. Preserve every current proposal-first family and train/holdoutwrong_ids == [].