Restructure: docs, layout, linting, prose rules and cross-binding parity enforcement - #15
Merged
Merged
Conversation
`spec/` held 88 files of which 12 were markdown. The other 76 were a binary
fetch-test corpus — tarballs, signatures, index files — generated by core and
never read as a specification by anyone. One directory, two unrelated jobs, and
a name that described neither. Eric could not tell what it was from the outside,
which is the whole complaint.
spec/fixtures/fetch -> tests/fixtures/fetch (a test corpus, so tests/)
spec/c-abi.md -> docs/reference/c-abi.md
spec/bindings.md -> docs/reference/bindings.md
spec/artifact.md -> docs/reference/artifact.md
spec/README.md -> docs/reference/README.md
goldens/README.md -> docs/reference/goldens.md (goldens/ was one file
explaining its own absence since the set became served)
playground/ -> examples/
The ABI contract is DEMOTED, not moved out. Core argued this better than I did:
I claimed the header must stay public because a public Apache library whose
contract lives in a private repo is the wrong shape, then proposed moving the
contract into the private repo — the same objection, unapplied to my own
proposal. The day somebody writes a fifth binding they should not have to ask us
for the rules of a public interface.
Core accepts either fixture path already (`tests/fixtures/fetch` if present,
else the old one, `CHTYPES_SDK_FIXTURES` overrides), so there is no ordering
constraint between the repos and no false `sdk-fetch-fixtures` filing on this
rename.
TWO THINGS THIS NEARLY GOT WRONG, both caught by counting rather than reading:
1. The suites still passed after the move while QUIETLY SKIPPING 29 Python and
9 TypeScript tests. `pass` is not the signal here — this repo skips loudly
when fixtures are absent and only fails on a zero-run, so a partial skip
reads as green. Five fixture paths are built from SEPARATE string segments
(`/ "spec" / "fixtures" / "fetch"`, `path.resolve(…, 'spec', 'fixtures', …)`)
and a sweep matching the contiguous literal rewrote the prose in those same
files while leaving the lookup pointing at the old directory — so the skip
messages actively claimed the new path while reading the old one. Go hid it
twice over: check-standalone.sh passes CHTYPES_FETCH_FIXTURES explicitly, so
its broken default never showed.
Counts now match the pre-move baseline exactly: go 147, python 158 passed 0
skipped, ts 187 passed 4 skipped, rust 138 passed 4 skipped.
2. The fixtures are generated, signed and hash-verified, so a stray edit would
invalidate every signature. They were excluded from the sweep and then
re-verified byte-for-byte against core's generated set: 84 of 84 identical.
Relatedly, core warns that `gen.py` holds two byte strings spelling
`spec/fixtures/fetch` that are KEY-DERIVATION LABELS, not paths — "fixing"
them would change the test key and every signature under it. They live in
core, outside this sweep's reach, and I did the move by hand rather than
delegate it for exactly that reason.
include/chtypes.h's cross-reference to `artifact.md §Loading step 5` follows the
file to docs/reference/artifact.md in this same commit, as the header is owned
here. Core is sweeping its own ~90 references in one commit on its side.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JRrjwMiGbX4vWagJs38ndj
The old README opened with a Go snippet as its only Quickstart, buried install
behind a directory table, and led with a paragraph of implementation detail. A
reader arriving from PyPI or crates.io had to infer that their language was
supported at all.
Rewritten around what a stranger needs in order: what it is, install, run
something, is my version supported, where are the docs. All four languages are
peers in both Install and Quickstart — same program, four <details> blocks, no
default language. The snippets are the ones verified against the published
0.1.1 packages earlier today, including the two corrections that came from
RUNNING them rather than reading them: Python's `substituted` lives on
RowResult, and `using` is a syntax error on Node 22, this package's own engines
floor.
Added, because they earn their space:
- a centred header with a badge row carrying live version numbers from all
four registries, so the README cannot silently disagree with what ships;
- a "How it compares" table against the two things people actually do
instead — hand-rolled validation, and a round trip to a real server;
- an AI-assisted development disclosure, matching WaveHouse's. It states the
review gate is the same regardless of who authored a change, and asks for
drifted docs as issues — which is the failure mode this repo keeps hitting.
Removed from the front page: the directory table (a map of the repo is not what
a new user wants first) and the long "Developing here" section, which is
contributor material and belongs with CONTRIBUTING. 251 lines -> 188, and the
188 are aimed at someone deciding whether to use this.
Written unwrapped, one line per paragraph, ahead of the prose linter landing.
Verified: all six badge endpoints return 200; pkg.go.dev and PyPI resolve;
npmjs.com 403s and crates.io 404s to curl but both packages are confirmed live
via their APIs (bot protection and SPA routing, not broken links). Three links
-- docs/install.md, docs/quickstart.md, docs/guides/ -- point at files the docs
agent is writing in parallel; this branch must not merge before that one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JRrjwMiGbX4vWagJs38ndj
…-first The docs assumed you were writing Go and already knew what an artifact was. Neither held. This adds the three pages a new user actually reads first, with all four bindings as peers on every one of them: - docs/index.md — what chtypes is, the binding+artifact split, the three outcomes, and a map of the rest. - docs/install.md — both install steps in Go, Python, TypeScript and Rust, equal weight, plus how to check it worked and how to work against a checkout. - docs/quickstart.md — the same program four times. Every snippet was run against 25.8 on darwin-arm64 before it was written down; the printed values in the comments are the ones it actually printed. Python's Outcome is a StrEnum, so it prints `accepted`, not `Outcome.ACCEPTED` — which is the kind of thing that only shows up by running it. docs/artifacts.md and docs/fetch.md move under docs/guides/. fetch.md keeps its normative shape and gains a note pointing a mere user at the shorter road; artifacts.md is rewritten as that shorter road — get one, where it lands, how loading works, verification, pinning, the one error — and keeps a `## Loading` heading so include/chtypes.h's `#loading` anchor still resolves, with the ABI revision rule still at step 5. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRrjwMiGbX4vWagJs38ndj
Each binding README carried the same eight topics, written four times and drifting apart. They move here once, language-neutral, with all four bindings shown wherever a concept needs code: - guides/transformations.md — the silent-change report, the 24 reasons and which four are lossless, and the obligation substituted DEFAULTs create. - guides/settings.md — the four channels, the type-gate exception, why values cross as strings, the six reserved keys. - guides/batches.md — always rows, the two bad-row policies, the gateway/worker pairing rule, engineRows, and the export channel. - guides/filters.md — WHERE-side semantics, the four verdicts and failing closed, query parameters and brace-type sizing, the parse-once block. - guides/discovery.md — the three queries, and rebuilding DDL from a table. - guides/multi-version.md — RTLD_LOCAL, no nearest-match, and the thread-safety model per binding. - limitations.md — the four Known-limitations sections, merged. Two corrections found by checking the source rather than the prose. Go's dlopen'd *Library has no SetDefaultSettings at all — the symbol is deliberately absent from its function-pointer table — so the settings guide no longer shows a call that cannot compile. And Python's Verdict has no `answered()`; the fail- closed example uses a membership test, which is what the type actually offers. Reason spellings, verdict spellings and outcome spellings are quoted from the source constants; `default_materialised` keeps its British `s` because it is a frozen wire constant, and the guide says so where a spell-checker would reach. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRrjwMiGbX4vWagJs38ndj
…ctually has One page per binding, each carrying its own API table rather than four copies of one table drifting apart inside READMEs. Everything here was checked against the source; the corrections are the reason the pass was worth doing: - Go: SetDefaultSettings is package-level and linked-build-only. A dlopen'd Library does not carry the symbol at all, which the page now says and explains (the ABI requires it to exclude every other call on the image). - Python: the export channel is keyword-only on rows() — `export=`, not `export_format=` — and there is no rows_export method. Verdict has no answered() helper. substituted is on RowResult, never on BatchResult. - TypeScript: rows() takes a fourth RowsOptions argument; there is no rowsExport. DiscoveredColumn requires all five fields, so reconstructDdl is fed from parseColumnsResult rather than a hand-built row. exportDeclined is undefined, not ''. - Rust: Error::code() is Option<i32>, distinct from artifact_code(). rows_export takes Option<Format> plus DocFlags. The compile builder's .settings() takes anything iterable while rows() wants &[(K, V)] — a real mismatch a reader will hit, so it is called out. Column.default_kind is a DefaultKind with a None *variant*, not an Option. reference/c-abi.md and reference/bindings.md are linked but not written here: they arrive as the coordinator's demotion out of spec/. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRrjwMiGbX4vWagJs38ndj
…to ~85 These are what PyPI, npm and crates.io display, so each keeps what a reader needs to decide and to start — what it is, install (binding AND artifact), a working quickstart, the three-outcome rule — plus the handful of facts that are genuinely specific to that language and would be wrong to file under a general guide: - Go: the linked build is not a consumer path, and SetDefaultSettings exists only there. - Python: no float settings values; substituted is on RowResult; ctypes drops the GIL, so the locks are real. - TypeScript: `using` is a syntax error on Node 22, this package's own floor; string|bigint but never number; for() never fetches and open() can; worker_threads is the boundary. - Rust: two settings shapes (iterable on the builder, &[(K,V)] on the call); lifetimes enforce the free order at compile time; two constructors. Everything else now lives once in docs/ and is linked with absolute GitHub URLs, because relative links do not resolve on the package registries these READMEs are rendered by. Every quickstart in these four was executed against 25.8 on darwin-arm64 and prints what the comments claim. Also sweeps unmodelled/modelled/marshalling to American spelling across docs/; `default_materialised` is deliberately left alone as a frozen wire constant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRrjwMiGbX4vWagJs38ndj
…t.md The four-language playground tour was linked from the root README and from nowhere in docs/, which is exactly where someone who just finished the quickstart goes looking for a longer one. Linked from index.md and from the end of quickstart.md. support.md's hand-written tail still pointed at docs/fetch.md. The generated block is untouched and `scripts/support-matrix.sh --check` still reports the file current — the generator splices only between its markers, so prose outside them is safe to fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRrjwMiGbX4vWagJs38ndj
Two sweeps the docs rewrite left behind, plus the spelling pass Eric asked for. `docs/fetch.md` and `docs/artifacts.md` moved into `docs/guides/`, leaving 52 files citing the old paths in comments — prose citations, so nothing broke, but every one of them pointed at a file that no longer existed. Swept, along with the last `playground/` references the rename missed. American spelling, via `misspell -locale US` rather than a hand-rolled regex — which was the right call twice over. 187 files rewritten, 214 corrections. THE ONE THING THAT WOULD HAVE BROKEN THE SDK: `default_materialised` is a FROZEN WIRE CONSTANT the artifact emits across the ABI, not prose. Renaming it to `_materialized` would have silently desynchronised every binding from what the library actually returns, in a way no compiler catches — the constant is a string compared against artifact output. It is excluded via `-i`, and the docs rewrite had already documented why it keeps its `s`. 27 occurrences survive on purpose; that is the whole set. Also fixed `scripts/support-matrix.sh`, which emitted "neighbour" INTO the generated block of docs/support.md — a British spelling in a file no human can hand-edit, so the fix had to be in the generator. Regenerated. LICENSE excluded (legal text is not ours to reword) and tests/fixtures excluded (generated, signed, hash-verified — re-verified byte-identical against core's set afterwards, 84 of 84). Two of my own greps returned falsely clean results while checking this, and both are worth naming. A nested-alternation regex reported ZERO British spellings across a tree that had 214. And the "would this hit a code identifier?" check was case-sensitive, so it missed `DEFAULT_MATERIALISED = "default_materialised"` — the exact line that made this dangerous. A clean result from a regex I just wrote is not evidence; running the real tool and reading its output is. Suites after the rewrite, each at its true value: go 150 assertions, python 159 passed, ts 190 passed 4 skipped, rust 138 passed 4 skipped. Confirmed against the same commit and against 095644e that these are unchanged, not improved — an earlier "187" reading of mine was a stale ts/dist, not a real baseline. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRrjwMiGbX4vWagJs38ndj
Three internal links did not resolve after the merge. None were caught by the earlier path sweep, because that sweep ran before the docs rewrite existed: docs/guides/fetch.md ../../goldens/README.md -> ../reference/goldens.md docs/support.md ../goldens/README.md -> reference/goldens.md docs/reference/bindings.md ../docs/production-plan.md -> unlinked The third is worth naming: `production-plan.md` is a CORE document. The link came along when bindings.md moved out of spec/, and it could never resolve in this repo — a public page pointing into a private one. The citation stays as prose naming the core repository; the dead link is gone. docs/reference/README.md arrived as the old spec/README.md and still opened by re-pitching the product, which docs/index.md now does properly. Reframed as what it actually is: the index of the reference section, saying plainly that most readers never need it and who it is for. Its substantive content — conformance levels, frozen rules, provenance — is untouched; only the duplicated pitch and a table that the new header already carried are gone. 152 -> 140 lines. Verified: every internal link in every tracked markdown file now resolves, and `support-matrix.sh --check` still reports docs/support.md current (the line I edited there is outside the generated block). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRrjwMiGbX4vWagJs38ndj
ClickHouse's keyword is MATERIALIZED. I asked its parser through a 25.8 artifact rather than trusting memory: MATERIALIZED ACCEPTED MATERIALISED REJECTED -> [62] Syntax error (table properties declaration list) So the reason naming that concept was spelled a way ClickHouse itself refuses. Eric's rule is to match ClickHouse, with the exception being a word that mirrors a genuinely British ClickHouse identifier; this is the opposite of that case. The codebase was already internally inconsistent about it: `KindMaterialized`, the DEFAULT-kind enum, has always carried the z. Only the reason constant was British. Now unified. I HAD THIS WRONG AND IT WOULD HAVE BEEN EXPENSIVE. I described this constant as "a frozen wire constant the artifact emits across the ABI" — in an earlier commit message and to Eric — and proposed an ABI revision bump to 5 on that basis. It is the opposite. The artifact emits `default_substituted`; each binding TRANSLATES that into this reason for a value the gateway invented. The mapping lines say so plainly and I had them on screen: python/src/chtypes/transform.py:81 "default_substituted": Reason.DEFAULT_MATERIALISED rust/src/transform.rs:141 "default_substituted" => Some(reason::DEFAULT_MATERIALISED) I read a mapping FROM the wire value as proof it WAS the wire value. Core caught it, measured that the string appears nowhere under core/lib and zero times in the served golden set, and priced the bump I had proposed: every published artifact unloadable by the new SDK until 33 relinks and republishes finished, about three hours for darwin alone, buying detection of a mismatch that cannot occur. ABI stays at revision 4. So this is purely an SDK API change: a renamed exported constant and its string value, breaking for anyone comparing against either. Recorded as BREAKING in all four CHANGELOGs under Unreleased. The wire value `default_substituted` is untouched — verified after the rename that every binding still matches on it. Core lands five doc lines naming the string as SDK vocabulary in the same push as the header pull, once this branch is on origin/main. Suites unchanged: go 150, python 159, ts 190 passed 4 skipped, rust 138 passed 4 skipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRrjwMiGbX4vWagJs38ndj
Eric asked me to confirm include/chtypes.h is consistently American. It was not, and the tool said it was. `misspell -locale US` reports the header clean. The header contained `canonicalise`. misspell's US-locale dictionary is PARTIAL for the -ise family: it knows `serialise`, `recognise` and `normalise`-adjacent words it happens to list, and does not know `canonicalise`, `optimisation`, `specialised`, `generalised`, `initialisation`, `parameterised`, `desynchronises` or `unmodelled`. A clean misspell run is not a claim that a file is American. Found by pattern-matching the -ise/-ys/-our/-lled families out of the whole tree and testing each candidate word against misspell one at a time, so the dictionary's gaps became visible instead of silent. 91 occurrences across 34 files, none of them a symbol — every one checked, case-insensitively, against being an identifier or a string literal first. canonicalis* 43 specialised 8 initialisation 4 normalis* 25 desynchronises 2 optimisation 1 unmodelled 5 initialisers 1 generalised 1 parameterised 1 unmarshalling 1 THE TOOLING LESSON, which matters more than the words: `git grep -E` does not support `\b` and does not error on it — it silently matches nothing. Three "clean" results of mine today came from that, including one where I nearly reported a tree with 214 British spellings as clean, and one where a case-sensitive identifier check gave an all-clear on the exact line that made a rename dangerous. Every detector here is perl-based for that reason. A regex I just wrote returning nothing is not evidence. The four CHANGELOG entries describing the materialised -> materialized rename are reworded to say "previously the same word spelled with an `s`" rather than quote the old spelling. Eric asked for no exceptions in this repo, and a documented rename is the one case that would otherwise need an ignore list. There is now no ignore list: misspell reports ZERO findings across every tracked file except the generated fixtures and LICENSE. include/chtypes.h specifically: clean under misspell AND under the pattern detector, 0 hits. Suites unchanged: go 150, python 159, ts 190 passed 4 skipped, rust 138 passed 4 skipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRrjwMiGbX4vWagJs38ndj
spec/bindings.md has named what every binding must expose since the repository
split, and nothing has ever verified a row of it: 33 source files cite the
document in comments and the guarantee that "the four bindings give one answer"
has been prose plus discipline. This is the machine-readable half — 157 logical
capabilities, each with its spelling in all four languages, and 76 of them
carrying the VALUE every binding must answer with (the 24 transform reasons, the
10 frozen format codes, the three discovery queries verbatim, the six
docs/fetch.md §6 artifact codes, the ABI revision and the document flags).
Three things it is deliberately not. It is not a parse of the markdown table:
markdown is for humans, and a test that reads one is a test that breaks on a
reflow. It is not a list of NAMES — CompileDDL / compile_ddl / compileDdl /
lib.compile(ddl).compile() is one capability spelled four idiomatic ways, and
the idiom is the point. And it is not silent about gaps: a binding that should
not carry a capability writes an {"absent": "<why>"} in its column, an absence
with no reason fails the manifest's own integrity check, and the two real ones
here are the spec's own argument — Go's dlopen'd function-pointer table
deliberately resolves neither chs_shutdown nor chs_set_default_settings, which
is what makes "a Library structurally cannot make that call" true rather than
promised.
The `unlisted` map is the other direction, and it is the half a contract usually
lacks: every public name a binding exports that the others do not need, with a
reason. It is what makes "a capability the table omits" a failure too.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JRrjwMiGbX4vWagJs38ndj
…s it found The check reads every spelling tests/parity/manifest.json assigns to the `go` column off the package's own syntax tree, and the shared VALUES off the real constants through a table of compile-time references — so a rename fails the BUILD, earlier and louder than a test, while the scan is what proves the manifest's spelling still matches, which a compiling package cannot. It runs in the bare copy. scripts/check-standalone.sh tars go/ into a scratch directory "with nothing around it", so there is no repository root there and no tests/parity/manifest.json to read; a check that skipped there would pass by doing nothing in the one gate built to stop exactly that. So the manifest is embedded (testdata/parity.json, //go:embed) and the one seam that opens — the copy drifting from the original — is asserted by python, ts and rust always and by Go itself whenever it can see the repository root. Measured in the bare copy: 51 tests ran, the parity ones among them, with no artifact and no registry. Two gaps it named, both additive: * `Registry.Libraries` did not exist. Python, TypeScript and Rust all answer libraries(), and spec/bindings.md §Version selection rule 2 governs "every ordered surface a binding exposes — Versions(), a libraries() list, the not-found error's 'have […]' text" by name — a rule written after two SDKs were found listing in directory order. Go had the rule and not the surface. It lists what is LOADED and loads nothing, which is what TypeScript and Rust answer; Python's eagerly opens the lazily-discovered lines, and opening 120 MB of artifact as the side effect of a listing call is not something a caller can undo. Noted for the report, not changed here. * `FilterOutcome` had no String. Outcome, Verdict and DefaultKind each had one, so a Go caller could render every verdict in the ABI except the filter call's own — and Python and TypeScript both carry that vocabulary as strings already. The ordering behind both is now one function: sortMinorLines and Libraries share minorSortKey rather than each carrying a copy of rule 2's numeric comparison. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRrjwMiGbX4vWagJs38ndj
Python resolves its column of tests/parity/manifest.json the way a caller would
— dotted paths against the real, imported `chtypes`, with an AST fallback for
the instance attributes a class cannot be asked about (`Library.version`,
`Schema.columns`, `Registry.search_path` are all assigned in __init__ and
invisible to getattr on the class). Values are compared after the same
normalisation a caller gets: str() of a StrEnum, int() of an IntEnum.
Nothing here constructs a Registry, so it runs in the artifact-free CI job — the
whole point, since parity is a claim about the API surface and not about
dlopening anything.
It found one thing on its first run, and it is recorded rather than fixed:
`Library` is the only lifetime object in registry.py that is NOT a context
manager. Registry, Schema, Filter and Block all are, and spec/bindings.md's
teardown row names "Registry.close() / Library.close(), context manager" in one
breath — so `with registry.for_version("25.8") as lib:` reads as though it should
work and raises TypeError. Adding __enter__/__exit__ is three lines, and it is
deliberately NOT done here: §Teardown measured that reopening after a full close
SEGFAULTS, so scope-based teardown on a Library makes the mid-lifecycle close
that section warns against the easiest thing to write. That is a decision for
the owner, not a gap to close quietly. TypeScript has the same shape — Symbol.dispose
on Schema, Filter, Block and Registry, and not on Library.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JRrjwMiGbX4vWagJs38ndj
…unnamed
The check resolves TypeScript's column of tests/parity/manifest.json against the
real package index at runtime, falling back to src/index.ts's own export list for
the type-only names that do not survive erasure. Nothing here constructs a
Registry, so it runs in the artifact-free CI job.
It found that TypeScript was the only binding with no runtime names for four
vocabularies the other three all name — and the odd thing is that this binding
already establishes the pattern itself: Format and Reason are `as const` objects
with the type derived from them. Outcome, Verdict, FilterOutcome and the default
kinds were string unions with nothing behind them at runtime, so a caller wrote
the strings by hand and a typo was a comparison silently always false. The six
docs/fetch.md §6 artifact codes were the same story beside an already-exported
CODE_UNSUPPORTED: reachable as `err.code`, matchable only by retyping the
literal.
Every addition is additive and every existing type is unchanged — each union is
now derived from its own const (`(typeof Outcome)[keyof typeof Outcome]`), which
is the identical type, so no consumer's annotation moves. `DefaultKind` is new
here and one member of it is load-bearing: EPHEMERAL is the column a gateway MUST
decline to preview rather than mispreview (spec/bindings.md §EPHEMERAL columns),
and a TypeScript caller had no name for it.
Not changed, and in the report instead: this binding renders a filter verdict as
'true'/'false'/'error'/'decline' where Go and Python both render the wire
character the C document carries ('t'/'f'/'e'/'d'). The four-state concept is at
parity and the fail-closed semantics are identical; the spelling is not, and
moving it is a breaking change to a published type.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JRrjwMiGbX4vWagJs38ndj
…tems Rust has no reflection, so presence is read from the crate's own source in the manifest's spelling grammar — `name` at the crate root, `Type::method`, `module::NAME`, `Enum::Variant`, `fetch::name` for the fetch module's files. The values come from a table of REAL references instead, which makes the two halves cover each other: renaming an item fails the BUILD before this test runs, and the scan is what proves the manifest's spelling still matches, which a compiling crate cannot. 147 spellings resolve; nothing here opens a Registry, so it runs in the artifact-free CI job. One gap, additive and symmetrical with the Go half of this change: `FilterOutcome` had no `as_str` where `Outcome` did, so a caller could render every verdict in the ABI except the filter call's own — and Python and TypeScript both carry that vocabulary as strings already. `Display` comes with it, as `Outcome` has. Two absences stay absences and the manifest now argues both: `Schema`, `Filter` and `Block` implement Drop, so the free-before-schema ordering the other three enforce at runtime is a compile error here and an inherent close() would be a second way to say it; and rows_export takes `export: Option<Format>`, so None IS CHS_EXPORT_NONE and a public constant for it would be a second spelling of the same sentinel. Not changed, and in the report instead: `DefaultKind` has no public renderer here — parse() is private and Other(String) carries an unrecognised spelling — so its five values are the one vocabulary Rust cannot be value-checked on. The manifest records that as value_check:false with the reason, rather than dropping the rows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRrjwMiGbX4vWagJs38ndj
Both gate scripts already refuse the quiet failure they were written for — a runner that collected nothing and exited 0. The parity contract opens a second shape of it: the check needs no artifact, no registry and no repository root, so there is no state in which it may legitimately be absent, and a parity test that was deleted, renamed or skipped would leave the census looking exactly as healthy as one that ran. So each arm now asserts it off the runner's own output, the way everything else here is asserted: check-standalone.sh reads the seven Go parity tests out of the -json census and names any that did not PASS; the rust arm counts them in cargo's own `... ok` lines; the ts arm in vitest's verbose per-test lines; and the python arm off pytest's collector, because `pytest -q` names no passing test — collected, plus no failures, plus a non-zero pass count, is them having passed, and these particular tests cannot skip. Measured, artifact-free: the Go gate runs 51 tests in the bare copy with the parity ones among them, and only the embedded-copy-matches-the-repository assertion skips there — by design, since that tree has no repository root, and python, ts and rust each assert the same equality. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRrjwMiGbX4vWagJs38ndj
…check
The document's own second paragraph says "Every language — go, python, ts, rust
— is a peer SDK over the same C ABI; no language is privileged", and its
object-model table then had three columns, with Rust described in Notes cells
("Rust: `lib.compile(ddl).compile()` (builder)"). Rust now has a column, and the
Notes it was living in are back to being notes.
Six rows are new, each one a capability all four bindings already had and the
table did not name — measured by asking the manifest which of its object-model
spellings the document never mentions: `libraries()`, the search path,
`has_compile_settings()`, the export channel on `rows`, `parse_block()` and
`compile_filter()`. `has_compile_settings` is the one worth calling out: an
artifact linked before `chs_schema_compile_with_settings` answers false, so a
caller that needs its declared profile honoured has to ask, and the contract had
never said so anywhere.
Three corrections where the document described something that is not true:
* The teardown row claimed Python's `Library` is a context manager and that
TypeScript's carries `Symbol.dispose`. Neither does — in both languages it is
the `Registry` (and `Schema`/`Filter`/`Block`) that carries the scope-based
form and the `Library` that does not. That turns out to be the reopening rule
further down the same section made structural, since a scope-based close on a
Library is exactly the mid-lifecycle teardown measured to SEGFAULT on the next
open, so the row now says it and says why.
* §Revision 3 and §Revision 4 both read "specified for the SDK cycle that
FOLLOWS the C surface" about `RowsExport`, the filter surface, the params
argument and the block surface. All four of those are in all four bindings.
The sequencing rule is kept — it is the useful part — and each section now says
the cycle landed rather than leaving a reader to infer the surface is missing.
* The compile row named `WithCompileSettings` and not `WithCompileMode`.
And the table is now checked. `test_the_manifest_and_the_spec_table_agree`
asserts that every object-model spelling the manifest requires is one this
document's TABLE ROWS actually name. Rows, not prose, and that is the whole
difference between a check and a coincidence: the first version searched the
whole document, and a negative control that renamed `validate_type` in the table
still passed, because a sentence elsewhere explains why validate_type alone is
insufficient. Scoped to the tables, the same control fails as it must.
The direction is deliberately one-way. The manifest is never derived from the
markdown, at test time or any other — a table is a layout, and a test that parses
one breaks on a reflow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JRrjwMiGbX4vWagJs38ndj
…ruff golangci-lint (go), Biome (ts) and an extended clippy [lints] table (rust) had no config in this repo before now; python already had ruff. Each is tuned as a curated allowlist rather than every available rule turned on, and every rule kept is verified clean on the current tree with zero source edits — a rule that would need touching binding source to satisfy is left off rather than silenced with scattered nolint comments (see the comments in each config for exactly which rules were dropped and why: mostly pre-existing errcheck/gosec/unwrap_used-class findings in fetch.go, ffi.ts and the CLI binary that are real but out of a lint-tooling change's scope). ts/package.json also gets a pnpm packageManager pin, for reproducible tooling versions between CI and a dev machine. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRrjwMiGbX4vWagJs38ndj
…script Neither ran here before. shellcheck itself isn't installed by this script — ubuntu-latest (this repo's only CI runner) ships it — but actionlint is pure Go, so it's go-install'd on demand at a pinned version into .bin/ (now gitignored), the same pattern used for misspell. actionlint runs with -shellcheck so it also lints every workflow's embedded `run:` blocks, not just the standalone scripts. File discovery is git ls-files '*.sh', not a hard-coded directory list, so a script landing anywhere in the tree — or a directory rename — doesn't silently fall out of coverage. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRrjwMiGbX4vWagJs38ndj
misspell -locale US alone is not sufficient and was almost shipped as the guard anyway: it lists pairs like "specialised"/"specialized" and "normalisation"/"normalization" in its own built-in dictionary, but its matching engine silently fails to fire on a handful of them regardless of whether the pair comes from the built-in list or a custom -dict file — confirmed by calling misspell's vendored Replacer directly. A spell checker that catches nothing looks identical to a clean repo, so scripts/lint-prose.sh now runs two independent mechanisms that must both pass: misspell -locale US (for its large, separately-verified-reliable set — colour, behaviour, licence, neighbour, modelled, serialise, and more), plus a from-scratch grep pass with explicit non-letter word boundaries (not \b — git grep -E doesn't support it and silently matches nothing) for the specific word family proven to slip past misspell's matcher. --selftest proves the gap and the fix on a throwaway probe file and runs as its own CI step, so the guard's own correctness is checked on every run, not just today. Scope is the whole tree — every tracked file except **/fixtures/fetch/** (generated, ed25519-signed; editing it breaks every signature) and LICENSE. Not a docs-only allowlist: all four SDKs' source, the header, examples, scripts and the root governance files are all in scope, per instruction — no exceptions anywhere in this repo. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRrjwMiGbX4vWagJs38ndj
Merges the parity workstream and reconciles it with the restructure.
"The four bindings give one answer" was prose plus discipline: a 60-row table
in bindings.md, 33 source files citing it in comments, and nothing that failed
when a binding drifted. Now there is one machine-readable manifest of 157
capabilities x 4 spellings (76 of them carrying a required VALUE), and each
language checks its own column in its own idiom — runtime resolution for
python and ts, go/parser for go because a cgo package cannot be reflected on,
a source scan for rust because no reflection exists.
137 of 157 were already at full parity. Five real gaps were closed, all
additive, no rename and no behavior change: Go gained `Registry.Libraries()`
and `FilterOutcome.String()`, Rust gained `FilterOutcome::as_str`/`Display`,
and TypeScript gained runtime constants for four enums plus the six
`CODE_ARTIFACT_*` codes — one of which matters more than tidiness, since
`DefaultKind.EPHEMERAL` names the column a gateway must DECLINE to preview and
TypeScript had no name for it at all.
The Go asymmetry I asked about is DELIBERATE and must stay: the dlopen'd
*Library has no SetDefaultSettings and no teardown because the function-pointer
table does not dlsym them, so a Library structurally cannot reach them.
Omitting the symbol is what BUYS that guarantee; adding it would remove the
thing the design exists for. Recorded in the manifest as an absence carrying
its own argument rather than as a gap.
It cannot pass by doing nothing, at three levels: manifest floors, a
resolved-zero guard in every suite, and a requirement that the value tables
cover the contract. It runs with no artifact — nothing in it constructs a
Registry — so it lives in the artifact-free CI job. And check-standalone.sh
tars go/ into a bare directory with no repo root, so Go embeds its own copy of
the manifest and the seam that opens is asserted four separate ways.
RECONCILING IT COST FOUR REAL BUGS, all from the branch predating the
restructure and the rename:
- the manifest and every value table named ReasonDefaultMaterialised
- the doc-agreement test read (REPO / "spec" / "bindings.md") — SEGMENTED,
so the same regex that fixed the literal missed it. Identical to the
fixture-path bug earlier today, in the same session, by me
- the merge reintroduced spec/ citations the sweep had already cleared
- ts/src/errors.ts and bindings.md conflicted; resolved by taking the
parity side wholesale and re-applying the restructure's transformations,
which the manifest-vs-table test then verified rather than my eyesight
Also fixed, flagged by the audit: python/src/chtypes/__init__.py declared
__version__ = "0.1.0" while pyproject declared 0.1.1, so the PUBLISHED 0.1.1
wheel reported itself as 0.1.0. The release workflow gates on the manifest and
never reads __version__, so nothing caught it. Now guarded by a test, and the
test was negative-controlled: forced to 9.9.9 it fails, naming both values.
Gates: go 157 assertions from a bare copy, python 169, ts 198 passed 4 skipped,
rust 145 passed 4 skipped. Artifact-free python 119 passed 19 skipped.
Nine items need a decision and are going to Eric, not resolved here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JRrjwMiGbX4vWagJs38ndj
dprint (Rust/wasm), not prettier: chosen per direction to move off Node tooling where a native alternative exists, and it turns out to fit better anyway — textWrap: "never" in dprint.json is a first-class formatter option here, not a side effect of a prose-wrap flag. Verified against every tracked *.md file in this repository before adopting it (not assumed): fenced-code-block contents come out byte-identical to the original with embeddedLanguageFormatting off (so a JSON example showing an exact indent=1 byte layout, or a Go/C snippet, is never silently reformatted), and every table's cell count is unchanged — with one exception, which is a pre-existing bug in the source, not a dprint defect: a table in the C ABI spec has an accidental blank line splitting it into a headerless orphan (around the chs_shutdown/chs_free rows), and both dprint and a from-scratch remark/prettier parse merge that into one corrupted row, because any strict CommonMark/GFM table parser resolves the ambiguity the same way. That line needs a manual fix independent of which formatter runs it. markdownlint (.markdownlint.json) carries none of WaveHouse's custom no-hard-wrap rule — that rule is hand-written regex over raw lines with no AST, and it has already silently corrupted content twice there (a pipe-less table, a short setext underline). dprint replaces it entirely. Both tools report real, pre-existing findings on the current tree (91 markdownlint errors, all 35 tracked *.md files not yet dprint-formatted) — see the `prose` CI job for why it runs non-blocking for now. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRrjwMiGbX4vWagJs38ndj
Nothing repo-specific to argue about here: per-filetype indent rules matching what's already on disk, Node 22 pinned to match ts/package.json's engines field and this repo's CI, and a Summary/Test plan/Related issues PR template matching what contributors already write by convention. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRrjwMiGbX4vWagJs38ndj
Six new jobs, all additive — nothing above gained a needs: on any of them,
and none renames one of the six existing job names branch protection
already keys on:
lint-go, lint-ts, lint-actions, misspell blocking (once someone adds
them to branch protection's required-checks list); each is clean on
this tree today via its own commit's verification.
prose, security non-blocking (continue-on-error)
prose stays non-blocking because this tree's markdown hasn't been through
a dprint fmt pass yet and two other efforts are already reworking large
parts of this repository's docs in parallel — blocking here would redden
every unrelated PR on debt this job can't pay down itself.
security (govulncheck, pip-audit, pnpm audit, cargo audit) stays
non-blocking on purpose: a scanner that reds `main` the moment a fresh
advisory lands against a pinned dependency, with no fixed version to
upgrade to yet, blocks every unrelated PR for a problem this repository
can't fix in the hour it happened. It still runs every push and PR and
still fails its own step loudly — only the required-checks gate is
skipped. All four scans are clean on the current tree as of this commit.
Every tool is installed directly at a pinned version (go install, pnpm
dlx <pkg>@<version>, uvx <pkg>==<version>, cargo install --locked) rather
than through a new third-party Action, so this needed no new `uses:`
entries and no new SHAs to pin beyond what ci.yml already carries.
CONTRIBUTING.md gets a short Linting section pointing at all of this.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JRrjwMiGbX4vWagJs38ndj
…four
Merges the lint workstream and reconciles it with the restructure, the rename
and the parity work.
MISSPELL IS NOT A SPELLING CHECKER YOU CAN TRUST ALONE, and the reason is worse
than I reported earlier. I said its US dictionary was PARTIAL. It is not: the
agent read misspell's vendored source and called its Replacer directly, and the
dictionary DOES contain pairs like specialised/specialized — the matching engine
silently fails to fire on some of them, whether they come from the built-in list
or from a custom -dict. So the fix I proposed (add -dict) would not have worked.
scripts/lint-prose.sh runs misspell AND an independent grep backstop, and
--selftest proves both halves: it builds a probe file, shows misspell missing
four of five, and shows the backstop catching five of five. That selftest runs
as its own CI step, because a spell checker that catches nothing looks exactly
like a clean repository.
Added: golangci-lint (Go had none), Biome for TypeScript (chosen over oxlint
because it also formats; formatting left off since the existing source is not
Biome-formatted), clippy [lints] for Rust, ruff already existed. Vulnerability
scanning for all four ecosystems — govulncheck, pip-audit, pnpm audit, cargo
audit — which this repo had in NONE of its four languages while SECURITY.md made
security claims. Non-blocking by design: a fresh upstream advisory nobody can
fix that hour should not redden main. actionlint + shellcheck, blocking.
.editorconfig, .nvmrc, a pnpm pin, a PR template.
dprint with textWrap "never" replaces the hard-wrap rule, per Eric: no prettier,
and specifically not WaveHouse's hand-written regex line-classifier. 40 files
reformatted, every table's row count verified unchanged before and after.
FOUR THINGS THIS NEARLY BROKE, each caught by looking rather than by a green
exit:
- dprint's config globbed **/*.md and excluded only build directories, so it
would have reformatted eight files under tests/fixtures/. Those are not in
SHA256SUMS, so signatures would have survived — but they must stay
byte-identical to what core's gen.py produces, which is what
fetch-fixtures-check compares. Excluded, and re-verified 84/84 identical
afterwards.
- docs/support.md is machine-written between markers, and dprint pads table
columns while the generator does not, so the two disagreed forever. The
generator now emits unwrapped prose (the same rule everything else follows)
and dprint leaves that one file to its owner.
- the parity doc-check asserted a table row LITERALLY, so column padding broke
it. It is about which columns exist, not their alignment; now
whitespace-insensitive.
- docs/reference/c-abi.md had a blank line INSIDE the function table, splitting
it into a table plus a headerless fragment. Pre-existing and independent of
tooling — dprint and prettier merely surface it by merging the halves into a
corrupted row. Fixed, and the whole tree scanned for others: none.
lint-prose.sh excludes exactly one file: itself. Its wordlist is British
spellings by construction and a lookup table cannot be reworded around the
problem. Everything else is checked with no exceptions — the CHANGELOG entries
were reworded rather than excused precisely so this stayed the only one, and
CONTRIBUTING.md now points at the script instead of duplicating its wordlist,
so the documentation cannot drift from the rule.
Gates: go 157 assertions, python 169, ts 198 passed 4 skipped, rust 145 passed
4 skipped; dprint, lint-prose, lint-actions and support-matrix --check all
exit 0; fixtures 84/84 identical to core's set.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JRrjwMiGbX4vWagJs38ndj
Eric's objection to the fix I shipped an hour ago is right: a test that two hand-maintained copies must match is jank. It catches the drift but still requires remembering both places, and the reminder arrives as a red build rather than as a thing that cannot happen. pyproject.toml is now the only place the version is written. `__version__` comes from importlib.metadata, so it cannot disagree with the package it names. Proven rather than asserted: setting pyproject to 7.7.7 and reinstalling makes the runtime report 7.7.7; restoring 0.1.1 makes it report 0.1.1. The test is deleted — there is no longer a second copy for it to compare against. Worth recording that this was ONE binding's problem, not four. Rust gets its version from Cargo at compile time if it wants it, TypeScript has package.json, Go has no manifest at all because the tag is the version — and none of the three declares a version constant today. Python was the only binding carrying a second hand-written copy, which is exactly why it was the only one that drifted and shipped 0.1.1 reporting 0.1.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRrjwMiGbX4vWagJs38ndj
… dprint
Eric asked why `prose` and `docs` were not required checks. `prose` had no good
answer: it was report-only because the tree was still hard-wrapped, which is a
reason to format the tree, not to exempt the rule. Formatted, fixed, now gating.
118 markdownlint findings, and the two biggest groups were TOOLS ARGUING rather
than defects:
- MD010 no-hard-tabs, 69 of them, every one inside a Go code block. gofmt
emits tabs; the rule is about prose indentation. Scoped with
{"code_blocks": false}.
- MD060 table-column-style, 14. dprint pads table columns and markdownlint
wanted them unpadded. Two formatters with opinions about the same bytes is
a fight nobody wins, so dprint owns tables and MD060 is off.
That left 35 real ones, all fixed: 28 fenced blocks gained a language (opening
fences only, tracked with a state machine so closers were untouched), three
bold lines standing in for headings became headings, a bare email got angle
brackets, a trailing period left a heading, and an indented code block inside a
numbered list became a fence at list indent.
One of the 35 was in GENERATED output: docs/support.md opened its block at h3
under an h1. Fixed in scripts/support-matrix.sh, not in the file, because the
file is machine-written and a hand-edit there is erased on the next run.
`docs` and `security` STAY non-blocking, and the distinction is not squeamish-
ness — it is that a required check should be a pure function of the diff.
`prose` is: nothing outside this repository can turn it red. `docs` compares
the committed support matrix against the live index, so it goes red when CORE
publishes a ClickHouse line, on somebody's unrelated PR, with no change to
their diff. `security` goes red when an upstream advisory is filed, likewise.
A required check that a third party can fail for you is a flaky required check,
and flaky required checks erode trust in every gate beside them.
Blocking now: abi, go, python, ts, rust, artifacts, lint-go, lint-ts,
lint-actions, misspell, prose. Non-blocking: docs, security.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JRrjwMiGbX4vWagJs38ndj
Two failures on PR #15 that passed locally, because `check-suite.sh` runs the SUITE while the CI jobs also run fmt and lint. Exactly what the PR workflow was put in for — these would have landed on main under the old push-direct habit. rust/tests/parity.rs: my path rewrites (spec/bindings.md -> docs/reference/bindings.md, and the segmented "spec","bindings.md" form) pushed two chained calls past rustfmt's width. `cargo fmt`. go/chtypes/parity_test.go: staticcheck flagged parser.ParseDir, deprecated since Go 1.25. Worth more than a nolint, because the deprecation reason applies here specifically: ParseDir ignores build tags, and this package has one that changes the answer — chtypes_linked adds a second API surface that is NOT part of the dlopen-only contract the parity manifest governs. The test already filtered it correctly by hand (behindLinkedTag), so the hazard was handled and only the deprecated call remained; it is now os.ReadDir + parser.ParseFile, with a guard that an empty file list FAILS rather than reporting a surface of zero capabilities as a pass. Every gate CI runs now runs clean locally: go 157 assertions, python 168, ts 198/4, rust 145/4, golangci-lint, cargo fmt, cargo clippy -D warnings, lint-prose, lint-actions, dprint, markdownlint — all exit 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRrjwMiGbX4vWagJs38ndj
EricAndrechek
added a commit
that referenced
this pull request
Sep 14, 2026
A REQUIRED check has been passing vacuously. The `python` job's lint step was
cd python && uv sync … && uv run ruff check src tests && uv run ruff format --check src tests
uv run python -c 'import chtypes; …'
and `bash -e` does not exit when a command inside an `&&` list fails — only
the command following the final `&&` is subject to it. So `ruff check` failed,
`ruff format` never ran, the list returned non-zero, the next line ran anyway,
and the step's status became that of the LAST command, which passes.
Measured, not reasoned: the job logs for a0a236c, f92e68e, 92c3360 and 2ff98c1
all report `python` = success while containing `E501 Line too long` lines —
eleven of them, then fifteen once ruff 0.16.7 added four F541 findings. The
gate printed its failures into its own log on every run and reported green.
Proved both directions before and after:
bash -e 'true && false && echo unreached\necho last' -> exit 0 (swallowed)
bash -e 'true\nfalse\necho unreached\necho last' -> exit 1 (fails)
The fix is one command per line. Fixing the gate alone would turn the required
check red, so the fifteen findings it should have caught are fixed in the same
commit: four F541 auto-fixed, eleven E501 wrapped by splitting string literals
at word boundaries with the concatenated text unchanged, then `ruff format`.
The python suite still passes, 168 tests.
None of the fifteen were introduced by my earlier commits — checked: the four
files involved were last touched by #15, #19 and the release, and the two
lines citing docs/guides/fetch.md already read that path before #22.
Also audited every other multi-line `run:` block across all five workflows for
the same shape. Two matched the pattern and are safe: release-python.yml's and
release-rust.yml's version-agreement steps put the `&&` inside a `$( )`
substitution, and the following line compares the result and exits 1, so an
empty value still fails.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
28 commits. Four workstreams — three delegated to agents in isolated worktrees, one done here — merged and reconciled.
Layout
spec/held 88 files of which 12 were markdown; the other 76 were a binary fetch-test corpus. One directory, two unrelated jobs, a name describing neither.Top level a visitor now sees:
.github docs examples go include python rust scripts tests ts.go/could not move — the published module path froze at v0.1.0.The ABI contract is demoted, not moved out. It stays public: a public Apache library whose normative contract lives in a private repo is the wrong shape, and the day someone writes a fifth binding they should not have to ask for the rules of a public interface.
Docs
22 pages under
docs/, all four languages as peers. The four binding READMEs cut from ~450 lines to ~85 as registry landing pages. Root README rewritten — install-to-usage order, live version badges from all four registries, a comparison table.Every snippet was run against real artifacts, not reviewed. That found four API errors that would otherwise have shipped, the sharpest being that Go's dlopen'd
*Libraryhas noSetDefaultSettingsat all.Linting and prose
golangci-lint(Go had none), Biome for TS, clippy[lints]for Rust,dprintwithtextWrap: neverfor the no-hard-wrap rule,actionlint+shellcheck,.editorconfig,.nvmrc, a PR template.Dependency vulnerability scanning for all four ecosystems — this repo had none in any language while
SECURITY.mdmade security claims. Non-blocking by design.misspell -locale USis not sufficient on its own, and not for the reason first assumed. Its dictionary contains pairs likespecialised/specialized; its matching engine silently fails to fire on some of them, from the built-in list or a custom-dictalike.scripts/lint-prose.shruns misspell and an independent grep backstop, and--selftestproves both halves — misspell missing four of five probe words, the backstop catching five of five. That selftest runs as its own CI step, because a spell checker that catches nothing looks exactly like a clean repository.91 British spellings that misspell missed are swept, including
canonicaliseininclude/chtypes.h— a file misspell called clean. Exactly one exclusion exists:lint-prose.shexcludes itself, because its wordlist is British spellings by construction. The CHANGELOGs were reworded rather than excused specifically to keep it the only one.Cross-binding parity
tests/parity/manifest.json— 157 capabilities × 4 languages, 76 carrying a required value — checked by each language's own suite in its own idiom. 137 were already at parity; five real gaps closed, all additive.This is what was missing. Every test in this repo asked "does this binding work?" The only cross-binding test was the golden set, which asks whether the four give the same answers, not whether they expose the same surface. Four bindings can pass all 32 golden cases identically with completely different APIs — which is what happened, for months, because the contract lived in prose that 33 files cited and nothing failed on.
Twelve divergences are recorded rather than resolved — see #13. A green parity suite means "the divergences are exactly the ones we wrote down," not "the bindings are equivalent."
Also
default_materialised->default_materialized: ClickHouse's parser rejects thesspelling with a syntax error. SDK-only; ABI stays at revision 4 and no artifact needs relinking — the reason is derived in the binding, not received from the artifact. BREAKING, marked in all four CHANGELOGs.python__version__is now derived from package metadata. It had drifted, so the published 0.1.1 wheel reported itself as 0.1.0. One source of truth, no assertion test.Verification
After merge
The new
lint-*andmisspelljobs are not yet required checks — a required context's name must match exactly or merges block forever on a check that never arrives, so they get added after this PR proves the names.prose,securityanddocsare non-blocking by design and should stay that way.Core is holding a header pull, five doc lines and a goldens run until this lands.
🤖 Generated with Claude Code
https://claude.ai/code/session_01JRrjwMiGbX4vWagJs38ndj