{"spec":"AIP-BS-KNOWLEDGE-GRAPH","version":"2026-10-03","standard":"knowledge-graph-build-standard","source":"https://aipathway.com.au/explore-ai/knowledge-graph-build-standard","markdown":"https://aipathway.com.au/explore-ai/knowledge-graph-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":["let production read the index or the pile instead of live nodes","promote a node to live on confidence","invent a node type or an edge type the contract does not name","let the model pick a winner between two live nodes","treat customer files or interviews as a source"],"stays_with_person":["accepting a source into the library","accepting a proposed node or edge as live","resolving a conflict","accepting a change delta so affected nodes may answer again","declaring two entities the same thing","any statement that the graph is complete or approved for client use"]},"job":"a fact lives once, and retiring a source takes its answers with it","skill":"knowledge-graph","stub":"local.mock_graph","production":null,"product":null,"insert":null,"with_us":{"kind":"own_systems","note":"No hosted step. The production check runs over the business's own corpus, register ids and accepted library. Identity, the change list and acceptance are the three parts not to hand-roll, and they are data, not a product."}},"with_us_next":"No hosted step. The production check runs over the business's own corpus, register ids and accepted library. Identity, the change list and acceptance are the three parts not to hand-roll, and they are data, not a product. 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":["let production read the index or the pile instead of live nodes","promote a node to live on confidence","invent a node type or an edge type the contract does not name","let the model pick a winner between two live nodes","treat customer files or interviews as a source"],"stays_with_person":["accepting a source into the library","accepting a proposed node or edge as live","resolving a conflict","accepting a change delta so affected nodes may answer again","declaring two entities the same thing","any statement that the graph is complete or approved for client use"]},"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_rule","cite","read_node","write_node"]},{"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","answer"]}],"registry":"https://aipathway.com.au/substrates.json"},"checks":[{"id":"key-contract-before-extract","n":1,"title":"The key contract is written and versioned before the first extract","statement":"Node types, edge types, predicates and required fields are a closed list at each version. An extract that invents a type has forked the graph and is refused.","assertable":"stub","ports":["read_rule","write_node"],"stimulus":"A build that reads the contract, then proposes one node of a published type and one of a type the contract does not name.","assertion":"The contract read precedes the first proposal in the trace. The published type is accepted as proposed. The invented type is refused and the refusal is recorded. A build that never reads the contract fails."},{"id":"source-is-an-instrument","n":2,"title":"Every source is registered as an instrument, not a file path","statement":"Title, issuer, edition, jurisdiction, effective dates, checksum and retrieved_at. A drive link with no edition is a pile entry, not a Source.","assertable":"stub","ports":["write_node"],"stimulus":"Two sources registered: one with every field, one with a title and a link only.","assertion":"The complete one is registered as proposed. The bare one is refused with the missing fields named. A source accepted without a checksum fails."},{"id":"nothing-live-without-acceptance","n":3,"title":"No node is live without a named acceptance and a replayable locator","statement":"Ingestion is not production. A node reaches live only when a named person accepts it, and only when its provenance names a passage with a locator that can be opened and highlighted.","assertable":"stub","ports":["write_node","gate"],"stimulus":"A source accepted by a person, nodes proposed from it, one accepted with a name, one accepted with no name, one with no locator.","assertion":"Exactly one node is live: the one with a name and a locator. The nameless acceptance and the locator-less node are refused, and both refusals are recorded. A live node with empty provenance fails."},{"id":"production-reads-live-nodes","n":4,"title":"Production reads live nodes, and a card names a node, never a file","statement":"Cited Answer and Advice File attach source cards from live nodes. An answer that cites a passage hash, a chunk id or a filename has not used the graph.","assertable":"stub","ports":["read_node","answer","cite"],"stimulus":"A question the live graph covers, with the index also holding a matching chunk.","assertion":"The answer's citations are node ids that are live on the asked-about date. A citation that is a filename or a chunk id fails, and so does an answer with no node id."},{"id":"identity-from-the-system-of-record","n":5,"title":"Entity identity comes from a system of record, or SAME_AS is an accepted edge","statement":"A lender, a product, a site or a person carries the id the CRM, the panel or the register already uses. Two labels that look alike are two things until a person accepts SAME_AS. An embedding that thinks they match is not identity.","assertable":"both","ports":["write_node","gate"],"stimulus":"A source naming a lender in full, and a CRM key for the same lender.","assertion":"Either one Entity node carrying the CRM id, or two nodes joined by a SAME_AS edge that carries accepted_by. Two nodes with no edge, or a merge by label, fails.","stubLimit":"The stub carries a register the runner seeded. Only a live CRM or panel proves the id the build wrote is the id the business actually uses."},{"id":"scope-is-an-edge","n":6,"title":"Scope is an APPLIES_TO edge, and an unscoped clause cannot answer a scoped question","statement":"A clause with no jurisdiction, product, work type or entity on it can decline or sit in conflict. It cannot answer a question asked about one state or one product.","assertable":"stub","ports":["read_node","answer"],"stimulus":"Two live clauses on the same topic, one scoped to a state and one with no scope, and a question about that state.","assertion":"The answer cites the scoped clause only. An answer citing the unscoped clause for a scoped question fails."},{"id":"supersedes-and-conflicts-are-written","n":7,"title":"Supersession and conflict are written edges, and the model never picks a winner","statement":"A new node retires the old through SUPERSEDES and the old cannot answer. Two live nodes that disagree get a CONFLICTS edge and a named person, never an average and never a confidence score.","assertable":"stub","ports":["write_node","gate","escalate"],"stimulus":"A replacement clause accepted, and separately two live clauses that answer one question differently.","assertion":"The superseded node is out of live and the edge exists. The conflicting pair carries a CONFLICTS edge and an escalation to a named person, and neither is cited until a person's pick is recorded."},{"id":"change-computes-affected-nodes","n":8,"title":"Replacing a source produces a delta with a computed list of affected nodes","statement":"When the checksum moves, the graph lists every node built from that source and every historic answer that used them. If you cannot produce that list, you have an index.","assertable":"stub","ports":["write_node","read_node"],"stimulus":"A source replaced by a new edition with a different checksum, after nodes were built from it and answers were given.","assertion":"The delta's affected node ids equal the nodes whose provenance is that source, and those nodes are no longer live. A delta with an empty or guessed list, or nodes still live after the move, fails."},{"id":"queues-are-states","n":9,"title":"Proposed, live, conflict, change_pending and retired are states, not labels","statement":"A retired node must not appear on a new source card. It must remain findable for the answers that used it.","assertable":"stub","ports":["read_node","cite"],"stimulus":"A clause cited, then retired, then the same question asked again.","assertion":"The second answer does not cite the retired node. The retired node is still returned when asked what an earlier answer relied on. Citing a retired node as live fails."},{"id":"index-proposes-only","n":10,"title":"The index can propose and rank. Emptying it does not empty the graph and does not invent an answer","statement":"Detach the vector index and the model weights. The live nodes are unchanged. A question the graph covers still answers from the same node; a question it does not cover declines.","assertable":"stub","ports":["read_node","answer"],"stimulus":"The index detached, then the covered question from check 4 and an uncovered question.","assertion":"The covered question cites the same node as before. The uncovered one declines. An answer that appears only while the index is attached, or a new paragraph with no node id, fails."},{"id":"customer-material-is-not-a-source","n":11,"title":"Customer files and interviews are not sources unless a person promotes a claim","statement":"A claim from an interview becomes a Fact only when a named person promotes it, and the Fact still needs provenance. A crawler finding it in the same drive does not make it policy.","assertable":"stub","ports":["write_node","gate"],"stimulus":"A customer interview transcript in the same folder as the policy, and a claim in it that a build might want as a Fact.","assertion":"No source of kind customer_material is accepted. The claim exists as a Fact only if promoted with a person's name, and that Fact carries provenance. A live node built from unpromoted customer material 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 graph, the acceptances and the change list 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":"Load, ask, move, identity, the forbidden path and one-fact-two-places, on a corpus the business controls with at least one system-of-record id, as the pass test describes.","assertable":"production","ports":["write_node","read_node","answer"],"stimulus":"Two real sources loaded and accepted by a named person, a real question, the PDF replaced by a new edition, a lender named two ways, and the same PDF dropped in a folder and embedded with no Source written.","assertion":"Nothing live before acceptance; a card built from a node id; the delta lists the old clause and it leaves live; SAME_AS accepted or two nodes in conflict; the folder path cannot be called production; the restated fact goes change_pending with its parent.","stubLimit":"Deliberately unpassable on a stub. A stub corpus moves when you tell it to and a stub register agrees with whatever you wrote. Only a real edition change and a real CRM id prove the graph survives the thing it exists for."}]}