The READS/WRITES resolvers and the USAGE registry fallback hand bare
reference text to the short-name registry, which contains Field nodes -
so once Go struct fields exist (#1935), every Go local err := ... binds
whichever struct field is named err, project-wide: 21308 USAGE and 5191
WRITES onto Go fields on the measured repo, top target a test struct's
field T collecting 3013 edges. In Go that binding is impossible by
construction: a field is only reachable through a selector expression
(x.f), and selector references resolve on the LSP path - every
Field-targeted reference edge in the census carried dot-less text.
Add cbm_go_suppress_bare_field_ref() next to the #1928 predicate and
consult it at the same four sites (both READS/WRITES resolvers, both
USAGE registry fallbacks): drop the bind when the file is Go, the
target label is Field, and the reference text has no '.'. Go-gated
because C#/Java/C++/Python method bodies legitimately reference their
own members bare (cp_reads_writes_cs_static_field pins that shape).
Field-validated on the #1940 stack: USAGE onto Go fields 21308 -> 0,
WRITES 5191 -> 0; the only remaining field-targeted edges are 2466
CALLS, which are #1906/#1907's selector-guard territory. Reproduce-
first pipeline probes (sequential + parallel twins) were RED on the
stack without this commit.
Fixes#1942
Signed-off-by: Ilya Brykau <ilya.brykau@orca.security>
Dependabot split this bump across two PRs -- #1898 bumps init, #1900 bumps
analyze -- but codeql-action requires every step in a workflow to run the
same version. Either PR alone produces a mismatch and analyze fails with:
Loaded a configuration file for version '4.37.4', but running version '4.37.9'
so neither can be green on its own, and merging either would leave main's
CodeQL job broken for every subsequent PR. Bump both pins in one commit.
Pin verified: cdf488f595d80d6e07e03d4674febd5ab45fa938 is the commit that
the annotated tag v4.37.9 dereferences to in github/codeql-action.
Supersedes #1898 and #1900.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Reproduce-first probe for the two fixes on this branch: RED without the
descend fix (count_defs_with_label(r, "Field") == 0, expected 3), GREEN
with it. Asserts the three named fields with their declared types in
return_type, the absence of the blank identifier, and that interface
members keep extracting exactly as before.
Signed-off-by: Ilya Brykau <ilya.brykau@orca.security>
USAGE, WRITES and READS edges resolve through the same short-name
registry as CALLS but never consulted the #725 cross-language guard.
On a Go tree with eBPF C probes every Go identifier spelled like a C
one produced a reference edge into the C file: 31.5% of all WRITES on
the originally measured repo crossed the Go->C boundary, led by dozens
of Go test locals named event writing a C probe's automatic variable.
Add cbm_suppress_cross_language_ref() - the reference-edge analog of
cbm_suppress_cross_language_suffix_match - and consult it on BOTH
resolvers of each edge type: the sequential pass (pass_usages.c
resolve_usage_edges registry-fallback branch, resolve_rw_edges) and
their parallel twins (pass_parallel.c resolve_file_usages,
resolve_file_rw). The sequential-only version of this change left 344
Go->C WRITES alive on a ~1150-file repo because large repos resolve
through pass_parallel.c - the field census caught it, and the
parallel-twin test now pins it.
Unlike the CALLS guard the predicate takes no strategy parameter: a
reference edge carries no import-closure evidence, so every registry
strategy is a bare-name guess across a boundary. LSP-backed semantic
references resolve before the fallback and are unaffected. JS/TS stay
one family, and C/C++ count as one family too (.h maps to
CBM_LANG_CPP, so a .c file referencing its own header is not a
boundary).
Field-validated on the ~1150-file Go+C repo: Go->C/C++ WRITES 835->0,
USAGE 1545->0; C->Go 15/90->0; 6432 reference edges dropped in total,
every one cross-language (Go->.json 3034, Go->.hpp/.h 1612, Go->.c
746, Go->.sh/.yaml/.yml 443, ...) and none same-language. CALLS and
IMPORTS totals are byte-identical to main.
Fixes#1928
Signed-off-by: Ilya Brykau <ilya.brykau@orca.security>
A Go import path names a package - never a function, method, field or
another language's build target. Strategy 1 resolves every correct Go
import (module path -> the package's Folder node); when it misses the
import is external and the correct result is NO edge. Two name-guess
fallbacks violated that:
- Strategy 3's language-agnostic symbol-name fallback matched the
path's last segment against any project definition of the same
simple name and returned the lexicographically smallest survivor:
import "os/exec" bound a test harness's exec() method (52 edges on
the measured repo), two imports bound a Function extracted from a
Makefile. 89 of 2295 Go IMPORTS edges (3.9%) were false; 27 of those
are the import "C" case (#1926/#1931), the remaining 62 this.
- Strategy 1b's sibling-file resolution admits symbol labels through
import_targetable_label, re-creating the same bug one directory
closer: with Strategy 3 gated, the field census still bound one
os/exec import to a same-package exec() method.
Gate both on the importing file's language through a new pure
predicate, cbm_import_symbol_fallback_allowed(): false for Go, true
for everything else - member-importing languages (Python, Java, Rust
use crate::ops::helper) legitimately need the symbol fallback, and
build/markup grammars (SCSS partials, Meson subdir, Pony use) the
sibling one. Strategies 1, 2 and 4 are untouched.
Reproduce-first test ei_go_import_never_binds_symbol asserts the
IMPORTS relation EXACTLY (an internal package import stays; neither
the cross-package nor the same-package decoy may be bound): RED
count=2 expected==1 before each gate, GREEN after. Adds
ei_edge_count_is, the exact-count sibling of ei_edge_present, since a
floor cannot catch a fabricated extra edge.
Field-validated on a real ~1150-file Go+C repo: Go IMPORTS by target
label went Folder 2206 / Method 85 / Function 4 on main to
Folder 2206 / Method 0 / Function 0 with the fix; all 2206 correct
package-Folder edges survive.
Fixes#1934
Signed-off-by: Ilya Brykau <ilya.brykau@orca.security>
A RETURN could name a variable the WITH before it dropped. The query ran,
exited clean, printed a column for that name, and filled it with empty
strings. Nothing said anything was wrong.
That silence is the harm. A reader sees a column of nothing and reads it as
"the graph holds no such data". The true answer is "your query named
something that is out of scope". For a code graph, a false "nothing calls X"
is the worst answer it can give.
The check runs on the parsed query rather than on the run-time bindings, and
that split is the whole point. It asks whether the query DECLARED the name,
which is a different question from whether a row happened to bind it. So the
existing convention is untouched: an OPTIONAL MATCH target that matched
nothing is still declared, still legal, and still projects "".
Two placeholders are skipped, because neither names anything the query
declared: count(*) stores "*", and a CASE expression stores "CASE".
A query with more names than the guard can model skips the check rather than
guessing. A wrong refusal costs the caller a working query, which is worse
than the silence this removes.
Same failure shape as #373, and the same answer: say so out loud.
Fixes#1919
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
Fixes the 2026-08-29 daemon zombie class: a client disconnected while its
application request was in flight, the request never observed cancellation,
and runtime_worker_finish joined the request thread with no deadline. The
disconnect path wedged forever, the generation stayed formally RUNNING, and
a dead process held the endpoint pipes and UI port for nine hours while
every new client timed out with no diagnostic.
Four changes:
- runtime: the disconnect-path reap of an in-flight application request now
waits at most RUNTIME_ABANDONED_REQUEST_JOIN_TIMEOUT_MS (30 s), then logs
daemon.application_request_unresponsive (peer pid, request token) and
fail-stops the process. The kernel releases every native claim and the
next client starts a fresh generation. Test seams expose the ceiling and
replace the terminal stop with a recordable hook.
- diagnostics: a transport connect that reaches a live process but gets no
valid answer now resolves the holder's kernel pid
(GetNamedPipeServerProcessId / SO_PEERCRED). `daemon start` classifies a
mute holder as RESERVED (no doomed competitor spawns) and names the pid
in its timeout message; `daemon status` prints "not responding (endpoint
held by pid N)" with recovery guidance instead of "not running".
- frontend: the maintenance monitor's quiet-state poll drops from 10 ms to
250 ms. Each probe revalidates the whole ancestor directory chain on
Windows; at 100 Hz that burned ~20% of a core for the life of every MCP
session (7762 CPU-seconds on one 9.5 h session). Detection latency spends
0.25 s of the 15 s activation drain budget.
- version_cohort: the healthy fresh-claim record is renamed from
"claimed_unheld" (misread as a stale-claim anomaly during the incident
diagnosis) to "claimed_fresh" with prior_holder=none.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: OhOkThisIsFine <102485413+OhOkThisIsFine@users.noreply.github.com>
daemon_runtime_process_fingerprint_never_hashes_replacement_path (POSIX
variant) copied /bin/cat to a temp file named "image" and executed the
copy. On Ubuntu 25.10+/26.04 and any other uutils system, /bin/cat is a
multi-call binary: a copy invoked under the name "image" prints
"coreutils: unknown program 'image'" and exits 1, so the fixture died at
ASSERT(setup). Reproduced on WSL Ubuntu 26.04 (uutils coreutils 0.8.0).
The Windows variant of the same test already avoids this class of donor:
it copies the test runner itself and parks it in a
__cbm_runtime_image_holder argv mode. The POSIX variant now does the
same. The holder mode gains a POSIX branch that blocks reading stdin
until EOF — exactly the behaviour the cat donor provided — and
runtime_test_spawn_blocked_executable passes the holder argument, so its
target must now be a copy of this runner (it has exactly one caller).
The /bin/echo replacement donor is gone too: it was only ever hashed,
never executed, and on a uutils system cat and echo can be the same
multi-call bytes, which would have broken the fixture's
original != replacement precondition. The replacement is now a small
data file, mirroring the Windows variant.
No signing change is needed on macOS: the image copy stays
byte-identical to the runner, and the identical-copy fixture already
proves such a copy executes.
daemon_runtime suite on WSL Ubuntu 26.04: 44 passed / 0 failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: OhOkThisIsFine <102485413+OhOkThisIsFine@users.noreply.github.com>
The reconcile_basis marker was never written on Windows, so all six
build_trusted_artifact_repo() tests plus artifact_export_marks_clean_basis
failed there (16 passed, 7 failed) while every Linux and macOS leg was green.
Root cause is a stamp-encoding mismatch, not shell quoting. file_hashes rows
are stamped by the semantic manifest via cbm_path_info_utf8(), which on Windows
derives mtime_ns from ftLastWriteTime -- 100-nanosecond FILETIME ticks.
db_hashes_match_disk() re-stat()ed those same files through _wstat64, whose
st_mtime carries WHOLE SECONDS, and compared the two. The values can only agree
when a file's write time lands exactly on a second boundary, so the trust check
rejected the very first row on every export, on Windows and only on Windows.
On POSIX both paths read st_mtim nanoseconds and agree, which is why the split
was clean along platform lines.
The disk side now reads through cbm_path_info_utf8 -- the same function that
wrote the rows -- so there is no encoding left to keep in sync. Its
is_regular/is_symlink guard preserves exactly the symlink refusal
reconcile_stat_no_symlink provided.
reconcile_restamp_rows deliberately keeps art_stat_mtime_ns: a restamped row is
read by the incremental classifier and by check_index_coverage, both of which
stat() and encode whole seconds on Windows. The asymmetry is now pinned with a
comment at both sites so neither is "unified" into breaking the other.
Diagnostics, which is why this took two wrong diagnoses to find:
- Export's four clean-basis preconditions were one short-circuiting && chain
collapsing to a single bool. They are now evaluated by reconcile_basis_trusted,
which records the first failing gate (head_unresolved / head_not_hex_oid /
tree_not_clean / db_hashes_differ_from_disk, the last carrying the offending
path and BOTH stamps) and logs it as artifact.reconcile_basis_omitted. The
reason is recorded rather than recomputed on demand: export's own
ensure_gitattributes leaves an untracked .gitattributes behind, so any later
evaluation answers tree_not_clean whatever the real cause was.
- build_trusted_artifact_repo() had six `return NULL` exits and reported none of
them; CI could not tell a failed `git commit` from a missing marker. Every exit
now names its step and carries the number that decided it -- git exit status,
pipeline rc, or export's recorded blocker -- surfaced through
BUILD_TRUSTED_REPO_OR_FAIL at the call site so file:line still points at the
failing test.
Verified locally on macOS by emulating the Windows encoding: truncating the
disk-side mtime to whole seconds reproduces the CI signature exactly (16 passed,
7 failed, same seven sites), and the new diagnostic names it as
`db_hashes_differ_from_disk path=a.rs reason=mtime_differs stored_mtime_ns=...
disk_mtime_ns=...`. Restored, the artifact/pipeline/incremental/mcp/index_format/
daemon_bootstrap suites are green and make lint-ci passes.
Co-authored-by: moofone <35233167+moofone@users.noreply.github.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
clone_to_b() cloned the source repo but never configured user.email or
user.name on the result. A clone does not inherit the source repo's LOCAL
config, and CI runners have no global identity, so every commit made in the
clone exited 128 -- surfacing as:
FAIL tests/test_artifact.c:734:
runf("git -C ... add -A && git -C ... commit -qm edits") == 32768, expected 0
32768 is 128 << 8. It passed locally only because developer machines carry a
global git identity; the Linux CI shards do not.
git_init() already configures identity for the source repo (:574-575); the
clone needs the same treatment. This is the defect originally diagnosed on
#868 as "git identity in clone_to_b" -- correct for the POSIX legs, and
separate from the cmd.exe quoting bug that caused the Windows failure.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Every ADR fixture in the tree used \n, so nothing exercised a CRLF-stored
document — and CI could not have found this, because the fixtures are LF
whichever platform runs them. A red would only have appeared once a real
user's CRLF ADR met this code.
The exposure is specific to splicing. The old rebuild normalised everything
on the way out, so line-ending confusion was invisible; a byte-span
replacement can miscount a separator run, and byte-identity is the property
the splice design exists to provide.
Two of the three paths were already correct, and now say so in tests:
- Replacing a section preserves CRLF exactly. The walk-back over a trailing
break run steps across both '\r' and '\n', so it cannot cut mid-pair.
- Heading matching is ending-agnostic: "## PURPOSE\r\n" locates the same as
"## PURPOSE\n", because the scanner trims '\r' from the name. Trimming and
matching were different claims and only trimming was pinned.
Appending a new section was wrong. It counted trailing '\n' characters, so a
document ending "...Foo\r\n" was read as having one break but then had a
BARE '\n' appended, and the "## NAME" line it wrote used '\n' too — leaving a
mixed document that neither the author nor the tool asked for. A CRLF file
ending in a blank line also gained a spurious extra break.
Appends now count a "\r\n" pair as one break, and write the separator and
heading line using the document's own ending.
The contract, now that a document can be mixed:
- Bytes that already exist are never rewritten, so each line keeps whatever
ending it had. This follows from splicing and is what the acceptance
property already guaranteed.
- Text this code writes itself — the separator and the "## NAME" line —
follows the document's majority ending, LF on a tie.
- A caller's body is inserted verbatim. Rewriting bytes a caller supplied
would be the same silent modification this whole change set removes.
Co-authored-by: andis777 <24672074+andis777@users.noreply.github.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
`CBMDefinition.base_classes` carries the SOURCE SPELLING of each base
("Base", "django.db.Model"): extraction strips keywords and generic
arguments, but it cannot know where the name is declared. The Python and
TS cross-file registrars, however, consume `CBMLSPDef.embedded_types` as
fully-qualified names — py_lookup_attribute and ts_lookup_member feed each
entry straight into cbm_registry_lookup_type.
An unqualified spelling therefore matched nothing declared in ANOTHER
file. `class Child(Base)` in child.py never saw Base in base.py, so a
call to an inherited method through a typed receiver found no member and
fell through to the weak textual cascade, where the receiver-aware guard
(#592/#606) correctly kills it. The member-lookup walk over
embedded_types was already there in both languages; only the names it
walked were unusable across files.
cbm_pxc_collect_all_defs now resolves each base spelling to a project QN
using exactly the inputs pass_semantic uses to draw its INHERITS edge:
the project registry, the declaring module, and the file's import map.
Two properties follow. The LSP's inheritance view is the same relation
the graph records, so the two cannot diverge. And the binding is import-
or same-module-backed rather than a short-name guess, so the CALLS edge
it enables is a supported fact that the weak-member guard keeps.
Weak registry strategies (suffix_match / unique_name / field_type_hint /
fuzzy) are rejected for bases via an explicit drop-list: a base bound
because some project type happens to share its name is precisely the
fabricated relation #606 removed, and inheritance multiplies it — every
inherited member of the wrong base would become a callable target. An
unresolved base keeps its raw spelling and the behaviour that predates
this change, so stdlib and third-party bases are unaffected.
Scoped to the languages whose registrars read embedded_types as QNs
(Python, JS/TS/TSX). Go, JVM, C#, C++ and Rust already qualify their own
embedded types and keep the raw join.
Cost is O(defs) hash lookups plus ONE import map per file: no per-call-
site hierarchy walk, no registry scan, no per-file registry rebuild. The
complexity guard stays linear (nodes 1.93, edges 2.07, per-file defs
2.00 against a files x corpus coupling of ~4).
Tests:
- ts/S6 flips to its own documented condition: was `calls == 0` as a
tripwire recording the gap, now asserts calls >= 1 AND an lsp_ts_*
strategy. Measured: strategy=lsp_ts_method, confidence 0.95.
- python/S6b is new and covers what python/S6 structurally cannot: S6's
`def run(c)` parameter is un-annotated, so no receiver type exists to
inherit through and any binding there is a guess about a name (it
resolves via unique_name at 0.75). S6b supplies the receiver three ways
— annotated parameter, `self` in the subclass, constructor result — and
asserts INHERITS >= 1 plus an lsp_* strategy. Revert-check: all three
fall back to unique_name at 0.75 without this change, lsp_method at
0.90 with it.
- Both rows assert the STRATEGY, not just the edge count: a short-name
guess and a resolved inherited member produce the same count in a
two-file fixture, and only the strategy tells them apart.
- Corrects python/S6's stale comment citing a base_classes extraction
bug; extraction_inheritance is green for Python and TS, and
base_classes now holds clean names.
macOS full suite 7690 passed / 0 failed / 8 skipped (140 suites);
make lint-ci clean.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
An imported artifact carries the EXPORTER's mtime/size in every file_hashes
row, so the first incremental run after a bootstrap sees every file as
changed and re-parses ~the whole repository — most of the time the artifact
was supposed to save.
Reconcile the rows instead. After a successful import, ask the local git
which files are unchanged between the artifact's commit and the working
tree, and re-stamp exactly those rows with local stat() values. Everything
git cannot vouch for stays foreign and is re-parsed.
Export writes an optional "reconcile_basis":"git-clean-head" marker into the
existing artifact.json, and only when the tree is provably clean outside
.codebase-memory/ AND every hash row still matches disk. Unknown fields are
ignored by older binaries, so no schema_version bump.
Eligibility is deliberately narrow, and the polarity is skip-on-doubt:
a row is restamped only if it is tracked at the artifact commit (git
diff/ls-files are blind to ignored files — a .cbmignore negation can index
a gitignored file, #500) and absent from the changed set. Any uncertainty —
no git, missing/non-hex commit, shallow clone, truncated -z output,
allocation failure — skips reconciliation entirely and falls back to
today's behavior. Reconciliation can never fail the import.
Windows correctness: every git argument uses DOUBLE quotes, per the rule
already stated in artifact.c. cbm_popen runs cmd.exe /c on Windows, which
does not honor single quotes, so a single-quoted ":(exclude)" pathspec made
the clean-tree probe report dirty and the marker was never written there.
The commit-existence probe uses `cat-file -t <oid>` rather than
`cat-file -e <oid>^{commit}` because ^ is cmd.exe's escape character.
Fail closed on allocation failure: both cbm_ht_create() results are checked,
and every insertion is verified to have landed (cbm_ht_set discards the
underlying insert's status, so OOM drops entries silently). A dropped entry
in the changed set would make a genuinely modified file read as unchanged
and be restamped — a silently stale graph. This is the one place where the
membership-set primitive has unsafe polarity; classify_files uses it where a
dropped entry only means "re-parse".
Symlinks are refused rather than followed when stamping: git tracks a
symlink's link text, not its target's content, so "unchanged" from git says
nothing about the bytes the indexer parses. Uses the established
lstat + S_ISLNK / reparse-point idiom (discover.c, pass_pkgmap.c,
pass_envscan.c), which on Windows also keeps non-ASCII paths off the ANSI
CRT.
The trust trade-off is documented at both the import site and in
artifact.h: foreign mtimes currently force a re-parse that auto-scrubs a
poisoned artifact at an unpredictable clone time, so today's exposure is
transient and self-healing. Restamping makes it durable and gates it on a
producer-written string — acceptable only because the marker alone never
suffices, as every restamped row must additionally be proven unchanged by
the local git against a commit present in this clone.
Also skip synthetic file_hashes rows. The pipeline stores semantic-input
digests under .codebase-memory/.semantic-input/ that have no file on disk;
stat()ing them made the export-side check fail unconditionally, so the
marker was never written on ANY platform and the whole feature was inert.
find_deleted_files skips the same class. Both halves of the trust check now
ignore the .codebase-memory/ subtree, matching the clean-tree pathspec.
Distilled from #868 by moofone; #869 and #870 are not included.
Co-authored-by: moofone <35233167+moofone@users.noreply.github.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
parallel_lsp_index_exact_ambiguity_does_not_fall_through_to_legacy
asserted alpha_edges == 1, which the Python weak-member guard suppresses.
That assertion is a side effect, not the test's subject. Its subject —
that an ambiguous exact LSP match must not fall through to the
lower-ranked legacy semantic row — is unchanged: beta_edges == 0,
legacy_edges == 0, and all four probe assertions (ambiguity.injected,
legacy_injected, carrier_allows_fallback, shared_matcher_failed_closed)
still hold. Verified before changing the number, not after.
The harness deliberately injects an ambiguous exact match and forces the
shared matcher to fail closed, so value.render() drops past the LSP into
the registry with three same-named candidates (Alpha/Beta/Legacy.render)
and bound by suffix_match -- measured: strategy=suffix_match, cands=3,
conf=0.5500. That is a 1-in-3 guess that happened to land on Alpha:
exactly the weak member-call class this change suppresses, and the same
accepted trade recorded on python/S6 in test_lsp_resolution_probe.c.
Not over-suppression: sibling tests in this suite index the same
value.render() source and resolve it via lsp_method (cands=1,
conf=0.9000), a strategy the guard keeps -- they still assert 1. Only the
sabotaged-LSP path degrades to a weak textual guess. Flips back to 1 once
py_lsp_cross resolves the receiver on this path.
Refs #1276
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
manage_adr mode='set_sections' rebuilt the ADR from cbm_adr_parse_sections()
and cbm_adr_render(). That model is lossy, so the rebuild silently rewrote
text nobody asked to change. Measured on the previous commit:
- Everything before the first recognised heading was dropped.
- A mis-cased '## Purpose' was not a heading, so when it came first its
whole block was dropped with the preamble — an ordinary typo cost the
author a section.
- Sections stored out of canonical order were reordered.
mode='update' never had this exposure because it writes the caller's bytes
verbatim; the section merge introduced it.
The merge now locates the target heading's byte span and replaces only that
span, appending a block when the heading is absent. Bytes outside the span
are never reconstructed, so a preamble, a code fence, an unrecognised
heading and the author's section order all survive byte-for-byte. That
property is the acceptance test: every document in
adr_splice_preserves_untouched_bytes is a case the rebuild corrupted.
Arbitrary headings now work, which is what an "add an entry" mode needs.
The canonical six become a convention rather than a privilege: they are
still what ADR_EMPTY_HINT suggests, but they get no ordering priority and
none is required. Under splicing, ordering is whatever the document says,
and any privileged reordering would reintroduce the data loss above.
Names match exactly, including case, so '## Purpose' and '## PURPOSE' are
different sections. Folding them would silently merge two blocks the
author chose to keep apart — the same class of loss this commit removes.
cbm_adr_validate_section_keys() no longer enforces the canonical set. It
now rejects only names that could not round-trip through a '## NAME' line
(empty, '#'-leading, newline-bearing, edge-whitespace, over-long), because
such a name scans back as a different heading or as none, and a second
identical write would append a duplicate instead of being a no-op.
A heading inside a fenced code block is a code sample, not a section. An
unterminated fence hides every heading after it, so a write would append a
duplicate rather than replace: that is refused explicitly, and mode='sections'
reports the same ambiguity instead of answering with a partial list.
manage_adr mode='sections' now uses cbm_adr_scan_headings(), the same
classifier the write path splices with. It used to list any '#'-prefixed
line, so a '## Foo' in prose or inside a fence was reported as a section no
write could target. Two components disagreeing about what a section is was
how a section write came to be able to destroy one.
cbm_adr_parse_sections() is unchanged, so documents parse exactly as before.
TEST(adr_parse_sections_non_canonical) still passes untouched.
Co-authored-by: andis777 <24672074+andis777@users.noreply.github.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
manage_adr could only replace: mode='update' overwrites the stored
document in full, so adding one entry costs a re-send of the whole ADR.
That is a data-integrity problem before it is a cost one — the caller has
to reproduce every byte it did not intend to change, so the unchanged
prefix survives only as well as the round-trip that carried it.
mode='set_sections' rewrites only the sections named in `section_updates`
and leaves the rest of the stored document untouched, so the stored copy
stays the authority for everything the caller did not name. Unlike a
whole-document append it is idempotent: a client that loses a response
and retries re-sets the same section to the same body and the document is
byte-identical, where an append would silently duplicate the chunk.
Only the six canonical section names are writable. That is a correctness
constraint rather than a style rule: adr_try_section_header() parses ONLY
canonical headers, so a non-canonical '## FOO' written here would be read
back as body text of the section above it, and a second identical write
would append a duplicate — destroying the idempotence the mode exists for.
Three things the new mode needed that were not there:
- cbm_store_adr_update_sections() now wraps its read-modify-write in
BEGIN IMMEDIATE. Three writers replace this row wholesale — the indexing
pipeline, the UI POST /api/adr handler, and mode='update' — so the
unguarded get/merge/store lost whichever of them committed between the
read and the UPSERT. mode='update' is a single atomic UPSERT and never
had that window; a section merge introduces it.
- set_sections joins the write_request classification. A mode missing from
it takes no per-project mutation lease, resolves the store query-only,
and never reaches open_adr_store_for_write — its write would be
attempted through a read-only handle while an index runs.
- The write path reads the legacy <root>/.codebase-memory/adr.md itself.
The existing migration runs on the read path only because it must not
block on the lease; without this a section write would merge onto an
empty document and discard an ADR still present on disk.
CBM_ADR_MAX_LENGTH now applies to an MCP write path: it is enforced inside
cbm_store_adr_update_sections, which mode='update' does not go through. An
empty section body, an unknown section name and a missing section_updates
are all rejected before any store is opened, so a caller that meant to
write never receives a success-shaped read.
Section-level update was chosen over the whole-document append proposed in
PR #1243; the analysis that established the problem is from that PR.
Co-authored-by: andis777 <24672074+andis777@users.noreply.github.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Python attribute calls were not classified as member calls, so weak
short-name strategies (suffix_match / unique_name / field_type_hint)
could bind `obj.method()` to an arbitrary same-named project symbol —
`accelerator.print()` resolving to MockAccelerator.print.
Flag a Python attribute call is_method when its receiver is unresolved,
and extend the existing TS/JS weak-member guard to Python. Receivers that
ARE known stay exempt: self/cls, super(), and any identifier bound by one
of the file's imports (including the root of an attribute chain), so
Python's canonical `module.function()` cross-file call still resolves.
The guard helper is renamed cbm_tsjs_suppress_weak_method_match ->
cbm_suppress_weak_member_match. Its drop-list is unchanged, and the
per-language gate stays at the two call sites, which enumerate the same
language set in pass_calls.c and pass_parallel.c so the sequential and
parallel resolvers cannot diverge. ArkTS remains in that set.
python/S6/inherited_method in the resolution probe now asserts calls == 0,
recording the same trade TypeScript took when the receiver-aware guard
landed: the edge it used to produce came from a unique_name registry
fallback (measured on main: strategy=unique_name, cands=1, conf=0.7500)
where a weak short-name guess happened to be right in a 2-file fixture.
It flips back to >= 1 once py_lsp_cross resolves inheritance, matching the
flip-back note already carried on the TypeScript S6 case.
Fixes#1276
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Co-authored-by: Enferlain <15861396+Enferlain@users.noreply.github.com>
The cross-file Constant resolution test walks USAGE edges and calls
cbm_store_find_node_by_id() for each source, which populates the caller's
cbm_node_t with heap_strdup'd fields (store.c:195 via scan_node). It freed
the arrays it owned -- consts and edges -- but never released the per-edge
node, leaking one set of fields per iteration.
LeakSanitizer caught it in every sanitizer-enabled lane while the plain
test shards and both Windows legs stayed green, which is why the local
test-runner could not see it:
test-lsan-macos 417 bytes in 6 allocations
test-unix (ubuntu-latest, 2/3) 401 bytes in 6 allocations
test-unix (ubuntu-24.04-arm, 2/3) same signature
test-diag same signature
Use cbm_node_free_fields(), not cbm_store_free_nodes(). The latter frees
the array pointer itself after releasing each element's fields, so calling
it on a stack-allocated node is a bad free -- ASan aborts with
"bad-free test_lang_contract.c" if you try. cbm_node_free_fields() releases
the strdup'd members and leaves the struct alone, which is the idiom the
neighbouring probes already use (test_edge_types_probe.c:245).
The three other find_node_by_id call sites in this file predate this branch
and already handle their own cleanup; only the new Chialisp contract leaked.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Co-authored-by: darkverbito <8049985+darkverbito@users.noreply.github.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The tag lookup and one mutual-consistency call ended in pipelines whose
grep legitimately matches nothing — no v* tags in a --no-tags CI
checkout, no bare-return form in server.json. Under `set -euo pipefail`
that exit status killed the contract mid-run: it exited 1 having printed
nothing, which reads like a failure that forgot to explain itself, and in
the tagless case it died before it could report that its truth source was
unavailable.
Found by running the contract in a real `git clone --no-tags --depth 1`,
not by inspection. With the guards, that clone now passes while
announcing the degradation, and still fails on a drifted surface through
mutual consistency alone.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Seven packaging surfaces declared 0.8.1 while v0.10.8 was the shipped
release — nine releases of drift in metadata published to package
registries, and the third version-metadata drift this month.
pkg/go was not cosmetic: it hardcodes its version into the release URL
it downloads from, so `go install` fetched v0.8.1 binaries.
Chocolatey had to move as a unit. The nuspec is version-only but
chocolateyInstall.ps1 pins the asset sha256, so bumping the nuspec alone
would ship a package declaring 0.10.8 while installing 0.8.1. The new
checksum is the v0.10.8 windows-amd64.zip digest, verified by downloading
the published asset and hashing it, not merely copied from checksums.txt.
The gate (tests/test_version_metadata_contract.sh, scripts/test.sh Step
0x) checks against the newest stable v-prefixed tag, because nothing in
the tree declares the version: CBM_VERSION is injected at build time and
the release is dispatched with a version input. That absence is exactly
why these files drifted. Mutual consistency across the surfaces needs no
git and always runs, so a shallow clone degrades the gate loudly instead
of passing silently.
Homebrew (0.10.3), Scoop and AUR (0.8.1) are left pinned: each embeds
per-release asset checksums, so bumping them is a distribution chore
rather than a metadata edit. They are still gated — internally
consistent, and never naming a release that does not exist — and each
carries a recorded reason for being frozen.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>