Provenance: how to show a name was derived, not taken

This project resolves imports by name, and names are the one artefact here that somebody could reasonably ask hard questions about. A hash-to-name mapping is valuable, public databases of them exist, and "we worked it out ourselves" is a claim like any other unless something can check it.

So nothing here asks to be believed. Every name orbistoun reports carries a record of how it was arrived at, and every record is re-run rather than read - by CI where CI holds the material, and by whoever holds the rest.

The argument, in one line

A name this repository's own generator can produce is self-evidently derivable from this repository. A name it cannot produce is the one that needs explaining.

That reduces a question about intent - did you generate this, or copy it? - to a question about arithmetic, which anybody can answer without trusting anyone.

Be precise about what that does and does not claim

It is easy to read the sentence above as "the whole database could be rebuilt with no guest module at all". It could not, and it is worth being exact about why.

Comes from
The names This repository. The grammar and the word list, and nothing else
Knowing which of them are real A module's import table

The generator produces billions of candidates. It has no idea which of them name a function that exists - that is precisely what it cannot know, and precisely what a real import table supplies. A candidate is accepted only when its hash equals one the module declares it needs.

So: the names are ours; the selection is the module's. The audit proves the first half, which is the half a provenance question is actually about - it shows every name in the tree came out of inputs that are visible in the tree, rather than out of somebody else's database.

Two questions, and only one of them varies

Every record here answers how a candidate was proposed. None of them describes how it was confirmed, because confirmation is the same act every time:

The candidate is hashed. The hash either equals one a real module declares it needs, or it does not.

The hash is the oracle. A match is proof, not a judgement, and it needs no external authority to confirm - nothing is consulted, and there is nothing to consult that would help. That is true of a name generated from the grammar, a name read out of a module's own data, and a name a guest printed while running. They differ in where the candidate came from and in nothing else.

Which leaves two axes worth recording, and the vocabulary carries both:

The vocabulary

Record Evidence Reproducible Means
published-standard derived from this repository Fixed by ISO C or POSIX, from a list shipped here
generated derived from this repository Candidate n of a named pattern in crates/orbistoun-names/data/vendor.toml
static static + the module Read out of guest material at rest. Nothing executed
runtime runtime + a run of it, or hardware Learned from something executing
supplied external only from its source Came from outside this project

The two axes are deliberately not the same partition. A name a conformance probe reported is runtime evidence, but it sits a tier above a local run, because the hardware it came off is not something CI - or anybody here - can be handed.

static and runtime carry a closed subtype naming the mechanism, so records can be counted rather than read. A new mechanism adds a value; it does not add a new sentence.

Why observed is gone

There used to be a value called observed, covering everything that was neither generated nor imported. Its own documentation said "a name worked out by watching something run" - and 137 of the 154 names carrying it had never run anything. They were literal strings read out of a file at rest.

That is not a wording problem. The two are different claims with different checks:

A bucket holding both had to call both documented, not verified, because there is no single check that applies to both - so neither got one, and the first was sold far short of what it could prove. Splitting them made the static tier checkable for the first time (D213).

Every way a name gets here

Eight mechanisms. The by column is the value a record carries; the last column is what each has actually put in symbols/generated.json, which is a different question from whether it runs.

# Mechanism Evidence by Names today
1 Published standard-library list, harvested from FreeBSD's own Symbol.map files derived - 247
2 Grammar enumeration - prefix, module, action, object derived - 273
3 Identifier-shaped strings in a module's own bytes (D193) static module-strings 182
4 The same, pooled across a corpus: one module's strings naming another's imports static cross-module 18
5 Reasoning about a real call trace, confirmed by hash runtime call-trace 17, all by hand
6 Strings in what a guest passed to an import, read out of its memory as it ran runtime argument-dump 0 - runs, finds nothing yet
7 A name our own conformance probe reported on hardware runtime probe-transcript 0 - never exercised
8 A name taken from outside this project external - 0 - deliberately

Three zeroes, and they are not the same zero.

Mechanism 6 runs over the whole corpus on every ./bin/orbistoun names and reports what it did: 20 identifier-shaped candidates out of 113 captures across 6 previous runs, none of which hashed to a wanted import. The mechanism is not the limit - the source is. Argument dumps are forced per-import rather than captured broadly, so almost all of them so far are scalars carrying no text at all. It stays wired in and says so out loud each run, because a source that quietly contributes nothing looks exactly like one that is working.

