change-summary: an advisory digest of what changed on this branch, and why - #149
change-summary: an advisory digest of what changed on this branch, and why#149Oleg67 wants to merge 17 commits into
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (1)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughAdds the ChangesChange Summary Digest
Priority: ➖ Normal Estimated code review effort: 4 (Complex) | ~45 minutes Merge Risk: ⚪ Minimal · up to This adds an advisory, bounded change-summary command with human and JSON output. Current coverage supports its output, privacy, failure-handling, and non-gating behavior, with no unresolved merge risk identified. Sequence Diagram(s)sequenceDiagram
participant CLI
participant ChangeSummaryCommand
participant Git
participant DecisionLog
CLI->>ChangeSummaryCommand: invoke change-summary
ChangeSummaryCommand->>Git: resolve window and changed files
ChangeSummaryCommand->>DecisionLog: filter decision events
ChangeSummaryCommand->>CLI: render bounded digest or JSON payload
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
code-rankerBuilt on a fork. View full report ↗ python
|
bdb825e to
12a9252
Compare
|
Restacked on #133's On the restacked head: full suite 5,381 passed, 213 tests across the three change-summary suites, |
4672e3d to
89ba388
Compare
--json output can crash with UnicodeEncodeError on non-UTF-8 filenamesSeverity: Major Problem How to reproduce
Expected behavior Actual behavior Impact Suggested correction How to verify Original location: skills/studio/scripts/studio/utils/ui.py:118 -- inline anchoring could not be resolved after 1 attempt(s). |
e252746 to
19c26a2
Compare
19c26a2 to
73f4e92
Compare
`cfs change-summary` composes at most ten lines from the window, the changed-file linkage and the decision-log selection: the span covered, what changed with its marker denominator, the requirements served, and the decisions recorded while the work was done. `--json` carries the data behind every line. Advisory is structural, not asserted: * exit 0 on every path except a usage error (2). A test forces each row of the behaviour matrix; a last-resort guard turns an unforeseen exception into a stated reason rather than a traceback; and a test greps the Makefile and workflows so the command cannot be promoted into a gate quietly; * the ceiling is a ceiling — only lines backed by data are emitted, and when it bites the last line says how many were cut. No window and no changes are each one line, deliberately; * every degraded dimension states its reason and its denominator; * the digest never counts itself: telemetry events are not "why", and this command's own invocations are excluded from the payload, so consecutive runs are byte-identical although each logs an invocation; * no absolute path in either rendering; no network; no commit author. Registered in all five dispatch tables. Spec: a flow, a nine-rule composition algo, a definition of done, the module row and an acceptance criterion; TOC refreshed. Tests: 34 across unit, real-CLI integration, golden fixture with pinned commit dates, edge, invariant, privacy, fail-safe, determinism and scope reporting; module at 100% line coverage. Eight mutation checks each fail only the tests written for them. Signed-off-by: ou <ou@constructor.tech>
…rules Maintainer round on the digest command, alongside the linkage fixes it now sits on: * The changes and markers lines named `changed` as their denominator while every tally was computed over the entries actually examined, so a capped scan read "300 of 1,500" as a breakdown of 1,500. When the cap bit, both lines now name the examined population, and the payload carries `examined` beside `changed`. * A project check that *failed* — permission error, unreadable mount — was folded into "not inside a Studio project" and logged at debug. It now has its own reason carrying the exception type, and is logged at warning like the last-resort guard. * The not-a-file tally from the linkage half is rendered, and the unreadable label says "read or parsed" now that a parse failure is reported as one. * `--help` gains an epilog stating the rules that shape the output: the window's origin, the scope policy, the examined ceiling (quoted from the one constant the report uses), both marker directions, the line ceiling and its omission rule, and that nothing is published. * The no-gate test also scans `*.yaml` workflows and any pre-commit configuration, and asserts it scanned at least two files. The linker is called with the window alone, per its new signature. Six mutation checks each fail only the tests written for them. Signed-off-by: ou <ou@constructor.tech>
… with its anchor Maintainer round 2 on the digest command, alongside the linkage fixes it now sits on: * A changed file with a non-UTF-8 name crashed `--json`: git paths are decoded with surrogateescape so they round-trip, but a lone surrogate cannot be printed, and the UnicodeEncodeError rose from inside `print`, past the last-resort guard. Every string in the payload — the human lines included — is now made encodable once, in one place; undecodable bytes render as `\xNN` escapes. Tested against a strictly encoding output stream, since a StringIO would never have noticed. * Two runs sharing their first eight characters rendered as identical labels. The prefix now grows until the shown ids differ, as git does. * The window line reads "since <bound> against <ref> @ <sha>", so an explicit `--since` composed with `--base` (which the linkage half now supports) shows both, and `--help` says the two compose. * Boundary tests at exactly the cap and one over, for the requirements and runs "+N more" arithmetic. Three mutation checks each fail only the tests written for them. Signed-off-by: ou <ou@constructor.tech>
…me kind `surrogateescape` re-encodes only the surrogates it produced itself (U+DC80–U+DCFF); any other lone surrogate — a `"\ud800"` that a decision- log line carries through `json.loads` — made the sanitiser raise, and the whole digest fell to the last-resort guard for one bad event field. Those are now escaped as themselves, so the decisions dimension states the value and the rest of the digest stands. Reverting the fallback fails the test. Signed-off-by: ou <ou@constructor.tech>
… shown or folded The prefix width was computed over the three runs shown, so a fourth run folded into "(+N more)" that shared its first eight characters with a shown one left that label matching two runs in the log, and the digest gave no sign of it. The width is now computed over every run in the window: a prefix printed is a prefix unique in the log it points into. Test: a folded run sharing eight characters with a shown one widens the shown label to nine; computing the width over the shown runs alone fails only that test. Signed-off-by: ou <ou@constructor.tech>
…ributed bucket, and leave a trail on the escape fallback Maintainer re-verification round on the digest command. - The payload counted events with no run id; the plain digest never said so, and the runs line under-reported the breakdown with no sign of it. A `decision log: N event(s) carry no run id` line now appears when the count is non-zero, beside the skipped, undated and shared lines. - The unattributed bucket was cut to eight characters like an id and read `(unattri`. It is a word, not an identifier, and is rendered whole. - The fallback for a lone surrogate that `surrogateescape` did not make -- a corrupt decision-log line -- escaped it silently. It now warns, at the level the module's other guards use, so a mangled field has a trail. - `_is_own_invocation` documents that the exclusion is by kind, not by instance: every change-summary invocation is a read of the log, and excluding only the current run's would break the determinism the suite pins. Tests: the exit-and-status truth table over the behaviour matrix; a real overflow at the default ceiling agreeing with the payload's omission count; two invocations from distinct runs both excluded with the decision kept; the runless line and the full unattributed label; the exact two-byte escape surviving JSON; the warning on a non-filename surrogate and silence on a filename one; the privacy test asserting a digest was produced before checking what it omits. Signed-off-by: ou <ou@constructor.tech>
The privacy test covered the ordinary digest; the degraded fields -- a file that could not be read carrying a reason, a shared log carrying the fact of its override -- are where a path would leak if anywhere. With an unreadable changed file and an absolute CFS_DECISION_LOG override both in force, the serialised payload is asserted to contain neither the project root, the override path, the home directory nor the user, and to show that both degradations were actually present. Signed-off-by: ou <ou@constructor.tech>
The error-scenario list grouped "no changes against the base" with the outcomes that state a reason and a denominator, while the command prints one line naming the base and nothing else -- deliberately, as the description says. The spec now describes the one-line outcome as its own scenario and says why no zero-file denominator is printed with it. Signed-off-by: ou <ou@constructor.tech>
…mension that is unavailable The scenario sentence promised a reason and a denominator for every degraded outcome; an unavailable dimension has nothing to count and states its reason alone, which is what the command prints. The sentence now says so. Signed-off-by: ou <ou@constructor.tech>
… the boundary The count was pinned at one length. A review read that as an off-by-one and proposed `len(lines) - LINE_CEILING`, which reports 1 where two source lines are genuinely absent -- so the digest would understate its own omission at every overflow, which is the one direction that matters for a command whose purpose is making what it drops visible. Parametrized at ceiling-1, ceiling, ceiling+1, ceiling+2 and well past, with `dropped` counted by asking which input strings are missing from the output rather than by any formula. Applying the proposed change fails this test at three of the five lengths. Signed-off-by: ou <ou@constructor.tech>
…, and reject both spellings The zero-wiring promise was checked against an enumerated set -- `workflows/*.yml`, the Makefile, pre-commit -- and only against the hyphenated CLI name. Both limits let the failure it exists to catch pass one directory over: a gate added as a composite action, a nested workflow, or a `settings.yml` naming required checks was never opened, and `python -m studio.commands.change_summary` wires the command without ever writing the hyphen. Now everything under `.github` at any depth and whatever the extension, plus the Makefile and any pre-commit config, with both spellings rejected. Verified against three fixtures the previous version passed: a settings file naming the command in `required_status_checks`, a nested composite action using the module path, and an underscore invocation appended to the Makefile. Each fails the test now; removing it passes. Two limits stated in the docstring rather than left to be discovered again: branch protection and rulesets are account-side configuration and not repository text, so no test here can assert the required-status-check list; and `pyproject.toml` is excluded by intent, since it declares tool configuration rather than gate invocations and its coverage and vulture sections name modules legitimately. Signed-off-by: ou <ou@constructor.tech>
…ish the exit truth table The tally label said "could not be read or parsed", but the counter behind it holds every reason no marker could be established -- undeterminable scope and an unexpected scan failure as well as a failed read. So a scope-policy failure was reported to a reader as a file-access one. Now "yielded no marker information", which is what the count actually means; the per-file `reason` in the JSON still names the individual cause, so no detail is lost, only the false specificity. Pinned by a test that forces a real scope failure and asserts both halves: the row keeps `REASON_SCOPE_UNKNOWN`, and the line does not name one cause for all of them. The exit truth table asserted its six baseline rows and one usage error, an unknown flag. An option supplied without its value is the form a person actually hits and was untested, so `--base`, `--since` and `--root` without values and an unexpected positional are now rows of the same table, each asserting exit 2 and status ERROR -- keeping "a usage error is the only non-zero exit" checked across every way the parser can fail rather than one. Signed-off-by: ou <ou@constructor.tech>
…ings about it `assert all(lines)` passes on an empty list, and so does `not any(...)`, so a regression suppressing the whole human digest satisfied all three assertions in this test. The non-empty guard now comes first, which is what gives the other three their force. Verified by returning `[]` from the composition for this fixture: the test fails, and passes again once restored. Signed-off-by: ou <ou@constructor.tech>
…pin the two exclusion rules where they diverge The whole-payload privacy sweep combined an unreadable file with a shared log, but ran on events that were all dated and all attributed, so the undated and runless fields -- separate reported fields, and as good a place for a path to surface as any -- were never in force while those assertions ran. They had only been exercised at unit level against an `EventSelection`, never through the real serialisation. Both are now in the same sweep and asserted in force before absence is checked, so the test cannot pass by having nothing to leak. Separately, two independent rules drop events and only their agreeing cases were asserted. `_is_own_invocation` drops by command, so a digest never reports its own footprint; `_TELEMETRY_EVENTS` drops by kind, so no invocation or read is a decision whoever issued it. An `invocation` from another command is where they disagree -- retained among `events` and in `by_event`, contributing nothing to `decisions` -- and that case is now pinned. Widening the first rule to drop any invocation, which collapses the two into one, fails it. Signed-off-by: ou <ou@constructor.tech>
…ty lines first The digest emitted the per-run breakdown before the four "decision log:" lines, and the ceiling cuts from the end -- so the lines admitting the log could not be read fully were structurally the *first* thing dropped. A change set large enough to reach ten lines therefore made the digest stop reporting that three log lines were unreadable in order to keep printing which runs the readable ones came from. That is the silent omission this command exists to avoid, arriving through the ordering rather than through the counting. The breakdown now goes last. It also reads better: a caveat about the decision count sits beside that count, and the per-run detail is what a reader can most afford to lose. Stated as a priority rather than a guarantee, in the code, the spec and the test: the omission summary occupies a slot of its own, so even the smallest overflow costs two lines and a deep enough one still reaches an integrity line. What covers that case is the omission count plus the JSON, which carries every one of these counts as a field whatever the human rendering had room for. An earlier draft of the test claimed the four always fit; the fixture disproved it, and the claim is gone rather than weakened. One assertion elsewhere was pinned to a line index rather than its content and moved with the order; it now matches on the label it was always about. Signed-off-by: ou <ou@constructor.tech>
73f4e92 to
09f0ae4
Compare
Diff and untracked-sweep readers disagree on newline translation, breaking dedup for CR-bearing pathsSeverity: Minor Problem Reproduction, impact, suggested fix, verificationHow to reproduce
Expected behavior Actual behavior Impact Suggested correction How to verify Original location: skills/studio/scripts/studio/utils/change_summary.py:811 -- inline anchoring could not be resolved after 1 attempt(s). |
ci target only checks for an empty job list, not act's exit statusSeverity: Minor Problem Reproduction, impact, suggested fix, verificationHow to reproduce
Expected behavior Actual behavior Impact Suggested correction How to verify Original location: Makefile:384 -- inline anchoring could not be resolved after 1 attempt(s). |
| "changed": report.changed, "examined": report.examined, "marked": _marked(report), | ||
| "linked": report.linked, "declaring": report.declaring, | ||
| "excluded": report.excluded, "deleted": report.deleted, | ||
| "unreadable": report.unreadable, "not_a_file": report.not_a_file, |
There was a problem hiding this comment.
Decision-log scan denominator is omitted and human output mislabels the filtered count
Severity: Minor
Problem
EventSelection.scanned counts every parseable decision-log event before timestamp filtering, but the command exports only the number of retained events. The human no-decisions line labels len(events) as 'scanned' even though events has already been filtered to the window and stripped of change-summary invocation events. Thus the command hides the actual denominator and can report '0 event(s) scanned' after it did scan older or self-invocation events.
Reproduction, impact, suggested fix, verification
How to reproduce
- Put a valid event before the resolved window boundary in the decision log. 2. Run cfs change-summary in human and JSON modes. 3. Observe EventSelection.scanned is 1, decisions.events is 0, JSON has no scanned field, and the human output can say '0 event(s) scanned'.
Expected behavior
Both JSON and human output should distinguish total parseable events scanned from events retained in the window, using EventSelection.scanned as the denominator.
Actual behavior
_decisions_payload omits selection.scanned. _decision_lines uses len(events), a filtered and self-invocation-excluded list, in text labelled 'scanned'.
decision-log snapshot -> parseable events (selection.scanned) -> timestamp window filter -> selection.events -> remove self invocations -> displayed events/decisions.\nCurrent output exposes only the final stage but labels it scanned.
Impact
Machine consumers cannot determine whether an empty decision digest examined no entries or excluded entries outside the window. Human readers can receive a false scan count, weakening the command's stated denominator and degraded-result diagnostics.
Suggested correction
Add decisions.scanned to _decisions_payload. Render selection.scanned distinctly in human output, for example 'no decisions recorded in this window (0 retained of 1 event scanned)', and add fixtures containing pre-window and self-invocation events.
How to verify
Verified on reviewed commit 09f0ae4. select_events increments scanned before timestamp filtering in utils/change_summary.py lines 709-718 and stores it at line 729. commands/change_summary.py _decisions_payload lines 341-350 omits it; _decision_lines lines 230-246 reports len(filtered events) as scanned.
There was a problem hiding this comment.
Half right, and the right half is fixed in 84050ef1. The other half is refused with a reason, and I nearly made the change before checking.
The label was wrong. len(events) is the events left after the window filter and after this command's own invocations were dropped, and the line called it "scanned". So a log holding a hundred older entries reported 0 event(s) scanned having scanned all hundred — a denominator that was not one. The word is wrong rather than the number, so the word is what changed:
lines = [f"why: no decisions recorded in this window ({len(events)} event(s) in it)"]Pinned by test_the_no_decisions_line_counts_the_window_not_the_whole_log, which sets scanned=100 against a single in-window event and asserts the line says 1, does not contain the word "scanned", and does not contain 100.
Carrying selection.scanned in the payload is the part I cannot do, and it is not an oversight. It counts every parseable event in the log — including this command's own invocations, which the dispatcher records on every run. So exporting it makes two consecutive digests of an unchanged repository differ by one. That is precisely the determinism _is_own_invocation exists to hold, and it is already pinned:
assert len({out for _, out in as_json}) == 1, \
"the payload's totals too — telemetry is excluded from 'why', but only the " \
"self-exclusion keeps the event count from growing by one per run"I added "scanned": selection.scanned first and let the suite decide. It failed:
FAILED test_consecutive_runs_are_byte_identical_although_each_logs_an_invocation
So the omission is load-bearing. And there is no self-excluded variant to offer instead: EventSelection keeps no events from outside the window — only the count — so the invocations among them cannot be identified and subtracted. A denominator that is right for one run and wrong for the next is worse than a narrower one that is always right, in a command whose output is meant to be diffable.
It is now asserted as a deliberate absence, test_the_payload_omits_the_log_total_because_it_grows_every_run, because adding it looks like an improvement — it is the change that test exists to stop. The stable counts are all carried and asserted present: events, decisions, undated, runless, skipped_lines.
If the log's true total is wanted despite the drift, the honest way is for the core to expose a count that excludes invocations by command — which is a change to EventSelection in utils/change_summary.py and belongs in its own PR rather than here. Say the word and I will open it.
Suite at 84050ef1: 5,637 passed, cfs validate 0 errors, granularity 0.4605, pylint and vulture-ci clean.
The no-decisions line printed the events left after the window filter and after this command's own invocations were dropped, and labelled that number "scanned". So a log holding a hundred older entries reported "0 event(s) scanned" having scanned all hundred -- a denominator that was not one. The word is wrong, not the number, so the word is what changed: the line now says how many events fell inside the window, which is what it counts. The review also asked for `selection.scanned` -- the log's true total -- to be carried in the payload, and that one is refused with a reason. It counts this command's own logged invocations, so exporting it makes two consecutive digests of an unchanged repository differ by one, which is the determinism `_is_own_invocation` exists to hold. Adding it fails `test_consecutive_runs_are_byte_identical_although_each_logs_an_invocation`, which documents exactly that. There is no self-excluded variant to offer instead: the selection keeps no events from outside the window, so their invocations cannot be identified and subtracted. I had made that addition before checking, and the determinism test caught it. It is now asserted as a deliberate absence, because adding it looks like an improvement. Signed-off-by: ou <ou@constructor.tech>
The two findings that could not be anchored inlineBoth landed on this PR but neither is in this PR's diff — it is four files now that #154 has merged: the digest command, its CLI registration, its spec section and its test suite. Answering them here rather than leaving them unaddressed. 1. Newline translation between the two readers — real, and worse than reported. Fixed in #160This one is correct and precise. The reported consequence — deduplication missing the file — is real. But there is a second one that is worse, and it is a false statement rather than an incomplete one: the translated name opens nothing, so a tracked file present on disk came back as deleted. The code is in Worth noting the earlier round on #154 shared the codec between the readers for exactly this class of bug and I treated that as closing it. It did not: the codec and the newline translation are two separate behaviours of 2.
|
…ture, both renderings Two independent rules filter events -- self-invocations by command, telemetry by kind -- and each test exercised a slice of the result. So a regression could retain self events or misclassify a foreign command's telemetry without any single assertion failing on the *partition* itself. The existing telemetry test already covered both renderings and held a telemetry-only run; what it lacked was the one category the two rules disagree on, a foreign command's invocation, which survives the self-exclusion and is dropped by the telemetry rule. Rather than add a fifth overlapping fixture, that case joins this one and the assertions are extended to the partition: `by_event` must show exactly one surviving invocation -- the foreign one, not ours -- and neither telemetry-only run may be named in the breakdown. Verified as the review asked: mutating either rule alone fails it. Dropping the self-exclusion retains our own invocation, and emptying `_TELEMETRY_EVENTS` makes all three events decisions. Its narrower siblings stay, since they pin properties this one does not: that the exclusion is by kind rather than by instance, and that the divergent case holds through the real CLI rather than a hand-built selection. Signed-off-by: ou <ou@constructor.tech>
|



