mirror of
https://github.com/sdwolf4103/opencode-working-memory.git
synced 2026-07-17 12:56:42 +02:00
refactor(memory-diag): remove legacy aliases, centralize command metadata, prepare v1.5.4
- Remove legacy CLI aliases (health, quality, rejections, disappearances, trace) - Centralize command metadata in command-metadata.ts - Move trace lifecycle into explain command - Move disappearance helpers into missing formatter - Remove cleanup:workspaces from package scripts (dev tool preserved) - Bump version to 1.5.4
This commit is contained in:
+15
-66
@@ -1,4 +1,5 @@
|
||||
import type { CliOptions, Command, LegacyCommand, ParsedArgs } from "./types.ts";
|
||||
import { HIDDEN_COMMAND_NOTICES, isCommand } from "./command-metadata.ts";
|
||||
import type { CliOptions, Command, ParsedArgs } from "./types.ts";
|
||||
|
||||
export type { ParsedArgs } from "./types.ts";
|
||||
|
||||
@@ -21,23 +22,6 @@ function error(message: string): ParsedArgs {
|
||||
return { ok: false, message, usage: usage(), exitCode: 1 };
|
||||
}
|
||||
|
||||
function isCommand(value: string | undefined): value is Command {
|
||||
return value === "status"
|
||||
|| value === "rejected"
|
||||
|| value === "missing"
|
||||
|| value === "explain"
|
||||
|| value === "coverage"
|
||||
|| value === "audit";
|
||||
}
|
||||
|
||||
function isLegacyCommand(value: string | undefined): value is LegacyCommand {
|
||||
return value === "health"
|
||||
|| value === "quality"
|
||||
|| value === "rejections"
|
||||
|| value === "disappearances"
|
||||
|| value === "trace";
|
||||
}
|
||||
|
||||
function isValidSince(rawSince: string): boolean {
|
||||
if (/^(\d+)([dhm])$/i.test(rawSince)) return true;
|
||||
return !Number.isNaN(new Date(rawSince).getTime());
|
||||
@@ -50,44 +34,19 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
||||
}
|
||||
|
||||
let command: Command = "status";
|
||||
let legacyCommand: LegacyCommand | undefined;
|
||||
let deprecationNotice: string | undefined;
|
||||
let rest = argv;
|
||||
if (first && !first.startsWith("--")) {
|
||||
rest = tail;
|
||||
if (isCommand(first)) {
|
||||
command = first;
|
||||
if (first === "coverage") {
|
||||
deprecationNotice = "Note: 'coverage' is now a maintainer-only diagnostic. This alias will be removed in v2.0 unless replaced by 'audit evidence'.";
|
||||
} else if (first === "audit") {
|
||||
deprecationNotice = "Note: 'audit' is now a maintainer-only diagnostic. This alias will be removed in v2.0 unless replaced by 'audit migrations'.";
|
||||
}
|
||||
} else if (isLegacyCommand(first)) {
|
||||
legacyCommand = first;
|
||||
if (first === "health") {
|
||||
command = "status";
|
||||
deprecationNotice = "Note: 'health' is now 'status'. This alias will be removed in v2.0.";
|
||||
} else if (first === "quality") {
|
||||
command = "status";
|
||||
deprecationNotice = "Note: 'quality' is now 'status --verbose'. This alias will be removed in v2.0.";
|
||||
} else if (first === "rejections") {
|
||||
command = "rejected";
|
||||
deprecationNotice = "Note: 'rejections' is now 'rejected'. This alias will be removed in v2.0.";
|
||||
} else if (first === "disappearances") {
|
||||
command = "missing";
|
||||
deprecationNotice = "Note: 'disappearances' is now 'missing'. This alias will be removed in v2.0.";
|
||||
} else {
|
||||
command = "explain";
|
||||
deprecationNotice = "Note: 'trace --memory <id>' is now 'explain <memory-id>'. This alias will be removed in v2.0.";
|
||||
}
|
||||
deprecationNotice = HIDDEN_COMMAND_NOTICES[first];
|
||||
} else {
|
||||
return error(`Unknown subcommand: ${first}`);
|
||||
}
|
||||
}
|
||||
|
||||
const options: CliOptions = { raw: false, positional: [] };
|
||||
if (legacyCommand) options.legacyCommand = legacyCommand;
|
||||
if (legacyCommand === "quality") options.verbose = true;
|
||||
for (let i = 0; i < rest.length; i += 1) {
|
||||
const arg = rest[i];
|
||||
if (arg === "--raw") options.raw = true;
|
||||
@@ -98,8 +57,6 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
||||
else if (arg === "--soft-only") options.softOnly = true;
|
||||
else if (arg === "--trigger-only") options.triggerOnly = true;
|
||||
else if (arg === "--include-historical") options.includeHistorical = true;
|
||||
else if (arg === "--quality") options.quality = true;
|
||||
else if (arg === "--unique") options.unique = true;
|
||||
else if (arg === "--explain") options.explain = true;
|
||||
else if (arg === "--workspace") {
|
||||
const value = rest[++i];
|
||||
@@ -121,14 +78,13 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
||||
const value = rest[++i];
|
||||
if (!value) return error("--memory requires an id");
|
||||
options.memory = value;
|
||||
} else if (!arg.startsWith("--") && command === "explain" && legacyCommand !== "trace") {
|
||||
} else if (!arg.startsWith("--") && command === "explain") {
|
||||
options.positional?.push(arg);
|
||||
} else {
|
||||
return error(`Unknown option: ${arg}`);
|
||||
}
|
||||
}
|
||||
|
||||
const displayCommand = legacyCommand ?? command;
|
||||
if (command === "explain") {
|
||||
const positional = options.positional ?? [];
|
||||
if (positional.length > 1) return error("explain accepts at most one memory id");
|
||||
@@ -138,34 +94,27 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
||||
return error(`Unknown option: ${options.positional?.[0]}`);
|
||||
}
|
||||
|
||||
if (legacyCommand === "health") {
|
||||
if (options.all && options.workspace) return error("Use either --all or --workspace, not both");
|
||||
if (options.json && options.all) return error("health --json does not support --all");
|
||||
} else if (command === "status") {
|
||||
if (options.all) return error(`${displayCommand} does not accept --all`);
|
||||
if (command === "status") {
|
||||
if (options.all) return error(`${command} does not accept --all`);
|
||||
} else if (command === "rejected" || command === "missing" || command === "coverage" || command === "explain") {
|
||||
if (options.all) return error(`${displayCommand} does not accept --all`);
|
||||
if (options.all) return error(`${command} does not accept --all`);
|
||||
} else {
|
||||
if (options.all || options.workspace) return error(`${displayCommand} does not accept --all or --workspace`);
|
||||
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") {
|
||||
return error(`${displayCommand} does not accept --json`);
|
||||
return error(`${command} does not accept --json`);
|
||||
}
|
||||
if (legacyCommand === "rejections" && options.json && !options.quality) return error("rejections --json requires --quality");
|
||||
if (command !== "rejected" && (options.softOnly || options.triggerOnly || options.since)) {
|
||||
return error(`${displayCommand} does not accept rejection filters`);
|
||||
return error(`${command} does not accept rejection filters`);
|
||||
}
|
||||
if (command !== "coverage" && options.includeHistorical) return error(`${displayCommand} does not accept --include-historical`);
|
||||
if (command !== "rejected" && (options.quality || options.reason || options.unique)) return error(`${displayCommand} does not accept rejection quality filters`);
|
||||
if (command !== "missing" && options.explain) return error(`${displayCommand} does not accept --explain`);
|
||||
if (command !== "coverage" && options.includeHistorical) return error(`${command} does not accept --include-historical`);
|
||||
if (command !== "rejected" && options.reason) return error(`${command} does not accept rejection filters`);
|
||||
if (command !== "missing" && options.explain) return error(`${command} does not accept --explain`);
|
||||
if (command !== "audit" && options.migration) {
|
||||
return error(`${displayCommand} does not accept --migration`);
|
||||
}
|
||||
if (legacyCommand === "trace" && !options.memory) {
|
||||
return error("--memory requires an id");
|
||||
return error(`${command} does not accept --migration`);
|
||||
}
|
||||
if (command !== "explain" && options.memory) {
|
||||
return error(`${displayCommand} does not accept --memory`);
|
||||
return error(`${command} does not accept --memory`);
|
||||
}
|
||||
if (command === "rejected" && options.since && !isValidSince(options.since)) {
|
||||
return error(`Invalid --since value: ${options.since}`);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
export const VISIBLE_COMMANDS = ["status", "rejected", "missing", "explain"] as const;
|
||||
export const HIDDEN_COMMANDS = ["coverage", "audit"] as const;
|
||||
|
||||
export type VisibleCommand = typeof VISIBLE_COMMANDS[number];
|
||||
export type HiddenCommand = typeof HIDDEN_COMMANDS[number];
|
||||
export type Command = VisibleCommand | HiddenCommand;
|
||||
|
||||
export const HIDDEN_COMMAND_NOTICES: Partial<Record<Command, string>> = {
|
||||
coverage: "Note: 'coverage' is a maintainer-only diagnostic and is not part of the public CLI surface.",
|
||||
audit: "Note: 'audit' is a maintainer-only diagnostic and is not part of the public CLI surface.",
|
||||
};
|
||||
|
||||
export function isCommand(value: string | undefined): value is Command {
|
||||
return value !== undefined
|
||||
&& ([...VISIBLE_COMMANDS, ...HIDDEN_COMMANDS] as readonly string[]).includes(value);
|
||||
}
|
||||
@@ -1,23 +1,12 @@
|
||||
import { runAudit } from "./commands/audit.ts";
|
||||
import { runCoverage } from "./commands/coverage.ts";
|
||||
import { runDisappearances } from "./commands/disappearances.ts";
|
||||
import { runExplain } from "./commands/explain.ts";
|
||||
import { runHealth } from "./commands/health.ts";
|
||||
import { runMissing } from "./commands/missing.ts";
|
||||
import { runQuality } from "./commands/quality.ts";
|
||||
import { runRejected } from "./commands/rejected.ts";
|
||||
import { runRejections } from "./commands/rejections.ts";
|
||||
import { runStatus } from "./commands/status.ts";
|
||||
import { runTrace } from "./commands/trace.ts";
|
||||
import type { CliOptions, Command, CommandResult } from "./types.ts";
|
||||
|
||||
export async function dispatch(command: Command, options: CliOptions): Promise<CommandResult> {
|
||||
if (options.legacyCommand === "health") return runHealth(options);
|
||||
if (options.legacyCommand === "quality") return runQuality(options);
|
||||
if (options.legacyCommand === "rejections") return runRejections(options);
|
||||
if (options.legacyCommand === "disappearances") return runDisappearances(options);
|
||||
if (options.legacyCommand === "trace") return runTrace(options);
|
||||
|
||||
switch (command) {
|
||||
case "status": return runStatus(options);
|
||||
case "rejected": return runRejected(options);
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { buildInspectionReadModel, disappearanceRows } from "../inspection-model.ts";
|
||||
import { buildDisappearancesJSON, formatDisappearances } from "../formatters/disappearances.ts";
|
||||
import type { CliOptions, CommandResult } from "../types.ts";
|
||||
|
||||
export async function runDisappearances(options: CliOptions): Promise<CommandResult> {
|
||||
const model = await buildInspectionReadModel(options);
|
||||
const rows = disappearanceRows(model);
|
||||
|
||||
if (options.json) {
|
||||
return { stdout: JSON.stringify(buildDisappearancesJSON(rows, { explain: options.explain }), null, 2) };
|
||||
}
|
||||
|
||||
return { stdout: formatDisappearances(rows, { explain: options.explain }) };
|
||||
}
|
||||
@@ -1,10 +1,18 @@
|
||||
import { traceMemoryLifecycle } from "../../../src/evidence-log.ts";
|
||||
import { formatExplain } from "../formatters/explain.ts";
|
||||
import { formatTrace } from "../formatters/trace.ts";
|
||||
import { snapshotForOptions } from "../workspace-snapshot.ts";
|
||||
import type { CliOptions, CommandResult } from "../types.ts";
|
||||
import { runTrace } from "./trace.ts";
|
||||
|
||||
export async function runExplain(options: CliOptions): Promise<CommandResult> {
|
||||
if (options.memory) return runTrace(options);
|
||||
if (options.memory) {
|
||||
const root = options.workspace ?? process.cwd();
|
||||
const [snapshot, trace] = await Promise.all([
|
||||
snapshotForOptions(options),
|
||||
traceMemoryLifecycle(root, { memoryId: options.memory }),
|
||||
]);
|
||||
return { stdout: formatTrace(options.memory, snapshot, trace) };
|
||||
}
|
||||
|
||||
const snapshot = await snapshotForOptions(options);
|
||||
return { stdout: formatExplain(snapshot) };
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { join } from "node:path";
|
||||
import { workspaceKey, workspaceMemoryPath, workspacePendingJournalPath } from "../../../src/paths.ts";
|
||||
import { scanWorkspaceResidues } from "../../../src/workspace-cleanup.ts";
|
||||
import type { PendingMemoryJournalStore, WorkspaceMemoryStore } from "../../../src/types.ts";
|
||||
import { formatWorkspaceHealth, type WorkspaceHealthInput } from "../formatters/health.ts";
|
||||
import { pathExists, readJSONFile } from "../io.ts";
|
||||
import { buildMemoryDiagJSON } from "../workspace-snapshot.ts";
|
||||
import type { CliOptions, CommandResult } from "../types.ts";
|
||||
|
||||
type WorkspaceHealthCommandInput = Omit<WorkspaceHealthInput, "now">;
|
||||
|
||||
async function workspaceHealthOutput(input: WorkspaceHealthCommandInput): Promise<string> {
|
||||
return formatWorkspaceHealth({ ...input, now: Date.now() }, {
|
||||
rawStore: await readJSONFile<WorkspaceMemoryStore>(input.memoryPath),
|
||||
rawJournal: await readJSONFile<PendingMemoryJournalStore>(input.pendingPath),
|
||||
pendingExists: pathExists(input.pendingPath),
|
||||
});
|
||||
}
|
||||
|
||||
export async function runHealth(options: CliOptions): Promise<CommandResult> {
|
||||
if (options.json) {
|
||||
const root = options.workspace ?? process.cwd();
|
||||
return { stdout: JSON.stringify(await buildMemoryDiagJSON(root), null, 2) };
|
||||
}
|
||||
|
||||
if (options.all) {
|
||||
const scan = await scanWorkspaceResidues({ includeOrphans: true, minAgeMs: 0 });
|
||||
const lines: string[] = ["Workspace memory health", ""];
|
||||
if (scan.results.length === 0) {
|
||||
lines.push("No workspace stores found.");
|
||||
return { stdout: lines.join("\n") };
|
||||
}
|
||||
for (let i = 0; i < scan.results.length; i += 1) {
|
||||
const result = scan.results[i];
|
||||
if (i > 0) lines.push("");
|
||||
lines.push(await workspaceHealthOutput({
|
||||
root: result.root,
|
||||
key: result.workspaceKey,
|
||||
memoryPath: join(result.workspaceDir, "workspace-memory.json"),
|
||||
pendingPath: join(result.workspaceDir, "workspace-pending-journal.json"),
|
||||
raw: options.raw,
|
||||
}));
|
||||
}
|
||||
return { stdout: lines.join("\n") };
|
||||
}
|
||||
|
||||
const root = options.workspace ?? process.cwd();
|
||||
const key = await workspaceKey(root);
|
||||
return {
|
||||
stdout: await workspaceHealthOutput({
|
||||
root,
|
||||
key,
|
||||
memoryPath: await workspaceMemoryPath(root),
|
||||
pendingPath: await workspacePendingJournalPath(root),
|
||||
raw: options.raw,
|
||||
includeTitle: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { buildInspectionReadModel } from "../inspection-model.ts";
|
||||
import { buildQualityJSON, formatQuality } from "../formatters/quality.ts";
|
||||
import type { CliOptions, CommandResult } from "../types.ts";
|
||||
|
||||
export async function runQuality(options: CliOptions): Promise<CommandResult> {
|
||||
const model = await buildInspectionReadModel(options);
|
||||
const now = Date.now();
|
||||
|
||||
if (options.json) {
|
||||
return { stdout: JSON.stringify(buildQualityJSON(model, new Date(now).toISOString(), now), null, 2) };
|
||||
}
|
||||
|
||||
return { stdout: formatQuality(model, now) };
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { formatRejections, formatRejectionQuality, buildRejectionQualityJSON } from "../formatters/rejections.ts";
|
||||
import { loadRejectionRecords, rejectionFalsePositiveRisk, rejectionQualitySummary, uniqueByCanonicalText } from "../rejections-model.ts";
|
||||
import type { CliOptions, CommandResult } from "../types.ts";
|
||||
|
||||
export async function runRejections(options: CliOptions): Promise<CommandResult> {
|
||||
const { path, invalidLines, records } = await loadRejectionRecords(options);
|
||||
const normalized = options.unique ? uniqueByCanonicalText(records) : records;
|
||||
|
||||
if (options.quality) {
|
||||
const summary = rejectionQualitySummary(records);
|
||||
if (options.json) {
|
||||
return { stdout: JSON.stringify({ ...buildRejectionQualityJSON(summary), falsePositiveRisk: rejectionFalsePositiveRisk(summary) }, null, 2) };
|
||||
}
|
||||
|
||||
return { stdout: formatRejectionQuality({ path, invalidLines, summary, raw: options.raw }) };
|
||||
}
|
||||
|
||||
return { stdout: formatRejections({ path, invalidLines, records: normalized, raw: options.raw }) };
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import { retentionCandidatesForDiag, retentionClockSummary, daysSinceIso } from
|
||||
import { rejectionFalsePositiveRisk, rejectionQualitySummary } from "../rejections-model.ts";
|
||||
import { isWithinDays, memoryDiagJSONFromSnapshot } from "../workspace-snapshot.ts";
|
||||
import type { CliOptions, CommandResult, MemoryInspectionReadModel } from "../types.ts";
|
||||
import { runHealth } from "./health.ts";
|
||||
|
||||
export function buildStatusReadout(model: MemoryInspectionReadModel, now = Date.now()): StatusReadout {
|
||||
const active = model.store.entries.filter(entry => entry.status !== "superseded");
|
||||
@@ -81,8 +80,6 @@ export function buildStatusReadout(model: MemoryInspectionReadModel, now = Date.
|
||||
}
|
||||
|
||||
export async function runStatus(options: CliOptions): Promise<CommandResult> {
|
||||
if (options.legacyCommand === "health" && options.all) return runHealth(options);
|
||||
|
||||
const now = Date.now();
|
||||
const model = await buildInspectionReadModel(options);
|
||||
const readout = buildStatusReadout(model, now);
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { traceMemoryLifecycle } from "../../../src/evidence-log.ts";
|
||||
import { formatTrace } from "../formatters/trace.ts";
|
||||
import { snapshotForOptions } from "../workspace-snapshot.ts";
|
||||
import type { CliOptions, CommandResult } from "../types.ts";
|
||||
|
||||
export async function runTrace(options: CliOptions): Promise<CommandResult> {
|
||||
const root = options.workspace ?? process.cwd();
|
||||
const memoryId = options.memory;
|
||||
if (!memoryId) return { stdout: "", stderr: "--memory requires an id", exitCode: 1 };
|
||||
|
||||
const [snapshot, trace] = await Promise.all([
|
||||
snapshotForOptions(options),
|
||||
traceMemoryLifecycle(root, { memoryId }),
|
||||
]);
|
||||
return { stdout: formatTrace(memoryId, snapshot, trace) };
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import type { disappearanceRows } from "../inspection-model.ts";
|
||||
import { eventCounts } from "../inspection-model.ts";
|
||||
import { formatDetails } from "../text.ts";
|
||||
|
||||
export type DisappearanceRows = ReturnType<typeof disappearanceRows>;
|
||||
|
||||
export function buildDisappearancesJSON(rows: DisappearanceRows, options: { explain?: boolean; generatedAt?: string } = {}): Record<string, unknown> {
|
||||
return {
|
||||
version: 1,
|
||||
generatedAt: options.generatedAt ?? new Date().toISOString(),
|
||||
disappearances: rows.map(row => ({
|
||||
id: row.id,
|
||||
classification: row.classification,
|
||||
terminalType: row.terminalType,
|
||||
reasonCodes: row.reasonCodes,
|
||||
eventCounts: eventCounts(row.events),
|
||||
details: options.explain ? row.event?.details : undefined,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function formatDisappearances(rows: DisappearanceRows, options: { explain?: boolean } = {}): string {
|
||||
const lines: string[] = [];
|
||||
lines.push("Memory disappearances");
|
||||
lines.push("");
|
||||
if (rows.length === 0) {
|
||||
lines.push("No evidence-only memories found.");
|
||||
return lines.join("\n");
|
||||
}
|
||||
for (const row of rows) {
|
||||
const reasons = row.reasonCodes.length > 0 ? row.reasonCodes.join(",") : "none";
|
||||
lines.push(`Memory ${row.id}: ${row.classification} terminal=${row.terminalType} reasons=${reasons}`);
|
||||
if (options.explain) {
|
||||
lines.push(` events: ${row.events.map(event => event.type).join(", ")}`);
|
||||
if (row.event?.type === "memory_removed_capacity") {
|
||||
lines.push(` memory_removed_capacity details: ${formatDetails(row.event.details)}`);
|
||||
}
|
||||
const renderTypeCap = row.events.find(event => event.type === "render_omitted" && event.reasonCodes.includes("type_cap"));
|
||||
if (renderTypeCap) {
|
||||
lines.push(` render_omitted type-cap observation: reasons=${renderTypeCap.reasonCodes.join(",")} details=${formatDetails(renderTypeCap.details)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
import { assessMemoryQuality } from "../../../src/memory-quality.ts";
|
||||
import {
|
||||
DORMANT_DECAY_MULTIPLIER,
|
||||
RETENTION_TYPE_MAX,
|
||||
WORKSPACE_DORMANT_AFTER_DAYS,
|
||||
} from "../../../src/retention.ts";
|
||||
import { renderWorkspaceMemory } from "../../../src/workspace-memory.ts";
|
||||
import type { LongTermSource, PendingMemoryJournalStore, WorkspaceMemoryStore } from "../../../src/types.ts";
|
||||
import { LONG_TERM_LIMITS } from "../../../src/types.ts";
|
||||
import { SUSPICIOUS_REASONS, TYPES } from "../constants.ts";
|
||||
import {
|
||||
ageDays,
|
||||
daysSinceIso,
|
||||
formatStrength,
|
||||
isSafetyCriticalForDiag,
|
||||
promotionLimit,
|
||||
retentionCandidatesForDiag,
|
||||
} from "../retention-model.ts";
|
||||
import {
|
||||
canonicalMemoryText,
|
||||
cleanPath,
|
||||
cleanText,
|
||||
countBy,
|
||||
formatPercent,
|
||||
formatWorkspaceIdentity,
|
||||
truncate,
|
||||
} from "../text.ts";
|
||||
import { normalizedJournal, normalizedStore } from "../workspace-snapshot.ts";
|
||||
|
||||
export type WorkspaceHealthInput = {
|
||||
root?: string;
|
||||
key: string;
|
||||
memoryPath: string;
|
||||
pendingPath: string;
|
||||
raw: boolean;
|
||||
now: number;
|
||||
includeTitle?: boolean;
|
||||
};
|
||||
|
||||
export type WorkspaceHealthLoadedData = {
|
||||
rawStore: WorkspaceMemoryStore | null;
|
||||
rawJournal: PendingMemoryJournalStore | null;
|
||||
pendingExists: boolean;
|
||||
};
|
||||
|
||||
export function formatWorkspaceHealth(input: WorkspaceHealthInput, loadedData: WorkspaceHealthLoadedData): string {
|
||||
const lines: string[] = [];
|
||||
if (input.includeTitle) {
|
||||
lines.push("Workspace memory health");
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
const rawStore = loadedData.rawStore;
|
||||
const storeRoot = rawStore?.workspace?.root ?? input.root ?? "";
|
||||
const storeKey = rawStore?.workspace?.key ?? input.key;
|
||||
const store = normalizedStore(rawStore, storeRoot, storeKey);
|
||||
const journal = normalizedJournal(loadedData.rawJournal);
|
||||
|
||||
const identity = formatWorkspaceIdentity(storeKey, storeRoot || undefined, input.raw);
|
||||
if (identity) lines.push(identity);
|
||||
lines.push(`memoryPath=${cleanPath(input.memoryPath, input.raw)}`);
|
||||
lines.push(`pendingPath=${cleanPath(input.pendingPath, input.raw)}`);
|
||||
if (!rawStore) lines.push("memory store: missing or unreadable (treated as empty)");
|
||||
if (!loadedData.pendingExists) lines.push("pending journal: missing (treated as empty)");
|
||||
lines.push("");
|
||||
|
||||
const active = store.entries.filter(entry => entry.status !== "superseded");
|
||||
const superseded = store.entries.filter(entry => entry.status === "superseded");
|
||||
const retention = retentionCandidatesForDiag(store, input.now);
|
||||
const renderedEntries = retention.rendered.map(item => item.entry);
|
||||
const renderedEstimate = renderWorkspaceMemory(store).length;
|
||||
|
||||
lines.push(`Stored active memories: ${active.length}`);
|
||||
lines.push(`Superseded memories: ${superseded.length}`);
|
||||
lines.push(`Rendered candidates: ${renderedEntries.length}`);
|
||||
lines.push(`Rendered estimate: ${renderedEstimate.toLocaleString()} chars`);
|
||||
lines.push("");
|
||||
|
||||
const pendingEntries = journal.entries;
|
||||
const retryable = pendingEntries.filter(entry => (entry.promotionAttempts ?? 0) < promotionLimit(entry.source)).length;
|
||||
const nearRetryLimit = pendingEntries.filter(entry => (entry.promotionAttempts ?? 0) >= promotionLimit(entry.source) - 1).length;
|
||||
const pendingBySource = countBy(pendingEntries.map(entry => entry.source));
|
||||
lines.push("Pending journal:");
|
||||
lines.push(` total: ${pendingEntries.length}`);
|
||||
lines.push(` retryable: ${retryable}`);
|
||||
lines.push(` near retry limit: ${nearRetryLimit}`);
|
||||
lines.push(" by source:");
|
||||
for (const source of ["explicit", "manual", "compaction"] as LongTermSource[]) {
|
||||
lines.push(` ${source}: ${pendingBySource.get(source) ?? 0}`);
|
||||
}
|
||||
lines.push("");
|
||||
|
||||
lines.push("By type:");
|
||||
for (const type of TYPES) {
|
||||
const storedCount = active.filter(entry => entry.type === type).length;
|
||||
const renderedCount = renderedEntries.filter(entry => entry.type === type).length;
|
||||
const supersededCount = superseded.filter(entry => entry.type === type).length;
|
||||
lines.push(` ${type.padEnd(9)} stored=${String(storedCount).padEnd(3)} rendered=${String(renderedCount).padEnd(3)} typeCap=${RETENTION_TYPE_MAX[type]} superseded=${supersededCount}`);
|
||||
}
|
||||
lines.push("");
|
||||
|
||||
lines.push("Retention caps:");
|
||||
lines.push(` type-capped entries: ${retention.typeCapped.length}`);
|
||||
lines.push(` global-cap overflow: ${retention.globalCapped.length}`);
|
||||
lines.push("");
|
||||
|
||||
const olderThan30 = active.filter(entry => (ageDays(entry, input.now) ?? 0) > 30).length;
|
||||
const olderThan90 = active.filter(entry => (ageDays(entry, input.now) ?? 0) > 90).length;
|
||||
const staleMarked = active.filter(entry => {
|
||||
const days = ageDays(entry, input.now);
|
||||
return Boolean(entry.staleAfterDays && days !== null && days > entry.staleAfterDays);
|
||||
}).length;
|
||||
lines.push("Age:");
|
||||
lines.push(` stale-marked: ${staleMarked}`);
|
||||
lines.push(` older than 30d: ${olderThan30}`);
|
||||
lines.push(` older than 90d: ${olderThan90}`);
|
||||
lines.push("");
|
||||
|
||||
const wallDaysSinceActivity = daysSinceIso(store.lastActivityAt, input.now);
|
||||
const dormantDiscountActive = wallDaysSinceActivity !== null && wallDaysSinceActivity > WORKSPACE_DORMANT_AFTER_DAYS;
|
||||
const dormantDaysPastGrace = wallDaysSinceActivity === null
|
||||
? 0
|
||||
: Math.max(0, wallDaysSinceActivity - WORKSPACE_DORMANT_AFTER_DAYS);
|
||||
lines.push("Dormancy:");
|
||||
lines.push(` lastActivityAt: ${store.lastActivityAt ?? "(missing)"}`);
|
||||
lines.push(` wall days since activity: ${wallDaysSinceActivity === null ? "unknown" : wallDaysSinceActivity.toFixed(1)}`);
|
||||
lines.push(` dormant discount active: ${dormantDiscountActive ? "yes" : "no"}`);
|
||||
lines.push(` dormant days past grace: ${dormantDaysPastGrace.toFixed(1)}`);
|
||||
lines.push(` dormant multiplier: ${DORMANT_DECAY_MULTIPLIER}`);
|
||||
lines.push("");
|
||||
|
||||
const highImportanceCount = active.filter(entry => entry.userImportance === "high").length;
|
||||
const safetyCriticalCount = active.filter(isSafetyCriticalForDiag).length;
|
||||
const maxReinforcedCount = active.filter(entry => (entry.reinforcementCount ?? 0) >= 6).length;
|
||||
const highImportanceRatio = active.length === 0 ? 0 : highImportanceCount / active.length;
|
||||
const maxReinforcedRatio = active.length === 0 ? 0 : maxReinforcedCount / active.length;
|
||||
const highImportanceAlert = highImportanceRatio > 0.3;
|
||||
const safetyCriticalWarning = safetyCriticalCount > 0;
|
||||
const maxReinforcedAlert = maxReinforcedRatio > 0.1;
|
||||
lines.push("Retention monitoring:");
|
||||
lines.push(` high_importance_ratio: ${formatPercent(highImportanceRatio)} (alert > 30%)${highImportanceAlert ? " ALERT" : ""}`);
|
||||
lines.push(` safety_critical_count: ${safetyCriticalCount} (deprecated field)${safetyCriticalWarning ? " WARNING" : ""}`);
|
||||
lines.push(` max_reinforced_count: ${maxReinforcedAlert ? `${maxReinforcedCount} (${formatPercent(maxReinforcedRatio)}, alert > 10%) ALERT` : `${maxReinforcedCount} (alert > 10% active)`}`);
|
||||
lines.push("");
|
||||
|
||||
const qualityByEntry = active.map(entry => ({ entry, quality: assessMemoryQuality(entry) }));
|
||||
const duplicateCounts = countBy(active.map(entry => `${entry.type}:${canonicalMemoryText(entry.text)}`));
|
||||
const duplicateExtras = [...duplicateCounts.values()].reduce((sum, count) => sum + Math.max(0, count - 1), 0);
|
||||
lines.push("Quality warnings:");
|
||||
lines.push(` progress-like active memories: ${qualityByEntry.filter(item => item.quality.reasons.includes("progress_snapshot")).length}`);
|
||||
lines.push(` path-heavy active memories: ${qualityByEntry.filter(item => item.quality.reasons.includes("path_heavy")).length}`);
|
||||
lines.push(` duplicate-ish exact canonical text: ${duplicateExtras}`);
|
||||
lines.push(` very long entries: ${active.filter(entry => entry.text.length > LONG_TERM_LIMITS.maxEntryTextChars).length}`);
|
||||
lines.push("");
|
||||
|
||||
lines.push("Suspicious active memories:");
|
||||
for (const reason of SUSPICIOUS_REASONS) {
|
||||
lines.push(` ${reason}-like: ${qualityByEntry.filter(item => item.quality.reasons.includes(reason)).length}`);
|
||||
}
|
||||
|
||||
const failingQuality = qualityByEntry.filter(item => !item.quality.accepted);
|
||||
if (failingQuality.length > 0) {
|
||||
lines.push("");
|
||||
lines.push("Active memories failing offline quality checks:");
|
||||
for (const item of failingQuality.slice(0, 8)) {
|
||||
lines.push(` - [${item.entry.type}] reasons=${item.quality.reasons.join(",")} ${JSON.stringify(truncate(cleanText(item.entry.text, input.raw)))}`);
|
||||
}
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
lines.push("Top rendered candidates:");
|
||||
const top = retention.rendered.slice(0, 5);
|
||||
if (top.length === 0) {
|
||||
lines.push(" (none)");
|
||||
} else {
|
||||
for (const item of top) {
|
||||
lines.push(` - strength=${formatStrength(item.strength)} [${item.entry.type}] ${truncate(cleanText(item.entry.text, input.raw))}`);
|
||||
}
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
lines.push("Weakest active memories:");
|
||||
const weakest = retention.sorted.slice(-5).reverse();
|
||||
if (weakest.length === 0) {
|
||||
lines.push(" (none)");
|
||||
} else {
|
||||
for (const item of weakest) {
|
||||
lines.push(` - strength=${formatStrength(item.strength)} [${item.entry.type}] ${truncate(cleanText(item.entry.text, input.raw))}`);
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
import { buildDisappearancesJSON, formatDisappearances, type DisappearanceRows } from "./disappearances.ts";
|
||||
import type { disappearanceRows } from "../inspection-model.ts";
|
||||
import { eventCounts } from "../inspection-model.ts";
|
||||
import { formatDetails } from "../text.ts";
|
||||
|
||||
export type DisappearanceRows = ReturnType<typeof disappearanceRows>;
|
||||
|
||||
export type MissingSummary = {
|
||||
total: number;
|
||||
@@ -21,6 +25,46 @@ export function buildMissingJSON(rows: DisappearanceRows, options: { explain?: b
|
||||
};
|
||||
}
|
||||
|
||||
function buildDisappearancesJSON(rows: DisappearanceRows, options: { explain?: boolean; generatedAt?: string } = {}): Record<string, unknown> {
|
||||
return {
|
||||
version: 1,
|
||||
generatedAt: options.generatedAt ?? new Date().toISOString(),
|
||||
disappearances: rows.map(row => ({
|
||||
id: row.id,
|
||||
classification: row.classification,
|
||||
terminalType: row.terminalType,
|
||||
reasonCodes: row.reasonCodes,
|
||||
eventCounts: eventCounts(row.events),
|
||||
details: options.explain ? row.event?.details : undefined,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function formatDisappearances(rows: DisappearanceRows, options: { explain?: boolean } = {}): string {
|
||||
const lines: string[] = [];
|
||||
lines.push("Memory disappearances");
|
||||
lines.push("");
|
||||
if (rows.length === 0) {
|
||||
lines.push("No evidence-only memories found.");
|
||||
return lines.join("\n");
|
||||
}
|
||||
for (const row of rows) {
|
||||
const reasons = row.reasonCodes.length > 0 ? row.reasonCodes.join(",") : "none";
|
||||
lines.push(`Memory ${row.id}: ${row.classification} terminal=${row.terminalType} reasons=${reasons}`);
|
||||
if (options.explain) {
|
||||
lines.push(` events: ${row.events.map(event => event.type).join(", ")}`);
|
||||
if (row.event?.type === "memory_removed_capacity") {
|
||||
lines.push(` memory_removed_capacity details: ${formatDetails(row.event.details)}`);
|
||||
}
|
||||
const renderTypeCap = row.events.find(event => event.type === "render_omitted" && event.reasonCodes.includes("type_cap"));
|
||||
if (renderTypeCap) {
|
||||
lines.push(` render_omitted type-cap observation: reasons=${renderTypeCap.reasonCodes.join(",")} details=${formatDetails(renderTypeCap.details)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
export function formatMissing(rows: DisappearanceRows, options: { verbose?: boolean; explain?: boolean } = {}): string {
|
||||
const summary = missingSummary(rows);
|
||||
const lines: string[] = [];
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
import { RETENTION_TYPE_MAX } from "../../../src/retention.ts";
|
||||
import { TYPES } from "../constants.ts";
|
||||
import { disappearanceRows } from "../inspection-model.ts";
|
||||
import { retentionCandidatesForDiag, retentionClockSummary } from "../retention-model.ts";
|
||||
import { rejectionFalsePositiveRisk, rejectionQualitySummary } from "../rejections-model.ts";
|
||||
import type { MemoryInspectionReadModel } from "../types.ts";
|
||||
|
||||
export function buildQualityJSON(model: MemoryInspectionReadModel, generatedAt = new Date().toISOString(), now = new Date(generatedAt).getTime()): Record<string, unknown> {
|
||||
const active = model.store.entries.filter(entry => entry.status !== "superseded");
|
||||
const retention = retentionCandidatesForDiag(model.store, now);
|
||||
const clocks = retentionClockSummary(active);
|
||||
const disappearances = disappearanceRows(model);
|
||||
const evidenceCovered = active.filter(entry => (model.evidenceByMemoryId.get(entry.id) ?? []).length > 0).length;
|
||||
const rejectionSummary = rejectionQualitySummary(model.rejectionRecords);
|
||||
const falsePositiveRisk = rejectionFalsePositiveRisk(rejectionSummary);
|
||||
const typeCounts = Object.fromEntries(TYPES.map(type => [type, active.filter(entry => entry.type === type).length]));
|
||||
const capsFull = active.length >= model.store.limits.maxEntries || TYPES.some(type => (typeCounts[type] ?? 0) >= RETENTION_TYPE_MAX[type]);
|
||||
const unknownDisappearances = disappearances.filter(row => row.classification === "historical_absent_unknown_reason").length;
|
||||
const status = unknownDisappearances > 0 || clocks.invalid > 0
|
||||
? "degraded"
|
||||
: capsFull || rejectionSummary.legacyUnscopedCount > 0 || falsePositiveRisk === "high"
|
||||
? "warning"
|
||||
: "ok";
|
||||
const summaryText = `Summary: Workspace memory quality is ${status}: ${active.length} active memories, ${evidenceCovered}/${active.length} with evidence, ${disappearances.length} evidence-only disappearances (${unknownDisappearances} unknown), ${clocks.invalid} invalid retention clocks, and ${rejectionSummary.legacyUnscopedCount} legacy unscoped rejection records.`;
|
||||
const caps = {
|
||||
active: active.length,
|
||||
maxEntries: model.store.limits.maxEntries,
|
||||
rendered: retention.rendered.length,
|
||||
typeCapped: retention.typeCapped.length,
|
||||
globalCapped: retention.globalCapped.length,
|
||||
typeCounts,
|
||||
capsFull,
|
||||
};
|
||||
const evidence = {
|
||||
currentWithEvidence: evidenceCovered,
|
||||
currentWithoutEvidence: active.length - evidenceCovered,
|
||||
evidenceMemoryIds: model.evidenceByMemoryId.size,
|
||||
disappearances: disappearances.length,
|
||||
unknownDisappearances,
|
||||
withTerminalReason: disappearances.length - unknownDisappearances,
|
||||
};
|
||||
|
||||
return {
|
||||
version: 1,
|
||||
generatedAt,
|
||||
status,
|
||||
summaryText,
|
||||
store: { active: active.length, pending: model.pending.entries.length, superseded: model.store.entries.length - active.length },
|
||||
caps,
|
||||
retention: clocks,
|
||||
evidence,
|
||||
rejections: { ...rejectionSummary, falsePositiveRisk },
|
||||
};
|
||||
}
|
||||
|
||||
export function formatQuality(model: MemoryInspectionReadModel, now: number): string {
|
||||
const data = buildQualityJSON(model, new Date(now).toISOString(), now) as {
|
||||
summaryText: string;
|
||||
caps: {
|
||||
active: number;
|
||||
maxEntries: number;
|
||||
rendered: number;
|
||||
typeCapped: number;
|
||||
globalCapped: number;
|
||||
typeCounts: Record<string, number>;
|
||||
capsFull: boolean;
|
||||
};
|
||||
retention: { present: number; missing: number; invalid: number };
|
||||
evidence: {
|
||||
currentWithEvidence: number;
|
||||
currentWithoutEvidence: number;
|
||||
evidenceMemoryIds: number;
|
||||
disappearances: number;
|
||||
unknownDisappearances: number;
|
||||
};
|
||||
rejections: {
|
||||
totalRecords: number;
|
||||
workspaceScopedCount: number;
|
||||
legacyUnscopedCount: number;
|
||||
falsePositiveRisk: string;
|
||||
};
|
||||
};
|
||||
const lines: string[] = [];
|
||||
lines.push("Memory quality inspection");
|
||||
lines.push("");
|
||||
lines.push(data.summaryText);
|
||||
lines.push("");
|
||||
lines.push("Caps:");
|
||||
lines.push(` active: ${data.caps.active} / ${data.caps.maxEntries}`);
|
||||
for (const type of TYPES) {
|
||||
const count = data.caps.typeCounts[type] ?? 0;
|
||||
const limit = RETENTION_TYPE_MAX[type];
|
||||
const marker = count >= limit ? " FULL" : "";
|
||||
lines.push(` ${type}: ${count} / ${limit}${marker}`);
|
||||
}
|
||||
lines.push(` rendered: ${data.caps.rendered}`);
|
||||
lines.push(` type-capped entries: ${data.caps.typeCapped}`);
|
||||
lines.push(` global-cap overflow: ${data.caps.globalCapped}`);
|
||||
lines.push(` caps full: ${data.caps.capsFull ? "yes" : "no"}`);
|
||||
lines.push("");
|
||||
lines.push("Retention clocks:");
|
||||
lines.push(` present: ${data.retention.present}`);
|
||||
lines.push(` missing: ${data.retention.missing}`);
|
||||
lines.push(` invalid: ${data.retention.invalid}`);
|
||||
lines.push("");
|
||||
lines.push("Evidence:");
|
||||
lines.push(` current with evidence: ${data.evidence.currentWithEvidence}`);
|
||||
lines.push(` current without evidence: ${data.evidence.currentWithoutEvidence}`);
|
||||
lines.push(` evidence memory ids: ${data.evidence.evidenceMemoryIds}`);
|
||||
lines.push(` disappearances: ${data.evidence.disappearances}`);
|
||||
lines.push(` unknown disappearances: ${data.evidence.unknownDisappearances}`);
|
||||
lines.push("");
|
||||
lines.push("Rejection scoping:");
|
||||
lines.push(` total records: ${data.rejections.totalRecords}`);
|
||||
lines.push(` workspace scoped: ${data.rejections.workspaceScopedCount}`);
|
||||
lines.push(` legacy unscoped: ${data.rejections.legacyUnscopedCount}`);
|
||||
lines.push(` false-positive risk: ${data.rejections.falsePositiveRisk}`);
|
||||
return lines.join("\n");
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
import { hasSoftReason, rejectionQualitySummary } from "../rejections-model.ts";
|
||||
import { cleanPath, cleanText, countBy, formatWorkspaceIdentity, sortedCounts, truncate } from "../text.ts";
|
||||
import type { NormalizedRejection } from "../types.ts";
|
||||
|
||||
export type RejectionQualitySummary = ReturnType<typeof rejectionQualitySummary>;
|
||||
|
||||
export function buildRejectionQualityJSON(summary: RejectionQualitySummary, generatedAt = new Date().toISOString()): Record<string, unknown> {
|
||||
return {
|
||||
version: 1,
|
||||
generatedAt,
|
||||
...summary,
|
||||
};
|
||||
}
|
||||
|
||||
export function formatRejections(input: {
|
||||
path: string;
|
||||
invalidLines: number;
|
||||
records: NormalizedRejection[];
|
||||
raw: boolean;
|
||||
}): string {
|
||||
const lines: string[] = [];
|
||||
lines.push("Extraction rejection summary");
|
||||
lines.push("");
|
||||
lines.push(`logPath=${cleanPath(input.path, input.raw)}`);
|
||||
if (input.invalidLines > 0) lines.push(`Invalid JSONL lines skipped: ${input.invalidLines}`);
|
||||
lines.push("");
|
||||
lines.push(`Total rejected: ${input.records.length}`);
|
||||
lines.push("");
|
||||
|
||||
lines.push("By reason:");
|
||||
const byReason = sortedCounts(countBy(input.records.flatMap(record => record.reasons)));
|
||||
if (byReason.length === 0) lines.push(" (none)");
|
||||
else for (const [reason, count] of byReason) lines.push(` ${reason.padEnd(24)} ${count}`);
|
||||
lines.push("");
|
||||
|
||||
lines.push("By origin:");
|
||||
const byOrigin = sortedCounts(countBy(input.records.map(record => record.origin)));
|
||||
if (byOrigin.length === 0) lines.push(" (none)");
|
||||
else for (const [origin, count] of byOrigin) lines.push(` ${origin.padEnd(24)} ${count}`);
|
||||
lines.push("");
|
||||
|
||||
lines.push("Trigger-origin rejections (high priority for v1.5):");
|
||||
const triggerReasons = sortedCounts(countBy(input.records.filter(record => record.fromTrigger || record.origin === "explicit_trigger").flatMap(record => record.reasons)));
|
||||
if (triggerReasons.length === 0) lines.push(" (none)");
|
||||
else for (const [reason, count] of triggerReasons) lines.push(` ${reason.padEnd(24)} ${count}`);
|
||||
lines.push("");
|
||||
|
||||
lines.push("Recent suspicious soft rejects:");
|
||||
const suspicious = input.records
|
||||
.filter(hasSoftReason)
|
||||
.sort((a, b) => (new Date(b.timestamp).getTime() || 0) - (new Date(a.timestamp).getTime() || 0))
|
||||
.slice(0, 8);
|
||||
if (suspicious.length === 0) {
|
||||
lines.push(" (none)");
|
||||
} else {
|
||||
for (const record of suspicious) {
|
||||
const identity = formatWorkspaceIdentity(record.workspaceKey, record.workspaceRoot, input.raw);
|
||||
lines.push(` - [${record.type}] ${JSON.stringify(truncate(cleanText(record.text, input.raw)))}`);
|
||||
lines.push(` reasons: ${record.reasons.join(",")}`);
|
||||
lines.push(` origin: ${record.origin}${identity ? ` (${identity})` : ""}`);
|
||||
}
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
export function formatRejectionQuality(input: {
|
||||
path: string;
|
||||
invalidLines: number;
|
||||
summary: RejectionQualitySummary;
|
||||
raw: boolean;
|
||||
}): string {
|
||||
const lines: string[] = [];
|
||||
lines.push("Extraction rejection quality inspection");
|
||||
lines.push("");
|
||||
lines.push("Possible false-positive grouping is heuristic, not deterministic truth.");
|
||||
lines.push(`logPath=${cleanPath(input.path, input.raw)}`);
|
||||
if (input.invalidLines > 0) lines.push(`Invalid JSONL lines skipped: ${input.invalidLines}`);
|
||||
lines.push("");
|
||||
lines.push(`Total records: ${input.summary.totalRecords}`);
|
||||
lines.push(`Unique texts: ${input.summary.uniqueTexts}`);
|
||||
lines.push(`Workspace scoped: ${input.summary.workspaceScopedCount}`);
|
||||
lines.push(`Legacy unscoped: ${input.summary.legacyUnscopedCount}`);
|
||||
lines.push("");
|
||||
lines.push("Reason distribution (raw records):");
|
||||
for (const [reason, count] of Object.entries(input.summary.reasonDistribution)) lines.push(` ${reason.padEnd(36)} ${count}`);
|
||||
if (Object.keys(input.summary.reasonDistribution).length === 0) lines.push(" (none)");
|
||||
lines.push("");
|
||||
lines.push("Reason distribution (unique text):");
|
||||
for (const [reason, count] of Object.entries(input.summary.uniqueReasonDistribution)) lines.push(` ${reason.padEnd(36)} ${count}`);
|
||||
if (Object.keys(input.summary.uniqueReasonDistribution).length === 0) lines.push(" (none)");
|
||||
lines.push("");
|
||||
lines.push("Possible false-positive groups (heuristic, not deterministic):");
|
||||
for (const [group, data] of Object.entries(input.summary.possibleFalsePositiveGroups)) {
|
||||
lines.push(` ${group}: ${data.count}`);
|
||||
for (const sample of data.samples) lines.push(` - ${JSON.stringify(cleanText(sample, input.raw))}`);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { EvidenceEventType, EvidenceEventV1, EvidenceOutcome } from "../../src/evidence-log.ts";
|
||||
import type { LongTermMemoryEntry, LongTermSource, LongTermType, PendingMemoryJournalStore, WorkspaceMemoryStore } from "../../src/types.ts";
|
||||
import type { Command } from "./command-metadata.ts";
|
||||
|
||||
export type { Command, HiddenCommand, VisibleCommand } from "./command-metadata.ts";
|
||||
|
||||
export type MemoryRenderStatus =
|
||||
| "rendered"
|
||||
@@ -47,8 +50,6 @@ export type MemoryDiagJSON = {
|
||||
}>;
|
||||
};
|
||||
|
||||
export type Command = "status" | "rejected" | "missing" | "explain" | "coverage" | "audit";
|
||||
export type LegacyCommand = "health" | "quality" | "rejections" | "disappearances" | "trace";
|
||||
export type Origin = "explicit_trigger" | "compaction_candidate" | "manual" | "migration_check" | "unknown";
|
||||
|
||||
export type CliOptions = {
|
||||
@@ -61,14 +62,11 @@ export type CliOptions = {
|
||||
softOnly?: boolean;
|
||||
triggerOnly?: boolean;
|
||||
includeHistorical?: boolean;
|
||||
quality?: boolean;
|
||||
reason?: string;
|
||||
unique?: boolean;
|
||||
explain?: boolean;
|
||||
since?: string;
|
||||
migration?: string;
|
||||
memory?: string;
|
||||
legacyCommand?: LegacyCommand;
|
||||
positional?: string[];
|
||||
auditMode?: "coverage" | "migrations";
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user