@remnic/core 10.0.9 → 10.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-admin-ops-surface.js +14 -14
- package/dist/access-authorization-probe.js +15 -15
- package/dist/access-boundary.js +14 -14
- package/dist/access-cli.js +52 -52
- package/dist/access-extraction-force-flush.js +14 -14
- package/dist/access-http-briefing.js +15 -15
- package/dist/access-http-query.js +16 -16
- package/dist/access-http.js +30 -30
- package/dist/access-identity-continuity-surface.js +14 -14
- package/dist/access-lcm-surface.js +14 -14
- package/dist/access-mcp.js +23 -23
- package/dist/access-namespace-preflight.js +14 -14
- package/dist/access-observe-write-surface.js +14 -14
- package/dist/access-operations-batch.js +15 -15
- package/dist/access-operations.js +22 -22
- package/dist/access-recall-concurrency.js +14 -14
- package/dist/access-recall-response.js +14 -14
- package/dist/access-recall-surface.js +14 -14
- package/dist/access-service.js +14 -14
- package/dist/access-surface-catalog.js +16 -16
- package/dist/active-recall.js +1 -1
- package/dist/briefing.js +2 -2
- package/dist/causal-consolidation.js +3 -3
- package/dist/{chunk-ZQYAE2C5.js → chunk-2U4BM3TW.js} +2 -2
- package/dist/{chunk-QFIPSKPC.js → chunk-3ZCRHW7U.js} +2 -2
- package/dist/{chunk-76KHEHT2.js → chunk-4N2MFNTG.js} +2 -2
- package/dist/{chunk-FBVLTTVV.js → chunk-4TP3MJ3D.js} +5 -5
- package/dist/{chunk-JSIQSZXI.js → chunk-56GJLXBA.js} +3 -3
- package/dist/{chunk-MVRIW6MI.js → chunk-6OAXWYE3.js} +2 -2
- package/dist/{chunk-KDYQYMJZ.js → chunk-7SUWY37K.js} +2 -2
- package/dist/{chunk-KRIEQCKE.js → chunk-A2L3P2IY.js} +2 -2
- package/dist/{chunk-AMFCRZYB.js → chunk-AV2OQF2O.js} +2 -2
- package/dist/{chunk-WJLGUY27.js → chunk-BD5QEP3T.js} +2 -1
- package/dist/{chunk-C3WJNRGR.js → chunk-BH44JOLA.js} +2 -2
- package/dist/{chunk-NYVKLB2I.js → chunk-BX4WDBTE.js} +11 -11
- package/dist/{chunk-V4WSQYO6.js → chunk-CAUNCRLQ.js} +21 -21
- package/dist/{chunk-RH6QEGCT.js → chunk-D5UPBXZM.js} +2 -2
- package/dist/{chunk-77HNHT6S.js → chunk-E57LYSHI.js} +114 -4
- package/dist/chunk-E57LYSHI.js.map +1 -0
- package/dist/{chunk-IOM5QSNR.js → chunk-EKKAHJ4B.js} +2 -2
- package/dist/{chunk-JK3DYMOC.js → chunk-GKOML66M.js} +2 -2
- package/dist/{chunk-QW7G7PWN.js → chunk-I2M4ZVDD.js} +2 -2
- package/dist/{chunk-JTUKDLAD.js → chunk-J6FFBORM.js} +2 -2
- package/dist/{chunk-MAJ2OKE4.js → chunk-J7MDPTX7.js} +6 -6
- package/dist/{chunk-5FNVK7MW.js → chunk-JVNEXW6X.js} +2 -2
- package/dist/{chunk-IJWEXCWF.js → chunk-JXWTPOMJ.js} +2 -2
- package/dist/{chunk-3R6SLDTH.js → chunk-KJOGIRMF.js} +2 -2
- package/dist/{chunk-GDI64ITR.js → chunk-KWVI52ER.js} +2 -2
- package/dist/{chunk-AV46CT77.js → chunk-KX36OCGK.js} +2 -2
- package/dist/{chunk-2K7D6EFT.js → chunk-L3G5CGL5.js} +674 -126
- package/dist/chunk-L3G5CGL5.js.map +1 -0
- package/dist/{chunk-CSUAHUOH.js → chunk-MK552I5J.js} +2 -2
- package/dist/{chunk-BPY4RRTF.js → chunk-MO25AN7J.js} +10 -10
- package/dist/{chunk-NAHUBSUF.js → chunk-N75Q5ZSP.js} +2 -2
- package/dist/{chunk-HZVYDWUP.js → chunk-NQMG2W5L.js} +2 -2
- package/dist/{chunk-L3Z4NXDO.js → chunk-O52WWLKU.js} +3 -3
- package/dist/{chunk-OC6JYPZX.js → chunk-O5MW6CQL.js} +2 -2
- package/dist/{chunk-6PHB45IU.js → chunk-O6YDGTH4.js} +2 -2
- package/dist/{chunk-E5274WUC.js → chunk-OAIWPB2I.js} +19 -2
- package/dist/chunk-OAIWPB2I.js.map +1 -0
- package/dist/{chunk-5NOPZYNI.js → chunk-OFDIGTC2.js} +7 -7
- package/dist/{chunk-5EOV7ANU.js → chunk-OGWEWJRH.js} +2 -2
- package/dist/{chunk-3ENX2RB4.js → chunk-OM3RWZQK.js} +2 -2
- package/dist/{chunk-GSAZBZMA.js → chunk-PDVWGGX4.js} +2 -2
- package/dist/{chunk-N44GZ73R.js → chunk-SOJ3IQD3.js} +3 -3
- package/dist/{chunk-4OKPH6FG.js → chunk-SQA2MM2F.js} +2 -2
- package/dist/{chunk-7O252IE6.js → chunk-SSAXGPXY.js} +2 -2
- package/dist/{chunk-R2AWYR4Q.js → chunk-SVDU4OHO.js} +2 -2
- package/dist/{chunk-WPFSZZ35.js → chunk-UOA2VQ7C.js} +2 -2
- package/dist/{chunk-S4GUMZRB.js → chunk-VA2HNBGQ.js} +17 -17
- package/dist/{chunk-YAF3NRNC.js → chunk-VBJD4RSJ.js} +2 -2
- package/dist/{chunk-A6G7K273.js → chunk-VDXTXVFZ.js} +3 -3
- package/dist/{chunk-5Q4NQNW7.js → chunk-VJGDQA3D.js} +2 -2
- package/dist/{chunk-QN77HA6P.js → chunk-WCTL3PVM.js} +2 -2
- package/dist/{chunk-NXLQQ6WI.js → chunk-WH4QXVKU.js} +2 -2
- package/dist/{chunk-3VTNNO2B.js → chunk-WXFKXCOQ.js} +3 -3
- package/dist/{chunk-K3DZ3G5I.js → chunk-XQIHFXYP.js} +2 -2
- package/dist/{chunk-VSHRMIJH.js → chunk-YYNHJF3N.js} +2 -2
- package/dist/cli.js +40 -40
- package/dist/coding/export-okf-codegraph.js +1 -1
- package/dist/compounding/engine.d.ts +1 -5
- package/dist/compounding/engine.d.ts.map +1 -1
- package/dist/compounding/engine.js +5 -3
- package/dist/compounding/iso-week.d.ts +18 -0
- package/dist/compounding/iso-week.d.ts.map +1 -0
- package/dist/compounding/weekly-evidence.d.ts +123 -0
- package/dist/compounding/weekly-evidence.d.ts.map +1 -0
- package/dist/config.js +1 -1
- package/dist/connectors/codex-materialize-runner.js +2 -2
- package/dist/connectors/index.js +2 -2
- package/dist/convergence-refresh.js +4 -4
- package/dist/corpus-watermark.js +4 -4
- package/dist/entity-retrieval.js +2 -2
- package/dist/external-wiki-access.js +15 -15
- package/dist/extraction.js +2 -2
- package/dist/harmonic-retrieval.js +2 -2
- package/dist/index.js +52 -52
- package/dist/location/index.js +4 -4
- package/dist/maintenance/memory-governance.js +2 -2
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +2 -2
- package/dist/maintenance/rebuild-memory-projection-runner.js +4 -4
- package/dist/maintenance/rebuild-memory-projection.js +3 -3
- package/dist/memory-browse-operations.js +15 -15
- package/dist/memory-browse.js +2 -2
- package/dist/namespaces/migrate.js +3 -3
- package/dist/namespaces/storage.js +2 -2
- package/dist/operator-doctor-corpus.js +5 -5
- package/dist/operator-toolkit.js +10 -10
- package/dist/orchestration/generic-recall-paths.d.ts.map +1 -1
- package/dist/orchestration/maintenance.js +11 -11
- package/dist/orchestrator.js +52 -52
- package/dist/recall-navigation-operations.js +15 -15
- package/dist/recall-navigation.js +14 -14
- package/dist/recall-state-view-admission.js +2 -2
- package/dist/recall-why-service.js +2 -2
- package/dist/resume-bundles.js +1 -1
- package/dist/semantic-consolidation.js +3 -3
- package/dist/semantic-rule-promotion.js +2 -2
- package/dist/semantic-rule-verifier.js +2 -2
- package/dist/shared-context/manager.d.ts +18 -0
- package/dist/shared-context/manager.d.ts.map +1 -1
- package/dist/shared-context/manager.js +7 -1
- package/dist/source-agent-qualifier.js +2 -2
- package/dist/standup/build.js +2 -2
- package/dist/storage.js +1 -1
- package/dist/support-passport/index.js +17 -17
- package/dist/transfer/export-okf.js +2 -2
- package/dist/verified-recall.js +2 -2
- package/dist/who-knows-cli.js +3 -3
- package/dist/who-knows.js +2 -2
- package/package.json +2 -2
- package/src/compounding/engine.ts +135 -130
- package/src/compounding/iso-week.ts +60 -0
- package/src/compounding/weekly-evidence.ts +698 -0
- package/src/orchestration/generic-recall-paths.ts +27 -1
- package/src/shared-context/manager.ts +132 -3
- package/dist/chunk-2K7D6EFT.js.map +0 -1
- package/dist/chunk-77HNHT6S.js.map +0 -1
- package/dist/chunk-E5274WUC.js.map +0 -1
- /package/dist/{chunk-ZQYAE2C5.js.map → chunk-2U4BM3TW.js.map} +0 -0
- /package/dist/{chunk-QFIPSKPC.js.map → chunk-3ZCRHW7U.js.map} +0 -0
- /package/dist/{chunk-76KHEHT2.js.map → chunk-4N2MFNTG.js.map} +0 -0
- /package/dist/{chunk-FBVLTTVV.js.map → chunk-4TP3MJ3D.js.map} +0 -0
- /package/dist/{chunk-JSIQSZXI.js.map → chunk-56GJLXBA.js.map} +0 -0
- /package/dist/{chunk-MVRIW6MI.js.map → chunk-6OAXWYE3.js.map} +0 -0
- /package/dist/{chunk-KDYQYMJZ.js.map → chunk-7SUWY37K.js.map} +0 -0
- /package/dist/{chunk-KRIEQCKE.js.map → chunk-A2L3P2IY.js.map} +0 -0
- /package/dist/{chunk-AMFCRZYB.js.map → chunk-AV2OQF2O.js.map} +0 -0
- /package/dist/{chunk-WJLGUY27.js.map → chunk-BD5QEP3T.js.map} +0 -0
- /package/dist/{chunk-C3WJNRGR.js.map → chunk-BH44JOLA.js.map} +0 -0
- /package/dist/{chunk-NYVKLB2I.js.map → chunk-BX4WDBTE.js.map} +0 -0
- /package/dist/{chunk-V4WSQYO6.js.map → chunk-CAUNCRLQ.js.map} +0 -0
- /package/dist/{chunk-RH6QEGCT.js.map → chunk-D5UPBXZM.js.map} +0 -0
- /package/dist/{chunk-IOM5QSNR.js.map → chunk-EKKAHJ4B.js.map} +0 -0
- /package/dist/{chunk-JK3DYMOC.js.map → chunk-GKOML66M.js.map} +0 -0
- /package/dist/{chunk-QW7G7PWN.js.map → chunk-I2M4ZVDD.js.map} +0 -0
- /package/dist/{chunk-JTUKDLAD.js.map → chunk-J6FFBORM.js.map} +0 -0
- /package/dist/{chunk-MAJ2OKE4.js.map → chunk-J7MDPTX7.js.map} +0 -0
- /package/dist/{chunk-5FNVK7MW.js.map → chunk-JVNEXW6X.js.map} +0 -0
- /package/dist/{chunk-IJWEXCWF.js.map → chunk-JXWTPOMJ.js.map} +0 -0
- /package/dist/{chunk-3R6SLDTH.js.map → chunk-KJOGIRMF.js.map} +0 -0
- /package/dist/{chunk-GDI64ITR.js.map → chunk-KWVI52ER.js.map} +0 -0
- /package/dist/{chunk-AV46CT77.js.map → chunk-KX36OCGK.js.map} +0 -0
- /package/dist/{chunk-CSUAHUOH.js.map → chunk-MK552I5J.js.map} +0 -0
- /package/dist/{chunk-BPY4RRTF.js.map → chunk-MO25AN7J.js.map} +0 -0
- /package/dist/{chunk-NAHUBSUF.js.map → chunk-N75Q5ZSP.js.map} +0 -0
- /package/dist/{chunk-HZVYDWUP.js.map → chunk-NQMG2W5L.js.map} +0 -0
- /package/dist/{chunk-L3Z4NXDO.js.map → chunk-O52WWLKU.js.map} +0 -0
- /package/dist/{chunk-OC6JYPZX.js.map → chunk-O5MW6CQL.js.map} +0 -0
- /package/dist/{chunk-6PHB45IU.js.map → chunk-O6YDGTH4.js.map} +0 -0
- /package/dist/{chunk-5NOPZYNI.js.map → chunk-OFDIGTC2.js.map} +0 -0
- /package/dist/{chunk-5EOV7ANU.js.map → chunk-OGWEWJRH.js.map} +0 -0
- /package/dist/{chunk-3ENX2RB4.js.map → chunk-OM3RWZQK.js.map} +0 -0
- /package/dist/{chunk-GSAZBZMA.js.map → chunk-PDVWGGX4.js.map} +0 -0
- /package/dist/{chunk-N44GZ73R.js.map → chunk-SOJ3IQD3.js.map} +0 -0
- /package/dist/{chunk-4OKPH6FG.js.map → chunk-SQA2MM2F.js.map} +0 -0
- /package/dist/{chunk-7O252IE6.js.map → chunk-SSAXGPXY.js.map} +0 -0
- /package/dist/{chunk-R2AWYR4Q.js.map → chunk-SVDU4OHO.js.map} +0 -0
- /package/dist/{chunk-WPFSZZ35.js.map → chunk-UOA2VQ7C.js.map} +0 -0
- /package/dist/{chunk-S4GUMZRB.js.map → chunk-VA2HNBGQ.js.map} +0 -0
- /package/dist/{chunk-YAF3NRNC.js.map → chunk-VBJD4RSJ.js.map} +0 -0
- /package/dist/{chunk-A6G7K273.js.map → chunk-VDXTXVFZ.js.map} +0 -0
- /package/dist/{chunk-5Q4NQNW7.js.map → chunk-VJGDQA3D.js.map} +0 -0
- /package/dist/{chunk-QN77HA6P.js.map → chunk-WCTL3PVM.js.map} +0 -0
- /package/dist/{chunk-NXLQQ6WI.js.map → chunk-WH4QXVKU.js.map} +0 -0
- /package/dist/{chunk-3VTNNO2B.js.map → chunk-WXFKXCOQ.js.map} +0 -0
- /package/dist/{chunk-K3DZ3G5I.js.map → chunk-XQIHFXYP.js.map} +0 -0
- /package/dist/{chunk-VSHRMIJH.js.map → chunk-YYNHJF3N.js.map} +0 -0
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Week-scoped inputs for compounding synthesis.
|
|
3
|
+
*
|
|
4
|
+
* Corrections, namespace action ledgers, and the feedback inbox are read here
|
|
5
|
+
* so CompoundingEngine stays inside its line ceiling. Scans are bounded:
|
|
6
|
+
* correction files outside the ISO week are skipped before they are read.
|
|
7
|
+
* Parsing stops after WEEKLY_CORRECTION_READ_LIMIT accepted corrections or
|
|
8
|
+
* WEEKLY_CORRECTION_SCAN_LIMIT opened files, whichever comes first. Visible
|
|
9
|
+
* store candidates are opened before other namespaces, so a foreign store
|
|
10
|
+
* cannot fill the accepted cap and hide an older default-store correction.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Stats } from "node:fs";
|
|
14
|
+
import { lstat, readFile, readdir, realpath } from "node:fs/promises";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { log } from "../logger.js";
|
|
17
|
+
import { namespaceIdentityFromToken } from "../namespaces/identity.js";
|
|
18
|
+
import { resolveDefaultNamespaceRoot } from "../namespaces/storage.js";
|
|
19
|
+
import { isSafeRouteNamespace } from "../routing/engine.js";
|
|
20
|
+
import { secureStoreDir } from "../secure-store/header.js";
|
|
21
|
+
import * as keyring from "../secure-store/keyring.js";
|
|
22
|
+
import { readMaybeEncryptedFile } from "../secure-store/secure-fs.js";
|
|
23
|
+
import { type SharedFeedbackEntry, SharedFeedbackEntrySchema } from "../shared-context/manager.js";
|
|
24
|
+
import { readRawMemoryDocument } from "../storage/memory-frontmatter-metadata.js";
|
|
25
|
+
import { readMaybeEncryptedLines } from "../storage/secure-line-reader.js";
|
|
26
|
+
import type { MemoryActionEvent, MemoryFile, PluginConfig } from "../types.js";
|
|
27
|
+
import { isErrnoCode } from "../utils/errno.js";
|
|
28
|
+
import { assertPathInsideRoot } from "../utils/path-containment.js";
|
|
29
|
+
import { isIsoWeekId, isoWeekId } from "./iso-week.js";
|
|
30
|
+
|
|
31
|
+
const DAY_MS = 86_400_000;
|
|
32
|
+
|
|
33
|
+
/** Accepted in-week corrections kept per synthesis. */
|
|
34
|
+
export const WEEKLY_CORRECTION_READ_LIMIT = 500;
|
|
35
|
+
|
|
36
|
+
/** Opened in-week correction files per synthesis, including ones policy rejects. */
|
|
37
|
+
export const WEEKLY_CORRECTION_SCAN_LIMIT = 2000;
|
|
38
|
+
|
|
39
|
+
/** Skip a correction file whose on-disk size exceeds this, before parsing. */
|
|
40
|
+
export const CORRECTION_FILE_BYTE_LIMIT = 256 * 1024;
|
|
41
|
+
|
|
42
|
+
const CORRECTION_NOTE_LIMIT = 240;
|
|
43
|
+
|
|
44
|
+
export type FeedbackEntryWithProvenance = {
|
|
45
|
+
entry: SharedFeedbackEntry;
|
|
46
|
+
sourceLine: number;
|
|
47
|
+
sourcePath: string;
|
|
48
|
+
entryId: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type WeeklyActionEvent = {
|
|
52
|
+
line: number;
|
|
53
|
+
action: string;
|
|
54
|
+
outcome: "applied" | "skipped" | "failed";
|
|
55
|
+
policyDecision: "deny" | "defer" | null;
|
|
56
|
+
namespace: string;
|
|
57
|
+
reason: string | null;
|
|
58
|
+
/** Stable ledger label recorded in outcome provenance. */
|
|
59
|
+
ledgerLabel: string;
|
|
60
|
+
actionId: string | null;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type WeeklyCorrectionEvidence = {
|
|
64
|
+
id: string;
|
|
65
|
+
namespace: string;
|
|
66
|
+
pattern: string;
|
|
67
|
+
note: string;
|
|
68
|
+
created: string;
|
|
69
|
+
source: string | null;
|
|
70
|
+
entityRef: string | null;
|
|
71
|
+
confidence: number | null;
|
|
72
|
+
tags: string[];
|
|
73
|
+
provenance: string;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type WeeklyEvidenceStorage = {
|
|
77
|
+
readMemoryByPath(filePath: string): Promise<MemoryFile | null>;
|
|
78
|
+
readMemoryActionEventRows(limit: number): Promise<Array<{ line: number; event: MemoryActionEvent }>>;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
type CorrectionCandidate = {
|
|
82
|
+
filePath: string;
|
|
83
|
+
sortMs: number;
|
|
84
|
+
namespace: string;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
type NamespaceDir = {
|
|
88
|
+
dirName: string;
|
|
89
|
+
namespace: string;
|
|
90
|
+
root: string;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** Monday 00:00 UTC of an ISO week id, or null when `weekId` is not one this module emits. */
|
|
94
|
+
export function isoWeekStartMs(weekId: string): number | null {
|
|
95
|
+
if (!isIsoWeekId(weekId)) return null;
|
|
96
|
+
const year = Number(weekId.slice(0, 4));
|
|
97
|
+
const week = Number(weekId.slice(6));
|
|
98
|
+
const jan4 = Date.UTC(year, 0, 4);
|
|
99
|
+
const jan4Day = new Date(jan4).getUTCDay() || 7;
|
|
100
|
+
const start = jan4 - (jan4Day - 1) * DAY_MS + (week - 1) * 7 * DAY_MS;
|
|
101
|
+
if (isoWeekId(new Date(start)) !== weekId) return null;
|
|
102
|
+
return start;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function readFeedbackEntriesForWeekFile(
|
|
106
|
+
inboxPath: string,
|
|
107
|
+
weekId: string
|
|
108
|
+
): Promise<FeedbackEntryWithProvenance[]> {
|
|
109
|
+
// Includes entries whose date falls in the ISO week.
|
|
110
|
+
const out: FeedbackEntryWithProvenance[] = [];
|
|
111
|
+
try {
|
|
112
|
+
const raw = await readFile(inboxPath, "utf-8");
|
|
113
|
+
const lines = raw.split("\n");
|
|
114
|
+
for (let idx = 0; idx < lines.length; idx += 1) {
|
|
115
|
+
const line = lines[idx];
|
|
116
|
+
if (!line.trim()) continue;
|
|
117
|
+
try {
|
|
118
|
+
const obj = JSON.parse(line);
|
|
119
|
+
const parsed = SharedFeedbackEntrySchema.safeParse(obj);
|
|
120
|
+
if (!parsed.success) continue;
|
|
121
|
+
const d = new Date(parsed.data.date);
|
|
122
|
+
if (!Number.isFinite(d.getTime())) continue;
|
|
123
|
+
if (isoWeekId(d) !== weekId) continue;
|
|
124
|
+
const sourceLine = idx + 1;
|
|
125
|
+
out.push({
|
|
126
|
+
entry: parsed.data,
|
|
127
|
+
sourceLine,
|
|
128
|
+
sourcePath: inboxPath,
|
|
129
|
+
entryId: `${parsed.data.agent}-${parsed.data.date}-${sourceLine}`.replace(/[^a-zA-Z0-9._:-]/g, "_"),
|
|
130
|
+
});
|
|
131
|
+
} catch {
|
|
132
|
+
// ignore a malformed line
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
// missing feedback is normal
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function buildActionFailurePatterns(
|
|
142
|
+
events: WeeklyActionEvent[]
|
|
143
|
+
): Array<{ pattern: string; provenance: string }> {
|
|
144
|
+
const out: Array<{ pattern: string; provenance: string }> = [];
|
|
145
|
+
for (const event of events) {
|
|
146
|
+
const failed = event.outcome === "failed" || event.outcome === "skipped";
|
|
147
|
+
if (!failed && event.policyDecision === null) continue;
|
|
148
|
+
const suffix = event.reason && event.reason.trim().length > 0 ? ` - ${event.reason.trim().slice(0, 140)}` : "";
|
|
149
|
+
out.push({
|
|
150
|
+
pattern: `memory-action/${event.namespace}: ${event.action} ${event.outcome}${event.policyDecision ? `/${event.policyDecision}` : ""}${suffix}`,
|
|
151
|
+
provenance: `${event.ledgerLabel}:*`,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function readWeeklyActionEvents(input: {
|
|
158
|
+
storage: WeeklyEvidenceStorage;
|
|
159
|
+
memoryDir: string;
|
|
160
|
+
weekId: string;
|
|
161
|
+
}): Promise<WeeklyActionEvent[]> {
|
|
162
|
+
const seenActionIds = new Set<string>();
|
|
163
|
+
const out: WeeklyActionEvent[] = [];
|
|
164
|
+
const rootRows = await input.storage.readMemoryActionEventRows(Number.MAX_SAFE_INTEGER);
|
|
165
|
+
for (const row of rootRows) {
|
|
166
|
+
pushActionEvent(out, seenActionIds, row.line, row.event, "default", "memory-actions.jsonl", input.weekId);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const rootReal = await realpath(input.memoryDir);
|
|
170
|
+
const namespaces = await listNamespaceStorageDirs(input.memoryDir, rootReal);
|
|
171
|
+
const key = keyring.getKey(secureStoreDir(input.memoryDir));
|
|
172
|
+
for (const ns of namespaces) {
|
|
173
|
+
const filePath = path.join(ns.root, "state", "memory-actions.jsonl");
|
|
174
|
+
const label = `namespaces/${ns.dirName}/state/memory-actions.jsonl`;
|
|
175
|
+
let entryStat: Stats;
|
|
176
|
+
try {
|
|
177
|
+
entryStat = await lstat(filePath);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
if (isErrnoCode(err, "ENOENT")) continue;
|
|
180
|
+
throw err;
|
|
181
|
+
}
|
|
182
|
+
if (entryStat.isSymbolicLink() || !entryStat.isFile()) continue;
|
|
183
|
+
const fileReal = await realpath(filePath);
|
|
184
|
+
assertPathInsideRoot(rootReal, fileReal, filePath);
|
|
185
|
+
try {
|
|
186
|
+
let lineNumber = 0;
|
|
187
|
+
for await (const row of readMaybeEncryptedLines(
|
|
188
|
+
filePath,
|
|
189
|
+
() => readMaybeEncryptedFile(filePath, key, input.memoryDir),
|
|
190
|
+
)) {
|
|
191
|
+
lineNumber += 1;
|
|
192
|
+
const line = row.trim();
|
|
193
|
+
if (line.length === 0) continue;
|
|
194
|
+
const event = parseMemoryActionEventLine(line);
|
|
195
|
+
if (!event) continue;
|
|
196
|
+
pushActionEvent(out, seenActionIds, lineNumber, event, ns.namespace, label, input.weekId);
|
|
197
|
+
}
|
|
198
|
+
} catch (err) {
|
|
199
|
+
if (isErrnoCode(err, "ENOENT")) continue;
|
|
200
|
+
throw err;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function readWeeklyCorrections(input: {
|
|
207
|
+
storage: WeeklyEvidenceStorage;
|
|
208
|
+
memoryDir: string;
|
|
209
|
+
weekId: string;
|
|
210
|
+
/** Flat root stays visible when this is null. A routed default store passes its `namespaces/<dir>/` prefix. */
|
|
211
|
+
defaultNamespacePrefix?: string | null;
|
|
212
|
+
}): Promise<WeeklyCorrectionEvidence[]> {
|
|
213
|
+
const weekStartMs = isoWeekStartMs(input.weekId);
|
|
214
|
+
if (weekStartMs === null) return [];
|
|
215
|
+
const rootReal = await realpath(input.memoryDir);
|
|
216
|
+
const candidates: CorrectionCandidate[] = [];
|
|
217
|
+
await collectCorrectionCandidates(candidates, input.memoryDir, "default", input.weekId, weekStartMs, rootReal);
|
|
218
|
+
const namespaces = await listNamespaceStorageDirs(input.memoryDir, rootReal);
|
|
219
|
+
for (const ns of namespaces) {
|
|
220
|
+
await collectCorrectionCandidates(candidates, ns.root, ns.namespace, input.weekId, weekStartMs, rootReal);
|
|
221
|
+
}
|
|
222
|
+
candidates.sort((a, b) => b.sortMs - a.sortMs || a.filePath.localeCompare(b.filePath));
|
|
223
|
+
const ordered = preferVisibleCorrectionCandidates(
|
|
224
|
+
candidates,
|
|
225
|
+
input.memoryDir,
|
|
226
|
+
input.defaultNamespacePrefix ?? null,
|
|
227
|
+
);
|
|
228
|
+
const out: WeeklyCorrectionEvidence[] = [];
|
|
229
|
+
let scanned = 0;
|
|
230
|
+
for (const candidate of ordered) {
|
|
231
|
+
if (out.length >= WEEKLY_CORRECTION_READ_LIMIT || scanned >= WEEKLY_CORRECTION_SCAN_LIMIT) break;
|
|
232
|
+
scanned += 1;
|
|
233
|
+
const evidence = await readCorrectionCandidate(input.storage, input.memoryDir, input.weekId, candidate);
|
|
234
|
+
if (evidence) out.push(evidence);
|
|
235
|
+
}
|
|
236
|
+
if (scanned < candidates.length) {
|
|
237
|
+
log.warn(
|
|
238
|
+
`weekly synthesis kept ${out.length} accepted corrections after scanning ${scanned} of ${candidates.length} in-week candidates`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
return out;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Newest-first within the visible store, then newest-first everywhere else. */
|
|
245
|
+
function preferVisibleCorrectionCandidates(
|
|
246
|
+
candidates: readonly CorrectionCandidate[],
|
|
247
|
+
memoryDir: string,
|
|
248
|
+
defaultNamespacePrefix: string | null,
|
|
249
|
+
): CorrectionCandidate[] {
|
|
250
|
+
const visible: CorrectionCandidate[] = [];
|
|
251
|
+
const foreign: CorrectionCandidate[] = [];
|
|
252
|
+
for (const candidate of candidates) {
|
|
253
|
+
const relative = path.relative(memoryDir, candidate.filePath).split(path.sep).join("/");
|
|
254
|
+
if (provenanceVisibleInStoreRecall([relative], defaultNamespacePrefix)) visible.push(candidate);
|
|
255
|
+
else foreign.push(candidate);
|
|
256
|
+
}
|
|
257
|
+
return visible.concat(foreign);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function parseMemoryActionEventLine(line: string): MemoryActionEvent | null {
|
|
261
|
+
let parsed: Partial<MemoryActionEvent> & { recordKind?: unknown };
|
|
262
|
+
try {
|
|
263
|
+
parsed = JSON.parse(line) as Partial<MemoryActionEvent> & { recordKind?: unknown };
|
|
264
|
+
} catch {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
if (parsed.recordKind !== undefined) return null;
|
|
268
|
+
const outcome = parsed.outcome === "applied" || parsed.outcome === "skipped" || parsed.outcome === "failed"
|
|
269
|
+
? parsed.outcome
|
|
270
|
+
: null;
|
|
271
|
+
const timestamp = parsed.timestamp;
|
|
272
|
+
const action = parsed.action;
|
|
273
|
+
if (typeof timestamp !== "string" || typeof action !== "string" || outcome === null) return null;
|
|
274
|
+
return { ...parsed, timestamp, action, outcome };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function pushActionEvent(
|
|
278
|
+
out: WeeklyActionEvent[],
|
|
279
|
+
seenActionIds: Set<string>,
|
|
280
|
+
line: number,
|
|
281
|
+
event: MemoryActionEvent,
|
|
282
|
+
fallbackNamespace: string,
|
|
283
|
+
ledgerLabel: string,
|
|
284
|
+
weekId: string
|
|
285
|
+
): void {
|
|
286
|
+
const ts = new Date(event.timestamp);
|
|
287
|
+
if (!Number.isFinite(ts.getTime()) || isoWeekId(ts) !== weekId) return;
|
|
288
|
+
const actionId = typeof event.actionId === "string" && event.actionId.length > 0 ? event.actionId : null;
|
|
289
|
+
if (actionId) {
|
|
290
|
+
if (seenActionIds.has(actionId)) return;
|
|
291
|
+
seenActionIds.add(actionId);
|
|
292
|
+
}
|
|
293
|
+
out.push({
|
|
294
|
+
line,
|
|
295
|
+
action: event.action,
|
|
296
|
+
outcome: event.outcome,
|
|
297
|
+
policyDecision: event.policyDecision === "deny" || event.policyDecision === "defer" ? event.policyDecision : null,
|
|
298
|
+
namespace: typeof event.namespace === "string" && event.namespace.length > 0 ? event.namespace : fallbackNamespace,
|
|
299
|
+
reason: typeof event.reason === "string" ? event.reason : null,
|
|
300
|
+
ledgerLabel,
|
|
301
|
+
actionId,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function listNamespaceStorageDirs(memoryDir: string, rootReal: string): Promise<NamespaceDir[]> {
|
|
306
|
+
const nsRoot = path.join(memoryDir, "namespaces");
|
|
307
|
+
let rootStat: Stats;
|
|
308
|
+
try {
|
|
309
|
+
rootStat = await lstat(nsRoot);
|
|
310
|
+
} catch (err) {
|
|
311
|
+
if (isErrnoCode(err, "ENOENT")) return [];
|
|
312
|
+
throw err;
|
|
313
|
+
}
|
|
314
|
+
// A symlinked namespaces directory can point outside the store. Reject the
|
|
315
|
+
// whole tree rather than following it.
|
|
316
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) return [];
|
|
317
|
+
const nsReal = await realpath(nsRoot);
|
|
318
|
+
assertPathInsideRoot(rootReal, nsReal, nsRoot);
|
|
319
|
+
const names = await readdir(nsRoot);
|
|
320
|
+
const out: NamespaceDir[] = [];
|
|
321
|
+
for (const dirName of names) {
|
|
322
|
+
const namespace = namespaceFromDirName(dirName);
|
|
323
|
+
if (!namespace) continue;
|
|
324
|
+
const root = path.join(nsRoot, dirName);
|
|
325
|
+
let entryStat: Stats;
|
|
326
|
+
try {
|
|
327
|
+
entryStat = await lstat(root);
|
|
328
|
+
} catch (err) {
|
|
329
|
+
if (isErrnoCode(err, "ENOENT")) continue;
|
|
330
|
+
throw err;
|
|
331
|
+
}
|
|
332
|
+
if (entryStat.isSymbolicLink() || !entryStat.isDirectory()) continue;
|
|
333
|
+
const dirReal = await realpath(root);
|
|
334
|
+
assertPathInsideRoot(rootReal, dirReal, root);
|
|
335
|
+
out.push({ dirName, namespace, root });
|
|
336
|
+
}
|
|
337
|
+
out.sort((a, b) => a.dirName.localeCompare(b.dirName));
|
|
338
|
+
return out;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function namespaceFromDirName(dirName: string): string | null {
|
|
342
|
+
if (dirName.length === 0 || dirName.includes("/") || dirName.includes("\\") || dirName.includes("..")) {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
if (dirName.startsWith("ns-")) {
|
|
346
|
+
const decoded = namespaceIdentityFromToken(dirName);
|
|
347
|
+
if (decoded === null) return null;
|
|
348
|
+
// `ns-default` is the empty identity. It is the migrated default store,
|
|
349
|
+
// not a foreign namespace.
|
|
350
|
+
if (decoded.length === 0) return "default";
|
|
351
|
+
return isSafeRouteNamespace(decoded) ? decoded : null;
|
|
352
|
+
}
|
|
353
|
+
return isSafeRouteNamespace(dirName) ? dirName : null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function collectCorrectionCandidates(
|
|
357
|
+
candidates: CorrectionCandidate[],
|
|
358
|
+
storageRoot: string,
|
|
359
|
+
namespace: string,
|
|
360
|
+
weekId: string,
|
|
361
|
+
weekStartMs: number,
|
|
362
|
+
rootReal: string
|
|
363
|
+
): Promise<void> {
|
|
364
|
+
await collectCorrectionDir(
|
|
365
|
+
candidates,
|
|
366
|
+
path.join(storageRoot, "corrections"),
|
|
367
|
+
namespace,
|
|
368
|
+
weekId,
|
|
369
|
+
weekStartMs,
|
|
370
|
+
rootReal
|
|
371
|
+
);
|
|
372
|
+
const factsDir = path.join(storageRoot, "facts");
|
|
373
|
+
const dayNames = await readContainedDirNames(factsDir, rootReal);
|
|
374
|
+
if (!dayNames) return;
|
|
375
|
+
for (const dayName of dayNames) {
|
|
376
|
+
if (!utcDayDirInWeek(dayName, weekId)) continue;
|
|
377
|
+
await collectCorrectionDir(candidates, path.join(factsDir, dayName), namespace, weekId, weekStartMs, rootReal);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function collectCorrectionDir(
|
|
382
|
+
candidates: CorrectionCandidate[],
|
|
383
|
+
dir: string,
|
|
384
|
+
namespace: string,
|
|
385
|
+
weekId: string,
|
|
386
|
+
weekStartMs: number,
|
|
387
|
+
rootReal: string
|
|
388
|
+
): Promise<void> {
|
|
389
|
+
const names = await readContainedDirNames(dir, rootReal);
|
|
390
|
+
if (!names) return;
|
|
391
|
+
for (const name of names) {
|
|
392
|
+
if (!name.startsWith("correction-") || !name.endsWith(".md")) continue;
|
|
393
|
+
// A timestamped name outside this week is historical. Skip it before
|
|
394
|
+
// lstat or realpath so a large corrections directory stays inside the
|
|
395
|
+
// weekly budget.
|
|
396
|
+
const filenameMs = correctionFilenameTimestampMs(name);
|
|
397
|
+
if (filenameMs !== null && isoWeekId(new Date(filenameMs)) !== weekId) continue;
|
|
398
|
+
const filePath = path.join(dir, name);
|
|
399
|
+
let entryStat: Stats;
|
|
400
|
+
try {
|
|
401
|
+
entryStat = await lstat(filePath);
|
|
402
|
+
} catch (err) {
|
|
403
|
+
if (isErrnoCode(err, "ENOENT")) continue;
|
|
404
|
+
throw err;
|
|
405
|
+
}
|
|
406
|
+
if (entryStat.isSymbolicLink() || !entryStat.isFile()) continue;
|
|
407
|
+
if (entryStat.size > CORRECTION_FILE_BYTE_LIMIT) continue;
|
|
408
|
+
const fileReal = await realpath(filePath);
|
|
409
|
+
assertPathInsideRoot(rootReal, fileReal, filePath);
|
|
410
|
+
if (filenameMs !== null) {
|
|
411
|
+
candidates.push({ filePath, sortMs: filenameMs, namespace });
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (entryStat.mtimeMs < weekStartMs) continue;
|
|
415
|
+
candidates.push({ filePath, sortMs: entryStat.mtimeMs, namespace });
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async function readContainedDirNames(dir: string, rootReal: string): Promise<string[] | null> {
|
|
420
|
+
let entryStat: Stats;
|
|
421
|
+
try {
|
|
422
|
+
entryStat = await lstat(dir);
|
|
423
|
+
} catch (err) {
|
|
424
|
+
if (isErrnoCode(err, "ENOENT")) return null;
|
|
425
|
+
throw err;
|
|
426
|
+
}
|
|
427
|
+
if (entryStat.isSymbolicLink() || !entryStat.isDirectory()) return null;
|
|
428
|
+
const dirReal = await realpath(dir);
|
|
429
|
+
assertPathInsideRoot(rootReal, dirReal, dir);
|
|
430
|
+
return readdir(dir);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function utcDayDirInWeek(name: string, weekId: string): boolean {
|
|
434
|
+
if (name.length !== 10 || name[4] !== "-" || name[7] !== "-") return false;
|
|
435
|
+
for (let i = 0; i < name.length; i += 1) {
|
|
436
|
+
if (i === 4 || i === 7) continue;
|
|
437
|
+
const code = name.charCodeAt(i);
|
|
438
|
+
if (code < 48 || code > 57) return false;
|
|
439
|
+
}
|
|
440
|
+
const year = Number(name.slice(0, 4));
|
|
441
|
+
const month = Number(name.slice(5, 7));
|
|
442
|
+
const day = Number(name.slice(8, 10));
|
|
443
|
+
if (month < 1 || month > 12 || day < 1 || day > 31) return false;
|
|
444
|
+
const stamp = new Date(Date.UTC(year, month - 1, day));
|
|
445
|
+
if (stamp.getUTCFullYear() !== year || stamp.getUTCMonth() !== month - 1 || stamp.getUTCDate() !== day) {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
return isoWeekId(stamp) === weekId;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function correctionFilenameTimestampMs(fileName: string): number | null {
|
|
452
|
+
const prefix = "correction-";
|
|
453
|
+
if (!fileName.startsWith(prefix) || !fileName.endsWith(".md")) return null;
|
|
454
|
+
const rest = fileName.slice(prefix.length, fileName.length - 3);
|
|
455
|
+
let i = 0;
|
|
456
|
+
while (i < rest.length) {
|
|
457
|
+
const code = rest.charCodeAt(i);
|
|
458
|
+
if (code < 48 || code > 57) break;
|
|
459
|
+
i += 1;
|
|
460
|
+
}
|
|
461
|
+
if (i < 10) return null;
|
|
462
|
+
if (i < rest.length && rest[i] !== "-") return null;
|
|
463
|
+
const ms = Number(rest.slice(0, i));
|
|
464
|
+
if (!Number.isSafeInteger(ms)) return null;
|
|
465
|
+
return ms;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async function readCorrectionCandidate(
|
|
469
|
+
storage: WeeklyEvidenceStorage,
|
|
470
|
+
memoryDir: string,
|
|
471
|
+
weekId: string,
|
|
472
|
+
candidate: CorrectionCandidate
|
|
473
|
+
): Promise<WeeklyCorrectionEvidence | null> {
|
|
474
|
+
const memory = await storage.readMemoryByPath(candidate.filePath);
|
|
475
|
+
if (!memory) return null;
|
|
476
|
+
const raw = readRawMemoryDocument(memory);
|
|
477
|
+
if (!raw) return null;
|
|
478
|
+
if (rawFrontmatterScalar(raw, "category") !== "correction") return null;
|
|
479
|
+
// Missing status is the writer default (`active`). Any other explicit
|
|
480
|
+
// status, including forgotten, stays out of compounding so a forget is
|
|
481
|
+
// not copied back into recall or promotion.
|
|
482
|
+
const status = rawFrontmatterScalar(raw, "status");
|
|
483
|
+
if (status && status !== "active") return null;
|
|
484
|
+
const created = rawFrontmatterScalar(raw, "created");
|
|
485
|
+
if (!created) return null;
|
|
486
|
+
const createdDate = new Date(created);
|
|
487
|
+
if (!Number.isFinite(createdDate.getTime()) || isoWeekId(createdDate) !== weekId) return null;
|
|
488
|
+
const note = collapseWhitespace(memory.content).slice(0, CORRECTION_NOTE_LIMIT);
|
|
489
|
+
if (note.length === 0) return null;
|
|
490
|
+
const rawSource = rawFrontmatterScalar(raw, "source");
|
|
491
|
+
const source = rawSource && rawSource !== "unknown" ? rawSource : null;
|
|
492
|
+
const entityRef =
|
|
493
|
+
typeof memory.frontmatter.entityRef === "string" && memory.frontmatter.entityRef.length > 0
|
|
494
|
+
? memory.frontmatter.entityRef
|
|
495
|
+
: null;
|
|
496
|
+
const confidence = rawConfidence(raw);
|
|
497
|
+
const tags = Array.isArray(memory.frontmatter.tags)
|
|
498
|
+
? memory.frontmatter.tags.filter((tag) => typeof tag === "string" && tag.length > 0)
|
|
499
|
+
: [];
|
|
500
|
+
const id =
|
|
501
|
+
typeof memory.frontmatter.id === "string" && memory.frontmatter.id.length > 0
|
|
502
|
+
? memory.frontmatter.id
|
|
503
|
+
: path.basename(candidate.filePath, ".md");
|
|
504
|
+
return {
|
|
505
|
+
id,
|
|
506
|
+
namespace: candidate.namespace,
|
|
507
|
+
pattern: `correction: ${note}`,
|
|
508
|
+
note,
|
|
509
|
+
created,
|
|
510
|
+
source,
|
|
511
|
+
entityRef,
|
|
512
|
+
confidence,
|
|
513
|
+
tags,
|
|
514
|
+
provenance: path.relative(memoryDir, candidate.filePath).split(path.sep).join("/"),
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Relative `namespaces/` prefix for the routed default store, or null when
|
|
520
|
+
* that store is still the flat memory root. Recall and promotion treat this
|
|
521
|
+
* prefix as the caller's own store.
|
|
522
|
+
*/
|
|
523
|
+
export async function defaultNamespaceRecallPrefix(config: PluginConfig): Promise<string | null> {
|
|
524
|
+
const root = await resolveDefaultNamespaceRoot(config);
|
|
525
|
+
const rel = path.relative(path.resolve(config.memoryDir), path.resolve(root));
|
|
526
|
+
if (rel === "" || rel === "." || rel.startsWith("..") || path.isAbsolute(rel)) return null;
|
|
527
|
+
const posix = rel.split(path.sep).join("/");
|
|
528
|
+
if (!posix.startsWith("namespaces/")) return null;
|
|
529
|
+
return `${posix}/`;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Store recall and promotion keep the flat root and the routed default
|
|
534
|
+
* namespace. Other `namespaces/` provenance stays on the weekly report.
|
|
535
|
+
*/
|
|
536
|
+
export function visibleRubricEntries<T extends {
|
|
537
|
+
observations: string[];
|
|
538
|
+
provenance: string[];
|
|
539
|
+
observationEntries?: Array<{ note: string; provenance: string[] }>;
|
|
540
|
+
}>(
|
|
541
|
+
entries: T[],
|
|
542
|
+
observationsFor: (entry: T) => Array<{ note: string; provenance: string[] }>,
|
|
543
|
+
defaultNamespacePrefix: string | null,
|
|
544
|
+
): T[] {
|
|
545
|
+
return entries.flatMap((entry) => {
|
|
546
|
+
const visible = observationsFor(entry).filter((item) =>
|
|
547
|
+
provenanceVisibleInStoreRecall(item.provenance, defaultNamespacePrefix),
|
|
548
|
+
);
|
|
549
|
+
if (visible.length === 0) return [];
|
|
550
|
+
return [{
|
|
551
|
+
...entry,
|
|
552
|
+
observations: visible.map((item) => item.note),
|
|
553
|
+
provenance: [...new Set(visible.flatMap((item) => item.provenance))],
|
|
554
|
+
observationEntries: visible,
|
|
555
|
+
}];
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* The mistake registry keeps 500 patterns. Store-visible action failures stay
|
|
561
|
+
* ahead of correction text, and every visible pattern stays ahead of another
|
|
562
|
+
* namespace, so a full correction week cannot drop the caller's own outcomes.
|
|
563
|
+
*/
|
|
564
|
+
export function capWeeklyPatternDetails<T extends { pattern: string; provenance: readonly string[] }>(
|
|
565
|
+
details: readonly T[],
|
|
566
|
+
categoryFor: (pattern: string) => string | undefined,
|
|
567
|
+
defaultNamespacePrefix: string | null,
|
|
568
|
+
limit = 500,
|
|
569
|
+
): T[] {
|
|
570
|
+
const visibleActions: T[] = [];
|
|
571
|
+
const visibleOther: T[] = [];
|
|
572
|
+
const foreign: T[] = [];
|
|
573
|
+
for (const detail of details) {
|
|
574
|
+
if (!provenanceVisibleInStoreRecall(detail.provenance, defaultNamespacePrefix)) {
|
|
575
|
+
foreign.push(detail);
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
if (categoryFor(detail.pattern) === "action") visibleActions.push(detail);
|
|
579
|
+
else visibleOther.push(detail);
|
|
580
|
+
}
|
|
581
|
+
return [...visibleActions, ...visibleOther, ...foreign].slice(0, limit);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Active correction mistakes whose visible source files are all missing or
|
|
586
|
+
* non-active. The next synthesis retires those registry entries immediately.
|
|
587
|
+
* Every active correction entry is checked. The weekly pattern cap and the
|
|
588
|
+
* retirement window already bound how many of those entries exist.
|
|
589
|
+
*/
|
|
590
|
+
export async function forgottenCorrectionPatterns(
|
|
591
|
+
storage: WeeklyEvidenceStorage,
|
|
592
|
+
memoryDir: string,
|
|
593
|
+
registry: ReadonlyArray<{ category?: string; status?: string; pattern: string; provenance?: readonly string[] }>,
|
|
594
|
+
defaultNamespacePrefix: string | null,
|
|
595
|
+
): Promise<Set<string>> {
|
|
596
|
+
const out = new Set<string>();
|
|
597
|
+
for (const entry of registry) {
|
|
598
|
+
if (entry.category !== "correction" || entry.status !== "active") continue;
|
|
599
|
+
const visible = (entry.provenance ?? []).filter((item) =>
|
|
600
|
+
provenanceVisibleInStoreRecall([item], defaultNamespacePrefix),
|
|
601
|
+
);
|
|
602
|
+
if (visible.length === 0) continue;
|
|
603
|
+
let stillActive = false;
|
|
604
|
+
for (const rel of visible) {
|
|
605
|
+
if (rel.includes("..") || path.isAbsolute(rel)) {
|
|
606
|
+
stillActive = true;
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
let memory: MemoryFile | null;
|
|
610
|
+
try {
|
|
611
|
+
memory = await storage.readMemoryByPath(path.join(memoryDir, rel));
|
|
612
|
+
} catch {
|
|
613
|
+
stillActive = true;
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
616
|
+
if (!memory) {
|
|
617
|
+
// readMemoryByPath returns null for a missing file and for a read
|
|
618
|
+
// or decrypt failure. Only a confirmed absence can retire the lesson.
|
|
619
|
+
const sourcePath = path.join(memoryDir, rel);
|
|
620
|
+
try {
|
|
621
|
+
await lstat(sourcePath);
|
|
622
|
+
stillActive = true;
|
|
623
|
+
break;
|
|
624
|
+
} catch (err) {
|
|
625
|
+
if (!isErrnoCode(err, "ENOENT")) {
|
|
626
|
+
stillActive = true;
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
const raw = readRawMemoryDocument(memory);
|
|
633
|
+
const status = raw ? rawFrontmatterScalar(raw, "status") : undefined;
|
|
634
|
+
if (!status || status === "active") {
|
|
635
|
+
stillActive = true;
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
if (!stillActive) out.add(entry.pattern);
|
|
640
|
+
}
|
|
641
|
+
return out;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
export function provenanceVisibleInStoreRecall(
|
|
645
|
+
provenance: readonly string[],
|
|
646
|
+
defaultNamespacePrefix: string | null = null,
|
|
647
|
+
): boolean {
|
|
648
|
+
return provenance.length === 0 || provenance.some((item) => {
|
|
649
|
+
if (!item.startsWith("namespaces/")) return true;
|
|
650
|
+
return defaultNamespacePrefix !== null && item.startsWith(defaultNamespacePrefix);
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/** Ledger rows that belong to the flat root or the routed default namespace. */
|
|
655
|
+
export function actionEventsVisibleInStore<T extends { ledgerLabel: string; line: number }>(
|
|
656
|
+
events: readonly T[],
|
|
657
|
+
defaultNamespacePrefix: string | null,
|
|
658
|
+
): T[] {
|
|
659
|
+
return events.filter((event) =>
|
|
660
|
+
provenanceVisibleInStoreRecall([`${event.ledgerLabel}:L${event.line}`], defaultNamespacePrefix),
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function collapseWhitespace(value: string): string {
|
|
665
|
+
return value.replace(/\s+/g, " ").trim();
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function rawConfidence(raw: string): number | null {
|
|
669
|
+
const text = rawFrontmatterScalar(raw, "confidence");
|
|
670
|
+
if (!text) return null;
|
|
671
|
+
const value = Number(text);
|
|
672
|
+
if (!Number.isFinite(value)) return null;
|
|
673
|
+
return Math.max(0, Math.min(1, value));
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/** Top-level frontmatter scalar. Missing keys stay missing — parser defaults are not used. */
|
|
677
|
+
function rawFrontmatterScalar(raw: string, key: string): string | undefined {
|
|
678
|
+
const newline = raw.startsWith("---\r\n") ? 5 : raw.startsWith("---\n") ? 4 : -1;
|
|
679
|
+
if (newline < 0) return undefined;
|
|
680
|
+
const headerEnd = raw.indexOf("\n---", newline);
|
|
681
|
+
if (headerEnd < 0) return undefined;
|
|
682
|
+
const block = raw.slice(newline, headerEnd);
|
|
683
|
+
const prefix = `${key}:`;
|
|
684
|
+
for (const line of block.split("\n")) {
|
|
685
|
+
const normalized = line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
686
|
+
if (normalized.startsWith(" ") || normalized.startsWith("\t")) continue;
|
|
687
|
+
if (!normalized.startsWith(prefix)) continue;
|
|
688
|
+
let value = normalized.slice(prefix.length).trim();
|
|
689
|
+
if (value.length >= 2) {
|
|
690
|
+
const quote = value[0];
|
|
691
|
+
if ((quote === '"' || quote === "'") && value.endsWith(quote)) {
|
|
692
|
+
value = value.slice(1, -1);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return value.length > 0 ? value : undefined;
|
|
696
|
+
}
|
|
697
|
+
return undefined;
|
|
698
|
+
}
|