Support queue decision twin: pilot contract
Status: exploratory implementation contract. A support operator must confirm the data definitions, actual data feeds, capacity constraints, and acceptance thresholds before production recommendations.
Decision question
Section titled “Decision question”For a specified support queue and time window, compare a baseline staffing schedule with one alternative schedule. The model reports backlog, completed tickets, completions within the SLA, and staffing cost. It does not execute the proposed staffing change.
Data dictionary
Section titled “Data dictionary”| Field | Meaning | Unit / source rule |
|---|---|---|
arrivals |
New tickets entering the queue during a UTC day | Count of distinct ticket IDs; reopening policy must be specified by the operator |
backlog_start |
Tickets unresolved at the start of the day | Count; must equal the previous day’s backlog_end in a continuous series |
resolved |
Tickets completed during the day | Count; must not exceed opening backlog plus arrivals |
backlog_end |
Tickets unresolved at the end of the day | Count; must satisfy start + arrivals = resolved + end |
agents |
Equivalent staffed agent capacity for the day | Integer pilot approximation; partial shifts require upstream normalization |
fixed_extra_capacity |
Additional capacity independent of staffed agents | Tickets/day; document the source and intervention cost |
initial_backlog.age_days |
Age of unresolved tickets at snapshot start | Whole days; required for the model’s SLA calculation |
source_version_hashes |
Versions of ticket and staffing extracts used in a snapshot | Immutable identifiers tied to an exact access scope |
tickets[].queue_id, staffing[].queue_id |
Queue each source row belongs to | New local imports require the same nonempty ID on every row; the ID is included in the canonical source digest but is not proof of upstream authenticity |
UTC day boundaries, SLA pauses, priority queues, transfers, reopened tickets, multiple skill pools, and shared staff can invalidate the single-queue approximation. Record these as exclusions or extend the model before claiming operational fidelity.
The ticket export adapter accepts one creation and at most one resolution per unique ticket ID, plus one staffing row per UTC day. It rejects duplicate IDs, non-UTC timestamps, events after the data cutoff, resolutions before creation, missing staffing days, daily stock violations, resolutions exceeding fixed extra capacity when no agents are staffed, and mixed or partially labeled queue IDs. Tickets already open at the start become initial age cohorts; partial prior days are rounded up. Legacy stored exports without any row-level queue ID remain readable, but decision-import-pilot requires one exact queue ID on every ticket and staffing row. The verified ID is stored in the immutable snapshot and shown in a decision brief; later source reconstruction must reproduce it. Reopened or transferred tickets and partial shifts need a documented upstream normalization rule before import. The adapter produces the immutable demand snapshot, baseline staffing scenario, and observed daily stocks from the same export.
duduclaw decision-import-pilot --db decisions.sqlite --causal-db causal.sqlite --export-file pilot.json --model-file model.json --source-output tickets-source.json --tenant <tenant> --acl <acl> --queue-id <expected-queue> --source-lineage <lineage> --retention-until-utc <RFC3339 UTC> imports the same SupportPilotExport JSON used by the adapter. The operator-declared --queue-id must exactly match every ticket and staffing row before any output file is created. It is separate from the source lineage and does not authenticate the upstream connector. The model file contains a versioned QueueModel; it is supplied explicitly rather than fitted on the import window. The export must declare exactly one source_version_hashes entry equal to the SHA-256 of its canonical [tickets, staffing] JSON. The command requires new output paths, writes owner-only local files on Unix, binds the snapshot to a causal-store artifact in the exact tenant/ACL scope, and returns a replay command with an expected hash. A changed or revoked stored source artifact blocks replay; editing the exported source file alone does not change the immutable stored artifact. The local export’s upstream identity, data dictionary, model calibration, and forecast skill remain unverified until an operator and real data are available.
Deterministic transition
Section titled “Deterministic transition”For day t, available work is backlog_start[t] + arrivals[t]. Maximum service is agents[t] * service_capacity_per_agent_day + fixed_extra_capacity[t]. The empirical paired mode may supply a replay-bound capacity_per_agent[t] path in place of the scalar capacity; the same path is used for both compared staffing scenarios. The engine resolves the smaller value in first-in-first-out cohort order. It counts tickets as within SLA only when day_resolved - day_arrived < sla_days. The remaining work becomes backlog_end[t]; all arithmetic and stock conservation are checked.
The exploratory ticket-event engine uses exact whole-second creation timestamps, a declared UTC shift start and duration, and equal service slots per agent. It rejects subsecond timestamps rather than silently rounding arrivals across a service boundary. Each slot can begin service only for tickets already in the FIFO queue; SLA is checked at service completion in seconds. It reports day stocks, staffing cost, overdue tickets, and median/95th-percentile wait to service among tickets served within the horizon; pending waits are censored. It validates the SHA-256 of the exact [tickets, staffing] export before running and includes the engine source digest, export, model, scenario, and shift configuration in its replay hash. Historical resolution timestamps validate the input but never drive counterfactual service. Fixed extra capacity is rejected until it has an event-level schedule. This deterministic D/D/c approximation assumes synchronized agents and fixed service time; it is not a fitted service-time distribution or validated operational twin.
Two scenarios use the same immutable DecisionSnapshot and therefore identical recorded arrivals and initial state. A result’s replay hash covers the snapshot, model, scenario, engine schema label, and SHA-256 digest of the numeric transition source in the running binary. A decision brief reports the engine digest, both replay hashes, and source versions. Its status remains exploratory, and its uncertainty interval remains absent until calibration is supported by held-out evidence.
When a verified empirical-run manifest exists for those exact snapshot/model/scenario digests, compare_scenarios_with_empirical_run attaches its fit and run digests, plan, and complete resampling report under exploratory_empirical. The deterministic KPI deltas and descriptive backlog/SLA resampling quantiles then appear in one auditable brief. A different scenario order or input digest is rejected. uncertainty_interval stays null: these quantiles do not have validated predictive coverage.
The synthetic brief also attaches exploratory_event from two stored event-run manifests after checking source bytes, input digests, scenario order, time window, shift configuration, daily and event engine versions, opening backlog, and arrivals. It reports each scenario’s event backlog, resolved count, within-SLA count, staff cost, and median/p95 wait among resolved tickets, plus the descriptive p95 difference. Pending tickets are censored from wait quantiles, so the difference is not a calibrated population wait effect or a causal staffing estimate.
The brief also reports the SHA-256 digest of each immutable stored input and a structured duduclaw decision-replay argument list for each scenario. The CLI loads the exact local tenant/ACL scope, runs the read-only engine, checks --expected-hash, and prints JSON. A changed engine implementation changes this hash, so an old command fails rather than silently presenting a different result as the same replay. It requires access to the owner-protected SQLite file; the tenant and ACL flags select stored data and are not a remote authentication mechanism. A revoked source snapshot makes this replay fail.
When a snapshot is bound to causal-store artifacts, the brief’s source_artifact_links lists each active artifact ID, exact tenant/ACL, and content-version SHA-256. The list is revalidated before the brief is produced and contains no source text. These links identify simulation input provenance; they do not establish a causal staffing effect. An unbound snapshot has an empty list.
duduclaw decision-brief --db <decision.db> --causal-db <causal.db> --tenant <tenant> --acl <acl> --snapshot <id> --model <version> --baseline <id> --alternative <id> --assumption <text> --effect-id <id> emits a fresh comparison and can link one or more previously reviewed observational estimates. Repeat --assumption and --effect-id as needed; omit --effect-id for a simulation-only brief. Add --empirical-run <id> for a stored resampling report. Add --policy-screen <hash> with that empirical run to include its saved joint-risk review screen under exploratory_policy_screen; a screen for another run or scenario order is refused. Add --forecast-validation <id> --source-file <export.json> for a revalidated retrospective forecast assessment; it may appear with or without an empirical run. Add --sla-holdout <id> --source-file <export.json> for the separately revalidated ticket-level SLA assessment under exploratory_sla_holdout; it must match the exact snapshot, model, baseline, source versions, and stored input digests. Add --baseline-event-hash <hash> --alternative-event-hash <hash> --source-file <export.json> together with the empirical run for source-verified event evidence. Incomplete event arguments fail. Each selected component is revalidated against the same scoped snapshot, model, and scenario order. The forecast link must also match the baseline scenario digest; its observed coverage is kept under exploratory_forecast and never becomes the brief’s uncertainty_interval. The SLA link also stays descriptive and cannot change scenario KPIs or that interval. Each effect is re-read in the exact scope and must retain an active digest-checked source, passing model/assumption review, and a study window within the snapshot’s data cutoff. The brief records treatment and outcome names/units, population, window, source digest, ingestion time, point estimate, conditional approximate normal interval, and unit/stratum counts under observational_effects. Invalid or nonfinite numeric estimates are refused. Retrospective analysis may ingest historical observations after their observation cutoff. An invalidated effect fails the whole request. These effect numbers remain observational evidence only: they do not modify scenario KPIs, uncertainty_interval, or the exploratory status. The CLI needs local owner access to both stores; tenant and ACL flags are selectors, not remote credentials.
duduclaw decision-event-replay loads the same scoped snapshot/model/scenario, the exact local [tickets, staffing] source file, the recorded window start and baseline scenario ID, and the shift configuration. It rejects a changed source-file digest or an export that reconstructs a different stored snapshot. Its --expected-hash checks the event-engine replay hash. The synthetic report provides event_baseline_command and event_alternative_command as structured executable argument lists. A bound causal artifact is checked during snapshot loading, so its invalidation also blocks event replay.
duduclaw decision-empirical-replay loads an immutable, source-verified empirical fit and the same scoped model and scenarios. Its flags specify the run count, --arrival-block-days, backlog and cost limits, an optional --min-sla-resolved target for the full horizon, and a paired --capacity-min/--capacity-max range when capacity was unidentified. The report always gives descriptive resampling quantiles for each scenario’s completions within SLA, paired alternative-minus-baseline quantiles, and the fraction of runs where the alternative improves that count. It repeats backlog, cost, and optional SLA thresholds in the standalone report, and gives SLA-target violation rates only when the operator supplies a target. One-day blocks reproduce independent daily resampling; longer blocks copy contiguous segments from the training prefix, preserving demand dependence only within each block. The synthetic demo uses seven-day blocks and an illustrative target of 15 within-SLA completions per day. The report includes empirical_plan and an empirical_replay_command with --expected-hash and --run-id. That run ID binds an immutable manifest containing input IDs and digests, source versions, data cutoff, seed, horizon, initial-backlog digest, engine digests, plan, and recomputed result; replay refuses flags that differ from the manifest. The command fails when its hash differs or the source is revoked. In the default mode, capacity is sampled independently of demand. Optional --paired-saturated-days draws same-day arrival and identifiable per-agent capacity pairs, applies the resulting daily capacity path to both staffing scenarios, and permits longer --arrival-block-days only when the training prefix contains a consecutive run of saturated days of that length. Partial final blocks use a prefix of a valid full source block. This mode preserves observed joint values and within-block dependence only among saturated training days; their selection may overrepresent high-demand conditions. Unidentified capacity or a requested block with no valid source window is refused. Both modes remain exploratory, not calibrated against real support demand.
The admin Decision Lab exposes the same path for a selected canonical synthetic pilot pair through POST /api/decision/empirical-resampling. The request names the exact tenant/ACL, snapshot, model, baseline and alternative IDs, training-prefix length, saturated-day minimum, bounded run/block counts, sampling mode, backlog/cost/SLA limits, and an optional explicit capacity fallback range. An optional screen supplies the existing staffing resource plan and joint-risk criteria; it is evaluated and saved as an immutable source-bound policy screen only when requested, and its cost/backlog caps must match the run. The handler rejects a noncanonical pair, rechecks the exact active synthetic source bytes/causal digest with the existing engineering validator, regenerates only that canonical fixture, and passes its exact [tickets, staffing] bytes into the existing source-bound empirical fit/run store methods. Fit and run IDs derive from immutable inputs and the plan, so an identical request is idempotent and the stored run remains replayable and revocation-sensitive. When capacity is unidentified, an operator must provide a nondegenerate capacity band and use independent sampling; identified capacity refuses that fallback. The response contains only fit counts/identification, paired draw quantiles and constraint rates, run/replay hashes, optional screen choice/failed checks, and limitations. A final source and scenario check rejects a changed or revoked input. This synthetic screen is a candidate for inspection only; it does not establish calibrated probabilities, promote a model, or execute staffing.
The synthetic demo also reports joint_capacity_holdout. It fits pairs only on the declared training prefix, then scores each later saturated day with exactly divisible staff service using the capacity from the training pair with nearest arrivals. It compares the sum of absolute capacity errors with the independent training-capacity median and the last identifiable training capacity. The report counts later days with arrivals outside the training pair range and separately counts unsaturated and nondivisible saturated days that cannot be scored as exact integer capacity. It uses actual held-out arrivals and only checks whether the paired relationship helps predict identifiable per-agent capacity; it does not assess a future demand forecast, backlog interval coverage, or a staffing intervention.
The synthetic demo also reports sla_holdout. The diagnostic requires the export’s single declared source version to equal the SHA-256 of canonical [tickets, staffing]. It freezes a capacity fit from the training prefix, reconstructs unresolved ticket age cohorts at the first later UTC midnight from the ticket export, and replays the later observed arrivals and staffing under the daily FIFO model. It reports both the supplied model capacity and the fitted replay capacity, including whether they match, so a good replay cannot mask a different capacity stored in the imported model. It compares day-by-day completions within the model’s whole-day SLA rule with the actual later ticket resolutions and reports their absolute-error sum. Version 2 also scores three frozen training-only baselines for daily within-SLA counts: repeat the final training day, repeat the final training week by day, and repeat the rounded training mean. Each baseline is fixed before the holdout; no later SLA outcomes are used to set it. The report gives their absolute-error sums and a strict all-baseline comparison. A non-FIFO ticket fixture demonstrates that the diagnostic can reveal an SLA mismatch despite plausible total daily service; a shifted-demand FIFO fixture shows the conditional replay can beat all three frozen baselines. Because the replay receives actual later arrivals and staffing while these count baselines do not, this is an unequal-information descriptive comparison. It does not test a prospective demand forecast, calibrated interval, or staffing effect. Operational SLA clocks, pauses, and priority rules require explicit source semantics and real-data validation.
Version 3 adds one_step_forecast, a separate rolling retrospective SLA backtest. At each target UTC day, it reconstructs only tickets unresolved before that day, takes the scheduled staffing as known, predicts arrivals from the preceding seven days, fits capacity from strictly earlier observed days, and runs a one-day FIFO transition. It then scores actual within-SLA completions against this prediction and three one-step count baselines formed from earlier observed SLA counts (last day, same weekday one week ago, and the preceding seven-day mean). A target-day change in the synthetic fixture changes the score without changing its forecast. If a later capacity fit becomes unidentified, the conditional holdout remains available and one_step_unavailable_reason states why the rolling forecast is absent. This is a retrospective as-of reconstruction; no forecast was committed before a real outcome, upstream opening-ticket state and staffing have not been authenticated, and predictive coverage or intervention effects remain unverified.
forecast_next_day_sla is the shared pure prediction boundary for the one-step backtest and prospective shadow commitment. Its KnownSlaDayInputs contains an exact UTC target midnight, optional queue label for legacy sources, distinct positive opening-age cohorts, and planned staffing. An opening cohort must be at least one whole day old: age zero belongs to target-day arrivals and would double-count them. It rejects cohorts whose total differs from the known opening stock, malformed target times, duplicate ages, and an inconsistent prior-day backlog. It combines the existing prior-only seven-day demand/capacity forecast with one day of the numeric FIFO engine, returning the predicted SLA completions and engine digest. The calculated input digest in this function is for deterministic replay only; it does not authenticate when or where the opening state was captured. The prospective store separately requires a queue label and a pre-outcome source artifact; an independent later ticket observation is still needed to score it.
duduclaw decision-shadow-sla-forecast --db <decision.db> --causal-db <causal.db> --opening-file <opening.json> --tenant <tenant> --acl <acl> --sla-id <id> --forecast-id <existing-shadow-forecast-id> --model-version <version> --source-lineage <lineage> --retention-until-utc <time> commits an SLA prediction during the frozen shadow policy’s issue window. The opening file has exactly inputs, opening_tickets, and prior_resolved_tickets. inputs contains target_day_utc, queue_id, opening_cohorts, and known; each opening ticket contains only ticket_id and created_at_utc. The prior resolved rows use TicketEvent with queue, ID, UTC creation, and UTC resolution. Preflight requires unique IDs across both sets, exact reconstruction of every opening-age cohort, and every prior resolution to occur in the seven complete days before the target. Each day’s resolved count must match the corresponding day in the frozen aggregate training export. It computes prior-day no-change, seven-day seasonal, and rounded seven-day-mean SLA baselines using the same age rule as the retrospective one-step backtest. These baseline predictions are saved with the prospective SLA forecast, before target-day outcomes. The store also checks queue, stock, staffing, model, policy deadline, source lineage and digests, and recomputes prediction and baselines on read.
duduclaw decision-shadow-sla-score --db <decision.db> --causal-db <causal.db> --observation-file <tickets.json> --tenant <tenant> --acl <acl> --score-id <id> --sla-forecast-id <id> --aggregate-score-id <id> --source-lineage <lineage> --retention-until-utc <time> scores it after day end. The independent ticket file has queue_id, target_day_utc, observed_through_utc, and tickets (TicketEvent rows with queue, ID, creation, and optional resolution time). It must include every opening ID with the same creation time and every target-day arrival exactly once. Resolutions must occur during the target day; ticket arrival, resolution, and closing-backlog counts must match the linked aggregate score. The stored SLA score binds the exact aggregate score revision, SLA forecast, ticket artifact and hashes; loading recomputes the model and three frozen-baseline absolute errors.
duduclaw decision-shadow-sla-score-correction --db <decision.db> --causal-db <causal.db> --observation-file <repaired-tickets.json> --tenant <tenant> --acl <acl> --correction-id <id> --sla-forecast-id <id> --previous-revision-id <id> --aggregate-revision-id <id> --reviewer <name> --reason <text> --source-lineage <lineage> --retention-until-utc <time> appends a reviewer-attributed replacement. The predecessor must be the current SLA head, and the aggregate revision must be the current aggregate head. Both conditions are checked again inside the SQLite transaction. Historical revisions retain their exact sources and digest links. duduclaw decision-shadow-sla-score-current --db <decision.db> --causal-db <causal.db> --tenant <tenant> --acl <acl> --sla-forecast-id <id> refuses a stale SLA head after an aggregate correction until a corresponding ticket correction is committed. Prior revoked sources may remain only as opaque predecessor audit digests; they are never used as active outcomes. Synthetic tests cover two successive corrections and revocation. This is engineering verification so far; upstream export authentication, real-data calibration, and multi-day measured SLA skill remain open.
duduclaw decision-shadow-sla-policy-assess --db <decision.db> --causal-db <causal.db> --tenant <tenant> --acl <acl> --policy-id <id> enumerates every completed UTC day in the predeclared policy window. Each day carries both the aggregate-day status and the SLA-day status (aggregate_unavailable, missing_sla_forecast, sla_forecast_invalid, sla_forecast_revoked, unscored, score_invalid, score_revoked, score_stale, cross_day_identity_mismatch, or scored). It reports the selected score revision and digest for valid days. Whole-window model and frozen-baseline absolute-error totals, plus strict model superiority to all three baselines, are present only when every due day has a valid current score; zero due days is incomplete. With at least 21 consecutive, fully scored days it also fixes a 90th-percentile absolute-residual radius from the first 14, evaluates later SLA counts against that unchanged band, and reports later observed coverage and misses in the most recent seven days. A three-of-seven miss signal flags possible drift. Recent seven-day baseline errors and skill are separate from whole-window totals. This is a forward diagnostic under an unverified exchangeability assumption, not a calibrated production interval or real-world skill claim.
duduclaw decision-shadow-sla-policy-screen --db <decision.db> --causal-db <causal.db> --tenant <tenant> --acl <acl> --policy-id <id> --min-complete-days <21..366> --min-fixed-coverage-bps <0..10000> [--save-run] applies explicit human-inspection gates to that SLA assessment. It requires every due day to be valid, enough days, strict whole-window and recent seven-day superiority over all three frozen baselines, a consistent fixed-prefix interval, minimum held-out coverage, and no recent drift signal. Adjacent scored days must carry the same unresolved ticket identities and creation times across the day boundary; the next opening source’s previous-day resolved rows must also match the prior observed-day source. When the previous day already has a valid ticket score, SLA forecast preflight and commitment reject a mismatched next opening source; otherwise the policy assessment checks the boundary after both days are scored. A later correction can therefore invalidate a previously complete assessment. A mismatch appears as cross_day_identity_mismatch and prevents a complete assessment. The screen reports each failed check and exact covered/evaluated/required day counts. --save-run stores the historical screen with exact training, opening, aggregate-observation, and SLA-observation source digests; duduclaw decision-shadow-sla-policy-load-screen --db <decision.db> --causal-db <causal.db> --tenant <tenant> --acl <acl> --replay-hash <hash> rechecks those sources and the historical score revisions without substituting later corrections. A passing screen permits human inspection only; it does not authorize a staffing change or claim calibrated population coverage.
Local decision-import-pilot reports the same diagnostic after verifying the canonical ticket/staffing source hash. For an export of at least 14 days, it reserves the larger of seven days and one third of the horizon as the later holdout, fits capacity using at least seven identifiable saturated training days, and reports the source digest and daily SLA error. An unidentified capacity leaves sla_holdout empty with an explicit sla_holdout_unavailable_reason; shorter exports likewise lack a valid split. Neither condition blocks an otherwise valid exploratory import. Available diagnostics are stored immutably with the exact scoped snapshot, model, baseline, source versions, and calculation-code digest. duduclaw decision-sla-holdout --db <decision.db> --causal-db <causal.db> --source-file <source.json> --tenant <tenant> --acl <acl> --record <id> --expected-sha256 <digest> rechecks the source and recomputes the entire saved assessment before returning it. The import and synthetic demo reports provide executable argument lists. Revoking the bound source blocks later reads. The record remains descriptive and cannot justify a live staffing decision by itself.
For snapshots linked to the causal evidence store, register each source with DecisionStore::with_causal_store(...).bind_causal_artifact(...). The artifact must have the same tenant and ACL as the snapshot, and the SHA-256 of its exact content must appear in source_version_hashes. Replay uses --causal-db <path> to verify every bound artifact is still active and within retention. Missing source access, a missing causal database, or a database without the bound record blocks replay without scrubbing the snapshot. A recorded source that is invalidated, erased, expired, or digest-mismatched revokes and scrubs dependent snapshots in the decision store. The synthetic demo creates and binds its source artifact, and its generated replay commands include --causal-db. Reads verify cross-store bindings; explicit admin invalidation or erase proactively propagates to trusted CCR and decision bindings. Automatic source-change events for other ingestion paths remain open.
For an explicit local revocation, run duduclaw decision-invalidate-source --db <decision-db> --causal-db <causal-db> --ccr-db <instance-ccr-db> --tenant <tenant> --acl <acl> --artifact <artifact-id>. The CCR database path is required; an absent file is created so a durable artifact-version tombstone can be written before the causal source changes. The command then invalidates the exact-scope artifact, demotes citing claims, and scrubs bound decision snapshots. It reports CCR and decision scrub counts and can be retried after a partial cross-database failure. A source invalidated through another path still blocks decision replay on the next snapshot read, but bound CCR handles need their own upstream revocation event. The command is local owner-operated.
The admin causal source inspector also exposes POST /api/causal/source/invalidate and POST /api/causal/source/erase with {tenant_id,acl,artifact_id}. Both require the gateway’s instance-admin JWT and an allowed Origin; the tenant and ACL are exact store selectors, not JWT claims. The gateway writes a durable tombstone for the trusted causal artifact/version in <home>/ccr/ccr.db before changing the causal source, then scrubs bound snapshots in <home>/decisions.db. A source version longer than CCR’s 512-byte binding limit has no possible bound CCR handle and skips that tombstone. If one database step fails, the request fails and the same request can be retried using the causal record’s retained version. Invalidation retains the causal source wording for audit but blocks access; erase clears that wording and cited excerpts while preserving opaque IDs and digests. The dashboard hides cached source and claim details before sending the request, keeps the source ID for retry after an error, and reloads claims and models after success. A third admin route, POST /api/causal/source/clear-revocation-fence, covers the opposite direction: it clears the fence an abandoned revoke left behind, and only that. It never reverses a revocation that took effect, and refuses with conflict once the artifact is invalidated or erased, while a CCR tombstone notice is still queued, or while any delivery lease for that version is recorded. See causal-evidence-graph.md for its exact contract and the equivalent duduclaw causal-clear-revocation-fence command. These endpoints cover explicit administrator actions; automatic propagation from unrelated connector changes still needs trusted source-change events.
The store records each snapshot’s source versions within the same tenant and ACL scope. Revoking a version scrubs every matching snapshot payload, blocks replay, and prevents a new snapshot from reusing that revoked version in the scope. An upstream source system still needs to call this revocation hook when its ACL, retention, or deletion state changes.
Snapshot source references and derived parameter-fit/run references are keyed by input kind. Opening an older decision database migrates its untyped source references from active, digest-verified input records before revocation proceeds. This prevents a snapshot and a parameter fit with the same ID but different sources from revoking each other.
The observational-effect read path also recomputes the estimate and diagnostics from the exact source without writing; a saved row that differs from the recomputation is refused. The brief exposes available permutation, pre-treatment placebo, negative-control, leave-one-unit-out, leave-one-stratum-out, and temporal-holdout diagnostics beside the effect estimate. These are descriptive falsification and stability checks. Passing them does not establish exchangeability or validate a simulated staffing effect.
Calibration and backtest gate
Section titled “Calibration and backtest gate”The first capacity fitter uses only days that end with backlog and whose staffed resolutions divide exactly by the number of agents. On those days, service is assumed saturated, so observed completed tickets can constrain the model’s integer per-agent throughput. A nondivisible saturated training day returns Unidentified instead of being rounded into a point. An accepted fit reports an observed range and median, not a confidence interval; too few exact observations also return Unidentified.
The rolling one-step backtest fits using strictly prior days and predicts each held-out day’s ending backlog using that day’s observed demand and staffing. It compares absolute backlog error with (1) no change from the day’s opening backlog and (2) the ending backlog seven days earlier. This is a diagnostic using known exogenous inputs, not a live demand forecast. It cannot validate SLA predictions because the observed daily totals lack ticket-level age and resolution data.
Before a recommendation is released, the pilot needs ticket-level time-split evaluation, parameter ranges and demand draws, prediction-interval calibration, drift monitoring, resource feasibility checks, shadow-mode comparison, and a measured intervention. Human approval and existing policy gates remain required for any operational action.
The exploratory sensitivity runner accepts explicit daily demand and service-capacity bands. It uses a versioned SplitMix64 draw stream and common draws for the two scenarios, then reports backlog-delta quantiles, backlog and cost violation rates, and the worst observed alternative backlog. The bands are uniform inputs supplied by an operator or later fitting process; their output quantiles are not calibrated prediction intervals.
An additional empirical resampling path fits only from a chosen training prefix. It collects arrivals from each training day and exact integer per-agent service observations only from saturated days whose staffed resolutions divide by the agent count. It independently resamples these finite pools with replacement using the snapshot seed and gives each staffing scenario the same arrival path and capacity draw in every run. The fit retains even a subthreshold number of exact saturated-day samples as partial evidence. Unsaturated days and nondivisible saturated days contribute separate per-day lower-bound records; neither is converted into a precise capacity. Too few exact samples, any nondivisible saturated day, or an unsaturated-day lower bound above the exact-sample support makes capacity unidentified. Simulation then requires an explicit positive-width operator range containing every exact sample and whose maximum covers every lower bound; it is labeled operator_range_uniform and remains an assumption. The report records both bound counts alongside sample counts, capacity source, replay hash, and common-draw hash. The synthetic demo reserves its final 14 days outside this fit. Daywise independent resampling ignores seasonality, autocorrelation, demand response, and parameter covariance; quantiles are stress diagnostics, not calibrated predictive intervals. Real historical calibration and held-out coverage remain open.
The decision store also persists the empirical fit as an immutable, scoped parameter input. On write it reconstructs the stored snapshot from the exact ticket/staffing source, recomputes the fit from the declared training prefix, and rejects supplied samples that differ. The record includes snapshot and engine digests; subsequent sensitivity reads the stored fit and rechecks the active source snapshot. Fit method training_prefix_empirical_resampling_v4 preserves lower-bound evidence from unsaturated days and nondivisible saturated days. The latter cannot be rounded into an exact integer per-agent capacity; it requires an explicit operator range with positive width before sensitivity simulation and is excluded from paired capacity draws. Older v2/v3 fits remain immutable and require a new fit ID under the active source instead of being silently reinterpreted. A source-version revocation scrubs both the snapshot and derived fit payload while preserving their opaque audit digests. The synthetic report includes the fit ID and stored payload digest.
The staffing policy sweep uses the same snapshot and two scenarios, then tests each integer per-agent capacity in a bounded range. Its explicit resource plan supplies available agents for every day, the maximum daily added agents, total agent-days, staff-cost budget, and ending-backlog target. A schedule is feasible only if all limits pass. Among feasible schedules, the rule chooses lower staff cost, then lower ending backlog, and keeps the baseline on a tie. The report includes every tested point, all capacity values where this choice changes, and a replay hash. A scoped store entry point loads immutable inputs and refuses replay after source revocation. It rejects fixed extra capacity because the current model has no price or resource definition for that lever. This is a deterministic stress test using recorded arrivals, not a calibrated recommendation or permission to change staffing.
The empirical report counts a joint constraint violation on each common draw when backlog, staff cost, or an explicitly supplied within-SLA target fails. It also counts paired draws where the baseline violates any limit and the alternative meets all of them. These rates preserve correlation between constraints; adding marginal violation rates would overcount overlapping failures. duduclaw decision-empirical-screen --db decisions.sqlite --tenant tenant-a --acl private --run-id run-1 --resource-plan-file resources.json --max-joint-violation-bps 2000 --min-joint-recovery-bps 5000 --min-sla-improvement-bps 5000 recomputes a resource sweep from the immutable run’s scoped inputs. Add --causal-db when the snapshot is bound to a causal source and --expected-hash to pin the screen result. The resource plan JSON uses the StaffingResourcePlan fields shown above. Its backlog and cost limits must equal the empirical run’s limits, and that run must have an explicit SLA target. The result lists each failed check and can mark an alternative as a candidate for human review. alternative_review_thresholds gives the minimum resource caps and the joint-risk criterion boundaries at which this fixed alternative could pass; all gates must hold together, and worse joint risk than baseline remains a fixed blocker. New output_integrity_version: 3 screens bind the displayed choice, failed checks, risk numbers, and limitations into their replay hash, which is rechecked on load. Historical version-0 screens retain their input-only hash for read-only audit; their displayed verdict is not independently bound and they cannot be attached to a new decision brief. Cost and backlog thresholds are diagnostic because changing those caps requires a new empirical run. It does not authorize a staffing action or claim calibrated risk probabilities.
Add --save-run to decision-empirical-screen to commit the exact screened result only after its expected hash passes. The demo saves its screen automatically and reports joint_risk_screen_manifest_id. duduclaw decision-empirical-load-screen --db decisions.sqlite --tenant tenant-a --acl private --replay-hash <screen-hash> reads that historical record without needing the original resource-plan file; add --causal-db for a causally bound snapshot. The immutable record embeds the plan, criteria, report, empirical-run digest, and source versions. Loading rechecks active source and input digests; source revocation scrubs it. The record preserves the screening engine version used at creation instead of silently substituting a later policy result.
Prospective shadow forecast journal
Section titled “Prospective shadow forecast journal”The decision store now exposes put_shadow_policy, put_shadow_forecast, and put_shadow_score as separate exact-scope operations. A policy must be registered before its first eligible UTC day, binds one source lineage and a separate expected queue ID, a nonoverlapping effective window, at least seven training days, a saturated-day minimum, the calibration code digest, and a forecast issue deadline no later than one hour after midnight. A reviewed handoff carries the same queue ID into the successor policy. A forecast requires an active policy plus a causal-store artifact of kind shadow_training_export whose content is a contiguous UTC ObservedOutcomeExport ending at the target day’s midnight. New forecasts require a nonempty queue_id in that export matching the policy and freeze it in the forecast record; historical policies, exports, and forecasts without one remain readable. The artifact must be ingested before the forecast is committed and after that training window ends. The forecast uses only that training prefix, the opening backlog, and planned staffing; it is committed inside the policy’s issue window. The policy digest, calibration-code digest, and prediction remain immutable.
Scoring requires a different artifact of kind shadow_observation_export, containing exactly the completed target day with matching opening backlog and staffing. Its source lineage and queue_id must match the committed training artifact’s lineage and frozen queue ID; the same checks apply to reviewed score corrections and to later reads. A different queue in the same tenant and ACL is refused even if a source is mislabeled with the same lineage. The store reserves one forecast ID per lineage and UTC target day, and one score ID per forecast, so later alternatives cannot be selected after seeing results. Each forecast reservation and payload now commit in one SQLite transaction. Each initial score reservation, payload, and revision audit commit together; a failed payload or audit check rolls back the reservation. These reservations remain if a committed source is later revoked. The observation’s source and ingestion times must be at or after the day’s end, and it must have been ingested after the forecast commitment. The score records absolute arrival/backlog errors against the frozen forecast and each simple baseline. Both records recheck exact source bytes and scope on reads, and source revocation scrubs their payloads. A later code version can still read the historical prediction without substituting a new computation. This is a storage and timing boundary; a live feed, CLI scheduling, operator provenance, and measured shadow run remain open.
For local operation, register a future policy with duduclaw decision-shadow-policy --db ... --tenant ... --acl ... --policy-id ... --source-lineage ... --queue-id <expected-queue> --effective-from-utc ... --effective-until-utc .... Its optional deadline and training settings are fixed at registration. duduclaw decision-shadow-forecast then accepts the policy ID, existing decision/causal databases, a completed training export file, target UTC day, planned staffing, opening backlog, source lineage, and retention deadline. Prospective shadow source files require a nonempty top-level queue_id; the training export must match the predeclared policy, and later observations must match the frozen forecast. The CLI checks queue ID syntax and these matches before registering a new artifact. The command registers the exact training bytes as a scoped artifact and commits the forecast within the policy deadline. After the day ends, duduclaw decision-shadow-score accepts a separate one-day observation export and the forecast ID, then registers and scores it. Both commands emit the artifact ID and immutable record as JSON. Scheduling, authenticating the upstream exports, and collecting a real prospective series remain operator integration work; historical files cannot be submitted as if the forecast had been committed in the past.
If calibration settings or code need a prospective handoff inside an existing policy window, the owner can run duduclaw decision-shadow-policy-supersede --db ... --tenant ... --acl ... --old-policy-id ... --new-policy-id ... --cutoff-utc ... --effective-until-utc ... --reviewer ... before the cutoff. The successor starts at a future UTC midnight within the original window and records both immutable policy digests and the reviewer. Existing forecasts before the cutoff retain the old policy. Target-day reservation checks the active window inside a SQLite transaction, so the old policy cannot claim a target on or after the cutoff; the handoff is rejected if that future target was already reserved. The successor uses the current calibration code digest. A completed handoff can be queried again with identical settings even after the cutoff; an unfinished handoff must be completed before it. This is a local owner review record, not a remote approval identity.
Reservations survive source revocation. If a committed observation proves faulty, duduclaw decision-shadow-score-correction --db ... --causal-db ... --observation-file ... --tenant ... --acl ... --correction-id ... --forecast-id ... --previous-revision-id ... --source-lineage ... --retention-until-utc ... --reviewer ... --reason ... appends a reviewed replacement from a separate exact-scope observation artifact. The original score and earlier corrections remain immutable. A correction must name the current revision; a transaction advances the head only when it still matches that predecessor. duduclaw decision-shadow-score-current --db ... --causal-db ... --tenant ... --acl ... --forecast-id ... resolves the latest valid revision and refuses a revoked latest source instead of silently falling back. New score revisions keep a metadata-only audit row with the forecast ID, commit time, and payload digest, allowing a later correction to cite an invalidated predecessor after its observation payload has been scrubbed. Legacy revisions whose payload was already scrubbed before this audit row existed cannot be reconstructed. The reviewer is a local owner-supplied audit label, and real source authenticity remains an operator control. Repeating policy registration with the same settings returns the original timestamped policy, even after its effective start; a changed setting is refused. Retrying a failed write with the same forecast or score ID is allowed when its immutable inputs still match.
duduclaw decision-shadow-policy-assess --db ... --causal-db ... --tenant ... --acl ... --policy-id ... enumerates every completed UTC day in the policy’s active window, including a window shortened by a reviewed handoff. It shows the policy’s expected queue ID beside source lineage and reports missing forecasts, unscored forecasts, revoked or invalid records, and corrected scores. A score correction now commits its immutable payload, revision audit, predecessor link, and current head in one SQLite transaction; a failed link or conflicting audit rolls all of them back. Absolute-error sums against the frozen forecast and three backlog baselines are emitted only when every due day has a valid current score; the report otherwise withholds aggregate errors and lists the gaps. Complete reports also show whether the model’s total backlog absolute error is strictly below each baseline. With at least 21 complete days, the report separately computes the same absolute-error sums and baseline skill for the latest seven scored days, so early wins cannot hide a recent regression. The first 14 scored residuals also set a fixed 90th-percentile radius, and later days expose observed coverage and the existing seven-day drift signal. A gap or revocation withholds all aggregate skill and interval fields. These are descriptive checks on the current journal, not an immutable historical as-of snapshot, a calibrated future interval, or an approval to act. Reservation compares UTC timestamps, so Z and +00:00 cannot reserve the same day twice.
duduclaw decision-shadow-policy-screen --db ... --causal-db ... --tenant ... --acl ... --policy-id ... --min-complete-days 21 --min-fixed-coverage-bps 8000 computes a current, read-only review screen from that exact assessment. It lists failed checks for missing due-day scores, too few complete days, whole-window or recent-seven-day backlog error not strictly below every baseline, missing or insufficient fixed-prefix coverage, and a recent drift signal. Its coverage evidence shows covered holdout days, evaluated holdout days, and the exact number needed to meet the operator threshold; a seven-day holdout therefore displays 6/7 explicitly instead of only 8,571 basis points. The screen rejects internally inconsistent interval counts, bounds, coverage, or drift flags. Previously saved screens without count evidence retain their original audit hash for read-only loading; current-review checks still require the current screen engine. The minimum day count must be 21–366 and coverage must be 0–10,000 basis points. A pass only marks a candidate for human inspection; these operator limits are not calibrated risk guarantees and do not authorize model promotion or staffing action.
Add --save-run to save a complete screen under its replay hash, including failed screens with valid daily evidence. The hash covers the criteria, assessment, engine digest, displayed status, failed checks, eligibility, and limitations. Incomplete windows remain available for read-only diagnostics and cannot be saved because the absence of a future or missing score cannot be source-bound as a complete result. duduclaw decision-shadow-policy-load-screen --db ... --causal-db ... --tenant ... --acl ... --replay-hash <hash> reads the historical screen with its original criteria, policy digest, selected score-revision digests, and training/observation source digests. It rechecks the exact source records and rejects revocation; a later score correction changes the current assessment but does not rewrite or redirect the old screen. A saved screen is an audit artifact, not a model-promotion approval.
duduclaw decision-shadow-screen-request-review --home <approval-home> --db ... --causal-db ... --tenant ... --acl ... --replay-hash <hash> --agent <agent> --summary <text> requests a time-limited human inspection only for a saved screen marked eligible for human review and still matching the current policy assessment and screen engine. duduclaw decision-shadow-screen-check-review takes the same home/database/scope/hash plus --approval <id> and returns its immutable receipt only after the exact broker payload, human decision, deadline, screen digest, policy digest, active source versions, and current score revisions pass. Pending, denied, expired, wrong-hash, revoked-source, and later-correction checks fail closed. The old screen remains loadable as an audit artifact after a correction, but its prior review receipt no longer verifies. The receipt means the candidate screen was inspected; it does not promote a model or authorize staffing action. CLI --home points at the existing local approval store and must exist.
duduclaw decision-shadow-sla-screen-request-review and duduclaw decision-shadow-sla-screen-check-review use the same arguments for a saved SLA screen. They use a distinct approval action and receipt kind, bind the exact SLA screen digest and policy, and recheck its source versions and current SLA score revisions. A failed screen, pending or mismatched decision, expired approval, later correction, or revoked source cannot produce a valid receipt. This receipt records human inspection of the SLA evidence only; it does not promote a model or authorize staffing action.
The deadline is bounded to the first hour of the target UTC day and is checked again on record reads. The shadow CLI validates each training export against the frozen policy and each observed-day export against the frozen forecast before registering a source artifact; the store repeats the same checks after resolving the exact scoped artifact. Both prospective readers reject undeclared fields inside daily rows, so target-day outcomes cannot be hidden in otherwise accepted records; the top-level export already rejects unknown fields. This reduces one input leakage path, but a forecast submitted after midnight can still be influenced by early target-day information outside the declared training export. Before calling this a measured shadow trial, the operator must verify upstream timestamps, export provenance, and actual issue times.
If the calibration code changes before a policy window ends, new forecasts under that policy fail closed because its engine digest no longer matches. Historical forecasts remain readable. The reviewed, nonoverlapping policy supersession path described above can register a successor for a future cutoff; uninterrupted live operation still requires an operator to perform that handoff before the cutoff.
The Decision Lab dashboard exposes the same aggregate (non-SLA) shadow journal as JSON endpoints, one per CLI command above, all under the same instance-admin gate and exact scope: POST /api/decision/shadow-policy/create and /load register and read a policy (dashboard_create_shadow_policy / dashboard_load_shadow_policy); /api/decision/shadow-forecast/create and /load commit and read a forecast, accepting either an existing training-artifact ID or inline training-source JSON; /api/decision/shadow-score/create and /load do the same for a day-end score against an observation artifact or inline JSON; /api/decision/shadow-policy/assess reads the current per-day assessment; /api/decision/shadow-screen/evaluate, /save, and /load compute, persist, and reload a review screen by its replay hash; and /api/decision/shadow-screen/review/request and /status file and poll a human-inspection receipt through the existing ApprovalBroker, bounding the summary to 240 characters and the TTL to at most one day. Every create endpoint is idempotent on an exact repeat (a retry with identical inputs and the same ID returns the already-committed record) and refuses a changed input under the same ID as a version conflict, matching the CLI’s own registration and re-registration behavior. No endpoint accepts or returns a historical backdated commitment; each write is checked against the policy’s live issue window at call time.
Post-run observation journal
Section titled “Post-run observation journal”Before a decision is issued, duduclaw decision-store-run --db decisions.sqlite --tenant tenant-a --acl private --snapshot forecast --model model --scenario base persists the current daily prediction under its replay hash. The manifest binds the exact snapshot, model, scenario, source versions, engine hash, and predicted daily result. The synthetic demo stores manifests for both reported scenarios. Later observation ingest reads this historical manifest; it does not rerun an upgraded engine to reconstruct the old prediction.
duduclaw decision-record-outcome accepts a separate JSON observation export after a forecast snapshot was recorded and its run manifest was saved. The export contains window_start_utc, observed_through_utc, and observed_days with the same fields as ObservedSupportDay, plus queue_id for a queue-bound snapshot. The UTC window must start on the first complete UTC day at or after the snapshot cutoff and end exactly after the supplied daily rows; later selected windows and future outcomes are rejected. The command requires the exact expected replay hash, an observation ID, a recorder ID, and the original snapshot/model/scenario IDs. It checks the queue ID, daily backlog conservation, zero-staff capacity bound, and that observed staffing matches the selected scenario. It stores immutable, scoped daily aggregates and an assessment with the SHA-256 of the exact export bytes, absolute-error sums for arrivals, resolutions, and backlog, and final backlog values. The journal also records the existing run’s local SQLite creation time and whether that time precedes the observation window; a historical backfill is marked false rather than presented as prospective evidence. Reads recheck those aggregates and the local timing evidence against the frozen replay. A changed export must use a new observation ID; the old record is never rewritten. Source-version revocation scrubs affected run manifests and observation payloads. The assessment is an observational forecast diagnostic and does not estimate a staffing intervention effect or authorize an action.
Example export:
{"window_start_utc":"2026-09-01T00:00:00Z","observed_through_utc":"2026-09-02T00:00:00Z","observed_days":[{"arrivals":4,"backlog_start":0,"resolved":2,"backlog_end":2,"agents":1,"fixed_extra_capacity":0}]}Use duduclaw decision-record-outcome --db decisions.sqlite --source-file observed.json --tenant tenant-a --acl private --observation obs-1 --snapshot forecast --model model --scenario base --expected-hash <replay-hash> --recorded-by reviewer-1. Add --causal-db to both commands when the snapshot is bound to a causal source. The local database and export file remain owner controlled; the tenant and ACL flags select a scope and are not remote authentication credentials. Verified external observation-source identity remains open; the local digest establishes exact bytes, not the source’s authenticity.
duduclaw decision-fit-outcome --db decisions.sqlite --tenant tenant-a --acl private --outcome obs-1 --fit-id fit-v2 --training-days 14 creates a new immutable capacity-fit candidate from the stored daily observations. It fits only the declared leading training days and uses only days ending with backlog; at least three saturated training days are required by default (--min-saturated-days can raise that requirement). At least three later days must remain held out. On those later days, it compares ending-backlog absolute errors for the frozen candidate capacity, the parent model capacity, and an unchanged-backlog baseline. Each error sum can be divided by the number of held-out days for MAE. The calculation supplies actual held-out arrivals and opening backlog to all three methods, so it checks the capacity assumption rather than live demand forecasting. The record binds the observed export digest, historical run hash, parent model digest, and fitter source digest. When the saved fitter digest matches the installed fitter, reads recompute the training fit and held-out errors from the stored observations and parent model; a changed numeric result is rejected even if its database payload digest was recomputed. Historical records from another fitter version remain audit records and are not thereby validated for promotion. Too little or unidentifiable training history fails instead of producing a number. A new candidate does not rewrite the parent QueueModel, auto-promote a staffing change, or claim validated prediction intervals. Source revocation also invalidates the candidate.
duduclaw decision-screen-outcome-model --db decisions.sqlite --tenant tenant-a --acl private --fit-id fit-v2 --min-saturated-days 7 --min-holdout-days 7 produces a read-only, replay-hashed model-inspection screen. It requires the installed fitter version, operator-supplied saturated-training and holdout minimums, strict held-out error wins over both the parent capacity and unchanged backlog, a queue ID, and evidence that the local run existed before the observation window. Defaults are seven days for each minimum; both accept 3–366. Each failed check appears by name. A passing screen permits human inspection only. Held-out arrivals and opening backlog are observed inputs, local timestamps and digests do not authenticate the upstream source, and no model is activated or staffing action authorized by this command.
duduclaw decision-propose-outcome-model --db decisions.sqlite --tenant tenant-a --acl private --screen-hash <saved-screen-hash> --candidate-id capacity-v2 saves a source-bound, simulation-only candidate from a currently eligible saved fit screen. It copies the parent model’s SLA and staffing-cost assumptions and changes only per-agent service capacity. The candidate lives under a separate outcome_model_candidate kind; it is never inserted into the operational model namespace. decision-load-outcome-model-candidate rechecks the fit, screen, model lineage, source versions, and proposal engine. duduclaw decision-compare-outcome-model-candidate --db decisions.sqlite --tenant tenant-a --acl private --candidate-id capacity-v2 --target-snapshot <later-snapshot> --scenario <staffing-scenario> runs both parent and candidate on the same later snapshot and staffing path. The target snapshot’s queue must match and its cutoff must be at or after the observed-window end; an older snapshot is rejected to prevent an ex-post fit from being presented as an as-of forecast. The report is model-assumption sensitivity, not a staffing intervention effect, model activation, or action approval. Add --causal-db when the target snapshot has causal-source bindings.
Add --save-run to that comparison command to store an immutable, source-bound outcome_model_candidate_run manifest. Its replay hash covers the comparison engine, exact candidate/snapshot/scenario payload digests, source-version union, and both simulation results. duduclaw decision-load-outcome-model-candidate-run --db decisions.sqlite --tenant tenant-a --acl private --replay-hash <hash> checks the manifest hash, re-derives the candidate and comparison from current active inputs, and rejects changed inputs or revoked sources. An engine change makes an older run audit-only until it is intentionally recomputed and saved under the new engine. This replayability does not establish prospective accuracy or authorize model promotion.
After a separately imported observation for the same later snapshot, parent model, and staffing scenario, run duduclaw decision-score-outcome-model-candidate --db decisions.sqlite --tenant tenant-a --acl private --comparison-run <saved-comparison-hash> --outcome <later-observation-id>. The command requires the parent daily run and saved candidate comparison to have local journal creation times before the observed window, with candidate creation between its training observation and the comparison. It saves a source-bound score with parent/candidate arrival, resolution, and backlog absolute-error sums over identical days; decision-load-outcome-model-candidate-score --db ... --tenant ... --acl ... --replay-hash <score-hash> revalidates the exact sources, comparison, observation, timing, engine, and score. An observation export may include a complete resolved_within_sla_by_day array together with sla_days matching the frozen parent model. Its length must equal the observation horizon and each count must not exceed that day’s resolved count; either missing member or a different SLA threshold is rejected. The producer must apply the simulator’s UTC day-bucket SLA rule to each resolved ticket; the aggregate validation cannot prove it did so. When supplied, the journal and candidate score report SLA absolute-error sums; otherwise SLA accuracy stays unset. Local SQLite timestamps and a digest of an imported export do not attest external forecast commitment or source authenticity. A candidate with lower error remains unactivated and cannot authorize staffing changes.
For a ticket-backed observation, add --ticket-source-file <support-pilot-export.json> --ticket-source-retention-until-utc <UTC> to decision-record-outcome. Retention uses whole UTC seconds, must end after the observed window, remain in the future, and be no more than 366 days after the window. The ticket export must name the same snapshot, scenario, queue, UTC window, and horizon, declare the canonical SHA-256 of its ticket and staffing rows, and reproduce every observed aggregate. The importer derives each resolved-within-SLA count from ticket creation and resolution day buckets using the frozen model’s sla_days; it rejects any mismatch with the observed export. The journal and exact ticket bytes are committed in one scoped SQLite transaction. On every ticket-backed journal read, the store checks expiry, verifies the retained bytes against their digest, and re-derives aggregates and SLA labels. duduclaw decision-scrub-expired-ticket-sources --db ... --tenant ... --acl ... proactively clears expired scoped blobs. Expiry or explicit source revocation scrubs the byte blob and invalidates the journal and derived score, including scores created before the ticket-source revocation index was added. This verifies internal consistency and replay of the supplied file; local possession of its bytes does not authenticate its upstream producer.
Decision Lab dashboard
Section titled “Decision Lab dashboard”config.toml [decision] enabled = false retires the whole surface, dashboard included: system.status reports decision_enabled: false, and the SPA then drops the Decision Lab row from the management rail, its 進階設定 index entry, its ⌘K command and its /app/system card, while the /app/system/decision-lab route itself stays reachable and states that the operator switched the line off (naming the key, since turning it back on is a host-side edit plus a gateway restart). The route is deliberately not redirected — a bookmark lands on that statement instead of a silent bounce that reads as a bug. A gateway too old to report the field is treated as enabled, so a missing field can never hide a live feature; the real gate stays the 404 middleware over every /api/decision/* route, not this presentation.
The admin-only /app/system/decision-lab dashboard reads the conventional <duduclaw-home>/decisions.db store through GET /api/decision/overview. The operator supplies the exact tenant and ACL store selectors; the gateway’s current JWT has no tenant claim, so only instance administrators may use the endpoint. The response contains digest-checked counts and selected identifiers for recent immutable artifacts, invalidation totals, and ticket-source retention status. It never returns stored payloads or ticket-source bytes. POST /api/decision/ticket-sources/scrub runs the same scoped expired-source cleanup as the CLI and returns a refreshed overview. The cleanup commits every expired source in one decision-store transaction. The UI binds cleanup to the loaded scope and clears it if either scope selector changes.
GET /api/decision/catalog lists bounded active snapshot, model, and scenario choices after digest and causal-source checks, plus complete verified synthetic pilot pairs. The dashboard selects a complete pair on reload instead of assuming the first two scenarios belong together. POST /api/decision/synthetic-pilot creates an idempotent 21–90 day synthetic FIFO fixture in the same decision and causal stores, with stable IDs, exact source bytes retained as a scoped causal artifact, a fitted illustrative capacity model, and two staffing scenarios. POST /api/decision/compare computes a fresh DecisionBrief for one snapshot/model and two scenarios. It optionally accepts a stored empirical_run_id, a linked policy_screen_hash, and reviewed observational effect_ids; the screen requires the run, while effect IDs may be supplied alone or alongside it. The store revalidates their exact scope, snapshot/model/scenario order, active source versions, and saved digests before attaching descriptive quantiles and the screen to the brief. The Dashboard accepts stored IDs, shows the verified fit/run and source digests, descriptive paired quantiles, screen choice, separately labeled observational estimates, limitations, and replay arguments. Linked observational estimates do not change simulation KPIs or establish an intervention effect. The dashboard also displays the observation window and ingestion time separately, highlights evidence ingested after the snapshot cutoff, and exposes negative-control/placebo imbalance, temporal and leave-one-out sign flips, positivity skips, and permutation counts so a displayed interval does not hide a refutation warning. Editing resampling assumptions clears the displayed derived result; a saved ID may be entered again for a fresh read. POST /api/decision/replay recomputes a selected scenario and requires its expected replay hash. All four endpoints require the same instance-admin JWT gate and exact scope. The dashboard shows scenario KPI differences, daily trajectories, source identifiers, replay status, and limitations. Fixed demand and fitted synthetic capacity make this an engineering sandbox, not a measured staffing effect or calibrated forecast. None of these calls promotes a model, approves a decision, or changes staffing.
Wire types for large integers
Section titled “Wire types for large integers”Every absolute-error sum leaves the gateway as a decimal string, on the CLI JSON and the dashboard endpoints alike. That includes the decision brief’s exploratory_forecast sums, which previously crossed as JSON numbers while the same stored quantity crossed as a string in POST /api/decision/forecast-validation — two views of one record that could not be compared without knowing which one you held. The stored records keep u128; changing them would rewrite every historical payload digest, so only the response shape differs. A JSON number is an IEEE-754 double in most readers and silently rounds past 2^53, so a reader must parse these with an exact big-integer type (BigInt in the dashboard), never Number. Bounded per-day counts, day counts, and basis points stay JSON numbers.
The same rule now covers every accumulated integer the decision brief reports, not just error sums. baseline, alternative, and delta (final_backlog, total_resolved, resolved_within_sla, total_staff_cost_cents), the exploratory_event ticket-event totals and their resolved_wait_seconds_p50/p95, and the resolved_wait_seconds_p95_delta are decimal strings; the delta values are signed and are subtracted in i128 before rendering, so the string is the exact difference rather than a rounded one. A null wait still means “no resolved ticket in that scenario” and never becomes "0". These totals accumulate over a caller-chosen horizon and nothing in the simulator bounds them, so they belong on the same side of the line as the error sums.
exploratory_sla_holdout.diagnostic used to embed the stored SlaHoldoutDiagnostic verbatim — the one hashed structure the brief exposed directly, and therefore the one place u128 sums still crossed as JSON numbers. It now carries the same wire mirror POST /api/decision/engineering-validation returns for that record, so one stored record has exactly one wire shape wherever it is displayed: daily_abs_error_sum, no_change_abs_error_sum, seasonal_naive_abs_error_sum, and training_mean_abs_error_sum are decimal strings, while observed_within_sla_by_day, predicted_within_sla_by_day, and the day counts stay JSON numbers. The mirror is built after the record has been reloaded, recomputed from the exact source bytes, and digested: record_sha256 and the store’s replay_hash are always taken over the stored u128 struct, never over this presentation type, so no stored digest moved.
Engine state of a stored run
Section titled “Engine state of a stored run”A response that carries a stored historical run also reports engine_matches_current: whether the installed simulator still carries the engine identity recorded with that run. Loading never recomputes a stored run — “historical” and “reproducible by current code” stay two separate facts — so this flag is the only signal a reader gets that a displayed prediction came from an earlier engine. It is derived at read time and is never part of a hashed payload. The dashboard capacity-fit (/api/decision/outcome-fit/*) and its review screen (/api/decision/outcome-screen/*) report it for the daily run they score, a fit whose run is stale also gains an explicit limitation line, and the candidate comparison KPIs report it for each simulated side. On the CLI the flag sits beside the record rather than inside it — {"record": …, "engine_matches_current": …} — which changed the top-level JSON shape of decision-record-outcome, decision-load-outcome-model-screen, and decision-event-load-run.
The synthetic pilot creator also binds the exact locally generated ticket/staffing artifact to the trusted local connector lifecycle journal. An instance admin can select that canonical pilot in Decision Lab and call POST /api/decision/synthetic-pilot/lifecycle with its exact tenant, ACL, seed, days, and one source-change kind. The server derives the target from regenerated fixture bytes and an existing binding, stages a same-transaction causal disclosure fence, and lets the background worker retry dependent CCR and decision cleanup. The UI clears its cached simulation views after staging and links to the tenant-scoped CCR dashboard, where pending and retained completed event counts can be inspected. This tests source removal and dashboard wiring with synthetic data; it does not receive or authenticate a production support-system event.
POST /api/decision/policy-sweep evaluates an operator-supplied staffing resource plan across a bounded per-agent capacity band. It reports feasibility at each capacity and the thresholds where the stated choice rule flips. POST /api/decision/sensitivity accepts explicit arrival perturbation, capacity band, draw count, backlog cap, and cost cap; it uses paired draws for the two scenarios and reports backlog delta quantiles and violation rates. The dashboard displays these inputs as operator assumptions and clears a report when its inputs change. Neither endpoint fits a demand distribution or turns a policy sweep into an intervention estimate.
POST /api/decision/engineering-validation accepts a selected snapshot and model and recomputes descriptive engineering diagnostics for it, but the request can resolve to either of two disjoint provenance branches, each with its own status string and its own response shape.
For a snapshot/model pair from a complete verified synthetic pilot pair, it reloads the scoped snapshot/model and active causal binding, reads the exact active synthetic_support_export bytes, checks their content/version SHA-256, reconstructs the canonical synthetic fixture, and confirms the active ticket and staffing rows match byte-for-byte before recomputing the rolling one-day forecast backtest, seven-day prefix capacity fit, fixed-prefix interval diagnostic, and ticket-level SLA holdout. Its status is synthetic_only_exploratory.
For a snapshot/model pair that instead matches a completed POST /api/decision/import-pilot receipt, it reloads that operator-uploaded export, verifies it against its own stored digest, and rebuilds the same family of diagnostics from those real-shaped (but operator-supplied and unauthenticated) rows. Its status is exploratory_operator_upload, and — unlike the synthetic branch, which withholds these aggregates when its own preconditions are not met — this branch’s response carries one_day_backtest (a per-day predicted-vs-observed arrivals/backlog series), fixed_interval_diagnostic (a per-day band and covered flag), and ticket_sla_holdout (observed_within_sla_by_day/predicted_within_sla_by_day, per-day vectors) whenever each has enough history to compute. None of the three ever includes a ticket ID, an opening-ticket list, or a per-ticket row — the response is bounded to daily aggregates — but a real customer-support day count and its daily SLA-compliance counts do leave the gateway in this branch, to the same instance-admin caller who uploaded them.
Both branches share the same envelope: the response includes source artifact/hash provenance, a composite validation engine SHA-256 over the dashboard, fixture, calibration, ticket/SLA, and queue-simulator source digests, the separate queue-simulator SHA-256, and a deterministic replay hash over the snapshot/model, exact source digest, displayed diagnostics/limitations, and these engine digests; generation time is excluded. Neither branch is persisted as an outcome, calibration record, or promotion. A fixed-prefix interval is null with a reason unless the backtest has more than 14 evaluation points after its 14-point calibration prefix (so 21–28 day pilots report it unavailable). Source and input state are checked again before returning, so concurrent revocation fails closed. The rolling backtest refits capacity from each prior prefix and is a retrospective as-of reconstruction from known target-day arrivals/staffing and opening backlog, not a score for the selected stored model or a pre-outcome committed shadow forecast. All fit, interval coverage/drift, and SLA comparisons are engineering diagnostics without real-data predictive or causal claims, on either branch.
Replay, policy sweep, and sensitivity reload their immutable inputs and causal-source bindings after calculation, before returning a result. A revocation completed during calculation therefore rejects that result. This final check narrows the cross-database race; it does not make a later revocation atomic with an already returned response. Historical results must be revalidated before reuse.
The Decision Lab also exposes the ticket-SLA shadow path under the same instance-admin gate and exact scope. POST /api/decision/shadow-sla-forecast/create commits one SLA forecast against a frozen aggregate forecast, taking either an existing shadow_sla_opening_export artifact ID or an inline opening export plus a future UTC retention deadline; /api/decision/shadow-sla-score/create does the same with a day-end ticket export once the target UTC day has ended. Both accept up to a two-mebibyte inline source under a dedicated request limit (2 MiB plus 64 KiB of JSON envelope headroom) rather than the 16 KiB decision-model cap; the server previews the payload against the frozen record before it registers a source artifact, and a write that fails afterwards erases only a source this request created and no committed row references. An exact same-ID retry with identical inputs returns the completed record; changed inputs or a changed retention deadline are refused as a version conflict. /load, /load-current, /api/decision/shadow-sla-policy/assess, /api/decision/shadow-sla-screen/evaluate, /save, /load, and /api/decision/shadow-sla-screen/review/request and /status complete the read path; the status endpoint reports pending, approved, denied, or expired only while the saved screen, active sources, current assessment, and broker payload all still match. Every response is bounded to counts, digests, identifiers, timestamps, and limitations: no ticket ID, opening ticket list, ticket row, or cohort row is returned, and source-parser failures are reported as a generic invalid request so an operator-controlled field name is never echoed back. Policy registration and reviewed handoff, SLA score corrections, upstream source attestation, and operational authorization remain outside this Dashboard path.
The Dashboard also exposes the CLI-only capacity-fit and candidate-model chain (decision-fit-outcome onward, described above) as JSON endpoints under the same instance-admin gate and exact scope. POST /api/decision/import-pilot accepts an operator’s own ticket/staffing export and registers it as a receipt this instance can later select in engineering-validation (see that section); it enforces the same size limit as the synthetic-pilot creator and never echoes a rejected field name back to the caller. /api/decision/outcome-fit/create and /load fit and read a leading-training-day capacity candidate from stored observations (dashboard_create_outcome_fit / dashboard_load_outcome_fit); /api/decision/outcome-screen/save and /load persist and reload the read-only inspection screen over that fit, and /api/decision/outcome-screen/review/request and /status file and poll its human-inspection receipt through the same ApprovalBroker pattern as the shadow screens. /api/decision/model-candidate/create proposes a simulation-only candidate model from an eligible saved fit screen; /load rereads it; /compare runs both parent and candidate on a later snapshot and scenario and saves the comparison run; /load-run rereads that run and rejects a changed input; /score scores the saved comparison against a separately imported observation; and /load-score rereads that score. Every create/compare/score endpoint rechecks its declared source versions, model lineage, and local-journal ordering (training before comparison, comparison before its observation) on every subsequent read, and an exact-ID retry with unchanged inputs returns the already-committed record rather than recomputing it. POST /api/decision/event-compare runs the ticket-event FIFO simulator (rather than the aggregate daily transition) for two scenarios on one snapshot/model and returns the same kind of descriptive KPI comparison as /compare. POST /api/decision/forecast-validation recomputes the synthetic demo’s rolling-forecast and interval-coverage diagnostics for a stored snapshot/model/scenario pair. None of these six families promotes a candidate into the operational model namespace, authorizes a staffing action, or establishes a validated production capacity — each is a replayable, source-bound inspection record for the existing human review path.
Add --save-run to decision-screen-outcome-model to store the exact review report with the fit payload digest, parent model digest, observation source version, snapshot sources, criteria, verdict, and replay hash. duduclaw decision-load-outcome-model-screen --db ... --tenant ... --acl ... --replay-hash <hash> revalidates the historical report and active sources. duduclaw decision-outcome-model-screen-request-review --home <approval-home> --db ... --tenant ... --acl ... --replay-hash <hash> --agent <agent> --summary <text> can request human inspection only for a saved report that remains eligible under the installed review engine; decision-outcome-model-screen-check-review adds --approval <id> to verify the exact time-limited broker decision, scope, report digest, fit digest, and active sources. Ineligible reports, pending decisions, wrong hashes, expiration, and revocation fail closed. This is an inspection receipt, not model activation or staffing authorization. The approval home must exist; add --causal-db when the snapshot is bound to a causal source.
Human pilot review
Section titled “Human pilot review”duduclaw decision-request-review --home <duduclaw-home> --db decisions.sqlite --tenant tenant-a --acl private --replay-hash <replay-hash> --agent support --summary "Review staffing pilot" files a pending request in the existing ApprovalBroker and stores an immutable link to the exact daily run in the decision database. The request has a bounded lifetime (default one hour, maximum one day). The existing human approval interface decides it. duduclaw decision-check-review --home <duduclaw-home> --db decisions.sqlite --tenant tenant-a --acl private --replay-hash <replay-hash> --approval <approval-id> succeeds only when that precise request was approved, remains within its lifetime, and the linked run is still accessible. Pending, denied, expired, cross-scope, mismatched, or revoked requests fail closed. Add --causal-db to both commands for bound snapshots. This review is a prerequisite record for a future pilot action bridge; it does not change staffing or bypass any existing action policy gate.
The admin Decision Lab now connects this path for one canonical synthetic run. After a scoped catalog load, comparison, and verification of both daily replays, choose the baseline or alternative, enter the inspection reason, and request review. POST /api/decision/pilot-review/request requires exact tenant, ACL, snapshot, model, scenario, expected 64-character replay hash, summary, and bounded TTL; the authenticated admin identity is the requester. The server revalidates canonical synthetic source bytes/model, recomputes the selected replay, saves an immutable daily run, and files a pending support_pilot_review request. The response includes the approval ID, pending state, and expiry. The existing approval inbox decides the request; the Decision Lab cannot self-approve it. POST /api/decision/pilot-review/status requires the same exact run plus approval ID and returns pending, approved, denied, or expired only after the stored link, broker payload, source binding, run digests, and a fresh current-engine simulation match. A stored run with a recalculated database digest but changed result fails this comparison; an engine change retires the old receipt for current inspection while preserving its historical audit record. A prior approval becomes unusable after source revocation or expiry; its displayed state is only current at the last check. No endpoint performs a staffing action or promotes a model. The dashboard uses synthetic data only; no real customer-support outcome has yet been validated.
The Inbox shows an internal Decision Lab link only when the broker payload names a canonical synthetic fixture, scenario, SHA-256 replay hash, exact scope, and UUID approval ID. The model ID is derived from that fixture’s canonical identity; malformed payloads produce no link. Opening it as an admin reloads the scope, checks the active catalog pair, recomputes the comparison, asks the server to verify both replay hashes, then checks the exact approval status before rendering trajectories. The application route guard redirects non-admin users, and the API also requires admin authentication. The link never decides the approval.
Because only admins can open Decision Lab, the generic approval inbox and channel decision handlers also require an admin for support_pilot_review. A mapped manager, employee, unknown channel identity, or solo destination-only press leaves it pending; a verified admin may decide. The dashboard refuses a decision when its broker record cannot be read, and the kickoff channel path accepts only goal_kickoff records, so a forged kickoff-form action cannot decide a pilot review. This restriction applies to dashboard and channel decisions, while the local owner CLI remains a separate trusted maintenance path. Approval records state a human decision after exact-run access was made available, but cannot prove that the reviewer read every trajectory row.
Synthetic engineering fixture
Section titled “Synthetic engineering fixture”No real support data is available for this pilot yet. synthetic_support_export(seed, days) generates a reproducible ticket-level FIFO queue named synthetic-support-queue with 10 initially open tickets, 18–22 arrivals per day, two staffed agents, and exactly 16 daily resolutions. Its known per-agent capacity is eight. Every ticket and staffing row carries that queue ID, and the fixture hashes those exact rows as the source version. The fixture uses a fixed UTC window and is intended to verify that import, conservation, capacity fitting, rolling backtest, simulation replay, scenario comparison, sensitivity, ACL scope, and source revocation agree on one dataset.
Run duduclaw decision-demo --db /tmp/support-demo.sqlite --seed 47 --days 35 > /tmp/support-demo-report.json to create a persistent local fixture. The command requires new decision, causal, and source-file paths. It writes the exact compact JSON source array [tickets, staffing] to /tmp/support-demo.source.json, creates /tmp/support-demo.causal.sqlite, and binds its artifact to the snapshot. The report includes the source SHA-256 and artifact ID, the fitted capacity, empirical training-prefix samples and paired sensitivity with a replay command, rolling backtest, exploratory decision brief, event-queue baseline/alternative results and replay commands, the operator-band sensitivity run, policy sweep, joint-risk review screen, and limitations. Its event queue assumes a 09:00–17:00 UTC shift with eight equal service slots per agent. The screen’s risk thresholds and within-SLA target are illustrative synthetic inputs, not operator-approved limits. On Unix, the databases and source file are owner readable only. The brief’s replay.baseline_command and replay.alternative_command contain complete daily-engine duduclaw decision-replay arguments, including the causal database path and expected hash. The accepted duration is 21–366 days to retain a rolling backtest window.
The demo also stores immutable event-run manifests for both scenarios and reports their replay-hash IDs. decision-event-replay --save-run stores a manifest for another verified event replay, while decision-event-load-run --db ... --causal-db ... --source-file ... --tenant ... --acl ... --replay-hash ... reads a historical result. Loading requires the exact original export bytes and verifies the snapshot, model, scenario, baseline scenario, source, and causal binding. It preserves the recorded engine result rather than silently recomputing it after an engine change. Revoking the source scrubs the manifest with the other derived records.
The synthetic backtest is an implementation check because the data generator uses the same FIFO capacity rule as the simulator. A zero synthetic error or a favorable scenario delta is not evidence of real forecast accuracy, causal staffing impact, or a calibrated uncertainty interval. Real time-split validation, demand forecasting, operator review, shadow mode, and measured intervention remain open.
The demo also emits forecast_backtest, a rolling one-day-ahead diagnostic. At each target day it fits capacity using earlier days and predicts arrivals from the prior seven days; it uses only the opening backlog and scheduled staffing known for that day. Its point records expose predicted versus observed arrivals and backlog, and its aggregate errors compare the queue model with no change, weekly seasonal naive, and a seven-day mean backlog-change baseline. This is a synthetic test of the time split and forecast plumbing. It does not establish forecast skill on real demand, a calibrated interval, or a multi-day policy forecast. The separate backtest field deliberately uses actual arrivals to isolate capacity-fit error.
When the forecast backtest has more than 14 points, the demo adds two separate diagnostics. interval_diagnostic forms each target day’s symmetric backlog band from the ranked absolute residuals of the preceding 14 forecast points only. fixed_interval_diagnostic calibrates once on the earliest 14 forecast residuals and holds that radius fixed for all later points, exposing later coverage loss without adapting to those outcomes. Both report observed coverage and a descriptive drift signal if at least three of the latest seven points fall outside their bands. A 21–28 day demo returns null for both fields because there is insufficient prior forecast history. Synthetic demand and changing conditions provide no formal future coverage guarantee; the decision brief’s uncertainty_interval remains null.
The demo saves these forecast diagnostics as an immutable forecast_validation record and reports its ID, payload SHA-256, and structured duduclaw decision-forecast-validation command. The record binds the exact source export digest, snapshot and baseline scenario digests, window start, training settings, and calibration source-code digest. The command requires the exact local export file, rechecks the expected SHA-256, and prints the recomputed record. Changed bytes, source revocation, or a changed implementation cannot silently serve the old assessment. This verifies synthetic engineering provenance only; real-data interval coverage remains open.
The prospective forecast_next_day API accepts only a validated observed training prefix, the target day’s opening backlog, and planned staffing. It rejects an opening backlog that does not match the preceding close. It returns a point forecast plus no-change, seasonal-naive, and mean-change baselines before target-day arrivals or outcomes are supplied. The rolling backtest calls this same API for each target day and scores the result only afterward. A live shadow runner must still commit each forecast before the actual day is observed; this API alone is not a shadow-mode evaluation.
The test-only C7 shadow validation guide gives one cargo test command for an isolated, deterministic 21-day backlog and ticket-SLA sequence. Each day writes only prior training and opening-ticket sources, checks that no target-day observation source exists, commits both forecasts, and then registers the later aggregate and ticket outcomes. The same store engines score all days and produce assessments and review screens. The harness also checks missing-day, paired correction, source-revocation, and saved historical-screen behavior. It prints a content-free synthetic_test_only JSON summary. Its historical ingestion times are set only inside #[cfg(test)]; production time gates are unchanged. The Decision Lab synthetic engineering validation remains the dashboard view for a local synthetic pilot. Neither validation is a real pre-outcome shadow run or evidence of calibrated operational performance.
An admin can inspect the current declared shadow window with GET /api/decision/shadow-monitor?tenant_id=...&acl=...&policy_id=.... The handler requires admin authentication, uses the exact requested tenant and ACL scope, loads the policy through the DecisionStore bound to the causal database, and recomputes aggregate and ticket-SLA assessments at one captured time. Because SLA assessment re-reads aggregate evidence, the handler re-runs both full assessments and requires each to match its first pass, including score revisions and metrics; it checks that SLA’s embedded aggregate day statuses and targets match too. Two reads of the active reservation window and a final policy reload check the displayed window against a concurrent reviewed handoff; the response uses the active cutoff rather than the immutable policy’s original end. Inconsistent reads fail closed. The direct JSON response is a bounded descriptive_shadow_monitor summary: assessment time, policy ID/queue/window, and per-lane due/scored/corrected counts, completeness, nullable whole-window and recent skill, fixed-interval availability, nullable drift, and fixed-key day-status counts. It omits ticket rows, source artifacts, per-day IDs, replay hashes, and forecasts. The endpoint stores no monitor report and cannot promote a policy or authorize staffing. An empty future window has zero due days and null skill. These are current descriptive assessments, not calibrated or operational performance evidence; the 21-day end-to-end case is still synthetic and test-only.
The admin Decision Lab accepts an exact shadow policy ID and shows separate backlog and ticket-SLA progress, missing/unavailable/revoked counts, nullable baseline-skill and drift signals, and the current effective window. Changing the selected scope or policy clears the prior result.
Current code entry points
Section titled “Current code entry points”duduclaw-gateway/src/decision_sim.rs: deterministic queue transition.duduclaw-gateway/src/decision_event.rs: ticket-event FIFO simulation with explicit shift slots and exact source verification.duduclaw-gateway/src/decision_store.rs: scoped immutable inputs and replay.duduclaw-gateway/src/decision_brief.rs: exploratory scenario comparison.duduclaw-cli/src/decision_cmd.rs: persistent synthetic demo, local replay with expected-hash validation, and explicit event-run storage/loading.duduclaw-gateway/src/decision_calibration.rs: saturated-day fit and rolling diagnostic.duduclaw-gateway/src/decision_sensitivity.rs: common-draw sensitivity and constraint rates.duduclaw-gateway/src/decision_empirical.rs: training-prefix empirical parameter draws and paired exploratory sensitivity.duduclaw-gateway/src/decision_policy.rs: bounded capacity sweep, explicit staffing feasibility, and policy flip thresholds.duduclaw-gateway/src/decision_ingest.rs: ticket export adapter and daily stock reconstruction.duduclaw-gateway/src/decision_synthetic.rs: fixed-seed synthetic export and end-to-end verification.
First real data source: the DuDuClaw task board
Section titled “First real data source: the DuDuClaw task board”Until 2026-09-29 every pilot this line could replay was either synthetic or an operator-uploaded file whose upstream nobody could check. duduclaw-gateway/src/decision_task_board_export.rs is the first feed that reads something the product itself produced.
| Contract field | Task-board source | Status |
|---|---|---|
tickets[].ticket_id |
tasks.id |
measured |
tickets[].created_at_utc |
tasks.created_at |
measured (normalized to UTC) |
tickets[].resolved_at_utc |
tasks.completed_at, null while open |
measured |
queue_id |
task-board:<assigned_to>, or task-board:all |
measured |
staffing[].agents |
the agent’s agent.toml [heartbeat] max_concurrent_runs |
proxy |
staffing[].fixed_extra_capacity |
always 0 |
not applicable |
staff_cost_cents_per_agent_day |
always 0 |
not applicable |
sla_days |
fixed at 2 |
stipulated definition |
Only the agents row is not a measurement. A support queue’s agents means staffed human capacity for that day; the task board has no such series. max_concurrent_runs is the nearest structural analogue, it is a static setting, and the exporter repeats today’s value across every historical day. Any conclusion that depends on the staffing series varying over time is therefore unsupported by this source, and the whole-board queue sums the per-agent values. The SLA definition is stipulated, not agreed: “closed on the same or the next UTC day” is what the board’s own columns can express, and it reuses the ticket day-bucket rule defined above (resolution_day - creation_day < sla_days) without change.
The window is always whole days ending at the most recent complete UTC midnight. A partially elapsed final day would report a fabricated drop in arrivals, so it is refused rather than truncated.
source_version_hashes carries the canonical sha256(JSON [tickets, staffing]) the importer recomputes — that digest is what binds the stored snapshot to the source artifact, so it cannot hold anything else. The provenance (the tasks.db file digest plus the exporter version) travels in source_lineage as task-board-export@<sha256>, which is the field the contract reserves for it.
Entry points:
POST /api/decision/task-board-export(admin-only, like every/api/decision/*route). Optionalqueue/horizon_daysoverride this run; tenant, ACL, retention and the fixed “+1 agent” alternative come from config, so the endpoint cannot mint a pilot under an arbitrary scope.duduclaw decision-task-board-export --queue <agent|all> --horizon-days <n> [--db <tasks.db>] [--out <file>].config.toml [decision] task_board_shadow = false(default) plustask_board_shadow_every_hours/task_board_retention_days/task_board_horizon_days/task_board_queuerun it on a schedule. It is gated twice: by that key and by the[decision] enabledkill switch.
Each scheduled period exports, imports through the same import_operator_pilot path a manual upload uses (so the receipt, the artifact binding, the replay hashes and the held-out SLA diagnostic are all existing, reviewed code), then computes one prospective SLA nowcast for the day that started at the cutoff. That nowcast is recorded in decision_task_board_runs.jsonl and is not committed as a scored shadow_sla_forecast: that store requires an operator-created shadow policy, a prior shadow_forecast and an opening-stock artifact ingested after the target midnight — a pre-registration chain a background job must not fabricate on an operator’s behalf. Scoring this feed against outcomes still goes through /api/decision/shadow-sla-*, with a human choosing the policy.
Odoo Helpdesk / Project as a data source
Section titled “Odoo Helpdesk / Project as a data source”duduclaw-odoo/src/support_export.rs maps helpdesk.ticket (Enterprise) or project.task (Community) onto the same contract; duduclaw-gateway/src/decision_odoo_export.rs assembles the export and resolves the per-agent credential.
- Creation and closure are measured. Odoo stores naive UTC datetimes (
"2026-09-01 08:30:00"); conversion to RFC3339 happens in exactly one function, and an unparseable value drops the row (counted inskipped_rows) rather than being dated “now”. helpdesk.ticketcloses onclose_date.project.taskcloses ondate_endwhen it is set, otherwise ondate_last_stage_updatebut only while the task sits in a folded stage (project.task.type.fold = true, Odoo’s own convention for “this column means done”). Usingdate_last_stage_updatealone would report every touched task as closed; an unknown stage set therefore closes nothing.staffing[].agentscounts the distinct assignees who created or closed a ticket in that queue that day, with a floor of 1. This is a proxy: Odoo has no staffing roster.fixed_extra_capacityis always0.- Only
id, the timestamps, the queue many2one and the assignee are read. No subject, description, customer or email field ever enters a pilot source artifact. - Rows are paged 500 at a time (
MAX_TICKETS200 000) over the windowcreate_date < until AND (closed IS NULL OR closed >= since), which is exactly the set that can produce an arrival, a resolution or an opening-backlog cohort. queue_idisodoo:<model>:<team_or_project_id>;source_lineageisodoo-export@<canonical digest>.
Entry points: POST /api/decision/odoo-export and duduclaw decision-odoo-export --agent <id> [--profile <p>] --model helpdesk.ticket|project.task --queue <id> --since <utc> --until <utc> [--out <file>]. Both connect with the agent’s own agent.toml [odoo] override merged over the global [odoo] block, resolving the credential through the shared config_crypto path (so a secret:// reference resolves and is never sent to Odoo as a literal). An explicit --profile must match the agent’s own [odoo] profile; a mismatch is refused rather than silently falling back to another identity.
Neither adapter imports anything by itself: both produce a file for an operator to review and feed to /api/decision/import-pilot deliberately. An adapter that both read a customer’s ERP and committed a pilot would be the “passed a screen ⇒ promoted” slide this spec exists to prevent.
The remaining implementation work is tracked in the TODO list.