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.
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.