# Semantics (cp-v1.0)

This file defines what the kernel computes. Where it differs from a paper, this file wins, and the oracle in `src/qualneg/oracle/` implements the same definitions independently.

## 1. Objects

- **Attribute**: id, type (`boolean, categorical, integer, decimal, money, duration, timestamp, interval, set`), optional finite `domain`. Money is integer minor units with a declared currency (a written amount may use a `k`/`K` thousand or `M` million suffix: "$595k", "1.2M EUR"); duration is integer seconds; timestamp is integer UTC epoch seconds (an ISO string needs an offset; one without is read in the attribute's declared `timezone` when it has one, and rejected otherwise); decimal has a declared precision. No currency conversion without an explicit dated rate; none is implemented.
- **Optional attribute fields** (compiler-side; the kernel ignores them): `description`; for money, `price_basis` (`per_person, per_child, per_vehicle, per_group, per_hour, per_night, per_day, per_class, per_week, per_month, per_year, per_item, total`) and `price_statistic` (`exact` default, or `from`: the attribute holds starting-at prices); `is_primary_price`; `applies_to` (candidate kinds or party ids, carried, not enforced); `role` (`column | condition`, descriptive). Unset, they are omitted from dumps and hashes. Definitions in sift-ts factor shape (`name`, `format`, `options`, `range_unit`) are normalized by `schemas/attributes.py` `make_attribute_def`; a definition no type fits is reported (`attribute_unsupported`), not guessed.
- **Quoted facts**: a fact may carry the `quote` it was read from. A numeric value its quote's numbers cannot reach (directly, ×10ⁿ, a unit factor, or a ratio/product of two stated numbers) stays `unknown` (`quote_contradicts_value`). A money value stays `unknown` when the quoted figure is on another basis than `price_basis` (`price_basis_mismatch`; unless the same quote states a figure on the declared basis, or the bundle size to divide by), is a "from" figure or a band and the attribute is not `from` (`price_lower_bound`, `price_range`), is a deposit, fee, surcharge or tax under a price attribute (`not_a_price`), is in another currency (`price_foreign_currency`), or is a 0 the quote does not call free (`price_unevidenced_zero`). Read deterministically by `compile/price_basis.py` and `compile/quote_evidence.py`; no ratio the source does not state is applied.
- **Text facts vs. the table**: a fact read from free text never overrides or conflicts a known cell the request supplied; the supplied value is kept and the text's value is reported (`text_fact_ignored`, info). Supplied structured facts that disagree still make a `conflicted` cell. A fact stated in evidence fills a cell that is missing or explicitly `unknown` (`fact_filled`, info): that is how an answer to a "Can't tell" question lands.
- **Per-party quantities**: an attribute may carry `belongs_to` (a party id) and `measure` (a name shared by one such attribute per party). Only declared selection rules (§7) read them; feasibility and dominance ignore the tags.
- **Named preferences**: a party may name an outcome it hopes for (`{name, predicate, label}`). Only declared selection rules read them; they never make a candidate infeasible or dominated, and never rank anything by themselves.
- **Cell**: `known(value)`, `unknown`, `conflicted(claims)`, `not_applicable`. Zero, false, empty set, unknown, and not applicable are distinct. A conflicted cell keeps every claim.
- **Candidate**: a complete proposed joint outcome: id, cells per attribute, `is_outside_option`, origin (`supplied, generated, discovered`).
- **Party model**: a list of rules. Rules never cross parties.
- **Rule status**: `accepted | hypothesis | superseded | conflicted | unsupported`. Only accepted rules act under the default `explicit_only` policy. Hypotheses act only under `include_attributed_inferences`, and every conclusion is then labelled conditional on them.

## 2. Predicates

A typed AST: `const`, `all`, `any`, `not`, and attribute comparisons `eq, ne, lt, le, gt, ge, in, not_in, contains, subset_of, superset_of, within, overlaps, before, after, is_known`. Evaluation is three-valued:

- `TRUE` / `FALSE` when every referenced cell is known.
- `UNRESOLVED(reason)` when a referenced cell is unknown, conflicted, or not applicable, or the operator does not apply to the type. `all` is FALSE if any argument is FALSE, else UNRESOLVED if any is, else TRUE; `any` is symmetric. Missing is never false. On a set-valued attribute, `in` means the set contains at least one listed value and `not_in` means it contains none of them.

Literals are normalized per operator when a rule is validated; a literal that cannot be read makes the rule `unsupported` with a diagnostic (never an accepted rule unresolved on every candidate):

- `in` / `not_in`: a list of values; a single scalar is a one-value list (a string is never split into characters).
- `within`, `overlaps`: an interval literal, `{start, end}`, `[start, end]`, or `"start/end"`. On a timestamp attribute, `within` is `start ≤ t ≤ end` and `overlaps` is `start ≤ t < end`; on an interval, `within` is containment and `overlaps` is a non-empty intersection.
- `before` / `after`: a timestamp (inclusive: `t ≤ x`, `t ≥ x`), or an interval literal (before its start, after its end). On an interval attribute `before` tests its end and `after` its start.
- **Clock time of day**: `{"time_of_day": "22:00", "tz": "America/Chicago"}` with `before`/`after` (or `le`/`lt`/`ge`/`gt`) on a timestamp or interval attribute compares the candidate's local clock time, in `tz` (an IANA zone or an offset like `-05:00`; default the attribute's `timezone`; neither is a validation problem), against the limit on the candidate's own date. `before`/`le` are inclusive. On an interval, `before` tests the end (an end on a later local day than the start counts as past any clock limit) and `after` tests the start.
- **Day of the week**: `{"weekday": ["tue", "wed", "thu"], "tz": "America/New_York"}` with `in` / `eq` (the candidate's local day is one of them) or `not_in` / `ne` (it is none of them) on a timestamp or interval attribute. Days are names (`mon` … `sun`, full names accepted) or ISO numbers (1 = Monday). `tz` defaults to the attribute's `timezone`; neither is a validation problem. On an interval, every local day it touches counts: `in` needs all of them listed, `not_in` none of them (an interval ending exactly at midnight does not touch the next day). The text compiler writes it for "only Tuesday to Thursday" and "no weekends", completing a missing zone with the options' offset.
- Timestamp strings without an offset are read in the attribute's declared `timezone`, else rejected. (The text compiler first completes a literal the reader wrote without an offset, on an attribute with no declared zone, with the offset the options are written in; a bare date in an interval literal runs from the start of its first day to the last second of its last day.)
- **Daily clock window** (text compiler): `within` / `overlaps` with `{"start_time_of_day": "09:00", "end_time_of_day": "16:00", "tz": ...}` is compiled to clock-time comparisons on each option's own date. On a timestamp, `within` is `after lo ∧ before hi` and `overlaps` is `after lo ∧ ¬after hi` (lo ≤ t < hi); on an interval, `within` is `after lo ∧ before hi` (starts no earlier, ends no later) and `overlaps` is `¬before lo ∧ ¬after hi` (ends after lo and starts before hi). A window across midnight joins its halves with `any` on a timestamp and is not read on an interval. A list of interval literals (or windows) under `within` / `overlaps` means any one of them.

## 3. Feasibility

Structural rules (constraints with `structural: true`) define what counts as a meaningful outcome. The compiler lifts them out of whichever party supplied them into the problem's `structural_rules`, so they apply to every party and to flip-search states; the supplying party is kept in the diagnostic and the rule's provenance.

Let the active constraints be the accepted structural rules plus each required party's accepted hard constraints (and its hypotheses under the inclusive policy). A candidate is:

- `known_infeasible` if any active constraint whose `when` holds evaluates FALSE (witness: rule, proof). A constraint whose `when` is UNRESOLVED (its condition rests on an unknown or conflicted fact) counts as UNRESOLVED unless its predicate is TRUE anyway;
- `unresolved` if none is FALSE and at least one is UNRESOLVED (witness carries the reason; an evidence request names the missing fact);
- `known_feasible` otherwise.

An empty feasible set with unresolved candidates is `no_known_feasible_candidate`, not a proof that nothing fits.

## 4. A party's comparison of two candidates

Fix a party `i`. Its **relevant attributes** `R_i` are the attributes its accepted preference rules order (`value_pref`, `monotone`, `indifference`, `importance`, `outcome_comparison`) plus the attributes those rules' contexts mention (a parent is part of the state), minus any attribute an `irrelevance` rule names that no context needs. An explicitly irrelevant attribute that a context needs stays in the state, and every change to it is an indifferent move. Being unmentioned does not remove an attribute.

For candidates `x`, `y`, in order:

1. If the party's model is **inconsistent** (section 6), the relation is `inconsistent_model`.
2. If `x` and `y` have known, equal values on every attribute (not-applicable on both counts as equal), they are the same outcome: `indifferent` (proof `identical_assignment`).
2b. If every attribute on which they differ (or is not known on both) is one the party explicitly declared irrelevant, they are equivalent for this party: `indifferent` (proof `differ_only_on_irrelevant`). This holds even for a party with no preference rules.
3. If the party has no preference rules: `undetermined(missing_evidence)`.
4. If any attribute in `R_i` is not known on either side: `undetermined(unknown_fact)`.
5. If `x` and `y` differ (or either is not known) on an attribute outside `R_i` that is not explicitly irrelevant: `undetermined(missing_evidence)`. **Silence is not indifference.**
6. If `x` and `y` agree on all of `R_i`: `indifferent` (proof `equal_on_relevant_attributes`).
7. Otherwise compare by the party's fragment.

### 4a. Product-order fragment

Applies when every preference rule is unconditional and there are no importance or outcome-comparison rules (a separable model). For each attribute `a` in `R_i` where the values differ, the unconditional rules induce an order on `a`'s domain: `value_pref` edges (better → worse, transitively closed), `monotone` (the numeric order on the modeled domain), `indifference` (an equivalence). The per-attribute relation is `better`, `worse`, `indifferent`, or `undetermined`.

`x` is `strictly_better` than `y` iff every differing attribute is `better` or `indifferent` and at least one is `better`; symmetric for `strictly_worse`; `indifferent` if all are indifferent; otherwise `undetermined(non_entailment)` (a tradeoff the rules do not settle, or a pair the rules do not order). This is the standard dominance result for unconditional CP-nets with indifference.

### 4b. Conditional fragment (bounded flip search)

Applies otherwise. States are assignments to `R_i` drawn from the **modeled domain**: the declared domain for boolean and categorical attributes; for numeric attributes the declared finite domain if any, otherwise the set of known values present in the candidate table (a documented abstraction: paths may pass through any observed value). States that violate an accepted structural constraint are excluded. Under `path_semantics = feasibility_restricted` the party's own hard constraints also exclude states; the default `structural_domain` does not.

From a state `s`, the allowed moves are:

- **Improving / indifferent flip** on attribute `a`: the rules for `a` whose `when` holds at `s` induce an order on `a`'s domain (as in 4a, in that context). Change `a` to a value that is `better` (strict edge) or `indifferent` (weak edge) than the current one. If the party has rules for `a` but none applies at `s`, `a` is **blocked** at `s`: no move. (One conditional statement makes every other context unspecified; say "otherwise" explicitly.)
- **Importance flip** for an accepted unconditional `importance(m, l)`: change `m` to a value better than its current value under the rules applicable at `s`, and change `l` to any other value, everything else fixed (a TCP-style tradeoff flip; strict edge). Conditional importance is outside the fragment and is excluded with a diagnostic.
- **Outcome comparison** `better ≻ worse`: if `s` agrees with `worse` on its keys and the rule's `when` holds, move to `s` with `better`'s keys applied (strict edge).

Let `reach(y)` be the set of states reachable from `y` by these moves, remembering whether some path uses a strict edge. Then:

- `x` reachable from `y` with a strict edge, and `y` not reachable from `x` with a strict edge: `strictly_better` (proof: the path).
- strict both ways: `inconsistent_model` for this pair, with both paths as the witness.
- reachable both ways without any strict edge: `indifferent`.
- if either search hit the state budget and no cycle was already witnessed: `undetermined(budget_exhausted)`. A truncated search cannot rule out a strict path back, so no determinate conclusion is drawn from it.
- otherwise: `undetermined(non_entailment)`.

Intermediate states need not be candidate rows (G03). One search per start state serves every pair with that endpoint and is cached by the party-model hash and the domain hash.

## 5. Group dominance and the returned set

Over the known-feasible candidates (plus the outside option when declared), for required parties `P`:

**`A` dominates `B` iff every party in `P` is proven `strictly_better` or `indifferent` for `A` over `B`, and at least one is proven `strictly_better`.** `undetermined` is neither.

The result's `known_nondominated` set is the known-feasible candidates for which no dominator was found. Each elimination carries a witness with every party's proof. The label is `known_nondominated_under_accepted_model`; the kernel never claims `necessarily_pareto_optimal`.

Two completeness flags are independent: `comparison_search_complete` (no budget cut) and `preferences_complete` (no undetermined pair, every party certified). With a budget cut the set is a conservative superset.

## 6. Consistency

A party's model is **certified** when: its conditional dependency graph (attribute → attributes its rules' contexts mention) is acyclic; every per-context value table is a strict partial order with no strict preference inside a declared indifference class; the importance graph is acyclic; no two outcome comparisons contradict each other. Acyclic CP-nets with locally consistent tables are satisfiable (Boutilier et al. 2004), so strict conclusions are sound. When outcome comparisons are present, cycles that run through them are detected per query (strict both ways).

