From 66d829abed1710861d6529a24c9b3668ed3ddacd Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 5 Jun 2026 14:32:04 -0700 Subject: [PATCH] docs(solutions): schema-version literal sweep must include plugin workspaces --- CONCEPTS.md | 7 +- ...on-sweep-must-include-plugin-workspaces.md | 93 +++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 docs/solutions/test-failures/schema-version-sweep-must-include-plugin-workspaces.md diff --git a/CONCEPTS.md b/CONCEPTS.md index fd47dbecd2..3f403f93c4 100644 --- a/CONCEPTS.md +++ b/CONCEPTS.md @@ -180,7 +180,7 @@ A persisted crash-safe marker (`tasks.transitionPending`) written in the same tr *Behind the `experimentalFeatures.workflowGraphExecutor` flag (orthogonal to `workflowColumns`). With the flag off, and for the Default workflow always, step policy is the legacy engine-owned path (PROMPT.md parsing, in-session review verdicts, RETHINK reset) — unchanged.* ### Step instance -One runtime expansion of a `foreach` template subgraph, bound to a single planned step (`Task.steps[i]`). Identity is deterministic — `#:` — so resume reconstructs the full instance set from the pinned step count without persisting the expansion itself. Each instance carries its own run-state (current node, rework count, baseline/checkpoint, and in worktree mode its branch and integration status) in `workflow_run_step_instances` (schema v108). The step count is pinned at expansion; a later disagreement with the live step list is a `pin-mismatch` failure, never a silent re-expansion. An instance's lifecycle writes flow through `store.updateStep` so `Task.steps[]` stays the physical projection sink for every existing consumer. +One runtime expansion of a `foreach` template subgraph, bound to a single planned step (`Task.steps[i]`). Identity is deterministic — `#:` — so resume reconstructs the full instance set from the pinned step count without persisting the expansion itself. Each instance carries its own run-state (current node, rework count, baseline/checkpoint, and in worktree mode its branch and integration status) in its own persisted run-state table. The step count is pinned at expansion; a later disagreement with the live step list is a `pin-mismatch` failure, never a silent re-expansion. An instance's lifecycle writes flow through `store.updateStep` so `Task.steps[]` stays the physical projection sink for every existing consumer. ### parse-steps A workflow graph node that reads a declared Artifact and runs a registry parser to write the canonical step list (`Task.steps[]`) — the only graph-side writer of steps. Built-in parsers are `step-headings` (the `### Step N:` convention, extracted byte-identically from the legacy regex, including the `(depends: N,M)` annotation) and `json-steps`; plugins contribute parsers under `plugin::`. Parsing failures fail closed to a routable `outcome:parse-error` rather than crashing. A parse-steps node must dominate (precede on all paths) any `foreach(source:"task-steps")`, and running one after a foreach has already expanded trips pin protection (an audited failure) so re-plan loops cannot desynchronize an expanded region. @@ -188,6 +188,11 @@ A workflow graph node that reads a declared Artifact and runs a registry parser ### Custom task field A workflow-declared, typed task field (`string | text | number | boolean | enum | multi-enum | date | url`, with enum options and render hints) whose values live in `tasks.customFields`, keyed by field id. The task model is thereby recast as core fields (title, description) + standard metadata + these workflow-defined fields. Writes pass through a single store authority (`updateTaskCustomFields`) that validates each value against the resolving workflow's schema and returns typed rejections (offending `fieldId` + `code`); agents write them via `fn_task_update`'s `custom_fields` patch. Editing a workflow's fields or switching a task's workflow orphans (never destroys) values for removed or type-incompatible ids — orphans are retained and surfaced under a detail disclosure, excluded from cards. Same id means the same field within a project; there is no cross-workflow shared field namespace. +## Persistence & migrations + +### Schema-Version Sweep +The named process performed atomically with any bump of the core schema-version counter: a repo-wide hunt for hard-coded assertions of the old version number, updated in the same commit as the bump. The sweep's scope is every workspace that can embed the core database — packages *and* plugins — because any package instantiating the core store observes the current version; scoping the hunt to one workspace silently strands assertions in the others. Downstream consumers should prefer asserting against the exported version constant instead of a literal, which removes them from the sweep entirely. + ## Flagged ambiguities - "Merging" a shared-branch-group Task had been used for both member integration and group promotion — these are distinct steps with independent gating and must not be conflated. diff --git a/docs/solutions/test-failures/schema-version-sweep-must-include-plugin-workspaces.md b/docs/solutions/test-failures/schema-version-sweep-must-include-plugin-workspaces.md new file mode 100644 index 0000000000..8d6bd7668e --- /dev/null +++ b/docs/solutions/test-failures/schema-version-sweep-must-include-plugin-workspaces.md @@ -0,0 +1,93 @@ +--- +title: "Schema-version literal sweep must include plugin workspaces" +date: "2026-06-05" +category: test-failures +module: "packages/core schema-version sweep" +problem_type: test_failure +component: testing_framework +symptoms: + - "CI Test shard 4/4 fails with AssertionError: expected 109 to be 108 in plugins/fusion-plugin-roadmap roadmap-store.test.ts" + - "Failure invisible locally because pre-push verification runs packages/-scoped suites only" + - "grep -rn 'toBe(108)' packages/ returns zero hits post-sweep, so the sweep looks complete" +root_cause: missing_workflow_step +resolution_type: test_fix +severity: medium +related_components: + - database + - development_workflow +tags: + - schema-version + - pnpm-workspace + - plugin + - grep-scope + - ci-failure + - literal-sweep +--- + +# Schema-version literal sweep must include plugin workspaces + +## Problem + +When `packages/core`'s `SCHEMA_VERSION` was bumped 108 → 109 (adding the `workflow_settings` table), the established "broad literal sweep" — `grep -rn 'toBe(108)' packages/` — was executed correctly and updated ~40 assertion sites. CI still failed: `plugins/fusion-plugin-roadmap` has a store test asserting `getSchemaVersion()` against a hard-coded literal, and `plugins/` lives outside the sweep's grep scope. + +## Symptoms + +``` +FAIL plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-store.test.ts + RoadmapStore > schema version > schema version is 108 after init + AssertionError: expected 109 to be 108 +``` + +- CI shard 4/4 red on the first run after the bump landed; all `packages/` suites green. +- Invisible locally: the plan's execution note and pre-push verification both scoped to `packages/`, and the roadmap plugin's suite is not part of a `packages/`-only vitest run. + +## What Didn't Work + +- **Following the documented sweep convention diligently.** After the bump, `grep -rn 'toBe(108)' packages/` returned zero hits — the sweep *looked* complete. The gap was scope, not carefulness: at least two plan cycles (step-inversion v108, workflow-settings v109) codified the sweep as `packages/`-scoped, an assumption that silently became false when `fusion-plugin-roadmap` grew a store layer on `@fusion/core`'s `Database` and added a schema-version pinning test. + +## Solution + +One-line fix in `plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-store.test.ts`: + +```ts +// Before (failing) +it("schema version is 108 after init", () => { + expect(db.getSchemaVersion()).toBe(108); +}); + +// After +it("schema version is 109 after init", () => { + expect(db.getSchemaVersion()).toBe(109); +}); +``` + +The durable fix is the corrected sweep command — run at the **repo root**, not `packages/`, whenever `SCHEMA_VERSION` changes (substitute the old version): + +```sh +grep -rn --exclude-dir=node_modules 'toBe(108)' . +``` + +## Why This Works + +`SCHEMA_VERSION` in `packages/core/src/db.ts` is the authoritative migration counter. Any workspace that instantiates `@fusion/core`'s `Database` runs all migrations on `init()` and therefore observes the current version — including plugin workspaces. `pnpm-workspace.yaml` globs both `packages/*` and `plugins/*` (plus named plugin dirs); schema-version assertions can live in any of them. The sweep convention predated plugin store layers, so its `packages/` scope was stale, not wrong-by-construction. + +## Prevention + +- **Sweep the whole repo, not `packages/`.** Canonical command for a bump old → new: `grep -rn --exclude-dir=node_modules 'toBe()' .` — the workspace globs in `pnpm-workspace.yaml` are the authoritative list of places assertions can hide. +- **Prefer the import over the literal.** `SCHEMA_VERSION` is a named export of `@fusion/core`; plugin store tests should pin against it instead of a number, which survives every future bump with no sweep at all: + + ```ts + import { SCHEMA_VERSION } from "@fusion/core"; + + it("schema version matches core after init", () => { + expect(db.getSchemaVersion()).toBe(SCHEMA_VERSION); + }); + ``` + + (Core's own migration tests legitimately keep literals — they pin specific forward-path versions. The import pattern is for *downstream* consumers that just track core.) +- **As of 2026-06-05**, `fusion-plugin-roadmap` is the only plugin with a live `getSchemaVersion()` assertion, but any plugin adding a store layer backed by core's `Database` becomes a candidate. CI shards do run `plugins/` suites, so CI is the backstop — the sweep exists to catch it pre-push. + +## Related Issues + +- [[bundled-plugin-registration-drift]] (`docs/solutions/integration-issues/bundled-plugin-registration-drift.md`) — companion failure class: an operation scoped to `packages/` silently missing the `plugins/` workspace peer. Its `packages/`-scoped grep example is correct *for its own domain* (registration points live in `packages/`); do not read it as endorsing `packages/`-only scope for schema sweeps. +- `docs/solutions/architecture-patterns/i18n-foundation-vite-ink-monorepo-code-split-catalogs.md` — shared principle: eliminate the hardcoded second source of truth in favor of the derived/imported value.