Repository navigation
Commit 514bf3c
Fixes #22483
Clause-②: no
The protocol upgrade guide now has a public address: **the docs site,
one page per protocol major**, at
`https://objectstack.ai/docs/protocol-upgrade/MAJOR`, with an index at
`https://objectstack.ai/docs/protocol-upgrade`. The pages are generated
at docs build from the ADR-0087 registries and never committed.
`docs/protocol-upgrade-guide.md` stays at its path as a short,
hand-written pointer stub naming that address. That is condition (2) of
ruling `6078203801` on #22449 (B′), and the two notes in triage
`6081410390`.
## The address: the seat's three hypotheses, measured first
| | Stable across releases | No GitHub login | Versioned per major |
Verdict |
|---|---|---|---|---|
| H1: Release attachment | **no**: one URL per VERSION | yes (public
repo) | per version, not per major | fails |
| H2: docs-site page, generated at docs build | yes | yes (public site)
| yes: one page per major | **taken** |
| H3: npm CDN (`unpkg`/`jsDelivr`) | n/a | n/a | keyed by the npm range,
not a protocol major | not chosen (outside the ruling's two options) |
**H1, measured.** The newest spec Release, `@objectstack/spec@17.7.0`,
carries one asset, `spec-changes.json`, at
`https://github.com/objectstack-ai/objectstack/releases/download/%40objectstack/spec%4017.7.0/spec-changes.json`
(302 to the asset store, no auth). One URL per VERSION.
`releases/latest` answers 302 to
`releases/tag/@objectstack/verify@17.7.0`, another package's Release.
`releases/latest/download/spec-changes.json` redirects to
`.../download/@objectstack/verify@17.7.0/spec-changes.json`, which is
404. So no GitHub URL resolves to "the newest spec release of a major".
Also, no Release carries the guide yet: the 17.7.0 tarball holds
`spec-changes.json` and no guide, because the publish lane from #22533
landed after it.
**H2, measured.**
- **The build can run a spec script.** `apps/docs`'s `build` already
runs `@objectstack/spec`'s `gen:schema` and `gen:docs` before `next
build`. Vercel runs `pnpm turbo run build --filter=@objectstack/docs`
(`apps/docs/vercel.json`), and CI's `Build Docs` evaluates that same
string. This PR adds `gen:upgrade-guide` to that chain.
- **The URLs.** `SITE_ORIGIN` is `https://objectstack.ai`
(`apps/docs/lib/site.ts`) and the docs `baseUrl` is `/docs`
(`apps/docs/lib/source.ts`). So the pages are `/docs/protocol-upgrade`,
`/docs/protocol-upgrade/17` and `/docs/protocol-upgrade/18`.
- **The build sees unreleased majors.** `main` carries
`PROTOCOL_VERSION` `18.0.0` while `packages/spec/package.json` says
`17.7.0`. npm's `latest` is `17.7.0`, whose tarball carries protocol
`17.0.0` (read from its `dist`). So the 17 → 18 hop is unreleased, and
the docs site builds from `main`.
- **Choice: mark, never hide.** Every page carries one state line,
judged from the tree's own `@objectstack/spec` version under the
lockstep contract in `src/kernel/protocol-version.ts`:
- spec major below the page's major: **Not released yet** (the pre-mode
pending major);
- equal major on a prerelease version: **Prerelease**;
- otherwise: **Released**.
Every page also says it is generated from `main`, ahead of the release
that ships a newly landed entry. Why mark rather than hide:
1. `objectstack migrate meta` already replays the chain up to the
highest registered major, so the page shows what the tool prints.
2. The `18.0.0-next.N` line (`.changeset/22080-v18-line-opens.md`) has
readers, and hiding the page would leave them no address.
3. Even a released major's page gains entries between releases, because
launch-window minors register under the current major. "Released majors
only" would still not equal "what shipped". The shipped copy stays the
one in the tarball and on the Release, and every page says so.
- **The local build, with and without the pages.** Same box, one run
each, `rm -rf .next` before each. Each run was under the verify lock,
but unlocked sibling work was running beside it:
| | `next build` exit | compiled | static pages | peak `next-build` RSS
|
|---|---|---|---|---|
| with the pages | 0 | 105 s | 1255 in 35.5 s | 6,166,396 kB |
| without | 0 | 103 s | 1246 in 33.3 s | 6,653,292 kB |
So the pages add no measurable peak memory: noise between the two runs
is larger than the effect. Rendered sizes: `protocol-upgrade/18.html`
3.48 MB, `17.html` 1.20 MB, the index 235 KB. The sidebar shows
"Protocol Upgrade Guide" after "Upgrading".
- **Login-free.** The site is public. A live probe of `objectstack.ai`
is NOT MEASURED: this container's proxy refuses CONNECT to that host
(403).
**H3, measured.** Not chosen: the ruling names two options and this one
is outside them. It also does not key by protocol major: `npm view
@objectstack/spec@18 version` answers `E404 No match found for version
18`, because protocol 18 ships as `18.0.0-next.N` and a bare `@18` range
never matches a prerelease. `@17` resolves to `17.7.0`, which carries no
guide, and pre mode cuts no further 17.x. Live CDN reachability is NOT
MEASURED: the proxy refuses CONNECT to `unpkg.com` and
`cdn.jsdelivr.net` (403).
## What changed
- **`packages/spec/scripts/build-upgrade-guide.ts`**: `build()` is split
into section renderers:
- `howToUpgrade`, `hopSection` and `footer`;
- the single-file guide `--out` writes is byte-identical to before,
except its second header comment. That comment now names the command
that writes the file (`exec tsx scripts/build-upgrade-guide.ts --out
FILE`), because `gen:upgrade-guide` no longer does. Measured: `diff` of
the base's and this branch's `--out` output is that one line.
- **No flag** (`gen:upgrade-guide`) now writes the docs pages into
`content/docs/protocol-upgrade/`, never
`docs/protocol-upgrade-guide.md`, so the stub cannot be overwritten.
`--docs DIR` writes them elsewhere.
- `--check` and `--out` are unchanged.
- **`packages/spec/scripts/lib/upgrade-guide-docs.ts`** (new) assembles
the tree: `index.md`, one `MAJOR.md` per hop the chain carries, and a
`meta.json` listing them newest first.
- A page holds the shared how-to, its own hop section verbatim from the
same renderer, and the footer. It has no body h1, and its frontmatter is
quoted so YAML never reads a colon or an arrow as structure.
- The writer removes only files it wrote. A page whose hop fell below
the support floor does not linger, and a directory holding anything else
is refused untouched.
- **`apps/docs/package.json`**: `build` runs `gen:upgrade-guide` before
`next build`.
- **`content/docs/meta.json`**: `protocol-upgrade` is listed after
`upgrading`. When the folder is absent (a checkout that has not run the
docs build), fumadocs skips the entry. Measured in `fumadocs-core`'s
`resolveFolderItem`: an item that resolves to no node returns without
one.
- **`.gitignore`**: `content/docs/protocol-upgrade/`.
- **`docs/protocol-upgrade-guide.md`**: the stub. It names the index,
one line per major today (17 and 18), and the pattern for the next one.
It also says where a release's shipped copy lives, and how to generate
either form.
- **`packages/spec/scripts/check-generated.ts`**: the
`check:upgrade-guide` row names the docs pages as its artifact.
- **`packages/spec/scripts/lib/projection-cli.ts`**: `committedPath` is
optional. The guide no longer has a committed copy, so its runner passes
none. A bare run without one is a usage error (exit 2) that builds
nothing.
- **`.gitattributes` / `scripts/regen-artifacts.mjs`** (seat answer
`6092165230`, option B): the guide's `merge=os-regen` route and its
`REGEN_ARTIFACTS` row are removed. `gen:upgrade-guide` is recorded as
`NOT_DRIVER_MANAGED` for `content/docs/protocol-upgrade/**` (untracked).
The header prose no longer calls the guide a generated single file. The
`spec-changes.json` route stays for #22485.
- **`scripts/check-role-word.mjs`** (seat answer `6093145489`, Q2 → A):
`content/docs/protocol-upgrade` is in `SKIP_SUBTREES`, for the reason
`content/docs/references` is: generated prose whose fix site, the
registry `.ts`, this markdown gate cannot reach.
- The self-test now accepts an untracked tree that `NOT_DRIVER_MANAGED`
declares where it required one on disk, and it requires every such tree
under a ROOT to be skipped.
- A new program-level battery runs the gate over a fixture page inside
each skipped tree (exit 0, nothing read), one level up (NEW use, exit
1), and in a sibling directory sharing the prefix (NEW use, exit 1).
- Both directions hold: with the pages on disk the gate exits 0, and a
violation planted in `content/docs/build-without-code.mdx` still exits
1, the only problem reported.
- Ablation, dropping the entry: exit 1, with 3 failures with the pages
on disk and 2 without, restored to blob == HEAD.
- **`.changeset/22483-upgrade-guide-docs-address.md`**:
`@objectstack/spec` `patch`. The shipped `protocol-upgrade-guide.md`
changes by its header comment, and the address is user-facing news.
`apps/docs` is private.
## Every published pointer, with its resolves check
Every pointer spells the repo path `docs/protocol-upgrade-guide.md` as
text, never as a URL (`git grep -o` over the tree: 71 bare
`docs/protocol-upgrade-guide.md` spellings, no `github.com`,
`raw.githubusercontent.com` or `objectstack.ai` form). The check is the
same for each:
- the path exists on this branch and is the stub;
- the stub names `https://objectstack.ai/docs/protocol-upgrade`;
- for the one pointer that names an entry id, that id is on the page the
stub sends the reader to.
On GitHub,
`blob/claude/issue-22483-upgrade-guide-address/docs/protocol-upgrade-guide.md`
and its `raw.githubusercontent.com` form both answer 200, and the
fetched bytes name the address 4 times.
| Pointer (file · mentions · first line) | Shipped in its package |
Resolves |
|---|---|---|
| `packages/client/README.md` · 1 · `:141` (cites
`batch-options-validate-only-retired`) | yes (`files[]` has `README.md`)
| ✓ stub → `/docs/protocol-upgrade/17`; that id is on the 17 page (3
hits in the built `17.html`) |
| `packages/cli/CHANGELOG.md` · 3 · `:14221` | yes | ✓ stub |
| `packages/client/CHANGELOG.md` · 4 · `:5655` | yes | ✓ stub |
| `packages/core/CHANGELOG.md` · 1 · `:2013` | yes | ✓ stub |
| `packages/create-objectstack/CHANGELOG.md` · 1 · `:1544` | yes | ✓
stub |
| `packages/metadata-protocol/CHANGELOG.md` · 2 · `:14511` | yes | ✓
stub |
| `packages/runtime/CHANGELOG.md` · 3 · `:10073` | yes | ✓ stub |
| `packages/services/service-automation/CHANGELOG.md` · 2 · `:9260` |
yes | ✓ stub |
| `packages/services/service-package/CHANGELOG.md` · 1 · `:3432` | yes |
✓ stub |
| `packages/spec/CHANGELOG.md` · 41 · `:1825` | yes | ✓ stub. Some of
its mentions are the bare `protocol-upgrade-guide.md`, the in-package
file, which still ships |
| `packages/spec/src/migrations/entries/README.md` · 1 · `:99` | no
(`files[]` ships `src/**/*.zod.ts` only) | ✓ stub |
These are the eleven of the ruling: ten shipped files plus the entries
README. No CHANGELOG is edited. The in-repo, unpublished mentions keep
resolving through the same stub: ADR-0087 `:348`,
`docs/audits/2026-08-partial-retirement-annotation-signal.md:94`, the
#22482 changeset, and scripts.
## File surface
Inside the claim: the stub, `build-upgrade-guide.ts` and its tests, the
docs wiring that generates the address (`apps/docs/package.json`,
`content/docs/meta.json`, `.gitignore`), `check-generated.ts`'s row
(named by the dispatch), and one changeset.
Beyond it, each named here:
- **`scripts/check-future-spec-major.mjs`**: deleted one
`QUOTATION_EXEMPTIONS` row. This was forced: with the stub in place the
gate went red, `QUOTATION_EXEMPTIONS entry 6
(docs/protocol-upgrade-guide.md, major 4997) matched NOTHING … Re-point
it or delete it.` The row excused the generated copy of one entry's
evidence sentence, and the stub no longer carries it. After the deletion
the gate reads `7 witnessed ledger entr(y|ies), every witness still
matching`, and `--self-test` reports 29 cases.
- **`packages/spec/scripts/lib/projection-cli.ts`**: the optional
`committedPath` above. This is the no-flag contract of
`build-upgrade-guide.ts`'s default write target, the item the claim
names.
- **`packages/spec/scripts/lib/upgrade-guide-docs.ts`** and
**`upgrade-guide-docs.test.ts`**: new files in the same scripts tree.
- **`.gitattributes`** (the guide's route line, and the header prose
from `:20`) and **`scripts/regen-artifacts.mjs`** (the row moves to
`NOT_DRIVER_MANAGED`), per seat answer `6092165230`.
- **`scripts/check-role-word.mjs`** (seat answer `6093145489`): a local
docs build made the gate report 99 NEW uses (17.md 56, 18.md 43) that CI
never saw, because the tree is gitignored.
**Removed here (seat answer `6092165230`, option B).** `.gitattributes`
drops the guide's route, and its header no longer calls the guide a
generated single file. `regen-artifacts.mjs` drops the row and records
`gen:upgrade-guide` as `NOT_DRIVER_MANAGED`
`content/docs/protocol-upgrade/**` (untracked). `check:merge-driver`
reads 17 routed paths, 42 generators with a disposition, and 4 untracked
dispositions. The `spec-changes.json` route stays for #22485.
Why it went here: the route was actively wrong for a hand-written file.
Before the removal, `scripts/pm/os-regen-merge.sh` twice put main's
generated guide back over the stub (`bd5a3284d0`), and the stub was
restored by blob (`9473b0f21f`). The scratch merge after the removal
measured three cases:
- From a checkout carrying this PR, merging a branch with a regenerated
guide CONFLICTS on the stub. With the route (`8f4ae58944`), the same
merge was silent.
- A branch forked before this PR still defers on its own route at its
first plain `git merge` of `main`. Its next commit and push are then
refused, where with the route they passed and the stub was lost. So no
direction stays silent.
- This PR's own merge of `main` at `bf6942b723` conflicted on the stub
and kept it.
## Verification
All at `7bc190c714` unless a line says otherwise. The last merge of
`origin/main` is `bf6942b723`, which brought in `96e4be4829`, whose
`20e7d52097` regenerated the guide; it conflicted on the stub, which was
kept (blob `9473b0f21f`). `origin/main` has stayed at `99801d831f`
since, and GitHub reports the PR mergeable.
- **Spec suites**, under `scripts/pm/os-verify-lock.sh`, VERDICT
command-exit 0:
- at `87a9a3435e`: `vitest run --project local --maxWorkers=2`: 636
files passed, 18970 tests passed, 1 todo;
- at `87a9a3435e`: `--project repo`: 54 files, 915 tests passed;
- at `bf6942b723`: `build-schemas-check-mode.test.ts` (repo project)
88/88;
- at `7bc190c714`: the five generator and ledger test files, 50/50.
- **`pnpm --filter @objectstack/spec typecheck`**: VERDICT command-exit
0. `tsc -p tsconfig.scripts.json --listFiles` lists all five touched or
new script files, tests included.
- **The new and changed tests**: `upgrade-guide-docs.test.ts` (15) and
`projection-cli.test.ts` (12, one new), 27 of 27 passing. The
real-generator block proves three things:
- every page's hop section is the single file's section verbatim, and
pages exist for exactly the file's hops;
- `--docs` is deterministic;
- `--docs` with `--check`, with `--out`, or with no directory exits 2
and writes nothing;
- a directory holding a hand-written file exits 1 with nothing removed.
- **Gates**: `node scripts/pm/dispatch-gates.mjs --commands` derived 128
at `7bc190c714`. All ran with the gitignored docs pages on disk
(`gen:upgrade-guide` first), and `--ran` reports 128 derived, 128 run, 0
NOT-MEASURED, 0 UNRUN. All 128 exit 0, `check:role-word` included.
Without the pages (CI's tree) `check:role-word` also exits 0, and it
reads 254 files either way. Among them: `check:generated` ("All 15
generated artifacts are up to date"), `check:upgrade-guide`,
`check:spec-changes`, `check:future-spec-major`, `check:merge-driver`,
`check:cross-package-test-inputs`, `check:doc-frontmatter`,
`check:nul-bytes` and `check:pm-dispatch-gates`.
- **ESLint, narrowed and measured.**
- Population: the 9 changed JS/TS files (adding
`scripts/regen-artifacts.mjs` and `scripts/check-role-word.mjs`),
measured at `7bc190c714`, all matched by `eslint.config.mjs`'s
`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` object.
- `--no-inline-config --format json` reports 9 files, 0 errors and 0
warnings.
- The config sets no `parserOptions.project`, so no type-aware rule can
move a verdict on a file this diff does not touch.
- The repo-wide `pnpm lint` is CI's.
- **Docs build**: `apps/docs` `pnpm build` (gen chain plus `next
build`), exit 0, at `278751de7a` (this PR's content, before the merge).
See H2. `pnpm turbo run build --concurrency=2` gave 73/73 tasks with
`@objectstack/docs#build` at `bf6942b723` (6m23s), and 73/73, all
cached, at `7bc190c714`.
## Acceptance notes
- **A stale branch after this lands (carrier #22485, pointer
`6093148255`).** A branch forked before this PR that carries a
regenerated guide defers it at its first plain `git merge` of `main`.
Its next commit and push are then refused by `check-regen-pending.mjs`,
with a remedy that cannot apply: "absent from
scripts/regen-artifacts.mjs (cannot verify)". Restoring the stub does
not clear it; deleting the pending marker by hand does. #22485's route
removal creates the same state for `spec-changes.json`.
- **ADR-0087's true-up note** (`40735ea58d`, #22561) still says the
guide's route goes in #22485 and that `gen:upgrade-guide` writes the
committed copy. Per seat answer `6093145489` (Q3 → A), #22485 corrects
it in its governed ADR edit.
- **CI's `docs` path filter** (`ci.yml`) does not list
`packages/spec/**`. So a registry entry whose prose the docs build
cannot compile would surface in Vercel's production build, not in `Build
Docs`. The pages are `.md` (CommonMark, no JSX), which parses arbitrary
text, and `gen:docs` has the same pre-existing exposure.
- **Raw angle brackets in prose**: 8 lines of the generated prose carry
raw angle-bracket tokens outside code spans, for example the entry that
writes the field key as `('field', OBJECT.NAME)` with the two names in
angle brackets. A Markdown renderer reads those as HTML. That is a
property of the registry text, not of this change, and its rendering on
the site is not measured here.
- **The floor**: a hop leaves the site when the support floor rises past
it (ADR-0087 D3), and the stub says so. The next major needs no stub
edit to be found: the index lists it, and the stub states the per-major
pattern.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 874a38d commit 514bf3c
15 files changed
Lines changed: 860 additions & 1696 deletions
File tree
- .changeset
- apps/docs
- content/docs
- docs
- packages/spec/scripts
- lib
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
20 | | - | |
21 | | - | |
22 | | - | |
23 | | - | |
24 | | - | |
25 | | - | |
26 | | - | |
27 | | - | |
28 | | - | |
29 | | - | |
30 | | - | |
31 | | - | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
32 | 33 | | |
33 | 34 | | |
34 | 35 | | |
35 | 36 | | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
36 | 51 | | |
37 | 52 | | |
38 | 53 | | |
| |||
173 | 188 | | |
174 | 189 | | |
175 | 190 | | |
176 | | - | |
177 | 191 | | |
178 | 192 | | |
179 | 193 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
103 | 103 | | |
104 | 104 | | |
105 | 105 | | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
106 | 109 | | |
107 | 110 | | |
108 | 111 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | | - | |
| 9 | + | |
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
| 17 | + | |
17 | 18 | | |
18 | 19 | | |
19 | 20 | | |
| |||
0 commit comments