Skip to content

fix(client): the published README's packages.install example parses against ManifestSchema - #18771

Merged
os-support-ai merged 2 commits into
mainfrom
claude/issue-18607-client-readme-install-example
Sep 17, 2026
Merged

os-support-ai merged 2 commits into
mainfrom
claude/issue-18607-client-readme-install-example

Conversation

@os-support-ai

@os-support-ai os-support-ai commented Sep 17, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #18607

The client.packages.install example in the published @objectstack/client
README shipped a manifest that the contract its own call site declares refuses on
three counts. The example is what was wrong; ManifestSchema's strict close is
what is right, so nothing in packages/spec moves here.

BEFORE — the three refusals, reproduced

Driven against PackageInstallRequestSchema (whose manifest key is
ManifestSchema), with the literal read out of the README by content, not by
line number:

LITERAL AS PUBLISHED:
{
  name: 'vendor_plugin',
  label: 'Vendor Plugin',
  version: '1.0.0',
}

success: false
  code=invalid_type       path=[manifest, id]
  code=invalid_value      path=[manifest, type]
  code=unrecognized_keys  path=[manifest]  keys=["label"]
ISSUE COUNT: 3

All three reproduce exactly as the card states them.

AFTER — parsed and accepted

LITERAL AS PUBLISHED:
{
  id: 'com.vendor.plugin',
  type: 'plugin',
  name: 'Vendor Plugin',
  version: '1.0.0',
}

success: true

Parsed value, defaults applied:
{"id":"com.vendor.plugin","defaultDatasource":"default","version":"1.0.0","type":"plugin","scope":"project","name":"Vendor Plugin"}

Controls — each one had to fire, and each did

candidate verdict
chosen manifest accepted
chosen minus id invalid_type at [manifest, id]
chosen plus label unrecognized_keys at [manifest]
chosen with type: 'vendor' invalid_value at [manifest, type]

label at the ROOT — what the schema actually declares

Read off the schema itself, not off a neighbour: ManifestSchema declares 25
root keys and label is not one of them. The required-and-absent set, taken
from ManifestSchema.safeParse({}), is id name type version.

name — already present in the old example — is declared as the human-readable
package name, so the old label: 'Vendor Plugin' value moves to name, and the
machine identifier becomes the reverse-domain id that key documents.

The retired { id, label, path } shape carrying a label near this surface is
manifest.contributes.themes, a nested block removed in v17.x — a sibling of
the root, never the root itself. A root label was never declared and was
therefore never retired: it gets the strict close's plain unrecognised-key
refusal, with the surface's own history text attached.

Why type: 'plugin'

The closed set, quoted from the refusal the schema itself emits:
"plugin" | "ui" | "driver" | "server" | "app" | "theme" | "agent" | "objectql" | "module" | "gateway" | "adapter".

plugin is the schema's own "general-purpose functionality extension", and it is
what this example's subject already says it is — the literal was named
vendor_plugin/Vendor Plugin and the line beneath it enables a plugin id.
app is the consumer-installable business bundle (ADR-0019, the one user-visible
noun a tenant browses and installs) and would change what the example teaches;
gateway is marked deprecated in the enum's own docblock. Not the first member
that parses — the member the example means.

Duplicate copies — reported, NOT silently folded in

Swept the tree for the same example and for the same defect class.

  • The same literal: nowhere else. vendor_plugin and Vendor Plugin each
    occur exactly once in the repository, both in this README.
  • Same defect class, different literal, ⛔ NOT in this PR:
    content/docs/api/client-sdk.mdx line 337 —
    await client.packages.install({ id: 'com.objectstack.plugin-auth', version: '1.0.0' });
    — refused on two counts, invalid_value at [manifest, type] and
    invalid_type at [manifest, name]. It is a hand-written page (it is listed in
    scripts/docs-audit/handwritten-docs.json). Left for a ruling rather than
    silently widened or silently left broken.
  • Not a defect: packages/client/src/return-type-precision.test.ts carries the
    same two-count shape, but as a expectTypeOf pin against a parameter typed
    any; nothing parses it and it is not published material.
  • Not a duplicate: the many label: keys under content/docs/references/api/
    belong to sibling collections (apps, pages, flows, agents, …) which
    each declare label legitimately. That is the root-versus-sibling distinction,
    confirmed rather than assumed.
  • packages/client/CHANGELOG.md also quotes an install call. Release-owned —
    untouched, by rule.

