diff --git a/docs/backlog/P2/B-0086-port-tools-hygiene-python-to-typescript-bun-aaron-2026-04-28.md b/docs/backlog/P2/B-0086-port-tools-hygiene-python-to-typescript-bun-aaron-2026-04-28.md new file mode 100644 index 00000000..8799f3f1 --- /dev/null +++ b/docs/backlog/P2/B-0086-port-tools-hygiene-python-to-typescript-bun-aaron-2026-04-28.md @@ -0,0 +1,123 @@ +--- +id: B-0086 +priority: P2 +status: open +title: Port tools/hygiene Python scripts to TypeScript/Bun (factory-default; AI/ML carve-out applies) +tier: factory-tooling +effort: M +ask: maintainer Aaron 2026-04-28T19:56Z (TypeScript/Bun-as-default substrate framing) +created: 2026-04-28 +last_updated: 2026-04-28 +composes_with: + - B-0061 +tags: [aaron-2026-04-28, typescript, bun, factory-default, language-discipline, port-candidate] +--- + +# B-0086 — Port `tools/hygiene/` Python scripts to TypeScript/Bun + +## Source + +Aaron 2026-04-28T19:56Z verbatim (during autonomous-loop tick +right after `sort-tick-history-canonical.py` was used to fix a +chronological-order lint failure on PR #684): + +> *"sort-tick-history-canonical.py eventually we are going to use +> the typescript like ../scratch unless this is AL/ML AND is a +> better fit for python? typescript/bun being our default, we +> need to decide when to step out on typescript carefully."* + +Substrate captured durably in +`memory/feedback_typescript_bun_default_step_out_carefully_aaron_2026_04_28.md`. + +## Scope + +Port the following non-AI/ML Python scripts under `tools/` to +TypeScript on Bun, following the existing pattern at +`tools/invariant-substrates/tally.ts` (script entry in root +`package.json` under `scripts:`): + +- `tools/hygiene/sort-tick-history-canonical.py` — canonical + chronological sort + dedupe of `docs/hygiene-history/loop-tick-history.md`. + Pure markdown/string manipulation; trivial Bun port. +- `tools/hygiene/fix-markdown-md032-md026.py` — markdown + formatting fixes (MD032 + MD026 rules). Pure string/regex + manipulation; trivial Bun port. +- (Audit other `tools/**/*.py` per future review pass.) + +## Deferred — port doesn't have to be immediate + +Per Aaron's framing *"we need to decide when to step out on +typescript carefully"*, the discipline applies to: + +1. **All NEW tooling**: default to TypeScript/Bun. +2. **Existing Python tooling**: port when changes are substantive; + leave alone otherwise (rewrite churn isn't free). + +This row tracks the **port-when-it-makes-sense** plan. +Triggering events that make it "make sense": + +- Python script needs a substantive feature add (port instead of + extend). +- Python toolchain breaks on a contributor's machine (port to + remove the cross-toolchain dependency). +- Audit pass identifies enough small Python scripts that a + bundle-port amortizes the work. + +## Why P2 (not P0/P1) + +- **Existing Python tools work** — no acute breakage; the + discipline applies forward, not as emergency cleanup. +- **Lint hooks already enforce** — `tools/hygiene/sort-tick-history-canonical.py` + is invoked from CI lint job; whether it's Python or TypeScript + doesn't change correctness. +- **Heaviest tooling already TypeScript** — `tally.ts` (the + invariant-substrate analyzer) is already TypeScript; the + Python residue is small + scoped. + +## Acceptance criteria + +- [ ] `tools/hygiene/sort-tick-history-canonical.py` ported to + `tools/hygiene/sort-tick-history-canonical.ts` (or similar) + with `package.json` script entry. +- [ ] `tools/hygiene/fix-markdown-md032-md026.py` ported similarly. +- [ ] All callers (CI workflows, pre-commit hooks, manual usage + in tick-history workflow) updated to invoke the TS version. +- [ ] Python originals deleted (the discipline is consistency, + not parallel-implementations). +- [ ] One trial round of canonical-sort run via the TS port to + verify equivalent output on `loop-tick-history.md`. + +## Composes with + +- `memory/feedback_typescript_bun_default_step_out_carefully_aaron_2026_04_28.md` + — the substrate this row operationalizes. +- `tools/invariant-substrates/tally.ts` — the existing TypeScript + pattern to mirror. +- `package.json` — root scripts entry point + `bun@1.3.13` pin. +- B-0061 (per-row backlog migration) — same shape of "incremental + migration with discipline applied to NEW work + opportunistic + port on existing". + +## What this is NOT + +- **NOT a port-all-Python directive.** AI/ML scripts (if any + exist or are added later) keep Python per the carve-out. +- **NOT urgent factory-fitness work.** P2 reflects the + do-when-substantive cadence, not "ship next round". +- **NOT a TypeScript-everywhere-in-Zeta directive.** F# is the + language for the Zeta library proper; TypeScript is the + language for tooling around it. Two-tier choice. + +## Pickup notes for future-Otto + +When picking this up: + +1. Read the substrate memory first for the discipline framing. +2. Start with `sort-tick-history-canonical.py` (smaller surface, + pure markdown manipulation, has a CI lint job that exercises + it — easy to verify equivalence). +3. Mirror the `tally.ts` pattern for `package.json` scripts + + tsconfig + bun-test setup. +4. Verify lint job continues passing on PR with the port. +5. Delete the Python original in the same PR (avoid parallel + implementations). diff --git a/memory/MEMORY.md b/memory/MEMORY.md index 11744e71..8b785f9d 100644 --- a/memory/MEMORY.md +++ b/memory/MEMORY.md @@ -1,7 +1,10 @@ [AutoDream last run: 2026-04-23] -**📌 Fast path: read `CURRENT-aaron.md` and `CURRENT-amara.md` first.** These per-maintainer distillations show what's currently in force. Raw memories below are the history; CURRENT files are the projection. (`CURRENT-aaron.md` refreshed 2026-04-28 with sections 26-29 — speculation rule + EVIDENCE-BASED labeling + JVM preference + dependency honesty + threading lineage Albahari/Toub/Fowler.) +**📌 Fast path: read `CURRENT-aaron.md` and `CURRENT-amara.md` first.** These per-maintainer distillations show what's currently in force. Raw memories below are the history; CURRENT files are the projection. (`CURRENT-aaron.md` refreshed 2026-04-28 with sections 26-29 — speculation rule + EVIDENCE-BASED labeling + JVM preference + dependency honesty + threading lineage Albahari/Toub/Fowler.) +- [**Incomplete Source-Set Regeneration Hazard + Workflow Null-Result Audit Signal — Amara class names + controls (2026-04-28)**](feedback_incomplete_source_set_regeneration_hazard_and_workflow_null_result_audit_amara_2026_04_28.md) — Two reusable classes: (1) "regenerate from sources" tools become destructive when source-set is incomplete; control is `--check` / `--stdout` first, force-write only after completeness proven. (2) `gh run list []` on existing workflow is audit signal not conclusion; six diagnostic questions (too-new / disabled / non-default-branch / cron / event-trigger / identifier-filter). Both fold into task #269. +- [**Chronological Insertion Polarity Error — class name + append-only-on-oldest-first discipline (Amara 2026-04-28)**](feedback_chronological_insertion_polarity_error_amara_class_name_otto_2026_04_28.md) — Edit-tool prepend on oldest-first append-only files = chronological reversal. Discipline: `cat >> file <> file <<'EOF'` (always-append semantics) OR run `tools/hygiene/sort-tick-history-canonical.py` post-edit. Mechanism-over-vigilance: the tick-history lint hook caught it within 1 minute. Reusable; small; demonstrates 'stability is the substrate of velocity'. +type: feedback +--- + +# Chronological Insertion Polarity Error + +## Class name (Amara 2026-04-28T19:58Z) + +**Chronological Insertion Polarity Error** — when an edit +operation's directional semantics (prepend / insert-before / +unshift) are applied to a file whose invariant is the opposite +direction (oldest-first; newest at end). The mistake is tiny; +the class is reusable. + +## Concrete incident (Otto 2026-04-28T19:54Z) + +- File: `docs/hygiene-history/loop-tick-history.md`. +- Invariant: oldest tick at top, newest tick at bottom (rows + ordered by ISO timestamp ascending). +- Mistake: used `Edit` tool with `old_string = "| 2026-04-28T19:41:27Z ..."` + and `new_string = "| 2026-04-28T19:50:30Z ...\n| 2026-04-28T19:41:27Z ..."`. + This **prepended** the 19:50 row in front of the 19:41 row. + But oldest-first means 19:41 should come BEFORE 19:50 — my + prepend was chronological reversal. +- Caught: by `lint (tick-history order)` CI job within ~1 min + of push to PR #684. +- Fixed: `python3 tools/hygiene/sort-tick-history-canonical.py` + (the canonical-sort script designed exactly for this). +- Net cost: 1 minute of CI feedback + 1 small commit + 1 + force-push. + +## Causal chain (Amara's framing) + +``` +mistake → lint catches it within 1 minute → PR blocked before +merge → canonical write pattern discovered → future rule encoded +``` + +This is **mechanism-over-vigilance** working as designed: the +hook substrate makes the mistake cheap, fast, and +non-catastrophic. Instead of "be more careful next time," the +factory has a substrate-level answer: append safely, or +canonicalize after editing. + +## The discipline + +When adding a row to a file with oldest-first append-only +invariant (history files; tick logs; chronological journals): + +1. **Prefer append-only semantics**: + + ```bash + cat >> path/to/history <<'EOF' + | 2026-04-28T19:50Z (...) | ... | + EOF + ``` + + Always-append is impossible to get the polarity wrong. + +2. **OR sort-canonically post-edit**: + + ```bash + python3 tools/hygiene/sort-tick-history-canonical.py + ``` + + The script reorders rows + dedupes; safe even if your edit + landed at the wrong position. + +3. **Don't prepend or insert-before via `Edit` tool** unless you + have explicitly verified the file's polarity invariant. + +## Lint hook control + +The CI lint job +(`lint (tick-history order)` in +`.github/workflows/.yml`) is the +**mechanism-over-vigilance** control. Without it, the +chronological reversal would have merged silently and corrupted +the durable history. With it, the mistake costs 1 minute of CI +feedback. + +## Generalization beyond tick-history + +The same class applies anywhere an append-only history file is +in use: + +- `docs/hygiene-history/loop-tick-history.md` (this incident) +- `docs/budget-history/snapshots.jsonl` (oldest-first JSONL, + similar polarity invariant — but JSONL appends are line-based + so the bug shape is different; still an append-only invariant) +- ADR records (`docs/DECISIONS/`) — filename-dated; insertion + order is timestamp-ordered by file naming convention +- Round-history (`docs/ROUND-HISTORY.md`) — chronological +- (Any future append-only journal) + +For each: the lint hook + the canonical-sort tool are the +control pair. If a new history file lacks both, file as a +factory-hygiene gap. + +## What this is NOT + +- **NOT a directive to be more careful**. The discipline is + mechanism-based, not vigilance-based. Future-Otto WILL make + this mistake again on a new file; the lint hook catches it + again; that's the design. +- **NOT a ban on `Edit` tool for history files**. `Edit` is + fine for FIXING rows that already exist (typo fixes, wording + corrections). The ban is on `Edit`-prepend for NEW rows. +- **NOT specific to tick-history.md**. The class generalizes; + the specific incident was just the worked example. + +## Stability is the substrate of velocity (Amara's framing) + +This bug is small + boring; that's the proof. The factory's +ability to absorb a chronological-reversal mistake without +slowing down — the lint catches it, the canonical-sort fixes +it, the work continues — IS what stability buys. Without the +mechanism, this class of mistake would either be a manual +review-time catch (slow, expensive) or a silent corruption +(catastrophic if undiscovered). + +The discipline scales: every mechanism-over-vigilance hook +added (to any history file, any append-only structure) adds +this kind of resilience. + +## Composes with + +- `tools/hygiene/sort-tick-history-canonical.py` — the + canonical-sort script that fixes the bug after-the-fact. +- `memory/feedback_orthogonal_axes_factory_hygiene.md` — + hygiene-axes design heuristic; this class is "polarity" + axis. +- `memory/feedback_destructive_git_op_5_pre_flight_disciplines_codex_gemini_2026_04_28.md` + — same shape of "small mistake, large potential impact, + controlled by mechanism not vigilance". +- `memory/feedback_emit_empty_security_result_on_conditional_skip_ci_maturity_pattern_aaron_2026_04_28.md` + — same family: design the hook in advance, let the metric + self-heal / let the lint catch / let the mechanism do the + work. +- `memory/feedback_self_healing_metrics_on_regime_change_factory_design_principle_aaron_2026_04_28.md` + — adjacent: self-healing for metrics; mechanism-over-vigilance + for code/data hygiene; both are "design substrate that makes + failures cheap" patterns. + +## Pickup notes for future-Otto + +If you find yourself about to `Edit` a history file with a new +row: + +1. **Stop.** Use `cat >> file <` returning `[]` for an existing workflow is a reusable audit signal, not a conclusion; inspect six diagnostic questions (too-new-to-fire / disabled / non-default-branch / cron-mismatch / event-trigger-incompatible / wrong-identifier-filter). Both fold into task #269 cadenced-counterweight-audit. +type: feedback +--- + +# Two Amara-named classes from PR #683 work + +## Source + +Amara 2026-04-28T20:00Z review of Otto's PR #683 close-out +insight (forwarded by Aaron via /btw aside style). Amara +sharpened two reusable patterns into named classes with +explicit controls. + +## Class 1 — Incomplete Source-Set Regeneration Hazard + +### Definition + +When tooling **regenerates a canonical artifact from a set of +source files**, the regeneration is destructive if the +source-set is **incomplete**. The artifact loses any content +that was supposed to be in the artifact but isn't yet +represented in the sources. + +### Concrete incident (Otto 2026-04-28T19:51Z) + +- Tool: `tools/backlog/generate-index.sh` + (with `BACKLOG_WRITE_FORCE=1`). +- Source set: per-row files under `docs/backlog/PN/B-NNNN-*.md`. +- Canonical artifact: `docs/BACKLOG.md`. +- Source-set state: **incomplete migration in progress** (per + B-0061; the legacy stockpile of un-migrated rows is still + embedded in the artifact and is NOT yet represented in + per-row files). +- What happened: ran the generator → it wrote a 108-line + auto-generated stub that drops the un-migrated content → + 17000+ lines clobbered. +- Caught: by `git diff --stat` immediately after commit, + before push. +- Recovery: `git checkout HEAD~1 -- docs/BACKLOG.md` + commit + amend. +- Net cost: 1 minute of recovery work; no published damage. + +### The control (Amara's prescription) + +Generators that overwrite canonical state need: + +1. **`--check` mode first** — fails if the regenerated output + would differ from the canonical state. Safe to run; never + writes. +2. **`--stdout` / diff-preview second** — print the + regenerated content (or diff against canonical) without + writing. Lets the operator inspect the proposed change. +3. **Force-write only after migration/source-completeness is + proven**. The "force" flag is gated by an explicit human + or automated assertion that the source-set is now complete. + +This matches the existing counterweight taxonomy: + +- **Cheap prevention**: `--check` mode in CI lint job (catches + drift between sources and artifact without ever writing). +- **Cadenced detect+repair**: periodic `--check` runs on the + artifact; failure = either source-set still incomplete OR + artifact has been hand-edited and needs re-regeneration. +- **Defense-in-depth for high-cost misses**: pre-push hook + + branch-protection + this-row's-discipline. + +### Generalizes to + +Anywhere a generator regenerates a canonical artifact from a +multi-file source set: + +- `docs/BACKLOG.md` ← per-row backlog files (this incident) +- `docs/MEMORY.md` (in-repo if/when one exists) ← per-memory + files +- Generated docs (e.g. API reference) ← source code annotations +- Any "compile multiple files into one canonical bundle" tool + +For each: identify the canonical artifact + its source set, +verify completeness invariant, design `--check` / `--stdout` +modes, gate force-write. + +## Class 2 — Workflow Null-Result Audit Signal + +### Definition + +When `gh run list --workflow=` (or equivalent) returns +`[]` (no runs) for a workflow that **exists in the repository**, +the empty result is **a reusable audit signal, not a +conclusion**. Inspect the cause before drawing conclusions. + +### Concrete incident (Otto 2026-04-28T19:46Z) + +- Workflow: `.github/workflows/budget-snapshot-cadence.yml`. +- Query: `gh run list --workflow=.github/workflows/budget-snapshot-cadence.yml --limit=5`. +- Result: `[]`. +- Initial conclusion: "the workflow has never fired". +- Actual cause: workflow was committed to LFG main today + (2026-04-28T16:22Z); cron is `23 16 * * 0` (Sundays only); + next natural fire is 2026-05-03 (after task #287 deadline); + too-new-to-fire AND deployment-context-mismatch combined. + +### The six diagnostic questions (Amara's checklist) + +When `gh run list --workflow=` returns `[]` for an +existing workflow: + +1. **Has it ever fired?** — check workflow file's first commit + date vs cron schedule's first natural fire date. +2. **Is it on the default branch?** — GitHub Actions scheduled + workflows only run from the default branch. A workflow file + in a feature branch will never fire on schedule. +3. **Is it disabled?** — public-repo schedules can be disabled + after inactivity (60+ days no commits to the workflow). + `gh run list --all` may show runs for disabled workflows + that the default `gh run list` filters out. +4. **Is the cron valid UTC?** — cron is always UTC; a `0 9 * * *` + that the author intended as "9am local" fires at the wrong + time. Validate against `crontab.guru` or similar. +5. **Is the event trigger compatible with this deployment context?** + — `pull_request_target` from external forks needs different + permissions; `workflow_run` requires the upstream workflow + to fire first; `schedule` needs default-branch (see #2). +6. **Are we filtering by the right workflow identifier?** — + `gh run list --workflow` accepts name (display name from + YAML), ID (numeric), or filename (path). Mismatched + identifier returns `[]` even when runs exist. + +### The control + +Fold the six-question checklist into task #269 +(cadenced-counterweight-audit skill). The audit's cadence is +the right home: every audit cycle, walk all workflows in +`.github/workflows/`, query `gh run list`, and run the +six-question check on any that return `[]`. + +This matches the counterweight taxonomy's three-phase shape: + +- Phase 1: shell tooling (`scripts/audit-workflow-coverage.sh` + walks all workflows, runs `gh run list`, applies the six + checks, prints findings). +- Phase 2: skill wrapper (cadenced-counterweight-audit invokes + the script + interprets findings). +- Phase 3: tick-open hook (auto-runs on session-start when no + recent audit has been done). + +### Critical caveat (Amara's "tiny blade note") + +**Don't overfit the signal to two causes.** The original Otto +framing collapsed the six questions to "too-new-to-fire OR +cron-doesn't-fit-deployment-context". That's incomplete — +disabled-workflow and event-trigger-incompatibility are +genuinely distinct failure modes that need their own +inspection. + +The class name **Workflow Null-Result Audit Signal** preserves +the broader category; the six questions enumerate the failure +modes; the control invokes them in order. + +## Both classes compose with + +- `tools/hygiene/sort-tick-history-canonical.py` + the + Chronological Insertion Polarity Error class + (`memory/feedback_chronological_insertion_polarity_error_amara_class_name_otto_2026_04_28.md`) + — same factory-design philosophy: substrate-level controls + beat vigilance. +- `memory/feedback_destructive_git_op_5_pre_flight_disciplines_codex_gemini_2026_04_28.md` + — adjacent: 5 pre-flight disciplines for destructive git + operations. Generator-clobber is in the same family. +- `memory/feedback_emit_empty_security_result_on_conditional_skip_ci_maturity_pattern_aaron_2026_04_28.md` + — same shape of CI maturity: design the workflow's coverage + semantics carefully so the metric is honest. +- `memory/feedback_self_healing_metrics_on_regime_change_factory_design_principle_aaron_2026_04_28.md` + — same shape of "design the substrate, let the mechanism do + the work". +- Task #269 (cadenced counterweight-audit skill) — both classes + are direct entries for this skill's audit catalogue. +- Task #287 (cost-visibility deadline window) — Workflow + Null-Result class identified the gap that fed B-0085. + +## What this is NOT + +- **NOT a directive to retrofit `--check` modes everywhere + immediately.** The discipline applies forward to new + generators + opportunistically to existing ones during + substantive changes. +- **NOT a ban on regenerating artifacts from sources.** That's + the right pattern; the discipline is about the safe-mode + controls around it. +- **NOT specific to GitHub Actions.** The Workflow Null-Result + class generalizes to any cadenced workflow / cron / scheduled + job whose run-history can be empty: GitLab CI, Jenkins, + CircleCI, internal cron daemons, systemd timers. + +## Pickup notes for future-Otto + +When you see `gh run list --workflow=` return `[]`: + +1. Don't conclude "never fired". +2. Run through the six diagnostic questions. +3. If the cause is too-new-to-fire AND a deadline-window + matters, file a backlog row asking the maintainer to + manually dispatch (don't dispatch autonomously — see + visibility-constraint). +4. If the cause is disabled / non-default-branch / cron-bug / + event-trigger-bug, the workflow is BROKEN; file as P1+ + factory-hygiene gap. + +When you write a new generator that overwrites canonical state: + +1. Default mode is `--check` (read-only diff against canonical). +2. Add `--stdout` for preview. +3. Force-write requires explicit flag (`--write` or + `BACKLOG_WRITE_FORCE=1`-style), and DOCUMENT in the script + header what completeness invariant the force-write requires. +4. Wire `--check` into CI lint as the cheap-prevention layer. diff --git a/memory/feedback_typescript_bun_default_step_out_carefully_aaron_2026_04_28.md b/memory/feedback_typescript_bun_default_step_out_carefully_aaron_2026_04_28.md new file mode 100644 index 00000000..daf31603 --- /dev/null +++ b/memory/feedback_typescript_bun_default_step_out_carefully_aaron_2026_04_28.md @@ -0,0 +1,134 @@ +--- +name: TypeScript/Bun is the default; step out on TypeScript carefully (Aaron 2026-04-28) +description: Aaron 2026-04-28T19:56Z framing — TypeScript+Bun is the factory's default scripting/tooling language; Python is the carve-out for AI/ML where it's a better fit. Stepping out on TypeScript needs a justified reason (AI/ML primary library availability is the canonical one). Applies to all NEW tooling and to PORT-CANDIDATES on existing Python tooling that has no AI/ML reason to be Python. +type: feedback +--- + +# TypeScript/Bun is the default — step out carefully + +## The rule (Aaron verbatim 2026-04-28T19:56Z) + +> *"sort-tick-history-canonical.py eventually we are going to use the +> typescript like ../scratch unless this is AL/ML AND is a better fit +> for python? typescript/bun being our default, we need to decide when +> to step out on typescript carefully."* + +## What this codifies + +**Default tooling language:** TypeScript on Bun +(`bun@1.3.13` per `package.json`). + +**Step-out threshold:** explicit justification — usually +**AI/ML primary library availability** (the canonical reason +for Python: numpy, scipy, scikit-learn, transformers, +ml-frameworks-with-no-bun-equivalent). + +**Sibling-repo precedent:** `../scratch` (sibling to Zeta) +runs the same TypeScript+Bun substrate (bun.lock at root, +declarative + docker subdirs). The factory's tooling default +is consistent across sibling repos, not just within Zeta. + +## Why + +- **Substrate consistency.** Tooling spread across two languages + doubles the install cost (Python + Bun toolchains), doubles the + test surface, doubles the dependency-update burden, and forces + contributors to context-switch. +- **Bun is fast enough for tooling.** Bun startup + execution is + competitive with Python for the kind of work tooling does + (markdown manipulation, git invocation, JSON munging, file + walks). The historic Python advantage (faster prototyping, more + scripting batteries) has eroded as Bun's stdlib + ecosystem + matured. +- **AI/ML is the legitimate carve-out.** numpy / pandas / pytorch + / transformers / scikit-learn are not optional in ML work and + have no Bun-native equivalents. For ML scripts, Python remains + correct. + +## When to STEP OUT on TypeScript (i.e. write Python or other) + +Bar is high. Justify with one of: + +1. **AI/ML library is load-bearing** — the script computes against + `numpy` / `pandas` / `torch` / `transformers` / similar, and a + pure-TypeScript implementation would re-implement the library. +2. **Existing Python skeleton with deep dependency** — the work is + a small extension of an existing Python tool that would cost + more to port than to extend. +3. **One-shot research script that won't outlive the round** — + ephemeral scratch work where language choice doesn't compound. +4. **Native dependency that's Python-only** — e.g. a vendored CLI + that ships Python bindings only. + +If none of those apply: write TypeScript on Bun. + +## When NOT to step out (i.e. default to TypeScript even though +Python feels easier) + +- "It's a quick script" — quick scripts compound. Today's + one-shot is tomorrow's `tools/hygiene/`. +- "I know Python better" — agent fluency is not the criterion; + factory consistency is. +- "Bash is shorter" — bash is fine for ≤10-line shell glue + (Otto-235 4-shell discipline still applies); past that, + TypeScript. +- "Markdown manipulation feels Python-y" — it isn't. Bun has + excellent string handling + fast file IO; the + `tools/invariant-substrates/tally.ts` pattern shows the shape. + +## Concrete port candidates (existing Python tooling) + +Audit `tools/**/*.py` for non-AI/ML scripts that should be +TypeScript: + +- `tools/hygiene/sort-tick-history-canonical.py` — markdown + table sort + dedupe; no AI/ML reason; **B-0086 port candidate**. +- `tools/hygiene/fix-markdown-md032-md026.py` — markdown + formatting fixes; no AI/ML reason; **also a port candidate**. +- (Audit other `tools/**/*.py` per future review.) + +Port doesn't have to be immediate; the discipline is "when these +need substantive changes, port them rather than extending +Python." + +## Composes with + +- `package.json` — `tally:substrates` script + `bun@1.3.13` pin + + existing TypeScript tooling pattern at + `tools/invariant-substrates/tally.ts`. +- `memory/feedback_aaron_visibility_constraint_no_changes_he_cant_see_2026_04_28.md` + — same tier of substrate-discipline (defaults Aaron sets that + agents respect without re-litigating each occurrence). +- `memory/feedback_orthogonal_axes_factory_hygiene.md` — language + choice IS a factory-hygiene axis; pick the default + design + rules around it. +- `memory/feedback_bash_compatibility_target_four_shells_macos_32_ubuntu_git_bash_wsl_otto_235_2026_04_24.md` + — adjacent bash discipline (4-shell target for shell scripts); + TypeScript discipline applies above the shell-glue layer. + +## What this is NOT + +- **NOT a directive to port everything immediately.** Port when + changes are substantive; otherwise leave existing Python alone + until natural rewrite cadence. +- **NOT a ban on Python.** AI/ML legitimately uses Python; the + carve-out is the carve-out. +- **NOT a TypeScript-vs-everything-else fight.** F# is the + language for Zeta proper (the library); TypeScript is the + language for tooling around it. Two-tier choice, not a + monoculture push. + +## Pickup notes for future-Otto + +When asked to write a new script: + +1. **Default to TypeScript on Bun.** Place under `tools/...` + with a `package.json` script entry. +2. **If AI/ML library is needed**: Python with a clear + justification comment at the top of the file naming the + library (e.g. `# Python: torch dependency for X`). +3. **If shell glue ≤10 lines**: bash with `set -euo pipefail` per + Otto-235. +4. **Existing Python tool that needs substantive changes**: file + a port-candidate row (e.g. B-0086 shape), evaluate whether + port-now or extend-now-port-later.