dodil/erp-method

node_modules/.cache/erp-method/v0.1.21/modules/lifecycle.md

Developing a module on DODIL — the lifecycle

For the agent (or person) building business software for someone. Read it once before the first conversation; come back to a phase when you reach it.

The rule that shapes everything else: phases 0–3 produce documents, not code, and the customer signs off on them before phase 4. The reference modules exist so you never start from a blank page — not so you can skip understanding this customer. Most of them already have their code as bricks: public git repos on erp-platform that phase 4 composes and configures: with rows first, a brick of the customer's own when rows cannot say it, and the customer's own copy of a brick when nothing else will. The customer's signed process and change list are the intent; an upgrade re-applies them.

How far a customer can change it: three depths, one kept foundation

Everything is customizable, including the code (seemo, 2026-10-18). Every change is classified as one depth, drawn on the customer's diagram and signed before it is built:

DepthWhat the customer getsMechanismOn an upgrade
1 Configurea diagram edit or a chat request becomes rowsthe brick's own keep_* use casesautomatic: rows survive a version bump
2 Extendbehaviour the brick does not havea capability provider in the customer's own small brick (hooks: not built yet)the capability contract is versioned
3 Ownthe brick itself, changeddodil git repo fork into the customer's orgthe agent re-applies the recorded intent on the new tag; the bench proves it

The kept foundation is refused if broken, each part for a reason: stable identifiers (process and step keys, APQC PCF ids, event types, use-case names: re-applying intent, mining and audit join on them); the event-log format (not built yet); shared keys and data integrity (natural keys, cross-brick keys, exact money, versions, idempotency: DataK3 enforces none of it, the write path does); the security model (permission names, row visibility, gateway sign-in: it fails closed); and erp-platform itself (one pinned runtime; copies drift).

Best practices are defaults with warnings, not blocks. A deviation from a standard: or typical: default is shown to the customer with its warning and source, signed, and then built.

BPMN is the customer's view. It is generated from the spec and the live configuration, never hand-edited, and the bricks never execute it. Stories are shown on it as paths (scenarios), and the signed diagram is the contract through test, operate and change.

0 FRAME       what are they asking for? is there already a system here?
1 DISCOVER    the module catalog -> the reference module's spec
2 UNDERSTAND  actors, stories, workflows, entities, master data, rules
3 DESIGN      map it onto the bricks -> ERP_<MODULE>.md -> sign-off
4 BUILD       compose the published bricks; generate only a missing module, as a brick
5 TEST        a test per story, role, invariant + the generated probes, in process and live
6 PROVISION   dev before prod; merge role catalogs
7 DEPLOY      git push -> CI -> registry -> CD
8 VERIFY      the app reaches its own data; a real person signs in; their agent connects
9 OPERATE     change -> back to 2; next module -> back to 0

Modular in design, incremental in delivery, shared in data, one app by default.

