Skip to content

Commit 465edfa

Browse files
committed
fix: unify all memory quality rules in single module
1 parent 6a80f4b commit 465edfa

5 files changed

Lines changed: 41 additions & 55 deletions

File tree

src/extractors.ts

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -224,8 +224,8 @@ function extractFirstPath(text: string): string | undefined {
224224
}
225225

226226
/**
227-
* Quality gate for workspace memory candidates.
228-
* Rejects low-quality entries like git hashes, error messages, etc.
227+
* Acceptance gate for workspace memory candidates.
228+
* Keeps extraction-specific checks local and delegates memory quality rules to memory-quality.ts.
229229
*/
230230
function shouldAcceptWorkspaceMemoryCandidate(
231231
entry: {
@@ -246,34 +246,12 @@ function shouldAcceptWorkspaceMemoryCandidate(
246246
return false;
247247
}
248248

249-
// Git history / commit hash
250-
if (/\b[0-9a-f]{7,40}\b/.test(text)) return false;
251-
if (/^(fix|feat|chore|docs|refactor|test):/i.test(text)) return false;
252-
253-
// Raw error / stack trace
254-
if (/^\s*(Error|TypeError|ReferenceError|SyntaxError):/i.test(text)) return false;
255-
if (/at \S+ \([^)]+:\d+:\d+\)/.test(text)) return false;
256-
257-
// Active file list
258-
if (/^(modified|created|deleted|renamed)\s+\S+\.\S+$/i.test(text)) return false;
259-
260-
// Temporary progress
261-
if (/^(currently|now|pending|in progress|todo|wip):/i.test(text)) return false;
262-
263-
// Code signature / API doc
264-
if (/^(function|class|interface|type|const|let|var)\s+\w+/.test(text)) return false;
265-
if (/^(GET|POST|PUT|DELETE|PATCH)\s+\//.test(text)) return false;
266-
267249
// Indirect Prompt Injection / Adversarial Instructions
268250
// Rejects attempts to overwrite system behavior or "ignore" rules.
269251
// comparative "instead of" is allowed.
270252
if (/\b(ignore\s+all|ignore\s+previous|ignore\s+instruction|overwrite\s+system|overwrite\s+rules|forget\s+all|delete\s+root)\b/i.test(text)) return false;
271253
if (/\b(ignore|instruction|overwrite)\b/i.test(text) && /\b(previous|all|rules|behavior|prompt|system)\b/i.test(text)) return false;
272254

273-
// Path-heavy facts (rediscoverable from repo)
274-
const pathCount = (text.match(/\/[\w.-]+(\/[\w.-]+)+/g) || []).length;
275-
if (pathCount > 2) return false;
276-
277255
const quality = assessMemoryQuality({ type: entry.type, text, source: "compaction" });
278256
if (!quality.accepted) return false;
279257

src/memory-quality.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export function assessMemoryQuality(entry: MemoryQualityInput): MemoryQualityRes
1919
if (isCommitOrCiViolation(text)) reasons.push("commit_or_ci_snapshot");
2020
if (isPathHeavyViolation(text)) reasons.push("path_heavy");
2121
if (isTemporaryStatusViolation(text)) reasons.push("temporary_status");
22+
if (isActiveFileSnapshotViolation(text)) reasons.push("active_file_snapshot");
23+
if (isCodeOrApiSignatureViolation(text)) reasons.push("code_or_api_signature");
2224
if (entry.type === "feedback" && isFeedbackQualityViolation(text)) reasons.push("bad_feedback");
2325
if (entry.type === "decision" && isDecisionQualityViolation(text)) reasons.push("bad_decision");
2426

@@ -77,6 +79,7 @@ function isRawErrorViolation(text: string): boolean {
7779
}
7880

7981
function isCommitOrCiViolation(text: string): boolean {
82+
if (/^(fix|feat|chore|docs|refactor|test):/i.test(text)) return true;
8083
if (/\b[0-9a-f]{7,40}\b/.test(text)) return true;
8184
if (/\bCI\b.*\b(?:passed|failed|run|compatibility|flaky)\b/i.test(text)) return true;
8285
if (/\b(?:passed|failed|run|compatibility|flaky)\b.*\bCI\b/i.test(text)) return true;
@@ -94,3 +97,13 @@ function isTemporaryStatusViolation(text: string): boolean {
9497
if (/\b(?:run npm test|tests? are running|next reply|before continuing)\b/i.test(text)) return true;
9598
return false;
9699
}
100+
101+
function isActiveFileSnapshotViolation(text: string): boolean {
102+
return /^(modified|created|deleted|renamed)\s+\S+\.\S+$/i.test(text);
103+
}
104+
105+
function isCodeOrApiSignatureViolation(text: string): boolean {
106+
if (/^(function|class|interface|type|const|let|var)\s+\w+/.test(text)) return true;
107+
if (/^(GET|POST|PUT|DELETE|PATCH)\s+\//.test(text)) return true;
108+
return false;
109+
}

src/workspace-memory.ts

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { LongTermMemoryEntry, WorkspaceMemoryStore } from "./types.ts";
22
import { LONG_TERM_LIMITS } from "./types.ts";
33
import { workspaceKey, workspaceMemoryPath } from "./paths.ts";
44
import { atomicWriteJSON, readJSON, updateJSON } from "./storage.ts";
5-
import { assessMemoryQuality } from "./memory-quality.ts";
5+
import { assessMemoryQuality, isProgressSnapshotViolation } from "./memory-quality.ts";
66

77
// Minimum length for workspace_memory envelope: <workspace_memory>\n...\n</workspace_memory>
88
const MIN_ENVELOPE_LENGTH = 80;
@@ -254,28 +254,6 @@ export function redactCredentials(text: string): string {
254254
return result;
255255
}
256256

257-
export function isProjectSnapshotViolation(text: string): boolean {
258-
// Test/suite counts
259-
if (/\d+\s+tests?\s+pass(?:ed)?/i.test(text)) return true;
260-
if (/\d+\s+suites?\s+(?:pass|fail)/i.test(text)) return true;
261-
262-
// File counts with snapshot context, excluding limit statements
263-
if (/\d+\s*(?:|)?\s*(?:files?|)/i.test(text)) {
264-
const hasSnapshotContext = /|synced|uploaded|downloaded|completed|generated|created|modified|processed|/i.test(text);
265-
const hasLimitContext = /limit|max|maximum|min|minimum|supports?|allowed|per\s+(?:batch|request|upload)/i.test(text);
266-
if (hasSnapshotContext && !hasLimitContext) return true;
267-
}
268-
269-
// Phase/Wave/Sprint/Milestone/Task progress
270-
if (/(?:phases?|waves?|sprints?|milestones?|tasks?)\s*\d+(?:\s*[-]\s*\d+)?/i.test(text)) {
271-
if (/completed|done|finished|/i.test(text)) return true;
272-
}
273-
274-
if (/(?:|).{0,30}(?:phases?|waves?|sprints?|milestones?|tasks?)/i.test(text)) return true;
275-
276-
return false;
277-
}
278-
279257
export function runMigrationP0Cleanup(
280258
store: WorkspaceMemoryStore,
281259
nowIso: string,
@@ -288,7 +266,7 @@ export function runMigrationP0Cleanup(
288266
if (entry.source !== "compaction") return entry;
289267
if (entry.type !== "project") return entry;
290268

291-
if (isProjectSnapshotViolation(entry.text)) {
269+
if (isProgressSnapshotViolation(entry.text)) {
292270
return {
293271
...entry,
294272
status: "superseded" as const,

tests/memory-quality-eval.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,23 @@ test("decision must be future-facing rule, not completed implementation note", (
136136
assert.equal(assessMemoryQuality({ type: "decision", text: "Added semantic merge tests in the previous wave", source: "compaction" }).accepted, false);
137137
});
138138

139+
test("shared quality gate owns extractor low-quality syntax rejections", () => {
140+
const rejected = [
141+
{ type: "project" as const, text: "fix: add new feature" },
142+
{ type: "reference" as const, text: "modified src/plugin.ts" },
143+
{ type: "reference" as const, text: "function buildCompactionPrompt(privateContext: string): string" },
144+
{ type: "reference" as const, text: "GET /api/sessions" },
145+
];
146+
147+
for (const entry of rejected) {
148+
assert.equal(
149+
assessMemoryQuality({ ...entry, source: "compaction" }).accepted,
150+
false,
151+
`${entry.type}: ${entry.text}`,
152+
);
153+
}
154+
});
155+
139156
test("explicit memories bypass extraction quality gate", () => {
140157
const entries = extractExplicitMemories("remember: Wave 1 completed successfully and all tests passed");
141158
assert.equal(entries.length, 1);

tests/workspace-memory.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ import {
1515
workspaceMemoryExactKey,
1616
workspaceMemoryIdentityKey,
1717
redactCredentials,
18-
isProjectSnapshotViolation,
1918
runMigrationP0Cleanup,
2019
loadWorkspaceMemory,
2120
saveWorkspaceMemory,
2221
updateWorkspaceMemoryWithAccounting,
2322
} from "../src/workspace-memory.ts";
23+
import { isProgressSnapshotViolation } from "../src/memory-quality.ts";
2424
import { reviewerCurrent28Fixture, expectedAcceptedFixtureIds } from "./fixtures/memory-quality-current-28.ts";
2525

2626
function entry(id: string, text: string, type: LongTermMemoryEntry["type"] = "decision"): LongTermMemoryEntry {
@@ -890,13 +890,13 @@ test("redactCredentials is idempotent and also redacts rationale text", () => {
890890
assert.equal(migrated.entries[0].rationale, "password: [REDACTED]");
891891
});
892892

893-
test("isProjectSnapshotViolation detects wave progress and avoids limit context false positives", () => {
894-
assert.equal(isProjectSnapshotViolation("1237 tests pass, 226 suites"), true);
895-
assert.equal(isProjectSnapshotViolation("USB 同步:37 個文件"), true);
896-
assert.equal(isProjectSnapshotViolation("Waves 1-5 已完成,Wave 6 deferred"), true);
893+
test("shared progress snapshot rule detects wave progress and avoids limit context false positives", () => {
894+
assert.equal(isProgressSnapshotViolation("1237 tests pass, 226 suites"), true);
895+
assert.equal(isProgressSnapshotViolation("USB 同步:37 個文件"), true);
896+
assert.equal(isProgressSnapshotViolation("Waves 1-5 已完成,Wave 6 deferred"), true);
897897

898-
assert.equal(isProjectSnapshotViolation("Upload limit is 10 files"), false);
899-
assert.equal(isProjectSnapshotViolation("Project supports 5 test suites"), false);
898+
assert.equal(isProgressSnapshotViolation("Upload limit is 10 files"), false);
899+
assert.equal(isProgressSnapshotViolation("Project supports 5 test suites"), false);
900900
});
901901

902902
test("runMigrationP0Cleanup marks only non-explicit project snapshots and runs once", () => {

0 commit comments

Comments
 (0)