A cyclic dependency graph, or a context count above the certification cap, yields `not_established`: conditional comparisons return `undetermined(consistency_not_established)` and candidates are preserved. A detected cycle yields `inconsistent` with the rule ids; no rule is removed. Repairs are data for a revision, never applied by the kernel.

## 7. Selection and acceptance

Selection acts only on `known_nondominated` and is always labelled procedural:

- `frontier_only` (default): nothing selected.
- `stable_id`: the lexicographically smallest id; `tie` is true when more than one survived.
- `party_priority`: one party's declared lexicographic keys (direction or value order); requires that data.
- `declared_rules`: an ordered list of steps the sides declared (in a session, before joining). Starting from the known-nondominated set sorted by id, each step computes a key per remaining candidate and keeps those with the best key; the next step sees only those. A candidate is selected only when one remains; if a tie survives every step nothing is selected and `selection.remaining` holds the tied set (`tie` is true). Steps (all deterministic; an unknown fact never helps a candidate):
  - `minimize_max` / `maximize_min` / `equalize` over a per-party measure (the attributes sharing `measure` with a `belongs_to` party, or listed `attributes`): the largest value (smaller is better), the smallest value (larger is better), the spread max − min (smaller is better). A candidate with any unknown value ranks after every candidate whose values are all known.
  - `fewest_violations`: the number of named preferences (of every required party, filtered by `name`, plus inline ones) that evaluate FALSE, then the number UNRESOLVED.
  - `priority_by_history`: a candidate's losers are the parties whose named preference `name` is not TRUE (or, with `measure`, the parties holding its largest value). Over `decision.history` from the most recent entry back, the key is (|losers ∩ lost|, −|losers ∩ won|) per entry, compared lexicographically. With no history every candidate ties.
  - `fewest_changes`: the number of attributes (listed, or every attribute in the previous outcome) on which the candidate differs from the most recent history entry's `selected_facts` (unknown counts as a change). With no previous outcome every candidate ties.
  - `prefer`: one attribute's direction or declared order (unknown or unlisted values last).
  - `party_priority`, `stable_id`: as above.
  Each step is recorded on the selection (`steps`: considered, kept, set aside, and the values compared). The kernel never invents a rule; the explanation names the step that decided.

