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
33 changes: 33 additions & 0 deletions .changeset/lucky-donkeys-invent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
"@cosyte/cli": patch
---

Make the published package installable, and stop `VERSION` lying about the release
(CLI-UNINSTALLABLE-MANIFEST, CLI-VERSION-DRIFT).

`0.0.1` and `0.0.2` both published with all ten `@cosyte/*` siblings declared as `file:vendor/*.tgz`
local paths. `vendor/` is not in `files`, so the tarball shipped none of them and every install route
died with `ENOENT` on `node_modules/@cosyte/cli/vendor/cosyte-fhir-0.0.0.tgz`. The siblings are now
real registry ranges: `@cosyte/hl7` and `@cosyte/terminology` as hard `dependencies`, the six breadth
parsers plus `@cosyte/transform` as `optionalDependencies`. Proven by packing the tarball and
installing it in a clean directory outside the repo, then running both bins and importing the `.`
subpath under ESM and CJS: the check a `npm publish --dry-run` cannot perform.

`src/core/version.ts` exported `"0.0.0"` while `package.json` said `0.0.2`, so `cosyte --version` and
the MCP server's advertised `serverInfo.version` both lied; confirmed in the published tarball.
`scripts/sync-version.mjs` now runs inside the `version` script, and the test suite compares the
export against `package.json`. The two assertions that let the same defect through five sibling
releases are fixed too, not just the value: the docs smoke test asserted `typeof VERSION` and now
asserts the exact version, and the declaration's `: string` annotation that the sync script keys on is
pinned by its own test.

One dependency could not be made real. `@cosyte/fhir` is not on the npm registry, and declaring it
alongside `@cosyte/transform` (which requires it) fails the whole install with `ERESOLVE`, so it is
not declared at all and `@cosyte/transform` is skipped by npm. FHIR `parse`/`inspect`/`fmt`/`validate`
and `convert` therefore degrade to a value-free `CLI_PARSER_UNAVAILABLE` (exit `69`) instead of
crashing on a bare `await import()`, with a diagnostic that says the package is not on the registry
rather than "install it". HL7 v2, `map-codes` and the six breadth formats work from a plain install.