Is there already a pin? Measured, with a control

No — and the two nearest instruments are provably blind to this class.

scripts/measure-markdown-ts-blocks.mjs compiles the fenced TypeScript blocks of
package-root Markdown, which is exactly this file. Run on this README before and
after the fix, its per-block diagnostic sets are byte-identical:

fixed  : [{"i":0,"tf":true,"codes":"2307"},{"i":1,"tf":true,"codes":"2304,18046,18046,18046,18046,18046"},{"i":2,"tf":false,"codes":"2304"},{"i":3,"tf":true,"codes":"2304,2304,2304,2304,2552,2552,18004,2304"}]
broken : [{"i":0,"tf":true,"codes":"2307"},{"i":1,"tf":true,"codes":"2304,18046,18046,18046,18046,18046"},{"i":2,"tf":false,"codes":"2304"},{"i":3,"tf":true,"codes":"2304,2304,2304,2304,2552,2552,18004,2304"}]
IDENTICAL: true

That is a lit instrument, not a dark one: it reports live diagnostics on other
blocks of the same file in both runs. It cannot see this defect because
install declares its first parameter any
(packages/client/src/index.ts, install: async (manifest: any, …)), so tsc
has nothing to check. It is also a census by its own header, wired to no CI job.

check:published-readme-exports has the right population but reads fenced blocks
for imported symbols and has no notion of a schema.
check:skill-examples type-checks marked prose blocks on a client SDK surface,
but packages/client/README.md is not in its population at all, and the
client.packages.install block on the one SDK docs page that is carries no
check marker.

So this PR adds the pin: packages/client/src/readme-package-install-example.test.ts
extracts every packages.install manifest literal from this README with the
TypeScript AST and parses it against PackageInstallRequestSchema. It refuses to
pass on an empty corpus, and it throws rather than skipping on an object member it
cannot model.

Ablation — the pin can fail, proven from the committed state

Restored the original broken literal, proved the mutation on disk (injected text
count 1, removed text count 0, blob 93c5186941… distinct from HEAD blob
59ea1c0638…), ran the pin:

× README line 246 is accepted by PackageInstallRequestSchema
→ invalid_type at [manifest, id]; invalid_value at [manifest, type]; unrecognized_keys at [manifest]
 Test Files  1 failed (1)
      Tests  1 failed | 1 passed (2)

The 1 passed is the anti-vacuity floor still finding the corpus, so the red is
about the manifest and not about a lost anchor. Restored with
git checkout HEAD -- packages/client/README.md; the restored blob hashes back to
59ea1c0638… and git diff HEAD plus git status --porcelain are both empty.

Verification

run verdict
pnpm --filter @objectstack/client test exit 0 — 45 files, 537 tests passed
pnpm --filter @objectstack/client typecheck exit 0 — test layer compiles, 0 errors, 0 pinned signatures
pnpm --filter '@objectstack/client^...' build exit 0 — dependency closure, 103 build successes
dispatch-gates families 58 derived, 58 run, 0 NOT-MEASURED, 0 UNRUN (reconciled with exit codes via --ran)
eslint, narrowed exit 0 — 1 file linted, 0 errors, 0 warnings

Two gate families first answered PREREQUISITE NOT MET on an unbuilt checkout
(check:skill-examples, check:dual-build-cjs-loads); both exit 0 after
pnpm build. Readings taken at a37583f9c1.

The eslint narrowing is a measurement, not a skip. Population, read from
eslint.config.mjs itself: files matching **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}
minus the config's five global ignores — so the two Markdown files in this diff are
outside eslint's population by the config's own selectors. Count, read from
--format json: 1 file. Invariance, quoted from that config: "this repo runs one
eslint.config.mjs, which never enables type-aware linting (no
parserOptions.project, no typed @typescript-eslint rules) for ANY file, test or
not"
— so this diff cannot move the verdict on any untouched file.

Changeset

patch on @objectstack/client, measured rather than assumed: the package's
files[] is ["dist", "README.md", "CHANGELOG.md"] and the package is not
private, so this README is published output and skip-changeset — the label
for a diff that publishes nothing from any released package — does not apply.

Clause-②: no

No schema key, no closed-set member, no published export and no registry entry is
added or narrowed; the only TypeScript this PR adds is a test.

Acceptance notes


Generated by Claude Code

