# Changelog

## 0.3.0 — 2026-08-17

### Added
- `defineMuster` describe/bind API replacing `createMuster`
- `AdmissionBridge` port + `admission.ts` — pluggable access-control gate for check-in
- `ScopeRefVO` — value object for cross-kernel subject refs

### Changed
- Check-in method enum, error types, outbox-store, models, repositories, schemas updated

## 1.0.0 - 2026-07-29

### Changed
- **License:** relicensed from MIT to the **Classytic Source-Available License** (Community & Commercial). Evaluation/development use remains free; production use now requires a commercial license from Classytic LLC. See `LICENSE`.
- Major bump marks the license change; versions published before 1.0.0 remain under their original MIT terms.

## [0.4.0] — 2026-07-26

### Added — companions, live occupancy, and admission control

Attendance answered *"was X present at Y"*. Three additions answer the question
one instant earlier — *"may this party come in, right now?"* — which is what a
gym turnstile, a school gate, and a hotel facility door all actually need.

- **Companions on a session** — `companionCount` (always present, default 0)
  plus an optional `companions[]` manifest (`{ ref?, model?, name?, relation? }`,
  subject-polymorphic like the session's own subject). A member's guest, a
  pupil's collecting parent, a patient's visitor, a contractor's crew. The
  manifest is WRITE-ONCE at check-in and bounded by the arriving party, so it is
  not the growing-array pattern break intervals correctly avoid.
- **`occupancy(filter, ctx)`** and **`occupancyByScope(filter, ctx)`** — live
  "who is inside, per room". Domain math, not a `count` alias: `people` sums
  subjects **plus their companions**, because a member with two guests is three
  bodies in the pool. Backed by a new partial index `muster_live_occupancy`
  (`{scope, sessionKind}` where `status:'open'`) so the hot dashboard read
  touches only who is currently inside, not the whole history.
  `occupancyByScope` renders a whole facility board in ONE aggregation.
- **`@classytic/muster/admission`** — a new mongoose-free subpath exporting the
  PURE decision `evaluateAdmission(facts, policy)` (+ `isAdmitted`,
  `validateAdmissionPolicy`), so the identical rule runs on the server, on an
  offline kiosk, and in an admin rule-preview UI. Same server/edge split
  `@classytic/access/decision` uses.
- **`checkIn` enforces admission when a policy is supplied** — `capacity`
  (against live occupancy), `maxCompanions` (the "one guest, or two" rule), and
  declarative `rules`. Refusal throws the new `AdmissionDeniedError` (403,
  `muster.admission.<reason>`), and because the check runs inside the check-in
  transaction, a refusal writes NOTHING. Omit `admission` and behaviour is
  unchanged — muster stays a pure recorder.

**House rules are DATA, not code.** `AdmissionRule.denyIf` / `.allowIf` are
`Condition`s from `@classytic/primitives/condition` — a JSON-serializable
predicate DSL that round-trips through the DB — so one engine covers
"Silver may not use the pool before 10:00 at weekends", "a visitor needs an
escort during exam week", and "spa access for suite guests only" with zero
package changes. Clock facts (`weekday`, `hour`, `minute`, `minutesOfDay`) come
from `primitives/timezone` `localTimeParts` in the ENGINE's business zone, so
"before 10:00" means local 10:00, not UTC.

### Boundary (deliberate)

The standing RIGHT to enter, and any metered allowance ("3 guest visits per
month"), stay with **`@classytic/access`** — a grant with
`usage: { limit, used }` whose `consume()` is atomically guarded. Admission is
about THIS visit; entitlement is about the standing right. A host composes both:
`access.check()` → `access.consume()` → `muster.checkIn({ admission, companionCount })`.

No domain event fires on refusal, by design: the decision happens inside a
transaction that then rolls back, so an outbox row written there would roll back
with it. The typed error carries `reason` / `ruleCode` for the host to audit at a
layer where the write actually commits.

### Honest limit — capacity under exact concurrency

Capacity is enforced inside the check-in transaction, reading that
transaction's own view. MongoDB is snapshot-isolated with no predicate locks, so
two exactly-simultaneous check-ins can both observe the pre-image and both be
admitted, overshooting `capacity` by the number of concurrent writers. It is
bounded and self-correcting (the next tap sees the true count) and acceptable
for a physical door, which serializes arrivals anyway. A hard cap under true
concurrency needs a guarded counter document (the pattern
`@classytic/access.consume` uses for quotas) — deliberately NOT added, because a
counter is a second source of truth for occupancy that can drift from the
session records.

### Changed

- Occupancy reads go through mongokit's `aggregatePipeline`, so the multi-tenant
  and soft-delete policy hooks inject the leading `$match` themselves — never a
  hand-rolled `{ organizationId }` (which would both hardcode the configurable
  tenant field and skip mongoose casting, silently matching nothing when the
  tenant id arrives as a hex string).
- Peer `@classytic/mongokit` raised to `>=3.25.0`; dev-verified against 3.28.0.

## [0.3.1] — 2026-07-16

### Fixed

- injectTenantField no longer force-indexes the tenant field: the compound
  prepend makes the tenant the leading key of every index, so the bare single
  was a redundant prefix paid on every insert (PACKAGE_RULES P11.1, fleet
  index audit). A dedicated tenant index is still created when a schema
  declares no compounds to prepend. Index-declaration change only.

## [0.3.0 addition] — unpublished (2026-07-15)

### Added — attendance-record fence backstop (peer floors: mongokit >=3.22.0, repo-core >=0.13.0, primitives >=0.11.0)

- The typed method overrides (`AttendanceRecordImmutableError`) fence the
  five common verbs but not `claim` / `claimVersion` / `restore` /
  `bulkWrite`; mongokit 3.22's registry-derived `appendOnlyPlugin` now
  backstops those — NO mutation path exists on attendance records at all
  (the same gap class ledger's hand-rolled guard had).

## 0.3.0 (unreleased)

### Changed (behavioral) — 2026-07-05 addendum

- **§P8.1 relay-only session guard.** `UnmanagedSessionError` now throws only
  when a host session arrives with NEITHER a `[PENDING_EVENTS]` queue NOR an
  outbox. Session + outbox + no queue is legal durable-relay-only mode: the
  outbox row commits with the host transaction, the relay delivers
  post-commit, and the immediate in-process publish is skipped. (The strict
  pre-addendum guard threw even with an outbox wired.)
  `assertManagedSession(ctx, hasOutbox)` gained the outbox parameter;
  `RunUnitConfig` gained `hasOutbox`. Aligns with invoice / catalog / order /
  purchase / transfer / promo / payrun / loyalty / review / access.

Device-fleet readiness — the prerequisites for biometric/RFID automation
(ZKTeco ADMS-style push fleets) live in the engine now.

- **`punch(input, ctx)`** — a direction-less device tap. Resolves check-in vs
  check-out from the subject's open-session state, with a debounce window
  (`punchDebounceMs`, default 60 s) so finger bounce never closes a
  40-second "shift" or opens a ghost session after checkout. Returns
  `{ action: 'checked_in' | 'checked_out' | 'duplicate' | 'debounced', session }`.
  Race-safe: concurrent first-punches resolve through the open-session unique
  index; the loser re-reads and resolves as the second punch.
- **Device-replay idempotency.** `checkIn` / `checkOut` / `punch` accept an
  `idempotencyKey` (natural key `SN:PIN:timestamp`); a partial unique index on
  AttendanceRecord (`muster_record_idempotency`) makes ADMS batch re-uploads
  no-ops that return the ORIGINAL session — one session, one record, proven
  by a 5-way concurrent-delivery test. Honest limit documented: a concurrent
  same-key loser may report `checked_in` instead of `duplicate` (sequential
  replays always report `duplicate`); summarize batches by session id.
- **`businessDate` (roster day).** Sessions carry a `CivilDate` string derived
  from `actualStart` in the new engine `timezone` (IANA, validated at boot,
  default UTC) via `@classytic/primitives/timezone.civilDateOf` — a
  23:30-local night-shift check-in lands on THAT local day, DST-exact. New
  index `(scope, businessDate, subjectModel)`: the daily muster roll is a
  string match, month ranges are string ranges.
- `punchSchema` zod validator (`/schemas/session`); `idempotencyKey` on
  checkIn/checkOut schemas. Primitives peer bumped to `>=0.9.0`.

## 0.2.0 — 2026-06-12

Stack upgrade + PACKAGE_RULES compliance pass. Clean breaks, no deprecation aliases.

### Breaking

- **Peers raised:** `@classytic/mongokit >=3.16.0`, `@classytic/repo-core >=0.6.0`. The engine now inherits mongokit's fail-closed tenant defaults (`onMismatch: 'throw'`, `allowDataInjection: false`).
- **Removed deprecated engine config** `multiTenant` and `tenantFieldType` — use `tenant` (canonical `TenantConfig` from `@classytic/repo-core/tenant`, plus mongokit plugin knobs via the new `MusterTenantOptions` type): `createMuster({ connection, tenant: { fieldType: 'objectId' } })`. `createMusterModels` likewise accepts only `tenant`. The internal `resolveMusterTenant` legacy shim is gone — resolution is pure `resolveTenantConfig`.
- **Errors implement `HttpError`** (`@classytic/repo-core/errors`): every domain error now carries `status` and a hierarchical lowercase `code` (`muster.session.already_open`, `muster.correction.not_pending`, …) instead of the old `MUSTER_*` UPPER_SNAKE codes. Arc serializes them to `ErrorContract` with the right HTTP status automatically.
- **`outbox.save` failures now propagate** (P8). Previously they were logged and swallowed — a doc could commit while its event row silently vanished. A failing outbox now aborts the verb (and rolls back the surrounding transaction).
- **Event wire payloads carry ISO strings:** `proposedChanges` / `appliedChanges` in `muster:session.corrected`, `muster:correction.requested`, and `muster:correction.approved` are now `ProposedSessionChangesWire` (dates as ISO-8601 strings, rule 25), not in-process `Date` objects.
- **CAS race losses throw `VersionMismatchError` (409)** instead of a misleading `SessionNotFoundError` / `CorrectionNotFoundError` when a `claimVersion` write loses a concurrent race.

### Added

- **Zod-source event catalog (rule 18):** `musterEventDefinitions[]` + per-event definitions exported from the root and the new `@classytic/muster/events` subpath. Register into arc's `EventRegistry` for publish-time validation, OpenAPI event introspection, and `arc describe`.
- **Capabilities boot gate:** `createMuster()` asserts `repo.capabilities.transactions` at engine creation (unless `allowNonTransactional: true`) and throws `MusterCapabilityError` instead of a cryptic driver error on the first `checkIn()`.
- **`engine.syncIndexes()`** for deploy-time index builds (`autoIndex: false` boots, rule 35).
- **`ctx.signal` / `ctx.retryPolicy`** on `MusterContext`, threaded through every repo call via `repoOptionsFromCtx` (repo-core 0.6 cancellation/retry contract).
- **Event meta enrichment:** `createEvent(type, payload, ctx?, meta?)` now fills `meta.userId` / `meta.organizationId` / `meta.correlationId` from the muster context on every emitted event.

### Changed

- Duplicate-key classification on concurrent check-in now uses mongokit's `isDuplicateKeyError` instead of a hand-rolled message matcher.
- Build hygiene: `declarationMap` / `sourceMap` disabled in tsconfig (they were never shipped — tsdown already had them off — but both layers are now explicit).
