<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>http://localhost/release-notes</id>
    <title>Apiome release notes</title>
    <updated>2026-10-08T14:21:07.650Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="http://localhost/release-notes"/>
    <subtitle>Apiome Blog</subtitle>
    <icon>http://localhost/img/bee-logo.png</icon>
    <entry>
        <title type="html"><![CDATA[Unreleased]]></title>
        <id>http://localhost/release-notes/unreleased</id>
        <link href="http://localhost/release-notes/unreleased"/>
        <updated>2026-10-08T14:21:07.650Z</updated>
        <summary type="html"><![CDATA[What has landed on main since RC4 and will ship in the next release.]]></summary>
        <content type="html"><![CDATA[
<p>What has landed on <code>main</code> since RC4, regenerated on every build of the site: the in-progress app notes and REST API 1.303.0 to 1.339.1 (29 versions).</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_tleR" id="whats-new-in-the-app">What's new in the app<a href="http://localhost/release-notes/unreleased#whats-new-in-the-app" class="hash-link" aria-label="Direct link to What's new in the app" title="Direct link to What's new in the app" translate="no">​</a></h2>
<p>From the in-app <strong>What's new</strong> for <em>Apiome 08-2026 RC5</em>.</p>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="featuresimprovements">Features/Improvements<a href="http://localhost/release-notes/unreleased#featuresimprovements" class="hash-link" aria-label="Direct link to Features/Improvements" title="Direct link to Features/Improvements" translate="no">​</a></h3>
<ul>
<li class="">API Formats:
<ul>
<li class="">Hardening import functionality</li>
<li class="">Adds formats:
<ul>
<li class="">MCP import and export</li>
<li class="">Kong import and export</li>
<li class="">Arazzo 1.1 import and export</li>
<li class="">AsyncAPI 2.x export</li>
<li class="">OData v2/v3 import and export</li>
<li class="">WSDL 2.0 import and export</li>
<li class="">Avro IDL (<code>.avdl</code>) import and export</li>
<li class="">Swagger 1.2 import</li>
<li class="">Postman Collection v2.0 import</li>
<li class="">Protobuf editions 2023/2024 normalization parity</li>
<li class="">RELAX NG import</li>
<li class="">DTD import</li>
<li class="">Schematron import</li>
<li class="">CDDL (RFC-8610) import and export</li>
<li class="">Apache Arrow/Flight import</li>
<li class="">Open Data Contract Standard (ODCS v3.1) import and export</li>
<li class="">Kafka Connect schema import and export</li>
<li class="">dbt model and semantic-manifest import</li>
<li class="">SQL DDL import</li>
</ul>
</li>
<li class="">Linting rule pack updates for scoring new import types
<ul>
<li class="">Data contracts are now scored on their own terms: ownership, service levels, freshness,
retention, column documentation, row identity, classification and declared quality checks</li>
</ul>
</li>
<li class="">Added pills to supported format list to show the source and type that the API format provides</li>
<li class="">Every format now declares which versions it reads and writes, and which version an export produces by default</li>
</ul>
</li>
<li class="">Mock Services:
<ul>
<li class="">Several mock services have been improved including mock rules and testing via UI and JSON rules</li>
</ul>
</li>
<li class="">Documentation:
<ul>
<li class="">The user guide is now a searchable documentation site, grouped by job; <strong>Help &amp; docs</strong> links open it</li>
<li class="">Docs pages show product screenshots in light and dark, retaken from the product every week</li>
<li class="">A new Getting started guide walks from sign-in to a published, browsable spec</li>
<li class=""><strong>Full release notes</strong> at the foot of this dialog opens a post per release on the documentation site, with every REST API change and links to the issues</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="bug-fixes">Bug Fixes<a href="http://localhost/release-notes/unreleased#bug-fixes" class="hash-link" aria-label="Direct link to Bug Fixes" title="Direct link to Bug Fixes" translate="no">​</a></h3>
<ul>
<li class="">Import:
<ul>
<li class="">Fixed upload to accept all file format extensions instead of just the 10 it had previously</li>
<li class="">Fixes dependency for AsyncAPI to re-enable import functionality</li>
<li class="">Fixing durability of import process, added extra tests to REST service test suite</li>
<li class="">Fixing bulk import functionality:
<ul>
<li class="">Now includes the ability to bulk import into existing or new projects without having to validate all selections</li>
<li class="">Visual indicator of the bulk import is now implemented properly</li>
</ul>
</li>
</ul>
</li>
<li class="">MCP:
<ul>
<li class="">Hardens tool array emit for LLM tool summary</li>
</ul>
</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_tleR" id="rest-api">REST API<a href="http://localhost/release-notes/unreleased#rest-api" class="hash-link" aria-label="Direct link to REST API" title="Direct link to REST API" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="13391--2026-10-07">1.339.1 — 2026-10-07<a href="http://localhost/release-notes/unreleased#13391--2026-10-07" class="hash-link" aria-label="Direct link to 1.339.1 — 2026-10-07" title="Direct link to 1.339.1 — 2026-10-07" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added">Added<a href="http://localhost/release-notes/unreleased#added" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class=""><strong>Generated REST reference on the documentation site (<a href="https://github.com/apiome/apiome/issues/5628" target="_blank" rel="noopener noreferrer" class="">#5628</a>, DOCS-1.11)</strong>:
<code>scripts/generate_rest_reference_docs.py</code> renders <code>openapi.yaml</code> into
<code>apiome-docs/docs/reference/rest/</code> — one page per tag (plus <code>untagged</code>) with every operation's
parameters, request body, responses and the schemas it uses. The index records the document's
SHA-256 (<code>openapi_sha256</code>), and <code>yarn docs:check</code> fails when <code>openapi.yaml</code> changes without
regenerating; <code>--check</code> and <code>tests/test_rest_reference_docs.py</code> fail on stale pages. Rendering
lives in <code>app.rest_reference_doc</code>. No API change; the OpenAPI version moves to 1.204.1.</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="13390--2026-10-07">1.339.0 — 2026-10-07<a href="http://localhost/release-notes/unreleased#13390--2026-10-07" class="hash-link" aria-label="Direct link to 1.339.0 — 2026-10-07" title="Direct link to 1.339.0 — 2026-10-07" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="changed">Changed<a href="http://localhost/release-notes/unreleased#changed" class="hash-link" aria-label="Direct link to Changed" title="Direct link to Changed" translate="no">​</a></h4>
<ul>
<li class=""><strong>Guide pages moved to the documentation site (<a href="https://github.com/apiome/apiome/issues/5619" target="_blank" rel="noopener noreferrer" class="">#5619</a>, DOCS-1.2)</strong>: every <code>docsPage</code> the API
returns now names a page of the Docusaurus site instead of the retired <code>docs/guide/</code> folder —
e.g. <code>GET /v1/lint/rules</code> → <code>"docsPage": "apiome-docs/docs/build/lint-rules.md"</code>. The value is
still a monorepo-relative source path; <code>apiome-ui</code> resolves it to
<code>https://apiome.github.io/apiome/build/lint-rules</code>. Affected fields: lint-rule catalog and
style-guide <code>docsPage</code>, blocking-rule <code>docs_page</code> (schema → <code>build/lint-rules</code>, MCP →
<code>govern/mcp-*-rules</code>), axis <code>algorithmDocsPage</code> (<code>build/axis-score</code>) and import-preflight rule
links. Blocking schema-rule <code>reference</code> URLs now point at the site
(<code>https://apiome.github.io/apiome/build/lint-rules#&lt;rule&gt;</code>).
<ul>
<li class="">New <code>app.docs_site</code> builds those paths, the site URLs and the pages' front matter.</li>
<li class=""><code>scripts/generate_lint_rule_docs.py</code>, <code>generate_supported_formats_doc.py</code> and
<code>generate_format_counts.py</code> write into <code>apiome-docs/docs/**</code>; generated pages open with front
matter and mark rule / format anchors as heading ids (<code>### </code>rule.id<code> {#rule-id}</code>).</li>
<li class="">OpenAPI <code>info.version</code> 1.204.0 (description text only).</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="13360--2026-10-06">1.336.0 — 2026-10-06<a href="http://localhost/release-notes/unreleased#13360--2026-10-06" class="hash-link" aria-label="Direct link to 1.336.0 — 2026-10-06" title="Direct link to 1.336.0 — 2026-10-06" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-1">Added<a href="http://localhost/release-notes/unreleased#added-1" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong>Agent toolset description enrichment (<a href="https://github.com/apiome/apiome/issues/4531" target="_blank" rel="noopener noreferrer" class="">#4531</a>, AGX-1.3)</strong>: flags tools an agent will struggle
with, and lets the copilot propose better descriptions that a person reviews before any agent
sees them.</p>
<div class="language-bash codeBlockContainer_mQmQ theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_t_Hd"><pre tabindex="0" class="prism-code language-bash codeBlock_RMoD thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_AclH"><div class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-sX</span><span class="token plain"> POST </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$APIOME</span><span class="token string" style="color:#e3116c">/v1/tenants/acme/agent-toolsets/</span><span class="token string variable" style="color:#36acaa">$TOOLSET</span><span class="token string" style="color:#e3116c">/enrichment"</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Authorization: Bearer </span><span class="token string variable" style="color:#36acaa">$JWT</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-sX</span><span class="token plain"> PATCH </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$APIOME</span><span class="token string" style="color:#e3116c">/v1/tenants/acme/agent-toolsets/</span><span class="token string variable" style="color:#36acaa">$TOOLSET</span><span class="token string" style="color:#e3116c">/enrichment/</span><span class="token string variable" style="color:#36acaa">$PROPOSAL</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">     </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Authorization: Bearer </span><span class="token string variable" style="color:#36acaa">$JWT</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'Content-Type: application/json'</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-d</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'{"decision": "accept"}'</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-s</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$APIOME</span><span class="token string" style="color:#e3116c">/v1/tenants/acme/agent-toolsets/</span><span class="token string variable" style="color:#36acaa">$TOOLSET</span><span class="token string" style="color:#e3116c">/compiled"</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Authorization: Bearer </span><span class="token string variable" style="color:#36acaa">$JWT</span><span class="token string" style="color:#e3116c">"</span><br></div></code></pre></div></div>
<ul>
<li class=""><strong>Agent-hostile flags</strong> with machine-readable reasons: <code>missing-description</code>,
<code>thin-description</code>, <code>missing-examples</code>, <code>undocumented-errors</code>,
<code>missing-parameter-description</code>, <code>thin-parameter-description</code>. They feed AGX-4.4.</li>
<li class=""><strong>Copilot proposals</strong> from an Ollama chat model (<code>APIOME_AGENT_ENRICHMENT_MODEL</code>; unset means
flag-only), written only from the spec's own documentation. The pass is idempotent and asks
about at most 20 operations per run.</li>
<li class=""><strong>Human review.</strong> <code>accept</code> (optionally edited) or <code>reject</code>. Only accepted text is compiled, and
only while the toolset's new <code>descriptionEnrichment</code> setting is on (the default). V273 adds
<code>agent_toolset_enrichments</code>; its CHECK makes accepted text without a review unrepresentable.</li>
<li class=""><strong><code>GET …/compiled</code></strong> returns the toolset as agents are served it.</li>
<li class="">Audited as <code>agent.toolset.enrichment.run</code> / <code>agent.toolset.enrichment.review</code> (metadata only).</li>
<li class="">New <code>app.ollama_chat</code>: the stdlib Ollama chat client for REST-side copilot features.</li>
<li class="">Docs: <code>docs/agent_toolset_enrichment.md</code>.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="13330--2026-10-05">1.333.0 — 2026-10-05<a href="http://localhost/release-notes/unreleased#13330--2026-10-05" class="hash-link" aria-label="Direct link to 1.333.0 — 2026-10-05" title="Direct link to 1.333.0 — 2026-10-05" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-2">Added<a href="http://localhost/release-notes/unreleased#added-2" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong>Agent toolsets: tool selection &amp; curation (<a href="https://github.com/apiome/apiome/issues/4530" target="_blank" rel="noopener noreferrer" class="">#4530</a>, AGX-1.2)</strong>: a tenant decides which
operations of a published version its AI agents may call as MCP tools. <strong>Safe by default:</strong>
reads on, write operations opt-in, each with an explicit confirmation.</p>
<div class="language-bash codeBlockContainer_mQmQ theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_t_Hd"><pre tabindex="0" class="prism-code language-bash codeBlock_RMoD thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_AclH"><div class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-sX</span><span class="token plain"> POST </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$APIOME</span><span class="token string" style="color:#e3116c">/v1/tenants/acme/agent-toolsets"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">     </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Authorization: Bearer </span><span class="token string variable" style="color:#36acaa">$JWT</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'Content-Type: application/json'</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">     </span><span class="token parameter variable" style="color:#36acaa">-d</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'{"versionId": "'</span><span class="token plain">"</span><span class="token variable" style="color:#36acaa">$VERSION</span><span class="token string" style="color:#e3116c">"'"</span><span class="token punctuation" style="color:#393A34">}</span><span class="token string" style="color:#e3116c">'</span><br></div><div class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">curl -sX PATCH "$APIOME/v1/tenants/acme/agent-toolsets/$TOOLSET/tools/$TOOL" \</span><br></div><div class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">     -H "Authorization: Bearer $JWT" -H '</span><span class="token plain">Content-Type: application/json</span><span class="token string" style="color:#e3116c">' \</span><br></div><div class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">     -d '</span><span class="token punctuation" style="color:#393A34">{</span><span class="token string" style="color:#e3116c">"enabled"</span><span class="token builtin class-name">:</span><span class="token plain"> true, </span><span class="token string" style="color:#e3116c">"confirmWriteOp"</span><span class="token builtin class-name">:</span><span class="token plain"> true</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain">'</span><br></div></code></pre></div></div>
<ul>
<li class=""><strong>Two tables (V270).</strong> <code>agent_toolsets</code>: one per published version, with <code>enabled</code> and
<code>target</code> (<code>prod</code> | <code>mock</code>, consumed by AGX-2.4). <code>agent_toolset_tools</code>: one row per callable
operation, holding the canonical operation key, the AGX-1.1 compiled tool name, <code>write_op</code>,
<code>enabled</code>, and who confirmed an enabled write op and when.</li>
<li class=""><strong>Seeding.</strong> Creating a toolset enables <code>GET</code>/<code>HEAD</code> operations and GraphQL queries
(deprecated ones excepted). Every other operation is a write op and starts disabled. Enabling
one without <code>confirmWriteOp: true</code> is a <code>422 agent-toolset-write-op-unconfirmed</code>, and a
database CHECK makes an enabled but unconfirmed write op unrepresentable.</li>
<li class=""><strong>Seven routes</strong> under <code>/v1/tenants/{t}/agent-toolsets</code>: list (<code>?versionId</code>), create, get
(with tools), <code>PATCH</code> (<code>enabled</code> / <code>target</code>), <code>DELETE</code>, <code>GET …/{id}/tools</code>, and
<code>PATCH …/{id}/tools/{toolId}</code>. Guarded by the existing <code>api_keys</code> permissions; no new RBAC
resource.</li>
<li class=""><strong>Audited.</strong> <code>agent.toolset.create</code>, <code>agent.toolset.update</code> (before/after),
<code>agent.toolset.delete</code> and <code>agent.toolset.tool.update</code> (operation, write-op flag, enabled
before/after, confirmation time), with metadata only.</li>
<li class="">Docs: <code>docs/agent_toolsets.md</code>.</li>
</ul>
</li>
</ul>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="changed-1">Changed<a href="http://localhost/release-notes/unreleased#changed-1" class="hash-link" aria-label="Direct link to Changed" title="Direct link to Changed" translate="no">​</a></h4>
<ul>
<li class=""><strong>AGX-2.2 / AGX-3.1 handoffs closed.</strong> V270 deletes upstream credentials and agent keys whose
toolset does not exist, then gives both <code>toolset_id</code> columns a <code>(tenant_id, toolset_id)</code> foreign
key to <code>agent_toolsets</code> with <code>ON DELETE CASCADE</code>. Deleting a toolset now deletes its upstream
credentials and agent keys. <code>POST /agent-keys</code> and the upstream-credential list/create routes
answer <code>404 agent-toolset-not-found</code> for a toolset that is not in the caller's tenant.</li>
<li class="">apiome-mcp's agent-access middleware now reads a key's enabled tools from the toolset by
default (<code>toolset_enabled_tools</code>), instead of failing closed on every request.</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="13320--2026-09-17">1.332.0 — 2026-09-17<a href="http://localhost/release-notes/unreleased#13320--2026-09-17" class="hash-link" aria-label="Direct link to 1.332.0 — 2026-09-17" title="Direct link to 1.332.0 — 2026-09-17" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-3">Added<a href="http://localhost/release-notes/unreleased#added-3" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong>Agent keys (<a href="https://github.com/apiome/apiome/issues/4537" target="_blank" rel="noopener noreferrer" class="">#4537</a>, AGX-3.1)</strong>: a new kind of API key for AI agents. Each one is bound to one
agent toolset, limited to an explicit tool allowlist, and can expire. It is the credential an
agent presents to the MCP agent runtime, and the identity that quotas (AGX-3.2) and usage
analytics (AGX-3.3 / 3.4) will attach to.</p>
<div class="language-bash codeBlockContainer_mQmQ theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_t_Hd"><pre tabindex="0" class="prism-code language-bash codeBlock_RMoD thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_AclH"><div class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-sX</span><span class="token plain"> POST </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$APIOME</span><span class="token string" style="color:#e3116c">/v1/tenants/acme/agent-keys"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">     </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Authorization: Bearer </span><span class="token string variable" style="color:#36acaa">$JWT</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'Content-Type: application/json'</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">     </span><span class="token parameter variable" style="color:#36acaa">-d</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'{"name": "claude-desktop", "toolsetId": "'</span><span class="token plain">"</span><span class="token variable" style="color:#36acaa">$TOOLSET</span><span class="token string" style="color:#e3116c">"'"</span><span class="token plain">,</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token string" style="color:#e3116c">"toolAllowlist"</span><span class="token builtin class-name">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"listPets"</span><span class="token plain">, </span><span class="token string" style="color:#e3116c">"getPetById"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain">, </span><span class="token string" style="color:#e3116c">"expiresAt"</span><span class="token builtin class-name">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"2026-12-31T00:00:00Z"</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain">'</span><br></div></code></pre></div></div>
<ul>
<li class=""><strong><code>api_keys</code> extended (V269).</strong> <code>kind</code> (<code>workspace</code> | <code>agent</code>; existing rows are
<code>workspace</code>), <code>toolset_id</code> and <code>tool_allowlist</code> (a JSON array of AGX-1.1 tool names, at most
1024, no wildcard). <code>expires_at</code> is reused. CHECKs keep the two kinds exclusive and pin agent
keys to the scope <code>agent:invoke</code>.</li>
<li class=""><strong>Five routes</strong> under <code>/v1/tenants/{t}/agent-keys</code>: list (<code>?toolsetId</code>, <code>?includeRevoked</code>),
create, get, <code>PUT …/{id}/allowlist</code>, and <code>DELETE …/{id}</code> (revoke, idempotent). Guarded by the
existing <code>api_keys</code> view / create / edit / delete permissions; no new RBAC resource.</li>
<li class=""><strong>Secret shown once.</strong> The secret is <code>ak_</code> + 64 hex characters, returned only by create. Rows
store a bcrypt hash and the usual lookup prefix; no response carries the hash.</li>
<li class=""><strong>Audited.</strong> <code>agent.key.create</code>, <code>agent.key.allowlist_update</code> (the list before and after) and
<code>agent.key.revoke</code> go to the access audit, with metadata only.</li>
<li class=""><strong>Enforced in apiome-mcp.</strong> <code>apiome_mcp.agent_access.AgentAccessMiddleware</code> narrows
<code>tools/list</code> and <code>tools/call</code> to the toolset's enabled tools ∩ the allowlist. A revoke,
expiry or allowlist edit applies to the agent's next request. Until AGX-1.2 (<a href="https://github.com/apiome/apiome/issues/4530" target="_blank" rel="noopener noreferrer" class="">#4530</a>) supplies
toolset curation, it fails closed.</li>
<li class=""><code>toolset_id</code> has no foreign key yet (<code>agent_toolsets</code> is AGX-1.2). Docs:
<code>docs/agent_keys.md</code>.</li>
</ul>
</li>
</ul>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="security">Security<a href="http://localhost/release-notes/unreleased#security" class="hash-link" aria-label="Direct link to Security" title="Direct link to Security" translate="no">​</a></h4>
<ul>
<li class=""><strong>An agent key never authenticates a REST call.</strong> <code>validate_api_key</code> now selects
<code>kind = 'workspace'</code> keys only, and falls back to the older queries on a pre-V269 database.
Separately, no REST scope-allowlist entry accepts <code>agent:invoke</code>, so an agent key would get a
<code>403</code> on every route even if the kind filter were bypassed.</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="13310--2026-09-17">1.331.0 — 2026-09-17<a href="http://localhost/release-notes/unreleased#13310--2026-09-17" class="hash-link" aria-label="Direct link to 1.331.0 — 2026-09-17" title="Direct link to 1.331.0 — 2026-09-17" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-4">Added<a href="http://localhost/release-notes/unreleased#added-4" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong>Upstream auth vault (<a href="https://github.com/apiome/apiome/issues/4534" target="_blank" rel="noopener noreferrer" class="">#4534</a>, AGX-2.2)</strong> — agents must never hold real API credentials. A
tenant now stores the upstream credential a managed MCP toolset presents to its real API, and
the AGX-2.1 invocation proxy injects it server-side; the agent only ever holds its Apiome key.</p>
<div class="language-bash codeBlockContainer_mQmQ theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_t_Hd"><pre tabindex="0" class="prism-code language-bash codeBlock_RMoD thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_AclH"><div class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-sX</span><span class="token plain"> POST </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$APIOME</span><span class="token string" style="color:#e3116c">/v1/tenants/acme/agent-toolsets/</span><span class="token string variable" style="color:#36acaa">$TOOLSET</span><span class="token string" style="color:#e3116c">/upstream-credentials"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">     </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"X-API-Key: </span><span class="token string variable" style="color:#36acaa">$APIOME_KEY</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'Content-Type: application/json'</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">     </span><span class="token parameter variable" style="color:#36acaa">-d</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'{"serverUrl": "https://api.example.com/v1", "kind": "apiKey",</span><br></div><div class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">          "in": "header", "name": "X-Api-Key", "secret": {"value": "sk_live_…"}}'</span><br></div></code></pre></div></div>
<ul>
<li class=""><strong>Encrypted at rest.</strong> <code>apiome.upstream_credentials</code> (V268) holds ciphertext only, sealed by
the shared envelope cipher under its own key map (<code>APIOME_UPSTREAM_CREDENTIAL_ENCRYPTION_KEYS</code>)
and vault magic. No key configured ⇒ storing is a <code>503</code> and nothing is written in the clear.</li>
<li class=""><strong>Write-only.</strong> <code>GET</code> lists metadata (binding, kind, placement, key version, whether it still
opens, created / rotated / last-used); create, rotate (<code>POST …/{id}/rotate</code>) and delete accept
a secret and never return one. A test sweeps every <code>GET</code> route in the app for any rendering of
a stored secret or its ciphertext. The router's <code>422</code>s no longer echo a submitted body
(<code>app.redacted_validation_route</code>).</li>
<li class=""><strong>Bound to a toolset and a server URL.</strong> <code>https://</code> origin plus base path, stored normalized;
a secret is only opened for a request with the same scheme, host and port whose path sits
under the base path — never for a look-alike host, a downgrade, userinfo or an encoded
traversal. Kinds: <code>apiKey</code> (header or query, configurable name), <code>bearer</code>, <code>basic</code>.</li>
<li class=""><strong>Rotation without downtime.</strong> One in-place <code>UPDATE</code>; concurrent resolves see the old secret or
the new one, never neither, and in-flight invocations keep the secret they opened.</li>
<li class=""><strong>Use is audited as metadata only</strong> in the write-once <code>upstream_credential_uses</code> ledger (which
credential, which toolset, when, <code>injected</code> / <code>unavailable</code>); create / rotate / delete land in
the access audit as <code>agent.upstream_credential.*</code>. A bound credential that won't open fails
the call closed.</li>
<li class="">Guarded by the existing <code>api_keys</code> permission (view / create / edit / delete); no new RBAC
resource. <code>toolset_id</code> gains its foreign key in AGX-1.2 (<a href="https://github.com/apiome/apiome/issues/4530" target="_blank" rel="noopener noreferrer" class="">#4530</a>). See
<code>docs/upstream_credential_vault.md</code>.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="13290--2026-09-17">1.329.0 — 2026-09-17<a href="http://localhost/release-notes/unreleased#13290--2026-09-17" class="hash-link" aria-label="Direct link to 1.329.0 — 2026-09-17" title="Direct link to 1.329.0 — 2026-09-17" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-5">Added<a href="http://localhost/release-notes/unreleased#added-5" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong>API change check suite (<a href="https://github.com/apiome/apiome/issues/4740" target="_blank" rel="noopener noreferrer" class="">#4740</a>, GNC-3.1)</strong> — a pull request that changed an API used to collect
a wall of checks (lint, diff, breaking-change gate, contract test, SDK build), each with its own
log, and a reviewer who read none of them. The platform already knew every answer; the suite
turns them into <strong>one</strong> verdict — <code>pending</code>, <code>pass</code>, <code>fail</code> or <code>skipped</code> — recorded as an
evaluation, reported on the pull request as the <code>apiome/api-change</code> check (replacing the pending
check GNC-2.2's webhook seeded), and optionally required before publish.</p>
<div class="language-bash codeBlockContainer_mQmQ theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_t_Hd"><pre tabindex="0" class="prism-code language-bash codeBlock_RMoD thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_AclH"><div class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-sX</span><span class="token plain"> POST </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$APIOME</span><span class="token string" style="color:#e3116c">/v1/tenants/acme/projects/pets/versions/2.0.0/check-suite"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">     </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"X-API-Key: </span><span class="token string variable" style="color:#36acaa">$APIOME_KEY</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'Content-Type: application/json'</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">     </span><span class="token parameter variable" style="color:#36acaa">-d</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"{</span><span class="token string entity" style="color:#36acaa">\"</span><span class="token string" style="color:#e3116c">commit_sha</span><span class="token string entity" style="color:#36acaa">\"</span><span class="token string" style="color:#e3116c">: </span><span class="token string entity" style="color:#36acaa">\"</span><span class="token string variable" style="color:#36acaa">$GITHUB_SHA</span><span class="token string entity" style="color:#36acaa">\"</span><span class="token string" style="color:#e3116c">, </span><span class="token string entity" style="color:#36acaa">\"</span><span class="token string" style="color:#e3116c">pr_number</span><span class="token string entity" style="color:#36acaa">\"</span><span class="token string" style="color:#e3116c">: 42}"</span><br></div></code></pre></div></div>
<ul>
<li class=""><strong>Five components, all existing evidence.</strong> <code>lint</code> (the stored-first lint report;
error-severity violations fail it, as they fail publish), <code>breaking</code> and <code>consumers</code> (the CTG
classification against the previous published revision and CTG-4.2's per-consumer verdicts for
that same diff), <code>contract</code> (the newest ECA contract run of this revision, counted only while
recompiling its own reference still yields the digest it executed) and <code>sdk</code> (the SDK kit
manifest built from the draft). The three CTG-4.5 components are judged by the deploy gate's
own evaluators and thresholds, so the pull request and the deploy gate cannot disagree.</li>
<li class=""><strong>Deterministic.</strong> A tenant (or project) policy marks each component <code>required</code>, <code>advisory</code>
or <code>off</code>; the required ones reduce with GNC-2.2's precedence (a failure fails, an unfinished
component holds, a set of skips is a skip). Missing evidence holds a required component
<code>pending</code> and skips an advisory one; unreadable evidence is <code>pending</code>, never green; a version
with no captured source skips contract and sdk rather than waiting forever.</li>
<li class=""><strong>The suite judges the draft.</strong> Its verdict is reported against the commit the binding is
synchronized with. A newer commit gets a placeholder: <code>skipped</code> (<code>spec-unchanged</code>) when it does
not touch the bound specification, otherwise <code>pending</code> (<code>draft-not-synchronized</code>).</li>
<li class=""><strong>Idempotent re-runs.</strong> An evaluation is keyed by the draft digest, commit, requirements,
thresholds, and every component's verdict and evidence; unchanged inputs are a <code>200</code> replay of
the same evaluation with the same evidence ids, and the provider is not called again.</li>
<li class=""><strong>Drill-down.</strong> <code>GET …/check-suite/runs/{id}</code> names every component's evidence (ids,
digests, counts), a link to it, and the rule behind it, plus snapshots of both policies. The
pull-request summary is the same thing as markdown. Consumer names are redacted for readers
without <code>consumer_contracts:view</code>.</li>
<li class=""><strong>Required before publish.</strong> <code>requiredForPublish</code> on the suite policy refuses (<code>422</code>,
<code>apiCheckSuiteGate</code>) a version whose current content has no passing evaluation under the
component requirements and thresholds in force; force-publish stays the escape, and every
judged publish is audited as <code>version.api_check_suite_gate</code>.</li>
<li class="">Endpoints: <code>POST</code>/<code>GET …/projects/{project}/versions/{version}/check-suite</code>, <code>GET …/runs</code>,
<code>GET …/projects/{project}/check-suite/runs/{run_id}</code>, and <code>GET</code>/<code>PUT</code>/<code>DELETE</code> of
<code>…/governance/check-suite-policy</code> and <code>…/projects/{project}/check-suite-policy</code> (tenant
administrators only for changes). The latest-evaluation read is allowlisted for <code>diff:read</code> /
<code>lint:read</code> CI keys. CLI: <code>apiome checks run|show</code>.</li>
<li class="">apiome-db <strong>V267</strong>: <code>apiome.api_check_suite_policy</code> (one row per scope, mutable — every
evaluation snapshots the policy it was judged under) and <code>apiome.api_check_suite_runs</code>
(append-only except the <code>ON DELETE SET NULL</code> of its author; a placeholder can never pass or
fail), plus an index on ECA-1.3 runs by <code>source-&gt;&gt;'revision_id'</code>.</li>
</ul>
</li>
</ul>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="fixed">Fixed<a href="http://localhost/release-notes/unreleased#fixed" class="hash-link" aria-label="Direct link to Fixed" title="Direct link to Fixed" translate="no">​</a></h4>
<ul>
<li class=""><strong>A retried provider check publish that succeeded was never recorded (GNC-2.2).</strong> The publish
ledger was unique on <code>(check_run_id, request_fingerprint)</code>, so a verdict's failed attempt took
the slot its successful retry needed: the retry reached the provider, collided in the ledger,
and never wrote the provider's check-run id back — so every later publish POSTed a <em>new</em> check
run onto the pull request. V267 re-keys the ledger per outcome and the lookup reads a verdict's
dispatch first; a verdict is still dispatched at most once.</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="13280--2026-09-16">1.328.0 — 2026-09-16<a href="http://localhost/release-notes/unreleased#13280--2026-09-16" class="hash-link" aria-label="Direct link to 1.328.0 — 2026-09-16" title="Direct link to 1.328.0 — 2026-09-16" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-6">Added<a href="http://localhost/release-notes/unreleased#added-6" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong>Three-way spec synchronization (<a href="https://github.com/apiome/apiome/issues/4739" target="_blank" rel="noopener noreferrer" class="">#4739</a>, GNC-2.3)</strong> — a bound draft has three descriptions of the
same API — the repository at the commit it is synchronized with (the <strong>base</strong>), the repository at
the commit its branch has moved to (<strong>Git</strong>), and the version as this platform has it (the
<strong>draft</strong>) — and they drift apart independently. Copying either repository side over the draft
destroys whatever somebody was editing and invalidates whatever reviewers already decided. A
<strong>semantic three-way merge</strong> now measures both sides against the base, applies the incoming
changes that touch nothing the draft touched, and hands back every overlap as an explicit
conflict.</p>
<div class="language-bash codeBlockContainer_mQmQ theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_t_Hd"><pre tabindex="0" class="prism-code language-bash codeBlock_RMoD thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_AclH"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># The branch moved. What would land, and what collides?</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-sX</span><span class="token plain"> POST </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$APIOME</span><span class="token string" style="color:#e3116c">/v1/tenants/acme/projects/pets/versions/2.0.0/binding/sync"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">     </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Authorization: Bearer </span><span class="token string variable" style="color:#36acaa">$TOKEN</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'Content-Type: application/json'</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-d</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'{}'</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># Decide one collision. `git` takes the repository's value; `draft` keeps the version's.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-sX</span><span class="token plain"> POST </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$APIOME</span><span class="token string" style="color:#e3116c">/v1/tenants/acme/projects/pets/sync-plans/</span><span class="token string variable" style="color:#36acaa">$PLAN</span><span class="token string" style="color:#e3116c">/conflicts/</span><span class="token string variable" style="color:#36acaa">$CONFLICT</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">     </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Authorization: Bearer </span><span class="token string variable" style="color:#36acaa">$TOKEN</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'Content-Type: application/json'</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">     </span><span class="token parameter variable" style="color:#36acaa">-d</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'{"resolution":"git","note":"the rename was agreed in review"}'</span><br></div></code></pre></div></div>
<ul>
<li class=""><strong>A merge result is a reading, never a write.</strong> There is no code path from this surface to the
canonical model, to <code>versions</code>, or to a review — not in the store, not in the accessors, and not
in the schema. Computing a merge writes one row; settling a conflict records which side a person
chose. Both leave the draft exactly where it was, which is what makes it safe for a webhook to
raise a candidate and for anyone at all to merge it.</li>
<li class="">apiome-db <strong>V266</strong>: <code>apiome.draft_sync_plans</code> (one merge per binding and trio of documents,
recording the base, Git and draft digests it was computed from, what merged and how much did
not, with the identity frozen by a trigger and a CHECK that keeps the status and the two counts
from disagreeing) and <code>apiome.draft_sync_conflicts</code> (one row per overlap, carrying all three
values with the repository file and line it lives at, settled once towards <code>git</code> or <code>draft</code>).</li>
<li class="">apiome-rest <code>app.spec_sync</code> (vocabulary, models, the merge engine, the plan fingerprint and the
YAML/JSON line locator — all pure), <code>app.spec_sync_store</code> (the rules), <code>app.spec_sync_routes</code>
(four endpoints), and the accessors at the end of <code>database.py</code>, whose <code>sync.planned</code> /
<code>sync.conflict_resolved</code> audit rows are written <strong>inside</strong> each write's transaction.</li>
<li class=""><strong>Conflicts are located, not just named.</strong> Each carries its RFC 6901 pointer, the shared
<code>document</code>/<code>path</code>/<code>operation</code>/<code>component</code>/<code>schema</code> grouping every other change list in the
product uses, what each side did to the base, all three values, and the repository file, 1-based
line and link at the commit. Lines come from <em>composing</em> the incoming document rather than
loading it, and a mapping entry reports its key's line — in block YAML the value of <code>summary:</code>
starts on the next line, which is not where anybody would look. A value too large to send to a
browser is replaced by its size, and a merge that collides in more places than one result holds
says so on the row — a result that quietly dropped findings would read as <em>less</em> conflicted than
it is.</li>
<li class=""><strong>Three proven reads, never a payload.</strong> Both commits are fetched through the same
credential-resolving read binding uses, so a repository the tenant cannot reach is answered
<code>403 binding-repository-forbidden</code> rather than merged from a guess. And the base is re-read and
re-hashed first: if it no longer matches the digest the binding recorded, history was rewritten
underneath the merge base and the request refuses with <code>sync-base-drifted</code> instead of guessing
which side changed what.</li>
<li class=""><strong>Reruns are free, not just idempotent.</strong> The rerun key is taken over the binding and the three
documents' digests — all known before any network call — so re-merging an unchanged trio returns
the stored result without fetching two commits to prove it would be the same. The draft is
identified by its content digest rather than its revision id, because an edited revision is a
different document; a stored result whose draft has moved on reads as <code>stale</code>.</li>
<li class=""><strong>Work that already exists is reported, not trampled.</strong> A result carries a <code>guard</code> — <code>none</code>,
<code>review_decided</code> (an open review's current round already holds a decision) or
<code>version_published</code>. A guard never refuses the merge: knowing exactly what would collide is
precisely what such a reader needs.</li>
<li class="">Documented in <code>apiome-rest/docs/spec_sync.md</code>. This ticket deliberately <strong>writes nothing back to
Git</strong> and <strong>applies nothing to a draft</strong>; the merged document is computed and proven
deterministic, but turning it into an edit is a separate, explicit act with no storage here.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="13270--2026-09-16">1.327.0 — 2026-09-16<a href="http://localhost/release-notes/unreleased#13270--2026-09-16" class="hash-link" aria-label="Direct link to 1.327.0 — 2026-09-16" title="Direct link to 1.327.0 — 2026-09-16" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-7">Added<a href="http://localhost/release-notes/unreleased#added-7" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong>Provider webhook and status adapter (<a href="https://github.com/apiome/apiome/issues/4738" target="_blank" rel="noopener noreferrer" class="">#4738</a>, GNC-2.2)</strong> — GNC-2.1 gave a draft a durable
relationship to a repository ref, but nothing could answer the provider: a pull request that
changed an API showed no verdict from this platform, because there was nowhere to record one and
nothing to send it with. Both halves now exist — a <strong>normalized check model</strong> that belongs to the
platform rather than to any provider, and one <strong>status adapter</strong> interface with GitHub, GitLab
and Bitbucket behind it.</p>
<div class="language-bash codeBlockContainer_mQmQ theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_t_Hd"><pre tabindex="0" class="prism-code language-bash codeBlock_RMoD thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_AclH"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># Record a verdict about the commit this draft is bound to, and put it on the pull request.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># (binding, commit, name) identifies the check, so the same call twice is one verdict.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-sX</span><span class="token plain"> POST </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$APIOME</span><span class="token string" style="color:#e3116c">/v1/tenants/acme/projects/pets/versions/2.0.0/binding/checks"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">     </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Authorization: Bearer </span><span class="token string variable" style="color:#36acaa">$TOKEN</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'Content-Type: application/json'</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">     </span><span class="token parameter variable" style="color:#36acaa">-d</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'{"name":"apiome/api-change","state":"fail","title":"2 breaking changes",</span><br></div><div class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">          "summary":"`GET /pets` lost a required field.","details_url":"https://…"}'</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># What did the provider actually do with it?</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-s</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$APIOME</span><span class="token string" style="color:#e3116c">/v1/tenants/acme/projects/pets/checks/</span><span class="token string variable" style="color:#36acaa">$CHECK_ID</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">     </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Authorization: Bearer </span><span class="token string variable" style="color:#36acaa">$TOKEN</span><span class="token string" style="color:#e3116c">"</span><br></div></code></pre></div></div>
<ul>
<li class=""><strong>Four words, provider-independent</strong>: <code>pending</code> / <code>pass</code> / <code>fail</code> / <code>skipped</code>. The three
providers disagree about what a status even is — GitHub splits it into <code>status</code> and
<code>conclusion</code>, GitLab has one commit <code>state</code>, Bitbucket has build states in capitals and no
<code>skipped</code> at all — so storing any one of them would make the other two lossy translations.
Only <code>pass</code> ever maps to a provider's passing status: a skip is "we did not look", which is not
"we looked and it is fine".</li>
<li class="">apiome-db <strong>V265</strong>: <code>apiome.provider_check_runs</code> (one verdict per binding, commit and check
name, backed by a unique index — the re-run idempotency — with <code>completed_at</code> tied to the state
by a CHECK and the identity frozen by a trigger) and <code>apiome.provider_check_deliveries</code> (an
append-only publish ledger, one row per distinct verdict sent, which may not claim a reach it
did not have).</li>
<li class="">apiome-rest <code>app.provider_checks</code> (vocabulary, models, codes, the provider spelling tables and
the fingerprint — all pure), <code>app.provider_status_adapter</code> (one <code>StatusAdapter</code> contract;
GitHub check runs, GitLab commit statuses, Bitbucket build statuses), <code>app.provider_check_store</code>
(the rules), <code>app.provider_check_routes</code> (four endpoints), and the accessors at the end of
<code>database.py</code>, whose <code>check.recorded</code> / <code>check.published</code> audit rows are written <strong>inside</strong> each
write's transaction.</li>
<li class=""><strong>Recording comes before publishing, always.</strong> A verdict is written first and only then offered
to the provider, so a refusal appends a <code>failed</code> row to the publish ledger and the verdict
stands. A check recorded and not published is evidence; a check published and not recorded is a
green tick with nothing behind it.</li>
<li class=""><strong>Idempotent twice over.</strong> Re-recording the same verdict moves one row rather than fanning out a
second, and each publish carries a fingerprint of the <em>verdict</em> — not of the HTTP call, which
changes once a provider hands back an id — consulted <strong>before</strong> the adapter runs. A redelivered
webhook therefore costs the provider nothing, not just us.</li>
<li class=""><strong>A delivery resolves to an authorized binding, or to nothing.</strong> A verified push or PR on a
bound ref now seeds a <code>pending</code> check beside GNC-2.1's sync candidate, through the <em>existing</em>
REPO-4.3 endpoint and deliberately outside its tracked-branch gate. Authorized means the binding
is active <em>and</em> its repository registration still exists — that registration is the credential a
verdict is published with. Both halves are best-effort: neither can turn a verified delivery
into a 500 the provider would retry forever. The acceptance audit carries <code>checksSeeded</code>.</li>
<li class=""><strong>Browser clients never receive repository tokens, and cannot supply one.</strong> The token is
resolved in server memory from the binding's registration (the same vault lookup an import
uses); V265 has no token column on either table, not an encrypted one; the request and response
models forbid extras; and a provider's refusal is redacted before it is stored.</li>
<li class="">New settings: <code>APIOME_PROVIDER_CHECKS_ENABLED</code> (publish kill switch — recording is never
gated), <code>APIOME_PROVIDER_CHECKS_WEBHOOK_SEED</code>, <code>APIOME_PROVIDER_CHECKS_DETAILS_BASE_URL</code>.</li>
<li class="">Documented in <code>apiome-rest/docs/provider_checks.md</code>. This ticket <strong>produces</strong> no verdicts — it
carries them; the check suite that decides one is GNC-3.1. Making a check <em>required</em> stays a
provider-side branch protection setting, which is the repository owner's to make.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="13260--2026-09-16">1.326.0 — 2026-09-16<a href="http://localhost/release-notes/unreleased#13260--2026-09-16" class="hash-link" aria-label="Direct link to 1.326.0 — 2026-09-16" title="Direct link to 1.326.0 — 2026-09-16" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-8">Added<a href="http://localhost/release-notes/unreleased#added-8" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong>Branch-to-draft binding (<a href="https://github.com/apiome/apiome/issues/4737" target="_blank" rel="noopener noreferrer" class="">#4737</a>, GNC-2.1)</strong> — repository import creates a snapshot; it does not
create a durable review unit. A <strong>binding</strong> now pins one draft version to one provider,
repository, ref and source path, together with the digest of the source it is synchronized with,
and every observed movement of that ref becomes an explicit, auditable <strong>sync candidate</strong> instead
of quietly rewriting the draft.</p>
<div class="language-bash codeBlockContainer_mQmQ theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_t_Hd"><pre tabindex="0" class="prism-code language-bash codeBlock_RMoD thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_AclH"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># Bind a draft to a branch. The ref is resolved and the selection read through a STORED</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># credential first: that read is the authorization check, and it produces the commit and digest.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-sX</span><span class="token plain"> POST </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$APIOME</span><span class="token string" style="color:#e3116c">/v1/tenants/acme/projects/pets/versions/2.0.0/binding"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">     </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Authorization: Bearer </span><span class="token string variable" style="color:#36acaa">$TOKEN</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'Content-Type: application/json'</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">     </span><span class="token parameter variable" style="color:#36acaa">-d</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'{"repository_id":"…","ref":"main","path":"spec/openapi.yaml"}'</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># Has the branch moved? A moved ref records a candidate; nothing about the draft changes.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-sX</span><span class="token plain"> POST </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$APIOME</span><span class="token string" style="color:#e3116c">/v1/tenants/acme/projects/pets/versions/2.0.0/binding/check"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">     </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Authorization: Bearer </span><span class="token string variable" style="color:#36acaa">$TOKEN</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># Decide: `applied` re-reads the source at that commit and advances the binding's base;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># `dismissed` leaves it exactly where it is. A candidate settles once.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-sX</span><span class="token plain"> POST </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$APIOME</span><span class="token string" style="color:#e3116c">/v1/tenants/acme/projects/pets/versions/2.0.0/binding/candidates/</span><span class="token string variable" style="color:#36acaa">$ID</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">     </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Authorization: Bearer </span><span class="token string variable" style="color:#36acaa">$TOKEN</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'Content-Type: application/json'</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">     </span><span class="token parameter variable" style="color:#36acaa">-d</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'{"status":"dismissed","note":"not ours to take"}'</span><br></div></code></pre></div></div>
<ul>
<li class="">apiome-db <strong>V264</strong>: <code>apiome.draft_repository_bindings</code> (one active binding per draft, backed by
a partial unique index; released rows kept as history and frozen by a trigger) and
<code>apiome.draft_binding_sync_candidates</code> (idempotent per target commit <em>and</em> per provider
delivery, settling exactly once). What a binding names never changes — only the synchronized
pair (<code>commit_sha</code>, <code>source_digest</code>) moves, and only while it is active.</li>
<li class="">apiome-rest <code>app.draft_bindings</code> (vocabulary, models, codes, the digest — all pure),
<code>app.draft_binding_store</code> (the rules), <code>app.draft_binding_routes</code> (seven endpoints), and the
accessors at the end of <code>database.py</code>, whose <code>binding.*</code> audit rows are written <strong>inside</strong> each
write's transaction.</li>
<li class=""><strong>Authorization is a proven read, not a claim.</strong> Binding, checking, and applying each resolve a
stored linked-account credential and actually read the ref and the selection through the
provider before anything is written. No token is ever accepted from a request body.</li>
<li class=""><strong>A ref update never rewrites a draft.</strong> A verified provider delivery on a bound ref raises a
candidate through the <em>existing</em> REPO-4.3 webhook endpoint — deliberately outside its
tracked-branch gate, because a bound branch need never have been imported from. A push moves the
branch it names; a pull request moves its head branch, never its base.</li>
<li class="">De-registering a repository releases every binding it authorized, in the same transaction as the
soft delete, reason <code>repository_removed</code>.</li>
<li class=""><code>Database._insert_review_audit</code> was generalized to <code>_insert_workflow_audit_tx</code>; COL-2.1's five
call sites are unchanged in behaviour.</li>
<li class="">Documented in <code>apiome-rest/docs/draft_bindings.md</code>. Writing back to the provider (check runs, PR
status) is GNC-2.2; merging repository changes into a draft is GNC-2.3, which will settle the
same candidates once it can reconcile without silent overwrite.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="13250--2026-09-15">1.325.0 — 2026-09-15<a href="http://localhost/release-notes/unreleased#13250--2026-09-15" class="hash-link" aria-label="Direct link to 1.325.0 — 2026-09-15" title="Direct link to 1.325.0 — 2026-09-15" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-9">Added<a href="http://localhost/release-notes/unreleased#added-9" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong>Notification model &amp; fan-out (<a href="https://github.com/apiome/apiome/issues/4521" target="_blank" rel="noopener noreferrer" class="">#4521</a>, COL-3.1)</strong> — the collaboration roadmap stops depending on
people polling pages. Every mention, review request, decision, thread resolution and publish now
writes one inbox row per person it concerns, <strong>in the same transaction as the event itself</strong>, and
three endpoints read that inbox.</p>
<div class="language-bash codeBlockContainer_mQmQ theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_t_Hd"><pre tabindex="0" class="prism-code language-bash codeBlock_RMoD thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_AclH"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># The bell badge</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-s</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$APIOME</span><span class="token string" style="color:#e3116c">/v1/tenants/acme/notifications/unread-count"</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Authorization: Bearer </span><span class="token string variable" style="color:#36acaa">$TOKEN</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># {"total":3,"by_type":{"mention":2,"review_requested":1,"review_decision":0,…}}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># The list behind the notification centre</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-s</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$APIOME</span><span class="token string" style="color:#e3116c">/v1/tenants/acme/notifications?unread=true&amp;limit=20"</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Authorization: Bearer </span><span class="token string variable" style="color:#36acaa">$TOKEN</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># Clearing it</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-sX</span><span class="token plain"> POST </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$APIOME</span><span class="token string" style="color:#e3116c">/v1/tenants/acme/notifications/read"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Authorization: Bearer </span><span class="token string variable" style="color:#36acaa">$TOKEN</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'Content-Type: application/json'</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-d</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'{"all": true}'</span><br></div></code></pre></div></div>
<ul>
<li class=""><strong>Storage</strong> (apiome-db <strong>V263</strong>): <code>notifications</code> — recipient, tenant, <code>type</code>, <code>payload jsonb</code>,
<code>read_at</code>, plus the actor and the project/version the row points at, so a deleted destination
takes its dead links with it while "somebody mentioned you" survives that somebody leaving.</li>
<li class=""><strong>Five event types</strong>: <code>mention</code> (an edit notifies only the members it <em>newly</em> names),
<code>review_requested</code> (every round's reviewers), <code>review_decision</code> (the member who asked for the
review), <code>thread_resolved</code> (everyone who took part), <code>version_published</code> (the version's
collaborators — its review and thread participants). Nobody is ever notified of their own
action, and a recipient set is bounded at 200.</li>
<li class=""><strong>Transactional fan-out</strong>: each write accessor takes a <code>notify</code> callable and runs it inside its
own transaction, so a committed event always has its rows and a refused one leaves none. The
insert joins <code>users</code>, so a recipient deleted mid-flight is skipped rather than rolling the
event back.</li>
<li class=""><strong>Retention</strong>: a statement-level trigger keeps each inbox to its newest 500 rows, so the bound
holds for every writer rather than depending on each one to remember.</li>
<li class=""><strong>API</strong>: <code>GET /v1/tenants/{tenantSlug}/notifications</code> (filters <code>unread</code>, <code>type</code>, paged),
<code>GET …/notifications/unread-count</code> (total and per type, zeroes included), and
<code>POST …/notifications/read</code> (<code>ids</code> or <code>all</code>, idempotent, answering with the new count). All
three need only authentication — an inbox is the caller's own, which no <code>resource:action</code> can
express — and listing never marks anything read.</li>
<li class="">Documented in <code>docs/notifications.md</code>. Per-type delivery preferences are <strong>not</strong> stored yet;
COL-3.2 needs them and should filter on read, not at write time.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="13240--2026-09-15">1.324.0 — 2026-09-15<a href="http://localhost/release-notes/unreleased#13240--2026-09-15" class="hash-link" aria-label="Direct link to 1.324.0 — 2026-09-15" title="Direct link to 1.324.0 — 2026-09-15" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-10">Added<a href="http://localhost/release-notes/unreleased#added-10" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong>Approval policy &amp; publish gate (<a href="https://github.com/apiome/apiome/issues/4519" target="_blank" rel="noopener noreferrer" class="">#4519</a>, COL-2.3)</strong> — review outcomes become binding at publish
time. A tenant can require <em>n</em> approvals (optionally from a named role) before a draft version
may be published; publishing without them is refused, and the force-publish escape is audited.</p>
<div class="language-bash codeBlockContainer_mQmQ theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_t_Hd"><pre tabindex="0" class="prism-code language-bash codeBlock_RMoD thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_AclH"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># Arm the gate on the governing style guide</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-sX</span><span class="token plain"> PUT </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$APIOME</span><span class="token string" style="color:#e3116c">/v1/style-guides/acme/</span><span class="token string variable" style="color:#36acaa">$GUIDE_ID</span><span class="token string" style="color:#e3116c">/policy"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Authorization: Bearer </span><span class="token string variable" style="color:#36acaa">$TOKEN</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'Content-Type: application/json'</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token parameter variable" style="color:#36acaa">-d</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'{"requiredApprovals": 2, "requiredReviewerRole": "release-manager"}'</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># A publish that has not collected them is refused with 422 + the verdict</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-sX</span><span class="token plain"> POST </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$APIOME</span><span class="token string" style="color:#e3116c">/v1/versions/acme/pets/</span><span class="token string variable" style="color:#36acaa">$REVISION</span><span class="token string" style="color:#e3116c">/publish"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Authorization: Bearer </span><span class="token string variable" style="color:#36acaa">$TOKEN</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'Content-Type: application/json'</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token parameter variable" style="color:#36acaa">-d</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'{"shortMessage": "Ship it"}'</span><br></div></code></pre></div></div>
<ul>
<li class=""><strong>Policy</strong> (apiome-db <strong>V262</strong>): <code>style_guides.required_approvals</code> (<code>0</code> = off, the default,
capped at the 20-reviewer limit) and <code>style_guides.required_reviewer_role</code> (a <code>roles.slug</code>,
nullable). Both resolve through the GOV-1.4 guide chain — project assignment → tenant
assignment → tenant default — beside the CTG-3.4 breaking-publish level, and both are frozen
into each <code>style_guide_revisions</code> snapshot. Read/written at
<code>GET</code>/<code>PUT /v1/style-guides/{tenantSlug}/{guideId}/policy</code>, where <code>requiredReviewerRole</code> is
the one field for which <em>omitted</em> (leave alone) and <em>null</em> (clear) differ.</li>
<li class=""><strong>Gate</strong>: only the version's <strong>open</strong> review and only its <strong>current round</strong> count. Blocked
with a stable <code>reason</code> for <code>no-review</code>, <code>changes-requested</code>, <code>spec-changed</code> (the content moved
since the round was requested, so its approvals are stale — COL-2.1's <code>spec_changed</code>),
<code>insufficient-approvals</code>, and <code>missing-required-role</code>. A reviewer's role is their effective
RBAC slug, so a tenant administrator resolves to <code>owner</code>.</li>
<li class=""><strong>422 contract</strong>: <code>{"detail": {"message": …, "approvalGate": {…}}}</code>, the same shape the
breaking-publish guardrail and the verification policy use. Force-publishing
(<code>skipPublishChecks</code> + <code>forcePublishReason</code>) gets past it, exactly as it does for them.</li>
<li class=""><strong>Audit</strong>: every publish an armed policy judged appends a <code>version.approval_policy_gate</code>
<code>workflow_audit</code> row with <code>action</code> of <code>satisfied</code>, <code>forced</code>, or <code>unavailable</code> — passing
verdicts included, so the trail can answer "who signed off on this release?".</li>
<li class="">Faults degrade to <code>unavailable</code> and never block a publish; the degradation is audited rather
than silent.</li>
<li class="">Documented in <code>docs/approval_publish_gate.md</code>; 46 tests in
<code>tests/test_approval_publish_gate.py</code>.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="13220--2026-09-14">1.322.0 — 2026-09-14<a href="http://localhost/release-notes/unreleased#13220--2026-09-14" class="hash-link" aria-label="Direct link to 1.322.0 — 2026-09-14" title="Direct link to 1.322.0 — 2026-09-14" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-11">Added<a href="http://localhost/release-notes/unreleased#added-11" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong>Comment anchor resilience (<a href="https://github.com/apiome/apiome/issues/4516" target="_blank" rel="noopener noreferrer" class="">#4516</a>, COL-1.4)</strong> — a comment thread no longer silently disappears
when its element is deleted, and renames and moves are proven to keep it in place.</p>
<div class="language-bash codeBlockContainer_mQmQ theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_t_Hd"><pre tabindex="0" class="prism-code language-bash codeBlock_RMoD thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_AclH"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># Threads whose element was deleted, with the element's last-known label</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-s</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$APIOME</span><span class="token string" style="color:#e3116c">/v1/tenants/acme/projects/pets/comment-threads?version=1.0.0&amp;status=orphaned"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Authorization: Bearer </span><span class="token string variable" style="color:#36acaa">$TOKEN</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># Re-attach one to another element of the same version</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-sX</span><span class="token plain"> POST </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$APIOME</span><span class="token string" style="color:#e3116c">/v1/tenants/acme/projects/pets/comment-threads/&lt;thread id&gt;/relink"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Authorization: Bearer </span><span class="token string variable" style="color:#36acaa">$TOKEN</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'Content-Type: application/json'</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token parameter variable" style="color:#36acaa">-d</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'{"anchor_type": "property", "anchor_id": "&lt;class property id&gt;"}'</span><br></div></code></pre></div></div>
<ul>
<li class=""><strong>Orphaning</strong> (apiome-db <strong>V260</strong>): <code>status</code> gains <code>orphaned</code>, with <code>anchor_label</code> (the element's
name when it was deleted — <code>Customer</code>, <code>Customer.email</code>, <code>/customers/{id}</code>, <code>GET /customers/{id}</code>)
and <code>orphaned_at</code>. Database triggers orphan threads on every delete path: the class, property,
path, and operation DELETE routes, a whole-version source-change rewrite, and foreign-key cascades
(a class takes its properties' threads, a path its operations'). A resolved thread keeps its
resolution while orphaned.</li>
<li class=""><strong>Relink</strong>: <code>POST …/comment-threads/{thread_id}/relink</code> <code>{anchor_type, anchor_id}</code> re-attaches an
orphaned thread to an element of its own version (or to the version). It comes back <code>resolved</code>
if it was, <code>open</code> otherwise. <code>404 comment-anchor-not-found</code> for a target outside the version,
<code>409 comment-thread-not-orphaned</code> for a thread that is not orphaned. Requires <code>projects:view</code>.</li>
<li class="">Resolving or reopening an orphaned thread is <code>409 comment-thread-orphaned</code>. Replies still work.</li>
<li class="">Renames and moves (class, property, path, operation, canvas position) are pinned by tests as
in-place updates keyed by the element id, and no orphan trigger watches a name column.</li>
<li class="">Documented in <code>docs/comments.md</code>.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="13210--2026-09-14">1.321.0 — 2026-09-14<a href="http://localhost/release-notes/unreleased#13210--2026-09-14" class="hash-link" aria-label="Direct link to 1.321.0 — 2026-09-14" title="Direct link to 1.321.0 — 2026-09-14" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-12">Added<a href="http://localhost/release-notes/unreleased#added-12" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong>Comment threads (<a href="https://github.com/apiome/apiome/issues/4513" target="_blank" rel="noopener noreferrer" class="">#4513</a>, COL-1.1)</strong> — discussion now lives next to the specification instead of in
Slack screenshots. Open a thread on a class, property, path, operation, or a whole version; reply
in Markdown; mention teammates; resolve and reopen.</p>
<div class="language-bash codeBlockContainer_mQmQ theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_t_Hd"><pre tabindex="0" class="prism-code language-bash codeBlock_RMoD thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_AclH"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># Ask about a class on version 1.0.0 and mention a teammate</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-sX</span><span class="token plain"> POST </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$APIOME</span><span class="token string" style="color:#e3116c">/v1/tenants/acme/projects/pets/comment-threads"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Authorization: Bearer </span><span class="token string variable" style="color:#36acaa">$TOKEN</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'Content-Type: application/json'</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token parameter variable" style="color:#36acaa">-d</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'{"version": "1.0.0", "anchor_type": "class", "anchor_id": "&lt;class id&gt;", "body": "Nullable, @bob.brown?"}'</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># Everything open on that version that mentions me</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-s</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$APIOME</span><span class="token string" style="color:#e3116c">/v1/tenants/acme/projects/pets/comment-threads?version=1.0.0&amp;status=open&amp;mentions_me=true"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Authorization: Bearer </span><span class="token string variable" style="color:#36acaa">$TOKEN</span><span class="token string" style="color:#e3116c">"</span><br></div></code></pre></div></div>
<ul>
<li class=""><strong>Storage</strong> (apiome-db <strong>V259</strong>): <code>comment_threads</code> (tenant, project, version, <code>anchor_type</code> +
<code>anchor_id</code>, <code>status</code>, resolution stamps, <code>last_activity_at</code>) and <code>comments</code> (<code>thread_id</code>,
<code>author_id</code>, Markdown <code>body</code>, <code>mentions uuid[]</code>, <code>edited_at</code>). A thread is anchored by the
element's <strong>primary key</strong>, never canvas coordinates, and the element must exist in the named
version when the thread is opened.</li>
<li class=""><strong>Endpoints</strong> under <code>…/projects/{project_ref}/comment-threads</code>: list (filters <code>version</code>, <code>status</code>,
<code>anchor_type</code>, <code>anchor_id</code>, <code>mentions_me</code>; paged with a <code>total</code>; each row carries its opening
comment), open, read, delete, <code>resolve</code>, <code>reopen</code>, and <code>…/comments</code> to reply, edit, and delete.
Deleting a thread's last comment deletes the thread.</li>
<li class=""><strong>Permissions</strong>: no new RBAC resource. <code>projects:view</code> is enough to read, comment, resolve, and
reopen. Editing or deleting a comment is limited to its author or a tenant administrator, and
deleting a thread to whoever opened it or a tenant administrator (<code>403 comment-forbidden</code>).</li>
<li class=""><strong>Mentions</strong> are resolved on the server against active and pending members, by full email, email
local part, or display name without spaces. A handle shared by several members resolves to
nobody. Code, escapes, addresses, and URLs are never mentions, and an edit re-resolves them.</li>
<li class=""><strong>Rate limit</strong>: opening and replying share a per-user budget,
<code>APIOME_COMMENT_CREATE_RATE_LIMIT_PER_MINUTE</code> (default 30); over it returns
<code>429 comment-rate-limited</code>. Documented in <code>docs/comments.md</code>.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="13200--2026-09-10">1.320.0 — 2026-09-10<a href="http://localhost/release-notes/unreleased#13200--2026-09-10" class="hash-link" aria-label="Direct link to 1.320.0 — 2026-09-10" title="Direct link to 1.320.0 — 2026-09-10" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-13">Added<a href="http://localhost/release-notes/unreleased#added-13" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong>Auto-regen on publish (<a href="https://github.com/apiome/apiome/issues/4497" target="_blank" rel="noopener noreferrer" class="">#4497</a>, SDK-4.3)</strong> — watch mode for SDKs. Subscribe a project's SDK once,
and every published version regenerates it and ships it: an SDK-4.1 registry publish, an SDK-4.2
pull request, or both. Nobody has to remember.</p>
<div class="language-bash codeBlockContainer_mQmQ theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_t_Hd"><pre tabindex="0" class="prism-code language-bash codeBlock_RMoD thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_AclH"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># Every publish of widgets: publish the npm SDK, then open a PR carrying that same version</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-sX</span><span class="token plain"> PUT </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$APIOME</span><span class="token string" style="color:#e3116c">/v1/projects/acme/widgets/sdk-regen-subscriptions/npm"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Authorization: Bearer </span><span class="token string variable" style="color:#36acaa">$TOKEN</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'Content-Type: application/json'</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token parameter variable" style="color:#36acaa">-d</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'{"deliveryMode": "registry_and_git"}'</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># What each publish did — and the dead letter</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-s</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$APIOME</span><span class="token string" style="color:#e3116c">/v1/projects/acme/widgets/sdk-regen-runs?status=dead_letter"</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Authorization: Bearer </span><span class="token string variable" style="color:#36acaa">$TOKEN</span><span class="token string" style="color:#e3116c">"</span><br></div></code></pre></div></div>
<ul>
<li class=""><strong>Subscriptions</strong> (apiome-db <strong>V258</strong> <code>sdk_regen_subscriptions</code>) — one per project per ecosystem,
with a <code>deliveryMode</code> of <code>registry</code>, <code>git</code> or <code>registry_and_git</code>, <code>options</code> and <code>active</code>.
<code>deliveryMode</code> has <strong>no default</strong>, because publishing to a public registry is irreversible.
<code>options</code> is a closed vocabulary: <code>dryRun</code> (registry modes only, default <code>false</code>) rehearses every
release, and an unknown option is refused, so a misspelt one can never silently publish. Listing
is <code>projects:view</code>. Subscribing or re-enabling needs <code>projects:edit</code> <strong>and</strong> <code>versions:publish</code>,
because a subscription publishes on the tenant's behalf on every later publish. Disabling or
unsubscribing needs only <code>projects:edit</code>.</li>
<li class=""><strong>The trigger</strong> — the publish route queues a run (<code>sdk_regen_runs</code>) and one pending job per active
subscription (<code>sdk_regen_jobs</code>) in one statement, as a background task that never fails the
publish. A project with no active subscription gains no rows.</li>
<li class=""><strong>The worker</strong> (<code>app.sdk_regen_worker</code>, ticking every <code>APIOME_SDK_REGEN_INTERVAL</code>) calls SDK-4.1's
<code>publish()</code> and SDK-4.2's <code>deliver()</code> <strong>unchanged</strong>, so an automatic release and a manual one are
identical. In <code>registry_and_git</code> mode the pull request is pinned to the version the publish just
claimed. Jobs are claimed with <code>FOR UPDATE SKIP LOCKED</code>, so replicas share the queue. A
subscription's publishes run in order, and one subscription's failure never blocks another's.</li>
<li class=""><strong>Failures go to a dead letter, like webhooks.</strong> Transient failures (a registry 5xx, a GitHub
outage or rate limit) retry with backoff (4 attempts: 60s/5min/30min). Permanent ones (a missing
credential, no delivery target, a registry refusal) are dead-lettered at once with a message that
names the fix. A dead letter fans out an <code>sdk.regen.dead_lettered</code> push-webhook event and is
retried with <code>POST …/sdk-regen-jobs/{jobId}/retry</code> (<code>versions:publish</code>). <strong>A step that succeeded
is never repeated</strong>: a retry after a publish only delivers, and the registry result is written
to the job before the git step starts, so this holds even if the worker dies. A job still running
past <code>APIOME_SDK_REGEN_LEASE_SECONDS</code> is dead-lettered (<code>sdk-regen-worker-lost</code>), never retried
automatically, since it may already have published. A fresh claim token per attempt stops a stale
worker from overwriting a job that was retried since.</li>
<li class=""><strong>History links publish event → jobs → artifacts → deliveries.</strong> Each run lists its jobs, and
each job gives the SDK-4.1 publish run (package, version, archive digest) and the SDK-4.2 delivery
run (pull request), with links to both ledgers and to the published version. Every attempt is
recorded against the runs it wrote.</li>
<li class=""><strong>Unsubscribing stops future runs without touching past artifacts.</strong> Queued jobs for a disabled or
removed subscription are <code>cancelled</code>, and a removed subscription's dead letters can no longer be
retried. Runs, jobs, packages and pull requests already produced are kept (<code>ON DELETE SET NULL</code>).</li>
<li class=""><code>PublishOutcome.retryable</code> (SDK-4.1, in-process only) now carries the registry's own
<code>RegistryUploadError.retryable</code>, mirroring SDK-4.2's <code>DeliveryOutcome.retryable</code>.</li>
<li class="">Settings: <code>APIOME_SDK_REGEN_ENABLED</code> (kill switch), <code>APIOME_SDK_REGEN_INTERVAL</code>,
<code>APIOME_SDK_REGEN_BATCH_SIZE</code>, <code>APIOME_SDK_REGEN_LEASE_SECONDS</code>. No new RBAC resource and no new
credential. Documented in <code>docs/sdk_regen_on_publish.md</code>.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="13190--2026-09-10">1.319.0 — 2026-09-10<a href="http://localhost/release-notes/unreleased#13190--2026-09-10" class="hash-link" aria-label="Direct link to 1.319.0 — 2026-09-10" title="Direct link to 1.319.0 — 2026-09-10" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-14">Added<a href="http://localhost/release-notes/unreleased#added-14" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong>Git delivery, PR mode (<a href="https://github.com/apiome/apiome/issues/4496" target="_blank" rel="noopener noreferrer" class="">#4496</a>, SDK-4.2)</strong> — registry publishing serves an SDK's consumers; git
delivery serves its owners. A regenerated SDK now arrives as a <strong>pull request</strong> against the
tenant's own repository, where their normal review and CI apply.</p>
<div class="language-bash codeBlockContainer_mQmQ theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_t_Hd"><pre tabindex="0" class="prism-code language-bash codeBlock_RMoD thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_AclH"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># Point the project's npm SDK at a repository already registered with Apiome</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-sX</span><span class="token plain"> PUT </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$APIOME</span><span class="token string" style="color:#e3116c">/v1/projects/acme/widgets/sdk-git-delivery-targets/npm"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Authorization: Bearer </span><span class="token string variable" style="color:#36acaa">$TOKEN</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'Content-Type: application/json'</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token parameter variable" style="color:#36acaa">-d</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'{"repositoryId": "5f0c…", "baseBranch": "main", "targetPath": "sdks/typescript"}'</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># Deliver version 1.4.2 — opens (or updates) apiome/sdk-regen-1.4.2-widgets-npm</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-sX</span><span class="token plain"> POST </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$APIOME</span><span class="token string" style="color:#e3116c">/v1/projects/acme/widgets/sdk-git-delivery"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Authorization: Bearer </span><span class="token string variable" style="color:#36acaa">$TOKEN</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'Content-Type: application/json'</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token parameter variable" style="color:#36acaa">-d</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'{"ecosystem": "npm", "version": "1.4.2"}'</span><br></div></code></pre></div></div>
<ul>
<li class=""><strong>No new credential type.</strong> A delivery target (apiome-db V257 <code>sdk_git_delivery_targets</code>: one
per project per ecosystem — repository, base branch, target path) names a repository registered
through a linked GitHub account, and a delivery pushes with that repository's existing
linked-account token. There is no token column in V257. A repository registered from a public
URL, or hosted outside GitHub, is refused with the reason.</li>
<li class=""><strong>The SDK-4.1 package, committed.</strong> The delivery builds the same file list a publish would
upload (<code>app.sdk_publish_pipeline</code> now exposes <code>resolve_release_branding</code>,
<code>resolve_release_series</code> and <code>build_release_distribution</code>), carrying the version the next
registry publish of the series would claim, and commits it under the target path through
GitHub's Git Database API — no clone, no git binary, no token on disk.</li>
<li class=""><strong>A pull request with provenance.</strong> Its body carries the spec version and revision id, the
generator version, a changed-files overview and the provenance table; the commit carries
<code>Apiome-Revision</code> / <code>Apiome-Version-Line</code> / <code>Apiome-Package</code> / <code>Apiome-Generator</code> trailers.</li>
<li class=""><strong>Idempotent per (version, target, options).</strong> The branch is
<code>apiome/sdk-regen-&lt;version&gt;-&lt;project&gt;-&lt;ecosystem&gt;</code> — the ticket's <code>apiome/sdk-regen-&lt;version&gt;</code>
plus the project and ecosystem, so several SDKs can share a repository without overwriting each
other's pull requests. Every delivery is rebuilt on the latest base branch and force-updates the
branch; re-running updates the open pull request (<code>updated</code>), writes nothing when it already
carries this SDK (<code>unchanged</code>) or when the base already contains it (<code>up_to_date</code>), and only
opens one when none is open (<code>opened</code>).</li>
<li class=""><strong>Only generated files are ever removed.</strong> A <code>.apiome/sdk-delivery.json</code> manifest beside the SDK
lists what Apiome generated; the next delivery removes files that list named and the new one
does not, and never touches anything else under the target path.</li>
<li class=""><strong>Failures are runs with actionable logs.</strong> Every attempt is a row in
<code>sdk_git_delivery_runs</code>; a missing or revoked credential, a read-only token, a rejected push, a
refused pull request or a package that cannot be built is a <code>failed</code> run with a stable
<code>errorCode</code>, a message naming the fix, and a step log redacted of the repository token.
<code>POST …/sdk-git-delivery</code> answers <code>200</code> with the run either way.</li>
<li class=""><strong>Shared run log.</strong> SDK-4.1's redacting event log moved to <code>app.sdk_run_log.RunLog</code> (with a
per-pipeline redaction marker) so both release pipelines redact through one implementation.</li>
<li class="">Listing targets is <code>projects:view</code>; saving one is <code>projects:edit</code> <strong>and</strong> <code>imports:edit</code> (a
target decides what Apiome pushes into a repository with that repository's credential);
delivering is <code>versions:publish</code>; history is <code>versions:view</code>. No new RBAC resource. Documented
in <code>docs/sdk_git_delivery.md</code>.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="13180--2026-09-09">1.318.0 — 2026-09-09<a href="http://localhost/release-notes/unreleased#13180--2026-09-09" class="hash-link" aria-label="Direct link to 1.318.0 — 2026-09-09" title="Direct link to 1.318.0 — 2026-09-09" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-15">Added<a href="http://localhost/release-notes/unreleased#added-15" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong>Package publishing pipelines (<a href="https://github.com/apiome/apiome/issues/4495" target="_blank" rel="noopener noreferrer" class="">#4495</a>, SDK-4.1)</strong> — downloading a zip is not how SDKs are
consumed at scale, so a published version can now be released as a package a consumer installs:
<code>npm install @acme/widgets-sdk</code>, <code>pip install acme-widgets</code>.</p>
<div class="language-bash codeBlockContainer_mQmQ theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_t_Hd"><pre tabindex="0" class="prism-code language-bash codeBlock_RMoD thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_AclH"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># Store the workspace's token once (write-only; never returned by any route)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-sX</span><span class="token plain"> PUT </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$APIOME</span><span class="token string" style="color:#e3116c">/v1/tenants/acme/governance/sdk-registry-credentials/npm"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Authorization: Bearer </span><span class="token string variable" style="color:#36acaa">$TOKEN</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'Content-Type: application/json'</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token parameter variable" style="color:#36acaa">-d</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'{"token": "npm_..."}'</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># Validate a release without publishing anything (the default)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-sX</span><span class="token plain"> POST </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$APIOME</span><span class="token string" style="color:#e3116c">/v1/projects/acme/widgets/sdk-publish"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Authorization: Bearer </span><span class="token string variable" style="color:#36acaa">$TOKEN</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'Content-Type: application/json'</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token parameter variable" style="color:#36acaa">-d</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'{"ecosystem": "npm", "version": "1.4.2"}'</span><br></div></code></pre></div></div>
<ul>
<li class=""><strong>Encrypted tenant registry credentials.</strong> npm / PyPI tokens are stored in apiome-db V256
<code>sdk_registry_credentials</code> as <strong>ciphertext only</strong>, sealed with AES-256-GCM envelope encryption
under <code>APIOME_SDK_REGISTRY_CREDENTIAL_ENCRYPTION_KEYS</code>. The scheme MCAT-6.2 already used was
extracted to <code>app.envelope_crypto</code> rather than copied, and the SDK vault seals under its own
magic so a blob cannot be moved between the two even on a shared key. A credential is
<strong>write-only</strong> — every read returns the ecosystem, the registry, the token's <em>public</em> scheme
prefix, its length and a truncated digest, and no route returns a token. A project credential
replaces the workspace one whole (unlike SDK-3.4's settings, a token is atomic), and both
scopes are managed at <code>…/sdk-registry-credentials</code>.</li>
<li class=""><strong>Publish with a dry run.</strong> <code>POST /v1/projects/{t}/{project}/sdk-publish</code> defaults to
<code>dryRun: true</code>: it resolves the branding, resolves <em>and decrypts</em> the credential, computes the
version the next real publish would claim, builds the exact archive that publish would upload
and reports its SHA-256 — and uploads nothing. Because the build is byte-deterministic, the
digest a dry run reports is the digest a publish uploads. Sending <code>dryRun: false</code> is the
deliberate act; a default that published would make a mis-typed request a public release.</li>
<li class=""><strong>Semver from the version line and a regen counter.</strong> The published version is
<code>major.minor.&lt;counter&gt;</code>, where <code>major.minor</code> come from the revision's version line and the
counter is how many releases that line's <em>release series</em> has already had: <code>1.4</code> → <code>1.4.0</code>,
<code>1.4.1</code>, <code>1.4.2</code>…; <code>v3</code> → <code>3.0.0</code>; <code>2026-01-04</code> → <code>2026.1.0</code>. The counter is allocated per
series rather than per line, so <code>1.4.2</code> and <code>1.4.3</code> cannot both claim <code>1.4.0</code>. A prerelease
line stays a prerelease (npm <code>1.5.0-beta.2</code>, PyPI <code>1.5.0b2</code>), and a line with no leading
number (<code>latest</code>, <code>draft</code>) is refused rather than given an invented ordering. The counter is
<strong>derived from the run ledger</strong>, never stored on a project row, and a partial unique index
makes that safe under concurrency — a run is inserted <code>in_progress</code> holding its version before
the upload, and a publish that loses the race takes the next number.</li>
<li class=""><strong>Provenance in the package's own metadata.</strong> <code>package.json</code> carries an <code>apiome</code> object and
PyPI's core metadata carries <code>Project-URL</code> entries naming the <strong>revision id</strong> (not merely the
label — a label can be re-published, a revision cannot), the version line, the release series,
the regen counter, the renderer and the SDK-3.4 settings fingerprint. An installed package
therefore traces back to its spec with nothing but the package manager.</li>
<li class=""><strong>What is in the package.</strong> SDK-4.1's stated dependencies — the two MVP client generators
(<a href="https://github.com/apiome/apiome/issues/4485" target="_blank" rel="noopener noreferrer" class="">#4485</a>/#4486), the generator SPI (<a href="https://github.com/apiome/apiome/issues/4482" target="_blank" rel="noopener noreferrer" class="">#4482</a>) and the SDK-1.1 job service and artifact store
(<a href="https://github.com/apiome/apiome/issues/4481" target="_blank" rel="noopener noreferrer" class="">#4481</a>) this was to be a step on — were all closed <strong>not-planned</strong>, so, following the
SDK-2.3/2.4/2.5/3.3/3.4 precedent, the pipeline publishes what did ship, read straight from
the persisted canonical model: the published contract verbatim, a README, one runnable snippet
per operation, and an importable module exposing the provenance and the spec. A distribution is
assembled from a list of files plus metadata, so a future client generator becomes another
contributor to that list rather than a second pipeline. <code>gomod</code> is deliberately not
publishable — it names a module path, and a Go module is released by pushing a tag (SDK-4.2).</li>
<li class=""><strong>Secrets never reach a log.</strong> The token is read into memory, handed to the transport and
never touched again; every run-log line — including anything the registry said — passes
through exact-substring redaction over the token in play, so even a registry that quotes the
credential it rejected cannot put one in the ledger. Uploads go through the SSRF-guarded
client, so a tenant cannot point a registry URL at an internal address and be handed a token.</li>
<li class=""><strong>History.</strong> <code>GET …/sdk-publish-runs[/{run_id}]</code> lists every attempt, dry runs included, with
the version it claimed, the digest it uploaded and its redacted event log.</li>
<li class="">Credential management is <code>projects:view</code> / <code>projects:edit</code>; publishing — including a dry run —
is <code>versions:publish</code>. No new RBAC resource, for the reason CTG-4.4, CTG-4.5 and SDK-3.4 all
gave. Documented in <code>docs/sdk_package_publishing.md</code>.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="13160--2026-09-08">1.316.0 — 2026-09-08<a href="http://localhost/release-notes/unreleased#13160--2026-09-08" class="hash-link" aria-label="Direct link to 1.316.0 — 2026-09-08" title="Direct link to 1.316.0 — 2026-09-08" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-16">Added<a href="http://localhost/release-notes/unreleased#added-16" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong>Go client generator (<a href="https://github.com/apiome/apiome/issues/4488" target="_blank" rel="noopener noreferrer" class="">#4488</a>, SDK-2.4)</strong> — Go was the most-requested third language for infra
buyers, and the client kit now carries one: a complete, <strong>dependency-free</strong> Go module generated
from the published contract, sitting in the kit's <code>go/</code> directory beside the snippets.</p>
<div class="language-bash codeBlockContainer_mQmQ theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_t_Hd"><pre tabindex="0" class="prism-code language-bash codeBlock_RMoD thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_AclH"><div class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-sO</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-J</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$APIOME</span><span class="token string" style="color:#e3116c">/v1/browse/tenants/</span><span class="token string variable" style="color:#36acaa">$TENANT</span><span class="token string" style="color:#e3116c">/projects/</span><span class="token string variable" style="color:#36acaa">$PROJECT</span><span class="token string" style="color:#e3116c">/versions/1.0.0/sdk/download"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">unzip</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-q</span><span class="token plain"> petstore-1.0.0-sdk.zip </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> </span><span class="token builtin class-name">cd</span><span class="token plain"> go </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> go build ./</span><span class="token punctuation" style="color:#393A34">..</span><span class="token plain">. </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> go vet ./</span><span class="token punctuation" style="color:#393A34">..</span><span class="token plain">.</span><br></div></code></pre></div></div>
<ul>
<li class=""><strong>What it generates.</strong> <code>go.mod</code> (standard library only — a generated SDK that pulls in a
dependency tree is a liability), a <code>net/http</code> transport behind an injectable <code>Doer</code> with
<code>WithHTTPClient</code> / <code>WithBaseURL</code> / <code>WithUserAgent</code> / <code>WithHeader</code> / <code>WithRequestEditor</code>,
exported types for every schema the contract declares, one <code>context.Context</code>-first method per
HTTP operation, auth options derived from the model's security schemes, a typed error per
declared error response, a <code>README.md</code>, and a runnable <code>examples/&lt;group&gt;/main.go</code> per operation
group. The tenant's SDK-3.4 licence header is commented onto every <code>.go</code> file and its
user-agent is baked in as <code>DefaultUserAgent</code>.</li>
<li class=""><strong>Why it generates from the canonical model.</strong> SDK-2.4's stated dependencies — the generator
SPI (<a href="https://github.com/apiome/apiome/issues/4482" target="_blank" rel="noopener noreferrer" class="">#4482</a>) and the codegen preprocessing pass (<a href="https://github.com/apiome/apiome/issues/4483" target="_blank" rel="noopener noreferrer" class="">#4483</a>) — were both closed <strong>not-planned</strong>, as
were the artifact store (<a href="https://github.com/apiome/apiome/issues/4481" target="_blank" rel="noopener noreferrer" class="">#4481</a>), the two MVP language generators (<a href="https://github.com/apiome/apiome/issues/4485" target="_blank" rel="noopener noreferrer" class="">#4485</a>/#4486) and the
dashboard/CLI surfaces (<a href="https://github.com/apiome/apiome/issues/4491" target="_blank" rel="noopener noreferrer" class="">#4491</a>/#4492). Following the precedent SDK-2.3/3.3/3.4 set,
<code>app.go_client_generator</code> is a pure function of the persisted canonical model and its output
rides the existing client kit rather than an artifact store — so the download stays
byte-deterministic and keeps its content-addressed <code>ETag</code>.</li>
<li class=""><strong><code>gomod</code>, a third SDK-3.4 package ecosystem.</strong> A tenant configures the <code>go.mod</code> module path
their consumers <code>go get</code>, with the same tenant → project merge and <code>{tenant}</code>/<code>{project}</code>
tokens as the npm and PyPI patterns. Unconfigured, it falls back to
<code>example.com/&lt;tenant&gt;/&lt;project&gt;-go</code> — <code>example.com</code> is IANA-reserved, so a default module path
can never point at somebody's real repository.</li>
<li class=""><strong>It never takes a download down.</strong> An operation with no HTTP binding gets no method and is
recorded with a reason, exactly as the snippets record theirs; a generator failure degrades the
kit to its snippets and says so in the README and the manifest.</li>
<li class=""><strong>The manifest gained a <code>go_client</code> block</strong> (module path, package name, Go version, method and
type counts, auth options, example groups, per-method coordinates and skips), and the public
SDK info route gained a <code>go_client</code> object so the browse panel can name the module without
paying to generate it.</li>
<li class=""><strong>The compile gate is real.</strong> <code>tests/test_go_client_generator.py</code> runs <code>go build ./...</code>,
<code>go vet ./...</code>, <code>gofmt -l</code> and a <code>go test</code> that drives the generated client against a live
<code>httptest</code> server — path, query, cookie, auth header, user-agent, JSON decoding and the typed
404 over an actual HTTP round trip — whenever a Go toolchain is on <code>PATH</code>, and skips otherwise.</li>
</ul>
</li>
</ul>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="changed-2">Changed<a href="http://localhost/release-notes/unreleased#changed-2" class="hash-link" aria-label="Direct link to Changed" title="Direct link to Changed" translate="no">​</a></h4>
<ul>
<li class=""><strong>Reading a model's auth moved to <code>app.canonical_security</code> (<a href="https://github.com/apiome/apiome/issues/4488" target="_blank" rel="noopener noreferrer" class="">#4488</a>).</strong> The canonical model has no
first-class security field, so importers record auth in two <code>extras</code> shapes that mean different
things: <code>operation.extras["security"]</code> is a per-operation <em>requirement</em>, while
<code>api.extras["inferred_auth_schemes"]</code> is only an <em>observation</em> that the API was seen using a
scheme. Three emitters now need that distinction, so <code>AUTH_SCHEME_HEADERS</code> and both readers live
in one module; <code>app.http_file_emitter</code> and <code>app.llm_tools_emitter</code> delegate to it. Behaviour is
unchanged.</li>
<li class=""><strong>Three <code>app.snippet_render</code> helpers became public</strong> — <code>upper_snake_token</code>, <code>request_message</code> and
<code>pick_content_type</code> — so the Go generator asks the same questions the snippets do rather than
keeping a second copy of the answers. <code>license_comment_block</code> learned the <code>go</code> comment prefix.</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="13150--2026-09-08">1.315.0 — 2026-09-08<a href="http://localhost/release-notes/unreleased#13150--2026-09-08" class="hash-link" aria-label="Direct link to 1.315.0 — 2026-09-08" title="Direct link to 1.315.0 — 2026-09-08" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-17">Added<a href="http://localhost/release-notes/unreleased#added-17" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong>Public "Get SDK" surface (<a href="https://github.com/apiome/apiome/issues/4493" target="_blank" rel="noopener noreferrer" class="">#4493</a>, SDK-3.3)</strong> — the browse portal is where API <em>consumers</em>
land, and until now a published spec page offered them no path to a working client. It now
offers two, both behind one per-project switch that is <strong>off by default</strong>.</p>
<div class="language-bash codeBlockContainer_mQmQ theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_t_Hd"><pre tabindex="0" class="prism-code language-bash codeBlock_RMoD thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_AclH"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># Opt a project in, then take the kit.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-sX</span><span class="token plain"> PUT </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"X-API-Key: </span><span class="token string variable" style="color:#36acaa">$KEY</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'Content-Type: application/json'</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$APIOME</span><span class="token string" style="color:#e3116c">/v1/projects/</span><span class="token string variable" style="color:#36acaa">$TENANT</span><span class="token string" style="color:#e3116c">/</span><span class="token string variable" style="color:#36acaa">$PROJECT</span><span class="token string" style="color:#e3116c">/sdk-settings"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token parameter variable" style="color:#36acaa">-d</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'{"settings":{"publicSdkEnabled":true}}'</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> /dev/null</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-sO</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-J</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$APIOME</span><span class="token string" style="color:#e3116c">/v1/browse/tenants/</span><span class="token string variable" style="color:#36acaa">$TENANT</span><span class="token string" style="color:#e3116c">/projects/</span><span class="token string variable" style="color:#36acaa">$PROJECT</span><span class="token string" style="color:#e3116c">/versions/1.0.0/sdk/download"</span><br></div></code></pre></div></div>
<ul>
<li class="">Two anonymous routes:
<code>GET /v1/browse/tenants/{t}/projects/{p}/versions/{v}/sdk</code> describes what is on offer (the
resolved package names and their install commands, the languages, the operation counts, the
settings fingerprint), and <code>…/sdk/download</code> serves the archive itself as <code>application/zip</code>.
Both share the MFX-7.3 public-export rate limit; the download reuses its size cap.</li>
<li class=""><strong>What the download is.</strong> The original scope served a generated client library from the
SDK-1.1 artifact store. SDK-1.1 (<a href="https://github.com/apiome/apiome/issues/4481" target="_blank" rel="noopener noreferrer" class="">#4481</a>), the generator SPI (<a href="https://github.com/apiome/apiome/issues/4482" target="_blank" rel="noopener noreferrer" class="">#4482</a>), both language generators
(<a href="https://github.com/apiome/apiome/issues/4485" target="_blank" rel="noopener noreferrer" class="">#4485</a>/#4486) and the dashboard/CLI surfaces (<a href="https://github.com/apiome/apiome/issues/4491" target="_blank" rel="noopener noreferrer" class="">#4491</a>/#4492) were closed <strong>not-planned</strong>, so
there is no artifact to serve and no generator to make one. The download is instead an
<code>sdk.client-kit.v1</code> archive built from what <em>did</em> ship — the SDK-2.3 renderer — carrying a
README, the published contract verbatim, and one runnable snippet per operation in
ts/python/curl. It is built <strong>per request</strong>, so there is no artifact lifecycle to retain or
expire.</li>
<li class=""><strong><code>publicSdkEnabled</code>, a new key on the SDK-3.4 <code>sdk.generation-settings.v1</code> body.</strong> It merges
tenant → project key by key like every other setting, so a workspace can open every project at
once and one project can override either way. It is the only setting that is an access control
rather than branding, so it defaults to <code>false</code> rather than <code>null</code> — for a permission, "not
configured" and "not allowed" are the same answer — and an unreadable settings row also reads
as <code>false</code>: <strong>the gate fails closed</strong>. Only a real boolean is accepted; <code>1</code> is refused.</li>
<li class=""><strong>A project that has not opted in gets a 404</strong>, identical to the one an unpublished, private or
unknown version gets. A <code>403</code> would confirm that the project exists and merely declined.</li>
<li class=""><strong>Provenance and determinism.</strong> The archive's <code>manifest.json</code> records the version record id,
version label, source format, renderer, API version and merged settings fingerprint, and
digests every entry. Building twice from the same inputs yields byte-identical archives, which
is what lets an unstored download carry a strong content-addressed <code>ETag</code> (with <code>If-None-Match</code>
→ 304) and a <code>Digest</code> over its exact bytes.</li>
<li class="">Operations with no HTTP binding (gRPC, GraphQL, events) are recorded in the manifest's
<code>skipped</code> list rather than failing the kit, and the work is capped at 250 renderable operations
with <code>truncated</code> reported.</li>
</ul>
</li>
</ul>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="changed-3">Changed<a href="http://localhost/release-notes/unreleased#changed-3" class="hash-link" aria-label="Direct link to Changed" title="Direct link to Changed" translate="no">​</a></h4>
<ul>
<li class=""><strong>The anonymous snippet route is now gated (<a href="https://github.com/apiome/apiome/issues/4493" target="_blank" rel="noopener noreferrer" class="">#4493</a>).</strong>
<code>GET /v1/browse/tenants/{t}/projects/{p}/versions/{v}/snippets/{operation_id}</code> is part of the
same consumer-facing SDK surface as the Get SDK download, so it now answers <strong>404</strong> for a project
whose <code>publicSdkEnabled</code> is not set. Since the setting defaults to off, <strong>public snippet URLs
that worked in 1.314.0 return 404 until a workspace or project owner opts in.</strong> The
<strong>authenticated</strong> snippet route is unchanged — it is tenant-scoped, not public exposure.</li>
<li class=""><strong>Every settings fingerprint changed value.</strong> The canonical settings body carries every key,
including unset ones, so that a fingerprint keeps meaning the same thing across releases; adding
<code>publicSdkEnabled</code> therefore changes the digest the same settings produce. Stored row
fingerprints are untouched until their row is next saved; the ones responses report changed
immediately.</li>
<li class=""><code>ExportSource</code> now carries the revision's captured <code>source_text</code> and <code>source_format</code>, so a bundle
can ship the contract alongside what was derived from it. Additive; existing callers are
unaffected.</li>
<li class="">The deterministic zip-entry writer moved from <code>app.export_job_engine</code> to a shared
<code>app.zip_bundle</code>, so the export bundle and the client kit cannot drift apart on the pinned
timestamp their reproducibility depends on.</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="13140--2026-09-07">1.314.0 — 2026-09-07<a href="http://localhost/release-notes/unreleased#13140--2026-09-07" class="hash-link" aria-label="Direct link to 1.314.0 — 2026-09-07" title="Direct link to 1.314.0 — 2026-09-07" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-18">Added<a href="http://localhost/release-notes/unreleased#added-18" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong>SDK generation settings &amp; branding (<a href="https://github.com/apiome/apiome/issues/4494" target="_blank" rel="noopener noreferrer" class="">#4494</a>, SDK-3.4)</strong> — an organisation wants the code Apiome
hands its consumers to carry the organisation's identity: packages under its own npm scope /
PyPI naming pattern, its licence header on the source, its own user-agent on the traffic those
clients generate. None of that is a property of any version, project row or generated file, so
it now has one durable home and one set of rules.</p>
<div class="language-bash codeBlockContainer_mQmQ theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_t_Hd"><pre tabindex="0" class="prism-code language-bash codeBlock_RMoD thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_AclH"><div class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-sX</span><span class="token plain"> PUT </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"X-API-Key: </span><span class="token string variable" style="color:#36acaa">$KEY</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'Content-Type: application/json'</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$APIOME</span><span class="token string" style="color:#e3116c">/v1/tenants/</span><span class="token string variable" style="color:#36acaa">$TENANT</span><span class="token string" style="color:#e3116c">/governance/sdk-generation-settings"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token parameter variable" style="color:#36acaa">-d</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'{"settings":{"packageNamePatterns":{"npm":"@acme/{project}-sdk"},</span><br></div><div class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">       "userAgent":"acme-sdk/{version}"}}'</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">|</span><span class="token plain"> jq </span><span class="token parameter variable" style="color:#36acaa">-r</span><span class="token plain"> .resolved.packageNames</span><br></div></code></pre></div></div>
<ul>
<li class="">Six endpoints, three per scope:
<code>GET|PUT|DELETE /v1/tenants/{t}/governance/sdk-generation-settings</code> and
<code>GET|PUT|DELETE /v1/projects/{t}/{project}/sdk-settings</code>. A <code>GET</code> never materialises a row —
a scope with nothing saved answers <code>source: "default"</code> — and a <code>DELETE</code> returns the settings
<strong>now</strong> in force rather than a bare <code>204</code>.</li>
<li class=""><strong>The merge is per key, not per row.</strong> A project that overrides only its user-agent still
inherits its tenant's package pattern, and <code>packageNamePatterns</code> merges one ecosystem at a
time. This is the one place SDK-3.4 departs from CTG-4.5's whole-body override, and the reason
a stored body carries only the keys its author named: an <strong>absent</strong> key inherits the next scope
up, an explicit <strong><code>null</code></strong> is deliberately none and blocks that inheritance.</li>
<li class=""><strong>A pattern is validated by being resolved.</strong> <code>@acme/{project}-sdk</code> is not itself a legal npm
name, so patterns are checked by substituting probe values and validating the result. At read
time a package pattern whose tokens the scope cannot fill is <em>omitted</em> rather than resolved
approximately — a package name is an exact identifier, and a nearly-right one is worse than
none. Tokens: <code>{tenant}</code>, <code>{project}</code>, <code>{version}</code>, <code>{year}</code>.</li>
<li class=""><strong>The snippet service applies them.</strong> SDK-2.3's authenticated and anonymous surfaces now stamp
the tenant's user-agent onto the example request and its licence header above the code (as
line comments — a block comment could be terminated from inside by a licence containing <code>*/</code>),
and report both back in a new <code>branding</code> field alongside the resolved package names.
Rendering with no branding is byte-identical to before, which is what keeps FMT-2.4's bulk
request-file emitter — which shares <code>synthesize_request</code> and <code>render_curl</code> — unchanged.</li>
<li class=""><code>projects:view</code> to read, <code>projects:edit</code> to change: no new RBAC resource and no new API-key
scope. Both writes are audited as <code>governance.sdk_generation_settings.update</code> / <code>.clear</code>,
with the licence header recorded by length rather than verbatim.</li>
<li class="">Schema: apiome-db <strong>V255</strong> (<code>sdk_generation_settings</code>, two partial unique indexes, one JSONB
body). Docs: <code>apiome-rest/docs/sdk_generation_settings.md</code>.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="13130--2026-09-07">1.313.0 — 2026-09-07<a href="http://localhost/release-notes/unreleased#13130--2026-09-07" class="hash-link" aria-label="Direct link to 1.313.0 — 2026-09-07" title="Direct link to 1.313.0 — 2026-09-07" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-19">Added<a href="http://localhost/release-notes/unreleased#added-19" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong>Deploy-gating status API (<a href="https://github.com/apiome/apiome/issues/4502" target="_blank" rel="noopener noreferrer" class="">#4502</a>, CTG-4.5)</strong> — a CD pipeline asks one question, <em>can I promote
this API version?</em>, and the answer lived in five places: the GOV lint grade on the revision,
CTG-3.1's breaking classification, CTG-4.2's per-consumer verdicts, CTG-4.3's conformance report
and CTG-4.4's freshness. Every team that wanted a gate scripted its own multi-call version, and
every one of them decided the combination rules differently.</p>
<div class="language-bash codeBlockContainer_mQmQ theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_t_Hd"><pre tabindex="0" class="prism-code language-bash codeBlock_RMoD thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_AclH"><div class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-sH</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"X-API-Key: </span><span class="token string variable" style="color:#36acaa">$KEY</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$APIOME</span><span class="token string" style="color:#e3116c">/v1/projects/</span><span class="token string variable" style="color:#36acaa">$TENANT</span><span class="token string" style="color:#e3116c">/</span><span class="token string variable" style="color:#36acaa">$PROJECT</span><span class="token string" style="color:#e3116c">/gate"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">|</span><span class="token plain"> jq </span><span class="token parameter variable" style="color:#36acaa">-r</span><span class="token plain"> .status</span><br></div></code></pre></div></div>
<p><code>GET /v1/projects/{tenant_slug}/{project_ref}/gate</code> returns <code>pass</code> / <code>warn</code> / <code>fail</code> plus a
per-signal breakdown, each signal carrying a stable reason code, a human sentence, a link to its
evidence, and the facts the judgment was made from. <strong>No analysis was added</strong>: every signal is a
read of something an earlier ticket already stored — the gate never re-lints (<a href="https://github.com/apiome/apiome/issues/5259" target="_blank" rel="noopener noreferrer" class="">#5259</a>'s stored
reports) and never re-diffs (the stored <code>ctg.changelog.v1</code> payload is rehydrated into the
classified diff CTG-4.2's <code>consumer_impact_for_diff</code> already takes, because a changelog entry and
a classified change carry the same fields).</p>
<p><strong>Partial inputs are the normal case, so there are five per-signal statuses rather than three.</strong>
A signal with nothing behind it reports <code>not_configured</code>; one that exists but could not be read
reports <code>unknown</code>. Both are excluded from the verdict — the endpoint has to ship before all four
sources exist everywhere — and both are counted apart, because "nobody has registered a consumer",
"I could not read the registry" and "no consumer is broken" are three different facts.
<code>evaluatedSignals</code> is how a strict pipeline tells an empty gate from a real pass.</p>
<p><strong>The status is always 200.</strong> A <code>409</code> on a failing build would make a network fault and a breaking
change look identical to a pipeline's error handling; the verdict is in the body and the caller
owns the exit code, exactly as the CLX-4.2 lint gate does.</p>
<p><strong>Thresholds are two-rung and tenant- or project-scoped.</strong> Each signal carries a warn threshold
and a fail threshold, either of which may be <code>null</code> to disable that rung, which is what lets one
vocabulary come out of four very different measurements without a per-signal "action" dial:
<em>warn below B, fail below D</em> says the whole lint policy in five words. A rung that could never
fire is refused with a <code>422</code> listing every problem at once.
<code>GET|PUT|DELETE /v1/tenants/{tenant}/governance/deploy-gate-policy</code> and the per-project
<code>…/gate/policy</code> configure it, resolving project → tenant → documented default, with <code>policy.source</code>
on every response saying which one applied. Unlike the other policy surfaces these rows are
mutable rather than append-only — the gate stores no verdict, so there is no past judgment for a
version history to explain; attribution lives in <code>access_audit</code>.</p>
<p>Unlike every other policy default in the platform, <strong>this one has teeth</strong>: it fails on a breaking
change and on a broken consumer, and warns on a lint grade below B or a verification older than a
day. The advisory defaults elsewhere exist because those gates hang off flows that already existed
and a blocking default would break people who never asked for one; nothing hangs off this one.</p>
<p><strong>Every signal fails soft, on its own.</strong> A store that is unreachable or a payload this release
cannot parse costs that signal — <code>unknown</code>, reason <code>signal-unavailable</code> — and nothing else. A gate
that returned <code>500</code> because one of four inputs was briefly unavailable would stop every pipeline in
the tenant. The policy read degrades the same way, flagging <code>policy.degraded</code> so "nothing is
configured" stays distinguishable from "I could not read what is".</p>
<p>A schedule is matched to the gated revision however its reference was spelled (<code>…/1.2.0</code>,
<code>…/&lt;uuid&gt;</code>, <code>…/latest</code>); across several, freshness is the most recent clean run and the status is
the worst, so a failing staging check is never averaged away by a passing production one. When
nothing schedules the version, the signal falls back to a manual CTG-4.3 report, read by resolved
artifact coordinates — and a failing report never becomes the freshness anchor.</p>
<p><strong>No new RBAC resource and no new API-key scope.</strong> Reading a gate is <code>versions:view</code> (what a CI
runner resolves to); moving the bar is <code>verification_targets:edit</code>, the same class of decision
V211 already keeps out of an Editor's hands. The consumer signal is gated inside the response on
<code>consumer_contracts:view</code>, reported <code>unknown</code> rather than leaked or silently passed. The gate is
allowlisted for <em>either</em> CTG-2.3 CI read scope (<code>diff:read</code> or <code>lint:read</code>), since it aggregates
inputs both of those already grant.</p>
<p>apiome-db <strong>V254</strong> adds <code>deploy_gate_policy</code> (one row per scope, two partial unique indexes) plus
the artifact index the report fallback reads. See <code>docs/deploy_gate.md</code>.</p>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="13110--2026-09-07">1.311.0 — 2026-09-07<a href="http://localhost/release-notes/unreleased#13110--2026-09-07" class="hash-link" aria-label="Direct link to 1.311.0 — 2026-09-07" title="Direct link to 1.311.0 — 2026-09-07" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-20">Added<a href="http://localhost/release-notes/unreleased#added-20" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong>Consumer-aware breaking analysis (<a href="https://github.com/apiome/apiome/issues/4480" target="_blank" rel="noopener noreferrer" class="">#4480</a>, CTG-4.2)</strong> — "breaking" is a claim about somebody.
A whole-spec verdict ("47 breaking changes") forces a provider to treat every change as an
incident and tells a consumer nothing. <code>POST /v1/diff/{tenant}/classified</code> with
<code>consumers: true</code> now intersects the CTG-1.1 classified changes with what each registered
consumer declared it uses (CTG-4.1) and answers the question that actually gates a release:</p>
<div class="language-text codeBlockContainer_mQmQ theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_t_Hd"><pre tabindex="0" class="prism-code language-text codeBlock_RMoD thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_AclH"><div class="token-line" style="color:#393A34"><span class="token plain">breaks 2 of 7 consumers: billing-service, mobile-app</span><br></div></code></pre></div></div>
<p>Nothing was added to the database. A verdict is derived from a stored contract and a diff, and
both already existed; what was missing was the join. <code>app.consumer_impact</code> is pure (pointer
algebra, attribution rules, markdown), <code>app.consumer_impact_service</code> is the one place the
registry is read for an analysis, and CTG-4.5's deploy gate calls the same seam.</p>
<p><strong>The exclusion is the point.</strong> A change inside a body schema, on an operation whose consumer
declared which fields it reads, attributes only when one of those fields is met — removing a
field nobody reads breaks nobody. Two exceptions keep that from under-reporting: an operation
declared with no fields is operation-wide throughout (nothing finer was declared, so nothing
finer is claimed), and a <code>/parameters/…</code> change is always operation-wide, because a newly
required query parameter breaks every caller whether or not they declared it. Root <code>security</code>,
<code>servers</code> and <code>securitySchemes</code> changes reach every declared consumer.</p>
<p>Pointer overlap is <strong>segment-aware</strong>, unlike the <code>starts_with</code> narrowing query behind
<code>db.find_consumer_contracts_by_pointers</code>: <code>/components/schemas/Pet</code> no longer "touches"
<code>/components/schemas/PetFood</code>. The SQL only narrows the candidate set; this renders the verdict.</p>
<p><strong>The denominator is honest.</strong> A consumer registered without a current contract cannot be
counted as safe, so it is reported beside the fraction (<code>2 registered consumers have declared no surface</code>) with verdict <code>undeclared</code>, never inside it. Each verdict also carries
<code>contractMatchesBase</code>, so a surface resolved against a different revision is flagged rather
than silently equated.</p>
<p>Changes touching nobody stay globally classified and are flagged: each change gains a
<code>consumers</code> array (<code>[]</code> = no registered consumer affected, <code>null</code> = not analysed), and the
report's <code>attribution</code> list is exact even when a consumer's <code>impacts</code> enumeration is capped.</p>
<p>Markdown responses gain a <strong>Consumer impact</strong> section, and <code>apiome diff --consumers</code> prints
<code>breaks billing-service</code> lines in text, JSON and markdown. The CI <strong>exit code is unchanged</strong> —
<code>--fail-on</code> still grades the whole specification, so a build never passes merely because nobody
has registered as a consumer yet.</p>
<p>See <code>apiome-rest/docs/consumer_impact.md</code>.</p>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="13100--2026-09-07">1.310.0 — 2026-09-07<a href="http://localhost/release-notes/unreleased#13100--2026-09-07" class="hash-link" aria-label="Direct link to 1.310.0 — 2026-09-07" title="Direct link to 1.310.0 — 2026-09-07" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-21">Added<a href="http://localhost/release-notes/unreleased#added-21" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong>Provider verification against a live deployment (<a href="https://github.com/apiome/apiome/issues/4489" target="_blank" rel="noopener noreferrer" class="">#4489</a>, CTG-4.3)</strong> — a published specification
can be perfectly versioned and still lie. <code>POST /v1/tenants/{tenant}/contracts/{version_ref}/ verify-provider</code> executes the version's compiled contract suite against a registered deployment
and returns a <strong>conformance report</strong>: per-operation verdicts, every schema violation located by
its JSON Pointer into the response body, and coverage measured against every operation the
specification declares.</p>
<p>Nothing here re-implements execution. ECA-1.1 compiles the requests, ECA-1.2 holds the target and
its credential <em>reference</em>, ECA-2.1 sends them and validates responses, ECA-1.3 stores the run.
What was missing was the judgment: coverage had no denominator (a run recorded the cases it
executed, never the operations it did not reach), drift had no location (the validator's JSON
Pointers were computed and then discarded), mutation was an all-or-nothing target flag, and there
was no report object a deploy gate could fetch.</p>
<p><strong>Safe by default.</strong> Only <code>GET</code>/<code>HEAD</code>/<code>OPTIONS</code> are sent. A mutating case runs only when the
target policy permits mutation, <em>and</em> the run opts in, <em>and</em> a tenant-supplied fixture names the
operation — because "you may write" and "here is the row you may write to" are different
permissions. A run can narrow what the registry permits and never widen it; a fixture supplies the
request but never the expectation, so it cannot make a failing case pass; a fixture carrying a
credential header is refused; and a fixture never overwrites a negative case's body or restores
the parameter that case exists to omit. A fixture that matches nothing is reported, not dropped.</p>
<p><strong>Coverage is honest.</strong> The denominator counts operations the suite compiler could not compile,
so a coverage number cannot be inflated by a document half of which was skipped, and every
operation the report does not vouch for is named with its reason.</p>
<p>Reports are persisted write-once beside their evidence (apiome-db <strong>V252</strong>,
<code>provider_verification_report</code>) with the coverage numbers and verdict as indexed columns, so
CTG-4.4 (scheduled verification) and CTG-4.5 (deploy gating) can ask "is the newest report for
this version passing?" without parsing a report body. No new RBAC resource: a conformance report
<em>is</em> verification evidence. See <code>docs/provider_verification.md</code>.</p>
</li>
</ul>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="changed-4">Changed<a href="http://localhost/release-notes/unreleased#changed-4" class="hash-link" aria-label="Direct link to Changed" title="Direct link to Changed" translate="no">​</a></h4>
<ul>
<li class=""><code>app/contract_runner.py</code> (ECA-2.1) now records <strong>one assertion per located schema violation</strong>
alongside the existing headline verdict, with the instance JSON Pointer as the assertion subject,
and accepts an optional per-case <code>decisions</code> map so a caller can hold a case back or substitute a
request without reimplementing the runner. Both are additive: omitting <code>decisions</code> is exactly the
previous behaviour, and the headline assertion's shape is unchanged.</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="13080--2026-08-31">1.308.0 — 2026-08-31<a href="http://localhost/release-notes/unreleased#13080--2026-08-31" class="hash-link" aria-label="Direct link to 1.308.0 — 2026-08-31" title="Direct link to 1.308.0 — 2026-08-31" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="changed-5">Changed<a href="http://localhost/release-notes/unreleased#changed-5" class="hash-link" aria-label="Direct link to Changed" title="Direct link to Changed" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong>There is now one mock engine (<a href="https://github.com/apiome/apiome/issues/5532" target="_blank" rel="noopener noreferrer" class="">#5532</a>, MSC-2.2)</strong> — the mock implementation that lived inside
apiome-rest is deleted, and every mock request is resolved by apiome-mock.</p>
<p>There were two. This one served <code>/v1/mock/{id}/…</code> — the short-lived sandbox instances the hosted
Mock Server (<a href="https://github.com/apiome/apiome/issues/3615" target="_blank" rel="noopener noreferrer" class="">#3615</a>) and the Export Studio's test drive (MFX-44.5) provision — from
<code>mock_instances.config</code>, a <em>list</em> of scenarios with <code>rules</code>. apiome-mock served
<code>/{tenant}/{project}/{version}/…</code> and portable bundles from <code>versions.mock_settings</code>, a <em>dict
keyed by scenario name</em>. They shared three routing symbols and reimplemented everything
downstream, so a feature built in one was invisible in the other: sandboxes had no templates, no
match predicates, no stateful CRUD, no fixture packs and no chaos, and every future mock feature
would have had to be written twice.</p>
<p>A sandbox request now takes one internal hop. apiome-rest keeps what a <em>sandbox</em> is — does the
instance exist, has it expired, is the caller inside its rate limit, what goes in the request
log — builds the instance's portable mock bundle, and asks apiome-mock's new <code>/__sandbox__</code>
endpoint to serve it through <code>serve_compiled_request</code>, the same function the hosted data plane
and the portable runtime call. Sandboxes gain every feature they lacked as a side effect.</p>
<p>There is deliberately <strong>no local fallback</strong>: a deployment that has not configured
<code>APIOME_MOCK_INTERNAL_BASE_URL</code> / <code>APIOME_MOCK_INTERNAL_TOKEN</code> answers <code>503</code> on the data plane
and says which switch is missing, rather than inventing an answer from a second engine.</p>
</li>
<li class="">
<p><strong>The built-in scenarios still resolve, everywhere (<a href="https://github.com/apiome/apiome/issues/5532" target="_blank" rel="noopener noreferrer" class="">#5532</a>)</strong> — <code>happy-path</code>, <code>server-error</code>,
<code>not-found</code> and <code>slow</code> are written into client code by name, so all four are now defined on
<em>every</em> version, supplied by the runtime and merged into whatever the version stores. A stored
scenario of the same name wins outright, exactly as the retired engine's <code>normalize_scenarios</code>
resolved the same collision.</p>
</li>
</ul>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-22">Added<a href="http://localhost/release-notes/unreleased#added-22" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong>Two additions to the scenario schema (<a href="https://github.com/apiome/apiome/issues/5532" target="_blank" rel="noopener noreferrer" class="">#5532</a>)</strong> — both needed to express the built-ins and the
migrated rules, and both available to any authored scenario:</p>
<ul>
<li class="">the operation key <code>"*"</code>, which applies to every operation a scenario does not name explicitly
(an exact key always wins). Its canned bodies are not checked against the spec at save time,
because a wildcard covers operations with different response schemas; status, media type,
headers and template syntax still are;</li>
<li class="">an override's <code>status</code>, which pins the response status but leaves the <strong>body to the spec</strong>,
resolved as a request sending <code>?__status=</code> resolves it. A canned response with no body would
serve an empty one, and freezing a synthesized body into settings would stop it tracking the
spec.</li>
</ul>
</li>
<li class="">
<p><strong><code>migrationNotes</code> on a mock instance (<a href="https://github.com/apiome/apiome/issues/5532" target="_blank" rel="noopener noreferrer" class="">#5532</a>)</strong> — every rule in a pre-fold <code>config</code> that could
not be translated is reported on the instance and stored in <code>mock_instances.migration_notes</code>:
one that matched no operation in the frozen spec, one an earlier rule had already made
unreachable, one that set nothing, or one whose latency was clamped to the 30 s ceiling. The
acceptance criterion is that untranslatable rules are reported, never silently dropped.</p>
</li>
</ul>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="migration">Migration<a href="http://localhost/release-notes/unreleased#migration" class="hash-link" aria-label="Direct link to Migration" title="Direct link to Migration" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong><code>V250__mock_instance_engine_fold_5532.sql</code></strong> adds <code>mock_instances.settings</code> (the
apiome-mock-shaped configuration an instance is served from) and <code>migration_notes</code>. <code>settings</code> is
NULL until an instance is folded; either plane folds a row the first time it reads one, so no
maintenance window is required. <code>apiome-rest/scripts/fold_mock_instance_configs.py</code> folds the
whole estate at once and prints the report (<code>--dry-run</code> translates without writing).</p>
<p>The translation (<code>app.mock_instance_config</code>) is spec-aware, because three legacy behaviours
cannot be reproduced from the stored shapes alone: a body-only rule served the operation's <em>own</em>
default success status; precedence was first-matching-rule-wins per operation, which inverts in
the keyed shape where an exact key beats the wildcard; and a rule matching nothing in the frozen
spec did nothing. <code>active_scenario</code> lands on <code>activeScenario</code>, the key <a href="https://github.com/apiome/apiome/issues/5531" target="_blank" rel="noopener noreferrer" class="">#5531</a> introduced — the two
spellings of one concept are now one. The legacy <code>config</code> column is kept, unread, as the pre-fold
record a migrated instance is diffed against.</p>
</li>
</ul>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="removed">Removed<a href="http://localhost/release-notes/unreleased#removed" class="hash-link" aria-label="Direct link to Removed" title="Direct link to Removed" translate="no">​</a></h4>
<ul>
<li class=""><strong><code>app.mock_engine</code> and <code>app.mock_data_generator</code> (<a href="https://github.com/apiome/apiome/issues/5532" target="_blank" rel="noopener noreferrer" class="">#5532</a>)</strong> — the retired engine's resolver
(<code>resolve_response</code>, <code>normalize_scenarios</code>, <code>resolve_active_scenario_name</code>, <code>BUILTIN_SCENARIOS</code>)
and its data generator, superseded by <code>apiome_mock</code>'s resolver and its format-aware
<code>schema_synthesizer</code>. What both packages genuinely shared — <code>MockOperation</code>,
<code>extract_operations</code>, <code>match_operation</code> and the path-template compiler — moved to
<code>app.mock_routing</code>, whose name says that it is routing and not an engine. The one piece of
<code>mock_data_generator</code> with no runtime counterpart, <code>validate_value</code>, moved to
<code>app.mock_schema_validation</code>.</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="13070--2026-08-31">1.307.0 — 2026-08-31<a href="http://localhost/release-notes/unreleased#13070--2026-08-31" class="hash-link" aria-label="Direct link to 1.307.0 — 2026-08-31" title="Direct link to 1.307.0 — 2026-08-31" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-23">Added<a href="http://localhost/release-notes/unreleased#added-23" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong>The version's active scenario is now a mock setting the hosted runtime honours (<a href="https://github.com/apiome/apiome/issues/5531" target="_blank" rel="noopener noreferrer" class="">#5531</a>,
MSC-2.1)</strong> — <code>GET</code>/<code>PUT .../mock/scenarios</code> carry <code>activeScenario</code>, the scenario the mock serves
when a request sends no <code>X-Mock-Scenario</code> header. It is stored in <code>versions.mock_settings</code>
alongside the scenarios it names, travels inside a portable mock bundle, and is a section of the
MSC-1.4 configuration document.</p>
<p>Until now the control plane could store and switch an active scenario that nothing in the hosted
data plane read: <code>grep -rn "active_scenario" apiome-mock/src</code> returned nothing, and the runtime
selected a scenario from the request header alone. Switching a mock to <code>server-error</code> and then
calling the hosted URL returned happy-path responses, with nothing to indicate why. A shipped
control that changes nothing is a correctness problem, not a nicety.</p>
<p>The precedence is explicit: <strong>request header → stored <code>activeScenario</code> → no scenario</strong>. The
header stays an outright override, and a version with no stored value behaves byte-identically
to before. Every response served while a scenario is in effect now names it in the
<code>X-Mock-Scenario</code> response header — including responses for operations the scenario does not
override, because a caller who sent no header cannot otherwise tell what answered them.</p>
<p><code>activeScenario</code> must name one of the scenarios saved with it, so a version can never store a
default that means nothing; the runtime, by contrast, is lenient with a stored name that stops
resolving (a renamed or deleted scenario) — it logs <code>mock_active_scenario_unknown</code> and serves the
default flow, because an unresolvable default must never take a serving mock down.</p>
<p>Unlike <code>scenarios</code> and <code>chaos</code>, the field is <em>preserved</em> when a <code>PUT</code> omits it and cleared only
when it is sent as <code>null</code>, so an editor written before the field existed cannot silently switch a
version's mock back to its default flow.</p>
</li>
</ul>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="changed-6">Changed<a href="http://localhost/release-notes/unreleased#changed-6" class="hash-link" aria-label="Direct link to Changed" title="Direct link to Changed" translate="no">​</a></h4>
<ul>
<li class=""><strong>Mock bundles carry <code>activeScenario</code> (<a href="https://github.com/apiome/apiome/issues/5531" target="_blank" rel="noopener noreferrer" class="">#5531</a>)</strong> — a bundle exported from a version defaults to
the same scenario the hosted mock does, asserted response-by-response by the PMR-3.1 parity
harness.</li>
<li class=""><strong>The MSC-1.2 preview trace reports <code>scenarioSource</code></strong> — <code>"header"</code> or <code>"config"</code> — so a preview
says <em>why</em> a scenario applied, not merely that one did.</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="13060--2026-08-29">1.306.0 — 2026-08-29<a href="http://localhost/release-notes/unreleased#13060--2026-08-29" class="hash-link" aria-label="Direct link to 1.306.0 — 2026-08-29" title="Direct link to 1.306.0 — 2026-08-29" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-24">Added<a href="http://localhost/release-notes/unreleased#added-24" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong><code>?dryRun=true</code> on the version mock-settings write routes (<a href="https://github.com/apiome/apiome/issues/5530" target="_blank" rel="noopener noreferrer" class="">#5530</a>, MSC-1.4)</strong> — <code>PUT .../mock/scenarios</code>, <code>PUT .../mock/correlation</code> and <code>PUT .../mock/fixture-packs</code> accept
<code>?dryRun=true</code>: the same validation runs, the response describes what <em>would</em> be stored, and
nothing is written.</p>
<p>It exists so that a mock configuration held as a file — <code>apiome mock config push --dry-run</code> —
can be checked in CI against the <em>authoritative</em> validators rather than a second copy of them,
and so that a real push can validate every section before any of them is stored. Without it a
document whose scenarios are valid and whose correlation block is not would leave a version
half-configured, and would report only the first problem rather than all of them.</p>
<p>The dry-run response is built by the very readers the write path reports with, so it cannot
describe an outcome the write would not produce — operation keys normalize the same way, fixture
packs gain the same format defaults, and a cleared correlation block reports as <code>null</code>. It
reports validation, not ownership: the creator/administrator gate lives in the write itself and
cannot be exercised without writing.</p>
<p>The parameter defaults to off, so every existing caller is unaffected.</p>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="13050--2026-08-28">1.305.0 — 2026-08-28<a href="http://localhost/release-notes/unreleased#13050--2026-08-28" class="hash-link" aria-label="Direct link to 1.305.0 — 2026-08-28" title="Direct link to 1.305.0 — 2026-08-28" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-25">Added<a href="http://localhost/release-notes/unreleased#added-25" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong>Mock authoring catalogue endpoint (<a href="https://github.com/apiome/apiome/issues/5529" target="_blank" rel="noopener noreferrer" class="">#5529</a>, MSC-1.3)</strong> — <code>GET /v1/versions/{tenant_slug}/{project_id}/{version_record_id}/mock/operations</code> describes a version
the way a mock editor needs it: every operation it has, with its path, query and header
parameters (each carrying the ready-to-insert <code>{{request.*}}</code> expression), the JSON Pointers the
success response body actually has, the top-level request-body field names, and the fixture names
<code>{{fixture.&lt;name&gt;}}</code> can read on this version.</p>
<p>The part that makes response correlation trustworthy is <code>bindings</code>: <strong>which response properties
the <code>path-params</code> and <code>inferred</code> passes would bind, and to what</strong>, projected before anything is
saved. Those are not a second implementation of inference — the name-matching rules moved into
the new <code>app.mock_correlation_rules</code>, which <code>apiome_mock.correlation</code> now imports, the same call
<code>app.mock_match</code> and <code>app.mock_template</code> already made. So the editor's preview cannot promise a
binding the runtime declines to make.</p>
<p>Two limits follow from projecting over a response <em>schema</em> rather than a rendered body, and are
reported rather than hidden: a pointer inside an array names member <code>0</code> and is flagged
<code>repeated</code> (the runtime binds every member), and a <code>oneOf</code>/<code>anyOf</code> schema is projected through
its first branch. The walk is bounded — depth 6, 200 pointers per operation, and a <code>$ref</code> cycle
guard — so a recursive schema terminates.</p>
<p>Read-only and cheap: it generates the version's OpenAPI document and walks it, needs only
<code>versions:view</code>, writes nothing, and answers for a version whose mock is switched <strong>off</strong> —
which is when correlation is usually configured for the first time.</p>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="13040--2026-08-28">1.304.0 — 2026-08-28<a href="http://localhost/release-notes/unreleased#13040--2026-08-28" class="hash-link" aria-label="Direct link to 1.304.0 — 2026-08-28" title="Direct link to 1.304.0 — 2026-08-28" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-26">Added<a href="http://localhost/release-notes/unreleased#added-26" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong>Mock response preview endpoint (<a href="https://github.com/apiome/apiome/issues/5528" target="_blank" rel="noopener noreferrer" class="">#5528</a>, MSC-1.2)</strong> — <code>POST /v1/versions/{tenant_slug}/{project_id}/{version_record_id}/mock/preview</code> answers the question a
mock author actually has: <em>given this request, what does the mock return?</em> Send a synthetic
request (<code>method</code>, <code>path</code>, <code>headers</code>, <code>query</code>, <code>body</code>, plus <code>scenario</code>/<code>seed</code> shorthands) and get
back the status, headers, media type and body the mock would serve — with no mock enabled, no
instance provisioned, and no live request.</p>
<p>Every response carries a <strong>decision trace</strong> naming which layer produced the body: <code>scenario</code>
(with the matched rule's zero-based index), <code>stateful</code>, <code>correlation</code> (with the mode and the
JSON Pointers it bound), <code>example</code> or <code>synthesis</code> — plus <code>forced-status</code>, <code>request-invalid</code>,
<code>no-operation</code>, <code>method-not-allowed</code>, <code>unknown-scenario</code>, <code>not-acceptable</code> and <code>template-limit</code>
for the paths that produce no ordinary body. Without it an author can see <em>that</em> a value
appeared but not <em>why</em>, which is most of the value of a preview.</p>
<p>The render is <strong>not</strong> re-implemented here. REST authenticates and authorizes the caller, builds
the version's portable mock bundle, and asks apiome-mock's internal <code>/__preview__</code> endpoint to
render it through <code>serve_compiled_request</code> — the same function its data plane and the portable
runtime call. A preview therefore cannot disagree with the served response, and apiome-mock's
suite asserts exactly that by rendering one correlated request both ways.</p>
<p>An optional <code>settings</code> override previews an <strong>unsaved draft</strong>: it overlays the stored settings
per key (scenarios, chaos, fixture packs, callbacks, correlation), is canonicalized and validated
with the same rules its save route applies — so a draft that could never be saved is a 422 rather
than a silently ignored no-op — and persists nothing. It requires <code>versions:edit</code>; previewing the
stored settings needs only <code>versions:view</code>.</p>
<p>Preview never writes: session state lives and dies inside the render, no callback is delivered,
and no usage, audit or provisioning row is touched. Chaos is <em>reported</em> rather than applied, so a
preview never sleeps for a configured latency or randomly answers 500. Renders are rate limited
per version (<code>APIOME_MOCK_PREVIEW_RATE_LIMIT_PER_MINUTE</code>, default 120) and the synthetic body is
capped (<code>APIOME_MOCK_PREVIEW_MAX_BODY_BYTES</code>, default 256 KiB).</p>
<p>Configuration: <code>APIOME_MOCK_INTERNAL_BASE_URL</code> (the mock service's <strong>internal</strong> address) and
<code>APIOME_MOCK_INTERNAL_TOKEN</code> (shared with apiome-mock). Both are required — with either unset the
endpoint fails closed with 503. The token is deliberately not <code>INTERNAL_SERVICE_TOKEN</code>:
rendering a preview should not carry the secret that unseals auth-provider credentials. See
<a class="" href="http://localhost/reference/mock-runtime/mock-response-preview">docs/guide/mock-response-preview.md</a>.</p>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="13030--2026-08-28">1.303.0 — 2026-08-28<a href="http://localhost/release-notes/unreleased#13030--2026-08-28" class="hash-link" aria-label="Direct link to 1.303.0 — 2026-08-28" title="Direct link to 1.303.0 — 2026-08-28" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-27">Added<a href="http://localhost/release-notes/unreleased#added-27" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong>Request-correlated mock responses (<a href="https://github.com/apiome/apiome/issues/5527" target="_blank" rel="noopener noreferrer" class="">#5527</a>, MSC-1.1)</strong> — <code>GET</code>/<code>PUT /v1/versions/{tenant_slug}/{project_id}/{version_record_id}/mock/correlation</code> read and write a
<code>responseCorrelation</code> block in <code>versions.mock_settings</code>, alongside the existing <code>scenarios</code>,
<code>chaos</code>, <code>fixturePacks</code> and <code>callbacks</code> keys. The block makes the mock's <strong>default</strong> response
path answer with the request's own values — with no request header, so a generated SDK or a
browser app gets it too, where scenario overrides (<code>X-Mock-Scenario</code>) and stateful CRUD
(<code>X-Mock-Session</code>) could never reach.</p>
<p>Four modes: <code>off</code> (the default — byte-identical to today), <code>path-params</code> (a response property
named after a path parameter takes the request's value, at every depth and inside array
members), <code>inferred</code> (that plus echoing request-body fields back on <code>POST</code>/<code>PUT</code>/<code>PATCH</code>, while
<code>id</code>/<code>createdAt</code>/<code>updatedAt</code> and anything absent from the request stay synthesized), and
<code>explicit</code> (only a per-operation map of response JSON Pointer to template expression). The
pointer map applies in every mode except <code>off</code> and always wins for the pointer it names.</p>
<p>Expressions are the existing bounded <code>{{ … }}</code> language, validated on save with
<code>validate_template_value</code>, so a bad expression is a 422 rather than a serve-time surprise;
operation keys must name a real operation in the version's generated OpenAPI document; bindings
saved with <code>mode: "off"</code> are refused rather than silently ignored. Limits: 200 operations, 50
pointers each, 64 KiB. <code>responseCorrelation</code> is a bundled settings key, so a portable bundle
correlates identically offline (the PMR-3.1 parity harness asserts it). See
<a class="" href="http://localhost/reference/mock-runtime/mock-response-correlation">docs/guide/mock-response-correlation.md</a>.</p>
</li>
</ul>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="removed-1">Removed<a href="http://localhost/release-notes/unreleased#removed-1" class="hash-link" aria-label="Direct link to Removed" title="Direct link to Removed" translate="no">​</a></h4>
<ul>
<li class=""><strong>The dead <code>mock_settings.fixtures</code> reader (<a href="https://github.com/apiome/apiome/issues/5527" target="_blank" rel="noopener noreferrer" class="">#5527</a>)</strong> — <code>apiome_mock.fixture_data.parse_fixtures</code>
read a flat fixtures map that nothing in apiome-rest ever wrote; only fixture <em>packs</em> are wired.
A configuration surface that silently did nothing was dropped rather than given a writer.
Hosted template fixture data comes from <code>fixturePacks</code> alone; the bundle path is unchanged.</li>
</ul>]]></content>
        <author>
            <name>The Apiome team</name>
            <uri>https://github.com/apiome/apiome</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Admin & tools, documented as they exist today]]></title>
        <id>http://localhost/release-notes/admin-pages</id>
        <link href="http://localhost/release-notes/admin-pages"/>
        <updated>2026-10-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[The admin console, data browser, migrations and an Operating Apiome index for operators.]]></summary>
        <content type="html"><![CDATA[<p>The <a class="" href="http://localhost/admin">Admin &amp; tools</a> group now covers every operator screen —
<a class="" href="http://localhost/admin/admin-console">the admin console</a>, <a class="" href="http://localhost/admin/users">Users</a>, <a class="" href="http://localhost/admin/admin-tenants">Tenants</a>,
<a class="" href="http://localhost/admin/licenses">Licenses</a>, <a class="" href="http://localhost/admin/feature-flags">Feature flags</a>,
<a class="" href="http://localhost/admin/auth-providers">Sign-in providers</a>, <a class="" href="http://localhost/admin/property-templates">Property templates</a>, the
<a class="" href="http://localhost/admin/data-browser">Data browser</a> and <a class="" href="http://localhost/admin/migrations">Migrations</a> — and
<a class="" href="http://localhost/admin/operating-apiome">Operating Apiome</a> links the runbooks, the environment reference and the
Compose stack. (<a href="https://github.com/apiome/apiome/issues/5627" target="_blank" rel="noopener noreferrer" class="">#5627</a>)</p>
<!-- -->
<ul>
<li class=""><strong>Marked as legacy</strong> — these screens predate the Hive redesign, so each page says so and links
<a href="https://github.com/apiome/apiome/issues/5272" target="_blank" rel="noopener noreferrer" class="">#5272</a>.</li>
<li class=""><strong>Screenshot manifest</strong> — entries can be tagged <code>legacy</code>; the weekly screenshot refresh lists the
legacy screens that changed, and <code>docs:check</code> requires the callout on any page that shows one.</li>
</ul>]]></content>
        <author>
            <name>The Apiome team</name>
            <uri>https://github.com/apiome/apiome</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Bring in, documented page by page]]></title>
        <id>http://localhost/release-notes/bring-in-pages</id>
        <link href="http://localhost/release-notes/bring-in-pages"/>
        <updated>2026-10-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Catalog, the import wizards, Repositories, MCP servers and Agent access — with screenshots.]]></summary>
        <content type="html"><![CDATA[<p>The <a class="" href="http://localhost/bring-in">Bring in</a> group now has a page for every screen in it — the <a class="" href="http://localhost/bring-in/catalog">Catalog</a>
and each item's tabs, both <a class="" href="http://localhost/bring-in/import-wizard">import wizards</a> with a worked example per
source, <a class="" href="http://localhost/bring-in/repositories">Repositories</a>, <a class="" href="http://localhost/bring-in/mcp-servers">MCP servers</a> and
<a class="" href="http://localhost/bring-in/agent-access">Agent access</a>. (<a href="https://github.com/apiome/apiome/issues/5623" target="_blank" rel="noopener noreferrer" class="">#5623</a>)</p>
<!-- -->
<ul>
<li class=""><strong>Sample data for every source</strong> — the files under <code>apiome-ui/examples/</code>, and Apiome's own MCP
server for the MCP example.</li>
<li class=""><strong>Corrected:</strong> <a class="" href="http://localhost/getting-started/first-project">Import your first project</a> no longer says the
Catalog opens the same wizard, and its screenshot caption names the sources the Projects importer
offers.</li>
<li class=""><strong>Every page links its REST endpoints and CLI commands.</strong></li>
</ul>]]></content>
        <author>
            <name>The Apiome team</name>
            <uri>https://github.com/apiome/apiome</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Build, documented page by page]]></title>
        <id>http://localhost/release-notes/build-pages</id>
        <link href="http://localhost/release-notes/build-pages"/>
        <updated>2026-10-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Projects, Versions, every version dialog, and Primitives and types — with screenshots.]]></summary>
        <content type="html"><![CDATA[<p>The <a class="" href="http://localhost/build">Build</a> group now has a page for every screen in it — <a class="" href="http://localhost/build/projects">Projects</a>,
<a class="" href="http://localhost/build/versions">Versions</a> and its tabs, <a class="" href="http://localhost/build/version-dialogs">Version dialogs</a>,
<a class="" href="http://localhost/build/primitives-and-types">Primitives and types</a> and <a class="" href="http://localhost/build/studio">Studio</a> — with a light and
dark screenshot of each tab and dialog. (<a href="https://github.com/apiome/apiome/issues/5622" target="_blank" rel="noopener noreferrer" class="">#5622</a>)</p>
<!-- -->
<ul>
<li class=""><strong>Flags are marked.</strong> The git-like features (merge, fork, tag, the change report) are documented
under a <span class="flag_OlCK"><span class="label_Hs_o">Flag off</span><code class="name_Hsaw">FEATURE_GITLIKE</code></span> badge: they are switched off in every shipped build.</li>
<li class=""><strong>Every page links its REST endpoints and CLI commands.</strong></li>
</ul>]]></content>
        <author>
            <name>The Apiome team</name>
            <uri>https://github.com/apiome/apiome</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[The docs gate runs on every pull request]]></title>
        <id>http://localhost/release-notes/docs-gate</id>
        <link href="http://localhost/release-notes/docs-gate"/>
        <updated>2026-10-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[docs:check now catches orphan pages, broken links and stale screenshots on every PR.]]></summary>
        <content type="html"><![CDATA[<p>Documentation is now part of the definition of done. <code>yarn docs:check</code> runs on <strong>every</strong> pull
request — not only those that touch the docs — so a change to a product screen or an endpoint
cannot merge with its page, screenshot or reference behind it.
(<a href="https://github.com/apiome/apiome/issues/5630" target="_blank" rel="noopener noreferrer" class="">#5630</a>)</p>
<!-- -->
<ul>
<li class=""><strong>Orphan pages</strong> — a page in no sidebar (outside every group, or hidden with <code>unlisted</code>) fails.</li>
<li class=""><strong>Broken internal links</strong> are named by page and link before the build starts.</li>
<li class=""><strong>Stale screenshots</strong> — an image captured before the last two releases, whose route's code has
changed since, fails with the command that recaptures it. A screen drawn by a dialog or a shared
panel names its component in the manifest's new <code>sources</code> field.</li>
<li class="">Each failure has a fixture site the tests run the gate over.</li>
<li class=""><a class="" href="http://localhost/admin/contribute-to-the-docs">Contribute to the docs</a> adds how to regenerate the reference and a
pull request checklist that matches every issue's <strong>Documentation (Docusaurus)</strong> section.</li>
</ul>]]></content>
        <author>
            <name>The Apiome team</name>
            <uri>https://github.com/apiome/apiome</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Host the documentation yourself]]></title>
        <id>http://localhost/release-notes/documentation-docker-image</id>
        <link href="http://localhost/release-notes/documentation-docker-image"/>
        <updated>2026-10-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[The documentation site now ships as a Docker image.]]></summary>
        <content type="html"><![CDATA[<p>The documentation site now ships as a Docker image, so you can serve it next to your own Apiome
installation. See <a class="" href="http://localhost/admin/docs-image">Host the documentation site</a>.</p>
<!-- -->
<p>The image serves the static site with nginx on port 8080, answers a <code>/healthz</code> health check, and is
published from <code>main</code> tagged with the docs version, <code>latest</code> and the commit SHA.</p>]]></content>
        <author>
            <name>The Apiome team</name>
            <uri>https://github.com/apiome/apiome</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[A documentation site for Apiome]]></title>
        <id>http://localhost/release-notes/documentation-site</id>
        <link href="http://localhost/release-notes/documentation-site"/>
        <updated>2026-10-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Apiome now has a searchable documentation site, built with Docusaurus.]]></summary>
        <content type="html"><![CDATA[<p>Apiome has a documentation site. It is grouped by the job you are doing — Getting started, Build,
Bring in, Ship, Govern, Workspace &amp; account, Admin &amp; tools and Reference — follows your system's
light or dark theme, and is searchable from the navbar. (<a href="https://github.com/apiome/apiome/issues/67" target="_blank" rel="noopener noreferrer" class="">#67</a>)</p>
<!-- -->
<p>The existing guides move onto the site over the RC6 documentation epic, together with screenshots of
every screen that regenerate from the product, generated REST / CLI / MCP reference and a release-notes
page per release.</p>]]></content>
        <author>
            <name>The Apiome team</name>
            <uri>https://github.com/apiome/apiome</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[REST, CLI and MCP reference, generated from the code]]></title>
        <id>http://localhost/release-notes/generated-reference</id>
        <link href="http://localhost/release-notes/generated-reference"/>
        <updated>2026-10-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Generated REST, CLI and MCP reference, with mock runtime and CI under Reference.]]></summary>
        <content type="html"><![CDATA[<p><a class="" href="http://localhost/reference">Reference</a> now carries the three surfaces generated straight from their source, so
they cannot drift from it: the <a class="" href="http://localhost/reference/rest">REST API</a> — a page for each of its tags — the
<a class="" href="http://localhost/reference/cli">CLI</a> — a page for each command, plus the exit codes — and the <a class="" href="http://localhost/reference/mcp">MCP server's
tools</a>. (<a href="https://github.com/apiome/apiome/issues/5628" target="_blank" rel="noopener noreferrer" class="">#5628</a>)</p>
<!-- -->
<ul>
<li class=""><strong>Fails on drift</strong> — each project's tests and a CI job fail when a page no longer matches its
source, and the site build fails when <code>openapi.yaml</code> changed without regenerating the REST pages.</li>
<li class=""><strong>Mock runtime and CI moved under Reference</strong> — the mock engine guides are now at
<a class="" href="http://localhost/reference/mock-runtime">Reference → Mock runtime</a>, and <a class="" href="http://localhost/reference/ci">Reference → CI</a> holds the
contract-gate guides with the <code>diff-action</code> and <code>mock-action</code> READMEs.</li>
<li class=""><strong>Quick-starts link the reference</strong> instead of restating the command and tool lists.</li>
</ul>]]></content>
        <author>
            <name>The Apiome team</name>
            <uri>https://github.com/apiome/apiome</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[A getting-started guide, with screenshots]]></title>
        <id>http://localhost/release-notes/getting-started-guide</id>
        <link href="http://localhost/release-notes/getting-started-guide"/>
        <updated>2026-10-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Nine pages walk from signing in to a published, browsable spec.]]></summary>
        <content type="html"><![CDATA[<p><a class="" href="http://localhost/">Getting started</a> now walks the whole spine as it ships today — sign in, set up your
organization, find your way around, import a project, version and publish it, browse and export
it, and connect an MCP host — with a light and dark screenshot of every screen along the way.
(<a href="https://github.com/apiome/apiome/issues/5621" target="_blank" rel="noopener noreferrer" class="">#5621</a>)</p>
<!-- -->
<ul>
<li class=""><strong>New pages:</strong> <a class="" href="http://localhost/getting-started/sign-in">Sign in and create an account</a>,
<a class="" href="http://localhost/getting-started/onboarding">Set up your first organization</a>,
<a class="" href="http://localhost/getting-started/launcher-and-home">The launcher and Home</a>,
<a class="" href="http://localhost/getting-started/first-project">Import your first project</a>,
<a class="" href="http://localhost/getting-started/versions-and-publishing">Version and publish</a>,
<a class="" href="http://localhost/getting-started/browse-and-export">Browse and export</a>,
<a class="" href="http://localhost/getting-started/connect-an-mcp-host">Connect an MCP host</a> and
<a class="" href="http://localhost/getting-started/keyboard-and-preferences">Keyboard and preferences</a>.</li>
<li class=""><strong>Corrected:</strong> the <a class="" href="http://localhost/reference/mcp-quickstart">MCP quick-start</a> now says where MCP API keys are
created (<strong>Tenants → Manage → Per-key capabilities</strong>), not the API keys page.</li>
<li class=""><strong>New in the screenshot manifest:</strong> <code>signed-out</code> entries capture a page such as sign-in without a
session.</li>
</ul>]]></content>
        <author>
            <name>The Apiome team</name>
            <uri>https://github.com/apiome/apiome</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Govern, documented page by page]]></title>
        <id>http://localhost/release-notes/govern-pages</id>
        <link href="http://localhost/release-notes/govern-pages"/>
        <updated>2026-10-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Style guides, Lint posture, Access audit and Reviews — with the waiver and approval flows.]]></summary>
        <content type="html"><![CDATA[<p>The <a class="" href="http://localhost/govern">Govern</a> group now has a page for every screen in it — <a class="" href="http://localhost/govern/style-guides">Style guides</a>
with the rule catalog, custom rules, assignment and policies, <a class="" href="http://localhost/govern/lint-posture">Lint posture</a>, the
<a class="" href="http://localhost/govern/access-audit">Access audit</a> and <a class="" href="http://localhost/govern/reviews">Reviews and the approval gate</a>.
(<a href="https://github.com/apiome/apiome/issues/5625" target="_blank" rel="noopener noreferrer" class="">#5625</a>)</p>
<!-- -->
<ul>
<li class=""><strong>Waivers, step by step</strong> — request a waiver on Lint posture, then approve it with an expiry.</li>
<li class=""><strong>Approvals, step by step</strong> — turn on the approval gate in a style guide's Policy, review a version,
and what publishing says when the gate is not met.</li>
<li class=""><strong>Every page links its rule-level guides</strong>, its REST endpoints and, where there is one, its CLI
command.</li>
</ul>]]></content>
        <author>
            <name>The Apiome team</name>
            <uri>https://github.com/apiome/apiome</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[The user guide moves onto the site]]></title>
        <id>http://localhost/release-notes/guides-on-the-site</id>
        <link href="http://localhost/release-notes/guides-on-the-site"/>
        <updated>2026-10-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Every guide from the repository is now a page on this site.]]></summary>
        <content type="html"><![CDATA[<p>All 39 guides that lived in the repository's <code>docs/guide/</code> folder are now pages on this site, grouped
by job: <a class="" href="http://localhost/bring-in">Bring in</a>, <a class="" href="http://localhost/build">Build</a>, <a class="" href="http://localhost/ship">Ship</a> (with its own <a class="" href="http://localhost/reference/mock-runtime">Mocks</a>
section), <a class="" href="http://localhost/govern">Govern</a> and <a class="" href="http://localhost/reference">Reference</a>. (<a href="https://github.com/apiome/apiome/issues/5619" target="_blank" rel="noopener noreferrer" class="">#5619</a>)</p>
<!-- -->
<ul>
<li class=""><strong>Search and a sidebar</strong> replace the old hand-kept “How do I…?” table.</li>
<li class=""><strong>New page:</strong> <a class="" href="http://localhost/getting-started/run-locally">Run Apiome locally</a> — the old index's <em>Before you
start</em> section.</li>
<li class=""><strong>In the app</strong>, <strong>Help &amp; docs</strong> — the guide search, the <strong>User guide</strong> and <strong>API &amp; CLI reference</strong>
cards — and every “View rule” and algorithm link open the page on this site.</li>
<li class=""><strong>Old links keep working.</strong> <a href="https://github.com/apiome/apiome/blob/main/docs/guide/README.md" target="_blank" rel="noopener noreferrer" class=""><code>docs/guide/README.md</code></a>
maps each old file to its new page, and a lint result that still names a <code>docs/guide/</code> page opens
the new one.</li>
<li class=""><strong>Generated pages stay generated.</strong> <em>Supported formats</em>, <em>Built-in lint rules</em> and the three MCP
rule catalogs are written by the same generators, now straight into the site, and the format
counts in <em>Import a specification</em> and <em>Export a spec</em> still regenerate.</li>
</ul>]]></content>
        <author>
            <name>The Apiome team</name>
            <uri>https://github.com/apiome/apiome</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Old implementation notes archived; product docs live only here]]></title>
        <id>http://localhost/release-notes/legacy-docs-triage</id>
        <link href="http://localhost/release-notes/legacy-docs-triage"/>
        <updated>2026-10-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[565 legacy notes archived with an inventory; a lint keeps new ones out.]]></summary>
        <content type="html"><![CDATA[<p>Hundreds of implementation notes — fix summaries, feature write-ups, test journeys — used to sit
beside the code with no index, most of them describing screens that have since changed. They are
now archived, every one accounted for, and this site is the one place product documentation lives.
(<a href="https://github.com/apiome/apiome/issues/5631" target="_blank" rel="noopener noreferrer" class="">#5631</a>)</p>
<!-- -->
<ul>
<li class=""><strong>An inventory of all 821 files</strong> outside the site and the mockups — 565 archived to
<code>docs/archive/</code>, 253 kept (package READMEs and changelogs, skills, test fixtures, runbooks, and the
contributor references code points at), 3 migrated — with the site page that now covers each
archived note's area.</li>
<li class=""><strong>Provider-side settings for sign-in</strong> — where to register each OAuth app and what else it needs,
such as Entra ID's <code>xms_edov</code> claim and GitLab's <code>read_user</code> scope — are now on
<a class="" href="http://localhost/admin/auth-providers#settings-on-the-providers-side">Sign-in providers</a>.</li>
<li class=""><strong>A loose-docs lint</strong> fails a pull request that adds a <code>.md</code> under <code>apiome-*/docs/</code>;
<a class="" href="http://localhost/admin/contribute-to-the-docs#where-docs-live">Contribute to the docs</a> says where docs go instead.</li>
</ul>]]></content>
        <author>
            <name>The Apiome team</name>
            <uri>https://github.com/apiome/apiome</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[A release-notes post for every RC]]></title>
        <id>http://localhost/release-notes/release-notes-per-rc</id>
        <link href="http://localhost/release-notes/release-notes-per-rc"/>
        <updated>2026-10-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Per-RC posts merging the REST changelog and What's new, plus a rolling Unreleased.]]></summary>
        <content type="html"><![CDATA[<p>Release information used to live in three places that never met: the REST API's changelog, the
app's <strong>What's new</strong> dialog and the GitHub milestones. The <a class="" href="http://localhost/release-notes">release notes</a> now bring
them together — one post per release candidate, starting with <a class="" href="http://localhost/release-notes/rc4">RC4</a>, and an
<a class="" href="http://localhost/release-notes/unreleased">Unreleased</a> post for what has landed since.
(<a href="https://github.com/apiome/apiome/issues/5629" target="_blank" rel="noopener noreferrer" class="">#5629</a>)</p>
<!-- -->
<ul>
<li class=""><strong>One post per RC</strong> — the app's What's new for that release, then every REST API version it
carries, with each <code>#1234</code> linked to its issue and the milestone's issue list one click away.</li>
<li class=""><strong>Unreleased regenerates on every build</strong> of the site, so it always matches <code>main</code>.</li>
<li class=""><strong>Written on tag</strong> — tagging <code>RC6</code> opens a pull request with the RC6 post.</li>
<li class=""><strong>"Full release notes"</strong> at the foot of the in-app <strong>What's new</strong> dialog opens these pages. See
<a class="" href="http://localhost/workspace/help#whats-new">Help &amp; docs</a>.</li>
</ul>]]></content>
        <author>
            <name>The Apiome team</name>
            <uri>https://github.com/apiome/apiome</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Screenshots that retake themselves]]></title>
        <id>http://localhost/release-notes/screenshot-pipeline</id>
        <link href="http://localhost/release-notes/screenshot-pipeline"/>
        <updated>2026-10-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Product screenshots on this site are captured by a script, in light and dark.]]></summary>
        <content type="html"><![CDATA[<p>Every product screenshot on this site is now captured by a script from the product itself, in the
light and the dark theme, and retaken every week — so a picture never shows a screen that no longer
exists. Switch the site theme and the screenshots switch with it. (<a href="https://github.com/apiome/apiome/issues/5620" target="_blank" rel="noopener noreferrer" class="">#5620</a>)</p>
<!-- -->
<ul>
<li class=""><strong>One manifest.</strong> <code>screens.json</code> lists each screen: its route, what to wait for, and what to mask.
<code>yarn docs:screenshots</code> captures it at 1440 × 900 with the clock fixed, so reruns match pixel for
pixel.</li>
<li class=""><strong>Real data or a fixture.</strong> Screens are captured signed in to the seeded golden-path stack, or from
a committed page fixture when the stack is not running.</li>
<li class=""><strong>A gate.</strong> <code>yarn docs:check</code> fails when a page shows a screenshot that has no light or dark image.</li>
<li class=""><strong>A weekly refresh</strong> opens a pull request whenever a screen changed.</li>
</ul>
<p><a class="" href="http://localhost/admin/contribute-to-the-docs">Contribute to the docs</a> has the recipe for adding one.</p>]]></content>
        <author>
            <name>The Apiome team</name>
            <uri>https://github.com/apiome/apiome</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[A user guide, an administration guide and a CLI guide]]></title>
        <id>http://localhost/release-notes/separate-guides</id>
        <link href="http://localhost/release-notes/separate-guides"/>
        <updated>2026-10-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[The site is now four guides, each with its own sidebar.]]></summary>
        <content type="html"><![CDATA[<p>The documentation is no longer one long sidebar. The top bar now has four guides, each with its own
sidebar and home page: the <a class="" href="http://localhost/">User guide</a> for working in Apiome, the
<a class="" href="http://localhost/admin">Administration guide</a> for running an installation, the
<a class="" href="http://localhost/reference/cli-quickstart">CLI</a> for the <code>apiome</code> command, and the <a class="" href="http://localhost/reference">Reference</a> for the
REST API, MCP, the mock runtime and CI.</p>
<!-- -->
<ul>
<li class=""><strong>Same addresses</strong> — every page keeps its URL, so bookmarks and the in-app help links still work.</li>
<li class=""><strong>The user guide's home</strong> is the site root: the jobs in the order of the product's navigation
rail, with links to the other guides.</li>
<li class=""><strong>The CLI guide</strong> puts the quick-start first and then a page per command, under
<strong>Command reference</strong>.</li>
</ul>]]></content>
        <author>
            <name>The Apiome team</name>
            <uri>https://github.com/apiome/apiome</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Ship, documented page by page]]></title>
        <id>http://localhost/release-notes/ship-pages</id>
        <link href="http://localhost/release-notes/ship-pages"/>
        <updated>2026-10-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Published, the Sunset timeline, the Export studio, SDK settings and Mock try-out — with screenshots.]]></summary>
        <content type="html"><![CDATA[<p>The <a class="" href="http://localhost/ship">Ship</a> group now has a page for every screen in it — <a class="" href="http://localhost/ship/published">Published</a> with
visibility, private keys and the hosted mock, the <a class="" href="http://localhost/ship/sunset-timeline">Sunset timeline</a>, the
five steps of the <a class="" href="http://localhost/ship/export-studio">Export studio</a>, <a class="" href="http://localhost/ship/sdk-settings">SDK settings</a> and
<a class="" href="http://localhost/ship/mock-try-out">Mock try-out</a>. (<a href="https://github.com/apiome/apiome/issues/5624" target="_blank" rel="noopener noreferrer" class="">#5624</a>)</p>
<!-- -->
<ul>
<li class=""><strong>Mock runtime</strong> — the mock engine guides now sit in their own <a class="" href="http://localhost/reference/mock-runtime">Mock runtime</a>
section, below the pages for the screens that use them.</li>
<li class=""><strong>Get SDK</strong> — <a class="" href="http://localhost/ship/sdk-settings">SDK settings</a> explains the public SDK download that the
<strong>Public SDK access</strong> switch turns on.</li>
<li class=""><strong>Every page links its REST endpoints and CLI commands</strong>, and the export and mock guides link
back to the screens.</li>
</ul>]]></content>
        <author>
            <name>The Apiome team</name>
            <uri>https://github.com/apiome/apiome</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Workspace & account, documented page by page]]></title>
        <id>http://localhost/release-notes/workspace-pages</id>
        <link href="http://localhost/release-notes/workspace-pages"/>
        <updated>2026-10-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Members, roles, API keys, tenants, profile, linked accounts, preferences, notifications and help.]]></summary>
        <content type="html"><![CDATA[<p>The <a class="" href="http://localhost/workspace">Workspace &amp; account</a> group now has a page for every screen in it —
<a class="" href="http://localhost/workspace/members">Members &amp; seats</a>, <a class="" href="http://localhost/workspace/roles">Roles &amp; permissions</a>,
<a class="" href="http://localhost/workspace/api-keys">API keys &amp; agent keys</a>, <a class="" href="http://localhost/workspace/tenants">Tenants &amp; the manage drawer</a>,
<a class="" href="http://localhost/workspace/profile">Profile &amp; security</a>, <a class="" href="http://localhost/workspace/linked-accounts">Linked accounts</a>,
<a class="" href="http://localhost/workspace/preferences">Preferences</a>, <a class="" href="http://localhost/workspace/notifications">Notifications</a> and
<a class="" href="http://localhost/workspace/help">Help &amp; shortcuts</a>. (<a href="https://github.com/apiome/apiome/issues/5626" target="_blank" rel="noopener noreferrer" class="">#5626</a>)</p>
<!-- -->
<ul>
<li class=""><strong>A theme gallery</strong> — every one of the nine themes, captured from the product.</li>
<li class=""><strong>Two-factor sign-in, step by step</strong> — set up an authenticator app and keep your backup codes.</li>
<li class=""><strong>Keyboard and Accessibility moved here</strong> from Reference; links from the app follow them.</li>
<li class=""><strong>Screenshot manifest</strong> — entries can pin a product theme with <code>appTheme</code>.</li>
</ul>]]></content>
        <author>
            <name>The Apiome team</name>
            <uri>https://github.com/apiome/apiome</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Apiome RC4]]></title>
        <id>http://localhost/release-notes/rc4</id>
        <link href="http://localhost/release-notes/rc4"/>
        <updated>2026-08-20T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Everything in Apiome RC4: the app's What's new and the REST API changes.]]></summary>
        <content type="html"><![CDATA[
<p>RC4 closed on 2026-08-20. It brings the app notes below and REST API 1.215.1 to 1.263.0 (40 versions). See <a href="https://github.com/apiome/apiome/milestone/1?closed=1" target="_blank" rel="noopener noreferrer" class="">every issue in the RC4 milestone</a>.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_tleR" id="whats-new-in-the-app">What's new in the app<a href="http://localhost/release-notes/rc4#whats-new-in-the-app" class="hash-link" aria-label="Direct link to What's new in the app" title="Direct link to What's new in the app" translate="no">​</a></h2>
<p>From the in-app <strong>What's new</strong> for <em>Apiome 07-2026 RC4</em>.</p>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="featuresimprovements">Features/Improvements<a href="http://localhost/release-notes/rc4#featuresimprovements" class="hash-link" aria-label="Direct link to Features/Improvements" title="Direct link to Features/Improvements" translate="no">​</a></h3>
<ul>
<li class="">Import: Arazzo workflow documents now import as first-class Workflow and Workflow Step entities; each step's <code>operationRef</code>/<code>operationId</code> links to the matching operation when that OpenAPI spec was imported in the same scan, and an unresolved reference keeps its raw value with a warning instead of being dropped</li>
<li class="">Repository: specs that reference schemas on external hosts are now governed by a per-tenant policy — <code>block</code> (the default; nothing is fetched and the file is flagged with exactly which references are missing), <code>inline</code> (permitted references are fetched once and snapshotted into the scanned spec), or <code>proxy-fetch</code> (the same, restricted to an allowlist of hostnames, wildcards like <code>*.acme.com</code> included). Every fetch is recorded in the audit trail</li>
<li class="">Repository: registered repositories now accept signed webhook deliveries, so a push to a branch you import from makes the repository due for a refresh immediately instead of at the end of its polling interval. Pull-request events can additionally index the PR's head branch so you can inspect the specs a review touches before it merges. Each repository gets its own signing secret, and the delivery history — including anything that failed to verify — is visible per repository</li>
<li class="">Repository: a repository's webhook signing secret can now be rotated without a break in service — the new secret is installed at the provider, the old one keeps working for a grace window (24 hours by default) so deliveries already in flight still arrive, and it then expires on its own. If the provider could not be updated, the repository says so, and how long is left before deliveries start failing</li>
<li class="">Repository: a new <strong>Spec catalog</strong> (Repositories → Spec catalog) lists every discovered spec across <em>all</em> your repositories in one searchable table. Search by path, format, repository or project; filter by format, repository, project, or status (needs attention / imported / mapped / discovered); sort by any of them. Each row links straight to that spec's detail view on its own repository, and the whole view lives in the URL, so a filtered catalog is a link you can paste to a colleague. Paging is server-side and stays fast on workspaces with tens of thousands of files</li>
<li class="">Repository: every repository now carries a <strong>health badge</strong> — healthy, warnings or error — on the repositories list and on the repository detail header. It rolls up how many scans succeeded over the last 30 days, how many discovered specs failed to parse, and whether the linked account's access token is still good. Hover it and the tooltip leads with the most recent thing that went wrong, so you can see <em>what changed</em> without opening the repository. A credential problem never shows as healthy, however clean everything else is</li>
<li class="">Repository: your webhook channels are now told when a repository needs attention — when auto-refresh pauses itself after repeated failures, when a sync introduces a breaking change, and when a repository has been failing for a while but has not paused yet. Each repository can opt out of each of those individually, and any one of them is sent at most once an hour per repository, so a repository stuck in a failure loop cannot flood the channel. A channel pointed at a Slack incoming webhook receives a proper Slack message rather than raw JSON</li>
<li class="">Repository: repository polling now has a per-tenant hourly ceiling (60 by default, 600 on the elevated plan), so one busy workspace can no longer crowd everyone else out of the refresh scheduler. Repositories over the ceiling are simply picked up on a later pass — they are never marked as failed, never backed off, and never paused — and manual "Refresh Now" is never limited</li>
<li class="">Repository: tenant administrators can now download the complete repository audit trail — refresh cycles, webhook activity, secret rotations, external-reference fetches and more — as a dated CSV or JSON file for SOC 2 / ISO 27001 reviews. Pick a date range and a format and the export streams no matter how large the ledger is; every export (even one that was cut off mid-download) is itself recorded in the audit trail, so the evidence includes who exported the evidence</li>
<li class="">Repository: a new <strong>Quota &amp; limits</strong> page (Repositories → Quota &amp; limits) shows what the polling ceiling and the scanner have actually been doing over the last 7, 30 or 90 days — polls, scans and content scanned, with the repositories and files the quota <em>deferred</em> charted separately so "we were throttled" is never mistaken for "we were quiet". The panel leads with how much of the current hour's budget is spent and warns as you approach the ceiling, rather than after work has already been postponed. Counters survive restarts and are combined across servers, and if they cannot be read the page says so instead of showing you a flat line</li>
<li class="">Repository: webhook deliveries can now be filtered by <strong>source address</strong> before their signature is ever checked (Repositories → Webhook IPs). The provider's own published ranges — GitHub's <code>meta</code> endpoint, Atlassian's for Bitbucket — are fetched daily and cached, so the list stays right as the providers move; your workspace can add its own ranges for a self-hosted runner or an egress gateway, and each one records why it exists. The page states in a sentence whether the filter is actually protecting anything right now, rather than leaving you to combine three switches, and flags a provider whose range list has stopped refreshing. Turning the filter off for your workspace is a tenant-administrator action and asks for a reason, which goes into the audit trail</li>
<li class="">Repository: you can now choose what an auto-refresh does when it finds a spec you edited in Apiome after it was first imported (repository detail → Settings → <strong>Refresh conflicts</strong>). <strong>Hold for review</strong> stays the default and changes nothing: the refresh is skipped and the file is flagged, so nothing is overwritten until someone looks. <strong>Overwrite</strong> lets the repository win — the divergence is still recorded, so you can always see what was replaced. <strong>New branch</strong> keeps both, landing the refresh on a new branch instead of touching your edited version. Set it once for the repository, and override it for the one file that needs to differ; clearing an override puts that file back on whatever the repository is set to next, not on a stale copy of today's choice</li>
<li class="">UI/UX
<ul>
<li class="">The Apiome bee is now drawn as vector art rather than a picture, so the logo stays sharp at every size and its outline lightens on the dark themes instead of disappearing into them. The browser-tab, home-screen and installed-app icons are all generated from that same mark</li>
<li class="">Major updates look and feel</li>
<li class="">Added tabbed sections to Style Guides</li>
<li class="">Softened the font of the entire application</li>
<li class="">Moved Preferences to the bottom of the left-hand sidebar</li>
<li class="">Changed "Themes" to "Preferences" in the upper right-hand profile button</li>
</ul>
</li>
<li class="">Primitives
<ul>
<li class="">Major UX improvements in the import functionality</li>
<li class="">Shows unregistered namespaces that were detected</li>
<li class="">Import now cautions when a type declares no "type" of its own, since it will accept any value</li>
<li class="">Now shows unassigned/unspecified namespaces in JSON Type definitions</li>
<li class="">Grouping primitives in a namespace now works logically as expected</li>
<li class="">Primitives are now clickable inside the reference graph</li>
<li class="">Example form now builds inputs from the schema and allows for testing</li>
<li class="">Cards for reference resolution and base chain details now include traversable $refs if any apply</li>
<li class="">Clarifies language when importing and creating $ref for a system type based on "format" in a property</li>
<li class="">Documentation-only schemas that contain no type still get imported, but are treated as warnings</li>
<li class="">Review section of import for primitives now classifies unresolved $ref as a warning, so now shows warning counts</li>
<li class="">Changed "Test this type" to be expand/collapse with a chevron for testing</li>
<li class="">Now shows any warnings generated during import</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="bug-fixes">Bug Fixes<a href="http://localhost/release-notes/rc4#bug-fixes" class="hash-link" aria-label="Direct link to Bug Fixes" title="Direct link to Bug Fixes" translate="no">​</a></h3>
<ul>
<li class="">Primitives
<ul>
<li class="">Added $ref lookups during primitive import, warning of unresolved $refs if any exist</li>
<li class="">Now shows the JSON Schema using monaco-editor</li>
<li class="">Added the ability to test a primitive by presenting a usable form that represents the content of the JSON Schema</li>
<li class="">Duplicate names are no longer treated as duplicates unless the namespace is identical</li>
<li class="">Removed invalid previously created primitives</li>
<li class="">Corrected resolution for $ref values in native system types</li>
<li class="">Updated import so that names with dashes are imported properly</li>
<li class="">Schemas that carry only documentation now import as the empty object type it describes instead of being rejected</li>
<li class="">Dependents card now shows properly</li>
<li class="">Added clarifying verbiage on unresolved $refs at import</li>
<li class="">$ref resolution is now local-only: references resolve to types by their place in this registry (namespace + name), never to a remote URL — imported documents' foreign $ids are ignored for resolution, and the review agrees with the import screen's preview</li>
<li class="">"Test this type" now handles additionalProperties: map objects offer named add/remove rows, each value validated live against the entry schema</li>
</ul>
</li>
<li class="">Repositories: Fixed file listing and scanning issues</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_tleR" id="rest-api">REST API<a href="http://localhost/release-notes/rc4#rest-api" class="hash-link" aria-label="Direct link to REST API" title="Direct link to REST API" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12630--2026-08-17">1.263.0 — 2026-08-17<a href="http://localhost/release-notes/rc4#12630--2026-08-17" class="hash-link" aria-label="Direct link to 1.263.0 — 2026-08-17" title="Direct link to 1.263.0 — 2026-08-17" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added">Added<a href="http://localhost/release-notes/rc4#added" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class=""><strong>Re-issue an outstanding member invitation (<a href="https://github.com/apiome/apiome/issues/5305" target="_blank" rel="noopener noreferrer" class="">#5305</a>)</strong> — <code>POST /v1/access/{tenant_slug}/members/{user_id}/resend-invite</code>.
Apiome does not mail invitations: an invitee already holds an account and their <code>pending</code>
membership flips to <code>active</code> the next time they sign in. Re-issuing therefore <em>renews</em> the
invitation rather than re-sending a message — the membership row is re-stamped
(<code>touch_pending_membership</code>, scoped to <code>status = 'pending'</code> in SQL so an invitation accepted
in between cannot be un-accepted) and the renewal is written to the access ledger as
<code>member.invite_resent</code>, which the existing <code>?filter=member</code> audit tab already covers.
Gated on <code>members:create</code>; consumes no seat, because the pending membership already holds
one. Answers 404 when the user has no membership in the tenant and 409 when their
membership is not pending.</li>
</ul>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="changed">Changed<a href="http://localhost/release-notes/rc4#changed" class="hash-link" aria-label="Direct link to Changed" title="Direct link to Changed" translate="no">​</a></h4>
<ul>
<li class=""><strong><code>GET /v1/access/{tenant_slug}/members</code> carries three more facts per member (<a href="https://github.com/apiome/apiome/issues/5305" target="_blank" rel="noopener noreferrer" class="">#5305</a>)</strong> —
<code>joined_at</code> (<code>tenant_users.created_at</code>, when the membership was first written, as distinct
from the existing <code>member_since</code> = <code>updated_at</code>, which moves with every status change),
<code>last_active</code> (<code>users.last_login_at</code>, V070) and <code>two_factor_enabled</code> (the Better Auth
<code>users."twoFactorEnabled"</code> flag, V201). All three read columns that already existed; the
response is additive, so existing clients are unaffected.</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12620--2026-08-15">1.262.0 — 2026-08-15<a href="http://localhost/release-notes/rc4#12620--2026-08-15" class="hash-link" aria-label="Direct link to 1.262.0 — 2026-08-15" title="Direct link to 1.262.0 — 2026-08-15" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="fixed">Fixed<a href="http://localhost/release-notes/rc4#fixed" class="hash-link" aria-label="Direct link to Fixed" title="Direct link to Fixed" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong>Project versions were re-linted when listed (<a href="https://github.com/apiome/apiome/issues/5259" target="_blank" rel="noopener noreferrer" class="">#5259</a>)</strong> — the Versions screen rendered a
lint badge per row that called <code>GET .../{version}/lint</code> for every revision, and for any
revision without a stored report (hand-authored, forked, pushed, pre-V160) that endpoint
rebuilt the OpenAPI document, ran the linter <strong>and</strong> the external validation pack — on
every list render, N times over. Fifty revisions meant fifty concurrent lints.</p>
<p><strong>The report now lives on the version record and is served from there.</strong>
<code>GET /v1/versions/{tenant_slug}/{project_id}</code> carries each row's stored <code>qualityScore</code> /
<code>qualityGrade</code> (<code>VersionSchema</code>; <code>get_versions_for_project</code> / <code>get_version_by_id</code> select the
V124 columns), so the list renders from the record and issues no lint requests.
<code>GET .../lint</code> serves the stored <code>quality_report</code> whenever it is current; the persisted
report now carries a <code>source_fingerprint</code> (sha256 of the reconstructed OpenAPI document,
<code>app.version_quality_capture.openapi_source_fingerprint</code>), and freshness is decided by
rebuilding the document and comparing — never by re-linting. A revision with no stored
report, or whose content changed since capture, is linted once and the result <strong>persisted</strong>
(<code>persist_version_lint_report</code>), so the next read is a plain read; a <code>baseRevisionId</code>
comparison is computed live and never stored. Reports without a fingerprint (pre-#5259,
canonical-model imports) are served as-is; a legacy native import re-linted from its
canonical model is stored on first open too.</p>
<p><strong>Linting runs on version changes and imports only.</strong> Push (<code>POST /v1/versions/{t}/{p}</code>)
and fork schedule the shared capture (<code>capture_version_quality_score</code>, moved out of
<code>spec_import_engine</code>) as a background task; the publish precheck stores the report it
already computed; import/conversion captures gained the fingerprint + guide context
(<code>persistable_lint_report</code>). Stored reports echo the guide they were scored under
(<code>guideId</code> / <code>guideName</code> / <code>guideSource</code>) so the read path has the same context as a live run.</p>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12610--2026-08-07">1.261.0 — 2026-08-07<a href="http://localhost/release-notes/rc4#12610--2026-08-07" class="hash-link" aria-label="Direct link to 1.261.0 — 2026-08-07" title="Direct link to 1.261.0 — 2026-08-07" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-1">Added<a href="http://localhost/release-notes/rc4#added-1" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong>Bounded primitives search (DWX-3.1, private-suite#2683)</strong> — <code>GET /v1/primitives/{tenant_slug}</code> has always answered with <em>every</em> primitive a tenant
can see. A tenant that has imported a standard library has thousands of rows, and
the unified workspace's type picker — a 320px rail — cannot be built on a read like
that. The endpoint now takes <code>q</code>, <code>scope</code>, <code>namespace</code>, <code>limit</code> and <code>cursor</code>, and
answers those with at most <code>limit</code> rows plus the four type-picker tab counts
(<code>app.primitives_search_store</code>, <code>PrimitiveSearchPage</code>).</p>
<p><strong>Both shapes, one path.</strong> A caller that asks none of the five bounded parameters
gets the classic JSON array, from the same <code>get_primitives_for_tenant</code> read, with
<code>category</code> working exactly as before — the classic property dialogs are unaffected
and keep working until private-suite's DWX-8.3 retires their callers. Asking any one
of the five switches the response to the paged envelope. The two shapes list the
same catalog: the same visibility scope (<code>is_system</code> ∪ the caller's own rows) and
the same <code>(namespace, name)</code> deduplication, so a primitive is never reachable
through one and not the other.</p>
<p><strong>The scope classification is the client's rule, in three places that must agree.</strong>
The four tabs — Standard, Core, Tenant, Custom — are derived in the designer today
by <code>classifyPrimitive</code>. A server that filtered by scope while the client grouped by
its own rule would silently hide types, so the rule is now written three times over
one shared fixture (<code>tests/fixtures/primitive_scope_cases.json</code>): the TypeScript
original, <code>classify_scope</code> in Python, and <code>SCOPE_EXPRESSION</code> in SQL. <code>pytest</code> checks
Python against the fixture and SQL against Python over real rows; the designer's jest
suite checks the TypeScript half against its copy of the same cases.</p>
<p><strong>The cursor is keyset, not an offset.</strong> It carries the sort key of the last row
handed out, so a primitive created mid-scroll cannot shift a page boundary and make
a row repeat or vanish; a live-DB test walks 5,000 rows and asserts each is visited
exactly once. It is opaque, and a token this endpoint did not mint is a 400 rather
than a silently ignored parameter that would restart a paging client at page one
forever. An unknown <code>scope</code> is likewise a 400 — a misspelling that quietly listed
every tab would make an unbounded read look like a bounded one.</p>
<p>Tenancy is unchanged and enforced by construction: another tenant's private types
are not filtered out of the result, they are never in the visibility CTE, so no
query, namespace, cursor or <code>$ref</code> reaches one.</p>
<p><code>apiome-db/scripts/V244__primitives_bounded_search_indexes_2683.sql</code> adds the
read-path indexes: a <code>(namespace, name, tenant_id)</code> b-tree for the dedupe and the
cursor ordering, an <code>(is_system, source, namespace)</code> b-tree for the scope
classification, and <code>pg_trgm</code> GIN indexes so a leading-wildcard <code>ILIKE</code> is not a
sequential scan of the registry. Nothing there changes a column, a constraint or a
value, and — as in V230 — the trigram block degrades to a <code>NOTICE</code> where the
migration role cannot install contrib extensions.</p>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12600--2026-08-05">1.260.0 — 2026-08-05<a href="http://localhost/release-notes/rc4#12600--2026-08-05" class="hash-link" aria-label="Direct link to 1.260.0 — 2026-08-05" title="Direct link to 1.260.0 — 2026-08-05" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-2">Added<a href="http://localhost/release-notes/rc4#added-2" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong>Programmable custom palette actions (DUW-5.5, private-suite#2592)</strong> — The ⌘K
palette's <code>Actions</code> band has been a fixed registry of five built-ins since DUW-5.4;
"programmable" means a tenant defining its own rows — <code>Open runbook for {subject}</code>
against every class whose name contains <code>Invoice</code> — and that needs the definitions
stored somewhere durable and tenant-scoped. <code>apiome.workspace_custom_actions</code> (V243)
and the CRUD surface under <code>/v1/workspace/{tenant_slug}/custom-actions</code> are that
storage.</p>
<p>A definition is a <em>declaration</em>, never a script. Its matcher is a subject kind
(<code>class</code>, <code>path</code>, <code>property</code>, <code>any</code>) plus an optional case-insensitive label
substring; its effects are an ordered list drawn from a closed vocabulary —
<code>hydrate-set</code>, <code>lens-switch</code>, <code>open-inspector-tab</code>, <code>run-consumption-query</code>,
<code>open-url</code> — each element validated down to exactly the fields its type declares
(<code>workspace_custom_action_rules</code>). Unknown keys are rejected rather than ignored, so
a typo cannot become an effect that silently does less than its author meant;
<code>open-url</code> accepts absolute <code>https://</code> URLs only, with no embedded credentials,
because <code>javascript:</code> and <code>data:</code> are not effects, they are payloads. Anything
resembling SDK-script execution stays out of scope by design and defers to the
DUW-7.4 sandbox. The database independently pins the outer shape — a JSON array of
1–5 elements under 16KB — so no write path can park a script here even if it skipped
the service schema.</p>
<p>Tenancy comes from the token, never the URL: every statement is scoped by the
caller's tenant, so another tenant's action is a 404 — whether it exists is not
something this API confirms. Reads are open to any authenticated member (the palette
performs one for everyone); writes require an attributable user holding
VERSIONS/EDIT, the same gate the domain folders use for reorganizing what everyone
sees. Deletes are soft, and a live action's name is unique per tenant
case-insensitively — two rows both drawn as <code>Open runbook…</code> in one band would be
indistinguishable to the reader. The management page that will wrap this API is
DUW-8.2 (private-suite#2602); until it lands, these routes are the management
surface, which is why every 422 names the offending field as a pointer
(<code>effects[1].lens</code>).</p>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12580--2026-08-05">1.258.0 — 2026-08-05<a href="http://localhost/release-notes/rc4#12580--2026-08-05" class="hash-link" aria-label="Direct link to 1.258.0 — 2026-08-05" title="Direct link to 1.258.0 — 2026-08-05" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-3">Added<a href="http://localhost/release-notes/rc4#added-3" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong>Response status codes on the scoped path read (DUW-4.3, private-suite#2583)</strong> —
The unified workspace's paths lens draws every operation as a lane, and a lane ends
in the codes it answers with (<code>200·400·401</code>), coloured by method. Those codes were
the one thing on the lane the scoped read did not carry: <code>GET /v1/workspace/{tenant}/version/{version_id}/paths</code> shipped each operation's
<code>operation_id</code>, <code>summary</code> and <code>deprecated</code> flag and left everything about a response
with the per-path <code>/full</code> endpoint. That is right for a response <em>body</em> — schemas,
content types and examples are inspector-sized data for one selected operation — but
a status code is a label the canvas prints on every lane it draws, and there is no
number of round trips between "one" and "one per operation" that answers it.</p>
<p>Each operation now carries <code>response_codes</code>: the status codes it declares, as
strings, ascending (<code>default</code> sorts after the numbers), empty when it declares none.
They come from a lateral aggregate over <code>path_operation_response_link</code> →
<code>shared_path_response</code> on the statement that was already reading the operations, so
a page of paths costs the same two statements it did before, whatever its size. The
codes are per <em>operation</em>, not per path: responses are shared per path in this schema
and linked per operation, so a read that rolled up by path would give every verb on
<code>/customers</code> the same list. Response bodies stay exactly where they were.</p>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12570--2026-08-04">1.257.0 — 2026-08-04<a href="http://localhost/release-notes/rc4#12570--2026-08-04" class="hash-link" aria-label="Direct link to 1.257.0 — 2026-08-04" title="Direct link to 1.257.0 — 2026-08-04" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-4">Added<a href="http://localhost/release-notes/rc4#added-4" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong>Schema↔path consumption index (DUW-1.4, private-suite#2571)</strong> — Five surfaces
of the unified workspace need to know which operations consume which classes and
<em>how</em>: the combined lens's edges (solid amber for a schema named directly by a
request or response, dashed rose for one reached through a parent class), the
tree's per-path <code>Schemas</code> rows (<code>Customer 200</code>, <code>Address nested</code>), the palette's
"find every path that consumes X" action, the inspector's <code>Consumes</code> list, and
the status bar's <code>N schema↔path links</code> chip. Today's derivation is the designer's
<code>createAllEdges</code> — O(classes×properties) over a full-catalog fetch, and
schema↔schema only; it has never known that an <em>operation</em> consumes anything.
<code>GET /v1/workspace/{tenant}/version/{version_id}/consumption</code> answers it from
the server, in seven statements.</p>
<p>Every edge names both members, how the consumption arrives (<code>request</code>,
<code>parameter</code>, <code>response.&lt;status&gt;</code>) and — for a nested one — the chain of classes
it hangs off, so one response drives all five surfaces. The facts arrive twice:
flat in <code>edges</code>, the shape the canvas draws, and rolled up per path in <code>paths</code>,
the shape the tree nests under a path with the badge each row prints.
<code>link_count</code> counts operation↔class edges, <code>path_link_count</code> distinct path↔class
pairs.</p>
<p>Five decisions are load-bearing:</p>
<ul>
<li class="">
<p><strong>A reference is a <code>$ref</code> anywhere in a payload.</strong> The catalog stores an
operation's schemas as a <code>class_id</code> column, an inline schema or a legacy <code>data</code>
blob, and a class's own references as <code>$ref</code>, <code>items.$ref</code>,
<code>allOf</code>/<code>anyOf</code>/<code>oneOf</code>, or any of those nested inside another. Enumerating the
shapes would mean re-deriving the emitter's rules in reverse and losing an edge
whenever they gained a case, so the resolver walks the JSON and collects every
<code>$ref</code> — exactly the set of names the emitted document carries. Only the tables
the emitter reads are indexed (<code>shared_path_response</code>(<code>_content</code>),
<code>shared_path_request_body_content</code>, <code>shared_path_parameter</code>); the V028-era
tables V031–V034 superseded are read by nothing, and indexing them would invent
edges no exported document contains.</p>
</li>
<li class="">
<p><strong>Nesting is resolved per class, not per operation, and breadth-first.</strong> Two
operations returning <code>Customer</code> reach the same descendants through the same
edges, so the walk runs once per class and is memoized — walking per operation
would be the client-side derivation moved to the server and multiplied by the
operation count. Breadth-first makes <code>via</code> the <em>shortest</em> parent chain, and
ties break on class name, so "nested via X" is a property of the catalog rather
than of row order. Cycles terminate by construction: the visited set includes
the root, so a self-referencing class and a mutual pair are each walked once
and the root is never nested under itself. Depth is capped at 6 hops
(<code>depth_cap</code>) and a graph continuing past it says so through <code>depth_capped</code>.</p>
</li>
<li class="">
<p><strong>A directly named class is never also nested.</strong> The canvas draws one line
between two nodes, and the solid one is the truthful description.</p>
</li>
<li class="">
<p><strong>The scope narrows paths; the graph is always whole.</strong> <code>domain_id</code> and
<code>path_ids</code> are mutually exclusive path selectors; <code>class_ids</code> narrows the class
side and composes with either, because "which of these classes does
<code>customers/</code> consume" is a real question. The class filter is applied <em>after</em>
the walk — filtering the graph first would drop the very parents a nested edge
is reached through, so "every path that consumes <code>Address</code>" would miss every
path that reaches it through <code>Customer</code>, which is most of them. A
domain-scoped answer therefore still names classes outside the domain, which is
what "nested via parent" means.</p>
</li>
<li class="">
<p><strong>Caching is content-addressed.</strong> The index is computed on read and never
persisted (no table in v1); the response carries a strong <code>ETag</code> digested from
the body, which keys it on version content by construction <em>and</em> on the scope —
something a stored version-content hash would not do — so a repeat read is a
<code>304</code> until the index actually changes. Same convention as the APX-3.4 agent
outputs.</p>
</li>
</ul>
<p>Bounded and honest about it: <code>edge_limit</code> caps the edge list at 5000 with
<code>truncated</code> set, and there is no cursor, because an edge means nothing without
both members it connects. Unresolvable path or class ids come back in
<code>missing_ids</code> rather than being silently absent, and the two id selectors share
the DUW-1.2 cap of 200 per request. Verified against a seeded 218-path /
250-class catalog under <code>pg_virtualenv</code>: the mockup's
Customer/Address/ContactMethod × 4 operations reproduced edge for edge including
<code>nested via parent</code> and the status bar's six path↔class pairs, a self-referencing
class and a mutual pair terminating on stored rows, seven statements whatever the
scope, and p95 ≈ 5.3 ms against the epic's 300 ms budget. No migration: V242's
<code>domain_id</code> indexes and the existing <code>version_id</code> indexes cover the reads. The
BFF routes and typed client are DUW-1.5.</p>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12560--2026-08-04">1.256.0 — 2026-08-04<a href="http://localhost/release-notes/rc4#12560--2026-08-04" class="hash-link" aria-label="Direct link to 1.256.0 — 2026-08-04" title="Direct link to 1.256.0 — 2026-08-04" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-5">Added<a href="http://localhost/release-notes/rc4#added-5" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class="">
<p><strong>Domain summary &amp; counts API (DUW-1.3, private-suite#2570)</strong> — The workspace
tree draws a badge on every domain folder before anything is hydrated —
<code>customers/ 3·4</code>, <code>billing/ 5·9</code>, <code>shared/ 8</code> — and each lens badges the same
folder with a different number (<code>3 classes</code> in the schemas lens, <code>4 ops</code> in the
paths lens). Deriving any of that in the browser would mean fetching the whole
catalog first, which is the read DUW-1.2 exists to eliminate.
<code>GET /v1/workspace/{tenant}/version/{version_id}/summary</code> answers it in one
round trip: every folder with <code>class_count</code>, <code>path_count</code>, <code>op_count</code> and
<code>enum_count</code>, plus shallow member lists — class rows (id, name, kind, version
badge) and path rows carrying their operations (verb, <code>operation_id</code>,
<code>summary</code>, <code>deprecated</code>) — which is every field the three tree lens panels
draw.</p>
<p>Counts are exhaustive; member lists are not. A badge that is only right for the
first page is not a badge, so each count covers the folder's whole membership,
while the rows beside it are capped per folder by <code>member_limit</code> (default 50,
clamped to 200, <code>0</code> for badges alone) and a folder that was cut reports
<code>classes_truncated</code> / <code>paths_truncated</code>, continuing through DUW-1.2's paged
reads. <code>class_count</code> and <code>enum_count</code> <em>partition</em> a folder's classes rather
than overlapping, matching the mockup's <code>customers/ 3 classes</code> above three
objects and one enum; each row carries a <code>kind</code> of <code>object</code>/<code>enum</code>/<code>union</code> read
from the stored schema column, so the <code>Schemas</code> and <code>Enums &amp; unions</code> groups
need no second pass, and objects sort first so a truncated list is cut from the
enum group upward. The <code>v2.1</code> badge is the version's own label repeated per
class row — a class has no version of its own — so a tree row renders without
consulting the envelope.</p>
<p>The cost is four statements whatever the version holds: a window function
carries each domain's totals onto its own member rows, so a 40-folder catalog
costs what a one-folder catalog does. A per-domain count query would be exactly
the N+1 this endpoint exists to prevent, and would pass every functional test.
The <code>shared/</code> bucket is joined with <code>IS NOT DISTINCT FROM</code>, because <code>NULL = NULL</code> would silently drop the largest folder in most catalogs, and empty
folders are listed with zeroes so a newly created one cannot look like a failed
write. Reads commit, matching the scoped reads: psycopg2 opens a transaction
for a bare SELECT too. Verified against a seeded 218-path / 250-class catalog
under <code>pg_virtualenv</code> — every badge checked against its own <code>SELECT COUNT(*)</code>,
the mockup's numbers reproduced, and p95 ≈ 3.5 ms against the ticket's 300 ms
budget. No migration: V242's <code>domain_id</code> indexes and the existing <code>version_id</code>
indexes cover the aggregates. The per-path schema rows the combined lens nests
under an operation remain DUW-1.4.</p>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12550--2026-08-04">1.255.0 — 2026-08-04<a href="http://localhost/release-notes/rc4#12550--2026-08-04" class="hash-link" aria-label="Direct link to 1.255.0 — 2026-08-04" title="Direct link to 1.255.0 — 2026-08-04" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-6">Added<a href="http://localhost/release-notes/rc4#added-6" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class=""><strong>Selection-scoped class and path reads (DUW-1.2, private-suite#2569)</strong> — The
only way to read a version's classes with their properties and tags was
<code>GET /v1/classes/{tenant}/version/{version_id}/with-properties-tags</code>, three
queries with no LIMIT that return the whole catalog. Twelve designer call
sites use it, <code>/editor</code> fires it twice on mount and again after every
single-class edit, and that is the direct cause of the browser choking on a
large catalog. <code>/v1/workspace/{tenant}/version/{version_id}/classes</code> and
<code>…/paths</code> answer the question the canvas actually asks: <em>these items</em>, or
<em>this folder</em>, never <em>this version</em>. A selection is mandatory — omitting both
<code>class_ids</code> and <code>domain_id</code> is a 400 rather than a convenience default,
because that default would be the very read this endpoint exists to replace,
and supplying both is a 400 too rather than a guess about which one wins. The
server cap is enforced two different ways for one reason: a page size over 200
is clamped, since a domain listing hands back a cursor and the client has a
working continuation, while an id list over 200 is refused, since there is no
cursor for an arbitrary id set and quietly answering a different question than
the one asked would be undetectable. Both bounds are echoed in every response
and documented in the OpenAPI parameter descriptions, so a client never has to
trigger a 400 to discover them. A bounded read is still a bulk read: three
statements hydrate a page of classes and two hydrate a page of paths, whatever
the page size, so cost tracks the selection rather than the catalog. Ids that
no longer resolve — a class deleted since the selection was made, one
belonging to another version, one that is not a UUID at all — come back in
<code>missing_ids</code> rather than as a silently short response that would leave an
unexplained hole on the canvas. Every query is scoped by <code>version_id</code> even in
id mode, which is the tenancy boundary: the version is resolved against the
caller's tenant first, so an id from another tenant's catalog matches nothing.
<code>total</code> is the size of the whole selection rather than of the page, which is
what the workspace sizes its node budget against, and pagination reuses the
same opaque cursor format the export and import manifest surfaces already
speak. Paths carry their operations with each operation's <code>operation_id</code>,
<code>summary</code> and <code>deprecated</code> flag, because the mockup's paths lens draws the
operationId beside every verb; parameters, request bodies and responses stay
with the per-path <code>/full</code> endpoint, which is inspector-sized data for one
selected operation. The legacy full-version read is deliberately unchanged —
exports, scoring and readiness sweeps really do want every class — but now
carries a deprecation note pointing here. No migration: V242's <code>domain_id</code>
indexes were added for exactly these reads.</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12540--2026-08-04">1.254.0 — 2026-08-04<a href="http://localhost/release-notes/rc4#12540--2026-08-04" class="hash-link" aria-label="Direct link to 1.254.0 — 2026-08-04" title="Direct link to 1.254.0 — 2026-08-04" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-7">Added<a href="http://localhost/release-notes/rc4#added-7" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class=""><strong>Domain folders for schemas and paths (DUW-1.1, private-suite#2568)</strong> — The
unified workspace organizes a catalog into domain folders and scopes the canvas
to one of them, but classes and paths had no hierarchy at all: two flat lists
read with <code>ORDER BY name ASC</code>. Tags and canvas groups are not that hierarchy —
tags are project-scoped, many-to-many and for filtering; canvas groups are
per-layout visual furniture. A folder that scopes a <em>fetch</em> has to be exactly
one per item, version-scoped, and stored beside the item it groups, so V242
(<code>apiome-db</code>) adds <code>apiome.domains</code> plus a nullable <code>domain_id</code> on
<code>apiome.classes</code> and <code>apiome.version_path</code>. <code>/v1/domains</code> lists, creates,
renames and deletes them, and moves a class or a path between them.
<code>shared/</code> is deliberately <strong>not</strong> a row: a member with <code>domain_id IS NULL</code> <em>is</em>
in it, so the bucket always exists, cannot be renamed or deleted, and is
synthesized into the list response with <code>id: null</code> and <code>virtual: true</code>. The
slug <code>shared</code> is reserved by a CHECK so no stored domain can draw the same
folder. Deleting a domain never deletes its contents — the delete is a soft
delete, and V242's <code>trg_domains_soft_delete_release</code> releases every member to
<code>shared/</code> in the same statement, with <code>ON DELETE SET NULL</code> covering a hard
delete too; the response reports how many classes and paths moved. A database
trigger, not a service-layer check, rejects a domain assignment that crosses
versions or targets a deleted domain, because a foreign key can constrain
<code>domain_id</code> but knows nothing about <code>version_id</code> on either side. Existing
catalogs are backfilled: paths seed domains from their first <em>meaningful</em> path
segment — skipping templated segments (<code>/{customerId}</code> names an instance) and
API version prefixes (<code>/v1/</code> is on every path, so it partitions nothing) — and
classes follow a project tag whose name slugifies to a seeded domain. Anything
unmatched stays in <code>shared/</code>, which is the honest outcome for a catalog with no
path structure and no tags. Per-domain counts for the tree badges are DUW-1.2 /
DUW-1.3, not this release.</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12530--2026-08-03">1.253.0 — 2026-08-03<a href="http://localhost/release-notes/rc4#12530--2026-08-03" class="hash-link" aria-label="Direct link to 1.253.0 — 2026-08-03" title="Direct link to 1.253.0 — 2026-08-03" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-8">Added<a href="http://localhost/release-notes/rc4#added-8" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class=""><strong>Slate custom domains + DNS/TLS (Slate 10.1, private-suite#119)</strong> — The
editing half of the domain inventory APX-3.1 could only report, under
<code>/v1/slate</code>: attach a hostname to a lane and get back the exact DNS rows to
publish, verify ownership against the tenant's live records, probe the host to
see what certificate it is actually serving, make a host canonical, park
renewal, and detach. A subdomain is delegated with one CNAME; an apex is
proven with a TXT record and pointed with ALIAS/ANAME, because RFC 1034
forbids a CNAME beside the SOA and NS records every apex carries. A failed
check reports what the resolver found, not merely that it failed.
<code>app.slate_dns</code> is a dependency-free DNS client (the stdlib resolver discards
the CNAME chain and every TXT record — the two things verification needs);
<code>app.slate_tls_probe</code> completes a verified TLS handshake and reads the peer
certificate, so every certificate field is an observation of the live host at
a stated instant and a renewal is <em>detected</em> (the serial changes) rather than
assumed. Nothing here issues, stores or renews a certificate: the edge does
(<code>deploy/Caddyfile</code>, Caddy on-demand TLS against Let's Encrypt), and
<code>GET /v1/slate/tls/authorize?domain=</code> is the gate it asks first — a single
conjunction (row exists, ownership verified, renewal on), unauthenticated
because the caller is a TLS handshake with no session to present. Requires
V241 (<code>apiome-db</code>), which adds the verification/certificate lifecycle columns
and CHECKs that make "verified with no timestamp" and "active with no expiry"
unrepresentable. New settings: <code>APIOME_SLATE_DOMAIN_DNS_TARGET</code>,
<code>APIOME_SLATE_DOMAIN_RESERVED_ZONE</code>,
<code>APIOME_SLATE_DOMAIN_VERIFICATION_SECRET</code> (fails closed in production).</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12520--2026-08-03">1.252.0 — 2026-08-03<a href="http://localhost/release-notes/rc4#12520--2026-08-03" class="hash-link" aria-label="Direct link to 1.252.0 — 2026-08-03" title="Direct link to 1.252.0 — 2026-08-03" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-9">Added<a href="http://localhost/release-notes/rc4#added-9" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class=""><strong>Snippet service (SDK-2.3, <a href="https://github.com/apiome/apiome/issues/4487" target="_blank" rel="noopener noreferrer" class="">#4487</a>)</strong> — Per-operation usage snippets
(install + call code) rendered server-side from the persisted canonical
model, as the single source of truth for the browse operation pages
(SDK-3.3) and the Try It copy-as-code feature (SIM-3.5). Two surfaces share
one pure renderer (<code>app.snippet_render</code>):
<code>GET /v1/versions/{tenant_slug}/{project_id}/{version_record_id}/snippets/{operation_id}?lang=</code>
(authenticated, published revisions only) and
<code>GET /v1/browse/tenants/{t}/projects/{p}/versions/{v}/snippets/{operation_id}?lang=</code>
(anonymous, published+public with uniform 404s, sharing the public-export
rate limit). Languages: <code>ts</code> (built-in <code>fetch</code>), <code>python</code> (<code>httpx</code>, with a
<code>pip install httpx</code> install line), and <code>curl</code>, plus browse-vocabulary
aliases <code>fetch</code>/<code>httpx</code>. Output shape, escaping, and <code>$API_KEY</code>-style
secret placeholders mirror the client-side Try It generators; request
bodies are minimal valid instances synthesized deterministically from the
payload schema, so responses are content-addressed (<code>ETag</code> / 304). The
structured response carries the resolved operation, the synthesized
request, and a placeholder inventory so consumers need no post-processing.
Snippets derive from the canonical spec directly — the original SDK-2.1/2.2
template dependency was dropped when those tickets were cancelled.</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12510--2026-08-02">1.251.0 — 2026-08-02<a href="http://localhost/release-notes/rc4#12510--2026-08-02" class="hash-link" aria-label="Direct link to 1.251.0 — 2026-08-02" title="Direct link to 1.251.0 — 2026-08-02" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-10">Added<a href="http://localhost/release-notes/rc4#added-10" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class=""><strong>WIT (WebAssembly Component Model) import (IXH-7.9, <a href="https://github.com/apiome/apiome/issues/5134" target="_blank" rel="noopener noreferrer" class="">#5134</a>)</strong> — A new <code>wit</code>
<code>ImportSource</code> adapter makes WIT packages importable (file, URL, paste, or a
multi-file package fileset). Worlds and interfaces normalize to canonical
services on the RPC paradigm, functions to operations (a top-of-return
<code>result&lt;ok, err&gt;</code> becomes the RESPONSE/ERROR message pair; <code>option&lt;t&gt;</code> maps to
canonical nullability, <code>list&lt;t&gt;</code> to list nesting), and the WIT type system to
canonical types: <code>record</code> → RECORD, <code>enum</code> → ENUM, <code>variant</code> → UNION with case
payloads preserved, <code>flags</code> → ENUM with bitset semantics flagged, <code>type</code>
aliases → ALIAS, <code>resource</code> → RECORD carrying its constructor and methods in
extras.</li>
<li class=""><strong>Cross-file <code>use</code> resolution</strong> — Archive/git filesets merge every <code>.wit</code>
member into one package, so <code>use iface.{type}</code> statements resolve against
sibling files; a <code>use</code> naming another package is recorded as an external
reference (<code>inferred</code> / <code>source_incomplete</code> ledger row), never fabricated or
dropped.</li>
<li class=""><strong>Capability limits, never silent drops</strong> — Constructs the canonical model
cannot hold (resources with methods, <code>borrow&lt;…&gt;</code> handle semantics, tuples,
nested results, <code>stream</code>/<code>future</code> wrappers) are preserved in extras and
reported on the import preview coverage ledger as <code>partially-mapped</code>
capability limits; declared parser limits (<code>include</code> expansion, secondary
nested package blocks) carry <code>not-parsed-by-adapter</code> registry entries.</li>
<li class=""><strong>Corpus ladder</strong> — Full six-rung WIT corpus (minimal, typical calculator,
world composition, type-system stress, WASI-style key-value real-world, and a
multi-file package set), a five-class negative tier, golden snapshots,
round-trip matrix rows, and the lint capability matrix / catalog format
registry entries.</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12500--2026-08-02">1.250.0 — 2026-08-02<a href="http://localhost/release-notes/rc4#12500--2026-08-02" class="hash-link" aria-label="Direct link to 1.250.0 — 2026-08-02" title="Direct link to 1.250.0 — 2026-08-02" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-11">Added<a href="http://localhost/release-notes/rc4#added-11" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class=""><strong>Gateway configuration import (IXH-7.8, <a href="https://github.com/apiome/apiome/issues/5133" target="_blank" rel="noopener noreferrer" class="">#5133</a>)</strong> — Two new <code>ImportSource</code>
adapters make gateway configs importable: <code>kong</code> (Kong declarative / deck
YAML-JSON, single file or split fileset) and <code>gateway-api</code> (Kubernetes Gateway
API <code>HTTPRoute</code> manifests, single document, multi-document stream, or manifest
directory). Routes normalize to canonical REST operations — hosts, path
patterns (regex paths become inferred <code>{param}</code> templates with the original
pattern preserved as evidence), methods, header/query matches, and backends.
Kong auth plugins map to canonical security where a mapping exists
(<code>key-auth</code> → apiKey, <code>jwt</code> → bearer, <code>oauth2</code>, <code>basic-auth</code>, <code>mtls-auth</code>,
<code>openid-connect</code>) and are preserved as unmapped hints otherwise; Gateway API
filters are preserved verbatim in extras.</li>
<li class=""><strong>Schema absence as a capability limit</strong> — Gateway configs carry no
request/response schemas, so both formats route to the catalog as
non-publishable with the reason stated (supply schemas and convert to
promote), and the import preview coverage ledger reports the missing schemas
as <code>inferred</code> / <code>source_incomplete</code> — a capability limit of the source
format, never a drop.</li>
<li class=""><strong>Credential hygiene</strong> — Kong consumer credentials (key-auth keys, basic-auth
passwords, JWT secrets) and secret-shaped plugin config values are redacted at
parse time (counts retained, values never imported); <code>kong</code> joins the
always-enforced intake secret-scrub formats, and a <code>secrets-kong.yaml</code>
adversarial fixture guards the pipeline end to end.</li>
<li class=""><strong>Corpus ladder</strong> — Full six-rung corpus for both formats (single-service,
multi-service, and plugin-heavy Kong configs; single-route, multi-document,
and filter-heavy HTTPRoute manifests, plus split-file/manifest-directory
filesets), five-class negative tiers, golden snapshots, and round-trip matrix
rows.</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12490--2026-08-02">1.249.0 — 2026-08-02<a href="http://localhost/release-notes/rc4#12490--2026-08-02" class="hash-link" aria-label="Direct link to 1.249.0 — 2026-08-02" title="Direct link to 1.249.0 — 2026-08-02" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-12">Added<a href="http://localhost/release-notes/rc4#added-12" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class=""><strong>OpenAPI Overlay 1.0 pre-processor (IXH-7.7, <a href="https://github.com/apiome/apiome/issues/5132" target="_blank" rel="noopener noreferrer" class="">#5132</a>)</strong> — The OpenAPI adapter now
resolves a base document plus one or more Overlay Specification 1.0 documents at
import time, with per-value provenance (<code>app/openapi_overlay.py</code>).
<ul>
<li class=""><strong>Action semantics</strong>: <code>update</code> deep-merges into object targets (nested objects
merge recursively; primitives and arrays replace), <strong>appends</strong> to array targets,
and replaces primitive targets in place; <code>remove: true</code> deletes the selected
nodes (list indices deleted highest-first so survivors never shift under the
removal). Targets are JSONPath, evaluated through the custom-rule DSL's hardened
Spectral-compatible parser (<code>parse_jsonpath_expression</code>, now public).</li>
<li class=""><strong>Fileset intake</strong>: the adapter accepts multi-document filesets
(<code>InputKind.FILESET</code>) — members classified by version marker (exactly one
<code>openapi</code>/<code>swagger</code> base; every <code>overlay: 1.x</code> member applied in member-path
order, each seeing the previous one's result, so a chain's last writer wins);
unclassified members (e.g. <code>$ref</code> targets) ride along untouched and are listed
as ignored.</li>
<li class=""><strong>Per-value provenance</strong>: each set/replaced/appended/removed value is recorded
(JSON Pointer, kind, contributing overlay, action index, target expression) on
the canonical model's <code>extras["overlay"]</code>, rendered by the import preview
coverage ledger as document-scoped <code>mapped</code> rows — capped at 500 records with a
declared-truncation row, never a silent cut.</li>
<li class=""><strong>Bare overlay prompt</strong>: a lone overlay document is <em>detected</em> (claimed at 0.9,
no format pinned) and rejected with new taxonomy code
<code>INPUT_OVERLAY_BASE_MISSING</code>, whose remediation prompts for the base document —
instead of an obscure parse error. A fileset with overlays but no base gets the
same code.</li>
<li class=""><strong>Findings, not silence</strong>: actions whose target matches nothing, or that are
structurally unusable (no target, neither <code>update</code> nor <code>remove</code>, invalid
JSONPath, type-mismatched update, root removal), surface as new registered
warning rules <code>intake.overlay-unmatched-target</code> / <code>intake.overlay-action-invalid</code>
merged into the import lint report (tenant-governable like any registered rule).</li>
<li class=""><strong>Corpus ladder</strong>: <code>openapi/34-overlay-basic-set/</code> (add + update + remove in one
overlay), <code>openapi/35-overlay-chain-set/</code> (two-overlay chain with a last-writer
override), and negative <code>openapi/negative/06-bare-overlay.yaml</code>
(<code>INPUT_OVERLAY_BASE_MISSING</code>), with canonical goldens; the openapi <code>multi-file</code>
rung waiver is retired.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12480--2026-08-02">1.248.0 — 2026-08-02<a href="http://localhost/release-notes/rc4#12480--2026-08-02" class="hash-link" aria-label="Direct link to 1.248.0 — 2026-08-02" title="Direct link to 1.248.0 — 2026-08-02" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-13">Added<a href="http://localhost/release-notes/rc4#added-13" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class=""><strong>GraphQL Federation supergraph and subgraph import (IXH-7.6, <a href="https://github.com/apiome/apiome/issues/5131" target="_blank" rel="noopener noreferrer" class="">#5131</a>)</strong> — The GraphQL
adapter is now composition-aware: a supergraph SDL and a multi-file subgraph set both
import with per-type / per-field subgraph ownership carried through the canonical model
(<code>app/graphql_federation.py</code>, docs in <code>docs/graphql_federation.md</code>).
<ul>
<li class=""><strong>Ownership</strong>: supergraph ownership is read off the Apollo <code>join</code>-spec directives
(<code>@join__type</code> / <code>@join__field</code>, <code>external: true</code> references excluded); a subgraph
set derives ownership from file boundaries (<code>@external</code> stubs excluded). Recorded as
<code>extras["federation"]</code> on the artifact and <code>extras["subgraphs"]</code> on every owned
type/field/service/operation, so ownership participates in the fingerprint.</li>
<li class=""><strong>Subgraph SDL builds bare</strong>: real-world subgraph files apply <code>@key</code>/<code>@shareable</code>/
<code>@link</code> without defining them; the parser injects exactly the missing Federation v2
definitions before <code>validate_sdl</code> (author definitions never overridden).</li>
<li class=""><strong>Diff attribution</strong>: <code>GraphQlDiffLabeler</code> — the first provider on the MFI-3.x
<code>DiffLabeler</code> SPI — labels every change with its owning subgraph(s)
(<code>owned by subgraph 'reviews'</code>, <code>subgraph ownership: products → reviews</code>).</li>
<li class=""><strong>Composition lint dimension</strong>: new <code>composition</code> category in the GraphQL rule pack —
<code>graphql.composition-invalid-key</code>, <code>graphql.composition-non-shareable-field</code>,
<code>graphql.composition-unresolvable-selection</code> (pure checks over the subgraph set), and
<code>graphql.composition-error</code> surfacing the bundled <code>rover supergraph compose</code> verdict
captured at import time (worker-loop bridge; degrades to "no verdict" when the tool
or its composition plugin is unavailable). Every finding names the offending
subgraph. Federation spec-machinery names (<code>join__Graph</code>, …) are exempt from the
GraphQL naming rules.</li>
<li class=""><strong>Directive preservation</strong>: applied directives are no longer stripped —
<code>print_schema_with_directives</code> restores them on the parser's canonical SDL and the
normalizer's <code>raw["sdl"]</code>, and the emitter rebuilds custom directive definitions
(<code>extras["directive_definitions"]</code>) as real <code>GraphQLDirective</code>s and re-attaches the
per-entity <code>extras["directives"]</code> applications onto the printed SDL with validation
fallback. A GraphQL→GraphQL supergraph round-trip is canonical-diff clean.</li>
<li class=""><strong>Corpus ladder</strong>: <code>graphql/13-federation-set/</code> (products/reviews/inventory,
<code>multi-file</code> rung) and <code>graphql/14-federation-supergraph.graphql</code> (<code>composition</code>
rung), with canonical goldens.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12470--2026-08-02">1.247.0 — 2026-08-02<a href="http://localhost/release-notes/rc4#12470--2026-08-02" class="hash-link" aria-label="Direct link to 1.247.0 — 2026-08-02" title="Direct link to 1.247.0 — 2026-08-02" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-14">Added<a href="http://localhost/release-notes/rc4#added-14" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class=""><strong>Protobuf descriptor set / buf image binary intake (IXH-7.5, <a href="https://github.com/apiome/apiome/issues/5130" target="_blank" rel="noopener noreferrer" class="">#5130</a>)</strong> — Real gRPC
deployments distribute a serialized <code>FileDescriptorSet</code> (or a buf image), not a <code>.proto</code>
source tree; the gRPC adapter now imports that artifact directly.
<ul>
<li class=""><strong>Binary parse seam</strong> (<code>ImportSource.accepts_bytes</code> / <code>parse_bytes</code>): the import
pipeline consults the adapter before decoding an upload to text and routes claimed
binary payloads to <code>parse_bytes</code> under the same IXH-6.5 size/time/memory stage guards
(the raw-bytes ceiling applies before the parse ever runs). Text-only adapters are
unaffected (default declines).</li>
<li class=""><strong>gRPC adapter</strong>: <code>parse_bytes</code> decodes a <code>FileDescriptorSet</code> / buf image with the
pure MFI-9.1 read layer — dependencies resolve from within the set, with no
filesystem, network, or <code>buf</code> toolchain access — and feeds the existing Protobuf
normalizer. Payloads are claimed by content sniff (<code>sniff_file_descriptor_set</code>) or by
conventional suffix (<code>.binpb</code>/<code>.desc</code>/<code>.protoset</code>), so malformed descriptor uploads
fail with descriptor-specific taxonomy codes (<code>INPUT_MALFORMED</code>, or <code>INPUT_TRUNCATED</code>
when the wire stream is cut off mid-element via a top-level wire walk) instead of
<code>INPUT_ENCODING_INVALID</code>. The Connect-RPC adapter delegates the same seam.</li>
<li class=""><strong>Detection</strong>: <code>DetectionInput</code> gains optional undecoded <code>data</code> bytes; the gRPC
adapter and the registry sniffer claim descriptor-set bytes at 0.9 confidence, so
binary uploads auto-detect and pre-flight routes them to the <code>grpc</code> importer.</li>
<li class=""><strong>Paired corpus contract</strong>: <code>protobuf/07-inventory-source.proto</code> and the descriptor
set / buf image compiled from it (<code>08</code>/<code>09-*.binpb</code>) must import to the same
canonical model and fingerprint; binary negatives assert the truncated/malformed
codes through the real pipeline. The corpus harness reads binary entries as bytes and
drives <code>parse_bytes</code>.</li>
<li class="">gRPC server reflection discovery through the SSRF-guarded fetcher already shipped in
MFI-9.3 and is unchanged; <code>parse_bytes</code> completes the pairing by importing the same
descriptor bytes reflection returns.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12460--2026-08-02">1.246.0 — 2026-08-02<a href="http://localhost/release-notes/rc4#12460--2026-08-02" class="hash-link" aria-label="Direct link to 1.246.0 — 2026-08-02" title="Direct link to 1.246.0 — 2026-08-02" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-15">Added<a href="http://localhost/release-notes/rc4#added-15" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class=""><strong>Import/export observability — stage timings, failure reasons, metrics (IXH-6.6, <a href="https://github.com/apiome/apiome/issues/5125" target="_blank" rel="noopener noreferrer" class="">#5125</a>)</strong> —
Diagnosing "imports are slow for one tenant" meant reading logs; now the two job pipelines
emit aggregate metrics and a correlation id that survives from the request to every log line.
<ul>
<li class=""><strong>Metrics</strong> (<code>app/import_export_metrics.py</code>, in-process/per-replica like the rest of the
<code>/v1/ops/metrics</code> plane): per-stage duration histograms + byte totals, terminal job totals
keyed adapter/target × format × outcome, and failure counters keyed by the IXH-6.4
taxonomy code. Every tag comes from a closed vocabulary (registered adapters/targets, the
engine stage names, the two taxonomies) with out-of-vocabulary values clamped to <code>other</code> —
<strong>no per-tenant or per-job tags exist</strong>. Each record also emits one structured log line
(<code>import_export.stage|job|failure</code>).</li>
<li class=""><strong>Stage timings</strong>: the in-process import pipeline now emits <code>PHASE_TIMING</code> events (the
exact shape the tsx worker always emitted, including a <code>failed</code> outcome for interrupted
stages), ingested into the metrics at the engine's event-dedupe seam so both paths feed
the same aggregates; the export engine times its five stages at the <code>_publish</code> funnel.
Timing events persist per job inside <code>async_job.status</code>, so durable evidence survives
restarts even though the aggregates are per-replica.</li>
<li class=""><strong>Correlation id (additive <code>correlation_id</code> fields)</strong>: captured from the middleware's
<code>X-Request-ID</code> at schedule time, stamped onto every stored import/export job status and —
structurally — onto <code>SpecImportJobError</code>/<code>ExportJobError</code>, bound into every job log line
(explicitly on the export engine's thread loop), and therefore returned to the caller on
failure: the 202's <code>X-Request-ID</code> equals every subsequent poll's <code>correlation_id</code>.</li>
<li class=""><strong>Operator view</strong>: new <code>GET /v1/ops/import-export</code> (platform-admin) rendering the
aggregates plus the complete documented tag set; <code>/v1/ops/metrics</code>/<code>status</code> carry the
snapshot under <code>import_export</code>; the ops dashboard gains <em>Import/Export jobs</em> and
<em>Import/Export failures</em> cards. Documented in <code>docs/import_export_observability.md</code>.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12450--2026-08-02">1.245.0 — 2026-08-02<a href="http://localhost/release-notes/rc4#12450--2026-08-02" class="hash-link" aria-label="Direct link to 1.245.0 — 2026-08-02" title="Direct link to 1.245.0 — 2026-08-02" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-16">Added<a href="http://localhost/release-notes/rc4#added-16" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class=""><strong>Saved schema test suites and regression tracking (IXH-5.7, <a href="https://github.com/apiome/apiome/issues/5119" target="_blank" rel="noopener noreferrer" class="">#5119</a>)</strong> —
A payload validated once is worth keeping. <code>/v1/tenants/{tenant}/schema-suites</code> persists
named, tenant-scoped suites — payloads plus expected verdicts in the IXH-1.1
<code>validity_class</code> vocabulary — attached to a stable schema reference that survives
revisions (<code>{kind}/{artifact}[/{type}]</code>; a 5.1-shaped reference's version segment is
discarded; <code>registry/…</code> is rejected because it has no revisions to regress across).
<ul>
<li class=""><strong>Runs</strong> (<code>POST …/{id}/runs</code>) execute every payload through the IXH-5.1 validator
against one revision — resolved once and pinned, so a moving <code>latest</code> cannot split a
run — judge each verdict exactly like the CLI (<code>passed</code>/<code>failed</code>/<code>error</code>), and record
the run plus per-payload results (apiome-db V240). An unresolvable reference records a
<code>status: error</code> run: that history is the product, not an exception.</li>
<li class=""><strong>Regression tracking</strong>: each result is diffed by payload name against the suite's
previous completed run, whatever revision it targeted. <code>passed → failed</code> flags the
result and the run; <code>passed → error</code> deliberately does not (no verdict was produced),
staying visible via <code>previous_status</code>. Listings carry each suite's newest run summary
so the catalog and version detail surfaces can badge regressions from one query.</li>
<li class=""><strong>Corpus round trip</strong>: <code>GET …/{id}/export</code> produces an IXH-1.1 corpus manifest plus
payload files, directly consumable by <code>apiome schema test --suite</code> once materialized;
<code>POST …/schema-suites/import</code> reads the same envelope back losslessly.</li>
<li class=""><strong>Bounded and documented</strong> (<code>docs/schema_test_suites.md</code>): payloads per suite
(<code>APIOME_SCHEMA_SUITE_MAX_PAYLOADS</code>, 50), 256 KiB per payload (V240 CHECK), findings
per result (<code>APIOME_SCHEMA_SUITE_RESULT_FINDINGS_CAP</code>, 20); run history pruned on
write beyond <code>APIOME_SCHEMA_SUITE_RUN_MAX_PER_SUITE</code> (200) and by age on the IXH-6.3
retention tick (<code>APIOME_SCHEMA_SUITE_RUN_RETENTION_DAYS</code>, 180) — always keeping each
suite's newest <code>APIOME_SCHEMA_SUITE_RUN_KEEP_MIN</code> (20) so a rarely-run suite never
loses its regression baseline.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12440--2026-08-02">1.244.0 — 2026-08-02<a href="http://localhost/release-notes/rc4#12440--2026-08-02" class="hash-link" aria-label="Direct link to 1.244.0 — 2026-08-02" title="Direct link to 1.244.0 — 2026-08-02" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-17">Added<a href="http://localhost/release-notes/rc4#added-17" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class=""><strong>Cross-format schema conformance, canonical → target (IXH-5.6, <a href="https://github.com/apiome/apiome/issues/5118" target="_blank" rel="noopener noreferrer" class="">#5118</a>)</strong> —
Fidelity reporting describes <em>structural</em> loss; it never answered the question that
actually breaks a consumer at runtime: does a payload that is valid against the source
schema remain valid against the emitted target schema? <code>app/cross_format_conformance.py</code>
answers it empirically, per emit target and per entity.
<ul>
<li class="">For every target with a validatable schema language — JSON Schema
(<code>validate_json_instance</code>), Avro (<code>fastavro</code>), protobuf (<code>buf</code> compile +
<code>json_format.ParseDict</code>), GraphQL input types (<code>graphql-core</code> input coercion), and XSD
(<code>xmllint</code>) — the IXH-5.2 source-valid instances (minimal, full, branch; never mutants)
are validated against the <strong>actually emitted</strong> schema. Failures are reported per entity
with the target-side constraint that rejected the instance.</li>
<li class=""><strong>Wire-format transcoding is explicit</strong> (<code>app/conformance_transcoding.py</code>): base64 →
Avro binary, canonical-model-driven JSON → XML documents mirroring the emitted XSD
grammar, and the proto3 canonical JSON mapping. Transcode failures are a separate
failure kind — they never masquerade as a pass or a conformance verdict.</li>
<li class="">Targets without a validatable schema language are reported <strong>not applicable, never
passing</strong>; a missing toolchain (<code>buf</code>, <code>xmllint</code>) reports <em>not validated</em> with the
reason, mirroring the <code>export_validation</code> honesty contract.</li>
<li class=""><strong>Feeds the IXH-2.4 readiness rank</strong>: <code>POST …/export/preflight</code> accepts
<code>include_conformance</code>, attaches each target's verdict beside its structural fidelity
envelope, demotes a <code>ready</code> target to <code>caution</code> when its emitted schema rejected
source-valid instances, re-ranks, and refreshes the ranking fingerprint.</li>
<li class="">Covered across the IXH-1.7 grid: every corpus source-format representative × every
production emit target asserts the applicability split and that no target ever reads
as passing without instances actually judged.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12430--2026-08-02">1.243.0 — 2026-08-02<a href="http://localhost/release-notes/rc4#12430--2026-08-02" class="hash-link" aria-label="Direct link to 1.243.0 — 2026-08-02" title="Direct link to 1.243.0 — 2026-08-02" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-18">Added<a href="http://localhost/release-notes/rc4#added-18" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class=""><strong>On-demand export round-trip comparison (IXH-4.4, <a href="https://github.com/apiome/apiome/issues/5112" target="_blank" rel="noopener noreferrer" class="">#5112</a>)</strong> —
The strongest possible answer to "is this export honest?" is empirical: emit the artifact,
re-import it through the matching import adapter, and diff the re-imported canonical model
against the source. The IXH-1.7 conformance matrix proves this in CI over the corpus; the
user had no way to see it for their own document.
<ul>
<li class=""><code>POST /v1/export/{tenant}/roundtrip</code> runs the same loop, on demand, for one
(source revision, target, options): a read-only emit via the dispatch primitive (so the
verdict and the Studio's fidelity surfaces describe one snapshot), re-import through the
matrix's own adapter join, <code>canonical_diff</code> against the source, and the matrix's
<code>reconcile</code> against the fidelity report. Nothing is persisted — no artifact, no job row,
no field-identity rows.</li>
<li class="">Differences come back <strong>grouped</strong>: <code>matched</code> (each explained difference paired with the
fidelity finding covering it — expected loss), <code>unexplained</code>, and <code>overclaims</code> (<code>ok</code>
findings reality contradicts) — the latter two flagging a fidelity bug worth reporting,
with reproduction provenance (model fingerprints + emitter/apiome/registry versions,
never source content) inline.</li>
<li class="">A target with no import adapter is <strong>skipped with the matrix's own explanation</strong>
(<code>status: unsupported</code>), never silently; a re-import failure is reported as a <code>fail</code>
verdict rather than a 500. <code>app/export_roundtrip.py</code> holds the composition; the verdict
vocabulary is the 1.7 matrix's, so Studio results reconcile with the published grid for
corpus entries.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12420--2026-08-02">1.242.0 — 2026-08-02<a href="http://localhost/release-notes/rc4#12420--2026-08-02" class="hash-link" aria-label="Direct link to 1.242.0 — 2026-08-02" title="Direct link to 1.242.0 — 2026-08-02" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-19">Added<a href="http://localhost/release-notes/rc4#added-19" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class=""><strong>Multi-file and archive intake explorer (IXH-3.5, <a href="https://github.com/apiome/apiome/issues/5107" target="_blank" rel="noopener noreferrer" class="">#5107</a>)</strong> —
MFI-29.1/29.2 made a single import dozens of files, but the preview still showed one grade and
one entity tree for all of them: which file failed, which was never read, which import could not
be resolved, and whether the detected entry point was even right were all unanswerable. That is
the failure mode that makes multi-file gRPC imports frustrating.
<ul>
<li class=""><code>POST /v1/tenants/{tenant}/import/bundle-inventory</code> unpacks the candidate through the <em>same</em>
MFI-29.1 archive intake the commit uses and runs the <em>same</em> IXH-2.1 pre-flight the quality step
already ran (so it rides that cached run rather than parsing the bundle twice), then returns per
file: its <strong>role</strong> (entry-point / dependency / unreferenced / ignored — always with the reason —
/ unreadable), its <strong>verdict</strong> plus the parse diagnostic naming it, its resolved
<strong>import/include edges</strong> and incoming references, and the <strong>canonical entities it appears to
contribute</strong>.</li>
<li class="">Every <strong>unresolved</strong> reference lists <em>the search paths that were tried, in order</em>. Imports the
format's own toolchain supplies (protobuf well-known types, Cap'n Proto builtins) resolve as
<code>provided</code> instead of being reported missing.</li>
<li class=""><code>app/intake_bundle_graph.py</code> holds the pure half: a per-suffix directive table (proto, Thrift,
FlatBuffers, Cap'n Proto, TypeSpec, GraphQL, RAML, Avro IDL, JSON/YAML <code>$ref</code>, XSD/WSDL/EDMX),
include-root resolution, role classification, and the declaration-scan attribution — whose
method is carried on the response (<code>attribution</code>) so its evidence quality is never overstated
as parser provenance.</li>
<li class="">Ranked <strong>entry-point candidates</strong> come from the same ranking <code>resolve_fileset_root</code> decides
with; overriding is a plain re-run against <code>archive_root</code>. An ambiguous root and a failed parse
both still return the complete file list.</li>
<li class="">Archive unpack can now report <em>what it skipped and why</em> (<code>unpack_archive_members(ignored=…)</code>),
and its skip normalisation is fixed: <code>lstrip("./")</code> stripped characters, so <code>.git/</code> internals
and a top-level <code>.DS_Store</code> were never actually being skipped despite both rules existing.</li>
<li class="">Bounded: files are cursor-paginated, unresolved references ride the first page with the full
total stated, and a per-process LRU keeps the built inventory so paging never re-unpacks.</li>
<li class="">UI: a <strong>Bundle files</strong> tab in the import wizard's quality step (mounted only for a bundle
candidate) with a windowed ARIA file tree, role legend, per-file detail, unresolved-imports
list, and an entry-point picker that re-runs the whole pre-flight.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12410--2026-08-02">1.241.0 — 2026-08-02<a href="http://localhost/release-notes/rc4#12410--2026-08-02" class="hash-link" aria-label="Direct link to 1.241.0 — 2026-08-02" title="Direct link to 1.241.0 — 2026-08-02" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-20">Added<a href="http://localhost/release-notes/rc4#added-20" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class=""><strong>Quality-rank telemetry and grade drift over revisions (IXH-2.7, <a href="https://github.com/apiome/apiome/issues/5102" target="_blank" rel="noopener noreferrer" class="">#5102</a>)</strong> —
scores were captured per revision but never aggregated across intake, so nobody could see that
a team's imports were trending downward, or that one format consistently graded low — which is
as likely to be an <em>adapter gap</em> as a spec problem, and a per-revision score cannot tell the
two apart.
<ul>
<li class=""><strong>An append-only observation series.</strong> <code>apiome.quality_rank_observations</code> (V239) records one
row every time a grade is produced — an import pre-flight, a committed import, an export
pre-flight ranking, a delivery gate decision — keyed by tenant, format, adapter and
<strong>style-guide version</strong> (the guide's content fingerprint, which is what actually moves a
score), plus the policy version, the gate outcome, and the severity tally.</li>
<li class=""><strong>Attribution, not just grades.</strong> Every observation carries the finding split that separates
what apiome's intake is answerable for from what the specification is: <code>intake.*</code> findings
(an external <code>$ref</code> never resolved or refused) are adapter-attributable, everything else is
spec-attributable and classed by the rule id's namespace. An unrecognised rule is
spec-attributable by construction — the opposite default would blame the adapter for every
new rule anybody adds. The adapter's <em>declared</em> parser limits
(<code>import_preview_manifest.KNOWN_PARSER_LIMITS</code>) ride alongside as a separate count and are
never folded into the finding tallies.</li>
<li class=""><strong>Export readiness in the same series.</strong> An export pre-flight records the readiness composite,
band and rank of its top-ranked targets, so a target whose readiness is sliding shows up
beside the specs feeding it rather than in a second, parallel view.</li>
<li class=""><strong>One read.</strong> <code>GET /v1/lint/workspace/quality-ranks</code> groups the window by <code>(scope, format)</code>
and returns each group's grade distribution, average score, drift (<code>scoreDelta</code>), outcome
tally, attribution split, style-guide versions, and a per-day point series. A day with no
observation is a <strong>gap</strong> (<code>averageScore: null</code>), never a zero. Rendered in the lint workspace
as a new <strong>Quality ranks</strong> tab with a selectable 7/30/90/180-day window.</li>
<li class=""><strong>Bounded by construction.</strong> Recording is best-effort everywhere (telemetry never fails an
import, an export, or a pre-flight); an export pre-flight records only the head of its
ranking rather than all 30-odd targets; the read caps its window at 180 days and its format
count at 24, stating <code>truncated</code> rather than dropping rows silently; and the series is pruned
by <code>APIOME_QUALITY_RANK_RETENTION_DAYS</code> (default 180) on the IXH-6.3 retention sweep tick,
which is already the deployment's retention worker.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12390--2026-08-02">1.239.0 — 2026-08-02<a href="http://localhost/release-notes/rc4#12390--2026-08-02" class="hash-link" aria-label="Direct link to 1.239.0 — 2026-08-02" title="Direct link to 1.239.0 — 2026-08-02" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-21">Added<a href="http://localhost/release-notes/rc4#added-21" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class=""><strong>Corpus provenance, licensing and contributor guide (IXH-1.9, <a href="https://github.com/apiome/apiome/issues/5095" target="_blank" rel="noopener noreferrer" class="">#5095</a>)</strong> —
real-world examples are the corpus's most valuable tier and the easiest to add carelessly:
a third-party spec carries a license the repository must honor, and a payload captured from
a running system carries personal data that must never reach git history. Neither was tracked.
<ul>
<li class=""><strong>Declared origin.</strong> Manifest entries gained <code>origin</code>
(<code>hand-authored</code> | <code>derived</code> | <code>captured</code>, absent means hand-authored), <code>source_url</code> (the
upstream document a derived entry came from) and <code>anonymization</code> (how a captured payload was
scrubbed before commit). Both loaders expose them — <code>corpus_loader.CorpusEntry.effective_origin</code>
plus a <code>load_corpus(origin=…)</code> filter, and the same in <code>apiome-ui/lib/corpus/corpus.ts</code>.</li>
<li class=""><strong>An enforced gate.</strong> <code>scripts/check_corpus_provenance.py</code> is a stdlib-only CI check: every
entry with a non-empty <code>source</code> must declare a <code>license</code>, that license must be on a reviewed
SPDX allowlist (copyleft, share-alike and non-commercial terms fail), <code>origin</code> and <code>source</code>
must agree, derived entries must link their upstream, and captured entries must carry an
anonymization statement under a license the contributor can actually grant. It runs as its own
lightweight workflow (<code>.github/workflows/corpus-provenance.yml</code>) so a manifest-only pull
request is gated even though the corpus lives under <code>apiome-ui/</code>.</li>
<li class=""><strong>The guide.</strong> <code>docs/CORPUS_CONTRIBUTOR_GUIDE.md</code> documents the tiers and the six-rung ladder,
every manifest field, the licensing rules (including what to do when the upstream license is
not acceptable — reconstruct, do not vendor), the anonymization rule for captured payloads,
the end-to-end add-an-example workflow, and the reviewer checklist. The generated examples
README links it and now publishes the corpus's licensing bill of materials by origin.</li>
<li class=""><strong>Tests.</strong> <code>tests/test_corpus_provenance.py</code> fires every rule against a purpose-built bad
entry, asserts the committed corpus is clean, and pins the guide/README linkage.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12380--2026-08-02">1.238.0 — 2026-08-02<a href="http://localhost/release-notes/rc4#12380--2026-08-02" class="hash-link" aria-label="Direct link to 1.238.0 — 2026-08-02" title="Direct link to 1.238.0 — 2026-08-02" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-22">Added<a href="http://localhost/release-notes/rc4#added-22" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class=""><strong>Scale corpus and import/export performance budgets (IXH-1.5, <a href="https://github.com/apiome/apiome/issues/5091" target="_blank" rel="noopener noreferrer" class="">#5091</a>)</strong> —
nothing in the corpus approached the size of the specs teams actually hold, so import and
export timings were unmeasured and a regression in normalization or fidelity analysis would
have shipped unnoticed. There is now a measured, gated scale tier.
<ul>
<li class=""><strong>The corpus.</strong> <code>scripts/generate_scale_corpus.py</code> is a committed spec of six large,
deterministic, <em>valid</em> documents — one per paradigm and one for the mainframe half: a
550-path OpenAPI 3.1 spec, a 1500-method OpenRPC service, a CloudEvents envelope with a
15000-attribute payload, a 900-type Avro snapshot, a 1500-transaction-set X12 interchange,
and a 7500-item COBOL copybook. The bytes are built at test time, so repository size stays
flat (the IXH-1.4 rule). Every fixture stays under the 10 MiB intake ceiling and uses an
adapter with no external toolchain, so none of them can silently skip.</li>
<li class=""><strong>Ten measured stages.</strong> <code>tests/scale_benchmark.py</code> drives each fixture through
<code>parse → normalize → fingerprint → lint → persist</code> and
<code>load-source → analyze-fidelity → emit → validate → package</code>, calling the same functions
the running pipelines call, and records per-stage wall-clock plus peak allocation
(<code>tracemalloc</code>, which is attributable per stage) alongside the process peak RSS. The two
database-straddling stages are measured up to the row write — <code>persist</code> is the source
capture and secret scrub, <code>load-source</code> the full re-parse/re-normalize — because a
Postgres round-trip's cost cannot be attributed to a code change.</li>
<li class=""><strong>Budgets with a margin, not a threshold.</strong> <code>tests/scale/scale_budgets.json</code> is the one
committed baseline, carrying the margins, the noise floors, and the machine it was measured
on. A stage fails only when it is both over <code>baseline × margin</code> <em>and</em> over an absolute
floor, so a 2 ms stage tripling is ignored while a 30 % slowdown of a two-second stage
fails. <code>SCALE_REGRESSION_MARGIN</code> / <code>SCALE_MEMORY_MARGIN</code> override per run, and absolute
ceilings (180 s, 1 GiB per stage) fail regardless of any baseline. Refresh with
<code>pytest tests/test_scale_corpus.py --update-scale-budgets</code>.</li>
<li class=""><strong>Opt-in locally, scheduled in CI.</strong> <code>tests/test_scale_corpus.py</code> runs only under
<code>--scale</code> / <code>RUN_SCALE_SUITE=1</code>; <code>.github/workflows/apiome-rest-scale.yml</code> runs it weekly
and on dispatch, uploading <code>reports/scale-benchmark.json</code> — a machine-readable per-stage
report with each budget, ratio, and the environment measured in. <code>tests/test_scale_harness.py</code>
runs on every PR and keeps the spec, the baseline, and the comparison rules honest between
scheduled runs.</li>
<li class=""><strong>First findings, for IXH-6.5.</strong> Fidelity analysis is the memory hot spot at roughly
300 KiB per canonical type (~265 MiB for the 900-type Avro snapshot); OpenAPI export
validation dominates wall-clock at ~15 s for a 1.5 MiB spec; exporting a revision costs
about what importing it did, because the canonical model is rebuilt from the captured
source. See <code>docs/scale_benchmarks.md</code>.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12370--2026-08-01">1.237.0 — 2026-08-01<a href="http://localhost/release-notes/rc4#12370--2026-08-01" class="hash-link" aria-label="Direct link to 1.237.0 — 2026-08-01" title="Direct link to 1.237.0 — 2026-08-01" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-23">Added<a href="http://localhost/release-notes/rc4#added-23" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class=""><strong>Breaking-publish guardrail (CTG-3.4, <a href="https://github.com/apiome/apiome/issues/4478" target="_blank" rel="noopener noreferrer" class="">#4478</a>)</strong> —
CTG-3.1 made breaking changes visible <em>after</em> publish, but nothing stopped a publisher from
shipping one as a minor or patch bump — the semver violation that destroys consumer trust,
committed with the platform already knowing the change was breaking. Publish now says so
first.
<ul>
<li class=""><strong>The check.</strong> At publish time the head is classified against the previous <strong>published</strong>
revision (the CTG-1.1 taxonomy through the CTG-1.3 changelog builder, so the guardrail lists
exactly what the published changelog will list) and the two version labels are compared for
a semver major bump. Breaking <strong>and</strong> no major bump is the only combination that triggers.
The baseline is resolved independently of the request's change-report baseline mode, so
selecting <code>initial</code> cannot dodge the guardrail, and the check runs <em>after</em> the
<code>allowBreaking</code> gate — a publisher who opted into shipping breaking changes is exactly the
one it exists for.</li>
<li class=""><strong>Tenant policy, on the guide.</strong> <code>style_guides.breaking_publish_policy</code> (migration V237) is
<code>off</code> / <code>warn</code> (default) / <code>block</code>, resolved through the GOV-1.4 chain (project → tenant →
default) and editable at <code>PUT …/style-guides/{tenantSlug}/{guideId}/policy</code> beside the
CLX-1.3 gates. Under <code>block</code>, publish is refused with <code>422</code> carrying the full assessment;
force-publish (<code>skipPublishChecks</code> + reason) gets past it exactly as it does for style-guide
errors, per GOV-2.5. The level is frozen into each GOV-1.6 guide revision, so escalating to
<code>block</code> is auditable history.</li>
<li class=""><strong>Preflight for the dialog.</strong> <code>GET /v1/versions/{tenantSlug}/{projectId}/{versionRecordId}/breaking-publish-guardrail</code>
returns the same payload read-only: status, the breaking changes (capped at 50, with
<code>truncated</code> and a true <code>breakingCount</code>), <code>majorBumped</code>, and the <code>recommendedVersion</code> a
compliant bump would use.</li>
<li class=""><strong>Never fails closed.</strong> Version labels are free-form, so "was the major bumped?" has three
answers — a non-semver label yields <code>null</code>, which warns but never blocks, since that tenant
has not committed a semver violation. Every fault (unbuildable spec, missing baseline, DB
error, unknown policy value) degrades to <code>status: unavailable</code> or the <code>warn</code> default: a
guardrail that failed closed on its own bugs would be worse than the violation it guards
against.</li>
<li class=""><strong>Audited.</strong> Every flagged publish appends a <code>version.breaking_publish_guardrail</code> workflow
audit row with <code>action: warned | forced</code>, the force reason, and the full assessment. The
forced case is assessed after publish, since <code>skipPublishChecks</code> skips the prechecks
wholesale — precisely the case where the trail matters most.</li>
<li class="">Documented in <code>docs/breaking_publish_guardrail.md</code>; 50 tests in
<code>test_breaking_publish_guardrail.py</code> plus guide-policy and revision-snapshot coverage.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12360--2026-08-01">1.236.0 — 2026-08-01<a href="http://localhost/release-notes/rc4#12360--2026-08-01" class="hash-link" aria-label="Direct link to 1.236.0 — 2026-08-01" title="Direct link to 1.236.0 — 2026-08-01" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-24">Added<a href="http://localhost/release-notes/rc4#added-24" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class=""><strong>Guide versioning &amp; audit (GOV-1.6, <a href="https://github.com/apiome/apiome/issues/4432" target="_blank" rel="noopener noreferrer" class="">#4432</a>)</strong> —
style guides were edited in place, so a lint score recorded last month named the guide that
produced it but not <em>what that guide contained</em>: once the guide changed, the result could no
longer be explained or defended. Guides now keep an immutable history, lint results pin the
revision they ran against, and governance changes land in the tenant's audit ledger.
<ul>
<li class=""><strong>A revision per edit, and only per edit.</strong> <code>apiome.style_guide_revisions</code> (migration V236)
is append-only and write-once (the shared V128 UPDATE-forbid trigger). Creating a guide,
renaming it, saving the rule catalog, saving the custom-rules YAML, or changing policy gates
each append one row carrying the guide's whole state — name, description, external lint
profile, every rule row (enable flag, severity, custom definition) and the draft gates —
plus a <code>changeKind</code> and the actor. A save that changed nothing appends nothing: the two
fingerprints on each revision separate "the rules moved" (<code>contentFingerprint</code>) from
"anything moved" (<code>snapshotFingerprint</code>). Assigning a guide changes no content and is
therefore an audit event, not a revision.</li>
<li class=""><strong>Lint results pin their ruleset.</strong> <code>GET …/{versionRecordId}/lint</code> now returns
<code>guideRevisionId</code> alongside <code>guideId</code> / <code>guideName</code>, and every immutable lint evidence row
(<code>lint_evidence_runs.guide_revision_id</code>) records the same pin at capture time, so
import-time scores are as explainable as live recomputes. The pin is exact rather than
heuristic: a revision's <code>contentFingerprint</code> is produced by the <em>same</em> function that stamps
the compiled guide's fingerprint, so matching content is provably the same ruleset.</li>
<li class=""><strong>History is readable, and self-heals.</strong> <code>GET /v1/style-guides/{tenantSlug}/{guideId}/revisions</code>
lists the history newest-first with rule rollups; <code>GET …/revisions/{revisionId}</code> returns the
frozen rules and gates behind any past score. Both are readable by any tenant member —
compliance review is not an admin-only activity. Guides that predate this feature are
captured on first read/edit/lint, and every edit path captures the <strong>pre-edit</strong> state first,
so what an edit replaced is preserved rather than lost.</li>
<li class=""><strong>Audit events on create / edit / assign.</strong> <code>style_guide.created</code>, <code>.updated</code>, <code>.deleted</code>,
<code>.rules_updated</code>, <code>.custom_rules_updated</code>, <code>.policy_updated</code>, <code>.assigned</code> and <code>.unassigned</code>
append to the existing hash-chained <code>apiome.access_audit</code> ledger — one ledger for a
reviewer to read — filterable with <code>GET /v1/access/{tenantSlug}/audit?filter=styleGuide</code>.
Only changes that actually happened are recorded, and history/audit capture is best-effort
by contract: a ledger or capture failure can never fail the guide change it describes.</li>
<li class="">Documented in <code>docs/guide/style-guide-revisions.md</code>; 48 tests across
<code>test_style_guide_revisions.py</code> and <code>test_style_guide_routes.py</code>.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12350--2026-08-01">1.235.0 — 2026-08-01<a href="http://localhost/release-notes/rc4#12350--2026-08-01" class="hash-link" aria-label="Direct link to 1.235.0 — 2026-08-01" title="Direct link to 1.235.0 — 2026-08-01" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-25">Added<a href="http://localhost/release-notes/rc4#added-25" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class=""><strong>Spectral ruleset importer (GOV-1.5, <a href="https://github.com/apiome/apiome/issues/4431" target="_blank" rel="noopener noreferrer" class="">#4431</a>)</strong> —
teams migrating from Stoplight/Redocly arrive with a <code>.spectral.yaml</code> holding years of org
standards. Re-authoring every rule by hand was a real switching cost, so
<code>POST /v1/lint/custom-rules/import</code> now reads that file — pasted/uploaded via <code>content</code>, or
fetched from a <code>url</code> through the existing SSRF-guarded ingestion boundary (256 KiB cap,
redirects re-validated) — and translates it into Apiome governance state. Nothing is
persisted: the response is a review-then-store payload whose <code>yaml</code> is exactly what
<code>PUT /v1/style-guides/{tenantSlug}/{guideId}/custom-rules</code> accepts and whose <code>builtinRules</code>
are exactly what <code>PUT …/rules</code> accepts.
<ul>
<li class=""><strong>Three outcomes, no silent loss.</strong> Every <code>rules.&lt;id&gt;</code> entry of the source lands in exactly
one outcome, so the report accounts for the whole document: <code>builtin</code> (resolved onto the
GOV-1.2 rule catalog via the <code>extends: spectral:oas</code> map — <code>info-description</code>,
<code>operation-description</code>, and the four <code>valid-*-example</code> rules), <code>custom</code> (translated into
the GOV-1.3 DSL and validated <em>by</em> that module, so anything emitted is guaranteed storable
and evaluable), or <code>unsupported</code>.</li>
<li class=""><strong>Unsupported rules say why.</strong> Ten stable reason codes — <code>js_function</code>,
<code>unsupported_function</code> (Spectral's <code>schema</code> / <code>alphabetical</code> / <code>xor</code> / <code>falsy</code> /
<code>unreferencedReusableObject</code>), <code>unsupported_extends</code>, <code>unmapped_builtin</code>, <code>unknown_rule</code>,
<code>unsupported_severity</code>, <code>invalid_definition</code>, <code>malformed_rule</code>, <code>unknown_alias</code>,
<code>rule_limit</code> — each with a human <code>detail</code> and, for DSL rejections, the <code>pointer</code> to the
offending node (<code>rules.my-rule.then.functionOptions.separator</code>).</li>
<li class=""><strong>Lossy translations are declared, not hidden.</strong> A rule that imports while losing something
carries <code>notes</code>: a dropped <code>message</code> template or <code>formats</code> restriction, <code>resolved: false</code>,
<code>severity: hint</code> folded to <code>info</code>, a normalized rule id (an id that would shadow a built-in
becomes <code>imported.&lt;id&gt;</code>). Rules the source turned <strong>off</strong> (<code>off</code> / <code>false</code> /
<code>recommended: false</code>) are reported with <code>enabled: false</code> and deliberately left out of the
emitted YAML, so applying an import never silently switches a rule on. Document-level
<code>notes</code> cover ignored <code>overrides</code>, <code>parserOptions</code>, and unknown top-level keys.</li>
<li class=""><strong>Spectral dialect handling.</strong> Severity tokens (<code>error</code>/<code>warn</code>/<code>info</code>/<code>hint</code>/<code>off</code>,
booleans, numeric <code>DiagnosticSeverity</code> including YAML 1.1 resolving bare <code>off</code> to <code>false</code>),
<code>extends</code> as a scalar/list/<code>[target, modifier]</code> pair (<code>off</code> inherits everything disabled),
and simple <code>aliases</code> (including <code>#Alias.suffix</code> expansion) all resolve.</li>
<li class=""><strong>Acceptance criterion pinned by fixture.</strong> <code>tests/fixtures/spectral/zalando-style.spectral.yaml</code>
— a 27-rule Zalando-style ruleset — imports at <strong>81.5%</strong> coverage, above the ≥70% bar, and
its output round-trips through <code>POST /v1/lint/custom-rules/validate</code>. Documented in
<code>docs/guide/spectral-import.md</code>; 84 tests across <code>test_spectral_import.py</code> and
<code>test_spectral_import_routes.py</code>.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12340--2026-08-01">1.234.0 — 2026-08-01<a href="http://localhost/release-notes/rc4#12340--2026-08-01" class="hash-link" aria-label="Direct link to 1.234.0 — 2026-08-01" title="Direct link to 1.234.0 — 2026-08-01" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-26">Added<a href="http://localhost/release-notes/rc4#added-26" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class=""><strong>Protocol / format facets on public browse (MFI-6.1, <a href="https://github.com/apiome/apiome/issues/3753" target="_blank" rel="noopener noreferrer" class="">#3753</a>)</strong> —
the directory now spans many API description formats, so browsing it needs more than a name
search: a visitor has to be able to ask for "the event-driven ones" or "everything published as
gRPC". Both facet axes read the columns MFI-7.1 put on <code>apiome.versions</code> (<code>protocol</code>,
<code>source_format</code>), backed by that migration's partial facet indexes.
<ul>
<li class=""><strong>Two filters, one vocabulary.</strong> <code>GET /v1/browse/tenants</code> and
<code>GET /v1/browse/tenants/{slug}/projects</code> accept <code>protocol</code> (the canonical <code>ApiParadigm</code>:
<code>rest</code>, <code>rpc</code>, <code>event</code>, <code>graph</code>, <code>data_schema</code>, <code>agent</code>) and <code>format</code> (the specific source
format key an adapter recorded at import: <code>openapi-3.1</code>, <code>protobuf</code>, <code>graphql</code>, …). Matching is
case- and punctuation-insensitive — <code>data-schema</code>, <code>event-driven</code> and <code>graphql</code> all resolve —
and an unrecognised value <em>narrows to nothing</em> rather than erroring, the same contract the
existing <code>search</code>/<code>domain</code> filters have. The two axes compose with AND, and an entry matches
when <strong>any</strong> of its listed versions carries the value.</li>
<li class=""><strong>Counts per facet.</strong> Both responses gained a <code>facets</code> block — <code>{protocols, formats}</code>, each a
list of <code>{value, label, count}</code>. Counts honour the listing's <em>other</em> filters (<code>search</code>,
<code>domain</code>) but deliberately ignore the facet selection itself, so a chip row always answers
"what else could I pick" instead of collapsing to what is already selected. Protocols come
back in canonical paradigm order; formats by descending count, ties broken by key.</li>
<li class=""><strong>Rows say what they are.</strong> Every tenant and project row now carries <code>protocols</code> / <code>formats</code>
(the distinct values across its listed versions) and every version row carries its own
<code>protocol</code> / <code>source_format</code>, so a listing stays readable once a facet has narrowed it.</li>
<li class=""><strong>Labels reuse the registries.</strong> <code>app/browse_facets.py</code> owns the normalization and the
labelling; format labels come from the import-source registry (so a newly registered adapter
labels its own chips) with a versioned-key rule that keeps <code>openapi-3.0</code>, <code>openapi-3.1</code> and
<code>swagger-2.0</code> distinguishable. An unknown key still renders — as itself.</li>
<li class=""><strong>Note.</strong> Revisions imported before MFI-7.1 carry no protocol/format and so contribute no
chip; the MFI-7.3 backfill (<a href="https://github.com/apiome/apiome/issues/3758" target="_blank" rel="noopener noreferrer" class="">#3758</a>) is what lights the facets up for pre-existing specs.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12330--2026-08-01">1.233.0 — 2026-08-01<a href="http://localhost/release-notes/rc4#12330--2026-08-01" class="hash-link" aria-label="Direct link to 1.233.0 — 2026-08-01" title="Direct link to 1.233.0 — 2026-08-01" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-27">Added<a href="http://localhost/release-notes/rc4#added-27" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class=""><strong>Per-repository / per-file refresh conflict policy (RAR-4.5, <a href="https://github.com/apiome/apiome/issues/3531" target="_blank" rel="noopener noreferrer" class="">#3531</a>)</strong> —
the RAR-4.4 divergence guard gave auto-refresh one answer when it meets a version that was
hand-edited after the original import: hold, do not clobber. That is the right default and
it is only one of the three answers teams want. This makes the answer configurable, at the
two scopes the work actually needs.
<ul>
<li class=""><strong>Three policies, stored as their wire tokens.</strong> <code>overwrite</code> lets the refresh supersede
the hand edit — the divergence is still <em>detected and reported</em>, it just does not stop the
refresh; <code>hold-for-review</code> (the default) skips the refresh and flags the file <code>diverged</code>;
<code>new-branch</code> leaves the current version untouched and lands the refresh on a side branch
so neither the edit nor the upstream change is lost.</li>
<li class=""><strong>The default does not move.</strong> <code>tenant_repositories.refresh_conflict_policy</code> is
<code>NOT NULL DEFAULT 'hold-for-review'</code>, so every existing repository keeps the behaviour it
has today. Opting into a policy that can lose work not held in the repository is an
explicit act, never a migration side-effect — and every degradation path (an unrecognised
token, a missing row, a blank value) falls back toward that same safe default rather than
failing a refresh.</li>
<li class=""><strong>Per-file overrides are exceptions, not enrolments.</strong>
<code>apiome.repository_conflict_policy_override</code> holds one row per file that deviates, keyed on
the same <code>(repository_id, branch, path)</code> lineage tuple as RAR-1.1's <code>repository_import_spec</code>.
A file with no row inherits its repository's policy, so the table stays tiny and clearing an
override is a <em>delete</em> — the file then follows whatever the repository says next, not a
frozen copy of today's setting. The table is separate from <code>tenant_repository_files</code>
deliberately: that one is rewritten by every scan, and policy must outlive the scan index.</li>
<li class=""><strong>One decision site.</strong> <code>app/repository_conflict_policy.py</code> resolves
<code>per-file → repository → default</code>, runs the RAR-4.4 guard under the resolved policy, and
returns a <code>ConflictOutcome</code> carrying the action (<code>apply</code> / <code>hold</code> / <code>new-branch</code>), whether
a manual edit was detected, the reason code, the policy and where it came from. Branch
names for the <code>new-branch</code> policy are deterministic
(<code>apiome-refresh/&lt;branch&gt;/&lt;file stem&gt;-&lt;short sha&gt;</code>), so a refresh that runs twice for one
commit targets one branch rather than accumulating near-duplicates. Like RAR-4.1–4.4 the
module is pure and DB-free; acting on the outcome remains the EPIC-4 dispatcher's job.</li>
<li class=""><strong>API.</strong> <code>GET/PUT /v1/tenants/{slug}/repositories/{id}/conflict-policy</code> reads and sets the
repository policy; <code>PUT …/conflict-policy/file</code> sets an override, or clears it with
<code>"policy": null</code>. Both the read and every mutation return the same projection — policy,
default, accepted tokens and overrides — so a settings panel cannot drift from stored
state. An unrecognised token is a <code>400</code> that lists what is accepted, not a <code>500</code> from the
column's CHECK. The repository policy is also patchable through the existing dashboard
<code>PATCH /v1/tenants/{slug}/repositories/{id}</code> as <code>refreshConflictPolicy</code>.</li>
<li class="">See <code>docs/repository_conflict_policy.md</code>.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12320--2026-08-01">1.232.0 — 2026-08-01<a href="http://localhost/release-notes/rc4#12320--2026-08-01" class="hash-link" aria-label="Direct link to 1.232.0 — 2026-08-01" title="Direct link to 1.232.0 — 2026-08-01" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-28">Added<a href="http://localhost/release-notes/rc4#added-28" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class=""><strong>Source-IP allowlist for webhook ingestion (REPO-7.6, <a href="https://github.com/apiome/apiome/issues/2804" target="_blank" rel="noopener noreferrer" class="">#2804</a>)</strong> —
<code>POST /v1/repositories/webhook/{provider}</code> is the one repository route with no bearer token,
so the HMAC signature is its only authentication. That check is sound and it is reached by
anyone who can open a socket: every unsigned POST on the internet buys a subscription
lookup, a constant-time comparison against a real secret, and a ledger row. The allowlist
filters on the source address <strong>before</strong> any of that runs.
<ul>
<li class=""><strong>A blocked delivery is a <code>403</code> and is never verified.</strong> The guard runs in the route,
ahead of <code>ingest_webhook_delivery</code>, so a refused source never reaches <code>verify_signature</code>
at all — the ticket's defense-in-depth requirement, and the reason this is not a branch
inside the dispatcher. The response body says only that the source was refused; naming the
allowlist, the tenant or the matched range would turn the endpoint into a probe for the
deployment's network policy.</li>
<li class=""><strong>Provider ranges are fetched daily, not hard-coded.</strong> GitHub's <code>hooks</code> array from
<code>api.github.com/meta</code> and Bitbucket's entries from <code>ip-ranges.atlassian.com</code> are refreshed
on a daily cadence into <code>apiome.webhook_provider_ip_range</code>, shared across replicas so
every process filters on the same list. Due-ness is measured from the last <em>success</em>, so a
provider whose endpoint is failing is retried on the next hourly tick rather than
tomorrow. GitLab.com publishes no machine-readable list; its ranges come from
<code>APIOME_REPOSITORY_WEBHOOK_IP_RANGES_GITLAB</code>, and the same setting exists for the other
two providers for self-hosted instances.</li>
<li class=""><strong>Per-tenant additional ranges, scoped to the tenant that owns the repository.</strong> A
self-hosted runner or an egress gateway is added per workspace and consulted only for the
tenants that registered the repository the payload names — resolved by parsing, which
reaches no secret. A union across tenants would let one workspace widen the filter
protecting all the others.</li>
<li class=""><strong>The bypass is an administrator's act, with a reason.</strong> <code>enforcement_enabled = false</code> on
<code>apiome.tenant_webhook_ip_policy</code> turns the filter off for one tenant's repositories;
setting it, and adding a range, both require a signed-in tenant administrator (API keys
are refused) and both write to <code>apiome.workflow_audit</code>. Disabling without a stated reason
is a 400.</li>
<li class=""><strong>Failure modes are chosen.</strong> Enforcement is off by default
(<code>APIOME_REPOSITORY_WEBHOOK_IP_ALLOWLIST</code>), so an upgrade changes nothing. A provider with
no cached ranges allows and logs rather than rejecting everything;
<code>..._IP_ALLOWLIST_STRICT</code> flips that to fail-closed. An empty provider fetch is treated as
a failure and leaves the previous cache standing. An unidentifiable client address blocks
— unless the owning tenant has bypassed enforcement, which is exactly the escape hatch
that case calls for.</li>
<li class=""><strong><code>X-Forwarded-For</code> is worth what the deployment says it is.</strong>
<code>APIOME_REPOSITORY_WEBHOOK_TRUSTED_PROXY_HOPS</code> (default 0) decides how many hops in to
read; at 0 the header is ignored entirely, since honouring it unverified would let any
caller name its own source address. A header shorter than the configured chain is refused
rather than guessed at.</li>
<li class=""><code>GET|POST|PATCH|DELETE|PUT /v1/tenants/{slug}/repository-webhook-ip-allowlist[...]</code> back
the admin panel. The read needs only import-view permission — seeing the filter is how
anyone diagnoses "our webhooks stopped" — while every mutation needs the admin role. Every
mutation answers with the whole allowlist, so a panel can never drift from what was
stored.</li>
<li class="">Blocked deliveries land in the existing <code>apiome.repository_webhook_event</code> ledger with the
<code>rejected</code> outcome and an <code>ip-not-allowed</code> reason, and are audited per owning tenant as
<code>repository.webhook.ip_blocked</code> — which carries the <code>repository.</code> prefix, so it appears in
the REPO-7.5 compliance export with no further wiring.</li>
<li class="">V234 adds the four tables (provider range cache, per-provider refresh state, per-tenant
entries, per-tenant policy). Tables and indexes only; no existing data is touched.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12310--2026-07-31">1.231.0 — 2026-07-31<a href="http://localhost/release-notes/rc4#12310--2026-07-31" class="hash-link" aria-label="Direct link to 1.231.0 — 2026-07-31" title="Direct link to 1.231.0 — 2026-07-31" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-29">Added<a href="http://localhost/release-notes/rc4#added-29" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class=""><strong>SOC 2 / ISO 27001 audit export (REPO-7.5, <a href="https://github.com/apiome/apiome/issues/2803" target="_blank" rel="noopener noreferrer" class="">#2803</a>)</strong> — compliance reviews need a structured,
dateable artifact of everything the repository subsystem wrote to the audit ledger, not a
paginated API a reviewer has to page through by hand.
<ul>
<li class=""><code>GET /v1/tenants/{slug}/repository-audit-export?from=&amp;to=&amp;format=csv|json</code> streams every
<code>repository.*</code> row of <code>apiome.workflow_audit</code> (refresh cycles, webhook registrations /
deliveries / secret rotations, external-ref fetches, …) in the inclusive <code>created_at</code>
range, oldest first, served as an attachment with a range-stamped filename
(<code>repository-audit-export_20260101-20260731.csv</code>).</li>
<li class=""><strong>Admin-only.</strong> The ledger names every repository and actor in the workspace, so the
export requires a signed-in tenant administrator; API keys are refused outright rather
than resolved to their creating user.</li>
<li class=""><strong>Streamed at any size.</strong> Rows are read oldest-first with a <code>(created_at, id)</code> keyset
cursor in 1,000-row batches, so an export far beyond 10k rows holds one batch in memory
and every batch costs the same — no OFFSET cliff, and rows appended mid-export cannot
shift between batches.</li>
<li class=""><strong>CSV or JSON.</strong> CSV is a header plus one RFC-4180 row per entry with <code>detail</code>
JSON-encoded in its cell; JSON is a single document — an <code>export</code> metadata envelope, the
<code>entries</code> array streamed element by element, and a trailing <code>rowCount</code> — that only parses
when the download ran to completion, so a truncated artifact is detectably incomplete
instead of silently short.</li>
<li class=""><strong>The export is itself evidence.</strong> Every attempt appends a
<code>repository.audit_exported</code> row to the same ledger: <code>success</code> with the exact row count on
completion, <code>failure</code> with the partial count when the stream errors or the client
disconnects mid-download. Because the action carries the <code>repository.</code> prefix, each
export shows up in the next one. Recording is best-effort, so audit bookkeeping can never
break the download it describes.</li>
<li class="">Ledger and endpoint only; no migration — <code>apiome.workflow_audit</code> is reused unchanged.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12300--2026-07-31">1.230.0 — 2026-07-31<a href="http://localhost/release-notes/rc4#12300--2026-07-31" class="hash-link" aria-label="Direct link to 1.230.0 — 2026-07-31" title="Direct link to 1.230.0 — 2026-07-31" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-30">Added<a href="http://localhost/release-notes/rc4#added-30" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class=""><strong>Quota &amp; rate-limit telemetry (REPO-7.3, <a href="https://github.com/apiome/apiome/issues/2801" target="_blank" rel="noopener noreferrer" class="">#2801</a>)</strong> — the REPO-4.6 polling quota and the
REPO-2.5 scan budget both work silently. The only record of a deferral was a log line and a
per-replica in-memory counter that died with the process, so "is this workspace permanently
parked against its ceiling, or was that one bad afternoon?" had no answer anyone could give.
There is now a durable per-tenant counter behind it.
<ul>
<li class=""><strong>Five metrics in a rolling-window table</strong> (<code>apiome.repository_quota_window</code>, V233):
<code>polls</code>, <code>polls_deferred</code> and <code>files_deferred</code> bucket hourly (matching the REPO-4.6 quota
window); <code>scans</code> and <code>bytes_scanned</code> bucket daily. Aggregates, not events — a workspace
polling 600 times an hour costs one row an hour, not 600.</li>
<li class=""><strong>A window boundary is the reset.</strong> Nothing zeroes a counter: an increment lands on the
bucket its timestamp falls in, so crossing a boundary writes to a different row and the new
window starts at zero. That holds across restarts, across replicas, and across a sweep tick
that straddles the boundary — none of which a "reset the counter" job would survive.</li>
<li class=""><strong>Deferrals are counted apart from work.</strong> <code>polls</code> says how much refreshing happened;
<code>polls_deferred</code> / <code>files_deferred</code> say how much the quota pushed into a later window.
Folding them together would erase the one signal the dashboard exists to show.</li>
<li class=""><strong>Increments are a single atomic upsert</strong> on <code>(tenant_id, metric, window_start)</code>, so two
replicas sweeping the same tenant in the same window converge on one row rather than each
creating their own and halving every subsequent read.</li>
<li class=""><strong>Recording can never fail a caller.</strong> Every counter write is best-effort and swallowed:
telemetry that can raise would turn an observability problem into a refresh outage. A scan
pass that <em>raised</em> records nothing at all — reporting it as scan volume would make a broken
repository look like a busy one.</li>
<li class=""><code>GET /v1/tenants/{slug}/repository-quota-telemetry?days=7</code> returns the trailing series
alongside the tenant's current quota position, so a dashboard renders "42 of 600 used this
hour" and "here is the last week" from one request. Every metric is present and zero-filled
across the whole range, so a workspace that has never been deferred sees a flat line rather
than a missing panel. A counter read that fails comes back <code>available: false</code> with zeros —
the flag is what stops "we could not read this" being shown as "nothing happened".</li>
<li class="">Counter rows are pruned by the existing async-job retention sweep after
<code>APIOME_REPOSITORY_QUOTA_WINDOW_RETENTION_DAYS</code> (default 120, comfortably longer than the
90-day maximum range the API serves). <code>0</code> keeps them forever.</li>
<li class="">V233 adds the counter table. Table and indexes only; no existing data is touched.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12290--2026-07-31">1.229.0 — 2026-07-31<a href="http://localhost/release-notes/rc4#12290--2026-07-31" class="hash-link" aria-label="Direct link to 1.229.0 — 2026-07-31" title="Direct link to 1.229.0 — 2026-07-31" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-31">Added<a href="http://localhost/release-notes/rc4#added-31" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class=""><strong>Notifications on scan / sync events (REPO-7.2, <a href="https://github.com/apiome/apiome/issues/2800" target="_blank" rel="noopener noreferrer" class="">#2800</a>)</strong> — RAR-5.4 gave the auto-refresh loop
a voice, but no off switch and no volume control: a repository stuck in a failure loop could
page the same on-call every sweep tick, and nobody could ask it to stop. Repository
notifications now go through a policy layer before a single channel is resolved.
<ul>
<li class=""><strong>Three operator-facing events</strong>, all inside the <code>repository.refresh.*</code> namespace subscribers
already route on: <code>auto_paused</code> (scheduled refresh has stopped and must be resumed by hand),
<code>breaking_change</code> (a sync produced a version whose change report classifies as breaking), and
<code>repeated_failures</code> (the warning shot — failing repeatedly but not paused yet).</li>
<li class=""><strong>Per-repository, per-event-type opt-out.</strong> <code>apiome.repository_notification_preference</code> holds
exceptions, not enrolments: a repository with no rows is subscribed to everything, and only an
explicit <code>enabled = FALSE</code> mutes an event, so a partial preference set fails <em>open</em>. A
preference read that errors mutes nothing — failing closed would silence a repository during
the incident that broke the read.</li>
<li class=""><strong>At most one notification per repository per event type per hour.</strong> The slot claim is a
single conditional upsert on <code>apiome.repository_notification_throttle</code>, so the decision and
the timestamp write are the same statement and two sweep workers racing on one repository
cannot both win. A losing claim bumps <code>suppressed_count</code> instead, which is how "quiet because
nothing happened" is later told apart from "quiet because we muffled 400 of them". A tenant
with no channels does not burn its hourly slot.</li>
<li class=""><strong>Channels are resolved per tenant</strong> from the existing push-webhook subscriptions, with their
retry and dead-letter semantics unchanged, and each is shaped for its destination: a Slack
incoming webhook receives a Slack <code>text</code>/<code>blocks</code> message (Slack rejects a body without
<code>text</code>), every other endpoint receives the structured JSON. One dead channel never takes the
rest of the fan-out down with it.</li>
<li class=""><strong>Wired into the refresh sweep.</strong> An auto-pause transition sends the auto-pause event; an
unpaused repository past three consecutive failures sends the repeated-failures warning on
every tick, which the hourly throttle makes safe. A newly paused repository sends only the
pause — pairing it with a warning about the same failures is just noise.</li>
<li class=""><code>GET</code>/<code>PUT /v1/tenants/{slug}/repositories/{id}/notification-preferences</code> report and set the
opt-outs. The read always lists every event type, with a one-sentence description of what
muting it would cost and the throttle state for that pair. The write is partial (events it
does not mention keep their state) and rejects unknown or repeated event types with a 400
rather than returning 200 for an opt-out that mutes nothing.</li>
<li class="">V232 adds the two policy tables. Indexes and tables only; no existing data is touched.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12280--2026-07-31">1.228.0 — 2026-07-31<a href="http://localhost/release-notes/rc4#12280--2026-07-31" class="hash-link" aria-label="Direct link to 1.228.0 — 2026-07-31" title="Direct link to 1.228.0 — 2026-07-31" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-32">Added<a href="http://localhost/release-notes/rc4#added-32" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class=""><strong>Per-repository health badge (REPO-6.5, <a href="https://github.com/apiome/apiome/issues/2798" target="_blank" rel="noopener noreferrer" class="">#2798</a>)</strong> — the repository surface exposed plenty of
individual signals (scan job outcomes, per-spec quality attempts, the linked account a private
repository authenticates with) but nothing that answered an operator's first question at a
glance: <em>is this repository fine?</em> Every repository now carries one three-valued badge, on the
repositories list rows (REPO-6.1) and the repository detail header (REPO-6.2).
<ul>
<li class=""><strong>Three inputs, three levels.</strong> <code>healthy</code> / <code>warnings</code> / <code>error</code> is rolled up from the scan
success rate over the last 30 days, the count of discovered specs on the default branch whose
REPO-2.8 quality attempt errored or could not parse, and the health of the linked account's
access token (REPO-7.4). Below 50% of scans succeeding is an <code>error</code>, below 90% a <code>warnings</code>;
parse errors are a <code>warnings</code> until ten of them, at which point the repository is not usable
as an import source; a disconnected account, a missing token or an expired one is an <code>error</code>,
and a token expiring within seven days is a <code>warnings</code>.</li>
<li class=""><strong>Token issues always demote to at least <code>warnings</code>.</strong> A repository Apiome can no longer
authenticate to never reads as healthy, however spotless its scan history. Every token factor
is emitted at warnings-or-worse and the roll-up clamps as well, so the guarantee survives a
future factor being added at the wrong level.</li>
<li class=""><strong>A tooltip that says what changed.</strong> Each contributing factor carries a stable machine code,
a level, a one-sentence operator-facing summary and — where an event lies behind it — when it
was last observed. <code>primary_factor</code> is the <em>most recently observed</em> factor, which is what the
badge tooltip leads with; the full list is ordered most severe first. A standing condition
with no event behind it ("this token expires soon") never displaces something that actually
happened.</li>
<li class=""><strong>No signal is not a problem.</strong> A repository registered a minute ago has no scans, no scored
files and (for a public clone URL) no token to expire; it reads as <code>healthy</code> rather than
manufacturing an alarm out of missing data. Public-URL repositories are read anonymously and
so always have perfect token health.</li>
<li class=""><strong>One query per page, not one per row.</strong> <code>Database.get_repository_health_signals</code> answers the
whole batch with two correlated laterals, and V231 indexes both: <code>(repository_id, created_at DESC) INCLUDE (status, finished_at)</code> on the scan queue turns the trailing window into a
bounded range scan, and a partial index on the file table holding only rows that actually
failed to parse — empty on a healthy monorepo. The migration adds indexes only.</li>
<li class=""><strong>Decoration, never a point of failure.</strong> The badge is computed by a pure, side-effect-free
module that cannot raise; if the signal query itself fails, the affected rows carry no badge
and the listing is unaffected. The access token's <em>value</em> is never selected — only whether
one exists — so a credential cannot leak through a listing response.</li>
<li class=""><code>GET /v1/tenants/{slug}/repositories</code> and <code>GET /v1/tenants/{slug}/repositories/{id}</code> (and the
PATCH / refresh-resume reads that return the same record) gain a <code>health</code> object.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12270--2026-07-31">1.227.0 — 2026-07-31<a href="http://localhost/release-notes/rc4#12270--2026-07-31" class="hash-link" aria-label="Direct link to 1.227.0 — 2026-07-31" title="Direct link to 1.227.0 — 2026-07-31" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-33">Added<a href="http://localhost/release-notes/rc4#added-33" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class=""><strong>Cross-repository discovered-specs catalog (REPO-6.4, <a href="https://github.com/apiome/apiome/issues/2797" target="_blank" rel="noopener noreferrer" class="">#2797</a>)</strong> — the repository surface could
only answer "what specs are in <em>this</em> repo on <em>this</em> branch" (REPO-6.2). An operator running
more than a handful of repositories had no way to ask "where does this spec live", short of
opening each repository in turn.
<ul>
<li class=""><strong><code>GET /v1/tenants/{slug}/repository-files</code>.</strong> One tenant-wide, server-paginated listing of
every discovered spec across every registered repository. Free-text <code>q</code> matches the file
path, its detected kind, the repository's full name and the mapped project's name; <code>format</code>,
<code>repository_id</code>, <code>project_id</code> and <code>status</code> narrow it further; <code>sort</code> orders by repository,
path, format, status or recent activity. Search, filtering, ordering and pagination all
evaluate in SQL, so the response carries only the requested page.</li>
<li class=""><strong>Derived per-spec status.</strong> Each row resolves to exactly one of <code>needs_attention</code> (quality
scoring errored, or the last scan left external <code>$ref</code>s unresolved), <code>imported</code>, <code>mapped</code>
(bound to a project, no import yet) or <code>discovered</code>, in that precedence. The status is
projected and filtered by the same SQL expression, so a row can never be listed under a
value it cannot be filtered by.</li>
<li class=""><strong>Project and version context per row.</strong> Each spec carries the project it is mapped to (or
was last imported into) and the version its most recent import produced, resolved through a
lateral join so a file imported fifty times still contributes one catalog row.</li>
<li class=""><strong>Opt-in facets.</strong> <code>include_facets=true</code> returns the filter dropdown options — formats,
statuses, repositories and projects, each with a catalog-wide count. Facets are computed
over the whole catalog rather than the filtered page, so picking one filter never hides the
others. The catalog page requests them once on mount.</li>
<li class=""><strong>Scoped for signal, not volume.</strong> Vendored trees (<code>node_modules</code>, <code>vendor</code>, <code>.git</code>) and
dot-directories are always excluded; only each repository's default branch is listed unless
<code>all_branches=true</code>; only classified spec types unless <code>importable_only=false</code>.</li>
<li class=""><strong>Indexes for the 10k-file bar (V230).</strong> A <code>pg_trgm</code> GIN index makes the substring search
indexable, and a <code>(repository_id, branch, path, created_at DESC)</code> index answers the
latest-import lookup with one backwards scan. The trigram block degrades to a notice where
the migration role cannot install contrib extensions — the catalog stays correct, just
sequential.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12220--2026-07-31">1.222.0 — 2026-07-31<a href="http://localhost/release-notes/rc4#12220--2026-07-31" class="hash-link" aria-label="Direct link to 1.222.0 — 2026-07-31" title="Direct link to 1.222.0 — 2026-07-31" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-34">Added<a href="http://localhost/release-notes/rc4#added-34" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class=""><strong>Arazzo 1.x workflow importer (REPO-3.4, <a href="https://github.com/apiome/apiome/issues/2773" target="_blank" rel="noopener noreferrer" class="">#2773</a>)</strong> — Arazzo describes orchestrated multi-step
API workflows and is a sibling specification to OpenAPI, but Apiome could only <em>read</em> one
(detect → parse → normalize → lint → emit → diff, MFI-30.2). An imported Arazzo document landed
as a store-raw catalog item and its orchestration was invisible. It is now a first-class entity.
<ul>
<li class=""><strong>Workflow + WorkflowStep entities (V225).</strong> <code>apiome.api_workflows</code> and
<code>apiome.api_workflow_steps</code> hang off the version's <code>api_artifacts</code> row like every other
canonical child, so a re-import replaces the previous orchestration instead of accumulating
duplicates. Each <code>workflows[]</code> entry becomes one workflow row (<code>workflowId</code>, <code>summary</code>,
<code>description</code>, <code>inputs</code>, <code>outputs</code>) plus N step rows in source order.</li>
<li class=""><strong><code>operationRef</code> resolution.</strong> A step that points at an OpenAPI operation imported in the same
scan resolves to that internal <code>path_operation</code> id. "The same scan" is concrete: with git
provenance it is every project <code>repository_import_spec</code> links to the same repository and
branch; otherwise it is the importing project. Every reference spelling in the wild is
handled — <code>operationId</code>, <code>$sourceDescriptions.&lt;name&gt;.&lt;operationId&gt;</code>, an <code>operationRef</code>
JSON-pointer (<code>…#/paths/~1pets~1{petId}/get</code>), and Arazzo 1.0.0's <code>operationPath</code> route
pointer, which resolves only when the route carries exactly one operation.</li>
<li class=""><strong>A miss is not a failure.</strong> An unresolved reference keeps its raw string verbatim, leaves the
FK NULL, and records a stable <code>resolution_reason</code> (<code>unknown-operation</code>, <code>ambiguous-operation</code>,
<code>no-operation-target</code>, …) plus a human-readable warning. A step that calls a sibling workflow
is <code>not_applicable</code> rather than "unknown"; a step that cannot be read at all is isolated as
<code>parse_error</code> and its siblings still import. Workflow persistence is an enrichment over the
catalog item, so a failure there never fails an import whose source bytes are already stored.</li>
<li class=""><strong>Verbatim step payloads.</strong> <code>parameters</code>, <code>successCriteria</code>, <code>onFailure</code>, <code>outputs</code> and
<code>dependsOn</code> are stored exactly as written — Arazzo's runtime-expression grammar
(<code>$response.body#/id</code>, <code>$steps.foo.outputs.bar</code>) is never re-parsed, which keeps round-trip
honest.</li>
<li class=""><strong>Verified against the official bundles.</strong> <code>tests/fixtures/arazzo</code> carries the OAI example
documents verbatim (<code>pet-coupons</code>, <code>LoginAndRetrievePets</code>, <code>oauth</code>); they round-trip
normalize → map → persist → load with identical workflows and steps.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12180--2026-07-31">1.218.0 — 2026-07-31<a href="http://localhost/release-notes/rc4#12180--2026-07-31" class="hash-link" aria-label="Direct link to 1.218.0 — 2026-07-31" title="Direct link to 1.218.0 — 2026-07-31" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-35">Added<a href="http://localhost/release-notes/rc4#added-35" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class=""><strong>Quality scoring per discovered spec (REPO-2.8, <a href="https://github.com/apiome/apiome/issues/2769" target="_blank" rel="noopener noreferrer" class="">#2769</a>)</strong> — the repository scanner classified
a discovered file by filename but said nothing about whether it was any good, so triaging a
repository's specs meant opening each candidate by hand. Every <em>classified</em> spec now carries a
rough 0–100 quality score.
<ul>
<li class=""><strong>Reuses the existing engines, not new scoring.</strong> A discovered spec is scored through its
import-source adapter — <code>parse</code> → <code>normalize</code> → <code>lint</code> — which for OpenAPI is the native
path/schema linter (<code>schema_lint.lint_openapi_spec</code>: the PATH-QUALITY and SCHEMA-QUALITY rule
groups) and for every other format the canonical-model rule packs behind <code>ImportSource.lint</code>.
A repository file and an imported revision therefore land on one comparable scale, and a rule
added to either engine shows up here for free.</li>
<li class=""><strong>Only classified specs.</strong> <code>unknown_spec</code> files — no <code>detected_kind</code>, or a generic container
kind like <code>json-candidate</code> on a <code>package.json</code> — are never scored, selected by the same
importable predicate the Files browser filters on. A classified format with no adapter yet
(Prisma, SQL DDL, DBML) is skipped for its own distinct, labelled reason.</li>
<li class=""><strong>Persisted on the file row.</strong> <code>apiome.tenant_repository_files</code> gains <code>quality_score</code>,
<code>quality_grade</code>, <code>quality_status</code> (<code>scored</code> | <code>skipped</code> | <code>error</code>), <code>quality_reason</code>,
<code>quality_scored_at</code> and <code>quality_scored_blob_sha</code> (V222). All nullable, so every
already-indexed repository reads as "not scored yet" and no re-scan is required.</li>
<li class=""><strong>Bounded background pass.</strong> Scoring runs in its own sweep
(<code>repository_quality_sweep.process_repository_spec_quality_batch</code>), not inside the REPO-2.5
tree walk, so a monorepo scan keeps its current cost. Each tick claims at most
<code>APIOME_REPOSITORY_QUALITY_BATCH_SIZE</code> (default 10) due files every
<code>APIOME_REPOSITORY_QUALITY_INTERVAL</code> seconds (default 30); set
<code>APIOME_REPOSITORY_QUALITY_SCORING=false</code> to disable it entirely.</li>
<li class=""><strong>One download per revision.</strong> Every attempt stamps the blob sha it read, success or not, so
an unscorable file settles instead of being re-fetched each tick, and editing the file makes
it due again. Private repositories are read only with their linked-account token, and files
above the 900 KB content cap are skipped without being downloaded.</li>
<li class=""><strong>Informational only.</strong> No scoring path can raise: an unparseable document, a missing
toolchain, a provider failure, or an adapter bug is recorded on the row as a stable machine
reason. Nothing gates a scan, a refresh, or an import on the score — spec promotion gating
remains REPO-5.6's job.</li>
<li class=""><code>GET /v1/tenants/{slug}/repositories/{id}/files</code> returns <code>quality_score</code>, <code>quality_grade</code>,
<code>quality_status</code> and <code>quality_reason</code> per row, and the Repository detail Files tab renders
them in a new <strong>Quality</strong> column (REPO-6.2).</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12170--2026-07-30">1.217.0 — 2026-07-30<a href="http://localhost/release-notes/rc4#12170--2026-07-30" class="hash-link" aria-label="Direct link to 1.217.0 — 2026-07-30" title="Direct link to 1.217.0 — 2026-07-30" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-36">Added<a href="http://localhost/release-notes/rc4#added-36" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class=""><strong>Large-monorepo support: sparse / paged repository walk (REPO-2.5, <a href="https://github.com/apiome/apiome/issues/2766" target="_blank" rel="noopener noreferrer" class="">#2766</a>)</strong> — a repository
with more than ~25k entries could not be indexed at all: the walker pulled the whole branch
tree in one <code>recursive=1</code> GitHub Trees call, buffered every blob in memory, and turned
GitHub's own <code>truncated: true</code> size signal into the hard failure "repository too large for
this scan pass". The walk is now bounded and resumable.
<ul>
<li class=""><strong>Streams in chunks.</strong> <code>walk_github_tree_in_chunks</code> hands entries to its sink in batches of
at most 1000 (<code>repository_scan_budget.MAX_WALK_CHUNK_SIZE</code>, capped regardless of
<code>APIOME_REPOSITORY_SCAN_CHUNK_SIZE</code>), written through the new upserting
<code>Database.append_tenant_repository_files</code> instead of one whole-branch statement.</li>
<li class=""><strong>Provider-side sparse primitive.</strong> A <code>truncated</code> recursive response now switches the walk
to a breadth-first per-directory descent over the non-recursive Trees API, so only a queue
of unvisited <em>directories</em> is ever held in memory.</li>
<li class=""><strong>Per-tenant wall-clock budget.</strong> <code>tenants.repository_scan_budget_seconds</code> (default 300 =
5 min) bounds one scan pass, clamped at read time into
<code>[APIOME_REPOSITORY_SCAN_BUDGET_MIN, APIOME_REPOSITORY_SCAN_BUDGET_MAX]</code>. A pass that spends
its budget stores its position and comes back incomplete rather than being killed.</li>
<li class=""><strong>Resume via stored cursor.</strong> <code>apiome.tenant_repository_scan_cursors</code> holds one cursor per
(repository, branch) — the pinned tree SHA, the RAR-2.1 branch-tip anchors, the walk mode and
the pending sub-tree queue. A paused pass, or one interrupted by a transient provider failure
(network error, 429, 5xx), re-queues its scan job instead of failing it, and the next sweep
tick continues from the cursor. The tree SHA is pinned so a resumed pass keeps indexing the
same snapshot even if the branch moves.</li>
<li class=""><strong>Fair queueing.</strong> A resumed job is claimed on <code>COALESCE(requeued_at, created_at)</code>, so a
monorepo needing twenty passes yields to other repositories between them instead of owning
the scan worker for the whole walk.</li>
<li class="">Safeguards: a cursor older than 24h is discarded and the branch rewalked; a walk that indexes
nothing new for too many consecutive passes is abandoned with an error (progress resets that
counter, so a long walk is never penalized for being long); a transient failure with no stored
position still fails the job; and a completed scan re-reads its counts from the persisted rows
so a re-emitted chunk cannot inflate them.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12160--2026-07-29">1.216.0 — 2026-07-29<a href="http://localhost/release-notes/rc4#12160--2026-07-29" class="hash-link" aria-label="Direct link to 1.216.0 — 2026-07-29" title="Direct link to 1.216.0 — 2026-07-29" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="fixed-1">Fixed<a href="http://localhost/release-notes/rc4#fixed-1" class="hash-link" aria-label="Direct link to Fixed" title="Direct link to Fixed" translate="no">​</a></h4>
<ul>
<li class=""><strong>Dependents of a registry type (<a href="https://github.com/apiome/apiome/issues/3477" target="_blank" rel="noopener noreferrer" class="">#3477</a>)</strong> — opening <code>number</code> from <code>decimal</code>'s base chain
showed "No types reference this primitive yet" while <code>decimal</code> plainly referenced it.
<code>apiome.primitives.refs</code> records only a type's <em>outgoing</em> edges, so nothing ever answered
the reverse question; the field was declared on the detail page and never populated.
<code>GET /v1/primitives/{tenant}/{id}</code> now returns <code>dependents</code>, the reverse index built by
scanning the visible types' edge lists for the viewed type's <code>$id</code>
(<code>Database.get_dependent_primitives</code>).
<ul>
<li class="">Matching is on the stored absolute <code>resolved_target</code> (the form V218 normalized to), so a
dependent is found however its relative <code>$ref</code> was written, and an edge still flagged
<code>unresolved</code> by a stale resolver run is still listed — the target exists, so the
dependency is real.</li>
<li class="">One entry per referencing <em>edge</em>, each labelled with the property carrying it
(<code>ref_location_label</code> over the new <code>iter_ref_locations</code> walk): <code>money</code> shows up under
<code>decimal</code> as <code>amount</code>, while <code>decimal</code> under <code>number</code> carries no property because the
<code>$ref</code> is the whole type. Read scope is unchanged — system-core ∪ the caller's own
(<a href="https://github.com/apiome/apiome/issues/3453" target="_blank" rel="noopener noreferrer" class="">#3453</a>) — and per-tenant seeded copies of a core dependent collapse to one row.</li>
<li class="">OpenAPI 1.80.1 → 1.81.0 (<code>PrimitiveSchema.dependents</code> added; existing fields unchanged).</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_tleR" id="12151--2026-07-28">1.215.1 — 2026-07-28<a href="http://localhost/release-notes/rc4#12151--2026-07-28" class="hash-link" aria-label="Direct link to 1.215.1 — 2026-07-28" title="Direct link to 1.215.1 — 2026-07-28" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_tleR" id="added-37">Added<a href="http://localhost/release-notes/rc4#added-37" class="hash-link" aria-label="Direct link to Added" title="Direct link to Added" translate="no">​</a></h4>
<ul>
<li class=""><strong>CPDO user guide and format-detail documentation (<a href="https://github.com/apiome/apiome/issues/4806" target="_blank" rel="noopener noreferrer" class="">#4806</a>, CPDO-4.3)</strong> — the user-facing
guides <code>docs/guide/catalog-format-details.md</code> (Format details tab: status vocabulary,
value-visibility/redaction, analysis bounds, X12 and copybook inspector boundaries, the
absence-category table) and <code>docs/guide/convert-to-openapi.md</code> (conversion walkthrough:
projection-graph legend, status and reason-code vocabulary with remediations, safe
defaults, acknowledgement gating, historical-vs-fresh evidence, CLI/REST surfaces), with
authoritative X12 and IBM COBOL references.
<ul>
<li class=""><strong><code>tests/test_cpdo_docs_guide.py</code></strong> couples the prose to the code registries: every
analysis status/reason, value-visibility level, conversion status, projection reason
code, and absence-category label must appear in the guides, the required primary
references must stay linked, and every external link must be <code>https</code>. The UI-side twin
(<code>apiome-ui/tests/cpdo-guide-terminology.test.ts</code>) holds the guides to the exact labels
and symbols the UI renders.</li>
<li class="">OpenAPI 1.80.0 → 1.80.1 (no contract shape changes).</li>
</ul>
</li>
</ul>]]></content>
        <author>
            <name>The Apiome team</name>
            <uri>https://github.com/apiome/apiome</uri>
        </author>
    </entry>
</feed>