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: superseded with superseded_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

  1. Check registry-index.json (generated on build) for the highest number in that domain — its rules array is one flat, id-sorted list of every documented rule.
  2. Take the next one. Never reuse a gap.
  3. 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 in localStorage.

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 build strips this front-matter with gray-matter before rendering, so it never shows up as body text, and it feeds registry-index.json. If you add a field the index should expose, update build.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):

  1. Read the referenced repo at HEAD.
  2. Compare actual behaviour against the entry.
  3. Matches → bump verified_at_commit / verified_on. Differs → update the entry (or raise a defect if production is wrong).
  4. 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-COMPLETE keeps that ID even if the business renames the term. The slug is readable so a terms: 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: .tree wraps a diagram; .row / .col / .split lay it out; .edge-v, .edge-h and .edge-label are the connectors; every box is a .node with a data-kind of start, decision, step, ok, stop or watch.
  • 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-title and data-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.md rather 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_cycle defaults to quarterly; use monthly for high-churn domains (FRT, FUL).
  • A git-backed entry may omit review_cycle entirely. Its code_refs carry 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 on vcs: none (§7) has no such answer, so periodic review is its only drift detection and a cycle stays mandatory. npm run check enforces 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_owner and developers — plus the team it is for. See WORKING_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.
  • developers is amended whenever a new name shows up in GitHub against the documented behaviour (WORKING_AGREEMENT §3). Add, don't replace, and don't bump last_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_stage were 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_refs include verified_at_commit and verified_on
  • script_updated read from GitHub or Celigo, not estimated
  • related updated in both directions, and runs_after set if something starts this rule
  • depends_on set where something must have run first without starting this rule (§11)
  • MAP.md gaps: updated if the work revealed something undocumented
  • Every business term the entry leans on is in terms: and defined in GLOSSARY.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_prefix and a status each (§13)
  • Test cases are in test_cases:, each with an expected and, where it is known, the rule it exercises (§14)
  • Any defect whose rule you actually know is linked with breaks: — never guessed (§13)
  • documented_updated bumped and change-history row added (there is no version)
  • Integration README.md index 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).