Skip to content

fix(memory): persist decision content (B-MCP-4 critical data loss)#20

Merged
h2devx merged 1 commit intodevelopfrom
feature/b-mcp-4-decision-content
May 1, 2026
Merged

fix(memory): persist decision content (B-MCP-4 critical data loss)#20
h2devx merged 1 commit intodevelopfrom
feature/b-mcp-4-decision-content

Conversation

@h2devx
Copy link
Copy Markdown
Contributor

@h2devx h2devx commented May 1, 2026

Que cambia

Cierra B-MCP-4 (issue #3, critical, data loss) via Option B
(decision tomada en el cuadro comparativo previo, alineada con la
regla durable "siempre priorizar la estabilidad"):

  • Migracion 008: agrega columna decisions.content TEXT NOT NULL DEFAULT '', backfill content = rationale para filas existentes,
    rebuild de decisions_fts virtual table incluyendo la nueva
    columna, triggers actualizados con UPDATE OF column-scope.
  • Domain: nuevo VO DecisionContent (max 50K chars vs 5K de
    Rationale). Aggregate Decision lleva el campo en record y
    rehydrate.
  • Application: RecordDecision port + use case aceptan content
    optional con fallback a rationale (defensa para flujos internos
    no-wire). Importer de handoff + JSON tambien.
  • Infrastructure: SqliteDecisionRepository lee/escribe la
    columna; SqliteMemoryProjectionRepository (lado recall) la
    surface en el wire response. JsonMemoryExporter la incluye para
    round-trip lossless.
  • Composition: RememberFacadeAdapter ya no descarta
    silenciosamente input.content para kind=decision — lo pasa
    intacto al use case.

Por que

Fixes #3. Wire schema documentaba content como campo top-level
para todas las kinds desde v0.1.0; la tabla decisions no tenia la
columna; la facade descartaba el valor sin error. Recall retornaba
rationale en el campo wire content para empeorar la confusion.

Eleccion entre Option A (drop content del wire) y Option B
(agregar columna + migracion): Option B porque preservar el
contrato publico documentado tiene prioridad sobre la velocidad.

Audit turns/tasks confirmado: ambos tienen sus columnas
dedicadas (summary/description) que reciben content correctamente.
Solo decisions tenia el silent-drop. Sin scope creep.

Tipo de cambio

  • fix — bug fix critical (data loss)
  • test — agrega tests
  • schema — migracion 008

Checklist

  • npm run typecheck EXIT=0
  • npm run lint y npm run lint:tests EXIT=0
  • npm run validate:modules EXIT=0
  • npm run build EXIT=0
  • npm run test EXIT=0 — 2519 tests passing en 208 archivos (was 2512 in 207)
  • Cero any, cero as any, cero // @ts-ignore
  • Tests nuevos cubren el cambio
  • Wire/protocolo MCP documentado en docs/02 §4.4 (sin cambio — el contrato existente AHORA se honra)
  • N/A — no introduce ADR (Option B es el cumplimiento del contrato, no un nuevo principio)
  • HANDOFF.md se actualiza al cierre de v0.1.2-beta.3

E2E que validan VALORES

  • Los tests asertan valores reales (no solo shape)

tests/integration/N-decision-content-roundtrip.test.ts (2 casos)
sigue la metodologia de Phase-9:

  1. Estado conocido: workspace sin decisions.
  2. Insertar via wire: mem.remember(kind=decision) con
    rationale SHORT y content LONG, distintos.
  3. SQL inspection: SELECT content FROM decisions WHERE id=?
    debe contener el content largo, NO el rationale.
  4. Wire round-trip: mem.recall con un token que aparece SOLO
    en content (no en title ni rationale) — debe traer el hit, y
    result.content debe ser el content largo (no el rationale,
    que era el comportamiento pre-fix).
  5. Defensive default: insertar via use case interno sin
    content debe persistir content = rationale.

Pre-fix: result.content === rationale (siempre). Post-fix:
result.content === content (lo que el cliente envio).

Notas para el reviewer

Migracion 008 sobre datos existentes:

  • Backfill content = rationale (no empty string) para
    preservar searchability via FTS5 sobre la dogfood DB del usuario
    (27 decisions reales). Documentado en el SQL header.
  • DROP TABLE + CREATE de decisions_fts necesario porque FTS5 no
    soporta ALTER. Operacion idempotente porque la migration runner
    honra _meta-based applied versions (la migracion no se reaplica).
  • Los triggers se reescriben con UPDATE OF title, rationale, content
    para preservar la optimizacion de migration 007 (no reindexar FTS
    en updates de confidence).

Forward-compat de exports legacy:

  • JsonMemoryImporter.DecisionSchema marca content como
    optional para consumir snapshots v0.1.0/v0.1.1 que no lo tienen.
    Cuando absent → buildDecision usa rationale.
  • SqliteMemoryProjectionRepository.DecisionRowSchema igual:
    content: z.string().optional() con fallback a rationale en el
    preview.

Por que NO se toca turns ni tasks:

  • Audit del facade routing:
    • turn: summary: input.content → tiene su columna summary.
    • task: description: input.content → tiene su columna description.
    • Ambos persisten correctamente. Solo decisions tenia el bug.

Issue #3 (CRITICAL data loss). The wire schema in docs/02 §4.4
documented `content: string` as the canonical full-text body for
every mem.remember kind, but the `decisions` table had no `content`
column. The RememberFacadeAdapter routed `input.content` into
`rationale` (when rationale was absent) and silently dropped it
otherwise. Recall returned `rationale` in the wire `content` field
to compound the confusion.

Decision: Option B from the comparative ADR — preserve the
documented wire contract by adding the column. Stability of the
public protocol outweighs the cost of a one-time schema migration
on existing workspaces (see feedback memory:
"Always prioritize stability over velocity").

Schema:
- migration 008__decisions-content.sql adds `content TEXT NOT NULL
  DEFAULT ''`, backfills existing rows with `content = rationale`
  (preserves searchability across legacy data), drops/recreates the
  decisions_fts virtual table to include the new column, and
  reindexes from the base table. Triggers updated to keep the FTS
  mirror in sync; carries forward migration 007's column-scoped
  UPDATE OF optimization.

Domain:
- New `DecisionContent` VO extending `NonEmptyString`; max 50,000
  chars (well above Rationale's 5,000 — content is the long-form
  body where rationale is the short why).
- Decision aggregate gains `content: DecisionContent`. Both factories
  (`record`, `rehydrate`) accept the new field. New `getContent()`
  accessor.

Application:
- RecordDecision port adds optional `content`; the use case falls
  back to `rationale` when absent (defensive default for internal
  workflows that bypass the wire).
- ImportHandoffUseCase + JsonMemoryImporter mirror the same
  fallback so handoff/JSON importers from pre-B-MCP-4 snapshots
  rehydrate cleanly.

Infrastructure:
- SqliteDecisionRepository writes/reads the new column. Zod schema
  updated; UPSERT and SELECT statements include `content`.
- SqliteMemoryProjectionRepository (recall side) selects `content`
  and uses it as the preview returned in the wire response. Falls
  back to rationale if the row predates migration 008.
- JsonMemoryExporter serializes `content` so round-trip
  exports/imports stay lossless.

Composition:
- RememberFacadeAdapter passes `input.content` through to the use
  case for kind=decision. The previous code path silently dropped
  the field.

Tests:
- N-decision-content-roundtrip.test.ts (NEW, 2 cases): VALUES
  validation per Phase-9 lesson. Insert via mem.remember with
  rationale != content, assert SQL row has both intact, recall a
  token that appears ONLY in content, assert the wire response
  surfaces the actual content (not rationale). Plus the
  rationale-fallback default for content-less internal calls.
- value-objects.test.ts (+5 cases): DecisionContent invariants.
- memory-database fixture: applies migration 008 alongside 000/004/005.
- 6 existing test files updated: Decision builders inject content
  field; importer/exporter tests assert content round-trips; smoke
  test asserts versions [0..8].
- Audit of turns/tasks confirmed they have their own dedicated
  text columns (summary/description) so the wire content field
  routes correctly there — only decisions had the silent-drop
  defect.

5/5 EXIT=0:
- typecheck, lint, lint:tests, validate:modules, build, test
- 2519 tests passing in 208 files (was 2512 in 207)

Closes #3

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@h2devx h2devx merged commit 52fbfd9 into develop May 1, 2026
1 check passed
@h2devx h2devx deleted the feature/b-mcp-4-decision-content branch May 1, 2026 22:51
@h2devx h2devx mentioned this pull request May 1, 2026
10 tasks
h2devx added a commit that referenced this pull request May 2, 2026
## Que cambia

Cuts `v0.1.2-beta.3` consolidando los 4 fixes de Phase-11 que cierran
los 4 bugs de dogfood reportados en Phase-9 (B-MCP-2/3/4/5).

## Por que

`@netzi/recall@0.1.2-beta.0` (en npm beta channel) tenia 4 issues
abiertos que rompian la promesa central del producto (semantic
recall, mem.health diagnostics, decision content storage). Phase-11
los cerro todos via PRs squash-mergeados a `develop`. Esta release
branch consolida los version bumps + release notes + HANDOFF
update para promover beta.3 a `main` y publish a npm.

## Tipo de cambio

- [x] chore — release (no code change beyond version bumps)

## Cambios incluidos (commits ya en develop, no parte de este PR)

| Issue | Tag | PR | Commit en develop |
|---|---|---|---|
| [#2](#2) | B-MCP-3 critical
| [#17](#17) | `229e7cd` |
| [#1](#1) | B-MCP-2 high |
[#18](#18) | `05b6731` |
| [#4](#4) | B-MCP-5 low |
[#19](#19) | `c4a2d1d` |
| [#3](#3) | B-MCP-4 critical
(data loss) | [#20](#20) |
`52fbfd9` |

## Cambios de este PR

- `code/package.json`: `0.1.2-beta.0` → `0.1.2-beta.3`
- `code/src/bootstrap/composition-root.ts`: default `serverInfo.version`
actualizado
- `code/sonar-project.properties`: `projectVersion` actualizado
- `docs/RELEASE-NOTES-v0.1.2-beta.3.md`: NUEVO, documentando los 4 fixes
+ migration safety + engineering metrics + outstanding caveats + path a
v0.1.2 stable
- `HANDOFF.md`: §0 actualizado al estado post-Phase-11; §6.16 nueva con
la cronologia completa, 10 decisiones del orquestador (D-1101..D-1110),
y 6 hallazgos durables

## Checklist

- [x] `npm run typecheck` EXIT=0
- [x] `npm run lint` y `npm run lint:tests` EXIT=0
- [x] `npm run validate:modules` EXIT=0
- [x] `npm run build` EXIT=0
- [x] `npm run test` EXIT=0 — 2519 tests passing en 208 archivos
- [x] Cero `any`, cero `as any`, cero `// @ts-ignore`
- [x] HANDOFF.md actualizado (§0 + §6.16 nueva)
- [x] Release notes consolidadas en
`docs/RELEASE-NOTES-v0.1.2-beta.3.md`

## E2E que validan VALORES

- [x] N/A — release branch sin cambios de codigo nuevos. Los 3 archivos
test value-validation (L/M/N) ya estan en develop via PRs #17/#18/#20.

## Plan post-merge

```bash
git checkout main && git pull
git tag -a v0.1.2-beta.3 -m "v0.1.2-beta.3"
git push origin v0.1.2-beta.3
gh release create v0.1.2-beta.3 --prerelease --notes-file docs/RELEASE-NOTES-v0.1.2-beta.3.md

# Usuario ejecuta el publish:
cd code && npm publish --tag beta --auth-type=web

# Merge-back develop:
git checkout develop && git merge --no-ff main && git push origin develop

# Dogfood real:
npm install -g @netzi/recall@beta && recall mcp serve  # o el comando equivalente
```

Si el dogfood post-publish surface nuevos defectos → cortar `beta.4`.
Si pasa limpio → cortar `release/0.1.2` (stable) + promover `latest`
desde 0.1.1 → 0.1.2 + hard-deprecate 0.1.1.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
h2devx added a commit that referenced this pull request May 2, 2026
#23)

## Que cambia

Hotfix que actualiza los docs publicos (README.md raiz, code/README.md
shipped en npm tarball, SECURITY.md, CONTRIBUTING.md) para reflejar
que los 4 bugs B-MCP-2..5 quedaron cerrados en \`v0.1.2-beta.3\`.

## Por que

PR #21 cerro el release v0.1.2-beta.3 pero solo actualizo:
- bumps de version (package.json, composition-root, sonar)
- HANDOFF.md (§0 + §6.16)
- docs/RELEASE-NOTES-v0.1.2-beta.3.md (nuevo)

**Olvidamos los public-facing docs**. Cuando el usuario tomo el
codigo a publish, el README.md root y el code/README.md (shipped en
npm tarball como package README) seguian diciendo:
- "trabajo activo en 0.1.2-beta.x"
- "4 issues abiertos hoy (B-MCP-2..5)"
- "npm install -g @netzi/recall" (defaults a latest=0.1.1 deprecada)

Sin este hotfix, la pagina del paquete en npmjs.com mostraria
informacion incorrecta sobre el estado del producto justo despues
del publish.

## Tipo de cambio

- [x] docs — solo documentacion publica
- [x] hotfix — fix sobre main (perdimos esto en el release; sin esto
      el npm publish sale con package.json correcto pero README
      stale)

## Updates

- **README.md** (root): banner refleja "v0.1.2-beta.3 cierra los 4
  bugs"; quick start nota canal beta; "Issues" reads "0 issues
  abiertos al cierre de Phase-11 (cerrados via PRs #17/#18/#19/#20)".
- **code/README.md** (shipped en npm tarball como package README):
  install command recomienda `@beta` con nota sobre por que; CTA
  apunta a beta.3 en vez de latest=0.1.1 deprecada.
- **SECURITY.md**: tabla de versiones soportadas incluye
  `0.1.2-beta.3` (active), marca `0.1.2-beta.0` como superseded,
  nota que latest=0.1.1 sigue deprecada hasta que 0.1.2 stable
  promote.
- **CONTRIBUTING.md**: "Issues y bugs" reads "0 issues abiertos al
  cierre de Phase-11" con pointers a §6.16 + release notes.

## Artefactos historicos intencionalmente sin tocar

- `docs/RELEASE-NOTES-v0.1.2-beta.0.md` — snapshot de un release
  anterior; release notes son inmutables por convencion.
- `CONTRIBUTING.md` line 139 commit-format example mencionando
  beta.0 — illustrative, no truth claim.

## Checklist

- [x] `npm run typecheck` EXIT=0
- [x] `npm run lint` y `npm run lint:tests` EXIT=0
- [x] `npm run validate:modules` EXIT=0
- [x] `npm run build` EXIT=0
- [x] `npm run test` EXIT=0 (no source files touched, suite previa sigue
valida)
- [ ] N/A — wire/protocolo MCP no cambia
- [ ] N/A — no introduce ADR

## Plan post-merge

Per CONTRIBUTING.md hotfix flow: tras merge a main, hacer
merge-back a develop (PR separado). Adicionalmente, **actualizar el
tag v0.1.2-beta.3** para que apunte al commit con los doc fixes
incluidos antes del npm publish:

\`\`\`bash
git checkout main && git pull
git tag -d v0.1.2-beta.3
git push origin :refs/tags/v0.1.2-beta.3
git tag -a v0.1.2-beta.3 -m "v0.1.2-beta.3"
git push origin v0.1.2-beta.3
gh release edit v0.1.2-beta.3 --target main # apunta el release al nuevo
SHA
cd code && npm publish --tag beta --auth-type=web
\`\`\`

(El tag v0.1.2-beta.3 actualmente apunta a \`a826ef0\`, sin los
doc fixes. Re-tagging es seguro porque el GitHub release de
beta.3 aun no tiene downloads y npm publish todavia no se completo.)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
h2devx added a commit that referenced this pull request May 3, 2026
## Summary

**First stable release of `@netzi/recall`.** Channel promotion from
`0.1.2-beta.6` (commit `f3aca46`) to the `latest` dist-tag. **No code
changes** — same binary bit-for-bit modulo the version string.

Decision: skip the soak 24-48h post-beta.6 (justified by fresh smoke
10/10 PASS + 0 issues + cycle of 7 betas that already closed 8 bugs
vinculated to real use). The cycle's bug discovery rate has been "1 bug
per beta surfaced by dogfood" — staying on beta.6 longer would not
surface new information.

| Item | Value |
|---|---|
| Tag | `v0.1.2` (no suffix) |
| Channel | `latest` (was `beta` for the entire `0.1.2-beta.*` cycle) |
| Tests | **2560 passing** in 212 files (no change vs beta.6) |
| Coverage SonarQube | overall 96.4%, ratings A/A/A |
| Issues at release | **0** open |

## What 0.1.2 stable consolidates

The `0.1.2-beta.*` cycle ran from 2026-04-28 (beta.0) to 2026-05-03
(beta.6). Each release surfaced exactly one bug from real-world dogfood
that the previous release exposed:

| Beta | Date | Bugs closed | PR |
|---|---|---|---|
| `0.1.2-beta.0` | 2026-04-28 | preventive cut after Phase-9 dogfood
discovery | n/a |
| `0.1.2-beta.3` | 2026-05-01 | **B-MCP-2** + **B-MCP-3** + **B-MCP-4**
+ **B-MCP-5** | [#17](#17),
[#18](#18),
[#19](#19),
[#20](#20) |
| `0.1.2-beta.4` | 2026-05-02 | **B-MCP-7** |
[#27](#27) |
| `0.1.2-beta.5` | 2026-05-02 | **B-MCP-8** |
[#33](#33) |
| `0.1.2-beta.6` | 2026-05-03 | **carryover** `serverInfo.version` |
[#37](#37) |
| **`0.1.2` (this)** | 2026-05-03 | channel promotion + npm deprecation
of 0.1.0/0.1.1 | this PR |

Plus B-MCP-1 closed in v0.1.1 (Phase-8 same-day patch). Total: 8 bugs
closed end-to-end via dogfood + smoke loop.

## Files in this release branch

| Capa | Archivos |
|---|---|
| Code | (cero cambios — channel promotion sin cambios de logica) |
| Release tooling | `code/package.json` +
`code/sonar-project.properties` (bump 0.1.2-beta.6 → 0.1.2) |
| Release notes | `docs/RELEASE-NOTES-v0.1.2.md` (NEW, ~250 LOC
consolidating the full cycle + migration guide) |
| Public docs | `README.md`, `code/README.md`, `SECURITY.md` (banner
"stable", install command without @beta, version table updated) |
| HANDOFF | `HANDOFF.md` (§0 + new §6.21 Phase-16 + footer) |

## Validation

| Check | Result |
|---|---|
| `npm run typecheck` | EXIT=0 |
| `npm run lint` (max-warnings 0) | EXIT=0 |
| `npm run lint:tests` (max-warnings 0) | EXIT=0 |
| `npm run validate:modules` | PASS — no module violations |
| `npm run build` (tsup) | EXIT=0 |
| `npm test` | **2560 passing** in 212 files |

## Test plan

- [x] 5+1 EXIT=0 over the release branch.
- [x] PR #38 (release v0.1.2-beta.6) merged to main.
- [x] Fresh smoke 10/10 PASS against npx-installed beta.6 in fresh
workspace (this is the same binary).
- [ ] CI green on this PR.
- [ ] Squash-merge to main.
- [ ] Tag `v0.1.2` annotated to merge commit + push (`git switch
--detach v0.1.2` because of protected-branch hook).
- [ ] `gh release create v0.1.2 --target main --notes-file
docs/RELEASE-NOTES-v0.1.2.md` — **NO `--prerelease`**.
- [ ] `cd code && npm publish --auth-type=web` — **NO `--tag beta`** →
publishes directly to `latest`.
- [ ] `npm deprecate @netzi/recall@0.1.0 "deprecated due to bugs
B-MCP-1..8 (closed in 0.1.2). Use @netzi/recall@latest"`.
- [ ] `npm deprecate @netzi/recall@0.1.1 "deprecated due to bugs
B-MCP-2..8 (closed in 0.1.2). Use @netzi/recall@latest"`.
- [ ] `npm view @netzi/recall dist-tags` should return `{ latest:
'0.1.2', beta: '0.1.2-beta.6' }`.
- [ ] `npx --yes @netzi/recall@latest --help` should execute 0.1.2 (not
0.1.1).
- [ ] Merge-back develop ← main via `chore/sync-develop-after-0.1.2`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

[B-MCP-4] mem.remember silently drops 'content' field for kind='decision' (data loss)

1 participant