Mechanism 7 is reachable - --words-from probe - and has still never been given a list, but the reason has changed and is now a much smaller one. obSCEne exports the list. Its Makefile builds build/symbols.txt from obscene-host --symbols, generated from the check registry rather than hand-maintained, so it cannot drift from what the probe actually calls.

What does not line up is the shape. obSCEne emits <library> <symbol> per line - it has to, because mkmodule needs to know which library resolves each name - and word_list hashes each line whole. So every entry would hash as "libkernel sceKernelWrite" rather than as sceKernelWrite, match nothing, and report zero named - which reads as the mechanism failing rather than as a column that needed dropping. Taking the second field is the whole of the remaining work.

Mechanism 8 has produced nothing on purpose, and the day it produces something the audit will say so on its own line. That is the whole reason the category exists.

Mechanism 4 is the newest and it changed the shape of the search. A name lying in one title's diagnostic text is the vendor's own spelling of a function that every title on the platform imports - so pooling the corpus found the whole C11 Annex K family (memcpy_s, strcpy_s, sprintf_s and the rest) in the vendor C library module's own strings, and used them to name imports of titles that never mention them. A module-at-a-time search structurally could not have found those, however many titles arrived.

And one that was checked and does not work

Symbol-table residue. An incompletely stripped module would leave real names in a .symtab, which would be the cheapest source on this list. Every module in the local corpus was checked: none has one. Five carry a .shstrtab and nothing else, and the dynamic symbol names are the encoded-hash form rather than text. It is not implemented because there is nothing for it to read - which is worth writing down, so the next person does not spend an afternoon establishing it again.

Three more are described but not built, in BACKLOG.md: strings from mapped memory after relocation, call-position inference, and cross-title argument correlation.

Why reading an import table is a different kind of act

An import table is a list of hashes of system library function names - the same values appear in everything built for the platform, because they identify the operating system's own interface rather than anything belonging to a title. Reading one is what a linker does, and it is the minimum possible act of interoperability: no code is copied, no content is examined, and nothing about how the module works is inspected.

Reading a module's strings is a larger act, and the document should not blur that. Mechanisms 3, 4 and 6 above do examine content - specifically, runs of identifier-shaped bytes. What they do not do is examine code: nothing is disassembled, no control flow is followed, and no structure is reproduced. What is taken is a function's published name, which is an interface identifier and is the same in every title on the platform. The convergence problem principle 1 exists to prevent is about reimplementing behaviour from someone else's implementation; a name is not behaviour.

The provenance CI job fails the build on any of the material this could shade into - firmware, keys, dumps, disassembly, guest binaries - and the corpus itself is never tracked.

What is recorded

Every name found is stored with a derivation: what proposed it, and where.

{
  "names": ["memcpy", "sceKernelDirectMemoryQuery", "strcpy_s", "__cxa_throw"],
  "derivations": {
    "memcpy": {
      "found": "published-standard",
      "list": "crates/orbistoun-names/data/standard.txt",
      "on": "2026-08-19"
    },
    "sceKernelDirectMemoryQuery": {
      "found": "generated",
      "pattern": "prefix-module-object-verb-object",
      "index": 87680,
      "on": "2026-08-19"
    },
    "strcpy_s": {
      "found": "static",
      "by": "cross-module",
      "from": "titles/PPSA02664-app0/sce_module/libc.prx",
      "on": "2026-08-24"
    },
    "__cxa_throw": {
      "found": "runtime",
      "by": "call-trace",
      "how": "a libc import taking 53.5% of all calls is allocation or static initialisation, both C++ ABI - confirmed by hash",
      "on": "2026-08-20"
    }
  }
}

Every entry carries the day it was recorded. Derivations are written by whatever did the work, at the moment of discovery: a provenance record assembled afterwards is a reconstruction, one written at the time is evidence.

On supplied

Never verifies, and is listed loudly and on its own. Taking a name from a public database is lawful and sometimes sensible - but it is the single category that changes the answer to "did you work all of this out yourselves?", so it must never be quiet.

Nothing in symbols/ is supplied today. The one thing in this repository that is comes from outside is the hash suffix, discussed below.

How it is checked

orbistoun-cli audit symbols/generated.json                    # what CI runs
orbistoun-cli audit symbols/generated.json --verify-harvest   # and what a corpus adds
orbistoun-cli audit symbols/generated.json --repair           # re-derive stale coordinates

For each name, the recorded derivation is re-run, not trusted:

Names with no record can be searched for from scratch with --deep, which walks the entire candidate space and answers "could this repository have produced this at all?" with no help from the file being audited.

This runs in ./bin/orbistoun check and in CI, over every database in symbols/. It is cheap - verifying a recorded derivation is an array lookup - which is what makes it a gate on every commit rather than a thing somebody meant to do before publishing.

Why --repair exists, and what it revealed

A generated record is a pattern plus an index. The index is what makes checking it a microsecond instead of a full sweep, and it is also a position in a mixed-radix enumeration over the vocabularies - so adding one word to a vocabulary renumbers every candidate built from it.

Adding words is the loop. Every confirmed name is split into parts and fed back into the grammar so the next search reaches further (D195). Each time that happens, some already-recorded name stops being at the index its record names. Nothing is wrong with the name and nothing is wrong with the claim; the coordinates moved underneath it.

Those names were falling onto the unaccounted ceiling - a file whose whole rule is that it may only shrink. It had accumulated 202 entries, and its own header described them as vendor names the grammar could not spell. That description was wrong: the grammar could spell every one of them. --repair hashes the stale names, hands them to the ordinary pattern search as targets, and re-derives all of them in one sweep. The ceiling is now empty (D213).

The knowledge base holds a second copy, and it is checked now

crates/orbistoun-hle/data/knowledge/*.toml carries a found_by on each function - the same claim about the same name, hand-written. It drifted into eleven contradictions with the audited record, including one supplied on a name the generator produces, and a test asserting the knowledge base "accounts for everything it claims" was green throughout, because it had never looked at that field (D213).

It compares against symbols/generated.json now, and the label must be current vocabulary. The duplication itself survives for one reason worth knowing: an implemented function's name never enters the unnamed set, so no search ever records a derivation for it. 57 of the 95 declared functions have no record at all, and the 38 that do have one only because a search happened to confirm them before anything implemented them.

So the audit covers the names this project has not yet acted on, and covers the ones it has only where the ordering favoured it. That is the largest remaining hole in this document's claim, it is not hidden, and BACKLOG.md carries the fix - which is not a retrofitted record, because one written now would be a reconstruction, but a sweep that answers the only thing the audit ever claims: could this repository produce this name?

Why the search itself is defensible

Published standards are not guesses. The target C library is FreeBSD-derived, so a large part of it is ISO C and POSIX under the names those standards publish, and they ship in crates/orbistoun-names/data/standard.txt. Nothing was read out of a vendor binary to write that list; the standards are the source, and they are public.

That list used to be hand-curated, which was the weakest link in this whole document. "Somebody wrote these down from the standards" cannot be audited, and it is bounded by what one person thought of. It is now generated, and the file says so in its own header:

git clone --filter=blob:none --sparse https://github.com/freebsd/freebsd-src
cd freebsd-src
git sparse-checkout set lib/libc lib/libthr lib/msun lib/libutil lib/libsys
orbistoun-cli harvest /path/to/freebsd-src --revision releng/14.0

That reads the Symbol.map files FreeBSD publishes with its own source - the authoritative statement of what those libraries export - and overwrites the word list with a generated one whose header names the source and revision it came from. Bigger, current, and repeatable by anyone: 470 hand-written names became 3,018 cited ones.

Vendor names are enumerated, then confirmed by arithmetic. The convention is strict - prefix, module, action, object, revision mark - so the plausible space is small enough to exhaust. crates/orbistoun-names/data/vendor.toml generates billions of candidates across eight patterns, searched at around 30 million per second, and the exact figure moves every time a confirmed name teaches it a word.

Reading a module's own strings outperforms both, and it is why the corpus is searched as one thing rather than a module at a time. Anything confirmed is split into words and fed back into the grammar, so each success makes the next search cheaper (D195) - and the --repair pass above is what keeps that from costing the records already written.

What is deliberately not here

The honest limits

If you are picking this up cold

# What can this repo name, and how much is still unknown? One search over the whole corpus.
orbistoun-cli names titles --out symbols/generated.json --wanted symbols/wanted.txt

# Prove every name in that file came from here, and re-read the modules that back the rest.
orbistoun-cli audit symbols/generated.json --verify-harvest

# And for names with no record, search the whole space rather than trusting the file.
orbistoun-cli audit symbols/generated.json --deep

./bin/orbistoun names is all three, in order, with --repair in between.

symbols/wanted.txt is the work list: the hashes still unnamed, which is what the next round of vocabulary work is aimed at.