diff --git a/src/extractors.ts b/src/extractors.ts index 3190d2f..d06babd 100644 --- a/src/extractors.ts +++ b/src/extractors.ts @@ -224,8 +224,8 @@ function extractFirstPath(text: string): string | undefined { } /** - * Quality gate for workspace memory candidates. - * Rejects low-quality entries like git hashes, error messages, etc. + * Acceptance gate for workspace memory candidates. + * Keeps extraction-specific checks local and delegates memory quality rules to memory-quality.ts. */ function shouldAcceptWorkspaceMemoryCandidate( entry: { @@ -246,34 +246,12 @@ function shouldAcceptWorkspaceMemoryCandidate( return false; } - // Git history / commit hash - if (/\b[0-9a-f]{7,40}\b/.test(text)) return false; - if (/^(fix|feat|chore|docs|refactor|test):/i.test(text)) return false; - - // Raw error / stack trace - if (/^\s*(Error|TypeError|ReferenceError|SyntaxError):/i.test(text)) return false; - if (/at \S+ \([^)]+:\d+:\d+\)/.test(text)) return false; - - // Active file list - if (/^(modified|created|deleted|renamed)\s+\S+\.\S+$/i.test(text)) return false; - - // Temporary progress - if (/^(currently|now|pending|in progress|todo|wip):/i.test(text)) return false; - - // Code signature / API doc - if (/^(function|class|interface|type|const|let|var)\s+\w+/.test(text)) return false; - if (/^(GET|POST|PUT|DELETE|PATCH)\s+\//.test(text)) return false; - // Indirect Prompt Injection / Adversarial Instructions // Rejects attempts to overwrite system behavior or "ignore" rules. // comparative "instead of" is allowed. 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; if (/\b(ignore|instruction|overwrite)\b/i.test(text) && /\b(previous|all|rules|behavior|prompt|system)\b/i.test(text)) return false; - // Path-heavy facts (rediscoverable from repo) - const pathCount = (text.match(/\/[\w.-]+(\/[\w.-]+)+/g) || []).length; - if (pathCount > 2) return false; - const quality = assessMemoryQuality({ type: entry.type, text, source: "compaction" }); if (!quality.accepted) return false; diff --git a/src/memory-quality.ts b/src/memory-quality.ts index 8b4a0eb..bf046b8 100644 --- a/src/memory-quality.ts +++ b/src/memory-quality.ts @@ -19,6 +19,8 @@ export function assessMemoryQuality(entry: MemoryQualityInput): MemoryQualityRes if (isCommitOrCiViolation(text)) reasons.push("commit_or_ci_snapshot"); if (isPathHeavyViolation(text)) reasons.push("path_heavy"); if (isTemporaryStatusViolation(text)) reasons.push("temporary_status"); + if (isActiveFileSnapshotViolation(text)) reasons.push("active_file_snapshot"); + if (isCodeOrApiSignatureViolation(text)) reasons.push("code_or_api_signature"); if (entry.type === "feedback" && isFeedbackQualityViolation(text)) reasons.push("bad_feedback"); if (entry.type === "decision" && isDecisionQualityViolation(text)) reasons.push("bad_decision"); @@ -77,6 +79,7 @@ function isRawErrorViolation(text: string): boolean { } function isCommitOrCiViolation(text: string): boolean { + if (/^(fix|feat|chore|docs|refactor|test):/i.test(text)) return true; if (/\b[0-9a-f]{7,40}\b/.test(text)) return true; if (/\bCI\b.*\b(?:passed|failed|run|compatibility|flaky)\b/i.test(text)) return true; if (/\b(?:passed|failed|run|compatibility|flaky)\b.*\bCI\b/i.test(text)) return true; @@ -94,3 +97,13 @@ function isTemporaryStatusViolation(text: string): boolean { if (/\b(?:run npm test|tests? are running|next reply|before continuing)\b/i.test(text)) return true; return false; } + +function isActiveFileSnapshotViolation(text: string): boolean { + return /^(modified|created|deleted|renamed)\s+\S+\.\S+$/i.test(text); +} + +function isCodeOrApiSignatureViolation(text: string): boolean { + if (/^(function|class|interface|type|const|let|var)\s+\w+/.test(text)) return true; + if (/^(GET|POST|PUT|DELETE|PATCH)\s+\//.test(text)) return true; + return false; +} diff --git a/src/workspace-memory.ts b/src/workspace-memory.ts index 1039d83..af64f0d 100644 --- a/src/workspace-memory.ts +++ b/src/workspace-memory.ts @@ -2,7 +2,7 @@ import type { LongTermMemoryEntry, WorkspaceMemoryStore } from "./types.ts"; import { LONG_TERM_LIMITS } from "./types.ts"; import { workspaceKey, workspaceMemoryPath } from "./paths.ts"; import { atomicWriteJSON, readJSON, updateJSON } from "./storage.ts"; -import { assessMemoryQuality } from "./memory-quality.ts"; +import { assessMemoryQuality, isProgressSnapshotViolation } from "./memory-quality.ts"; // Minimum length for workspace_memory envelope: \n...\n const MIN_ENVELOPE_LENGTH = 80; @@ -254,28 +254,6 @@ export function redactCredentials(text: string): string { return result; } -export function isProjectSnapshotViolation(text: string): boolean { - // Test/suite counts - if (/\d+\s+tests?\s+pass(?:ed)?/i.test(text)) return true; - if (/\d+\s+suites?\s+(?:pass|fail)/i.test(text)) return true; - - // File counts with snapshot context, excluding limit statements - if (/\d+\s*(?:個|个)?\s*(?:files?|文件)/i.test(text)) { - const hasSnapshotContext = /同步|synced|uploaded|downloaded|completed|generated|created|modified|processed|完成/i.test(text); - const hasLimitContext = /limit|max|maximum|min|minimum|supports?|allowed|per\s+(?:batch|request|upload)/i.test(text); - if (hasSnapshotContext && !hasLimitContext) return true; - } - - // Phase/Wave/Sprint/Milestone/Task progress - if (/(?:phases?|waves?|sprints?|milestones?|tasks?)\s*\d+(?:\s*[-–]\s*\d+)?/i.test(text)) { - if (/completed|done|finished|完成/i.test(text)) return true; - } - - if (/(?:已完成|完成).{0,30}(?:phases?|waves?|sprints?|milestones?|tasks?)/i.test(text)) return true; - - return false; -} - export function runMigrationP0Cleanup( store: WorkspaceMemoryStore, nowIso: string, @@ -288,7 +266,7 @@ export function runMigrationP0Cleanup( if (entry.source !== "compaction") return entry; if (entry.type !== "project") return entry; - if (isProjectSnapshotViolation(entry.text)) { + if (isProgressSnapshotViolation(entry.text)) { return { ...entry, status: "superseded" as const, diff --git a/tests/memory-quality-eval.test.ts b/tests/memory-quality-eval.test.ts index ea3023b..cb8e3e8 100644 --- a/tests/memory-quality-eval.test.ts +++ b/tests/memory-quality-eval.test.ts @@ -136,6 +136,23 @@ test("decision must be future-facing rule, not completed implementation note", ( assert.equal(assessMemoryQuality({ type: "decision", text: "Added semantic merge tests in the previous wave", source: "compaction" }).accepted, false); }); +test("shared quality gate owns extractor low-quality syntax rejections", () => { + const rejected = [ + { type: "project" as const, text: "fix: add new feature" }, + { type: "reference" as const, text: "modified src/plugin.ts" }, + { type: "reference" as const, text: "function buildCompactionPrompt(privateContext: string): string" }, + { type: "reference" as const, text: "GET /api/sessions" }, + ]; + + for (const entry of rejected) { + assert.equal( + assessMemoryQuality({ ...entry, source: "compaction" }).accepted, + false, + `${entry.type}: ${entry.text}`, + ); + } +}); + test("explicit memories bypass extraction quality gate", () => { const entries = extractExplicitMemories("remember: Wave 1 completed successfully and all tests passed"); assert.equal(entries.length, 1); diff --git a/tests/workspace-memory.test.ts b/tests/workspace-memory.test.ts index a906767..6898e29 100644 --- a/tests/workspace-memory.test.ts +++ b/tests/workspace-memory.test.ts @@ -15,12 +15,12 @@ import { workspaceMemoryExactKey, workspaceMemoryIdentityKey, redactCredentials, - isProjectSnapshotViolation, runMigrationP0Cleanup, loadWorkspaceMemory, saveWorkspaceMemory, updateWorkspaceMemoryWithAccounting, } from "../src/workspace-memory.ts"; +import { isProgressSnapshotViolation } from "../src/memory-quality.ts"; import { reviewerCurrent28Fixture, expectedAcceptedFixtureIds } from "./fixtures/memory-quality-current-28.ts"; function entry(id: string, text: string, type: LongTermMemoryEntry["type"] = "decision"): LongTermMemoryEntry { @@ -890,13 +890,13 @@ test("redactCredentials is idempotent and also redacts rationale text", () => { assert.equal(migrated.entries[0].rationale, "password: [REDACTED]"); }); -test("isProjectSnapshotViolation detects wave progress and avoids limit context false positives", () => { - assert.equal(isProjectSnapshotViolation("1237 tests pass, 226 suites"), true); - assert.equal(isProjectSnapshotViolation("USB 同步:37 個文件"), true); - assert.equal(isProjectSnapshotViolation("Waves 1-5 已完成,Wave 6 deferred"), true); +test("shared progress snapshot rule detects wave progress and avoids limit context false positives", () => { + assert.equal(isProgressSnapshotViolation("1237 tests pass, 226 suites"), true); + assert.equal(isProgressSnapshotViolation("USB 同步:37 個文件"), true); + assert.equal(isProgressSnapshotViolation("Waves 1-5 已完成,Wave 6 deferred"), true); - assert.equal(isProjectSnapshotViolation("Upload limit is 10 files"), false); - assert.equal(isProjectSnapshotViolation("Project supports 5 test suites"), false); + assert.equal(isProgressSnapshotViolation("Upload limit is 10 files"), false); + assert.equal(isProgressSnapshotViolation("Project supports 5 test suites"), false); }); test("runMigrationP0Cleanup marks only non-explicit project snapshots and runs once", () => {