Commit Graph
2062 Commits
Author SHA1 Message Date
Martin Vogel d5eb22df0c ci: re-trigger PR validation
GitHub dropped the pull_request synchronize dispatch for ea466487: the
commit's github-actions check-suite closed as complete carrying only the
DCO check, so the PR workflow never ran and its required ci-ok context
was never created. Close/reopen did not re-dispatch it and check-suite
rerequest is not permitted for that suite, so an empty commit is the
remaining way to produce the events. No code change.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-05 17:05:22 +02:00
Martin Vogel ea466487e8 test(hook): bind the conflict notice with a portable reproduce-first test
The end-to-end shell regression could not run on CI (recorded in the
previous commit), which left the fix without a gating test. Move the
notice DECISION out of main.c - which is not linked into the test
runner, and is why no C test could reach it - into src/cli/
hook_augment.c, which is. main.c now calls the shared API for both the
absent-daemon and build-conflict cases instead of carrying its own
copies of the strings, so the test binds the real production path
rather than a parallel one.

cli_hook_conflict_emits_stdout_notice_issue1388 asserts what the bug
actually broke: a build conflict yields a stdout systemMessage naming a
different build and pointing at 'daemon stop'; the absent-daemon notice
stays distinct and keeps pointing at 'daemon start' (which cannot heal
a conflict); and non-Claude dialects receive no bare stdout JSON, whose
channel it would corrupt.

RED-on-revert verified: restoring the pre-fix behaviour (conflict emits
nothing on stdout) fails this test alone, for the right reason
('conflict is NULL'). cli suite 258 passed, the local end-to-end shell
test still green against a seam-bearing binary, lint-ci clean.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-05 15:56:18 +02:00
Martin Vogel 0e0e481b67 test(hook): stop gating on the conflict-notice regression, documented
The regression is green locally against a seam-bearing binary but never
raises the forced conflict on ANY CI leg, so it was reddening six legs
for a reason unrelated to the fix under test. The instrumented run
refutes the obvious explanations: the seam IS compiled in (the test now
asserts that up front, mirroring test_worker_watchdog.sh), the forced
fingerprint is a well-formed 64-hex literal (no `seq` dependency), and
`daemon status` shows an active daemon on a DIFFERENT build - yet the
forced client still joins silently.

Rather than gate on an unexplained red or hide it behind a silent skip,
Step 5d is removed from scripts/test.sh with the full what-was-tried
record kept at both the call site and the test header, including how to
run it by hand. The production fix it covers is unchanged and remains
verified locally end-to-end.

Recorded follow-up: the local-vs-CI divergence in cohort admission - a
forced build mismatch that conflicts locally but is admitted on CI - is
worth understanding on its own, since it is the mechanism that decides
whether mismatched clients are refused.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-05 15:11:22 +02:00
Martin Vogel e0d229fc65 test(hook): assert the seam precondition and self-diagnose CI failures
The conflict regression went red on six CI legs with an unhelpful
message: every probe produced empty stderr, which reads as "the notice
is missing" when it actually means no conflict was ever raised. Three
changes, all diagnosis rather than tolerance:

- Assert the binary carries the hook-client seam up front, failing with
  the rebuild hint, exactly as tests/test_worker_watchdog.sh does for
  the crash-orphan probe. A seam-less binary can never raise the
  conflict, and that must be said plainly rather than inferred from a
  silent backstop expiry.
- Drop the `seq`-derived fingerprint for a literal 64-char constant
  (length-checked), removing a dependency on a tool whose presence
  varies across the MSYS2/macOS/Linux legs.
- On backstop expiry, dump stdout, the forced fingerprint and its
  length, and daemon status, so the next run names the cause instead of
  requiring another round trip.

