Behavioral Protocol and Ownership Contracts
These are the executable kit's acceptance boundaries. Patterns do not supply these choices automatically. Changing a policy requires changed expectations and a migration explanation; naming it a refactor does not preserve behavior.
All models are sequential and in memory. They do not establish crash durability, thread safety, delivery guarantees, resource limits, or real authorization safety.
- L01
quote(cents, policy): exact nonnegative int (bool excluded), else ValueError('cents') before calling policy. Invoke policy exactly once, propagate its exception. Return its exact int in [0,cents], else ValueError('quote'). No currency conversion, tax or external services. Rounding belongs to policy. - L02
Subject.subscribe(callback)accepts a callable, gives each registration a separate identity, returns an idempotent zero-argument dispose handle.publish(event)uses registration-order snapshot membership from call start: changes during callbacks affect subsequent publications only. Callbacks receive the same event object; use immutable payloads when isolation is required. Nested publication raises RuntimeError('reentrant') before delivery. Ordinary Exception failures are collected as (class-name,message) tuples in callback order and remaining callbacks run; return that list, including nested-publish errors escaping callbacks. The publishing flag resets even after a BaseException (which propagates). Handles remove retained callbacks after active snapshots end; no garbage collection timing promise. No asynchronous callback support. - L03
History(text=''): exact str else ValueError('text'). Read-onlytextis current document.replace(start,end,replacement)uses exact int indices 0 <= start <= end <= len(text), exact str replacement, else ValueError('edit') with document/history unchanged. Every valid edit, including a no-op, records before/after versions and discards redo.undo/redorestore a version and return True; empty stack returns False. History owns all edits. No external effects, concurrent editors, history cap or persistence; memory grows with retained text. - L04
transition(state,event)recognizes states draft/review/published/archived and events submit/revise/approve/archive (exact strings). Validate state before event: ValueError('state') or ValueError('event'). Only draft+submit->review, review+revise->draft, review+approve->published, published+archive->archived are allowed; others raise ValueError('transition').Workflow()starts draft with empty records.send(event)returns/appends (before,event,after) only on success; rejected operations leave both state and records unchanged. Read-onlystateand immutable tuplerecordsexpose observations.replay(records)starts draft, validates contiguous exact three-tuples and each transition and returns final state. Shape, mismatched before/after -> ValueError('record'); transition errors use the transition API errors. Records are accepted transition facts, not a durable event store. No external effects are called during replay. - L05
pages(fetch): fresh single-pass iterator, no fetch until first next().fetch(index)receives 0,1,... and returns (finite tuple items, exact bool more). That return shape is a precondition, not validated here. Empty nonfinal pages are allowed; total number of pages is finite. Fetch following page only when next() demands an item past current page. Yield in page/item order, propagate fetch errors without retry; repeated exhaustion performs no calls. Individual iterators have independent cursors. No resource-owning page handles in this kit. - L06
first_decision(handlers,request): consume a finite iterable of callables in order. Each returns exact string pass/allow/deny; otherwise ValueError('decision'). Stop on first allow or deny. All-pass/empty -> deny. Propagate handler errors; no fallback or execution of later handlers. This is a teaching first-match policy, not a generic security policy engine. - L07 immutable
Num(value),Add(left,right),Mul(left,right)represent finite acyclic arithmetic trees. Exact int leaf values and valid children are caller preconditions.accept(visitor)calls num/add/mul respectively.Evaluaterecursively evaluates integer arithmetic;Prettyreturns fully parenthesized infix text, e.g. (1 + (2 * 3)). Recursion depth and integer bit cost remain resource limits; hostile input parsing is outside this lab.
Composition boundary
For the module project, accepted workflow state is authoritative before observer notification. Report notification failures separately; replay accepted records without notifying. This is a deliberate local contract, not durable transactions or a guarantee about external systems. See protocol composition.
Two explicit behavioral contracts
Both packages are required and intentionally differ. Learn the core contract first, then implement the runtime comparison without changing either package's reference semantics. The second package adds close/lifetime and immutable-history evidence; it is not a replacement for the seven broader core labs.
| Observation | Core: behavioral-patterns.zip | Runtime comparison: behavioral-contracts.zip |
|---|---|---|
| Publication membership | Snapshot of callbacks; removing B during A still delivers to B this time | Snapshot of tokens with a live check; removing B during A skips B |
| Registration added during delivery | Next publication | Next publication |
| Duplicate registration | Separate idempotent disposal handle per registration | Separate integer token; unsubscribe returns whether it removed one |
| Callback error report | Ordered (exception class, message) values | Ordered (token, exception class) values |
| Closing publisher | Outside core Subject API | Repeatable close cancels pending delivery, releases registrations and rejects new subscribe/publish |
| Valid no-op edit after undo | Records a version and discards redo | Leaves complete history unchanged, preserving redo |
| Public history observations | Read-only current text | Immutable current text, cursor and full history snapshot |
Snapshot and live cancellation are alternative policies, not a correct/broken pair without a named contract. Likewise, no-op history behavior is a product choice. Each package's seeded faults violate its own API; a fault for one package may represent an intentional policy in the other. Preserve both expectation sets and explain the differences on an unseen trace.
Use runtime lessons and tasks with BD1–BD6; use the core contracts with C01–C10. Shared reasoning can support both gate sets, but policy-specific traces and lifecycle checks cannot be waived.