{"spec":"AIP-BS-JOB-CLOSE","version":"2026-11-01","standard":"job-close-build-standard","source":"https://aipathway.com.au/explore-ai/job-close-build-standard","markdown":"https://aipathway.com.au/explore-ai/job-close-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":5,"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"},"read_sales":{"verb":"read","subject":"sales"},"read_sale":{"verb":"read","subject":"sale"},"draft_message":{"verb":"write","subject":"draft"},"send_message":{"verb":"say","subject":"message"},"read_memory":{"verb":"read","subject":"memory"},"propose_memory":{"verb":"write","subject":"proposal"},"write_memory":{"verb":"write","subject":"memory"},"close_job":{"verb":"write","subject":"job"},"raise_exception":{"verb":"write","subject":"exception"},"list_exceptions":{"verb":"read","subject":"exception"},"evidence":{"verb":"attach","subject":"evidence"},"cite":{"verb":"attach","subject":"citation"},"rank":{"verb":"decide","subject":"order"},"gate":{"verb":"decide","subject":"gate"}},"provider":{"boundary":{"never":["create a job; a close starts from the job id the job system issued","invent a part, or a price the business's own price book does not hold","price a variation the quote did not cover","write a card the assigned tech has not confirmed","mark a job complete before the write returns the job system's ids","write a second card for one capture, or mint a new intent id on a retry","send an invoice"],"stays_with_person":["confirming the draft: the assigned tech, from their own phone","pricing every part parked as not in the book or ambiguous","pricing every variation, and getting the customer's signature for it","chasing a tech who has not confirmed, as their supervisor","fixing a write that failed"]},"job":"the job card done on site","skill":"job-close","stub":"local.mock_job_close","production":null,"product":null,"insert":null,"with_us":{"kind":"scoped_build","note":"Office Voice reads the job and the price book today. Its close_job tool, which writes the card, lines and photos into ServiceM8 or Simpro once per intent id and marks the job complete, and closing by text from site are being built and are not live. Until they ship, the write into the job system is quoted as a scoped build, or run on the business's own job system key."}},"with_us_next":"Office Voice reads the job and the price book today. Its close_job tool, which writes the card, lines and photos into ServiceM8 or Simpro once per intent id and marks the job complete, and closing by text from site are being built and are not live. Until they ship, the write into the job system is quoted as a scoped build, or run on the business's own job system key. Send the failing check id for a fixed price; do not build the live step yourself.","default_path":null,"boundary":{"never":["create a job; a close starts from the job id the job system issued","invent a part, or a price the business's own price book does not hold","price a variation the quote did not cover","write a card the assigned tech has not confirmed","mark a job complete before the write returns the job system's ids","write a second card for one capture, or mint a new intent id on a retry","send an invoice"],"stays_with_person":["confirming the draft: the assigned tech, from their own phone","pricing every part parked as not in the book or ambiguous","pricing every variation, and getting the customer's signature for it","chasing a tech who has not confirmed, as their supervisor","fixing a write that failed"]},"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":["write_job","read_job","read_pricebook","evidence","read_rule","read_inbox","write_log","close_job","raise_exception"]},{"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":["send_sms"]}],"registry":"https://aipathway.com.au/substrates.json"},"checks":[{"id":"job-exists-tech-on-it","n":1,"title":"The job exists and this tech is on it","statement":"A close starts from the job id the job system issued, read back from the job system before anything else. No job, no close: a close is never the thing that creates a job. A capture from a tech who is not assigned to that job goes to the office, not onto the card.","assertable":"both","ports":["read_job","raise_exception","close_job"],"stimulus":"Three captures: one for a real job from its assigned tech, one quoting a job number the job system does not hold, and one from a tech who is not on the job they named.","assertion":"Only the first is written. The second is raised as no_job and the third as not_assigned, each to the office manager, and neither has a write attempted against it.","stubLimit":"The stub proves the job is read and the tech compared before anything is written. Only the live job system proves the build reads the field that means assigned there, which is a different shape in ServiceM8, Simpro, AroFlo and Fergus."},{"id":"verbatim-stored-first","n":2,"title":"What the tech said is stored before anything is taken from it","statement":"The transcript, the notes, the photos and the parts as spoken are kept word for word as evidence first. The draft is worked out from the stored copy, so what the tech said can always be set beside what was written.","assertable":"stub","ports":["evidence","read_pricebook"],"stimulus":"Two captures from site, each with a transcript, photos and parts spoken in the tech's own words.","assertion":"Each capture has one stored copy equal to what arrived, transcript and photos exactly, and it was stored before the price book was read or a draft went to the tech for that capture."},{"id":"parts-from-the-book","n":3,"title":"Every part is a price book line or it is parked","statement":"Each part the tech names resolves to exactly one catalogue line in the business's own price book, at the price the book holds when the card is written. A part that matches nothing is parked as not_in_book; one that matches more than one line is parked as ambiguous. No price is ever estimated, averaged or remembered.","assertable":"both","ports":["read_pricebook","raise_exception","close_job"],"stimulus":"A capture naming two parts the book holds, one it does not, and one that matches two catalogue lines, with the price of one known part changed in the book between the draft and the write.","assertion":"The written card carries only lines with a catalogue code, each at the book's price at the time of the write, including the changed one. The unknown part is parked not_in_book and the double match parked ambiguous, and neither appears on the card.","stubLimit":"The stub proves the rule against a small book with tidy names. Only the business's own catalogue proves the build resolves the names its techs actually say against codes it actually holds, in ServiceM8 materials or the Simpro catalogue."},{"id":"variation-never-priced","n":4,"title":"A variation is never priced at close","statement":"On a quoted job, a part or line the quote did not cover is parked as a variation and goes to the person who prices variations. It is not put on the card at any price, even one the book holds. Invoice Out bills it only once the customer has signed for it.","assertable":"stub","ports":["read_job","raise_exception","close_job"],"stimulus":"A quoted hot water job where the tech also fitted a tempering valve the quote did not include. The valve is in the price book.","assertion":"The valve is parked as variation to the variation pricer and is on no written card. Lines the quote did cover are written as usual."},{"id":"tech-confirms","n":5,"title":"The tech confirms the draft before it is written","statement":"The draft goes to the tech in one message: the work done, the lines, and what was parked. Only a yes from the assigned tech's own number, naming that draft, lets it be written. A yes from the office, or a draft the model marked confirmed itself, is not the tech's confirmation.","assertable":"stub","ports":["send_sms","read_inbox","close_job"],"stimulus":"Two drafts go to the tech. The office manager replies YES to one from her own phone; later the tech replies YES to the other.","assertion":"Every write follows a yes from the job's assigned tech naming that draft. The draft the office manager answered is not written."},{"id":"unconfirmed-to-supervisor","n":6,"title":"A draft nobody confirms goes to the tech's supervisor","statement":"If the tech has not confirmed within the written window, two hours by default, the draft is raised as not_confirmed to the tech's supervisor, once. It is not written and the job is not marked complete while it waits.","assertable":"stub","ports":["read_rule","raise_exception"],"stimulus":"Two drafts go out and the tech answers neither. The clock moves past the window and the build runs twice more.","assertion":"Each draft is raised once as not_confirmed to the supervisor after the window and not before, and neither job has a write or a completion."},{"id":"one-write-ids-back","n":7,"title":"One write, with the job system's ids back","statement":"The card, its lines and its photos are written once, with the intent id close:<tenant>:<job>:<capture>. A retry carries the same intent id and lands once. A write that returns no ids is treated as not written and raised as write_failed to the office manager.","assertable":"both","ports":["close_job","raise_exception"],"stimulus":"The first write for a confirmed card lands but its answer is lost on a bad signal; the build runs again. Another write is refused outright.","assertion":"Exactly one card is in the job system for the first job, with both photos and the intent id in the agreed shape, and every attempt for it carried that same id. The lost answer and the refusal are each raised as write_failed.","stubLimit":"On the stub the job system holding one card per intent id is the runner's memory, so it proves the build sends the same id on every try. Only a real job system proves a retry after a slow write from a phone in a roof space does not leave two cards."},{"id":"complete-only-when-written","n":8,"title":"The job is marked complete only after the write returns ids","statement":"Completion is the last step and the only trigger Invoice Out accepts, so it happens only once the write has come back with the job system's ids. A parked item does not hold completion: it travels to Invoice Out, which parks the invoice.","assertable":"both","ports":["close_job","write_job"],"stimulus":"One confirmed card whose write fails the first time and lands the second, and another whose write lands first time with two items parked.","assertion":"Each job is marked complete once, after a write for it that returned ids, and never after a failed write alone. The job with parked items is complete.","stubLimit":"The stub proves the order of the two writes. Only the live job system proves the status set is the one its own invoicing and Invoice Out read as complete."},{"id":"parked-to-the-desk","n":9,"title":"Every parked item goes to the exceptions queue with an owner","statement":"Each part not in the book, ambiguous match and variation is raised once to the Exception Desk queue with the job id, the tech's words exactly and a named owner: the office manager for a part, the variation pricer for a variation. Nothing parked is left only in a message.","assertable":"stub","ports":["raise_exception"],"stimulus":"Two confirmed closes between them parking a part not in the book, an ambiguous match and a variation, and the build run three times.","assertion":"Each parked item has exactly one exception carrying its job id, the words the tech said and the owner the table names for its reason."},{"id":"logged-by-job","n":10,"title":"The close is on the record, by job","statement":"For each job: what the tech sent, what was taken from it, who confirmed it and when, and what the write returned, ids or the reason it failed. The history of one close can be read in one place.","assertable":"both","ports":["write_log","evidence"],"stimulus":"A run with two closes, one write lost and retried, and one parked variation.","assertion":"Every capture has its stored copy, and every draft, confirmation and write attempt has its own record entry naming the job, each write entry holding the ids or the failure.","stubLimit":"On the stub the record is the runner's memory, so it proves the entries are made. Only a store outside the build, that outlives it and nobody edits, proves the history survives."},{"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 close and its record 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 a real job in the business's own ServiceM8 or Simpro: a thirty-second voice note naming one part in the price book and one that is not, and two photos. Within five minutes the card shows the work done, the known part at the book price, both photos and the job complete; the unknown part is on the exceptions list, not the card. Sent again, nothing duplicates.","assertable":"production","ports":["read_job","read_pricebook","close_job","raise_exception"],"stimulus":"A tester's own voice note and two photos from a real job, confirmed from the assigned tech's phone, then the same capture sent a second time.","assertion":"The live job system shows one card with the known part as a catalogue line at the book price, both photos and the job complete; the unknown part is one exception and on no card; the second send adds nothing.","stubLimit":"Deliberately unpassable on a stub. On a stub the line, the price and the photos land in the runner's memory with ids it made up; live, they have to appear in the business's own job system with its own ids, after a phone on site sent them, and a retry over a bad signal is where the second card comes from."}]}