Skip to content

feat(desktop,store,packtool): C3 Node PackManager lifecycle on SQLite with cross-backend parity (issue #70) - #115

Merged
zaxbysauce merged 2 commits into
masterfrom
fix/issue-70
Sep 17, 2026
Merged

zaxbysauce merged 2 commits into
masterfrom
fix/issue-70

Conversation

@zaxbysauce

@zaxbysauce zaxbysauce commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Closes #70

Root Cause

Issue #70 (C3) is a feature-slot issue: the desktop Node backend had no
pack lifecycle at all. The Python reference (pack_manager.py, C2/#69)
implements install/supersede/rollback/remove/list_installed against
ChromaDB + a JSON registry, but nothing on the Node/SQLite side could
install, supersede, roll back, remove, or list Knowledge Packs — the
packs table (contracts/store.schema.sql, v2) had no writers and, more
deeply, a lone id PRIMARY KEY that cannot represent C2's semantics,
which require multiple installed versions per pack id (supersede needs
both rows; rollback reactivates a retained inactive row).

Fix

  • Schema v3 (contracts/store.schema.sql): packs re-shaped per-version —
    PK (id, version), new active INTEGER NOT NULL DEFAULT 0 and
    install_path TEXT; supersedes becomes a JSON-array TEXT (FK dropped:
    composite-PK parent), docs.pack_id FK dropped (links.pack_id precedent).
    meta.schema_version bumped to '3' per the schema's own
    SCHEMA-BUMP-CONTRACT.
  • Both migrate ladders extended: desktop/main/backend/store/migrate.ts
    (v2→v3 with the official FK-off schema-change dance, supersedes scalar
    wrap, active=1 mapping) and the mirror contracts/tests/store-interop/ migrate.py (plus a --selftest CLI).
  • New desktop/main/backend/store/pack-manager.ts (~700 lines): faithful
    port of pack_manager.py — ADR-0004 content-hash chunk ids
    (sha256(${docSha}:${index}:${normalized})), the fixed-words chunker
    (deliberately NOT TextChunker, which would break parity), semver-2.0.0
    precedence, delete-before-reingest keyed on path drop OR sha change,
    deactivate/activate asymmetry, single BEGIN IMMEDIATE per operation, Ajv
    2020-12 validation against the authoritative contracts/pack.schema.json
    (new runtime deps: ajv@^8 + ajv-formats@^3), AC4 index.schema_version
    mismatch refusal, D4 link helpers composed in-transaction.
  • Wired in the production host start path (desktop/main/backend/index.ts)
    as an instance field + optional engine.attachPackManager (implemented in
    both engine.ts StubEngine and inference/llama-engine.ts) — b3
    duck-type pin respected.
  • packtool consistency: STORE_SCHEMA_VERSION = 3; writePackIndex /
    installPackRows insert active=1, install_path=NULL explicitly;
    schema-version pins updated in 9 pre-existing test files and ADR-0004.

Recurrence Prevention (defect class)

  • Defect class: parallel implementations of a frozen cross-runtime contract
    (chunk-id formula, chunking algorithm, lifecycle semantics, schema shape)
    drift apart when only one copy is edited.
  • Sweep result: 4 predicates, 34 hits, 34 per-hit dispositions
    (08a-recurrence-sweep.md): 12 FIXED-BY-THIS-PR / INTENDED (new pinned
    copies), the rest pre-existing and already guarded.
  • Guardrail: the C1 cross-backend parity driver runs the REAL Python
    PackManager and the REAL Node PackManager over the same fixture and fails
    on any chunk-id divergence; C6 pins the two migration ladders to each
    other. Both demonstrated RED at base and GREEN at head (frozen checks).

Tests

  • Regression test: python contracts/tests/test_pack_parity.py → exit 0,
    identical chunk-id sets (199b3af6…) from both runtimes
  • Impacted suite: vitest run in desktop/ → 400 passed / 0 failed
    (repro/logs/desktop-final-green.log); vitest run in packtool/ → 95
    passed; pytest tests/test_pack_manager.py contracts/tests/test_pack_schema.py
    → 64 passed; python contracts/tests/store-interop/migrate.py --selftest
    → OK
  • Lint/type/build checks: tsc --noEmit clean; black + flake8 clean on
    changed Python files
  • Deferred-work scan: scan-deferred.sh → clean

Regression Protection

  • New: desktop/src/__tests__/c3-pack-manager.test.ts (10 scenarios:
    stale-chunk, cross-id supersede, rollback id-set restore, symmetry, 5
    refusal classes, embedder mismatch, scoped/bulk remove, listInstalled
    shape), c3-pack-wiring.test.ts (host-attach identity + lifecycle through
    the attached instance), c3-schema-migration.test.ts (v2→v3 on a real
    seeded store + DDL parity + Python mirror), c3-pack-parity.test.ts
    (frozen Node parity leg), contracts/tests/test_pack_parity.py
    (dual-runtime driver).
  • Negative cases: traversal, duplicate paths, corrupt/missing pack.json,
    sha tampering, unsupported mime, over-size non-fixed-words strategy,
    overlap ≥ size, embedder length mismatch — all refused before any write.
  • Test drift review: the 9 pre-existing test files touched are version-pin
    updates (2→3) and literal-templating only — no deleted assertions
    (reviewer-verified).

Acceptance Criteria -> Evidence

Acceptance criterion (from intake) Evidence (command + output, or test name)
AC1 fresh-install parity (Node vs Python identical chunk-id sets) python contracts/tests/test_pack_parity.py[parity] OK; frozen check C1 RED→GREEN (repro/C1.head.log)
AC2 stale-chunk fix on the Node side frozen check C2 RED→GREEN; vitest "stale-chunk: edit-in-place re-supersede removes the old chunk id on SQLite"
AC3 supersede/rollback on SQLite frozen check C3 RED→GREEN; cross-id supersede + rollback id-set restore + all refusal verbs
AC4 schema-version mismatch explicit error frozen check C4 RED→GREEN; index.schema_version 2 vs store 3 → PackManagerError; 3 installs cleanly
AC5 wired Node lifecycle module frozen check C5 RED→GREEN; c3-pack-wiring.test.ts asserts host construction + engine attach identity; full lifecycle through the attached instance
AC6 schema evolution via migration framework frozen check C6 RED→GREEN; v2→v3 both runtimes + DDL parity + packtool consistency

Coordination with B5 (#63)

B5 is closed; the coordination mechanism its schema mandates is the
SCHEMA-BUMP-CONTRACT (contracts/store.schema.sql:19-24): bump the meta
seed, extend BOTH migrate ladders (desktop/main/backend/store/migrate.ts

Invariant Audit

The repo has no checked-in invariant doc (docs/engineering-invariants.md
does not exist here); audited against the in-process contracts instead:
SCHEMA-BUMP-CONTRACT (honored — see AC6), ADR-0004 chunk identity
(byte-matched, parity-proven), the b3 host prototype pin (respected —
instance field + engine attach only), the C1 manifest schema (validated
against the authoritative file, never copied).

Risk and Rollback

  • Risk level: medium (store schema v3 migration; all stores migrate on
    open, v1→v2→v3 ladder preserves rows; back/restore range check extends
    automatically)
  • Rollback: revert this commit (schema file + ladders revert together);
    a store already migrated to v3 would need the v3 file restored before
    reopen — no data loss (ladder is additive/lossless)
  • Residual risk: recorded in the issue itself — if ADR-0003 ([Workstream A] PR 7 of 8: Spike — prove the packaged native stack and choose Node backend vs Electron-hosted Python sidecar (ADR-0003) #57, still
    open) flips the desktop backend from Node to a Python sidecar, the
    schema and semantics survive but pack-manager.ts specifically is
    invalidated; the C2 Python implementation remains the reference either way
  • Full-suite context: pytest tests/ has ~99-100 pre-existing environment
    failures on this Windows box at BOTH base and head (flaky API/integration
    timing; e.g. test_bm25_index_search fails at base because rank-bm25 is
    installed locally); every failure is present at base or passes in
    isolation at head — no deterministic regression

Waivers (or none)

none

Merge status

Awaiting explicit user approval; not merged.

PR head: 401c855

Issue Closure

Closes #70

… with cross-backend parity (issue #70)

Schema v3 (contracts/store.schema.sql): packs becomes per-version —
PK (id, version) + active/install_path — because C2's supersede/rollback
semantics need multiple installed versions per pack id; supersedes and
docs.pack_id FKs dropped (composite-PK parent; links.pack_id precedent).
Both migrate ladders extended (migrate.ts + store-interop/migrate.py with
--selftest), packtool STORE_SCHEMA_VERSION reconciled, writePackIndex/
installPackRows insert active=1 explicitly.

New desktop/main/backend/store/pack-manager.ts ports pack_manager.py's
lifecycle (install/supersede/rollback/remove/listInstalled) onto SQLite:
ADR-0004 content-hash chunk ids, fixed-words chunker (NOT TextChunker),
Ajv-2020 validation against the authoritative pack.schema.json, single
BEGIN IMMEDIATE per operation, D4 link helpers composed in-transaction.
Wired in the backend host start path and attached to the engine
(attachPackManager) — b3 duck-type pin respected.

Cross-backend parity: contracts/tests/test_pack_parity.py installs
versioned-a-1.0.0 through BOTH the Python and Node PackManagers and
asserts identical chunk-id sets (verified: 199b3af6... on both sides).

Acceptance: C1-C6 frozen NEW-SURFACE checks all RED at base da5a9d4 and
GREEN at head; desktop vitest 400 passed, packtool 95 passed,
test_pack_manager + test_pack_schema 64 passed, migrate selftest OK.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It introduces a production store schema migration (v2→v3) that runs on every store open plus a large new cross-runtime lifecycle module, which warrants final human review despite the changes otherwise looking correct.

Pull request overview

This PR implements C3 (issue #70): a Node/SQLite PackManager lifecycle (install / supersede / rollback / remove / listInstalled) that mirrors the Python C2 reference (pack_manager.py) with byte-identical chunk-id semantics, proven by a cross-backend parity test. To support multiple installed versions per pack id, it reshapes the shared store's packs table (schema v2→v3) and extends both migration ladders. It fits into Workstream C (Knowledge packs) and depends on B5's frozen store schema and C1/C2's chunk-id formula.

Changes:

  • Schema v3: packs becomes per-version (PK (id, version), new active/install_path), packs.supersedes/docs.pack_id FKs dropped; meta.schema_version bumped to 3 with both migrate ladders (migrate.ts + migrate.py) extended using the FK-off schema-change dance.
  • New pack-manager.ts (~880 lines): content-hash identity, fixed-words chunker, semver precedence, delete-before-reingest, Ajv validation against contracts/pack.schema.json, AC4 schema-version refusal, D4 link maintenance — wired into the host start path and both engines via an optional attachPackManager seam.
  • Tests + pins: new C3 tests (manager, wiring, migration, parity) plus the Python dual-runtime parity driver; version-pin updates (2→3) across pre-existing tests, packtool (STORE_SCHEMA_VERSION, index-writer), and ADR-0004.
File summaries
File Description
contracts/store.schema.sql Reshapes packs to per-version v3; drops inert FKs; bumps schema_version to 3
desktop/main/backend/store/migrate.ts Adds v2→v3 ladder (FK-off restructure, supersedes JSON-wrap, active=1 mapping)
contracts/tests/store-interop/migrate.py Mirror v2→v3 ladder + --selftest CLI
desktop/main/backend/store/pack-manager.ts New Node PackManager lifecycle port of pack_manager.py
desktop/main/backend/index.ts Constructs PackManager on host start and attaches to engine
desktop/main/backend/engine.ts / inference/llama-engine.ts Add attachPackManager/attachedPackManager seam
desktop/package.json / package-lock.json Adds ajv@^8 + ajv-formats@^3 as runtime deps
packtool/build/pack-json.ts / index-writer.ts STORE_SCHEMA_VERSION=3; explicit active=1, install_path=NULL inserts
docs/adr/0004-knowledge-packs.md Notes schema_version 3 since C3
contracts/tests/test_pack_parity.py New dual-runtime chunk-id parity driver
desktop/src/tests/c3-*.test.ts New C3 acceptance tests (manager, wiring, migration, parity)
desktop/src/tests/{b5,b6,d4}-*.test.ts, packtool tests, ac8 Version-pin updates 2→3

I verified: chunk-id normalization/formula, semver comparison, and fixed-words chunking are byte-faithful to the Python reference; both migration ladders agree; transaction atomicity keeps async embedding outside BEGIN IMMEDIATE; and the AC4 mismatch refusal is correct. The supersede/rollback deactivate-then-activate ordering deliberately (and correctly) diverges from Python to keep shared-content chunk ids live. The only concrete issue found is a stale inline test comment.

Review details

Files not reviewed (1)

  • desktop/package-lock.json: Generated file
  • Files reviewed: 25/26 changed files
  • Comments generated: 1
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 313 to +316
// The explicit ladder is idempotent at v2.
const again = openStore({ dbPath, dims: 8, repoRoot: REPO_ROOT });
try {
expect(migrate(again.db)).toBe(2);
expect(migrate(again.db)).toBe(3);
@zaxbysauce

Copy link
Copy Markdown
Collaborator Author

Swarm PR review (run pr115-20260917-033230) — REQUEST_CHANGES

Structured review: 6 base explorer lanes + 11 mandatory micro risk families (8 matched/dispatched, 3 not-triggered with absence evidence) → 18 normalized candidates → 2 independent reviewer shards → critic challenge on HIGH/CRITICAL.

Verified findings (16):

ID Severity Finding
PRR-001 CRITICAL c3-schema-migration "Python mirror ladder agrees" runs execFileSync('python', …) under vitest's default 5s test timeout — this is the observed Electron shell (unsigned NSIS x64) CI failure (Error: Test timed out in 5000ms). Fix: per-test timeout.
PRR-002 HIGH The AC1 cross-backend parity proof (contracts/tests/test_pack_parity.py) never runs in CI — pytest job covers tests/ only; desktop vitest skips the env-gated parity leg. The parity evidence exists from the dev run but has no CI enforcement.
PRR-003 MEDIUM PackManager binds a direct StoreHandle; clearDocuments()/store-swap paths leave it pointing at a closed connection (no rebind). Latent (no pack routes yet), fail-loud, restart-recoverable.
PRR-004 MEDIUM remove() deletes managed dirs before the transaction; retry-recovers (rmSync force is idempotent) — accepted with disposition.
PRR-005 MEDIUM No serialization port of C2's RLock — cross-surface race window (policy checks → embed await → BEGIN IMMEDIATE) vs ingest/other ops.
PRR-006 MEDIUM Committed blobs of packtool/storyline/__tests__/build-storyline.ac8.test.ts + links-verify-integrity.test.ts flipped LF→CRLF (224/468-line churn; settled via `git cat-file
PRR-007 MEDIUM No CHANGELOG entry (C1/#113 and C2/#114 both added one).
PRR-008 LOW desktop/README.md:192 says "schema v1" (stale); PackManager surface undocumented.
PRR-009 LOW C2-coverage gaps in Node tests: zip refusal, semver pre-release ordering, supersedes-warning, reopen persistence, index-newer-than-store direction.
PRR-010 MEDIUM c3-pack-parity.test.ts same timeout hazard as PRR-001 (cold native-addon imports).
PRR-011 MEDIUM fs.cpSync follows symlinks — deferred to C8 (#75), the issue-sanctioned hardening owner; disposition recorded.
PRR-012 MEDIUM No resource caps (file bytes/text chars/chunk count) vs IngestPipeline's — deferred to C8; disposition recorded.
PRR-013 INFO validate→copy→chunk TOCTOU is C2-parity-faithful (Python identical) — no action.
PRR-014 LOW Error-message polish: dropped fs detail (":642"), swallowed ROLLBACK (":875"), unwrapped readPackRows.
PRR-016 LOW host.stop() doesn't detach packManager (stale ref; ops fail loud).
PRR-018 LOW Wiring test's private-cast assertion — acceptable given the b3 prototype pin; no action.

Rejected (transparency): PRR-015 (installPackRows scalar-supersedes: unreachable — no v2-era producer of non-NULL supersedes exists, exhaustive git grep); plus by-design candidates (old-code-can't-open-v3 fail-loud, asymmetric backups, AC4 mismatch hard-block mandated by the issue, frozen chunks schema, ajv promotion, zero-word chunk C2-parity, one-way schema).

Fixes incoming on this branch via the feedback phase.

#115)

PRR-001/010 (CI blocker): explicit 120s per-test timeouts on the
Python-mirror migration test and the parity vitest leg — vitest's default
5s wrapper fired before the child timeout on cold CI runners (the observed
'Electron shell' job failure).

PRR-002: cross-backend parity driver wired into CI's store-interop job
(pip chromadb + python contracts/tests/test_pack_parity.py) so the AC1
proof executes on every run instead of only on dev machines.

PRR-003: PackManager rebinds across clear-cache/recovery store swaps
(rebindStore + host set-accessor hook, mirroring the document-surface
accessor contract) instead of holding a closed handle.

PRR-005: public lifecycle operations serialize behind a promise queue —
the C2 registry-lock (pack_manager.py:82) parity for cross-surface races.

PRR-006: normalize the two packtool storyline test files back to LF
(blob-level churn from an earlier edit; settled via git cat-file).

PRR-007/008: CHANGELOG entry for C3/#70; desktop/README schema v1 text
corrected to v3 with a Pack lifecycle section; packtool/README links-table
note clarified.

PRR-009: five C2-parity tests added — zip refusal, semver pre-release
ordering (unit + install-level downgrade), supersedes absent-target
warning, rows persisting across store reopen, index.schema_version newer
than store refusal.

PRR-014: unreadable-doc error keeps the fs detail; readPackRows wraps raw
sqlite errors (C2 'pack registry read failed' parity).
PRR-016: host stop() detaches the pack manager like the other surfaces.
PRR-004/011/012/013/018: dispositioned (retry-idempotent / C8-owned
deferred / C2-parity-faithful / acceptable given the b3 pin) — closure
ledger in the PR comment.

Desktop vitest 405 passed (5 new), packtool 95, C2 pytest 64, parity
driver OK, checkpoint verify 6/6 OK (frozen checks untouched).
@zaxbysauce

Copy link
Copy Markdown
Collaborator Author

Closure ledger — swarm-pr-feedback (fix commit 401c855)

Every finding from the review above, with its outcome. Original IDs preserved; provenance chain: explorer lanes → reviewer shards (MiniMax-M3) → critic challenge (GLM) → fix → fresh swarm-reviewer APPROVE (per-item FIXED-VERIFIED, frozen checks re-run 6/6 PASS) → final critic (items below addressed pre-push).

id outcome fix-ref / disposition
PRR-001 (CRITICAL) FIXED 401c855 — 120s per-test timeout on the Python-mirror test (itReal(name, fn, 120_000)); reviewer re-ran the test green
PRR-002 (HIGH) FIXED 401c855Cross-backend pack parity step in the store-interop job (pip install chromadb + python contracts/tests/test_pack_parity.py); job has node + desktop deps + jsonschema; driver exits non-zero on divergence so parity breaks fail CI
PRR-003 (MEDIUM) FIXED 401c855rebindStore + host set-accessor hook; null-guard getter fails loud with PackManagerError between swap halves
PRR-004 (MEDIUM) DISPOSITIONED (accepted) retry-recovers: fs.rmSync(…, {force:true}) is ENOENT-idempotent and the transaction re-runs cleanly (reviewer-verified downgrade); order matches C2's rmtree-then-registry flow
PRR-005 (MEDIUM) FIXED 401c855 — promise-queue serialization of all five public ops (byte-parity with pipeline.ts enqueue pattern; failed op cannot poison the queue)
PRR-006 (MEDIUM) FIXED 401c855 — both storyline test blobs normalized back to LF (settled via git cat-file); the final critic caught that the first amend had re-introduced the churn in packtool/README.md — also normalized in the amended commit
PRR-007 (MEDIUM) FIXED 401c855 — CHANGELOG entry for C3/#70
PRR-008 (LOW) FIXED 401c855 — desktop/README "schema v1" → v3 + new "Pack lifecycle (C3)" section; packtool/README links-table note clarified
PRR-009 (LOW) FIXED 401c855 — five new tests: zip refusal, semver pre-release ordering (unit + install-level downgrade), supersedes absent-target warning, rows across reopen, index NEWER-than-store refusal
PRR-010 (MEDIUM) FIXED 401c855{ timeout: 120_000 } on the parity vitest leg
PRR-011 (MEDIUM) DISPOSITIONED (deferred to C8/#75) symlink-dereference hardening is the issue-designated C8 owner; folder-form local packs only until then
PRR-012 (MEDIUM) DISPOSITIONED (deferred to C8/#75) resource caps are C8-owned (same scope line)
PRR-013 (INFO) DISPOSITIONED (no action) TOCTOU is C2-parity-faithful — pack_manager.py has the identical validate→copy→chunk flow
PRR-014 (LOW) PARTIALLY FIXED + sub-disposition fs-detail + readPackRows wrap fixed in 401c855; the swallowed ROLLBACK secondary-error is LEFT AS-IS deliberately — it matches the repo-wide pattern (migrate.ts, pipeline.ts: comment documents that the ORIGINAL error is rethrown; only the secondary ROLLBACK diagnostic is lost)
PRR-015 (HIGH→REJECTED) INVALID unreachable: no v2-era producer of non-NULL scalar supersedes exists (exhaustive git grep — writePackIndex always bound NULL; all other writers NULL)
PRR-016 (LOW) FIXED 401c855 — stop() detaches packManager (attachPackManager(null) + field null), mirroring the other surfaces
PRR-017 (INFO, numbering note) NON-FINDING both subclaims confirmed C2-parity-faithful by the reviewer (schema minItems + split_words [text]); the ID gap 016→018 in the synthesis was a numbering skip during normalization, not a dropped finding — every candidate that existed in any artifact has an outcome here
PRR-018 (LOW) DISPOSITIONED (no action) private-cast is the only way to assert instance identity under the b3 prototype pin; documented in the test header

Accounting correction from the final critic: 11 findings fixed in code, 5 dispositioned (004, 011, 012, 013 + 018), 1 rejected (015), 1 sub-disposition inside 014 — all 16 verified findings + the PRR-015 rejection accounted for above.

Post-fix verification: desktop vitest 405 passed / 0 failed, packtool 95 passed, C2 pytest 64 passed, parity driver OK, verify-checkpoint 6/6 OK (frozen checks untouched), fresh swarm-reviewer APPROVE on the fix diff, final-critic items (README line-ending blocker, this ledger, PR-head refresh) all resolved before push.

@zaxbysauce
zaxbysauce merged commit 98d3cef into master Sep 17, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Workstream C] PR 3 of 9: Node PackManager lifecycle on the SQLite store, sharing C2's semantics and test vectors

2 participants