//#region src/domain/value-objects/scope-ref.vo.d.ts /** * ScopeRef — qualifies `AttendanceSessionDocument.scope` with WHAT KIND of * place it names. * * Muster review finding #10: `scope` is a free string, and the open-session * uniqueness key is `(organizationId, subjectModel, subjectRef, scope)` (per * §33, `organizationId` is prepended by tenant injection — collision is only * possible WITHIN one branch). A branch code, a classroom, and a physical * gym zone drawn from the same string space can collide on IDENTICAL values, * and — worse for zone-wise attendance — nothing can tell "pool" the zone * from "pool" the class without asking a human. * * ## Deliberately additive — no index change, no migration * * The obvious fix reaches for a schema change: split `scope` into * `{ scopeKind, scopeId }` and rebuild the unique index. `DEVICE-CONTROL- * CENTER.md` already flagged why that is the wrong first move: "changing a * unique-index key later is a migration on live sessions," and this * deployment already has real `scope` values written during this session's * testing (`pool`, `sauna`, …). * * Instead, the KIND rides inside the same string, as a prefix: * `zone:pool`, `branch:main`, `classroom:101`. This is purely a VALUE * convention: * - The model, the schema, and every index in `attendance-session.model.ts` * are untouched — `scope` is still one trimmed string. * - An existing unqualified value (`"pool"`) keeps working exactly as * before; `parseScopeRef` reports it has no recognised kind, and callers * that only care about zones treat "no kind" the same as "not a zone" — * the safe reading, not a guess. * - A NEW zone scope is written as `zone:pool`, which cannot collide with an * old unqualified `"pool"` (different string), and a caller that adopts * the convention gets an honest answer instead of a coin-flip on a shared * string space. * * `device.model.ts`'s `scope` field is the same free string, forwarded * opaquely to consumers — device-registry does not parse it, so this type * lives only where the parsing actually happens (muster's admission / * occupancy / zone-registry reads), never as a dependency the other way. */ /** Open string, like `DEVICE_KINDS` — the registry must not decide which kinds may exist. */ declare const SCOPE_KINDS: Readonly<{ zone: "zone"; branch: "branch"; classroom: "classroom"; }>; interface ScopeRef { /** e.g. 'zone', 'branch', 'classroom' — open, not an enum. */ kind: string; /** The identifier within that kind — a zone code, a branch id, a room number. */ id: string; } declare class InvalidScopeRefError extends Error { constructor(reason: string); } /** * Build the qualified `scope` string. Throws on a malformed `kind` — this is * the WRITE side, so a typo here is a code bug, not a legacy value; it * should fail loudly rather than write an unparseable scope. */ declare function encodeScopeRef(ref: ScopeRef): string; /** * Read the qualified `scope` string. Returns `undefined` for anything that * is not a recognised `kind:id` pair — including every unqualified legacy * value. This is a READ over a historical free-text field, so it degrades * gracefully rather than throwing; a caller must treat "no kind" as "cannot * tell", never as "confirmed not a zone" if that distinction matters to it. */ declare function parseScopeRef(scope: string | undefined): ScopeRef | undefined; /** `true` only when `scope` is qualified AND its kind matches. Unqualified/legacy scopes are `false`, never a throw. */ declare function isScopeKind(scope: string | undefined, kind: string): boolean; /** Convenience: `zoneScope('pool')` -> `'zone:pool'`. */ declare function zoneScope(zoneCode: string): string; declare function isZoneScope(scope: string | undefined): boolean; //#endregion export { InvalidScopeRefError, SCOPE_KINDS, ScopeRef, encodeScopeRef, isScopeKind, isZoneScope, parseScopeRef, zoneScope };