Disciplinary System
At a glance
The Disciplinary System is the federation's end-to-end judicial pipeline for misconduct, cheating, doping, violence, and abuse cases. It runs incident reporting through classification, investigation, hearing, decision, sanction enforcement, and multi-tier appeal, with a full immutable audit trail, configurable alternative dispute resolution paths, and cross-federation sanction sharing so that suspended individuals cannot circumvent active bans by competing elsewhere.
How it works
Any authorised actor (umpire, official, player, or registered observer) may file an incident report directly from the competition app or web console. The report is classified at intake into one of the established categories: misconduct, cheating, doping, violence, or abuse. Classification routes the case to the correct commission and triggers the appropriate procedural template (deadlines, required evidence, mandatory hearing, anonymisation rules).
A disciplinary case progresses through clearly defined states: open, under investigation, hearing scheduled, decided, closed. During investigation, the assigned commission attaches evidence (photographs, video clips, witness statements, umpire reports) and may request additional submissions from involved parties. Hearings are scheduled with all relevant parties summoned through the platform; the hearing record captures attendees, the questions raised, and the testimony heard. The commission issues a sanction drawn from the catalogue: warning, fine, suspension for a defined period, ban, or competition-points deduction. Some federations support an alternative dispute resolution path, for instance Spain's arbitraje de equidad with a three-arbiter panel, and the configurable escalation rules determine when ADR replaces full proceedings.
Once a sanction is recorded, enforcement is automatic. Active suspensions block the individual from being entered into any competition during the suspension period; the entry pipeline rejects them and notifies the organiser. Notification fans out to all relevant parties: the sanctioned individual, their club, the home federation, and (for cross-tenant cases) any federations where the person is licensed. The sanction register is searchable by federation level, with expiry dates triggering automatic rehabilitation when the suspension lapses.
Appeals escalate to higher federation levels following the configured chain. Every action, document, and state transition is captured in an immutable audit trail so that the case file is forensically complete should it eventually reach the Court of Arbitration for Sport.
Key capabilities
- Multi-channel incident reporting from umpires, officials, players, and observers with classification-driven routing
- Full case lifecycle (open, investigate, hearing, decide, close) with hearing scheduling, summons, and testimony capture
- Sanction catalogue covering warnings, fines, suspensions, bans, and point deductions
- Automatic enforcement of active suspensions across the entry pipeline
- Cross-federation sanction sharing preventing forum shopping during a ban
- Configurable alternative dispute resolution paths (ADR/arbitration)
- Immutable audit trail across all disciplinary actions and appeal escalation
In practice
An umpire files a violence-classified incident report at a regional event. The case auto-routes to the regional disciplinary commission, which gathers two witness statements and a video clip from the official broadcast. A hearing is convened within the procedural deadline; the player attends, the commission deliberates, and a six-month suspension is issued from the sanction catalogue.
The notification fans out to the player, club, and federation. Two weeks later the player attempts to enter a competition in a neighbouring federation; the cross-tenant sanction registry blocks the entry and notifies the receiving organiser. The player files an appeal, which escalates one tier under the configured chain.
Features in this subsystem
37| ID | Status | Features |
|---|---|---|
| F05.05.01 | Shipped | Incident reporting (by umpire, official, player, or observer) ✅ PL-F0505a |
| F05.05.02 | Shipped | Incident classification (misconduct, cheating, doping, violence, abuse) ✅ PL-F0505a |
| F05.05.03 | Shipped | Disciplinary case management (open > investigate > hearing > decision) ✅ PL-F0505a |
| F05.05.04 | Shipped | Hearing scheduling and management ✅ PL-F0505a |
| F05.05.05 | Shipped | Evidence attachment (photos, videos, witness statements) — rebuilt on the Go contract: POST /v1/discipline/cases/{id}/evidence (multipart file + optional label), per-tenant MIME allow-list, magic-signature sniff, EXIF strip before hashing, 50 MiB cap, and the additive evidence timeline on the case detail. ✅ PL-F0505a · ✅ 7635e69d.m-2 (t-1) |
| F05.05.06 | Shipped | Sanction types (warning, fine, suspension, ban, point deduction) — rebuilt as the per-tenant catalogue GET PUT /v1/discipline/sanction-catalog (whole-layer PUT under If-Match, hidden entries stay valid on old decisions, money as INTEGER minor units + currency). Point deduction is not a category of its own: it belongs to the competition's own result model, not to the sanction ledger. | ✅ PL-F0505b · ✅ 7635e69d.m-3 (t-1) |
| F05.05.07 | Shipped | Sanction enforcement (automatic competition ban during suspension) — rebuilt as the active_sanction lookup (initiative §5d.3, NORMATIVE): an active suspension/ban in ANY scope — including a purely intra-tenant club/district sanction that never propagates — writes the lookup row D-COMP's entry gate (BR04.09) reads via GET /v1/internal/active-sanctions (M2M, service-scope, scope-rank coverage). warning/fine never write one. The licence bar goes through D-MEMBER's OWN seam (BR01.14). ✅ PL-F0505b · ✅ 7635e69d.m-3 (t-1) |
| F05.05.08 | Shipped | Appeal process (to higher federation level) — rebuilt: the next instance is resolved from the tenant's OWN org-node tree (the strictly higher node, m-1's depth rule), the window is m-3's EXISTING AppealWindowDays (one truth about the same deadline, never two), and the instance rule applies: in tier ≥ 2 the decision maker's mandate must reach escalation_target_org_node_id or higher — the instance that ruled first may not review its own decision. A federation without a higher node has an EXHAUSTED chain: no window opens, both fields are null and the answer says so in plain language. ✅ PL-F0505b · ✅ 7635e69d.m-5 (t-1) |
| F05.05.09 | Shipped | Sanction notification to all relevant parties ✅ PL-F0505b |
| F05.05.10 | Shipped | Sanction register (searchable by federation level) — rebuilt as GET /v1/discipline/sanctions ({data,page}, filters status/category/scope/case_id/subject, newest first). Deliberately a READ surface: an application is mutated only through propose/confirm/lift, the seams that also write the audit trail. ✅ PL-F0505b · ✅ 7635e69d.m-3 (t-1) |
| F05.05.11 | Shipped | Sanction expiry and rehabilitation tracking — rebuilt as the F1-7 job discipline_sanction_expiry_tick (*/15 * * * *): an active sanction whose effective_until has passed becomes expired, its lookup row is removed and the licence bar lifted through the SAME clearing seam the amnesty uses. A ban imposed *tills vidare* (null duration) is never touched — the clock cannot end what no one time-limited. ✅ PL-F0505c · ✅ 7635e69d.m-3 (t-1) |
| F05.05.12 | Shipped | Cross-federation sanction sharing (suspended player cannot play elsewhere) — rebuilt end to end (BR07.04): a national/international suspension/ban from a propagating catalogue entry is published to the FIPJP registry and mirrored into every subscribing federation's local_suspension_cache, so the subscriber's own GET /v1/internal/active-sanctions answers active=true with source.kind=cross_tenant for a player it never sanctioned itself. Subscription is an OPT-OUT (tenant_configs.cross_tenant_suspension_subscribed, default true). ✅ PL-F0505c · ✅ 7635e69d.m-4 (t-1) |
| F05.05.13 | Shipped | Full audit trail on all disciplinary actions ✅ PL-F0505c |
| F05.05.14 | Shipped | Alternative dispute resolution — configurable per tenant: formal arbitration/mediation as alternative to disciplinary proceedings (Spain: arbitraje de equidad with 3 arbiters). Configurable escalation path. ⊘ Byggs INTE i D-DISCIPLINE (7635e69d.m-5 non-goal 3): ADR står inte i den auktoritativa visionens §Vad som ingår och ägs av D-DISPUTE (docs/migration/initiatives/D-DISPUTE.md §Scope 3/5). Mockupens ADR-kort renderas därför inte i adminytans överklagandevy — en knapp utan en domän bakom sig är en attrapp. ⊘ ägs av D-DISPUTE ✅ PL-F0505c |
| F05.05.15 | Shipped | Discipline case-spine — full state-machine: draft → submitted → triage → evidence_collection → hearing_scheduled → hearing_in_progress → deliberation → decision_issued → appeal_window_open → appeal_window_closed/under_appeal → appeal_decision_issued → archived (+ withdrawn/dismissed/escalated side-paths). ✅ PL-T213 |
| F05.05.16 | Shipped | Role-template-based intake — five role-driven intake forms (club_referee, court_marshal, event_marshal, jury_president, public) with required-field validation per role. ✅ PL-T213 |
| F05.05.17 | Shipped | Severity auto-classification — derives severity_hint from offence-code → severity rank table at submit time; manual override requires rationale. ✅ PL-T213 |
| F05.05.18 | Shipped | Tenant offence vocabulary — per-tenant offence taxonomy (codes, names, severity defaults) layered on canonical FIPJP offences; configurable per federation. ✅ PL-T213 |
| F05.05.19 | Shipped | Tamper-evident evidence locker — SHA-256 hash chain per case (chain_hash = sha256(prev \ | content_hash || iso_ms(submitted_at) || submitted_by)); periodic verifier flags integrity_alert. Rebuilt: the rule is ONE exported pure function frozen by a golden test, iso_ms pinned to 2026-06-21T14:32:05.123Z (UTC, exactly three decimals, Z), GET …/evidence/verify is a pure read returning {ok, items_checked, first_breach_index, breach_evidence_id}. | ✅ PL-T213 · ✅ 7635e69d.m-2 (t-1) |
| F05.05.20 | Shipped | WORM evidence blob storage — locked retention (default 10 years), MIME-type allow-list, EXIF stripping for photos. Rebuilt: the EvidenceBlobStore port has NO delete and NO overwrite path (second PutLocked ⇒ error); backend chosen by PL_EVIDENCE_BLOB_BACKEND (unknown ⇒ boot error, surface not mounted); image/heic dropped from the allow-list (no CGO-free strip — an unstripped photo blob would be a defect). Azure Blob is no longer assumed. ✅ PL-T213 · ✅ 7635e69d.m-2 (t-1) |
| F05.05.21 | Shipped | Evidence supersede / withdraw / redact — versioned evidence with chain-preserving supersede, withdrawal marker, and GDPR-redaction overlay (original blob untouched). Rebuilt: If-Match on the CASE ETag + Idempotency-Key, mandatory rationale on withdraw/redact, supersede refused on top of a broken chain (409 evidence_chain_breach), redact allowed in EVERY state including archived. ✅ PL-T213 · ✅ 7635e69d.m-2 (t-1) |
| F05.05.22 | Shipped | Sanction catalog with recommended-range engine — propose-sanction validates against precedent-derived [min,max] window; deviation requires rationale. Rebuilt: the percentile method is pinned NEAREST-RANK (1-based d[⌈0,25n⌉]/d[⌈0,5n⌉]/d[⌈0,75n⌉], golden [2,4,6,10,20] ⇒ 4/6/10) as ONE pure function in api/internal/discipline; cohort minimum 5 with widening to the same severity, and below that sample_size: 0 with the deviation gate SUPPRESSED — a range computed on three cases is a guess with decimals. ✅ PL-T213 · ✅ 7635e69d.m-3 (t-1) |
| F05.05.23 | Shipped | Panel decision with optimistic locking — confirm_panel_decision requires expected_deliberation_version; majority-vote enforced; auto-advances case to appeal_window_open. Rebuilt as POST /v1/discipline/cases/{id}/sanctions/{aid}/confirm: stale panel version ⇒ 409 sanction_conflict with ZERO writes (distinct from the case ETag's 412 stale_version), majority is approve > ⌊n/2⌋ with abstentions in the denominator (⇒ 422 majority_not_reached), and the two spine edges run through m-1's own transition seam in ONE transaction. ✅ PL-T213 · ✅ 7635e69d.m-3 (t-1) |
| F05.05.24 | Shipped | Sanction lifecycle ticks — rebuilt as discipline_appeal_window_close_tick (*/5 * * * *) and discipline_sanction_expiry_tick (*/15 * * * *), runnable as api run-job <name> and by the worker. Both are SINGLE-FIRE (source status + clock are WHERE conditions in the write itself ⇒ a dense re-run changes zero rows and writes zero audit), take the clock as an argument, are batch-capped and handle errors per row. The close tick also drives the case's own appeal_window_open → appeal_window_closed edge — with an anonymous system actor, because no human closed that window. ✅ PL-T213 · ✅ 7635e69d.m-3 (t-1) |
| F05.05.25 | Shipped | Formal appeals workflow — file → admissibility → hearing → decided (overturned/modified/upheld/dismissed); cascades sanction status; opens next-tier escalation window if upheld/modified. Rebuilt on the Go contract: TWO filing routes but ONE seam (POST /v1/me/discipline/sanctions/{aid}/appeal self-guarded per BR24.09, POST /v1/discipline/cases/{id}/appeals under the case ETag), the appeal FSM in api/internal/discipline (fail-closed; an illegal edge ⇒ 409 appeal_lifecycle_error, distinct from the case's case_lifecycle_error and the sanction's sanction_conflict), and the outcome cascade INSIDE m-3's two lifecycle seams — clearSanctionEffects for overturned/modified, activateSanction for upheld/dismissed/inadmissible with the start RE-ANCHORED to the decision moment (the time under review was not a served suspension). Appealing is suspensive only BEFORE enforcement (an already-active sanction keeps its lookup, licence bar and ACTIVE registry row), the clock decides the window (two additive spine edges so a tick can never beat a human by milliseconds), and modified gives birth to a NEW sanction row with its own registry entry. ✅ PL-T213 · ✅ 7635e69d.m-5 (t-1) |
| F05.05.26 | Shipped | Cross-tenant suspension registry — published to FIPJP tenant, signed payload (HMAC-SHA512 stub → ES512 in prod), replicated to subscriber LocalSuspensionCache for O(1) license checks. Rebuilt: there is NO publish endpoint — the entry and its fan-out are written INSIDE m-3's activateSanction (a DB write + a pure signature; no network in the transaction) and the activation is NEVER failed by the registry, only booked (cross_tenant_seam: published / skipped_not_eligible / skipped_unidentifiable_subject / skipped_no_registry_tenant / skipped_no_signing_key / deferred_error). Eligibility is ONE pure predicate over five conditions, the canonical payload's twelve fields are frozen by a unit test (privacy as a schema property: no column exists for case number, offence, severity, grounds, evidence or parties), and source_sanction_application_id is UNIQUE — one activation, one entry, for ever. ✅ PL-T213 · ✅ 7635e69d.m-4 (t-1) |
| F05.05.27 | Shipped | Cross-tenant fan-out reconciler — every 4 h: retries pending fan-outs (max 8 attempts), expires entries past effective_until, ack tracking per tenant. Rebuilt as the F1-7 job discipline_cross_tenant_reconcile (0 */4 * * *, api run-job …) with four duties in order — publish sweep (the GUARANTEE: a missed publication can never become a permanent gap), retry of UNCONFIRMED deliveries, new subscribers, expiry (effective_until = null is never touched). The retry discriminator is CONFIRMED DELIVERY, not the status word, so a dense re-run changes zero rows; the cap of 8 sets failed once with an operator alarm. Injected clock, batch caps, per-row error handling. ✅ PL-T213 · ✅ 7635e69d.m-4 (t-1) |
| F05.05.28 | Shipped | Precedent materializer — every 1 h: snapshots terminal cases into SanctionPrecedent registry (anonymised case-id hash) so search & recommended-range stay warm. Rebuilt as the F1-7 job discipline_precedent_materialize: the A-7 filter lives in the QUERY (terminal AND a panel-confirmed sanction — a dismissed, sanctionless case has no days to recommend from, and counting it as "0 days" would drag every recommended range down), the key is the domain's OWN AnonymizedCaseKey (never a hex literal), appeal_outcome is read from the case's DECIDED appeal_record (m-5's written hand-over) else not_appealed, a case without a decision node lands on org level national (the cohort m-3's fixtures already warmed, never a "null level" nobody reads), and precedent_materialized_at is null sits in the UPDATE's own WHERE ⇒ a dense re-run writes zero rows and zero audit. The sweep IS the guarantee: a transition hook could be forgotten in one of the paths to terminal and would never catch up with history. ✅ PL-T213 · ✅ 7635e69d.m-6 (t-1) |
| F05.05.29 | Shipped | Public protocol publication — anonymised, locked Markdown ProtocolDocument; refuses safeguarding cases and minor subjects; corrections appended as immutable diffs (original markdown preserved). Rebuilt on the Go contract: publishable states are EXACTLY {appeal_window_closed, appeal_decision_issued, archived} (fail-closed on unknown), the protection gates are structurally un-overridable (no redaction_overrides in the body, no override/force/bypass parameter in ProtocolProtectedParty's signature — mechanically guarded both statically and behaviourally) and carry a THIRD reason beyond legacy: unverifiable_age (a person subject without subject_birth_year cannot be proven an adult ⇒ protected, A-2). Refusal is 403 protected_party with the reason CLASS in detail, never the subject's data; the gate ORDER (locale-422 → spine-409 → protection-403 → duplicate-409) is normative because the wrong order leaks a case's content through WHICH refusal answers. Parties are aliased stably and irreversibly, evidence is a COUNT, markdown is written once (no code path UPDATEs the column), corrections are append-only diffs, and unpublishing is TERMINAL: all three public URLs answer 410 with the one-liner *"Dokumentet har dragits tillbaka."* ✅ PL-T213 · ✅ 7635e69d.m-6 (t-1) |
| F05.05.30 | Shipped | GDPR legal-hold + retention sweep — cases default legal_hold=True; case_retention_sweep (24 h) flags expired non-held archived cases for purge. Rebuilt with the HALF the legacy surface never had: POST /v1/discipline/cases/{id}/legal-hold {held, rationale} (A-5 — without a release action no case could ever become un-held and the whole retention model was decoration), the F1-7 job discipline_case_retention_sweep with all four conditions in the UPDATE's WHERE (dense re-run ⇒ zero rows), and the actual erasure in D-GDPR's pipe: D-DISCIPLINE registers its erasure module (Guard + OnErasure) and answers RetentionGuard per row — keep(legal_hold) / keep(retention_until) / anonymise — with every keep reported in deletion_proof. Anonymisation replaces the party JSONB with ALIAS content, nulls user_ref (⇒ the generated columns go null and /me stops matching) and scrubs intake_partial_payload + appeal_record.grounds; the case row, case_number, statuses, sanctions and audit REMAIN de-identified (A-13), and evidence_item is never touched — chain_hash binds submitted_by/submitted_at/content_hash, so discipline_evidence_chain_verify is green before AND after (mechanically measured). ✅ PL-T213 · ✅ 7635e69d.m-6 (t-1) |
| F05.05.31 | Shipped | Discipline audit dual-write — every action writes both DisciplinaryAuditEntry (domain-specific) and platform AuditLog. ✅ PL-T213 |
| F05.05.32 | Shipped | Self-service player view — /me/discipline/cases lists own cases & sanctions; /me/discipline/sanctions/{id}/appeal files appeal within window. ✅ PL-T213 |
| F05.05.33 | Shipped | Rebuild /v1 Go contract — case-spine core (migration 00181, package api/internal/discipline): the 16-state machine driven by VERBS (POST /v1/discipline/cases/{id}/transition; illegal jump ⇒ 409 case_lifecycle_error), role-template intake (soft at create, hard at submit), severity derivation + override-with-rationale, race-safe immutable DISC-<YYYY>-<NNNN> case numbers, tenant offence vocabulary over the canonical FIPJP base, the scope-filtered commission queue with SLA/overdue, /me (404-not-403) and audit dual-write in ONE transaction. New capabilities: discipline:create, discipline:manage, discipline:decide, discipline:configure, self.discipline.access. The OrgNode ladder (club ≤ MODERATE, district ≤ SEVERE, national/cep/fipjp ≤ CRITICAL, plus org_node_autonomy.discipline) gates issue-decision ⇒ 403 decision_rights_exceeded. Local fixture seed-discipline; contract specs/api/endpoints/discipline-cases.md. Evidence locker, sanctions/panel, cross-tenant registry, appeals and protocols follow in later milestones. ✅ 7635e69d.m-1 (t-1) |
| F05.05.34 | Shipped | Rebuild /v1 Go contract — tamper-evident evidence locker (migration 00182, api/internal/discipline/evidence.go): the append-only chain (seq allocated under the case row lock, unique (case_id, seq), ms-normalised submitted_at that survives the DB round-trip), the WORM port without a delete path, the per-tenant MIME layer over code constants + EXIF strip before hashing, supersede/withdraw/redact, chain verification, HMAC-signed short-lived downloads (503 rather than an unsigned link), the additive evidence timeline + integrity_alert on the case detail, dual-written audit (discipline.evidence.*, discipline.case.integrity_alert) and the F1-7 job discipline_evidence_chain_verify (24 h, single-fire, never auto-clears the flag). Local fixture: the seed-discipline evidence delta (a REAL five-item chain built through the API's own seams — no hand-written hashes). Contract specs/api/endpoints/discipline-evidence.md. Admin/app surfaces follow in 7635e69d.m-2 (t-2); sanctions/panel, cross-tenant registry, appeals and protocols in later milestones. ✅ 7635e69d.m-2 (t-1) |
| F05.05.35 | Shipped | Rebuild /v1 Go contract — sanction core (migration 00183, api/internal/discipline/sanction.go, api/internal/store/discipline_sanction_lifecycle.go): the per-tenant sanction catalogue (whole-layer PUT under If-Match, hidden entries, money as integer minor units), the NEAREST-RANK recommended-range engine over anonymised precedent, propose (state gate, category rules with field pointers, BR24.05's deviation gate, snapshot of the range, bumping BOTH the case version and deliberation_version), the panel confirm (409 sanction_conflict on a stale panel version, 422 majority_not_reached, window anchored at confirmed_at + 14 d, two spine edges through m-1's seam), the manual lift, the active_sanction lookup + GET /v1/internal/active-sanctions (M2M) that makes an INTRA-tenant suspension visible to D-COMP's entry gate, the licence seam to D-MEMBER (BR01.14 — discipline never writes a licence table itself), the sanction register and the additive detail//me projections (the party never sees voting record, range snapshot or deviation rationale), audit dual-write (discipline.sanction.*) and the two F1-7 jobs discipline_appeal_window_close_tick (5 min) and discipline_sanction_expiry_tick (15 min). Local fixture: the seed-discipline sanction delta (catalogue, armed precedent cohort, an OPEN panel case and an ACTIVE one driven through the close tick's OWN job seam — no hand-written statuses). Contract specs/api/endpoints/discipline-sanctions.md. Admin/app surfaces follow in 7635e69d.m-3 (t-2); cross-tenant registry, appeals and protocols in later milestones. ✅ 7635e69d.m-3 (t-1) |
| F05.05.36 | Shipped | Rebuild /v1 Go contract — cross-tenant suspension registry (migration 00186, api/internal/discipline/cross_tenant.go, api/internal/store/discipline_cross_tenant.go): the registry on the FIPJP tenant (resolved on code='FIPJP' + tenant_type='fipjp' — D-OFF's exact resolution), the eligibility predicate, the FROZEN twelve-field canonical payload in the {kid, alg, payload, signature} envelope (HS512 hex-HMAC-SHA512 over sorted-key, whitespace-free JSON with ms-precision UTC; key from PL_DISCIPLINE_CROSS_TENANT_SIGNING_SECRET, documented dev fixture only in local/test, and NEVER an unsigned row), publication + fan-out INSIDE m-3's activation seam and the status cascade inside clearSanctionEffects, the normalised propagation table projected to the vision's propagation_status_per_tenant, the subscriber mirror (local_suspension_cache, idempotent, soft source_entry_id), the F1-7 job discipline_cross_tenant_reconcile (0 */4 * * *, four duties, retry on CONFIRMED DELIVERY, cap 8, single-fire), the registry routes (list/detail/ack/lift; a non-FIPJP tenant ⇒ 403 registry_tenant_required, M2M read+ack but never lift), the PUBLIC privacy-safe check GET /v1/public/discipline/suspensions/verify (five fields, {found:false} instead of an existence oracle, per-IP cap) and the read-only operator health GET /v1/sys/discipline/suspension-health, plus the ADDITIVE source.kind ∈ {local, cross_tenant} on GET /v1/internal/active-sanctions. OverturnCrossTenantSuspension is built as the RECEIVING edge only — the appeal sender is m-5. Local fixture: the seed-discipline registry delta (FIPJP tenant ensured through the house's own tenant seam, ops identity disciplin-fipjp-seed@demo.petanque.life, the published row produced by the REAL chain, FRA acknowledged through the real ack seam, NOR left pending, no seeded failed row). Contract specs/api/endpoints/discipline-fipjp-suspensions.md. FIPJP/sys surfaces follow in 7635e69d.m-4 (t-2); appeals and protocols in later milestones. ✅ 7635e69d.m-4 (t-1) |
| F05.05.37 | Shipped | Rebuild /v1 Go contract — public protocol, GDPR retention & measurement (migration 00192, api/internal/discipline/protocol.go, api/internal/store/discipline_protocols.go / …_retention.go / …_precedent_materialize.go / …_dashboard.go / gdpr_erasure_discipline.go): the nine routes (publish / corrections / unpublish, the legal-hold release, the commission dashboard, the operator overview and the three PUBLIC read surfaces …/protocols/{year}[/{alias}[/print]]?tenant=<KOD>), the two NEW problem codes protected_party (403) and protocol_conflict (409), the two F1-7 jobs discipline_precedent_materialize (hourly) and discipline_case_retention_sweep (24 h), and the erasure module registered in the house's registry (RetentionGuard per row + OnErasure + the THIRD retentionSweepSources row {discipline, discipline_case, discipline_records}, row-anchored so a federation without a configured TTL is still swept — otherwise the marking would be inert exactly there). Privacy is a SCHEMA property, not a filter: the rendering input struct carries only {party type, index}, the public row's key set is FROZEN, corrections carry the role word "kommissionen" publicly, and avg_resolution_days is null without evidence — never 0 as a lie. Local fixture: the seed-discipline protocol delta (a published, locked demo protocol WITH a correction driven through the real seams — the audit rows are the proof — the two protection-gate negatives left UNPUBLISHED, a retention-ripe case released through the hold ROUTE, and materializer substrate). Contracts specs/api/endpoints/discipline-protocols.md + specs/api/endpoints/discipline-precedents.md. Admin/web/sys surfaces follow in 7635e69d.m-6 (t-2). ✅ 7635e69d.m-6 (t-1) |
Stakeholders who need this subsystem
Surfaces in 3 stakeholder analyses