Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2179,6 +2179,40 @@ jobs:
- name: Check no new same-name dual-source spec exports
run: pnpm --filter @objectstack/spec run check:dual-source-exports

# [#10199] The mechanized form of the 2026-08-20 ruling on #10096:
# 「浏览器可达的 spec 导出面必须 schema-free」. Walks the built module graph
# behind every subpath the exports map publishes and fails if one DECLARED
# browser-reachable (browser-reachable-entries.json — today `./meta-spelling`)
# links zod, directly or through a relative hop, or links an external it has
# not declared.
#
# WHY IT NEEDS A GATE AT ALL. The principle bound as prose until now, and
# the failure it guards was invisible to every check this repo had: one
# string fold reached through `/shared` cost +246.9 KB minified / +69.7 KB
# gzipped, one pure predicate through `/security` cost +261.5 KB, and
# objectui#5324 records the CONSUMER's own bundle-budget check reporting
# PASS on the PR that added the bytes. A budget cannot attribute a delta
# that arrives inside a dependency it already imports; a module graph can.
#
# WHY THIS JOB AND THIS POSITION. It reads the BUILT `.mjs`/`.js` bundles a
# consumer's import resolves to — not the source, and not the `.d.ts` — so
# it belongs after the build step with the other dist-reading consumer
# gates (`check:exported-any`, `check:dual-source-exports` above), not with
# the tsx-only artifact gates near the top. On an unbuilt or stale tree it
# is a HARD REFUSAL naming the build command, never a skip: a
# build-dependent gate that reads nothing reports NOT MEASURED as if it
# were measured and clean (#4690).
#
# It adds no required context — a step in an existing job, so no open PR
# waits on a check whose name no head has ever reported (#9325). Reads 34
# bundles in ~1.5s. Self-tests first, like its two neighbours, and that
# self-test is load-bearing here for a specific reason: this tree has ZERO
# relative hops between bundles (every entry is self-contained), so the
# transitive walk that keeps the gate honest the day `splitting` is turned
# on can only be exercised against fixtures.
- name: Check declared browser-reachable spec entries link no zod
run: pnpm --filter @objectstack/spec run check:browser-reachable-entries

# Anti-drift for the skill EXAMPLES, not just the skill reference indexes
# (#3094). The TypeScript in skills/ is the first thing an AI copies when
# authoring metadata, yet nothing type-checked it — so it rotted silently
Expand Down
33 changes: 33 additions & 0 deletions packages/spec/browser-reachable-entries.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"_comment": "The DECLARED browser-reachable export surface of @objectstack/spec (#10199), and the contract half of check:browser-reachable-entries. The 2026-08-20 maintainer ruling on #10096 minted the standing principle 「浏览器可达的 spec 导出面必须 schema-free」: an export surface browser/client consumers reach carries vocabulary — maps, folds, enums, pure predicates — without linking the zod schema/validation machinery. This file says WHICH entries make that promise; the gate reads the BUILT bundle each one resolves to and proves the promise. Every subpath in package.json's exports map must appear in exactly one of the three sections below — an entry nobody classified fails the gate's reconciliation rather than quietly dropping out of coverage (the check:generated ledger pattern). Hand-maintained on purpose, like dual-source-exports.baseline.json next door: promoting an entry to browserReachable is a CONTRACT decision a maintainer makes, not something a gen: command should be able to grant. The gate is additive/shrink-only — an unlisted entry is unjudged, so no weakening question arises.",
"browserReachable": {
"./meta-spelling": {
"why": "The reference pattern the ruling shipped (#10096): the /meta URL-spelling contract as vocabulary — one frozen map, one fold, two refusal predicates — with the map derived at BUILD time by gen:meta-url-spelling so no schema graph is linked. Reaching the same four symbols through ./shared cost a browser consumer +246.9 KB minified / +69.7 KB gzipped.",
"externals": []
}
},
"_unjudgedComment": "Entries this gate asserts NOTHING about. They are not declared browser-reachable, so linking zod is not a defect here — these are the server/build-time surfaces. Listing them is what makes the reconciliation two-directional: a new exports-map subpath cannot land without someone deciding which list it belongs in. Moving one UP to browserReachable is the only direction this file is meant to travel, and it is a maintainer decision plus a passing gate, never a ledger edit alone.",
"unjudged": [
".",
"./ai",
"./api",
"./automation",
"./cloud",
"./contracts",
"./data",
"./identity",
"./integration",
"./kernel",
"./qa",
"./security",
"./shared",
"./studio",
"./system",
"./ui"
],
"_notAModuleComment": "Exports-map subpaths that resolve to something other than a JS module, so there is no module graph to walk and no zod link to look for. A separate section from `unjudged` because the two say different things to a reader: `unjudged` means 'a module we chose not to judge', this means 'not a module at all'. The gate refuses to scan these rather than silently reporting them clean.",
"notAModule": [
"./openapi.json",
"./package.json"
]
}
1 change: 1 addition & 0 deletions packages/spec/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@
"check:export-origins": "tsx scripts/build-export-origins.ts --self-test && tsx scripts/build-export-origins.ts --check",
"check:exported-any": "tsx scripts/check-exported-any.ts --self-test && tsx scripts/check-exported-any.ts",
"check:dual-source-exports": "tsx scripts/check-dual-source-exports.ts --self-test && tsx scripts/check-dual-source-exports.ts",
"check:browser-reachable-entries": "tsx scripts/check-browser-reachable-entries.ts --self-test && tsx scripts/check-browser-reachable-entries.ts",
"check:authorable-surface": "OS_EAGER_SCHEMAS=1 tsx scripts/build-schemas.ts --check",
"gen:migration-registry": "tsx scripts/build-migration-registry.ts",
"check:migration-registry": "tsx scripts/build-migration-registry.ts --self-test --check",
Expand Down
Loading
Loading