# module.yaml — the skeleton for a new module source.
#
# Copy this to modules/<id>/module.yaml, fill it in, and run `npm run check:modules` (it enforces
# most of what follows). The page at /modules/<id>/ and the agent spec at /modules/<id>/spec.yaml
# are both generated from this one file — never write either by hand.
#
# Two rules decide whether this file is any good:
# 1. every story carries its acceptance criteria, because that is the test somebody writes later
# 2. everything a customer is likely to argue about is a config_row, not a constant in code
#
# The anatomy this must conform to: /modules/pattern.md
id: <kebab-case-id> # must match the directory name
name: <what a customer would call it — "Invoicing & collections", not "AR">
short: <one or two words for a breadcrumb>
version: 1
maturity: planned # planned | reference | tested | deployed — see /modules/pattern.md §14
summary: >-
<one or two sentences, in the customer's language, saying what this covers and what it spares
them. No platform vocabulary.>
facets:
domain: <sales | finance | it-operations | people | legal | master-data | ...>
industries: [any] # or the ones an overlay exists for
customer_words: # how people ASK for it — an agent matches a request on these
- "<a sentence a customer would actually say>"
replaces: [<spreadsheets, the incumbent product, ...>]
pillars: [sql] # add graph / vector only if the module genuinely needs them
first_module_fit: <foundation | high | medium | low>
depends_on:
- module: business-partner
why: <what breaks without it>
required: <true | recommended | optional>
feeds: [<modules that consume this one's work>]
owns_master_data: [<shared entities this module is the sole writer of>]
references_master_data: [<shared entities it reads but never writes>]
actors:
- id: <role_id>
name: <the job, in their words>
does: <what they do here>
sees: <everything | only what they own | a named subset>
permissions: [<namespace>:<object>:<verb>]
- id: system
name: The system
does: <scheduled and event-driven work>
sees: everything, through its own service account
permissions: []
# person = someone acts through a screen · system = it happens on its own · integration = it
# crosses into another module, which OWNS it. Ids are prefixed so they stay unambiguous in tests,
# commits and other modules' design files.
stories:
- id: <PREFIX>-01
kind: person
actor: <role_id>
story: As a <actor>, I want <capability>, so that <business reason>.
given: <the starting state, concretely — with numbers where numbers matter>
when: <the action>
then: <the observable outcome, including what is REFUSED and what the refusal says>
screen: <screen id>
path: { process: <module>.<process key>, choices: { <Gateway_key>: <outcome key> } } # the walk on the process view
reference: "<implemented | partial — what is missing | not built>"
customer_varies: "<optional — the part of this that differs per customer>"
- id: <PREFIX>-10
kind: system
actor: system
trigger: "<every 6 hours | when X happens | on read (derived)>"
story: <what the system does, and why>
given: <...>
when: <...>
then: <..., and what happens when it runs twice>
reference: <...>
- id: <PREFIX>-20
kind: integration
actor: <role_id | system>
trigger: <when it fires>
story: <what crosses the boundary>
given: <...>
when: <...>
then: <...>
owner: <the module that owns the write — never this one, if the data is theirs>
reference: <...>
# The business processes this module implements, keyed on APQC PCF ids (L3-L4). The process view a
# customer sees and signs is generated from the providing brick's seed configuration. Element keys
# are frozen once published: a renamed concept gets a new key (pattern.md §6).
processes:
- key: <process key, e.g. onboarding>
pcf_id: <e.g. 10469>
pcf_name: <the APQC name>
reference_names: [<vendor end-to-end names as reference points only, e.g. "Hire to Retire">]
brick: <the brick that provides it>
workflows: # the LIFECYCLE view: one entity's states (a drill-down, not the customer's view)
- entity: <entity_id>
states: [<...>]
transitions:
- { from: <state>, to: <state>, by: <actor>, gate: <condition>, requires: <field>, effect: <what else happens> }
note: <anything derived rather than stored>
entities:
- id: <entity_id>
meaning: <in business terms — what it IS, not what it stores>
natural_key: <the real-world identifier; there are no sequences on this platform>
key_attributes: [<...>]
relationships: [<"-> other_entity">]
master_data: "<module-owned | SHARED — sole writer | references <module>>"
reference_table: <prefix>_<plural>
master_data:
owns: [<tables this module is the ONLY writer of>]
references:
- entity: <shared entity>
owner: <module>
how: <the join column>
if_absent: <what the customer loses, stated plainly>
warning: >-
<the duplication this module would cause if the shared record is skipped>
jobs: # one per system story. Ignite has no scheduler.
- story: <PREFIX>-10
trigger: <...>
runs: <what>
where_options: [derived on read, in the same request, always-on worker (--reserved 1 --max-replicas 1), external scheduler]
idempotent: "<how — the key that makes a second run a no-op>"
seams: # one per integration story
- story: <PREFIX>-20
with: <module>
owner: <module that owns the write>
rule: "<X calls Y's route; it never writes Y's tables>"
screens: # derived from the person stories, not invented later
- { id: <screen_id>, name: <what the tab says>, stories: [<PREFIX>-01], actors: [<role_id>] }
- { id: settings, name: Settings, stories: [], actors: [<admin role>],
note: "every config_row below lives here" }
permissions:
namespace: <short form — may be shorter than the id (business-partner -> bp)>
catalog:
- <namespace>:<object>:<verb>
row_visibility:
rule: "<everyone sees everything is a legitimate answer — ask and record it>"
enforced: <where>
not_found_not_forbidden: true
# The test for each: could the customer's office manager change it on a settings screen?
config_rows:
- { table: <table>, changes: "<what the customer edits, in their words>" }
in_code_not_rows:
- "<the rules that must not become configuration, and why>"
tests:
acceptance: "one per story id above, named for it"
access:
- "<per role: what they can see, what is refused>"
invariants:
- "<what must always hold — totals, idempotence, frozen records, currency separation>"
scale:
reads: "the tables reader serves 16 concurrent reads; <what this module's wide reads are>"
hot_paths: [<...>]
workers: <what must run pinned warm, or "none">
volumes: <the size this shape is good for, and what to do above it>
platform_constraints: # optional: where an open platform fault shapes the design
- constraint: <...>
today: <the workaround, and what to tell the customer>
customer_varies: # the Phase 2 questions, in the customer's language
- question: <a question a business owner can answer>
changes: <what in this spec it changes>
# Give them something to REACT to. A business user can rarely recall a policy on demand and
# can always correct one — so read these out rather than asking an open question and
# waiting. Two kinds, never to be confused:
standard: >- # optional. A NAMED authority they can look up and hold
<a directive, accounting standard or statute, named precisely enough to find — e.g.
"EU VAT Directive 2006/112/EC art. 226". If you cannot cite it, delete this key: calling
convention a standard is how an implementation ends up defending a choice nobody made.>
typical: # optional. Arrangements we have SEEN. No authority.
<a kind of business>: <what businesses like that commonly do>
<another kind>: <...>
warning: <optional — the one that costs money to get wrong later>
variation_points: # customer_varies joined to the node it configures
- node: "<Task_<process>_<key> or Gateway_<key>>"
op: <on_off | narrow | re_time | re_lane | row_value>
default: <the reference behaviour>
depth: <1 | 2 | 3>
guideline: "<warn - why the default is the best practice, with its standard or typical source>" # or
requirement: "<refuse - the foundation rule and its reason>"
reference:
# The code this spec is built from: PUBLIC git repos, anonymous clone, one release tag each.
# Fetch with `git clone --depth 1 --branch <tag> <https>`; read_first lists what to open first.
# role: brick = this module's code; platform = the runtime every brick builds on (erp-platform);
# ui = the screens' components (erp-ui). `npm run check:packages` clones each at its tag and
# checks its schema files against the same rules as a package. Prefer repos over `packages`.
repos:
- name: <repo-name> # brick-<module>, erp-platform, erp-ui
role: brick # brick | platform | ui
https: https://git.dodil.io/dodil/<repo-name>.git
ssh: git@git.dodil.io:dodil/<repo-name>.git
tag: v0.1.0 # a release tag, v<semver> - never a branch
read_first: [README.md, AGENTS.md]
packages: # the OLD code/ tarballs; leave empty once repos cover it
- { id: <package-id>, what: "<what it contains>", url: /code/<package-id>/ }
contracts: []
build_guide: "</library/<post-id>>" # or null
known_gaps:
- "<what the reference does NOT do — state it plainly; a hidden gap costs somebody a day>"