Two defects in readArtifactContent() combine so that a directory context artifact silently embeds compiled bytecode and other build output. Read out of the published package, socraticode@1.12.0, dist/services/context-artifacts.js (lines 125–145).
Both are one-line-ish fixes; I'm happy to send a PR for either or both.
1. The directory walk has a hardcoded ignore list and imports no ignore service
if (stat.isDirectory()) {
// Find all files in the directory (recursively, skip hidden/dot-files)
const files = await glob("**/*", {
cwd: resolved,
nodir: true,
dot: false,
ignore: ["**/node_modules/**", "**/.git/**"],
});
node_modules and .git, and nothing else. Grepping the module for ignore returns exactly these two occurrences and no .gitignore / .socraticodeignore reference anywhere — so there is no code path by which either ignore file can apply to a context artifact. This is not a load-order or staleness effect; I confirmed it behaviourally first (adding __pycache__/ and *.pyc to .socraticodeignore and re-indexing left the artifact's chunk count identical) and then in the source.
That is defensible as a deliberate choice, but it is not what the README's index-scope framing leads a user to expect, and it is the general case that lets the next consumer embed dist/, .pytest_cache/ or coverage data.
2. The binary guard is already written, and it can never fire
The loop immediately below is what turns (1) into embedded mojibake rather than merely extra text files:
for (const file of files) {
const filePath = path.join(resolved, file);
try {
const content = await fsp.readFile(filePath, "utf-8");
parts.push(`# ── ${file} ──\n${content}`);
}
catch {
// skip unreadable files (binary, permissions, etc.)
logger.debug(`Artifact: skipping unreadable file ${file}`);
}
}
The comment states the intent exactly — skip binary. It never happens, because fsp.readFile(path, "utf-8") does not throw on binary input; it returns a string with U+FFFD replacement characters. Verified against a .pyc fixture on Node:
readFile utf-8 on binary: RETURNED (no throw), len 8, hasFFFD true
So every .pyc takes the success branch and is embedded, mojibake and all. Nothing is unreadable, so nothing is skipped, so nothing is logged — the logger.debug line is dead code.
Measured consequence
A project declaring {"name": "alembic-migrations", "path": "./alembic/versions"} — a natural, documented-shape artifact. Every test run and every alembic invocation drops __pycache__/ into that directory:
- 70
.pyc files embedded, 352K of bytecode against 320K of source
- 32 of the artifact's 86 chunks were compiled bytecode
- a
codebase_context_search for "what is the current alembic migration head revision" returned decompiled bytecode as its top hit
|
chunks |
| baseline |
86 |
after adding __pycache__/ + *.pyc to .socraticodeignore, re-indexed |
86 |
after rm -rf alembic/versions/__pycache__, re-indexed |
54 |
Top-hit score for the same query went 0.5417 → 0.6111 once the bytecode was gone.
Why this is worth fixing rather than documenting
It fails silently and upward. Nothing errors, the chunk count rises, and codebase_status reports more artifact content indexed than before. Every available signal says the artifact got healthier while search quality got worse — so there is no observation a user can make that would prompt them to look. The only reason I found it was chasing a bad search result back to its chunk.
Suggested fixes
-
Make the binary guard actually detect binary. This alone fixes the bytecode case and preserves the existing intent. Read as a Buffer and either sniff for a NUL byte in the first few KB, or decode with a fatal decoder:
const buf = await fsp.readFile(filePath);
if (buf.includes(0)) { logger.debug(`Artifact: skipping binary file ${file}`); continue; }
const content = buf.toString("utf-8");
(new TextDecoder("utf-8", { fatal: true }).decode(buf) inside the existing try would also work and would make the catch live, at the cost of rejecting a few legitimately-latin1 text files.)
-
Wire in the ignore chain the code indexer already uses, so a directory artifact honours the built-in defaults + .gitignore + .socraticodeignore. This is the general fix and the one that stops the next unanticipated build directory.
A cheap partial alternative to (2), if wiring the ignore service into artifacts is unwanted for scope reasons: extend the hardcoded list with the common build-output names (**/__pycache__/**, **/dist/**, **/build/**, **/*.pyc). Less principled, but it covers the case above.
Worth noting for (1) alone: the artifact walk already passes dot: false, so dot-directories like .pytest_cache/ are excluded — __pycache__ is the notable build directory that is not hidden and therefore is walked.
Two defects in
readArtifactContent()combine so that a directory context artifact silently embeds compiled bytecode and other build output. Read out of the published package,socraticode@1.12.0,dist/services/context-artifacts.js(lines 125–145).Both are one-line-ish fixes; I'm happy to send a PR for either or both.
1. The directory walk has a hardcoded ignore list and imports no ignore service
node_modulesand.git, and nothing else. Grepping the module forignorereturns exactly these two occurrences and no.gitignore/.socraticodeignorereference anywhere — so there is no code path by which either ignore file can apply to a context artifact. This is not a load-order or staleness effect; I confirmed it behaviourally first (adding__pycache__/and*.pycto.socraticodeignoreand re-indexing left the artifact's chunk count identical) and then in the source.That is defensible as a deliberate choice, but it is not what the README's index-scope framing leads a user to expect, and it is the general case that lets the next consumer embed
dist/,.pytest_cache/or coverage data.2. The binary guard is already written, and it can never fire
The loop immediately below is what turns (1) into embedded mojibake rather than merely extra text files:
The comment states the intent exactly — skip binary. It never happens, because
fsp.readFile(path, "utf-8")does not throw on binary input; it returns a string with U+FFFD replacement characters. Verified against a.pycfixture on Node:So every
.pyctakes the success branch and is embedded, mojibake and all. Nothing is unreadable, so nothing is skipped, so nothing is logged — thelogger.debugline is dead code.Measured consequence
A project declaring
{"name": "alembic-migrations", "path": "./alembic/versions"}— a natural, documented-shape artifact. Every test run and everyalembicinvocation drops__pycache__/into that directory:.pycfiles embedded, 352K of bytecode against 320K of sourcecodebase_context_searchfor "what is the current alembic migration head revision" returned decompiled bytecode as its top hit__pycache__/+*.pycto.socraticodeignore, re-indexedrm -rf alembic/versions/__pycache__, re-indexedTop-hit score for the same query went 0.5417 → 0.6111 once the bytecode was gone.
Why this is worth fixing rather than documenting
It fails silently and upward. Nothing errors, the chunk count rises, and
codebase_statusreports more artifact content indexed than before. Every available signal says the artifact got healthier while search quality got worse — so there is no observation a user can make that would prompt them to look. The only reason I found it was chasing a bad search result back to its chunk.Suggested fixes
Make the binary guard actually detect binary. This alone fixes the bytecode case and preserves the existing intent. Read as a
Bufferand either sniff for a NUL byte in the first few KB, or decode with a fatal decoder:(
new TextDecoder("utf-8", { fatal: true }).decode(buf)inside the existingtrywould also work and would make thecatchlive, at the cost of rejecting a few legitimately-latin1 text files.)Wire in the ignore chain the code indexer already uses, so a directory artifact honours the built-in defaults +
.gitignore+.socraticodeignore. This is the general fix and the one that stops the next unanticipated build directory.A cheap partial alternative to (2), if wiring the ignore service into artifacts is unwanted for scope reasons: extend the hardcoded list with the common build-output names (
**/__pycache__/**,**/dist/**,**/build/**,**/*.pyc). Less principled, but it covers the case above.Worth noting for (1) alone: the artifact walk already passes
dot: false, so dot-directories like.pytest_cache/are excluded —__pycache__is the notable build directory that is not hidden and therefore is walked.