v0.3.0
Upgrade notes (v0.2.1 → v0.3.0)
binder never rejects input, and no emitted output changes: convert bundles are byte-identical. Almost every change below is additive or a diagnostic-label change. There is one exit-code exception, and it is a correctness fix rather than an arbitrary break: lint --strict can now exit 1 on a corpus that exited 0 under v0.2.1 — but only when that corpus contains a heading anchor that is genuinely dead on GitHub and that v0.2.1 wrongly accepted. The link was already broken; binder simply was not saying so. If you gate CI on binder lint --strict, read the heading-anchor bullet below before upgrading. Nothing that was correct under v0.2.1 now fails.
- Entrypoints are now reported separately from orphans. A corpus-root
README.mdwith no inbound links was reported as an orphan in v0.2.1; it is now reported as an entrypoint and is not counted as a finding. Only a rootREADME.mdis recognised by name —index.mdandINDEX.mdare not, and remain orphans exactly as in v0.2.1. - Report section labels changed.
lintandreviewnow printentrypoints (no inbound links):(new section) andorphans (no inbound or outbound links):(v0.2.1 printedorphans:). If you grep binder's prose output, update your patterns. --strictmay now exit 0 where it exited 1 — but only where the root-README orphan was the last remaining finding. Other findings still gate.- Heading-anchor checking is now correct, and it moves findings in BOTH directions. binder's slug function claimed GitHub parity but collapsed runs of hyphens and dropped underscores; GitHub does neither. So
#agent-skill--plugin(valid on GitHub) was wrongly reported broken, and#agent-skill-plugin(dead on GitHub) was wrongly accepted. Both are fixed.- On this repo's own docs that removes 50 false reports and adds none (85 → 35 findings; every one of the 35 remaining is a deliberate test fixture).
- But if your corpus contains an anchor that was silently dead before,
lintnow reports it — andlint --strictcan go from exit 0 to exit 1. Confirmed, not theoretical. A CI job gating onbinder lint --strictcan newly fail. That is the fix working, not a regression: the link was already broken on GitHub, binder just wasn't saying so. - No emitted bytes change.
convertoutput is byte-identical; this affectslintdiagnostics only. - Unchanged and carried forward, NOT introduced: headings with accented or non-Latin characters (
## Café,## 配置) still produce false "broken anchor" reports, exactly as in v0.2.1. binder's kept character set is ASCII where GitHub's is Unicode. Now documented in code; a follow-up issue is filed.
- JSON additions, all additive, under an unchanged
binder.report/v1schema tag.review --jsongainsentrypoints;infer --jsongainswarnings, and itsmappingsfield is now[]instead ofnullwhen empty;validate --jsongainsreserved_structure_checked(boolean, alwaysfalsein v0.3.0). The schema tag did not change, so consumers pinning on the tag will not see these coming. validatenow discloses what it does not check. When a bundle contains reserved files (index.md,log.md), the prose report gains one line:scope: reserved-file structure (index.md, log.md) not validated; verdict covers concept files only. Nothing about the verdict changed — v0.2.1 did not check reserved-file structure either. What changed is that it now says so, rather than letting a clean verdict imply a coverage it never had. Bundles with no reserved files print byte-identical output to v0.2.1.- Usage errors now exit 2, not 3. A malformed
--type-mapand an unknown subcommand both returned 3 in v0.2.1 and return 2 in v0.3.0 (ExitUsage). Scripts branching on exit 3 for usage errors need updating; genuine I/O errors still exit 3. infer's "no mappings inferred" diagnostic moved from stdout to stderr, so it no longer pollutes piped stdout.- If you installed with
go install, your provenance stamp loses itsv. v0.2.1 installed that way stampedby: binder/v0.2.1; v0.3.0 stampsby: binder/0.3.0, matching what the downloaded release binaries have always written. This appears inside bundles you have already generated, so a re-run will show aby:diff on otherwise unchanged concepts. Binaries downloaded from the releases page are unaffected — they were neverv-prefixed.
0.3.0 (2026-08-16)
Features
- convert: --external-root suppresses advisories for known sibling workspaces (#25) (7c3174f)
- enrich: --overwrite-keys refreshes named keys in place, refusing trust keys (#22) (8fcf727)
- graph: read-only query_graph MCP tool with five traversal operations (#33) (bb383ae)
- mcp: expose external_root on convert and name all seven tools in help (#62) (8ea12b0)
- review: reclassify entrypoints vs orphans in review and lint (#24) (3546fda)
- status: validate --status-map vocabulary; opt-in canonicalization (#23) (7f4ca6b)
Bug Fixes
- build: canonicalize the version stamp to a single no-v form across all install paths (e9df155)
- cli: stop claiming root index.md is a recognized entrypoint (b98f502), closes #73
- cli: usage errors exit 2 and infer emits stable empty arrays (8b2083a)
- infer: write the zero-mapping diagnostic to stderr so stdout stays machine-consumable (#67) (bcba1fd)
- keep underscores and hyphen runs in okf heading slugs (#84) (9b34a0f)
- lint: stop electing a root index.md as an entrypoint (#75) (34c9483), closes #71 #72
- make validate disclose unchecked reserved-file scope (#83) (6208383)
Documentation
- clijson: envelope keys are struct-order, not sorted (948260e)
- correct entrypoints label, tutorial claims, MCP parity (#78) (69cd81f)
- correct README, tutorial and RELEASING against the shipped v0.3.0 binary (#65) (9df8eaf)
- graph: document the graph surface and ship a graph-format sample (#36) (1fb9284)
- lint: correct the divergence example in a doc comment (#81) (5bdc3e1)
- readme: correct lint/review agreement and root entrypoint claims (#69) (7bad872)
- user-guide: correct and complete user guide for v0.3.0 (#70) (2293734)
Known issues — present in v0.3.0 and v0.3.1
None of the issues below is fixed in v0.3.1. Fixes are landing in v0.3.2.
These were found by an audit of every guarantee binder states in its help text and documentation: 45
claims were checked by running the binary, and the ones below did not hold. We are listing all of
them, including the ones that reflect badly on us, because binder's central promise is that it never
asserts something it has not verified — and that promise has to bind our release notes too.
1. binder enrich can write frontmatter that binder itself cannot read back
This is the most serious issue here: it can leave a file unusable.
When frontmatter contains a multi-line flow sequence — a [ … ] collection spread over several
lines — and binder appends an entry to it, the collection is re-encoded to block style but its
original closing ] is left orphaned on its own line. The result is not valid YAML.
Given demo/metric.md:
---
type: Metric
verified: [
{ by: human:x, at: 2024-02-01T09:30:00Z },
{ by: human:y, at: 2024-03-01T09:30:00Z },
]
---
# Body
running binder enrich demo --verified-by human:ghchinoy reports success:
1 file(s): 1 enriched, 0 unchanged, 0 skipped
enriched metric.md (added: generated, title, verified)
but produces frontmatter with a stray ] on its own line:
---
type: Metric
verified:
- at: "2024-02-01T09:30:00Z"
by: human:x
- at: "2024-03-01T09:30:00Z"
by: human:y
- at: "2026-08-16T21:53:51Z"
by: human:ghchinoy
]
title: Body
generated:
at: "2026-08-16T21:53:51Z"
by: binder/dev
---
A subsequent run cannot read the file it just wrote:
1 file(s): 0 enriched, 0 unchanged, 1 skipped
skipped metric.md (unparseable frontmatter: invalid frontmatter: yaml: line 8: did not find expected key)
This transcript is real output, captured from a source build at the v0.3.1 commit — which is why the
provenance line reads binder/dev. The two 2026 timestamps are wall-clock values from that run; your
timestamps will differ, the corruption will not.
If you have run binder enrich over a corpus containing multi-line flow sequences, we recommend
checking that those files still parse. Single-line flow sequences are not affected. A fix is
landing in v0.3.2.
2. binder enrich inserts a blank line between the frontmatter and the body
If a document's body starts immediately after the closing --- with no blank line, binder inserts
one when it rewrites the file. The body's content is preserved exactly, but its first byte
changes.
This happens on the default enrich path — it is the common case, not an edge case. It also
happens when only an existing value changes and no key is added at all.
This is a defect, and v0.3.2 is landing a change that makes the byte-faithfulness guarantee true
rather than narrowing it.
Two further deviations are deliberate, documented, and long-standing — design bounds rather than
defects, but listed here because anyone relying on byte-faithfulness needs to predict every
difference between their input bytes and their output bytes:
- CRLF line endings are normalised to LF.
- A file that does not end in a newline gains one.
3. Several stated guarantees are broader than the behaviour
v0.3.2 addresses all of these.
enrichis described as byte-faithful, without qualification, in the command's help text, in
README.md, in the packaged skill documentation, and in the ingestion-workflow and JSON-contract
reference docs. Issue 2 above is why that is currently wrong, everywhere it is stated.convertis described as deterministic, without qualification. Two runs over identical input
differ in thegeneratedprovenance timestamp, which is deliberately a live record of when the run
happened. Output is byte-identical when the clock is pinned viaSOURCE_DATE_EPOCH. The
behaviour is correct; the description is missing the qualifier — which the same user guide
already applies correctly toenrich.enrichis described as idempotent, without qualification. It is idempotent on the default
path, including across a live clock. It is not idempotent when--verified-byis used: a
second run appends a second attestation.enrichis described as adding only absent keys. A trust key that is already present is
appended to rather than skipped. binder still never clobbers or removes an existing value.
For the avoidance of doubt, the fourth guarantee in that same description — that writes are
atomic — was tested and holds. binder writes to a temporary file in the same directory and
renames it over the target, so an interrupted run leaves either the original file or the complete
replacement, never a partial one.
4. A verified-by value from the environment stamps without an explicit flag
If BINDER_VERIFIED_BY is set, binder writes a verified attestation without --verified-by being
given on the command line. An environment variable can arrive by means other than a deliberate
decision to attest. v0.3.2 is landing a change so that binder refuses to stamp from the
environment alone, and says so. An explicit --verified-by, or a verified-by value in your own
global config, continues to stamp as before.
5. The JSON report classifies a modified trust key as added
When a trust key that was already present is appended to, the binder.report/v1 envelope lists it
under added. Consumers reading that field programmatically get a wrong classification for the trust
key. The file content is correct; the report's description of what happened to it is not.
The link-resolution bug reported in #99 is fixed and ships in v0.3.1.