mirror of
https://github.com/sdwolf4103/opencode-working-memory.git
synced 2026-06-02 06:19:36 +02:00
feat(memory-diag): add quality review board command
Add memory-diag quality command for objective review of memory-system mechanisms and active memory content. The command is read-only and non-authoritative, providing evidence, heuristic flags, and review questions without making quality judgments or suggesting mutations. Key components: - quality-review-model.ts: builds ReviewBoardReport with provenance, re-absorption detection, mechanism facts (rejection, reinforcement, eviction/caps, identity/dedup), and memory content facts - formatters/quality.ts: human and JSON output with separate system-mechanism and memory-content sections - commands/quality.ts: command entry point with --json, --verbose, --no-emoji, --raw options - cli.ts: parser whitelist for quality accepting --workspace, --json, rejecting mutation/filter flags co-author: code-execute-agent, comprehensive-code-reviewer, systems-architect, creative-disruptor Closes docs/plans/2026-05-11-memory-diag-quality-review-board.md
This commit is contained in:
@@ -248,6 +248,7 @@ npx --package opencode-working-memory memory-diag status
|
||||
npx --package opencode-working-memory memory-diag rejected
|
||||
npx --package opencode-working-memory memory-diag missing
|
||||
npx --package opencode-working-memory memory-diag explain <memory-id>
|
||||
npx --package opencode-working-memory memory-diag quality
|
||||
```
|
||||
|
||||
See [Diagnostics](docs/diagnostics.md) for the full command reference, numbered memory command reports, and dry-run recovery workflow.
|
||||
@@ -276,7 +277,7 @@ See [Configuration](docs/configuration.md) for customization options.
|
||||
## Requirements
|
||||
|
||||
- OpenCode plugin API `>=1.2.0 <2.0.0`
|
||||
- Node.js >= 22.6.0 (for `memory-diag` CLI, which runs TypeScript with `--experimental-strip-types`)
|
||||
- Node.js >= 22.6.0 (the published `memory-diag` CLI runs compiled JavaScript)
|
||||
|
||||
## Limitations
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ The npm package is `opencode-working-memory`; the installed bin is `memory-diag`
|
||||
| Where did my memory go? | `npx --package opencode-working-memory memory-diag missing` |
|
||||
| Why is this memory shown or hidden? | `npx --package opencode-working-memory memory-diag explain <memory-id>` |
|
||||
| How are numbered memory commands behaving? | `npx --package opencode-working-memory memory-diag commands` |
|
||||
| How do I review memory quality without automatic cleanup? | `npx --package opencode-working-memory memory-diag quality` |
|
||||
| Revert a numbered replacement? | `npx --package opencode-working-memory memory-diag revert --memory <replacement-memory-id>` |
|
||||
|
||||
## Global Options
|
||||
@@ -29,9 +30,22 @@ npx --package opencode-working-memory memory-diag rejected --verbose
|
||||
npx --package opencode-working-memory memory-diag missing --workspace /path/to/project
|
||||
npx --package opencode-working-memory memory-diag status --json
|
||||
npx --package opencode-working-memory memory-diag commands --verbose
|
||||
npx --package opencode-working-memory memory-diag quality
|
||||
npx --package opencode-working-memory memory-diag revert --memory <replacement-memory-id>
|
||||
```
|
||||
|
||||
## Quality Review Board
|
||||
|
||||
Use `memory-diag quality` for a read-only, evidence-first review of memory quality without automatic cleanup.
|
||||
|
||||
- Primarily provides memory-system mechanism observations for human/agent interpretation.
|
||||
- Secondarily helps review active memory content quality.
|
||||
- Separates system-mechanism facts, memory-content facts, heuristic flags, and review questions.
|
||||
- Includes inferred evidence provenance because historical records do not record producer package version.
|
||||
- Labels uncertain provenance as `unversioned_ambiguous` so old artifacts are not treated as current mechanism failures.
|
||||
- Does not decide what to delete or mutate.
|
||||
- Use `--json` for agent/objective review.
|
||||
|
||||
## Numbered Memory Command Reports
|
||||
|
||||
Use `memory-diag commands` to inspect `REINFORCE [M#]` and `REPLACE [M#]` outcomes from compaction.
|
||||
|
||||
@@ -10,6 +10,7 @@ export function usage(): string {
|
||||
memory-diag missing [--workspace <path>] [--verbose] [--json]
|
||||
memory-diag explain [memory-id] [--workspace <path>] [--raw]
|
||||
memory-diag commands [--workspace <path>] [--verbose] [--json]
|
||||
memory-diag quality [--workspace <path>] [--verbose] [--json] [--raw] [--no-emoji]
|
||||
memory-diag revert (--memory <replacement-id> | --event <event-id>) [--workspace <path>] [--apply]
|
||||
|
||||
Global options:
|
||||
@@ -103,12 +104,12 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
||||
|
||||
if (command === "status") {
|
||||
if (options.all) return error(`${command} does not accept --all`);
|
||||
} else if (command === "rejected" || command === "missing" || command === "coverage" || command === "explain" || command === "commands" || command === "revert") {
|
||||
} else if (command === "rejected" || command === "missing" || command === "coverage" || command === "explain" || command === "commands" || command === "quality" || command === "revert") {
|
||||
if (options.all) return error(`${command} does not accept --all`);
|
||||
} else {
|
||||
if (options.all || options.workspace) return error(`${command} does not accept --all or --workspace`);
|
||||
}
|
||||
if (options.json && command !== "status" && command !== "rejected" && command !== "missing" && command !== "coverage" && command !== "commands") {
|
||||
if (options.json && command !== "status" && command !== "rejected" && command !== "missing" && command !== "coverage" && command !== "commands" && command !== "quality") {
|
||||
return error(`${command} does not accept --json`);
|
||||
}
|
||||
if (command !== "rejected" && (options.softOnly || options.triggerOnly || options.since)) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const VISIBLE_COMMANDS = ["status", "rejected", "missing", "explain", "commands", "revert"] as const;
|
||||
export const VISIBLE_COMMANDS = ["status", "rejected", "missing", "explain", "commands", "quality", "revert"] as const;
|
||||
export const HIDDEN_COMMANDS = ["coverage", "audit"] as const;
|
||||
|
||||
export type VisibleCommand = typeof VISIBLE_COMMANDS[number];
|
||||
|
||||
@@ -3,6 +3,7 @@ import { runCommands } from "./commands/commands.ts";
|
||||
import { runCoverage } from "./commands/coverage.ts";
|
||||
import { runExplain } from "./commands/explain.ts";
|
||||
import { runMissing } from "./commands/missing.ts";
|
||||
import { runQuality } from "./commands/quality.ts";
|
||||
import { runRejected } from "./commands/rejected.ts";
|
||||
import { runRevert } from "./commands/revert.ts";
|
||||
import { runStatus } from "./commands/status.ts";
|
||||
@@ -17,6 +18,7 @@ export async function dispatch(command: Command, options: CliOptions): Promise<C
|
||||
case "audit": return runAudit(options);
|
||||
case "explain": return runExplain(options);
|
||||
case "commands": return runCommands(options);
|
||||
case "quality": return runQuality(options);
|
||||
case "revert": return runRevert(options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { buildQualityJSON, formatQualityReviewBoard } from "../formatters/quality.ts";
|
||||
import { buildInspectionReadModel } from "../inspection-model.ts";
|
||||
import { buildQualityReviewBoard } from "../quality-review-model.ts";
|
||||
import type { CliOptions, CommandResult } from "../types.ts";
|
||||
|
||||
export async function runQuality(options: CliOptions): Promise<CommandResult> {
|
||||
const model = await buildInspectionReadModel(options);
|
||||
const report = buildQualityReviewBoard(model, {
|
||||
verbose: options.verbose,
|
||||
raw: options.raw,
|
||||
noEmoji: options.noEmoji,
|
||||
json: options.json,
|
||||
});
|
||||
|
||||
if (options.json) {
|
||||
return { stdout: JSON.stringify(buildQualityJSON(report, options.raw), null, 2) };
|
||||
}
|
||||
|
||||
return { stdout: formatQualityReviewBoard(report, { verbose: options.verbose, noEmoji: options.noEmoji }) };
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
import { cleanText, formatDetails } from "../text.ts";
|
||||
import type {
|
||||
CandidateProvenance,
|
||||
HeuristicFlag,
|
||||
ProvenanceClassification,
|
||||
ReviewBoardActiveMemory,
|
||||
ReviewBoardCandidate,
|
||||
ReviewBoardReport,
|
||||
} from "../quality-review-model.ts";
|
||||
|
||||
const PROVENANCE_ORDER: ProvenanceClassification[] = [
|
||||
"explicit_migration_evidence",
|
||||
"legacy_unversioned_format",
|
||||
"reabsorbed_post_rejection",
|
||||
"suspected_pre_migration_legacy",
|
||||
"likely_current_behavior",
|
||||
"unversioned_ambiguous",
|
||||
];
|
||||
|
||||
const REVIEW_FLAG_CAVEAT = "This flag is a prompt for review, not a conclusion.";
|
||||
|
||||
export function buildQualityJSON(report: ReviewBoardReport, raw = false): unknown {
|
||||
if (raw) return report;
|
||||
return redactUnknown(report);
|
||||
}
|
||||
|
||||
export function formatQualityReviewBoard(
|
||||
report: ReviewBoardReport,
|
||||
options: { verbose?: boolean; noEmoji?: boolean },
|
||||
): string {
|
||||
const bullet = "-";
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push("Memory quality review board");
|
||||
lines.push("Purpose: evidence for human/agent review only; no automatic judgment or cleanup.");
|
||||
lines.push("Producer version note: historical records do not include package/plugin version; provenance below is inferred.");
|
||||
lines.push("Primary review purpose: SYSTEM MECHANISM observations (filters, reinforcement, eviction/caps, identity/dedup).");
|
||||
lines.push("Secondary review purpose: MEMORY CONTENT quality (staleness, durability, redundancy, specificity).");
|
||||
lines.push("");
|
||||
|
||||
pushEvidenceProvenance(lines, report, bullet);
|
||||
lines.push("");
|
||||
pushSystemMechanismFacts(lines, report, bullet);
|
||||
lines.push("");
|
||||
pushMemoryContentFacts(lines, report, bullet);
|
||||
lines.push("");
|
||||
pushSystemMechanismCandidates(lines, report, bullet);
|
||||
lines.push("");
|
||||
pushMemoryContentCandidates(lines, report, bullet, options);
|
||||
lines.push("");
|
||||
pushReviewQuestions(lines, report, bullet);
|
||||
lines.push("");
|
||||
pushNextCommands(lines, report, bullet);
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function pushEvidenceProvenance(lines: string[], report: ReviewBoardReport, bullet: string): void {
|
||||
const context = report.provenanceContext;
|
||||
lines.push("Evidence provenance");
|
||||
lines.push(` ${bullet} method: migration/timestamp/format inference`);
|
||||
lines.push(` ${bullet} confidence: ${context.confidenceDisclaimer}`);
|
||||
lines.push(` ${bullet} migration timeline: ${formatMigrationTimeline(context.migrationTimeline)}`);
|
||||
if (context.lastActivityAt) lines.push(` ${bullet} last activity: ${context.lastActivityAt}`);
|
||||
}
|
||||
|
||||
function pushSystemMechanismFacts(lines: string[], report: ReviewBoardReport, bullet: string): void {
|
||||
const facts = report.facts.systemMechanisms;
|
||||
lines.push("Facts - system mechanisms");
|
||||
lines.push(" Provenance counts for mechanism evidence");
|
||||
lines.push(` ${bullet} ${formatProvenanceCounts(report.provenanceContext.countsByClassification)}`);
|
||||
lines.push(" Rejection filters");
|
||||
lines.push(` ${bullet} rejected records: ${facts.rejectionFilters.totalRecords} (unique: ${facts.rejectionFilters.uniqueTexts})`);
|
||||
lines.push(` ${bullet} raw reason-code distribution: ${formatCounts(facts.rejectionFilters.byRawReasonCode)}`);
|
||||
lines.push(` ${bullet} type distribution: ${formatCounts(facts.rejectionFilters.byType)}`);
|
||||
lines.push(` ${bullet} ambiguous/architecture-like rejected candidates: ${facts.rejectionFilters.ambiguousOrArchitectureLike}`);
|
||||
lines.push(` ${bullet} status-or-hard-reason evidence: ${facts.rejectionFilters.hardReasonOrNoiseHeuristic}`);
|
||||
lines.push(` ${bullet} re-absorbed rejected texts: ${facts.rejectionFilters.reabsorbedRejectedTexts}`);
|
||||
lines.push(" Reinforcement rules");
|
||||
lines.push(` ${bullet} reinforce attempts: ${facts.reinforcementRules.reinforceEvents}, reinforced: ${facts.reinforcementRules.reinforcedEvents}, rejected/blocked: ${facts.reinforcementRules.rejectedOrBlockedEvents}`);
|
||||
lines.push(` ${bullet} reinforcement-window blocked: ${facts.reinforcementRules.windowBlockedEvents} (rate: ${formatPercent(facts.reinforcementRules.windowBlockRate)})`);
|
||||
lines.push(` ${bullet} repeated blocks by memory: ${formatRepeatedBlocks(facts.reinforcementRules.repeatedBlocksByMemory)}`);
|
||||
lines.push(` ${bullet} malformed command events: ${facts.reinforcementRules.malformedCommandEvents}`);
|
||||
lines.push(" Eviction and caps");
|
||||
lines.push(` ${bullet} active memories: ${facts.evictionAndCaps.activeMemories} / ${facts.evictionAndCaps.maxEntries}`);
|
||||
lines.push(` ${bullet} rendered memories: ${facts.evictionAndCaps.renderedMemories}`);
|
||||
lines.push(` ${bullet} full caps: ${formatFullCaps(facts.evictionAndCaps.fullCaps, facts.evictionAndCaps.typeCounts, facts.evictionAndCaps.typeCaps, facts.evictionAndCaps.activeMemories, facts.evictionAndCaps.maxEntries)}`);
|
||||
lines.push(` ${bullet} capacity removals: total=${facts.evictionAndCaps.removedByCapacity}, global=${facts.evictionAndCaps.removedByGlobalCap}, type=${facts.evictionAndCaps.removedByTypeCap}`);
|
||||
lines.push(` ${bullet} recent evictions by type: ${formatCounts(facts.evictionAndCaps.recentEvictionsByType)}`);
|
||||
lines.push(` ${bullet} recent evicted content shown: ${facts.evictionAndCaps.recentEvictedContentShown}`);
|
||||
lines.push(` ${bullet} evidence-only disappearances: ${facts.evictionAndCaps.missingEvidenceOnly} (unknown: ${facts.evictionAndCaps.unknownDisappearances})`);
|
||||
lines.push(" Identity and dedup");
|
||||
lines.push(` ${bullet} replacements: total=${facts.identityAndDedup.replacementEvents}, same-type=${facts.identityAndDedup.sameTypeReplacementEvents}, cross-type=${facts.identityAndDedup.crossTypeReplacementEvents}`);
|
||||
lines.push(` ${bullet} superseded entries: ${facts.identityAndDedup.supersededEntries}`);
|
||||
lines.push(` ${bullet} exact duplicate/identity groups identified: ${facts.identityAndDedup.duplicateTextOrIdentityGroups}`);
|
||||
}
|
||||
|
||||
function pushMemoryContentFacts(lines: string[], report: ReviewBoardReport, bullet: string): void {
|
||||
const facts = report.facts.memoryContent;
|
||||
lines.push("Facts - memory content");
|
||||
lines.push(` ${bullet} rendered memories: ${facts.renderedMemories}`);
|
||||
lines.push(` ${bullet} evidence coverage: ${facts.evidenceCoverage.covered} / ${facts.evidenceCoverage.total}`);
|
||||
lines.push(` ${bullet} type counts: ${formatTypeCountsWithCaps(facts.typeCounts, facts.typeCaps)}`);
|
||||
lines.push(` ${bullet} weakest/strongest active memory previews: weakest=${formatMemoryPreviews(facts.weakestActiveMemories)}; strongest=${formatMemoryPreviews(facts.strongestActiveMemories)}`);
|
||||
}
|
||||
|
||||
function pushSystemMechanismCandidates(lines: string[], report: ReviewBoardReport, bullet: string): void {
|
||||
const display = report.provenanceContext.candidateDisplay;
|
||||
if (report.provenanceContext.candidateLimit && display && display.shown < display.total) {
|
||||
lines.push(`System mechanism review candidates (representative; ${display.shown} shown of ${display.total} total; limit ${report.provenanceContext.candidateLimit} per mechanism category)`);
|
||||
} else {
|
||||
lines.push("System mechanism review candidates");
|
||||
}
|
||||
pushCandidateGroup(lines, "Rejection filter evidence", candidatesFor(report, ["rejection_rule_evidence"]), bullet);
|
||||
pushCandidateGroup(lines, "Re-absorption evidence", candidatesFor(report, ["reabsorption_evidence"]), bullet);
|
||||
pushCandidateGroup(lines, "Reinforcement rule evidence", candidatesFor(report, ["numbered_command_evidence"]), bullet);
|
||||
pushCandidateGroup(lines, "Eviction/cap evidence", candidatesFor(report, ["eviction_cap_evidence", "missing_evidence"]), bullet);
|
||||
pushCandidateGroup(lines, "Identity/dedup evidence", candidatesFor(report, ["identity_dedup_evidence"]), bullet);
|
||||
}
|
||||
|
||||
function pushCandidateGroup(lines: string[], title: string, candidates: ReviewBoardCandidate[], bullet: string): void {
|
||||
lines.push(` ${title}`);
|
||||
if (candidates.length === 0) {
|
||||
lines.push(" (none)");
|
||||
return;
|
||||
}
|
||||
const shared = sharedProvenance(candidates);
|
||||
if (shared) lines.push(` shared provenance for displayed candidates in this group: ${formatProvenance(shared)}`);
|
||||
for (const candidate of candidates) pushCandidate(lines, candidate, bullet, shared);
|
||||
}
|
||||
|
||||
function pushCandidate(lines: string[], candidate: ReviewBoardCandidate, bullet: string, groupProvenance?: CandidateProvenance): void {
|
||||
const rawReasonCodes = candidate.evidence.rawReasonCodes && candidate.evidence.rawReasonCodes.length > 0
|
||||
? candidate.evidence.rawReasonCodes.join(", ")
|
||||
: "none";
|
||||
const question = candidate.reviewQuestions[0] ?? "What should a reviewer infer from this evidence?";
|
||||
lines.push(` ${bullet} concern=${formatConcern(candidate.concernKind)} id=${candidate.id} source=${candidate.source} mechanism=${candidate.mechanism ?? "unspecified"} raw reason codes=${rawReasonCodes} question=${question}`);
|
||||
if (candidate.provenance && (!groupProvenance || formatProvenance(candidate.provenance) !== formatProvenance(groupProvenance))) {
|
||||
lines.push(` provenance: ${formatProvenance(candidate.provenance)}`);
|
||||
}
|
||||
if (candidate.evidence.eventIds && candidate.evidence.eventIds.length > 0) lines.push(` event ids: ${candidate.evidence.eventIds.join(", ")}`);
|
||||
if (candidate.evidence.textAvailable) {
|
||||
lines.push(` text preview: ${candidate.evidence.textPreview ?? "available but empty after redaction"}`);
|
||||
} else {
|
||||
lines.push(" text preview: unavailable in historical evidence");
|
||||
}
|
||||
lines.push(` facts: ${formatCandidateFacts(candidate.facts)}`);
|
||||
pushHeuristicFlags(lines, candidate.heuristicFlags, " ", bullet);
|
||||
}
|
||||
|
||||
function pushMemoryContentCandidates(lines: string[], report: ReviewBoardReport, bullet: string, options: { verbose?: boolean }): void {
|
||||
const display = report.activeMemoryDisplay;
|
||||
lines.push("Memory content review candidates");
|
||||
if (report.reviewQuestions.memoryContent.length > 0) {
|
||||
lines.push(" Standard review questions (applicable to all active memories below):");
|
||||
for (const question of report.reviewQuestions.memoryContent) lines.push(` ${bullet} ${question}`);
|
||||
}
|
||||
if (display.total === 0) {
|
||||
lines.push(" Active memories (none)");
|
||||
return;
|
||||
}
|
||||
if (display.total <= display.threshold) {
|
||||
lines.push(` Active memories (showing all ${display.total} because <= ${display.threshold})`);
|
||||
} else if (display.mode === "all" || options.verbose) {
|
||||
lines.push(` Active memories (showing all ${display.total} because --verbose)`);
|
||||
} else {
|
||||
lines.push(` Active memories (showing ${display.shown} of ${display.total})`);
|
||||
lines.push(` Showing ${display.shown} of ${display.total} active memories. Use --verbose or --json for all active memory text.`);
|
||||
}
|
||||
display.items.forEach((item, index) => pushActiveMemory(lines, item, index + 1, bullet, report.reviewQuestions.memoryContent));
|
||||
}
|
||||
|
||||
function pushActiveMemory(lines: string[], item: ReviewBoardActiveMemory, index: number, bullet: string, standardQuestions: string[]): void {
|
||||
const strength = typeof item.strength === "number" ? item.strength.toFixed(3) : "unknown";
|
||||
lines.push(` [${index}] id=${item.id} type=${item.type} source=${item.source} status=${item.status} strength=${strength}`);
|
||||
lines.push(" text: " + indentContinuation(item.text, " "));
|
||||
const rawReasonCodes = item.evidence.rawReasonCodes.length > 0 ? item.evidence.rawReasonCodes.join(", ") : "none";
|
||||
lines.push(` evidence: events=${item.evidence.eventCount} raw reason codes=${rawReasonCodes}`);
|
||||
if (item.provenance) lines.push(` provenance: ${formatProvenance(item.provenance)}`);
|
||||
pushHeuristicFlags(lines, item.heuristicFlags, " ", bullet);
|
||||
if (questionsEqual(item.reviewQuestions, standardQuestions)) return;
|
||||
const additionalQuestions = item.reviewQuestions.filter(question => !standardQuestions.includes(question));
|
||||
if (additionalQuestions.length > 0 && additionalQuestions.length < item.reviewQuestions.length) {
|
||||
lines.push(" additional review questions:");
|
||||
for (const question of additionalQuestions) lines.push(` ${bullet} ${question}`);
|
||||
return;
|
||||
}
|
||||
lines.push(" review questions:");
|
||||
for (const question of item.reviewQuestions) lines.push(` ${bullet} ${question}`);
|
||||
}
|
||||
|
||||
function pushHeuristicFlags(lines: string[], flags: HeuristicFlag[], indent: string, bullet: string): void {
|
||||
if (flags.length === 0) return;
|
||||
lines.push(`${indent}heuristic flags:`);
|
||||
for (const flag of flags) {
|
||||
const caveat = flag.caveat || REVIEW_FLAG_CAVEAT;
|
||||
lines.push(`${indent} ${bullet} ${flag.label}: ${flag.evidence}. ${caveat}`);
|
||||
}
|
||||
}
|
||||
|
||||
function pushReviewQuestions(lines: string[], report: ReviewBoardReport, bullet: string): void {
|
||||
lines.push("Review questions");
|
||||
lines.push(" SYSTEM MECHANISM");
|
||||
for (const question of report.reviewQuestions.systemMechanism) lines.push(` ${bullet} ${question}`);
|
||||
lines.push(" MEMORY CONTENT");
|
||||
for (const question of report.reviewQuestions.memoryContent) lines.push(` ${bullet} ${question}`);
|
||||
}
|
||||
|
||||
function pushNextCommands(lines: string[], report: ReviewBoardReport, bullet: string): void {
|
||||
lines.push("Next commands");
|
||||
for (const command of report.nextCommands) lines.push(` ${bullet} ${command}`);
|
||||
}
|
||||
|
||||
function candidatesFor(report: ReviewBoardReport, sources: ReviewBoardCandidate["source"][]): ReviewBoardCandidate[] {
|
||||
const sourceSet = new Set(sources);
|
||||
return report.reviewCandidates.filter(candidate => candidate.concernKind === "system_mechanism" && sourceSet.has(candidate.source));
|
||||
}
|
||||
|
||||
function sharedProvenance(candidates: ReviewBoardCandidate[]): CandidateProvenance | undefined {
|
||||
if (candidates.length <= 1) return undefined;
|
||||
const first = candidates[0]?.provenance;
|
||||
if (!first) return undefined;
|
||||
const key = formatProvenance(first);
|
||||
return candidates.every(candidate => candidate.provenance && formatProvenance(candidate.provenance) === key) ? first : undefined;
|
||||
}
|
||||
|
||||
function formatConcern(concern: ReviewBoardCandidate["concernKind"]): string {
|
||||
return concern === "system_mechanism" ? "SYSTEM MECHANISM" : "MEMORY CONTENT";
|
||||
}
|
||||
|
||||
function formatMigrationTimeline(timeline: ReviewBoardReport["provenanceContext"]["migrationTimeline"]): string {
|
||||
if (timeline.length === 0) return "(none)";
|
||||
return timeline.map(row => `${row.migrationId}=${row.presentInStore ? "present" : "absent"}${row.firstEvidenceAt ? ` firstEvidenceAt=${row.firstEvidenceAt}` : ""}`).join(", ");
|
||||
}
|
||||
|
||||
function formatProvenanceCounts(counts: Record<ProvenanceClassification, number>): string {
|
||||
return PROVENANCE_ORDER.map(classification => `${classification}=${counts[classification] ?? 0}`).join(", ");
|
||||
}
|
||||
|
||||
function formatCounts(counts: Record<string, number>): string {
|
||||
const entries = Object.entries(counts).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
||||
return entries.length === 0 ? "(none)" : entries.map(([key, count]) => `${key}=${count}`).join(", ");
|
||||
}
|
||||
|
||||
function formatTypeCountsWithCaps(counts: Record<string, number>, caps: Record<string, number>): string {
|
||||
const keys = uniqueSorted([...Object.keys(caps), ...Object.keys(counts)]);
|
||||
return keys.length === 0 ? "(none)" : keys.map(key => `${key} ${counts[key] ?? 0}/${caps[key] ?? "?"}`).join(", ");
|
||||
}
|
||||
|
||||
function formatFullCaps(fullCaps: string[], typeCounts: Record<string, number>, typeCaps: Record<string, number>, active: number, maxEntries: number): string {
|
||||
if (fullCaps.length === 0) return "(none)";
|
||||
return fullCaps.map(cap => cap === "global" ? `global ${active}/${maxEntries}` : `${cap} ${typeCounts[cap] ?? 0}/${typeCaps[cap] ?? "?"}`).join(", ");
|
||||
}
|
||||
|
||||
function formatRepeatedBlocks(blocks: ReviewBoardReport["facts"]["systemMechanisms"]["reinforcementRules"]["repeatedBlocksByMemory"]): string {
|
||||
if (blocks.length === 0) return "(none)";
|
||||
return blocks.map(block => `${block.memoryId} count=${block.count} refs=${block.refs.join("|") || "none"} raw reason codes=${block.rawReasonCodes.join("|") || "none"}`).join(", ");
|
||||
}
|
||||
|
||||
function formatMemoryPreviews(items: ReviewBoardReport["facts"]["memoryContent"]["weakestActiveMemories"]): string {
|
||||
if (items.length === 0) return "(none)";
|
||||
return items.map(item => `${item.id} type=${item.type} strength=${typeof item.strength === "number" ? item.strength.toFixed(3) : "unknown"} text=${JSON.stringify(item.textPreview)}`).join(" | ");
|
||||
}
|
||||
|
||||
function formatPercent(value: number): string {
|
||||
return `${(Number.isFinite(value) ? value * 100 : 0).toFixed(1)}%`;
|
||||
}
|
||||
|
||||
function formatProvenance(provenance: CandidateProvenance): string {
|
||||
return `${provenance.classification} confidence=${provenance.confidence}; basis=${provenance.basis.join("; ") || "unavailable"}; caveat=${provenance.interpretationCaveat}`;
|
||||
}
|
||||
|
||||
function formatCandidateFacts(facts: Record<string, unknown>): string {
|
||||
if (Object.keys(facts).length === 0) return "(none)";
|
||||
return formatDetails(Object.fromEntries(
|
||||
Object.entries(facts).map(([key, value]) => [key, formatFactValue(value)]),
|
||||
));
|
||||
}
|
||||
|
||||
function formatFactValue(value: unknown): string | number | boolean | string[] | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
||||
if (value === null) return "null";
|
||||
if (Array.isArray(value)) return value.map(item => typeof item === "string" || typeof item === "number" || typeof item === "boolean" || item === null ? String(item) : stringifyUnknown(item));
|
||||
return stringifyUnknown(value);
|
||||
}
|
||||
|
||||
function stringifyUnknown(value: unknown): string {
|
||||
try {
|
||||
const serialized = JSON.stringify(value);
|
||||
return serialized === undefined ? String(value) : serialized;
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
function questionsEqual(a: string[], b: string[]): boolean {
|
||||
return a.length === b.length && a.every((value, index) => value === b[index]);
|
||||
}
|
||||
|
||||
function indentContinuation(text: string, indent: string): string {
|
||||
return text.split("\n").map((line, index) => index === 0 ? line : `${indent}${line}`).join("\n");
|
||||
}
|
||||
|
||||
function uniqueSorted(values: string[]): string[] {
|
||||
return [...new Set(values)].sort();
|
||||
}
|
||||
|
||||
function redactUnknown(value: unknown): unknown {
|
||||
if (typeof value === "string") return cleanText(value, false);
|
||||
if (Array.isArray(value)) return value.map(item => redactUnknown(item));
|
||||
if (!value || typeof value !== "object") return value;
|
||||
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactUnknown(item)]));
|
||||
}
|
||||
@@ -0,0 +1,930 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import type { EvidenceEventV1 } from "../../src/evidence-log.ts";
|
||||
import { RETENTION_TYPE_MAX } from "../../src/retention.ts";
|
||||
import type { LongTermMemoryEntry, LongTermType } from "../../src/types.ts";
|
||||
import { TYPES } from "./constants.ts";
|
||||
import { disappearanceRows } from "./inspection-model.ts";
|
||||
import { rejectionQualitySummary, uniqueByCanonicalText } from "./rejections-model.ts";
|
||||
import { canonicalMemoryText, cleanText, countBy, objectFromCounts, truncate, uniqueStrings, workspaceRootHash } from "./text.ts";
|
||||
import type { MemoryInspectionReadModel, NormalizedRejection } from "./types.ts";
|
||||
|
||||
export type ReviewBoardReport = {
|
||||
version: 1;
|
||||
generatedAt: string;
|
||||
workspace: { rootHash: string; key: string };
|
||||
purpose: "review_evidence_only";
|
||||
languageGuidance: {
|
||||
nonAuthoritative: true;
|
||||
mutation: "none";
|
||||
rawReasonCodesAreEvidence: true;
|
||||
producerVersionRecorded: false;
|
||||
provenanceInferenceOnly: true;
|
||||
primaryReviewPurpose: "system_mechanism_observations";
|
||||
secondaryReviewPurpose: "memory_content_quality";
|
||||
};
|
||||
provenanceContext: {
|
||||
method: "migration_timestamp_and_format_inference";
|
||||
confidenceDisclaimer: string;
|
||||
falseCurrentRiskBias: "prefer_unversioned_ambiguous_when_uncertain";
|
||||
producerVersionAvailable: false;
|
||||
migrationTimeline: Array<{ migrationId: string; presentInStore: boolean; firstEvidenceAt?: string }>;
|
||||
lastActivityAt?: string;
|
||||
countsByClassification: Record<ProvenanceClassification, number>;
|
||||
candidateLimit?: number;
|
||||
candidateDisplay?: {
|
||||
shown: number;
|
||||
total: number;
|
||||
byMechanism: Record<string, { shown: number; total: number }>;
|
||||
};
|
||||
};
|
||||
facts: {
|
||||
systemMechanisms: {
|
||||
rejectionFilters: {
|
||||
totalRecords: number;
|
||||
uniqueTexts: number;
|
||||
byRawReasonCode: Record<string, number>;
|
||||
byType: Record<string, number>;
|
||||
ambiguousOrArchitectureLike: number;
|
||||
hardReasonOrNoiseHeuristic: number;
|
||||
reabsorbedRejectedTexts: number;
|
||||
};
|
||||
reinforcementRules: {
|
||||
reinforceEvents: number;
|
||||
reinforcedEvents: number;
|
||||
rejectedOrBlockedEvents: number;
|
||||
windowBlockedEvents: number;
|
||||
windowBlockRate: number;
|
||||
repeatedBlocksByMemory: Array<{ memoryId: string; count: number; refs: string[]; rawReasonCodes: string[] }>;
|
||||
malformedCommandEvents: number;
|
||||
};
|
||||
evictionAndCaps: {
|
||||
activeMemories: number;
|
||||
maxEntries: number;
|
||||
renderedMemories: number;
|
||||
typeCounts: Record<string, number>;
|
||||
typeCaps: Record<string, number>;
|
||||
fullCaps: string[];
|
||||
missingEvidenceOnly: number;
|
||||
unknownDisappearances: number;
|
||||
removedByCapacity: number;
|
||||
removedByGlobalCap: number;
|
||||
removedByTypeCap: number;
|
||||
recentEvictionsByType: Record<string, number>;
|
||||
recentEvictedContentShown: number;
|
||||
};
|
||||
identityAndDedup: {
|
||||
replacementEvents: number;
|
||||
sameTypeReplacementEvents: number;
|
||||
crossTypeReplacementEvents: number;
|
||||
supersededEntries: number;
|
||||
duplicateTextOrIdentityGroups: number;
|
||||
};
|
||||
};
|
||||
memoryContent: {
|
||||
activeMemories: number;
|
||||
renderedMemories: number;
|
||||
evidenceCoverage: { covered: number; total: number };
|
||||
typeCounts: Record<string, number>;
|
||||
typeCaps: Record<string, number>;
|
||||
weakestActiveMemories: Array<{ id: string; type: string; strength?: number; textPreview: string }>;
|
||||
strongestActiveMemories: Array<{ id: string; type: string; strength?: number; textPreview: string }>;
|
||||
};
|
||||
};
|
||||
activeMemoryDisplay: {
|
||||
threshold: number;
|
||||
mode: "all" | "sample";
|
||||
shown: number;
|
||||
total: number;
|
||||
items: ReviewBoardActiveMemory[];
|
||||
};
|
||||
reviewCandidates: ReviewBoardCandidate[];
|
||||
reviewQuestions: {
|
||||
systemMechanism: string[];
|
||||
memoryContent: string[];
|
||||
};
|
||||
nextCommands: string[];
|
||||
};
|
||||
|
||||
export type ReviewBoardActiveMemory = {
|
||||
id: string;
|
||||
type: string;
|
||||
source: string;
|
||||
status: string;
|
||||
strength?: number;
|
||||
text: string;
|
||||
evidence: { eventCount: number; eventIds: string[]; rawReasonCodes: string[] };
|
||||
provenance?: CandidateProvenance;
|
||||
heuristicFlags: HeuristicFlag[];
|
||||
reviewQuestions: string[];
|
||||
};
|
||||
|
||||
export type ReviewBoardCandidate = {
|
||||
concernKind: "system_mechanism" | "memory_content";
|
||||
mechanism?: "rejection_filter" | "reinforcement_rule" | "eviction_cap" | "identity_dedup" | "retention_rendering";
|
||||
source:
|
||||
| "active_memory"
|
||||
| "rejection_rule_evidence"
|
||||
| "missing_evidence"
|
||||
| "numbered_command_evidence"
|
||||
| "eviction_cap_evidence"
|
||||
| "reabsorption_evidence"
|
||||
| "identity_dedup_evidence";
|
||||
id: string;
|
||||
facts: Record<string, unknown>;
|
||||
evidence: { eventIds?: string[]; rawReasonCodes?: string[]; textPreview?: string; textAvailable: boolean };
|
||||
provenance?: CandidateProvenance;
|
||||
heuristicFlags: HeuristicFlag[];
|
||||
reviewQuestions: string[];
|
||||
nextCommands: string[];
|
||||
};
|
||||
|
||||
export type ProvenanceClassification =
|
||||
| "explicit_migration_evidence"
|
||||
| "legacy_unversioned_format"
|
||||
| "reabsorbed_post_rejection"
|
||||
| "suspected_pre_migration_legacy"
|
||||
| "likely_current_behavior"
|
||||
| "unversioned_ambiguous";
|
||||
|
||||
export type CandidateProvenance = {
|
||||
classification: ProvenanceClassification;
|
||||
confidence: "high" | "medium" | "low";
|
||||
basis: string[];
|
||||
interpretationCaveat: string;
|
||||
};
|
||||
|
||||
export type HeuristicFlag = {
|
||||
id: string;
|
||||
label: string;
|
||||
evidence: string;
|
||||
caveat: string;
|
||||
};
|
||||
|
||||
export const ACTIVE_MEMORY_FULL_TEXT_THRESHOLD = 40;
|
||||
export const REPRESENTATIVE_CANDIDATE_LIMIT = 10;
|
||||
export const RECENT_EVICTION_DAYS = 7;
|
||||
|
||||
const KNOWN_MIGRATION_IDS = [
|
||||
"2026-04-26-p0-cleanup",
|
||||
"2026-04-28-quality-cleanup",
|
||||
"2026-05-01-retention-clock-backfill",
|
||||
] as const;
|
||||
|
||||
const PROVENANCE_CLASSIFICATIONS: ProvenanceClassification[] = [
|
||||
"explicit_migration_evidence",
|
||||
"legacy_unversioned_format",
|
||||
"reabsorbed_post_rejection",
|
||||
"suspected_pre_migration_legacy",
|
||||
"likely_current_behavior",
|
||||
"unversioned_ambiguous",
|
||||
];
|
||||
|
||||
const HARD_OR_NOISE_REASON_CODES = new Set([
|
||||
"progress_snapshot",
|
||||
"active_file_snapshot",
|
||||
"commit_or_ci_snapshot",
|
||||
"temporary_status",
|
||||
"raw_error",
|
||||
"code_or_api_signature",
|
||||
"bad_feedback",
|
||||
]);
|
||||
|
||||
const MALFORMED_COMMAND_REASON_CODES = new Set([
|
||||
"invalid_memory_command",
|
||||
"invalid_memory_ref",
|
||||
"invalid_memory_type",
|
||||
"empty_replacement_text",
|
||||
]);
|
||||
|
||||
type ReabsorbedMatch = {
|
||||
key: string;
|
||||
record: NormalizedRejection;
|
||||
activeMemory: LongTermMemoryEntry;
|
||||
};
|
||||
|
||||
type ProvenanceContextInputs = {
|
||||
firstMigrationBoundary?: string;
|
||||
latestMigrationBoundary?: string;
|
||||
lastActivityAt?: string;
|
||||
};
|
||||
|
||||
type DatedCandidateInput = {
|
||||
candidate: ReviewBoardCandidate;
|
||||
timestamp?: string;
|
||||
tieId: string;
|
||||
textHash?: string;
|
||||
};
|
||||
|
||||
export function buildQualityReviewBoard(
|
||||
model: MemoryInspectionReadModel,
|
||||
options: { verbose?: boolean; raw?: boolean; noEmoji?: boolean; json?: boolean },
|
||||
generatedAt = new Date().toISOString(),
|
||||
): ReviewBoardReport {
|
||||
const raw = options.raw === true;
|
||||
const activeMemories = model.store.entries.filter(entry => entry.status !== "superseded");
|
||||
const typeCounts = typeCountsFor(activeMemories);
|
||||
const typeCaps = Object.fromEntries(TYPES.map(type => [type, RETENTION_TYPE_MAX[type]]));
|
||||
const rejectionSummary = rejectionQualitySummary(model.rejectionRecords);
|
||||
const rejectionGroups = rejectionSummary.possibleFalsePositiveGroups;
|
||||
const migrationTimeline = buildMigrationTimeline(model);
|
||||
const provenanceInputs = migrationBoundaries(model);
|
||||
const activeKeyMatches = reabsorbedRejectedMatches(activeMemories, model.rejectionRecords);
|
||||
const reabsorbedKeys = new Set(activeKeyMatches.map(match => match.key));
|
||||
const activeMemoryByKey = new Map(activeKeyMatches.map(match => [match.key, match.activeMemory]));
|
||||
const disappearances = disappearanceRows(model);
|
||||
const reinforcementFacts = buildReinforcementFacts(model.evidenceEvents);
|
||||
const evictionFacts = buildEvictionFacts(model, activeMemories, typeCounts, typeCaps, disappearances, generatedAt);
|
||||
const identityFacts = buildIdentityFacts(model, activeMemories);
|
||||
const memoryContentFacts = buildMemoryContentFacts(model, activeMemories, typeCounts, typeCaps, raw);
|
||||
const systemMechanismCandidateInputs = {
|
||||
rejection_filter: [
|
||||
...buildRejectionCandidates(model.rejectionRecords, provenanceInputs, raw),
|
||||
...buildReabsorptionCandidates(activeKeyMatches, provenanceInputs, raw),
|
||||
],
|
||||
reinforcement_rule: buildReinforcementCandidates(model.evidenceEvents, provenanceInputs, raw),
|
||||
eviction_cap: buildEvictionCandidates(disappearances, model.evidenceEvents, provenanceInputs, raw, generatedAt),
|
||||
identity_dedup: buildIdentityCandidates(model, activeMemories, provenanceInputs, raw),
|
||||
};
|
||||
const showAllSystemMechanismCandidates = options.verbose === true || options.json === true;
|
||||
const systemCandidateDisplay = buildSystemCandidateDisplay(systemMechanismCandidateInputs, showAllSystemMechanismCandidates);
|
||||
const allSystemMechanismCandidates = Object.values(systemMechanismCandidateInputs)
|
||||
.flatMap(inputs => selectRepresentative(inputs, true).map(item => item.candidate));
|
||||
const reviewCandidates = [
|
||||
...systemCandidateDisplay.candidates,
|
||||
...buildMemoryContentCandidates(model, activeMemories, raw),
|
||||
];
|
||||
const activeMemoryDisplay = buildActiveMemoryDisplay(model, activeMemories, reabsorbedKeys, activeMemoryByKey, provenanceInputs, raw, options.verbose === true);
|
||||
const countsByClassification = countProvenanceClassifications(allSystemMechanismCandidates);
|
||||
|
||||
return {
|
||||
version: 1,
|
||||
generatedAt,
|
||||
workspace: {
|
||||
rootHash: workspaceRootHash(model.snapshot?.store?.workspace?.root ?? model.store.workspace.root),
|
||||
key: model.snapshot?.store?.workspace?.key ?? model.store.workspace.key,
|
||||
},
|
||||
purpose: "review_evidence_only",
|
||||
languageGuidance: {
|
||||
nonAuthoritative: true,
|
||||
mutation: "none",
|
||||
rawReasonCodesAreEvidence: true,
|
||||
producerVersionRecorded: false,
|
||||
provenanceInferenceOnly: true,
|
||||
primaryReviewPurpose: "system_mechanism_observations",
|
||||
secondaryReviewPurpose: "memory_content_quality",
|
||||
},
|
||||
provenanceContext: {
|
||||
method: "migration_timestamp_and_format_inference",
|
||||
confidenceDisclaimer: "Producer version is not recorded in historical evidence; provenance is inferred and should not be used as proof of current behavior.",
|
||||
falseCurrentRiskBias: "prefer_unversioned_ambiguous_when_uncertain",
|
||||
producerVersionAvailable: false,
|
||||
migrationTimeline,
|
||||
lastActivityAt: model.store.lastActivityAt,
|
||||
countsByClassification,
|
||||
...(systemCandidateDisplay.limited ? { candidateLimit: REPRESENTATIVE_CANDIDATE_LIMIT, candidateDisplay: systemCandidateDisplay.summary } : {}),
|
||||
},
|
||||
facts: {
|
||||
systemMechanisms: {
|
||||
rejectionFilters: {
|
||||
totalRecords: rejectionSummary.totalRecords,
|
||||
uniqueTexts: rejectionSummary.uniqueTexts,
|
||||
byRawReasonCode: rejectionSummary.reasonDistribution,
|
||||
byType: objectFromCounts(countBy(model.rejectionRecords.map(record => record.type))),
|
||||
ambiguousOrArchitectureLike:
|
||||
(rejectionGroups.architecture_like_possible_false_positive?.count ?? 0)
|
||||
+ (rejectionGroups.ambiguous?.count ?? 0),
|
||||
hardReasonOrNoiseHeuristic: rejectionGroups.clearly_garbage?.count ?? 0,
|
||||
reabsorbedRejectedTexts: new Set(activeKeyMatches.map(match => match.key)).size,
|
||||
},
|
||||
reinforcementRules: reinforcementFacts,
|
||||
evictionAndCaps: evictionFacts,
|
||||
identityAndDedup: identityFacts,
|
||||
},
|
||||
memoryContent: memoryContentFacts,
|
||||
},
|
||||
activeMemoryDisplay,
|
||||
reviewCandidates,
|
||||
reviewQuestions: {
|
||||
systemMechanism: systemMechanismQuestions(),
|
||||
memoryContent: memoryContentQuestions(),
|
||||
},
|
||||
nextCommands: nextCommands(),
|
||||
};
|
||||
}
|
||||
|
||||
function typeCountsFor(entries: LongTermMemoryEntry[]): Record<string, number> {
|
||||
return Object.fromEntries(TYPES.map(type => [type, entries.filter(entry => entry.type === type).length]));
|
||||
}
|
||||
|
||||
function buildMigrationTimeline(model: MemoryInspectionReadModel): ReviewBoardReport["provenanceContext"]["migrationTimeline"] {
|
||||
const present = new Set(model.store.migrations ?? []);
|
||||
return KNOWN_MIGRATION_IDS.map(migrationId => {
|
||||
const matchingTimes = model.evidenceEvents
|
||||
.filter(event => event.details?.migrationId === migrationId)
|
||||
.map(event => event.createdAt)
|
||||
.sort();
|
||||
const row: { migrationId: string; presentInStore: boolean; firstEvidenceAt?: string } = {
|
||||
migrationId,
|
||||
presentInStore: present.has(migrationId),
|
||||
};
|
||||
if (matchingTimes[0]) row.firstEvidenceAt = matchingTimes[0];
|
||||
return row;
|
||||
});
|
||||
}
|
||||
|
||||
function migrationBoundaries(model: MemoryInspectionReadModel): ProvenanceContextInputs {
|
||||
const present = new Set(model.store.migrations ?? []);
|
||||
const matchingTimes = model.evidenceEvents
|
||||
.filter(event => typeof event.details?.migrationId === "string" && present.has(event.details.migrationId))
|
||||
.map(event => event.createdAt)
|
||||
.sort();
|
||||
return {
|
||||
firstMigrationBoundary: matchingTimes[0],
|
||||
latestMigrationBoundary: matchingTimes[matchingTimes.length - 1],
|
||||
lastActivityAt: model.store.lastActivityAt,
|
||||
};
|
||||
}
|
||||
|
||||
function classifyProvenance(input: {
|
||||
event?: EvidenceEventV1;
|
||||
rejection?: NormalizedRejection;
|
||||
reabsorbed?: boolean;
|
||||
}, context: ProvenanceContextInputs): CandidateProvenance {
|
||||
if (input.event?.details?.migrationId) {
|
||||
return provenance("explicit_migration_evidence", "high", [`migration evidence event ${String(input.event.details.migrationId)}`]);
|
||||
}
|
||||
if (input.rejection && !hasWorkspaceScope(input.rejection)) {
|
||||
return provenance("legacy_unversioned_format", "high", ["rejection record without workspace scope fields"]);
|
||||
}
|
||||
if (input.reabsorbed) {
|
||||
return provenance("reabsorbed_post_rejection", "high", ["typed canonical rejected text appears in active memory"]);
|
||||
}
|
||||
|
||||
const timestamp = input.event?.createdAt ?? input.rejection?.timestamp;
|
||||
if (timestamp && context.firstMigrationBoundary && compareIso(timestamp, context.firstMigrationBoundary) < 0) {
|
||||
return provenance("suspected_pre_migration_legacy", "medium", ["evidence timestamp predates first known migration boundary"]);
|
||||
}
|
||||
if (timestamp && context.latestMigrationBoundary && compareIso(timestamp, context.latestMigrationBoundary) >= 0) {
|
||||
if (!context.lastActivityAt || compareIso(timestamp, context.lastActivityAt) >= 0) {
|
||||
return provenance("likely_current_behavior", "medium", ["evidence timestamp is after known migration evidence and workspace last activity"]);
|
||||
}
|
||||
}
|
||||
return provenance("unversioned_ambiguous", "low", ["no producer version or decisive migration/timestamp signal is recorded"]);
|
||||
}
|
||||
|
||||
function provenance(classification: ProvenanceClassification, confidence: CandidateProvenance["confidence"], basis: string[]): CandidateProvenance {
|
||||
return {
|
||||
classification,
|
||||
confidence,
|
||||
basis,
|
||||
interpretationCaveat: "Producer version is not recorded; treat this as inferred review context rather than proof of current behavior.",
|
||||
};
|
||||
}
|
||||
|
||||
function hasWorkspaceScope(record: NormalizedRejection): boolean {
|
||||
return Boolean(record.workspaceKey || record.workspaceRoot || record.workspaceRootHash);
|
||||
}
|
||||
|
||||
function compareIso(a: string, b: string): number {
|
||||
const aTime = new Date(a).getTime();
|
||||
const bTime = new Date(b).getTime();
|
||||
if (!Number.isFinite(aTime) || !Number.isFinite(bTime)) return 0;
|
||||
return aTime - bTime;
|
||||
}
|
||||
|
||||
function reabsorbedRejectedMatches(activeMemories: LongTermMemoryEntry[], records: NormalizedRejection[]): ReabsorbedMatch[] {
|
||||
const activeByKey = new Map(activeMemories.map(memory => [typedCanonicalKey(memory.type, memory.text), memory]));
|
||||
const matches: ReabsorbedMatch[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const record of records) {
|
||||
const key = typedCanonicalKey(record.type, record.text);
|
||||
const activeMemory = activeByKey.get(key);
|
||||
if (!activeMemory || seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
matches.push({ key, record, activeMemory });
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
function typedCanonicalKey(type: LongTermType | string, text: string): string {
|
||||
return `${type}:${canonicalMemoryText(text)}`;
|
||||
}
|
||||
|
||||
function buildReinforcementFacts(events: EvidenceEventV1[]): ReviewBoardReport["facts"]["systemMechanisms"]["reinforcementRules"] {
|
||||
const attempts = events.filter(isReinforcementEvent);
|
||||
const windowBlocked = attempts.filter(event => event.reasonCodes.includes("reinforcement_window_blocked"));
|
||||
const grouped = new Map<string, { memoryId: string; count: number; refs: Set<string>; rawReasonCodes: Set<string>; eventIds: string[] }>();
|
||||
for (const event of windowBlocked) {
|
||||
const memoryId = event.memory?.memoryId ?? "unknown";
|
||||
const current = grouped.get(memoryId) ?? { memoryId, count: 0, refs: new Set<string>(), rawReasonCodes: new Set<string>(), eventIds: [] };
|
||||
current.count += 1;
|
||||
current.eventIds.push(event.eventId);
|
||||
const ref = typeof event.details?.ref === "string" ? event.details.ref : undefined;
|
||||
if (ref) current.refs.add(ref);
|
||||
for (const reason of event.reasonCodes) current.rawReasonCodes.add(reason);
|
||||
grouped.set(memoryId, current);
|
||||
}
|
||||
|
||||
return {
|
||||
reinforceEvents: attempts.length,
|
||||
reinforcedEvents: attempts.filter(event => event.outcome === "reinforced" || event.type === "memory_reinforced" && event.outcome !== "rejected").length,
|
||||
rejectedOrBlockedEvents: attempts.filter(event => event.outcome === "rejected" || event.reasonCodes.includes("reinforcement_window_blocked")).length,
|
||||
windowBlockedEvents: windowBlocked.length,
|
||||
windowBlockRate: attempts.length === 0 ? 0 : windowBlocked.length / attempts.length,
|
||||
repeatedBlocksByMemory: [...grouped.values()]
|
||||
.filter(group => group.count > 1)
|
||||
.sort((a, b) => b.count - a.count || a.memoryId.localeCompare(b.memoryId))
|
||||
.map(group => ({ memoryId: group.memoryId, count: group.count, refs: [...group.refs].sort(), rawReasonCodes: [...group.rawReasonCodes].sort() })),
|
||||
malformedCommandEvents: events.filter(isMalformedCommandEvent).length,
|
||||
};
|
||||
}
|
||||
|
||||
function isReinforcementEvent(event: EvidenceEventV1): boolean {
|
||||
const type = String(event.type);
|
||||
return type === "memory_reinforced"
|
||||
|| type === "reinforce_memory"
|
||||
|| type === "reinforced"
|
||||
|| event.phase === "reinforcement";
|
||||
}
|
||||
|
||||
function isMalformedCommandEvent(event: EvidenceEventV1): boolean {
|
||||
return event.type === "extraction_candidate_rejected"
|
||||
&& event.reasonCodes.some(reason => MALFORMED_COMMAND_REASON_CODES.has(reason));
|
||||
}
|
||||
|
||||
function buildEvictionFacts(
|
||||
model: MemoryInspectionReadModel,
|
||||
activeMemories: LongTermMemoryEntry[],
|
||||
typeCounts: Record<string, number>,
|
||||
typeCaps: Record<string, number>,
|
||||
disappearances: ReturnType<typeof disappearanceRows>,
|
||||
generatedAt: string,
|
||||
): ReviewBoardReport["facts"]["systemMechanisms"]["evictionAndCaps"] {
|
||||
const capacityEvents = model.evidenceEvents.filter(event => event.type === "memory_removed_capacity");
|
||||
const recentCapacityEvents = capacityEvents.filter(event => isWithinDaysOf(event.createdAt, generatedAt, RECENT_EVICTION_DAYS));
|
||||
const fullCaps = [
|
||||
...(activeMemories.length >= model.store.limits.maxEntries ? ["global"] : []),
|
||||
...TYPES.filter(type => (typeCounts[type] ?? 0) >= (typeCaps[type] ?? Number.POSITIVE_INFINITY)),
|
||||
];
|
||||
|
||||
return {
|
||||
activeMemories: activeMemories.length,
|
||||
maxEntries: model.store.limits.maxEntries,
|
||||
renderedMemories: model.snapshot.retention.rendered.length,
|
||||
typeCounts,
|
||||
typeCaps,
|
||||
fullCaps,
|
||||
missingEvidenceOnly: disappearances.length,
|
||||
unknownDisappearances: disappearances.filter(row => row.classification === "historical_absent_unknown_reason").length,
|
||||
removedByCapacity: capacityEvents.length,
|
||||
removedByGlobalCap: capacityEvents.filter(event => event.reasonCodes.includes("global_cap")).length,
|
||||
removedByTypeCap: capacityEvents.filter(event => event.reasonCodes.includes("type_cap")).length,
|
||||
recentEvictionsByType: objectFromCounts(countBy(recentCapacityEvents.map(event => event.memory?.type ?? "unknown"))),
|
||||
recentEvictedContentShown: recentCapacityEvents.length,
|
||||
};
|
||||
}
|
||||
|
||||
function isWithinDaysOf(iso: string, referenceIso: string, days: number): boolean {
|
||||
const time = new Date(iso).getTime();
|
||||
const reference = new Date(referenceIso).getTime();
|
||||
return Number.isFinite(time) && Number.isFinite(reference) && time >= reference - days * 86_400_000;
|
||||
}
|
||||
|
||||
function buildIdentityFacts(model: MemoryInspectionReadModel, activeMemories: LongTermMemoryEntry[]): ReviewBoardReport["facts"]["systemMechanisms"]["identityAndDedup"] {
|
||||
const replacementEvents = model.evidenceEvents.filter(event => event.type === "memory_replaced_numbered_ref");
|
||||
return {
|
||||
replacementEvents: replacementEvents.length,
|
||||
sameTypeReplacementEvents: replacementEvents.filter(isSameTypeReplacement).length,
|
||||
crossTypeReplacementEvents: replacementEvents.filter(isCrossTypeReplacement).length,
|
||||
supersededEntries: model.store.entries.filter(entry => entry.status === "superseded").length,
|
||||
duplicateTextOrIdentityGroups: duplicateGroups(activeMemories, model.evidenceEvents).length,
|
||||
};
|
||||
}
|
||||
|
||||
function isSameTypeReplacement(event: EvidenceEventV1): boolean {
|
||||
if (event.reasonCodes.includes("same_type_replace")) return true;
|
||||
const types = relationTypes(event);
|
||||
return types.length >= 2 && new Set(types).size === 1;
|
||||
}
|
||||
|
||||
function isCrossTypeReplacement(event: EvidenceEventV1): boolean {
|
||||
if (event.reasonCodes.includes("cross_type_replace")) return true;
|
||||
const types = relationTypes(event);
|
||||
return types.length >= 2 && new Set(types).size > 1;
|
||||
}
|
||||
|
||||
function relationTypes(event: EvidenceEventV1): string[] {
|
||||
return uniqueStrings(event.relations?.map(relation => relation.memory?.type ?? "") ?? []);
|
||||
}
|
||||
|
||||
function duplicateGroups(activeMemories: LongTermMemoryEntry[], events: EvidenceEventV1[]): Array<{ id: string; memoryIds: string[]; basis: string }> {
|
||||
const groups: Array<{ id: string; memoryIds: string[]; basis: string }> = [];
|
||||
const byText = groupBy(activeMemories, memory => typedCanonicalKey(memory.type, memory.text));
|
||||
for (const [key, memories] of byText.entries()) {
|
||||
if (memories.length > 1) groups.push({ id: `text:${hashText(key)}`, memoryIds: memories.map(memory => memory.id).sort(), basis: "exact typed canonical text" });
|
||||
}
|
||||
const identityRefs = events
|
||||
.map(event => event.memory)
|
||||
.filter((memory): memory is NonNullable<EvidenceEventV1["memory"]> => Boolean(memory?.identityKeyHash && memory.memoryId));
|
||||
const byIdentity = groupBy(identityRefs, memory => String(memory.identityKeyHash));
|
||||
for (const [key, refs] of byIdentity.entries()) {
|
||||
const ids = uniqueStrings(refs.map(ref => ref.memoryId ?? "")).sort();
|
||||
if (ids.length > 1) groups.push({ id: `identity:${hashText(key)}`, memoryIds: ids, basis: "shared evidence identity hash" });
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
function groupBy<T>(items: T[], keyFor: (item: T) => string): Map<string, T[]> {
|
||||
const grouped = new Map<string, T[]>();
|
||||
for (const item of items) {
|
||||
const key = keyFor(item);
|
||||
grouped.set(key, [...(grouped.get(key) ?? []), item]);
|
||||
}
|
||||
return grouped;
|
||||
}
|
||||
|
||||
function buildMemoryContentFacts(
|
||||
model: MemoryInspectionReadModel,
|
||||
activeMemories: LongTermMemoryEntry[],
|
||||
typeCounts: Record<string, number>,
|
||||
typeCaps: Record<string, number>,
|
||||
raw: boolean,
|
||||
): ReviewBoardReport["facts"]["memoryContent"] {
|
||||
const evidenceCovered = activeMemories.filter(memory => (model.evidenceByMemoryId.get(memory.id) ?? []).length > 0).length;
|
||||
const weakest = model.snapshot.retention.sorted.slice(-5).reverse();
|
||||
const strongest = model.snapshot.retention.sorted.slice(0, 5);
|
||||
return {
|
||||
activeMemories: activeMemories.length,
|
||||
renderedMemories: model.snapshot.retention.rendered.length,
|
||||
evidenceCoverage: { covered: evidenceCovered, total: activeMemories.length },
|
||||
typeCounts,
|
||||
typeCaps,
|
||||
weakestActiveMemories: weakest.map(item => retentionPreview(item.entry, item.strength, raw)),
|
||||
strongestActiveMemories: strongest.map(item => retentionPreview(item.entry, item.strength, raw)),
|
||||
};
|
||||
}
|
||||
|
||||
function retentionPreview(entry: LongTermMemoryEntry, strength: number | undefined, raw: boolean): { id: string; type: string; strength?: number; textPreview: string } {
|
||||
return { id: entry.id, type: entry.type, strength, textPreview: truncate(cleanText(entry.text, raw), 120) };
|
||||
}
|
||||
|
||||
function buildActiveMemoryDisplay(
|
||||
model: MemoryInspectionReadModel,
|
||||
activeMemories: LongTermMemoryEntry[],
|
||||
reabsorbedKeys: Set<string>,
|
||||
activeMemoryByKey: Map<string, LongTermMemoryEntry>,
|
||||
provenanceInputs: ProvenanceContextInputs,
|
||||
raw: boolean,
|
||||
verbose: boolean,
|
||||
): ReviewBoardReport["activeMemoryDisplay"] {
|
||||
const mode: "all" | "sample" = activeMemories.length <= ACTIVE_MEMORY_FULL_TEXT_THRESHOLD || verbose ? "all" : "sample";
|
||||
const shownMemories = mode === "all" ? activeMemories : activeMemories.slice(0, ACTIVE_MEMORY_FULL_TEXT_THRESHOLD);
|
||||
const items = shownMemories.map(memory => {
|
||||
const events = model.evidenceByMemoryId.get(memory.id) ?? [];
|
||||
const key = typedCanonicalKey(memory.type, memory.text);
|
||||
const item: ReviewBoardActiveMemory = {
|
||||
id: memory.id,
|
||||
type: memory.type,
|
||||
source: memory.source,
|
||||
status: memory.status,
|
||||
strength: model.snapshot.retention.sorted.find(candidate => candidate.entry.id === memory.id)?.strength,
|
||||
text: cleanText(memory.text, raw),
|
||||
evidence: {
|
||||
eventCount: events.length,
|
||||
eventIds: events.map(event => event.eventId),
|
||||
rawReasonCodes: uniqueStrings(events.flatMap(event => event.reasonCodes)).sort(),
|
||||
},
|
||||
heuristicFlags: activeMemoryFlags(memory, events),
|
||||
reviewQuestions: memoryContentQuestions(),
|
||||
};
|
||||
if (reabsorbedKeys.has(key) && activeMemoryByKey.get(key)?.id === memory.id) {
|
||||
item.provenance = classifyProvenance({ reabsorbed: true }, provenanceInputs);
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return { threshold: ACTIVE_MEMORY_FULL_TEXT_THRESHOLD, mode, shown: items.length, total: activeMemories.length, items };
|
||||
}
|
||||
|
||||
function activeMemoryFlags(memory: LongTermMemoryEntry, events: EvidenceEventV1[]): HeuristicFlag[] {
|
||||
const flags: HeuristicFlag[] = [];
|
||||
if (events.length === 0) {
|
||||
flags.push(flag("no_evidence", "No linked evidence events", `memory ${memory.id} has no lifecycle evidence events`));
|
||||
}
|
||||
if ((memory.supersedes ?? []).length > 0) {
|
||||
flags.push(flag("supersedes_other_memory", "Supersession relationship present", `memory ${memory.id} supersedes ${memory.supersedes?.length ?? 0} prior entries`));
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
function buildSystemCandidateDisplay(
|
||||
candidateInputs: Record<string, DatedCandidateInput[]>,
|
||||
showAll: boolean,
|
||||
): { candidates: ReviewBoardCandidate[]; limited: boolean; summary: NonNullable<ReviewBoardReport["provenanceContext"]["candidateDisplay"]> } {
|
||||
const candidates: ReviewBoardCandidate[] = [];
|
||||
const byMechanism: Record<string, { shown: number; total: number }> = {};
|
||||
let shown = 0;
|
||||
let total = 0;
|
||||
let limited = false;
|
||||
|
||||
for (const [mechanism, inputs] of Object.entries(candidateInputs)) {
|
||||
const selected = selectRepresentative(inputs, showAll);
|
||||
candidates.push(...selected.map(item => item.candidate));
|
||||
byMechanism[mechanism] = { shown: selected.length, total: inputs.length };
|
||||
shown += selected.length;
|
||||
total += inputs.length;
|
||||
if (selected.length < inputs.length) limited = true;
|
||||
}
|
||||
|
||||
return { candidates, limited, summary: { shown, total, byMechanism } };
|
||||
}
|
||||
|
||||
function buildRejectionCandidates(records: NormalizedRejection[], context: ProvenanceContextInputs, raw: boolean): DatedCandidateInput[] {
|
||||
const candidateRecords = records
|
||||
.filter(record => record.reasons.includes("bad_decision"))
|
||||
.map(record => ({ record, label: neutralRejectionLabel(record) }))
|
||||
.filter(item => item.label === "architecture_like_rejected_candidate" || item.label === "ambiguous_rejected_candidate")
|
||||
.sort((a, b) =>
|
||||
timestampValue(b.record.timestamp) - timestampValue(a.record.timestamp)
|
||||
|| a.record.type.localeCompare(b.record.type)
|
||||
|| a.record.text.localeCompare(b.record.text)
|
||||
);
|
||||
|
||||
const candidates = uniqueByCanonicalText(candidateRecords.map(item => item.record))
|
||||
.map(record => ({ record, label: neutralRejectionLabel(record) }))
|
||||
.map(({ record, label }) => ({
|
||||
candidate: candidate({
|
||||
concernKind: "system_mechanism",
|
||||
mechanism: "rejection_filter",
|
||||
source: "rejection_rule_evidence",
|
||||
id: `rejection:${record.timestamp || "unknown"}:${hashText(record.type + record.text)}`,
|
||||
facts: { type: record.type, neutralLabel: label, timestamp: record.timestamp || undefined, origin: record.origin },
|
||||
evidence: { rawReasonCodes: record.reasons, textPreview: truncate(cleanText(record.text, raw), 120), textAvailable: true },
|
||||
provenance: classifyProvenance({ rejection: record }, context),
|
||||
heuristicFlags: [flag(label, label.replaceAll("_", " "), "existing rejection summary grouped this record for human review")],
|
||||
reviewQuestions: ["Are rejection filters over-filtering durable decisions or under-filtering non-durable candidates for this workspace?"],
|
||||
nextCommands: ["memory-diag rejected --verbose"],
|
||||
}),
|
||||
timestamp: record.timestamp,
|
||||
tieId: record.timestamp || "unknown",
|
||||
textHash: hashText(record.text),
|
||||
}));
|
||||
return candidates;
|
||||
}
|
||||
|
||||
function neutralRejectionLabel(record: NormalizedRejection): "architecture_like_rejected_candidate" | "ambiguous_rejected_candidate" | "status_or_hard_reason_evidence" {
|
||||
const hardReasons = record.reasons.filter(reason => HARD_OR_NOISE_REASON_CODES.has(reason));
|
||||
const statusLike = /\b(?:implemented|added|updated|fixed|completed|reviewed|tests?|CI|commit|wave|phase|task|session)\b/i.test(record.text);
|
||||
const architectureLike = /\b(?:architecture|retention|migration|schema|policy|model|dedup|identity|parser|formatter|diagnostic|evidence|cap|window|api|contract)\b/i.test(record.text);
|
||||
if (architectureLike && hardReasons.length === 0 && !statusLike) return "architecture_like_rejected_candidate";
|
||||
if (hardReasons.length > 0 || statusLike) return "status_or_hard_reason_evidence";
|
||||
return "ambiguous_rejected_candidate";
|
||||
}
|
||||
|
||||
function buildReabsorptionCandidates(matches: ReabsorbedMatch[], context: ProvenanceContextInputs, raw: boolean): DatedCandidateInput[] {
|
||||
const candidates = matches.map(match => ({
|
||||
candidate: candidate({
|
||||
concernKind: "system_mechanism",
|
||||
mechanism: "rejection_filter",
|
||||
source: "reabsorption_evidence",
|
||||
id: `reabsorbed:${match.activeMemory.id}:${hashText(match.key)}`,
|
||||
facts: { activeMemoryId: match.activeMemory.id, type: match.activeMemory.type, rejectedAt: match.record.timestamp || undefined },
|
||||
evidence: { rawReasonCodes: match.record.reasons, textPreview: truncate(cleanText(match.record.text, raw), 120), textAvailable: true },
|
||||
provenance: classifyProvenance({ rejection: match.record, reabsorbed: true }, context),
|
||||
heuristicFlags: [flag("reabsorbed_rejected_text", "Rejected text appears in active memory", "typed canonical text is present in both rejection records and active memory")],
|
||||
reviewQuestions: ["Did later context make this rejected candidate worth reviewing for filter calibration?"],
|
||||
nextCommands: ["memory-diag rejected --verbose", `memory-diag explain ${match.activeMemory.id}`],
|
||||
}),
|
||||
timestamp: match.record.timestamp,
|
||||
tieId: match.activeMemory.id,
|
||||
textHash: hashText(match.key),
|
||||
}));
|
||||
return candidates;
|
||||
}
|
||||
|
||||
function buildReinforcementCandidates(events: EvidenceEventV1[], context: ProvenanceContextInputs, raw: boolean): DatedCandidateInput[] {
|
||||
const blocked = events.filter(event => isReinforcementEvent(event) && event.reasonCodes.includes("reinforcement_window_blocked"));
|
||||
const grouped = [...groupBy(blocked, event => event.memory?.memoryId ?? "unknown").entries()].map(([memoryId, group]) => ({ memoryId, group }));
|
||||
const repeated = grouped.filter(item => item.group.length > 1).map(item => {
|
||||
const latest = newestEvent(item.group);
|
||||
return {
|
||||
candidate: candidate({
|
||||
concernKind: "system_mechanism",
|
||||
mechanism: "reinforcement_rule",
|
||||
source: "numbered_command_evidence",
|
||||
id: `reinforcement:${item.memoryId}:${item.group.length}`,
|
||||
facts: { memoryId: item.memoryId, blockCount: item.group.length, refs: uniqueStrings(item.group.map(event => String(event.details?.ref ?? "")).filter(Boolean)).sort() },
|
||||
evidence: { eventIds: item.group.map(event => event.eventId), rawReasonCodes: uniqueStrings(item.group.flatMap(event => event.reasonCodes)).sort(), textAvailable: false },
|
||||
provenance: classifyProvenance({ event: latest }, context),
|
||||
heuristicFlags: [flag("repeated_reinforcement_window_block", "Repeated reinforcement window block", `${item.group.length} reinforcement attempts were blocked for memory ${item.memoryId}`)],
|
||||
reviewQuestions: ["Is the day-based reinforcement window too restrictive when the same memory receives repeated reinforce intent?"],
|
||||
nextCommands: ["memory-diag commands --verbose", `memory-diag explain ${item.memoryId}`],
|
||||
}),
|
||||
timestamp: latest?.createdAt,
|
||||
tieId: item.memoryId,
|
||||
textHash: item.memoryId,
|
||||
};
|
||||
});
|
||||
const malformed = events.filter(isMalformedCommandEvent).map(event => ({
|
||||
candidate: candidate({
|
||||
concernKind: "system_mechanism",
|
||||
mechanism: "reinforcement_rule",
|
||||
source: "numbered_command_evidence",
|
||||
id: `malformed-command:${event.eventId}`,
|
||||
facts: { eventType: event.type, createdAt: event.createdAt },
|
||||
evidence: { eventIds: [event.eventId], rawReasonCodes: event.reasonCodes, textPreview: event.textPreview ? truncate(cleanText(event.textPreview, raw), 120) : undefined, textAvailable: Boolean(event.textPreview) },
|
||||
provenance: classifyProvenance({ event }, context),
|
||||
heuristicFlags: [flag("malformed_numbered_command", "Malformed numbered-memory command evidence", "command parser rejected a memory command form")],
|
||||
reviewQuestions: ["Do numbered-memory command rules match how agents actually express reinforcement intent?"],
|
||||
nextCommands: ["memory-diag commands --verbose"],
|
||||
}),
|
||||
timestamp: event.createdAt,
|
||||
tieId: event.eventId,
|
||||
textHash: event.eventId,
|
||||
}));
|
||||
return [...repeated, ...malformed];
|
||||
}
|
||||
|
||||
function buildEvictionCandidates(
|
||||
disappearances: ReturnType<typeof disappearanceRows>,
|
||||
events: EvidenceEventV1[],
|
||||
context: ProvenanceContextInputs,
|
||||
raw: boolean,
|
||||
generatedAt: string,
|
||||
): DatedCandidateInput[] {
|
||||
const recentCapacity = events.filter(event => event.type === "memory_removed_capacity" && isWithinDaysOf(event.createdAt, generatedAt, RECENT_EVICTION_DAYS));
|
||||
const capacityCandidates = recentCapacity.map(event => ({
|
||||
candidate: candidate({
|
||||
concernKind: "system_mechanism",
|
||||
mechanism: "eviction_cap",
|
||||
source: "eviction_cap_evidence",
|
||||
id: `eviction:${event.eventId}`,
|
||||
facts: { ...(safeDetails(event.details, raw) ?? {}), createdAt: event.createdAt, memoryId: event.memory?.memoryId, type: event.memory?.type },
|
||||
evidence: { eventIds: [event.eventId], rawReasonCodes: event.reasonCodes, textPreview: event.textPreview ? truncate(cleanText(event.textPreview, raw), 120) : undefined, textAvailable: Boolean(event.textPreview) },
|
||||
provenance: classifyProvenance({ event }, context),
|
||||
heuristicFlags: [flag("recent_capacity_removal", "Recent capacity-removal evidence", "memory_removed_capacity appeared within the recent eviction window")],
|
||||
reviewQuestions: ["Are eviction and cap rules preserving the intended memories under pressure?"],
|
||||
nextCommands: ["memory-diag missing --verbose --explain"],
|
||||
}),
|
||||
timestamp: event.createdAt,
|
||||
tieId: event.eventId,
|
||||
textHash: event.textPreview ? hashText(event.textPreview) : event.eventId,
|
||||
}));
|
||||
|
||||
const unknownCandidates = disappearances
|
||||
.filter(row => row.classification === "historical_absent_unknown_reason")
|
||||
.map(row => {
|
||||
const latest = newestEvent(row.events);
|
||||
return {
|
||||
candidate: candidate({
|
||||
concernKind: "system_mechanism",
|
||||
mechanism: "eviction_cap",
|
||||
source: "missing_evidence",
|
||||
id: `missing:${row.id}`,
|
||||
facts: { memoryId: row.id, terminalType: row.terminalType, eventCount: row.events.length },
|
||||
evidence: { eventIds: row.events.map(event => event.eventId), rawReasonCodes: uniqueStrings(row.events.flatMap(event => event.reasonCodes)).sort(), textPreview: latest?.textPreview ? truncate(cleanText(latest.textPreview, raw), 120) : undefined, textAvailable: Boolean(latest?.textPreview) },
|
||||
provenance: classifyProvenance({ event: latest }, context),
|
||||
heuristicFlags: [flag("unknown_disappearance", "Evidence-only disappearance without terminal removal evidence", `memory ${row.id} has evidence but is not active`)],
|
||||
reviewQuestions: ["Does missing-memory evidence indicate a cap, retention, or recording rule needs review?"],
|
||||
nextCommands: ["memory-diag missing --verbose --explain"],
|
||||
}),
|
||||
timestamp: latest?.createdAt,
|
||||
tieId: row.id,
|
||||
textHash: row.id,
|
||||
};
|
||||
});
|
||||
return [...capacityCandidates, ...unknownCandidates];
|
||||
}
|
||||
|
||||
function safeDetails(details: EvidenceEventV1["details"], raw: boolean): Record<string, unknown> | undefined {
|
||||
if (!details) return undefined;
|
||||
return Object.fromEntries(Object.entries(details).map(([key, value]) => [key, typeof value === "string" ? cleanText(value, raw) : value]));
|
||||
}
|
||||
|
||||
function buildIdentityCandidates(model: MemoryInspectionReadModel, activeMemories: LongTermMemoryEntry[], context: ProvenanceContextInputs, raw: boolean): DatedCandidateInput[] {
|
||||
const replacementCandidates = model.evidenceEvents
|
||||
.filter(event => event.type === "memory_replaced_numbered_ref" || event.type === "promotion_superseded")
|
||||
.map(event => ({
|
||||
candidate: candidate({
|
||||
concernKind: "system_mechanism",
|
||||
mechanism: "identity_dedup",
|
||||
source: "identity_dedup_evidence",
|
||||
id: `identity-event:${event.eventId}`,
|
||||
facts: { eventType: event.type, memoryId: event.memory?.memoryId, relationRoles: event.relations?.map(relation => relation.role) ?? [] },
|
||||
evidence: { eventIds: [event.eventId], rawReasonCodes: event.reasonCodes, textPreview: event.textPreview ? truncate(cleanText(event.textPreview, raw), 120) : undefined, textAvailable: Boolean(event.textPreview) },
|
||||
provenance: classifyProvenance({ event }, context),
|
||||
heuristicFlags: [flag("replacement_or_supersession", "Replacement or supersession evidence", `${event.type} records identity/dedup behavior`)],
|
||||
reviewQuestions: ["Are identity and dedup rules preserving separate memories when expected to remain distinct?"],
|
||||
nextCommands: ["memory-diag commands --verbose", event.memory?.memoryId ? `memory-diag explain ${event.memory.memoryId}` : "memory-diag missing --verbose --explain"],
|
||||
}),
|
||||
timestamp: event.createdAt,
|
||||
tieId: event.eventId,
|
||||
textHash: event.eventId,
|
||||
}));
|
||||
const duplicateCandidates = duplicateGroups(activeMemories, model.evidenceEvents).map(group => ({
|
||||
candidate: candidate({
|
||||
concernKind: "system_mechanism",
|
||||
mechanism: "identity_dedup",
|
||||
source: "identity_dedup_evidence",
|
||||
id: `duplicate:${group.id}`,
|
||||
facts: { memoryIds: group.memoryIds, basis: group.basis },
|
||||
evidence: { eventIds: group.memoryIds.flatMap(id => (model.evidenceByMemoryId.get(id) ?? []).map(event => event.eventId)), rawReasonCodes: [], textAvailable: false },
|
||||
provenance: classifyProvenance({}, context),
|
||||
heuristicFlags: [flag("exact_duplicate_group", "Exact duplicate text or identity group", `${group.memoryIds.length} memories share ${group.basis}`)],
|
||||
reviewQuestions: ["Are exact duplicate text or identity groups expected for this workspace?"],
|
||||
nextCommands: group.memoryIds.map(id => `memory-diag explain ${id}`).slice(0, 3),
|
||||
}),
|
||||
timestamp: undefined,
|
||||
tieId: group.id,
|
||||
textHash: group.id,
|
||||
}));
|
||||
return [...replacementCandidates, ...duplicateCandidates];
|
||||
}
|
||||
|
||||
function buildMemoryContentCandidates(model: MemoryInspectionReadModel, activeMemories: LongTermMemoryEntry[], raw: boolean): ReviewBoardCandidate[] {
|
||||
return activeMemories.slice(0, ACTIVE_MEMORY_FULL_TEXT_THRESHOLD).map(memory => {
|
||||
const events = model.evidenceByMemoryId.get(memory.id) ?? [];
|
||||
return candidate({
|
||||
concernKind: "memory_content",
|
||||
mechanism: "retention_rendering",
|
||||
source: "active_memory",
|
||||
id: `active:${memory.id}`,
|
||||
facts: { id: memory.id, type: memory.type, source: memory.source, status: memory.status },
|
||||
evidence: { eventIds: events.map(event => event.eventId), rawReasonCodes: uniqueStrings(events.flatMap(event => event.reasonCodes)).sort(), textPreview: truncate(cleanText(memory.text, raw), 120), textAvailable: true },
|
||||
heuristicFlags: activeMemoryFlags(memory, events),
|
||||
reviewQuestions: memoryContentQuestions(),
|
||||
nextCommands: [`memory-diag explain ${memory.id}`],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function candidate(input: ReviewBoardCandidate): ReviewBoardCandidate {
|
||||
return input;
|
||||
}
|
||||
|
||||
function selectRepresentative(items: DatedCandidateInput[], verbose: boolean): DatedCandidateInput[] {
|
||||
const sorted = [...items].sort((a, b) => {
|
||||
const timeDelta = timestampValue(b.timestamp) - timestampValue(a.timestamp);
|
||||
if (timeDelta !== 0) return timeDelta;
|
||||
const idDelta = a.tieId.localeCompare(b.tieId);
|
||||
if (idDelta !== 0) return idDelta;
|
||||
return (a.textHash ?? "").localeCompare(b.textHash ?? "");
|
||||
});
|
||||
return verbose ? sorted : sorted.slice(0, REPRESENTATIVE_CANDIDATE_LIMIT);
|
||||
}
|
||||
|
||||
function timestampValue(iso: string | undefined): number {
|
||||
const time = iso ? new Date(iso).getTime() : 0;
|
||||
return Number.isFinite(time) ? time : 0;
|
||||
}
|
||||
|
||||
function newestEvent(events: EvidenceEventV1[]): EvidenceEventV1 | undefined {
|
||||
return [...events].sort((a, b) => timestampValue(b.createdAt) - timestampValue(a.createdAt) || a.eventId.localeCompare(b.eventId))[0];
|
||||
}
|
||||
|
||||
function flag(id: string, label: string, evidence: string): HeuristicFlag {
|
||||
return {
|
||||
id,
|
||||
label,
|
||||
evidence,
|
||||
caveat: "This flag is a prompt for review, not a conclusion.",
|
||||
};
|
||||
}
|
||||
|
||||
function systemMechanismQuestions(): string[] {
|
||||
return [
|
||||
"Are rejection rules over-filtering durable decisions or under-filtering non-durable candidates for this workspace?",
|
||||
"Is the reinforcement window too restrictive when the same memory receives repeated reinforce intent?",
|
||||
"Are eviction and cap rules preserving target memories under full caps?",
|
||||
"Are identity and dedup rules collapsing items expected to remain separate, or not collapsing equivalent items?",
|
||||
];
|
||||
}
|
||||
|
||||
function memoryContentQuestions(): string[] {
|
||||
return [
|
||||
"Does this memory remain durable and actionable for future sessions?",
|
||||
"Is this memory non-stale, specific, and supported by available evidence?",
|
||||
"Does this memory overlap with other active memories in a way a reviewer should consider?",
|
||||
];
|
||||
}
|
||||
|
||||
function nextCommands(): string[] {
|
||||
return [
|
||||
"memory-diag rejected --verbose",
|
||||
"memory-diag missing --verbose --explain",
|
||||
"memory-diag commands --verbose",
|
||||
"memory-diag explain <memory-id>",
|
||||
];
|
||||
}
|
||||
|
||||
function countProvenanceClassifications(candidates: ReviewBoardCandidate[]): Record<ProvenanceClassification, number> {
|
||||
const counts = Object.fromEntries(PROVENANCE_CLASSIFICATIONS.map(classification => [classification, 0])) as Record<ProvenanceClassification, number>;
|
||||
for (const provenanceItem of candidates.map(candidate => candidate.provenance)) {
|
||||
if (!provenanceItem) continue;
|
||||
counts[provenanceItem.classification] += 1;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
function hashText(text: string): string {
|
||||
return createHash("sha256").update(text).digest("hex").slice(0, 12);
|
||||
}
|
||||
@@ -9,9 +9,10 @@ test("help returns usage without exposing hidden or removed commands", () => {
|
||||
assert.equal("help" in parsed && parsed.help, true);
|
||||
assert.match(parsed.usage, /Usage:/);
|
||||
assert.match(parsed.usage, /memory-diag \[status\]/);
|
||||
assert.match(parsed.usage, /memory-diag quality/);
|
||||
assert.match(parsed.usage, /memory-diag commands/);
|
||||
assert.match(parsed.usage, /memory-diag revert/);
|
||||
for (const command of ["health", "quality", "rejections", "disappearances", "trace", "coverage", "audit"]) {
|
||||
for (const command of ["health", "rejections", "disappearances", "trace", "coverage", "audit"]) {
|
||||
assert.doesNotMatch(parsed.usage, new RegExp(command));
|
||||
}
|
||||
});
|
||||
@@ -35,7 +36,7 @@ test("unknown command returns usage error", () => {
|
||||
});
|
||||
|
||||
test("removed legacy aliases are ordinary unknown subcommands", () => {
|
||||
for (const command of ["health", "quality", "rejections", "disappearances", "trace"]) {
|
||||
for (const command of ["health", "rejections", "disappearances", "trace"]) {
|
||||
const parsed = parseArgs([command]);
|
||||
|
||||
assert.equal(parsed.ok, false, command);
|
||||
@@ -45,6 +46,41 @@ test("removed legacy aliases are ordinary unknown subcommands", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("quality accepts read-only workspace json and display flags", () => {
|
||||
const parsed = parseArgs(["quality", "--workspace", "/tmp/workspace", "--json", "--verbose", "--raw", "--no-emoji"]);
|
||||
|
||||
assert.equal(parsed.ok, true);
|
||||
assert.equal("command" in parsed && parsed.command, "quality");
|
||||
assert.equal("options" in parsed && parsed.options.workspace, "/tmp/workspace");
|
||||
assert.equal("options" in parsed && parsed.options.json, true);
|
||||
assert.equal("options" in parsed && parsed.options.verbose, true);
|
||||
assert.equal("options" in parsed && parsed.options.raw, true);
|
||||
assert.equal("options" in parsed && parsed.options.noEmoji, true);
|
||||
});
|
||||
|
||||
test("quality rejects mutation filter and drill-down flags", () => {
|
||||
const cases: Array<{ args: string[]; message: string }> = [
|
||||
{ args: ["quality", "--all"], message: "quality does not accept --all" },
|
||||
{ args: ["quality", "--apply"], message: "quality does not accept --apply" },
|
||||
{ args: ["quality", "--memory", "mem-1"], message: "quality does not accept --memory" },
|
||||
{ args: ["quality", "--event", "evt-1"], message: "quality does not accept --event" },
|
||||
{ args: ["quality", "--reason", "bad_decision"], message: "quality does not accept rejection filters" },
|
||||
{ args: ["quality", "--since", "7d"], message: "quality does not accept rejection filters" },
|
||||
{ args: ["quality", "--soft-only"], message: "quality does not accept rejection filters" },
|
||||
{ args: ["quality", "--trigger-only"], message: "quality does not accept rejection filters" },
|
||||
{ args: ["quality", "--include-historical"], message: "quality does not accept --include-historical" },
|
||||
{ args: ["quality", "--explain"], message: "quality does not accept --explain" },
|
||||
];
|
||||
|
||||
for (const item of cases) {
|
||||
const parsed = parseArgs(item.args);
|
||||
assert.equal(parsed.ok, false, item.args.join(" "));
|
||||
if (parsed.ok) continue;
|
||||
assert.equal(parsed.message, item.message);
|
||||
assert.match(parsed.usage, /Usage:/);
|
||||
}
|
||||
});
|
||||
|
||||
test("hidden maintainer commands are accepted with neutral notices", () => {
|
||||
const coverage = parseArgs(["coverage"]);
|
||||
assert.equal(coverage.ok, true);
|
||||
|
||||
@@ -0,0 +1,628 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdtempSync } from "node:fs";
|
||||
import { mkdir, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { appendEvidenceEvents, type EvidenceEventInput, type EvidenceEventType, type EvidenceEventV1, type EvidenceOutcome, type EvidencePhase } from "../src/evidence-log.ts";
|
||||
import { LONG_TERM_LIMITS, type LongTermMemoryEntry, type WorkspaceMemoryStore } from "../src/types.ts";
|
||||
import { workspaceKey, workspaceMemoryPath } from "../src/paths.ts";
|
||||
import { buildQualityJSON, formatQualityReviewBoard } from "../scripts/memory-diag/formatters/quality.ts";
|
||||
import { groupEvidenceByMemoryId } from "../scripts/memory-diag/evidence-model.ts";
|
||||
import { buildQualityReviewBoard, type ProvenanceClassification, type ReviewBoardReport } from "../scripts/memory-diag/quality-review-model.ts";
|
||||
import { retentionCandidatesForDiag } from "../scripts/memory-diag/retention-model.ts";
|
||||
import type { MemoryInspectionReadModel, NormalizedRejection, WorkspaceDiagSnapshot } from "../scripts/memory-diag/types.ts";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const generatedAt = "2026-05-11T12:00:00.000Z";
|
||||
|
||||
async function runMemoryDiag(args: string[]): Promise<string> {
|
||||
const { stdout } = await execFileAsync(process.execPath, [
|
||||
"--experimental-strip-types",
|
||||
"scripts/memory-diag.ts",
|
||||
...args,
|
||||
], { cwd: repoRoot });
|
||||
|
||||
return stdout.trim();
|
||||
}
|
||||
|
||||
test("quality command returns review board skeleton for empty workspace", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "opencode-memory-diag-quality-empty-"));
|
||||
try {
|
||||
const stdout = await runMemoryDiag(["quality", "--workspace", root]);
|
||||
|
||||
assert.match(stdout, /Memory quality review board/);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("quality formatter returns required human sections in review-board order", () => {
|
||||
const model = inspectionModel([
|
||||
entry("mem-section", "Durable formatter section memory", "decision"),
|
||||
], [
|
||||
event("evt-section", { type: "render_selected", phase: "render", outcome: "rendered", memory: { memoryId: "mem-section", type: "decision", source: "compaction" } }),
|
||||
]);
|
||||
const report = buildQualityReviewBoard(model, {}, generatedAt);
|
||||
|
||||
const output = formatQualityReviewBoard(report, {});
|
||||
|
||||
assert.match(output, /Memory quality review board/);
|
||||
assert.match(output, /Purpose: evidence for human\/agent review only; no automatic judgment or cleanup\./);
|
||||
assert.match(output, /Primary review purpose: SYSTEM MECHANISM observations/);
|
||||
assert.match(output, /Secondary review purpose: MEMORY CONTENT quality/);
|
||||
const orderedSections = [
|
||||
"Evidence provenance",
|
||||
"Facts - system mechanisms",
|
||||
"Facts - memory content",
|
||||
"System mechanism review candidates",
|
||||
"Memory content review candidates",
|
||||
"Review questions",
|
||||
"Next commands",
|
||||
];
|
||||
let previous = -1;
|
||||
for (const section of orderedSections) {
|
||||
const index = output.indexOf(section);
|
||||
assert.ok(index > previous, `${section} should appear after the previous section`);
|
||||
previous = index;
|
||||
}
|
||||
});
|
||||
|
||||
test("quality formatter shows provenance counts alongside system mechanism facts", () => {
|
||||
const model = inspectionModel([entry("mem-provenance", "Reabsorbed formatter candidate", "decision")], [], [
|
||||
rejection("Reabsorbed formatter candidate", { type: "decision", reasons: ["bad_decision"], timestamp: "2026-05-10T00:00:00.000Z" }),
|
||||
]);
|
||||
const report = buildQualityReviewBoard(model, {}, generatedAt);
|
||||
|
||||
const output = formatQualityReviewBoard(report, {});
|
||||
const factsIndex = output.indexOf("Facts - system mechanisms");
|
||||
const memoryFactsIndex = output.indexOf("Facts - memory content");
|
||||
const countsIndex = output.indexOf("Provenance counts for mechanism evidence");
|
||||
|
||||
assert.ok(countsIndex > factsIndex);
|
||||
assert.ok(countsIndex < memoryFactsIndex);
|
||||
assert.match(output, /reabsorbed_post_rejection=[1-9]\d*/);
|
||||
});
|
||||
|
||||
test("quality human output uses neutral language and keeps raw reason codes in context", () => {
|
||||
const model = inspectionModel([entry("mem-neutral", "Architecture decision remains durable", "decision")], [], [
|
||||
rejection("Architecture candidate for parser policy", { type: "decision", reasons: ["bad_decision"], timestamp: "2026-05-10T00:00:00.000Z" }),
|
||||
]);
|
||||
const report = buildQualityReviewBoard(model, {}, generatedAt);
|
||||
|
||||
const output = formatQualityReviewBoard(report, {}).toLowerCase();
|
||||
|
||||
for (const forbidden of [
|
||||
"bad memory",
|
||||
"delete",
|
||||
"obsolete",
|
||||
"should remove",
|
||||
"must remove",
|
||||
"false-positive risk",
|
||||
"clearly_garbage",
|
||||
"preserving the right memories",
|
||||
"lacks terminal removal",
|
||||
"needing review",
|
||||
"highest-value",
|
||||
"failing to",
|
||||
"too rigid",
|
||||
"noisy candidates",
|
||||
"when they should remain distinct",
|
||||
]) {
|
||||
assert.equal(output.includes(forbidden), false, `human output should not contain ${forbidden}`);
|
||||
}
|
||||
for (const line of output.split("\n").filter(line => line.includes("bad_decision"))) {
|
||||
assert.match(line, /raw reason[- ]codes?/);
|
||||
}
|
||||
});
|
||||
|
||||
test("quality flattens eviction details and defensively stringifies malformed nested facts", () => {
|
||||
const model = inspectionModel([], [
|
||||
event("evt-cap-details", {
|
||||
type: "memory_removed_capacity",
|
||||
phase: "storage",
|
||||
outcome: "removed",
|
||||
createdAt: "2026-05-11T11:00:00.000Z",
|
||||
memory: { memoryId: "mem-cap-details", type: "decision", source: "compaction" },
|
||||
reasonCodes: ["global_cap"],
|
||||
details: { globalCap: 28, typeCap: 10, malformed: { nested: true } } as unknown as EvidenceEventV1["details"],
|
||||
}),
|
||||
]);
|
||||
|
||||
const report = buildQualityReviewBoard(model, { verbose: true }, generatedAt);
|
||||
const candidate = report.reviewCandidates.find(item => item.id === "eviction:evt-cap-details");
|
||||
assert.ok(candidate);
|
||||
assert.equal(candidate.facts.globalCap, 28);
|
||||
assert.equal(candidate.facts.typeCap, 10);
|
||||
assert.equal(Object.hasOwn(candidate.facts, "details"), false);
|
||||
|
||||
const output = formatQualityReviewBoard(report, { verbose: true });
|
||||
assert.doesNotMatch(output, /\[object Object\]/);
|
||||
assert.match(output, /globalCap=28/);
|
||||
assert.match(output, /typeCap=10/);
|
||||
assert.match(output, /malformed=\{"nested":true\}/);
|
||||
});
|
||||
|
||||
test("quality deduplicates rejection candidates after filtering eligible records", () => {
|
||||
const model = inspectionModel([], [], [
|
||||
rejection("Architecture parser policy should remain durable", { type: "decision", reasons: ["temporary_status"], timestamp: "2026-05-09T00:00:00.000Z" }),
|
||||
rejection("Architecture parser policy should remain durable", { type: "decision", reasons: ["bad_decision"], timestamp: "2026-05-10T00:00:00.000Z" }),
|
||||
]);
|
||||
|
||||
const report = buildQualityReviewBoard(model, {}, generatedAt);
|
||||
const candidates = report.reviewCandidates.filter(candidate => candidate.source === "rejection_rule_evidence");
|
||||
|
||||
assert.equal(report.facts.systemMechanisms.rejectionFilters.totalRecords, 2);
|
||||
assert.equal(report.facts.systemMechanisms.rejectionFilters.uniqueTexts, 1);
|
||||
assert.equal(candidates.length, 1);
|
||||
assert.equal(candidates[0].facts.timestamp, "2026-05-10T00:00:00.000Z");
|
||||
});
|
||||
|
||||
test("quality deduplicates eligible rejection candidates newest first", () => {
|
||||
const model = inspectionModel([], [], [
|
||||
rejection("Ambiguous architecture memory candidate", { type: "decision", reasons: ["bad_decision"], timestamp: "2026-05-09T00:00:00.000Z" }),
|
||||
rejection("Ambiguous architecture memory candidate", { type: "decision", reasons: ["bad_decision"], timestamp: "2026-05-10T00:00:00.000Z" }),
|
||||
]);
|
||||
|
||||
const report = buildQualityReviewBoard(model, {}, generatedAt);
|
||||
const candidates = report.reviewCandidates.filter(candidate => candidate.source === "rejection_rule_evidence");
|
||||
|
||||
assert.equal(report.facts.systemMechanisms.rejectionFilters.totalRecords, 2);
|
||||
assert.equal(report.facts.systemMechanisms.rejectionFilters.uniqueTexts, 1);
|
||||
assert.equal(candidates.length, 1);
|
||||
assert.equal(candidates[0].facts.timestamp, "2026-05-10T00:00:00.000Z");
|
||||
});
|
||||
|
||||
test("quality formatter collapses uniform system candidate provenance by displayed group", () => {
|
||||
const model = inspectionModel([], capacityEvents(3));
|
||||
const report = buildQualityReviewBoard(model, { verbose: true }, generatedAt);
|
||||
|
||||
const output = formatQualityReviewBoard(report, { verbose: true });
|
||||
const groupLineCount = output.match(/shared provenance for displayed candidates in this group/g)?.length ?? 0;
|
||||
|
||||
assert.equal(groupLineCount, 1);
|
||||
assert.doesNotMatch(output, /^ provenance:/m);
|
||||
assert.doesNotMatch(output, /not annotated for likely current active-memory content/);
|
||||
});
|
||||
|
||||
test("quality formatter prints standard active-memory questions once in section header", () => {
|
||||
const model = inspectionModel([
|
||||
entry("mem-question-1", "Question header active memory one", "decision"),
|
||||
entry("mem-question-2", "Question header active memory two", "feedback"),
|
||||
], []);
|
||||
const report = buildQualityReviewBoard(model, {}, generatedAt);
|
||||
report.activeMemoryDisplay.items[1].reviewQuestions = [
|
||||
...report.reviewQuestions.memoryContent,
|
||||
"Does this memory have additional workspace-specific review context?",
|
||||
];
|
||||
|
||||
const output = formatQualityReviewBoard(report, {});
|
||||
const activeSection = output.slice(output.indexOf("Memory content review candidates"), output.indexOf("Review questions"));
|
||||
|
||||
assert.match(activeSection, /Standard review questions \(applicable to all active memories below\):/);
|
||||
for (const question of report.reviewQuestions.memoryContent) {
|
||||
assert.equal(activeSection.match(new RegExp(escapeRegExp(question), "g"))?.length ?? 0, 1);
|
||||
}
|
||||
assert.doesNotMatch(activeSection, /^ review questions:/m);
|
||||
assert.match(activeSection, /^ additional review questions:/m);
|
||||
assert.match(activeSection, /Does this memory have additional workspace-specific review context\?/);
|
||||
});
|
||||
|
||||
test("quality formatter shows full active memory text under threshold and samples over threshold", () => {
|
||||
const shortEntries = [
|
||||
entry("mem-1", "Short active memory one with FULL_TEXT_SENTINEL_1", "feedback"),
|
||||
entry("mem-2", "Short active memory two with FULL_TEXT_SENTINEL_2", "decision"),
|
||||
entry("mem-3", "Short active memory three with FULL_TEXT_SENTINEL_3", "project"),
|
||||
];
|
||||
const shortOutput = formatQualityReviewBoard(buildQualityReviewBoard(inspectionModel(shortEntries, []), {}, generatedAt), {});
|
||||
assert.match(shortOutput, /Active memories \(showing all 3 because <= 40\)/);
|
||||
assert.match(shortOutput, /FULL_TEXT_SENTINEL_3/);
|
||||
|
||||
const largeEntries = Array.from({ length: 41 }, (_, index) => entry(
|
||||
`mem-${index.toString().padStart(2, "0")}`,
|
||||
`Large active memory ${index} FULL_TEXT_SENTINEL_${index}`,
|
||||
"feedback",
|
||||
));
|
||||
const largeDefault = formatQualityReviewBoard(buildQualityReviewBoard(inspectionModel(largeEntries, []), {}, generatedAt), {});
|
||||
assert.match(largeDefault, /Showing 40 of 41 active memories\. Use --verbose or --json for all active memory text\./);
|
||||
const defaultActiveSection = largeDefault.slice(largeDefault.indexOf("Memory content review candidates"), largeDefault.indexOf("Review questions"));
|
||||
assert.doesNotMatch(defaultActiveSection, /FULL_TEXT_SENTINEL_40/);
|
||||
|
||||
const largeVerbose = formatQualityReviewBoard(buildQualityReviewBoard(inspectionModel(largeEntries, []), { verbose: true }, generatedAt), { verbose: true });
|
||||
assert.match(largeVerbose, /Active memories \(showing all 41 because --verbose\)/);
|
||||
assert.match(largeVerbose, /FULL_TEXT_SENTINEL_40/);
|
||||
});
|
||||
|
||||
test("quality formatter no-emoji output contains no emoji glyphs", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "opencode-memory-diag-quality-noemoji-"));
|
||||
try {
|
||||
await seedWorkspace(root, [entry("mem-noemoji", "No emoji command memory", "feedback")]);
|
||||
const stdout = await runMemoryDiag(["quality", "--workspace", root, "--no-emoji"]);
|
||||
|
||||
assert.doesNotMatch(stdout, /[\u{1F300}-\u{1F9FF}]/u);
|
||||
assert.match(stdout, /Memory quality review board/);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("quality command json returns report shape and raw mode preserves unredacted text", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "opencode-memory-diag-quality-json-"));
|
||||
try {
|
||||
await seedWorkspace(root, [entry("mem-json-cli", "JSON text with token=secret-value and path /Users/alice/project/private.txt", "project")]);
|
||||
|
||||
const redacted = JSON.parse(await runMemoryDiag(["quality", "--workspace", root, "--json"]));
|
||||
assertReviewBoardShape(redacted);
|
||||
assert.doesNotMatch(JSON.stringify(redacted), /secret-value|\/Users\/alice/);
|
||||
|
||||
const raw = JSON.parse(await runMemoryDiag(["quality", "--workspace", root, "--json", "--raw"]));
|
||||
assertReviewBoardShape(raw);
|
||||
assert.match(JSON.stringify(raw), /secret-value|\/Users\/alice/);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("buildQualityJSON redacts by default and preserves report shape when raw", () => {
|
||||
const rawReport = buildQualityReviewBoard(inspectionModel([
|
||||
entry("mem-json-builder", "Builder text token=secret-value in /Users/alice/private.txt", "project"),
|
||||
], []), { raw: true }, generatedAt);
|
||||
|
||||
const redacted = buildQualityJSON(rawReport, false) as ReviewBoardReport;
|
||||
assertReviewBoardShape(redacted);
|
||||
assert.doesNotMatch(JSON.stringify(redacted), /secret-value|\/Users\/alice/);
|
||||
|
||||
const raw = buildQualityJSON(rawReport, true) as ReviewBoardReport;
|
||||
assert.match(JSON.stringify(raw), /secret-value|\/Users\/alice/);
|
||||
});
|
||||
|
||||
test("quality review model applies active memory threshold, full text, and redaction", () => {
|
||||
const entries = Array.from({ length: 41 }, (_, index) => entry(
|
||||
`mem-${index.toString().padStart(2, "0")}`,
|
||||
index === 0
|
||||
? "Keep full text with token=secret-value and path /Users/alice/project/private.txt for review"
|
||||
: `Keep full text for active memory ${index}`,
|
||||
"feedback",
|
||||
));
|
||||
const model = inspectionModel(entries, []);
|
||||
|
||||
const defaultReport = buildQualityReviewBoard(model, {}, generatedAt);
|
||||
assert.equal(defaultReport.activeMemoryDisplay.threshold, 40);
|
||||
assert.equal(defaultReport.activeMemoryDisplay.mode, "sample");
|
||||
assert.equal(defaultReport.activeMemoryDisplay.shown, 40);
|
||||
assert.equal(defaultReport.activeMemoryDisplay.total, 41);
|
||||
assert.equal(defaultReport.activeMemoryDisplay.items.at(-1)?.id, "mem-39");
|
||||
assert.equal(defaultReport.activeMemoryDisplay.items[0].text.includes("Keep full text with"), true);
|
||||
assert.doesNotMatch(defaultReport.activeMemoryDisplay.items[0].text, /secret-value|\/Users\/alice/);
|
||||
assert.match(defaultReport.activeMemoryDisplay.items[0].text, /\[REDACTED\]|<path>/);
|
||||
|
||||
const verboseReport = buildQualityReviewBoard(model, { verbose: true }, generatedAt);
|
||||
assert.equal(verboseReport.activeMemoryDisplay.mode, "all");
|
||||
assert.equal(verboseReport.activeMemoryDisplay.shown, 41);
|
||||
|
||||
const rawReport = buildQualityReviewBoard(model, { raw: true }, generatedAt);
|
||||
assert.match(rawReport.activeMemoryDisplay.items[0].text, /secret-value|\/Users\/alice/);
|
||||
});
|
||||
|
||||
test("quality provenance counts use all mechanism candidates when human output is representative", () => {
|
||||
const active = Array.from({ length: 28 }, (_, index) => entry(
|
||||
`mem-active-${index}`,
|
||||
`Active display stability memory ${index}`,
|
||||
"feedback",
|
||||
));
|
||||
const events = capacityEvents(12);
|
||||
const model = inspectionModel(active, events);
|
||||
|
||||
const defaultReport = buildQualityReviewBoard(model, {}, generatedAt);
|
||||
const verboseReport = buildQualityReviewBoard(model, { verbose: true }, generatedAt);
|
||||
|
||||
assert.equal(defaultReport.provenanceContext.countsByClassification.unversioned_ambiguous, 12);
|
||||
assert.equal(verboseReport.provenanceContext.countsByClassification.unversioned_ambiguous, 12);
|
||||
assert.equal(defaultReport.reviewCandidates.filter(candidate => candidate.mechanism === "eviction_cap").length, 10);
|
||||
assert.equal(verboseReport.reviewCandidates.filter(candidate => candidate.mechanism === "eviction_cap").length, 12);
|
||||
assert.equal(defaultReport.provenanceContext.candidateLimit, 10);
|
||||
assert.deepEqual(defaultReport.provenanceContext.candidateDisplay?.byMechanism.eviction_cap, { shown: 10, total: 12 });
|
||||
assert.match(formatQualityReviewBoard(defaultReport, {}), /System mechanism review candidates \(representative; 10 shown of 12 total; limit 10 per mechanism category\)/);
|
||||
assert.equal(defaultReport.activeMemoryDisplay.shown, 28);
|
||||
assert.equal(defaultReport.activeMemoryDisplay.total, 28);
|
||||
assert.equal(verboseReport.activeMemoryDisplay.shown, 28);
|
||||
assert.equal(verboseReport.activeMemoryDisplay.total, 28);
|
||||
});
|
||||
|
||||
test("quality json includes all system mechanism candidates without verbose", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "opencode-memory-diag-quality-json-candidates-"));
|
||||
try {
|
||||
await seedWorkspace(root, []);
|
||||
await appendEvidenceEvents(root, capacityEventInputs(12));
|
||||
|
||||
const report = JSON.parse(await runMemoryDiag(["quality", "--workspace", root, "--json"])) as ReviewBoardReport;
|
||||
const evictionCandidates = report.reviewCandidates.filter(candidate => candidate.mechanism === "eviction_cap");
|
||||
|
||||
assert.equal(evictionCandidates.length, 12);
|
||||
assert.equal(report.provenanceContext.candidateLimit, undefined);
|
||||
assert.equal(report.activeMemoryDisplay.shown, 0);
|
||||
assert.equal(report.activeMemoryDisplay.total, 0);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("quality review model builds system mechanism facts and neutral candidates", () => {
|
||||
const active = [
|
||||
entry("mem-a", "Retention architecture uses evidence windows for durable review", "decision"),
|
||||
entry("mem-b", "Duplicate durable instruction", "feedback"),
|
||||
entry("mem-c", "Duplicate durable instruction", "feedback"),
|
||||
entry("old-a", "Superseded same type replacement", "decision", { status: "superseded" }),
|
||||
entry("old-b", "Superseded cross type replacement", "project", { status: "superseded" }),
|
||||
];
|
||||
const events = [
|
||||
event("evt-reinforced", { type: "memory_reinforced", phase: "reinforcement", outcome: "reinforced", memory: { memoryId: "mem-a", type: "decision", source: "compaction" } }),
|
||||
event("evt-block-1", { type: "memory_reinforced", phase: "reinforcement", outcome: "rejected", memory: { memoryId: "mem-a", type: "decision", source: "compaction" }, reasonCodes: ["reinforcement_window_blocked"], details: { ref: "1" } }),
|
||||
event("evt-block-2", { type: "memory_reinforced", phase: "reinforcement", outcome: "rejected", memory: { memoryId: "mem-a", type: "decision", source: "compaction" }, reasonCodes: ["reinforcement_window_blocked"], details: { ref: "2" } }),
|
||||
event("evt-malformed", { type: "extraction_candidate_rejected", phase: "extraction", outcome: "rejected", reasonCodes: ["invalid_memory_command"] }),
|
||||
event("evt-cap-type", { type: "memory_removed_capacity", phase: "storage", outcome: "removed", memory: { memoryId: "old-type", type: "decision", source: "compaction" }, reasonCodes: ["type_cap"], createdAt: "2026-05-10T12:00:00.000Z" }),
|
||||
event("evt-cap-global", { type: "memory_removed_capacity", phase: "storage", outcome: "removed", memory: { memoryId: "old-global", type: "feedback", source: "compaction" }, reasonCodes: ["global_cap"], createdAt: "2026-05-10T11:00:00.000Z", textPreview: "evicted token=secret-value from /tmp/private.txt" }),
|
||||
event("evt-missing", { type: "promotion_promoted", phase: "promotion", outcome: "promoted", memory: { memoryId: "historical-unknown", type: "reference", source: "compaction" } }),
|
||||
event("evt-replace-same", { type: "memory_replaced_numbered_ref", phase: "storage", outcome: "superseded", memory: { memoryId: "old-a", type: "decision", source: "compaction" }, reasonCodes: ["same_type_replace"], relations: [{ role: "superseded", memory: { memoryId: "old-a", type: "decision" } }, { role: "superseded_by", memory: { memoryId: "mem-a", type: "decision" } }] }),
|
||||
event("evt-replace-cross", { type: "memory_replaced_numbered_ref", phase: "storage", outcome: "superseded", memory: { memoryId: "old-b", type: "project", source: "compaction" }, reasonCodes: ["cross_type_replace"], relations: [{ role: "superseded", memory: { memoryId: "old-b", type: "project" } }, { role: "superseded_by", memory: { memoryId: "mem-a", type: "decision" } }] }),
|
||||
];
|
||||
const rejections = [
|
||||
rejection("Retention architecture uses evidence windows for durable review", { type: "decision", reasons: ["bad_decision"], timestamp: "2026-05-10T10:00:00.000Z" }),
|
||||
rejection("Ambiguous useful candidate", { type: "decision", reasons: ["bad_decision"], timestamp: "2026-05-10T09:00:00.000Z" }),
|
||||
rejection("Temporary progress note", { type: "feedback", reasons: ["bad_feedback", "temporary_status"], timestamp: "2026-05-10T08:00:00.000Z" }),
|
||||
];
|
||||
const model = inspectionModel(active, events, rejections, { limits: { maxRenderedChars: LONG_TERM_LIMITS.maxRenderedChars, maxEntries: 2 } });
|
||||
|
||||
const report = buildQualityReviewBoard(model, {}, generatedAt);
|
||||
|
||||
assert.deepEqual(report.facts.systemMechanisms.rejectionFilters.byRawReasonCode, { bad_decision: 2, bad_feedback: 1, temporary_status: 1 });
|
||||
assert.equal(report.facts.systemMechanisms.rejectionFilters.byType.decision, 2);
|
||||
assert.equal(report.facts.systemMechanisms.rejectionFilters.ambiguousOrArchitectureLike, 2);
|
||||
assert.equal(report.facts.systemMechanisms.rejectionFilters.hardReasonOrNoiseHeuristic, 0);
|
||||
assert.equal(report.facts.systemMechanisms.rejectionFilters.reabsorbedRejectedTexts, 1);
|
||||
|
||||
assert.equal(report.facts.systemMechanisms.reinforcementRules.reinforceEvents, 3);
|
||||
assert.equal(report.facts.systemMechanisms.reinforcementRules.reinforcedEvents, 1);
|
||||
assert.equal(report.facts.systemMechanisms.reinforcementRules.rejectedOrBlockedEvents, 2);
|
||||
assert.equal(report.facts.systemMechanisms.reinforcementRules.windowBlockedEvents, 2);
|
||||
assert.equal(report.facts.systemMechanisms.reinforcementRules.windowBlockRate, 2 / 3);
|
||||
assert.deepEqual(report.facts.systemMechanisms.reinforcementRules.repeatedBlocksByMemory, [{ memoryId: "mem-a", count: 2, refs: ["1", "2"], rawReasonCodes: ["reinforcement_window_blocked"] }]);
|
||||
assert.equal(report.facts.systemMechanisms.reinforcementRules.malformedCommandEvents, 1);
|
||||
|
||||
assert.equal(report.facts.systemMechanisms.evictionAndCaps.activeMemories, 3);
|
||||
assert.deepEqual(report.facts.systemMechanisms.evictionAndCaps.fullCaps, ["global"]);
|
||||
assert.equal(report.facts.systemMechanisms.evictionAndCaps.removedByCapacity, 2);
|
||||
assert.equal(report.facts.systemMechanisms.evictionAndCaps.removedByGlobalCap, 1);
|
||||
assert.equal(report.facts.systemMechanisms.evictionAndCaps.removedByTypeCap, 1);
|
||||
assert.equal(report.facts.systemMechanisms.evictionAndCaps.unknownDisappearances, 1);
|
||||
assert.equal(report.facts.systemMechanisms.evictionAndCaps.recentEvictionsByType.decision, 1);
|
||||
assert.equal(report.facts.systemMechanisms.evictionAndCaps.recentEvictedContentShown, 2);
|
||||
|
||||
assert.equal(report.facts.systemMechanisms.identityAndDedup.replacementEvents, 2);
|
||||
assert.equal(report.facts.systemMechanisms.identityAndDedup.sameTypeReplacementEvents, 1);
|
||||
assert.equal(report.facts.systemMechanisms.identityAndDedup.crossTypeReplacementEvents, 1);
|
||||
assert.equal(report.facts.systemMechanisms.identityAndDedup.duplicateTextOrIdentityGroups, 1);
|
||||
|
||||
assert.ok(report.reviewCandidates.some(candidate => candidate.source === "reabsorption_evidence" && candidate.provenance?.classification === "reabsorbed_post_rejection"));
|
||||
assert.ok(report.reviewCandidates.some(candidate => candidate.mechanism === "reinforcement_rule"));
|
||||
assert.ok(report.reviewCandidates.some(candidate => candidate.source === "eviction_cap_evidence" && candidate.evidence.textAvailable === false));
|
||||
assert.ok(report.reviewCandidates.some(candidate => candidate.source === "identity_dedup_evidence"));
|
||||
assert.ok(report.reviewCandidates.every(candidate => Array.isArray(candidate.heuristicFlags) && Array.isArray(candidate.reviewQuestions)));
|
||||
assert.ok(report.reviewCandidates.filter(candidate => candidate.source !== "active_memory").every(candidate => candidate.provenance));
|
||||
assert.doesNotMatch(JSON.stringify(report), /secret-value|\/tmp\/private/);
|
||||
});
|
||||
|
||||
test("quality review model includes provenance timeline and classification counts", () => {
|
||||
const active = [entry("mem-active", "Reabsorbed post rejection candidate", "decision")];
|
||||
const events = [
|
||||
event("evt-migration-1", { type: "memory_migration_superseded", phase: "storage", outcome: "superseded", createdAt: "2026-05-01T00:00:00.000Z", details: { migrationId: "2026-05-01-retention-clock-backfill" } }),
|
||||
event("evt-before", { type: "memory_removed_capacity", phase: "storage", outcome: "removed", createdAt: "2026-04-30T00:00:00.000Z", memory: { memoryId: "before", type: "decision", source: "compaction" }, reasonCodes: ["global_cap"] }),
|
||||
event("evt-before-unknown", { type: "promotion_promoted", phase: "promotion", outcome: "promoted", createdAt: "2026-04-30T01:00:00.000Z", memory: { memoryId: "before-unknown", type: "reference", source: "compaction" } }),
|
||||
event("evt-after", { type: "memory_removed_capacity", phase: "storage", outcome: "removed", createdAt: "2026-05-11T11:00:00.000Z", memory: { memoryId: "after", type: "feedback", source: "compaction" }, reasonCodes: ["type_cap"] }),
|
||||
];
|
||||
const rejections = [
|
||||
rejection("Legacy unscoped architecture rule", { type: "decision", reasons: ["bad_decision"], timestamp: "2026-04-29T00:00:00.000Z", legacy: true }),
|
||||
rejection("Reabsorbed post rejection candidate", { type: "decision", reasons: ["bad_decision"], timestamp: "2026-05-10T00:00:00.000Z" }),
|
||||
];
|
||||
const model = inspectionModel(active, events, rejections, {
|
||||
migrations: ["2026-04-26-p0-cleanup", "2026-04-28-quality-cleanup", "2026-05-01-retention-clock-backfill"],
|
||||
lastActivityAt: "2026-05-10T00:00:00.000Z",
|
||||
});
|
||||
|
||||
const report = buildQualityReviewBoard(model, {}, generatedAt);
|
||||
const byMigration = new Map(report.provenanceContext.migrationTimeline.map(row => [row.migrationId, row]));
|
||||
|
||||
assert.equal(report.provenanceContext.method, "migration_timestamp_and_format_inference");
|
||||
assert.equal(report.provenanceContext.producerVersionAvailable, false);
|
||||
assert.equal(report.provenanceContext.falseCurrentRiskBias, "prefer_unversioned_ambiguous_when_uncertain");
|
||||
assert.equal(byMigration.get("2026-04-26-p0-cleanup")?.presentInStore, true);
|
||||
assert.equal(byMigration.get("2026-05-01-retention-clock-backfill")?.firstEvidenceAt, "2026-05-01T00:00:00.000Z");
|
||||
assert.equal(report.provenanceContext.lastActivityAt, "2026-05-10T00:00:00.000Z");
|
||||
for (const classification of provenanceClassifications()) {
|
||||
assert.equal(typeof report.provenanceContext.countsByClassification[classification], "number");
|
||||
}
|
||||
assert.ok(report.provenanceContext.countsByClassification.legacy_unversioned_format >= 1);
|
||||
assert.ok(report.provenanceContext.countsByClassification.reabsorbed_post_rejection >= 1);
|
||||
assert.ok(report.provenanceContext.countsByClassification.suspected_pre_migration_legacy >= 1);
|
||||
assert.ok(report.provenanceContext.countsByClassification.likely_current_behavior >= 1);
|
||||
});
|
||||
|
||||
test("quality review model exposes required JSON shape with neutral language", () => {
|
||||
const model = inspectionModel([entry("mem-json", "Durable JSON shape memory", "project")], [
|
||||
event("evt-json", { type: "render_selected", phase: "render", outcome: "rendered", memory: { memoryId: "mem-json", type: "project", source: "compaction" } }),
|
||||
]);
|
||||
|
||||
const report = buildQualityReviewBoard(model, {}, generatedAt);
|
||||
assertReviewBoardShape(report);
|
||||
|
||||
const serialized = JSON.stringify(report).toLowerCase();
|
||||
for (const forbidden of ["bad memory", "delete", "obsolete", "should remove"]) {
|
||||
assert.equal(serialized.includes(forbidden), false, `report should not contain ${forbidden}`);
|
||||
}
|
||||
});
|
||||
|
||||
function entry(id: string, text: string, type: LongTermMemoryEntry["type"], overrides: Partial<LongTermMemoryEntry> = {}): LongTermMemoryEntry {
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
text,
|
||||
source: "compaction",
|
||||
confidence: 0.75,
|
||||
status: "active",
|
||||
createdAt: "2026-05-01T00:00:00.000Z",
|
||||
updatedAt: "2026-05-10T00:00:00.000Z",
|
||||
retentionClock: new Date("2026-05-10T00:00:00.000Z").getTime(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function event(
|
||||
eventId: string,
|
||||
overrides: Partial<EvidenceEventV1> & { type: EvidenceEventType; phase: EvidencePhase; outcome: EvidenceOutcome },
|
||||
): EvidenceEventV1 {
|
||||
return {
|
||||
version: 1,
|
||||
eventId,
|
||||
createdAt: "2026-05-11T00:00:00.000Z",
|
||||
workspaceKey: "workspace-key",
|
||||
workspaceRootHash: "workspace-root-hash",
|
||||
reasonCodes: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function capacityEvents(count: number): EvidenceEventV1[] {
|
||||
return Array.from({ length: count }, (_, index) => event(`evt-cap-${index.toString().padStart(2, "0")}`, {
|
||||
type: "memory_removed_capacity",
|
||||
phase: "storage",
|
||||
outcome: "removed",
|
||||
createdAt: `2026-05-11T${String(index).padStart(2, "0")}:00:00.000Z`,
|
||||
memory: { memoryId: `evicted-${index}`, type: "feedback", source: "compaction" },
|
||||
reasonCodes: ["global_cap"],
|
||||
}));
|
||||
}
|
||||
|
||||
function capacityEventInputs(count: number): EvidenceEventInput[] {
|
||||
return Array.from({ length: count }, (_, index) => ({
|
||||
type: "memory_removed_capacity",
|
||||
phase: "storage",
|
||||
outcome: "removed",
|
||||
memory: { memoryId: `evicted-${index}`, type: "feedback", source: "compaction" },
|
||||
reasonCodes: ["global_cap"],
|
||||
}));
|
||||
}
|
||||
|
||||
function rejection(text: string, options: { type: NormalizedRejection["type"]; reasons: string[]; timestamp: string; legacy?: boolean }): NormalizedRejection {
|
||||
return {
|
||||
timestamp: options.timestamp,
|
||||
workspaceKey: options.legacy ? undefined : "workspace-key",
|
||||
workspaceRoot: undefined,
|
||||
workspaceRootHash: options.legacy ? undefined : "workspace-root-hash",
|
||||
type: options.type,
|
||||
source: "compaction",
|
||||
origin: "compaction_candidate",
|
||||
fromTrigger: false,
|
||||
text,
|
||||
reasons: options.reasons,
|
||||
};
|
||||
}
|
||||
|
||||
function inspectionModel(
|
||||
entries: LongTermMemoryEntry[],
|
||||
events: EvidenceEventV1[],
|
||||
rejectionRecords: NormalizedRejection[] = [],
|
||||
storeOverrides: Partial<WorkspaceMemoryStore> = {},
|
||||
): MemoryInspectionReadModel {
|
||||
const store: WorkspaceMemoryStore = {
|
||||
version: 1,
|
||||
workspace: { root: "/tmp/workspace", key: "workspace-key" },
|
||||
limits: { maxRenderedChars: LONG_TERM_LIMITS.maxRenderedChars, maxEntries: LONG_TERM_LIMITS.maxEntries },
|
||||
entries,
|
||||
migrations: [],
|
||||
updatedAt: generatedAt,
|
||||
lastActivityAt: "2026-05-10T00:00:00.000Z",
|
||||
...storeOverrides,
|
||||
};
|
||||
const retention = retentionCandidatesForDiag(store, new Date(generatedAt).getTime());
|
||||
const snapshot: WorkspaceDiagSnapshot = {
|
||||
store,
|
||||
journal: { version: 1, workspace: { root: "", key: "" }, entries: [], updatedAt: new Date(0).toISOString() },
|
||||
retention,
|
||||
memories: [],
|
||||
recentEvents: [],
|
||||
allEvents: events,
|
||||
summary: { storedActive: entries.length, rendered: retention.rendered.length, pending: 0, rejectedLast7Days: 0, corruptStoresQuarantinedLast30Days: 0 },
|
||||
};
|
||||
return {
|
||||
snapshot,
|
||||
store,
|
||||
pending: snapshot.journal,
|
||||
evidenceEvents: events,
|
||||
rejectionRecords,
|
||||
currentById: new Map(entries.map(memory => [memory.id, memory])),
|
||||
evidenceByMemoryId: groupEvidenceByMemoryId(events),
|
||||
};
|
||||
}
|
||||
|
||||
function provenanceClassifications(): ProvenanceClassification[] {
|
||||
return [
|
||||
"explicit_migration_evidence",
|
||||
"legacy_unversioned_format",
|
||||
"reabsorbed_post_rejection",
|
||||
"suspected_pre_migration_legacy",
|
||||
"likely_current_behavior",
|
||||
"unversioned_ambiguous",
|
||||
];
|
||||
}
|
||||
|
||||
function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
async function seedWorkspace(root: string, entries: LongTermMemoryEntry[]): Promise<void> {
|
||||
const key = await workspaceKey(root);
|
||||
const memoryPath = await workspaceMemoryPath(root);
|
||||
const store: WorkspaceMemoryStore = {
|
||||
version: 1,
|
||||
workspace: { root, key },
|
||||
limits: { maxRenderedChars: LONG_TERM_LIMITS.maxRenderedChars, maxEntries: LONG_TERM_LIMITS.maxEntries },
|
||||
entries,
|
||||
migrations: [],
|
||||
updatedAt: generatedAt,
|
||||
lastActivityAt: "2026-05-10T00:00:00.000Z",
|
||||
};
|
||||
|
||||
await mkdir(dirname(memoryPath), { recursive: true });
|
||||
await writeFile(memoryPath, JSON.stringify(store, null, 2));
|
||||
}
|
||||
|
||||
function assertReviewBoardShape(report: ReviewBoardReport): void {
|
||||
assert.equal(report.version, 1);
|
||||
assert.equal(typeof report.generatedAt, "string");
|
||||
assert.equal(typeof report.workspace.rootHash, "string");
|
||||
assert.equal(typeof report.workspace.key, "string");
|
||||
assert.equal(report.purpose, "review_evidence_only");
|
||||
assert.equal(report.languageGuidance.nonAuthoritative, true);
|
||||
assert.equal(report.languageGuidance.mutation, "none");
|
||||
assert.equal(report.languageGuidance.rawReasonCodesAreEvidence, true);
|
||||
assert.equal(report.languageGuidance.producerVersionRecorded, false);
|
||||
assert.equal(report.languageGuidance.provenanceInferenceOnly, true);
|
||||
assert.equal(report.languageGuidance.primaryReviewPurpose, "system_mechanism_observations");
|
||||
assert.equal(report.languageGuidance.secondaryReviewPurpose, "memory_content_quality");
|
||||
assert.ok(Array.isArray(report.provenanceContext.migrationTimeline));
|
||||
assert.equal(typeof report.provenanceContext.countsByClassification.unversioned_ambiguous, "number");
|
||||
assert.equal(typeof report.facts.systemMechanisms.rejectionFilters.totalRecords, "number");
|
||||
assert.equal(typeof report.facts.systemMechanisms.reinforcementRules.windowBlockRate, "number");
|
||||
assert.ok(Array.isArray(report.facts.systemMechanisms.evictionAndCaps.fullCaps));
|
||||
assert.equal(typeof report.facts.systemMechanisms.identityAndDedup.duplicateTextOrIdentityGroups, "number");
|
||||
assert.equal(typeof report.facts.memoryContent.evidenceCoverage.covered, "number");
|
||||
assert.ok(Array.isArray(report.activeMemoryDisplay.items));
|
||||
assert.ok(Array.isArray(report.reviewCandidates));
|
||||
assert.ok(Array.isArray(report.reviewQuestions.systemMechanism));
|
||||
assert.ok(Array.isArray(report.reviewQuestions.memoryContent));
|
||||
assert.ok(Array.isArray(report.nextCommands));
|
||||
}
|
||||
@@ -214,7 +214,7 @@ test("memory-diag defaults to status when no subcommand is supplied", async () =
|
||||
test("removed legacy aliases return unknown subcommand", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "opencode-memory-diag-legacy-health-"));
|
||||
try {
|
||||
for (const command of ["health", "quality", "rejections", "disappearances", "trace"]) {
|
||||
for (const command of ["health", "rejections", "disappearances", "trace"]) {
|
||||
await assert.rejects(
|
||||
runMemoryDiagResult([command, "--workspace", root]),
|
||||
(error: unknown) => {
|
||||
|
||||
Reference in New Issue
Block a user