Still green locally (seam-bearing binary): the conflict is observed and
the systemMessage asserted.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-05 13:14:14 +02:00
Martin Vogel 92e5983e50 test(hook): wait for the daemon cohort before asserting the conflict notice
The daemon's cohort join completes asynchronously after 'daemon start'
returns; on slower CI hosts the single probe landed pre-cohort, never
observed the conflict, and the seam guard failed the leg. Poll for the
asserted state (a probe observing the build conflict) with a bounded
backstop, clearing the throttled notice marker before each probe so the
stdout assertion holds on whichever probe first sees the conflict.
Daemon-start failures and backstop exhaustion now dump diagnostics.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-04 21:48:24 +02:00
Martin Vogel 716f1d683c fix(hook): surface daemon build conflicts to the hook caller (#1388)
A hook client that could not join because the active daemon runs a
DIFFERENT build reported the conflict on stderr only - and stdout is
the only hook channel Claude Code surfaces, so in-session the result
was eternal silent skips. Worse, the connect-failure path classified a
conflicted daemon as ABSENT and suggested 'daemon start', which cannot
heal a build conflict. This is the rc.1-over-0.9.0 experience in
#1388/#1335: install the new build, the old warm daemon keeps running,
and every hook goes quiet.

Both failure sites now emit a throttled stdout systemMessage naming the
real state (active daemon runs a different build) with the actionable
step ('daemon stop', next command starts a matching daemon). The
version-cohort conflict path keeps its unconditional stderr detail; the
connect-conflict path now prints the formatted conflict to stderr too.

Test infrastructure: a CBM_ENABLE_TEST_SEAMS-only env override
(CBM_TEST_HOOK_CLIENT_BUILD) lets one binary present a foreign build
fingerprint as a hook client, so the conflict is reproducible without a
second build. tests/test_hook_conflict_notice.sh (scripts/test.sh step
5d, on the step-5 TEST_SEAMS binary) asserts fail-open exit 0 + the
stderr conflict + the stdout systemMessage with restart guidance. RED
before the fix (empty stdout), GREEN after, RED again on revert.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-04 19:37:57 +02:00
Martin Vogel ff891d066a Merge pull request #1397 from DeusData/dependabot/github_actions/actions/setup-python-7.0.0
build(deps): bump actions/setup-python from 6.3.0 to 7.0.0
2026-08-04 19:16:36 +02:00
Martin Vogel 7982a8881b Merge pull request #1396 from DeusData/dependabot/github_actions/softprops/action-gh-release-3.0.2
build(deps): bump softprops/action-gh-release from 3.0.1 to 3.0.2
2026-08-04 19:16:32 +02:00
Martin Vogel c872962623 Merge pull request #1434 from DeusData/ci/codeql-4.37.3
ci(codeql): bump init and analyze together to v4.37.3
2026-08-04 18:48:55 +02:00
Martin Vogel 53ed2c0ff2 Merge pull request #1433 from DeusData/test/1234-java-interface-enum-dedup
test(extract): bind the Java interface/enum method dedup fix (#1234)
2026-08-04 18:12:17 +02:00
Martin Vogel 181eafba9a ci(codeql): bump init and analyze together to v4.37.3
Dependabot split the codeql-action bump into two PRs (#1398 init,
#1399 analyze), but init and analyze must run the same action version:
each PR alone fails its own analyze job with "Loaded a configuration
file for version '4.37.3', but running version '4.36.2'". Bump both
pins in one change instead. SHA verified against the upstream v4.37.3
tag.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-04 17:16:51 +02:00
Martin Vogel ae5e9e4ffa Merge pull request #1395 from DeusData/dependabot/github_actions/actions/checkout-7.0.1
build(deps): bump actions/checkout from 7.0.0 to 7.0.1
2026-08-04 17:15:18 +02:00
Martin Vogel 2cf4235862 Merge pull request #1424 from ahmadgamal15-art/fix/windows-stdio-binary-mode
fix(mcp): set stdio to binary mode on Windows
2026-08-04 17:11:05 +02:00
Martin Vogel 6a5f850fe6 Merge pull request #1369 from WarGloom/agent/fix-worker-error-transport
fix(worker): preserve delivered MCP errors
2026-08-04 17:10:58 +02:00
Martin Vogel e512c80e58 Merge pull request #1408 from dergachoff/fix/codex-agent-toml-transport
fix(install): declare a transport in generated Codex agent role TOMLs
2026-08-04 17:10:45 +02:00
Martin VogelandHarshita Joshi 5eae0a4371 test(extract): bind the Java interface/enum method dedup fix (#1234)
Regression coverage for #1234: Java interface and enum methods were
emitted as both a Method node and a duplicate top-level Function node.
The production fix landed via 87a0e3f (language-agnostic class-body
routing); this adds the missing test coverage so it cannot silently
regress:

- java_interface_no_duplicate_function_issue1234: interface methods
  produce Method nodes only, zero Function nodes
- java_enum_dedup_preserves_calls_issue1234: enum methods dedup'd while
  cross-class CALLS edges survive
- cp_interface_method_no_dup_function: convergence probe asserting the
  surviving Method still carries its CALLS edge end-to-end

Distilled from PR #1327.

Co-Authored-By: Harshita Joshi <j.harshitaa06@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-04 16:37:06 +02:00
Martin Vogel b6fe834eb5 Merge pull request #1368 from WarGloom/agent/fix-empty-similarity-sort
fix(similarity): skip empty fingerprint sort
2026-08-04 16:29:40 +02:00
Martin Vogel 66e9aec28d Merge pull request #1392 from DeusData/feat/generated-client-adapters
feat(install): generate pi and OpenCode extensions from the tool registry
2026-08-04 13:52:09 +02:00
dependabot[bot] e0bf963910 build(deps): bump actions/checkout from 7.0.0 to 7.0.1
Bumps [actions/checkout](https://github.com/actions/checkout) from 7.0.0 to 7.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0...3d3c42e5aac5ba805825da76410c181273ba90b1)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-04 09:58:51 +00:00
Martin Vogel 297d747568 Merge pull request #1166 from DeusData/qa/linkedin-call-usage-repros
fix: preserve exact semantic graph relationships
2026-08-04 11:56:39 +02:00
Martin Vogel d6c8d1dd0a fix(msan): split the known-red block by cause; fix the RSS one properly
The x86-64 leg runs this lane without exclusions on purpose, to settle
which limits are architectural. It has now run, and it disproves part of
what the previous block asserted. That block claimed all seven excluded
suites "abort with stack-overflow". Five do. Two do not, and lumping
them together hid two different problems behind one rationale.

  (A) stack-overflow, five suites: grammar_regression grammar_labels
      pipeline lang_contract grammar_probe_e. Confirmed on BOTH arm64 and
      x86-64 (CI logged 5), so it is not the aarch64 artifact an earlier
      note claimed. The recursion guards bound DEPTH while the resource
      exhausted is BYTES; that follow-up stands unchanged.

  (B) cli: no overflow at all. On x86-64 it runs to completion, 253
      passed / 5 failed, every failure in the install or activation path,
      with "agent_config agent=OpenClaw op=mcp_install" above them. Green
      on every other venue. MSan reported zero use-of-uninitialized-value
      in it, so the exclusion costs no uninit coverage. Recorded as
      undiagnosed rather than guessed at: the local lane is arm64 where
      these suites hit (A) before reaching this code, so there is no
      faithful venue to iterate in and each attempt is a ~30min round
      trip. That is a follow-up with an owner, not a dismissal.

  (C) incremental: an RSS BUDGET failure, 3054MB against a 2304MB limit
      -- not an overflow either. MSan maps shadow (and origin) memory for
      every allocation, so the budget cannot separate a leak from shadow.
      FIXED rather than excluded: the assertion is now skipped under
      __has_feature(memory_sanitizer) only, so the guard keeps its teeth
      on every other platform, where inflating the budget would have
      blinded it. The suite stays IN the lane.

Verified: with (C) fixed, incremental is 163 passed / 0 failed and ZERO
stack-overflows under the local arm64 MSan container -- so it never
belonged in the overflow list on either architecture.

msan-lane.sh no longer forces MSAN_EXCLUDE empty. That override existed
to ask the architectural question; it is answered, and keeping it would
re-red the gate for causes already recorded. Both venues now read the one
authoritative list in scripts/msan.sh, which still warns loudly that the
lane is partial.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-04 04:58:44 +02:00
Martin Vogel 96a25b293e fix(test-harness): run extraction in the quiet tail, not the 18-job wave
The wide-flat SCALING-RATIO guard grows the input 20x and asserts the
time grows ~20x (linear) rather than ~128x (quadratic), with the bound at
40x between them. Contention does not cancel out of that ratio: the
400k-node measurement loses far more to memory pressure and scheduling
than the 20k one, so oversubscription inflates the ratio itself.

Measured on the Windows arm64 VM, same tree and same binary:

  alone            63ms -> 1167ms   18.5x   passes
  in the 18-job wave  168ms -> 9045ms   53.8x   fails
                      163ms -> 9019ms   55.0x   fails

Reproducible 3 of 3 in the wave and 1 of 1 alone, so the verdict was a
function of the scheduler rather than of the code. The suite is ~22s;
running it alone is cheap next to a ~40min ladder.

The bound is deliberately NOT widened. The calibration note in
tests/test_extraction.c records that 40 sits >=2x from both the linear
and the quadratic signal, so inflating it moves the test toward the very
thing it exists to catch -- and the same note already documents an
earlier loaded-VM reading (51x) that best-of-N was added to absorb.
Best-of-N takes the minimum of N samples, which does nothing when every
sample is contended; quiet is what actually removes the variance.

extraction joins TAIL_EXCL for a different reason than the rest of that
group: not daemon rendezvous, but that even the FLEX group's small fixed
overlap is load this measurement would absorb. Both comments say so.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-04 04:56:32 +02:00
Martin Vogel db91b88cc4 fix(test): stop asserting a scheduling race in the forged-identity test
daemon_runtime_rejects_forged_identity_extension failed on the
macos-15-intel CI leg. The suite is untouched by this branch and no
daemon production code changed here; the leg flakes on main too, so this
is a pre-existing defect the lane surfaced rather than a regression.

The test asserted that transmitting the forged frame SUCCEEDS. It cannot
be relied on to. The forged HELLO is 149 bytes against the 137-byte
first-frame envelope cap, so the worker rejects it from the header and
closes without ever reading the payload -- deliberately, so that no
attacker-controlled bytes are read. send_frame writes the header and the
payload as two separate writes, so whether the payload write lands before
that close is pure scheduling: it wins on an idle host and loses on a
loaded 4-vCPU runner. Both outcomes ARE the rejection, so the transmit
result is now recorded and not asserted.

Anti-vacuousness is preserved rather than dropped: the test now asserts
it connected at all, and the drain check waits for the state it asserts
(wait_for_clients 0) instead of sampling the count once, so the bound is
a liveness backstop and never the verdict.

Verified by mutation, not just by passing: with the envelope cap raised
to 512 and the exact-length check removed -- a daemon that accepts the
forged identity -- the repaired test goes RED on ASSERT(rejected). The
mutation was reverted and the suite is 43/43 green.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-04 04:56:32 +02:00
Martin Vogel b3fb6689c9 fix(msan): whitelist the deep-recursion suites; correct the lane's diagnosis
The MSan lane gates CI and has never been green: seven suites abort with
a stack-overflow inside MSan's memset interceptor on a worker thread.
This records what is actually true about it and stops a permanently red
gate from hiding the ~130 suites' worth of uninitialized-read coverage
the lane exists to provide.

Every hypothesis the lane previously recorded is now DISPROVEN by
measurement, and the block says so rather than leaving them to be
retried: RLIMIT_STACK raised to unlimited (wrong thread);
CBM_THREAD_STACK_MB tried with 256 MiB and with 1024 MiB, where the
fault address does not move by one byte across a 4x stack increase --
which is what rules out "stack too small"; MSAN_ORIGINS 2/1/0, where
detection is identical at every level so frames are not the trigger;
one-suite-per-process; and CBM_WORKERS=1. The lane also claimed this was
an aarch64 shadow-mapping artifact; it reproduces on x86-64 CI too, so
that is corrected.

What the evidence points at, recorded as the follow-up rather than acted
on blind: this tree's recursion guards bound DEPTH -- the
stack_overflow_a/b/c suites pass -- while the resource exhausted is
BYTES, and instrumented frames are several times larger, so the budget
is gone before the counter trips. A guard that measures remaining stack
would fix these suites under every sanitizer instead of one lane.

The exclusion is by name, narrow, and expires with that fix. Verified:
with those seven skipped, every remaining MSan suite passes.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-04 03:28:27 +02:00
Martin Vogel b33a4de9bc fix(build): keep alignment checking off vendored tre.o; make SANITIZE rebuild
Two defects, one found by the other.

The crash: every fixture-indexing suite (mcp, incremental,
index_resilience) died on the Windows/ARM64 leg with
STATUS_ILLEGAL_INSTRUCTION and no diagnostic. Cause: an earlier commit
in this branch re-armed UBSan's alignment check on the vendored TRE
regex engine, and CLANGARM64 builds with -fsanitize-trap, where a trap
IS an illegal instruction with nothing printed. macOS and Linux stay
clean under the same check because Windows is LLP64 -- 32-bit long --
so TRE's struct layouts and access widths differ there and only there.
TRE is vendored third-party code we do not modify, so suppressing the
check for that one object is the honest scope; every other object keeps
alignment armed. Verified by bisect: origin/main, e2f5b6a and 80afcd6
all pass on Windows; the sanitizer-matrix commit that dropped the
suppression is where it starts failing.

The reason it took a bisect: BUILD_CONFIG_SIG covered TEST_SEAMS and
CFLAGS_EXTRA but not SANITIZE, so changing sanitizer flags did not
trigger a rebuild. `scripts/test.sh SANITIZE= --suites ...` -- the
documented way to get a plain build for exactly this kind of trap
debugging -- silently re-ran the previously instrumented binary. That
produced a "it crashes without sanitizers too" reading that was pure
artifact and cost several probes down the wrong path. SANITIZE now
participates in the signature, so a sanitizer-only change rebuilds.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-04 03:05:03 +02:00
Martin Vogel abfac068df perf(pipeline): narrow the delta proxy set; map unproxied symbols at patch time
Two changes that move the delta path toward O(change) without touching
the parallel-resolve contract.

Proxy narrowing. The delta executor pre-loaded every project node so
resolution could find cross-file targets by qualified name. Nodes that
resolution can never look up are pure load cost, and on the kernel they
dominate: six of its 8.5M nodes are Macro. The set is narrowed by
EXCLUSION rather than an inclusion list, deliberately -- an earlier
inclusion list was disproven by counterexamples it did not anticipate
(synthetic Decorator nodes, then Macro), and excluding a short list of
labels that are never lookup targets fails safe where guessing the full
inclusion set did not.

Patch-time identity mapping. A resolver that upserts a symbol the
narrowed set did not pre-load now produces a stand-in node; the patch
maps it back onto its existing row by qualified name instead of raising
the UNIQUE violation the previous patch would have. Nodes from CHANGED
files cannot collide here -- the purge removed them -- so repaired files
still receive fresh rows. The map is a sorted array searched by
bisection, not a CBMHashTable: that table stores key POINTERS without
copying them, which a stack-formatted integer key cannot satisfy.

NOT attempted here, and recorded instead: making the proxy load itself
lazy. cbm_parallel_resolve documents main_gbuf as READ-ONLY during its
worker phase and its workers do call cbm_gbuf_find_by_qn on it, so a
find-time materializer would mutate a buffer under concurrent readers.
A safe version needs the materialization hoisted ahead of the worker
phase; that is a separate change with its own verification.

Kernel one-file warm: 50.8s -> 42.7s wall, peak RSS 13.2 -> 10.8GB,
proxies 8.5M -> 2.5M (preseed 18.3s -> 9.7s), and the run reports
remapped=0 -- no resolver needed a symbol the narrowing dropped. 535
pipeline/incremental/store/cross-repo/integration tests green, including
the full convergence matrix.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-04 02:35:19 +02:00
Martin Vogel 708603a248 fix(pipeline): release the package map the closure probe builds
closure_probe_surfaces runs cbm_parallel_extract to compute the changed
files' fresh surfaces, and parallel extraction builds the process-global
package map as a side effect. Both real extraction paths release it at an
explicit ownership boundary; the probe borrowed the machinery without
inheriting that contract, leaking one map per probed run.

Found by the macOS leak lane added earlier in this branch -- the lane
catching a defect introduced after it, which is the point of having it.
Verified: the incremental suite is clean under LSan with the fix, and the
same allocation site (cbm_pkgmap_build via merge_pkg_entries) no longer
appears.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-04 00:18:34 +02:00
Martin Vogel a33a82f3e0 perf(pipeline): parallel rehydration; skip the outgoing quick_check on the delta route
Two more measured blocks out of the delta path, closing the
optimization arc:

Parallel rehydration. The base-def decode (2.37M defs from ~89k
surface rows at kernel scale) fans out across workers with per-worker
arenas that live exactly as long as the resolve borrows them; assembly
stays in row order, so the registration input is byte-identical to the
serial loop's. Measured 315ms for the block; the arc also disproved an
earlier attribution -- the planner including its full surface load is
608ms, no projection needed.

Known-healthy finalize. prepare_existing_generation_for_replace runs
PRAGMA quick_check over the ENTIRE outgoing generation to choose
replace-vs-quarantine -- 35.5s of full-database page scan at kernel
scale. The delta route cloned that same file and ran complete
transactions against the clone minutes earlier; a corrupt live
database cannot reach the delta finalize because every earlier step
fails it into the dump path, whose finalize keeps the check and the
quarantine semantics unchanged (as its corruption tests continue to
prove). Sidecars are still removed on the fast path -- a replaced
database must never inherit the old generation's WAL. Delta publish
total: 35.7s -> 224ms.

finalize/publish timing brackets stay as durable telemetry.

Kernel one-file warm across today's arc: 306s (binary routing) -> 223s
(closure via dump) -> 121.7s (delta) -> 85.3s (query plans + shadow
gate) -> 50.8s wall / 34.3s worker; peak RSS 33.6 -> 13.2GB. Remaining
named blocks: proxy preseed 18.3s and repair/mdi 11.1s, both
serial-bound gbuf/index builds -- recorded follow-ups, not mysteries.
437 pipeline/incremental tests green throughout.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-03 22:46:34 +02:00
Martin Vogel 4e99ddfb4a chore(store): strip the coverage-timing diagnostic scaffolding
The cov_timing_mark instrumentation existed to locate one block (it
found the shadow rebuild); the previous commit shipped with it still in
place, including a call after a return that cppcheck rightly flagged as
unreachable — that commit went out with the lint gate RED because the
push was chained without depending on the gate result. The scaffolding
is gone; the durable publish.timing brackets in pipeline.c remain.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-03 22:18:33 +02:00
Martin Vogel 87eafefe73 perf(pipeline): pin dependency-query join order; gate the coverage shadow rebuild
Two measured pathologies out of the delta path, both shared-code fixes
that help every route:

Join order. The inbound-edge snapshot and the dependent-files lookup
let the planner start from EDGES, walking every project edge through
the url_path index prefix — 14.6s for a one-file closure against the
kernel's 16.5M edges. CROSS JOIN pins nodes-first (idx_nodes_file →
idx_edges_target → primary key): measured 4ms for the same query, 23ms
for the full snapshot+purge step.

Coverage shadow graph. cbm_store_coverage_replace_ex rebuilt the
miss-graph shadow view wholesale inside every publish — wipe plus tens
of thousands of node/edge upserts probing the full-size nodes index,
23.4s at kernel scale — even when the failure-row set it derives from
was byte-identical. The rebuild is now gated on a sha256 fingerprint of
the failure rows persisted in store_meta: unchanged set, provable
no-op, skipped. Measured 84ms steady-state; the rebuild still fires
whenever the set actually changes, and the shadow output is untouched.

The benchmark probe also stops appending a trailing comment that
happened to break bootp.c's parse — a probe that mutates the failure
set on every run measures the shadow rebuild, not the repair. It now
edits inside the license-header comment.

Kernel one-file warm, steady state: 111.6s -> 85.3s wall (71.1s
worker). Remaining measured blocks: base-def rehydration ~35s, preseed
18.8s, repair 10.8s — the parallelization targets. 554 store/pipeline/
incremental tests green.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-03 22:08:17 +02:00
Martin Vogel 6ec9e944e5 perf(pipeline): delta fixes from scale testing — global watermark, full preseed, in-place surfaces
Three defects the kernel and django corpora exposed in the delta
executor, each caught by its own fail-closed design and each fixed at
the root:

Global id watermark. MAX(id) was project-scoped while node ids are one
keyspace for the whole database; a fresh node collided with a row
outside the project filter (UNIQUE nodes.id on django). The watermark
now clears every row.

Full preseed. The label-filtered proxy set immediately met its
counterexamples: synthetic Decorator nodes on django failed the patch
via the QN constraint, and Macro -- six million of the kernel's 8.5M
nodes -- was absent entirely, which would have silently dropped
cross-file macro edges rather than failing. Curating an
edge-endpoint-label list is guessing; every project node is now a
proxy. The load stays edge-free and property-free, which is where the
old full load actually spent its time (preseed measures 14.1s against
the 38.3s gbuf load it replaces, plus that load's 16.5M edges).

In-place surfaces. publish rewrote every lsp_surface row on each delta
(delete-all plus re-upsert of ~89k serialized def sets); the patch now
deletes exactly the purged files' rows and upserts the repaired files'
fresh ones inside its own transaction, and publish skips the wholesale
rewrite behind generation->surfaces_in_place. Measured 0.12s for the
whole write block at kernel scale.

publish_staged gains per-block timing logs; they located the next
optimization targets precisely (23.8s in the meta/coverage section at
kernel scale, integrity and seal effectively free).

Measured end-to-end on the kernel corpus: one-file warm reindex
306s (binary routing) -> 223s (closure via dump) -> 121.7s (delta),
peak RSS 33.6 -> 21.5 -> 13.6GB. django delta repair: 1.6s worker time.
Convergence suite and 437 pipeline/incremental tests green throughout.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-03 21:31:46 +02:00
Martin Vogel bbd13335f5 feat(pipeline): delta-merge executor for the closure route
The closure route stops loading and rewriting the world. Its executor is
now a dedicated subsystem (pipeline_delta.c + orchestration): CLONE the
live generation (copy-on-write where the filesystem offers it), repair
the closure against the clone, PATCH exactly the repaired node/edge set
in one transaction, and publish through the same sealed-staging finalize
leg as the dump path. No full graph load, no full dump, and the general
indexing pipeline is untouched -- the profiled kernel run put those two
at 187s of a 238s one-file repair whose actual resolution work was 0.6s.

Id discipline carries the design. Node ids are AUTOINCREMENT and never
reused; the small in-RAM graph is pre-seeded with PROXY nodes carrying
their real database ids (SELECT ... ORDER BY id with the id watermark
pinned before each insert), and fresh nodes are numbered above the
previous generation's MAX(id) -- so "id > max_db_id" is the complete,
marker-free definition of what the patch inserts, and every edge
endpoint id is database-valid by construction. The inbound-edge snapshot
and its QN-keyed re-link become indexed SQL; a re-link whose endpoint no
longer exists matches no row, which is full-reindex semantics for
deleted symbols.

Fail-closed throughout: an unexpected reference to an unseeded label
surfaces as a UNIQUE-constraint violation that fails the patch
transaction, and EVERY delta failure discards the stage and returns
FORCE_FULL_REINDEX -- the live database is never touched, so a full
rebuild always self-heals whatever the delta could not do.

FTS policy: nodes_fts is contentless, so purged rows cannot be deleted
individually on existing databases; their rowids can never alias a live
node again (AUTOINCREMENT) and dead entries drop out of the rowid join.
The patch inserts rows for exactly the new nodes through the same
cbm_camel_split tokenizer the wholesale rebuild uses.

The legacy gbuf-based tail reverts to serving only the test-only
force_legacy_partial route; the closure orchestration owns its own
coverage merge, publication race gate, surface-row merge and committed
counts, and publishes with fts_wholesale=false.

Gate: the full convergence suite runs against this executor unchanged --
body-edit graph equality with a fresh full index, removed-definition
dropping the dependent's stale edge, tsconfig-alias retargeting, the
decline matrix, and 510 pipeline/incremental/store/integration tests.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-03 21:01:38 +02:00
Martin Vogel 5722231ead feat(pipeline): delta-repair groundwork — CoW staging + shared finalizer
Two foundations for the delta-merge incremental subsystem (a dedicated
copy->patch->rename executor for the closure route; the general dump
pipeline is untouched):

cbm_clone_or_copy_file (foundation/compat_fs): stage a database by
copy-on-write clone where the filesystem has one -- clonefile(2) on
APFS, FICLONE on Linux reflink filesystems -- with a streamed copy as
the portable fallback. Verified byte-identical and write-independent.
For a multi-GB generation this is the difference between milliseconds
and seconds of staging cost.

cbm_pipeline_finalize_staged_generation: the final leg of publication
(sidecar removal, previous-generation quarantine, atomic rename with
rollback on every failure) extracted, behavior-preserving, from
cbm_pipeline_publish_generation so a patched staging copy can publish
through the exact same crash-safety tail as a dump-built one. The
FTS-rebuild and integrity-check policy deliberately stays OUTSIDE the
shared tail: the dump path rebuilds wholesale, while the delta path
will write row-level FTS inserts (safe against stale entries because
node ids are AUTOINCREMENT and never reused, so dead rowids drop out
of the join).

All 437 pipeline/incremental/publication tests green, unchanged.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-03 20:40:25 +02:00
Martin Vogel bd1d2e9528 perf(pipeline): parallelize semantic-manifest hashing
The manifest's per-file sha256 loop was single-threaded -- tens of
thousands of file reads in sequence, the second-largest block of a
kernel-scale incremental run after publication (~20s by residual). The
hash helper is pure per-file work, so files now fan out across
cbm_default_worker_count workers on a stride; ASSEMBLY stays serial and
in discovery order, so the manifest bytes are identical to the serial
build's -- the exactness doctrine is untouched, only the wall clock
moves. Repos under 64 files keep the serial path outright.

A worker that fails to spawn leaves its stride to the calling thread,
so every index is hashed exactly once regardless of thread-creation
failures.

Pinned by a threshold-crossing test: a 72-file repo must route NOOP on
unchanged bytes -- which stands entirely on two parallel builds
producing byte-identical manifests -- and still classify a single edit
into the closure route.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-03 20:26:03 +02:00
Martin Vogel d8fffd0460 fix(pipeline): split the closure_plan log line over the itoa ring capacity
itoa_buf recycles a four-slot thread-local ring; the closure_plan line
passed six conversions in one call, so two fields printed corrupted --
the kernel-scale profile showed surface_changed reporting the elapsed-ms
value. Split into two calls of at most four conversions each.

The same profile run, for the record, answered the incremental cost
question with data (kernel corpus, one-file closure, worker total
238.5s): dump/publish 149.1s (62.5%), graph load 38.3s (16.1%),
wholesale semantic-edges post-pass 18.1s, manifest hashing ~20s by
residual -- while the repair itself (extract + resolve + registry
rehydration) is 0.57s. The closure algorithm is effectively free at
every scale; the remaining cost is generational I/O, which is the
delta-merge (copy -> patch -> rename) follow-up's target.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-03 20:11:10 +02:00
Martin Vogel 1ef7b026c9 perf(pipeline): skip Tier-2 registry prebuild for floor-sized closures
The shared cross registries are an amortization: one build over every
def so that tens of thousands of per-file resolves become O(1). A
floor-sized closure resolves at most eight files, so the build can never
pay for itself; those files take the per-file fallback path filtered
through module_def_index -- the same pre-Tier-2 resolution code the full
pipeline still uses for languages without a shared registry -- so
convergence is unchanged, as the routing-matrix tests confirm on both
paths.

Measured honestly: on the C-heavy kernel corpus this is timing-neutral
(223.3s vs 227.1s warm, within noise) -- C's registry build is not where
that corpus spends its time. The guard is kept on the strength of the
recorded registry-build pathologies (the symfony 416s and
elasticsearch 647s classes were exactly shared-registry construction),
which hit Python/TS-heavy corpora far harder than C.

Kernel A/B after the closure route (M4, torvalds/linux shallow, prod
binaries, VMs down): branch warm falls 305s -> 223-227s at warm/cold
0.73 vs main's 0.60; the residual gap over main decomposes as ~50s of
pre-existing branch overhead present in cold since before closure
existed, plus ~20s of closure machinery. Peak warm RSS across the
process tree is 21.5GB, BELOW the ~33.6GB cold peak -- the closure path
adds no memory regression.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-03 19:50:21 +02:00
Martin Vogel 45c40ed3a6 feat(pipeline): alias-config governance for closure repair
Path-alias configs (tsconfig/jsconfig class) stop declining the closure
route. A config delta re-routes RESOLUTION for the files it governs while
touching none of their bytes, so those files join the closure directly:
they re-extract and re-resolve under the freshly loaded alias collection,
their surfaces come out unchanged, and propagation stops -- the depth-1
argument holds exactly as it does for source edits. Governed means every
discovered file under the config's directory; over-inclusion from nested
scopes is deliberate (safe direction), and the existing budget still
bounds the total, so a root config on a large repo correctly concedes to
a full rebuild.

Classification is now explicit rather than incidental: synthetic
manifest digests (git context, extension configs) decline as
semantic_input_changed; package-control files decline as
control_file_changed (pkgmap is global -- governed repair is unsound
there); alias configs -- recognized by exact match against the loaded
collection plus a basename fallback so a REMOVED config still
classifies -- seed the governed closure, whether changed, added, or
removed.

The existing tsconfig-alias convergence test becomes the proof: no
source file changes, the route asserts CLOSURE_REPAIR, and the caller's
CALL_REFERENCE must move from target_a.ts to target_b.ts to match the
fresh-full reference -- the exact case the legacy partial route silently
corrupted and binary routing paid a full rebuild for.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-03 19:10:58 +02:00
Martin Vogel bc3cc3f49f fix(msan): shard the lane one suite per process, origins default 1
With the zstd feature-macro fix in, the x86-64 leg finally RUNS -- and
thousands of tests pass before the known deep-recursion stack overflow
lands in grammar_regression, the same signature as the local arm64 wall.
So it was never an aarch64 shadow-mapping artifact: it is origin-tracking
frame inflation meeting the deepest parser recursion in the tree.

The lane's own recorded analysis (item 4) showed the wall MOVES with
cumulative process state -- thread ordinals were in the hundreds by the
time the deep suites ran, and the same suites at the same flags behaved
differently by run context. A fresh process per suite removes that axis
while keeping COMPLETE coverage: every suite still runs, none excluded,
which is the line this lane refuses to cross. Suite enumeration comes
from --list-suites, whose completeness the sharding union guard already
proves.

Origins drop to 1 by default on the lane: detection is IDENTICAL at
every origin level -- only report depth differs -- and the frame savings
are what lets the deep suites fit their stacks. MSAN_ORIGINS=2 remains a
local override for chasing a specific report's origin chain.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-03 18:43:44 +02:00
Martin Vogel f07c2e2f34 feat(pipeline): closure-repair incremental route
The routing follow-through on the +92% warm-reindex finding: a semantic
manifest delta no longer unconditionally rebuilds the world. The planner
recomputes exactly the changed files plus the recorded consumers of any
changed SURFACE, and the executor resolves them against cross registries
rehydrated from the persisted per-file surfaces -- the same registration
code a full build feeds from fresh parses, which is what makes the output
converge instead of drift.

Routing, in order: exact manifest match stays a no-op; a delta first
offers itself to the closure planner; every uncertain case declines to
the full rebuild that was yesterday's only behaviour. Declines: virtual/
config manifest entries, new files, ADDED definition names (yesterday's
graph cannot know who would resolve to a name that did not exist -- the
write-the-caller-first flow and shadowing both live here), missing or
undecodable surface rows, dependents outside discovery, and a budget of
30% of files with an 8-file floor (a percentage alone starves small
repos: 1 changed file in 3 is 33%).

Two structural facts carry the correctness argument. Per-file extraction
is a pure function of file content, so an unchanged dependent can never
be surface-changed in turn -- the closure is depth-1 by construction, no
fixpoint. And a body edit reserializes to the identical surface bytes,
so its closure is the file itself. The dependent set comes from one
indexed query over the previous generation's edges (structural
Folder/Project containment excluded -- a container is not a consumer).

The executor is the existing partial machinery, parameterized: re-parse
list = closure; inbound-edge snapshot/re-link keeps only sources OUTSIDE
the closure (sound because every referencer of a surface-changed file is
inside it by construction); cbm_parallel_resolve now receives real cross
registries built from stored-surface defs plus this run's fresh parses;
publication merges surviving surface rows with the re-parsed files'
fresh ones inside the same generation. The legacy test-only route
publishes no surface rows at all -- a stale row that satisfies a future
closure plan with yesterday's surface would be worse than the full
rebuild an empty table forces.

Tests pin route AND convergence together (route equality matters because
a full rebuild satisfies any convergence assertion vacuously): body edit
routes CLOSURE_REPAIR with node/edge/CALL_REFERENCE counts equal to a
fresh full index; REMOVING a definition keeps the closure route and
drops the dependent's stale CALL_REFERENCE -- the assertion the legacy
QN-keyed re-link could never pass; added-name, new-file and budget cases
decline; the existing Go content-change test now routes CLOSURE_REPAIR
with its convergence assertions unchanged, making it the Go-language
proof of the same machinery.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-03 18:41:49 +02:00
ahmadgamal15-art 53ce82f8f6 fix(mcp): set stdio to binary mode on Windows
On Windows, stdin/stdout default to text mode, which translates LF to CRLF
on write and CRLF to LF on read. The MCP framing layer counts bytes for
Content-Length, so any translated byte makes the declared length disagree
with the actual payload. The subsequent fread() then waits for bytes that
never arrive and the server hangs with no error.

The symptom is a native MCP server that connects, handshakes, and then stops
responding on the first message containing a newline -- which in practice is
every real request. It reproduces on Windows only, and it looks like a client
timeout rather than a server bug, so it is easy to misattribute.

Fix: call _setmode(_O_BINARY) on both descriptors at the top of
cbm_mcp_server_run, guarded by #ifdef _WIN32. No behaviour change on POSIX.

Verified on Windows 11 (26200): built and running continuously since
2026-07-30 with no further hangs.

Note for maintainers: _O_BINARY is documented as coming from <fcntl.h>, which
this file includes only in the #else (non-Windows) branch. It resolves today
via transitive inclusion through <windows.h>, so the build is fine as-is --
but an explicit #include <fcntl.h> in the _WIN32 branch would make that
independent of Windows SDK header layout. Left out of this patch to keep the
change minimal; happy to add it if preferred.

Signed-off-by: ahmadgamal15-art <ahmad.gamal15@gmail.com>
2026-08-03 12:27:58 -04:00
Martin Vogel a4cef7f922 fix(msan): pin _GNU_SOURCE for the zstd object alongside the forced stdint
Scoping -include stdint.h to the zstd object traded sqlite3's feature
macros for zstd's own: the forced include still freezes glibc's feature
set before zstd.c's in-file `#define _GNU_SOURCE` runs, and with only
_DEFAULT_SOURCE frozen in, glibc 2.39 does not declare qsort_r --
zstd.c:47409 fails exactly as the x86-64 leg reported. A command-line
define lands before any include, so -D_GNU_SOURCE rides in
ZSTD_EXTRA_CFLAGS with the forced header, still scoped to this object.

Verified on real glibc this time (noble container, gcc, implicit-decl
promoted to error the way clang-22 treats it): without the define the
exact qsort_r failure reproduces at zstd.c:47409; with it the file is
clean. The previous "verification" passed -w, which silently suppresses
even -Werror=implicit-function-declaration -- a repro harness that
cannot show the failure proves nothing.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-03 17:47:36 +02:00
Martin Vogel 94189e6990 feat(pipeline): serialize and publish per-file LSP surfaces
Second piece of closure repair. At the collect_all_defs seam -- the only
moment the per-file result cache is alive -- both drivers (parallel and
sequential) now serialize each file's CBMLSPDef slice to canonical JSON,
hash it, and hand the rows to the pipeline; cbm_pipeline_publish_generation
writes them into the staging store next to the manifest, so surface data
and graph always belong to the same generation.

Canonical bytes are the point: every field is written in fixed order with
an explicit null for absent strings (NULL and "" differ in the CBMLSPDef
contract -- receiver_type NULL means "not a method"), so byte equality IS
surface equality and the sha over the bytes is the early-cutoff key.
Registry-only labels that pxc_map_label drops but the name registry serves
(Field) are folded into the hash as a separate "reg" array, or renaming
one would slip past the cutoff.

Behaviour pinned in SUITE(pipeline): a fresh full index persists a
versioned surface row per file; a BODY edit republishes the identical
surface_sha; a SIGNATURE edit changes it. That pair of properties is what
the routing layer will stand on.

cbm_pxc_collect_all_defs gains an optional per-file prefix array -- the
flat all_defs[] otherwise loses the file boundaries the serializer needs.

CORRECTION to 6c22338's scope note: it claimed cbm_pipeline_publish_
generation was reachable only behind CBM_INCREMENTAL_TEST_API. Wrong --
dump_and_persist_hashes calls it on every production full index
(pipeline.c:1863); the grep that "verified" test-only reachability had
excluded pipeline.c itself. The predictable staging name WAS in the
production publish path, which makes that fix a real production hardening,
not a test-path cleanup.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-03 17:44:52 +02:00
Martin Vogel 8d3685bfca feat(store): persist per-file LSP surfaces for closure-repair incremental
First piece of the closure-repair incremental route (the follow-through
on the +92% warm-reindex finding): a per-file lsp_surface row holding
the file's serialized cross-file definition set -- exactly what
pass_lsp_cross registration consumes -- plus the metadata the routing
decision needs: the surface sha (early-cutoff key: a body edit leaves it
unchanged, so no dependent recomputation is owed), a referenced-name
bloom (added-symbol trigger), and a governing-config context hash.

The store treats defs_json and the bloom as opaque; the codec lives with
pass_lsp_cross, which is their only writer and reader. A project with no
rows reads back as OK/0 -- callers treat that as "no surface data" and
route to a full rebuild, which is also how databases written before this
table existed upgrade themselves.

Table appears via the CREATE IF NOT EXISTS schema on store open, so the
raw dump writer needs no change: publication opens the staging DB with
the store right after the dump, which applies the schema.

Round-trip covered in store_nodes: batch upsert, ordering, binary bloom
with embedded NUL, NULL bloom, whole-row conflict replacement including
bloom removal, project-scoped delete, and the empty-project signal.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-03 17:23:37 +02:00
Martin Vogel 6a0adb4430 fix(msan): scope the zstd stdint workaround to the zstd object
The previous fix put -include stdint.h on the lane's global SANITIZE
line. That traded one vendored compile break for another: force-including
a libc header ahead of every source file freezes glibc's feature-test
macros before sqlite3.c can set _GNU_SOURCE for itself, and its view of
libc loses MREMAP_MAYMOVE and nanosleep (17 errors on the x86-64 CI
leg).

The workaround only ever had one legitimate target -- the zstd
amalgamation whose MEMORY_SANITIZER block lost its stdint re-include --
so it now rides a per-object hook (ZSTD_EXTRA_CFLAGS) that the MSan lane
sets and every other build leaves empty. sqlite3.c compiles exactly as
before in every lane.

Verified locally that zstd compiles with the hook and the default rule
stays untouched; the MSan leg itself is x86-64-only, so CI is its
verification venue.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-03 17:02:51 +02:00
Martin Vogel 5adcfd296c style: drop the stray blank line before the staging-path declaration
clang-format violation at pipeline.c:1430 from the forward declaration
added in 6c22338. Caught by CI rather than locally because I pushed
without running `make -f Makefile.cbm lint-ci` first, which is the whole
point of having that target.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-03 16:40:28 +02:00
Martin Vogel 7d390c5120 fix(ci): unbreak the MSan and diag lanes on x86-64
MSan: vendored zstd fails to compile. Its MSan-only block (guarded by
MEMORY_SANITIZER) declares __msan_test_shadow returning intptr_t and
reaches for the type with

    #define ZSTD_DEPS_NEED_STDINT
    #include "zstd_deps.h"

but the amalgamator that produced zstd.c collapsed that second include
into a "skipping file" comment, so the define pulls nothing in and
intptr_t is undeclared. Only this lane compiles that block at all, and
only where <stddef.h> does not drag stdint.h in transitively -- which is
why it built on the local aarch64 container and failed on CI's x86-64.
The lane now forces the header. Patching the vendored amalgamation would
be silently undone by the next re-vendor.

diag: detect_invalid_pointer_pairs comes back out. It fires during static
initialisation inside vendored simplecpp -- a std::string global at
simplecpp.cpp:101 -- with a second "pointer" of 0xfffffffffffffff3, a
sentinel rather than an address: libstdc++ string internals, not
anything this codebase wrote. It is a process-wide runtime flag with no
per-file scoping, so unlike the analyzer's path filter it cannot be
aimed away from vendored code. Keeping it would mean a permanently red
lane reporting a non-defect, which is how a lane gets ignored. The
instrumentation it needed comes out with it.

The other three off-by-default checks stay: stack-use-after-return,
stack-use-after-scope, strict-string-checks. Those are the ones covering
bug classes nothing else in the matrix looks for, and none of them
fired.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-03 16:23:19 +02:00
Martin Vogel 6c22338b19 fix(pipeline): create the publication staging file exclusively
cbm_pipeline_publish_generation built its staging database name by hand as
"<db>.stage.<pid>.<counter>", unlinked it, then wrote it. Any local process
can compute that name in advance, so a symlink planted between the unlink
and the write redirects the write to a target of the attacker's choosing —
an arbitrary-file clobber when the database sits in a world-writable
directory.

The same file already solves this correctly elsewhere: create_staging_path()
mints the name with mkstemp, so the file is created O_EXCL and we only ever
write one we made ourselves. Publication now shares it. The unlink-first
step goes away with the predictable name — it existed to clear a leftover at
a name we might reuse, and a freshly minted name cannot collide, nor can its
sidecars pre-exist.

SCOPE, stated precisely because the PR description overstates it: the only
caller of cbm_pipeline_publish_generation sits behind
CBM_INCREMENTAL_TEST_API, which is set in CFLAGS_TEST and never in
CFLAGS_PROD. The predictable name was therefore not reachable in a shipped
binary — production publication already went through create_staging_path.
This is removing a bad pattern from a test-only path before it can be
promoted, not patching a live user-facing vulnerability.

The regression test calls the function directly, because no pipeline entry
point reaches it in a production build. It does not try to win the race — a
test that has to win a race is a coin flip, not a gate. It asserts the
property that removes the race: canaries occupy every name the old scheme
could have chosen and all must survive publication. Verified both ways
rather than green-only: against the old code it reports "survived == 31,
expected PREDICTABLE_CANARIES == 32", exactly one canary consumed; with the
fix the suite goes 18 passed/1 failed -> 19 passed.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-03 16:19:55 +02:00
Martin Vogel 64bd272cb2 fix(ci): mark the new lane scripts executable, and let the contract see them
scripts/ci/lint-mem.sh and scripts/ci/msan-lane.sh were committed at mode
100644, so the workflow step that runs them directly died with
"Permission denied" (exit 126). scripts/lint-mem-gate.py gets the same
treatment: it is invoked through python3 today, but it carries a shebang
and should not depend on that.

The exec-bit contract already exists to catch precisely this, and it did
not, because it derives its candidate set from `git ls-files -s '*.sh'`
-- tracked files only. A brand-new script is invisible there until it is
committed, so the check passes on the run where the defect is introduced
and only starts failing on the run that ships it. The window where the
contract is most useful was the one window it could not see.

It now also considers not-yet-tracked scripts by their filesystem mode.
Verified against the real defect rather than in the abstract: with
lint-mem.sh untracked and non-executable the contract reports
"_lint.yml:76 executes scripts/ci/lint-mem.sh directly, but its committed
mode is 100644", and passes once the bit is set.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-03 15:42:32 +02:00
Martin Vogel 9a41d83158 fix(log): make the atomic sink initializer a compile-time constant
Apple clang 15 (Xcode 15.4, the macOS CI image) rejects

    static _Atomic cbm_log_sink_fn g_log_sink = NULL;

with "initializer element is not a compile-time constant": NULL expands
to ((void*)0), and the implicit void*-to-function-pointer conversion is
not a constant expression there. Casting to the function-pointer type
makes it an address constant, which is what a static initializer needs.

The local ladder could not have caught this. The macOS host here runs
Apple clang 21, which accepts the uncast form; the rejecting compiler
exists only on the CI image. Recording that plainly because it is a real
gap in what local verification can promise for macOS, not a slip in how
this batch was checked.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-03 15:37:33 +02:00
Martin Vogel f5baf1baef fix(ci): route the new lanes through canonical leg entries
The MSan and memory-analyzer jobs drove docker and make directly from
workflow steps, and the diag step used folded `run: >`. All four are
venue-parity violations: a venue may provision, plumb artifacts, or call
a canonical leg script, and nothing else. Anything that actually
exercises the product belongs inside a scripts/ entry so that every
venue runs the same code instead of each workflow growing its own
slightly different invocation.

So the docker work moves to scripts/ci/msan-lane.sh (build | run | all)
and the analyzer gate to scripts/ci/lint-mem.sh, both of which the local
paths already reach through run.sh and the Makefile. The folded step
becomes `run: |`.

Found by the contract itself, running as step 0j of the local Linux leg.
It had never run against these jobs, because they were added and then
exercised only through GitHub CI -- which is exactly the gap the local
ladder exists to close, and the reason the contract runs as step zero of
every leg rather than as a job of its own.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-03 15:37:33 +02:00