Acceptance is never inferred. The text compiler reads acceptances one way whatever encoding the reader chose: "I'll take it if X" written as a `when` with no condition is the acceptance of X; "I won't accept X" (decision `reject`) is also the constraint ¬X; a constraint read from the same words as an acceptance that keeps only some of its alternatives is widened to all of them; and a constraint that merely repeats an acceptance with no numeric limit is dropped unless the words also require it ("only if", "unless", "or I walk", "must"). An acceptance alone never becomes a constraint: saying an option is enough is not refusing the others. A party's acceptance state for a candidate is `accepted` or `rejected` only when one of its acceptance rules evaluates TRUE; otherwise `unknown`. A proposal authorizes nothing.

## 8. Revisions

A revision names its base version and lists operations (`upsert_rule`, `supersede_rule`, `set_rule_status`, `set_fact`, `add_candidate`, `remove_candidate`, `add_evidence`, `set_decision`). Replaced rules are kept with status `superseded` and the reason. Newer evidence never overrides older evidence by itself. The new version has a new content hash; results are cached by content hash, comparisons by party-model and domain hash, so unchanged work survives and changed work cannot be served stale.

## 9. The relevance gate for memories

Evidence of kind `inferred_memory` or `memory_file` compiles to rules with basis `inferred`. The request's `memory_gate` says which memories must be about this matter to act:

