mirror of
https://github.com/sdwolf4103/opencode-working-memory.git
synced 2026-07-17 12:56:42 +02:00
fix: unify workspace memory quality gate
This commit is contained in:
@@ -223,7 +223,7 @@ test("parseWorkspaceMemoryCandidates accepts bracketless candidate format", () =
|
||||
Memory candidates:
|
||||
- project Backend health improvements organized into phased milestones
|
||||
- reference Scrypt 參數必須是 N=16384, r=8, p=1
|
||||
- feedback 端口 9473 可能被舊進程佔用,需殺掉後重啟
|
||||
- feedback User prefers Traditional Chinese memory summaries
|
||||
- decision Use output.prompt to replace the default compaction template
|
||||
`;
|
||||
|
||||
@@ -451,14 +451,14 @@ test("parseWorkspaceMemoryCandidates allows benign ignore/instruction wording",
|
||||
Memory candidates:
|
||||
- [project] Use .gitignore to ignore generated files.
|
||||
- [reference] Instruction parser supports Markdown sections and bracketed memory types.
|
||||
- [decision] Prompt context uses a frozen workspace snapshot plus hot session state.
|
||||
- [decision] Use a frozen workspace snapshot plus hot session state for prompt context.
|
||||
`;
|
||||
const items = parseWorkspaceMemoryCandidates(summary);
|
||||
|
||||
assert.equal(items.length, 3);
|
||||
assert.equal(items[0].text, "Use .gitignore to ignore generated files.");
|
||||
assert.equal(items[1].text, "Instruction parser supports Markdown sections and bracketed memory types.");
|
||||
assert.equal(items[2].text, "Prompt context uses a frozen workspace snapshot plus hot session state.");
|
||||
assert.equal(items[2].text, "Use a frozen workspace snapshot plus hot session state for prompt context.");
|
||||
});
|
||||
|
||||
test("parseWorkspaceMemoryCandidates rejects direct system prompt override attempts", () => {
|
||||
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import type { LongTermMemoryEntry } from "../../src/types.ts";
|
||||
|
||||
const now = "2026-04-28T00:00:00.000Z";
|
||||
|
||||
function mem(
|
||||
id: string,
|
||||
type: LongTermMemoryEntry["type"],
|
||||
text: string,
|
||||
source: LongTermMemoryEntry["source"] = "compaction",
|
||||
): LongTermMemoryEntry {
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
text,
|
||||
source,
|
||||
confidence: source === "explicit" ? 1 : 0.75,
|
||||
status: "active",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
}
|
||||
|
||||
export const reviewerCurrent28Fixture: LongTermMemoryEntry[] = [
|
||||
// High-value durable entries. These should survive.
|
||||
mem("good_feedback_language", "feedback", "User prefers architecture reviews in Traditional Chinese", "explicit"),
|
||||
mem("good_feedback_direct", "feedback", "User wants direct architecture feedback with concrete file paths", "explicit"),
|
||||
mem("good_feedback_no_manual_cleanup", "feedback", "User prefers automatic memory cleanup over manual cleanup instructions", "explicit"),
|
||||
mem("good_decision_no_extra_api", "decision", "Do not add extra LLM API calls for memory consolidation"),
|
||||
mem("good_decision_no_semantic_merge", "decision", "Memory dedupe must use exact canonical keys and generic URL/path identity only"),
|
||||
mem("good_decision_no_render_tracking", "decision", "Do not use rendered-memory access tracking as evidence"),
|
||||
mem("good_reference_frozen", "reference", "Workspace memory is rendered as a frozen system[1] snapshot; pending memories remain in hot session state until compaction"),
|
||||
mem("good_project_plugin", "project", "The project is an OpenCode plugin using TypeScript and local JSON stores"),
|
||||
mem("good_reference_accounting", "reference", "Promotion accounting reports promoted, absorbed, superseded, and rejected outcomes"),
|
||||
|
||||
// Pseudo feedback/decision/progress snapshots. These should be superseded/rejected.
|
||||
mem("bad_feedback_wave_done", "feedback", "Wave 1 completed successfully and all tests passed"),
|
||||
mem("bad_feedback_plan_done", "feedback", "Plan 1 critical stability fixes were implemented"),
|
||||
mem("bad_feedback_session_note", "feedback", "The assistant reviewed the code reviewer feedback and updated the plan"),
|
||||
mem("bad_feedback_impl_note", "feedback", "Implemented owner-aware pending journal cleanup in plugin.ts"),
|
||||
mem("bad_decision_commit", "decision", "Commit 53aa6d3 completed consolidation accounting"),
|
||||
mem("bad_decision_tests", "decision", "180 tests pass and 0 tests fail after the latest change"),
|
||||
mem("bad_decision_pr_status", "decision", "PR1 is done and PR2 is ready to start"),
|
||||
mem("bad_project_files", "project", "Modified src/plugin.ts src/workspace-memory.ts src/pending-journal.ts during the last wave"),
|
||||
mem("bad_project_wave", "project", "Wave 3 finished after cache bounds and Bearer redaction were added"),
|
||||
mem("bad_reference_commit", "reference", "Commit a762e86 contains the owner scope fix"),
|
||||
mem("bad_reference_ci", "reference", "CI compatibility run 25033906652 passed"),
|
||||
mem("bad_reference_error", "reference", "TypeError: Cannot read properties of undefined"),
|
||||
mem("bad_project_current", "project", "Currently running npm test before continuing"),
|
||||
|
||||
// Borderline implementation facts. Reject unless they are written as future rules.
|
||||
mem("bad_decision_impl_detail", "decision", "dedupeLongTermEntriesWithAccounting was updated in the previous session"),
|
||||
mem("bad_feedback_internal", "feedback", "The migration writes to disk when redaction changes content"),
|
||||
mem("bad_reference_tmp", "reference", "storage.test.ts had a flaky cross-process test in CI"),
|
||||
|
||||
// Durable future-facing rules. These should survive.
|
||||
mem("good_decision_quality", "decision", "Reject completion and progress statements before storing compaction memory candidates"),
|
||||
mem("good_decision_quality_shared", "decision", "Use one shared memory quality gate for extraction and migration"),
|
||||
mem("good_reference_quality_migration", "reference", "Quality cleanup migration supersedes low-quality compaction memories and does not touch explicit memories"),
|
||||
];
|
||||
|
||||
export const expectedAcceptedFixtureIds = new Set([
|
||||
"good_feedback_language",
|
||||
"good_feedback_direct",
|
||||
"good_feedback_no_manual_cleanup",
|
||||
"good_decision_no_extra_api",
|
||||
"good_decision_no_semantic_merge",
|
||||
"good_decision_no_render_tracking",
|
||||
"good_reference_frozen",
|
||||
"good_project_plugin",
|
||||
"good_reference_accounting",
|
||||
"good_decision_quality",
|
||||
"good_decision_quality_shared",
|
||||
"good_reference_quality_migration",
|
||||
]);
|
||||
@@ -1,6 +1,8 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { parseWorkspaceMemoryCandidates } from "../src/extractors.ts";
|
||||
import { extractExplicitMemories, parseWorkspaceMemoryCandidates } from "../src/extractors.ts";
|
||||
import { assessMemoryQuality } from "../src/memory-quality.ts";
|
||||
import { expectedAcceptedFixtureIds, reviewerCurrent28Fixture } from "./fixtures/memory-quality-current-28.ts";
|
||||
|
||||
const acceptedCases = [
|
||||
{
|
||||
@@ -64,6 +66,18 @@ const rejectedCases = [
|
||||
name: "temporary pending task",
|
||||
line: "- [decision] currently: run npm test before the next reply",
|
||||
},
|
||||
{
|
||||
name: "misclassified feedback completion snapshot",
|
||||
line: "- [feedback] Wave 1 completed successfully and all tests passed",
|
||||
},
|
||||
{
|
||||
name: "misclassified decision implementation note",
|
||||
line: "- [decision] Implemented owner-aware cleanup in plugin.ts",
|
||||
},
|
||||
{
|
||||
name: "session internal review note",
|
||||
line: "- [feedback] The assistant reviewed the code reviewer feedback and updated the plan",
|
||||
},
|
||||
] as const;
|
||||
|
||||
for (const item of acceptedCases) {
|
||||
@@ -91,3 +105,40 @@ ${item.line}
|
||||
assert.equal(entries.length, 0);
|
||||
});
|
||||
}
|
||||
|
||||
test("reviewer current-28 fixture keeps durable memories and rejects pseudo memories", () => {
|
||||
for (const entry of reviewerCurrent28Fixture) {
|
||||
const result = assessMemoryQuality(entry);
|
||||
assert.equal(
|
||||
result.accepted,
|
||||
expectedAcceptedFixtureIds.has(entry.id),
|
||||
`${entry.id}: ${entry.text} -> ${result.reasons.join(",")}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("progress snapshot rejection is type independent", () => {
|
||||
for (const type of ["feedback", "project", "decision", "reference"] as const) {
|
||||
const result = assessMemoryQuality({ type, text: "Wave 2 completed successfully", source: "compaction" });
|
||||
assert.equal(result.accepted, false, `${type} progress snapshots must reject`);
|
||||
assert.ok(result.reasons.includes("progress_snapshot"));
|
||||
}
|
||||
});
|
||||
|
||||
test("feedback must be stable user preference or instruction", () => {
|
||||
assert.equal(assessMemoryQuality({ type: "feedback", text: "User prefers concise architecture reviews", source: "compaction" }).accepted, true);
|
||||
assert.equal(assessMemoryQuality({ type: "feedback", text: "Implemented owner-aware cleanup in plugin.ts", source: "compaction" }).accepted, false);
|
||||
});
|
||||
|
||||
test("decision must be future-facing rule, not completed implementation note", () => {
|
||||
assert.equal(assessMemoryQuality({ type: "decision", text: "Do not add semantic merge to memory dedupe", source: "compaction" }).accepted, true);
|
||||
assert.equal(assessMemoryQuality({ type: "decision", text: "Use the cache boundary that was chosen in ADR-2 for future memory rendering", source: "compaction" }).accepted, true);
|
||||
assert.equal(assessMemoryQuality({ type: "decision", text: "Added semantic merge tests in the previous wave", source: "compaction" }).accepted, false);
|
||||
});
|
||||
|
||||
test("explicit memories bypass extraction quality gate", () => {
|
||||
const entries = extractExplicitMemories("remember: Wave 1 completed successfully and all tests passed");
|
||||
assert.equal(entries.length, 1);
|
||||
assert.equal(entries[0].source, "explicit");
|
||||
assert.match(entries[0].text, /Wave 1 completed/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user