|
| 1 | +# Constraint Inventory (hard-constraint-contract discussion) |
| 2 | + |
| 3 | +Working research artifact for the hard-constraint-contract proposal: the |
| 4 | +rules Griff already enforces (and where), the rules the research memos |
| 5 | +proposed, and a classification of each under a closed taxonomy — so the |
| 6 | +future `RuleScope`/`RuleEvaluation` layer formalizes reality instead of |
| 7 | +inventing a parallel one, and so the MiniZinc oracle spike gets *real* |
| 8 | +problems instead of a demo toy. |
| 9 | + |
| 10 | +Status: for discussion (v1). Companion to |
| 11 | +[`hard-constraint-contract.md`](hard-constraint-contract.md); that |
| 12 | +proposal is the contract thesis, this file is the catalog. |
| 13 | +Scope: docs-only; binds nothing. |
| 14 | + |
| 15 | +**Acceptance gate (normative for this document).** The inventory |
| 16 | +*classifies rules*; it promises no production semantics, no API, and no |
| 17 | +delivery phase. Every `hard` classification still requires its own spec |
| 18 | +section, failure-code vocabulary, characterization tests over today's |
| 19 | +behaviour (SPEC hard rule 5 where behaviour already exists), and a |
| 20 | +red→green slice. A classification here is a licence to *specify*, never a |
| 21 | +decision already taken. |
| 22 | + |
| 23 | +## Classification taxonomy (closed — exactly four states) |
| 24 | + |
| 25 | +- **hard** — admissibility, evaluated in the rule layer |
| 26 | + (`RuleEvaluation`); any `Violated` rejects the candidate under the |
| 27 | + fail-closed aggregation policy of the contract proposal; |
| 28 | +- **soft** — preference; lives in scoring (`Axes`/`WeightPolicy`, |
| 29 | + ADR-0017) and never enters the rule layer; |
| 30 | +- **opt-in** — a scoped arrangement constraint the user explicitly turns |
| 31 | + on for a `RuleScope`; absent from the default rule set; |
| 32 | +- **defer** — blocked on a named prerequisite; no classification decision |
| 33 | + exists until that prerequisite does. |
| 34 | + |
| 35 | +Qualifiers live in the reason, never in the state. Rules that are hard |
| 36 | +*within one subsystem's contract* but not universal are recorded as hard |
| 37 | +with an explicit scope — scope is part of the rule, not a fifth state. |
| 38 | + |
| 39 | +## Survey baseline — what is enforced today |
| 40 | + |
| 41 | +- `core/src/fretboard.rs`: `infer_positions` (fingering DP, ADR-0019), |
| 42 | + `measure_playability` → `PlayabilityReport` — `is_playable()` is |
| 43 | + `unpositionable == 0`; `max_fret_jump` is a **carried fact, not a |
| 44 | + verdict**; chords are left unvoiced (deferred, ADR-0019 §7); |
| 45 | + `STANDARD_MAX_FRET = 24`. |
| 46 | +- `core/src/complement.rs`: `PairValidation` — `coincident_dissonances` |
| 47 | + (m2 / tritone / M7 on coincident onsets), `register_mud`, per-part |
| 48 | + playability; `is_clean()` requires all four. Doc comment states the |
| 49 | + house position explicitly: "jump thresholds are calibration data, not |
| 50 | + code". `ComplementError` is a typed-refusal vocabulary |
| 51 | + (`InvalidSpec`, `NoGapsToAnswer`, `NonUniformTimeline`, …). |
| 52 | +- `core/src/generate.rs`: `GenerationConstraints` — "hard constraints |
| 53 | + that all generated phrases must satisfy": `bar_count ≥ 1`, uniform |
| 54 | + time signature and tempo, `ticks_per_quarter > 0`, |
| 55 | + `pitch_lo ..= pitch_hi`. |
| 56 | +- `core/src/tonal.rs` (S15 Phase 1): tonal estimates are **evidence |
| 57 | + only** — scoped, optional, and by the accepted S15 contract they |
| 58 | + restrict no pitch and change no production behaviour. |
| 59 | +- `pattern/` + Swang seam: `ExpansionBudget` (typed refusal on breach), |
| 60 | + bar-geometry divisibility and tail policy (`SWG0301`/`SWG0302`), meter |
| 61 | + uniformity across a mapped span (`SWG0304`), `density` requires a seed |
| 62 | + (`SWG0303`). |
| 63 | +- DP layer (ADR-0013/0030): transition and local costs are **soft** by |
| 64 | + construction; feasibility is expressed as graph shape, not as cost ∞. |
| 65 | + |
| 66 | +## Summary |
| 67 | + |
| 68 | +| # | Rule | Dimension | State | Enforced today | |
| 69 | +| --- | --- | --- | --- | --- | |
| 70 | +| 1 | fretboard reachability (line notes) | Fretboard | **hard** | yes — `is_playable`, per-part filter | |
| 71 | +| 2 | chord voicing feasibility | Fretboard | **defer** | no — chords left unvoiced (ADR-0019 §7) | |
| 72 | +| 3 | max fret travel | Fretboard | **soft** | fact carried, never a verdict | |
| 73 | +| 4 | pitch range window | Pitch | **hard** | yes — `GenerationConstraints` | |
| 74 | +| 5 | string conflict (simultaneous notes, one string) | Fretboard | **hard** (monophonic scope) | implicit via monophonic lines | |
| 75 | +| 6 | coincident dissonance (pair) | Harmony × Complementarity | **hard** (complement scope) | yes — `PairValidation` | |
| 76 | +| 7 | register mud (pair) | Pitch × Complementarity | **hard** (complement scope) | yes — `PairValidation` | |
| 77 | +| 8 | strong-beat doubling (pair) | Complementarity | **opt-in** | no (decided in PR #149) | |
| 78 | +| 9 | timeline uniformity preconditions | Structure | **hard** (per-mode scope) | yes — typed `ComplementError`s | |
| 79 | +| 10 | bar-geometry / meter divisibility | Rhythm | **hard** | yes — `SWG0301/0302/0304` | |
| 80 | +| 11 | seed presence for stochastic ops | Structure | **hard** | yes — `SWG0303`, `PruneSpec` | |
| 81 | +| 12 | expansion / cycle budgets | Structure | **hard** | yes — `ExpansionBudget` | |
| 82 | +| 13 | tapping-transition reachability | Technique × Fretboard | **defer** | no — needs the technique evidence model | |
| 83 | +| 14 | harmonic-context membership | Harmony | **defer** | no — S15 forbids it by design today | |
| 84 | +| 15 | voice crossing (named rule) | Pitch × Complementarity | **opt-in** | partially shadowed by register mud | |
| 85 | + |
| 86 | +## Per-rule detail |
| 87 | + |
| 88 | +Field key: *Scope* is stated in `RuleScope` terms (tracks / voices / |
| 89 | +dimension / range / relation). *Lookback / lookahead* is what the rule |
| 90 | +must see beyond the event it judges. *Evidence* is what a `Violated` |
| 91 | +carries. *Incremental* — whether the rule can be re-evaluated per event |
| 92 | +for DP clients (ADR-0013/0030) without whole-candidate recomputation. |
| 93 | +*Oracle candidate* — suitability for the MiniZinc offline spike. |
| 94 | + |
| 95 | +### 1. Fretboard reachability — hard |
| 96 | + |
| 97 | +- **Statement**: every line note has at least one playable |
| 98 | + `(string, fret)` under the part's tuning within `max_fret`. |
| 99 | +- **Scope**: per track/voice; dimension Fretboard; whole line; relation: |
| 100 | + per-note. |
| 101 | +- **Today**: `PlayabilityReport.is_playable` (`unpositionable == 0`), |
| 102 | + used as the per-part filter (ADR-0019); typed loss report on import. |
| 103 | +- **Lookback / lookahead**: none — reachability of a pitch is per-note |
| 104 | + (path *quality* is rule 3's soft territory). |
| 105 | +- **Evidence**: the unpositionable note's musical path + pitch + tuning. |
| 106 | +- **Incremental**: yes (per note). |
| 107 | +- **Oracle candidate**: **yes — spike problem A.** The fingering DP is a |
| 108 | + heuristic over a real solution space; an exact model can (a) confirm |
| 109 | + UNSAT claims for unreachable lines and (b) hunt minimal |
| 110 | + counterexamples where the DP's greedy-ish path misses a feasible |
| 111 | + assignment under tighter travel bounds. |
| 112 | + |
| 113 | +### 2. Chord voicing feasibility — defer |
| 114 | + |
| 115 | +- **Statement**: simultaneous notes admit a non-conflicting joint |
| 116 | + `(string, fret)` assignment. |
| 117 | +- **Prerequisite (named)**: the chord-voicing model deferred by |
| 118 | + ADR-0019 §7 — today chords are deliberately left unvoiced rather than |
| 119 | + misvoiced, and a rule cannot precede its model. |
| 120 | +- **Note**: when it lands, this is the constraint-shaped problem par |
| 121 | + excellence (assignment + mutual exclusion) and an obvious later oracle |
| 122 | + target; it inherits rule 5 as a sub-constraint. |
| 123 | + |
| 124 | +### 3. Max fret travel — soft |
| 125 | + |
| 126 | +- **Statement**: consecutive-note fret jumps should stay small. |
| 127 | +- **Today**: `max_fret_jump` is a carried fact; the complement doc |
| 128 | + comment is the standing decision — "jump thresholds are calibration |
| 129 | + data, not code". |
| 130 | +- **Classification reason**: a threshold would be taste until calibrated |
| 131 | + against evidence (S9 territory); the fact is already stored, which is |
| 132 | + all the rule layer needs from it. Stays in scoring/weights. |
| 133 | + |
| 134 | +### 4. Pitch range window — hard |
| 135 | + |
| 136 | +- **Statement**: generated pitches lie in `pitch_lo ..= pitch_hi`. |
| 137 | +- **Scope**: per generation request; dimension Pitch; per-note. |
| 138 | +- **Today**: `GenerationConstraints` (documented as hard). |
| 139 | +- **Lookback / lookahead**: none. **Incremental**: yes. |
| 140 | +- **Evidence**: offending note + bounds. |
| 141 | +- **Oracle candidate**: no — trivially checkable; needs no solver. |
| 142 | + |
| 143 | +### 5. String conflict — hard (monophonic scope today) |
| 144 | + |
| 145 | +- **Statement**: two simultaneously sounding notes never occupy one |
| 146 | + string. |
| 147 | +- **Today**: implicit — the generator emits monophonic lines and chords |
| 148 | + are unvoiced, so the conflict cannot currently arise; nothing *checks* |
| 149 | + it. |
| 150 | +- **Classification reason**: hard now with the honest scope note; the |
| 151 | + full polyphonic rule activates with rule 2's model and must land as an |
| 152 | + explicit check, not remain an accident of monophony. |
| 153 | +- **Incremental**: yes (per onset group). |
| 154 | + |
| 155 | +### 6. Coincident dissonance — hard (complement scope) |
| 156 | + |
| 157 | +- **Statement**: no m2 / tritone / M7 between parts on coincident |
| 158 | + onsets, within the ComplementArranger's cleanliness contract. |
| 159 | +- **Scope**: tracks A×B; dimension Harmony × Complementarity; relation: |
| 160 | + simultaneous onsets. |
| 161 | +- **Today**: `PairValidation.coincident_dissonances`, part of |
| 162 | + `is_clean`. |
| 163 | +- **Classification reason**: hard *within the complement contract* — as |
| 164 | + a universal rule it would outlaw intentional tension, so its scope is |
| 165 | + the contract, not the repertoire. Widening it beyond complement would |
| 166 | + be a new opt-in rule, not a widening of this one. |
| 167 | +- **Lookback / lookahead**: the coincident onset pair only. |
| 168 | + **Incremental**: yes (per coincidence). |
| 169 | +- **Evidence**: onset, both pitches, interval class. |
| 170 | +- **Oracle candidate**: **yes — part of spike problem B** (below). |
| 171 | + |
| 172 | +### 7. Register mud — hard (complement scope) |
| 173 | + |
| 174 | +- **Statement**: parts A and B do not overlap registers so heavily that |
| 175 | + the pair loses separation. |
| 176 | +- **Today**: `PairValidation.register_mud`, part of `is_clean`. |
| 177 | +- **Note**: the boolean compresses a graded fact; when the rule layer |
| 178 | + formalizes it, the underlying overlap measurement should surface as |
| 179 | + evidence (same raw-fact-vs-verdict discipline as rule 3). |
| 180 | +- **Incremental**: windowed (needs both parts' registers over a span). |
| 181 | +- **Oracle candidate**: yes — as a register-window constraint in spike |
| 182 | + problem B. |
| 183 | + |
| 184 | +### 8. Strong-beat doubling — opt-in |
| 185 | + |
| 186 | +- **Statement**: the complement does not double the lead on strong |
| 187 | + beats. |
| 188 | +- **State source**: decided in the accepted PR #149 revision — a |
| 189 | + strong-beat unison is a legitimate arrangement device; the rule enters |
| 190 | + as an explicitly scoped opt-in, soft penalty by default. |
| 191 | +- **Today**: not implemented. **Incremental**: yes (per strong beat). |
| 192 | + |
| 193 | +### 9. Timeline uniformity preconditions — hard (per-mode scope) |
| 194 | + |
| 195 | +- **Statement**: certain arrangement modes require a uniform meter/span |
| 196 | + across the arranged range (e.g. `counter_melody`). |
| 197 | +- **Today**: typed refusals — `ComplementError::NonUniformTimeline`, |
| 198 | + `InvalidSpec`, `NoGapsToAnswer` — already model the contract |
| 199 | + proposal's `Unsupported`/`Violated` split in miniature. |
| 200 | +- **Classification reason**: these are problem-level preconditions, the |
| 201 | + easiest rules to port into `RuleEvaluation` because their vocabulary |
| 202 | + is already typed. |
| 203 | + |
| 204 | +### 10. Bar-geometry / meter divisibility — hard |
| 205 | + |
| 206 | +- **Statement**: a declared unit divides the bar exactly and is |
| 207 | + tick-representable (`SWG0301`); incomplete tails follow the declared |
| 208 | + tail policy (`SWG0302`); a mapped span keeps one meter (`SWG0304`). |
| 209 | +- **Today**: typed errors in the Swang seam; the master timeline is the |
| 210 | + single source of truth (SPEC hard rule 3). |
| 211 | +- **Incremental**: per span; cheap. |
| 212 | + |
| 213 | +### 11. Seed presence — hard |
| 214 | + |
| 215 | +- **Statement**: every stochastic operation names its seed; `density` |
| 216 | + without `seed` is refused (`SWG0303`); `PruneSpec` carries the rhythm |
| 217 | + seed by law. |
| 218 | +- **Classification reason**: determinism (SPEC hard rule 6) expressed as |
| 219 | + an admissibility rule on *requests*, not on musical content — the rule |
| 220 | + layer should keep that distinction visible. |
| 221 | + |
| 222 | +### 12. Expansion / cycle budgets — hard |
| 223 | + |
| 224 | +- **Statement**: expansion never exceeds its declared budget; breach is |
| 225 | + a typed refusal, never truncation. |
| 226 | +- **Today**: `ExpansionBudget` in `griff-pattern`; the operator |
| 227 | + inventory adds `CycleBudget` to the same family (Kani harnesses for |
| 228 | + the budget invariant are already a process-backlog item). |
| 229 | + |
| 230 | +### 13. Tapping-transition reachability — defer |
| 231 | + |
| 232 | +- **Statement**: technique transitions (tapping entries/exits) are |
| 233 | + physically reachable in time. |
| 234 | +- **Prerequisite (named)**: a technique evidence model over the rich |
| 235 | + note model (ADR-0018) that defines *what* a transition is and *when* |
| 236 | + reachability is measurable. Classifying it hard before that model |
| 237 | + exists would invent the model by side effect (the reset-on-event |
| 238 | + lesson from the operator inventory). |
| 239 | + |
| 240 | +### 14. Harmonic-context membership — defer |
| 241 | + |
| 242 | +- **Statement**: notes lie within a scoped harmonic context. |
| 243 | +- **State source**: the accepted S15 contract — context is optional |
| 244 | + evidence with **no pitch restriction and no production behaviour |
| 245 | + change**; chromatic passing tones, borrowed notes and tensions stay |
| 246 | + legal by design (recorded in the contract proposal after the PR #149 |
| 247 | + Codex finding). |
| 248 | +- **Prerequisite (named)**: a later, explicitly calibrated contract on |
| 249 | + top of the accepted S15 phases. Until then tonal context influences |
| 250 | + nothing in the rule layer. |
| 251 | + |
| 252 | +### 15. Voice crossing — opt-in |
| 253 | + |
| 254 | +- **Statement**: part B does not cross above/below part A where the |
| 255 | + arrangement declares a register order. |
| 256 | +- **Today**: not implemented as a named rule; heavy overlap partially |
| 257 | + shadowed by register mud (rule 7), which is a different fact |
| 258 | + (co-occupancy, not order inversion). |
| 259 | +- **Classification reason**: like rule 8 — a legitimate device in some |
| 260 | + arrangements (Cluster Engine treats it as a *selectable* rule, which |
| 261 | + is the right prior art posture), so opt-in per `RuleScope`, never |
| 262 | + universal. |
| 263 | + |
| 264 | +## Oracle spike selection (the two real problems) |
| 265 | + |
| 266 | +Per the Constraint Lab first increment, the spike models **existing** |
| 267 | +rules, not invented toys: |
| 268 | + |
| 269 | +- **Problem A — fretboard realization** (rule 1, with rule 3 as data): |
| 270 | + given a monophonic line, tuning, `max_fret`, and a travel bound, |
| 271 | + decide satisfiability of a `(string, fret)` assignment and return a |
| 272 | + witness or UNSAT. Compared against `infer_positions` on the same |
| 273 | + inputs: confirms unreachability claims exactly and hunts minimal |
| 274 | + counterexamples where the DP misses feasible assignments under tight |
| 275 | + bounds. Fixture export: deterministic lines + tunings, archived with |
| 276 | + solver identity. |
| 277 | +- **Problem B — complementary pair cleanliness** (rules 6, 7, and |
| 278 | + opt-in 15): given part A fixed and a candidate rhythm for part B, |
| 279 | + decide whether any pitch assignment for B satisfies no-coincident- |
| 280 | + dissonance + register windows (+ optional no-crossing), or prove the |
| 281 | + window empty. This checks `PairValidation`'s rule set for emptiness — |
| 282 | + exactly the "prove UNSAT honestly" use the Lab exists for. |
| 283 | + |
| 284 | +Both problems archive exact inputs/outputs per the Lab's manifest |
| 285 | +discipline; neither touches production (`deny.toml` unchanged). |
| 286 | + |
| 287 | +## What the inventory deliberately does not contain |
| 288 | + |
| 289 | +Workspace-level laws (forbid-unsafe, lint gates, TDD, English-only) — |
| 290 | +they are repository constitution (SPEC/AGENTS), not candidate-admissibility |
| 291 | +rules, and putting them in a `RuleScope` would confuse two different kinds |
| 292 | +of "hard". Scoring axes (`rerank.rs`, relation weights) — soft by |
| 293 | +definition, catalogued where they live. Rules whose subject matter is |
| 294 | +deferred by other documents (learned-model authority boundaries — the |
| 295 | +preference-learning proposal's non-goals govern). |
0 commit comments