| `memory_gate` | curated `inferred_memory` | raw `memory_file` |
|---|---|---|
| unset (the original behaviour; `/v1/decide`'s default) | accepted only when `matter_relevance` is `this_matter` | gated per line |
| `on` | accepted only when `this_matter` | gated per line |
| `auto` (the session default) | accepted unless labelled `prior_matter` (the agent chose to send it: it is the party's position) | gated per line |
| `off` | accepted unless labelled `prior_matter` | read whole, not gated |

A memory that does not pass compiles to hypotheses, visible in the record and inactive under `explicit_only`; its facts are not tabled.

The chunked text pipeline applies the same gate per line (and always runs the relevance filter over a gated `memory_file`, however short). Each line read carries a matter: the relevance model's label, `prior_matter` for a line tagged as an earlier occasion (`[old]`, `[months ago]`, `[last year]`, `(re: ...)`) whatever the model said, and `unknown` for a line only the deterministic safety net kept. The extraction model also labels each rule it reads (`this_matter` or `prior_matter`): `prior_matter` from either source wins, and a rule on an `unknown` line takes the extraction model's label when it gives one. A rule whose matter ends `prior_matter` or `unknown` compiles to a hypothesis (basis `inferred`), even when the rest of its chunk is about this matter; a fact quoted from a `prior_matter` line is not tabled.

## 10. What is outside the fragment

Conditional importance; importance between attributes that are themselves conditionally dependent; soft constraints; numerical tradeoffs of any kind; quantification over preference completions (`necessarily` / `possibly` claims); strategyproofness, equilibrium, or truthful-reporting assumptions. Inputs that need these are preserved with `unsupported` diagnostics.
