Skip to content

The Docuccino extension

Docuccino compiles your application into OpenAPI 3.2 — and records how it knew. That record travels inside the document, under one reserved member: x-docuccino, the Docuccino extension.

You never have to look at it. The OpenAPI export drops the whole member, and what’s left is the document you’d ship anywhere. But everything Docuccino does that a generator usually can’t is in there, and all of it is readable:

Semantic diff

Every operation, parameter and schema carries a stable id, so docuccino:diff pairs by meaning. Rename a controller and nothing moves; change a URL and the diff says exactly that.

Provenance

Each contribution records what produced it, which precedence layer it won at, and the file:line it came from — so why is this documented this way? has an answer you can read.

Guides that can't go stale

Your Markdown pages compile into the document with every reference resolved to a real operation. A guide pointing at something you deleted is a build diagnostic, not a broken link your readers find first.

Publishable workflows

Multi-step sequences, declared on the operations that take part in them and emitted as an Arazzo description.

{
"openapi": "3.2.0",
"jsonSchemaDialect": "https://spec.openapis.org/oas/3.2/dialect/base",
"info": { "title": "Billing API", "version": "2.1.0" },
"servers": [], "security": [], "tags": [],
"paths": {},
"components": { "schemas": {}, "responses": {}, "securitySchemes": {} },
"x-docuccino": {
"document": {
"id": "doc:default",
"configHash": "f7ea3a4314fc0a4e…",
"contentHash": "7f6b185b0fd29b96…"
},
"generator": {
"name": "docuccino/laravel", "version": "1.0.0",
"specVersion": "2.0.0", "schema": "https://spec.docuccino.app/uir/2.0/schema.json"
},
"content": { "pages": [], "nav": [] }
}
}

Optional members are simply absent rather than empty: a document with no servers has no servers key, and one with no content directory has no content.

There are no timestamps anywhere — they’d break determinism, so the format forbids them. The x-docuccino subtree is strictly defined and closed to undefined members — a determinism guarantee, since nothing non-deterministic can slip in. It grows through versioned, additive schema revisions rather than by readers tolerating unknown members (see schema hosting).

Every operation, parameter, named schema, response and content page carries an x-docuccino.id. Identities are computed from meaning, never from file paths, line numbers or array positions, so they survive refactors:

Kind Built from Survives Changes when
op: document + method + path template (params normalized to {p0}, {p1}…) file moves, controller/method renames, path-param renames the URL, method, or containing document changes
par: operation + location + name reorder, description edits the parameter is renamed or moves between query and path
sch: (named) class name and any generic type arguments (pin it with #[SchemaId]) file moves the class is renamed without a pin
sch: (inline) the schema’s structure prose edits the shape changes
res: operation + status + media type description edits the status or media type changes
page: the page’s slug title and body edits the file moves, unless you pin slug in frontmatter

Ids read as <kind>:v1:<16 chars> — op:v1:sdqfknnscimbfjgg — where v1 is the identity algorithm version, so a future algorithm can coexist with today’s ids. The document id is the one that stays legible: doc: plus your config key.

This is what makes a meaningful diff possible: rename a controller and nothing changes; change a URL and the diff shows exactly that operation removed and added.

Docuccino specifies a canonical ordering and serialization: fixed member order per object type, map keys sorted by code point, a fixed order of HTTP methods, parameters sorted by location then name, 2-space indentation, LF endings, and a trailing newline. The result:

Same code, same bytes

Regenerating without code changes produces a byte-for-byte identical file — clean diffs, every time.

Upgrades you can see

The content hash excludes tool metadata, so a version bump on its own moves the generator line and leaves contentHash exactly where it was. A release that corrects what a document says does move it — deliberately, and named in the changelog — so run docuccino:diff against the committed artifact when you upgrade, rather than reading the new one cold.

The document carries two hashes, and they answer different questions. contentHash digests the whole document with the tool metadata excluded — it tells you the documentation changed. configHash digests this document’s entry in the build configuration — it tells you the change came from configuration, not from code.

Each contribution to the document is recorded — what produced it, the precedence layer it applied at, which fields it set, and the source location — so tooling (and you) can answer “why is this documented this way?”:

{
"producer": "attribute",
"layer": "attribute",
"fields": ["summary"],
"source": {
"file": "app/Http/Controllers/InvoiceController.php",
"line": 34,
"symbol": "App\\Http\\Controllers\\InvoiceController::index"
},
"confidence": 1,
"overrode": [
{ "field": "summary", "value": "Index invoices", "producer": "docblock" }
]
}

producer names the specific source — inference, docblock, attribute, overlay, config, fallback, or integration:<name> such as integration:query-builder. When a higher-precedence layer wins a field, the value it replaced is kept in overrode rather than discarded, so the whole chain of decisions stays readable.

Choose how much of that to emit with docuccino:export --provenance:

Level Emits
full Every record, including the overrode trail. The complete audit.
winners (default) Every record, without the overrode trails.
none No provenance at all — smallest document, and immune to line-number churn.

Provenance survives only where the extension does: the OpenAPI emitters always drop it.

A schema node can carry x-docuccino.mock — faker, the expression a mock server evaluates for that property, and seedGroup, a name shared by properties whose values should correlate:

"per_page": {
"x-docuccino": { "mock": { "faker": "numberBetween:1,100" } },
"type": "integer"
}

Nothing here is a value: the document records the expression and never evaluates it, so a mock hint costs none of the determinism. Hints come from #[Mock] in the source; the OpenAPI emitters drop them unless you name a member to project the faker half onto, conventionally x-faker.

Point content.dir at a folder of Markdown and Docuccino compiles it into the document: guide pages with stable page: ids, plus a navigation tree derived from your folder structure (overridable in frontmatter). Reference operations and schemas directly from your prose with directives, and Docuccino resolves them at build time:

See ::operation{id="invoices.index"} for pagination, which returns ::schema{name="InvoiceData"}.

Each resolved directive is rewritten with the target’s stable id, so a consumer can link to an operation without re-resolving anything. A directive that points at something that doesn’t exist becomes a diagnostic — so your guides can’t silently drift from your API. And because narrative lives in the document, prose changes show up in the diff and changelog too, not just API changes.

This is the clearest example of what the extension is for: plain OpenAPI has nowhere to put a page tree, so the OpenAPI export drops it. See Adding your own pages for the authoring side.

An API is rarely one call. x-docuccino.workflows carries the sequences your application actually supports — reserve a basket, take payment, confirm — declared with #[WorkflowStep] on the operations that take part in them, so a step can never name an operation that isn’t there.

Each workflow records its steps in order, the operation each one calls, and how a value from one step reaches the next. docuccino:export --format=arazzo publishes them as an Arazzo 1.1 description — an OpenAPI Initiative specification for exactly this — so a consumer’s tooling can follow the sequence without being told anything about Docuccino.

Continue to schema hosting for where the JSON Schema lives and how it’s versioned.