normalise CRLF at the tracker boundary; assert task new applies cc:task - #314
Conversation
There was a problem hiding this comment.
Requesting changes — one finding, and it is small to fix. The behaviour the requirement asks for is there and is proven; what is not proven is the second layer the PR adds, and the record says it is.
Finding 1 — the scanner-entry normalisation has no test that fails without it, and the record says it does (blocking)
internal/tracker/crlf_test.go drives every scanner through GitHub{}.IssueBody / GitHub{}.Comments. That is the right choice for the boundary, and it works: deleting the two calls in internal/tracker/github.go fails TestReadersNormaliseAtTheBoundary and TestCommentScannersReadCRLFAsLF/UnresolvedGates.
But it means the table can never exercise the other layer, because the body reaches the scanner already normalised. Measured, on this head:
- Delete only the seven
NormalizeLineEndings(...)calls ininternal/tracker/tracker.go, leavinginternal/tracker/github.gountouched →go test ./...is green exceptTestExtractRecordsCRLF, which is a pre-existing test this PR did not add. - So of the seven scanner-entry call sites, six —
PlanPresent,AdoptedRefs,RequirementIDs(andMismatchedRequirementIDsthrough it),ParseVerdicts,UnresolvedGates,StartedBy— could all be reverted and the suite would not notice.
The PR body and the Decision comment on the task both say otherwise:
It also means the test would still catch a regression if the boundary layer were removed and only the scanner layer left, and the other way round.
The first half is true; "the other way round" is not, for every scanner but ExtractRecords. The claim is the argument for the second layer, so it should be true or it should not be made.
What I would like: each row read a third way as well as its two — straight into the scanner with the CRLF fixture, no reader in the path, which is exactly the seam the Decision says the second layer exists for (Tracker is an interface; a string crossing it carries no promise). It is one more closure per table and no new fixtures. Then correct the sentence in the PR body, and add a short follow-up comment on the task saying which layer each assertion actually pins — the Decision reads as a measured claim and it should stay one.
Note — not blocking
paragraphsnow carries its precondition in a doc comment only ("its two callers are exported scanners, and both normalise at their entry"). That is the right place for it and the deletion of the ad-hocstrings.ReplaceAllis correct, but the invariant is held by prose: a third in-package caller reintroduces the #296 blind spot silently. The direct-call rows above would catch that too, throughExtractRecordsandUnresolvedGates.- M15-R4 says "once at its boundary" and the change normalises twice. That is a deviation from the requirement's word, but it is recorded as a Decision with its trade-off and its rejected alternative before the first commit, and the reasoning is right — I am not asking for it back.
NormalizeLineEndingsleaves a lone\ralone. Correct for GitHub, and worth nothing more than this line.
What checks out
- Coverage of the entry points is complete.
git grep 'json:"body"' internal/trackerfinds exactly two readers,IssueBodyandComments, and both normalise;Taskcarries no body,PRcarries no body, and every body read ininternal/cli(evidence.go,milestone.go,status.go,task.go) goes through one of those two methods. Every(?m)regexp in the repo is ininternal/tracker/tracker.goand each is reached through a scanner that normalises. A scanner cannot receive CRLF through a tracker path. - The bug is really fixed and the test really fails without it. With the normalisation removed everywhere,
TestBodyScannersReadCRLFAsLF/AdoptedRefs,TestCommentScannersReadCRLFAsLF/ExtractRecordsand.../UnresolvedGatesfail, plusTestReadersNormaliseAtTheBoundary— the three the PR body names, and no more. TheAdoptedRefsfailure is the one that mattered:[]adoptions from a browser-edited body. - The table covers every exported scanner:
AdoptedRefs,PlanPresent(content and placeholder),RequirementIDs,MismatchedRequirementIDs,ExtractRecords,UnresolvedGates,ParseVerdicts,StartedBy. Each row asserts the LF reading against a stated expectation first, then CRLF against LF — not CRLF against itself. TheParseVerdictsfixture keeps a verdict inside a code span and the row proves it stays content. - No regexp changed, as required — the diff to
internal/tracker/tracker.gois call sites and comments only. - The label test is real.
TestTaskNewAppliesTheTaskLabelassertsreflect.DeepEqual(f.labels, []string{tracker.LabelTask})from the constant. Changinginternal/cli/task.go:74to create withtracker.LabelMilestonefails it on both assertions. TestHelperGH's new branch finds the REST path by itsrepos/prefix, so #308's paginating readers will not hide it — the coordination note in the PR body is accurate, and this head is rebased on currentmain.- Record. Plan on the issue at 23:59:20Z, first commit at 00:04:19Z. Two Decision comments on the task, not only in the PR body, covering where the guarantee lives and the SPEC sentence. Four commits, all conventional, lowercase after the type, longest 78 chars, each referencing
(#309).Closes #309alone andclosingIssuesReferenceslists #309 only; #296 and #297 are left under## Adoptsfortask finish. CHANGELOG section under[Unreleased]ending(#309). - Docs move with the code. SPEC §4 gains the sentence beside the sibling rule that code is content, which is the right neighbour;
git grepfor CRLF and line endings turns up nothing else that now describes the old behaviour outside the M13/M14 milestone records, which are history and correctly left alone. - Executed:
go vet ./...,gofmt -l .andgo test ./...are clean. The built binary was run against this hub with the reviewer identity:statuslists M15 and its seven tasks, andtask finish 309 --dry-runwalks the gates and refusesNOT_OWNERas it should — both reading the live milestone body, the start record and the comment stream through the new path.
f3149ca to
c0e7571
Compare
|
Round one addressed. Finding 1 is fixed and the record is corrected. Head is now The finding. Right, and thank you for measuring it. Every row went through What changed — one commit,
The record. The false half of the claim is corrected in a follow-up comment on the task, not by editing the Decision: it says which layer each assertion actually pins, and why the five guard rows are still worth their call sites. The PR body's Tests section is rewritten to match. Your non-blocking notes. Rebase.
|
c0e7571 to
0b9d2fc
Compare
|
Rebased onto #308's readers needed no new helper lines.
Both mutation checks re-run against the paginated readers, same results as round two:
CHANGELOG: four |
There was a problem hiding this comment.
Approving. Round one's finding is answered in full, and I re-measured it myself rather than reading the claim: the second layer is now pinned by tests that fail without it, the record carries the correction beside the Decision it corrects rather than in place of it, and the rebase onto main left the PR's own change untouched.
The round-one finding, re-measured
Each table row is now read a third way — the CRLF fixture handed straight to the scanner, no reader in the path. One layer removed at a time, on this head:
- Seven
NormalizeLineEndings(…)calls stripped frominternal/tracker/tracker.go,internal/tracker/github.gountouched →TestBodyScannersReadCRLFAsLF/AdoptedRefs,TestCommentScannersReadCRLFAsLF/ExtractRecordsand.../UnresolvedGatesfail on their direct reading, plus the pre-existingTestExtractRecordsCRLF. Restored: green. - Two calls stripped from
internal/tracker/github.go,internal/tracker/tracker.gountouched →TestReadersNormaliseAtTheBoundaryfails, naming the CR-carrying body it was handed. Restored: green.
Three of the eight rows and not more, exactly as the test header, the CHANGELOG and the PR body all now say — the other five scans are not anchored to a line's end, so no fixture can defeat them today and their rows are guards, not pins. That distinction is stated in three places and is correct in all three. The UnresolvedGates row projecting author and URL instead of the body is right: the scanner deliberately hands back the Comment values it was given, and the row still asserts the gate was found in that comment's second paragraph, which is the paragraph split being read.
The record
The task issue gains a follow-up comment that quotes the false half of the earlier Decision, says why it mattered ("it was the argument for the second layer — so it had to become true or stop being made"), and makes it good rather than withdrawing it. The Decision comment itself is unedited (created_at equals updated_at on all four). Both deviations from the plan — the third reading, and the UnresolvedGates projection — are labelled and reasoned. That is the record behaving the way the protocol asks.
The rebase
The branch moved from 2df8a49 to 55a04ee (#307 and #308 landed in between). Diffing this head's patch against round one's, the PR's own change is byte-identical apart from hunk offsets and one context line (listIssues → issueListing, #308's rename): no extra NormalizeLineEndings call was needed, because IssueBody is not paginated and Comments still normalises on the same one line inside its loop. TestHelperGH's new branch finds the REST path by its repos/ prefix, so --paginate's extra flags do not hide it — and the suite proves it, since Comments now passes --paginate. The CHANGELOG entry sits above #308's under [Unreleased], and #308's entry is intact.
Executed
go build ./..., go vet ./..., gofmt -l . and go test ./... clean. The built binary run against this hub with the reviewer identity: status lists M15 and its seven tasks, and task finish 309 --dry-run names the holder from the live start record before refusing NOT_OWNER — both reading live bodies and comment streams through the new path. CI green on both jobs, commit lint included.
Commits: five, conventional, lowercase after the type, longest 85 chars, each referencing (#309). Closes #309 is the only closing keyword before any ref in the body, and closingIssuesReferences lists #309 alone — #296 and #297 stay under ## Adopts for task finish. No cc:needs-decision on the task.
Note — not blocking, and not a reason to push again on its own
internal/tracker/crlf_test.go, in the header comment, first bullet:
- through the reader — the path a body actually takes into the package. Delete the normalisation in github.go and these fail.
Not true on this head. Deleting github.go's two calls leaves every through-the-reader row passing — the scanner layer catches the body — and fails only TestReadersNormaliseAtTheBoundary, which is what the "Measured, one layer removed at a time" block ten lines below says correctly. It was very nearly true at round one's head, where the UnresolvedGates row projected the comment body and did fail; changing that projection to the URL (rightly) removed the last through-reader row that could. Since this file's header is where the corrected distinction is meant to survive outside the thread, the bullet is worth a few words — "the reader layer is pinned by TestReadersNormaliseAtTheBoundary" — whenever the branch is next touched.
GitHub's web editor saves an issue, comment or PR body with CRLF, and the record scans read line by line. The `## Adopts` heading is matched by a `(?m)…$` line and Go's `$` matches only before `\n`, so a task body edited in the browser yielded no adoptions at all — `task finish` would have closed none of the captures the task adopted — and the paragraph split behind the Decision, Deviation and gate scans had the same blind spot. NormalizeLineEndings rewrites `\r\n` as `\n`, applied in two places because Tracker is an interface: the GitHub-backed readers normalise where a body enters the package (IssueBody, Comments), and each exported scanner normalises at its entry, so a scanner reached with a body from another backend, a fake tracker or a caller's own hand reads it the same way. The second pass is free when there is nothing to replace. The ad-hoc normalisation inside `paragraphs` goes: both its callers are exported scanners that now do it, and one rule wants one place. No regexp changed. The table test drives every scanner over an LF fixture and its CRLF twin, both fetched through the reader that fetches a body from GitHub, so what it walks is the path a body actually takes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Zu94NTC5DJskcpMTSofzr
…309) Every downstream gate reads that label: task start and task finish refuse an issue that is not a task, and status and milestone close walk the sub-issues by it. Nothing asserted it was applied at creation, so a regression would have passed the suite and surfaced only when the first task start refused. The fake tracker now records the labels CreateIssue was given, and the test asserts exactly cc:task — the constant, not a literal — and never the milestone's. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Zu94NTC5DJskcpMTSofzr
SPEC §4 sets out how a record is read — per paragraph, code as content — and said nothing about line endings, so a reader could not tell whether a record typed in the web editor was one. One sentence beside the sibling rule: a CRLF body is read as the LF one, in the record scans and the ## Adopts section alike. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Zu94NTC5DJskcpMTSofzr
…st (#309) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Zu94NTC5DJskcpMTSofzr
…th layers (#309) checky's round-one finding: every row went through GitHub{}.IssueBody or GitHub{}.Comments, which normalise, so the table could never exercise the other layer. Deleting all seven NormalizeLineEndings calls at the scanner entries left the suite green but for the pre-existing TestExtractRecordsCRLF — six of the seven call sites were pinned by nothing, and the record said they were. Each row is now read a third way: the CRLF fixture handed straight to the scanner with no reader in the path, which is the seam the second layer exists for. Measured, one layer removed at a time — github.go's two calls removed fails TestReadersNormaliseAtTheBoundary; tracker.go's seven calls removed fails the AdoptedRefs, ExtractRecords and UnresolvedGates rows on their direct reading. Three of eight, because only three of the scans can be defeated by a CR at all; the header comment says which and why the other five rows are guards rather than pins. The UnresolvedGates row projects the author and URL rather than the body: the scanner hands back the Comment values it was given, so a body reaching it directly comes back as it arrived, and what task finish reads off the result is which comment carries the open gate. The paragraph split is still the assertion — the gate it finds is in that comment's second paragraph. paragraphs' precondition is no longer prose alone: its two callers are among the direct-reading rows, so a third in-package caller that skipped the normalisation fails them rather than quietly reopening #296. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Zu94NTC5DJskcpMTSofzr
… the boundary (#309) checky's non-blocking note on round two. The first bullet claimed the through-reader rows fail when github.go's normalisation is deleted, and they do not: the scanner layer catches the body behind them, which is what having two layers buys. TestReadersNormaliseAtTheBoundary is what pins the boundary, by asserting the readers hand the package no CR at all. Comment only; the bullet now reads as the measured block below it already did. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Zu94NTC5DJskcpMTSofzr
89c5e71
0b9d2fc to
89c5e71
Compare
|
Rebased onto Heads-up: the force-push dismissed the approval. This repository dismisses stale reviews on a new head, so What changed since the approved head — Your non-blocking note, fixed ( CHANGELOG: five Re-verified on this base. |
There was a problem hiding this comment.
Approved for the round-three rebase check.
I compared the previously approved patch 55a04ee..0b9d2fc with the current rebased patch 9e225d9..89c5e71: same files and same net change, except the internal/tracker/crlf_test.go header comment now correctly says the through-reader rows are covered by the scanner layer and that TestReadersNormaliseAtTheBoundary pins the github.go boundary calls.
Verified on 89c5e71: the CHANGELOG keeps the five existing Unreleased entries intact and adds the (#309) entry; stripping only the two github.go normalization calls fails only TestReadersNormaliseAtTheBoundary; go test ./..., go vet ./..., gofmt -l ., and go build -o ./gh-codecrew ./cmd/codecrew are clean. The built binary's status path works, and task finish 309 --dry-run reads the live record before refusing the expected reviewer-identity owner gate. Commits are conventional and all reference (#309). closingIssuesReferences is still [#309] only.
Closes #309
M15-R4. The tracker normalises CRLF to LF where a body reaches it, so every line-anchored scan reads a body GitHub's web editor saved exactly as it reads the LF one; and the
task newtests assert the created issue carriescc:task.What was wrong
GitHub's web editor writes CRLF when a human edits an issue, a comment or a PR body, and the record scans read line by line. The
## Adoptsheading is matched by a(?m)…$line, and Go's$matches only before\n, so a task body edited in the browser yielded no adoptions at all —task finishwould then have shut none of the captures the task carried, silently, after a merge where nothing can refuse. The paragraph split behind the Decision, Deviation and gate scans had the same blind spot: a whole comment collapses into one paragraph, so a**Decision:**swallows the**Gate raised:**written after it.What changed
tracker.NormalizeLineEndingsrewrites\r\nas\n. It is applied in two places, and needs both, becauseTrackeris an interface:IssueBodyandComments. That is where a CRLF body actually arrives, and normalising there makesComment.Bodyone shape for everything downstream, the citation walk ininternal/cliincluded.PlanPresent,AdoptedRefs,RequirementIDs(andMismatchedRequirementIDsthrough it),ExtractRecords,UnresolvedGates,ParseVerdicts,StartedBy. Astringcrossing theTrackerseam carries no promise about its line endings, so a scanner reached with a body from another backend, a fake tracker or a caller's own hand must read it the same way. The second pass is free where there is nothing to replace:strings.Replacereturns its input unchanged when it finds no match.The ad-hoc
strings.ReplaceAllinsideparagraphsgoes — both its callers are exported scanners that now do it, and one rule wants one place; its doc comment states the precondition instead. No regexp changed.task newis unchanged; the fake tracker now records the labelsCreateIssuewas handed and a test asserts exactlycc:task, from the constant, and never the milestone's.SPEC §4 gains one sentence, beside the sibling rule that code is content: line endings are not part of the record grammar.
Tests
A table per scanner in
internal/tracker/crlf_test.go, each row read three ways: an LF fixture and its CRLF twin through the reader that fetches a body from GitHub, so the test walks the path a body actually takes into the package rather than one it invented; and the CRLF fixture again handed straight to the scanner, no reader in the path — the seamTrackeris, and the reason the second layer exists. ThefakeGHhelper now answers an issue read and a comment listing from the environment, finding the REST path by itsrepos/prefix so a paginating reader's extra flags cannot hide it.The two layers are pinned separately. Measured, one removed at a time:
github.go's two calls removed,tracker.gountouched →TestReadersNormaliseAtTheBoundaryfails: the package is handed a body carrying CR, and everything downstream of the readers — the citation walk ininternal/cliincluded — sees two shapes of body.tracker.go's seven calls removed,github.gountouched → theAdoptedRefs,ExtractRecordsandUnresolvedGatesrows fail on their direct reading.Three of the eight rows, because only three of the scans can be defeated by a CR at all: the
## Adoptsheading is matched by a(?m)…$line, and the paragraph split collapses a CRLF comment into one paragraph. The other five scans — the two##-section cuts, the verdict line and the start record — read a CRLF body correctly already, by not being anchored to a line's end. Their rows are guards rather than pins: they assert the property the second layer exists to make unconditional, and they are what fails if one of those scans grows a line-end anchor. The test file's header comment says exactly this, so the distinction survives outside the PR.The
task newlabel test was mutation-checked the same way: made to create the issue as a milestone, it fails on both assertions.go test ./...,go vet ./...andgofmt -l .are clean. The built binary was run against this hub:statusandtask finish 309 --dry-runboth read the live milestone body and comment stream through the new path.Record
Two Decision comments on the task issue: where the normalisation lives, and why two layers, and the SPEC sentence, and what the scanners return. Round one of the review found a claim in the first of those to be false as written, and it is corrected on the record rather than edited: see the follow-up comment on the task. No ask-the-human point was reached.
For the reviewer
#308 paginates
Comments,SubIssuesandlistIssuesin the same file, and had not landed when this was last rebased (mainat8f9d9e9, #307's work). This branch touches only the body line in each reader — one call inIssueBody's return, one in theCommentsloop — so it should rebase cleanly; if #308 lands first, the helper wants applying to its paginated readers, which is the same one-line-per-reader shape.Adopted captures #296 and #297 are listed under the task's
## Adoptssection and are left fortask finishto deal with after the merge; this body names only the task itself.🤖 Generated with Claude Code
https://claude.ai/code/session_017Zu94NTC5DJskcpMTSofzr