Uh oh!
There was an error while loading. Please reload this page.
feat(spec): gate the shipped llms.txt against the real package surface - #11688
Merged
Conversation
`packages/spec/llms.txt` ships in the npm tarball as context for AI consumers and is hand-kept with no generator, so nothing ever re-derived what it asserts. It had drifted: eleven advertised symbols existed in no entry point, two advertised packages did not exist, the schema-inventory heading disagreed with the sum of its own table, and the package heading claimed 19 against a real 68. Add `check:llms-txt`, which re-derives every checkable claim: symbols against the checked-in api-surface/ shards, subpaths against the manifest `exports`, per-domain counts against src/<domain>/**/*.zod.ts, and the package table against the workspace. Symbol claims resolve at the strictness their position earns — namespace bullets and fenced imports name an entry point and must resolve from it, the architecture overview resolves against the union. Repair the claims the gate was otherwise born red on. Deliberately no `gen:llms-txt`: the numbers are not the claim, the prose beside them is. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rxnd8cyFnoU8V5y21PaTsy
Contributor
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 126 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
os-warren
marked this pull request as ready for review
August 24, 2026 13:21
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#11344
Option B of the card ("checked hand-keeping"), per the triage fence: a freshness gate over
packages/spec/llms.txt, no new generation pipeline.Why
packages/spec/llms.txtships in the published tarball (filesarray, registered incheck-published-files.mjsas "Protocol summary for LLM consumers") and has no generator, sonothing ever re-derived what it asserts. It is read by the audience least able to notice staleness
and most likely to turn a claim into code: an agent that reads
- `FormSchema`: layoutunderimport * as UI from '@objectstack/spec/ui'writes an import that does not resolve.Re-measuring on
origin/mainthis stroke — the card's premise held, and was wider than itrecorded. #10833 fixed one phantom row (
IGraphQLService) and the header provenance line; it didnot reach the rest:
@objectstack/nextjs,@objectstack/nestjs)qamissing entirelyThe
IUIServicerow is the one that argues for a gate rather than another hand-audit: thatcontract was removed in v11 (this package's own
CHANGELOG.md: "Remove the deprecatedIUIServicecontract (useIMetadataService) — 11.0"), and the row outlived it by two majors.Worse — #10833's changeset cited the
IUIServicerow beside it as a live precedent for how totreat a superseded contract, never noticing it was itself dead. A hand-audit standing directly next
to the defect did not see it.
The gate —
check:llms-txtpackages/spec/scripts/check-llms-txt.ts, wired ascheck:llms-txtbeside its siblings, with a--self-testfirst leg per house pattern. Five invariants: SUBPATH, NAMED, COUNTED,LISTED, STRUCTURE (the script header is the authority; each is named there with the defect
it catches).
Authorities, all checked-in — the gate needs no build:
api-surface/shards, already kept equal to the builtdist/*.d.tsbycheck:api-surface, and shipped in the same tarball so a consumer can verify any claim withoutleaving the package. (
src/would be the wrong authority twice over: a name can be declaredthere and never re-exported, and a name can be exported from a barrel with no matching
export constline anywhere —DriverInterfaceSchemais exactly that, absent to asrcgrepand perfectly importable.)
exportskeys.*.zod.tspopulation under each domain directory ofpackages/spec/src,counted recursively. Not a semantics I picked: two of the file's thirteen rows (
kernel32,ui18) still match this measure exactly, which recovers the derivation the original author used.@objectstack/*workspace set, enumerated the waycheck-published-files.mjsenumerates it, with the same loud refusal on a workspace glob richerthan a bare directory or a single directory level.
Symbol claims resolve at the strictness their position earns, which is the one piece of design
worth reviewing:
architectural layers, and a concept legitimately lives in a neighbouring entry point
(
IDataDriveris described under the data layer and exported from./contracts, exactly asthis file's own §5 example imports it). Demanding entry-point agreement there would manufacture
failures for true claims. Self-test case 6 pins this deliberately, so tightening it later reads
as a decision rather than a cleanup.
import * as N from '@objectstack/spec/x', so the entry point is part of the claim../contractsonly. This is the positionIGraphQLServiceandIUIServiceoccupied.Fenced
import ... from '@objectstack/spec/x'statements are read at strictness 2 — the mostdirectly copy-pasteable claim in the file.
Deliberately out of population, stated in the script header: prose; code-fence bodies (only
import specifiers are read — deciding which other identifiers in a snippet are protocol symbols is
a judgement the gate would have to invent); and the
N+lower-bound figures (46+ types,7,095+,1,470+) — see Open question.No
gen:llms-txt, and the classification is a safety property rather than bookkeeping, thesame one
check:objectui-pin-citationscarries. The numbers are not the claim, the prose besidethem is: restamping
| integration | 7 | Connector (Database, File Storage, GitHub, MQ, SaaS, Vercel) |to1would leave a freshly-dated row listing six connectors that do not exist —converting a loud staleness into a silent lie. Registered
NO_GENERATORin thecheck:generatedledger (without which this PR fails its own
--reconcile-onlystep).Repairs — every one evidence-pinned, none invented
The gate is born red without these. Repair rule applied, stated so each is reviewable:
(1) exists under another entry point → keep (§1) or re-attribute (§4); (2) phantom whose
own bullet text denotes a real same-domain export → rename to it; (3) phantom with no
counterpart → delete the claim (#10833's landed precedent on this file: a deprecation note for
something with no replacement invents a migration path that does not exist).
AnalyticsSchemaCubeSchema./dataexportsCube/CubeSchema/DimensionSchema/defineCubeIdentitySchemaOrganizationSchema./identityexportsOrganizationSchema,MemberSchema,SCIMUserSchema; noIdentity*anywhereEndpointSchemaApiEndpointSchema./apiThemeSchemaThemesubstring scan matches nothing across all 17 shardsRAGPipelineSchemaKnowledgeSourceSchemaRAG/Ragprefix scan matches nothing;./aiexports theKnowledge*familyMCPSchemaMCPServerRefSchema./aiFilterSchemaFilterArraySchema./dataIDataDriverunder/data./contractsnote./contractsFormSchemaFormViewSchema./uiPolicySchemaKernelSecurityPolicySchema./kernelSystemblock on/systemKernelblock on/kernel./kernelexportsContractSchemaResponseEnvelopeConfigSchema,ApiErrorSchema./apiIUIServicerow@objectstack/nextjs,@objectstack/nestjsdriver-sql,typespackages/adapters/holds onlyhono§3's table is rewritten against the tree (14 domains, 207) and §7's heading corrected to 68, with a
line making the curation explicit rather than implicit — the
check:quick-reference-countslessonthat a curated table must say it is curated.
Verification
All readings on the final commit
4ebac563a; every exit code captured before any pipe.pnpm --filter @objectstack/spec check:llms-txt→ exit 0:check-llms-txt self-test: 20 cases pass.andpackages/spec/llms.txt: 97 claim(s) re-derived — every advertised symbol resolves against api-surface/ (17 entry points), every subpath is a real exports key, the inventory matches src/ (14 domains, 207 schemas) and the package table matches the workspace (68 published).origin/mainhad it): exit 1, 35 findings— 18
named, 13count, 4listed— reproducing every drift measured by hand above.IUIServicerow.On disk: injected-token count
0 → 1,git diff --stat1 insertion(+). Mutated run exit 1,naming
llms.txt:173 [named] service-contract table advertises IUIService, which @objectstack/spec/contracts does not export (it exists nowhere). Restore leg: injected count backto
0,git status --porcelainclean, gate exit 0. No rebuild leg is stated because the gatereads no
dist— its inputs arellms.txt,api-surface/*.json,package.json,src/andpnpm-workspace.yaml, all source or checked-in.pnpm --filter @objectstack/spec typecheck→ exit 0 (tsc --noEmit+check:scripts-typecheck+check:test-typecheck).pnpm --filter @objectstack/spec check:generated --reconcile-only→ exit 0 (the new script isclassified).
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackon thereal changeset (6 paths, derived from the merge base — no hand-written path list), all exit 0:
check:published-files,check:nul-bytes,check:merge-driver,check:slot-lookup,check:test-source-alias,check:type-source-resolution,check:cross-package-test-inputs,check:override-consistency,check:release-page-status,check:changeset-gate-self-tests,check:objectui-changeset,check:node-version,check:required-contexts,check:shard-attestation,check:workflow-status-functions,check:pnpm-filter-targets,check:type-check-coverage, speccheck:liveness/check:empty-state/check:strictness-ledger/check:variant-docs, and thecheck-adr-0087-registration,check-empty-changeset,check-changeset-no-major,check-changeset-fixed,check-osv-exemptions,check-aggregator-roster,check-step-collectors,check-plugin-teardown-shapescripts.pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2→ exit 0:Test Files 420 passed (420)·Tests 11218 passed (11218).Declared narrowing
pnpm check:type-check-debtrefuses in an unbuilt worktree — its--re-measureleg needs 56workspace dependencies'
dist/*.d.tson disk and says so explicitly rather than measuring adifferent world. Not run here, and it cannot be moved by this diff:
@objectstack/speccarries noDEBT/TEST_DEBT ledger entry, and the only TypeScript added sits in
packages/spec/scripts/, whoseprogram is
tsconfig.scripts.json— covered bycheck:scripts-typecheck(green above) and holdingzero ledger entries by design. Its coverage half,
pnpm check:type-check-coverage, ran green. CIbuilds the closure before this step.
Repo-wide
pnpm lintwas not run locally; CI owns that scan.Open question for the maintainer
The three
N+figures —46+field types,7,095+.describe()annotations,1,470+JSONSchemas (really 1,586 today) — are out of the gate's population on purpose. They are checkable
only in the weak sense "real is at least claimed", which goes red only if the protocol shrinks,
so such a check would sit green while the figure drifts arbitrarily far — the shape
check-quick-reference-countsnames when it refuses a comparison that can never fail. Making themhonest means choosing between an exact number that churns on most spec PRs and no number at all;
that is a content decision, not something this gate should settle by picking an operator. Happy to
follow up either way.
Generated by Claude Code