Skip to content

fix: CRLF-robust knowledge parsing + refuse malformed entry headers - #177

Open
bilersan wants to merge 3 commits into
ActiveMemory:mainfrom
bilersan:fix/knowledge-crlf-and-malformed-headers
Open

bilersan wants to merge 3 commits into
ActiveMemory:mainfrom
bilersan:fix/knowledge-crlf-and-malformed-headers

Conversation

@bilersan

Copy link
Copy Markdown
Contributor

fix: CRLF-robust knowledge parsing + refuse malformed entry headers

Three read-side bugs, all observed live on Windows, where core.autocrlf=true
checks .context/ files out with CRLF line endings. The accepted entry-header
format is unchanged; no writer changes.

Spec: specs/fix-knowledge-crlf-and-malformed-headers.md (committed on-branch).

What ships

A. \r leaks into entry titles (internal/heading/entry.go, internal/heading/index.go)
Content is split on \n, so on CRLF files regex.EntryHeader's (.+) title
capture keeps the trailing \r. ctx disclosure inspect --json printed titles
like "Foo (consolidated)\r", and an apply plan written with clean titles
failed with "entry that is not in the root's staging zone". Fix: strings.TrimSpace
on the captured title at both read points; block lines are untouched.

B. False stale_header drift warnings (internal/drift/check.go)
checkTemplateHeaders compared the first comment of the live file and the
embedded template byte-for-byte, so a line-ending-only difference produced one
warning per knowledge file that ctx init --reset cannot fix. Fix: normalize
CRLF to LF inside extractFirstComment, which both sides go through.

build CRLF files: stale_header warnings LF files
upstream, LF-built binary 7 0
this branch, LF-built binary 0 0
upstream, binary built on a CRLF checkout (CRLF-embedded templates) 0 7
this branch, binary built on a CRLF checkout 0 0

C. Date-only entry header silently absorbed — data integrity (internal/disclosure)
ParseEntryBlocks only starts a block at a strict ## [YYYY-MM-DD-HHMMSS] Title
header, so a legacy/hand-written ## [2026-09-26] Title entry becomes part of
the previous entry's block: disclosure apply moved it into the previous
entry's
theme file, and inspect never listed it. Fix: root validation (which
already refuses structurally malformed roots so the pass never mutates) now
refuses a root containing a ## [ heading that is not a full entry header, with
a typed MalformedEntryHeaderError{Line, Heading} (it also satisfies
errors.Is(err, ErrStagingUnparsable), so existing callers are unaffected).
HTML comments are skipped, so the ## [YYYY-MM-DD] Decision Title example inside
DECISIONS.md's <!-- DECISION FORMATS --> template does not trigger;
CONVENTIONS roots are exempt. Message:

progressive disclosure: line 32: "## [2026-09-26] Legacy" is not a full entry header; give it a full timestamp ("## [YYYY-MM-DD-HHMMSS] Title") so it is not folded into the entry above it

On the same file upstream reports "Moved 1 entries" and writes the Legacy entry
into the neighbour's theme file; this branch exits 1 with the root byte-identical.

Verification

  • New tests: heading (CRLF titles for blocks and index), drift (the real
    LEARNINGS template in LF and CRLF, plus an edited header that must still warn),
    disclosure validate (date-only header in LF and CRLF with exact line/heading,
    the real DECISIONS template comment, a convention title containing ## [).
  • golangci-lint v2.13.2 (go1.27.1): 0 issues.
  • Full go test ./... on Windows: identical failure-name roster before and after
    every commit (pre-existing Windows-only failures: CRLF checkout, file
    permissions, raft cluster) — 0 new. The changed files add no LineLength /
    NoMagicValues findings.

Non-goal

ctx disclosure inspect is documented as never failing, so on a malformed root it
still lists the well-formed staging entries and leaves the malformed one out; only
apply refuses. Happy to follow up if you'd rather have inspect surface it too.

🤖 Generated with Claude Code

On a CRLF knowledge file (core.autocrlf=true on Windows),
ParseEntryBlocks splits on LF, so every line keeps its trailing
carriage return and the EntryHeader title group (.+) captures it.
ParseHeaders has the same bug over the whole file: "." stops at LF,
not at CR. `ctx disclosure inspect --json` printed titles such as
"Foo (consolidated)\r", and a digest plan that names the clean title
fails apply with ErrEntryNotInStaging.

Trim the captured title at both read points, as conventionBlocks
already does for convention sections. Block lines stay verbatim, so
the disclosure mover's byte-exact cuts are unaffected. No writer or
regex change.

Also add the spec covering this and the two follow-up fixes (drift
header check on CRLF files; malformed entry headers absorbed by the
previous entry).

Spec: specs/fix-knowledge-crlf-and-malformed-headers.md
Signed-off-by: Ersan Bilik <ersanbilik@gmail.com>
checkTemplateHeaders compares extractFirstComment(template) with
extractFirstComment(live) byte-for-byte; TrimSpace only trims the
ends, not interior newlines. A CRLF context file (core.autocrlf=true
on Windows) against the LF embedded template, or a Windows-built
binary with CRLF-embedded templates against an LF file, mismatches on
line endings alone. Every context file then warns "comment header ...
does not match template", and `ctx init --reset` cannot clear it.

Normalize CRLF to LF inside extractFirstComment, so both sides of the
comparison are LF. The check stays content-sensitive: an edited
header still warns.

Spec: specs/fix-knowledge-crlf-and-malformed-headers.md
Signed-off-by: Ersan Bilik <ersanbilik@gmail.com>
ParseEntryBlocks starts a block only at a line matching the strict
EntryHeader regex, so an entry whose header lacks the time part
("## [2026-09-26] Title", legacy or hand-written) is silently absorbed
into the previous entry's block. `ctx disclosure inspect` never lists
it, and `ctx disclosure apply` moves it, inside the previous entry's
span, into that entry's theme file. Validate only caught the case
where no staged entry parsed at all.

For the timestamped kinds, Validate now refuses any "## [" line
outside an HTML comment that is not a full EntryHeader, with a typed
MalformedEntryHeaderError naming the 1-based line and the heading and
telling the user to give it a full YYYY-MM-DD-HHMMSS timestamp. Its Is
matches ErrStagingUnparsable, of which it is the located form, so
existing callers and the "unparsable staging" case keep working. The
comment skip keeps DECISIONS.md's shipped "## [YYYY-MM-DD] Decision
Title" format example from tripping the guard. Conventions are
exempt: their sections carry no timestamp. The accepted header format
is unchanged.

Spec: specs/fix-knowledge-crlf-and-malformed-headers.md
Signed-off-by: Ersan Bilik <ersanbilik@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant