{"spec":"AIP-BS-TWO-RECORDS","version":"2026-10-03","standard":"two-records-build-standard","source":"https://aipathway.com.au/explore-ai/two-records-build-standard","markdown":"https://aipathway.com.au/explore-ai/two-records-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 runs against your own live systems. 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; runs against your own live systems (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":["join on a name or an amount","correct a record without a person's acceptance","overwrite history","stay silent on an all-clear","guess which source is master"],"stays_with_person":["accepting every correction","declaring the master per field","setting a tolerance","deciding an orphan is real"]},"job":"two systems say the same thing about the same job","skill":"two-records","stub":"local.mock_pair","production":null,"product":null,"insert":null,"with_us":{"kind":"own_systems","note":"No hosted step. The production check runs over the business's own two sources with a person on every correction."}},"with_us_next":"No hosted step. The production check runs over the business's own two sources with a person on every correction. There is nothing to buy: your build does this step on the business's own systems. No production run is offered yet, because the live port map is not published, so do not run AIPATHWAY_ENV=production. Run the pass test by hand on the real systems and attach the evidence.","default_path":null,"boundary":{"never":["join on a name or an amount","correct a record without a person's acceptance","overwrite history","stay silent on an all-clear","guess which source is master"],"stays_with_person":["accepting every correction","declaring the master per field","setting a tolerance","deciding an orphan is real"]},"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_ledger","evidence","read_rule","write_ledger"]},{"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","publish_report"]}],"registry":"https://aipathway.com.au/substrates.json"},"checks":[{"id":"a-pair-is-declared-not-guessed","n":1,"title":"A pair is declared in a written contract, never guessed","statement":"Every reconciliation names its two sources, the shared key that joins them, and which source is master for each field, in a contract a person wrote. A run with no declared master fails.","assertable":"stub","ports":["read_ledger","read_job"],"stimulus":"A written pair contract naming both sources, the shared key, and a master for each compared field, beside two seeded sources.","assertion":"The contract is read before either source. The published run names the contract and declares the same master per field the contract holds. A run under no contract, or one that declares no master, fails."},{"id":"keys-join-ids-not-labels","n":2,"title":"Records are joined on the shared id, never on a name or an amount","statement":"The join key is the id the contract names. Two records with the same customer name, or the same amount, are not a pair unless they share that id.","assertable":"stub","ports":["read_ledger","read_job"],"stimulus":"Two sources where one record in each carries the same customer name and the same amount but different ids.","assertion":"The run declares the contract's key as the join. Those two records are not matched and no difference is raised between them. A run joined on name or amount fails."},{"id":"matched-is-silent-mismatched-is-an-object","n":3,"title":"A matched pair produces nothing; a mismatch produces a Difference, never a corrected record","statement":"Agreement is counted and not shown. A disagreement becomes a Difference carrying the field, both values and the master's value. No record in either source is changed by the comparison.","assertable":"stub","ports":["read_ledger","read_job"],"stimulus":"Five shared records: three that agree on every field and two that disagree on the total.","assertion":"No Difference exists for the three that agree. The mismatch carries the field, both values and the master's value. No write reached either source and no field changed. A corrected row fails."},{"n":4,"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":"nothing-is-auto-corrected","title":"A correction is proposed with both values and written only after a named person accepts it","statement":"The run proposes the master's value for the other source. The write happens once, after a named person accepts the proposal, and never before. A write with no acceptance fails.","ports":["gate","write_ledger"],"stimulus":"One open difference. The build runs, a named person accepts the proposal an hour later, and the build runs again.","assertion":"No write exists before the acceptance. Exactly one write exists after it, carrying the person's name, and the proposal preceded it. A write with no acceptance, or a second write, fails."},{"id":"orphans-are-a-queue","n":5,"title":"A record with no partner is an orphan with a reason and an owner, not a silent gap","statement":"A record in one source with no record under the same id in the other is raised as an orphan naming the source, a reason and the person who owns it. It is counted on the run and never dropped.","assertable":"stub","ports":["escalate"],"stimulus":"One record in the first source with no partner in the second, and one in the second with no partner in the first.","assertion":"Two orphans exist, one per source, each with a reason and an owner, and each was handed to that owner. The run counts two orphaned. A run that reports none fails."},{"id":"tolerance-is-written-down","n":6,"title":"Rounding and timing tolerances are declared per field with a value, and never assumed","statement":"A tolerance is a number a person wrote against a field in the contract. A difference inside it is recorded as within_tolerance, still an object. A field with no declared tolerance has none, so any difference on it is open.","assertable":"stub","ports":["read_ledger","gate"],"stimulus":"A contract declaring a small tolerance on one field and none on another, with one mismatch inside that tolerance and one outside it.","assertion":"The small difference is recorded with status within_tolerance and the large one as open. Every within_tolerance difference names a field the contract holds a tolerance for. An assumed tolerance fails."},{"id":"runs-on-a-schedule-and-publishes-regardless","n":7,"title":"The run publishes on schedule with its counts, including the all-clear","statement":"Every scheduled run publishes a count of matched, within tolerance, mismatched, orphaned and corrected, even when every number but matched is zero. Its absence is never the signal.","assertable":"stub","ports":["publish_report"],"stimulus":"Two sources that agree on every record, and the clock advanced through two scheduled runs.","assertion":"Two runs are published, each with all five counts and a published time, each an all-clear. A run that stays silent because there was nothing to report fails."},{"id":"thresholds-route-to-a-person","n":8,"title":"A difference over the declared threshold, or too many mismatches, reaches a named person that run","statement":"The contract declares an amount and a rate. A single difference over the amount, or a run whose open mismatches exceed the rate, goes to the named person in the same run, not the next one.","assertable":"stub","ports":["notify","escalate"],"stimulus":"A contract with a threshold amount and a mismatch rate, and one difference over the amount that also tips the run over the rate.","assertion":"The named person is notified of that difference before the run publishes, and the run is escalated for its rate. A threshold breach that waits for the next run fails."},{"id":"history-survives-the-fix","n":9,"title":"Every accepted correction keeps both original values and the person who accepted","statement":"The Difference keeps both values as they were. The Correction records the proposed value, who accepted, when, and the id the write returned. A fix that overwrites the history fails.","assertable":"stub","ports":["evidence"],"stimulus":"One difference, accepted by a named person and written.","assertion":"The other source now holds the master's value, the Difference still holds both original values, and the Correction names the person, the time and the written id. Any changed field with no Correction behind it fails."},{"id":"the-record-by-key","n":10,"title":"For any shared id, the runs, differences and corrections are retrievable","statement":"When somebody asks why the ledger says what it says about a job, the record by that id shows every run that raised a difference on it, each difference with both values, and each correction with who accepted it.","assertable":"stub","ports":["read_ledger","evidence"],"stimulus":"One shared id that was mismatched in one run, accepted, and written in the next.","assertion":"A read by that id returns the runs that touched it, the difference with both values, and the correction with the person's name. A record missing the correction 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 pair contract and the runs 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 business's real sources: one seeded mismatch found and shown side by side, one orphan raised, one correction accepted by a person and written once, and an all-clear published the following run.","assertable":"production","ports":["read_ledger","read_job","write_ledger","gate"],"stimulus":"A real pair on the business's own tenants, with one value deliberately changed in one source and one record added to only one side.","assertion":"The difference shows both values and the master's. The orphan carries a reason and an owner. After a person accepts, the other source holds the master's value once, with the history kept, and the next run publishes an all-clear.","stubLimit":"Deliberately unpassable on a stub. A stub source accepts every write and holds whatever id you seed. Only the real tenant proves the write landed once, under the id the ledger keeps, and that the next scheduled run saw it."}]}