Skip to content

Checklist auto-verification: scalability plan (phase 2)

Status: first tranche built on branch feat/checklist-auto-tracking (see section 15, read it first). Sections 13 and 14 are current; where §2, §3 or §12 differ, they win. Follows phase 1 (2026-10-08-accounting-checklist-spec.md, PR #33).

A checklist row can be tied to real work in the ERP. The accountant opens the row, lands on the page where the work is done, does it, and the row proves itself complete from the real data. Rows that cannot be seen by the system stay manual and are shown as manual. A user-written description never decides whether a row is verified; the attached signal does.

1.1 Phase 1 baseline this plan builds on (as merged in PR #33)

Section titled “1.1 Phase 1 baseline this plan builds on (as merged in PR #33)”
  • Page: Workspace > Checklist (/checklist, /checklist/{runId}), permissions checklist.read/work/manage.
  • “Done” today means the row has a Performed Date; N/A counts as complete in the progress bar and section counters. Nothing is verified. Phase 2 adds the verified/manual split to these counts.
  • Managers edit text (category, description, deadline, headings, section titles) in place; structure changes use “Edit layout”. Cells grow to their text. Rows, history (checklist_event), activity_log and the hash-chained audit_log already exist and are reused unchanged.

2. Three kinds of row (kinds of outcome; there is one row type, see §13)

Section titled “2. Three kinds of row (kinds of outcome; there is one row type, see §13)”
Kind Example Tracking
Signal row Prepare bank reconciliation Verified from data. Performed date is set by the system.
Manual row Communicate the closing timetable Recorded (who, when, audit log), optionally with evidence. Never “verified”.
Custom row Added by a user Manual unless the user picks a signal from the list.

Rows carry a badge (verified by system / manual) so a manager can see how much of a run is proven and how much is claimed.

3. Data model (superseded by §13.2: one track field, not signal_key/signal_params)

Section titled “3. Data model (superseded by §13.2: one track field, not signal_key/signal_params)”

On the row (and on the template row, so it copies into every run, as everything else does):

  • link_route (text, nullable): app route with placeholders, e.g. /banking/reconciliation?to={period_end}.
  • signal_key (text, nullable): key into the signal registry. Null = manual.
  • signal_params (jsonb, default {}): per-row options, e.g. {"account_group": "bank"}.
  • verification (enum: none, satisfied, unsatisfied, error) and verified_at, verified_basis (jsonb: the numbers or ids the check used). Cached result, never the source of truth.

New table checklist_signal_state(org_id, run_id, item_id, state, checked_at, basis) is the alternative if the cache should not touch the item row (avoids version bumps and stale-write conflicts for users editing the same row). Recommended: use this table; leave item.version for human edits only.

Nothing per-signal lives in the checklist schema. Adding a signal adds no migration.

type Signal struct {
Key string
Label i18n.Text
Params []ParamSpec // what a row may configure
Check func(ctx, tx, org, run Run, p Params) (Result, error) // pure read, no writes
Triggers []string // eventbus kinds that make a re-check worthwhile
Route string // default link_route
}
  • Each module registers its own signals in init(), the way permissions are registered today. The checklist module imports none of them. Dependency direction stays modules to platform to kernel.
  • Check is a read-only function of the data and the run’s period. It is idempotent and safe to call any number of times. This is the single most important scalability property: correctness never depends on delivery of an event.
  • The UI lists registered signals in a dropdown. A key that is no longer registered shows as “unavailable” and the row falls back to manual instead of breaking.

5. Evaluation: pull first, events as an accelerator

Section titled “5. Evaluation: pull first, events as an accelerator”

Only some modules publish eventbus events today (documents, banking statement import, workflow, payables batch and a few others). Period close, reconciliation lock and posting runs may not. So the design cannot rest on events.

  1. Pull (always correct): a worker job evaluates open signal rows. Cadence: every 5 minutes for runs that are open and were viewed or changed in the last 24 h; hourly for other open runs; never for locked runs or templates.
  2. Push (fast): each signal lists Triggers. When one of those events fires, the checklist enqueues “re-check signal X for org O” into the same job queue. De-duplicated by (org, signal) within a short window, so a burst of 500 postings causes one check, not 500.
  3. On demand: opening a run page checks its visible signal rows, and a “Re-check” button exists per row. Both go through the same code path as the job.

Adding event triggers to a module later is an optimisation only; nothing breaks without them.

Numbers to design for: 1,000 orgs, about 85 rows per run, about 40 linked, 1 to 3 open runs per org. That is about 40,000 to 120,000 signal checks per pass.

  • Batch by signal, not by row. Many rows and runs in one org share a signal and period. Evaluate once per (org, signal_key, params, period_end) and fan the result out to every matching row. Typical cost drops from 40 checks per run to the number of distinct signals.
  • Cheap checks. Each Check must be one indexed query, budgeted at under 50 ms, with a per-check statement timeout. A check that exceeds it records error and backs off (exponential, capped at 1 h).
  • Per-org fairness. Jobs are keyed by org and rate-limited per org, so one large tenant cannot starve the rest. Use the existing kernel/jobs queue and RLS context per job.
  • No locks on business tables. Checks read at normal isolation and never hold anything.
  • Write amplification. State is written only when it changes (satisfied to unsatisfied etc.), so a steady-state pass is read-only.
  • Growth in rows or signals costs one registry entry per signal; run size growth costs linear work in distinct signals, not rows.

7. Behaviour rules (what a manager can trust)

Section titled “7. Behaviour rules (what a manager can trust)”
  1. A signal row is satisfied only if Check says so. The system then sets Performed Date to the date the condition first became true, not the date of the check.
  2. No early manual tick on a signal row. A manager may override with a reason. The override is logged and the row is badged manual override.
  3. Un-satisfied later: if the work is reversed (period reopened, statement voided), the row keeps its history but shows no longer satisfied and counts as open in progress. It is not silently unticked and not silently kept green; a person decides, with a reason.
  4. N/A still works with a reason, as in phase 1.
  5. Every automatic change writes a checklist_event (auto_satisfied, auto_unsatisfied) with the basis (the ids and counts the check used) and the actor who caused it when known, else system. Sign-offs still go to the hash-chained audit log.
  6. Locked runs are never re-evaluated; their state is frozen at lock time.
  7. A signal row’s check uses the run’s period (period_end). A run without one cannot use period-based signals; the row shows “set the period end”.
  • Evaluation runs as a system actor inside the org’s RLS context, never as a user, and reads only what that org owns.
  • A user can only attach signals whose own module permission they hold. Seeing “verified” does not reveal data from a module the viewer cannot read: only state and a short, non-sensitive basis are shown.
  • Setting or changing signal_key on a row needs checklist.manage and is logged.

9. Candidate signals for the default 19 sections

Section titled “9. Candidate signals for the default 19 sections”

First wave (the module already holds the facts): fiscal period closed, bank reconciliation locked through period end, payroll run posted, depreciation run posted, FX revaluation posted, journals all posted (no drafts in the period), sub-ledger control accounts agree with GL, intercompany balances agree, inventory count posted, trial balance balances. These cover most of sections B, C, D, F, I, J, L, N and parts of A, E, H, P.

Stays manual: confirmations, document collection, filings made outside the ERP, management review, audit preparation and board approval. Phase 2 adds optional evidence (attachment or note) for them.

Failure Result
Event lost or module publishes none Pull job catches it within its cadence.
Check throws Row state error, shown as “could not verify”, retried with backoff. Does not tick and does not untick.
Signal removed in a release Row becomes manual with a visible note.
Slow or large tenant Per-org queue and statement timeout isolate it.
Backup restore or late data import Next pull corrects state.
  1. Done before phase 2: description and heading boxes grow to their text; columns have minimum widths.

  2. Migration: row columns (or checklist_signal_state), event kinds. Registry package in platform, no signals yet. Tests: registry, de-dup, state transitions, locked runs skipped.

  3. Worker job (pull) plus on-demand evaluation on run open and per-row Re-check.

  4. First three signals end to end (fiscal period closed, bank reconciliation locked, payroll posted). Link button, badge, activity entries, i18n en/zh/ms.

  5. Event triggers for the modules that already publish events.

  6. Signal picker in the row dialog, evidence on manual rows, remaining first-wave signals.

  7. Load test: seed 1,000 orgs, assert one pass stays within budget and per-org fairness holds.

  1. Actor on an auto-tick: the member who did the work when known, else system.
  2. Link: a “Go to page” button on the row, not a clickable row, so editing text is never ambiguous.
  3. A manager override of a signal row is allowed with a reason.
  4. Signal state lives in checklist_signal_state, not on the row.

13. Revision: one setup, tracking is optional (supersedes §2, §3, §12 where they differ)

Section titled “13. Revision: one setup, tracking is optional (supersedes §2, §3, §12 where they differ)”

There is a single kind of row. Tracking is an optional attachment, chosen in the same dialog that sets the row up, and changeable at any time.

Add or edit row:

  1. Description (free text, always editable).
  2. Tracking (one choice): Manual (default) or Track automatically.
  3. If Track automatically: pick a source, add conditions, pick done when (exists / none left / all done / totals agree), optional link page. Picking a source from the catalog pre-fills the description (still editable) and the link page.
  4. Save. The row shows a badge: manual, verified by system, or ready to review.

The same dialog works for a column: type Auto value plus a source gives a live value per row (status, count such as “2 of 3 reconciled”, or an amount) with a link; any other type is manual input.

Switching mode later never loses data:

  • Manual to tracked: the description and any ticked date stay; the rule starts checking.
  • Tracked to manual: the rule is detached, the last verified date stays as an ordinary entry, and the change is logged with a reason.

On the row (and template row) a single nullable track jsonb:

{ "source": "bank_reconciliation", "where": [{"field":"status","op":"eq","value":"locked"},
{"field":"period_end","op":"eq","value":"{run.period_end}"}],
"done_when": "all_done", "over": "bank_account.active", "link": "/banking/reconciliation" }

NULL means manual. No separate table per mode, no per-signal columns, no migration when a source is added. The cached result stays in checklist_signal_state (state, checked_at, basis), never on the item. Custom columns of type auto store the same track object on the column.

13.3 Scalability points specific to this combined design

Section titled “13.3 Scalability points specific to this combined design”
  • One code path for manual and tracked rows. The row is the same object; only track differs. Counters, filters, history and locking need no tracked/manual branches.
  • Rules are data, evaluated by one engine. Cost grows with the number of distinct (org, source, conditions, period) combinations, not with rows or runs (see §6). Hash the normalised track object to dedupe checks across rows, runs and templates.
  • Catalog is declarative. Each module registers its sources (entity, filterable fields, sum fields, default link, required permission). The dialog, validation and engine all read the same declaration, so a new source needs only its registration.
  • Validation at save time. A rule is rejected if the source is unknown, a field or operator is not on the source’s allow-list, there are more than 5 conditions, or the user lacks the source’s read permission.
  • Versioned rules. track carries v; the engine keeps old versions readable, so catalog changes do not break saved rules. A source that disappears makes the row fall back to manual with a visible note.
  • Templates and runs. Starting a run copies track with the row. Placeholders ({run.period_end}) resolve per run, so one template serves every month and company.
  • Bulk setup. Selecting several rows and choosing “Track as…” applies one rule to all of them in a single commit; large templates (hundreds of rows) are set up in one pass.
  • Observability. Per-org counters for checks run, errors, slow checks and rows verified, so a bad rule or source is found before users report it.
  1. Mode is a toggle in one dialog; default Manual.
  2. Editing a tracked row’s wording is allowed; a “Tracked: ” label is always shown beside it.
  3. Manager override of a verified row needs a reason and is badged.
  4. State in checklist_signal_state; the rule in track on the row or column.

Grounded in what the repo already has: River job queues (kernel/jobs), the in-process eventbus, RLS per org, checklist_event (insert-only, indexed by run and by item), activity_log, and the hash-chained audit_log, whose writes take a per-org advisory lock (pg_advisory_xact_lock(audit-chain, hashtext(org))).

Dimension Target (design for 10x of this) What breaks first if ignored
Orgs (tenants) 5,000 Job fan-out per org, noisy neighbours
Open runs per org 10 (monthly, per company, plus templates) Re-check cost, page payload
Rows per run 85 now; users may reach 500 to 2,000 Run page payload, render time, commit batch size
Tracked rows per run 40 now; up to 500 Check count
Distinct rules per org 30 to 200 Engine cost (this is the real cost driver)
Concurrent editors per run 5 to 20 Version conflicts, event ordering
History per run per year 10,000 to 100,000 events checklist_event size, drawer load
Sources in the catalog 6 first, 50+ later Per-source query cost, catalog drift

Service levels to hold: run page open p95 < 800 ms at 2,000 rows; a cell commit round trip p95 < 250 ms; a tracked row reflects completed work within 10 s via events and within 5 min worst case via the pull job; one tenant never delays another tenant’s checks by more than one scheduling cycle.

14.2 The hot spots and how each is handled

Section titled “14.2 The hot spots and how each is handled”

A. Audit chain serialization (the real contention point). Auto-ticks that write audited sign-offs would queue behind each other per org on the advisory lock. A month-end that completes 40 rows within seconds would serialize 40 audit writes.

  • Auto-ticks write checklist_event + activity_log always; the hash-chained audit entry is written once per evaluation batch per run (one entry summarising N rows, with the row ids and bases in its payload), not once per row. One lock acquisition per batch.
  • Manual sign-offs stay one audit entry each (human speed, no contention).
  • The batch audit entry is written last in the transaction to keep the lock held for the shortest time.

B. Evaluation fan-out.

  • Key every check by rule_hash = H(source, normalised where, done_when, over, period_end, org). Evaluate once per hash; write the result to checklist_signal_state keyed by (org, rule_hash); rows reference it. 1,000 rows sharing 12 rules cost 12 queries.
  • Rows resolve their state by joining to checklist_signal_state at read time, so a result change is one upsert, not N row updates (no write amplification, no item.version bumps, no edit conflicts).
  • Only the transition unsatisfied to satisfied (and back) fans out: it then updates the affected items and writes their events in one transaction per run, chunked at 200 rows.

C. Job queue and fairness.

  • A dedicated River queue checklist_check with its own concurrency cap, separate from posting and payroll queues, so checks can never starve business jobs.
  • Job args are (org_id, rule_hash); unique-by-args with a short window collapses event bursts.
  • Per-org token bucket (e.g. 20 checks per minute, burst 60) and per-org max in-flight of 2, so a tenant with thousands of rules is throttled to its own share.
  • The periodic sweep does not scan all rows: it selects due (org, rule_hash) by next_check_at, indexed, with FOR UPDATE SKIP LOCKED. Cadence is adaptive: 5 min if the run was viewed or edited in the last 24 h, 1 h if open and idle, none if locked, a template, or the rule’s source reports no change (see D).
  • Backoff on errors is per rule hash, exponential to 1 h, with a circuit breaker after 10 consecutive errors that marks the rule “unavailable” and alerts instead of retrying forever.

D. Cheap change detection. Each source may declare a ChangeToken(ctx, tx, org, period) (for example max(updated_at) or a row count on its indexed table). The engine stores the last token with the state; if the token is unchanged the check is skipped. Sources without a token are always re-checked on the slower cadence. This removes most steady-state reads for idle orgs.

E. Source queries.

  • Each source ships its query as a fixed, parameterised function over indexed columns; the rule only fills parameters. A source must pass an EXPLAIN guard test (index used, no seq scan on tables above a size threshold in the test fixture) and a statement timeout (SET LOCAL statement_timeout = '2s').
  • The engine runs checks on a read replica when one is configured; on the primary otherwise, at READ COMMITTED, no locks held.
  • “Over a set” rules (all active bank accounts) cap the set size (default 500) and report truncation as an error state, never as a false “done”.

F. Run page payload and rendering (large runs).

  • Keep the run header, columns and sections in one call; load rows per section on expand, and paginate a section above 200 rows (keyset by position). Collapsed sections load only counts.
  • Row JSON stays lean: signal state is joined as {state, label, checked_at}; the basis blob is fetched on demand when the user opens the row’s detail.
  • Virtualise the table body client-side above 300 rows; the editable cell stays uncontrolled until focus (it already keeps its draft locally), so typing never re-renders the table.
  • The description box grows to its text and column widths have minimums, so the table does not reflow on edit.

G. Edit path.

  • Commits already batch at about 500 ms and apply each change under a savepoint with optimistic version. Cap a batch at 100 changes and 256 KB; a larger paste is chunked client-side.
  • Bulk “Track as…” and bulk “Mark N/A” are single commits with one history entry per row but one audit entry per batch (same rule as A).
  • Concurrent editors on the same row get the existing 409 with the other person’s value merged by the client; tracked-state changes never touch item.version, so system updates cannot cause user conflicts.

H. History growth.

  • checklist_event is append-only and grows linearly. Partition by month on occurred_at (declarative partitioning), keep the two existing indexes per partition, and add (org_id, kind, occurred_at) for “show auto-ticks only”.
  • Drawer reads use keyset pagination (already cursor-based) and never count.
  • Locked, old runs move their per-row detail to cold partitions after the retention window set in retention_test.go; the audit chain is never pruned.
  • Auto events are compact: basis is a bounded JSON (at most 4 KB; larger sets store counts and the first 50 ids plus a “see report” link).

I. Catalog and rule evolution.

  • Rules carry v. The engine registers a migrator per source version; a catalog change that cannot migrate a rule marks it unavailable (row falls back to manual with a note), never silently changes meaning.
  • A contract test per source: every rule the default template ships must validate and evaluate on a seeded fixture, so a module refactor that breaks a checklist rule fails CI in that module’s own test run.

J. Multi-region and tenancy.

  • Every query and job runs inside the org’s RLS context; no cross-org batching. Rule-hash dedup is per org.
  • A group of companies (group register) can share a template; runs and rules are still per org entity, so group-wide rollups read checklist_signal_state counts only, never re-run checks.
Condition Behaviour
Queue backlog Rows show last known state with a “checked 12 min ago” age; nothing ticks or unticks while stale beyond the SLO, and a banner appears.
Source slow or down That rule shows “could not verify”; other rules unaffected.
Database under load Checks drop to the slow cadence automatically (adaptive), user commits are untouched (separate queue and pool share).
Audit lock contention Batched audit write (14.2 A); if the lock wait exceeds 2 s the batch retries later rather than blocking user edits.
Bad rule saved Save-time validation plus per-rule circuit breaker; one rule cannot affect others.

Metrics per org and per source: checks run, skipped by change token, p50/p95 duration, errors, timeouts, rows transitioned, queue age, audit batch size and lock wait. Alerts on: queue age > 10 min, any source p95 > 1 s, circuit breaker open, audit lock wait p95 > 500 ms. A support view lists an org’s rules with last check, last error and cost.

14.5 Load and correctness testing (part of phase 2 done criteria)

Section titled “14.5 Load and correctness testing (part of phase 2 done criteria)”
  1. Dedup test: 1,000 rows over 12 rules produce 12 source queries per pass.
  2. Fairness test: 1 tenant with 5,000 rules and 50 tenants with 20 rules each; small tenants’ queue age stays under one cycle.
  3. Burst test: 500 postings in 10 s causes at most one check per affected rule and one audit entry per run.
  4. Idempotence test: run the same pass 100 times; state, events and audit rows change exactly once.
  5. Reversal test: satisfy, reverse, satisfy again; history is correct and the date follows rule 7.1.
  6. Large-run test: 2,000 rows, 20 simulated editors, p95 targets in 14.1.
  7. Chaos test: kill the worker mid-batch; the transaction rolls back and the next pass completes it.
  8. EXPLAIN guard: every source’s query uses its index on the seeded 1M-row fixture.
  1. Ship the catalog and engine dark (no UI), evaluating rules created by a seed script, with metrics only.
  2. Enable per org via a flag; start with the internal org, then 5%, 25%, all.
  3. Keep manual-only behaviour as the fallback at every step: turning the flag off leaves all rows manual and loses nothing.

14.7 Open scalability questions (defaults chosen)

Section titled “14.7 Open scalability questions (defaults chosen)”
  1. Read replica for checks: use if present, not required (default).
  2. Partition checklist_event now or when it passes 50M rows: now, because converting later is a rewrite.
  3. Adaptive cadence bounds: 5 min active, 1 h idle (default; tunable per org).
  4. Batch audit entry for auto-ticks instead of one per row: yes (default); manual sign-offs stay one each.

15. Phase 2 as built (first tranche; this list wins over the text above)

Section titled “15. Phase 2 as built (first tranche; this list wins over the text above)”

Branch feat/checklist-auto-tracking: migration 01145, internal/platform/checklist/{track,tracking,sweep}.go, the module’s three endpoints, OpenAPI, and web/src/features/checklist/{tracking.tsx,...}.

Built

  • One row type, optional tracking (13.1). checklist_item.track (JSONB rule, NULL = manual) and performed_auto. The row dialog has the Manual / Track automatically choice, source, up to 5 conditions, “done when” and the Go-to-page link; the same fields are in the Add row dialog. The sources list and each source’s default rule come from the catalog, so the dialog needs no change when a source is added.
  • Catalog and engine (13.3, 14.2 B/E). Three sources: fiscal_period (closed or locked), bank_reconciliation (every active bank account has a live lock through the period end; an unlocked reconciliation does not count), payroll_run (approved for the month). Rule: source, conditions (ops eq, ne, gte, lte, in; value or placeholder {run.period_end} / {run.period_month}), done when exists / none / all_done. Validated at save (unknown source or field, operator by kind, value shape, at most 5 conditions, link must stay inside the app). SQL is built from the registry’s declarations; every value is a bound parameter; the source’s permission is required to attach it.
  • Shared checks and state (14.2 B). Rows with the same resolved rule share one check per pass. Results are cached in checklist_signal_state (keyed by run and rule hash), never on the item, so a system update never bumps a row’s edit version. Date a row is ticked = the source’s own timestamp (reconciliation lock time, period close time, approval time), not the time of the check.
  • Evaluation paths. On run open and the Re-check button (POST /checklist/runs/{id}/evaluate, checklist.work), and a background sweep (checklist_sweep, every 5 minutes, per run in its own transaction; a run nobody touched for 24 hours is checked at most hourly; locked runs and templates never). The sweep is the safety net the plan calls for.
  • Behaviour rules (section 7). Auto-tick marks done with no person recorded; “no longer satisfied” is flagged once (the date stays, a person decides); a hand-entered date on a tracked row needs checklist.manage and a reason and then counts as manual; removing a rule needs a reason and keeps the date as an ordinary entry; an N/A row is never ticked. History records auto_satisfied, auto_unsatisfied, track_set/changed/cleared with source system. One activity line and one audit entry per run per pass (14.2 A), not one per row.
  • Page. A tracked row shows a badge (verified by system, “2 of 3 done”, waiting, no longer satisfied, could not verify), its source and a Go-to-page button; the header adds “n verified by the system”; the performed cell says “Verified by the system”. Manual rows show nothing extra. Staff without checklist.manage get no tracking button.
  • Default template. Row 7 (Prepare bank reconciliation for all bank accounts) ships tracked. Others stay manual until a source honestly fits them; users add rules from the dialog.
  • Tests. Platform (validation, tick once and log, shared check, hand override, remove with reason, period placeholder, locked/template skipped, copy to a new run, catalog by permission, default rules valid, sweep); a real-data test on the bank source (lock, tick, unlock, flag once); HTTP (catalog, set, permissions, evaluate, remove); web (badges, states, dialog, no button for staff).

Not built yet (deferred, in this order)

  1. Event triggers (plan 5.2): no module publishes an event the three sources need (reconciliation lock, period close and payroll approval publish none today), so the 5-minute sweep is the only background path. Add Triggers to a source when its module publishes.
  2. Auto-value columns (13.1 column dialog).
  3. Dedicated checklist_check queue with per-org rate limits, change tokens, circuit breaker, adaptive cadence beyond the active/idle rule, statement_timeout on checks (14.2 C/D/E). The sweep uses the maintenance queue.
  4. checklist_event monthly partitioning (14.2 H) and the metrics/support view (14.4). Converting a populated table to partitions is a rewrite; do it as its own migration.
  5. Bulk “Track as…”, evidence on manual rows, the “suggest a rule from wording” idea, load tests (14.5 items 2, 3, 6 to 8), per-org feature flag rollout (14.6).
  6. More sources (journals, receivables, payables, fixed assets, intercompany) and default rules for more of the 85 rows, which need each module’s own “done” definition agreed first.

Setup is the weak point of any tracking feature, so the common case needs no setup at all.

  • Pre-wired defaults. The default template ships three rows already tracked: bank statements imported (row 6), bank reconciliation locked (row 7) and depreciation posted (row 27). Only rows with real data behind them are wired; judgment rows stay manual.
  • Suggestion on Add row. When a new row’s wording names work the system can check (“reconcile bank accounts”, “post depreciation”), the dialog offers “We can tick this row for you. [Use this]”. The phrases live in i18n (tracking.hints.<source>, per language), so a new source needs only its server entry plus its name, sentence and hints.
  • Plain wording; the rule editor is folded. The dialog shows one dropdown and one sentence. Conditions and “done when” sit under “Advanced”. The go-to-page link is no longer typed: it comes from the source’s catalog entry.
  • New sources: bank_statement (imported, not voided, through the period end, for every active account) and depreciation_run (a run posted for the fiscal period that contains the period end; its dates come from the period through a field written as a sub-query in the registry).

15.2 Section-level design (built; supersedes the per-row tracking dialog)

Section titled “15.2 Section-level design (built; supersedes the per-row tracking dialog)”

Matching the wording of rows is not a safe foundation (typos, other languages, rewording), so nothing the system acts on is inferred from text.

  • Section link. A section may be linked, by a person, to one area of the ERP: accounting periods, fixed assets, banking or payroll (checklist_section.module). The link is stored on the section, so renaming it or its rows cannot break it. The header then shows what the system sees there (“Every bank account has its statement imported: 3 of 3”, “Not yet”, “Go to page”). It informs; it never ticks a row. The default checklist links General Closing, Cash & Bank, Fixed Assets and Payroll.
  • Header disagreement. When every row is ticked but a check is not satisfied, the header says so.
  • Row auto-check. A row’s only setting is a choice from the short list of named checks, or None. Rows we know ship pre-wired (6, 7, 27). No suggestion from the description, no condition editor, no typed link: the destination comes from the source.
  • Sign-off (optional, per section). A manager switches “Needs sign-off” on. A second person (anyone with checklist.manage who did not perform a row of the section) signs once every row is done or not applicable. Changing a row’s done state, adding, removing or moving a row clears it. A run cannot be locked while a required sign-off is missing. Withdrawing needs a reason. All of it is in the history.
  • Not built: a separate checklist.review permission (uses checklist.manage), required-evidence attachments per row, and a settings page for custom rules (the engine still accepts them over the API).