Skip to content

Commit f2eaae8

Browse files
os-litantclaude
andauthored
fix(spec): converge root docs meta.json onto the declared page list (#11482) (#12213)
build-docs.ts §3 built the root content/docs/references/meta.json (the sidebar's category list) by filtering on `categoryZodFiles` — the `.zod.ts` files found on disk — a third, independent enumeration of "the pages of this category", alongside §2's own meta.json and §2.5's card grid (both already keyed off `categoryMetaPages` per #11260). A category whose published pages all come from plain `.ts` files (the `misc` catch-all class — `security/misc` proves the shape) has zero `.zod.ts` files while still publishing a page. The old filter would drop such a category from the root sidebar even though it is fully generated and routed everywhere else. No category is in that state today (all 14 have `zodFiles.size > 0`), so this was latent and the regenerated root meta.json is byte-identical (verified: same sha256, empty git diff). §3 now reads `categoryMetaPages`, the same declared list §2.5 already reads, via an extracted `rootCategoryDirs` (scripts/lib/root-meta.ts) pinned with the all-`misc` fixture shape #11260 used for the card grid — the edge has no live instance in the repo, so only a unit test can assert it. Co-authored-by: Claude <noreply@anthropic.com>
1 parent d18bc32 commit f2eaae8

4 files changed

Lines changed: 178 additions & 7 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
fix(spec): the generated docs root sidebar lists categories from the same declared page list `meta.json` and the category index already agree on (#11482)
6+
7+
`build-docs.ts` writes the docs tree from what should be one answer to "which
8+
categories/pages exist", but the ROOT `content/docs/references/meta.json` (§3
9+
— the sidebar's top-level category list) was still answering it a third way:
10+
11+
- a category's own `meta.json` (§2) is built from the pages the run
12+
**emitted**;
13+
- that category's `index.mdx` card grid (§2.5) reads the SAME declared list
14+
(#11260) — no longer a second, independently-derived enumeration;
15+
- the root `meta.json` (§3), until now, filtered on `categoryZodFiles` — the
16+
`.zod.ts` files found **on disk** — a third, independent enumeration.
17+
18+
A category whose published pages all come from plain `.ts` files rather than
19+
`.zod.ts` ones (the `misc` catch-all class `security/misc` proves is real) has
20+
zero `.zod.ts` files while still publishing a page, a `meta.json` and an
21+
`index.mdx`. The old filter would drop such a category from the sidebar even
22+
though it is fully generated and routed everywhere else — a folder complete on
23+
disk and unreachable from the nav.
24+
25+
**No category is in that state today** — all 14 have at least one `.zod.ts`
26+
file — so this was a latent defect with no live instance, and the regenerated
27+
root `meta.json` is byte-identical. The filter now reads `categoryMetaPages`,
28+
the same map §2.5 already reads, so all three files answer from one list
29+
instead of three that happen to agree today. The rule moved into
30+
`scripts/lib/root-meta.ts` (`rootCategoryDirs`), pinned directly with the
31+
all-`misc`-category shape that has no instance in the repo — the same move
32+
#11260 made for the category card grid, for the same reason: the defect's
33+
output is an ABSENT sidebar entry, which `check:docs` cannot see any more than
34+
it could see an absent card, and the edge that has no live instance cannot be
35+
pinned from emitted output at all.

packages/spec/scripts/build-docs.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
renderRootIndex,
4646
type RootIndexCategory,
4747
} from './lib/root-index';
48+
import { rootCategoryDirs } from './lib/root-meta';
4849
import {
4950
buildSchemaIndex,
5051
formatConflicts,
@@ -936,13 +937,12 @@ if (managedCount > 0) {
936937
}
937938

938939
// 3. Update root meta.json
939-
// Collect categories that have actual generated content (non-empty zod files)
940-
const categoryDirs = Object.keys(CATEGORIES)
941-
.filter(cat => {
942-
const zodFiles = categoryZodFiles.get(cat);
943-
return zodFiles && zodFiles.size > 0;
944-
})
945-
.sort();
940+
// Which categories reach the sidebar — from `categoryMetaPages`, the same
941+
// declared list §2.5 reads for its card grid, never a `.zod.ts`-keyed
942+
// re-derivation. See `lib/root-meta.ts` for the defect this replaces and why
943+
// the rule lives in an extracted, unit-testable function (#11482, same fix
944+
// pattern as #11260 one enumeration up).
945+
const categoryDirs = rootCategoryDirs(Object.keys(CATEGORIES), categoryMetaPages);
946946

947947
// Collect other root files (if any exist, like implementation-status.mdx).
948948
// Root-level .mdx is hand-written and never generated, so this reads the disk in
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Which category folders get an entry in the root
5+
* `content/docs/references/meta.json` — the sidebar's top-level category list
6+
* (#11482).
7+
*
8+
* ## The defect this replaces
9+
*
10+
* `build-docs.ts` writes THREE things from what should be one answer to "which
11+
* categories/pages exist": a category's own `meta.json` (§2, from the pages
12+
* this run emitted), that category's `index.mdx` card grid (§2.5, fixed by
13+
* #11260 to read the same declared list), and the ROOT `meta.json` (§3) —
14+
* which, until this fix, filtered on `categoryZodFiles`: the `.zod.ts` files
15+
* found on disk, a fourth, independently derived enumeration.
16+
*
17+
* A category whose published pages all come from plain `.ts` files rather than
18+
* `.zod.ts` ones — the `misc` catch-all class `security/misc` proves is real —
19+
* has ZERO `.zod.ts` files while still publishing a page, a `meta.json` and an
20+
* `index.mdx`. The old filter dropped such a category from the sidebar even
21+
* though §2 and §2.5 both fully generated it: a folder that is complete on
22+
* disk and unreachable from the nav. No category is in that state today (all
23+
* 14 have at least one `.zod.ts` file), so the defect was latent — `check:docs`
24+
* cannot see an ABSENT sidebar entry any more than #11260's card grid could see
25+
* an absent card.
26+
*
27+
* ## Why a function, and why here
28+
*
29+
* Same move `category-index.ts` (#11260), `schema-section.ts` (#7658) and
30+
* `format-type.ts` (#4912) made: the generator is a top-level script with side
31+
* effects, so the only way to assert on the root `meta.json`'s category list
32+
* was to run the whole thing and inspect emitted output — which cannot pin a
33+
* category shape that has no live instance in the repo. Asserting an absence
34+
* needs the rule out of the script.
35+
*
36+
* ## The invariant, now held by construction
37+
*
38+
* A category reaches the sidebar exactly when `categoryMetaPages` has a
39+
* non-empty entry for it — the SAME map §2.5 reads to build that category's
40+
* card grid, and the same "no meta.json ⇒ absent" discipline §2 already
41+
* documents for `categoryMetaPages` itself. One answer, read three times,
42+
* instead of three answers that happen to agree today.
43+
*/
44+
45+
/**
46+
* The category directories that belong in the root `meta.json`'s `pages`
47+
* array, alphabetically sorted.
48+
*
49+
* `categories` is `Object.keys(CATEGORIES)` — every known protocol module,
50+
* whether or not it published anything this run. `metaPages` is
51+
* `categoryMetaPages`: the `pages` array §2 wrote to a category's own
52+
* `meta.json`, present only for a category that published at least one page.
53+
*/
54+
export function rootCategoryDirs(
55+
categories: readonly string[],
56+
metaPages: ReadonlyMap<string, readonly string[]>,
57+
): string[] {
58+
return categories.filter(cat => (metaPages.get(cat) ?? []).length > 0).sort();
59+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Pin for the root `content/docs/references/meta.json` category filter — WHICH
5+
* categories reach the sidebar (#11482).
6+
*
7+
* THE DEFECT THIS PINS. `build-docs.ts` §3 built the root `meta.json`'s
8+
* category list from `categoryZodFiles` — the `.zod.ts` files found on disk —
9+
* a THIRD enumeration of "the pages of this category", independent of both the
10+
* category's own `meta.json` (§2) and its card grid (§2.5, fixed by #11260 to
11+
* read the same declared list §2 wrote). A category whose published pages all
12+
* come from plain `.ts` files (the `misc` catch-all class — `security/misc`
13+
* proves the shape) has zero `.zod.ts` files while still publishing a page, so
14+
* the old filter dropped it from the sidebar even though it was fully
15+
* generated and routed everywhere else.
16+
*
17+
* WHY A UNIT TEST, rather than the regenerated root `meta.json` alone. Same
18+
* two reasons #11260's `category-index.test.ts` gives for the card grid:
19+
*
20+
* - the defect's output is an ABSENT sidebar entry. `check:docs` compares
21+
* generated output to committed output, so a category that was never
22+
* emitted into `pages` is green forever;
23+
* - the edge this exists to catch — a category with pages but NO `.zod.ts`
24+
* file at all — HAS NO INSTANCE in the repo today (measured on
25+
* `origin/main`: all 14 categories have `zodFiles.size > 0`), so no
26+
* generated artifact can pin it in either direction. Asserting on a
27+
* category that does not exist requires the rule out of the side-effecting
28+
* script.
29+
*/
30+
31+
import { describe, expect, it } from 'vitest';
32+
33+
import { rootCategoryDirs } from './lib/root-meta';
34+
35+
describe('rootCategoryDirs — which categories reach the root sidebar (#11482)', () => {
36+
it('includes an all-misc category — no `.zod.ts` files, but it published a page', () => {
37+
// The exact latent shape #11482 named: a category whose every published
38+
// schema falls to the catch-all bucket, so `categoryZodFiles` would record
39+
// zero files for it while `categoryMetaPages` (built from the pages §2
40+
// actually emitted) still holds `['misc']`.
41+
const metaPages = new Map([['ghost', ['misc']]]);
42+
43+
expect(rootCategoryDirs(['ghost'], metaPages)).toEqual(['ghost']);
44+
});
45+
46+
it('excludes a category that published no page', () => {
47+
// §2 writes no `meta.json` for a category with zero pages, so
48+
// `categoryMetaPages` has no entry for it — the same "no meta.json ⇒
49+
// absent" discipline the map already documents at its declaration site.
50+
const metaPages = new Map<string, string[]>();
51+
52+
expect(rootCategoryDirs(['contracts'], metaPages)).toEqual([]);
53+
});
54+
55+
it('sorts the result alphabetically, independent of input or map order', () => {
56+
const metaPages = new Map([
57+
['zebra', ['a']],
58+
['alpha', ['b']],
59+
]);
60+
61+
expect(rootCategoryDirs(['zebra', 'alpha'], metaPages)).toEqual(['alpha', 'zebra']);
62+
});
63+
64+
it('reproduces the real 14-category shape unchanged (#11482 "Measured")', () => {
65+
// Every category on `origin/main` today has both `.zod.ts` files AND
66+
// declared pages, so the old (`.zod.ts`-keyed) and new (`meta.json`-keyed)
67+
// filters agree on all 14 — the byte-identical root `meta.json` the issue
68+
// itself measured, and the acceptance bar this fix must not move.
69+
const categories = [
70+
'ai', 'api', 'automation', 'cloud', 'data', 'identity', 'integration',
71+
'kernel', 'qa', 'security', 'shared', 'studio', 'system', 'ui',
72+
];
73+
const metaPages = new Map(categories.map(c => [c, ['some-page']]));
74+
75+
expect(rootCategoryDirs(categories, metaPages)).toEqual([...categories].sort());
76+
});
77+
});

0 commit comments

Comments
 (0)