Five rules that hold in every phase

  1. Interactive by default. Every decision the customer can make is a short question with choices; every change you make is shown before and after. Work silently only on what was already agreed.
  2. The repo remembers, not you. State lives in ERP_<MODULE>.md (status, people, decisions, changes), not in your memory or the chat. Another agent — or another person — must be able to pick up from the file alone. See Starting a session.
  3. Test copy first, live only on an explicit yes. The customer usually cannot tell which database they are changing. You always can, and you always say. See Environments.
  4. Show it on localhost. Whenever something can be seen, run it locally against the test copy and let the customer look before anything goes live.
  5. The signed BPMN drives every change; the code follows, never the reverse. The customer signs a DIAGRAM (never the markdown alone), and every later change is redrawn with the changes marked, confirmed in plain words, re-signed as a new numbered version, and only then built (dodil://erp/phase/change). Agents forget follow-ups, and the end user can't read code. Enforced from erp-platform 0.8: erp-compose plan/migrate and CI refuse an unsigned, pending or drifted module.

Starting a session

Before anything else, every session:

  1. Reads every ERP_*.md in the repo — the Status block first, then People, Change requests, and Decisions.
  2. Works out who it is talking to. If it is not obvious, ask one light question: "Before we continue — who am I speaking with, and what's your part in the business?"
  3. Brings the file up to the current template (/modules/template.md) if it is older: adds any missing Status, People, Environments or Change requests section in the template's form, filled from what the file and the repo already say. Content stays; only the shape changes.
  4. Opens with a three-line where we are: what is live, what is being worked on, what is waiting on the customer. In that person's words.

When the person changes

Projects outlive conversations. The developer who signed off the design hands over to the owner; the owner delegates to an office manager. Signals: the vocabulary shifts ("schema" becomes "the list"), the questions change, or they say so.

  • Recalibrate, don't restart. Ask the new person the two Phase 0 calibration questions — what their part in the business is, and whether they want to weigh in or have you decide — and record both under People, with the words they use. Do this before acting on their first request.
  • Retell the state in their words. A business user gets "your quotes now lock once sent", not "status transitions guard the write path". Never carry the previous person's jargon forward.
  • Earlier decisions stand unless the new person changes them — and a change is a change request (Phase 9), recorded with who asked.
  • Authority is theirs to state. If the new person asks for something that reverses a signed-off decision, say so plainly and ask whether they can make that call or whether someone else should confirm.

0 · FRAME

Listen for capabilities, not labels

People say "an app", "a system", "a portal", "an ERP", "something to replace the spreadsheet". The word tells you almost nothing. "An app that tracks our customers, sends invoices and handles support tickets" is three modules whatever it is called. Listen for the nouns and verbs of their business and map them to candidate modules using customer_words in the catalog.

Speak their word back. If they said "app", it is their app. Never make a customer learn "ERP", "module", "master data" or "entity" — those are your working words, not theirs.

Calibrate to the person — ask about the situation, not their competence

Two questions do the job, and both feed the design:

  • "What do you use for this today?" — spreadsheets, an old tool, nothing. It shows their experience and what you are replacing.
  • "Should I make the technical calls and explain them as I go, or would you like to weigh in?" — asks about involvement, not skill. "You decide" is a fine answer.

The answer changes how much you explain and how many options you offer. It never changes the quality of the design underneath.

Recommend a sequence, not a big bang

Name the first module — the one that hurts most or unlocks most (first_module_fit in the catalog) — and show the rest as a roadmap in the customer's words.

DecisionDefault
Build orderone module at a time
Deploymentone app serving every brick (tier 1), plus the public app only if a brick issues links; split further only for a stated reason (independent scaling, a trust boundary)
Datashared from day one — one bucket, one sign-in pool, one master-data register

The real danger for a first-time builder is not a monolith. It is module one and module two each inventing their own customer list. So if anything on the roadmap touches money (invoicing, purchasing, accounting), the first module references a shared business partner instead of keeping its own customer table. Check depends_on in the catalog.

Detect an existing system — don't ask, look

SignalWhere
apps of the same systemdodil ignite app list / app get → labels.group
the sign-in already in useapp get → user_pool
where the data livesapp get → env.BUCKET
what each module decidedERP_*.md at the root of the customer repo — the most reliable signal

If a system exists: reuse its pool, bucket, master data and repo, and read every ERP_*.md before designing anything.

Hold the conversation, don't hand over a questionnaire

Most customers are not system builders. For them, how you ask decides whether you learn anything.

  • One topic per turn, one to three questions. Never a numbered list of six open questions — a non-technical customer answers the first two and the rest are lost.
  • Offer choices with a recommended default, plus "something else". "Everyone sees everything (recommended for a team of five)" is answerable; "what visibility model do you need?" is not.
  • Say back what you recorded after each answer, in one line, so a wrong assumption is caught now rather than at sign-off.
  • Put anything the customer must read in the message itself, before the question. A multiple-choice prompt can cover the text around it; a summary placed there goes unseen.
  • Review the design in parts (who uses it and the screens → how the main thing flows → what happens on its own → what comes later), each confirmed before the next. Offer a visual page when a part is long.
  • Explain a decision you made for them in their terms, one sentence: "quotes expire by date, so nothing has to run overnight".

With an experienced builder the same rules loosen: batch related questions, use their vocabulary (a process person says "workflow" and "approval step"; a developer says "schema"), and skip what they have already decided. Notice the words they use — it is a better signal than asking.

Output: a short scope note back to the customer — what you heard, the modules it maps to, the recommended first module and order, and how involved they want to be.

1 · DISCOVER

/modules/index.json → the candidate module → /modules/<id>/spec.yaml.

If the catalog says spec: pending, there is no reference vocabulary for that module yet: Phase 2 is done from scratch with the customer, in the house style of the modules that do have specs (the entry's start_from names them). Write the customer's design file as usual — and if the module is one the catalog expects to exist, write its spec too, from /modules/module.template.yaml, so the next engagement starts from something. The spec gives you the reference actors, stories, workflows, entities, master data, jobs and seams — a vocabulary to correct with the customer, and customer_varies: the questions that most often change the design.

Show the reference process. For each PCF id in scope, show the brick's reference process view (the process BPMN a brick generates from its seed configuration, e.g. brick-hr's show_playbook_bpmn; the blog's /modules/<id>/process.bpmn once published) and the landscape of what is covered. The customer recognises it or says "not like that"; either answer is recorded.

The reference code is for phase 4. Do not open it to decide what to build — but note whether the spec has any: reference.repos lists the bricks (clone URL, release tag, read_first), and a spec with no repos means phase 4 generates that module. That changes the estimate you give the customer, not the questions you ask them.

2 · UNDERSTAND THE BUSINESS

The only creative phase, and the whole value of the engagement.

Work through the reference spec with the customer. Ask the customer_varies questions in their language, one or two at a time. For each reference story: keep, change, or drop. Then ask what is missing. A novice will not volunteer system stories — ask "is there anything that should happen on its own, on a schedule, or when something else happens?"

Propose, do not interrogate. Where a question carries standard: or typical:, read it out and ask them to correct it. "Most firms like yours pay net 30, and the EU late-payment directive assumes 30 unless you agree otherwise — is that you?" gets a truer answer, faster, than "what are your payment terms?" — because people rarely recall a policy on demand and can always react to one. An open question asked of someone who has never had to write the answer down produces a guess, and the guess becomes a config row nobody revisits.

Two kinds of answer, and never blur them:

  • standard: is a named authority — a directive, an accounting standard, a statute. The customer can look it up and hold you to it. Say which one, so adopt it and deliberately differ from it are both available to them. Most will adopt; the ones who differ usually have a reason worth writing into the design.
  • typical: is what we have seen. No survey, no authority, no implication that a customer doing otherwise is wrong. It is conversation, and presenting it as more than that is how an implementation ends up defending a choice nobody ever made.

Record the answer either way — "they keep the standard" is as much a decision as departing from it, and in six months nobody remembers which was which.

2a · Actors

Who uses it and what each may do and see. Becomes the role catalog.

2b · Stories — three kinds, each with acceptance criteria

  • Person — someone does something through a screen.

    CRM-06 · As the commercial director, I want quotes above our discount threshold routed to me, so that margin is protected. Given a 15% threshold and an 18% quote, when the rep submits it, then it waits in my queue and the rep cannot approve it.

  • System — the system acts on its own, on a schedule or an event.

    CRM-10 · Every 6 hours, the system re-scores open leads. Given a lead active an hour ago, when the run completes, then its score reflects it, and running it twice changes nothing.

  • Integration — the work crosses into another module, which owns it.

    CRM-20 · When a quote is accepted, a draft invoice appears in invoicing for the same customer and amount.

Prefix story ids with the module (CRM-06) so they stay unambiguous in tests, commits and other modules' designs. The Given/When/Then is the phase 5 test.

Every story with a Given/When/Then gets a path: the choices it makes at each gateway of the process view (path: { process: hr.onboarding, choices: { Gateway_worker_type: freelancer_company } }).

2c · Workflows

For each entity with a lifecycle: states, transitions, who may move it, and gates (thresholds, approvals, locks). This is where "every customer has their own process" is captured. Play every story's path on the diagram with the customer; an unwalked branch is a question.

2d · Entities

In business terms first: meaning, natural key, key attributes, relationships, owner. Decide keys now — DataK3 has no sequences.

2e · Master data

For every entity: shared (one module is the sole writer, all others reference it) or module-owned (table prefixed with the module: crm_, gl_). A foreign key is accepted and never enforced, so "sole writer" is the integrity mechanism — decide it, don't assume it.

2f · Rules and controls

Every business rule and where it is enforced — almost always a guarded write path, not a database constraint.

2g · Non-functional

Volumes, concurrency (16 concurrent reads per bucket), retention, residency, consent, and who operates it after go-live.

3 · DESIGN

From phase 2Becomes
Entitiestables, natural PKs; prefix = module-owned, no prefix = shared
Relationships that need walkinga typed graph (filter on rel)
"Find similar"a VECTOR column
Person storiesuse cases (each one a route and an MCP tool) and screens — the UI is derived from the stories
System storiesjobs: trigger, what runs, where it runs — Ignite has no scheduler: an always-on worker app with its own loop, or an external scheduler calling a route
Integration storiesa call to the owning brick's use case (in process inside one app), never a write into its tables
Where the customer differs from the referencea change item at one of three depths: configure (a row the brick already takes: stages, caps, playbooks, terms), extend (a capability in their own brick), own (a fork they keep, re-applied on upgrade). Each is drawn on their diagram and signed
Actorspermissions <module>:<object>:<verb>

Write ERP_<MODULE>.md

One file per module at the root of the customer's repo (ERP_CRM.md, ERP_INVOICING.md). Another module finds it with a glob. Template: /modules/template.md. Same sections in every file.

The filename says ERP because it is for tooling. The content uses the customer's words.

Sign-off — a hard stop

Show the customer the design in plain language — their actors, their stories, their screens, what runs on its own, what is deliberately left out — and wait for a yes before phase 4.

Sign-off is three artefacts: their BPMN (processes/<process>.bpmn, generated, recorded with its sha256), the scenario list (id, path, expected end state), and the deviation list, each warning accepted by name. Record them in the file's Processes, Scenarios and Deviations sections and the yes in Decisions, with the date and who approved. An approval is only ever words the customer wrote themselves, naming the version.

4 · BUILD

Build it to the pattern: /modules/pattern.md — the anatomy of a module and of a brick. It is short, and it is what keeps the fifth module looking like the first.

Compose; do not copy. For each module, spec.yaml → reference.repos names its bricks and erp-platform, each with an https and ssh clone URL, a release tag and read_first:

git clone --branch v0.2.0 https://git.dodil.io/dodil/erp-brick-hr.git   # the tag from the spec
# read README.md and AGENTS.md before writing anything

The customer's repo depends on them; it does not contain them:

customer-repo/
  ERP_HR.md  ERP_CRM.md     the design files
  requirements.txt          erp-platform and each brick, pinned to its tag:
                            brick-hr @ git+https://git.dodil.io/dodil/erp-brick-hr.git@v0.2.0
  compose.yaml              name, version, tier: 1, bricks: [{name, brick, version}], capabilities
  roles.yaml                role -> permissions; the pool's catalog is seeded from it
  scenarios/                the cross-brick stories, SC-<MODULE>-nn (pattern §9)
  Dockerfile  .dodil/deploy.yaml  .github/workflows/ci.yml

erp-compose check refuses table and tool collisions and version drift; plan prints both apps' environment; migrate applies every brick's schema plus the platform's tables, once per release; serve is the one staff app — every brick under /<brick>, one /mcp — and serve --public the public app, only when a brick issues links. Pin every brick explicitly: a brick does not always declare the neighbours it seams to (brick-hr needs brick-business-partner beside it).

The customer's specifics are built from the signed change list, by depth. Depth 1 is rows, written through the bricks' own use cases (a seed script calling the routes or tools, never INSERTs into a brick's tables). Depth 2 is a capability in the customer's own brick, wired in compose.yaml. Depth 3 is a fork — never an unrecorded patch. A fork is a signed depth-3 change: its intent lives in processes/*.changes.yaml, the agent re-applies it on each new tag, and the bench proves it. A difference every customer would want is a change to the brick upstream, released as a new tag.

A module no brick covers is generated as a new brick in the same shape — erp-platform/BRICKS.md, started from erp-platform/templates/brick-example/: domain/ (rules, plain Python), application/ (use cases; each is a route and an MCP tool), infrastructure/ (prefixed tables, Aggregate repositories), three test layers and a scenario catalogue. It joins compose.yaml like any other. Generate its business logic from ERP_<MODULE>.md, not from another module's code. Put the story id in each use case's docstring.

Transition — modules with no bricks yet. A spec whose reference.repos is empty (gl, ar, purchasing, the pending modules) has no brick to clone: generate a new brick in the brick shape on erp-platform (domain / application / infrastructure, use cases, Aggregate repositories, the three test layers), from ERP_<MODULE>.md. Any reference.packages entry is a worked example to read, never a layer to copy. The brick replaces this path once it is published.

5 · TEST

Against a dev bucket:

  • Acceptance — one test per story, from its Given/When/Then. A story without a test is unverified.

  • Access — per role: right rows, others' rows are not found, excluded actions refused.

  • Invariants — totals net to zero, locked records refuse edits, re-imports are idempotent.

  • Generated probes — erp_platform.scenarios derives them from the use-case declarations: every role × every use case × both doors (401 / exactly 403 / served, 404 for another person's record), every idempotent command replayed, extra fields refused (mass assignment). Hand-written negatives miss the use case added last month; generated ones cannot.

  • Scenarios — the cross-brick stories (SC-<MODULE>-nn), run in process (the whole composition on SQLite, seconds) in CI and live against the dev deploy. The crm-demo composition is the bar (2026-10-17): security probes 486/486, stories 28/28.

  • Paths and conformance — each scenario's recorded trace equals its signed path, and the run's events replayed on the signed BPMN score conformance fitness 1.0, with a negative control (a run that skips a signed approval) scoring below it. Not built yet: the path walker and the shared event log. Until then, check each path by hand against the diagram and say so in the report.

Every rule in pattern.md §3–§4 has a probe or a scenario that fails when it is broken; a rule with neither is a convention.

A composition ships its OWN test, in its own folder — the bricks' suites are not it. Running each cloned brick's tests proves the bricks, not that your compose.yaml, roles and configuration rows work together. Add tests/ beside compose.yaml that (1) boots the composition, (2) runs the bricks' scenario catalogues against it (erp-scenarios run <brick>/tests/scenarios/catalogue.py with your compose file), and (3) asserts the customer's configuration is in place (read it back through the bricks' use cases: the chase cadence, the signer, the timezone they chose). Learned 2026-10-17: a lab build passed 31/31 hidden scenarios yet shipped no test of its own, so nothing would have caught its own configuration breaking later.

6 · PROVISION

Environments — a test copy and a live copy, always

Test copy (dev)Live
Bucket<customer>-dev<customer>
App<customer>-dev, or localhost against the dev bucket<customer>
Sign-in pool<customer>-dev with test users<customer> with real staff
Dataseed and test data; safe to wipethe business's real records
Who changes ityou, freely, as work progressesyou, only after the customer says "go live"
  • Local .env points at the test copy, never at live. Live credentials exist only as secret references in .dodil/deploy.yaml. A local process that can write to live is how a customer's records get damaged by a test.
  • Say which one, every time. "I've added the field on your test copy"; "this will change your live app". The customer should never have to ask.
  • Reading live is allowed, writing is not — reading real counts ("you have 214 sent quotes") makes impact feedback concrete.
  • Preview on localhost first. uvicorn main:app --reload plus the UI dev server against the test copy is the fastest way to let the customer see a change. Give them the URL and what to click. Deploy the dev app only when localhost is not enough (sign-in, phone, someone else needs to look).

Creating things

  • Dev first, live when the first release is approved.

  • Joining an existing system: merge the role catalog, never replace it. appid roles set replaces the whole catalog; a module that writes only its own roles silently strips every other module's access. roles get → merge → roles set --file.

  • Tables: erp-compose migrate creates each brick's prefixed tables and the platform's own; a generated brick adds only its prefixed tables and any new shared master data (with its owner recorded in the register).

  • The staff app is deployed --user-pool <pool> WITH --allow-unauthenticated: the gateway logs in optionally, strips forged identity headers and injects them when a session or bearer is present, and the app's sign-in gate (erp_platform _SignInGate) refuses every route without identity except /healthz and /.well-known/oauth-protected-resource — the RFC 9728 metadata that Claude.ai/ChatGPT connector discovery and the browser sign-in redirect need anonymously (crm-demo v4–v7, 2026-10-16/17: an anonymous /lead/leads is a 401 from the app, the metadata a 200). Gateway-level public_invoke=false is right only for an app that serves no MCP and no browser sign-in redirect.

  • Two service accounts when there is a public app. The staff app's: k3.editor scoped to the bucket (+ ignite.model-user if a brick calls Models). The public app's: its own, k3.editor only, no Models role, no connector credential — it is the one on the internet with no sign-in. The public app has no pool, public_invoke=true, and never migrates (erp-compose plan prints both environments).

  • A pool with no redirect_uris cannot log anybody in. Attaching user_pool to an app is half the job: the gateway's front door only opens for a callback the pool allows, so every app in the pool needs its callback added to the allowlist —

    https://<app>-<org>-<port>.ignite.dodil.cloud/.dodil/auth/callback
    

    and appid settings set now MERGES the flags you pass into the stored settings (CLI help, 2026-10-18; it used to replace the block), but the redirect list is a value of its own: read it first and pass every app's callback, not only the new one. Adding the fifth module means five URIs in that list, not one. A pool missing them answers

    {"error":"redirect_uri is not in the pool's allowlist (settings.redirect_uris)"}
    

    which arrives at the person trying to sign in, not at you.

  • Create at least one user, and say so. With allow_signup: false — the right setting for a business system — nobody can create their own account. A pool with a correct allowlist and no users still admits no one. Invite them (no password; they set one through the reset flow).

  • Bricks in one app call each other in process, as the caller — no service client. Only bricks deployed as separate apps (tier 2–3) need one per calling app: a dodil-appid service client with a role holding only what that call needs.

7 · DEPLOY

git push → CI → registry → CD from .dodil/deploy.yaml. There is no separate deploy command.

8 · VERIFY

  • The app reaches its own data — ship a POST / self-check that opens the bucket with the app's own credential; ignite invoke calls it. A public-URL 401 proves only the gateway is up.
  • The gateway strips forged identity headers.
  • A real person signs in, once per role. You cannot do this — you never set passwords — so tell the customer it is their step, and don't call the module done until it has happened. This is the step most easily skipped, because the other two pass without it: ignite invoke and a service-client token both go round the browser login entirely. Five apps were deployed, self-checked, and exercised end to end through their APIs here before anyone discovered the pool had no redirect_uris and no users — the login had never once been tried. If you cannot hand someone a URL they can actually sign into, the module is not verified.
  • Their agent connects, as them. A person adding the app to Claude.ai or ChatGPT signs in (authorization_code + PKCE) and sees exactly their own rows. Claude.ai registers itself (DCR) and arrives pending: it can authorize nobody until you approve it — dodil appid client list <pool>, then dodil appid client approve <pool> <id> (appid 0.14, 2026-10-16). A machine (a job, Claude Code) uses a service client instead; never hand a person's agent a service token.
  • The scenarios pass live against the deploy (erp-scenarios run … --live). The live runner refuses a bucket not stamped demo unless given --allow, and never injects faults.

9 · OPERATE AND EVOLVE

Change requests — a conversation, then autonomous execution

Customers ask for changes in passing: "can we add a site-visit step before quoting?", "office should not see margins", "a customer can have two VAT numbers". Most touch an entity or a workflow, and most customers cannot tell what that implies. The job is to make the implication visible before anything changes, then do all of it without further hand-holding.

1 · Understand — interactively. Restate it in their words and ask one to three questions with choices. Probe the edges they will not think of:

  • What about the ones that already exist? (existing records)
  • Who is allowed to do this? (roles)
  • What should happen automatically? (system stories)
  • Does anything else depend on it? (other modules, reports, the PDF, imports)

2 · Locate. By element id on the signed diagram, then from ERP_<MODULE>.md: which stories, workflow transitions, entities, screens, jobs and seams are touched. Read live counts where it helps.

3 · Show the impact — plain language, before any work. Send it as an ordinary chat message of its own, then ask for the go-ahead. The approval question may only refer to what the customer has already been shown: never "as described above" unless the description was actually sent, and never impact that lives only in your notes or the CR you are about to write. Get the real counts (read-only) before this message, not after the yes. This shape:

What you asked for: a Site visit step between Enquiry and Quoted.

What changes for your team

  • The pipeline board gets a new column, Site visit.
  • A deal can't be quoted until the visit is logged. (new rule — you asked for this)

What happens to what you already have

  • Your 37 open deals in Enquiry stay where they are.
  • Your 12 deals already in Quoted are not moved back.

What doesn't change: quotes, prices, the PDF, reports.

How it goes live: first on your test copy — I'll show you on localhost — then live when you say so. Nothing is deleted; undoing it means removing the column again.

Shall I go ahead?

Put the diagram before and after beside this message (added, removed and changed elements by key), so the customer sees the change where they signed the process.

Name the migration honestly: each brick's change is a versioned migration (pattern.md §11), with its counts read from the test copy. On this platform ALTER TABLE … ADD COLUMN is the only cheap change — and it currently unsettles reads on that table for minutes afterwards, so it happens on the test copy first and at a quiet hour on the live one. A rename or type change is add new → backfill → switch reads → drop old later; nothing is dropped in the same release that stops using it. If existing records need a value, say which value and why.

4 · Record. On a yes: add CR-<n> under Change requests (date, who asked, who approved, summary, element ids), and amend the stories, workflows, entities and deviations it touches. Record it as a new signed process version (v2, the previous hash kept as supersedes). The design file changes first, the code second — so it never describes a system that no longer exists.

5 · Execute — autonomously. No further questions unless something contradicts what was agreed: update code, add or amend the story tests, migrate the test copy, run the whole suite, start localhost, and tell the customer what to look at.

6 · Go live — on their word. After they have seen it: export or snapshot the affected live tables, migrate live, deploy, run the self-check, and report back in one short message — what changed, how many records were touched, and how to undo it. Mark the CR live.

A change request that turns out to be a new capability ("can it also send invoices?") is not a change — it is the next module, and goes back to Phase 0.

Also

  • Rollback: ignite version rollback returns the app to an earlier deployed version; the data is not versioned with it. That is why step 6 exports first.
  • Recurring work — whatever Phase 2 named, and where it runs.
  • The next module → back to Phase 0, as "joining an existing system": read every ERP_*.md first.