Tree-sitter's `(_) @import.source` wildcard matches the identifier node
of `#include PLATFORM_HEADER`, so the cleaned value `PLATFORM_HEADER`
slipped past the system-header / `..` filters and was emitted as a
permanently orphaned consumer contract (no file is named after a macro
identifier, so no provider can ever match). Add a shape guard that
skips cleaned values lacking both a path separator and an extension
dot, plus regression tests for single and multi-macro files.
Also document `IncludeExtractor.canExtract()` as unused by sync.ts
(gated via `config.detect.includes` instead) and kept solely for
ContractExtractor interface uniformity.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two HIGH findings from the Codex adversarial review on
feat/group-include-extractor:
1. Default-on extraction silently changes existing groups (BLOCKER)
DEFAULT_DETECT.includes was true, so any pre-existing group.yaml
that omits the new field would gain a wave of include::* contracts
on the next sync after upgrade. Flipped to false (opt-in). The
integration test already declares includes: true explicitly so it
survives unchanged; the unit extractor tests bypass parseGroupConfig
entirely; the sync test uses extractorOverride. Only config-parser
needed regression tests covering omitted/explicit/false variants.
2. IncludeExtractor scans outside the indexed file universe (BLOCKER)
The extractor was running glob('**/*', { ignore: STANDARD_IGNORES })
twice with a hand-rolled 9-pattern list, no .gitignore/.gitnexusignore
honoring, and no max-file-size cap. That meant File:<path> contracts
could appear for files ingestion would never index, producing
cross-links group impact cannot fan out to (silent false-negatives).
Refactored to a single discoverIndexableFiles() helper that mirrors
walkRepositoryPaths exactly: createIgnoreFilter + getMaxFileSizeBytes,
one discovery pass shared by provider and consumer paths. Dropped
STANDARD_IGNORES and SOURCE_GLOB entirely.
third_party and 3rdparty (the C/C++ vendored-deps conventions) were
in the local ignore list but not in the canonical DEFAULT_IGNORE_LIST
used by ingestion. Folded both into the canonical set rather than
keep a parallel list — the whole point of the Codex finding is that
two file-discovery implementations drift. Single source of truth.
Tests: 5 new regression tests for the discovery alignment (.gitignore,
.gitnexusignore, max-file-size on both provider and consumer paths)
plus 4 for the opt-in default. All 30 include-extractor tests + the
494-test group suite + ignore-service tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses two blockers and two mediums from the deep review.
BLOCKER 1: Windows CI ENOTEMPTY in sync.test.ts
After this PR added writeBridge() to syncGroup, the existing test
"writes registry to groupDir when skipWrite is false" fails on
windows-latest. LadybugDB's checkpoint thread briefly outlives
closeBridgeDb, holding a Win32 lock on bridge.lbug; the test's
fs.rmSync then fails with ENOTEMPTY. Switched the test cleanup to
cleanupTempDir from test/helpers/test-db.ts which already tolerates
EBUSY/EPERM/EACCES/ENOTEMPTY with bounded retries — same pattern
used elsewhere for LadybugDB-touching tests.
BLOCKER 2: Graph provider absolute-path bug
extractProvidersGraph queried File.filePath from the LadybugDB graph
but never stripped the repo root, so provider contract IDs ended up
as include::/abs/path/foo.h while consumers emitted include::foo.h.
These never matched through runExactMatch — silently producing 0
cross-links for any indexed C++ repo (the primary use case).
Now passes repoPath into extractProvidersGraph and applies
path.relative(); rows that resolve outside repoPath (stale absolute
paths from another machine, system headers somehow indexed) are
dropped instead of polluting the registry.
MEDIUM: `../` relative includes produce spurious noise
`#include "../foo.h"` is almost always intra-repo, but the suffix
index can never match a `..`-prefixed path so it became a consumer
contract no provider could satisfy. Now skipped before matching;
covers both forward-slash and backslash forms.
MEDIUM: writeBridge error in sync.ts propagates uncaught
contracts.json is the canonical source of truth and was just written
successfully when writeBridge runs. A bridge-only failure (disk full,
schema error, permission denied) shouldn't mask the registry. Wrapped
writeBridge in try/catch with a logger.warn surfacing the path and
recovery instructions.
Tests added:
- extractProvidersGraph repo-relative ID generation (stub Cypher
executor returns absolute paths)
- extractProvidersGraph drops rows whose path resolves outside repo
- `../foo.h` forward-slash skip
- `..\foo.h` backslash-form skip
Skipped findings:
- canExtract() removal (#5, low): canExtract is part of the
ContractExtractor interface; every other extractor implements the
same `return true` shape. Removing it from IncludeExtractor would
break the interface contract — keeping for consistency.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Claude Deep Review raised 7 findings on the IncludeExtractor. #1/#2
(BLOCKERs) were fixed earlier. This commit closes the remaining five.
#3 HIGH case-sensitive FS -> provider contract-id collision
Document the deliberate case-folding trade-off on normalizeIncludePath
(matches C/C++ convention on Windows/macOS; collapses Foo.h & foo.h on
Linux). Add a unit test pinning the behavior.
#4 HIGH suffixResolve short-suffix match silently drops cross-repo include
When a local file ends with the same basename as an external include
(e.g. local internal/api.h vs. #include "ext/api.h"), suffixResolve
returned a bogus local hit and suppressed the cross-repo consumer.
Replace the suffixResolve lookup inside include-extractor with a
strict isLocalInclude() that only accepts full-path hits via
SuffixIndex.get / getInsensitive. Callers of suffixResolve elsewhere
are unaffected. Add 3 unit tests covering the regression.
#5 MEDIUM regex fallback matched #include inside /* ... */
Strip block comments before running the fallback regex scan.
Add a unit test.
#6 MEDIUM meta.source was hard-coded to 'tree_sitter'
Track the actual extraction path with an extractionSource local and
write it into meta.source so downstream audits can distinguish
tree-sitter parses from regex fallbacks. Add 2 unit tests.
#7 MEDIUM missing end-to-end coverage
Add test/integration/group/include-extractor-sync.test.ts with 3
cases exercising extractor -> syncGroup -> CrossLink (mocked
contracts, mixed-case/backslash normalization, real temp repos).
Tests: 21 unit + 3 integration, all green.
- Remove unused HEADER_GLOB constant in include-extractor.ts
- Use fs.mkdtempSync for secure temp dir creation in tests
(CodeQL: 'Insecure temporary file')