{"spec":"AIP-BS-TIMESHEET-CHECK","version":"2026-10-03","standard":"timesheet-check-build-standard","source":"https://aipathway.com.au/explore-ai/timesheet-check-build-standard","markdown":"https://aipathway.com.au/explore-ai/timesheet-check-build-standard.md","licence":"CC BY 4.0","how_to_read":"Eleven of these thirteen 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":13,"onStub":11,"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":["type a total","apply a penalty or allowance with no cited rule edition","interpret which clause applies","clear an anomaly without a person and a reason","export without a release, or twice for one period"],"stays_with_person":["every anomaly resolution","the release","which award applies","any dispute with the person whose hours they are"]},"job":"every hour on the payroll traces to a person, a day and a job","skill":"timesheet-check","stub":"local.mock_roster","production":null,"product":null,"insert":null,"with_us":{"kind":"own_systems","note":"No hosted step. The production check runs over the business's own roster, entries and payroll system, released by a person."}},"with_us_next":"No hosted step. The production check runs over the business's own roster, entries and payroll system, released by a person. 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":["type a total","apply a penalty or allowance with no cited rule edition","interpret which clause applies","clear an anomaly without a person and a reason","export without a release, or twice for one period"],"stays_with_person":["every anomaly resolution","the release","which award applies","any dispute with the person whose hours they are"]},"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","evidence","read_rule","cite","read_contacts","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":"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."}],"ports":["escalate","publish_report"]}],"registry":"https://aipathway.com.au/substrates.json"},"checks":[{"id":"entry-traces-to-person-day-job","n":1,"title":"Every entry traces to a person, a day and a job","statement":"Every entry carries a person id from the contact list, a date, and a job id from the job system that the person was allocated on that day. An entry with no job, or a job the person was not allocated, is an anomaly and never a line on the sheet.","assertable":"stub","ports":["read_job","read_contacts"],"stimulus":"Eight entries across three people, one of them on a job the person was never allocated, with the roster and the allocations in the system.","assertion":"The contact list and the allocations are read before any anomaly is raised. Every line on the sheet names a person, a date and an allocated job. The unallocated entry is an anomaly of kind not_allocated and is not a line."},{"id":"totals-computed-never-typed","n":2,"title":"Daily and weekly totals are computed from entries, never typed","statement":"A total is the sum of the entries it names, computed at run time. A total that names no entries, or whose hours differ from the sum of the entries it names, is a typed total and fails.","assertable":"stub","ports":["read_job","publish_report"],"stimulus":"A period of clean entries for two people, with a daily and a weekly total expected for each.","assertion":"Every published total lists the entry ids it derives from and its hours equal the sum of those entries' hours after breaks. A total with no entry ids, or with hours that do not add up, fails."},{"id":"anomalies-are-a-closed-list","n":3,"title":"An anomaly is one of a closed list of kinds, and it has an owner","statement":"Over-hours in a day, overlapping entries, a missing break, an entry on a day not rostered, a job not allocated, and a rule that does not plainly apply. Each anomaly carries a kind from that list and the name of the person who owns it. A kind outside the list, or an anomaly with no owner, fails.","assertable":"stub","ports":["escalate"],"stimulus":"The eight seeded entries, which include one overlapping pair, one twelve-hour day and one unallocated job.","assertion":"The overlap, the over-hours day and the unallocated job are each raised as an anomaly with a kind from the list and an owner's name. No anomaly carries a kind outside the list or an empty owner."},{"id":"award-rule-is-cited-with-an-edition","n":4,"title":"Any penalty, overtime or allowance applied cites the rule, its edition and its effective date","statement":"An entry that attracts overtime, a weekend penalty or an allowance carries a card naming the rule, its edition and the date it took effect, read from the rule store at run time. A rule applied with no card fails.","assertable":"stub","ports":["read_rule","cite"],"stimulus":"One entry over eight hours on a weekday and one entry on a Saturday, with two rule editions in the store.","assertion":"The rule store is read before any card is written. Both entries carry a card with a rule id the store holds, an edition and an effective date. A line that applies a rule and has no card fails."},{"id":"superseded-rule-cannot-apply","n":5,"title":"A retired rule edition cannot apply to the next period","statement":"Once a rule edition is retired, the next period applies the edition in force for its dates or parks the entry. A card for a period after the retirement that cites the retired edition fails.","assertable":"stub","ports":["read_rule"],"stimulus":"A period run under the first edition, then the first edition retired and a later period run with the second edition in force.","assertion":"Cards in the first period cite the first edition. Cards in the later period cite the second edition only. A card in the later period citing the retired edition fails."},{"id":"no-interpretation","n":6,"title":"An entry the dated rule does not plainly cover is parked for a person","statement":"A disputed break, an unusual shift, a situation the rule text does not plainly name: the entry is parked with kind rule_unclear and an owner, and no card is written for it. The build never decides which clause applies.","assertable":"stub","ports":["read_rule","escalate"],"stimulus":"One entry flagged as a disputed break, which no rule edition in the store plainly covers.","assertion":"The entry is an anomaly of kind rule_unclear with an owner, carries no card, and is not a line on the sheet. A card written for it, from any edition, fails."},{"id":"anomaly-is-resolved-by-a-person-with-a-reason","n":7,"title":"An anomaly leaves the queue only with a person's name and a reason","statement":"A resolution is a person's name, a reason from the list approved, corrected or rejected, and the time. The person's decision is attached as evidence. An anomaly resolved with no name, or with no decision behind it, fails.","assertable":"stub","ports":["gate","evidence"],"stimulus":"The seeded anomalies raised on a first run, then a named person decides each one in the queue, then a second run.","assertion":"Every anomaly is resolved with the name and reason the person gave, with the decision attached as evidence. An anomaly cleared with no person, or cleared before any person decided, fails."},{"n":8,"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-reaches-payroll-without-a-release","title":"Nothing reaches payroll without a release by a named person","statement":"The period export is written only after a named person releases the period, and only when every anomaly in it is resolved. A run with no release writes nothing. An export with no release behind it, or with an open anomaly, fails.","ports":["gate","write_ledger"],"stimulus":"A first run with anomalies open and nobody having released, then the anomalies decided, a named person releasing, and a second run.","assertion":"No export exists after the first run. After the second, one export exists, its write comes after a release carrying the person's name, and no anomaly was open when it was written."},{"id":"one-export-per-period","n":9,"title":"One export per period, written once, updated on a second release","statement":"The export is keyed to the period. A second release for the same period updates the existing export or refuses; it never writes a second one. A null external id is a failed export, not a success.","assertable":"stub","ports":["write_ledger"],"stimulus":"The same released period run twice, an hour apart.","assertion":"Exactly one export exists for the period, it carries the payroll system's external id, and the second run produced an update or no write. A second export, or a null id treated as success, fails."},{"id":"the-run-publishes-regardless","n":10,"title":"Every period publishes hours, anomalies found and resolved, and the release, including a clean one","statement":"Each run publishes a report for the period: the hours by person and day, how many anomalies were found and how many resolved, and who released it or that nobody has. A clean period publishes too, so silence never means clean.","assertable":"stub","ports":["publish_report"],"stimulus":"A period with only clean entries and nothing to raise.","assertion":"One report exists for the period with hours by person, anomalies found and resolved both zero, and the release field present. A clean period with no report fails."},{"id":"the-record-by-person-and-period","n":11,"title":"Entries, anomalies, resolutions, rules cited and the release are retrievable by person and period","statement":"When a person disputes their pay, the record for that person and period shows every entry, every anomaly with who resolved it and why, every rule card, and who released the period.","assertable":"stub","ports":["read_contacts","evidence"],"stimulus":"One period run through anomalies, decisions, release and export for three people.","assertion":"A read by person and period returns the entries, each anomaly with its resolution and evidence, the cards with rule id and edition, and the release with a name. A record missing the release or a resolution fails."},{"id":"survives-the-builder","n":12,"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 rules, the roster and the release 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":13,"title":"The pass test passes","statement":"On a real period with real entries: one overlapping pair caught, one unallocated job caught, one rule edition change parking the next period, one release by a person, and one export in the payroll system with its id.","assertable":"production","ports":["read_job","write_ledger","gate"],"stimulus":"A real period of entries captured on site or in the app, on the business's own job system and payroll.","assertion":"The overlap and the unallocated job are in the anomaly queue with owners, the entry the new edition does not plainly cover is parked, the export exists in payroll with its id, and the release names the person who gave it.","stubLimit":"Deliberately unpassable on a stub. A stub payroll accepts every export and a stub rule store holds every edition you seed. Only the real system proves the id is real and the second release did not make a second export."}]}