Version Packages - #31
Merged
Merged
Conversation
NSchatz
force-pushed
the
changeset-release/main
branch
from
August 4, 2026 19:27
928a025 to
6cc2339
Compare
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 free
to 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.
This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and publish to npm yourself or setup this action to publish automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
Releases
@cosyte/cli@0.0.4
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog,
and this project adheres to Semantic Versioning.
Versions and publishing are managed with Changesets;
this file is maintained by hand (Changesets handles the version bump and publish only).
The released sections below were reconstructed on 2026-08-04. Until then every entry sat under
Unreleased, including entries describing releases that had already shipped, so each publishedtarball carried a changelog calling its own contents unreleased. Those tarballs are immutable and
still do. Each entry was assigned to the release whose tag first contains it, read off
v0.0.1,v0.0.2andv0.0.3; no entry text was rewritten to fit a section.Unreleased
Fixed
The shipped documentation sidebar was off the canonical IA spine, and it was holding up the
docs site's deploy (CLI-SIDEBAR-IA-NONCANONICAL).
docs-content/sidebars.jsondeclared twotop-level categories that are not on the spine, "MCP server" and "Reference". The docs
site lints the sidebar it receives from each package's released
docs-content.tar.gz, and instrict mode a non-canonical top-level label grades as an error, so this package failed that gate
and the site stopped deploying. Pre-existing since the pages were authored; it surfaced only once
an unrelated build failure stopped masking it.
no page left the navigation and none was orphaned (verified: the nine documents in
docs-content/and the nine referenced by the sidebar are the same nine).mcpandreference-commandsjoin Guides, next toguides-overview, both being task-oriented:what to run and what comes back.
limitationsjoins Troubleshooting, next totroubleshooting, which is where the documentation standard puts known limitations.Installation,Quickstart,Core Concepts,GuidesandTroubleshooting, in that order, under theintrodocument.the docs site, and a hand-authored one is refused outright rather than warned about, so the
rename would have traded a failing check for a differently failing one. Whether and where the
site adds it is the site's decision, not this package's, and this package neither authors nor
positions it.
the previous release as a negative control: that one produces two errors and this one
produces no findings. Only a new release can clear the gate, because it reads the sidebar
out of the released artifact and published releases are immutable, so the previous release
keeps its non-canonical sidebar permanently. That is reported without gating once a release is
no longer current.
moves, and no page content was rewritten.
The pre-commit PHI gate never saw a
git mvinto a scan root(PHI-SCAN-RENAME-BLIND-AT-PRECOMMIT).
scripts/phi-scan.ts --stagedlisted the index withgit diff --cached --name-only --diff-filter=AM. Rename detection is on by default, sogit mv <path> test/__fixtures__/<name>stages as a two-pathR100record, andRandCare in neither
AMnorAMT: the status filter deleted the record outright and the destinationwas never enumerated. Measured on this repo, both shapes walking through the
pnpm phi-scan --stagedpre-commit hook at exit 0: a regular file carrying a value thisscanner's own floor catches (
:100644 100644 <sha> <sha> R100), and a symbolic link(
:120000 120000 <sha> <sha> R100, index mode120000under the scan root).git mvis anordinary developer action, not crafted input, and the pre-commit hook is the gate it walked
through.
--no-renamesis the remedy, and it costs the record stride nothing. The destinationarrives as a single-path
Aand the source as aDthe filter drops, so the enumeration is astrict superset of the previous one. Verified under
diff.renamesset totrue,copies,falseand1, and underdiff.renameLimit=1: noRorCrecord survives in any of them,which makes the two-field record stride structural rather than conditional.
copiesis nothypothetical; it produces a live
C100here.--name-onlyand readcontent with
git show :<path>, and git stores a symbolic link as its target path undermode
120000, so the scan was handed the path text and never the target's bytes. It now listswith
--raw -zand refuses (exit2) any in-scope entry whose destination mode is not aregular blob. A refusal names the entry's own repo-relative path and an engine-owned token for
its kind, and never the link target, which is working-tree text that can itself carry PHI.
Every offender is named, not just the first. That guarantee is about a refusal and does not
extend to a hit; see the linked-scan-root residual below.
T(typechange) is now in the filter, because replacing a tracked regular fixture with alink is neither an add nor a modify (
:100644 120000 <sha> <sha> T), so--diff-filter=AMdeleted the record before any mode could be read. The reverse direction, a link replaced by a
real file, is now scanned as the file it became.
a directory, so an entry at exactly
test/__fixtures__orsrcis that root replaced by a blobor a link, and a prefix test requiring the trailing slash let it through while the whole corpus
went unscanned.
while its CI half silently drops one is not one a developer can reason about. The walk
enumerates
Dirent.isFile(), an lstat answer, so a link (and a linked directory, whichisDirectory()also answers false for) fell out of the loop silently. The.mdexemptiondeliberately does not reach a link: it is a judgement about a file whose bytes the walk could
have read, and a link's name is no evidence about what is on the other side of it. A gitignored
entry stays out of scope, by the same rule that already excludes a gitignored fixture.
unreadable scan root, both threw past every handler and exited 1 with a raw stack trace.
1is this contract's code for hits found, so a caller branching on the exit code read a broken
invocation as a PHI finding and a caller branching on "not 0" read it as the gate working.
Neither was true. Both are now exit
2with a diagnostic.staged route still does not enumerate
D(a deletion has no staged blob to scan) orU(anunmerged path has no single one). The
Uhalf costs nothing that can reach a commit, and thatwas measured rather than assumed:
git commitrefuses an unmerged index outright. Undersrc/the staged route still covers only
.tsfiles while the all-mode walk covers every non-.mdfile, so the CI sweep is what covers the difference; widening the staged half is a scope
decision and was not taken here.
false. It covers an entry the walk reached beneath a root it had already opened, and a staged
record in scope per the boundary rule, which under
src/is.tsfiles only, so a staged linkat
src/notes.jsonis under a scan root and--stagedstill exits 0 over it while the all-modesweep refuses it. Three shapes escape the rule, all pre-existing, all measured, none
closed here: (1) a scan root that is itself a live link is followed by the all-mode walk,
because
existsSyncandreaddirSyncboth resolve, so the walk reads files no commit containsand reports their values under a fabricated in-repo path that holds no such file (the
dangling direction is the mirror image: it reports clean over a corpus it never opened);
(2) an ancestor of a scan root is in neither route's scope, so staging
testas a linkleaves
--stagedat exit 0 and the walk then follows it; (3) paths mode follows anexplicitly named link, because
statSyncresolves. The--stagedhalf of shape (1) isclosed here. Closing the rest needs a refuse-a-scan-that-observed-nothing rule plus a decision
about how far above a scan root to look, which is its own change.
test/scripts/phi-scan.test.tsbuilds throwaway git repositories for all of it, because theseare properties of what
git diff --cachedreports and cannot be reproduced by scanning a path.16 of its 34 tests run red against the scanner on
a7a92f8; the 18 that stay green are thepre-existing floor tests, the deliberate controls (an ordinary staged hit, a clean pass, a
staged link outside both roots, an ignored link, and a check that the payload under test is
something this scanner would otherwise catch), and the four that pin the disclosed residuals
above. Those four are green on both trees by design: they pin behaviour this change does not
alter, so that the scoped wording of the refusal rule cannot quietly revert to the absolute one.
Synthetic values only.
export moves. This is repository tooling.
0.0.3 - 2026-08-03
Changed
FHIR support is unavailable in an npm-installed copy, and now says so instead of crashing.
@cosyte/fhiris 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 installwith
ERESOLVE.@cosyte/transformis therefore skipped by npm as an unresolvable optionaldependency. Both were previously loaded with a bare
await import(), which in an installed copywould have surfaced a raw resolver error and a stack frame, so FHIR
parse/inspect/fmt/validateand
convertnow degrade to a value-freeCLI_PARSER_UNAVAILABLE(exit69) with a diagnostic thatsays the package is not on the registry rather than "install it". Neither diagnostic says "install
it", and that is deliberate:
npm install @cosyte/transformfailsE404on its own@cosyte/fhirpeer, soloadOptional()'s stock wording ("install it to use this format, it is anoptional dependency") would point a user at a command that cannot succeed. New
loadOptionalPackage(detail, load)underloadOptional, exported on the.subpath.@cosyte/fhiris retained as adevDependencyon the vendored tarball so this repo's own FHIR andconverttests still run; note thatdevDependenciesare published, so that onefile:vendor/*.tgzspecifier does remain in the manifest, harmlessly, because a consumer neverinstalls a dependency's
devDependencies(verified: the install exits0). HL7 v2,map-codesandthe 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=optionalproduces an install in which thecosytecommand does not run at all. Theinstall exits
0, then every invocation,--versionincluded, fails withERR_MODULE_NOT_FOUNDon@modelcontextprotocol/sdkand a raw stack trace, because the builtdist/bin/cosyte.mjsimportsthe SDK statically at the top level rather than only on the
mcppath. Verified identical on thebase 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 itis 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 parsenever pulls it" claim insrc/bin/cosyte.ts.The docs no longer tell you to run
npx @cosyte/cli …, which never worked. Separate from thepackaging defect and not fixed by it:
npxruns the executable whose name matches the packagename's last segment, which would be
cli, and this package shipscosyteandcosyte-mcp, so theshort form fails with
could not determine executable to run. Reproduced on the published0.0.2and on the fixed tarball, whose
binblock is byte-identical.README.md,docs-content/installation.mdand both MCP registration snippets now use
npx --package @cosyte/cli cosyte …/npx -y --package @cosyte/cli cosyte-mcp, each measured working. Aclibin alias would fix theshort form and is deliberately not added, because
npm install -gwould then claim a command namedclion the user'sPATH.Fixed
@cosyte/clican be installed from npm again (CLI-UNINSTALLABLE-MANIFEST).0.0.1and0.0.2both published with all ten
@cosyte/*sibling packages declared asfile:vendor/*.tgzlocalpaths.
vendor/is not infilesand there is nobundledDependencies, so the tarball shippednone 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 harddependencies, and the sixbreadth parsers plus
@cosyte/transform(^0.0.4) asoptionalDependencies. Verified the way adry-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.VERSIONandcosyte --versionnow report the release you are running (CLI-VERSION-DRIFT).src/core/version.tsexported"0.0.0"whilepackage.jsonsaid0.0.2, and the constant's owndoc comment claimed it was "synced with
package.json#versionon release by the Changesetsversionscript" when no such step existed. Confirmed in the published tarball:@cosyte/cli@0.0.2ships
VERSION = "0.0.0"indist/index.mjsanddist/index.cjs. It reached two user-visiblesurfaces,
cosyte --versionand the MCP server's advertisedserverInfo.version.scripts/sync-version.mjs(ported from
@cosyte/transform) now runs inside theversionscript, andtest/sanity.test.tscompares the export against
package.jsonso 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.mdassertedtypeof VERSION(true of every wrongvalue) and now asserts the exact version, which the sync script keeps in step; and the declaration's
: stringannotation, which the sync script's pattern keys on, is pinned by its own test, sodropping it fails at
pnpm testinstead of silently at release time.The
attwpublish gate no longer exits 0 on an untyped pack (ATTW-FALSE-GREEN-PORT). Theattwscript was the bare CLI (attw --pack . --profile node16), and@arethetypeswrong/cli@0.18.4'sgetExitCode.jsopens withif (!analysis.types) return 0,returning before the problem list is read. So a tarball carrying no declarations at all printed
"This package does not contain types." and exited 0, and
verify.shpropagated that 0 as apass. Reproduced on this package with zero concurrency and the real
--profile node16invocation, twice: with
dist/removed, and with all ten of the build's declaration filesdeleted and its JS left in place. The second is the realistic window, because
tsupwrites JS inone pass and declarations in a later one; instrumented on one build here at a 10ms poll, all
eight
.mjs/.cjsfiles appeared on a single poll at 4.92s and all ten declarations on a singlelater poll at 11.82s, a 6.90s interval. That interval moves with load (7.85s on a busier box);
the ordering, and the ten declarations landing together, do not. Not answered with a lock or a
build queue: the gate is now able to report that its own inputs were missing, whatever removed
them.
scripts/attw.mjs(ported from the fix shipped in@cosyte/terminology) runs two nets aroundthe real binary. A preflight that every relative path
package.jsonpromises exists and isnon-empty, naming the missing file; and a post-check that promotes the untyped sentence to a
failure, which catches declarations that are on disk but excluded from the tarball. Every
argument this repo passes is forwarded, so
--profile node16keeps its exact meaning: measuredhere, without it
@cosyte/cli/mcpfailsnode10resolution andattwexits 1.bin, which the ported original did not, because this is abinpackage and the sibling it came from ships no executable. Measured: with
dist/bin/cosyte.mjsdeleted and everything else built,
attw --pack . --profile node16printed every subpath greenand exited 0 over a tarball with no
cosytecommand in it.--quiet,-q,--format,-fand--config-pathare refused by option name and not byvalue, as is a
.attw.jsonsettingquietorformat(readConfig()applies it afterargv). Each was measured here to remove the untyped sentence from the output while still
exiting 0.
--config-pathis refused on a measurement rather than the inference the originalrecorded. Two limits are disclosed rather than closed, both measured: the match is on an
exact argv token, so commander's attached and clustered short forms
-fjsonand-Pf jsongetthrough (
-qPdoes not; the wrapper treats an empty transcript as a failure), and a declaredpath not starting with
.is skipped by the preflight. The invocation this replaced exited 0on the same pack with no arguments at all.
files: ["dist"]packs all ten declarationstsupemits while
package.jsonnames only four; the shareddist/io-<hash>.d.*and the fourdist/bin/*.d.*ride along unnamed, andanalysis.typesis true if the tarball carries anydeclaration. Measured on this tree: losing the four declared declarations gives "No types"
problems and exit 1 while any of the other six survives, and only losing all ten gives the
untyped sentence and exit 0. So a partial declaration loss is caught by
attwitself andonly a total one is the false green. The preflight reports both outcomes instead of asserting
the exit 0, which the inherited wording would have done falsely.
test/scripts/attw-gate.test.tspins all of it against the real binary, including attw's ownexit 0, a negative control on a well-formed package, that a genuine attw failure still fails
with attw's own status, and that the profile flag survives the wrapper in both directions.
scripts/verify.shin the meta-repo is unchanged; its propagation was never at fault.The route to an installable release, recorded in
RELEASING.mdand verified against theregistry.
@cosyte/hl7(0.0.3),@cosyte/terminology(0.0.4) and all six breadth parserswould swap to real ranges today.
@cosyte/fhiris unpublished (FHIR-NPM-NAME, an npm E403name-similarity rejection) and
@cosyte/transform@0.0.2failsE404on its@cosyte/fhirpeer,so neither can. An installable release is nonetheless reachable before that unblocks, because npm
tolerates an
optionalDependencythat fails to resolve (measured). It needs a code change first:@cosyte/fhirand@cosyte/transformare imported with a rawawait import(), so they must berouted through a guarded loader to degrade to
CLI_PARSER_UNAVAILABLE(exit69) rather thancrash.
loadOptional()cannot be reused unchanged: it takes aCosyteFormat, and"transform"isnot one, and its diagnostic hardcodes the word "parser". Not undertaken here.
0.0.3entries above: the swap and the loader change were bothcarried out. Two claims that entry made did not survive
contact: the "name similarity" reading of the
@cosyte/fhirE403was retracted across theecosystem on 2026-08-03 (the cause is unexplained; do not assert one), and "npm tolerates an
optionalDependencythat fails to resolve" is true only in isolation. Declaring both@cosyte/fhirand@cosyte/transformoptional fails the install outright withERESOLVE, whichis why
@cosyte/fhirended up not declared at all rather than declared optional.0.0.2 - 2026-07-31
Added
README.mdnow opens with the shared Cosyte lockup in a<picture>block, above the H1. Thedark-ground tile (
cosyte-lockup-tile-on-dark-1200x300.png) sits behind aprefers-color-scheme: darkmedia query and the light-ground tile(
cosyte-lockup-tile-on-light-1200x300.png) is the inner<img>fallback, so the mark is read ona ground that matches the page it is read on. The
# @cosyte/cliheading and the blockquote underit are unchanged: the artwork reads "Cosyte" and the heading names the package, so nothing on the
page is duplicated. Both URLs were re-checked immediately before the push and returned
200 image/png(10513 bytes dark, 10455 light). The block was copied byte for byte out of@cosyte/hl7'sREADME.mdrather than retyped and diffed against it, because a transcriptionerror in one of these URLs is a broken image on a public package page.
This replaces the per-package banner added earlier in the same unreleased window, and the reason
it replaces it is recorded rather than dropped. That banner was a plain markdown image, chosen
over
<img>or<picture>on the stated ground that whether npm's markdown sanitizer preserves a<picture>element was unverified. That was an accurate account of what was known when it waswritten, and it has since been measured: GitHub honours the
prefers-color-schemeswitch(observed on
@cosyte/astmin dark mode, where the rendered image'scurrentSrcresolves to theon-dark tile and its parent element is
PICTURE), and on the npm package page the<img>ishoisted out of its
<picture>by the anchor wrapper rather than the element being stripped, sothe light cut renders, which is the correct one there because npmjs.com has no dark mode. Those two
measurements were taken on
astmand reported into this repo rather than re-taken here; what wasre-checked directly for
cliis that both tile URLs return200 image/png. The failure mode issafe either way: a renderer that strips
<source>renders the inner<img>, so the worst case isa light-ground mark on a dark page, never a missing or broken image.
Why this is corrected rather than annotated in place.
0.0.1published on 2026-07-29, a daybefore the banner landed, so no
@cosyte/clitarball has ever carried the banner or the sentenceannouncing it. Annotating an entry no consumer received would publish an addition and its
replacement as two changes when only one was ever visible, so the banner entry is removed from this
release and its reasoning is carried here instead of being silently reversed.
The alt text describes the artwork, a plus mark set in two overlapping rounded squares beside the
Cosyte wordmark, rather than the package. It is what a screen reader on the npm page reads out and
what a reader gets when the image fails to load, so repeating the
@cosyte/cliheading below itwould be a wasted line. The wording is the one eight sibling packages already carry, confirmed
against both rendered PNGs here rather than copied on trust.
Fixed
0.0.1is published and uninstallable (ASSETS-P8).0.0.1published on 2026-07-29 with all ten
file:vendor/*.tgzdependency specifiers intact.vendor/isnot in
filesand there is nobundledDependencies, so every install route (npm i,npm i -g,npx) fails withENOENTonnode_modules/@cosyte/cli/vendor/cosyte-fhir-0.0.0.tgz. Reproducedin a clean directory. A published version is immutable (ADR 0001), so
0.0.1stays broken and thefix must ship as a later version.
README.mdgains a "Known issue" section stating what fails, the exact error, why, and thata source checkout is the only workaround.
docs-content/installation.mdgains the same under"Installing fails today". Both copies of the broken
npx-based MCP registration snippet areannotated (
README.mdanddocs-content/mcp.md).README.mdsaid "not yet published to npm"; it ispublished. (2) It described swapping the vendored sibling deps for real
@cosyte/*npm ranges asa step still to come "at that flip"; the flip already happened without the swap, which is the
defect. (3) The same "not yet published" claim appeared in three
docs-content/pages. (4)The
redact/deidterminal diagnostic, the--helptext, three JSDoc blocks that compile intodist/*.d.ts, and five docs pages said@cosyte/deidwas unpublished, unshipped, or unbuilt;it is published at
0.0.2, and the accurate statement is that the CLI does not wire it yet. Everycount here was re-derived by census of the base tree, not estimated.
CosyteFormatandOP_SUPPORTbothenumerate exactly eight.
RELEASING.mdrecords that the documented dependency-swap step was skipped, that a greennpm publish --dry-runcannot catch this (it packs a tarball but never resolves its deps from aregistry), and adds a checklist step to install the published version from outside the repo.
0.0.1 - 2026-07-29
Added
Phase 7: release hardening (the final roadmap phase; the CLI is feature-complete). No new
runtime command surface: this phase is publish-readiness.
test/fuzz.property.test.tsfuzzes the terminal(
run, over arbitrary argv vectors + stdin bytes) and the agent surface (dispatchTool, over anarbitrary tool name + arguments), asserting neither ever throws an unhandled exception, always
resolves to a documented exit code, and never leaks a raw stack frame onto a secondary channel. The
case count scales via
CLI_FUZZ_RUNS; a scheduled Fuzz workflow (.github/workflows/fuzz.yml)runs it nightly at a high count, and
pnpm test:fuzzruns it on demand.test/exit-code-matrix.test.tslocks one representative invocation forevery code in the
0/1/2/65/66/69/70contract, driven end-to-end throughrun, so a regression thatturns an invalid-input exit
1into a0(or renumbers a code) fails CI. The exit-code map and thestable
CLI_*diagnostic codes are a stability surface: renaming one is a breaking change.smokegate (scripts/smoke.mjs, wired intoverify.sh)exercises the built package (the dual ESM/CJS
.and./mcpsubpath exports, and bothcosyte/cosyte-mcpbins undernode) andnpm publish --dry-runassembles a clean tarball(
dist+README/LICENSE/CHANGELOG).attwremains a publish gate.docs-content/limitations.md(wraps-not-implements, the non-goals, thehonest per-(format, operation) support matrix, the PHI-default posture), a man-page-style
docs-content/reference-commands.md, andRELEASING.md(the one-package-two-bins publish,provenance/OIDC, the vendored-
file:→npm dep swap, and the two standing founder stops).Phase 6: six more formats + streaming + shell completion (ADR 0025). The
cosyteCLI now wrapsall eight cosyte formats, routed through a single lazy per-format adapter registry
(
src/core/parsers.ts) that replaces the old per-commandhl7 ? : fhirbranches and makes supportper (format, operation). An unsupported (format, op) is a value-free
CLI_FORMAT_UNSUPPORTED,never a fake (ADR 0018).
x12,astm,ncpdp(SCRIPT) → parse · inspect ·fmt · validate;
ccda→ inspect · fmt (XML re-serialize) · validate (parsedeferred, nolibrary-blessed JSON model; XML is the canonical form);
dicom→ inspect · validate (parse/fmtdeferred. The model is binary);
mllp→ parse · inspect (a transport container the CLI de-framesto its enclosed HL7 message(s)). Content autodetection now covers all eight (conservative + disjoint:
a leading
0x0BVT byte routes tomllp,ISA→x12, anH-record→astm,<ClinicalDocument>→ccda,a
<Message>in the NCPDP namespace→ncpdp,DICM@128→dicom);--formatacceptsmllp.parseemits NDJSON with per-record isolation for inherentlymulti-record inputs: an MLLP stream (one record per frame) and any input under the new
--ndjsonflag (one record per non-empty line: the FHIR bulk-data convention). A record thatfails to parse becomes a value-free
{ record, error }line and the stream continues; the overallexit is a data error (
65) if any record failed. A single message is unchanged (one pretty, or--jsoncompact, envelope + a value-free warning-count note).cosyte completion <bash|zsh|fish>prints a static, value-free completionscript generated from the command tree.
optionalDependencies, lazy-loaded per format and outside the hard-runtime-dep closure(ADR 0025, mirroring the MCP SDK isolation of ADR 0024). An absent optional parser degrades to a
value-free
CLI_PARSER_UNAVAILABLE(exit69), never a crash. Pinned sibling commits: dicomd1ed590, x120c60606, ccda3753216, ncpdp184eecc, astm92ac210, mllpaecff75(allv0.0.1 except astm records-layer). Third-party CLI-core runtime deps stay zero.
CLI_PARSER_UNAVAILABLE(exit69); the exit-code contract is otherwiseunchanged (
0/1/2/65/66/69/70). New value-free inspect summaries per format; new programmaticexports (
OP_SUPPORT,supportsOp,formatsSupporting,parseFormat,inspectFormat,fmtFormat,validateFormat,deframeMllp,loadOptional,valueFreeLocator,DETECTABLE_FORMATS,completionCommand, and the result/summary types). The publicWIRED_FORMATSset is removed infavour of the per-op
OP_SUPPORTmatrix (pre-alpha0.0.xsurface change).Phase 5: the
cosyte-mcpMCP server (the agent front door). A stdio Model Context Protocolserver that exposes the shared command core to an LLM/agent as callable tools: the second adapter
over one core (ADR 0022, 0024). Reachable three ways: the new
cosyte-mcpbin, thecosyte mcpsubcommand, and the
@cosyte/cli/mcpsubpath export. Tools:parse,validate,inspect, andconvert, each a thin wrapper that calls the same command handler the terminaluses (with
--json), socosyte parseand the MCPparsetool agree by construction; the CLIre-implements nothing.
no
--unsafe-show-valuesdoor on the agent surface. A tool result carries the requested data(the parsed model / converted Bundle: the explicit request); a tool error carries only the value-
free diagnostic (a stable code + positional context), never an input value. A parsed-but-invalid
validateverdict is a successful call reporting the verdict, not a tool error; only a hardfailure (unparseable / no input / usage) sets
isError.@modelcontextprotocol/sdk(the CLI'sfirst and only third-party runtime dependency) is declared in
optionalDependencies(pinned1.29.0) and imported only insrc/mcp/server.ts, reachable solely via the./mcpboundary(the subpath, the
cosyte-mcpbin, and a dynamicimport()on thecosyte mcpbranch). Acosyte parseinvocation never loads it; the core works with the SDK absent (--omit=optional). Because itis not part of the hard runtime closure, the umbrella
verify-policy.jsoncap oncliruntimedependenciesstays 4: unchanged. A static isolation test proves nocore/commandsmoduleimports the SDK.
@cosyte/cli/mcpand newcosyte-mcpbin; new programmatic exports(
createMcpServer,startStdioServer,dispatchTool,TOOL_DEFS, and the MCP result types) on the./mcpsubpath.redact/deid(gated on@cosyte/deid) andmap-codesare deliberately notexposed as tools yet.
Phase 4:
convert/map-codes(the consumer-of-consumers commands). Two commands that wrapthe higher-layer libraries; the CLI adds no mapping or terminology logic of its own.
convert <file|-> --to fhir [--json] [--quiet]: HL7 v2 → FHIR R4 via@cosyte/transform. Parses the input with@cosyte/hl7, hands the parsed message totransform.toFhir, and emits the serialized FHIR messageBundle(the library's canonicalserialization) on stdout:
cosyte convertequalstransform's programmatic output. Theconversion's value-free issues (a stable code + a v2-index → FHIRPath locator, never a field value)
render on stderr (or as a JSON envelope under
--json);--quietsuppresses them. Theload-bearing rule mirrors
validate: an error-severity transform issue drives exit1,never
0.--to fhiris required (the only target); a non-HL7 source (e.g. a FHIR document) isa value-free
CLI_FORMAT_UNSUPPORTEDdata error (65), never a fake conversion; an unparseableHL7 input is
CLI_PARSE_FAILED(65).map-codes <conceptmap|-> --code <code> [--system <uri>] [--version] [--display] [--json] [--quiet]: translate a single source coding through a BYO FHIR R4 ConceptMap via@cosyte/terminology($translate). The positional is the ConceptMap document; the sourcecoding is named by flags. A ConceptMap and a code are reference data, not PHI, so the
translation result goes to stdout: a match → the target coding(s) + exit
0; anunmapped code → the never-fabricate
TERM_TRANSLATE_UNMAPPEDsignal + exit1. A map thatis not valid JSON or not a loadable ConceptMap is the new value-free
CLI_MAP_INVALIDdataerror (
65), surfacing the stable terminology-loader code (e.g.TERM_CONCEPTMAP_MALFORMED),never the map's bytes.
CLI_MAP_INVALIDdiagnostic code. New programmatic exports:convertCommand,convertOutcome,mapCodesCommand. New runtime dependencies (ADR 0023):@cosyte/transform(
e6c4531, v0.0.0) and@cosyte/terminology(e5ed368, v0.0.1) as hard, first-party,lazy-loaded deps: vendored as
pnpm packtarballs undervendor/until PUB-FLIP(
pnpm vendor:refresh; umbrella ADR 0008). The umbrellaverify-policy.jsoncap oncliruntimedeps was raised 2 → 4; third-party CLI-core runtime deps stay zero (both siblings are
lazy-loaded per command, so the
parsefast path never loads them).0023: wire@cosyte/transform+@cosyte/terminology; the deliberate 2 → 4 cap raise(amends ADR 0021).
Phase 3:
validate/inspect/fmt. Three commands over the two wired parsers(HL7 v2 + FHIR R4), each a thin wrapper that re-implements no library logic.
validate <file|-> [--profile] [--json] [--quiet]: parse + run the wrapped parser's ownvalidation surface, with the verdict in the exit code:
0valid,1invalid (parseablebut non-conformant),
65unparseable,66no input,2usage. The load-bearing rule: avalidation failure is never exit
0; "unparseable" (65) is a distinct signal from "parsedbut invalid" (
1). Findings are value-free: a stable code, a severity, and a positionallocator (a FHIRPath, or an HL7 segment/field index), on stderr by default, or as value-free JSON
on stdout under
--json;--quietmakes the exit code the whole signal. The CLI invents noverdict: FHIR validity is
@cosyte/fhir'svalidateResource().valid(plus any error-severityread issue); HL7 validity is "parseable" (its warnings are non-fatal by the library's design:
surfaced, never failing).
--profileis gated to an honestCLI_NOT_IMPLEMENTED(exit69):the CLI bundles no profiles yet, so it never fakes or silently drops a profile verdict.
inspect <file|-> [--json]: a value-free structural summary: HL7 message type, version,per-segment-type counts, and a warning count; FHIR
resourceType, Bundle entry counts by type, anda read-issue count. Counts and structural type codes only, never a field value.
fmt <file|->: canonical re-serialization via the wrapped library's spec-cleanserializer (
Hl7Message.toString()/serializeResource); its stdout is the data channel. Anunparseable input is a data error (
65) with no partial emit.EXIT.INVALID(1): thevalidateverdict code (the exit-code contract is now0/1/2/65/66/69/70). All four commands share one input + format front door (core/resolveInput)and one value-free parser-failure boundary (
core/wrap), so the value-free-by-default posture andthe
--unsafe-show-valueschokepoint apply uniformly;parsewas refactored onto the sharedhelpers (behavior-preserving). New programmatic exports:
validateCommand,inspectCommand,fmtCommand,resolveInput,parseFailureResult,formatHl7Position,errorResult. No newruntime dependencies: stays within the cap of 2.
Phase 2: PHI posture hardened +
redact/deid+--unsafe-show-values.--unsafe-show-values: a global, opt-in, PHI-exposing flag, resolved once and order-independently and funnelled through a single chokepoint (
core/phi.ts), so the "a valuereaches a secondary surface iff the flag is set" property holds in one place. Off by default;
with it set, a
CLI_PARSE_FAILEDdiagnostic appends a bounded, single-line excerpt of theoffending input. Every other surface stays value-free, and a successful parse still puts values
only on the stdout data channel.
redact/deid(<file|->,--format): the de-identification command, shipped as anhonest, typed
CLI_NOT_IMPLEMENTED(new exit code69,EX_UNAVAILABLE). It is gated on@cosyte/deid(unpublished,DEID-1in flight), never reads the input, and never emits apartial Safe-Harbor scrub presented as de-identified. A built-in redactor is deliberately
withheld: a partial scrub over only the obvious PHI loci would leave PHI behind and present a
false-safety impression (the cardinal hazard). It delegates to
@cosyte/deidvia a documentedseam (
core/deid.ts) when that library ships and is vetted.working directory) and by design (commands return a
RunResult; only the thinbinwrites toprocess streams).
CLI_NOT_IMPLEMENTEDdiagnostic code andEXIT.UNAVAILABLE(69); new programmatic exports(
PhiPosture,VALUE_FREE/SHOW_VALUES,extractPhiPosture,unsafeInputSuffix,deidStatus,redactCommand).Phase 1: the
cosyte parsefoundation. Reshaped the scaffold from a library skeleton into abinpackage:package.json#binmapscosyte→dist/bin/cosyte.mjs(a shebang entry over atestable
core), argument-parsed with Node's built-inutil.parseArgs+ a hand-rolled subcommanddispatcher (no third-party CLI framework).
cosyte parse <file|->: reads a file argument or stdin (-); autodetects the format bycontent (HL7 v2
MSHframing, FHIR JSONresourceType): conservative and fail-safe (a confidentsingle match parses; ambiguity/no-match is a data error asking for
--format, never a guess); routesto the wrapped parser (lazy-loaded per format); emits the parsed model as typed JSON on
stdout. Flags:
--format,--json,--quiet,--no-color.The exit-code contract (
sysexits.h-grounded, documented, tested):0success ·2usage ·65data/parse error (EX_DATAERR) ·66no input (EX_NOINPUT) ·70internal (EX_SOFTWARE).The CLI never exits
0on input it could not handle.Value-free diagnostic channel with stable
CLI_*codes (CLI_FORMAT_UNDETECTED,CLI_FORMAT_AMBIGUOUS,CLI_FORMAT_UNSUPPORTED,CLI_NO_INPUT,CLI_EMPTY_INPUT,CLI_PARSE_FAILED,CLI_USAGE,CLI_INTERNAL). stdout is the data channel; every stderr line isvalue-free: code + position only, never a field value. No temp files, no file logging.
Programmatic
coreAPI (the.subpath):detectFormat/classifyCandidates/detectionError,EXIT,CLI_CODES/CliError,run,parseCommand,VERSION.Runtime dependencies (ADR 0021):
@cosyte/hl7(46d50eb, v0.0.1) and@cosyte/fhir(7a099b2,v0.0.0) as hard, first-party deps (an
npxbin cannot peer-depend) vendored aspnpm packtarballs under
vendor/until PUB-FLIP (pnpm vendor:refresh; umbrella ADR 0008). Capped at 2by the umbrella
verify-policy.json; third-party CLI-core runtime deps stay zero.ADRs:
0021(developer-tooling tier is abinthat hard-depends on first-party siblings;third-party runtime deps minimized) and
0022(one repo, two bins: the CLI and the futurecosyte-mcpMCP server over one core; the web playground is out of scope).Changed
The documented per-(format, operation) support matrix now states its negatives in words. In
docs-content/limitations.mdthe table used a bareU+2014as a cell value meaning "notsupported". The em-dash sweep below rewrote it as punctuation, so
dicomfmt,mllpfmtandmllpvalidaterendered as a stray colon: "support absent" silently became "support unstated",on the page whose whole job is honest capability disclosure, in a form a reader would read as a
rendering artifact rather than as a claim. Caught by the conformance refuter, not by CI, and nothing
in this repo's CI could have caught it (
test/docs-content.test.tsonly executes runnabletsblocks, and Prettier's glob does not cover
docs-content/). Those three cells now readnot supported. The support facts are unchanged; the page now says them. Before sweeping anyrepo for this character, find the places it is a value rather than punctuation and convert those by
hand, to a word, first.
No cosyte surface in this repo uses an em dash any more, and a CI gate keeps it that way. The
brand rule (
knowledgebase/06-brand/voice-and-tone.md, "No em dashes. Ever.") bansU+2014outright and names commit messages explicitly. Measured byte-level over all 124 tracked files,
not over markdown alone: 659 occurrences across 87 files, all as the literal character and none
in an encoded form. 61 of the 87 are not markdown (26 are), and one is
package.json, whosedescriptionis published to npm and rendered on the package page. Every occurrence was rewrittenin place with a period, a colon, a comma, or parentheses, never re-encoded. Consumer-visible text
that changed wording: the npm description,
README.md,docs-content/,cosyte --help, thegenerated
bash/zsh/fishcompletion scripts, and theredact/deidand reserved--profileunavailability messages. No stable code, exit code, flag name, or JSON field changed, so nothing
branching on an exit code or parsing
--jsonis affected. Commit subjects already onmainareleft alone: the message half runs on
pull_requestonly, and history is not rewritten.scripts/check-no-emdash.sh(pnpm check:no-emdash) plus.github/workflows/no-emdash.ymlscan both halves the rule covers: every tracked file, and thepull request title, body, and commit messages, on the non-default
editedtrigger so retitling apull request re-checks it. The new
no-emdashcheck-run context is required by theci-required-checksruleset.clitracks eleven binaries (the tenvendor/cosyte-*.tgzpacked sibling dependencies andtest/__fixtures__/sample.dcm), so the script partitions on an explicit NUL-byte rule rather thanon
grep -I's heuristic, which would silently skip a genuine text file with a broken encoding.The reason is measured rather than precautionary:
vendor/cosyte-hl7-0.0.0.tgzalreadycontains the byte sequence
E2 80 94by compression coincidence, so a text-only scan reds on ittoday, naming a compressed byte stream nobody wrote and offering no way to fix it.
holding a NUL byte is exempt and its em dash would be missed (there is none today; the excluded
count on the OK line is the tell). Encoded-form matching is literal, so lowercase
%e2%80%94, acapital-X
—, and a semicolon-less—pass. The scan reads file contents, neverfile names.
The published type declarations no longer carry internal project bookkeeping.
dist/index.d.tsand
dist/mcp.d.ts(and their.d.ctstwins) are compiled from the JSDoc on every exported symbol,and that JSDoc was citing item identifiers (
CLI-6), ADR numbers (ADR 0018,0021,0022,0025), the meta-repo roadmap in prose and by path (cli roadmap §7,operations/roadmaps/cli.md), and phase language (§Phase 4,Phase-5, "in a later phase"). Allof it rendered on hover for anyone who installed the package. Measured on
62fba77with the ruleset that ships alongside: 61 occurrences across 23 tracked
src/files, producing 56 across thethree declaration files a consumer receives (43 in
dist/index.d.ts, 12 indist/mcp.d.tsand 1in the shared
dist/io-*.d.tschunk both entries import), each mirrored in its.d.ctstwin. Now0 on every one of them. (The shared chunk is easy to miss and was missed once here: a count that
scans only the named entry points under-reports.) Every removal is a cut, not a rewrite: the
surrounding guarantees (value-free stderr, the never-a-fake
CLI_FORMAT_UNSUPPORTED, the gatedredact, the exit-code contract) are worded exactly as strongly as before. Internal traceabilitystays where the convention puts it: this file, the changesets, the commits and the roadmap.
A dependency-budget figure left
src/core/deid.tstoo. The module said wiring@cosyte/deid"would breach the CLI's runtime-dep cap (2)". Only the number was stale: the cap has been 4 since
convert/map-codeslanded, and the constraint itself still holds, because the package declaresexactly 4 hard runtime dependencies and a fifth would indeed breach it. The clause went because a
dependency-budget figure is internal bookkeeping a consumer cannot act on, not because the
constraint lapsed. The reasons that actually ground the refusal to ship a built-in redactor are
untouched:
@cosyte/deidis unpublished, the wrapped parsers expose no de-identification API, anda partial scrub would present a false-safety impression.
CosyteFormat's documentation no longer understates autodetection. It read as though contentdetection recognised only HL7 v2 and FHIR, with the other six accepted by
--formatbut "not yetwired", which has not been true since all eight formats gained signatures. The stale sentence is
removed rather than restated.
A gate now enforces the public-surface rule, which is why the class stops regrowing.
scripts/check-no-internal-refs.sh(pnpm check:no-internal-refs, on theverify.sh cliladder)plus
.github/workflows/no-internal-refs.ymlport the shape ofhl7's gate(hl7#62, hl7#64)
and
ncpdp's (ncpdp#36) rather than the file. Fourpasses: the public markdown surface line by line and paragraph-joined, the npm metadata,
src/doc comments, and
src/string literals (the pass that would have caught the two identifiers thispackage printed to a user's terminal). Seven rules. Re-derived for this repo: the scan surface, a
standards-designation exclusion list covering all eight formats (this is the package where
HL7-V2,FHIR-R4,DICOM-SR,NCPDP-SCRIPT,X12-837P,CCDA-R2.1,MSH-2,NM1-03,439-E4andICD-10-CMare live at once, so theWORD-Ntrap is at its widest), and a seventhrule no sibling has, a prose roadmap citation, which was 30 of the 61 and which
hl7'spath-keyed rule cannot see. Both self-test halves run on every invocation: positive samples prove
each rule still matches, negative samples prove none has been widened into the
WORD-Nshape thatwould delete the reference material the CLI's docs exist to provide.
CHANGELOG.mdis excluded onpurpose, as it is in
hl7andncpdp: it ships inside the npm tarball, yet the convention names itas one of the places identifiers belong. That contradiction is ecosystem-wide, and it is recorded
here rather than settled by one repo.
redact/deidno longer names an internal tracking identifier on any consumer surface. TheCLI_NOT_IMPLEMENTEDtext printed when de-identification is unavailable carried an internal workitem that means nothing to anyone running the command, and the same identifier reached the
published type declarations (
dist/index.d.ts/dist/index.d.cts), where it surfaces in editortooltips. Both now state only the consumer-observable fact: the command delegates to
@cosyte/deid, which is unpublished, and the CLI ships no built-in redactor because a partialscrub would present a false-safety impression. The stable
CLI_NOT_IMPLEMENTEDcode and the exit69are unchanged.CLI_PARSER_UNAVAILABLE's message no longer cites an internal decision record. The errorraised when an optional per-format parser is not installed pointed at an ADR number, which is
meaningless to a caller; it now just names the package to install. The stable
CLI_PARSER_UNAVAILABLEcode and the exit69are unchanged. The same sweep removed theremaining roadmap-phase language from
README.mdanddocs-content/troubleshooting.md.The CI checks are now binding on
main.ci / verify (22, ubuntu-latest),ci / verify (24, ubuntu-latest),ci / actionlintandcodeql / analyze (javascript-typescript)are required status checks, each pinned to the
github-actionsapp, alongside branch deletion andforce-push protection. They were advisory before: a red check could not stop a merge. Dependabot
now watches the npm and GitHub Actions dependency surfaces weekly, which nothing did previously.
Reshaped the package from the parser-library scaffold to a
binpackage. Removed the archetypestubs (
parseCli,WARNING_CODES,FATAL_CODES); replaced the librarysrc/index.tsand theround-trip property test with the command tree, the programmatic
coreAPI, and command-contract /autodetection / PHI-leak / equivalence tests. Rewrote
docs-content/andREADME.mdfor the CLI.Fixed
README + guides now describe the shipped Phase-3 command surface. The
README.mdanddocs-content/guides-overview.md"Status" blurbs read as a Phase-1-forward roadmap ("Phase 1 shipsparse…"); they now state the current surface directly (parse/validate/inspect/fmtand the gated
redact/deid) over the two wired parsers (HL7 v2 + FHIR R4). The pre-alpha,not-yet-published-to-npm status is unchanged (accurate), and the
npx/npm install -gexamples nowcarry a "not on npm yet" caveat (docs-only; README-ORG-SWEEP).
phi-scannow scans the real fixture directory. The scanner's fixture root pointed at anonexistent
test/fixtures/; it now walkstest/__fixtures__/(and the same path in the stagedfilter), so the PHI commit-gate actually covers the CLI's synthetic fixtures.