Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,37 @@ Reverse-chronological, newest on top; prepend-only. Promote to `## YYYY-MM` sect

---

### 2026-08-12 · [issue #67] The stale-copy warning now names a remedy proportionate to the finding, and bare `--dry-run` no longer writes

- **Change:** `tools/methodology_dashboard.py` (+ `starter-kit/` twin, kept byte-identical) and
`tools/test_methodology_dashboard.py`. `DASHBOARD_VERSION` 2.10.5 → 2.10.6. Closes
[issue #67](https://github.com/KJ5HST/methodology/issues/67).
- **Defect 1 — a disproportionate remedy.** `check_stale_version()` answered "this one copy is
old" with `Re-sync: python3 <canonical> --sync`. But `--sync` is scoped from the **canonical's
own location**, not the working directory, so it rewrites every discovered sibling — measured at
26 files across 25 repos, including 7 creates in repos that do not gitignore the path and 1
where the file is git-tracked. An adopter following a one-line instruction verbatim dirtied
eight unrelated repositories. The warning now leads with the safe per-project action
(`cp <canonical> <this copy>`) and offers the portfolio path only as `--sync --dry-run`, with
its scope stated. **Why it matters beyond tidiness:** a remedy nobody can safely run is one
mechanism behind an *ignored* warning — in one adopter this line rode ~28 consecutive handoffs
unacted-on. The measurement was never missing; the actionable remedy was.
- **Defect 2 — a flag named `--dry-run` that writes.** It was consulted only inside the `--sync`
branch, so bare `--dry-run` fell through to a full scan and wrote `dashboard.html` *and*
appended to `dashboard_history.jsonl`. It is now an error (exit 2) that writes nothing.
Refusing rather than silently no-opping is deliberate: a silent no-op leaves the caller unable
to distinguish "nothing to do" from "flag ignored" — the same unreadable-signal class as
defect 1.
- **Tests:** new `TestCliRemedyProportionality` (3 cases, unit suite 208 → 211). Both defect
tests were driven RED against the pre-fix scanner and the failing run read, not assumed;
the third is a presence control (a plain run must still write `dashboard.html`), without which
a scanner that refused *every* invocation would pass and look fixed.
- **Scope deliberately not taken:** the issue also suggests `--sync-self` and a `--yes` gate on
`--sync`. Both change the CLI contract rather than fix a defect, so they are left for a
separate deliverable; the `cp` line already gives the per-project remedy with no new surface.
- **Distribution:** the scanner is `bin/_manifest.py`-TRACKED, so adopters receive both fixes via
`bin/sync`.

### 2026-08-11 · [ad hoc] `methodology_dashboard.py`'s `LANG_MAP`/`DOC_EXTS` now recognize R, Quarto, and R Markdown

- **Change:** `tools/methodology_dashboard.py` (+ `starter-kit/` twin, kept byte-identical) and
Expand Down
22 changes: 22 additions & 0 deletions HANDOFFS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,28 @@ This repository dogfoods its own methodology: every session records a durable, m

---

```handoff
session: S11
date: 2026-08-12
status: complete
self_score: 8
predecessor_score: 8
active_task: Fix issue #67 — `check_stale_version()` advertises `--sync` as the remedy for a stale copy, but `--sync` is scoped from the script's own location and writes 26 files across 25 sibling repos; and bare `--dry-run` is consulted only inside the `--sync` branch, so it falls through to a full scan and writes `dashboard.html` + `dashboard_history.jsonl`. ONE DELIVERABLE (one issue, two defects in the same CLI surface), not a vertical slice — no prior plan-mode contract, so it does not pass the FM #26 slice test and is not labelled one. COMPLETE — both fixed, RED-verified, PR opened.
what_was_done: Two commits after the claim — 026b4a6 (1B claim), 0f30cb8 (both defects + tests + ledger). THIS SESSION WAS MOSTLY REVIEW, NOT AUTHORING — it began by reviewing PRs #68–#72 and merging all five, and the issue work came after; see the review docs in the (non-git) oversight dir `/Users/terrell/code/docs/reviews/`. (1) DEFECT 1 was fixed as a MESSAGE change, not a behavior change: `--sync` still does exactly what it did, but the staleness warning no longer advertises it as the answer to "this copy is old". The per-project `cp` line is now first; the portfolio path appears only as `--sync --dry-run` with its blast radius stated inline. (2) DEFECT 2 is now an error with exit 2, deliberately NOT a silent no-op — a no-op leaves the caller unable to tell "nothing to do" from "flag ignored", which is the same unreadable-signal class as defect 1. (3) I DID NOT TAKE THE ISSUE'S OTHER TWO SUGGESTIONS (`--sync-self`, a `--yes` gate): both change the CLI contract rather than fix a defect, and the `cp` line already supplies the per-project remedy with no new surface area. Recorded in the ledger entry as deliberately-not-taken so the next session does not read it as an oversight. Unit suite 208 → 211; `bin/tests.sh` 114/114 unchanged (no shell checks added).
next_steps: (a) PR IS OPEN AND UNMERGED — `fix/issue-67-stale-version-remedy`, and issue #67 stays OPEN until it merges (S9's precedent: closing on an unmerged implementation would be false). (b) THE TWO UNTAKEN SUGGESTIONS ARE A REAL FOLLOW-UP, NOT NOISE: `--sync-self` and the `--yes`/scope gate on `--sync` — the issue's measured 26-file/25-repo blast radius is unchanged by this fix, which only stops the tool from *recommending* it. File as a backlog item or a new issue before it is forgotten. (c) DASHBOARD_VERSION IS NOW 2.10.6 AND FOUR VERSIONS SHIPPED IN ONE DAY (2.10.3 #71, 2.10.4 #70, 2.10.5 #72, 2.10.6 this) — `dashboard_history.jsonl` carries a `dashboard_version` stamp per entry, so the trend is interpretable, but there is still NO `Released` ledger entry for any of it since v3.6. A dot release is the operator's call. (d) DERIVED, NOT GUESSED — `git merge-tree --write-tree --name-only origin/main fix/issue-67-stale-version-remedy` returns NO conflicting paths as of this commit; re-derive rather than trust it if anything lands on `main` first, since both CHANGELOG.md and HANDOFFS.md prepend anchors were written here.
key_files: tools/methodology_dashboard.py:583 (check_stale_version, with the new `cp`-first message at :605 and the comment recording WHY the old remedy was disproportionate), tools/methodology_dashboard.py:3323 (main — the bare `--dry-run` refusal, placed AFTER the `--sync` branch so `--sync --dry-run` still routes to sync), tools/methodology_dashboard.py:668 (print_usage — both flags re-documented), tools/test_methodology_dashboard.py:2142 (TestCliRemedyProportionality — subprocess-driven because importing the module cannot observe main()'s arg handling), starter-kit/methodology_dashboard.py (the byte-identical twin — edit BOTH or the twin-diff test fails), CHANGELOG.md:38 (this session's entry)
gotchas: (1) THE UNIT TESTS FOR THIS FIX MUST BE SUBPROCESS-BASED. Importing the module cannot reach `main()`'s argument handling at all, so an import-based test would assert nothing about either defect while looking like coverage. (2) DRIVING RED REQUIRES SWAPPING THE SCANNER FILE, AND S10 LOST A PATCH DOING EXACTLY THAT when a 2-minute timeout killed the run mid-swap. I backed the file up OUTSIDE the repo first and verified the restore with `cmp` against the backup plus a twin-identity check — do the same, and never rely on a trailing restore line in the same command. (3) `bin/tests.sh` TAKES OVER 2 MINUTES and will time out on a default bash timeout; run it with an explicit long timeout or not at all. (4) THE PRESENCE CONTROL IS LOAD-BEARING — without `test_a_normal_run_still_writes`, a scanner that refused every invocation would satisfy the dry-run test and look fixed. It passed in the RED run too, which is what proves the other two failures were specific. (5) STILL NO `core.hooksPath` IN THIS CLONE (verified unset again this session), so `.githooks/pre-commit` never runs; both commits used `--no-verify` and the ledger was co-staged by hand. This is now the third consecutive session to record the same thing — the gate is real but is not running for anyone here, which is worth fixing at the clone level rather than re-noting.
runtime_smoke: n/a as an application launch — this repo ships no application. Build-equivalent: unit suite 211/211 OK, `bin/tests.sh` 114/114 (0 failed), `bin/check-links` OK (83 links / 21 files), `bin/check-handoff` OK, twins byte-identical. BOTH DEFECTS ALSO VERIFIED END TO END AGAINST A REAL SCRATCH PORTFOLIO, not only through the suite: bare `--dry-run` on the pre-fix scanner exited 0 and wrote `dashboard.html` + `dashboard_history.jsonl`, and on the fixed scanner exited 2 and wrote nothing; a stale v2.6.1 copy under a `<portfolio>/methodology/starter-kit/` canonical printed the old bare-`--sync` line before and the `cp`-first line after. BOTH DEFECT TESTS DRIVEN RED FIRST and the failing run read (2 failed, 1 control passed) against the pre-fix scanner swapped in from `origin/main`. Learning #10 corpus sweep executed rather than assumed: grepped all markdown for the `--sync` remedy and found no live operative site — the only hits are frozen `docs/planning` records left verbatim per the v2.7.1 precedent.
changelog_ref: CHANGELOG.md "2026-08-12 · [issue #67] The stale-copy warning now names a remedy proportionate to the finding, and bare `--dry-run` no longer writes" — one entry, one source tag, covering both defects of the one deliverable
commit: 026b4a6 (1B claim) + 0f30cb8 (both defects) + this commit (close-out)
```

**Self-assessment: 8/10.** Plus: both defects reproduced against a real scratch portfolio before and after the change rather than argued from the diff; both driven RED first with the failing run actually read, and the presence control confirmed non-vacuous by passing in the same RED run; the file-swap was backed up outside the repo and the restore verified by `cmp` plus a twin-identity check, which is the specific trap S10 recorded; the Learning #10 sweep was run and returned a real (negative) answer rather than being asserted; the two untaken suggestions were recorded as deliberate in both the ledger and this receipt so they cannot be misread as oversights. Minus: I invented a constant (`SYNC_SCOPE_HINT`) in the first edit and had to correct it — a name I had not defined, caught only because I re-read my own edit; the review half of this session merged five PRs before the issue work began, which is more than "1 and done" contemplates even though merging is not authoring; and I left the issue's measured 26-file blast radius entirely unaddressed, which is defensible but means #67's headline number is still true after its own fix.

**Predecessor (S10): 8/10.** What helped, specifically: gotcha (1) — "the suite takes longer than 2 minutes and WILL time out, and it left the patch reverted in the working tree; back up OUTSIDE the repo first and verify the restore" — is the single reason this session's RED swap was safe rather than lucky; I copied the file to the scratchpad and `cmp`-verified the restore before trusting the tree, and the same 2-minute timeout did fire here on `bin/tests.sh`. Gotcha (4) — a 1B stub carries only STUB_KEYS, `self_score: pending` is a hard error — meant the claim stub passed `check-handoff` on the first try. Gotcha (5)'s note about `core.hooksPath` being unset in this clone explained the `--no-verify` convention before I could mis-diagnose it. Not 9 or 10: `next_steps` (a) said PR #66's two review threads were unresolved on GitHub and should be resolved or replied to — that is a GitHub-state claim with no derivation given, and it had been overtaken by events by the time I read it (the PR merged). A forward-looking claim about *someone else's* state decays fastest of all, which is Learning #13's own point pointed one step further out than the row currently reaches.

---

```handoff
session: S10
date: 2026-08-10
Expand Down
37 changes: 34 additions & 3 deletions starter-kit/methodology_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
# Every other copy (portfolio root + per-project) is a synced copy of the canonical and must
# carry the same value. A copy whose DASHBOARD_VERSION is older than the canonical is stale —
# re-sync from the canonical. Bump on any change to the canonical script.
DASHBOARD_VERSION = "2.10.5"
DASHBOARD_VERSION = "2.10.6"

ROOT = Path(__file__).parent
EXCLUDE_DIRS = {"methodology", "BrogueCE-iOS", ".git", "__pycache__", "node_modules", ".venv", "venv"}
Expand Down Expand Up @@ -589,10 +589,23 @@ def check_stale_version():
return
canon_ver = parse_version(canonical)
if canon_ver and version_key(canon_ver) > version_key(DASHBOARD_VERSION):
# The remedy must be PROPORTIONATE TO THE FINDING (issue #67). The finding is "this one
# copy is old"; the old remedy was `--sync`, which is scoped from the CANONICAL's location
# rather than the working directory, so it rewrites every discovered sibling — measured at
# 26 files across 25 repos, including 7 creates in repos that do not gitignore the path and
# 1 target where the file is git-tracked. An adopter who follows a one-line instruction
# verbatim should not dirty eight unrelated repositories.
#
# A disproportionate remedy is one mechanism behind an IGNORED warning: in one adopter this
# staleness line rode ~28 consecutive handoffs unacted-on. The measurement was never the
# missing part — the safe per-project action is one `cp`, and the message never printed it.
sys.stderr.write(
f" ⚠ methodology_dashboard.py is stale: this copy is v{DASHBOARD_VERSION}, "
f"canonical is v{canon_ver}.\n"
f" Re-sync: python3 {canonical} --sync\n"
f" Re-sync THIS project: cp {canonical} {self_path}\n"
f" Re-sync the PORTFOLIO: python3 {canonical} --sync --dry-run\n"
f" (rewrites EVERY discovered sibling repo — preview with\n"
f" --dry-run first, then re-run without it to apply)\n"
)


Expand Down Expand Up @@ -662,8 +675,11 @@ def print_usage():
print(" --with-submodules In single-project mode, also scan git submodules as")
print(" separate entries (default: scan the project only).")
print(" --sync Copy the canonical dashboard to the portfolio root and")
print(" every discovered project (use --dry-run to preview).")
print(" EVERY discovered project (use --dry-run to preview first).")
print(" Scoped from the canonical's location, not the working")
print(" directory: to update one project, copy the file instead.")
print(" --dry-run With --sync, show planned changes without writing.")
print(" On its own it is an error, never a silent full run.")
print(" -h, --help Show this help and exit.")


Expand Down Expand Up @@ -3296,6 +3312,21 @@ def main():
sync_dashboards(Path(__file__).resolve().parent, dry_run="--dry-run" in args)
return

# A flag named --dry-run must never write (issue #67). It is consulted ONLY inside the --sync
# branch above, so bare `--dry-run` used to fall straight through to a full scan that wrote
# dashboard.html AND appended to dashboard_history.jsonl — the exact opposite of what the name
# promises, and silently, since nothing said the flag had been ignored. Refuse rather than
# no-op: a silent no-op leaves the caller unable to tell "nothing to do" from "flag ignored",
# which is the same class of unreadable signal as the defect above.
if "--dry-run" in args:
sys.stderr.write(
" --dry-run applies to --sync only; on its own it would have run a full scan and\n"
" written dashboard.html + dashboard_history.jsonl. Refusing rather than writing.\n"
" Preview a sync: python3 methodology_dashboard.py --sync --dry-run\n"
" Generate normally: python3 methodology_dashboard.py\n"
)
sys.exit(2)

# Warn (best-effort) if this copy is older than the canonical.
check_stale_version()

Expand Down
Loading