From 619c646b298d950c6a8f977bf42b086a1c8079fa Mon Sep 17 00:00:00 2001 From: Harsh Singh Date: Sat, 24 Oct 2026 17:18:19 +0530 Subject: [PATCH 1/6] docs(context): record the v3.1.0 release and refresh the handoff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Why: the release-state facts now exist on the remote — PR #318 merged at c5a141d, tag v3.1.0 pushed, GitHub release published, the back-merge brought the main-only showreel hotfix home — but the handoff still described the pre-release state ("cut the release" as the next step) and the landed-waves ledger had no release line. The closeout DOX pass runs after the gate by design, so these claims are written only now. - What: HANDOFF.md rewritten for the post-release state — release receipts with the branch-guard governance disclosure (admin merge bypassed the 1-review requirement on the principal's order; disclosed here and in current.md), next steps pointing at #313/#314 and the parked spec PRs, Verify block updated to this session's numbers. current.md gains a release bullet on the landed-waves ledger, including the quoted gate evidence, and a numbering correction: the repo's own ledger tops out at Wave 26 (the landing gate); the inherited session summary's "Wave 27" does not exist. - Verification: quoted from this session — bun test 1393 pass / 0 fail (211 files) on both the release branch and the back-merged dev tree; PR #318 CI test pass at 3m14s; check:landed → RESULT: LANDED at dev == origin/dev == c5a141d; the pre-review gate on this branch runs on the PR (branch pushed, PR open, checks green). --- .agents/artifacts/HANDOFF.md | 116 ++++++++++++++++++++--------------- .agents/context/current.md | 15 +++++ 2 files changed, 83 insertions(+), 48 deletions(-) diff --git a/.agents/artifacts/HANDOFF.md b/.agents/artifacts/HANDOFF.md index ef67503..3496a9b 100644 --- a/.agents/artifacts/HANDOFF.md +++ b/.agents/artifacts/HANDOFF.md @@ -1,55 +1,74 @@ -# HANDOFF — 2026-10-22T17:55:00Z -branch: dev | phase: Waves 23, 24 and 25 landed on `dev` via PR #315 (v3.0.2 on `package.json`; the work sits under `[Unreleased]`) +# HANDOFF — 2026-10-24T11:46:48Z +branch: `dev` | phase: **v3.1.0 released to `main`** — tag `v3.1.0`, GitHub release published, release train empty ## Next Step -Cut the release. The engine changes are merged but unreleased: `package.json` -is still `3.0.2` and `CHANGELOG.md` carries T-320, T-321 and the Wave 25 -surface completion under `[Unreleased]`. Per `.agents/standards/workflows.md`, -release cutting is principal-confirmed only — do not tag or publish -unprompted. +Resume the backlog. The two filed issues are the highest-value defects the +last two waves surfaced, and both are ready: -## In-Flight -- **PR #315 merged** into `dev` at `8092ae6`. It landed: - - **Wave 23 / T-320** — the contradiction engine now compares *polarity - claims* (subject, relation, target, scope) instead of seven bare regex - pairs. Measured 8/10 realistic non-contradictory pairs flagged before, - 0/10 after. Calibration fixture `test/calibration/contradiction_calibration.json` - (21 pairs) gated at precision/recall ≥ 0.9. Dry-run surfaces: - `memory conflicts [--dry-run]`, `propose({conflictDryRun})`, - `detectConflict(…, {dryRun})`. - - **Wave 24 / T-321** — the health gate is scoped to one memory project. - `listForProject()` (`src/store.ts`) owns the filter decision; the report - carries `scope` (`filter`, `projects`, `entry_count`, `code_scope`) and - never guesses a project label. - - **Wave 25** — the project filter plumbed through the five surfaces that - still ignored it (`memory_anchor_audit`, `memory_drift_audit`, - `/api/drift`, `/api/cognition/clusters`, `/api/cognition/debt`) and a - labelled **Project Scope** control added to the studio header. -- `origin/dev` (v3.0.2, the website rebuild) was merged into the branch - first; conflicts were in `.agents/context/current.md`, `CHANGELOG.md` and - the generated `claims.json`, and all three were resolved by keeping both - sides. +- **#313 (T-405, HIGH)** — conflicted memories are still injected into agent + prompts. Three retrieval paths (`src/retrieval.ts:1686-1701`, + `src/retrieval/ranking.ts:272-273`, `src/orchestrator/context.ts:144-147`) + exclude `superseded`/`rejected`/`archived`/`expired` but not `conflicted`; + the soft −0.8 `STATUS_PENALTY` (`src/types.ts:342`) is a discount, not a + filter. It changes retrieval output, so the golden recall fixtures must be + regenerated in the same PR. +- **#314 (T-406, LOW)** — `--project` matches case-sensitively and an + exact-match miss returns an empty report whose label still reads the input + (e.g. `--project ALPHA` against `alpha`). + +Both are recorded in `current.md` → *Next Up* and in `.agents/context/ticket-plan.md`. +Also waiting on the principal: the parked spec PRs (#52–#56, R28–R32) each +carry a `feat/*` spec branch and need review before their waves can start. + +## Released (this session) +- **v3.1.0** — PR #318 merged into `main` at `c5a141d` (*"Release: v3.1.0"*), + annotated tag `v3.1.0` pushed on the merge commit, GitHub release + published with notes derived from the stamped changelog: + https://github.com/harshsinghmp/musememory/releases/tag/v3.1.0 +- **Governance disclosure**: the `main` branch-guard ruleset + ("Main Branch Guard & Standards", ruleset 21574872) requires 1 approving + review. The principal ordered the release cut; no second reviewer was + available, so the merge used `--admin` and the bypass of that one review + requirement is written down here and in `current.md`. The `test` check was + green before the merge; previous releases (e.g. `3a7edb1` "Release: + v3.0.2") landed the same way. +- The release carried **Waves 23–26** (PRs #315/#316/#317 on `dev`): + T-320 claim-based contradiction detection (+ `memory conflicts + --dry-run`), T-321 per-project health scoping across every audit + surface, the studio Project Scope control, the README flattening, the + handoff refresh, and the landing gate (CI wiring + vocabulary contract). +- `main`'s only child was the un-back-merged showreel hotfix + (`fix/showreel-hash-path`, `e683ef2`); the post-release back-merge brought + it home (`scripts/showreel-hash.ts`), the branch was pruned local+remote, + and `dev` @ `c5a141d` now shares history with `main` — the Phase-10 merge + was clean, no conflicts. +- Release artifacts: `package.json` `3.1.0`; CHANGELOG stamped + `[3.1.0] - 2026-10-24` with the `v3.0.2...v3.1.0` compare link and one + consolidated `### Added` section (12 items moved byte-identically, + verified per category against HEAD); `claims.json` regenerated because it + embeds the package version; `docs/mcp-tool-matrix.json` and + `docs/cli-reference.md` regenerated byte-identically. +- Process note: this very file was rewritten *after* the release merged, + not inside the release branch — the landing gate forbids writing + release-state claims before `check:landed` proves them, so the closeout + rides its own small PR once the facts are on the remote. ## Decisions +- **SemVer minor (3.0.2 → 3.1.0), not a patch.** The release adds one CLI + command, optional `project` arguments on five existing MCP tools and one + studio control — additive surface, no breaking changes. - **Code-side metrics stay workspace-wide.** Source code is shared between memory projects and cannot be attributed to one, so `scope.code_scope` - always reads `workspace` rather than implying a per-project number. -- **The "−0.8 rank" note was wrong and is corrected.** `STATUS_PENALTY` - maps `conflicted: -0.8` (`src/types.ts:342`) and both retrieval paths - apply it. What is true is that it is a soft discount, not a filter. -- **The two new gaps are filed as issues, not fixed here**: #313 (T-405, - conflicted memories still injected into agent prompts) and #314 (T-406, - case-sensitive `--project` fails silently). Both change retrieval or - filter semantics and need their own ticket and test pass. + always reads `workspace`. +- **The "−0.8 rank" note stays corrected** (from Wave 25's close-out): + `STATUS_PENALTY` maps `conflicted: -0.8` and both retrieval paths apply + it — but a rank discount is not a filter, which is why #313 is filed. +- **Generated docs are regen-at-release, not hand-patched**: `claims.json`, + `docs/mcp-tool-matrix.json` and `docs/cli-reference.md` came from + `bun run claims:generate`. ## Open Issues -- **#313 (T-405, HIGH)** — conflicted memories are still served into agent - prompts with no badge. The gap is wider than recorded: three retrieval - paths, not one (`src/retrieval.ts:1686-1701`, - `src/retrieval/ranking.ts:272-273`, `src/orchestrator/context.ts:144-147`). - Changes retrieval output and therefore the golden recall fixtures. -- **#314 (T-406, LOW)** — `--project` matches case-sensitively; `--project - ALPHA` returns an empty report whose label still reads `ALPHA`. +- **#313 (T-405, HIGH)** and **#314 (T-406, LOW)** — as above, both ready. - **Backlog, unblocked and ready**: T-316 (Hebbian → write-time `linked_memory_ids` seeding; the retrieval-expansion half already landed as T-307, so the premise is partly stale), T-318 (retrieval weight @@ -62,8 +81,9 @@ unprompted. exported with no external caller). ## Verify -`bun test` 1385 pass / 0 fail (210 files) · `bunx tsc --noEmit` clean · -`bun run build` clean · `bun run scripts/generate-claims.ts --check` up to -date · `bun run scripts/check-doc-links.ts` 137 links resolve · -`bun run site:build` clean · axe-core a11y gate 25/25 · -CI on `dev` at `8092ae6`: **success** +`bun test` **1393 pass / 0 fail** (211 files, 6490 expect() calls), re-run +twice on the release branch and again on the back-merged `dev` tree · +`tsc --noEmit` clean · `bun run build` clean · `claims:check` up to date · +`docs:check` 150 links across 102 files resolve · +PR #318 CI `test`: **pass** (3m14s) · +`bun run check:landed` after the back-merge: **RESULT: LANDED**. diff --git a/.agents/context/current.md b/.agents/context/current.md index 99cede5..33bfcc4 100644 --- a/.agents/context/current.md +++ b/.agents/context/current.md @@ -136,6 +136,21 @@ on any blocking failure, so CI runs it. Report: --- ## 🏁 Recently Landed Waves (Verified on `dev`) +- [x] **Released: v3.1.0** (2026-10-24) — Waves 23–26 shipped to `main`: + - PR #318 merged at `c5a141d` (*"Release: v3.1.0"*); annotated tag + [`v3.1.0`](https://github.com/harshsinghmp/musememory/releases/tag/v3.1.0) + pushed on the release commit; GitHub release published with notes + derived from the stamped changelog. `package.json` is `3.1.0` and + `[Unreleased]` is empty again. + - Governance disclosure: the `main` branch-guard ruleset requires one + approving review; the merge used `--admin` on the principal's order to + cut the release (the `test` check was green first). Disclosed also in + the handoff. + - The post-release back-merge reunited `main` with `dev` + (`fix/showreel-hash-path` lived only on `main` until now; the branch is + pruned). Gate evidence, quoted: `bun run check:landed` → + **RESULT: LANDED**; `git merge-base --is-ancestor HEAD origin/dev` + exits 0 at `c5a141d`; `bun test` re-run on the merged tree: 1393 / 0. - [x] **Wave 26: The Landing Gate — a commit is not a delivery**: - On 2026-10-22 two complete waves (T-320, T-321) were committed to a local branch and a local `dev`, and the session ended. No push, no PR, no CI run — while the close-out document described them as landed, under a heading reading *"Recently Landed Waves (Verified on `dev`)"*. `dev` did not contain them; `origin/dev` had already moved to v3.0.2, so the branch had diverged and the work was invisible to review and to the release. **The code was correct and every test was green. The failure was a false claim about the remote that nothing had ever checked.** - `scripts/check-landed.ts` is now the machine-checkable form of that claim: work is **landed** only when it is reachable from the integration branch **on the remote**, and until then it is **local**. The gate verifies `HEAD` is an ancestor of `origin/dev` and that nothing is unpushed, exits non-zero otherwise, and prints the exact commands and SHAs so a report can quote the evidence rather than assert it. A pre-review mode (`--pr --branch `) verifies a branch is pushed, has an open PR, and has no failing checks. Wired into CI for both `pull_request` and `push`. From 76bffe3b672617a104f6a5a3b940703106baf872 Mon Sep 17 00:00:00 2001 From: Harsh Singh Date: Sat, 24 Oct 2026 21:00:20 +0530 Subject: [PATCH 2/6] docs(standards): pin the derived-artifact rule and correct the production branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Why: two Release Invariants were factually off. Nothing required a version bump to regenerate derived artifacts, so bumping the version failed CI's claims:check with no documented cause — this was hit live on the v3.1.0 cut (claims:check failed within seconds of the bump and the branch needed a regeneration commit). And the standard still named `master` as the production branch; this repository's production branch is `main`, and the whole release pipeline that actually ran (v3.1.0, PRs #316–#319) operates on dev/main. - What: .agents/standards/git-workflow.md Release Invariants gains one bullet — any version bump runs `bun run claims:generate` in the same commit, because claims.json embeds the package version and CI runs claims:check on drift — and the staging invariant now says merge to `main` instead of `master`. - Verification: bun run docs:check — 150 relative links across 102 markdown files all resolve (link audit covers this file); the rule text matches the behavior CI already enforces (claims:check in the suite); docs-only change, no code touched, levitated through staging by explicit-path add after git status review. --- .agents/standards/git-workflow.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.agents/standards/git-workflow.md b/.agents/standards/git-workflow.md index 33e8f12..1d036d5 100644 --- a/.agents/standards/git-workflow.md +++ b/.agents/standards/git-workflow.md @@ -97,7 +97,8 @@ All releases and git tags must follow strict `vX.Y.Z` semantic versioning: ### Release Invariants - Sync `package.json` `"version"` with the `vX.Y.Z` tag in the release commit. -- Stage on `release/vX.Y.Z` from `dev` → merge to `master` → back-merge to `dev`. +- Any version bump regenerates derived artifacts **in the same commit**: `claims.json` embeds the package version, so a bump without `bun run claims:generate` fails CI's `claims:check` (caught live on the v3.1.0 cut). +- Stage on `release/vX.Y.Z` from `dev` → merge to `main` → back-merge to `dev`. - CI publishes on `v*` tag push (`git tag -a vX.Y.Z -m "release: vX.Y.Z"`). Never publish manually. - **README "What's New" Rule**: - Keep only the current release/feature highlights in `README.md` under `## 🚀 What's New`. From 87d16045badba6df22822031f5a663a55f721d3b Mon Sep 17 00:00:00 2001 From: Harsh Singh Date: Sat, 24 Oct 2026 21:50:16 +0530 Subject: [PATCH 3/6] fix(retrieval): withhold conflicted memories from prompt-serving paths (#313) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: conflicted memories (contradiction-engine verdicts — known potential falsehoods) were only rank-discounted (-0.8), never filtered, so they still surfaced into recall results, ranked candidates (kept:high_relevance), fused muse context (relevant_memories, confident_answers, route manifests, code anchors) and FTS store queries without a badge (#313 / T-405). What: one shared includeConflicted opt-in honored at every consumer gate, defaulting to exclusion — SearchOptions cascade (ContextQueryOptions → UnifiedSearchOptions); queryContext lifecycle gate (status-scoped guard + cache-key discrimination); FTS hidden-gate + overfetch connective (armed iff any post-fetch hide-gate stays active — OR, not AND); vector passesRowFilter + engine hybrid passthrough; ranking candidate veto; muse context pool choke point, anchor/route lens strips, constraint projection and withheld notice; MCP include_conflicted on both search/memory_recall schemas + auto-branch mapping; CLI --include-conflicted mirror. Temporal/as_of path unchanged (state-at-the-time semantics). Status-scoped audit queries preserved on FTS and queryContext paths. Golden recall fixture untouched (corpus carries zero conflicted entries); tests-first discipline, RED receipts in ledger. Verification: bun test 1400 pass / 0 fail across 212 files (+7 tests); tsc --noEmit clean; build clean; claims.json + derived docs regenerated (1398→1400); Fresh Critic verdict 9.5/10 with mutation-verified overfetch test (reverted connective in isolation → exactly the new test fails). --- claims.json | 6 +- src/cli/retrieval.ts | 1 + src/mcp.ts | 3 + src/orchestrator/context.ts | 47 +++++++- src/orchestrator/types.ts | 5 + src/retrieval.ts | 10 +- src/retrieval/engine.ts | 1 + src/retrieval/ranking.ts | 8 ++ src/sqlite.ts | 17 ++- src/types.ts | 8 ++ src/vector.ts | 3 + test/conflicted_injection.test.ts | 194 ++++++++++++++++++++++++++++++ 12 files changed, 297 insertions(+), 6 deletions(-) create mode 100644 test/conflicted_injection.test.ts diff --git a/claims.json b/claims.json index 5c71c17..53170f3 100644 --- a/claims.json +++ b/claims.json @@ -14,8 +14,8 @@ "networkMillis": 300 }, "tests": { - "count": 1393, - "assertions": 6490, - "files": 211 + "count": 1400, + "assertions": 6511, + "files": 212 } } diff --git a/src/cli/retrieval.ts b/src/cli/retrieval.ts index cdee117..f3c1723 100644 --- a/src/cli/retrieval.ts +++ b/src/cli/retrieval.ts @@ -138,6 +138,7 @@ export async function handleSearchCommand({ positional, flags }: ParsedArgs): Pr tokenBudget, project: flags["project"], includeSuperseded: flags["include-superseded"] === "true", + includeConflicted: flags["include-conflicted"] === "true", type: flags["type"], status: flags["status"], verified: flags["verified"] === "true", diff --git a/src/mcp.ts b/src/mcp.ts index 2179be1..cf22415 100644 --- a/src/mcp.ts +++ b/src/mcp.ts @@ -248,6 +248,7 @@ You are equipped with Muse Memory, an autonomous persistent cognitive memory sys limit: { type: "number" }, token_budget: { type: "number", description: "Maximum token budget to consume" }, include_superseded: { type: "boolean" }, + include_conflicted: { type: "boolean", description: "Include memories flagged conflicted by the contradiction engine (excluded by default — known potential falsehoods)" }, hybrid: { type: "boolean", description: "Use the offline vector+BM25 index (requires 'memory reindex'); falls back to live scoring when absent" }, type: { type: "string" }, status: { type: "string" }, @@ -372,6 +373,7 @@ You are equipped with Muse Memory, an autonomous persistent cognitive memory sys limit: { type: "number" }, token_budget: { type: "number", description: "Maximum token budget to consume" }, include_superseded: { type: "boolean" }, + include_conflicted: { type: "boolean", description: "Include memories flagged conflicted by the contradiction engine (excluded by default — known potential falsehoods)" }, type: { type: "string" }, status: { type: "string" }, verified: { type: "boolean" }, @@ -1988,6 +1990,7 @@ You are equipped with Muse Memory, an autonomous persistent cognitive memory sys limit: clampLimit(a.limit, 10, 100, 1), tokenBudget: typeof a.token_budget === "number" ? a.token_budget : undefined, includeSuperseded: a.include_superseded === true, + includeConflicted: a.include_conflicted === true, type: a.type ? String(a.type) : undefined, status: a.status ? String(a.status) : undefined, verified: a.verified === true, diff --git a/src/orchestrator/context.ts b/src/orchestrator/context.ts index 8c61948..f42af88 100644 --- a/src/orchestrator/context.ts +++ b/src/orchestrator/context.ts @@ -139,12 +139,16 @@ export async function resolveMuseContext( // Also query store for timeless or active constraints const allEntries = list(store); + const conflictedIds = new Set( + allEntries.filter((e) => e.status === "conflicted").map((e) => e.id), + ); const constraintEntries = allEntries.filter( (e) => (e.type === "constraint" || e.temporal_mode === "timeless") && e.status !== "archived" && e.status !== "superseded" && - e.status !== "rejected" + e.status !== "rejected" && + !(e.status === "conflicted" && !input.include_conflicted) ); for (const ce of constraintEntries) { @@ -173,6 +177,10 @@ export async function resolveMuseContext( for (const e of allEntries) { if (!e.anchors || e.anchors.length === 0) continue; if (e.status === "archived" || e.status === "superseded") continue; + // T-405: conflicted entries' code anchors stay out of the lens too — + // otherwise the anchor coordinates leak the existence of a memory the + // pool gate just withheld (mirror of the route-lens strip). + if (e.status === "conflicted" && !input.include_conflicted) continue; for (const anc of e.anchors) { let fileMatch = false; @@ -217,6 +225,7 @@ export async function resolveMuseContext( suppressReinjection: false, suppressSelfEcho: false, suppressSeenChunks: false, + includeConflicted: input.include_conflicted === true, recordSeenChunks: false, echoHorizonMs: input.echo_horizon_ms, agent: input.agent, @@ -234,6 +243,19 @@ export async function resolveMuseContext( }); // Merge anchored memories to front of candidate pool + // T-405: strip conflicted ids from the ids-only lenses too (matched_routes, + // route_prioritized_ids) so a badgeless existence leak cannot route around + // the content gate through the route manifest. + if (!input.include_conflicted && conflictedIds.size > 0) { + routeLookup.matched_routes = routeLookup.matched_routes.map((rt) => ({ + ...rt, + entry_ids: rt.entry_ids.filter((id) => !conflictedIds.has(id)), + })); + routeLookup.prioritized_entry_ids = routeLookup.prioritized_entry_ids.filter( + (id) => !conflictedIds.has(id), + ); + } + const candidatePool: MemoryEntry[] = trivialGate.isTrivial ? [] : [...entriesWithMatchingAnchors]; if (!trivialGate.isTrivial) { @@ -254,6 +276,23 @@ export async function resolveMuseContext( } } + // T-405: Contradiction engine verdicts — conflicted memories withheld by + // default unless include_conflicted: true. Applied at the pool choke point + // (before the trust gate) so code anchors, route pulls and ranked candidates + // share ONE gate instead of three per-source filters. + const withheldConflicted: Array<{ id: string; reason: string }> = []; + if (!input.include_conflicted && conflictedIds.size > 0) { + for (let i = candidatePool.length - 1; i >= 0; i--) { + if (conflictedIds.has(candidatePool[i].id)) { + const removed = candidatePool.splice(i, 1)[0]; + withheldConflicted.push({ + id: removed.id, + reason: "conflicted_status - resolve in memory conflicts; opt-in via include_conflicted: true", + }); + } + } + } + // T-075: Ingest trust tiers — untrusted memories withheld by default unless include_untrusted: true const withheldUntrusted: Array<{ id: string; reason: string; file_path: string; symbol_name?: string }> = []; let filteredCandidates = candidatePool; @@ -492,6 +531,12 @@ export async function resolveMuseContext( ); } + if (withheldConflicted.length > 0) { + suggestedNextSteps.push( + `${withheldConflicted.length} conflicted memor${withheldConflicted.length === 1 ? "y" : "ies"} withheld — resolve them via memory conflicts or pass include_conflicted: true`, + ); + } + if (digestRefusalReason) { suggestedNextSteps.push( `Refused incomplete or corrupted CURRENT.md digest: ${digestRefusalReason} — re-generate with memory handoff or repair CURRENT.md`, diff --git a/src/orchestrator/types.ts b/src/orchestrator/types.ts index 635ab09..8555fc7 100644 --- a/src/orchestrator/types.ts +++ b/src/orchestrator/types.ts @@ -24,6 +24,11 @@ export interface MuseContextInput { include_drifted?: boolean; /** T-075: Include memories with untrusted provenance (filtered by default). */ include_untrusted?: boolean; + /** + * T-405: Include memories flagged `conflicted` by the contradiction engine + * (excluded by default — a conflicted memory is a known potential falsehood). + */ + include_conflicted?: boolean; /** T-108: Session ID for turn-level re-injection suppression and LRU tracking. */ session_id?: string; /** T-108: Turn number within session (1-indexed). Defaults to auto-incremented session turn. */ diff --git a/src/retrieval.ts b/src/retrieval.ts index 5e6c41e..b8fac07 100644 --- a/src/retrieval.ts +++ b/src/retrieval.ts @@ -1689,6 +1689,14 @@ export function queryContext( hidden_count++; return false; } + // T-405: conflicted memories are known potential falsehoods — withheld + // from prompts by default (badgeless injection defect #313). A caller + // that explicitly asks for the conflicted status (audits, conflicts + // listings) still receives them — same guard as the archived gate. + if (!options.includeConflicted && options.status !== "conflicted" && e.status === "conflicted") { + hidden_count++; + return false; + } if (!options.includeArchived && options.status !== "archived" && e.status === "archived") { hidden_count++; return false; @@ -2096,7 +2104,7 @@ export function formatPromptContext( // made a "what did we believe in Q1?" query return today's answer from the // 30s TTL cache. The include* / types / tags flags have the same problem. const cacheKey = store.cache - ? `ctx:${query}:${options.project || ""}:${options.depth || "L2"}:${options.limit ?? 5}:${options.tokenBudget ?? ""}:${options.type || ""}:${options.status || ""}:${options.verified ? "1" : "0"}:${options.minVeracity ?? ""}:${options.as_of || ""}:${options.includeSuperseded ? "1" : "0"}:${options.includeArchived ? "1" : "0"}:${options.includeUntrusted ? "1" : "0"}:${options.types?.join(",") || ""}:${options.tags?.join(",") || ""}` + ? `ctx:${query}:${options.project || ""}:${options.depth || "L2"}:${options.limit ?? 5}:${options.tokenBudget ?? ""}:${options.type || ""}:${options.status || ""}:${options.verified ? "1" : "0"}:${options.minVeracity ?? ""}:${options.as_of || ""}:${options.includeSuperseded ? "1" : "0"}:${options.includeConflicted ? "1" : "0"}:${options.includeArchived ? "1" : "0"}:${options.includeUntrusted ? "1" : "0"}:${options.types?.join(",") || ""}:${options.tags?.join(",") || ""}` : undefined; if (cacheKey && store.cache) { diff --git a/src/retrieval/engine.ts b/src/retrieval/engine.ts index 91cfe0f..7cc001d 100644 --- a/src/retrieval/engine.ts +++ b/src/retrieval/engine.ts @@ -201,6 +201,7 @@ export class RetrievalEngine { // row filter as the exact path or stale/cross-project rows leak into context. project: options.project, includeSuperseded: options.includeSuperseded, + includeConflicted: options.includeConflicted, includeArchived: options.includeArchived, includeExpired: options.includeExpired, now: options.now, diff --git a/src/retrieval/ranking.ts b/src/retrieval/ranking.ts index 4cea077..89b417e 100644 --- a/src/retrieval/ranking.ts +++ b/src/retrieval/ranking.ts @@ -176,6 +176,8 @@ export async function rankAndRetrieveMemories( activeFilePath?: string; targetSymbol?: string; includeSuperseded?: boolean; + /** T-405: include conflicted memories in ranked candidates (excluded by default). */ + includeConflicted?: boolean; now?: number; /** R21: precomputed semantic cosine scores by entry id. */ semanticScores?: Map; @@ -272,6 +274,12 @@ export async function rankAndRetrieveMemories( if (!options.includeSuperseded) { entries = entries.filter((e) => e.status !== "superseded" && e.status !== "rejected"); } + // T-405: conflicted candidates vetoed before scoring — a rank discount + // (STATUS_PENALTY -0.8) is not a filter; #313 showed conflicted entries + // surfacing as kept:high_relevance. + if (!options.includeConflicted) { + entries = entries.filter((e) => e.status !== "conflicted"); + } // T-108: Re-injection suppression per session if (options.sessionId && options.suppressReinjection !== false) { const filtered = defaultReinjectionTracker.filterSuppressed(options.sessionId, entries, { diff --git a/src/sqlite.ts b/src/sqlite.ts index 5303a7a..602afbd 100644 --- a/src/sqlite.ts +++ b/src/sqlite.ts @@ -611,6 +611,8 @@ export function searchMemoriesFts( status?: string; limit?: number; includeSuperseded?: boolean; + /** T-405: include conflicted rows in FTS results (excluded by default). */ + includeConflicted?: boolean; overfetch?: boolean; }, ): FtsSearchHits { @@ -661,7 +663,15 @@ export function searchMemoriesFts( // query an over-fetch buffer so active results fill the requested limit. const targetLimit = filters?.limit; if (targetLimit) { - const shouldOverfetch = filters?.overfetch !== false && !filters?.includeSuperseded && !filters?.status; + // Overfetch whenever any post-fetch hide-gate can still remove rows: an + // explicit status filter owns the window (its caller asked for the status), + // otherwise both lifecycle gates must be fully open. The connective is OR + // — includeSuperseded alone does not disarm the conflicted gate, and vice + // versa (T-405: sibling-gate truncation, critic round 1). + const shouldOverfetch = + filters?.overfetch !== false && + !filters?.status && + (!filters?.includeSuperseded || !filters?.includeConflicted); const fetchLimit = shouldOverfetch ? Math.min(500, Math.max(targetLimit * 2, targetLimit + 10)) : targetLimit; sql += ` LIMIT ?`; params.push(fetchLimit); @@ -678,6 +688,11 @@ export function searchMemoriesFts( hidden_count++; continue; } + // T-405: conflicted rows count as hidden prompt material, not results + if (!filters?.includeConflicted && !filters?.status && entry.status === "conflicted") { + hidden_count++; + continue; + } activeHits.push({ entry, rank: r.rank }); } diff --git a/src/types.ts b/src/types.ts index 30c6a5a..751b90f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -459,6 +459,14 @@ export interface SearchOptions { limit?: number; project?: string; includeSuperseded?: boolean; + /** + * T-405: include memories flagged `conflicted` by the contradiction engine. + * Default: excluded from every prompt-serving read path (recall, ranked + * context, fused muse context, FTS store queries), because a conflicted + * memory is a known potential falsehood. Audit surfaces (conflicts + * listings, status-scoped queries) are unaffected. + */ + includeConflicted?: boolean; type?: MemoryType | string; /** Multi-type filter (SOW-106 agent contracts). */ types?: string[]; diff --git a/src/vector.ts b/src/vector.ts index 8cde85e..4da6c8f 100644 --- a/src/vector.ts +++ b/src/vector.ts @@ -209,6 +209,8 @@ export interface HybridSearchOptions { /** Restrict candidate rows to this project (undefined = cross-project search). */ project?: string; includeSuperseded?: boolean; + /** T-405: include conflicted rows in hybrid search candidates (excluded by default). */ + includeConflicted?: boolean; includeArchived?: boolean; includeExpired?: boolean; /** Clock for expiry evaluation; defaults to Date.now(). */ @@ -229,6 +231,7 @@ export interface HybridSearchOptions { function passesRowFilter(entry: MemoryEntry, o: HybridSearchOptions, now: number): boolean { if (o.project && entry.project !== o.project) return false; if (!o.includeSuperseded && (entry.status === "superseded" || entry.status === "rejected")) return false; + if (!o.includeConflicted && entry.status === "conflicted") return false; if (!o.includeArchived && entry.status === "archived") return false; if (!o.includeExpired && isExpired(entry, now)) return false; return true; diff --git a/test/conflicted_injection.test.ts b/test/conflicted_injection.test.ts new file mode 100644 index 0000000..265cce1 --- /dev/null +++ b/test/conflicted_injection.test.ts @@ -0,0 +1,194 @@ +import { describe, it, expect, afterEach } from "bun:test"; +import { mkdirSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { openStore, save } from "../src/store.ts"; +import { searchMemoriesFts } from "../src/sqlite.ts"; +import { queryContext } from "../src/retrieval.ts"; +import { rankAndRetrieveMemories } from "../src/retrieval/ranking.ts"; +import { resolveMuseContext } from "../src/orchestrator/context.ts"; +import type { MemoryStatus } from "../src/types.ts"; + +/** + * T-405 / issue #313 — conflicted memories must not be injected into agent + * prompts with no badge. + * + * Pre-fix defect: every prompt-serving read path filters `superseded`, + * `rejected`, `archived` and `expired` — but NOT `conflicted`. The soft + * -0.8 STATUS_PENALTY (src/types.ts) is a rank discount, not a filter, so a + * memory flagged by the contradiction engine (a known potential falsehood) + * is still served, unseen and unmarked, into recall results, ranked context + * tier candidates, fused muse context and the constraint projection. + * + * Bar: the Wave-24 scope precedent (single flag honoured at consumer gates, + * PR #315) — one `includeConflicted` opt-in, default excluded, accepted at + * every prompt-serving surface that already honours `includeSuperseded`. + * + * This corpus is self-contained (not the frozen recall-golden.json corpus, + * which carries no conflicted entry and must not grow without usage + * telemetry — T-143 promotion discipline). + */ + +const TEST_DIR = join(import.meta.dir, ".tmp_conflicted_injection_test"); + +describe("T-405: conflicted memories are withheld from prompt-serving paths by default", () => { + let root: string; + let store: ReturnType; + + const ACTIVE_ID = "t405_active_port"; + const CONFLICTED_ID = "t405_conflicted_port"; + + const newStore = () => { + mkdirSync(TEST_DIR, { recursive: true }); + root = TEST_DIR; + store = openStore(TEST_DIR); + + const now = new Date().toISOString(); + save(store, { + id: ACTIVE_ID, + title: "Auth gateway port is 3000", + content: "The auth gateway serves on port 3000 (verified in production).", + type: "discovery", + status: "active", + tags: ["networking", "gateway"], + project: "core", + created_at: now, + updated_at: now, + }); + + save(store, { + id: CONFLICTED_ID, + title: "Auth gateway port is 8080", + content: "The auth gateway serves on port 8080 (--conflicted by the contradiction engine).", + type: "discovery", + status: "conflicted", + tags: ["networking", "gateway"], + project: "core", + created_at: now, + updated_at: now, + }); + }; + + afterEach(() => { + rmSync(TEST_DIR, { recursive: true, force: true }); + }); + + it("FTS store query hides conflicted rows and counts them as hidden", () => { + newStore(); + const hits = searchMemoriesFts(store.db!, "port", { limit: 10 }); + const ids = hits.map((h) => h.entry.id); + expect(ids).not.toContain(CONFLICTED_ID); + expect(hits.hidden_count).toBe(1); + expect(ids).toContain(ACTIVE_ID); + + // Opt-in: the operator may still ask for conflicted rows explicitly. + const opted = searchMemoriesFts(store.db!, "port", { limit: 10, includeConflicted: true }); + expect(opted.map((h) => h.entry.id)).toContain(CONFLICTED_ID); + }); + + it("queryContext excludes conflicted entries from recall output", () => { + newStore(); + const result = queryContext(store, "port", { limit: 5 }); + const ids = result.results.map((r) => r.entry.id); + expect(ids).not.toContain(CONFLICTED_ID); + expect(ids).toContain(ACTIVE_ID); + + // Opt-in symmetric with includeSuperseded. + const opted = queryContext(store, "port", { limit: 5, includeSuperseded: true, includeConflicted: true }); + expect(opted.results.map((r) => r.entry.id)).toContain(CONFLICTED_ID); + }); + + it("ranked retrieval vetoes conflicted candidates before scoring", async () => { + newStore(); + const result = await rankAndRetrieveMemories(store, "auth gateway port", { limit: 5, now: Date.now() }); + const serialized = JSON.stringify(result); + expect(serialized).not.toContain(CONFLICTED_ID); + + const opted = await rankAndRetrieveMemories(store, "auth gateway port", { + limit: 5, + now: Date.now(), + includeSuperseded: true, + includeConflicted: true, + }); + expect(JSON.stringify(opted)).toContain(CONFLICTED_ID); + }); + + it("fused muse context never injects conflicted constraints", async () => { + newStore(); + const fused = await resolveMuseContext(store, root, { query: "auth gateway port", token_budget: 400 }); + const serialized = JSON.stringify(fused); + expect(serialized).not.toContain(CONFLICTED_ID); + expect(serialized).toContain(ACTIVE_ID); + }); + + it("an explicit status query asking for conflicted rows still returns them", () => { + newStore(); + // Producers that query a specific status (audits, conflicts listings, + // health tools) must keep working — the exclusion is the *default* + // lifecycle filter, not a ban on reading the status. + const hits = searchMemoriesFts(store.db!, "port", { limit: 10, status: "conflicted" }); + expect(hits.map((h) => h.entry.id)).toContain(CONFLICTED_ID); + }); + + it("an explicit status query reaches conflicted rows through queryContext too", () => { + newStore(); + // Critic round 1 (Major #2): the conflicted hide-gate in queryContext must + // carry the same options.status guard the archived gate already has, or a + // status-scoped audit query hides the rows it explicitly asked for and + // returns an unexplained empty result. + const res = queryContext(store, "port", { limit: 5, status: "conflicted" }); + expect(res.results.map((r) => r.entry.id)).toContain(CONFLICTED_ID); + expect(res.results.map((r) => r.entry.id)).not.toContain(ACTIVE_ID); + }); + + it("overfetch stays armed when a sibling lifecycle gate remains active", () => { + newStore(); + const db = store.db!; + const now = new Date().toISOString(); + const dense = (id: string, title: string, content: string, status: MemoryStatus) => + save(store, { + id, + title, + content, + type: "discovery", + status, + tags: ["networking"], + project: "core", + created_at: now, + updated_at: now, + }); + + // Two actives match the query once; the two hidden-class entries match + // twice (tf=2) so bm25 ranks them INSIDE the limit-2 fetch window. That + // reproduces the truncation the critic proved: with the window full of + // rows that a sibling gate will hide, overfetch is the only thing that + // lets the actives through. + dense("t405_active_z1", "Zebra deployment notes", "zebra migration notes for the edge cluster", "active"); + dense("t405_active_z2", "Zebra rollout checklist", "zebra rollout checklist for the edge cluster", "active"); + dense("t405_superseded_z", "Zebra migration superseded", "zebra zebra superseded migration notes", "superseded"); + dense("t405_conflicted_z", "Zebra port conflicted", "zebra zebra conflicted port mismatch notes", "conflicted"); + + // Opt into superseded (its gate opens) — but the conflicted gate stays + // active, so overfetch must remain armed or the actives vanish behind the + // hidden rows filling the limit window. limit=2 with two tf=2 hidden rows + // means the unfixed connective returns zero actives; which ONE active + // accompanies the dense rows is bm25 tie-order, so pin membership, not position. + const opened = searchMemoriesFts(db, "zebra", { limit: 2, includeSuperseded: true }); + const openedIds = opened.map((h) => h.entry.id); + expect(openedIds.filter((id) => id.startsWith("t405_active_z")).length).toBeGreaterThanOrEqual(1); + expect(opened.hidden_count).toBeGreaterThanOrEqual(1); + + // Mirror case: opt into conflicted — the superseded gate stays active. + const mirrored = searchMemoriesFts(db, "zebra", { limit: 2, includeConflicted: true }); + const mirroredIds = mirrored.map((h) => h.entry.id); + expect(mirroredIds.filter((id) => id.startsWith("t405_active_z")).length).toBeGreaterThanOrEqual(1); + + // Default options: both hidden classes stay out; overfetch guarantees the + // actives still fill the limit. + const plain = searchMemoriesFts(db, "zebra", { limit: 2 }); + const plainIds = plain.map((h) => h.entry.id); + expect(plainIds).toContain("t405_active_z1"); + expect(plainIds).toContain("t405_active_z2"); + expect(plainIds).not.toContain("t405_superseded_z"); + expect(plainIds).not.toContain("t405_conflicted_z"); + }); +}); From 30fa0a9a0d7f857e4f2144dfd3222d84c62e9cd7 Mon Sep 17 00:00:00 2001 From: Harsh Singh Date: Sat, 24 Oct 2026 22:19:31 +0530 Subject: [PATCH 4/6] fix(health): resolve project names case-insensitively, fail loud (#314) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: `memory health --project ALPHA` against a store holding `alpha` returned a silent empty report labelled with the caller's casing (#314 / T-406) — and the MCP `muse_health` tool and Studio `/api/health?project=` endpoint shared the silent-empty behavior, so scripts, agents and dashboards could not tell a typo from a store with no data. What: one resolution owner — `resolveProjectName` + `listProjectNames` + `formatAvailable` in `src/health/resolve.ts` (exact match wins; unique case-insensitive match returns the store's spelling; unknown or ambiguous stays unresolved). All three surfaces resolve first and fail loud second: CLI exits 1 with the requested name echoed plus the available list and a stderr note when canonical differs from the requested form (--json stdout stays pure JSON); MCP `muse_health` returns an isError result; Studio answers 404 with the same JSON error. `evaluateProjectHealth`/ `listForProject` stay pure exact-canonical reporters — no store rewrite, no gate change; the existing scope tests pass untouched (tests-first RED receipts in the iteration ledger; helper-first disposition noted there). Verification: bun test 1408 pass / 0 fail across 213 files (+8 tests); RED receipts: 2 pass / 6 fail pre-wiring, independently reproduced by the Fresh Critic (reverted wiring, restored byte-identical); tsc --noEmit, build, claims/docs artifacts current; secret scan clean; golden recall fixtures untouched (no retrieval change). Fresh Critic final: 9/10, integrate confirmed (note-stream verified by empirical stdout probe). --- claims.json | 6 +- src/cli/health.ts | 18 +- src/health/index.ts | 1 + src/health/resolve.ts | 45 +++++ src/mcp.ts | 14 +- src/ui.ts | 19 +- test/health_project_resolution.test.ts | 236 +++++++++++++++++++++++++ 7 files changed, 329 insertions(+), 10 deletions(-) create mode 100644 src/health/resolve.ts create mode 100644 test/health_project_resolution.test.ts diff --git a/claims.json b/claims.json index 53170f3..3d5f4a9 100644 --- a/claims.json +++ b/claims.json @@ -14,8 +14,8 @@ "networkMillis": 300 }, "tests": { - "count": 1400, - "assertions": 6511, - "files": 212 + "count": 1408, + "assertions": 6541, + "files": 213 } } diff --git a/src/cli/health.ts b/src/cli/health.ts index 380cdca..7e3aef8 100644 --- a/src/cli/health.ts +++ b/src/cli/health.ts @@ -1,6 +1,6 @@ import { join } from "node:path"; import { openStore } from "../store.ts"; -import { evaluateProjectHealth } from "../health/index.ts"; +import { evaluateProjectHealth, resolveProjectName, formatAvailable } from "../health/index.ts"; import { requireRoot, type ParsedArgs } from "./shared.ts"; export async function handleHealthCommand(parsed: ParsedArgs): Promise { @@ -13,7 +13,21 @@ export async function handleHealthCommand(parsed: ParsedArgs): Promise { // T-321: `--project` scopes the report to one memory project. Without it // the report covers the whole store and says so. - const project = parsed.flags["project"] as string | undefined; + // T-406: resolve the requested project to the canonical store casing, or + // fail loudly — a silent empty report labelled with the caller's casing + // reads like a store problem to the person who just spent the tokens. + const requestedProject = (parsed.flags["project"] as string | undefined)?.trim() || undefined; + const canonicalProject = resolveProjectName(store, requestedProject); + if (requestedProject && canonicalProject === undefined) { + console.error( + `Error: no memories for project "${requestedProject}" — available: ${formatAvailable(store)}`, + ); + return 1; + } + const project = canonicalProject; + if (canonicalProject && canonicalProject !== requestedProject) { + console.error(`Note: project filter matched canonical "${canonicalProject}" (requested "${requestedProject}").`); + } const report = evaluateProjectHealth(store, root, { project }); diff --git a/src/health/index.ts b/src/health/index.ts index f438ea0..4d10b74 100644 --- a/src/health/index.ts +++ b/src/health/index.ts @@ -1,3 +1,4 @@ export * from "./types.ts"; export * from "./gate.ts"; export * from "./reconcile.ts"; +export * from "./resolve.ts"; diff --git a/src/health/resolve.ts b/src/health/resolve.ts new file mode 100644 index 0000000..4986e17 --- /dev/null +++ b/src/health/resolve.ts @@ -0,0 +1,45 @@ +import type { Store } from "../store.ts"; +import { list } from "../store.ts"; + +/** + * T-406: sorted unique project names present in the store, canonical casing. + */ +export function listProjectNames(store: Store): string[] { + return [ + ...new Set( + list(store) + .map((e) => e.project) + .filter((p): p is string => Boolean(p)), + ), + ].sort(); +} + +/** + * T-406: resolve a user-supplied project name to the canonical store casing. + * + * Exact match wins — a store holding both `alpha` and `ALPHA` is resolved by + * exactly what the caller asked for. Otherwise a UNIQUE case-insensitive + * match returns the store's own spelling. No match — or an AMBIGUOUS one + * (two projects differing only in case) — returns `undefined` so the caller + * can fail loudly and list what is actually available. + * + * The gate's own compare (`listForProject`) stays exact-canonical; this is + * the user-input layer, not a store rewrite. + */ +export function resolveProjectName(store: Store, requested?: string): string | undefined { + const name = requested?.trim(); + if (!name) return undefined; + const projects = listProjectNames(store); + if (projects.includes(name)) return name; + const matches = projects.filter((p) => p.toLowerCase() === name.toLowerCase()); + return matches.length === 1 ? matches[0] : undefined; +} + +/** + * T-406: render the available project list for loud-failure messages — one + * formatter so the three surfaces cannot drift apart in wording. + */ +export function formatAvailable(store: Store): string { + const available = listProjectNames(store); + return available.length > 0 ? available.join(", ") : "none"; +} diff --git a/src/mcp.ts b/src/mcp.ts index cf22415..c31adf6 100644 --- a/src/mcp.ts +++ b/src/mcp.ts @@ -111,7 +111,7 @@ import { analyzeTechnicalDebt, runDistillRouter, } from "./cognition/index.ts"; -import { evaluateProjectHealth, reconcileCodeAnchors } from "./health/index.ts"; +import { evaluateProjectHealth, reconcileCodeAnchors, resolveProjectName, formatAvailable } from "./health/index.ts"; import { analyzeMemoryCodeImpact } from "./intelligence/index.ts"; import { generatePrContext } from "./compaction/index.ts"; import { @@ -3026,8 +3026,18 @@ You are equipped with Muse Memory, an autonomous persistent cognitive memory sys } case "muse_health": { // T-321: optional project scope filter. + // T-406: the requested name resolves case-insensitively to the + // canonical store spelling; an unknown project is a loud tool error + // naming what IS available — not a silent empty report. + const requestedProject = a.project ? String(a.project).trim() || undefined : undefined; + const canonicalProject = resolveProjectName(activeStore, requestedProject); + if (requestedProject && canonicalProject === undefined) { + return toolError( + `no memories for project "${requestedProject}" — available: ${formatAvailable(activeStore)}`, + ); + } const report = evaluateProjectHealth(activeStore, activeRoot, { - project: a.project ? String(a.project) : undefined, + project: canonicalProject, }); return toolResult(report); } diff --git a/src/ui.ts b/src/ui.ts index b774114..81cccec 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -11,7 +11,7 @@ import { RetrievalEngine } from "./retrieval/index.ts"; import { validateStore } from "./schema.ts"; import { exportSnapshot } from "./snapshot.ts"; import { getStorageLayout } from "./store.ts"; -import { evaluateProjectHealth } from "./health/index.ts"; +import { evaluateProjectHealth, resolveProjectName, formatAvailable } from "./health/index.ts"; import { listAdrs, detectDocumentationCodeDrift } from "./adrs/index.ts"; import { explainWhyCodeIsTheWayItIs, @@ -511,9 +511,22 @@ export function startUiServer(opts: UiServerOptions): Promise<{ port: number; cl if (pathname === "/api/health" && req.method === "GET") { try { // T-321: optional project scope filter. - const healthProject = url.searchParams.get("project") || undefined; + // T-406: the requested name resolves case-insensitively to the + // canonical store spelling; an unknown project answers 404 with + // what IS available instead of a silent 200-empty report. + const requestedProject = url.searchParams.get("project")?.trim() || undefined; + const canonicalProject = resolveProjectName(opts.store, requestedProject); + if (requestedProject && canonicalProject === undefined) { + res.writeHead(404, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + error: `no memories for project "${requestedProject}" — available: ${formatAvailable(opts.store)}`, + }), + ); + return; + } const report = evaluateProjectHealth(opts.store, workspaceRoot, { - project: healthProject, + project: canonicalProject, }); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify(report)); diff --git a/test/health_project_resolution.test.ts b/test/health_project_resolution.test.ts new file mode 100644 index 0000000..18608b1 --- /dev/null +++ b/test/health_project_resolution.test.ts @@ -0,0 +1,236 @@ +import { describe, it, expect, beforeEach, afterEach } from "bun:test"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { openStore, save, type Store } from "../src/store.ts"; +import { createServer } from "../src/mcp.ts"; +import { startUiServer } from "../src/ui.ts"; +import { handleHealthCommand } from "../src/cli/health.ts"; +import { resolveProjectName } from "../src/health/index.ts"; +import type { MemoryEntry } from "../src/types.ts"; + +/** + * T-406 / issue #314 — `memory health --project P` matched project names + * case-sensitively and silently: `--project ALPHA` against a store holding + * `alpha` returned an empty report labelled with the caller's casing. This + * diff introduces one resolution helper (`resolveProjectName`, owned in + * src/health/resolve.ts) and wires all three surfaces (CLI, MCP `muse_health`, + * Studio `/api/health`) to resolve first and fail loud second. + * + * The gate itself (`evaluateProjectHealth`) stays a pure reporting function — + * its compare remains exact-canonical, so the existing + * `test/health_project_scope.test.ts` behaviour ("empty scope for a project + * that does not exist") is untouched. + */ +function entry(partial: Partial & { id: string; project: string }): MemoryEntry { + return { + title: partial.id, + content: `${partial.id} content`, + status: "confirmed", + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + tags: [], + ...partial, + } as MemoryEntry; +} + +function seedStore(projects: string[]): { store: Store; memoryDir: string; tmpDir: string } { + const tmpDir = mkdtempSync(join(tmpdir(), "muse-t406-")); + const memoryDir = join(tmpDir, ".memory"); + const store = openStore(memoryDir); + for (const project of projects) { + save(store, entry({ id: `m_${project}_1`, project, type: "discovery" })); + } + return { store, memoryDir, tmpDir }; +} + +describe("T-406: resolveProjectName resolution helper", () => { + it("returns the canonical name for a case-insensitive unique match", () => { + const { store, tmpDir } = seedStore(["alpha", "beta"]); + try { + expect(resolveProjectName(store, "ALPHA")).toBe("alpha"); + expect(resolveProjectName(store, " alpha ")).toBe("alpha"); + expect(resolveProjectName(store, "alpha")).toBe("alpha"); + } finally { + rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + it("returns undefined for unknown and for ambiguous requests", () => { + const { store, tmpDir } = seedStore(["alpha", "beta"]); + try { + expect(resolveProjectName(store, "nonexistent")).toBeUndefined(); + expect(resolveProjectName(store, undefined)).toBeUndefined(); + expect(resolveProjectName(store, " ")).toBeUndefined(); + } finally { + rmSync(tmpDir, { recursive: true, force: true }); + } + + const ambiguous = mkdtempSync(join(tmpdir(), "muse-t406-amb-")); + const ambMemoryDir = join(ambiguous, ".memory"); + const ambStore = openStore(ambMemoryDir); + save(ambStore, entry({ id: "m_a_low", project: "alpha", type: "discovery" })); + save(ambStore, entry({ id: "m_a_up", project: "ALPHA", type: "discovery" })); + try { + // Exact request wins even in an ambiguous store. + expect(resolveProjectName(ambStore, "ALPHA")).toBe("ALPHA"); + expect(resolveProjectName(ambStore, "alpha")).toBe("alpha"); + // A request matching two projects that differ only in case is unresolved. + expect(resolveProjectName(ambStore, "alpHa")).toBeUndefined(); + } finally { + rmSync(ambiguous, { recursive: true, force: true }); + } + }); +}); + +describe("T-406: CLI surface is loud and canonical", () => { + let harness: ReturnType; + + beforeEach(() => { + harness = seedStore(["alpha", "beta"]); + }); + + afterEach(() => { + rmSync(harness.tmpDir, { recursive: true, force: true }); + }); + + it("exits 1 with an explicit line when the project matches nothing", async () => { + const logs: string[] = []; + const errs: string[] = []; + const originalLog = console.log; + const originalError = console.error; + console.log = (...args: unknown[]) => logs.push(args.map(String).join(" ")); + console.error = (...args: unknown[]) => errs.push(args.map(String).join(" ")); + let exitCode = 0; + try { + exitCode = await handleHealthCommand({ + positional: [], + flags: { dir: harness.tmpDir, project: "nonexistent" }, + }); + } finally { + console.log = originalLog; + console.error = originalError; + } + + expect(exitCode).toBe(1); + const errText = errs.join("\n"); + expect(errText).toContain('no memories for project "nonexistent"'); + expect(errText).toContain("alpha"); + expect(errText).toContain("beta"); + }); + + it("labels the report with the canonical name and notes the requested form", async () => { + const logs: string[] = []; + const errs: string[] = []; + const originalLog = console.log; + const originalError = console.error; + console.log = (...args: unknown[]) => logs.push(args.map(String).join(" ")); + console.error = (...args: unknown[]) => errs.push(args.map(String).join(" ")); + try { + await handleHealthCommand({ + positional: [], + flags: { dir: harness.tmpDir, project: "ALPHA" }, + }); + } finally { + console.log = originalLog; + console.error = originalError; + } + + // The note rides the error stream so --json stdout stays parseable. + expect(errs.join("\n")).toContain('(requested "ALPHA")'); + expect(errs.join("\n")).not.toContain("Error:"); + // The report itself never echoes the caller's casing (no more lying label). + expect(logs.join("\n")).not.toContain("ALPHA"); + expect(logs.join("\n")).toContain("alpha"); + }); +}); + +describe("T-406: MCP muse_health resolves and fails loud", () => { + let harness: ReturnType; + + beforeEach(() => { + harness = seedStore(["alpha", "beta"]); + }); + + afterEach(() => { + rmSync(harness.tmpDir, { recursive: true, force: true }); + }); + + const getCallHandler = () => { + const server = createServer(harness.tmpDir, "full"); + return (server as any)._requestHandlers.get("tools/call") as ( + req: unknown, + ) => Promise<{ isError?: boolean; content: Array<{ text: string }> }>; + }; + + it("serves the canonical report for a case-insensitive request", async () => { + const callHandler = getCallHandler(); + const res = await callHandler({ + method: "tools/call", + params: { + name: "muse_health", + arguments: { dir: harness.tmpDir, project: "ALPHA" }, + }, + }); + + expect(res.isError).toBeFalsy(); + const report = JSON.parse(res.content[0].text) as { project: string; scope: { entry_count: number } }; + expect(report.project).toBe("alpha"); + expect(report.scope.entry_count).toBe(1); + }); + + it("returns no memories-for-project error for an unknown project", async () => { + const callHandler = getCallHandler(); + const res = await callHandler({ + method: "tools/call", + params: { + name: "muse_health", + arguments: { dir: harness.tmpDir, project: "nonexistent" }, + }, + }); + + expect(res.isError).toBe(true); + const text = res.content[0].text; + expect(text).toContain('no memories for project "nonexistent"'); + expect(text).toContain("alpha"); + expect(text).toContain("beta"); + }); +}); + +describe("T-406: Studio /api/health resolves and fails loud", () => { + let harness: ReturnType; + + beforeEach(() => { + harness = seedStore(["alpha", "beta"]); + }); + + afterEach(() => { + rmSync(harness.tmpDir, { recursive: true, force: true }); + }); + + it("returns the canonical report for a case-insensitive query param", async () => { + const srv = await startUiServer({ port: 0, memoryDir: harness.memoryDir, store: harness.store }); + try { + const res = await fetch(`http://localhost:${srv.port}/api/health?project=ALPHA`); + const report = (await res.json()) as { project: string; scope: { entry_count: number } }; + expect(report.project).toBe("alpha"); + expect(report.scope.entry_count).toBe(1); + } finally { + srv.close(); + } + }); + + it("returns 404 with an explicit error for an unknown project", async () => { + const srv = await startUiServer({ port: 0, memoryDir: harness.memoryDir, store: harness.store }); + try { + const res = await fetch(`http://localhost:${srv.port}/api/health?project=nonexistent`); + expect(res.status).toBe(404); + const body = (await res.json()) as { error?: string }; + expect(body.error).toContain('no memories for project "nonexistent"'); + expect(body.error).toContain("alpha"); + expect(body.error).toContain("beta"); + } finally { + srv.close(); + } + }); +}); From 1d6d34a13c5f72a57faa12e1a0496911d62f1971 Mon Sep 17 00:00:00 2001 From: Harsh Singh Date: Sat, 24 Oct 2026 22:26:27 +0530 Subject: [PATCH 5/6] docs(context): close out the T-405/T-406 fix trains Why: both tickets (#313, #314) are merged into `dev` (PR #321 at `e30a1b7`, PR #322 at `d3a8f6b`) but the DOX tree still described them as open `Next Up` items with stale line references. What: `.agents/context/ticket-plan.md` stamps T-405/T-406 landed with the merge SHAs, receipts (suite 1408/0 across 213 files, gates, critic verdicts 9.5/10 and 9/10) and the deliberate decisions; `current.md` drops the two landed bullets from *Next Up* and records the joint *Recently Landed* block including the follow-up ledger (ambiguous-case wording, Studio client 404 echo, silent-casing fan-out, wikilink hop-gate, get_context schema mismatch, hybrid status plumbing, superseded-gate guard); `HANDOFF.md` is refreshed with the two fix trains, the backlog priority order and the optional v3.1.1 release note. Verification: doc-only change; `bun run docs:check` (all relative links resolve) and `bun run check:landed` re-quoted at closeout; no code touched. --- .agents/artifacts/HANDOFF.md | 70 +++++++++++++++++++++++++--------- .agents/context/current.md | 10 +++-- .agents/context/ticket-plan.md | 4 +- 3 files changed, 61 insertions(+), 23 deletions(-) diff --git a/.agents/artifacts/HANDOFF.md b/.agents/artifacts/HANDOFF.md index 3496a9b..ef6c42c 100644 --- a/.agents/artifacts/HANDOFF.md +++ b/.agents/artifacts/HANDOFF.md @@ -1,24 +1,60 @@ -# HANDOFF — 2026-10-24T11:46:48Z -branch: `dev` | phase: **v3.1.0 released to `main`** — tag `v3.1.0`, GitHub release published, release train empty +# HANDOFF — 2026-10-24T16:19:41Z +branch: `dev` | phase: **T-405 + T-406 fix trains landed on `dev`** — v3.1.0 on `main`, release train empty ## Next Step -Resume the backlog. The two filed issues are the highest-value defects the -last two waves surfaced, and both are ready: +The two filed issues (#313, #314) are landed — see *Fix trains, this session* +below. Backlog in priority order: -- **#313 (T-405, HIGH)** — conflicted memories are still injected into agent - prompts. Three retrieval paths (`src/retrieval.ts:1686-1701`, - `src/retrieval/ranking.ts:272-273`, `src/orchestrator/context.ts:144-147`) - exclude `superseded`/`rejected`/`archived`/`expired` but not `conflicted`; - the soft −0.8 `STATUS_PENALTY` (`src/types.ts:342`) is a discount, not a - filter. It changes retrieval output, so the golden recall fixtures must be - regenerated in the same PR. -- **#314 (T-406, LOW)** — `--project` matches case-sensitively and an - exact-match miss returns an empty report whose label still reads the input - (e.g. `--project ALPHA` against `alpha`). +1. **Optional patch release `v3.1.1`** — both trains are behavior fixes on + `dev`; cut via the 11-phase release engine when the principal says go + (same governance disclosure would apply on `main`). +2. **Production-Grade Showcase Demo** — re-record the terminal demo (see + `current.md` → *Next Up*). +3. **Wave 24 remainder** — T-316 (Hebbian co-activation edges, unblocked), + T-318 (ROI auto-tuner, unblocked); T-317 stays blocked on T-178. +4. **Silent-casing fan-out** — `muse_tech_debt`, `memory_anchor_audit`, + `memory_drift_audit`, `/api/drift`, `/api/cognition/*` still silently + accept casing misses; the resolution helper + (`src/health/resolve.ts`) is ready to fan out as one small train. +5. **T-405/T-406 closeout filings** — ambiguous-case wording (say the case + collision), Studio client 404 echo (`fetchHealth` shows placeholders), + wikilink hop-gate conflicted filter (retrieval.ts ~1462), `get_context` + handler hardcoding `includeSuperseded:false` while the schema advertises + it, hybrid-path `status` plumbing (pre-existing for every status), + queryContext superseded/rejected gate status guard (same class T-405 + fixed). +6. **Consolidation wave** — duplicate TTL tables, nine token estimators, + clamp ceilings, zero-caller exports (behaviour-changing; `current.md` → + *Next Up*). +7. Also waiting on the principal: the parked spec PRs (#52–#56, R28–R32) each + carry a `feat/*` spec branch and need review before their waves can start. -Both are recorded in `current.md` → *Next Up* and in `.agents/context/ticket-plan.md`. -Also waiting on the principal: the parked spec PRs (#52–#56, R28–R32) each -carry a `feat/*` spec branch and need review before their waves can start. +## Fix trains (this session) +- **T-405 (#313)** — PR #321 merged into `dev` at `e30a1b7` (2026-10-24). + Conflicted memories are withheld from prompts by default via one shared + `includeConflicted` opt-in honored at every consumer gate (queryContext + + cache key, FTS + overfetch connective, vector row gate, engine passthrough, + ranking veto, muse pool choke point + anchor/route lens strips + + constraints + withheld notice, MCP schemas/mapping, CLI flag). Golden + recall fixture untouched — the frozen corpus carries zero conflicted + entries (verified). Tests-first RED receipts (2 fail → 7 files, suite + 1408/0 across 213 files); Fresh Critic verdicts 8 → **9.5/10** with a + mutation-verified overfetch test (revert-in-isolation → exactly the new + test fails, restore byte-identical). Artifacts: + `.agents/artifacts/GAUNTLET_JOB_CONTRACT.md`, `ITERATION_LEDGER.md`, + `ACCEPTANCE_PACKET.md` (uncommitted working copies). +- **T-406 (#314)** — PR #322 merged into `dev` at `d3a8f6b` (2026-10-24). + `resolveProjectName`/`listProjectNames`/`formatAvailable` + (`src/health/resolve.ts`) resolve health-scoping requests case-insensitively + and fail loud on unknown/ambiguous asks (CLI exit 1 · MCP isError · Studio + 404); the gate stays a pure exact-canonical reporter; `--json` stdout kept + parseable (critic probe STDOUT_LINES 1/0). Tests-first RED receipts (2 pass + by construction / 6 fail on wiring → 8/0 full file, suite 1408/0); Fresh + Critic verdicts 8 → **9/10**. Artifacts: + `GAUNTLET_JOB_CONTRACT-T406.md`, `ITERATION_LEDGER-T406.md`, + `ACCEPTANCE_PACKET-T406.md`. +- Both tickets carry landing notes in `.agents/context/ticket-plan.md` + (T-405, T-406) and a joint block in `current.md` → *Recently Landed Waves*. ## Released (this session) - **v3.1.0** — PR #318 merged into `main` at `c5a141d` (*"Release: v3.1.0"*), diff --git a/.agents/context/current.md b/.agents/context/current.md index 33bfcc4..f660e90 100644 --- a/.agents/context/current.md +++ b/.agents/context/current.md @@ -126,16 +126,18 @@ on any blocking failure, so CI runs it. Report: --- ## ⏭️ Next Up (Post-Release Focus) -- **Production-Grade Showcase Demo**: Re-record high-fidelity terminal demo video/GIF reflecting v3.0.0 capabilities (failure attribution, HOT.md compounding, memory branch/checkout, MCP context recall) to replace removed legacy demo. -- **Wave 24 remainder**: `T-316` (Hebbian co-activation edges into write-time `linked_memory_ids` seeding — **unblocked**, dep `T-095` landed; note the ticket premise is partly stale: `T-307` already landed the retrieval-expansion side) and `T-318` (retrieval weight auto-tuner from ROI telemetry — **unblocked**, deps `T-127` + `T-140` both landed). `T-317` (query-seeded PPR restart set) stays **blocked** on `T-178` (graph retrieval A/B gate). -- **Conflicted memories are still served into agent prompts** (found while fixing `T-320`, tracked as **`T-405`** / [issue #313](https://github.com/harshsinghmp/musememory/issues/313)): the status filters in `src/retrieval.ts:1686-1701`, `src/retrieval/ranking.ts:272-273` and `src/orchestrator/context.ts:144-147` exclude `superseded`/`rejected`/`archived`/`expired` but not `conflicted`, so a genuinely contradicted memory is injected with no badge. It *is* soft-penalised (−0.8 via `STATUS_PENALTY`, `src/types.ts:342`) and removed from sync broadcasts — but a rank discount is not a filter, and the gap is **wider** than originally recorded (three retrieval paths, not one). Deliberately left out of `T-320` — it changes retrieval output and therefore the golden recall fixtures; it needs its own ticket and test pass. +- **Production-Grade Showcase Demo**: Re-record high-fidelity terminal demo video/GIF reflecting v3.0.0 capabilities (failure attribution, HOT.md compounding, memory branch/checkout, MCP context recall) to replace removed legacy demo.- **Wave 24 remainder**: `T-316` (Hebbian co-activation edges into write-time `linked_memory_ids` seeding — **unblocked**, dep `T-095` landed; note the ticket premise is partly stale: `T-307` already landed the retrieval-expansion side) and `T-318` (retrieval weight auto-tuner from ROI telemetry — **unblocked**, deps `T-127` + `T-140` both landed). `T-317` (query-seeded PPR restart set) stays **blocked** on `T-178` (graph retrieval A/B gate). - **Consolidate the remaining duplicated tables** (deferred from this session as behaviour-changing; full detail with exact locations in [`docs/roadmap.md`](../../docs/roadmap.md) → *Engineering debt*): the three divergent per-type TTL tables (`src/retention.ts` **and** `src/retention/expiry_policy.ts` — only two of the three are in `retention.ts`), the **nine** separate token estimators (across nine files, plus six inline expressions — the collapse is *not* behaviour-neutral, because `src/orchestrator/context.ts` deliberately consumes the pessimistic measure), the divergent limit-clamping ceilings across retrieval paths (**seven** sites in `src/mcp.ts` alone), and the remaining zero-caller exports (twelve genuinely dead; 74 exported with no external caller). -- **`--project` matching is case-sensitive** (**`T-406`** / [issue #314](https://github.com/harshsinghmp/musememory/issues/314)): `memory health --project ALPHA` against a store holding `alpha` returns an empty report whose label still reads `ALPHA`. Exact-match is honest but silent. - **Remove the legacy `web/` directory** — done in this session. It was a second, hand-written copy of the site that `apps/website/` superseded; keeping it was the same "second copy that gets forgotten" failure mode this session fixed everywhere else. --- ## 🏁 Recently Landed Waves (Verified on `dev`) +- [x] **Fix trains: T-405 + T-406** (2026-10-24) — issues #313/#314, delivered through the gauntlet-loop + code-review engines (contract-frozen scope, blind Fresh Critic, mutation-verified tests): + - **T-405** ([#313](https://github.com/harshsinghmp/musememory/issues/313)): PR #321 merged at `e30a1b7`. Conflicted memories are now excluded **by default** from every prompt-serving retrieval path — queryContext lifecycle gate (hidden_count-aware, status-scoped-guarded), FTS store gate with the overfetch connective armed iff any post-fetch hide-gate stays active, vector row gate + engine passthrough, ranking candidate veto before scoring, muse-context pool choke point + anchor/route lens strips + constraint projection + withheld notice — all sharing one `includeConflicted` opt-in (`SearchOptions` cascade; MCP `include_conflicted` schema symmetry on both search/memory_recall tools; CLI `--include-conflicted`). The frozen golden recall fixture was deliberately untouched — the eval corpus carries zero conflicted entries (verified, not assumed); new tests use a self-contained corpus (T-143 promotion discipline respected). RED receipts real (conflicted entry served `kept:high_relevance` into `confident_answers`); critic verdict **9.5/10** with a mutation-verified overfetch test. As-of/temporal queries deliberately keep serving state-at-the-time (documented decision). + - **T-406** ([#314](https://github.com/harshsinghmp/musememory/issues/314)): PR #322 merged at `d3a8f6b`. Project names resolve case-insensitively (`resolveProjectName`/`listProjectNames`/`formatAvailable`, `src/health/resolve.ts`) on all three health surfaces, failing **loud** (CLI exit 1 · MCP isError · Studio 404) with the requested name echoed and the available list; resolved-with-case-diff surfaces the canonical label plus a note naming the requested form; `--json` stdout stays pure JSON (probe-verified). The gate stays a pure exact-canonical reporter — `git diff test/health_project_scope.test.ts` empty. Final critic verdict **9/10**. + - Full-suite receipts at merge time: `bun test` **1408 pass / 0 fail** (213 files, +13 tests); `tsc --noEmit` clean; claims `1400→1408`; docs links all resolve; secret scan clean; `check:landed:pr` → `RESULT: REVIEWABLE` before both merges, `check:landed` → **`RESULT: LANDED`** after (at `e30a1b7`, then at `d3a8f6b`). + - Filed follow-ups (deliberate ledger, see HANDOFF): ambiguous-case wording, Studio client 404 echo, silent-casing fan-out (`muse_tech_debt`, anchor/drift audits, `/api/drift`, `/api/cognition/*`), wikilink hop-gate conflicted filter, `get_context` include_superseded schema-vs-handler mismatch, hybrid-path status plumbing, queryContext superseded-gate status guard. - [x] **Released: v3.1.0** (2026-10-24) — Waves 23–26 shipped to `main`: - PR #318 merged at `c5a141d` (*"Release: v3.1.0"*); annotated tag [`v3.1.0`](https://github.com/harshsinghmp/musememory/releases/tag/v3.1.0) diff --git a/.agents/context/ticket-plan.md b/.agents/context/ticket-plan.md index b97ceca..3d02aa0 100644 --- a/.agents/context/ticket-plan.md +++ b/.agents/context/ticket-plan.md @@ -524,8 +524,8 @@ Canonical detailed artifact: `.agents/artifacts/audit-and-realigned-roadmap-2026 - [x] T-403 · HIGH · Stored-command doctor sweep: sanitize/migrate legacy non-template `test_command`s across `.memory/memories/*.yaml` to restore 100% clean validation · src/doctor.ts, .memory/ · F - [x] T-404 · HIGH · Stdin TTY decoupling: add `process.stdin.isTTY` check in `src/hooks/pack.ts:readStdinJson()` to prevent test runner hanging in interactive terminals · src/hooks/pack.ts · F - [x] T-401 · MEDIUM · Bunfig surface hardening: configure explicit test runner timeout and isolation defaults · bunfig.toml · N -- [ ] T-405 · HIGH · [#313](https://github.com/harshsinghmp/musememory/issues/313) · Conflicted memories are still injected into agent prompts with no badge: the status filters in `src/retrieval.ts:1686-1701`, `src/retrieval/ranking.ts:272-273` and `src/orchestrator/context.ts:144-147` exclude `superseded`/`rejected`/`archived`/`expired` but not `conflicted`. A contradicted memory is −0.8 rank-penalised (`STATUS_PENALTY`, `src/types.ts:342`) and removed from sync broadcasts, yet it is still served. Note: the gap is **wider** than the original note — three retrieval paths are affected, not one. Needs its own ticket and test pass because it changes retrieval output and therefore the golden recall fixtures · retrieval, orchestrator · U -- [ ] T-406 · LOW · [#314](https://github.com/harshsinghmp/musememory/issues/314) · `memory health --project P` matches the project name case-sensitively, so `--project ALPHA` against a store holding `alpha` returns an empty report whose label still reads `ALPHA`. Exact-match is honest but silent. Needs either a case-insensitive match or an explicit "no memories for project X" line · cli/health · U +- [x] T-405 · HIGH · [#313](https://github.com/harshsinghmp/musememory/issues/313) · Conflicted memories are still injected into agent prompts with no badge: the status filters in `src/retrieval.ts:1686-1701`, `src/retrieval/ranking.ts:272-273` and `src/orchestrator/context.ts:144-147` exclude `superseded`/`rejected`/`archived`/`expired` but not `conflicted`. A contradicted memory is −0.8 rank-penalised (`STATUS_PENALTY`, `src/types.ts:342`) and removed from sync broadcasts, yet it is still served. Note: the gap is **wider** than the original note — three retrieval paths are affected, not one. Needs its own ticket and test pass because it changes retrieval output and therefore the golden recall fixtures · retrieval, orchestrator · U — **LANDED 2026-10-24**: PR #321 (`fix/T-405-conflicted-injection`) merged into `dev` at `e30a1b7`; a single shared `includeConflicted` opt-in now gates every prompt-serving read path (shown wider still by the build-out: tiered context, FTS/gate, vector, engine passthrough, muse pool/lenses all inherit or take the flag); the golden recall fixture was deliberately untouched — the frozen corpus carries zero conflicted entries, verified, so no regen was required; cryptic-suite delta +7 tests (1408/0); Fresh Critic 9.5/10 with a mutation-verified overfetch test. +- [x] T-406 · LOW · [#314](https://github.com/harshsinghmp/musememory/issues/314) · `memory health --project P` matches the project name case-sensitively, so `--project ALPHA` against a store holding `alpha` returns an empty report whose label still reads `ALPHA`. Exact-match is honest but silent. Needs either a case-insensitive match or an explicit "no memories for project X" line · cli/health · U — **LANDED 2026-10-24**: PR #322 (`fix/T-406-case-sensitive-project`) merged into `dev` at `d3a8f6b`; new `src/health/resolve.ts` owns the resolution (`resolveProjectName`: exact wins → unique case-insensitive → loud unresolved), wired on CLI (exit 1 + available list, `--json` stdout kept pure), MCP `muse_health` (isError) and Studio `/api/health` (404 + JSON); the gate stays a pure exact-canonical reporter (scope-case tests untouched); +8 tests (1408/0); Fresh Critic 9/10. - [x] T-105b · landed Wave 2 — Syntax-only promotion rule: enforce AST/schema verification before candidate confirmation · promotion · branch: feat/wave-2-trust-gates · F ## Realigned In-Line Execution Sequence From c22880eaf03dd3a21129dc9d25d878891073163e Mon Sep 17 00:00:00 2001 From: Harsh Singh Date: Sat, 24 Oct 2026 23:46:09 +0530 Subject: [PATCH 6/6] chore(release): v3.1.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: the two post-release defect fixes (T-405/#313 conflicted memories injected into prompts; T-406/#314 case-sensitive silent project scoping) are landed on `dev` (PR #321 at e30a1b7, PR #322 at d3a8f6b, closeout #323 at 82d290c) and carry to `main` as a SemVer patch. What: bump package.json 3.1.0 -> 3.1.1 and stamp CHANGELOG.md — the [3.1.1] block with Summary + two Fixed entries (both citing tickets, PR links, quality receipts and the deliberate decisions), compare URL v3.1.0...v3.1.1. No code changes on this branch. Verification: bun test 1408 pass / 0 fail across 213 files on this tree; tsc --noEmit clean; build clean; claims:check + docs:check current; secret scan clean; release notes derived from the stamped changelog. --- CHANGELOG.md | 22 ++++++++++++++++++++++ claims.json | 2 +- package.json | 2 +- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3eb7d2c..96c444a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [3.1.1] - 2026-10-24 + +**Full Changelog**: https://github.com/harshsinghmp/musememory/compare/v3.1.0...v3.1.1 + +### Summary +Patch release. Two defects found by the post-release bug-hunt are fixed and +delivered through the bounded gauntlet-loop process (contract-frozen scope, +blind fresh critics, mutation-verified tests): memories the contradiction +engine has flagged `conflicted` are no longer injected into agent prompts, and +project scoping on the health surfaces resolves case-insensitively and fails +loudly on unknown projects. + +**A SemVer patch: both items are defect fixes ([#313](https://github.com/harshsinghmp/musememory/issues/313), +[#314](https://github.com/harshsinghmp/musememory/issues/314)) — the new `include_conflicted` +opt-in is additive, and every other surface keeps its shipped shape. Stores +carrying conflicted memories will notice their absence from prompts; pass +`include_conflicted: true` to reproduce the old (leaky) behavior deliberately.** + +### Fixed +- **Conflicted memories are withheld from prompts by default ([#313](https://github.com/harshsinghmp/musememory/issues/313), [PR #321](https://github.com/harshsinghmp/musememory/pull/321))**: a memory the contradiction engine has marked `conflicted` is a known potential falsehood, yet it was only rank-discounted (−0.8), never filtered — it surfaced into recall results, ranked candidates (`kept:high_relevance`), fused muse context (`relevant_memories`, `confident_answers`, route manifests, code anchors) and FTS store queries with no badge. One shared `includeConflicted` opt-in is now honored at every prompt-serving gate: the `queryContext` lifecycle gate (hidden-count-aware, with the explicit-status guard so audit queries still reach conflicted rows), the FTS hidden-row gate with an overfetch connective armed whenever any post-fetch hide-gate stays active, the vector hybrid row gate and engine passthrough, the ranking candidate veto, and the muse-context pool choke point plus anchor/route lens strips, constraint projection and a withheld notice. The MCP `search`/`memory_recall` tools expose `include_conflicted` beside `include_superseded`, and the CLI mirrors `--include-conflicted`. Temporal/as-of queries deliberately keep serving state-at-the-time (a conflict verdict is dated after the historical point). The frozen golden recall fixture was untouched — the corpus carries zero conflicted entries (verified, not assumed); seven new tests pin the behavior, and the suite stands at **1408 pass / 0 fail** across 213 files. Fresh Critic verdict 9.5/10 after a mutation-verified overfetch test. +- **Project scoping is case-insensitive and fails loud ([#314](https://github.com/harshsinghmp/musememory/issues/314), [PR #322](https://github.com/harshsinghmp/musememory/pull/322))**: `memory health --project ALPHA` against a store holding `alpha` returned a silent empty report labelled with the caller's casing — the MCP `muse_health` tool and the studio `/api/health?project=` endpoint shared the silent-empty behavior. A single resolution owner (`resolveProjectName`, `src/health/resolve.ts` — exact match wins, unique case-insensitive match returns the store's spelling, unknown or ambiguous stays unresolved) is wired into all three surfaces, which now fail loudly (CLI exit 1, MCP error result, studio 404 — each echoing the requested name and the available projects) instead of silently. The gate itself stays a pure exact-canonical reporter (existing scope tests pass unmodified); `--json` output on the CLI stays pure (the resolution note rides stderr). Eight new tests; Fresh Critic verdict 9/10 after an empirical stdout-purity probe. + ## [3.1.0] - 2026-10-24 **Full Changelog**: https://github.com/harshsinghmp/musememory/compare/v3.0.2...v3.1.0 diff --git a/claims.json b/claims.json index 3d5f4a9..b35d72b 100644 --- a/claims.json +++ b/claims.json @@ -1,6 +1,6 @@ { "$comment": "Generated by scripts/generate-claims.ts. Do not hand-edit: test/marketing_claims.test.ts re-derives every value from src/ and fails on drift.", - "version": "3.1.0", + "version": "3.1.1", "mcpTools": 115, "mcpProfiles": 7, "agentPlatforms": 52, diff --git a/package.json b/package.json index 14a3230..ff3678b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "musememory", - "version": "3.1.0", + "version": "3.1.1", "description": "Autonomous, self-organizing cognitive memory system for AI agents and agency networks", "main": "dist/index.js", "type": "module",