Conventions
How entries in this registry are identified, written, structured and kept honest.
Read this before adding or updating an entry. See ACCESS_POLICY.md
for the read/write boundary and DEPLOYMENT.md for publishing.
1. Non-negotiables
| Rule | Meaning |
|---|---|
| Capture, don't change | Logic discovered in another repository is recorded here. It is never modified there. |
| This repo is the only writable one | No file in any other repository is created, edited, deleted, committed or pushed as part of registry work. |
| Pointers, not code | Entries reference source by repo + path + commit. Source is never copied in. |
| Production wins | If an entry and production disagree, the entry is wrong. Correct the entry, or raise a defect — never document the system you wish existed. |
| Behaviour change is a separate process | Want the logic itself changed? Raise a change request. Update the registry after it ships. |
2. What may and may not be stored
| Artifact | Stored? | Notes |
|---|---|---|
| Source code copied from another repo | ❌ | Any language, any length |
| Config files, Celigo exports, IaC, SQL migrations | ❌ | These are code |
| Screenshots of code | ❌ | Same thing, harder to search |
| Repo name, file path, commit SHA, line range | ✅ | Metadata — this is how we point at code |
| Plain-English rules | ✅ | The core of the registry |
| Field names, table names, endpoint names, status values | ✅ | Identifiers, not implementation |
| Hand-written pseudocode | ✅ | Subject to §3 |
| Secrets, keys, tokens, customer PII, supplier pricing | ❌ | Never, in any form |
3. The pseudocode boundary
Pseudocode is allowed because a dev reading if volume > parcel limit → require freight method
can act without opening the source. Copied code is not allowed because it drifts silently
and duplicates something we've deliberately chosen not to own.
The test: if a block would run — or nearly run — in the source language, it's too close.
| ✅ Acceptable | ❌ Not acceptable |
|---|---|
if volume is unknown → flag REVIEW, stop |
if (!rec.getValue({fieldId:'custbody_cbm'})) { ... } |
for each fulfilment line: sum cartons |
A pasted function, even a short one |
on order save, then again before consignment create |
A copied switch block with real field IDs and syntax |
Rules:
- Hand-write it. Never paste and lightly edit.
- Language-neutral. No
const, no=>, no SDK method names, no real syntax. - Describe decisions and sequence, not implementation.
- Always fenced as plain text, never tagged
javascript/python/sql. - Keep it short. More than ~15 lines usually means the rule should be split.
4. Rule IDs
LI-BL-<DOMAIN>-<NNN> e.g. LI-BL-FRT-007
│ └── zero-padded 3-digit sequence, never reused, never renumbered
└── 3-letter domain code
IDs are immutable. They land in Asana tickets, change requests and conversations. Renaming one is a breaking change.
- Domain-coded, not system-coded — the rule "orders over X m³ can't use the parcel service" outlives whichever Lambda implements it. Systems live in front-matter, where they can change freely.
- Location-independent. Moving an entry between folders changes its URL, never its ID.
- Retired logic keeps its ID with
status: retired. - A rule that splits: original goes
status: supersededwithsuperseded_by: [ID, ID].
Domain codes
| Code | Domain | Covers |
|---|---|---|
ORD |
Order | Capture, validation, holds, release |
INV |
Inventory | Allocation, location assignment, lead time, backorder |
FUL |
Fulfilment | Dispatch, WMS, consignment creation, ship dates |
FRT |
Freight | Carrier selection, ship-method eligibility, cost, CBM/cartons |
RET |
Returns | RA sourcing, credits, restocking |
CMS |
Comms | Customer notification triggers, delay stages, SMS/email scheduling |
FIN |
Finance | Pricing, GST, margin, reconciliation |
PRD |
Product | Item setup, kits/assemblies, attributes |
DAT |
Data | Reporting definitions, metric logic, pipeline transforms |
Add a domain only when three or more rules don't fit an existing one. Domain codes are as immutable as IDs.
Allocating a number
- Check
registry-index.json(generated on build) for the highest number in that domain — itsrulesarray is one flat, id-sorted list of every documented rule. - Take the next one. Never reuse a gap.
- If two branches claim the same number, the second to merge renumbers — before merge, never after.
registry-index.json
Generated on every build from the entries and the glossary, never hand-edited. It is what makes the registry answerable rather than only readable — by a person, by a script, or by an agent holding a rule id and nothing else. Three keys:
| Key | Holds | Answers |
|---|---|---|
entries |
Each entry's front-matter, with id/ids normalised to ids and a rules array of { id, name, url } |
"What does this automation touch, and who owns it?" |
rules |
Every rule, flat and sorted by id — name, domain, status, journey stage, entry, and a URL straight to its anchor | "What is LI-BL-INV-012, and where do I read it?" |
terms |
The glossary, flat — term, aliases, meaning, the rules that use it | "What do we mean by this word?" |
defects |
Every defect in every entry, open first, with a URL to its row | "What is broken, and is this a known one?" |
open_questions |
Every open question, open first | "What are we waiting on an answer for?" |
test_cases |
Every documented case, with its layer, rule and expected output | "What proves this rule still works?" |
A rule's name is its heading, read from ### <ID> — <Name> at build time (§8). It is not a
front-matter field and must not become one: two places to write a name is one place for it to go
stale. The file is served from public/ as well, because the search box in the sidebar reads it.
Nothing in it is timestamped. CI rebuilds and compares against the committed output, so a generated-on field would fail every build that changed nothing.
5. Where entries live
Files are organised by integration / system area; IDs are coded by domain. This is deliberate — the folder tells you where the logic runs today, the ID tells you what it means permanently.
integrations/
_TEMPLATE.md ← starting point for a new entry
shipping-automation/
module-5-courier-approval.md ← may contain several rules
README.md ← index of that integration's entries (optional)
Everything lives under integrations/. A folder groups the entries for one integration (or
system area); the front-matter systems: field records which platforms each rule touches
(NetSuite, AWS Lambda, DeTrack …) — a separate axis from the folder.
A file may hold one rule or several related ones. Each rule inside gets a stable anchor placed immediately before its heading:
<a id="LI-BL-FUL-003"></a>
### LI-BL-FUL-003 — Courier approval gate
The explicit anchor matters: auto-generated heading slugs change when a title is edited,
which silently breaks every link. The <a id> never does.
Canonical URL for a rule:
https://<registry-domain>/integrations/shipping-automation/module-5-courier-approval/#LI-BL-FUL-003
Split a file into per-rule files when it exceeds ~400 lines or when its rules stop sharing a trigger. IDs and anchors survive the split; only URLs change.
6. Front-matter
Every entry file opens with YAML front-matter. This is what makes the registry queryable — blast-radius analysis, drift detection and the generated index all read from here.
---
id: LI-BL-FUL-003 # omit if the file holds multiple rules; see below
title: Courier approval gate
domain: fulfilment
status: active
project_owner: Kim Hoang Nguyen # a person — ask to confirm; Kim is the default, not an answer
developers: [Jane Citizen] # people who built / maintain it — ask, then corroborate
for_department: Fulfilment Team # the team it is for — ask, offering the suggestions below
systems: [aws-lambda, netsuite, detrack]
source_of_truth: netsuite
triggers: [courier-approval-request]
journey_stage: pre-delivery # where in the order journey it runs — see §11 for the ladder
depends_on: [LI-BL-INV-006] # must have run already; not a trigger — that is runs_after
related: [LI-BL-FRT-007]
terms: [LI-TERM-SHIP-COMPLETE] # vocabulary this entry leans on
links: # deep links into the systems — see below
- type: netsuite-saved-search
id: 7132
name: "SCRIPT | SHIPPING AUTOMATION 2 | COURIER APPROVAL | Ship Complete NO - V5 *live"
label: Saved search — Flow 1 feed
code_refs:
- repo: lifeinteriors/shipping-v6-nswo
path: src/handlers/courierApproval.js
verified_at_commit: a3f9c21
verified_on: 2026-08-04
verified_by: backend-pm
documented_on: 2026-08-04 # when this page was written
documented_updated: 2026-08-21 # when this page was last edited
script_created: 2024-09-11 # when the source was created, per GitHub / Celigo
script_updated: 2026-07-21 # when it last changed there
script_source: from GitHub
last_reviewed: 2026-08-04
review_cycle: quarterly
---
| Field | Required | Type | Notes |
|---|---|---|---|
id |
if single-rule file | string | Omit when the file holds several rules; use ids: [...] instead. The build normalises it to ids in the index, so either form is queryable |
ids |
if multi-rule file | list | Every rule ID in the file. npm run check fails on a ### LI-BL-… heading this list does not declare — the index and both maps are built from here, not from the headings, so an undeclared rule would render on its page and exist nowhere else |
title |
✅ | string | Human title of the page |
domain |
✅ | enum | Full name of the domain code — one of the nine in §4, and checked against them. Warns if none of the entry's rules carries the code it names |
status |
✅ | enum | draft | active | superseded | retired |
project_owner |
✅ | string | A person — accountable for the project existing. See WORKING_AGREEMENT.md §2 |
developers |
✅ | list | People who built / maintain it. Amended from GitHub authorship — WORKING_AGREEMENT §3 |
for_department |
✅ | string | The team the automation is for — who feels it when it breaks |
fields |
list | The field registry — every field the automation reads or writes. See §8 and WORKING_AGREEMENT.md §4 |
|
systems |
✅ | list | Every system the logic touches — drives blast-radius queries. Slugs are checked against the list in systems — platform slugs below |
source_of_truth |
✅ | string | Whose value wins in a conflict |
triggers |
list | What causes the logic to run | |
terms |
list | Glossary term IDs this entry relies on. See GLOSSARY.md and §8 |
|
related |
list | Other rule IDs. Keep bidirectional — npm run check warns on a one-way link, because it draws a one-way integration map (§11) |
|
runs_after |
list | Rule IDs of logic that starts this one. Only for a real trigger, not for "happens later". Draws the chain edges on the map (§11) | |
depends_on |
list | Rule IDs that must have run already for this to read the right thing — a precondition, not a trigger. Draws the Needs first line on the Organisation Map (§11) | |
journey_stage |
enum | Where in the order journey this runs — buying | supply | order | pre-delivery | delivery | post-delivery. Groups the Organisation Map (§11) |
|
superseded_by |
if superseded | list | Required when status: superseded |
code_refs |
✅ | list | See §7 |
links |
if NetSuite or Celigo | list | Deep links into the systems. See Deep links below |
documented_on |
✅ | date | When this registry entry was first written |
documented_updated |
✅ | date | When this registry entry was last edited. Bump on every content change |
script_created |
date | When the source flow / script was created, per GitHub or Celigo | |
script_updated |
date | When the source last changed there. Read it; never estimate it | |
script_source |
string | Where the two script_* dates were read from — from GitHub, from Celigo |
|
last_reviewed |
✅ | date | ISO. Bump only after re-verifying against production |
review_cycle |
if vcs: none |
enum | monthly | quarterly | annual. Only required when a code_ref declares vcs: none — see below |
There is no version
The field was removed, and npm run check fails if it reappears. Nobody kept it accurate, so
what it recorded was not the version of anything — and a number nobody maintains reads as more
authority than the entry has earned. Two pairs of dates replaced it, and they answer different
questions:
| Pair | Question it answers | Where it comes from |
|---|---|---|
documented_on / documented_updated |
When was this page written, and last edited? | This repository — a registry fact |
script_created / script_updated |
When was the thing it describes created, and last changed? | GitHub or Celigo — a production fact |
A wide gap between the two pairs is the signal worth having: the automation moved and the page did
not. Both render in the metadata bar at the top of every entry. script_* is read, not
estimated — from the commit date at HEAD, or from the flow's createdAt / lastModified in
Celigo. If it cannot be read, leave it blank; it renders as needs confirming and npm run check
warns, which is the honest outcome.
for_department — suggestions to offer
Ask, and offer this list rather than an open question. It is far easier to correct a suggestion
than to answer from nothing. These are suggestions, not an enum — npm run check warns on a
value outside the list and never fails on one, and a team that is not listed is a team to add here.
Fulfilment Team · Warehouse & Logistics · Customer Service · Sales & Showroom · Buying & Merchandising · Inventory & Supply Chain · Finance · Marketing · E-commerce & Digital · IT & Data · Back End Team · Entire Life Interiors team · The customer (via Shopify)
The two in use today that are not team names — Entire Life Interiors team, and the customer and Customer (via Shopify) — are deliberate: some automations are read by everyone, and some are read only by the person buying.
The warning exists for the near-miss, not for the new team. Fulfilment Team and Fulfilment Team
and Customer Care are two strings and one team, and an entry filed under the second cannot be
found by anyone filtering on the first — which is the whole point of recording who feels it when it
breaks. If two teams genuinely both feel it, name the one that is called first and say so in the
business reading; for_department is singular on purpose.
systems — platform slugs
systems: and source_of_truth: drive the blast-radius queries and the systems view of the
Integration Map, so a second spelling of one platform draws two platforms. npm run check warns on
a slug outside the list below, and names the near match where there is one. Like the departments it
warns and never fails: a new platform is a normal thing to document.
AWS services carry the aws- prefix. aws-lambda set that shape and is the only AWS slug used
by more than one entry; an unprefixed sibling beside it reads as a separate platform. eventbridge
and aws-eventbridge were exactly that — one service counted twice across the registry.
aws-lambda · aws-eventbridge · aws-cloudwatch · aws-dynamodb · aws-s3 · aws-ses · aws-secrets-manager · aws-end-user-messaging · netsuite · celigo · shopify · detrack · kustomer · google-sheets · supabase
People are checked against each other, not against a roster
project_owner and developers are named people, and there is no roster to validate them against
— inventing one would be a guess about who works here (§1). So npm run check compares the names
the registry already uses against each other and warns when two differ by at most two characters.
One person entered under two spellings reads as two developers, and halves the entries you find
when you go looking for either of them.
The sidebar: search, then labels
Above the groups is a search box, because the sidebar alone was never going to be the way in.
A reader arrives holding a rule id from an Asana ticket, a NetSuite field name, or a word someone
used in a meeting — and none of those is a page title. It searches registry-index.json, so it
covers rule ids and names, entry titles, every field in every field registry, and every glossary
term and alias; it is generated from the entries, so it cannot drift from them. A hit on a rule
lands on that rule's anchor, not on the top of a thousand-line page. While a query is live the
results stand in for the nav — 290px cannot hold both, and a reader searching is not browsing.
Press / to focus it, Esc to clear it.
The layout has three widths. Above 1180px the sidebar sits at 290px. Between 900 and 1180 it narrows to 240 rather than disappearing — a 1024 laptop could not spare the full 290, and content there used to fall to 734px, narrower than at 768 where the sidebar collapses entirely. Below 900 it collapses behind the button.
The text scales with the screen, the line length does not. Body type runs from 15px to 18px between a phone and a large monitor, so a comfortable line covers more pixels rather than the same short line sitting in a wide empty column. The measure is capped at 100 characters — past the classic 45–75, deliberately, because this is a reference someone scans for a rule. The page itself caps at 1440px and centres: wide enough that the six-column test table never scrolls, narrow enough that a full line of text reaches most of the way across it.
Those two numbers are a pair. Raising the page cap without raising the type size is what stranded the prose at 744px in a 1630px column — 46% content, 54% margin.
The sidebar groups every page and arrives with every group shut, so the registry opens on a short column of headings rather than on every entry at once. Expanding is a deliberate click. Two rules keep that from becoming an obstacle:
- The group holding the page you are on opens itself, so the sidebar always shows where you are, and following a link does not shut the list you were reading.
- What you open is remembered for the current browsing session only. It survives navigating
between pages, and it is gone by the next visit — the registry always opens the same way, and
nobody inherits a sidebar shaped by a click they made months ago. Keep that property if you
touch the sidebar script in
build.mjs: never persist nav state inlocalStorage.
The count in each heading is what makes a shut group workable — it says how much is behind it, so nobody has to open one to find out whether it is worth opening. And because a group is usually the only thing a reader sees of its contents, the labels have to earn their line in a 290px column:
| Field | Effect |
|---|---|
nav_group |
Puts a root page under a named group instead of the default one |
nav_order |
Orders pages within a group; lower first. Entries without it sort by title, numerically, so flow 10 lands after flow 9 |
nav_label |
Replaces the page title in the sidebar only. The full title stays as the link's tooltip |
nav_hide |
Builds and links the page as normal but keeps it out of the sidebar. Used by 404.md |
An entry's title normally repeats its group — "Lead Time flow 3 — Kit item lead time…" under a
heading that already says Lead Time. The build strips that repeated prefix automatically, so
the heading carries the shared part and the link carries what distinguishes it. Reach for
nav_label only where that is not enough — two root pages whose titles start the same way, say.
Deep links
This registry names NetSuite saved searches and Celigo flows constantly. Naming one without
linking it makes every reader go and hunt for it, so an entry whose systems include netsuite
or celigo is expected to carry the matching link; npm run check warns when it does not.
links:
- type: celigo-flow
id: 66e0e59d32b330462921d448
integration: 5ad5bad381aef80b5ae1ec10
name: "Lead times V2: Inventory" # its name in the system — blank if unconfirmed
label: Celigo flow # what it is, in this entry's language
type |
Needs | URL the build makes |
|---|---|---|
netsuite-saved-search |
id |
system.netsuite.com/app/common/search/search.nl?id=<id> |
celigo-flow |
id, integration |
integrator.io/integrations/<integration>/flowBuilder/<id> |
github-repo |
repo, optional commit |
github.com/<repo>/tree/<commit> |
github-file |
repo, path, optional commit |
github.com/<repo>/blob/<commit>/<path> |
celigo-script · celigo-export · celigo-import · netsuite-script |
id |
None built. Renders the id with no direct link recorded |
other |
url |
Whatever url says |
Only URL shapes actually observed in this account are constructed. For the rest, supply an explicit
url: or accept the id on its own — a guessed link that 404s is worse than no link, because a
reader who follows it concludes the id is wrong.
name is the name the asset carries in the system, and it matters as much as the link: ids get
renumbered and hosts change, but a search stays findable by name. Leave it blank rather than
guessing, exactly as with a field label.
The build renders all of this as a Where this runs block above the tab switcher, so it appears in all three readings. Never hand-write that table into the body — two sources of the same fact drift apart.
Build note.
npm run buildstrips this front-matter withgray-matterbefore rendering, so it never shows up as body text, and it feedsregistry-index.json. If you add a field the index should expose, updatebuild.mjs.
7. Pointing at code, and drift
Because we never write to other repositories, nothing on the code side tells us when an
entry has gone stale. code_refs carries that burden instead.
| Field | Purpose |
|---|---|
repo |
owner/name |
path |
Exact file path in that repo |
verified_at_commit |
Short SHA the entry was checked against — load-bearing |
verified_on |
ISO date of that check |
verified_by |
Role that performed it |
Without verified_at_commit, there is no way to know whether an entry describes today's
system or last quarter's.
Logic that lives outside version control
Some logic does not live in a git repository at all — a Celigo flow, a NetSuite saved search, a script pasted into a vendor's UI. There is no commit to point at, so the rule above cannot be met literally, and pretending otherwise by inventing a SHA is worse than admitting the gap.
Such a code_ref declares vcs: none and carries dates instead:
| Field | Purpose |
|---|---|
vcs: none |
States plainly that the source has no version control |
path |
The asset as it is named in that system, with its id |
exported_on |
The date the definition was exported and read |
verified_on |
The date the behaviour was confirmed against production |
npm run check enforces exported_on + verified_on on these refs when an entry is active,
exactly as it enforces verified_at_commit on git refs.
Know what this costs. A git ref lets us ask "has this file changed since we checked?". A
non-git ref cannot answer that — if someone edits the flow tomorrow, nothing here will know.
Entries that rest on vcs: none therefore need a shorter review_cycle, because
re-verification is the only drift detection they have.
Re-verification loop (read-only, no writes to the source repo):
- Read the referenced repo at
HEAD. - Compare actual behaviour against the entry.
- Matches → bump
verified_at_commit/verified_on. Differs → update the entry (or raise a defect if production is wrong). - Commit here. The source repo is untouched throughout.
CI in this repo flags entries where verified_on is older than review_cycle. A later
enhancement can call the GitHub API read-only for the latest SHA touching those paths and
flag "changed since verified".
8. Entry anatomy
Three readings of the same rules, kept separate. The point is that the business can read the first without ever hitting the second.
| Layer | Section | Written for | Passes when |
|---|---|---|---|
| 1 | Front-matter | Machines, impact analysis | Schema validates, ID unique |
| 2 | ## Business logic |
Ops, CS, Buying, CEO | A new starter understands it with zero system access |
| 3 | ## Developer logic |
Devs and AI agents | A dev could rebuild the behaviour from this alone |
| 4 | ## Diagram |
Both, and training | Someone who has never seen the system can follow the flow |
The three headings are load-bearing: build.mjs splits the entry on them, in that order, at h2,
and renders each as a tab. Anything above the first heading — title, one-paragraph summary, rule
index — renders above the switcher and so appears in all three readings.
An entry that omits them still builds, as one plain page, and both npm run build and
npm run check warn. Don't rename or reorder them; don't nest them under another heading.
Never merge the business and developer readings. If the business reading needs a field ID to make sense, the rule is under-explained or is actually two rules.
Rule headings are load-bearing
A rule heading is written ### <ID> — <Name>, with the explicit anchor immediately above
it (§5). The build reads that heading to learn the rule's name, and then the name leads
everywhere and the id follows as a small chip — in the heading itself, the metadata bar, the
field registry, both maps, the glossary, and the hover helper on a diagram.
That is the whole mechanism. There is no name: field to keep in step, because the heading is
the name. It also means the format matters:
- Keep the
<ID> — <Name>shape. A heading the pattern cannot read falls back to showing the bare id, which is the thing this exists to avoid. - The business reading's name wins. A diagram usually repeats a rule under a shorter label — "Which day it ships" against the business reading's "Picking the day" — which is right for a diagram box and wrong as the rule's name. The build prefers the business heading.
- Write the name as something a person would say out loud. "Picking the day", "Whole bins, closest fit", "Overflow before Bonded". It is the label that will appear in a hover panel next to a diagram box, in a field's Used by column, and beside a glossary term.
Do not add a per-project rule number — "Shipping rule 1, rule 2". It reads tidier and is a
trap. The ids are domain-coded and cut across projects, so a local sequence never matches them:
this file's four rules are FUL-001, FUL-002, FRT-001, FUL-003, and FUL-004 is in a
different entry entirely. Worse, local numbers renumber when a rule is inserted, which is
precisely the drift §4 exists to prevent. The name is the readable handle; the id is the stable
one; there is no third.
Business logic — the six questions, in order
A reader arrives with the same six questions every time. Answering them in a fixed order means
nobody has to hunt, and a missing answer is visible rather than merely absent. Each is written as a
bold prompt, and npm run check warns when one is missing.
| Prompt | What belongs in it |
|---|---|
| What it does. | One paragraph. The decision this makes and the value it writes back |
| Why it exists. | The commercial or operational reason, and what it costs when it goes wrong |
| Who it affects. | Teams, customers, carriers, suppliers. Name who notices first |
| When it runs. | Trigger, timing, frequency, and what makes a record eligible |
| How it decides. | The whole logic at a high level, in five or six bullets |
| Outcomes. | Every way this can end, including the ones that write nothing |
How it decides is the one that is easy to get wrong. It is not a summary of the rules below and not a restatement of what it does: it is enough of the logic that a reader can predict the outcome for a normal case without opening a single rule. If it cannot do that, it is too thin. If it needs a field id to say it, the rule is under-explained or is really two rules.
Style:
- Present tense, active voice, subject first: "NetSuite assigns the order to the Sydney DC when…"
- No field IDs, no script names, no
custbody_, no Lambda names. - Every conditional becomes a table row or bullet — never a nested paragraph.
- State the cost of getting it wrong. That's usually the real reason the rule exists.
- Each rule gets its stable
<a id="…"></a>anchor here, above its###heading (§5), because this is the reading a shared link should land in.
The prompts may live in the section's opening or inside a single rule's block — both are inside
## Business logic, which is what the check looks at. An entry holding several rules normally puts
them in the opening, so they describe the whole automation rather than one rule of it.
Developer logic — required content
Every ### in this section becomes a collapsible on the site, so the layer can be as long as the
system demands. Ten sections are expected, and npm run check warns for any that is missing. They
fall into two halves: five that describe the behaviour, and five that prove it.
Describe it:
- Inputs: field/source, system, type, notes.
- Processing: pseudocode per §3.
- Outputs & side effects: what's written where, and who consumes it.
- Edge cases: with a handled ✅ / ❌ column. Unhandled edge cases are the point.
- Failure modes: symptom, detection, recovery. Write none automatic where nothing watches.
Prove it:
- Worked examples: real records, real values, traced end to end. One ordinary case and at least one awkward one. Where no real record can be traced, say so and say why — an example walked through the documented rules is fine if it is labelled as one.
- Test cases: given / when / expected / covers. Expected values, not "should work". Where today's behaviour is a known defect, give both today's result and the intended one — a test that asserts the bug is the test that proves the fix.
- UAT: how the business signs it off, in steps someone non-technical can follow with no access beyond the screens they use daily, with sign-off and date columns.
- Known limitations: what this deliberately does not do.
- Known defects: with refs, and Asana tickets where they exist.
A limitation is a decision; a defect is a mistake. Keeping them in separate sections is the point of having both: known scope that lives in the defect table gets re-reported as a bug forever, and a real defect parked among the limitations stops being anyone's problem.
Section titles may carry a tail — ### Inputs — the four saved searches, ### Processing — LI-BL-INV-006 — for the five descriptive sections. The five that carry proof are matched on their
exact title, because a reader asking "has this been tested?" should not have to guess the local
wording.
Field registry — one source, two renderings
Every field the automation reads or writes goes in the front-matter fields: block. The build
renders it twice, so a field is described once and each audience gets what it needs:
- in Business logic — the field's name as it reads in the system, what it means, and what it changes. No field ids, per the rule above.
- in Developer logic — keyed by
id, with record, source, type, read/written, and the rule IDs it drives.
Each rendering is split into Inputs and Outputs on the field's io — read lands in Inputs,
written in Outputs, and read-written in both. That is deliberate: people arrive with one of two
questions — what does this look at? and what does it change? — and a single mixed table makes
them scan for the answer. So io is not optional bookkeeping; leaving it off drops the field out
of both tables.
fields:
- id: custbody_to_approval # developer view — the field id
ui: Transfer Order Approval # business view — the label as it appears in the system
record: Transfer Order
source: NetSuite # or the saved search that supplies it
type: select
io: written # read | written | read-written
means: Whether the team has dealt with this transfer order yet.
impact: While it reads Pending Approval the automation may still add lines.
rules: [LI-BL-FUL-004]
confirmed: true # false → rendered as an open gap, not a guess
id, source, type, io, rules and impact are all derivable from the code, and should be
filled in from it. ui and means are not in the code — they come from the system or from the
business. Leave them blank rather than guessing: a blank renders as needs confirming, which is
honest and actionable. npm run check warns on unconfirmed fields; it never fails on them, because
an unconfirmed field must not block an otherwise-correct entry.
Vocabulary — one definition, referenced everywhere
A word that means something specific to this business is defined once, in
GLOSSARY.md, and referenced by ID from every entry that leans on it:
terms: [LI-TERM-SHIP-COMPLETE, LI-TERM-LINEHAUL, LI-TERM-CBM]
The build renders each as a link into the glossary, and npm run check fails if a term does not
resolve — the same integrity the rule IDs get.
- Term IDs are immutable, exactly as rule IDs are (§4).
LI-TERM-SHIP-COMPLETEkeeps that ID even if the business renames the term. The slug is readable so aterms:list means something at a glance; it is never rewritten to match new wording. - Never invent a meaning. A term nobody has defined is left blank and renders as needs
confirming — see
WORKING_AGREEMENT.md§4, which applies to terms exactly as it applies to field labels. - Using a word that isn't in the glossary? Add it, unconfirmed and undefined, and reference it. A term visible as a gap is worth far more than a word left floating in prose.
Diagram — required content
- Hand-written HTML, styled by
public/style.css. No diagram library: the boxes are plain elements, so they print, search and copy like text. - Printing gives the business reading followed by the diagrams, with the sidebar, the switcher
and the hover helper dropped. Put
class="no-print"on anything else that should not print — a screenshot placeholder, a note aimed only at on-screen readers. - Vocabulary:
.treewraps a diagram;.row/.col/.splitlay it out;.edge-v,.edge-hand.edge-labelare the connectors; every box is a.nodewith adata-kindofstart,decision,step,ok,stoporwatch. - The five node looks live in
style.css. Use inline styles only for a genuine one-off — a restyle must never have to touch an entry. - Every node carries
data-kicker,data-titleanddata-detail: one or two plain-language sentences feeding the hover helper. A diagram whose nodes have no notes is not finished. - The helper follows the cursor. Hovering a box draws its note beside that box; clicking pins it into the panel under the legend. A panel fixed at the top of a tall diagram is off screen by the time the reader reaches the bottom of it, which is exactly where the notes are needed most — so write the notes for someone reading them in isolation, next to one box, with no other context on screen.
- One overview tree, then one per rule. Put the rule ID in the tree's heading.
- Copy the worked example in
integrations/_TEMPLATE.mdrather than starting from nothing.
9. Statuses
| Status | Meaning | Requires |
|---|---|---|
draft |
Documented but not yet verified against production | — |
active |
Verified against production behaviour | Populated code_refs — with verified_at_commit, or exported_on + verified_on where vcs: none (§7) |
superseded |
Replaced by other rules | superseded_by |
retired |
No longer runs anywhere | Note of when it stopped |
draft → active requires verification, not just review. Reading the code is not the same
as confirming what production does.
10. Review and ownership
review_cycledefaults toquarterly; usemonthlyfor high-churn domains (FRT,FUL).- A git-backed entry may omit
review_cycleentirely. Itscode_refscarry a commit, so anyone can ask "has this changed since we checked?" at any moment — a clock adds nothing that the commit does not already give. An entry resting onvcs: none(§7) has no such answer, so periodic review is its only drift detection and a cycle stays mandatory.npm run checkenforces exactly that distinction. - Reviewing means re-verifying against production, then bumping
last_reviewed. Skimming and bumping the date is worse than leaving it stale — it launders an unverified entry as verified. - Ownership is recorded as named people —
project_owneranddevelopers— plus the team it is for. SeeWORKING_AGREEMENT.md§2. A name goes stale when someone leaves, which is exactly why §3 keeps it current from GitHub rather than falling back to a role. developersis amended whenever a new name shows up in GitHub against the documented behaviour (WORKING_AGREEMENT §3). Add, don't replace, and don't bumplast_reviewed— attribution verifies nothing about behaviour.- Ask before building anything new. WORKING_AGREEMENT §1 — documenting a system is never permission to build one.
11. The maps
Two pages, one nested under the other, both generated from entry front-matter at build time. Nothing on either is drawn by hand, so neither can drift from the entries the way a diagram in a whiteboarding tool would. Every rule ID on them links to the rule.
| Page | View | Built from | Answers |
|---|---|---|---|
ORGANISATION-MAP.md |
Diagram | journey_stage, source_of_truth, systems, runs_after, depends_on, status |
What exists and when in an order's life it happens: source of truth → automation → where the work lands |
MAP.md |
Systems and chains | systems, triggers, runs_after |
Which platforms a rule touches, what starts it, what to re-verify when a platform changes |
| Field ownership | the fields block of every entry |
Which rule reads or writes each field — and which fields are written from more than one entry |
The Organisation Map is the picture; the Integration Map is the detail beneath it. Both use the same node vocabulary as an entry diagram (§8), so they print, search and copy like text — the difference is only that these are generated rather than hand-written.
Rule IDs appear on the Integration Map, not on the Organisation Map. The picture answers what exists and in what order; a column of IDs under every box crowds out the box's name, which is the one thing it is there for. Anyone who needs the IDs is a click away, on the entry or on the Integration Map, and every ID there links to its rule.
The order journey
The Organisation Map reads in journey order, not folder order. journey_stage places an entry
on a six-rung ladder, and the map groups on it — business area is the sub-grouping inside a stage,
not the top level. An order has to be placed before anything can ship or deliver it, and someone
navigating the registry needs to see that sequence without already knowing which integration owns
which step. The ladder was confirmed with the business on 2026-08-31; it is not a shape
invented here, so do not re-cut it without asking.
| Stage | Covers |
|---|---|
buying |
What Buying & Merchandising commit to — what we choose to stock, and the purchase orders raised with suppliers |
supply |
Getting the stock into a warehouse and knowing when it will land: arrival dates, movements between warehouses, and the lead time a shopper reads |
order |
The order itself — what is stamped onto it at the moment of sale, and what is written back onto its lines while it waits |
pre-delivery |
The run-up to a delivery: picking the courier and the day, and agreeing that day with the customer. Nothing has moved yet |
delivery |
The consignment on its way, and what is known about where it has got to |
post-delivery |
Once the goods are with the customer — returns, claims, and anything that follows |
The stage is a business judgement, not a code fact, so it is asked for rather than inferred
(CLAUDE.md §4, WORKING_AGREEMENT.md §5). Two of the six are worth
naming because they read as more obvious than they are: buying is the Buying & Merchandising
team's own work, not the shopper's — the customer-facing lead time is supply, because it is
supply information the shopper happens to be shown; and an automation that writes onto an order
that already exists is order, wherever the information it writes came from.
An entry with no journey_stage is listed at the end as unplaced, never quietly filed under a
guess — npm run check warns, and the map says so on the page. A stage nothing is documented at
still appears, greyed: an empty rung is a gap worth seeing, and inferring that nothing happens
there would be exactly the mistake §6 of CLAUDE.md warns about. The ladder lives in JOURNEY in
build.mjs and in JOURNEY_STAGES in scripts/check-registry.mjs; changing it means changing
both.
Two kinds of dependency, and why they are not one field
| Field | Claim | On the map |
|---|---|---|
runs_after |
One automation starts this one when it finishes. A real trigger | An arrow labelled then starts |
depends_on |
This has to have run already for this one to read the right thing. Nothing chains them | A Needs first line under the box |
runs_after is the stronger claim and implies the dependency, so a rule named in both is recorded
once, under runs_after — npm run check warns on the overlap. A depends_on pointing at a rule
that is not documented is an error, not a warning: a precondition aimed at nothing reads as a
checked relationship. Record it in the gaps: list on MAP.md instead, where it is visible as a
gap rather than as an edge.
An entry with no runs_after draws no chain arrow, and that is information. Two automations
that look like a chain but are not chained will drift apart; the Organisation Map makes that
visible at a glance, and it has already been the shape of a real defect here. A depends_on with
no arrow above it is precisely that shape, drawn: the second automation can read yesterday's
answer, finish cleanly, and look like it worked.
The field view is the one that finds problems. Integrations rarely break at the system level; they break where two of them write the same field. Every cross-flow defect found in this registry so far has been field-level — a field written by two rules meaning different things, a field written to a name that does not exist, a field computed and mapped nowhere. A boxes-and-arrows diagram of systems shows none of those.
Fields are keyed by id and record. The same custom-field id on the Inventory Item and on the Kit Item is two different fields; collapsing them would bury the real collisions under false ones.
Keeping it honest. A generated map can only draw what is documented, and on its own would
quietly imply the rest does not exist. MAP.md therefore carries a hand-maintained gaps: list
of things known to exist that no entry covers. Add to it the moment you learn of something
undocumented — that list is the difference between a map that is honest about its edges and one
that misleads.
12. PR checklist
- No file in any other repository was created, edited or deleted
- No source code copied in; pseudocode is hand-written and language-neutral
- Front-matter complete and valid; ID unique against
registry-index.json - Explicit
<a id="...">anchor above each rule heading - Project owner, developers, department and
journey_stagewere asked for, not inferred (§6, §11, WORKING_AGREEMENT §2) - Everything that would render as needs confirming was asked for in one batch before publishing, not after (WORKING_AGREEMENT §5)
- Business reading answers what · why · who · when · how it decides · outcomes
- Plain English layer readable with zero system access
- Developer Logic reflects deployed behaviour, not intended behaviour
- Worked examples, test cases, UAT, known limitations and known defects all present
- The developer reading's sections are in skeleton order, and no heading carries a hand-written gloss (§15)
- Every field carries an
io, so it lands in Inputs, Outputs or both -
links:carries the saved searches and flows, if the entry touches NetSuite or Celigo -
code_refsincludeverified_at_commitandverified_on -
script_updatedread from GitHub or Celigo, not estimated -
relatedupdated in both directions, andruns_afterset if something starts this rule -
depends_onset where something must have run first without starting this rule (§11) -
MAP.mdgaps:updated if the work revealed something undocumented - Every business term the entry leans on is in
terms:and defined inGLOSSARY.md - Edge cases and known defects captured, with Asana refs where they exist
- Defects and open questions are in the front-matter registers, with a
defect_prefixand a status each (§13) - Test cases are in
test_cases:, each with anexpectedand, where it is known, the rule it exercises (§14) - Any defect whose rule you actually know is linked with
breaks:— never guessed (§13) -
documented_updatedbumped and change-history row added (there is noversion) - Integration
README.mdindex updated if a new entry was added - Feature branch → PR (no direct pushes to
main)
13. Defects and open questions
Both are front-matter registers, the same way the field registry is (§8). The build renders each
back under the ### Known defects / ### Open questions heading it was written beneath, and
gathers all of them onto DEFECTS.md and
OPEN-QUESTIONS.md, which are generated and never hand-edited.
They were body tables until 2026-09-28, in five different column shapes, which meant "what is open across the registry?" could only be answered by opening nine pages and reading them.
A defect is a mistake; a limitation is a decision
A defect is the automation doing something other than what the business decided it should. A limitation is known scope that was chosen. Limitations stay in the body and are deliberately not promoted: nobody works from them, and moving sixty-nine of them here would bury the list that people do work from. Keeping the two apart is what stops agreed scope being re-reported as a bug.
Record a defect whether or not anyone intends to fix it. "Not worth fixing" is a decision about a defect, not a reason to leave it unwritten.
Refs are immutable, and namespaced per entry
defect_prefix: LT2 # this entry's namespace
defects:
- id: LT2-D6
status: open # open · blocked · wont-fix · resolved · not-a-defect
severity: High # only where the entry recorded one
status_note: "Open — reporting only, no data impact"
breaks: [LI-BL-INV-008] # rule ids this defect breaks
effect: … # what it causes, where the entry recorded it separately
resolved_on: 2026-08-21 # set when status is resolved or not-a-defect
detail: >- # the defect itself
open_questions:
- id: LT2-Q1
status: open # open · part-answered · answered · moot
question: >-
why: … # why it matters
who: … # who can answer it
answered_on: 2026-09-23
A defect id lands in Asana tickets and in conversation, so it is as immutable as a rule id (§4)
— never renumbered, never reused. Each entry owns a prefix and numbers inside it, which is why the
four prefixes already in use (LT1-, LT2-, LT3-, SA-) were kept rather than folded into one
global sequence: they are cross-referenced several hundred times across the entries, and a global
renumber would have broken every one of those references to buy nothing.
npm run check enforces the namespace and uniqueness. Courier tracking's CT-1…CT-6 omit the
D that every other ref carries; that warns rather than fails, for the same reason — six ids are
not worth a rename.
Status is read, never assigned
An entry that resolved a defect already struck the ref through and wrote the date beside it. The
register keeps that reading. status_note preserves the entry's own wording where it had a Status
column, because "Open — blocked on Couriers Please" says more than blocked.
breaks is the column worth filling in
breaks links a defect to the rule it breaks, and almost none of them has one — the link was never
written down anywhere. It is not inferred, here or anywhere else: a guessed link between a
defect and a rule is worse than a blank one, because nobody re-checks the ones that look finished
(§1). npm run check warns per entry with the count, so the gap stays visible.
Filling it in is what turns "what is broken?" into "what is broken about the promise we make to customers?", which is the question the registry exists to answer.
14. Test cases
The third front-matter register, in the shape a test pack already uses:
test_cases:
- id: SA-TC-C1 # <defect_prefix>-TC-<the entry's own case id>
layer: Courier selection by service level
preconditions: Full Service, Life postcode valid (e.g. S308550)
input: The run executes
expected: Courier = Life Full; approved
rules: [LI-BL-FRT-001]
covers_defects: [SA-D2] # where the case exists to pin a defect down
note: Reason code 15
npm run build renders it under ### Test cases on the entry and gathers every case onto
TEST-CASES.md. The 144 cases were three different column shapes until
2026-09-28 — # / Given / When / Expected / Covers, ID / Scenario / Expected / Result, and this
one — so a pack spanning two entries had to be retyped before anyone could use it.
Case ids are namespaced, and never collide with a defect
<prefix>-TC-<case> keeps every id the entries already used — module 5's C1, E1, G1 and the
rest — while making them unique across the registry. The TC is not decoration: module 5 has both
a defect SA-D1 and a test case D1, and without it they would be the same string.
What the columns are, and what they are not
| Column | Is | Is not |
|---|---|---|
layer |
What is exercised — a named stage of the flow, the script, the saved search, the storefront | A folder or a system |
preconditions |
The state the system must be in for the case to mean anything | The action |
input |
What arrives, or what happens, against that state | |
expected |
What the registry says should happen. npm run check fails a case without one |
What production was observed doing, unless the entry says so |
rules |
The rule the case exercises. Checked against the entry's own ids | Guessed — a case whose entry never said reads needs confirming |
There are no result columns
Assistant verified, dev result, pass or fail describe one run of a pack, not the case. They
belong to the pack when it is issued, and putting them here would repeat the version mistake
(§6): a field nobody updates that reads as more authority than it has earned. The registry says
what should happen; a pack records what did.
An entry with no cases warns
The developer reading exists to let someone rebuild the behaviour and prove it still works (§8). Without cases it does only the first. It is a warning, never an error — writing cases for a flow whose logic has not been read would be inventing the expected column, which is worse than leaving it empty.
15. The developer skeleton, and its order
The developer reading is built from fourteen sections, in one order, on every entry. Both the
order and the one-line gloss the site prints under each heading come from SKELETON in
build.mjs — once, rather than from a line every entry has to write and keep true.
| # | Section | What it is |
|---|---|---|
| 1 | Inputs | What it reads, and where from |
| 2 | Processing | What it does with that, step by step |
| 3 | Decision tree | The same logic as a path through the decisions, in the order they run (§16) |
| 4 | Outputs & side effects | What it writes, and who reads it afterwards |
| 5 | Worked examples | Real records carried end to end |
| 6 | Test cases | What to run to know the behaviour is intact (§14) |
| 7 | UAT | What a person checks, by hand, before it is trusted |
| 8 | Edge cases | The inputs at the boundary, and whether each is handled |
| 9 | Failure modes | What breaks it, how that shows, how to recover |
| 10 | Known limitations | Scope deliberately chosen. A limitation is a decision |
| 11 | Known defects | Where it does something other than what was decided. A defect is a mistake (§13) |
| 12 | Open questions | What could not be established. Recorded rather than guessed (§13) |
| 13 | Source references (read-only) | The code and searches this reading was written from (§7) |
| 14 | Change history | What moved on this page, and when |
Why this order
It is the reading: what it reads · what it does · how it decides · what it writes · that it works · where it bends · what is wrong · where the truth is.
Proof before problems. Until 2026-09-28 the entries had drifted into two house styles — four
put the worked examples and tests straight after the outputs, four put the edge cases, defects and
failure modes there instead — and every shared heading ended up in a different position depending
on which entry you opened. Test cases alone appeared at position 5, 8, 12, 13 and 14. The tie is
broken in favour of proof, because limitations, defects and open questions each have a generated
page of their own now (DEFECTS.md, OPEN-QUESTIONS.md):
a reader hunting problems has a better route than scrolling one entry, so the entry establishes the
behaviour and shows it works first.
The tree sits next to Processing because they are the same mechanism at two altitudes. Splitting them puts two descriptions of one thing ten headings apart, which is what two entries did before this.
Sections of your own
The skeleton fixes the relative order of the fourteen, not the total set. An entry may add as many of its own as it needs — module 5 carries four, for the entry gate, the courier hierarchy, the reason codes and the schedule sheet.
- Put them between Processing and Decision tree, or as
####inside Processing. - Anything that sets the scene before the mechanism — which version is deployed, say — goes above Inputs.
- They are not counted by the order check, and they get no gloss. That is the right signal: the skeleton is the shape every entry shares, not everything an entry may say.
Never write the gloss by hand
The line under each heading is generated. Writing one into an entry gives you two descriptions of the same section that disagree within a year — the same reason a rule's name is read from its heading (§4) and never stored twice.
Order warns, it never fails
Where a section sits is a presentation choice, not a correctness one, and an entry that is right
about behaviour must never be blocked over it. npm run check names the sections that are out of
order and moves on.
16. The decision tree
Section 3 of the skeleton, and the one an analyst reaches for most. Four entries carry one; the shape below is drawn from them.
A tree is not a second copy of Processing. Processing is the mechanism; the tree is the same mechanism as a path through the decisions, in the order they run. What it adds, and what no table or diagram can express, is precedence — which branch wins, which step must happen before which, and why.
FOR EACH <subject>
├─ 1. <step> LI-BL-XXX-000
│ why this must come before the next one
├─ 2. <condition> ?
│ ├─ yes ──► <outcome> T3
│ └─ no ──► <outcome>
└─ 3. …
Every branch names its rule. A tree whose branches carry no LI-BL-… is a flowchart, and the
registry already has diagrams for that. Name the test case that proves the branch where there is
one (§14).
Say what must come first, and why. "Step 4 must stay in component units — a backorder on a part is covered by that part, one for one" is the whole value of the section. An ordering constraint with no reason beside it will be broken by the next person who tidies the code.
It shows deployed behaviour, not intended behaviour. Where an open defect changes what actually happens, say so on the branch and name it (§13). A tree that draws the rule as written, while production does something else, is worse than no tree: it reads as verified.
Mark what is not live. A planned change sitting beside current logic reads as current unless the tree says otherwise.
Stage a long one. Module 5 runs to four stages — does it enter, which pile, which courier, which day — each a separate fence with a sentence above it. One fence of sixty lines is a listing, not an explanation.
Do not write one you cannot source. Lead Time flow 4 has no tree because its calculation has not been read, and a tree drawn from guesswork would be the most confident-looking thing on the page (§1).