The docs also stop telling people to run `npx @cosyte/cli …`, which never worked and is not fixed by
any of the above: `npx` picks the executable matching the package name's last segment (`cli`), and
this package ships `cosyte` and `cosyte-mcp`.
19 changes: 12 additions & 7 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,18 @@ updates:
# WHAT DEPENDABOT CAN ACTUALLY SEE HERE, stated plainly so this entry is not mistaken for
# broader cover than it gives:
#
# * The four hard `dependencies` (`@cosyte/hl7`, `@cosyte/fhir`, `@cosyte/transform`,
# `@cosyte/terminology`) and six of the seven `optionalDependencies` are
# `file:vendor/*.tgz` vendored tarballs. Dependabot does NOT resolve `file:` specs, so it
# will never propose a bump for any of them. They are refreshed by hand with
# `pnpm vendor:refresh`, and they become real `@cosyte/*` npm ranges at the publish flip
# (a published package cannot ship a `file:` dep). Until then, keeping them current is a
# human job and this config does not pretend otherwise.
# * The `@cosyte/*` siblings are now REAL npm ranges, not `file:vendor/*.tgz` tarballs, so
# Dependabot resolves and bumps them like any other dependency. That is a change: it
# previously could not see ANY of them. Covered now: the two hard `dependencies`
# (`@cosyte/hl7`, `@cosyte/terminology`) and seven of the eight `optionalDependencies`
# (`@cosyte/astm`, `ccda`, `dicom`, `mllp`, `ncpdp`, `x12`, `transform`).
# * `@cosyte/fhir` is the ONE that is still invisible, and it is invisible because it is not
# DECLARED, not because of a `file:` spec. It is not on the npm registry, so it cannot be a
# dependency of this package at all; it is carried as a `file:vendor/*.tgz` DEV dependency so
# this repo's own FHIR tests still run. Dependabot does not resolve `file:` specs, so that one
# stays a hand-refresh (`pnpm vendor:refresh`). An installed copy simply has no FHIR library,
# and the CLI reports `CLI_PARSER_UNAVAILABLE` rather than pretending. This config does not
# imply cover it does not give.
# * `@modelcontextprotocol/sdk` (exact-pinned) is the one third-party RUNTIME dependency
# Dependabot can see and bump. It is kept OUT of the group below by an explicit
# `exclude-patterns`, so a runtime bump on the MCP agent surface lands as its own
Expand Down
71 changes: 71 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,30 @@ this file is maintained by hand (Changesets handles the version bump and publish

### Fixed

- **`@cosyte/cli` can be installed from npm again (CLI-UNINSTALLABLE-MANIFEST).** `0.0.1` and `0.0.2`
both published with all ten `@cosyte/*` sibling packages declared as `file:vendor/*.tgz` local
paths. `vendor/` is not in `files` and there is no `bundledDependencies`, so the tarball shipped
none of them and every install route died on the first: `ENOENT ...
node_modules/@cosyte/cli/vendor/cosyte-fhir-0.0.0.tgz`. The siblings are now real registry ranges:
`@cosyte/hl7` (`^0.0.7`) and `@cosyte/terminology` (`^0.0.9`) as hard `dependencies`, and the six
breadth parsers plus `@cosyte/transform` (`^0.0.4`) as `optionalDependencies`. Verified the way a
dry-run cannot: the packed tarball was installed in a clean directory outside the repo (exit `0`),
then both bins were run and the `.` subpath imported under ESM and CJS.

- **`VERSION` and `cosyte --version` now report the release you are running (CLI-VERSION-DRIFT).**
`src/core/version.ts` exported `"0.0.0"` while `package.json` said `0.0.2`, and the constant's own
doc comment claimed it was "synced with `package.json#version` on release by the Changesets
`version` script" when no such step existed. Confirmed in the published tarball: `@cosyte/cli@0.0.2`
ships `VERSION = "0.0.0"` in `dist/index.mjs` and `dist/index.cjs`. It reached two user-visible
surfaces, `cosyte --version` and the MCP server's advertised `serverInfo.version`. `scripts/sync-version.mjs`
(ported from `@cosyte/transform`) now runs inside the `version` script, and `test/sanity.test.ts`
compares the export against `package.json` so a skipped sync goes red rather than shipping.
**The two assertions that let five bad releases through the sibling packages are fixed here, not
just the value:** `docs-content/installation.md` asserted `typeof VERSION` (true of every wrong
value) and now asserts the exact version, which the sync script keeps in step; and the declaration's
`: string` annotation, which the sync script's pattern keys on, is pinned by its own test, so
dropping it fails at `pnpm test` instead of silently at release time.

- **The `attw` publish gate no longer exits 0 on an untyped pack (ATTW-FALSE-GREEN-PORT).** The
`attw` script was the bare CLI (`attw --pack . --profile node16`), and
`@arethetypeswrong/cli@0.18.4`'s `getExitCode.js` opens with `if (!analysis.types) return 0`,
Expand Down Expand Up @@ -90,6 +114,53 @@ this file is maintained by hand (Changesets handles the version bump and publish
routed through a guarded loader to degrade to `CLI_PARSER_UNAVAILABLE` (exit `69`) rather than
crash. `loadOptional()` cannot be reused unchanged: it takes a `CosyteFormat`, and `"transform"` is
not one, and its diagnostic hardcodes the word "parser". Not undertaken here.
- **Superseded within this same unreleased set**, by the two entries at the top of this section:
the swap and the loader change were both carried out. Two claims that entry made did not survive
contact: the "name similarity" reading of the `@cosyte/fhir` `E403` was **retracted across the
ecosystem on 2026-08-03** (the cause is unexplained; do not assert one), and "npm tolerates an
`optionalDependency` that fails to resolve" is true only in isolation. Declaring **both**
`@cosyte/fhir` and `@cosyte/transform` optional fails the install outright with `ERESOLVE`, which
is why `@cosyte/fhir` ended up not declared at all rather than declared optional.

### Changed

- **FHIR support is unavailable in an npm-installed copy, and now says so instead of crashing.**
`@cosyte/fhir` is not on the npm registry, so it cannot be declared as a dependency at all;
measured, declaring it alongside `@cosyte/transform` (which requires it) fails the whole install
with `ERESOLVE`. `@cosyte/transform` is therefore skipped by npm as an unresolvable optional
dependency. Both were previously loaded with a bare `await import()`, which in an installed copy
would have surfaced a raw resolver error and a stack frame, so FHIR `parse`/`inspect`/`fmt`/`validate`
and `convert` now degrade to a value-free `CLI_PARSER_UNAVAILABLE` (exit `69`) with a diagnostic that
says the package is not on the registry rather than "install it". **Neither diagnostic says "install
it", and that is deliberate**: `npm install @cosyte/transform` fails `E404` on its own
`@cosyte/fhir` peer, so `loadOptional()`'s stock wording ("install it to use this format, it is an
optional dependency") would point a user at a command that cannot succeed. New
`loadOptionalPackage(detail, load)` under `loadOptional`, exported on the `.` subpath.
`@cosyte/fhir` is retained as a `devDependency` on the vendored tarball so this repo's own FHIR and
`convert` tests still run; note that `devDependencies` **are** published, so that one
`file:vendor/*.tgz` specifier does remain in the manifest, harmlessly, because a consumer never
installs a dependency's `devDependencies` (verified: the install exits `0`). HL7 v2, `map-codes` and
the six breadth formats are unaffected and work from a plain install.

- **Documented a pre-existing defect that this release makes reachable for the first time:
`--omit=optional` produces an install in which the `cosyte` command does not run at all.** The
install exits `0`, then every invocation, `--version` included, fails with `ERR_MODULE_NOT_FOUND` on
`@modelcontextprotocol/sdk` and a raw stack trace, because the built `dist/bin/cosyte.mjs` imports
the SDK statically at the top level rather than only on the `mcp` path. Verified identical on the
base commit, so it is not introduced here; it simply could not be hit before, because the package
could not be installed at all. `docs-content/` now says not to use that flag instead of implying it
is a supported way to slim the install. **The code defect is not fixed here** and needs its own
change: it also falsifies the "a plain `cosyte parse` never pulls it" claim in `src/bin/cosyte.ts`.

- **The docs no longer tell you to run `npx @cosyte/cli …`, which never worked.** Separate from the
packaging defect and not fixed by it: `npx` runs the executable whose name matches the package
name's last segment, which would be `cli`, and this package ships `cosyte` and `cosyte-mcp`, so the
short form fails with `could not determine executable to run`. Reproduced on the published `0.0.2`
and on the fixed tarball, whose `bin` block is byte-identical. `README.md`, `docs-content/installation.md`
and both MCP registration snippets now use `npx --package @cosyte/cli cosyte …` /
`npx -y --package @cosyte/cli cosyte-mcp`, each measured working. A `cli` bin alias would fix the
short form and is deliberately not added, because `npm install -g` would then claim a command named
`cli` on the user's `PATH`.

### Added

Expand Down
60 changes: 54 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,50 @@ subpath still exports a small programmatic `core` API (`detectFormat`, `EXIT`, `
**Pre-existing and correctly not retroactive:** commit subjects already on `main` may carry `U+2014`.
The message half only runs on `pull_request`. History is not rewritten.

- **The published manifest is installable again, and `VERSION` no longer lies.** `0.0.1` and `0.0.2`
are permanently broken on npm (`file:vendor/*.tgz` specifiers in the published manifest, `ENOENT` on
every install route) and both shipped `VERSION = "0.0.0"` against a `0.0.2` manifest, which
`cosyte --version` and the MCP `serverInfo.version` both printed. Fixed together:
**`scripts/sync-version.mjs`** now runs inside the `version` script and rewrites **two** targets,
`src/core/version.ts` and the asserted literal in `docs-content/installation.md`; `test/sanity.test.ts`
compares the export against `package.json` **and** pins the declaration's `: string` shape, which the
script's pattern keys on. **Fix the ASSERTION, not just the value:** the docs block asserted
`typeof VERSION`, which is true of every wrong value and green-lit both bad releases.
**The dep swap is done except for one dependency, and the exception is the interesting part.**
`@cosyte/hl7` (`^0.0.7`) and `@cosyte/terminology` (`^0.0.9`) are hard deps at real ranges; the six
breadth parsers and `@cosyte/transform` (`^0.0.4`) are `optionalDependencies` at real ranges.
**`@cosyte/fhir` is not declared at all**, because it is not on the registry and, measured here,
declaring it in ANY form alongside `@cosyte/transform` (whose `@cosyte/fhir` peer is mandatory)
fails the whole install with `ERESOLVE`: optional dep and optional _peer_ both. Either one alone
installs clean; the pair does not. **Do not explain this with a missing
`peerDependenciesMeta.optional` flag** - measured across the suite, that flag does not decide the
outcome, and the mechanism is unexplained. `@cosyte/fhir` is kept as a **`devDependency`** on the
vendored tarball so this repo's own FHIR/`convert` tests run and so `transform`'s peer resolves in
the dev tree. Consequence, stated on every consumer surface rather than discovered: **an installed
copy has no FHIR support**, and FHIR `parse`/`inspect`/`fmt`/`validate` plus `convert` degrade to a
value-free `CLI_PARSER_UNAVAILABLE` (69). That required `loadOptionalPackage(detail, load)` beneath
`loadOptional` (which takes a `CosyteFormat` and hardcodes the word "parser", wrong for both cases)
plus `loadFhir()`; `test/absent-sibling.test.ts` includes a **static guard** that reds on a new
**single-line, unwrapped** `import("@cosyte/fhir")` / `import("@cosyte/transform")` in `src/`, which
is the shape the defect took. **Do not write "any new call site": a refuter falsified that wording
by adding a thunk assigned to a variable, and the suite stayed 10/10 green.** It also misses a
multi-line import and a **static** `import … from "@cosyte/fhir"` - and this repo now HAS the first
static reference to that package (`src/core/parsers.ts`, `import type`, erased at build, verified
absent from `dist/`). Dropping the word `type` loads it eagerly and breaks every command in an
installed copy, unseen by the guard. Also note the two diagnostics deliberately do NOT say
"install it": `npm install @cosyte/transform` fails `E404` on its own `fhir` peer, so that advice
would send a user at a command that cannot succeed. `loadOptional()`'s stock wording says exactly
that, which is why neither goes through it.
**Verified by installing, which a `--dry-run` cannot do**: pack, `npm install` the tarball in a clean
directory outside the repo (exit 0), run both bins, import `.` under ESM and CJS. Negative control:
the published `0.0.2` still `ENOENT`s. Keep that step; it is checklist step 6 in `RELEASING.md`.
**A THIRD fault is real, pre-existing, and NOT fixed by any of this: `npx @cosyte/cli …` fails with
`could not determine executable to run`.** `npx` runs the bin matching the package name's last
segment (`cli`); this package ships `cosyte` and `cosyte-mcp`. The `bin` block is byte-identical to
the published `0.0.2`, so the swap cannot have changed it. Docs now say
`npx --package @cosyte/cli cosyte …` (measured working). **A `cli` bin alias would fix it and is
deliberately not added** - `npm install -g` would then claim the name `cli` on the user's `PATH`.
That trade is a founder call.
- **Phase 7 shipped (CLI-7): release hardening: the final roadmap phase. The CLI is feature-complete.**
No new runtime command surface; this phase is publish-readiness. **Fuzz** over the CLI's two input
boundaries: the terminal (`run`, over argv plus stdin bytes) and the agent surface (`dispatchTool`,
Expand Down Expand Up @@ -186,12 +230,16 @@ subpath still exports a small programmatic `core` API (`detectFormat`, `EXIT`, `
- **Phase 1 shipped** (§Phase 1). `cosyte parse <file|->` for **HL7 v2** + **FHIR R4**, **content
format autodetection** (conservative, fail-safe, never a guessed parser), the documented
**exit-code contract**, and the **value-free diagnostic** channel with stable `CLI_*` codes.
- **Hard runtime deps (ADR 0021 + 0023):** `@cosyte/hl7` + `@cosyte/fhir` (parsers) and
`@cosyte/transform` + `@cosyte/terminology` (the higher-layer libs `convert`/`map-codes` wrap) are
**real `dependencies`** (an `npx` bin can't peer-depend), vendored as `pnpm pack` tarballs in
`vendor/` until PUB-FLIP: refresh with `pnpm vendor:refresh`. Pinned shas: hl7 `46d50eb`, fhir
`7a099b2`, transform `e6c4531`, terminology `e5ed368`. **Lazy-loaded per command.** Umbrella
`verify-policy.json` caps `cli` runtime deps at **4** (raised 2 → 4 for CLI-4, ADR 0023).
- **Hard runtime deps (ADR 0021 + 0023), as they stand AFTER the vendor → npm swap:** only
**`@cosyte/hl7` (`^0.0.7`) + `@cosyte/terminology` (`^0.0.9`)** are hard `dependencies` now, both
real registry ranges (an `npx` bin can't peer-depend). `@cosyte/transform` moved to
`optionalDependencies` and **`@cosyte/fhir` is undeclared** - see the swap note above for why, and
do not "restore" either without reading it. That is **2** hard runtime deps against an umbrella
`verify-policy.json` cap of **4**, so it is under the cap, not at it. **Lazy-loaded per command.**
On the `0.0.x` ladder `^0.0.7` permits no other version, so these are effectively exact pins, and
Dependabot now sees them (it never could while they were `file:` specs). `vendor/` survives only to
supply `@cosyte/fhir` as a **`devDependency`**; the other nine tarballs are refreshed by
`pnpm vendor:refresh` but wired to nothing, and removing them is a deliberate separate cleanup.
Third-party CLI-core runtime deps: **zero**. The MCP server's **`@modelcontextprotocol/sdk`** is the
CLI's only third-party runtime dep: declared in **`optionalDependencies`** (not `dependencies`),
isolated behind `./mcp`, so it is outside the hard-closure cap (ADR 0024).
Expand Down
Loading
Loading