Phase 1, wave 1 — contracts for building Plans 3 and 4 in parallel
Plans 3 (workflow engine) and 4 (accounting core) depend only on Plans 2 and 2b, which
are merged. They are built at the same time on two branches cut from phase1-wave1.
This document fixes everything the two branches share, so neither waits on the other and
the two merges do not conflict on meaning.
Authority: the architecture spec
(2026-09-20-erp-architecture-design.md).
§5 governs Plan 3 and §10–§11 govern Plan 4. This document only divides the work. It
changes no design decision.
1. The seam: internal/platform/posting
Section titled “1. The seam: internal/platform/posting”Already on phase1-wave1, with tests.
posting.Semanticand its nine constants mirrorconstants.yaml → workflowSemantics. A test pins them tovocab.Semantics.vocab.ControlKindsmirrorsconstants.yaml → controlKinds(§10.1).posting.Eventdescribes one instance entering one state:OrgID,Kind,InstanceID,SourceID,Semantic,ControlKind,PastAccrual,BusinessDate,ActorMemberID.Event.PostsToLedger()is true on enteringaccrualorpayout, and on enteringrejectedorcancelledwithPastAccrualset.posting.Dispatch(ctx, tx, ev)ignores events with no ledger effect. For ledger events it calls the registeredPoster, or returnsErrNoPosterif none is registered.
Plan 3 calls posting.Dispatch inside every transition’s transaction and aborts the
transition on any error. It never imports accounting. Its tests use a fake Poster.
Plan 4 implements posting.Poster by resolving the posting rule for
(Kind, Semantic) (§10.3) and registers it with posting.Register at boot. It never
imports the workflow engine. Its tests drive Poster.Post with hand-built Events.
If a plan needs to change the seam, the change lands on phase1-wave1 first and both
branches rebase onto it. Neither branch edits internal/platform/posting on its own.
2. Ownership
Section titled “2. Ownership”| Plan 3 — workflow | Plan 4 — accounting | |
|---|---|---|
| Go packages | internal/platform/workflow/…, internal/modules/workflow/… |
internal/platform/accounting/…, internal/modules/accounting/…, internal/platform/countrypack/… |
| Migrations | 00030–00049 |
00050–00069 |
| Provisioning step order | 60–69 |
70–79 |
Permissions (constants.yaml) |
workflow.* |
accounting.* |
| OpenAPI paths | /v1/workflow/… |
/v1/accounting/… |
| sqlc query files | db/queries/workflow*.sql |
db/queries/accounting*.sql, db/queries/fiscal*.sql |
00024–00029 stay free for hotfixes on main.
3. Shared files: append only
Section titled “3. Shared files: append only”These files are edited by both branches. Each branch only appends its own entries and never reorders or rewrites the other’s.
constants.yaml(permissions list), then re-runmake generateapi/openapi.yaml, then regenerateinternal/gen/apiinternal/app/app.goand the module imports incmd/api,cmd/migrateandcmd/orgadmin(internal/app/binaries_test.gokeeps the three in step)internal/kernel/db/retention_test.golistsdocs/ROADMAP.md: each branch ticks only its own plan’s lines
Conflicts in generated files are resolved by regenerating, never by hand-merging.
4. Merge order
Section titled “4. Merge order”Plans 3 and 4 merge into main separately, and the user decides when. Goose applies
migrations in version order. If Plan 4 merges first, Plan 3 renumbers its migrations
above Plan 4’s before it merges. No deployed database exists yet, so a renumber costs
only a rename. A renumber after the first deployment is forbidden.
5. Standing constraints for both plans
Section titled “5. Standing constraints for both plans”- Malaysian audit compliance (§11A). Every state change and every posting writes
audit.Writein the same transaction. The ledger and the decision log are append-only: corrections post reversals, andDELETEis revoked. - Tenant isolation: an explicit
org_id = $npredicate plus forced RLS, composite(org_id, id)foreign keys, and new tables in the retention lists. - Advisory locks: take every non-audit lock class before the first
audit.Write. - Money is
money.Amountend to end, and a string on the wire. The sqlcnumericoverride already exists. - The business date is
kernel/clock’s UTC date (§20.1.2). This is the spec’s decision. Whether to move to Asia/Kuala_Lumpur is still open with the user. Keep every date derivation behindclockso a later switch touches one place. - Plan 3 must persist the resolved approver set at decision time.
SuperiorOfreads reporting lines and unit heads as of now, and only assignments are dated. - Tests: run only the changed packages during development, and the full suite once before a merge.