# Known violations of the package contract, counted.
#
# A conformance gate that is red on day one gets ignored by day three, and a codebase this size
# has debt. So every violation is either FIXED or WAIVED HERE with a reason — and the waiver is
# a COUNT, not a blanket. Add a 35th unprefixed table to the CRM and the build fails; fix one
# and the build tells you to lower the number. The debt is visible, counted, and can only shrink.
#
# What a waiver is not: a place to put something because fixing it is annoying. Each of these
# costs a data migration or a test suite, and each names what it would take.
waivers:
- check: key-type
package: ar-core
allow: 5
what: ar_invoices.bp_id, ar_payments.bp_id, ar_credit_notes.bp_id, ar_dunning.bp_id, ar_customers.bp_id
reason: >-
NOT a type mismatch, which is what it was first recorded as. Invoicing's column named
bp_id holds what the party master calls a bp_KEY — the customer's own identifier, which is
a VARCHAR on purpose because plenty of businesses number their customers ACME-01. The
party master derives a BIGINT bp_id from that key. Both meanings now live in one column,
told apart only by whether they parse as a number, so the cross-module join needs a CAST
(pinned by archive/code/crm-suite-app/v1/tests/test_suite.py). Retyping the column to BIGINT would
make the party master a hard dependency of invoicing and break every business using its
own customer numbers — a worse outcome than the bug.
costs: >-
four releases, because two of the five columns are PRIMARY KEYs and a PK type cannot be
altered: expand, backfill and dual-write, switch reads, then drop a release later once the
rollback window has closed. Full impact summary, with row counts:
design/ar-bp-id-change-request.md
owner: unassigned
- check: prefix
package: gl-suite-app
allow: 12
what: journals, journal_lines, ledger, periods, trial_balance, subledger_items, reconciliation, fx_rates, …
reason: the rule postdates the package (as it did for the retired crm-suite-app)
costs: 12 table renames plus queries, and a migration for any bucket with books in it
owner: unassigned
- check: runnable
package: erp-shell
allow: 1
what: tests/
reason: >-
the shell holds no data and no rules: every path is a proxy to a module. A test worth
writing needs the modules running, which makes it a suite test rather than a unit one —
and the thing most worth asserting (that the user's own token is forwarded, so the module
sees the person rather than the shell) can only be observed through a real gateway.
costs: a deployed smoke test against erp-demo, of the shape archive/code/crm-suite-app/v1/tests/test_suite.py already has
owner: unassigned
# ── platform follow-ups, not package violations ────────────────────────────────────────────
# Recorded here because this file is read when someone asks "what do we know is wrong?".
- check: money
package: brick-hr
allow: 2
what: hr_pay_grade.min_amount, hr_pay_grade.max_amount
reason: >-
found when check:packages began cloning reference.repos: the pay-grade band is VARCHAR, not
NUMERIC(18,2). The spec calls for a 4-decimal rate type, which NUMERIC(18,2) cannot hold, so
this may be deliberate (exact decimal as text) rather than drift. Not verified with the
brick's owner; waived by count until it is.
costs: a column retype in brick-hr (an ALTER, disruptive) or a money rule that admits rates
owner: unassigned
follow_ups:
- what: re-scope every service account holding org-wide k3.editor
why: >-
No role in k3-authorization-service is conditionable (verified 2026-09-18 against
auth service-account list-roles: k3.admin, k3.editor and k3.viewer all lack
conditionable_on, while ignite.app-developer and the scriptum roles carry it). So a
credential that can write one bucket can write EVERY bucket in the org — which is why
db.py defaulting to a live bucket was as dangerous as it was: there was no backstop
underneath the env var.
blocked_on: per-resource k3.editor, expected by 2026-09-20
then: >-
scope each app's SA to its own bucket DRN — cli-erp-suite-sa, cli-erp-bp-sa, cli-crm-sa,
cli-crm-suite-sa, cli-gl-sa, cli-itsm-sa, cli-casting-sa, cli-retail-vision-app,
cli-dodil-growth-sa, cli-dodil-crm-public-sa. Until then the app-side bucket assert is
the only control, and it stays afterwards: scoping narrows the blast radius, but only
the app can refuse to start against a bucket it was not told to use.