@agentproto/memory 0.0.0 → 0.1.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 +76 -1
- package/dist/index.d.ts +319 -6
- package/dist/index.mjs +640 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { createDoctype } from '@agentproto/doctype';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { parseDocument, stringify } from 'yaml';
|
|
4
|
+
import { randomBytes, createHash } from 'crypto';
|
|
5
|
+
import { unlink, mkdir, rename, writeFile, readFile, readdir } from 'fs/promises';
|
|
6
|
+
import { join } from 'path';
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* @agentproto/memory
|
|
@@ -29,7 +32,7 @@ var MemoryParseError = class extends Error {
|
|
|
29
32
|
this.name = "MemoryParseError";
|
|
30
33
|
}
|
|
31
34
|
};
|
|
32
|
-
var memoryFrontmatterSchema = z.object({ "schema": z.literal("memory/v1").describe("Discriminator for a memory/v1 document. Optional only to tolerate legacy items written before this doctype existed; a canonical memory/v1 document MUST set it, and when present it MUST equal 'memory/v1'.").optional(), "slug": z.string().regex(new RegExp("^[a-z][a-z0-9-]*[a-z0-9]$")).min(2).max(96).describe("Stable kebab-case identifier (AIP-10 convention). Used as the item's path stem and cross-reference target.").optional(), "kind": z.string().regex(new RegExp("^[a-z][a-z0-9-]*$")).min(2).max(32).describe("Item kind (AIP-10 convention). Common values are the memory types: 'user', 'feedback', 'project', 'reference'.").optional(), "title": z.string().min(1).max(200).describe("Human-readable title (AIP-10 convention).").optional(), "updated_at": z.string().datetime({ offset: true }).describe("ISO 8601 timestamp of the last content write (AIP-10 convention). A writer MUST set it when it creates the item and when it changes any field other than `status`; a status-only lifecycle rewrite (to `superseded` or `archived`) MAY leave it unchanged.").optional(), "id": z.string().min(1).describe("Item identifier (e.g. 'mem_qc8f407g'). Legacy alias for the AIP-10 path stem; when the base fields are omitted it takes part in identifying the item.").optional(), "name": z.string().min(1).max(1e3).describe("Slugified name, distinct from `title`. Kept as a separate field so a legacy item round-trips without loss.").optional(), "type": z.enum(["user", "feedback", "project", "reference"]).describe("Memory type. The closed type set from the memory contract; `kind` is the AIP-10-aligned spelling of the same notion.").optional(), "description": z.string().max(4e3).describe("One-line human-readable summary, independent of the body.").optional(), "tags": z.array(z.string().min(1)).describe("Free-form labels. Scope anchors are carried here in their formatted form (e.g. 'global', 'project:fhcm', 'agent:assistant', 'pr:agentproto/ts#1858').").default([]), "polarity": z.enum(["do", "dont"]).describe("Feedback polarity. Present only on items of type 'feedback'.").optional(), "lifetime": z.object({ "kind": z.enum(["durable", "until", "while", "session"]).describe("Lifetime kind. 'durable' never expires; 'until' expires at `at`; 'while' lasts as long as `anchor` holds; 'session' lasts for the session."), "at": z.string().datetime({ offset: true }).describe("Expiry timestamp for kind 'until'.").optional(), "anchor": z.object({ "kind": z.enum(["global", "project", "task", "session", "pr", "contact", "agent"]).describe("Anchor kind. 'agent' extends the conversation-contract set for per-agent anchoring."), "slug": z.string().min(1).describe("Anchor target slug (project, contact, agent).").optional(), "id": z.string().min(1).describe("Anchor target id (task, session).").optional(), "repo": z.string().min(1).describe("Repository 'owner/name' for a pr anchor.").optional(), "number": z.number().int().gte(1).describe("Pull-request number for a pr anchor.").optional() }).strict().describe("Governing anchor for kind 'while'.").optional() }).strict().and(z.intersection(z.any(), z.any())).describe("How long the item stays valid. Typical values are { kind: 'durable' } or { kind: 'while', anchor: { \u2026 } }.").optional(), "status": z.enum(["active", "superseded", "archived"]).describe("Lifecycle status. A writer creates an item 'active'. 'superseded' means a newer item replaces it; 'archived' means it is retired without a replacement (decayed, or its anchor no longer resolves). Both are terminal: an item is marked, never erased. Absent means 'active'.").optional(), "created": z.string().datetime({ offset: true }).describe("Creation timestamp.").optional(), "written": z.string().datetime({ offset: true }).describe("Last-write timestamp. Mirrors the AIP-10 `updated_at`.").optional(), "decidedBy": z.object({ "rule": z.string().min(1).describe("Classifier rule that produced the item (e.g. 'lens-confident', 'brain-memory-file').").optional(), "source": z.string().min(1).describe("Where the deciding rule came from (e.g. 'builtin').").optional() }).strict().describe("Which classifier/rule decided the item's shape and status.").optional(), "provenance": z.object({ "source": z.string().min(1).describe("Provenance source class (e.g. 'brain-transcript', 'brain-memory-file', 'converse', 'task-close', 'recall-gap').").optional(), "sessionId": z.string().min(1).describe("Session the claim was observed in. Omitted, never empty, when the source has no session.").optional(), "ref": z.string().min(1).describe("Opaque reference into the source (message uuid, file path, ...). Omitted, never empty, when the source has none.").optional(), "quote": z.string().describe("Verbatim quote backing the claim.").optional(), "at": z.string().datetime({ offset: true }).describe("When the claim was observed.").optional(), "extractorVersion": z.string().min(1).describe("Identifier and version of the extractor that produced the claim (e.g. 'lens@3'), so a reader can tell items from different extractor generations apart.").optional() }).strict().describe("Origin of the item's claims.").optional(), "evidence": z.object({ "sourceKind": z.string().min(1).describe("Kind of source the evidence came from (e.g. 'brain-transcript', 'brain-memory-file').").optional(), "trust": z.string().min(1).describe("Trust class of the source (e.g. 'jeremy', 'internal').").optional(), "explicitness": z.string().min(1).describe("Whether the claim was stated outright or inferred (e.g. 'implicit', 'explicit').").optional(), "corroborations": z.number().int().gte(0).describe("Number of independent corroborating observations.").optional(), "contradictsActive": z.boolean().describe("True when this item contradicts an active item.").optional(), "impact": z.string().min(1).describe("Impact class of the item (e.g. 'low', 'high').").optional(), "lifetime": z.object({ "kind": z.enum(["durable", "until", "while", "session"]).describe("Lifetime kind. 'durable' never expires; 'until' expires at `at`; 'while' lasts as long as `anchor` holds; 'session' lasts for the session."), "at": z.string().datetime({ offset: true }).describe("Expiry timestamp for kind 'until'.").optional(), "anchor": z.object({ "kind": z.enum(["global", "project", "task", "session", "pr", "contact", "agent"]).describe("Anchor kind. 'agent' extends the conversation-contract set for per-agent anchoring."), "slug": z.string().min(1).describe("Anchor target slug (project, contact, agent).").optional(), "id": z.string().min(1).describe("Anchor target id (task, session).").optional(), "repo": z.string().min(1).describe("Repository 'owner/name' for a pr anchor.").optional(), "number": z.number().int().gte(1).describe("Pull-request number for a pr anchor.").optional() }).strict().describe("Governing anchor for kind 'while'.").optional() }).strict().and(z.intersection(z.any(), z.any())).describe("Lifetime as computed for the evidence pass.").optional(), "anchorsResolved": z.array(z.string().min(1)).describe("Resolved anchor labels in their formatted form (e.g. 'global', 'project:fhcm', 'pr:agentproto/ts#1858', 'session:sess_\u2026').").default([]), "ageHours": z.number().describe("Age of the evidence in hours (may be fractional or negative relative to the write clock).").optional(), "flags": z.array(z.string().min(1)).describe("Machine-stable flags raised during the evidence pass.").default([]) }).strict().describe("Trust/impact accounting captured at write time.").optional(), "capture": z.object({ "mode": z.enum(["live", "batch"]).describe("How the item was captured: 'live' during the session it describes, 'batch' by a later pass over stored transcripts or files.").optional(), "producer": z.string().min(1).describe("Who or what captured the item (e.g. a clerk lens, a migration, an importer). 'unknown' when the producer did not identify itself.").optional(), "capturedAt": z.string().datetime({ offset: true }).describe("ISO 8601 timestamp at which the source was captured or extracted.").optional() }).strict().describe("Who, what and how captured the item, kept apart from `provenance` (where the claim was observed).").optional(), "body": z.string().describe("Unrestricted markdown body of the MEMORY.md file. Not constrained by this schema.").optional() }).strict().and(z.union([z.any(), z.any()])).describe("Validates the YAML frontmatter of an AIP-72 memory/v1 item: one small typed assertion (user, feedback, project, reference) about a principal or its work, with a lifecycle. A third doctype in the memory layer: it neither absorbs nor replaces AIP-10 knowledge/v1 entries (curated, shareable wiki pages over immutable sources) nor AIP-11 lesson/v1 (distilled lessons from runs). A memory/v1 item carries the AIP-10 base fields (schema, slug, kind, title, updated_at); the remaining fields are the memory item shape and are OPTIONAL, so a writer that emits only the AIP-10 base fields and a writer that emits the full item both validate against the same schema. The markdown body (the `body` property) is unrestricted text.");
|
|
35
|
+
var memoryFrontmatterSchema = z.object({ "schema": z.literal("memory/v1").describe("Discriminator for a memory/v1 document. Optional only to tolerate legacy items written before this doctype existed; a canonical memory/v1 document MUST set it, and when present it MUST equal 'memory/v1'.").optional(), "slug": z.string().regex(new RegExp("^[a-z][a-z0-9-]*[a-z0-9]$")).min(2).max(96).describe("Stable kebab-case identifier (AIP-10 convention). Used as the item's path stem and cross-reference target.").optional(), "kind": z.string().regex(new RegExp("^[a-z][a-z0-9-]*$")).min(2).max(32).describe("Item kind (AIP-10 convention). Common values are the memory types: 'user', 'feedback', 'project', 'reference'.").optional(), "title": z.string().min(1).max(200).describe("Human-readable title (AIP-10 convention).").optional(), "updated_at": z.string().datetime({ offset: true }).describe("ISO 8601 timestamp of the last content write (AIP-10 convention). A writer MUST set it when it creates the item and when it changes any field other than `status`; a status-only lifecycle rewrite (to `superseded` or `archived`) MAY leave it unchanged.").optional(), "id": z.string().min(1).describe("Item identifier (e.g. 'mem_qc8f407g'). Legacy alias for the AIP-10 path stem; when the base fields are omitted it takes part in identifying the item.").optional(), "name": z.string().min(1).max(1e3).describe("Slugified name, distinct from `title`. Kept as a separate field so a legacy item round-trips without loss.").optional(), "type": z.enum(["user", "feedback", "project", "reference"]).describe("Memory type. The closed type set from the memory contract; `kind` is the AIP-10-aligned spelling of the same notion.").optional(), "description": z.string().max(4e3).describe("One-line human-readable summary, independent of the body.").optional(), "tags": z.array(z.string().min(1)).describe("Free-form labels. Scope anchors are carried here in their formatted form (e.g. 'global', 'project:acme', 'agent:assistant', 'pr:acme/app#42'). Two anchors tie an item to an AIP-34 tree and an AIP-9 operator: 'node:<id>' (the AIP-34 node id, e.g. 'node:@acme/agentik') scopes visibility, and 'operator:<id>' (e.g. 'operator:pm-simone') marks ownership and never widens visibility. An item carries at most one of each; the node projection and write rules are in AIP-72 section 10.").default([]), "polarity": z.enum(["do", "dont"]).describe("Feedback polarity. Present only on items of type 'feedback'.").optional(), "lifetime": z.object({ "kind": z.enum(["durable", "until", "while", "session"]).describe("Lifetime kind. 'durable' never expires; 'until' expires at `at`; 'while' lasts as long as `anchor` holds; 'session' lasts for the session."), "at": z.string().datetime({ offset: true }).describe("Expiry timestamp for kind 'until'.").optional(), "anchor": z.object({ "kind": z.enum(["global", "project", "task", "session", "pr", "contact", "agent", "node", "operator"]).describe("Anchor kind. 'agent' extends the conversation-contract set for per-agent anchoring. 'node' names an AIP-34 node and scopes visibility; 'operator' names an AIP-9 operator and marks ownership (AIP-72 section 10)."), "slug": z.string().min(1).describe("Anchor target slug (project, contact, agent).").optional(), "id": z.string().min(1).describe("Anchor target id (task, session, node, operator). For a node, the AIP-34 id ('@<owner>/<slug>'); for an operator, the operator body of an AIP-54 'operator:' ref ('<slug>' or '<slug>@<workspace>').").optional(), "repo": z.string().min(1).describe("Repository 'owner/name' for a pr anchor.").optional(), "number": z.number().int().gte(1).describe("Pull-request number for a pr anchor.").optional() }).strict().describe("Governing anchor for kind 'while'.").optional() }).strict().and(z.intersection(z.any(), z.any())).describe("How long the item stays valid. Typical values are { kind: 'durable' } or { kind: 'while', anchor: { \u2026 } }.").optional(), "status": z.enum(["active", "superseded", "archived"]).describe("Lifecycle status. A writer creates an item 'active'. 'superseded' means a newer item replaces it; 'archived' means it is retired without a replacement (decayed, or its anchor no longer resolves). Both are terminal: an item is marked, never erased. Absent means 'active'.").optional(), "created": z.string().datetime({ offset: true }).describe("Creation timestamp.").optional(), "written": z.string().datetime({ offset: true }).describe("Last-write timestamp. Mirrors the AIP-10 `updated_at`.").optional(), "decidedBy": z.object({ "rule": z.string().min(1).describe("Classifier rule that produced the item (e.g. 'classifier-confident', 'memory-file').").optional(), "source": z.string().min(1).describe("Where the deciding rule came from (e.g. 'builtin').").optional() }).strict().describe("Which classifier/rule decided the item's shape and status.").optional(), "provenance": z.object({ "source": z.string().min(1).describe("Provenance source class (e.g. 'transcript', 'memory-file', 'converse', 'task-close', 'recall-gap').").optional(), "sessionId": z.string().min(1).describe("Session the claim was observed in. Omitted, never empty, when the source has no session.").optional(), "ref": z.string().min(1).describe("Opaque reference into the source (message uuid, file path, ...). Omitted, never empty, when the source has none.").optional(), "quote": z.string().describe("Verbatim quote backing the claim.").optional(), "at": z.string().datetime({ offset: true }).describe("When the claim was observed.").optional(), "extractorVersion": z.string().min(1).describe("Identifier and version of the extractor that produced the claim (e.g. 'extractor@3'), so a reader can tell items from different extractor generations apart.").optional() }).strict().describe("Origin of the item's claims.").optional(), "evidence": z.object({ "sourceKind": z.string().min(1).describe("Kind of source the evidence came from (e.g. 'transcript', 'memory-file').").optional(), "trust": z.string().min(1).describe("Trust class of the source (e.g. 'owner', 'internal').").optional(), "explicitness": z.string().min(1).describe("Whether the claim was stated outright or inferred (e.g. 'implicit', 'explicit').").optional(), "corroborations": z.number().int().gte(0).describe("Number of independent corroborating observations.").optional(), "contradictsActive": z.boolean().describe("True when this item contradicts an active item.").optional(), "impact": z.string().min(1).describe("Impact class of the item (e.g. 'low', 'high').").optional(), "lifetime": z.object({ "kind": z.enum(["durable", "until", "while", "session"]).describe("Lifetime kind. 'durable' never expires; 'until' expires at `at`; 'while' lasts as long as `anchor` holds; 'session' lasts for the session."), "at": z.string().datetime({ offset: true }).describe("Expiry timestamp for kind 'until'.").optional(), "anchor": z.object({ "kind": z.enum(["global", "project", "task", "session", "pr", "contact", "agent", "node", "operator"]).describe("Anchor kind. 'agent' extends the conversation-contract set for per-agent anchoring. 'node' names an AIP-34 node and scopes visibility; 'operator' names an AIP-9 operator and marks ownership (AIP-72 section 10)."), "slug": z.string().min(1).describe("Anchor target slug (project, contact, agent).").optional(), "id": z.string().min(1).describe("Anchor target id (task, session, node, operator). For a node, the AIP-34 id ('@<owner>/<slug>'); for an operator, the operator body of an AIP-54 'operator:' ref ('<slug>' or '<slug>@<workspace>').").optional(), "repo": z.string().min(1).describe("Repository 'owner/name' for a pr anchor.").optional(), "number": z.number().int().gte(1).describe("Pull-request number for a pr anchor.").optional() }).strict().describe("Governing anchor for kind 'while'.").optional() }).strict().and(z.intersection(z.any(), z.any())).describe("Lifetime as computed for the evidence pass.").optional(), "anchorsResolved": z.array(z.string().min(1)).describe("Resolved anchor labels in their formatted form (e.g. 'global', 'project:acme', 'pr:acme/app#42', 'session:sess_\u2026').").default([]), "ageHours": z.number().describe("Age of the evidence in hours (may be fractional or negative relative to the write clock).").optional(), "flags": z.array(z.string().min(1)).describe("Machine-stable flags raised during the evidence pass.").default([]) }).strict().describe("Trust/impact accounting captured at write time.").optional(), "capture": z.object({ "mode": z.enum(["live", "batch"]).describe("How the item was captured: 'live' during the session it describes, 'batch' by a later pass over stored transcripts or files.").optional(), "producer": z.string().min(1).describe("Who or what captured the item (e.g. a clerk, a migration, an importer). 'unknown' when the producer did not identify itself.").optional(), "capturedAt": z.string().datetime({ offset: true }).describe("ISO 8601 timestamp at which the source was captured or extracted.").optional() }).strict().describe("Who, what and how captured the item, kept apart from `provenance` (where the claim was observed).").optional(), "body": z.string().describe("Unrestricted markdown body of the MEMORY.md file. Not constrained by this schema.").optional() }).strict().and(z.union([z.any(), z.any()])).describe("Validates the YAML frontmatter of an AIP-72 memory/v1 item: one small typed assertion (user, feedback, project, reference) about a principal or its work, with a lifecycle. A third doctype in the memory layer: it neither absorbs nor replaces AIP-10 knowledge/v1 entries (curated, shareable wiki pages over immutable sources) nor AIP-11 lesson/v1 (distilled lessons from runs). A memory/v1 item carries the AIP-10 base fields (schema, slug, kind, title, updated_at); the remaining fields are the memory item shape and are OPTIONAL, so a writer that emits only the AIP-10 base fields and a writer that emits the full item both validate against the same schema. The markdown body (the `body` property) is unrestricted text.");
|
|
33
36
|
|
|
34
37
|
// src/types.ts
|
|
35
38
|
var MEMORY_TYPES = ["user", "feedback", "project", "reference"];
|
|
@@ -247,10 +250,645 @@ function isInForce(doc, now = /* @__PURE__ */ new Date()) {
|
|
|
247
250
|
return true;
|
|
248
251
|
}
|
|
249
252
|
|
|
253
|
+
// src/node-scope.ts
|
|
254
|
+
var NODE_PREFIX = "node:";
|
|
255
|
+
var OPERATOR_PREFIX = "operator:";
|
|
256
|
+
function nodeTag(id) {
|
|
257
|
+
return `${NODE_PREFIX}${id}`;
|
|
258
|
+
}
|
|
259
|
+
function operatorTag(id) {
|
|
260
|
+
return `${OPERATOR_PREFIX}${id}`;
|
|
261
|
+
}
|
|
262
|
+
function idsOf(tags, prefix) {
|
|
263
|
+
const out = [];
|
|
264
|
+
for (const tag of tags ?? []) {
|
|
265
|
+
if (tag.startsWith(prefix) && tag.length > prefix.length) out.push(tag.slice(prefix.length));
|
|
266
|
+
}
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
269
|
+
function nodeAnchorIds(item) {
|
|
270
|
+
return idsOf(item.tags, NODE_PREFIX);
|
|
271
|
+
}
|
|
272
|
+
function operatorAnchorIds(item) {
|
|
273
|
+
return idsOf(item.tags, OPERATOR_PREFIX);
|
|
274
|
+
}
|
|
275
|
+
function isNodeOrOperatorTag(tag) {
|
|
276
|
+
return tag.startsWith(NODE_PREFIX) || tag.startsWith(OPERATOR_PREFIX);
|
|
277
|
+
}
|
|
278
|
+
function isInNodeProjection(item, scope, opts = {}) {
|
|
279
|
+
const nodes = nodeAnchorIds(item);
|
|
280
|
+
if (scope.nodes.length > 0) {
|
|
281
|
+
if (nodes.length === 0) return false;
|
|
282
|
+
if (!nodes.every((n) => scope.nodes.includes(n))) return false;
|
|
283
|
+
} else if (nodes.length > 0) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
const owners = operatorAnchorIds(item);
|
|
287
|
+
if (owners.length === 0) return true;
|
|
288
|
+
const granted = new Set(opts.grantedOperators ?? []);
|
|
289
|
+
return owners.every((o) => o === scope.operator || granted.has(o));
|
|
290
|
+
}
|
|
291
|
+
function refuse(rule, message) {
|
|
292
|
+
throw new MemoryValidationError("memory scope (AIP-72 section 10)", [
|
|
293
|
+
{ path: "tags", message, rule }
|
|
294
|
+
]);
|
|
295
|
+
}
|
|
296
|
+
function applyWriteScope(item, scope, opts = {}) {
|
|
297
|
+
const tags = item.tags ?? [];
|
|
298
|
+
const nodes = nodeAnchorIds(item);
|
|
299
|
+
const owners = operatorAnchorIds(item);
|
|
300
|
+
if (nodes.length > 1) refuse("rule-31", "an item carries at most one node anchor");
|
|
301
|
+
if (owners.length > 1) refuse("rule-32", "an item carries at most one operator anchor");
|
|
302
|
+
const basisNodes = opts.basis ? nodeAnchorIds(opts.basis) : [];
|
|
303
|
+
const basisOwned = opts.basis ? operatorAnchorIds(opts.basis).length > 0 : true;
|
|
304
|
+
const add = [];
|
|
305
|
+
if (scope === void 0) {
|
|
306
|
+
if (nodes.length === 0 && basisNodes[0] !== void 0) add.push(nodeTag(basisNodes[0]));
|
|
307
|
+
if (nodes.length === 0 && add.length === 0 && opts.declaresTrees) {
|
|
308
|
+
refuse("rule-33", "a document with no node anchor is refused where the host declares trees");
|
|
309
|
+
}
|
|
310
|
+
return add.length > 0 ? { ...item, tags: [...tags, ...add] } : item;
|
|
311
|
+
}
|
|
312
|
+
const inScope = new Set(scope.nodes);
|
|
313
|
+
for (const n of nodes) {
|
|
314
|
+
if (!inScope.has(n)) refuse("rule-31", `node anchor '${n}' is outside the session's node projection`);
|
|
315
|
+
}
|
|
316
|
+
if (scope.nodes.length > 0) {
|
|
317
|
+
if (tags.includes("global")) {
|
|
318
|
+
if (nodes.some((n) => n !== scope.tree)) {
|
|
319
|
+
refuse("rule-33", "a global item carries only the node anchor of the tree root");
|
|
320
|
+
}
|
|
321
|
+
if (nodes.length === 0) {
|
|
322
|
+
if (scope.tree === void 0 || !inScope.has(scope.tree)) {
|
|
323
|
+
refuse("rule-33", "the tree root is not in the session's node projection, so it cannot write a global item");
|
|
324
|
+
}
|
|
325
|
+
add.push(nodeTag(scope.tree));
|
|
326
|
+
}
|
|
327
|
+
} else if (nodes.length === 0) {
|
|
328
|
+
add.push(nodeTag(basisNodes[0] ?? scope.nodes[0]));
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
for (const o of owners) {
|
|
332
|
+
if (o !== scope.operator) refuse("rule-32", `operator anchor '${o}' is not the session's operator`);
|
|
333
|
+
}
|
|
334
|
+
if (owners.length > 0 && opts.owner === "node") {
|
|
335
|
+
refuse("rule-32", "owner 'node' and an operator anchor contradict each other");
|
|
336
|
+
}
|
|
337
|
+
if (owners.length === 0 && scope.operator !== void 0) {
|
|
338
|
+
const owner = opts.owner ?? (basisOwned ? "operator" : "node");
|
|
339
|
+
if (owner === "operator") add.push(operatorTag(scope.operator));
|
|
340
|
+
}
|
|
341
|
+
return add.length > 0 ? { ...item, tags: [...tags, ...add] } : item;
|
|
342
|
+
}
|
|
343
|
+
function assertRetagOnly(before, after) {
|
|
344
|
+
const was = nodeAnchorIds(before)[0];
|
|
345
|
+
const now = nodeAnchorIds(after)[0];
|
|
346
|
+
if (was === now) return;
|
|
347
|
+
const rest = (item) => [...new Set((item.tags ?? []).filter((t) => !t.startsWith(NODE_PREFIX)))].sort().join("\n");
|
|
348
|
+
if (rest(before) !== rest(after)) {
|
|
349
|
+
refuse("rule-34", "a re-tag moves the node anchor only: it changes neither the operator anchor nor the other anchors");
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/store.ts
|
|
354
|
+
var MemoryStoreError = class extends Error {
|
|
355
|
+
code;
|
|
356
|
+
constructor(code, message) {
|
|
357
|
+
super(`memory store (AIP-72): ${message}`);
|
|
358
|
+
this.name = "MemoryStoreError";
|
|
359
|
+
this.code = code;
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
var MemoryConflictError = class extends MemoryStoreError {
|
|
363
|
+
id;
|
|
364
|
+
/** The revision the caller sent, if any. */
|
|
365
|
+
expected;
|
|
366
|
+
/** The revision on disk, `undefined` when the item does not exist. */
|
|
367
|
+
actual;
|
|
368
|
+
constructor(id, expected, actual) {
|
|
369
|
+
super(
|
|
370
|
+
"conflict",
|
|
371
|
+
expected === void 0 ? `'${id}' exists: a write that changes it must carry ifRevision` : actual === void 0 ? `'${id}' does not exist, but ifRevision '${expected}' was given` : `'${id}' is at revision '${actual}', not '${expected}'`
|
|
372
|
+
);
|
|
373
|
+
this.name = "MemoryConflictError";
|
|
374
|
+
this.id = id;
|
|
375
|
+
this.expected = expected;
|
|
376
|
+
this.actual = actual;
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
var MemoryNotFoundError = class extends MemoryStoreError {
|
|
380
|
+
id;
|
|
381
|
+
what;
|
|
382
|
+
constructor(what, id) {
|
|
383
|
+
super("not-found", `no ${what} '${id}'`);
|
|
384
|
+
this.name = "MemoryNotFoundError";
|
|
385
|
+
this.id = id;
|
|
386
|
+
this.what = what;
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
var MemoryTransitionError = class extends MemoryStoreError {
|
|
390
|
+
id;
|
|
391
|
+
from;
|
|
392
|
+
to;
|
|
393
|
+
constructor(id, from, to, detail) {
|
|
394
|
+
super(
|
|
395
|
+
"invalid-transition",
|
|
396
|
+
`'${id}' cannot move from '${from}' to '${to}'${detail ? `: ${detail}` : ""}`
|
|
397
|
+
);
|
|
398
|
+
this.name = "MemoryTransitionError";
|
|
399
|
+
this.id = id;
|
|
400
|
+
this.from = from;
|
|
401
|
+
this.to = to;
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
var MemorySlugTakenError = class extends MemoryStoreError {
|
|
405
|
+
slug;
|
|
406
|
+
/** The active item holding it. */
|
|
407
|
+
holder;
|
|
408
|
+
constructor(slug, holder) {
|
|
409
|
+
super("slug-taken", `slug '${slug}' is held by active item '${holder}'`);
|
|
410
|
+
this.name = "MemorySlugTakenError";
|
|
411
|
+
this.slug = slug;
|
|
412
|
+
this.holder = holder;
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
var SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
416
|
+
var PROPOSAL_ID = /^[a-f0-9]{64}$/;
|
|
417
|
+
var ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
418
|
+
var DEFAULT_SEARCH_LIMIT = 20;
|
|
419
|
+
var SNIPPET_CHARS = 160;
|
|
420
|
+
var FilesMemoryStore = class {
|
|
421
|
+
#root;
|
|
422
|
+
#knowledge;
|
|
423
|
+
#now;
|
|
424
|
+
#declaresTrees;
|
|
425
|
+
#tail = Promise.resolve();
|
|
426
|
+
constructor(options) {
|
|
427
|
+
this.#root = options.root;
|
|
428
|
+
this.#knowledge = options.knowledge;
|
|
429
|
+
this.#now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
430
|
+
this.#declaresTrees = options.declaresTrees ?? false;
|
|
431
|
+
}
|
|
432
|
+
write(item, opts) {
|
|
433
|
+
return this.#exclusive(
|
|
434
|
+
() => opts.mode === "propose" ? this.#propose(item, opts) : this.#commit(item, opts)
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
async read(id, opts = {}) {
|
|
438
|
+
const stored = await this.#find(id);
|
|
439
|
+
return stored && visible(stored.record, opts.scope) ? stored.record : null;
|
|
440
|
+
}
|
|
441
|
+
async list(filter = {}) {
|
|
442
|
+
const stored = filter.proposals ? await this.#allProposals() : await this.#allItems();
|
|
443
|
+
const types = toArray(filter.type);
|
|
444
|
+
const statuses = toArray(filter.status);
|
|
445
|
+
const inForce = filter.inForce ?? (filter.status === void 0 && !filter.proposals ? true : void 0);
|
|
446
|
+
const now = this.#now();
|
|
447
|
+
let records = stored.map((s) => s.record).filter(({ item }) => {
|
|
448
|
+
if (types && !types.includes(itemType(item))) return false;
|
|
449
|
+
if (statuses && !statuses.includes(memoryStatus(item))) return false;
|
|
450
|
+
if (filter.slug !== void 0 && item.slug !== filter.slug) return false;
|
|
451
|
+
if (filter.tags && !filter.tags.some((t) => item.tags?.includes(t))) return false;
|
|
452
|
+
if (filter.scope && !isInNodeProjection(item, filter.scope)) return false;
|
|
453
|
+
if (inForce !== void 0) {
|
|
454
|
+
const holds = !filter.proposals && isInForce(item, now);
|
|
455
|
+
if (holds !== inForce) return false;
|
|
456
|
+
}
|
|
457
|
+
return true;
|
|
458
|
+
}).sort(byNewest);
|
|
459
|
+
if (filter.slug !== void 0 && !filter.proposals) {
|
|
460
|
+
const firstActive = records.find((r) => memoryStatus(r.item) === "active");
|
|
461
|
+
records = records.filter(
|
|
462
|
+
(r) => memoryStatus(r.item) !== "active" || r === firstActive
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
return records;
|
|
466
|
+
}
|
|
467
|
+
async search(query, opts = {}) {
|
|
468
|
+
const limit = opts.limit ?? DEFAULT_SEARCH_LIMIT;
|
|
469
|
+
const candidates = await this.list(opts.filter);
|
|
470
|
+
const byId = new Map(candidates.map((r) => [r.item.id, r]));
|
|
471
|
+
const hits = /* @__PURE__ */ new Map();
|
|
472
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
473
|
+
for (const record of candidates) {
|
|
474
|
+
const hit = keywordHit(record, terms);
|
|
475
|
+
if (hit) hits.set(record.item.id, hit);
|
|
476
|
+
}
|
|
477
|
+
const foreign = [];
|
|
478
|
+
if (this.#knowledge && !opts.filter?.scope) {
|
|
479
|
+
const result = await this.#knowledge.query({ query, topK: limit });
|
|
480
|
+
for (const kh of result.hits) {
|
|
481
|
+
const named = kh.metadata?.memoryId;
|
|
482
|
+
const memoryId = typeof named === "string" ? named : kh.sourceId;
|
|
483
|
+
const record = byId.get(memoryId);
|
|
484
|
+
if (record) {
|
|
485
|
+
const prior = hits.get(memoryId);
|
|
486
|
+
if (prior) prior.score = Math.max(prior.score ?? 0, kh.score);
|
|
487
|
+
else hits.set(memoryId, memoryHit(record, kh.text || void 0, kh.score));
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
if (typeof named === "string" || await this.read(memoryId)) continue;
|
|
491
|
+
const title = kh.metadata?.title;
|
|
492
|
+
foreign.push(
|
|
493
|
+
compact({
|
|
494
|
+
source: "knowledge",
|
|
495
|
+
title: typeof title === "string" && title ? title : kh.sourceId,
|
|
496
|
+
snippet: kh.text ? clip(kh.text) : void 0,
|
|
497
|
+
score: kh.score,
|
|
498
|
+
ref: kh.chunkId ? `${kh.sourceId}#${kh.chunkId}` : kh.sourceId
|
|
499
|
+
})
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return [...hits.values(), ...foreign].sort((a, b) => (b.score ?? 0) - (a.score ?? 0)).slice(0, limit);
|
|
504
|
+
}
|
|
505
|
+
supersede(oldId, next, opts = {}) {
|
|
506
|
+
return this.#exclusive(async () => {
|
|
507
|
+
const old = await this.#mustFind(oldId, opts.scope);
|
|
508
|
+
checkRevision(oldId, opts.ifRevision, old.record.revision);
|
|
509
|
+
const from = memoryStatus(old.record.item);
|
|
510
|
+
if (from !== "active") throw new MemoryTransitionError(oldId, from, "superseded");
|
|
511
|
+
if (next.id === oldId) {
|
|
512
|
+
throw invalid("id", "the next item must have a new id (rule 20)");
|
|
513
|
+
}
|
|
514
|
+
const committed = await this.#commit(
|
|
515
|
+
next,
|
|
516
|
+
{ mode: "commit", ...opts.scope ? { scope: opts.scope } : {} },
|
|
517
|
+
oldId,
|
|
518
|
+
old.record.item
|
|
519
|
+
);
|
|
520
|
+
const retired = await this.#rewriteStatus(old, "superseded");
|
|
521
|
+
return { old: retired, next: committed };
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
archive(id, opts = {}) {
|
|
525
|
+
return this.#exclusive(async () => {
|
|
526
|
+
const stored = await this.#mustFind(id, opts.scope);
|
|
527
|
+
checkRevision(id, opts.ifRevision, stored.record.revision);
|
|
528
|
+
const from = memoryStatus(stored.record.item);
|
|
529
|
+
if (from === "archived") return stored.record;
|
|
530
|
+
if (from !== "active") throw new MemoryTransitionError(id, from, "archived");
|
|
531
|
+
return this.#rewriteStatus(stored, "archived");
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
dismiss(proposalId, opts = {}) {
|
|
535
|
+
return this.#exclusive(async () => {
|
|
536
|
+
if (!PROPOSAL_ID.test(proposalId)) throw new MemoryNotFoundError("proposal", proposalId);
|
|
537
|
+
if (opts.scope && !await this.#proposalVisible(proposalId, opts.scope)) {
|
|
538
|
+
throw new MemoryNotFoundError("proposal", proposalId);
|
|
539
|
+
}
|
|
540
|
+
try {
|
|
541
|
+
await unlink(this.#proposalPath(proposalId));
|
|
542
|
+
} catch (err) {
|
|
543
|
+
if (isNotFound(err)) throw new MemoryNotFoundError("proposal", proposalId);
|
|
544
|
+
throw err;
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
async #propose(item, opts) {
|
|
549
|
+
const doc = clean(
|
|
550
|
+
applyWriteScope(item, opts.scope, {
|
|
551
|
+
...opts.owner ? { owner: opts.owner } : {},
|
|
552
|
+
declaresTrees: this.#declaresTrees
|
|
553
|
+
})
|
|
554
|
+
);
|
|
555
|
+
const result = validateMemory(doc);
|
|
556
|
+
if (!result.ok) {
|
|
557
|
+
throw new MemoryValidationError("FilesMemoryStore.write (AIP-72 rule 13)", result.errors);
|
|
558
|
+
}
|
|
559
|
+
const text = renderMemory(doc);
|
|
560
|
+
const proposalId = sha256(text);
|
|
561
|
+
const path = this.#proposalPath(proposalId);
|
|
562
|
+
const existing = await readText(path);
|
|
563
|
+
if (existing === null) await atomicWrite(path, text);
|
|
564
|
+
return { item: parseMemory(text), revision: proposalId, proposalId };
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* The commit half of `write`, unlocked. `ignoreSlugOf` is the item being
|
|
568
|
+
* superseded, `superseded` its document: the new item inherits its node
|
|
569
|
+
* and ownership when it names none.
|
|
570
|
+
*/
|
|
571
|
+
async #commit(item, opts, ignoreSlugOf, superseded) {
|
|
572
|
+
let doc = clean(item);
|
|
573
|
+
if (opts.fromProposal !== void 0) {
|
|
574
|
+
const known = PROPOSAL_ID.test(opts.fromProposal) && await readText(this.#proposalPath(opts.fromProposal)) !== null && (!opts.scope || await this.#proposalVisible(opts.fromProposal, opts.scope));
|
|
575
|
+
if (!known) throw new MemoryNotFoundError("proposal", opts.fromProposal);
|
|
576
|
+
}
|
|
577
|
+
if (doc.id !== void 0 && !SAFE_ID.test(doc.id)) {
|
|
578
|
+
throw invalid("id", "must be a file stem: [A-Za-z0-9] then [A-Za-z0-9_.-]");
|
|
579
|
+
}
|
|
580
|
+
const existing = doc.id === void 0 ? null : await this.#find(doc.id);
|
|
581
|
+
if (existing && !visible(existing.record, opts.scope)) {
|
|
582
|
+
throw new MemoryNotFoundError("item", doc.id);
|
|
583
|
+
}
|
|
584
|
+
doc = clean(
|
|
585
|
+
applyWriteScope(doc, opts.scope, {
|
|
586
|
+
...opts.owner ? { owner: opts.owner } : {},
|
|
587
|
+
...existing || superseded ? { basis: existing?.record.item ?? superseded } : {},
|
|
588
|
+
declaresTrees: this.#declaresTrees && !existing
|
|
589
|
+
})
|
|
590
|
+
);
|
|
591
|
+
const replaced = existing?.record.item ?? superseded;
|
|
592
|
+
if (replaced && opts.scope) assertRetagOnly(replaced, doc);
|
|
593
|
+
const now = this.#now().toISOString();
|
|
594
|
+
let next;
|
|
595
|
+
if (!existing) {
|
|
596
|
+
if (opts.ifRevision !== void 0) {
|
|
597
|
+
throw new MemoryConflictError(doc.id ?? "(new item)", opts.ifRevision, void 0);
|
|
598
|
+
}
|
|
599
|
+
const status = memoryStatus(doc);
|
|
600
|
+
if (status !== "active") {
|
|
601
|
+
throw new MemoryTransitionError(
|
|
602
|
+
doc.id ?? "(new item)",
|
|
603
|
+
"active",
|
|
604
|
+
status,
|
|
605
|
+
"a writer creates an item active (section 3)"
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
next = { ...doc, id: doc.id ?? await this.#newId(), updated_at: now };
|
|
609
|
+
if (next.written !== void 0) next.written = now;
|
|
610
|
+
assertValid(next, true);
|
|
611
|
+
} else {
|
|
612
|
+
const current = existing.record;
|
|
613
|
+
const id2 = doc.id;
|
|
614
|
+
if (sameContent(current.item, doc)) {
|
|
615
|
+
await this.#dropProposal(opts.fromProposal);
|
|
616
|
+
return current;
|
|
617
|
+
}
|
|
618
|
+
checkRevision(id2, opts.ifRevision, current.revision);
|
|
619
|
+
const from = memoryStatus(current.item);
|
|
620
|
+
const to = memoryStatus(doc);
|
|
621
|
+
if (from !== to) {
|
|
622
|
+
throw new MemoryTransitionError(
|
|
623
|
+
id2,
|
|
624
|
+
from,
|
|
625
|
+
to,
|
|
626
|
+
from === "active" ? "retire an item through supersede or archive" : void 0
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
next = { ...doc, updated_at: now };
|
|
630
|
+
if (next.written !== void 0 || current.item.written !== void 0) next.written = now;
|
|
631
|
+
assertValid(next, false);
|
|
632
|
+
}
|
|
633
|
+
const id = next.id;
|
|
634
|
+
await this.#assertSlugFree(next, ignoreSlugOf);
|
|
635
|
+
const path = this.#itemPath(itemType(next), id);
|
|
636
|
+
if (existing && existing.path !== path) {
|
|
637
|
+
await mkdir(join(path, ".."), { recursive: true });
|
|
638
|
+
await rename(existing.path, path);
|
|
639
|
+
}
|
|
640
|
+
const text = renderMemory(next);
|
|
641
|
+
await atomicWrite(path, text);
|
|
642
|
+
await this.#dropProposal(opts.fromProposal);
|
|
643
|
+
return { item: withId(parseMemory(text), id), revision: sha256(text) };
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Supersede and archive are status-only rewrites (rules 6 and 19): the
|
|
647
|
+
* `status` line is replaced, or added before the closing fence, and
|
|
648
|
+
* every other byte stays as it was, whoever wrote the file.
|
|
649
|
+
*/
|
|
650
|
+
async #rewriteStatus(stored, status) {
|
|
651
|
+
const text = setStatusLine(stored.text, status);
|
|
652
|
+
const item = withId(parseMemory(text), stored.record.item.id);
|
|
653
|
+
if (item.status !== status) {
|
|
654
|
+
throw new Error(`memory store (AIP-72): could not rewrite the status of '${item.id}'`);
|
|
655
|
+
}
|
|
656
|
+
await atomicWrite(stored.path, text);
|
|
657
|
+
return { item, revision: sha256(text) };
|
|
658
|
+
}
|
|
659
|
+
async #assertSlugFree(next, ignore) {
|
|
660
|
+
if (next.slug === void 0 || memoryStatus(next) !== "active") return;
|
|
661
|
+
for (const { record } of await this.#allItems()) {
|
|
662
|
+
const other = record.item;
|
|
663
|
+
if (other.id === next.id || other.id === ignore) continue;
|
|
664
|
+
if (other.slug === next.slug && memoryStatus(other) === "active") {
|
|
665
|
+
throw new MemorySlugTakenError(next.slug, other.id);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
async #dropProposal(proposalId) {
|
|
670
|
+
if (proposalId === void 0) return;
|
|
671
|
+
try {
|
|
672
|
+
await unlink(this.#proposalPath(proposalId));
|
|
673
|
+
} catch (err) {
|
|
674
|
+
if (!isNotFound(err)) throw err;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
async #newId() {
|
|
678
|
+
for (; ; ) {
|
|
679
|
+
const id = `mem_${Array.from(randomBytes(8), (b) => ID_ALPHABET[b % 36]).join("")}`;
|
|
680
|
+
if (!await this.#find(id)) return id;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
async #mustFind(id, scope) {
|
|
684
|
+
const stored = await this.#find(id);
|
|
685
|
+
if (!stored || !visible(stored.record, scope)) throw new MemoryNotFoundError("item", id);
|
|
686
|
+
return stored;
|
|
687
|
+
}
|
|
688
|
+
async #proposalVisible(proposalId, scope) {
|
|
689
|
+
const text = await readText(this.#proposalPath(proposalId));
|
|
690
|
+
return text !== null && isInNodeProjection(parseMemory(text), scope);
|
|
691
|
+
}
|
|
692
|
+
async #find(id) {
|
|
693
|
+
if (!SAFE_ID.test(id)) return null;
|
|
694
|
+
for (const dir of await listDir(join(this.#root, "items"))) {
|
|
695
|
+
const path = join(this.#root, "items", dir, `${id}.md`);
|
|
696
|
+
const text = await readText(path);
|
|
697
|
+
if (text !== null) return { record: itemRecord(text, id), path, text };
|
|
698
|
+
}
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
async #allItems() {
|
|
702
|
+
const out = [];
|
|
703
|
+
for (const dir of await listDir(join(this.#root, "items"))) {
|
|
704
|
+
for (const file of await listDir(join(this.#root, "items", dir))) {
|
|
705
|
+
if (!isMarkdown(file)) continue;
|
|
706
|
+
const path = join(this.#root, "items", dir, file);
|
|
707
|
+
const text = await readText(path);
|
|
708
|
+
if (text !== null) out.push({ record: itemRecord(text, file.slice(0, -3)), path, text });
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return out;
|
|
712
|
+
}
|
|
713
|
+
async #allProposals() {
|
|
714
|
+
const out = [];
|
|
715
|
+
for (const file of await listDir(join(this.#root, "proposals"))) {
|
|
716
|
+
if (!isMarkdown(file)) continue;
|
|
717
|
+
const path = join(this.#root, "proposals", file);
|
|
718
|
+
const text = await readText(path);
|
|
719
|
+
if (text === null) continue;
|
|
720
|
+
const proposalId = file.slice(0, -3);
|
|
721
|
+
out.push({ record: { item: parseMemory(text), revision: sha256(text), proposalId }, path, text });
|
|
722
|
+
}
|
|
723
|
+
return out;
|
|
724
|
+
}
|
|
725
|
+
#itemPath(type, id) {
|
|
726
|
+
return join(this.#root, "items", type, `${id}.md`);
|
|
727
|
+
}
|
|
728
|
+
#proposalPath(proposalId) {
|
|
729
|
+
return join(this.#root, "proposals", `${proposalId}.md`);
|
|
730
|
+
}
|
|
731
|
+
#exclusive(fn) {
|
|
732
|
+
const run = this.#tail.then(fn);
|
|
733
|
+
this.#tail = run.catch(() => void 0);
|
|
734
|
+
return run;
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
function visible(record, scope) {
|
|
738
|
+
return scope === void 0 || isInNodeProjection(record.item, scope);
|
|
739
|
+
}
|
|
740
|
+
function itemRecord(text, id) {
|
|
741
|
+
return { item: withId(parseMemory(text), id), revision: sha256(text) };
|
|
742
|
+
}
|
|
743
|
+
function withId(item, id) {
|
|
744
|
+
return item.id === void 0 ? { ...item, id } : item;
|
|
745
|
+
}
|
|
746
|
+
function itemType(item) {
|
|
747
|
+
return item.type ?? item.kind;
|
|
748
|
+
}
|
|
749
|
+
function assertValid(doc, creating) {
|
|
750
|
+
const result = validateMemory(doc, { canonical: creating });
|
|
751
|
+
if (!result.ok) {
|
|
752
|
+
throw new MemoryValidationError("FilesMemoryStore.write (AIP-72 rule 13)", result.errors);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
function invalid(path, message) {
|
|
756
|
+
const issue = { path, message, rule: "rule-13" };
|
|
757
|
+
return new MemoryValidationError("FilesMemoryStore (AIP-72)", [issue]);
|
|
758
|
+
}
|
|
759
|
+
function checkRevision(id, given, actual) {
|
|
760
|
+
if (given !== actual) throw new MemoryConflictError(id, given, actual);
|
|
761
|
+
}
|
|
762
|
+
function sameContent(a, b) {
|
|
763
|
+
const strip = (d) => {
|
|
764
|
+
const { updated_at: _u, written: _w, ...rest } = d;
|
|
765
|
+
return stableJson({ ...rest, status: memoryStatus(d) });
|
|
766
|
+
};
|
|
767
|
+
return strip(a) === strip(b);
|
|
768
|
+
}
|
|
769
|
+
function stableJson(value) {
|
|
770
|
+
return JSON.stringify(
|
|
771
|
+
value,
|
|
772
|
+
(_key, v) => v && typeof v === "object" && !Array.isArray(v) ? Object.fromEntries(Object.entries(v).sort(([x], [y]) => x < y ? -1 : x > y ? 1 : 0)) : v
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
function clean(item) {
|
|
776
|
+
return JSON.parse(JSON.stringify(item));
|
|
777
|
+
}
|
|
778
|
+
function setStatusLine(text, status) {
|
|
779
|
+
const lines = text.split("\n");
|
|
780
|
+
const open = lines[0]?.replace(/^/, "").trimEnd();
|
|
781
|
+
const close = lines.findIndex((l, i) => i > 0 && l.trimEnd() === "---");
|
|
782
|
+
if (open !== "---" || close < 0) {
|
|
783
|
+
throw new Error("memory store (AIP-72): item file has no frontmatter fence");
|
|
784
|
+
}
|
|
785
|
+
const cr = lines[close].endsWith("\r") ? "\r" : "";
|
|
786
|
+
const line = `status: "${status}"`;
|
|
787
|
+
const at = lines.findIndex((l, i) => i > 0 && i < close && /^status[ \t]*:/.test(l));
|
|
788
|
+
if (at >= 0) lines[at] = line + (lines[at].endsWith("\r") ? "\r" : "");
|
|
789
|
+
else lines.splice(close, 0, line + cr);
|
|
790
|
+
return lines.join("\n");
|
|
791
|
+
}
|
|
792
|
+
function keywordHit(record, terms) {
|
|
793
|
+
if (terms.length === 0) return null;
|
|
794
|
+
const { item } = record;
|
|
795
|
+
const title = [item.title, item.name, item.slug].filter(Boolean).join(" ").toLowerCase();
|
|
796
|
+
const description = (item.description ?? "").toLowerCase();
|
|
797
|
+
const body = item.body.toLowerCase();
|
|
798
|
+
let points = 0;
|
|
799
|
+
for (const term of terms) {
|
|
800
|
+
if (title.includes(term)) points += 3;
|
|
801
|
+
if (description.includes(term)) points += 2;
|
|
802
|
+
if (body.includes(term)) points += 1;
|
|
803
|
+
}
|
|
804
|
+
if (points === 0) return null;
|
|
805
|
+
const snippetFrom = [item.body, item.description ?? ""].find(
|
|
806
|
+
(t) => terms.some((term) => t.toLowerCase().includes(term))
|
|
807
|
+
);
|
|
808
|
+
const snippet = snippetFrom ? around(snippetFrom, terms) : void 0;
|
|
809
|
+
return memoryHit(record, snippet, points / (6 * terms.length));
|
|
810
|
+
}
|
|
811
|
+
function memoryHit(record, snippet, score) {
|
|
812
|
+
const { item } = record;
|
|
813
|
+
return compact({
|
|
814
|
+
source: "memory",
|
|
815
|
+
id: item.id,
|
|
816
|
+
slug: item.slug,
|
|
817
|
+
title: item.title ?? item.name ?? item.id,
|
|
818
|
+
snippet: snippet ?? (item.description || clip(item.body) || void 0),
|
|
819
|
+
score
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
function around(text, terms) {
|
|
823
|
+
const lower = text.toLowerCase();
|
|
824
|
+
const at = Math.min(
|
|
825
|
+
...terms.map((t) => lower.indexOf(t)).filter((i) => i >= 0)
|
|
826
|
+
);
|
|
827
|
+
const start = Math.max(0, at - SNIPPET_CHARS / 4);
|
|
828
|
+
return clip(text.slice(start));
|
|
829
|
+
}
|
|
830
|
+
function clip(text) {
|
|
831
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
832
|
+
return flat.length > SNIPPET_CHARS ? `${flat.slice(0, SNIPPET_CHARS - 1)}\u2026` : flat;
|
|
833
|
+
}
|
|
834
|
+
function compact(value) {
|
|
835
|
+
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
|
|
836
|
+
}
|
|
837
|
+
function byNewest(a, b) {
|
|
838
|
+
const ta = a.item.updated_at ?? a.item.written ?? "";
|
|
839
|
+
const tb = b.item.updated_at ?? b.item.written ?? "";
|
|
840
|
+
if (ta !== tb) return ta < tb ? 1 : -1;
|
|
841
|
+
const ia = a.proposalId ?? a.item.id ?? "";
|
|
842
|
+
const ib = b.proposalId ?? b.item.id ?? "";
|
|
843
|
+
return ia < ib ? -1 : ia > ib ? 1 : 0;
|
|
844
|
+
}
|
|
845
|
+
function toArray(value) {
|
|
846
|
+
if (value === void 0) return void 0;
|
|
847
|
+
return Array.isArray(value) ? value : [value];
|
|
848
|
+
}
|
|
849
|
+
function isMarkdown(file) {
|
|
850
|
+
return file.endsWith(".md") && !file.startsWith(".");
|
|
851
|
+
}
|
|
852
|
+
function sha256(text) {
|
|
853
|
+
return createHash("sha256").update(text).digest("hex");
|
|
854
|
+
}
|
|
855
|
+
async function atomicWrite(path, text) {
|
|
856
|
+
const dir = join(path, "..");
|
|
857
|
+
await mkdir(dir, { recursive: true });
|
|
858
|
+
const tmp = join(dir, `.${randomBytes(6).toString("hex")}.tmp`);
|
|
859
|
+
await writeFile(tmp, text, "utf8");
|
|
860
|
+
try {
|
|
861
|
+
await rename(tmp, path);
|
|
862
|
+
} catch (err) {
|
|
863
|
+
await unlink(tmp).catch(() => void 0);
|
|
864
|
+
throw err;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
async function readText(path) {
|
|
868
|
+
try {
|
|
869
|
+
return await readFile(path, "utf8");
|
|
870
|
+
} catch (err) {
|
|
871
|
+
if (isNotFound(err)) return null;
|
|
872
|
+
throw err;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
async function listDir(path) {
|
|
876
|
+
try {
|
|
877
|
+
return (await readdir(path)).sort();
|
|
878
|
+
} catch (err) {
|
|
879
|
+
if (isNotFound(err)) return [];
|
|
880
|
+
throw err;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
function isNotFound(err) {
|
|
884
|
+
const code = err?.code;
|
|
885
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
886
|
+
}
|
|
887
|
+
|
|
250
888
|
// src/index.ts
|
|
251
889
|
var SPEC_NAME = "memory/v1";
|
|
252
890
|
var SPEC_VERSION = "1.0.0-alpha";
|
|
253
891
|
|
|
254
|
-
export { MEMORY_STATUSES, MEMORY_TYPES, MemoryParseError, MemoryValidationError, SPEC_NAME, SPEC_VERSION, defineMemory, isAllowedStatusTransition, isInForce, memoryFrontmatterSchema, memoryStatus, parseMemory, renderMemory, validateMemory };
|
|
892
|
+
export { FilesMemoryStore, MEMORY_STATUSES, MEMORY_TYPES, MemoryConflictError, MemoryNotFoundError, MemoryParseError, MemorySlugTakenError, MemoryStoreError, MemoryTransitionError, MemoryValidationError, SPEC_NAME, SPEC_VERSION, applyWriteScope, assertRetagOnly, defineMemory, isAllowedStatusTransition, isInForce, isInNodeProjection, isNodeOrOperatorTag, memoryFrontmatterSchema, memoryStatus, nodeAnchorIds, nodeTag, operatorAnchorIds, operatorTag, parseMemory, renderMemory, validateMemory };
|
|
255
893
|
//# sourceMappingURL=index.mjs.map
|
|
256
894
|
//# sourceMappingURL=index.mjs.map
|