{"spec":"AIP-BS-RECURRING-REPORT","version":"2026-10-03","standard":"recurring-report-build-standard","source":"https://aipathway.com.au/explore-ai/recurring-report-build-standard","markdown":"https://aipathway.com.au/explore-ai/recurring-report-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":["publish a figure with no definition, source or as-of time","compute one measure two ways","show a previous value as current when the source cannot be read","write a number into the narrative that the figures do not carry","edit a published report in place"],"stays_with_person":["every correction","setting a threshold","what a figure means to a lender","whether a report goes outside the business"]},"job":"the Monday report arrives on Monday, and every number can be traced","skill":"recurring-report","stub":"local.mock_sources","production":null,"product":null,"insert":null,"with_us":{"kind":"own_systems","note":"No hosted step. The production check runs over the business's own sources and definitions register, on its own schedule."}},"with_us_next":"No hosted step. The production check runs over the business's own sources and definitions register, on its own schedule. 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":["publish a figure with no definition, source or as-of time","compute one measure two ways","show a previous value as current when the source cannot be read","write a number into the narrative that the figures do not carry","edit a published report in place"],"stays_with_person":["every correction","setting a threshold","what a figure means to a lender","whether a report goes outside the business"]},"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","read_rule","cite","read_contacts"]},{"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":"every-figure-has-a-definition","n":1,"title":"Every figure on the report names a definition in the register","statement":"A figure is published only from a definition written once in a definitions register: the query over the source, the period and the filters. A figure with a value and no definition is never published; a measure the register does not define publishes as unavailable with reason no_definition.","assertable":"stub","ports":["read_ledger","cite"],"stimulus":"A register holding three definitions, and an audience whose declared measures include a fourth the register does not define.","assertion":"Every figure with a value cites a definition id the register holds, for the same measure. The undefined measure is published with no value and reason no_definition. A valued figure with no definition, or one citing a definition for a different measure, fails."},{"id":"one-definition-per-measure","n":2,"title":"One definition of every measure, computed once per period and reused","statement":"A measure is computed once per period from its one definition and the same figure is reused in every view that carries it. Two definitions for one measure, or two computations that happen to agree today, fail.","assertable":"stub","ports":["read_ledger"],"stimulus":"Two audiences that both carry the same measure over the same period.","assertion":"The register holds one definition for the measure. The source is read once for that measure and period, and both views carry the same value citing the same definition. A second read of the same query and period fails."},{"id":"source-and-as-of-on-every-figure","n":3,"title":"Every figure carries the source it was read from and the time it was read","statement":"Each figure names the source system and the as-of time the source was read for it, taken from the read itself and never typed. A figure with a value and no source, or no as-of time, fails.","assertable":"stub","ports":["read_ledger","read_job","cite"],"stimulus":"Definitions over two sources, a ledger and a job system, for one period.","assertion":"Every valued figure carries a source and an as-of time, and the as-of time equals the one the recorded read of that source returned for that query and period. A figure with neither, or one whose as-of time matches no read, fails."},{"id":"publishes-on-schedule-regardless","n":4,"title":"The report publishes at the scheduled time whether or not anything changed","statement":"The schedule is the only trigger. The report for a period publishes when its scheduled time arrives, never before it and never because somebody remembered, and it publishes even when every figure is the same as last period. A skipped period fails; so does one published early.","assertable":"stub","ports":["publish_report"],"stimulus":"Two scheduled periods with the same source values in both, and the clock advanced to each scheduled time in turn.","assertion":"A report exists for each period, published at or after its scheduled time and not before, and the second is published although nothing changed. A missing period, or a report published before its time, fails."},{"id":"cannot-compute-says-so","n":5,"title":"A figure whose source could not be read publishes as unavailable, with the reason","statement":"When a source cannot be read, the figure is published with no value and a reason, and the definition's owner is told. It is never the previous period's value shown as current, and never a blank with no reason.","assertable":"stub","ports":["publish_report","escalate"],"stimulus":"Two periods over two sources, with one source made unreadable before the second period's run.","assertion":"In the second period the figure over the broken source has no value and a reason naming the failure, and an escalation names the figure. Its first-period value appearing in the second period, or a null value with no reason, fails."},{"id":"no-narrative-invents-a-number","n":6,"title":"Prose on the report describes figures that exist and never states a number they do not carry","statement":"A sentence on the report may name a figure and say what it is. It may not state a number, a percentage or a comparison the figure list does not carry. A sentence with a number that is not on the figure list fails.","assertable":"stub","ports":["publish_report"],"stimulus":"Two periods where the second's figures move against the first, so a comparison is tempting to write.","assertion":"Every number in every published narrative equals a value on that report's figure list, or is the period label. A percentage or a difference the figures do not carry fails, whether or not it happens to be arithmetically right."},{"id":"variance-thresholds-route-to-a-person","n":7,"title":"A figure past its declared threshold against the prior period notifies a named person that run","statement":"A threshold is a measure, a direction, an amount and a person, set once by a person. When a figure moves past it against the prior period, the named person is notified in the same run. Where the prior period's figure is unavailable the comparison is escalated, never assumed.","assertable":"stub","ports":["notify","escalate"],"stimulus":"A threshold on one measure with a named person, and two periods whose second value moves past it.","assertion":"In the second period's run a notification names that person and that measure. No notification in the first period, where there is nothing to compare. A crossing with no notification, or one sent to nobody in particular, fails."},{"id":"audience-gets-its-own-view","n":8,"title":"Each audience sees the figures declared for it and nothing else","statement":"An audience is a named recipient group with a declared list of measures. Its view of the report carries exactly those measures. A lender's view carrying staff-level detail fails; so does an owner's view missing a measure declared for it.","assertable":"stub","ports":["read_contacts","publish_report"],"stimulus":"Two audiences: one declared for every measure, one declared for a subset.","assertion":"For each audience a report exists whose figure measures are exactly the audience's declared measures. A figure outside the declaration, or a declared measure absent, fails."},{"n":9,"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":"corrections-are-a-new-version","title":"A published figure changes only by a named person publishing a new version with the reason","statement":"A published report is never edited in place. A correction is a gate carrying the person's name and the reason, followed by a new version that supersedes the old one. The old version stays readable. Editing a published figure in place, or a version with no person or no reason, fails.","ports":["gate","publish_report"],"stimulus":"One period published, then a named person asks for one figure to be corrected, with a reason.","assertion":"A gate call names the person and the reason before a new version is published. The new version supersedes the first and carries the person and the reason; the first version is unchanged and marked superseded. An in-place amendment of a published report fails."},{"id":"the-record-by-period","n":10,"title":"For any period: the figures, definitions, sources, as-of times, versions and who published, retrievable","statement":"When the lender asks where a number came from, the record for that period shows every version of every view, each figure's definition, source and as-of time, and who published each version and why.","assertable":"stub","ports":["read_ledger","cite"],"stimulus":"One period published to two audiences, then corrected once.","assertion":"A read by period returns every version of every view. Each valued figure carries a definition id, a source and an as-of time. Each version carries who published it and, for a correction, the reason. A record missing any of those 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 definitions and the schedule 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 real sources for two periods: one source made unreadable and its figure published as unavailable, one threshold crossed and a person notified, one correction published as a new version, and both periods published on time.","assertable":"production","ports":["read_ledger","publish_report","gate"],"stimulus":"Two real scheduled periods on the business's own sources, with one source taken offline before the second run and one correction requested by a named person.","assertion":"Both periods published at their scheduled time. The figure over the offline source is unavailable with the reason and its owner told. The crossed threshold notified its person. The correction exists as a new version with a name and a reason, and the first version is still readable.","stubLimit":"Deliberately unpassable on a stub. A stub source is readable whenever the runner says so and a stub clock fires whenever it is advanced. Only the real sources prove the as-of times are real and only a real schedule proves the report arrived on Monday without a person starting it."}]}