{"spec":"AIP-BS-MATERIAL-ORDER","version":"2026-10-03","standard":"material-order-build-standard","source":"https://aipathway.com.au/explore-ai/material-order-build-standard","markdown":"https://aipathway.com.au/explore-ai/material-order-build-standard.md","licence":"CC BY 4.0","how_to_read":"Ten of these twelve checks run on your own machine, against the stub, before you talk to anybody. The last one is a live tenant, and that one is quoted as a scoped build. One is not a software check at all: it asks what you measured before you started.","verdicts":{"pass":"asserted green here","fail":"asserted red here. This is a fixture: it names the check to price","shape_only":"shape holds on the stub, truth needs the live provider","with_us":"cannot be proved on the stub; the live step is quoted as a scoped build (see with_us_next)","evidence_required":"not a software check. Attach what proves it"},"tally":{"total":12,"onStub":10,"productionOnly":1,"partial":1,"preconditions":1},"envelope_version":"1.0","port_shape":{"read_ledger":{"verb":"read","subject":"ledger"},"read_job":{"verb":"read","subject":"job"},"read_asset":{"verb":"read","subject":"asset"},"read_rule":{"verb":"read","subject":"rule"},"read_contacts":{"verb":"read","subject":"contacts"},"read_segment":{"verb":"read","subject":"segment"},"resolve_address":{"verb":"read","subject":"address"},"write_job":{"verb":"write","subject":"job"},"read_pricebook":{"verb":"read","subject":"pricebook"},"draft_quote":{"verb":"write","subject":"quote"},"write_invoice":{"verb":"write","subject":"invoice"},"write_ledger":{"verb":"write","subject":"ledger"},"record_promise":{"verb":"write","subject":"promise"},"record_service":{"verb":"write","subject":"service_record"},"release_payment":{"verb":"write","subject":"payment"},"talk":{"verb":"say","subject":"turn"},"disclose":{"verb":"say","subject":"disclosure"},"send_sms":{"verb":"say","subject":"sms"},"notify":{"verb":"say","subject":"alert"},"escalate":{"verb":"say","subject":"handoff"},"issue_notice":{"verb":"say","subject":"notice"},"answer":{"verb":"say","subject":"answer"},"publish_report":{"verb":"say","subject":"report"},"record_consent":{"verb":"write","subject":"consent"},"read_file":{"verb":"read","subject":"file"},"write_file":{"verb":"write","subject":"file"},"raise_task":{"verb":"write","subject":"task"},"read_node":{"verb":"read","subject":"node"},"write_node":{"verb":"write","subject":"node"},"read_register":{"verb":"read","subject":"register"},"write_register":{"verb":"write","subject":"register"},"index_passage":{"verb":"attach","subject":"passage"},"search_index":{"verb":"read","subject":"index"},"read_inbox":{"verb":"read","subject":"message"},"file_message":{"verb":"attach","subject":"message"},"write_order":{"verb":"write","subject":"order"},"read_quote":{"verb":"read","subject":"quote"},"write_log":{"verb":"write","subject":"log"},"evidence":{"verb":"attach","subject":"evidence"},"cite":{"verb":"attach","subject":"citation"},"rank":{"verb":"decide","subject":"order"},"gate":{"verb":"decide","subject":"gate"}},"provider":{"boundary":{"never":["order a line the job does not carry","order what stock already covers","type a price; the supplier catalogue is the only source","order twice for one job and supplier","release over threshold or accept a substitution without a person"],"stays_with_person":["the release over threshold","approving a supplier","accepting a substitution","every variance"]},"job":"the materials are ordered from the job, from the catalogue, once","skill":"material-order","stub":"local.mock_supplier","production":null,"product":null,"insert":null,"with_us":{"kind":"scoped_build","note":"No hosted step. The idempotent write of the order into the system the business orders from is quoted as a scoped build."}},"with_us_next":"No hosted step. The idempotent write of the order into the system the business orders from is quoted as a scoped build. Send the failing check id for a fixed price; do not build the live step yourself.","default_path":null,"boundary":{"never":["order a line the job does not carry","order what stock already covers","type a price; the supplier catalogue is the only source","order twice for one job and supplier","release over threshold or accept a substitution without a person"],"stays_with_person":["the release over threshold","approving a supplier","accepting a substitution","every variance"]},"substrate":{"how_to_read":"These are roles, not products. Work out which you already have by matching `providers[].detect` against the repository, then build or add only the gap. No check asserts against a vendor, so any substrate that answers the role passes the same checks.","requires":[{"role":"record","label":"Somewhere the book lives","need":"Durable storage that the workflow owns the write path to, keyed so the same record can be found twice.","when_missing":"Half a day. Any Postgres will do and the free tiers are ample at this size. The trap is not the database, it is letting people edit around the app that is supposed to own the value.","ports":["read_job","read_pricebook","read_asset","evidence","read_rule","read_contacts","write_order"]},{"role":"schedule","label":"Something that runs it on time","need":"A trigger that fires on a cadence and can call an authenticated endpoint.","when_missing":"An hour. Expose one authenticated endpoint that starts the work and let anything call it, so the schedule is never welded to whoever is hosting you.","ports":["gate"]},{"role":"send","label":"Something that sends","need":"Email or SMS from a domain the recipient recognises, with delivery visible enough to tell a bounce from a silence.","when_missing":"An hour for the wiring, longer for the DNS. Sending from a domain you do not own is the single fastest way into a spam folder.","ports":["notify"]},{"role":"surface","label":"Something a person opens","need":"A view of the book that whoever does this job can read and act on without asking a developer.","when_missing":"Start at tier 0 and stop there until the business asks for more. The cheapest surface is not a screen, it is a scheduled email carrying the list, and for one person doing this job that is not a compromise, it is the right answer. On Microsoft 365 or Workspace you can skip straight past this: a list and a sheet are already what people open. Build upward only when two people need the same list at the same time.","tiers":[{"tier":0,"label":"A scheduled digest carrying the list","cost":"About an hour. Needs nothing you do not already have.","satisfiedBy":"send","whenYouNeedIt":"One person does this job. They want to know what is waiting when they sit down, and they act on it from their inbox."},{"tier":1,"label":"A read-only page showing the book","cost":"An afternoon. One query and one table.","whenYouNeedIt":"Somebody needs to look mid-day without waiting for tomorrow's digest, or to answer a customer on the phone."},{"tier":2,"label":"Editable rows, a queue and a log","cost":"A day or two with a coding agent, which is what agents are best at. It is bounded work: the fields are the schema, the queue is whatever escalate hands off, the log is the trace.","whenYouNeedIt":"More than one person shares the book and they need to correct it without treading on each other, and you need to see afterwards who changed what."}],"already_reachable":[{"tier":0,"label":"A scheduled digest carrying the list","because":"this standard already requires the send role, so nothing new is needed for it"}],"ports":["escalate"]}],"registry":"https://aipathway.com.au/substrates.json"},"checks":[{"id":"lines-come-from-the-job","n":1,"title":"Every ordered line traces to a material line on the job","statement":"An order line exists because the job carries a material line with that catalogue code and quantity. Nothing is ordered that the job does not list, and nothing on the job is dropped without a parked reason.","assertable":"stub","ports":["read_job","write_order"],"stimulus":"One job with four material lines, each with a catalogue code and a quantity.","assertion":"The job is read before any order is written. The ordered lines, plus any line reserved from stock, match the job's codes and quantities exactly. An order line with no job line fails."},{"id":"stock-checked-before-ordering","n":2,"title":"Stock on hand is read before the order, and a line stock covers is reserved rather than ordered","statement":"Before any order is written, stock on hand is read. A line the store or the van already covers is reserved against the job and does not appear on a purchase order. An order that ignores stock fails.","assertable":"stub","ports":["read_asset","write_order"],"stimulus":"The same job, with stock on hand covering one of its four lines in full.","assertion":"The stock read precedes the first order write. The covered line is reserved against the job and appears on no order. The other three lines are ordered. Ordering the covered line fails."},{"id":"supplier-catalogue-is-the-only-source","n":3,"title":"Every line carries the supplier's catalogue code and price, read at order time, and a line no catalogue holds parks the order","statement":"The price on a line is the supplier's catalogue price for that code, read when the order is drafted. No price is typed, remembered or averaged. A code no supplier's catalogue holds parks the order with reason not_in_catalogue and a person is told.","assertable":"stub","ports":["read_pricebook","write_order"],"stimulus":"A job whose lines include one code that no supplier's catalogue holds.","assertion":"Each catalogue is read before the first order write. Every priced line's price equals that supplier's catalogue price for its code. The unknown code sits on a parked order with reason not_in_catalogue and no price. A typed price fails."},{"id":"one-order-per-job-and-supplier","n":4,"title":"One order per job and supplier, written once, updated on a second run","statement":"The write is keyed to the job and the supplier. A second run for the same job updates the existing order or does nothing. Two orders for one job and supplier fails, and a null external id is a failed write, not a success.","assertable":"stub","ports":["read_job","write_order"],"stimulus":"The same job run through the build twice, an hour apart.","assertion":"Exactly one order exists per job and supplier, each with an external id from the ordering system, and the second run produced an update or no write. A second order for the same pair, or a null id treated as success, fails."},{"n":5,"assertable":"both","stubLimit":"The stub proves nothing leaves without a release event carrying a named person. Only the live path proves what left is what was released, rather than something regenerated after the approval.","id":"threshold-needs-a-person","title":"An order over the threshold, or to a supplier not on the approved list, is released only by a named person","statement":"A release is one of two things: a named person, or a rule a named person set with a threshold. An order whose total is over the threshold, or whose supplier is not on the approved list, waits for a person. Under the threshold and to an approved supplier, the rule may release.","ports":["gate","write_order"],"stimulus":"Three orders: one under the threshold to an approved supplier, one over it, one to a supplier not on the approved list.","assertion":"The first carries the rule's name on its release. The second and third are released only by a person's name or still wait, and each was routed to a person. A release by rule over the threshold or to an unapproved supplier fails, and a send with no release fails."},{"id":"lead-time-is-data","n":6,"title":"Each supplier carries a lead time, and an order that would miss the job's start tells the job owner","statement":"Lead time is a number on the supplier or the catalogue line, not a guess. When the order's arrival date lands after the job's start, the job owner is told in the same run, with the order and the supplier named.","assertable":"stub","ports":["read_contacts","notify"],"stimulus":"A job starting in three days, with one line from a supplier whose lead time is ten days and the rest from a supplier at two.","assertion":"The supplier list is read. One notification names the job owner, the slow supplier's order and the arrival date. No notification is raised for the order that arrives in time. A late order with no notification fails."},{"id":"delivery-matches-back","n":7,"title":"A delivery is recorded against the order line by line, and a delivery with no order is refused","statement":"When goods arrive, the docket is matched to the order and each line is recorded with the quantity received and, where it differs, a reason: short or damaged. A docket that matches no order is not recorded as a delivery; it is raised to a person.","assertable":"stub","ports":["evidence","read_job"],"stimulus":"Two dockets: one against a sent order with one line short, one against an order number that does not exist.","assertion":"The first docket produces a delivery record on its order with a line per ordered line, the short line carrying its received quantity and reason short. The second produces no delivery record and an escalation naming the docket. A delivery attached to no order fails."},{"id":"variance-is-a-queue","n":8,"title":"Short, over, wrong item and a changed price are escalated with the order id and an owner, never absorbed","statement":"Every difference between what was ordered and what arrived is a variance with a kind, the order id and an owner. It is never silently accepted into the job's cost, and it is never silently written off.","assertable":"stub","ports":["escalate"],"stimulus":"One delivery carrying a short line, an over-supplied line, a line the order does not hold, and a line whose docket price differs from the order.","assertion":"Four escalations exist, one per kind: short, over, wrong_item and price_changed. Each names the order id and a person as owner. A variance with no escalation, or one with no owner, fails."},{"id":"nothing-is-substituted-silently","n":9,"title":"A supplier's substitution is accepted only by a named person","statement":"When the supplier delivers a different item in place of the one ordered, the line is received only when a named person has accepted the substitute. Without that acceptance the line is held as a variance and a person is told. A substitution accepted by a rule, or by nobody, fails.","assertable":"stub","ports":["gate","escalate"],"stimulus":"One delivery with two substituted lines: one the site supervisor signed for on the docket by name, one nobody accepted.","assertion":"The signed line carries an acceptance naming the supervisor. The other is escalated as a substitution and is not counted as received. An acceptance with no person's name fails."},{"id":"the-record-by-job","n":10,"title":"Every order, release, delivery and variance is retrievable by job","statement":"When the supplier's invoice arrives, or the customer asks what a material cost, the record shows by job id: each order with its external id and the catalogue edition, who released it or which rule did, each delivery line by line, and every variance with its owner.","assertable":"stub","ports":["read_job","evidence"],"stimulus":"One job ordered, released, delivered short and escalated.","assertion":"A read by job id returns the orders with external ids and catalogue editions, the release with a name or a rule, the delivery lines and the variance. A record missing the release or the delivery fails."},{"id":"survives-the-builder","n":11,"title":"It survives the builder","statement":"Someone other than the builder can explain what it does, and it runs on an account the business owns.","assertable":"precondition","ports":["read_rule"],"stimulus":"No stimulus. The builder names who else can explain it and whose account the catalogue and the release rule runs on.","assertion":"The runner reports what was declared and never scores it green on its own say-so.","stubLimit":"Not a runtime property. Software cannot prove a second person understands it, and outliving its author is precisely what is being asked about."},{"id":"pass-test","n":12,"title":"The pass test passes","statement":"On the real job system and a real supplier account: one job ordered twice, one line covered from stock, one line no catalogue holds, one order over the threshold waiting for a person, and one delivery short.","assertable":"production","ports":["read_job","read_pricebook","write_order","gate"],"stimulus":"A real job on the business's own job system, real stock, a real supplier account with its catalogue, and a real delivery docket.","assertion":"One order per supplier in the ordering system with its own id after two runs, the stock line reserved and not ordered, the unknown code parked and unpriced, the over-threshold order waiting for a named person, and the short delivery recorded line by line with a variance owned by someone.","stubLimit":"Deliberately unpassable on a stub. A stub ordering system accepts every write and a stub catalogue holds every code you seed. Only the real account proves the id is real, the second run did not make a second order, and the supplier's price was the one read."}]}