Two corrections added by the dispatching seat (#6024) at landing

The deliverer reported both of these rather than silently patching its own body, which is the right call — a body PATCH was outside its declared write budget. Appending rather than rewriting its text, so the record shows what was written when.

1. The Acceptance notes say this card's claim comment carries no Clause-② declaration. That was true of comment 5719922216 — the one the dispatch order named — but ⛔ it is no longer the governing claim. The seat later posted a shape-corrected claim, 5720097817, which carries Claim:, a separate Branch: directive line, and Clause-②: no; by the repo's own selection rule (newest claim parsing a branch) that one governs. The deliverer's re-derivation from the measured diff is also no, so there is no divergence: check-clause2-carriers --pair 18771 exits 0 with both carriers agreeing.

2. The "duplicate copies" section predates the shape-based sweep and names only client-sdk.mdx. The completed sweep — 1549 files, a predicate about the manifest shape rather than the call name, literals extracted with the TypeScript AST, and an unmodellable member throwing rather than being skipped (0 throws) — found six further live sites of the same defect class, not one:

  • content/docs/api/client-sdk.mdx:337 — one more packages.install example, refused on 2 counts;
  • five defineStack({ manifest: … }) snippets — kernel/services-checklist.mdx:473, permissions/authorization.mdx:176, permissions/capabilities.mdx:103, permissions/record-view-auditing.mdx:86, docs/design/marketplace-publishing.md:398.

⭐ The sweep's headline result is unchanged and is the one that matters for this PR: the exact broken example is duplicated nowhere — zero root-level label keys across 41 judged manifest literals — and 7 further literals refuse only because they are declared partials, correctly excluded as non-findings.

⛔ This PR was not widened into any of the six. They are filed separately and deliberately as two cards, because their remedies differ and one card would let the easy half carry the hard half: #18776 (the install example, one obviously-correct repair) and #18777 (the five defineStack snippets, whose remedy is a docs-convention ruling — complete the manifest, or mark it elided as their 7 siblings do).

⚠️ Landing-order constraint from triage comment 5716545361, checked by the seat before arming: this card must land before or with #18604 / #18606. Both are still pm:queue, open and unassigned ⇒ landing now satisfies it.


Generated by Claude Code

…gainst ManifestSchema

The example in the `@objectstack/client` README — which ships in the npm
tarball — was refused on three counts by the contract its own call site
declares (`PackageInstallRequestSchema`, `manifest: ManifestSchema`):
`invalid_type` at `[manifest, id]`, `invalid_value` at `[manifest, type]`,
and `unrecognized_keys` at `[manifest]` for `label`, a key the root shape's
`strictObject` close refuses by name.

`label` is not a root manifest key: the root shape declares `name` for the
human-readable string, so the example's `label` value moves there and the
machine identifier becomes the reverse-domain `id` the key documents.
`type: 'plugin'` is the enum member the example's own subject names.

Pinned by a new test that parses every `packages.install` manifest literal
in this README against that schema, and fails when the corpus is empty.

Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 1 changed file(s) yielded no anchor (packages/client/README.md), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/client/README.md) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 15 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 447e2e895abc7866aaab5a5def72ff2effcb105b → packageMentionDocs.

@os-support-ai
os-support-ai marked this pull request as ready for review September 17, 2026 20:29
@os-support-ai
os-support-ai added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit be7763a Sep 17, 2026
43 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-18607-client-readme-install-example branch September 17, 2026 20:57
os-litant pushed a commit that referenced this pull request Sep 17, 2026
…eclare the union's dropped refinements

`main` landed #18771, which fixes the same README example and adds
`readme-package-install-example.test.ts` — a corpus reader over every
`packages.install` literal in the published README. Two files reading the same
README to ask the same question is a duplicate owner, so the F4 additions move
INTO that file and the branch's own copy is dropped: the corpus is now parsed
against `PackageInstallBodySchema` wrapped exactly as `packages.install` sends
it and bare as the door reads it, and the pre-#18058 literal is pinned refused
so a schema relaxation cannot make the block vacuous. The README keeps main's
landed literal (`type: 'plugin'`, no deprecated `namespace`) plus this card's
prose and the `overwrite` opt-in line.

`api/PackageInstallBody` is a new published schema whose bare branch carries the
`navigationContributions` refinements `z.toJSONSchema()` cannot project, so
`dropped-refinements.baseline.json` — which landed on main after this branch
forked — gains the entry `build-schemas.ts` printed, beside the identical
`api/PackageInstallRequest` and `api/InstallPackageRequest` rows.

Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants