@gmickel/gno 1.40.0 → 1.41.0
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/README.md +1 -0
- package/assets/skill/SKILL.md +17 -0
- package/assets/skill/cli-reference.md +48 -0
- package/assets/skill/mcp-reference.md +24 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v1.40.0.zip → gno-browser-clipper-v1.41.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.41.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +146 -7
- package/spec/db/schema.sql +17 -0
- package/spec/mcp.md +194 -0
- package/spec/output-schemas/memory-recall.schema.json +159 -0
- package/spec/output-schemas/memory-remember.schema.json +164 -0
- package/spec/output-schemas/status.schema.json +269 -54
- package/src/cli/commands/memory.ts +491 -0
- package/src/cli/commands/status.ts +23 -4
- package/src/cli/options.ts +4 -0
- package/src/cli/program.ts +127 -0
- package/src/config/types.ts +7 -0
- package/src/core/audit-provenance.ts +91 -0
- package/src/core/audit-workspace.ts +17 -0
- package/src/core/memory-diagnostics.ts +144 -0
- package/src/core/memory-fence.ts +239 -0
- package/src/core/memory-recall.ts +269 -0
- package/src/core/memory-record.ts +435 -0
- package/src/core/memory-remember.ts +425 -0
- package/src/core/memory-types.ts +211 -0
- package/src/core/memory.ts +87 -0
- package/src/ingestion/sync.ts +17 -0
- package/src/mcp/http-egress.ts +2 -0
- package/src/mcp/tools/index.ts +43 -0
- package/src/mcp/tools/memory-recall.ts +122 -0
- package/src/mcp/tools/memory-remember.ts +177 -0
- package/src/mcp/tools/memory-shared.ts +80 -0
- package/src/pipeline/search.ts +2 -0
- package/src/pipeline/types.ts +8 -0
- package/src/sdk/client.ts +94 -1
- package/src/sdk/index.ts +13 -0
- package/src/sdk/types.ts +28 -0
- package/src/serve/routes/api.ts +167 -0
- package/src/serve/server.ts +26 -0
- package/src/store/migrations/027-memory-scopes.ts +37 -0
- package/src/store/migrations/index.ts +2 -0
- package/src/store/sqlite/adapter.ts +127 -3
- package/src/store/types.ts +54 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.40.0.zip.sha256 +0 -1
package/src/cli/program.ts
CHANGED
|
@@ -354,6 +354,7 @@ export function createProgram(): Command {
|
|
|
354
354
|
wireSearchCommands(program);
|
|
355
355
|
wireOnboardingCommands(program);
|
|
356
356
|
wireCaptureCommand(program);
|
|
357
|
+
wireMemoryCommands(program);
|
|
357
358
|
wireManagementCommands(program);
|
|
358
359
|
wireTraceCommands(program);
|
|
359
360
|
wirePublishCommand(program);
|
|
@@ -1888,6 +1889,132 @@ function wireCaptureCommand(program: Command): void {
|
|
|
1888
1889
|
);
|
|
1889
1890
|
}
|
|
1890
1891
|
|
|
1892
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1893
|
+
// Memory Commands (remember, recall)
|
|
1894
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1895
|
+
|
|
1896
|
+
function wireMemoryCommands(program: Command): void {
|
|
1897
|
+
program
|
|
1898
|
+
.command("remember <text>")
|
|
1899
|
+
.description("Store a fact in a memory-managed collection")
|
|
1900
|
+
.option("-c, --collection <name>", "memory-managed target collection")
|
|
1901
|
+
.option(
|
|
1902
|
+
"--scope <scope>",
|
|
1903
|
+
"explicit scope (repeatable, required; no implicit global scope)",
|
|
1904
|
+
collectRepeatableValue,
|
|
1905
|
+
[]
|
|
1906
|
+
)
|
|
1907
|
+
.option("--decision <decision>", "add or supersede (omit for candidates)")
|
|
1908
|
+
.option("--add", "shorthand for --decision add")
|
|
1909
|
+
.option(
|
|
1910
|
+
"--supersede <uri>",
|
|
1911
|
+
"shorthand for --decision supersede --predecessor <uri>"
|
|
1912
|
+
)
|
|
1913
|
+
.option("--predecessor <uri>", "predecessor gno:// URI for supersede")
|
|
1914
|
+
.option(
|
|
1915
|
+
"--predecessor-hash <hash>",
|
|
1916
|
+
"predecessor contentHash from recall (required for supersede)"
|
|
1917
|
+
)
|
|
1918
|
+
.option(
|
|
1919
|
+
"--receipt <path>",
|
|
1920
|
+
"recall receipt JSON to fence against (recall --json output)"
|
|
1921
|
+
)
|
|
1922
|
+
.option(
|
|
1923
|
+
"--derived-from <uri>",
|
|
1924
|
+
"declared origin of the fact (repeatable; gno:// origins are fenced)",
|
|
1925
|
+
collectRepeatableValue,
|
|
1926
|
+
[]
|
|
1927
|
+
)
|
|
1928
|
+
.option("--source <text>", "free-form source evidence")
|
|
1929
|
+
.option(
|
|
1930
|
+
"--caller <id>",
|
|
1931
|
+
"caller identity (default: $GNO_MEMORY_CALLER or cli:<user>)"
|
|
1932
|
+
)
|
|
1933
|
+
.option(
|
|
1934
|
+
"--session <id>",
|
|
1935
|
+
"session identity (default: $GNO_MEMORY_SESSION or ppid:<pid>)"
|
|
1936
|
+
)
|
|
1937
|
+
.option("--json", "JSON output")
|
|
1938
|
+
.action(async (text: string, cmdOpts: Record<string, unknown>) => {
|
|
1939
|
+
const format = getFormat(cmdOpts);
|
|
1940
|
+
const globals = getGlobals();
|
|
1941
|
+
const { formatRememberResult, remember } =
|
|
1942
|
+
await import("./commands/memory");
|
|
1943
|
+
const result = await remember({
|
|
1944
|
+
configPath: globals.config,
|
|
1945
|
+
indexName: globals.index,
|
|
1946
|
+
text,
|
|
1947
|
+
collection: cmdOpts.collection as string | undefined,
|
|
1948
|
+
scopes: cmdOpts.scope as string[],
|
|
1949
|
+
decision: cmdOpts.decision as string | undefined,
|
|
1950
|
+
add: Boolean(cmdOpts.add),
|
|
1951
|
+
supersede: cmdOpts.supersede as string | undefined,
|
|
1952
|
+
predecessor: cmdOpts.predecessor as string | undefined,
|
|
1953
|
+
predecessorHash: cmdOpts.predecessorHash as string | undefined,
|
|
1954
|
+
receipt: cmdOpts.receipt as string | undefined,
|
|
1955
|
+
derivedFrom: cmdOpts.derivedFrom as string[],
|
|
1956
|
+
source: cmdOpts.source as string | undefined,
|
|
1957
|
+
caller: cmdOpts.caller as string | undefined,
|
|
1958
|
+
session: cmdOpts.session as string | undefined,
|
|
1959
|
+
});
|
|
1960
|
+
await writeOutput(
|
|
1961
|
+
formatRememberResult(result, {
|
|
1962
|
+
json: format === "json",
|
|
1963
|
+
quiet: globals.quiet,
|
|
1964
|
+
}),
|
|
1965
|
+
format
|
|
1966
|
+
);
|
|
1967
|
+
});
|
|
1968
|
+
|
|
1969
|
+
program
|
|
1970
|
+
.command("recall <query>")
|
|
1971
|
+
.description("Recall current facts from a memory-managed collection")
|
|
1972
|
+
.option("-c, --collection <name>", "memory-managed collection")
|
|
1973
|
+
.option(
|
|
1974
|
+
"--scope <scope>",
|
|
1975
|
+
"explicit scope (repeatable, required; no implicit global scope)",
|
|
1976
|
+
collectRepeatableValue,
|
|
1977
|
+
[]
|
|
1978
|
+
)
|
|
1979
|
+
.option("--max-facts <n>", "budget override: max facts (default 8)")
|
|
1980
|
+
.option(
|
|
1981
|
+
"--max-tokens <n>",
|
|
1982
|
+
"budget override: max payload tokens (default 512)"
|
|
1983
|
+
)
|
|
1984
|
+
.option(
|
|
1985
|
+
"--caller <id>",
|
|
1986
|
+
"caller identity (default: $GNO_MEMORY_CALLER or cli:<user>)"
|
|
1987
|
+
)
|
|
1988
|
+
.option(
|
|
1989
|
+
"--session <id>",
|
|
1990
|
+
"session identity (default: $GNO_MEMORY_SESSION or ppid:<pid>)"
|
|
1991
|
+
)
|
|
1992
|
+
.option("--json", "JSON output")
|
|
1993
|
+
.action(async (query: string, cmdOpts: Record<string, unknown>) => {
|
|
1994
|
+
const format = getFormat(cmdOpts);
|
|
1995
|
+
const globals = getGlobals();
|
|
1996
|
+
const { formatRecallResult, recall } = await import("./commands/memory");
|
|
1997
|
+
const result = await recall({
|
|
1998
|
+
configPath: globals.config,
|
|
1999
|
+
indexName: globals.index,
|
|
2000
|
+
query,
|
|
2001
|
+
collection: cmdOpts.collection as string | undefined,
|
|
2002
|
+
scopes: cmdOpts.scope as string[],
|
|
2003
|
+
maxFacts: cmdOpts.maxFacts as number | undefined,
|
|
2004
|
+
maxTokens: cmdOpts.maxTokens as number | undefined,
|
|
2005
|
+
caller: cmdOpts.caller as string | undefined,
|
|
2006
|
+
session: cmdOpts.session as string | undefined,
|
|
2007
|
+
});
|
|
2008
|
+
await writeOutput(
|
|
2009
|
+
formatRecallResult(result, {
|
|
2010
|
+
json: format === "json",
|
|
2011
|
+
quiet: globals.quiet,
|
|
2012
|
+
}),
|
|
2013
|
+
format
|
|
2014
|
+
);
|
|
2015
|
+
});
|
|
2016
|
+
}
|
|
2017
|
+
|
|
1891
2018
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1892
2019
|
// Retrieval Commands (get, multi-get, ls)
|
|
1893
2020
|
// ─────────────────────────────────────────────────────────────────────────────
|
package/src/config/types.ts
CHANGED
|
@@ -163,6 +163,13 @@ export const CollectionSchema = z.object({
|
|
|
163
163
|
*/
|
|
164
164
|
sourceAvailability: SourceAvailabilitySchema.optional(),
|
|
165
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Declares the collection as a GNO-managed memory substrate: `remember`
|
|
168
|
+
* writes fact files here and refuses every collection without the flag.
|
|
169
|
+
* Omitted means false; ordinary retrieval is unaffected either way.
|
|
170
|
+
*/
|
|
171
|
+
memoryManaged: z.boolean().optional(),
|
|
172
|
+
|
|
166
173
|
/** Optional per-collection model overrides */
|
|
167
174
|
models: z
|
|
168
175
|
.object({
|
|
@@ -5,6 +5,7 @@ import type { CaptureSource } from "./capture";
|
|
|
5
5
|
|
|
6
6
|
import { compareAuditFindingDrafts } from "./audit";
|
|
7
7
|
import { validateDeclaredCaptureProvenance } from "./capture";
|
|
8
|
+
import { diagnoseMemoryContent } from "./memory-diagnostics";
|
|
8
9
|
import {
|
|
9
10
|
hasDeclaredRecordProvenance,
|
|
10
11
|
validateDeclaredRecordProvenance,
|
|
@@ -20,6 +21,11 @@ export interface AuditProvenanceDocument {
|
|
|
20
21
|
captureSourceSupported?: boolean;
|
|
21
22
|
captureSource?: Partial<CaptureSource>;
|
|
22
23
|
captureSourceDeclared: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Present only for documents in a memory-managed collection: the file
|
|
26
|
+
* content the memory-record validator runs against (null when unreadable).
|
|
27
|
+
*/
|
|
28
|
+
memory?: { content: string | null };
|
|
23
29
|
record: {
|
|
24
30
|
recordKey?: string | null;
|
|
25
31
|
recordSourceLocator?: string | null;
|
|
@@ -52,6 +58,90 @@ const issueFinding = (input: {
|
|
|
52
58
|
],
|
|
53
59
|
});
|
|
54
60
|
|
|
61
|
+
const memoryFinding = (
|
|
62
|
+
document: AuditProvenanceDocument,
|
|
63
|
+
code: string,
|
|
64
|
+
message: string
|
|
65
|
+
): AuditFindingDraft => ({
|
|
66
|
+
subject: document.uri,
|
|
67
|
+
location: code,
|
|
68
|
+
severity: "warning",
|
|
69
|
+
message: `memory record excluded from recall: ${message}`,
|
|
70
|
+
evidence: [
|
|
71
|
+
{
|
|
72
|
+
kind: "memory-record-contract",
|
|
73
|
+
summary: code,
|
|
74
|
+
uri: document.uri,
|
|
75
|
+
path: document.relPath,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
guidance: [
|
|
79
|
+
"Repair the memory frontmatter (memory.recordId/scopes/caller/session/createdAt/contentHash) or re-create the fact with gno remember",
|
|
80
|
+
],
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Managed memory files are audited against the memory-record contract; each
|
|
85
|
+
* malformed file yields one finding per diagnostic code. Ordinary retrieval
|
|
86
|
+
* still sees such files; managed recall does not.
|
|
87
|
+
*/
|
|
88
|
+
export const evaluateMemoryRecordAudit = (
|
|
89
|
+
documents: readonly AuditProvenanceDocument[],
|
|
90
|
+
options: { truncated?: boolean } = {}
|
|
91
|
+
): AuditRuleContribution => {
|
|
92
|
+
const findings: AuditFindingDraft[] = [];
|
|
93
|
+
let managedDocuments = 0;
|
|
94
|
+
let unreadable = 0;
|
|
95
|
+
for (const document of documents) {
|
|
96
|
+
if (document.memory === undefined) continue;
|
|
97
|
+
managedDocuments += 1;
|
|
98
|
+
if (document.memory.content === null) {
|
|
99
|
+
unreadable += 1;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const diagnostics = diagnoseMemoryContent(document.memory.content) ?? [];
|
|
103
|
+
for (const diagnostic of diagnostics) {
|
|
104
|
+
findings.push(
|
|
105
|
+
memoryFinding(document, diagnostic.code, diagnostic.message)
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
findings.sort(compareAuditFindingDrafts);
|
|
110
|
+
const truncated = options.truncated === true;
|
|
111
|
+
return {
|
|
112
|
+
ruleId: "provenance.memory-record",
|
|
113
|
+
category: "provenance",
|
|
114
|
+
status: truncated
|
|
115
|
+
? "inconclusive"
|
|
116
|
+
: unreadable > 0
|
|
117
|
+
? "unavailable"
|
|
118
|
+
: findings.length > 0
|
|
119
|
+
? "fail"
|
|
120
|
+
: managedDocuments === 0
|
|
121
|
+
? "skip"
|
|
122
|
+
: "pass",
|
|
123
|
+
message: truncated
|
|
124
|
+
? "Audit selection was truncated; memory record contract not fully evaluated"
|
|
125
|
+
: unreadable > 0
|
|
126
|
+
? `${unreadable} memory-managed source(s) could not be read`
|
|
127
|
+
: findings.length > 0
|
|
128
|
+
? `${findings.length} memory record contract violation(s) across ${managedDocuments} managed document(s)`
|
|
129
|
+
: managedDocuments === 0
|
|
130
|
+
? "No memory-managed collections in scope"
|
|
131
|
+
: `${managedDocuments} managed memory record(s) satisfy the contract`,
|
|
132
|
+
findings: findings.slice(0, PROVENANCE_AUDIT_MAX_FINDINGS_PER_RULE),
|
|
133
|
+
findingCount: findings.length,
|
|
134
|
+
examinedCount: managedDocuments,
|
|
135
|
+
skipReason: truncated
|
|
136
|
+
? "snapshot_truncated"
|
|
137
|
+
: unreadable > 0
|
|
138
|
+
? "source_unavailable"
|
|
139
|
+
: managedDocuments === 0
|
|
140
|
+
? "no_memory_managed_collections"
|
|
141
|
+
: null,
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
|
|
55
145
|
/** Missing provenance is completeness evidence, never a truth judgment. */
|
|
56
146
|
export const evaluateProvenanceAudit = (
|
|
57
147
|
documents: readonly AuditProvenanceDocument[],
|
|
@@ -150,5 +240,6 @@ export const evaluateProvenanceAudit = (
|
|
|
150
240
|
recordFindings,
|
|
151
241
|
declaredRecordDocuments
|
|
152
242
|
),
|
|
243
|
+
evaluateMemoryRecordAudit(documents, options),
|
|
153
244
|
];
|
|
154
245
|
};
|
|
@@ -189,6 +189,7 @@ export const groupAuditPhysicalSources = (
|
|
|
189
189
|
const observeDocument = async (
|
|
190
190
|
document: DocumentRow,
|
|
191
191
|
roots: ReadonlyMap<string, string>,
|
|
192
|
+
managedCollections: ReadonlySet<string>,
|
|
192
193
|
readFrontmatter: boolean,
|
|
193
194
|
inspectFreshness: boolean,
|
|
194
195
|
signal?: AbortSignal
|
|
@@ -196,6 +197,8 @@ const observeDocument = async (
|
|
|
196
197
|
const markdownSource = MARKDOWN_SOURCE_EXTENSIONS.has(
|
|
197
198
|
document.sourceExt.toLowerCase()
|
|
198
199
|
);
|
|
200
|
+
const memoryManaged = managedCollections.has(document.collection);
|
|
201
|
+
const memoryUnreadable = memoryManaged ? { content: null } : undefined;
|
|
199
202
|
const root = roots.get(document.collection);
|
|
200
203
|
if (!root) {
|
|
201
204
|
return {
|
|
@@ -206,6 +209,7 @@ const observeDocument = async (
|
|
|
206
209
|
sourceState: "unreadable",
|
|
207
210
|
captureSourceSupported: markdownSource,
|
|
208
211
|
captureSourceDeclared: false,
|
|
212
|
+
memory: memoryUnreadable,
|
|
209
213
|
record: document,
|
|
210
214
|
},
|
|
211
215
|
freshness: {
|
|
@@ -238,6 +242,7 @@ const observeDocument = async (
|
|
|
238
242
|
sourceState: "missing",
|
|
239
243
|
captureSourceSupported: markdownSource,
|
|
240
244
|
captureSourceDeclared: false,
|
|
245
|
+
memory: memoryUnreadable,
|
|
241
246
|
record: document,
|
|
242
247
|
},
|
|
243
248
|
freshness: {
|
|
@@ -286,6 +291,11 @@ const observeDocument = async (
|
|
|
286
291
|
captureSource: incompleteFrontmatter
|
|
287
292
|
? undefined
|
|
288
293
|
: extractCaptureSourceFromFrontmatter(frontmatter),
|
|
294
|
+
// Memory facts are small single files, so the bounded frontmatter
|
|
295
|
+
// read covers the whole record the validator needs.
|
|
296
|
+
memory: memoryManaged
|
|
297
|
+
? { content: incompleteFrontmatter ? null : frontmatter }
|
|
298
|
+
: undefined,
|
|
289
299
|
record: document,
|
|
290
300
|
},
|
|
291
301
|
freshness: {
|
|
@@ -316,6 +326,7 @@ const observeDocument = async (
|
|
|
316
326
|
sourceState: "unreadable",
|
|
317
327
|
captureSourceSupported: markdownSource,
|
|
318
328
|
captureSourceDeclared: false,
|
|
329
|
+
memory: memoryUnreadable,
|
|
319
330
|
record: document,
|
|
320
331
|
},
|
|
321
332
|
freshness: {
|
|
@@ -382,11 +393,17 @@ const loadWorkspaceSnapshot = async (
|
|
|
382
393
|
const needsSourceObservation =
|
|
383
394
|
options.categories.includes("provenance") ||
|
|
384
395
|
options.categories.includes("freshness");
|
|
396
|
+
const managedCollections = new Set(
|
|
397
|
+
options.collections
|
|
398
|
+
.filter((collection) => collection.memoryManaged === true)
|
|
399
|
+
.map((collection) => collection.name)
|
|
400
|
+
);
|
|
385
401
|
const observed = needsSourceObservation
|
|
386
402
|
? await mapConcurrent(selected.documents, (document) =>
|
|
387
403
|
observeDocument(
|
|
388
404
|
document,
|
|
389
405
|
roots,
|
|
406
|
+
managedCollections,
|
|
390
407
|
options.categories.includes("provenance"),
|
|
391
408
|
options.categories.includes("freshness"),
|
|
392
409
|
options.signal
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Malformed-memory diagnostics projected through status and audit.
|
|
3
|
+
*
|
|
4
|
+
* A memory-managed collection may contain hand-edited files that no longer
|
|
5
|
+
* satisfy the record contract. Ingestion drops their scope rows (so managed
|
|
6
|
+
* recall never returns them) while ordinary retrieval still sees the file.
|
|
7
|
+
* This module names those files and their diagnostic codes.
|
|
8
|
+
*
|
|
9
|
+
* @module src/core/memory-diagnostics
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Collection } from "../config/types";
|
|
13
|
+
import type { StorePort } from "../store/types";
|
|
14
|
+
import type { MemoryDiagnostic } from "./memory-record";
|
|
15
|
+
|
|
16
|
+
import { validateMemoryRecord } from "./memory-record";
|
|
17
|
+
|
|
18
|
+
/** Bounded list of malformed files per collection in status output. */
|
|
19
|
+
export const MEMORY_STATUS_MAX_MALFORMED = 20;
|
|
20
|
+
|
|
21
|
+
export interface MalformedMemoryRecord {
|
|
22
|
+
uri: string;
|
|
23
|
+
relPath: string;
|
|
24
|
+
codes: string[];
|
|
25
|
+
diagnostics: MemoryDiagnostic[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface MemoryCollectionStatus {
|
|
29
|
+
collection: string;
|
|
30
|
+
/** Active documents indexed in the collection. */
|
|
31
|
+
documents: number;
|
|
32
|
+
/** Documents that satisfy the record contract (recallable). */
|
|
33
|
+
records: number;
|
|
34
|
+
malformed: number;
|
|
35
|
+
malformedRecords: MalformedMemoryRecord[];
|
|
36
|
+
/** True when more malformed files exist than were listed. */
|
|
37
|
+
truncated: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface MemoryStatus {
|
|
41
|
+
managedCollections: number;
|
|
42
|
+
records: number;
|
|
43
|
+
malformed: number;
|
|
44
|
+
collections: MemoryCollectionStatus[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const memoryManagedCollections = (
|
|
48
|
+
collections: readonly Collection[]
|
|
49
|
+
): Collection[] =>
|
|
50
|
+
collections.filter((collection) => collection.memoryManaged === true);
|
|
51
|
+
|
|
52
|
+
/** Validate one indexed file as a memory record (null when unreadable). */
|
|
53
|
+
export const diagnoseMemoryContent = (
|
|
54
|
+
content: string
|
|
55
|
+
): MemoryDiagnostic[] | null => {
|
|
56
|
+
const validation = validateMemoryRecord(content);
|
|
57
|
+
return validation.ok ? [] : validation.diagnostics;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Scan every active document of each memory-managed collection through the
|
|
62
|
+
* validator. Read-only; bounded by the collection's own document count.
|
|
63
|
+
*/
|
|
64
|
+
export async function buildMemoryStatus(
|
|
65
|
+
store: Pick<StorePort, "listDocuments" | "getContent">,
|
|
66
|
+
collections: readonly Collection[],
|
|
67
|
+
options: { maxListed?: number } = {}
|
|
68
|
+
): Promise<MemoryStatus> {
|
|
69
|
+
const maxListed = options.maxListed ?? MEMORY_STATUS_MAX_MALFORMED;
|
|
70
|
+
const statuses: MemoryCollectionStatus[] = [];
|
|
71
|
+
for (const collection of memoryManagedCollections(collections)) {
|
|
72
|
+
const listed = await store.listDocuments(collection.name);
|
|
73
|
+
const documents = listed.ok
|
|
74
|
+
? listed.value.filter((document) => document.active)
|
|
75
|
+
: [];
|
|
76
|
+
let records = 0;
|
|
77
|
+
const malformedRecords: MalformedMemoryRecord[] = [];
|
|
78
|
+
let malformed = 0;
|
|
79
|
+
for (const document of documents) {
|
|
80
|
+
if (!document.mirrorHash) continue;
|
|
81
|
+
const content = await store.getContent(document.mirrorHash);
|
|
82
|
+
if (!content.ok || content.value === null) continue;
|
|
83
|
+
const diagnostics = diagnoseMemoryContent(content.value);
|
|
84
|
+
if (diagnostics === null) continue;
|
|
85
|
+
if (diagnostics.length === 0) {
|
|
86
|
+
records += 1;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
malformed += 1;
|
|
90
|
+
if (malformedRecords.length < maxListed) {
|
|
91
|
+
malformedRecords.push({
|
|
92
|
+
uri: document.uri,
|
|
93
|
+
relPath: document.relPath,
|
|
94
|
+
codes: [...new Set(diagnostics.map((item) => item.code))],
|
|
95
|
+
diagnostics,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
malformedRecords.sort((left, right) =>
|
|
100
|
+
left.uri < right.uri ? -1 : left.uri > right.uri ? 1 : 0
|
|
101
|
+
);
|
|
102
|
+
statuses.push({
|
|
103
|
+
collection: collection.name,
|
|
104
|
+
documents: documents.length,
|
|
105
|
+
records,
|
|
106
|
+
malformed,
|
|
107
|
+
malformedRecords,
|
|
108
|
+
truncated: malformed > malformedRecords.length,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
managedCollections: statuses.length,
|
|
113
|
+
records: statuses.reduce((sum, item) => sum + item.records, 0),
|
|
114
|
+
malformed: statuses.reduce((sum, item) => sum + item.malformed, 0),
|
|
115
|
+
collections: statuses,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Terminal lines for the `gno status` memory section. */
|
|
120
|
+
export function formatMemoryStatusLines(status: MemoryStatus): string[] {
|
|
121
|
+
if (status.managedCollections === 0) {
|
|
122
|
+
return ["Memory: no memory-managed collections"];
|
|
123
|
+
}
|
|
124
|
+
const lines = [
|
|
125
|
+
`Memory: ${status.records} records, ${status.malformed} malformed across ${status.managedCollections} managed collection${status.managedCollections === 1 ? "" : "s"}`,
|
|
126
|
+
];
|
|
127
|
+
for (const collection of status.collections) {
|
|
128
|
+
lines.push(
|
|
129
|
+
` ${collection.collection}: ${collection.records} records` +
|
|
130
|
+
(collection.malformed > 0
|
|
131
|
+
? `, ${collection.malformed} malformed (excluded from recall)`
|
|
132
|
+
: "")
|
|
133
|
+
);
|
|
134
|
+
for (const record of collection.malformedRecords) {
|
|
135
|
+
lines.push(` ${record.relPath}: ${record.codes.join(", ")}`);
|
|
136
|
+
}
|
|
137
|
+
if (collection.truncated) {
|
|
138
|
+
lines.push(
|
|
139
|
+
` ... ${collection.malformed - collection.malformedRecords.length} more (run: gno audit --category provenance)`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return lines;
|
|
144
|
+
}
|