Skip to content

feat: expose the package version at runtime (VERSION + __version__) - #37

Merged
laynepenney merged 1 commit into
devfrom
feat/runtime-version-export
Jul 30, 2026
Merged

feat: expose the package version at runtime (VERSION + __version__)#37
laynepenney merged 1 commit into
devfrom
feat/runtime-version-export

Conversation

@laynepenney

Copy link
Copy Markdown
Member

Adds VERSION (TypeScript) and __version__ (Python) so a consumer can read which version of this package produced a document, instead of hand-copying a version string into its own constant.

Why

A consumer that records extraction provenance needs the producing module's version. Until now the package offered no way to obtain it, so hand-copying was the only option available — and a hand-copied version is a claim about the runtime rather than evidence of it. One downstream consumer did exactly that and its copy went stale, declaring 0.5.0 while 0.6.0 was current, with nothing able to detect the drift.

Asking consumers to keep a copy in sync is not a fix; it is the same failure deferred. Making the version readable removes the need for the copy.

The IL already has a producer-provenance field (produced_by). This gives consumers the value that belongs in it.

Why a literal, not a read of package.json

The default TypeScript entry has to stay importable in browser and WASM hosts, and scripts/check-ts-universal-entry.mjs fails the build if it reaches a Node built-in. So VERSION is an embedded literal — the same trade the embedded prompt fragments already make, and it carries the same obligation: an embedded copy needs a test that fails when it drifts from its source.

check:universal passes (10 modules scanned), so the constant did not compromise browser/WASM import.

Drift coverage, proven by mutation

The version is now written in four places (two manifests, two runtime constants). packages/ts/tests/test_version.ts and tests/python/test_version.py tie all four together, including across languages — the two surfaces ship as one product at one version, and nothing enforced that before. They were two hand-edited numbers that happened to agree.

Each location was mutated in turn and a named test went red; both suites green on restore:

mutated pytest vitest
packages/ts/package.json RED RED
packages/python/pyproject.toml RED RED
packages/ts/src/version.ts pass RED
packages/python/.../__init__.py RED pass

Coverage is asymmetric and worth stating rather than implying symmetry: a drifted TS constant is caught only by vitest, a drifted Python constant only by pytest. Complete across the two suites together (which is what CI runs), but running one suite alone leaves one location unguarded.

The Python suite also compares importlib.metadata against the source constant. That reads what pip actually installed — a genuinely different source than the literal — so it catches a stale editable install or a bump that was never reinstalled, which none of the other assertions can see. It skips rather than fails when no distribution is installed, since importing from a source tree is legitimate.

Note for whoever runs the mutation proof next

Clear __pycache__ between steps. Bytecode invalidation compares source mtime at one-second granularity plus size, and two same-length version edits inside the same second are indistinguishable to it — a stale .pyc served a mutated value after the file had been restored. The masking is silent and it cuts both ways, so a mutation result gathered without clearing the cache proves nothing. My first matrix was invalid for exactly this reason and was re-run clean.

scripts/bump-version.sh

Four hand-edited numbers is the shape that drifts, and this PR takes it from two to four, so the tooling ships with it rather than after it.

The script updates all four, rejects anything that is not a bare semver triple (a range like ^0.7.0 or a full specifier are each a plausible paste, and each would corrupt recorded provenance), and verifies afterward that the four agree rather than reporting success for having run. Tested end to end: 0.6.0 → 0.7.0, all four moved, reverted cleanly.

It deliberately does not commit, tag, or push — under the dev/main model a tag belongs to the release ceremony on main, not to whatever branch happens to be checked out when someone bumps a number.

One honest note on the flow: after a bump, test_installed_distribution_agrees_... goes red until you pip install -e packages/python, because it compares importlib.metadata against the source constant. That is the check working — it is the only assertion that can see a stale install — so the script prints the reinstall step first, ahead of running the suites.

Verification

gate result
pytest 388 passed (was 383)
vitest 268 passed (was 265)
tsc --noEmit 0
check:universal 0 — universal-safe, 10 modules
check-no-network (ts, py) 0, 0

Issue tracking

The originating issue lives in a private repository, so it is deliberately not linked here — a reference to it would name a private repo in a permanently public PR body, which is the same disclosure this project's pre-push leak scan blocks in source. It is tracked internally and will be closed manually when this merges.

Premium boundary

extract is OSS. A package's own version is public by definition; no premium, identity, or org semantics are involved.

Adds `VERSION` (TypeScript) and `__version__` (Python) so a consumer can read
which version of this package produced a document instead of hand-copying a
version string into its own constant.

WHY

A consumer that records extraction provenance needs the producing module's
version. Until now the package offered no way to obtain it, so hand-copying was
the only option available -- and a hand-copied version is a claim about the
runtime rather than evidence of it. One downstream consumer did exactly that and
its copy went stale, declaring 0.5.0 while 0.6.0 was current, with nothing able
to detect the drift. Asking consumers to keep a copy in sync is not a fix; it is
the same failure deferred. Making the version readable removes the need.

The IL already has a producer-provenance field (`produced_by`). This gives
consumers the value that belongs in it.

WHY A LITERAL AND NOT A READ OF package.json

The default TypeScript entry must stay importable in browser and WASM hosts;
`scripts/check-ts-universal-entry.mjs` fails the build if it reaches a Node
built-in. So `VERSION` is an embedded literal, the same trade the embedded
prompt fragments already make -- and it carries the same obligation an embedded
copy always carries: a test that fails when it drifts from its source.

DRIFT COVERAGE

The version is now written in four places (two manifests, two runtime
constants). `packages/ts/tests/test_version.ts` and
`tests/python/test_version.py` tie all four together, including across
languages: the two surfaces ship as one product at one version, and nothing
enforced that before -- they were two hand-edited numbers that happened to
agree.

Proven by mutation rather than asserted. Each of the four locations was mutated
in turn and a named test went red; both suites return green on restore:

  mutate ts/package.json      -> pytest RED, vitest RED   (cross-language tie)
  mutate python pyproject     -> pytest RED, vitest RED   (cross-language tie)
  mutate ts src/version.ts    -> vitest RED
  mutate python __init__.py   -> pytest RED

The Python suite also compares `importlib.metadata` against the source
constant. That reads what pip actually installed, which is a genuinely
different source than the literal, so it catches a stale editable install or a
bump that was never reinstalled -- something the other assertions cannot see.
It skips rather than fails when no distribution is installed, since importing
from a source tree is legitimate.

A note for whoever runs the mutation proof next: clear `__pycache__` between
steps. Bytecode invalidation compares source mtime at one-second granularity
plus size, and two same-length version edits inside the same second are
indistinguishable to it -- a stale `.pyc` served a mutated value after the file
had been restored. The masking is silent and it cuts both ways, so a mutation
result gathered without clearing the cache proves nothing.

scripts/bump-version.sh

Four hand-edited numbers is the shape that drifts, and this commit takes it
from two to four, so the tooling comes with it. The script updates all four,
rejects anything that is not a bare semver triple (a range or a full specifier
would each be a plausible paste and each would corrupt recorded provenance),
and verifies afterward that the four agree rather than reporting success for
having run.

It deliberately does not commit, tag, or push: under the dev/main model a tag
belongs to the release ceremony on main, not to whatever branch is checked out
when someone bumps a number.

Premium boundary: extract is OSS. A package's own version is public by
definition; no premium, identity, or org semantics are involved.
@laynepenney
laynepenney merged commit 110b619 into dev Jul 30, 2026
11 checks passed
@laynepenney
laynepenney deleted the feat/runtime-version-export branch July 30, 2026 12:33
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