-
876ccf1: fix(docs): the Backward Compatibility page said MINOR may break "during 0.x" — restate it as the launch-window rule it actually is (#13779)
content/docs/protocol/backward-compatibility.mdxclosed with aPre-1.0 Disclaimerreading:During the 0.x development phase, MINOR versions may contain breaking changes. The full backward compatibility policy takes effect starting with version 1.0.0.
The published stack is at 17.2.0, so a reader dismisses that paragraph as obviously stale and is left with the page's opening SemVer table, which says a MINOR keeps existing code working. That is the wrong way round. The disclaimer's substance is the part that survived; only its
0.x/1.0.0framing died.Deleting the paragraph would therefore have silently strengthened a customer-facing compatibility promise into one the repo contradicts on every release. Four independent sources say breaking changes ship as MINOR today:
.changeset/config.json— all 69 published packages sit in one Changesetsfixedgroup (check:changeset-fixed: "fixed group is in sync with 69 public workspace packages"), so no published surface is exempt and a singlemajorwould promote the whole stack.scripts/check-changeset-no-major.mjs— a wired, currently-enforcing CI guard (.changeset/pre.jsonis absent, so the RC exemption is not in play) whose header states the convention outright: "During the launch window we ship breaking changes asminor."--listreports 559 pending changesets, 0 declaring a major.packages/spec/CHANGELOG.md— the17.2.0Minor Changes section carries an entry marked**BREAKING**(thehttp_request_errors_totalretirement under ADR-0049).content/docs/releases/— v13, v14, v15 and v17 already tell customers this. v15.1.0: "Strict-semver breaking, shipped in a minor under the launch-window policy." v17: "17.1.0 and 17.2.0 are minors by version number, not by blast radius."
The section is retitled
Launch Window: MINOR Releases Can Contain Breaking Changesand now states the rule definitely rather than hedging it: which surfaces it covers (all 69), that it is gate-enforced, what an upgrader should do instead of trusting the version number, that MAJORs still happen when breaking density demands one, and that it overrides the tables above wherever they disagree.Nothing links to the old
#pre-10-disclaimeranchor (grepped repo-wide), so the retitle breaks no inbound reference. -
18b6d89: build(docs): stop emitting the 348 MB of server source maps the OOM-killed build was paying for (#12711)
Every
objectstack.aiproduction deploy on 2026-08-27 died withexit 137and Vercel'serrorCode: "out_of_memory"on a 4-core/8192 MB build machine. The build now declaresexperimental.turbopackSourceMaps: false, which suppresses 260 map files totalling 348 MB that no production serverless function reads.The knob matters because of where the kill lands. Every failing log places it between
Creating an optimized production build ...andCompiled successfully, with no output in between — inside the Turbopack compile phase, which the two knobs already present cannot reach:experimental.cpus: 2bounds static-generation workers that have not spawned yet when the process dies.NODE_OPTIONS=--max-old-space-sizebounds V8's old space, while Turbopack allocates from Rust outside it.
Measured on a local cold build of all 403 pages, as peak single-process RSS:
config peak compile before 5191 MB 22.6s turbopackSourceMaps: false4757 MB 19.5s turbopackScopeHoisting: false4806 MB 19.1s --max-old-space-size=20484734 MB 18.0s turbopackFileSystemCacheForBuild: true4779 MB 22.2s Only the first row moves anything; the rest are noise, which is the measurement that retires them as candidates rather than leaving them to be re-tried.
Set explicitly on purpose. Next documents this flag's build-time default as following
productionBrowserSourceMaps(false), but the server-side maps are emitted regardless — naming it is what suppresses them.Deliberately not paired with
turbopackMinify: false, which takes a further 972 MB off the peak (3785 MB) and 3s off the compile: it inflates client JS from 5.8 MB to 16 MB (+176%), a cost every reader pays on every visit to save memory in a machine they never touch. Confining minification to the server side — where the memory actually goes, 589 MB of server chunks against 5.8 MB of client — is not available:experimental.serverMinificationis read only bydist/build/webpack-config.js, never on the Turbopack path.No output change beyond the absent maps: same routes, same 1221 prerendered paths, same rendered bytes.
This buys margin; it does not prove the ceiling is cleared. The measurement above is macOS/arm64 and the build container is Linux/x86_64, where the same phase was measured at 7.6 GB (#12683) against local 4.7 GB — the ratio is the transferable part, and −8.4% onto 7592 MB leaves roughly 15% headroom, which is thin. #12683's option A (a larger build machine) is unaffected by this change and remains the answer if the next production build still dies.
-
168941c: build(docs): stop rebuilding the docs site on every push to
main(#12743)Every push to
mainrebuilt the documentation site, and almost none of them changed what it renders. Measured over one week across the team:project production builds build-minutes avg objectstack (docs) 228 2835 (98.6%) 12.4 min objectui 123 36 0.3 min hotcrm 42 5 0.1 min The team runs
concurrentBuilds: 1, so an 18-secondobjectuibuild queued behind a 12–46 minute docs build; the queue reached 92 deployments, the oldest 34 hours old. At 4 vCPU those docs builds cost roughly $171/month against a $20 included allowance, and 168 of the 228 were failures, so most of it bought nothing.apps/docs/vercel.jsonnow declares anignoreCommand(which overrides the dashboard's Ignored Build Step, moving the rule into version control where it is reviewable and revertible).scripts/vercel-ignore-docs.shdecides:- non-production → skip (unchanged from the rule it replaces)
content/**orapps/docs/**changed → build- otherwise → ask turbo whether the docs dependency graph is affected
- anything indeterminate → build
Step 2 is not redundant with step 3, and dropping it would silently stop publishing documentation.
turbo --filter=<pkg>...[range]computes affected packages by package directory, and this repo's MDX lives at the repo root incontent/, outside theapps/docsboundary.turbo.jsondoes list"$TURBO_ROOT$/content/**"under@objectstack/docs#build'sinputs, butinputsonly feeds the cache hash — it does not widen the affected-package calculation. Verified onmain: commit1265f12btouches onlycontent/docs/api/client-sdk.mdx, and a dependency-graph check alone answers SKIP for it.The asymmetry in step 4 is the point. A wrong "build" costs a few build-minutes; a wrong "skip" leaves the site quietly stale with no error anywhere. So a missing
VERCEL_GIT_PREVIOUS_SHA, a shallow clone that cannot reach it, an unparseable turbo verdict, and a non-0/1 exit from turbo all build.Deliberately not
npx turbo-ignore, which #12698 suggested: it is deprecated upstream ("Useturbo query affectedinstead") and derives its own comparison range, falling back to[HEAD^]when it cannot read Vercel's git environment — silently answering a different question than the one asked. The range is named explicitly here instead.scripts/vercel-ignore-docs.selftest.shpins all six cases against real commits from this repo's history, including thecontent/**one. -
002ddc5: fix(docs): give the three singular
section:form-section examples inlayout-dsl.mdxanamei18n anchor (#13759)content/docs/protocol/objectui/layout-dsl.mdxteaches form sections twice over: as asections:sequence, and as a singularsection:mapping — one section on its own. The sequence examples were givennameanchors in the sweep that added the gate's YAML arm (#13761); the three singular ones were outside that sweep's population and stayed nameless.FormSectionSchema.nameis the "Stable identifier for translation lookup", so a nameless section has no anchor and renders its authored label in every locale — on pages whose whole job is to teach the convention.Three sites, and they are not three copies of one edit:
fence before added ### Basic Grid Layoutlabel: Contact Informationname: contact_information### Custom Span Widthslabel: Product Detailsname: product_details### Responsive Breakpointsno label:— onlycolumns:+fields:name: responsive_gridThe first two take the snake_case of their own label, which is the convention #13761 used for the sequence examples on this same page (
contact_information,basic_info,billing_information). The third has no label to snake_case: it is deliberately minimal so the breakpoint discussion is aboutcolumnscollapsing, and none of the three fences' ASCII "Rendered Grid" diagrams draw a section header. So it gets a descriptivenameand no inventedlabel:— adding one would have desynchronised the diagram directly below it, and the i18n symptom the other two carry does not even arise for a section with no heading to mis-render.FormSectionSchema.namestaysz.string().optional()— no schema moves here, per #10709 and #10830.These three sites are correct now and still unguarded, deliberately.
check-docs-section-namejudgessections:sequences in both of its arms; a singularsection:mapping is outside both, which is how these three drifted in the first place. Widening the gate means deciding which YAML keys introduce a form section at all — a population question fenced out of this PR by the #13759 triage ruling and filed separately. -
787d757: fix(docs): stop listing
"index"inmeta.jsonpages— it detaches the folder index and shortens 164 breadcrumb trails (#12352)Fumadocs attaches a folder's
index.mdxas that folder's treeindexnode only when the folder'smeta.jsondoes not list it inpages. Listing it makes the page an ordinary child instead, and the folder node reaches every tree consumer with anameand nourl—loader-*.js,buildFolder():if (indexPath) { if (excludedPaths.has(indexPath)) delete node.index; // "index" was listed else excludedPaths.add(indexPath); }
Two surfaces read that one node, and both were degraded:
- Breadcrumb.
getBreadcrumbItems()links a folder crumb toitem.index?.url, so an un-linkable ancestor is dropped rather than emitted name-only (Google requiresitemon everyBreadcrumbListentry but the last). 172 of 404 doc pages advertised a two-level site structure they do not have. - Sidebar.
node.index ? SidebarFolderLink : SidebarFolderTrigger— the section header was inert text, and the section's own overview page sat below it as a child, in six cases under a label identical to the header's.
"index"is removed from 16 of the 17meta.jsonfiles that listed it. It was the firstpagesentry in 15 of them and the first entry after the---Start Here---separator ingetting-started, so no other entry's position depends on it: the measured tree delta is exactly 16 folder headers goingTRIGGER→LINKand 16 index children leaving the child list, with every removed child's URL now the header'shrefand no other line moved.Short trails: 172 → 8. The remaining 8 are
content/docs/releases/, which this PR does not touch — that directory is fenced by AGENTS.md, and itsmeta.jsonstill lists"index".No consumer-side change:
app/[lang]/docs/[[...slug]]/page.tsxreconstructs no URLs, deliberately, so a producer defect of this shape stays visible. - Breadcrumb.
-
49e7abb: feat(docs): give a doc page a short sidebar label (
navTitle) distinct from itstitle(#12311)A doc page's frontmatter
titlewas the only string the site had, so it served six consumers with different length budgets at once — measured onorigin/main, 405 pages undercontent/docs, whose only frontmatter keys aretitle(405) anddescription(405):consumer site SERP title, OG and twitter metadataapp/[lang]/docs/[[...slug]]/page.tsx:211,216,227,233on-page h1page.tsx:150JSON-LD TechArticleheadline/namepage.tsx:123,124JSON-LD BreadcrumbListpage.tsx:95— via the page treellms.txt,llms-full.txt, the.mdxendpointsapp/llms.txt/route.ts:10,lib/source.tsgetLLMTextOpen Graph card image app/og/docs/[...slug]/route.tsx:18sidebar / page tree lib/source.ts—loader()A 50–60 character title carrying search intent is right for the first six and unreadable in the last, which is why #12237's title rewrite stopped after the four pages that have no sidebar entry.
navTitleis the page tree's own string. It is declared on the docs page schema (docsSchema = pageSchema.extend({ navTitle: z.string().optional() })inapps/docs/source.config.ts) and resolved in exactly one place —apps/docs/lib/nav-title.ts, whose header is the mechanism's documentation — throughfumadocs-core's ownPageTreeTransformerhook, the same extension point its built-in icon plugin uses.titleis the declared fallback, stated there and at no read site, so all 405 pages keep their present sidebar entry with no frontmatter change.fumadocs-core@16.14.4ships no first-class equivalent: itspageSchemais{ title, description, icon, full, _openapi }, itsmetaSchemacarries no per-page label field, and its page-tree builder reads{ title, description, icon }off a page.scripts/check-docs-nav-label.mjsre-reads both schemas on every run, so the day an upgrade does ship one, the gate says migrate.The separation is pinned rather than described. That gate holds
navTitleto two code sites, executes the resolver over its fallback cases, and keeps the JSON-LD breadcrumb's leaf crumb onpage.data.title:getBreadcrumbItemsis now called withincludePage: false, so the leaf comes from the page's own title instead of its page-tree node — behaviour-identical today (the node's name is the title), and the one place the short label would otherwise have reached structured data a crawler reads. -
aed92e9: fix(docs): four pages enumerating the flow refusal codes now name
FLOW_INPUT_SCHEMA_INVALID(#13720)FlowRefusalCodegained a fourth member inpackages/runtime/src/flow-dispatch-status.ts(b6d3d76b5), answered422and classified never-dispatched. Three pages were updated with it; four others enumerate the same union and were not, so each stated the enumeration as complete while it was one code short — a teaching surface telling a reader that a status they will really receive does not exist.page the row that was short content/docs/api/declarative-endpoints.mdxthe type: 'flow'delegation rowcontent/docs/api/plugin-endpoints.mdxPOST /automation/:name/triggercontent/docs/protocol/kernel/http-protocol.mdxthe declared-endpoint type: 'flow'answer rowcontent/docs/ui/actions.mdxthe type: 'flow'over-REST rowProse only — no schema, no runtime behaviour and no generated artifact moves. The two generated reference pages (
references/api/contract.mdx,references/api/error-code-ledger.mdx) already carried the code, which is why the generator needed nothing here.Which group the new code joins was read off the source, not inferred from the status.
classifyFlowRefusaltestsFLOW_INPUT_SCHEMA_INVALIDinside the── never dispatched: the producer says WHICH refusal ──arm block, above theresult.status === 'failed'arm that answers400 FLOW_FAILED.ui/actions.mdxis the one page that splits its enumeration into "a run that ran and was rejected" versus "a dispatch that never happened", so the code is placed in the second group there; putting it besideFLOW_FAILEDwould have said the run started.422is now carried by two codes (FLOW_NO_START_NODEandFLOW_INPUT_SCHEMA_INVALID). Each page spells the status together with its code, so every entry stays a self-contained pair rather than a claim about what422alone means — the discriminator iserror.code, which is whathttp-protocol.mdxalready tells readers to branch on. The full table with per-code guidance stays where it is, incontent/docs/automation/flows.mdx. -
aee1fd9: docs(react-pages): delete the unreachable
Array.isArray(result)limb from the live-data sampleObjectStackAdapter.find()cannot resolve to an array, so theArray.isArray(result)arm the live-data sample carried could never be taken. Re-derived against objectui at the sha this repo pins (9602dc82) and again at objectuiorigin/main, which agree line for line:find()returns from five points —{ data: [], total: 0 }for a resource already memoized as missing,{ data: [], total: 0 }for a fresh 404 that is not anenable-block denial, twonormalizeQueryResult(...)calls (the$expand/$searchraw-GET path and the client-SDK path), andreturn existing, which hands back a promise produced by that same set.- Both branches of
normalizeQueryResult()return an object literal with exactlydata,total,page,pageSize,hasMore. The first branch is the one that makes the limb dead: it testsArray.isArray(result)on the transport response and wraps a bare array into that envelope. The array case is folded before any caller sees it.
The sample now reads
result.datadirectly, and a new paragraph under it states the envelope contract so the reason survives the next edit. The twokind:'react'pages inexamples/app-showcasecarrying the same dead limb —crm-workbenchandrenewals-pipeline— were repaired in the same edit, with the derivation recorded in the comment that already explains the neighbouring.recordstrap.Behaviour-preserving:
.datawas read first and always won. What goes is a shape the producer cannot emit, sitting in the page a customer — and a coding agent — copies from. -
d7e8f3e: docs(react-pages): scope the react-only half of the page to the
reacttier (#13737)content/docs/ui/pages.mdxroutes both source-authoring tiers tocontent/docs/ui/react-pages.mdx— the links at:66,:116and:295, the last of which advertised the target as "Thehtmlandreactsource-authoring tiers in full". On that page only the first two sections were tier-neutral. Everything from## What is in scopedown was react-only material carrying no tier marking, so anhtml-tier reader arriving from any of those links read it as their own.That is the mechanism behind the naming trap #13734 closed with one sentence. This closes the rest of the class the same way — marking, not a split: no new page, no repointed links, no section moved between files.
Nine react-only sections were audited against source for the one question "is there a statement here an
htmlauthor could act on and be wrong?". Seven were actively misleading, and all seven are consequences of the same fact the page already states twice up top — anhtmlpage's source is parsed, never executed:## What is in scope— the closure-scope table (React,useAdapter,Block,data/variables/page) is the react runtime's injected scope. Anhtmlpage has no closure scope at all.## Blocks take flat props—parse.tsrefuses everyon[A-Z]attribute (forbidden-attr), so theonRowClickcallback wiring has no html counterpart; and thetype→specTyperescue is the react runtime's (specTypeoccurs nowhere else in this repo). On html the parser builds{ type: tag, ...props }, so atypeattribute overwrites the discriminator — andobject-chartdeclares notypeinput insdui.manifest.jsonanyway.### Block — the escape hatch—compile()whitelistsObject.keys(manifest.components);blockis not one of the 57 keys, so<Block>is not a tag an html page may write.## Live data—useAdapterand hooks exist only where the source runs, and the sample is refused by the html grammar before that matters.## Accepted source shapes— inverted. The html grammar isdocument := element:function Page() { … }and() => …failno-root, and the prescribed fixexport default Page;is a second root (multiple-roots). An html author following the section verbatim writes source that cannot save.## When something throws— describes a runtime that executes. An html page's errors are save-time diagnostics (jsx-forbidden-tag,jsx-unknown-component,jsx-no-root, …), not a React error panel.## `record:*` blocks are not in this tier— inverted, and the sharpest:validateReactPagePropsskips every page whosekind !== 'react', andrecord:details/record:related_listare registered tags in the html manifest. The heading told html authors to stop using the blocks their tier composes record pages with. Retitled to name the tier (anchor#record-blocks-not-in-reactpreserved; the only inbound link is on the same page).
Two sections in the middle of that run are both-tier and are now marked as such rather than swept up:
## Styling's Tailwind rule (page.zod.ts: "Do not author Tailwind classes in page source in either tier") and## How you check your work's three commands. This is why a single marker at the top of the run would have been wrong.Every marker is one bold lead-in that names the tier and then names the html counterpart — #13734's own convention, with
On this tierspelled asOn the `react` tierso it cannot be read as either tier. The three occurrences of the bare phrase already on the page were normalised to match, so the page now contains none.pages.mdx:295no longer claims the page covers both tiers "in full" — it never did, and the audit makes the gap explicit. It now says what the page is: choosing between the tiers, plus thereacttier's guide in full.
-
72d75eb: docs site: drop
output: 'standalone'so the production build stops failingThe production build of the docs site died at the end of
next buildwithENOENT: no such file or directory, open '.../apps/docs/.next/next-server.js.nft.json', so nothing merged tomainreached the site.That file is opened by the standalone packer (
writeStandaloneDirectory->copyTracedFiles), which Next calls only whenoutput === 'standalone'. Nothing in this repo consumes.next/standalone— no Dockerfile, workflow, script or config references it, anddocker/Dockerfiledoes not buildapps/docsat all — and Vercel does its own serverless packaging. The setting served no consumer and was the sole reason that read happened, so removing it removes the only code path that can raise this error.
- 04a29c7: docs: add
concepts/metadata-lifecycle.mdxdocumenting the Repository → Change Log → Cache → Registry data path (ADR-0008), the overlay whitelist invariant (ADR-0005), and end-to-end HMR semantics. Cross-linked fromconcepts/metadata-drivenandguides/contracts/metadata-service. Closes M0 PR-11.
- 15e0df6: chore: unify all package versions to a single patch release
- c7267f6: Patch release for maintenance updates and improvements.
- Release v3.0.0 — unified version bump for all ObjectStack packages.
- Patch release for maintenance and stability improvements
- Patch release for maintenance and stability improvements
- Patch release for maintenance and stability improvements
- Patch release for maintenance and stability improvements
-
a0a6c85: Infrastructure and development tooling improvements
- Add changeset configuration for automated version management
- Add comprehensive GitHub Actions workflows (CI, CodeQL, linting, releases)
- Add development configuration files (.cursorrules, .github/prompts)
- Add documentation files (ARCHITECTURE.md, CONTRIBUTING.md, workflows docs)
- Update test script configuration in package.json
- Add @objectstack/cli to devDependencies for better development experience
-
109fc5b: Unified patch release to align all package versions.
- Patch release for maintenance and stability improvements. All packages updated with unified versioning.
- Patch release for maintenance and stability improvements
-
b2df5f7: Unified version bump to 0.5.0
- Standardized all package versions to 0.5.0 across the monorepo
- Fixed driver-memory package.json paths for proper module resolution
- Ensured all packages are in sync for the 0.5.0 release
- Unify all package versions to 0.4.2
- Patch release for maintenance and stability improvements