aedee4b8231bf050c3240a00ab6645ede5d87ee9
8 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8d54c5e049 |
fix(gate): the SQL-literal check WROTE to the tree it was checking — last of three (#3292)
Completes the family. `check-fnxc-future-dates` was #3287, `lifecycle-column-census` is #3289, and this is the third and last gate that rewrote its baseline during a plain check. ## Reproduction ``` inflated one allowance by 6, ran the gate with NO flags rc=0 entry RESET to 1 ← the check modified the tree it was checking ``` ## Why it matters The tightening is right in substance — an allowance nobody spends is a hole a literal can be regrown into. Performing it as a **side effect of checking** handed every worker a byte-identical uncommitted diff they had not authored, which they then reasonably committed. Measured across the family: nine PRs chased three defects on 2026-07-31/08-01, two of them (#3283/#3285, five minutes apart, `+0/-1` each) deleting the **same line neither author wrote**. #3289 states the class best — *a check that writes turns every reader into an author*. Two of us separately mis-attributed a gate-written baseline to our own work while debugging something else. ## Measured, all four directions | scenario | result | |---|---| | plain run, stale baseline | `rc=0`, reports `allowed 7, now 1`; **inflation survived** — read-only | | a new SQL literal added | **`rc=1`**, names `__sql_probe.ts` — regression detection intact | | `--update-baseline` | `rc=0`, entry written | | clean tree, plain run | `rc=0`, **zero files dirty** | Row 2 is the one worth checking: a read-only change to a gate is worthless if it also stops catching the thing it exists for. The rise path is untouched. `census --strict` 0, `check-fnxc-future-dates` 0, eslint clean. ## Correcting my own delay I measured this defect family on #3267 and then **declined to fix two of the three**, reasoning that the census *"deliberately fails on a drop"* so the port might be unsafe. That was wrong: it tightened and exited `0`, exactly as its own test asserts — *"TIGHTENS on a drop and exits 0, so somebody else's merge cannot redden the gate."* I had read the `--exact` contract and attributed it to the default path. The caution cost hours and prevented nothing. #3289 was written by someone else in the meantime; this finishes what I should have finished then. |
||
|
|
cd237ae760 |
gate: the SQL column-literal ratchet never scanned scripts/, where the raw SQL actually is (#3000)
## The gate could not see the one place raw SQL is actually written by hand `check-sql-column-literals` walked `packages/` only and took `.tsx?`. Every operator script is a repo-root `.mjs`. I found it by removing a raw-SQL lane literal in #2999 and watching this gate report: ``` [check-sql-column-literals] 22 known SQL column literal(s), none added. ``` Unchanged, and green. Its own header promises the opposite — *"a LOWER count fails too so the baseline is ratcheted down"* — so the silence was the tell. **Two changes, and either alone still sees nothing:** the root and the extension. Adding one without the other scans nothing new and reports a reassuring zero — the same trap #2978 hit when widening the lane-wiring census. ## Newly visible: 6 sites, audited not blind-baselined | site | verdict | | --- | --- | | `audit-branch-cross-contamination.mjs:182` — `"column" IN ('triage','todo','in-progress','in-review')` | **real** — the contamination audit scans only the legacy active lanes, so on a renamed board it scans nothing and reports no contamination. Read-only, and it does print its `scannedColumns`, which is the one thing keeping that from being fully silent. | | `reconcile-leaked-soft-deletes.mjs:53, :73` | already fixed by **#2999** — the PR that exposed this gap | ## Proven able to fail, not just to count A guard that has only ever printed a number is a number. A temporary `.mjs` holding one forbidden comparison: ``` scripts/zz-probe-tmp.mjs: 1 SQL column literal(s), baseline allows 0 ``` and the gate returned to green once removed. ## One claim I withdrew I initially wrote that the `ScriptKind` move to `JS` for `.mjs` was needed because *"TSX treats `<` as JSX and would misparse an ordinary comparison"*. I could not demonstrate it. I tried three JSX-ambiguous shapes — `x <div> y`, `f<b, c>(d)`, and a literal sandwiched between `<` and `>` comparisons — and TSX recovered from all three with counts identical to JS. So `JS` is used because it is the correct kind for the file, **not** because a miss was observed, and the code now says exactly that. The opposite claim would have been easy to make and wrong, and this gate's whole value is that its statements about its own coverage are true. ## Merge order **#2999 removes both literals in `reconcile-leaked-soft-deletes.mjs`.** Landing it *after* this PR drops the count, and this gate fails on DECREASE (by design), needing a re-record. Merge #2999 first, or say the word and I will re-record here. Note the widening is self-protecting afterwards: if someone narrows the walk back to `packages/`, the recorded `scripts/` entries vanish from the scan and the gate goes red on decrease. ## Verification (measured) - gate — green, **28 known / none added** (was 22 across `packages/` only) - its own suite — **32 passed** - `eslint` — clean - `lifecycle-column-census --strict`, `check-lane-wiring`, `check-fnxc-future-dates` — green Gate/tooling only; no product file touched. |
||
|
|
634d487c3e |
fix(gate): a lane id hoisted into a const evaded the SQL column-literal gate (#2980)
## What In #2979 I argued a ratchet should be mutation-probed with shapes its author did **not** have in mind, on the day it ships. Applying that to my own gate: two of three probes walked straight through. ```ts const LANE = "done"; sql`... WHERE "column" = ${LANE}` // MISSED const LANES = ["in-progress", "in-review"]; sql`... WHERE "column" IN (${sql.join(LANES)})` // MISSED ``` Both bind the query to the legacy vocabulary exactly as an inline `'done'` does. An interpolation that wasn't a column reference collapsed to the NUL sentinel, so the predicate dissolved before the matcher ever ran. **This is the shape a cleanup produces.** Hoisting a repeated string to a named const reads as tidying, which makes it the likeliest way one of these gets rewritten — and the gate would have gone quiet on a file that changed only in punctuation. Third time this scanner has had that failure (static-span join, element-access column ref, now this). The array form isn't hypothetical: `IN ('in-progress','in-review')` was the live workflow-analytics defect. ## The first version of this fix was wrong, and that's the useful part Resolving *any* string-valued const double-counted the analytics files, which build queries as: ```ts const completedClauses = [`t."column" = 'done'`, "t.columnMovedAt IS NOT NULL"]; ``` Those elements are SQL fragments **already counted where they're written**. Resolving the const re-injected each into the outer template. The three analytics files went `3/3/1` → `6/5/2` — and it read exactly like a genuine find. Only **bare lane ids** are resolved now; the fragment-array case is pinned as a test. I also nearly shipped that version on a bad probe: `node gate | tail` then `echo $?` reads *tail's* exit status, not the gate's. Every probe reported "caught" while the gate was actually failing on main for an unrelated reason. Worth repeating because the harness looked fine and agreed with what I expected. ## Measured | check | result | |---|---| | clean `main` | **22 sites, exit 0, unchanged** — no false positives introduced | | now caught | const string · const array via `sql.join` · as-const via `inArray` | | correctly **not** flagged | resolver-produced lanes · non-legacy ids · SQL-fragment array | | gate's own suite | **26 → 32 tests**, all green | | blinding the resolution | fails **exactly** the 3 new positive tests; the 3 negatives still pass | The negatives outnumber what feels necessary on purpose: eager resolution is how this went wrong the first time, and the fragment-array test is the one that would have caught it. ## Scope Same-file `const` declarations only. Cross-file imports need a type checker and a program-wide pass — a constant imported from another module is **still invisible**, and `--list` output is where that gets audited. Stating the boundary rather than half-resolving it and calling the gate complete. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3461ae7a92 |
docs(gate): record why the SQL-literal gate deliberately does not scan .sql (#2957)
Comment-only. No behavior change. ## Why this is worth a commit #2954 fixed the FNXC-date gate's walk: its extension filter listed the file types stamps were **expected** in rather than the ones they **occur** in, so it was blind to `.sql` and `.css`. That is a tempting pattern to generalize, and `check-sql-column-literals.mjs` is the obvious next candidate — a gate about *SQL* column literals that scans only `.tsx?`. Applying the same fix here would be wrong, and quietly so. ## The two gates are not the same kind of tool The FNXC gate is a plain-text regex scanner, so widening its extension list is trivially correct. This one is **AST-based**: `ts.createSourceFile(..., ScriptKind.TSX)` followed by a walk over string and template nodes. A `.sql` file is not TypeScript. Adding the extension would not widen coverage — it would feed DDL to the TS parser and traverse whatever lenient-mode nodes fell out. The gate would then **report coverage it does not have**, which is strictly worse than not looking, because the silence would read as "SQL is clean." ## Measured before deciding 38 tracked `.sql` files contain exactly one lifecycle-looking literal: ``` 0022_ideation.sql:19 CONSTRAINT ideation_sessions_status_check CHECK (status IN ('open','converged','archived')) ``` That is the **ideation-session** status enum — a different domain that happens to reuse the word — not a `tasks.column` comparison, and not something this gate would flag even if it could parse the file. **Zero real offenders.** So the honest scope is recorded as: raw SQL is **unwatched**, and the trigger that would make it worth watching is a data backfill (`UPDATE tasks SET column = ...`) landing in a migration. If that ever happens it needs a separate raw-text matcher against the exported `COMPARISON`, not an entry in the extension filter. ## Verification - `check-sql-column-literals` → exit 0 - `check-fnxc-future-dates` → exit 0, "478 known, none added" (the new stamp is dated today, local) - `scripts/__tests__/check-sql-column-literals.test.mjs` → **26 pass, 0 fail** - `scripts/__tests__/check-inert-flag-seams.test.mjs` → **12 pass, 0 fail** - `eslint` clean ## Why a comment rather than a doc Per AGENTS.md, decisions of this shape belong next to the code they constrain. The failure mode is specifically someone reading the walk, noticing `.sql` is missing, and "fixing" it — so the note has to be at the filter, where that person is looking, not in `docs/solutions/`. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b1bd571682 |
batch-sql-ratchet: the census / gate-ratchet family — collection branch, fold here (#2941)
## Family branch for consolidation directive item 4 `batch-sql-ratchet` did not exist and ~10 open PRs are waiting for a collection point, so this establishes it. **Fold your census/ratchet commit here and close your own PR as superseded.** ```bash git fetch origin batch-sql-ratchet git checkout -B batch-sql-ratchet origin/batch-sql-ratchet git cherry-pick <your-sha> # verify scoped, not full suite: pnpm --filter @fusion/core exec vitest run src/__tests__/archived-column-gate-parity.test.ts --silent=passed-only --reporter=dot git push origin HEAD:batch-sql-ratchet ``` **Candidates I can see open right now** (owners: please fold + close): | PR | branch | |---|---| | #2938 | `fix/comments-ops-sentinel` | | #2935 | `fix/task-artifacts-sentinels` | | #2933 | `chore/commit-tightened-census-baseline` | | #2931 | `fix/async-comments-sentinels` | | #2928 | `fix/audit-ops-sentinel-marker` | | #2925 | `live-task-column-lanes` | | #2923 | `fix/task-id-integrity-sentinel` | | #2921 | `fix/plugin-store-migration-marker` | | #2894 | `gate/sql-literals-match-census-placement` | That is **10 → 1** once folded. I have not cherry-picked anyone else's commits — folding someone's work without them verifying it is how a batch lands broken. --- ## What is in it so far (mine, from #2924) **Clears a live main red:** `archived-column-gate-parity` fails on `origin/main` today. ``` AssertionError: TypeScript encoding changed. async-comments-attachments.ts: 8 → 5 ``` #2886 fixed a real bug — archived-document guards failing in *opposite* directions on a renamed lane — by replacing three `column === "archived"` comparisons with `isArchivedLane(column, archivedColumns)`. The AST scan counts raw comparisons, so the tally dropped. **What I did not do is record it as three sites converted**, because measured, it is not: ``` grep -rn "archivedColumns:" packages/core/src packages/engine/src --include="*.ts" | grep -v __tests__ → (no matches) ``` No caller passes it. The parameter defaults to `LEGACY_ARCHIVED_LANES = new Set(["archived"])`, so every call resolves to the literal it replaced — byte-identical behaviour, resolved branch dead. That matters for this guard's whole argument: its header warns that converting the TypeScript half while the Drizzle and raw-`sql` halves still compare the string is a split brain *"no test would catch, because every builtin workflow spells the column `archived` so the two halves agree by accident on every board we ship."* **There is no split brain today precisely because the resolved half is unwired** — it becomes one the moment a caller threads real lanes in without the SQL sides moving. Recorded inline so `5` cannot be read as "3 sites done"; flagged on #2886. Verified not a split brain: the Drizzle and raw-sql inventories are unchanged and both pass — worth stating because those assertions run *after* the TypeScript one, so a plain red says nothing about them. Scoped edit to `AUDITED_TS_SITES` by line range: these paths appear in more than one inventory here, and an unscoped replace would quietly edit the raw-sql side too, making the parity guard agree with itself (the trap I hit in #2817). Guard still bites: appending a real `task.column === "archived"` to an audited file fails it. Core **4852 passed / 0 failed**, lint clean, test-only. Closing #2924 as superseded by this. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved task delegation messages when workflow pickup cannot be confirmed. * Delegation results now clearly indicate when a task has not been verified for pickup. * **Quality Improvements** * Added validation checks to catch future-dated markers and inconsistent SQL-column usage. * Refined workflow checks to distinguish stale configuration from incomplete configuration. * **Documentation** * Updated lifecycle conversion guidance with more accurate audit findings and limitations. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7b68f20501 |
batch(docs): fold the three workflow-learnings / annotation PRs into one (#2942)
## Family batch — replaces #2926, #2892, #2887 Per the consolidation directive: the u9/e2e **docs family**, folded into one branch and one CI run. Three PRs, five commits, **five files, comment and markdown only**. | folded PR | commits | |---|---| | #2892 `docs/union-vs-per-task` | the project union and the per-task answer are not ranked; date correction | | #2926 `docs/date-my-measured-claims` | date the measured claims (one was wrong); date the grep-vs-AST measurement in the SQL gate header | | #2887 `docs/archived-state-literals` | mark the three archived STATE literals as deliberate | Cherry-picked in original order with authorship preserved; all five applied clean, no conflicts. ## Scope is provably comment-only ``` docs/solutions/workflow-learnings/lifecycle-conversions-that-score-as-wins.md docs/solutions/workflow-learnings/project-union-versus-per-task-lanes.md packages/core/src/task-store/async-maintenance.ts ← FNXC DELIBERATE-LITERAL annotation packages/core/src/task-store/workflow-definitions.ts ← FNXC DELIBERATE-LITERAL annotation scripts/check-sql-column-literals.mjs ← header prose only ``` Every added line in `packages/` and `scripts/` is inside a comment — checked by filtering the diff for declarations, conditionals and returns, which returns nothing. The two core files gain `DELIBERATE-LITERAL` markers explaining that `'archived'` is a **state** marker there, not a lane: the sweep collects rows Fusion itself archived or soft-deleted, so widening to the resolved archived set would pull live cards into a cleanup pass. ## Verification (scoped, per the directive — not the full suite) - `pnpm lint` — clean - `check-sql-column-literals` — exit 0 (the file it annotates) - `check:lifecycle-columns` — exit 0 (the markers it adds are census-visible) - `sync-workflow-ir-callsite-allowlist.test.ts` — 3/3 ## A correction worth recording Mid-fold I saw a changeset, `self-healing.ts` and a test file in `git diff origin/main..HEAD` and nearly reported the batch as impure. They were **main's own commits** — `origin/main` advanced between branch creation and the diff, so the comparison was against a stale base. Rebasing onto current `main` reduced it to the five files above. Worth flagging for anyone else folding a family today: with `main` moving this fast, diff the branch **after** rebasing or the file list will lie to you. ## Closing the originals #2926, #2892 and #2887 are superseded by this and are being closed. I hold no PRs of my own in this family — all mine merged — so this fold is on behalf of the family rather than a rollup of my own work. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
63e1f81244 |
fix(gate): a dropped SQL-literal count tightens the baseline instead of failing the gate (#2888)
## Why `check-sql-column-literals` runs inside `pnpm test:gate` — the **blocking** lane. It hard-fails when a count *drops*, so a single converting PR that doesn't re-record takes down the gate for **every worker in the program** until someone fixes the baseline by hand. That is not hypothetical. It is happening on `main` right now (`team-analytics.ts` 6 → 3, fixed by #2880), and it is the **second** instance of the shape — the lifecycle census hit it from a merge wave that dropped eleven files at once. ## The census already resolved this exact trade-off From `docs/testing.md`, on why the census stopped hard-failing on a drop: > "the drop is almost never the failing author's to fix ... A permanently-red gate is a bigger hole than a stale allowance, because it gets ignored and then nothing is guarded at all." That reasoning applies here **with more force**, because the census is *not* in the blocking lane and this check *is*. Same failure mode, higher cost, opposite policy — this aligns them. ## What changes A drop now rewrites the baseline downward, reports what it lowered, and exits 0: ``` [check-sql-column-literals] baseline TIGHTENED — fewer literals than it allowed packages/core/src/team-analytics.ts: allowed 6, now 3 The baseline has been rewritten downward. COMMIT IT so the allowance cannot be regrown into; in CI this write is discarded with the runner, which is why the gate is green and not silent. ``` **The rise check is untouched.** "No new SQL column literals" is the ratchet's actual purpose and still fails hard. The stale-allowance concern the old comment raised is real and is preserved: the rewritten file must be committed, and in CI the write is discarded with the runner — so the gate goes green rather than silently passing a stale allowance, exactly as the census does. ## Verified in both directions | scenario | result | |---|---| | drop (`team-analytics.ts` 6 → 3, the live case) | **tightens, exit 0** | | rise (a literal added to a zero-allowance file) | **fails, exit 1** — `task-age-staleness.ts: 1 SQL column literal(s), baseline allows 0` | The rise probe needed a zero-allowance file: adding one literal to `team-analytics.ts` keeps it at 4 against an allowance of 6, which is correctly *not* a rise. Worth noting because it is an easy way to conclude the guard is dead when it is working. ## Relationship to #2880 #2880 fixes the **instance** — it re-records the current drift so the gate goes green now. This fixes the **class**, so the next conversion doesn't take the gate down again. They are independent and either can land first; if #2880 lands first, this becomes a no-op on a matching baseline. ## Verification - `pnpm test:gate` — exit 0 with this change applied - `pnpm lint` — clean No changeset: gate tooling, not published behaviour. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ef50244234 |
feat(gate): freeze the SQL column-literal surface — 30 sites, none may be added (#2841)
Instruments a surface no existing check can see. Follows #2839, and **corrects the count I reported there** (12 → 14). ## Why it was invisible The lifecycle census parses TypeScript **comparisons**; a legacy id inside a SQL string is string data. The inert-seam gate reasons about parameters and call sites. Neither has ever looked here. **What it cost:** `cleanupStaleMergeQueueRowsImpl` filtered on `t.column != 'in-review'`, so on a renamed board every queued card looked stale, its `merge_queue` row was deleted, and the card became **unleaseable**. The operator found it reviewing #2819 — in SQL I had already read past during that same work. The quieter half is analytics: five sites count `"column" = 'done'`, so throughput, cycle time, and team dashboards report **zero completed work** on a renamed board. Nothing errors, which is why nobody files it. ## What this does, and does not do It does **not** fix the sites. `resolveProjectColumnsForRoles` is the mechanism and its migration has an owner (#2839). This freezes the population so the surface cannot grow underneath that migration: a new file or a higher count fails, **and a lower count fails too** — so the baseline ratchets down as sites migrate rather than leaving slots to silently regrow into. That is the same rot as an allow-list entry for a deleted function, which this repo already hit once. AST-based, deliberately: a line grep for the same pattern reports **37** hits, **25 of them prose** quoting `column === "done"` in explanatory notes. A guard that is 68% false positives trains its readers to skip it — a lesson this program has already paid for. ## Two corrections found by mutation-testing my own gate **1. Clause fragments were missed.** Requiring a SQL keyword *in the same literal* skipped `team-analytics.ts`, which builds `["assignedAgentId IS NOT NULL", `"column" = 'done'`, ...]` and joins them into a `WHERE` later. That fragment is as vocabulary-bound as any full query but contains no keyword. Fixing it took the population **12 → 14**, so the number I put on #2839 was low. **2. My first mutation test proved a direction it had not.** I replaced the first textual occurrence in a file — which was inside a **comment** — and read the unchanged count as the scanner being broken. The scanner was right; my test was wrong. All three directions are now driven against real SQL: | mutation | result | |---|---| | add a full query with a legacy comparison | `3 SQL column literal(s), baseline allows 2` | | add a bare clause **fragment** (no keyword) | caught — same failure | | migrate one away (count drops) | `1 site(s) now, baseline still allows 2 — re-record it` | | restore | exit 0 | I am flagging that second one because it is the exact failure mode this program keeps finding: a green result read as evidence when the experiment was invalid. ## Verification `pnpm test:gate` green with the new check in it · lint 0 · single AST pass. Wired into `test:gate` and both `pretest` hooks. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Added automated checks to detect increases in legacy SQL column literals. * Added baseline tracking to ensure known SQL literal counts do not regress. * **Tests** * Expanded pre-test and gated verification steps with SQL literal and mock completeness checks. * Updated test validation workflows to enforce the new safeguards. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |