Skip to content

Let a consumer of the shared name reach the schema prose - #41

Merged
samuelduchesne merged 2 commits into
mainfrom
006-expose-the-prose-pool
Sep 4, 2026
Merged

Let a consumer of the shared name reach the schema prose#41
samuelduchesne merged 2 commits into
mainfrom
006-expose-the-prose-pool

Conversation

@samuelduchesne

Copy link
Copy Markdown
Contributor

Requested by idfkit-lsp, and envelop hits the same gap. Hover on a field shows its structural facts and no sentence saying what the field is for.

BuildingSurface:Detailed · view_factor_to_ground
number|string · optional · default `Autocalculate`

The sentence that belongs under that heading is the schema's own note. It exists, it ships, and describeObjectType already resolves it. What a consumer could not do is obtain the pool.

The gap, confirmed rather than taken on report

  • idfkit re-exports httpSource but not nodeSource or readBundleFileSync; those live behind @idfkit/schemas/node, a package a consumer of the shared name did not install.
  • idfkit/node is @idfkit/core/node, which exposes schemas() and schemaFor() and nothing for prose.
  • read returns unknown and docs.json is a bundle-layout detail, so the only route was to hardcode a name and cast. Both would break silently if the layout moved.

What this adds

SchemaBundle gains loadProse() and prose() — the same pair as load(version) / loaded(version), with the same in-flight sharing.

import { schemas } from 'idfkit/node';
const bundle = schemas();
await bundle.loadProse();   // once, off the critical path
bundle.prose();             // ProsePool | undefined, synchronous

On the bundle, not a free-standing loader

This is the one decision worth arguing, and it departs from the shape the request sketched.

The indices are only meaningful against manifests built in the same run. build.mjs writes docs.json and the manifests together, and SlimType.m / SlimField.n point into that exact array. A pool paired with manifests from another build resolves every sentence to a real sentence belonging to a different field, and nothing fails.

A module-level prose singleton in idfkit/node would make that pairing easy to reach: the consumer most likely to supply its own BundleSource is the browser one, which is envelop. Putting the loader on the bundle that carries the indices makes it hard to reach by accident.

It also answers both runtimes at once, which the request asked for separately. envelop reaches schemas through httpSource today and gets the pool the same way, with nothing node-specific in the path.

The synchronous accessor

Kept, and for the reason the request gave. Every reader of prose is synchronous: describeObjectType takes a pool rather than fetching one, and every answer in @idfkit/language is a pure function, which is what lets an editor server answer a cursor without holding a thread. Load once when a document arrives, read on the request path.

What does not change

ProsePool moves from @idfkit/core to @idfkit/schemas, because schemas cannot import from core and the pool is a schemas artifact. Core re-exports it, so the name a consumer imports is unchanged and the register entry still resolves.

Nothing about the parse path moves. check:bundle-purity still reports a minimal read-and-write bundle at 18.3 KB with no schema data. describeObjectType with no pool still returns structural facts with prose: undefined, never text derived from a field name (FR-022).

Measured

Over ten common types in 26.1.0: with the pool, 10/10 carry a memo and 52/103 fields carry a note; without it, none do. The other 51 have no note in the schema itself.

Install grows 5,657 bytes, 1,795,857 to 1,801,514 against a budget of 1,835,008: 98.2 percent, 32.7 KB headroom, down from 38.2 KB. The pool was already installed; this is the loader. The budget is not moved.

Governance

idfkit-conformance#3 registers SchemaBundle.loadProse and SchemaBundle.prose as divergent, on the same footing as SchemaBundle.load against get_schema.

It does not block this PR. The naming gate's coverage reaches top-level exports, and a method on an exported class sits outside that reach — check-naming-register.mjs says so in its own header. check:naming passes here at 136 of 136 either way. Unlike feature 005, no tag is a prerequisite.

Gates

typecheck, test (703, up from 699), format:check, check:naming, check:parity, check:facade, check:bundle-purity, check:install-size, check:no-index all green locally. check:publication still refuses on the pre-existing FR-044 conformance-level question.

Hover on a field in an editor showed its structural facts and no sentence
saying what the field is for. The sentence exists, ships, and is already
resolved by describeObjectType: what a consumer could not do is obtain the pool.

idfkit re-exports httpSource but not nodeSource or readBundleFileSync, which
live behind @idfkit/schemas/node, a package a consumer of the shared name did
not install. idfkit/node exposes schemas() and schemaFor() and nothing for
prose. So the only route was to depend on a package the consumer did not
declare, hardcode the file name docs.json, and cast the unknown that read
returns. Both the name and the layout are implementation details, and both
would have broken in silence.

SchemaBundle gains loadProse() and prose(), the same pair as load(version) and
loaded(version), with the same in-flight sharing.

ON THE BUNDLE, and not as a free-standing loader, which is the one decision
here worth arguing. The indices are only meaningful against manifests built in
the same run: build.mjs writes docs.json and the manifests together, and
SlimType.m and SlimField.n point into that exact array. A pool paired with
manifests from another build resolves every sentence to a real sentence
belonging to a different field, and nothing fails. Putting the loader on the
bundle that carries the indices makes that pairing hard to reach by accident,
where a module-level singleton beside httpSource would make it easy: the
browser consumer is precisely the one that supplies its own source.

It also answers both runtimes at once. envelop reaches schemas through
httpSource today and gets the pool the same way, with nothing node-specific in
the path.

THE SYNCHRONOUS ACCESSOR IS THE POINT. Every reader of prose is synchronous.
describeObjectType takes a pool rather than fetching one, and every answer in
@idfkit/language is a pure function, which is what lets an editor server answer
a cursor without holding a thread. A consumer calls loadProse once when a
document arrives and reads prose() on the request path. With only a promise it
would either await inside a path it deliberately keeps synchronous, or build a
second cache beside the one SchemaBundle already has.

ProsePool moves from @idfkit/core to @idfkit/schemas because schemas cannot
import from core and the pool is a schemas artifact. Core re-exports it, so the
name a consumer imports is unchanged and the register entry still resolves.

Nothing about the parse path moves. The pool stays its own file, loaded by its
own call, and check:bundle-purity still reports a minimal read-and-write bundle
at 18.3 KB carrying no schema data. A caller who never asks for prose still
pays nothing, and describeObjectType with no pool still returns structural facts
with the prose undefined rather than text derived from a field name.

MEASURED. Over ten common types in 26.1.0: with the pool, 10 of 10 carry a memo
and 52 of 103 fields carry a note; without it, none do. The remaining 51 have no
note in the schema itself.

The install grows 5,657 bytes, from 1,795,857 to 1,801,514 against a budget of
1,835,008. That is 98.2 percent with 32.7 KB of headroom, down from 38.2 KB. The
pool itself was already installed; this is the loader. The budget is not moved.
@samuelduchesne
samuelduchesne requested review from a team as code owners September 4, 2026 19:55
The level carrying the register entries for SchemaBundle.loadProse and
SchemaBundle.prose, the two names this branch adds. conformance-2026.8 does not
move: no corpus case changes.
@samuelduchesne
samuelduchesne merged commit f5184c3 into main Sep 4, 2026
38 checks passed
@samuelduchesne
samuelduchesne deleted the 006-expose-the-prose-pool branch September 4, 2026 20:04
@samuelduchesne samuelduchesne mentioned this pull request Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant