How Does This Not Exist?

Last July I started a tool that checks whether a package's docs still match its code. Before it can do that, it needs a complete, machine-readable answer to one question: what is this package's public surface. Every export, every signature, every type, in a format a program can walk.

I assumed I'd npm install that answer. The odds usually favor someone having built it already. This time they didn't.

Other ecosystems have this

EcosystemThe documentSince
RESTOpenAPI (Swagger), a JSON model of the API2011
GraphQLintrospection, built into the spec2015
TypeScriptnothing standard

The odd one out is the ecosystem with the best type information. The types are right there, checked by a compiler, and then they evaporate when the compiler's process exits. There's no document that says "here is this package's surface", nothing to diff between versions, nothing to hand to an agent or a CI gate.

The document

So the tool grew a spec, and the spec became the project: OpenPKG. One command against an entry point, JSON out. JSON Schema (draft 2020-12) does the type representation, because inventing a type language is how specs die.

Fig. 1The spec, trimmed
{
  "exports": [{
    "name": "connect",
    "parameters": [
      { "name": "host", "schema": { "type": "string" }, "required": true },
      
    ],
    
  }],
  
}
$ref: Named types resolve as real JSON Schema references into the document's types section. Walkable, diffable, never flattened into a string.
names + defaults: Destructured parameters keep their names and their defaults. That is the actual calling contract of the function.
types: Anything JSON Schema can't say natively rides an x-ts-* extension instead of being dropped.
Hover or tap a field. JSON Schema does the type work; the format stays boring on purpose.

The parts that usually get lost are the ones I care about:

  • named types resolve as real references, not inlined blobs or flattened strings
  • a literal union like 'json' | 'markdown' | 'html' becomes an honest enum
  • a destructured parameter ({ host, port = 5432 }) keeps its names and defaults, because that's the actual calling contract

"Isn't this just TypeDoc?"

TypeDoc has emitted JSON since 2014. Its JSON is a serialization of TypeDoc's internal reflection model: a doc generator's working memory, not an interchange format. Same function, both tools:

Fig. 2One function, two JSONs
connect({ host, port = 5432 }: { host: string; port?: number })

TypeDoc --json

reflection model, v0.28

{
"name": "__namedParameters",
"type": "reflection",
"target": 14
}

names gone; refs are internal IDs

OpenPKG

json schema, draft 2020-12

"parameters": [
{ "name": "host", "schema": { "type": "string" } },
{ "name": "port", "schema": { "type": "number" }, "default": 5432 }
]

names, defaults, resolvable $refs

Both outputs are real. Neither is wrong; only one is a contract.

__namedParameters is not a name any caller typed, and "target": 14 is an index into TypeDoc's own object tree. API Extractor's .api.json is closer in spirit, but it was built for Microsoft's docs-and-review pipeline and it shows. Neither had a neutral home the way OpenAPI got a foundation, which I suspect is why nothing stuck. That's a guess, not a documented fact.

The part I got wrong

The spec was the easy half. My instincts wanted the CLI to ship features: a docs-coverage command, a lint command, a report generator with three output formats. I built them, and in December I deleted about 1,400 lines of them in one commit, "consolidate and reduce cli + api features."

BeforeAfter
one coverage command, 542 lines: fetch docs, extract surface, validate, fix, reportspec + a set difference
lint, report --format md/html/jsonlist, get, filter
the tool decides what's acceptablethe tool returns facts; your CI decides

The test that named it for me came from Dan Shipper and Claude's essay on agent-native architecture, published the same week: "Tools should be atomic primitives. Features are outcomes achieved by an agent operating in a loop." To change behavior, do you edit prompts or refactor code? A compound command makes you refactor.

What's left is five verbs (list, get, snapshot, diff, docs) and everything the compound commands did is a composition of them. Who composes is changing, and agents are ruthless about tools that decide too much.

Try it

Terminal
npx @openpkg-ts/cli spec src/index.ts

The spec format is versioned, hosted, and small enough to read in one sitting. What went wrong with the outputs of those five verbs is its own note.