Closes #131.
The gap
There is no short, trustworthy read of what a run changed and why. A reviewer's options today are the full diff, or
cfs usage-report, which aggregates the decision log per method across all time — a cost lens, not a review lens. #125 landed the window and the decision-log selection, #133 the requirement linkage; neither renders anything a person reads.The change
cfs change-summary— an advisory digest of at most ten lines, meant to be pasted into a review. Run on this branch againstupstream/mainit prints:--jsoncarries the same lines plus the full data behind each of them — every file with its status and IDs, every run, every count.Why not
usage-report --sinceReasonable question, so answering it up front.
usage-reportreports tokens and reads per method over the whole log. This reports what changed on a branch, why, and against which requirement. Different audience, different output, different window; folding them together produces one command with two unrelated modes.Design principles
--jsonhas them. Two single-line digests are deliberate: no window means nothing to scope the other dimensions to, so one reason plus the remedy (--sincescopes decisions by time without git) rather than the same reason three times; no changes means nothing to review, so the log is not consulted for a line.invocationfor every command, this one included. Telemetry events (invocation,read) are not what a reviewer means by "why", so they are excluded from that line; and this command's own invocations are excluded from the payload entirely, so two consecutive--jsonruns are byte-identical although each one appended an event to the very log it reads. Tested by running it three times from inside the project and checking the log grew.surrogateescapeso a legal non-UTF-8 filename round-trips; a lone surrogate cannot be printed, and used to crash--jsonfrom insideprint, past the last-resort guard. The payload — human lines included, keys included — is made encodable once: an undecodable filename byte renders as\xNN, and any other lone surrogate (a log line's"\ud800") as itself, so one bad field never costs the whole digest. Tested against a strictly encoding stream, since aStringIOnever notices.socket.socketpatched to raise → still works.--help. Where the window comes from, the scope policy, the examined ceiling (quoted from the one constant the report uses), both marker directions, the line ceiling and its omission rule — so an excluded file or a capped change set is not mistaken for a bug by someone who never reads the source.Behaviour matrix
decisions: unavailable (decision log disabled)decision log: N unparseable line(s) skippedno changes against <ref>— one linemarkers: 0 of N changed files carry requirement markers; no requirements line--sinceremedyNon-goals
Gating anything. Judging whether code correctly implements its requirement. Replacing the diff. Offering the digest from the workflow-completion menu is deliberately not here either — it is a second surface and deserves its own change.
Round 3
The re-verification pass left four threads open; each is answered on its thread. One was a real defect, fixed in
228b2731: the run-prefix width was computed over the three runs shown, so a run folded into "(+N more)" that shared eight characters with a shown one left that label matching two runs in the log. The width now spans every run in the window, so a prefix printed is unique in the log it points into. The other three were pointers — the boundary tests exist at lines 324–341, the--base-with---sincetest lives in #154's diff, and the one residual assertion gap (the empty error list on a disabled ceiling) is closed on #154.Round 4
Fifteen threads from the re-verification pass, each answered on its thread; the fixes are in
6253654a.decision log: N event(s) carry no run id— where before only the payload counted them and the runs line under-reported with no sign of it. The unattributed bucket is rendered whole,(unattributed), not cut to(unattrilike an id.surrogateescapedid not make — a corrupt log line — is escaped and warned about; the routine filename case stays quiet.change-summaryread is no more a decision than this one's, and excluding only the current run's would break the run-to-run determinism above.omitted; two invocations from distinct runs both excluded with the decision kept; the exact\xff\xfeescape surviving a JSON round-trip; the privacy test asserting a digest was produced before checking what it omits.--jsonby design; a changed file under a vendored path is reported rather than hidden, as_in_project_scopedocuments; the "obsolete golden format" finding was against a superseded head.Round 5
Two findings on
6253654a. One is a test, added ineb361580(1d45e93dbefore the restacks): the degraded payload fields — an unreadable file's reason, a shared log's override — are where a path would leak if anywhere, so a test now forces both and asserts the serialised payload carries neither the project root, the override path, the home directory nor the user. The other, an eagerrglobin the shared entry resolver, predates this series, is never entered by the linkage (it refuses non-files before calling the resolver), and changes every consumer'sexcludedcount if fixed; it is answered as a change of its own, offered once these land.Round 6
One spec finding on the re-verification pass, fixed in
26a39734(doc only): the error-scenario line grouped the no-changes outcome with the paths that state a reason and a denominator, while the command deliberately prints one line naming the base. The no-changes outcome now has its own scenario line saying exactly that and why. Four re-verifications that ran against a superseded head were answered with the names and line numbers of the tests they could not find.Round 7
Doc only, in
31412cc0: the scenario sentence promised a reason and a denominator for every degraded outcome, while an unavailable dimension has nothing to count and states its reason alone; the sentence now says so. CodeRabbit's point about the registered-scan growth test's starting content is fixed on #154 (bf0124af), which this branch is restacked on. Three re-verifications that reported tests as missing were answered with the tests quoted in full.Round 8 — the granularity floor, measured against the current default branch
The floor check was being reported against the commit this stack was cut from. Re-measured against
mainas it stands now, because the merge is what CI scores:main@38434252alonemain, before its fixmain@ac9e06e9, with #154 now mergedmain(09f0ae46)The breach was #154's to fix and was fixed there, by declaring three further steps rather than by compressing anything. #154 has since merged, so the last two rows are now measured facts rather than predictions: the default branch went from sitting exactly on the floor to holding 15.1 line-granularity units of buffer, and this branch takes it to 25.4. This branch was never the one below the floor — its module carries enough declaration to lift the average — and every figure here is measured on the merge rather than on the branch alone.
Gates
Pinned to
de51e662, the current head, rebased directly ontomainnow that #154 has merged (ac9e06e9). The diff is this PR's own four files and nothing else.make testmake pylintmake vulture-cicfs validatetoc-heading-duplicatereports inarchitecture/DESIGN.md, untouched herespec-coverage --system studiomain+ this branch, against the 0.46 floor; coverage 90.8%. The new module is 12 traced blocks over 231 effective lines, g = 0.5195, above the system scoreThe module is CPT-traced with 12 blocks over 230 effective lines, g = 0.5217 — above the system score, so it raises the margin rather than consuming it. (An earlier revision of this table said 180 lines and g = 0.67; the measured figures are 230 and 0.5217.) Twelve mutation checks — the runless line dropped, the unattributed label cut like an id, the escape fallback silent again, the prefix width over the shown runs only, self-exclusion removed, telemetry counted, ceiling cutting silently, guard re-raising, project gate removed, one-liners removed, empty requirements line emitted, marker line dropped — each fail only the tests written for them.
Delivery
Last of the pull requests in this sequence — #125, #133 and #154 have all merged, so this one stands alone on
mainwith no stacked dependency.Summary by CodeRabbit
cfs change-summarycommand, providing an advisory digest of recent changes, affected files, requirement references, decision-log activity, and scan limitations.Round 11
Two test-coverage findings. The zero-wiring test was genuinely too narrow on both counts it was challenged on, and is rewritten in
bbaccfa0: the gate files are now discovered (everything under.github, any depth, any extension) rather than globbed fromworkflows/*.yml, and bothchange-summaryandchange_summaryare rejected, since the module path wires the command without ever writing the hyphen. Verified against three fixtures the previous version passed -- asettings.ymlnaming the command inrequired_status_checks, a nested composite action, and an underscore invocation in the Makefile. What a repository test cannot cover is stated in the docstring rather than implied: branch protection and rulesets are account-side, not repository text.The privacy finding is not reproducible --
test_degraded_inputs_carry_no_path_eithercombines the unreadable-file row with an absoluteCFS_DECISION_LOGoverride and asserts the project root, the override path, the home directory and the username are all absent from the serialised payload, with both degraded conditions asserted in force first so it cannot pass vacuously. It was present at31412cc0, the commit the re-verification names.Round 12
Two findings, both partly or wholly right, both fixed in
0ca73f2a.The tally label was a real misreport:
report.unreadableaggregates every reason no marker could be established -- undeterminable scope and an unexpected scan failure as well as a failed read -- while the line called it "could not be read or parsed", so a scope-policy failure was shown to a reader as a file-access one. Now "yielded no marker information". The counter was deliberately not split: the report's checkable invariant is that a marker-less row is in exactly one ofdeleted/unreadable/not_a_file, and four counters would put a counter on each cause and that invariant on none, for a distinction the per-filereasonalready carries exactly.The exit truth table was missing the parser forms -- only an unknown flag asserted exit 2, while an option given without its value is the form a person actually hits.
--base,--sinceand--rootwithout values plus an unexpected positional are now rows of the same table, eleven in total. The baseline matrix the finding also asked for was already rows 1--6 of that table and is unchanged; that part is answered on the thread with the row-by-row table.Round 13
Two findings.
GIT_DISCOVERY_ACROSS_FILESYSTEMwas a duplicate of the thread answered in round 12 and is fixed on #154, where the file lives.The other is right and fixed in
157dd25e:assert all(lines)passes on an empty list, so the no-blank-lines test accepted a digest that had vanished entirely -- and the same flaw ran through the twonot any(...)assertions above it, which are vacuously true on an empty list too, so all three of that test's assertions were satisfied by no output at all. Alen(lines) >= 3guard now comes first, sized to the three lines this fixture always yields rather than to a floor of one, which would still have accepted two of the three going missing.Round 14
Three findings, two of them real gaps in the tests and both fixed in
e252746b.The whole-payload privacy sweep combined an unreadable file with a shared log, but every event in it was dated and attributed, so the undated and runless fields were never in force while the path assertions ran -- and those two had only been exercised at unit level against an
EventSelection, never through the real serialisation. All four degradations now run in one CLI invocation, each asserted in force before absence is checked, so the test cannot pass by having nothing to leak.Separately, two independent exclusion rules had only their agreeing cases asserted:
_is_own_invocationdrops by command so a digest never reports its own footprint, while_TELEMETRY_EVENTSdrops by kind so no invocation is a decision whoever issued it. Aninvocationfrom another command is where they disagree, and that row is now pinned -- collapsing the two rules into one fails it.The third, a missing regression for injected git configuration masking untracked files, is not reproducible:
test_ambient_git_config_cannot_hide_a_new_file_from_the_listingis parametrized over all four injection interfaces and asserts the untracked file is still in the linkage, at line 592 oftests/test_change_summary_links.pyin this PR's diff.Round 15
One real defect, in the ordering rather than the counting, fixed in
73f4e92d.The four
decision log:lines are how the digest admits it could not see everything, and they were emitted after the per-run breakdown while the ceiling cuts from the end. So a change set large enough to reach ten lines made the digest stop reporting that three log lines were unreadable in order to keep printing which runs the readable ones came from -- the silent omission this command exists to avoid, arriving through the ordering. The breakdown now goes last, which also reads better: a caveat about the decision count sits beside that count.Stated as a priority rather than a guarantee. My first draft of the test claimed the four integrity lines always fit; the fixture disproved it, because the omission summary takes a slot of its own so even the smallest overflow costs two lines. The claim is gone rather than weakened, and the mitigation is named: the omission count plus the JSON, which carries every one of these counts as a field whatever the human rendering had room for. Spec steps 6 and 7 now carry the rule, including the fact that made this a defect and was previously written down nowhere -- that the ceiling cuts from the end, so emission order is sacrifice order.
Three further re-verifications reported against this head are already fixed in it, each verifiable with one
git showat19c26a27: the empty-digest guard (line 403), the exclusion-rule divergence test (line 561), and the undated/runless conditions in the privacy sweep (lines 821-822). Answered individually on their threads.Round 16 — rebased onto
main, #154 having merged#154 landed as
ac9e06e9, squash-merged, so this branch is rebased directly ontomainrather than restacked. Its fifteen commits replay unchanged and the diff is now exactly its own four files: the digest command, its CLI registration, its spec section and its test suite.The granularity story closes better than it opened, which was the point of the gating task behind it. Before this series the default branch sat at 0.4600 — exactly on the floor, less than one rounding step of buffer, so the next branch to add lines without declarations would have failed the gate and its author would reasonably have read the failure as their own regression. With #154 merged
mainis at 0.4603 (15.1 line-granularity units), and this branch takes it to 0.4605 (25.4 units). Both new modules sit above the system score rather than near it:utils/change_summary.pyat g = 0.5474 over 548 effective lines,commands/change_summary.pyat g = 0.5195 over 231.Re-verified on the new base: 5,635 passed, 4 skipped, 15 xfailed;
cfs validate0 errors; pylint 10.00;make vulture-ciclean.Round 17
Three findings, one of them this PR's.
The no-decisions line mislabelled its count.
len(events)is what is left after the window filter and after this command's own invocations are dropped, and the line called it "scanned" -- so a log holding a hundred older entries reported0 event(s) scannedhaving scanned all hundred. The word was wrong rather than the number; it now says how many events fell inside the window.Carrying the log's true total is refused, with a reason.
selection.scannedcounts this command's own logged invocations, so exporting it makes two consecutive digests of an unchanged repository differ by one -- the determinism_is_own_invocationexists to hold. I added it before checking and the existing determinism test caught it, so the omission is load-bearing and is now asserted as a deliberate absence. There is no self-excluded variant available: the selection keeps no events from outside the window, so their invocations cannot be identified and subtracted.The other two findings are not in this PR's diff and are answered in a comment below: a real newline-translation defect in
utils/change_summary.py, which merged with #154 and is fixed in #160 -- a path containing CR was reported under a name that opens nothing, so a tracked file present on disk came back as deleted -- and an observation aboutmake cidiscardingact's exit status, which came from #156 and is a maintainer's call.Round 18
One finding, and a fair one: the two filtering rules were each covered in slices, so no single assertion failed on the partition itself. Rather than add a fifth overlapping fixture, the one category that was missing -- a foreign command's invocation, the case the two rules disagree on -- joined the existing telemetry test, whose assertions now cover the whole population in both renderings. The
by_eventequality is what makes it a partition check: twoinvocationevents go in and exactly one comes out, so it fails whether the self-rule stops dropping ours or starts dropping the foreign one.Verified by the review's own criterion -- mutating either rule alone fails it. Its narrower siblings stay, since they pin what this one does not: that the exclusion is by kind rather than by instance, and that the divergent case holds through the real CLI rather than a hand-built selection.