@remnic/core 9.3.622 → 9.3.623
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-cli.js +24 -24
- package/dist/access-http.js +9 -9
- package/dist/access-mcp.js +8 -8
- package/dist/access-service.js +7 -7
- package/dist/briefing.js +4 -4
- package/dist/buffer-surprise.js +3 -3
- package/dist/calibration.js +2 -2
- package/dist/causal-consolidation.js +8 -8
- package/dist/{chunk-UK727RHF.js → chunk-2L54V4ZO.js} +3 -3
- package/dist/{chunk-YPNGPHNZ.js → chunk-2UFQYU5F.js} +2 -2
- package/dist/{chunk-XAZOWLW4.js → chunk-3VONWEQB.js} +3 -3
- package/dist/{chunk-BF7ZRHH2.js → chunk-66SLUXKM.js} +2 -2
- package/dist/{chunk-AVHPSLQ2.js → chunk-AYHXQR53.js} +2 -2
- package/dist/{chunk-LANHQ7EN.js → chunk-BNW5NJJH.js} +2 -2
- package/dist/{chunk-6GMPIJAZ.js → chunk-C3IW2F5Z.js} +2 -2
- package/dist/{chunk-VNR3K2R3.js → chunk-C4PZTWTG.js} +14 -14
- package/dist/{chunk-3GM7ZY6H.js → chunk-FMGWXIES.js} +4 -4
- package/dist/{chunk-LMZ7XQBB.js → chunk-GLWW3EJQ.js} +3 -3
- package/dist/{chunk-2GRRN7SZ.js → chunk-GYTVOLNX.js} +2 -2
- package/dist/{chunk-IMA6GU4Y.js → chunk-H3PHZLMF.js} +3 -3
- package/dist/chunk-H3PHZLMF.js.map +1 -0
- package/dist/{chunk-XQUIHXNI.js → chunk-I6UCUHLK.js} +4 -4
- package/dist/{chunk-2I2MDQIB.js → chunk-I74SUMNI.js} +2 -2
- package/dist/chunk-I74SUMNI.js.map +1 -0
- package/dist/{chunk-4H5ZJHEN.js → chunk-J6A3CX5N.js} +8 -3
- package/dist/{chunk-4H5ZJHEN.js.map → chunk-J6A3CX5N.js.map} +1 -1
- package/dist/{chunk-DEVUWMME.js → chunk-KGIGRNR6.js} +2 -2
- package/dist/{chunk-EOLCAPOU.js → chunk-KQFQ3IS5.js} +5 -5
- package/dist/{chunk-QSVPYQPG.js → chunk-LDXUBPMO.js} +2 -2
- package/dist/chunk-LDXUBPMO.js.map +1 -0
- package/dist/{chunk-JFEKNTX7.js → chunk-LN4YGHTM.js} +6 -2
- package/dist/chunk-LN4YGHTM.js.map +1 -0
- package/dist/{chunk-WB3LYXC5.js → chunk-MON3LMO7.js} +3 -3
- package/dist/{chunk-GA3PMY73.js → chunk-O4UNM6OR.js} +2 -2
- package/dist/{chunk-4G2RQTAE.js → chunk-OZXVGYGZ.js} +2 -2
- package/dist/{chunk-WCYKT2DE.js → chunk-P4BC54KI.js} +23 -14
- package/dist/chunk-P4BC54KI.js.map +1 -0
- package/dist/{chunk-DXBCNDVD.js → chunk-PJGB7XRR.js} +5 -5
- package/dist/chunk-PJGB7XRR.js.map +1 -0
- package/dist/{chunk-ZNCDQZIS.js → chunk-QFQQFX2H.js} +3 -3
- package/dist/{chunk-ZNCDQZIS.js.map → chunk-QFQQFX2H.js.map} +1 -1
- package/dist/{chunk-CCNZM5UM.js → chunk-R3OQGYOU.js} +2 -2
- package/dist/{chunk-UZB5KHKX.js → chunk-RGMVMVMF.js} +2 -2
- package/dist/chunk-RGMVMVMF.js.map +1 -0
- package/dist/{chunk-EDP57PFC.js → chunk-RKW6QR7W.js} +22 -18
- package/dist/chunk-RKW6QR7W.js.map +1 -0
- package/dist/{chunk-4MHHUPNH.js → chunk-UGEBPVNI.js} +3 -3
- package/dist/{chunk-4WMCPJWX.js → chunk-UQ7RN5HK.js} +22 -13
- package/dist/chunk-UQ7RN5HK.js.map +1 -0
- package/dist/{chunk-ZUNNG6PC.js → chunk-W3BKVM64.js} +2 -2
- package/dist/{chunk-K5O2QY6T.js → chunk-YTWNKQ2G.js} +2 -2
- package/dist/chunk-YTWNKQ2G.js.map +1 -0
- package/dist/{chunk-2SGJY2UY.js → chunk-Z3CCEP6F.js} +3 -3
- package/dist/{chunk-4NS2ELXF.js → chunk-ZJSZNTEI.js} +4 -4
- package/dist/{chunk-UCGCSZP2.js → chunk-ZZPIJPPD.js} +2 -2
- package/dist/chunking.js +1 -1
- package/dist/cli.js +19 -19
- package/dist/compounding/engine.js +4 -4
- package/dist/connectors/codex-materialize-runner.js +5 -5
- package/dist/connectors/codex-materialize.js +1 -1
- package/dist/connectors/index.js +5 -5
- package/dist/contradiction/index.js +2 -2
- package/dist/{contradiction-scan-GD7KUFWS.js → contradiction-scan-AZTGFMPY.js} +3 -3
- package/dist/entity-retrieval.js +4 -4
- package/dist/explicit-capture.js +1 -1
- package/dist/extraction-judge.js +3 -3
- package/dist/extraction.js +3 -3
- package/dist/fallback-llm.js +2 -2
- package/dist/identity-continuity.js +1 -1
- package/dist/index.js +39 -36
- package/dist/index.js.map +1 -1
- package/dist/json-extract.js +1 -1
- package/dist/maintenance/memory-governance.js +4 -4
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +4 -4
- package/dist/maintenance/rebuild-memory-projection.js +5 -5
- package/dist/namespaces/migrate.js +5 -5
- package/dist/namespaces/storage.js +4 -4
- package/dist/operator-toolkit.js +7 -7
- package/dist/orchestrator.js +21 -21
- package/dist/peers/index.js +1 -1
- package/dist/recall-planner-llm.js +2 -2
- package/dist/schemas.d.ts +22 -22
- package/dist/semantic-chunking.js +2 -2
- package/dist/semantic-consolidation.js +6 -6
- package/dist/semantic-rule-promotion.js +4 -4
- package/dist/semantic-rule-verifier.js +4 -4
- package/dist/source-attribution.js +1 -1
- package/dist/storage.js +3 -3
- package/dist/summarizer.js +3 -3
- package/dist/temporal-supersession.js +1 -1
- package/dist/transfer/types.d.ts +12 -12
- package/dist/verified-recall.js +4 -4
- package/package.json +1 -1
- package/src/chunking.ts +38 -23
- package/src/coding/review-context.ts +7 -1
- package/src/connectors/codex-materialize.ts +6 -1
- package/src/explicit-capture.ts +7 -2
- package/src/identity-continuity.ts +7 -1
- package/src/json-extract.ts +4 -1
- package/src/orchestrator.ts +5 -1
- package/src/peers/profile-reasoner.ts +4 -1
- package/src/semantic-chunking.ts +32 -16
- package/src/semantic-consolidation.ts +4 -1
- package/src/source-attribution.test.ts +21 -0
- package/src/source-attribution.ts +17 -2
- package/src/storage.ts +11 -2
- package/src/temporal-supersession.ts +4 -1
- package/dist/chunk-2I2MDQIB.js.map +0 -1
- package/dist/chunk-4WMCPJWX.js.map +0 -1
- package/dist/chunk-DXBCNDVD.js.map +0 -1
- package/dist/chunk-EDP57PFC.js.map +0 -1
- package/dist/chunk-IMA6GU4Y.js.map +0 -1
- package/dist/chunk-JFEKNTX7.js.map +0 -1
- package/dist/chunk-K5O2QY6T.js.map +0 -1
- package/dist/chunk-QSVPYQPG.js.map +0 -1
- package/dist/chunk-UZB5KHKX.js.map +0 -1
- package/dist/chunk-WCYKT2DE.js.map +0 -1
- /package/dist/{chunk-UK727RHF.js.map → chunk-2L54V4ZO.js.map} +0 -0
- /package/dist/{chunk-YPNGPHNZ.js.map → chunk-2UFQYU5F.js.map} +0 -0
- /package/dist/{chunk-XAZOWLW4.js.map → chunk-3VONWEQB.js.map} +0 -0
- /package/dist/{chunk-BF7ZRHH2.js.map → chunk-66SLUXKM.js.map} +0 -0
- /package/dist/{chunk-AVHPSLQ2.js.map → chunk-AYHXQR53.js.map} +0 -0
- /package/dist/{chunk-LANHQ7EN.js.map → chunk-BNW5NJJH.js.map} +0 -0
- /package/dist/{chunk-6GMPIJAZ.js.map → chunk-C3IW2F5Z.js.map} +0 -0
- /package/dist/{chunk-VNR3K2R3.js.map → chunk-C4PZTWTG.js.map} +0 -0
- /package/dist/{chunk-3GM7ZY6H.js.map → chunk-FMGWXIES.js.map} +0 -0
- /package/dist/{chunk-LMZ7XQBB.js.map → chunk-GLWW3EJQ.js.map} +0 -0
- /package/dist/{chunk-2GRRN7SZ.js.map → chunk-GYTVOLNX.js.map} +0 -0
- /package/dist/{chunk-XQUIHXNI.js.map → chunk-I6UCUHLK.js.map} +0 -0
- /package/dist/{chunk-DEVUWMME.js.map → chunk-KGIGRNR6.js.map} +0 -0
- /package/dist/{chunk-EOLCAPOU.js.map → chunk-KQFQ3IS5.js.map} +0 -0
- /package/dist/{chunk-WB3LYXC5.js.map → chunk-MON3LMO7.js.map} +0 -0
- /package/dist/{chunk-GA3PMY73.js.map → chunk-O4UNM6OR.js.map} +0 -0
- /package/dist/{chunk-4G2RQTAE.js.map → chunk-OZXVGYGZ.js.map} +0 -0
- /package/dist/{chunk-CCNZM5UM.js.map → chunk-R3OQGYOU.js.map} +0 -0
- /package/dist/{chunk-4MHHUPNH.js.map → chunk-UGEBPVNI.js.map} +0 -0
- /package/dist/{chunk-ZUNNG6PC.js.map → chunk-W3BKVM64.js.map} +0 -0
- /package/dist/{chunk-2SGJY2UY.js.map → chunk-Z3CCEP6F.js.map} +0 -0
- /package/dist/{chunk-4NS2ELXF.js.map → chunk-ZJSZNTEI.js.map} +0 -0
- /package/dist/{chunk-UCGCSZP2.js.map → chunk-ZZPIJPPD.js.map} +0 -0
- /package/dist/{contradiction-scan-GD7KUFWS.js.map → contradiction-scan-AZTGFMPY.js.map} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
StorageManager
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-PJGB7XRR.js";
|
|
4
4
|
import {
|
|
5
5
|
decideLifecycleTransition
|
|
6
6
|
} from "./chunk-TBBDFYXW.js";
|
|
@@ -729,4 +729,4 @@ export {
|
|
|
729
729
|
listMemoryGovernanceRuns,
|
|
730
730
|
readMemoryGovernanceRunArtifact
|
|
731
731
|
};
|
|
732
|
-
//# sourceMappingURL=chunk-
|
|
732
|
+
//# sourceMappingURL=chunk-GYTVOLNX.js.map
|
|
@@ -7,8 +7,8 @@ import {
|
|
|
7
7
|
|
|
8
8
|
// src/explicit-capture.ts
|
|
9
9
|
import { randomUUID } from "crypto";
|
|
10
|
-
var INLINE_NOTE_RE = /<memory_note
|
|
11
|
-
var INLINE_NOTE_MARKUP_RE = /<memory_note
|
|
10
|
+
var INLINE_NOTE_RE = /<memory_note>([\s\S]{0,100000}?)<\/memory_note>/gi;
|
|
11
|
+
var INLINE_NOTE_MARKUP_RE = /<memory_note>[\s\S]{0,100000}?<\/memory_note>/i;
|
|
12
12
|
var INLINE_ALLOWED_CATEGORIES = /* @__PURE__ */ new Set([
|
|
13
13
|
"fact",
|
|
14
14
|
"preference",
|
|
@@ -435,4 +435,4 @@ export {
|
|
|
435
435
|
shouldSkipImplicitExtraction,
|
|
436
436
|
shouldProcessInlineExplicitCapture
|
|
437
437
|
};
|
|
438
|
-
//# sourceMappingURL=chunk-
|
|
438
|
+
//# sourceMappingURL=chunk-H3PHZLMF.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/explicit-capture.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { Orchestrator } from \"./orchestrator.js\";\nimport { isSafeRouteNamespace } from \"./routing/engine.js\";\nimport { sanitizeMemoryContent } from \"./sanitize.js\";\nimport { ContentHashIndex } from \"./storage.js\";\nimport type { CaptureMode, MemoryCategory, MemoryLifecycleEvent, PluginConfig } from \"./types.js\";\n\nexport type ExplicitCaptureInput = {\n content: string;\n category?: string;\n confidence?: number;\n namespace?: string;\n tags?: string[];\n entityRef?: string;\n ttl?: string;\n sourceReason?: string;\n};\n\nexport type ValidExplicitCapture = {\n content: string;\n category: MemoryCategory;\n confidence: number;\n namespace?: string;\n tags: string[];\n entityRef?: string;\n expiresAt?: string;\n sourceReason?: string;\n /**\n * When true, `namespace` was already resolved AND authorized by the caller\n * (the access service's `resolveCodingScopedWriteNamespace`, which auth-checks\n * the base and derives a session-owned `project-*` overlay). The persist /\n * queue layer then routes to it directly instead of re-validating against the\n * static policy allow-list — which would otherwise reject legitimately-derived\n * dynamic project namespaces (#1434). Callers that do NOT pre-authorize the\n * namespace must leave this unset so the allow-list guard still applies.\n */\n namespacePreResolved?: boolean;\n};\n\nexport type ExplicitCaptureSource = \"memory_store\" | \"memory_capture\" | \"suggestion_submit\" | \"inline\";\ntype ExplicitCaptureValidationMode = \"legacy_tool\" | \"strict_explicit\";\n\n// Bounded body {0,100000} instead of an unbounded lazy *? so scanning for the\n// closing tag cannot backtrack polynomially on unterminated <memory_note>\n// markup in hostile turn text (CodeQL js/polynomial-redos). 100 000 chars far\n// exceeds any real inline note, so matching is behavior-preserving; the outer\n// \\s* groups were also dropped (body absorbs whitespace; captures are trimmed).\nconst INLINE_NOTE_RE = /<memory_note>([\\s\\S]{0,100000}?)<\\/memory_note>/gi;\nconst INLINE_NOTE_MARKUP_RE = /<memory_note>[\\s\\S]{0,100000}?<\\/memory_note>/i;\nconst INLINE_ALLOWED_CATEGORIES = new Set<MemoryCategory>([\n \"fact\",\n \"preference\",\n \"correction\",\n \"entity\",\n \"decision\",\n \"relationship\",\n \"principle\",\n \"commitment\",\n \"moment\",\n \"skill\",\n \"rule\",\n \"procedure\",\n \"reasoning_trace\",\n]);\n\nconst SECRET_PATTERNS: RegExp[] = [\n /\\bsk-[A-Za-z0-9]{16,}\\b/,\n /\\bAKIA[0-9A-Z]{16}\\b/,\n /\\bBearer\\s+[A-Za-z0-9._-]{16,}\\b/i,\n /\\b(?:api[_-]?key|secret|token|password|passwd)\\s*[:=]\\s*[^\\s]{8,}\\b/i,\n /\\b(?:authorization)\\s*:\\s*[^\\s]{8,}\\b/i,\n];\nconst SECRET_REDACTION_PATTERNS: Array<{ pattern: RegExp; replacement: string }> = [\n { pattern: /\\bsk-[A-Za-z0-9]{16,}\\b/g, replacement: \"[redacted openai key]\" },\n { pattern: /\\bAKIA[0-9A-Z]{16}\\b/g, replacement: \"[redacted aws key]\" },\n { pattern: /\\bBearer\\s+[A-Za-z0-9._-]{16,}\\b/gi, replacement: \"Bearer [redacted token]\" },\n {\n pattern: /\\b(?:api[_-]?key|secret|token|password|passwd)\\s*[:=]\\s*[^\\s]{8,}\\b/gi,\n replacement: \"[redacted credential]\",\n },\n {\n pattern: /\\b(?:authorization)\\s*:\\s*[^\\s]{8,}\\b/gi,\n replacement: \"authorization: [redacted credential]\",\n },\n];\nconst EXPLICIT_CAPTURE_REVIEW_TAGS = [\"explicit-capture\", \"queued-review\"];\n\nfunction explicitCaptureActor(source: ExplicitCaptureSource): string {\n switch (source) {\n case \"inline\":\n return \"inline.memory_note\";\n case \"memory_store\":\n return \"tool.memory_store\";\n case \"suggestion_submit\":\n return \"tool.suggestion_submit\";\n default:\n return \"tool.memory_capture\";\n }\n}\n\nfunction asTrimmed(value: string | undefined): string | undefined {\n const trimmed = value?.trim();\n return trimmed && trimmed.length > 0 ? trimmed : undefined;\n}\n\nfunction normalizeCaptureContent(value: string): string {\n return value\n .toLowerCase()\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction redactSecrets(value: string): string {\n let redacted = value;\n for (const { pattern, replacement } of SECRET_REDACTION_PATTERNS) {\n redacted = redacted.replace(pattern, replacement);\n }\n return redacted;\n}\n\nfunction containsSecretLikeValue(value: string): boolean {\n return SECRET_PATTERNS.some((pattern) => pattern.test(value));\n}\n\nfunction assertNoSecretLikeMetadata(field: string, value: string | undefined): void {\n const trimmed = asTrimmed(value);\n if (trimmed && containsSecretLikeValue(trimmed)) {\n throw new Error(`${field} appears to contain a secret or credential`);\n }\n}\n\nfunction assertNoSecretLikeMetadataList(field: string, values: string[] | undefined): void {\n for (const value of values ?? []) {\n assertNoSecretLikeMetadata(field, value);\n }\n}\n\nfunction sanitizeReviewText(value: string | undefined, fallback: string): string {\n const redacted = redactSecrets(asTrimmed(value) ?? fallback);\n const sanitized = sanitizeMemoryContent(redacted);\n const safe = sanitized.text.trim();\n return safe.length > 0 ? safe : fallback;\n}\n\nfunction sanitizeReviewMetadata(value: string | undefined): string | undefined {\n const trimmed = asTrimmed(value);\n if (!trimmed) return undefined;\n return sanitizeReviewText(trimmed, \"[redacted]\");\n}\n\nfunction sanitizeReviewTags(tags: string[] | undefined): string[] {\n return Array.from(new Set((tags ?? [])\n .map((tag) => sanitizeReviewMetadata(tag))\n .filter((tag): tag is string => typeof tag === \"string\" && tag.length > 0)));\n}\n\nfunction normalizeExplicitCaptureError(error: unknown): string {\n if (error instanceof Error && error.message.trim().length > 0) return error.message.trim();\n const rendered = String(error).trim();\n return rendered.length > 0 ? rendered : \"explicit capture failed\";\n}\n\nfunction resolveExplicitCaptureReviewNamespace(\n orchestrator: Orchestrator,\n namespace: string | undefined,\n): string | undefined {\n const normalized = asTrimmed(namespace);\n if (!normalized) return undefined;\n return resolveExplicitCaptureNamespace(orchestrator, normalized);\n}\n\nfunction resolveExplicitCaptureNamespace(\n orchestrator: Orchestrator,\n namespace: string | undefined,\n): string | undefined {\n const normalized = asTrimmed(namespace);\n if (!normalized) return undefined;\n if (!orchestrator.config.namespacesEnabled) {\n if (normalized !== orchestrator.config.defaultNamespace) {\n throw new Error(`unsupported namespace: ${normalized}`);\n }\n return normalized;\n }\n const allowed = new Set([\n orchestrator.config.defaultNamespace,\n orchestrator.config.sharedNamespace,\n ...orchestrator.config.namespacePolicies.map((policy) => policy.name),\n ].map((value) => value.trim()).filter(Boolean));\n if (!allowed.has(normalized)) {\n throw new Error(`unsupported namespace: ${normalized}`);\n }\n return normalized;\n}\n\nfunction parseExplicitCaptureTtl(ttl: string | undefined): string | undefined {\n const raw = asTrimmed(ttl);\n if (!raw) return undefined;\n\n const absoluteMs = Date.parse(raw);\n if (Number.isFinite(absoluteMs)) {\n return new Date(absoluteMs).toISOString();\n }\n\n const relative = raw.match(/^(\\d+)\\s*([mhdw])$/i);\n if (!relative) {\n throw new Error(\"ttl must be an ISO-8601 timestamp or relative duration like 30m, 12h, 7d, or 2w\");\n }\n\n const amount = Number.parseInt(relative[1] ?? \"\", 10);\n const unit = (relative[2] ?? \"\").toLowerCase();\n if (!Number.isFinite(amount) || amount <= 0) {\n throw new Error(\"ttl duration must be a positive integer\");\n }\n\n const multiplier =\n unit === \"m\" ? 60_000\n : unit === \"h\" ? 60 * 60_000\n : unit === \"d\" ? 24 * 60 * 60_000\n : 7 * 24 * 60 * 60_000;\n return new Date(Date.now() + amount * multiplier).toISOString();\n}\n\nfunction parseInlineConfidence(value: string): number {\n const trimmed = value.trim();\n if (!/^[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[+-]?\\d+)?$/i.test(trimmed)) {\n return Number.NaN;\n }\n const parsed = Number(trimmed);\n return Number.isFinite(parsed) ? parsed : Number.NaN;\n}\n\nfunction parseInlineNote(block: string): ExplicitCaptureInput | null {\n const lines = block.replace(/\\r/g, \"\").split(\"\\n\");\n const note: Partial<ExplicitCaptureInput> = {};\n let idx = 0;\n\n while (idx < lines.length) {\n const rawLine = lines[idx] ?? \"\";\n const line = rawLine.trim();\n idx += 1;\n if (line.length === 0) continue;\n const colonIdx = line.indexOf(\":\");\n if (colonIdx < 0) continue;\n const key = line.slice(0, colonIdx).trim();\n const value = line.slice(colonIdx + 1).trim();\n\n if (key === \"content\" && value === \"|\") {\n const contentLines: string[] = [];\n while (idx < lines.length) {\n const next = lines[idx] ?? \"\";\n if (next.startsWith(\" \") || next.startsWith(\"\\t\")) {\n contentLines.push(next.replace(/^( |\\t)/, \"\"));\n idx += 1;\n continue;\n }\n if (next.trim().length === 0) {\n contentLines.push(\"\");\n idx += 1;\n continue;\n }\n break;\n }\n note.content = contentLines.join(\"\\n\").trim();\n continue;\n }\n\n switch (key) {\n case \"content\":\n note.content = value;\n break;\n case \"category\":\n note.category = value;\n break;\n case \"confidence\":\n note.confidence = parseInlineConfidence(value);\n break;\n case \"namespace\":\n note.namespace = value;\n break;\n case \"tags\":\n note.tags = value\n .split(\",\")\n .map((entry) => entry.trim())\n .filter(Boolean);\n break;\n case \"entityRef\":\n note.entityRef = value;\n break;\n case \"ttl\":\n note.ttl = value;\n break;\n case \"sourceReason\":\n note.sourceReason = value;\n break;\n default:\n break;\n }\n }\n\n return asTrimmed(note.content) ? (note as ExplicitCaptureInput) : null;\n}\n\nexport function parseInlineExplicitCaptureNotes(text: string): ExplicitCaptureInput[] {\n const notes: ExplicitCaptureInput[] = [];\n for (const match of text.matchAll(INLINE_NOTE_RE)) {\n const parsed = parseInlineNote(match[1] ?? \"\");\n if (parsed) notes.push(parsed);\n }\n return notes;\n}\n\nexport function hasInlineExplicitCaptureMarkup(text: string): boolean {\n return INLINE_NOTE_MARKUP_RE.test(text);\n}\n\nexport function stripInlineExplicitCaptureNotes(text: string): string {\n return text.replace(INLINE_NOTE_RE, \"\").trim();\n}\n\nexport function validateExplicitCaptureInput(\n input: ExplicitCaptureInput,\n mode: ExplicitCaptureValidationMode = \"strict_explicit\",\n): ValidExplicitCapture {\n const content = asTrimmed(input.content);\n if (!content) throw new Error(\"content is required\");\n if (mode === \"strict_explicit\") {\n if (content.length < 10) throw new Error(\"content must be at least 10 characters\");\n if (content.length > 4000) throw new Error(\"content must be 4000 characters or fewer\");\n }\n if (/<memory_note>/i.test(content) || /<\\/memory_note>/i.test(content)) {\n throw new Error(\"nested memory_note blocks are not allowed\");\n }\n\n const category = (asTrimmed(input.category) ?? \"fact\") as MemoryCategory;\n if (!INLINE_ALLOWED_CATEGORIES.has(category)) {\n throw new Error(`unsupported category: ${input.category ?? category}`);\n }\n\n const sanitized = sanitizeMemoryContent(content);\n if (!sanitized.clean) {\n throw new Error(\"content failed memory sanitization\");\n }\n for (const pattern of SECRET_PATTERNS) {\n if (pattern.test(content)) {\n throw new Error(\"content appears to contain a secret or credential\");\n }\n }\n assertNoSecretLikeMetadata(\"sourceReason\", input.sourceReason);\n assertNoSecretLikeMetadata(\"entityRef\", input.entityRef);\n assertNoSecretLikeMetadata(\"ttl\", input.ttl);\n assertNoSecretLikeMetadataList(\"tags\", input.tags);\n\n if (input.confidence !== undefined && !Number.isFinite(input.confidence)) {\n throw new Error(\"confidence must be a finite number\");\n }\n const confidence = input.confidence === undefined ? 0.95 : Number(input.confidence);\n if (confidence < 0 || confidence > 1) {\n throw new Error(\"confidence must be between 0 and 1\");\n }\n const requestedNamespace = asTrimmed(input.namespace);\n if (requestedNamespace && !isSafeRouteNamespace(requestedNamespace)) {\n throw new Error(`unsafe namespace: ${requestedNamespace}`);\n }\n const expiresAt = parseExplicitCaptureTtl(input.ttl);\n\n return {\n content,\n category,\n confidence,\n namespace: asTrimmed(input.namespace),\n tags: Array.from(new Set((input.tags ?? []).map((tag) => tag.trim()).filter(Boolean))),\n entityRef: asTrimmed(input.entityRef),\n expiresAt,\n sourceReason: asTrimmed(input.sourceReason),\n };\n}\n\nasync function findDuplicateExplicitCapture(\n orchestrator: Orchestrator,\n resolvedNamespace: string | undefined,\n candidate: ValidExplicitCapture,\n): Promise<string | null> {\n const storage = await orchestrator.getStorage(resolvedNamespace);\n if (\n candidate.category === \"fact\"\n && typeof (storage as { hasFactContentHash?: (content: string) => Promise<boolean> }).hasFactContentHash === \"function\"\n ) {\n try {\n const hasHash = await (storage as { hasFactContentHash: (content: string) => Promise<boolean> }).hasFactContentHash(\n candidate.content,\n );\n if (!hasHash) {\n const authoritative =\n typeof (storage as { isFactContentHashAuthoritative?: () => Promise<boolean> | boolean }).isFactContentHashAuthoritative\n === \"function\"\n ? await (storage as { isFactContentHashAuthoritative: () => Promise<boolean> | boolean })\n .isFactContentHashAuthoritative()\n : false;\n if (authoritative) return null;\n }\n } catch (err) {\n // Fail open: hash index is only an optimization, so fall back to the full corpus scan.\n void err;\n }\n }\n const existing = await storage.readAllMemories();\n const normalizedCandidate = normalizeCaptureContent(candidate.content);\n const match = existing.find((memory) => {\n const status = memory.frontmatter.status ?? \"active\";\n if (status !== \"active\") return false;\n if (memory.frontmatter.category !== candidate.category) return false;\n return normalizeCaptureContent(memory.content) === normalizedCandidate;\n });\n return match?.frontmatter.id ?? null;\n}\n\nexport async function persistExplicitCapture(\n orchestrator: Orchestrator,\n candidate: ValidExplicitCapture,\n source: ExplicitCaptureSource,\n): Promise<{ id: string; duplicateOf?: string }> {\n const resolvedNamespace = candidate.namespacePreResolved\n ? asTrimmed(candidate.namespace)\n : resolveExplicitCaptureNamespace(orchestrator, candidate.namespace);\n const duplicateOf = await findDuplicateExplicitCapture(orchestrator, resolvedNamespace, candidate);\n if (duplicateOf) {\n return { id: duplicateOf, duplicateOf };\n }\n\n const storage = await orchestrator.getStorage(resolvedNamespace);\n const id = await storage.writeMemory(candidate.category, candidate.content, {\n confidence: candidate.confidence,\n tags: candidate.tags,\n entityRef: candidate.entityRef,\n expiresAt: candidate.expiresAt,\n source: source === \"inline\" ? \"explicit-inline\" : \"explicit\",\n });\n\n const created = new Date().toISOString();\n const event: MemoryLifecycleEvent = {\n eventId: `mle-${randomUUID()}`,\n memoryId: id,\n eventType: \"explicit_capture_accepted\",\n timestamp: created,\n actor: explicitCaptureActor(source),\n reasonCode: candidate.sourceReason,\n ruleVersion: \"explicit-capture.v1\",\n };\n await storage.appendMemoryLifecycleEvents([event]);\n\n return { id };\n}\n\nfunction buildExplicitCaptureReviewContent(input: ExplicitCaptureInput, reason: string): string {\n const requestedContent = asTrimmed(input.content);\n const safeContent = sanitizeReviewText(requestedContent, \"[empty explicit capture]\");\n const safeCategory = sanitizeReviewMetadata(input.category);\n const safeNamespace = sanitizeReviewMetadata(input.namespace);\n const safeEntityRef = sanitizeReviewMetadata(input.entityRef);\n const safeTtl = sanitizeReviewMetadata(input.ttl);\n const safeSourceReason = sanitizeReviewMetadata(input.sourceReason);\n const safeTags = sanitizeReviewTags(input.tags);\n const lines = [\n \"Explicit capture queued for review.\",\n \"\",\n `Reason: ${reason}`,\n \"\",\n \"Submitted content:\",\n safeContent,\n ];\n const metadata = [\n safeCategory ? `Requested category: ${safeCategory}` : undefined,\n safeNamespace ? `Requested namespace: ${safeNamespace}` : undefined,\n safeEntityRef ? `Requested entityRef: ${safeEntityRef}` : undefined,\n safeTtl ? `Requested ttl: ${safeTtl}` : undefined,\n safeSourceReason ? `Requested sourceReason: ${safeSourceReason}` : undefined,\n safeTags.length > 0 ? `Requested tags: ${safeTags.join(\", \")}` : undefined,\n ].filter((entry): entry is string => typeof entry === \"string\" && entry.length > 0);\n if (metadata.length > 0) {\n lines.push(\"\", ...metadata);\n }\n return lines.join(\"\\n\");\n}\n\nasync function findQueuedExplicitCaptureDuplicate(\n orchestrator: Orchestrator,\n namespace: string | undefined,\n content: string,\n): Promise<string | null> {\n const storage = await orchestrator.getStorage(namespace);\n const existing = await storage.readAllMemories();\n const normalized = normalizeCaptureContent(content);\n const match = existing.find((memory) => {\n const status = memory.frontmatter.status ?? \"active\";\n if (status !== \"pending_review\") return false;\n if (!(memory.frontmatter.tags ?? []).includes(\"queued-review\")) return false;\n return normalizeCaptureContent(memory.content) === normalized;\n });\n return match?.frontmatter.id ?? null;\n}\n\nexport async function queueExplicitCaptureForReview(\n orchestrator: Orchestrator,\n input: ExplicitCaptureInput,\n source: ExplicitCaptureSource,\n error: unknown,\n): Promise<{ id: string; duplicateOf?: string }> {\n const reason = sanitizeReviewText(normalizeExplicitCaptureError(error), \"explicit capture failed\");\n const requestedNamespace = asTrimmed(input.namespace);\n // A caller-pre-authorized namespace (e.g. a session-owned project overlay\n // from the access service) routes directly; otherwise apply the static\n // policy allow-list guard (#1434).\n const queueNamespace = (input as { namespacePreResolved?: boolean }).namespacePreResolved\n ? requestedNamespace\n : resolveExplicitCaptureReviewNamespace(orchestrator, requestedNamespace);\n const content = buildExplicitCaptureReviewContent(input, reason);\n const duplicateOf = await findQueuedExplicitCaptureDuplicate(orchestrator, queueNamespace, content);\n if (duplicateOf) {\n return { id: duplicateOf, duplicateOf };\n }\n\n const requestedCategory = asTrimmed(input.category);\n const reviewCategory = requestedCategory && INLINE_ALLOWED_CATEGORIES.has(requestedCategory as MemoryCategory)\n ? requestedCategory as MemoryCategory\n : \"fact\";\n const requestedTags = sanitizeReviewTags(input.tags);\n const storage = await orchestrator.getStorage(queueNamespace);\n const id = await storage.writeMemory(reviewCategory, content, {\n confidence: 0.2,\n tags: Array.from(new Set([...EXPLICIT_CAPTURE_REVIEW_TAGS, ...requestedTags])),\n entityRef: sanitizeReviewMetadata(input.entityRef),\n source: source === \"inline\" ? \"explicit-inline-review\" : \"explicit-review\",\n });\n const created = await storage.getMemoryById(id);\n if (created) {\n await storage.writeMemoryFrontmatter(created, {\n status: \"pending_review\",\n updated: new Date().toISOString(),\n }, {\n actor: explicitCaptureActor(source),\n reasonCode: reason,\n ruleVersion: \"explicit-capture.v1\",\n });\n }\n const event: MemoryLifecycleEvent = {\n eventId: `mle-${randomUUID()}`,\n memoryId: id,\n eventType: \"explicit_capture_queued\",\n timestamp: new Date().toISOString(),\n actor: explicitCaptureActor(source),\n reasonCode: reason,\n ruleVersion: \"explicit-capture.v1\",\n };\n await storage.appendMemoryLifecycleEvents([event]);\n return { id };\n}\n\nexport function shouldSkipImplicitExtraction(cfg: Pick<PluginConfig, \"captureMode\">): boolean {\n return cfg.captureMode === \"explicit\";\n}\n\nexport function shouldProcessInlineExplicitCapture(cfg: Pick<PluginConfig, \"captureMode\">): boolean {\n return cfg.captureMode !== \"implicit\";\n}\n"],"mappings":"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","names":[]}
|
|
@@ -11,13 +11,13 @@ import {
|
|
|
11
11
|
} from "./chunk-D24OXEPB.js";
|
|
12
12
|
import {
|
|
13
13
|
EngramAccessInputError
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-KQFQ3IS5.js";
|
|
15
15
|
import {
|
|
16
16
|
projectTagProjectId
|
|
17
17
|
} from "./chunk-EDQVAMQI.js";
|
|
18
18
|
import {
|
|
19
19
|
validateBriefingFormat
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-OZXVGYGZ.js";
|
|
21
21
|
import {
|
|
22
22
|
expandTildePath
|
|
23
23
|
} from "./chunk-EYIEWJNI.js";
|
|
@@ -2712,7 +2712,7 @@ ${body}`;
|
|
|
2712
2712
|
}
|
|
2713
2713
|
case "engram.contradiction_scan_run":
|
|
2714
2714
|
case "remnic.contradiction_scan_run": {
|
|
2715
|
-
const { runContradictionScan } = await import("./contradiction-scan-
|
|
2715
|
+
const { runContradictionScan } = await import("./contradiction-scan-AZTGFMPY.js");
|
|
2716
2716
|
return runContradictionScan({
|
|
2717
2717
|
storage: this.service.storageRef,
|
|
2718
2718
|
config: this.service.configRef,
|
|
@@ -2903,4 +2903,4 @@ ${body}`;
|
|
|
2903
2903
|
export {
|
|
2904
2904
|
EngramMcpServer
|
|
2905
2905
|
};
|
|
2906
|
-
//# sourceMappingURL=chunk-
|
|
2906
|
+
//# sourceMappingURL=chunk-I6UCUHLK.js.map
|
|
@@ -40,7 +40,7 @@ function buildPeerProfileReasonerPrompt(input) {
|
|
|
40
40
|
function parsePeerProfileReasonerResponse(raw) {
|
|
41
41
|
if (typeof raw !== "string" || raw.trim() === "") return [];
|
|
42
42
|
const trimmed = raw.trim();
|
|
43
|
-
const fenced = /^```(?:json)
|
|
43
|
+
const fenced = /^```(?:json)?([\s\S]*?)```$/u.exec(trimmed);
|
|
44
44
|
const payload = fenced ? fenced[1].trim() : trimmed;
|
|
45
45
|
let parsed;
|
|
46
46
|
try {
|
|
@@ -347,4 +347,4 @@ export {
|
|
|
347
347
|
parsePeerProfileReasonerResponse,
|
|
348
348
|
runPeerProfileReasoner
|
|
349
349
|
};
|
|
350
|
-
//# sourceMappingURL=chunk-
|
|
350
|
+
//# sourceMappingURL=chunk-I74SUMNI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/peers/profile-reasoner.ts"],"sourcesContent":["/**\n * Peer profile reasoner — issue #679 PR 2/5.\n *\n * Pure async function that, for each peer:\n *\n * 1. Reads recent interaction-log entries via `readPeerInteractionLog`.\n * 2. Calls an injected LLM client (same chat shape as\n * `FallbackLlmClient.chatCompletion`) to derive 0..N profile-field\n * proposals with provenance `{observedAt, signal, sourceSessionId,\n * note}`.\n * 3. Merges the proposals into the peer's existing `PeerProfile`,\n * appending provenance entries (never replacing existing\n * provenance — the reasoner is additive by design so the operator\n * retains the full audit trail).\n * 4. Writes via `writePeerProfile`.\n *\n * Gating is handled in two layers:\n *\n * - The orchestrator wires the call behind the\n * `peerProfileReasonerEnabled` config flag (default `false` —\n * opt-in per Gotcha #30/#48). The reasoner ALSO short-circuits\n * when `options.enabled !== true`, so direct callers can't\n * accidentally bypass the flag.\n * - Per-peer, the `peerProfileReasonerMinInteractions` threshold\n * skips peers whose log has fewer entries since the last reasoner\n * run than required.\n *\n * The reasoner is intentionally storage-agnostic — it accepts an LLM\n * client by interface (`PeerProfileReasonerLlm`) so tests can mock the\n * call and the orchestrator can inject either the gateway client or\n * a fast local model. No direct OpenAI imports here.\n */\n\nimport {\n appendInteractionLog,\n listPeers,\n readPeerInteractionLog,\n readPeerProfile,\n writePeerProfile,\n} from \"./storage.js\";\nimport type {\n Peer,\n PeerInteractionLogEntry,\n PeerProfile,\n PeerProfileFieldProvenance,\n} from \"./types.js\";\nimport { PEER_ID_PATTERN } from \"./types.js\";\n\n// ──────────────────────────────────────────────────────────────────────\n// Types\n// ──────────────────────────────────────────────────────────────────────\n\n/**\n * Minimal chat-completion contract the reasoner depends on. Matches\n * `FallbackLlmClient.chatCompletion` so the orchestrator can pass it\n * through directly. Tests inject a mock that returns canned strings.\n *\n * Returning `null` means the LLM is unavailable / failed — the\n * reasoner treats that as \"no proposals for this peer\" rather than an\n * error so a flaky LLM never aborts the whole pass.\n */\nexport interface PeerProfileReasonerLlm {\n chatCompletion(\n messages: Array<{ role: \"system\" | \"user\" | \"assistant\"; content: string }>,\n options?: { temperature?: number; maxTokens?: number; timeoutMs?: number },\n ): Promise<{ content: string } | null>;\n}\n\n/**\n * One LLM-proposed profile-field update.\n *\n * `value` is the new markdown string to set under `field`. The\n * provenance entry the LLM emits travels alongside it; the reasoner\n * does NOT trust the LLM's `observedAt` — it always overwrites with\n * the run's `now` timestamp so provenance can never claim future or\n * past observation timestamps the operator didn't witness.\n */\nexport interface PeerProfileReasonerProposal {\n /** Stable section key, e.g. \"communication_style\". */\n readonly field: string;\n /** Markdown value to set under that key. */\n readonly value: string;\n /**\n * Short label for the signal that justified the inference,\n * e.g. \"explicit_preference\", \"tool_pattern\", \"topic_recurrence\".\n */\n readonly signal: string;\n /** Optional free-form note explaining the inference. */\n readonly note?: string;\n /**\n * Originating session id, when the LLM can attribute the inference\n * to a specific log line. Reasoner clamps this to a value that\n * actually appeared in the log window so the LLM can't hallucinate.\n */\n readonly sourceSessionId?: string;\n}\n\nexport interface PeerProfileReasonerOptions {\n /** Memory directory containing the peers/ subtree. */\n readonly memoryDir: string;\n /**\n * Master gate. When `false` (the default the orchestrator passes\n * when the config flag is off), the reasoner is a no-op and\n * returns an empty result. Direct callers must explicitly pass\n * `true` so the gate can never be defaulted ON by accident\n * (Gotcha #48 — least-privileged default).\n */\n readonly enabled: boolean;\n /** Injected LLM client. Required when `enabled === true`. */\n readonly llm?: PeerProfileReasonerLlm;\n /** Model name to log for telemetry; not used to dispatch. */\n readonly model?: string;\n /**\n * Minimum new interaction-log entries since last reasoner run\n * before this peer is processed. Peers below the threshold are\n * skipped with `reason: \"below_min_interactions\"`.\n */\n readonly minInteractions: number;\n /**\n * Hard cap on profile fields the reasoner will accept across all\n * peers in a single run. Tracked in insertion order: once the cap\n * is reached, subsequent proposals are dropped with\n * `dropped_due_to_cap` in the per-peer result. Use to bound LLM\n * cost and reviewer load per pass.\n */\n readonly maxFieldsPerRun: number;\n /**\n * Optional restriction to specific peer ids. When omitted, the\n * reasoner enumerates the entire peer registry via `listPeers`.\n */\n readonly peerIds?: ReadonlyArray<string>;\n /**\n * Maximum number of recent log entries to feed the LLM per peer.\n * Defaults to 50. Bounded so a runaway log can't blow the prompt.\n */\n readonly maxLogEntriesPerPeer?: number;\n /**\n * Reasoner run timestamp. Defaults to `new Date()` at call time.\n * Tests inject a deterministic clock; the orchestrator passes\n * `new Date()` so provenance entries reflect actual wall time.\n */\n readonly now?: Date;\n /**\n * Optional logger; defaults to a no-op so the reasoner stays\n * silent in unit tests. The orchestrator wires its `log` here so\n * runs surface in the gateway log under the\n * `[peer-profile-reasoner]` prefix.\n */\n readonly log?: {\n debug?: (msg: string) => void;\n info?: (msg: string) => void;\n warn?: (msg: string) => void;\n };\n /**\n * Whether to append a `peer_profile_reasoner_run` entry to the\n * peer's interaction log when the reasoner emits at least one\n * field for that peer. Defaults to `true`. Disable in tests that\n * want to assert the log was untouched.\n */\n readonly appendRunMarkerToLog?: boolean;\n /**\n * Optional abort signal. The reasoner checks between peers and\n * returns the partial result if cancelled mid-run.\n */\n readonly signal?: AbortSignal;\n}\n\nexport interface PeerProfileReasonerPeerResult {\n readonly peerId: string;\n readonly status:\n | \"processed\"\n | \"skipped_below_min_interactions\"\n | \"skipped_no_log\"\n | \"skipped_disabled\"\n | \"skipped_no_llm\"\n | \"skipped_llm_unavailable\"\n | \"skipped_invalid_proposal\"\n | \"skipped_cap_reached\"\n | \"skipped_aborted\"\n | \"error\";\n /** Number of fields actually applied to the peer's profile. */\n readonly fieldsApplied: number;\n /** Number of proposals dropped because the per-run cap was hit. */\n readonly droppedDueToCap: number;\n /** Set of field keys applied; useful for tests and telemetry. */\n readonly fields: ReadonlyArray<string>;\n /** Error message, when `status === \"error\"`. */\n readonly error?: string;\n}\n\nexport interface PeerProfileReasonerResult {\n readonly peersConsidered: number;\n readonly peersProcessed: number;\n readonly fieldsApplied: number;\n readonly perPeer: ReadonlyArray<PeerProfileReasonerPeerResult>;\n}\n\n// ──────────────────────────────────────────────────────────────────────\n// Prompt + parser (pure, exported for tests)\n// ──────────────────────────────────────────────────────────────────────\n\n/**\n * Build the user-facing reasoner prompt. The system message carries\n * the strict-JSON instruction; this function emits the user message\n * with the peer context and the recent log slice.\n *\n * The prompt is intentionally schema-prescriptive — sibling modules\n * (`semantic-consolidation.ts`, `extraction-judge.ts`) demonstrated\n * that letting the LLM improvise field names produces unstable\n * profiles across runs.\n */\nexport function buildPeerProfileReasonerPrompt(input: {\n peer: Peer;\n existingProfile: PeerProfile | null;\n log: ReadonlyArray<PeerInteractionLogEntry>;\n maxFields: number;\n}): string {\n const existingFields = input.existingProfile\n ? Object.keys(input.existingProfile.fields)\n : [];\n const logBlock = input.log\n .map((e) => {\n const session = e.sessionId ? ` session=${e.sessionId}` : \"\";\n return `- [${e.timestamp}] (${e.kind})${session} ${e.summary}`;\n })\n .join(\"\\n\");\n return [\n `You are an async peer-profile reasoner. Your job is to read recent interaction-log entries for one peer and propose 0..${input.maxFields} profile-field updates.`,\n \"\",\n `Peer:`,\n ` id: ${input.peer.id}`,\n ` kind: ${input.peer.kind}`,\n ` displayName: ${input.peer.displayName}`,\n \"\",\n `Existing profile field keys (preserve names when proposing updates that refine an existing field): ${existingFields.length > 0 ? existingFields.join(\", \") : \"(none yet)\"}`,\n \"\",\n `Recent interaction log (oldest first):`,\n logBlock.length > 0 ? logBlock : \"(no entries)\",\n \"\",\n `Output a single JSON object: {\"proposals\": [{\"field\": \"<stable_key>\", \"value\": \"<markdown>\", \"signal\": \"<short_label>\", \"note\": \"<optional>\", \"sourceSessionId\": \"<optional>\"}]}.`,\n \"\",\n `Rules:`,\n `1. Only propose fields supported by evidence in the log. Do not invent.`,\n `2. Keys are short snake_case (e.g. \"communication_style\", \"tool_patterns\").`,\n `3. value is markdown. signal is a short label like \"explicit_preference\" or \"topic_recurrence\".`,\n `4. Omit fields you can't justify. Empty proposals array is valid.`,\n `5. Output JSON ONLY — no prose before or after.`,\n ].join(\"\\n\");\n}\n\n/**\n * Parse the LLM response. Tolerates a fenced code block wrapper.\n * Returns an empty array on any malformed payload — the contract is\n * that flaky LLM output silently produces zero proposals rather than\n * surfacing an error to the caller.\n *\n * Exported so unit tests can verify parser behavior without spinning\n * up the full reasoner.\n */\nexport function parsePeerProfileReasonerResponse(\n raw: string,\n): PeerProfileReasonerProposal[] {\n if (typeof raw !== \"string\" || raw.trim() === \"\") return [];\n const trimmed = raw.trim();\n // Dropped the \\s* groups around the lazy body (they overlapped it and\n // backtracked polynomially — CodeQL js/polynomial-redos). Input is already\n // trimmed and fenced[1] is trimmed below, so matches are identical.\n const fenced = /^```(?:json)?([\\s\\S]*?)```$/u.exec(trimmed);\n const payload = fenced ? fenced[1].trim() : trimmed;\n let parsed: unknown;\n try {\n parsed = JSON.parse(payload);\n } catch {\n return [];\n }\n // Gotcha #18: JSON.parse('null') succeeds. Reject non-objects.\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n return [];\n }\n const obj = parsed as { proposals?: unknown };\n if (!Array.isArray(obj.proposals)) return [];\n const out: PeerProfileReasonerProposal[] = [];\n // Gotcha — drop prototype-pollution keys at the field-name layer.\n const RESERVED_KEYS = new Set([\"__proto__\", \"constructor\", \"prototype\"]);\n for (const item of obj.proposals) {\n if (typeof item !== \"object\" || item === null || Array.isArray(item)) continue;\n const r = item as Record<string, unknown>;\n if (typeof r.field !== \"string\" || r.field.trim() === \"\") continue;\n if (RESERVED_KEYS.has(r.field)) continue;\n if (typeof r.value !== \"string\" || r.value.trim() === \"\") continue;\n if (typeof r.signal !== \"string\" || r.signal.trim() === \"\") continue;\n const proposal: PeerProfileReasonerProposal = {\n field: r.field,\n value: r.value,\n signal: r.signal,\n ...(typeof r.note === \"string\" && r.note.length > 0 ? { note: r.note } : {}),\n ...(typeof r.sourceSessionId === \"string\" && r.sourceSessionId.length > 0\n ? { sourceSessionId: r.sourceSessionId }\n : {}),\n };\n out.push(proposal);\n }\n return out;\n}\n\n// ──────────────────────────────────────────────────────────────────────\n// Reasoner core\n// ──────────────────────────────────────────────────────────────────────\n\nconst SYSTEM_MESSAGE =\n 'You are a peer-profile reasoner. Output ONLY a JSON object of the form {\"proposals\":[{\"field\":\"...\",\"value\":\"...\",\"signal\":\"...\",\"note\":\"...\",\"sourceSessionId\":\"...\"}]}. No prose, no fenced code block, no commentary.';\n\nconst RUN_MARKER_KIND = \"peer_profile_reasoner_run\";\n\n/**\n * Find the most recent reasoner-run marker timestamp in the log.\n * Used to count \"interactions since last run\" so the threshold\n * gate doesn't keep firing on the same dormant log forever.\n */\nfunction lastRunTimestamp(\n log: ReadonlyArray<PeerInteractionLogEntry>,\n): string | undefined {\n let latest: string | undefined;\n for (const entry of log) {\n if (entry.kind !== RUN_MARKER_KIND) continue;\n if (latest === undefined || entry.timestamp > latest) {\n latest = entry.timestamp;\n }\n }\n return latest;\n}\n\nfunction noopLogger(): NonNullable<PeerProfileReasonerOptions[\"log\"]> {\n return { debug: () => {}, info: () => {}, warn: () => {} };\n}\n\n/**\n * Run the reasoner across all (or the requested subset of) peers.\n *\n * Always returns a `PeerProfileReasonerResult` — never throws to the\n * caller — so the orchestrator can wire it as a best-effort\n * post-consolidation hook (Gotcha #13).\n */\nexport async function runPeerProfileReasoner(\n options: PeerProfileReasonerOptions,\n): Promise<PeerProfileReasonerResult> {\n const log = {\n debug: options.log?.debug ?? noopLogger().debug!,\n info: options.log?.info ?? noopLogger().info!,\n warn: options.log?.warn ?? noopLogger().warn!,\n };\n const result: {\n peersConsidered: number;\n peersProcessed: number;\n fieldsApplied: number;\n perPeer: PeerProfileReasonerPeerResult[];\n } = {\n peersConsidered: 0,\n peersProcessed: 0,\n fieldsApplied: 0,\n perPeer: [],\n };\n // Disabled flag is the master gate. Defaults to false in callers'\n // config; we additionally require strict `=== true` here so a\n // stray \"true\" string doesn't silently flip the flag (Gotcha #36).\n if (options.enabled !== true) {\n log.debug(\"[peer-profile-reasoner] disabled — no-op\");\n return result;\n }\n if (!options.llm) {\n log.warn(\"[peer-profile-reasoner] no LLM client supplied — skipping run\");\n return result;\n }\n const minInteractions = Number.isFinite(options.minInteractions)\n ? Math.max(0, Math.floor(options.minInteractions))\n : 0;\n const maxFields = Number.isFinite(options.maxFieldsPerRun)\n ? Math.max(0, Math.floor(options.maxFieldsPerRun))\n : 0;\n if (maxFields === 0) {\n log.debug(\"[peer-profile-reasoner] maxFieldsPerRun=0 — no-op\");\n return result;\n }\n const maxLogPerPeer = Number.isFinite(options.maxLogEntriesPerPeer ?? NaN)\n ? Math.max(1, Math.floor(options.maxLogEntriesPerPeer as number))\n : 50;\n const now = options.now ?? new Date();\n const nowIso = now.toISOString();\n\n let peers: Peer[];\n try {\n if (options.peerIds && options.peerIds.length > 0) {\n // Filter the explicit list against on-disk peers so we never\n // act on an id the operator typed but didn't register.\n const all = await listPeers(options.memoryDir);\n const wanted = new Set(\n options.peerIds.filter(\n (id) => typeof id === \"string\" && PEER_ID_PATTERN.test(id),\n ),\n );\n peers = all.filter((p) => wanted.has(p.id));\n } else {\n peers = await listPeers(options.memoryDir);\n }\n } catch (err) {\n log.warn(\n `[peer-profile-reasoner] listPeers failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n return result;\n }\n result.peersConsidered = peers.length;\n let fieldsAppliedTotal = 0;\n\n for (const peer of peers) {\n if (options.signal?.aborted) {\n result.perPeer.push({\n peerId: peer.id,\n status: \"skipped_aborted\",\n fieldsApplied: 0,\n droppedDueToCap: 0,\n fields: [],\n });\n continue;\n }\n try {\n // Codex P2 review on PR #736: the min-interactions threshold\n // must reflect the FULL log of new activity, not the\n // `maxLogPerPeer`-truncated slice. Otherwise a peer with a\n // genuinely active conversation history can be permanently\n // marked `skipped_below_min_interactions` whenever\n // `peerProfileReasonerMinInteractions > maxLogEntriesPerPeer`,\n // because the slice will never include enough new entries.\n // Read the full log first to compute the gate, then truncate\n // for prompt construction below.\n const fullLog = await readPeerInteractionLog(\n options.memoryDir,\n peer.id,\n );\n if (fullLog.length === 0) {\n result.perPeer.push({\n peerId: peer.id,\n status: \"skipped_no_log\",\n fieldsApplied: 0,\n droppedDueToCap: 0,\n fields: [],\n });\n continue;\n }\n // Count interactions since the last reasoner-run marker, so\n // dormant peers don't trigger another LLM call until enough\n // new signal accumulates. Run markers themselves don't count.\n const lastRun = lastRunTimestamp(fullLog);\n const sinceLastRunFull = lastRun\n ? fullLog.filter(\n (e) => e.timestamp > lastRun && e.kind !== RUN_MARKER_KIND,\n )\n : fullLog.filter((e) => e.kind !== RUN_MARKER_KIND);\n if (sinceLastRunFull.length < minInteractions) {\n result.perPeer.push({\n peerId: peer.id,\n status: \"skipped_below_min_interactions\",\n fieldsApplied: 0,\n droppedDueToCap: 0,\n fields: [],\n });\n continue;\n }\n // Truncate ONLY for prompt construction so the LLM context\n // stays bounded. Use the most recent `maxLogPerPeer` entries\n // from the full since-last-run set so the prompt prefers fresh\n // signal over older entries.\n const sinceLastRun =\n sinceLastRunFull.length > maxLogPerPeer\n ? sinceLastRunFull.slice(sinceLastRunFull.length - maxLogPerPeer)\n : sinceLastRunFull;\n\n const existingProfile = await readPeerProfile(options.memoryDir, peer.id);\n\n const remainingBudget = maxFields - fieldsAppliedTotal;\n if (remainingBudget <= 0) {\n result.perPeer.push({\n peerId: peer.id,\n status: \"skipped_cap_reached\",\n fieldsApplied: 0,\n droppedDueToCap: 0,\n fields: [],\n });\n continue;\n }\n\n const prompt = buildPeerProfileReasonerPrompt({\n peer,\n existingProfile,\n log: sinceLastRun,\n maxFields: remainingBudget,\n });\n const messages = [\n { role: \"system\" as const, content: SYSTEM_MESSAGE },\n { role: \"user\" as const, content: prompt },\n ];\n let response: { content: string } | null;\n try {\n response = await options.llm.chatCompletion(messages, {\n temperature: 0.2,\n maxTokens: 1500,\n });\n } catch (err) {\n log.warn(\n `[peer-profile-reasoner] LLM call failed for \"${peer.id}\": ${err instanceof Error ? err.message : String(err)}`,\n );\n result.perPeer.push({\n peerId: peer.id,\n status: \"skipped_llm_unavailable\",\n fieldsApplied: 0,\n droppedDueToCap: 0,\n fields: [],\n });\n continue;\n }\n if (!response || typeof response.content !== \"string\") {\n result.perPeer.push({\n peerId: peer.id,\n status: \"skipped_llm_unavailable\",\n fieldsApplied: 0,\n droppedDueToCap: 0,\n fields: [],\n });\n continue;\n }\n\n const proposals = parsePeerProfileReasonerResponse(response.content);\n if (proposals.length === 0) {\n result.perPeer.push({\n peerId: peer.id,\n status: \"processed\",\n fieldsApplied: 0,\n droppedDueToCap: 0,\n fields: [],\n });\n continue;\n }\n\n // Build the merged profile. We never replace existing\n // provenance entries — provenance is append-only so the\n // operator retains a full audit trail.\n const sessionIdsInWindow = new Set(\n sinceLastRun\n .map((e) => e.sessionId)\n .filter((s): s is string => typeof s === \"string\" && s.length > 0),\n );\n const baseFields: Record<string, string> = existingProfile\n ? { ...existingProfile.fields }\n : {};\n const baseProvenance: Record<string, PeerProfileFieldProvenance[]> = {};\n if (existingProfile) {\n for (const [k, list] of Object.entries(existingProfile.provenance)) {\n baseProvenance[k] = [...list];\n }\n }\n\n // Codex P1 review on PR #736: the global `fieldsAppliedTotal`\n // counter must NOT be incremented until the profile write\n // actually succeeds. Otherwise a transient I/O error here\n // poisons the per-run cap for every subsequent peer — they get\n // marked `skipped_cap_reached` for fields that were never\n // persisted. Track the candidate count locally and only\n // commit it to the run-wide budget after `writePeerProfile`\n // returns successfully (Gotcha #25 — don't destroy old state\n // before confirming new state succeeds).\n const appliedFieldsForPeer: string[] = [];\n let droppedDueToCap = 0;\n let invalidProposalSeen = false;\n for (const proposal of proposals) {\n // Use a candidate-budget projection so we never propose more\n // than the run-wide cap allows even before we know the write\n // will succeed.\n const candidateBudget =\n maxFields - fieldsAppliedTotal - appliedFieldsForPeer.length;\n if (candidateBudget <= 0) {\n droppedDueToCap += 1;\n continue;\n }\n // Final defensive guard against prototype keys (parser\n // already drops them, but be redundant for safety).\n if (\n proposal.field === \"__proto__\" ||\n proposal.field === \"constructor\" ||\n proposal.field === \"prototype\"\n ) {\n invalidProposalSeen = true;\n continue;\n }\n // Sanity-check field key matches a conservative pattern so a\n // hostile LLM can't sneak path-traversal-shaped keys through\n // for downstream consumers.\n if (!/^[a-zA-Z][a-zA-Z0-9_]{0,63}$/.test(proposal.field)) {\n invalidProposalSeen = true;\n continue;\n }\n baseFields[proposal.field] = proposal.value;\n const sourceSessionId =\n proposal.sourceSessionId &&\n sessionIdsInWindow.has(proposal.sourceSessionId)\n ? proposal.sourceSessionId\n : undefined;\n const provEntry: PeerProfileFieldProvenance = {\n observedAt: nowIso,\n signal: proposal.signal,\n ...(sourceSessionId ? { sourceSessionId } : {}),\n ...(proposal.note && proposal.note.length > 0\n ? { note: proposal.note }\n : {}),\n };\n const list = baseProvenance[proposal.field] ?? [];\n list.push(provEntry);\n baseProvenance[proposal.field] = list;\n appliedFieldsForPeer.push(proposal.field);\n // NOTE: fieldsAppliedTotal is NOT incremented here — see the\n // P1 comment above. We commit the budget after the write\n // succeeds.\n }\n\n if (appliedFieldsForPeer.length === 0) {\n result.perPeer.push({\n peerId: peer.id,\n status: invalidProposalSeen\n ? \"skipped_invalid_proposal\"\n : droppedDueToCap > 0\n ? \"skipped_cap_reached\"\n : \"processed\",\n fieldsApplied: 0,\n droppedDueToCap,\n fields: [],\n });\n continue;\n }\n\n const merged: PeerProfile = {\n peerId: peer.id,\n updatedAt: nowIso,\n fields: baseFields,\n provenance: baseProvenance,\n };\n await writePeerProfile(options.memoryDir, merged);\n // Write succeeded — NOW commit the budget. A throw above\n // bubbles to the outer catch, where the peer is recorded as\n // `error` and the global cap remains intact for subsequent\n // peers (Codex P1 fix on PR #736).\n fieldsAppliedTotal += appliedFieldsForPeer.length;\n\n // Append a run marker so the next reasoner pass can compute\n // \"interactions since last run\" without a dedicated state\n // file. The marker is best-effort — a write failure here\n // logs but does not roll back the profile (the operator\n // would prefer a slightly noisy threshold over a lost\n // profile update).\n const wantsMarker = options.appendRunMarkerToLog ?? true;\n if (wantsMarker) {\n try {\n await appendInteractionLog(options.memoryDir, peer.id, {\n timestamp: nowIso,\n kind: RUN_MARKER_KIND,\n summary: `applied ${appliedFieldsForPeer.length} field(s) via ${options.model ?? \"unknown-model\"}`,\n });\n } catch (err) {\n log.warn(\n `[peer-profile-reasoner] run-marker append failed for \"${peer.id}\": ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n\n result.perPeer.push({\n peerId: peer.id,\n status: \"processed\",\n fieldsApplied: appliedFieldsForPeer.length,\n droppedDueToCap,\n fields: appliedFieldsForPeer,\n });\n result.peersProcessed += 1;\n result.fieldsApplied = fieldsAppliedTotal;\n } catch (err) {\n log.warn(\n `[peer-profile-reasoner] error processing peer \"${peer.id}\": ${err instanceof Error ? err.message : String(err)}`,\n );\n result.perPeer.push({\n peerId: peer.id,\n status: \"error\",\n fieldsApplied: 0,\n droppedDueToCap: 0,\n fields: [],\n error: err instanceof Error ? err.message : String(err),\n });\n }\n }\n\n return result;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
var DEFAULT_CITATION_FORMAT = "[Source: agent={agent}, session={sessionId}, ts={ts}]";
|
|
3
3
|
var CITATION_UNKNOWN = "unknown";
|
|
4
4
|
function defaultCitationMatcher() {
|
|
5
|
-
return /\[Source
|
|
5
|
+
return /\[Source:([^\]\n]{1,1024})\]/gi;
|
|
6
|
+
}
|
|
7
|
+
function trimTrailingWhitespace(text) {
|
|
8
|
+
let end = text.length;
|
|
9
|
+
while (end > 0 && /\s/u.test(text[end - 1])) end--;
|
|
10
|
+
return text.slice(0, end);
|
|
6
11
|
}
|
|
7
12
|
function deriveSessionId(session) {
|
|
8
13
|
if (!session) return void 0;
|
|
@@ -111,7 +116,7 @@ function hasCitationForTemplate(text, template) {
|
|
|
111
116
|
function attachCitation(text, ctx, template = DEFAULT_CITATION_FORMAT) {
|
|
112
117
|
if (typeof text !== "string") return text;
|
|
113
118
|
if (hasCitationForTemplate(text, template)) return text;
|
|
114
|
-
const trimmedEnd = text
|
|
119
|
+
const trimmedEnd = trimTrailingWhitespace(text);
|
|
115
120
|
if (trimmedEnd.length === 0) return text;
|
|
116
121
|
const citation = formatCitation(ctx, template);
|
|
117
122
|
const trailing = text.slice(trimmedEnd.length);
|
|
@@ -235,4 +240,4 @@ export {
|
|
|
235
240
|
stripCitation,
|
|
236
241
|
stripCitationForTemplate
|
|
237
242
|
};
|
|
238
|
-
//# sourceMappingURL=chunk-
|
|
243
|
+
//# sourceMappingURL=chunk-J6A3CX5N.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/source-attribution.ts"],"sourcesContent":["/**\n * Inline Source Attribution Protocol (issue #369)\n *\n * Extracted facts carry provenance inline inside the fact body, so the\n * citation survives hostile memory text, copy/paste, and LLM quoting. This\n * complements — never replaces — the YAML frontmatter provenance stored on\n * disk.\n *\n * Default format (matches issue #369 proposal):\n *\n * The foo service uses Redis for rate limiting. [Source: agent=planner, session=abc123, ts=2026-04-10T14:25:07Z]\n *\n * Key properties:\n * - Inline (part of the body, not metadata).\n * - Compact (typically <80 chars of overhead per fact).\n * - Machine-parseable by a single regex.\n * - Opt-in via `inlineSourceAttributionEnabled` config flag (default off\n * for backwards compatibility with existing downstream consumers).\n * - Legacy facts without a citation remain fully readable.\n *\n * The format template is configurable via `inlineSourceAttributionFormat`\n * with supported placeholders:\n *\n * {agent} — principal / agent identifier\n * {session} — full session key (colon-delimited)\n * {sessionId} — short stable session id (trailing component)\n * {ts} — extraction timestamp (ISO 8601)\n * {date} — extraction date (YYYY-MM-DD)\n *\n * Any privacy-sensitive identifiers should be normalized before being passed\n * to `formatCitation` — the helper treats them as opaque strings.\n */\n\n/** Default citation format template (matches issue #369). */\nexport const DEFAULT_CITATION_FORMAT =\n \"[Source: agent={agent}, session={sessionId}, ts={ts}]\";\n\n/** Sentinel value used when a provenance field is missing. */\nexport const CITATION_UNKNOWN = \"unknown\";\n\nexport interface CitationContext {\n /** Principal / agent identifier (e.g. resolved via resolvePrincipal). */\n agent?: string;\n /** Full session key (e.g. \"agent:planner:main\"). */\n session?: string;\n /**\n * Opaque short session id. Derived from the trailing component of the\n * session key when not provided explicitly. Use this for compact formats\n * that do not need the full colon-delimited session key.\n */\n sessionId?: string;\n /** Extraction timestamp as an ISO 8601 string. */\n ts?: string;\n}\n\nexport interface ParsedCitation {\n /** Agent identifier parsed from the citation (never crashes on malformed input). */\n agent?: string;\n /** Session identifier parsed from the citation. */\n session?: string;\n /** Extraction timestamp parsed from the citation. */\n ts?: string;\n /** The full matched citation substring. */\n raw: string;\n}\n\n/**\n * Regex that matches the default `[Source: agent=X, session=Y, ts=Z]` shape\n * as well as human-edited variants (extra whitespace, reordered fields,\n * subset of fields). Matches non-greedily so it can be anchored anywhere in\n * the text. Kept as a getter factory so callers do not share regex state.\n */\nfunction defaultCitationMatcher(): RegExp {\n return /\\[Source:\\s*([^\\]\\n]+?)\\]/gi;\n}\n\n/**\n * Derive a short session id from a full session key.\n * Falls back to the raw session string if no colon is present.\n */\nexport function deriveSessionId(session: string | undefined): string | undefined {\n if (!session) return undefined;\n const trimmed = session.trim();\n if (trimmed.length === 0) return undefined;\n const parts = trimmed.split(\":\").filter((p) => p.length > 0);\n if (parts.length === 0) return trimmed;\n return parts[parts.length - 1];\n}\n\n/**\n * Format an inline citation tag using the provided template.\n *\n * Missing context fields fall back to {@link CITATION_UNKNOWN} — the caller\n * should always get a non-empty, parseable tag.\n *\n * Uses a single-pass substitution so that values which themselves contain\n * placeholder syntax (e.g. an agent literally named `\"{ts}\"`) cannot be\n * re-interpreted by subsequent replacement steps. Each placeholder slot\n * receives exactly one lookup and the substituted value is treated as\n * terminal text, not template source.\n */\nexport function formatCitation(\n ctx: CitationContext,\n template: string = DEFAULT_CITATION_FORMAT,\n): string {\n const session = ctx.session ?? \"\";\n const sessionId = ctx.sessionId ?? deriveSessionId(session) ?? CITATION_UNKNOWN;\n const ts = ctx.ts ?? CITATION_UNKNOWN;\n const agent = ctx.agent && ctx.agent.trim().length > 0 ? ctx.agent : CITATION_UNKNOWN;\n const date = ts && ts !== CITATION_UNKNOWN ? ts.slice(0, 10) : CITATION_UNKNOWN;\n const sessionForTemplate = session.trim().length > 0 ? session : CITATION_UNKNOWN;\n\n // Map from recognised placeholder names to their resolved value. Unknown\n // placeholder names are left intact (returning the original `{name}`).\n const values: Record<string, string> = {\n agent,\n session: sessionForTemplate,\n sessionId,\n ts,\n date,\n };\n\n // Single-pass scan: replace every recognised `{name}` in one sweep so that\n // substituted values cannot themselves be treated as template source on a\n // subsequent pass. The replacer-function form also guarantees that `$` /\n // `$&` / `$1` sequences inside values are emitted literally.\n return template.replace(/\\{([a-zA-Z_][\\w]*)\\}/g, (match, name: string) => {\n return Object.prototype.hasOwnProperty.call(values, name)\n ? values[name]!\n : match;\n });\n}\n\n/**\n * Returns true if the text already carries at least one citation marker.\n * Safe to call on any string — never throws.\n */\nexport function hasCitation(text: string): boolean {\n if (typeof text !== \"string\" || text.length === 0) return false;\n return defaultCitationMatcher().test(text);\n}\n\n/**\n * Escape a string for use as a regex literal.\n */\nfunction escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Escape a single character for use INSIDE a character class `[...]`.\n * Special chars inside character classes: `]`, `\\`, `^`, `-`.\n */\nfunction escapeRegExpCharClass(ch: string): string {\n if (ch === \"]\") return \"\\\\]\";\n if (ch === \"\\\\\") return \"\\\\\\\\\";\n if (ch === \"^\") return \"\\\\^\";\n if (ch === \"-\") return \"\\\\-\";\n // Other regex meta chars are NOT special inside [...] but escaping them is safe.\n return escapeRegExp(ch);\n}\n\n/**\n * Build a per-placeholder token pattern that excludes newlines, whitespace,\n * and any punctuation characters used as inner separators in the template.\n *\n * This prevents a placeholder span from consuming separator bytes and\n * matching strings that cross separator boundaries in user-supplied content.\n *\n * @param nonWordSepChars - Set of non-word (non-alphanumeric, non-`_`) chars\n * extracted from the inner separator literals of the template.\n */\nfunction buildTokenPattern(nonWordSepChars: Set<string>): string {\n // Always exclude newlines and whitespace.\n const base = \"\\\\n\\\\s\";\n if (nonWordSepChars.size === 0) {\n // No inner separator punctuation — the placeholder spans the full space\n // between prefix and suffix. Fall back to a generous no-newline match.\n return `[^\\\\n]+?`;\n }\n const escaped = [...nonWordSepChars].map(escapeRegExpCharClass).join(\"\");\n return `[^${base}${escaped}]+?`;\n}\n\n/** Regex that matches a `{placeholder}` token inside a template string. */\nconst PLACEHOLDER_REGEX = /\\{[a-zA-Z_][\\w]*\\}/g;\n\n/**\n * Build a regex that matches a citation produced by the given template.\n *\n * The approach depends on the shape of the template:\n *\n * - **Normal case (non-empty literal prefix or suffix).** Anchor the match\n * on the outer literal frame and reconstruct the interior as\n * `interToken + sep + interToken + sep + ... + sep + lastToken`.\n * All **intermediate** per-placeholder tokens exclude the combined set of\n * non-word separator characters used between any two adjacent placeholders,\n * preventing a value from consuming a separator and crossing placeholder\n * boundaries. The **last** token is only required to avoid newlines because\n * it is terminated by the literal suffix anchor — this lets placeholder\n * values that legitimately contain a separator character be recognised (e.g.\n * an ISO-8601 timestamp `2026-04-10T14:25:07Z` in `[src:{agent}:{ts}]`\n * where `:` is the inter-placeholder separator). A template like\n * `[src:{agent}/{sessionId}@{date}]` emits\n * `\\[src:[^\\n\\s/@]+?\\/[^\\n\\s/@]+?@[^\\n]+?\\]` so that `[src:foo/bar]`\n * is NOT matched (wrong separator count), `[src:foo/bar/extra@2026]`\n * is NOT matched (intermediate token crosses a `/` boundary), and\n * `[src:planner/main@2026-04-10]` IS matched correctly.\n *\n * - **Placeholder-bounded with whitespace separator.** Both prefix and\n * suffix are empty and the separator literal(s) between placeholders\n * contain at least one whitespace character (e.g. `{source}: {content}`,\n * `{agent} {sessionId}`). A whitespace-containing separator produces\n * output that is visually indistinguishable from ordinary prose, so the\n * safe strategy is to require a **hard bracket/paren/angle delimiter** on\n * both sides of the reconstructed match. Prose almost never places\n * `[...]` / `(...)` / `<...>` around a phrase, so this yields clean\n * false-positive rejection.\n *\n * - **Placeholder-bounded with compact (non-whitespace) separator.** Both\n * prefix and suffix are empty and the separator literal(s) contain NO\n * whitespace (e.g. `{agent}:{sessionId}`, `{agent}/{sessionId}`).\n * `formatCitation` emits a compact token like `planner:main` with no\n * surrounding delimiters, so the bracket strategy cannot detect it.\n * Instead, the pattern requires that the entire token is bordered by\n * whitespace or a bracket/paren/angle on each side:\n *\n * `(?:(?<=[\\[\\(\\<])|(?<!\\S))[\\w.-]+<sep>[\\w.-]+(?:(?=[\\]\\)\\>])|(?!\\S))`\n *\n * This accepts `planner:main` when it appears standalone or inside a\n * bracket-wrapped token, and rejects `host:80` embedded inside a URL like\n * `http://host:80` because `host` is immediately preceded by `/`\n * (non-whitespace, non-bracket).\n *\n * - **All-placeholder case (no literals between placeholders either).** No\n * reliable regex can be built — a template like `{agent}{sessionId}`\n * contains no anchor characters. Returns `null`; {@link\n * hasCitationForTemplate} treats this as \"cannot detect\" and returns\n * false, falling back on explicit sentinel/format detection only for the\n * default `[Source: ...]` shape.\n *\n * Returns `null` when the template has no placeholders (fully-literal\n * citation, handled by the string-equality fast path in {@link\n * hasCitationForTemplate}) **or** when the template is entirely placeholder-\n * only with no literal content whatsoever.\n */\nfunction templateMatcher(template: string): RegExp | null {\n // Split around all {placeholder} tokens.\n const parts = template.split(PLACEHOLDER_REGEX);\n if (parts.length <= 1) return null;\n\n const prefix = parts[0] ?? \"\";\n const suffix = parts[parts.length - 1] ?? \"\";\n\n // Normal case: at least one literal frame on the outside.\n // Tighten the per-placeholder token so it cannot consume separator\n // characters and match strings that cross separator boundaries\n // (Finding 3 — Uru3).\n if (prefix.length > 0 || suffix.length > 0) {\n const escapedPrefix = escapeRegExp(prefix);\n const escapedSuffix = escapeRegExp(suffix);\n\n // Inner parts: literal separators that sit between adjacent placeholders.\n // For `[src:{agent}/{sessionId}@{date}]`, parts = [\"[src:\", \"/\", \"@\", \"]\"]\n // so innerParts = [\"/\", \"@\"].\n const innerParts = parts.slice(1, -1);\n\n // Collect only the non-word (punctuation/symbol) characters from each\n // inner separator so alphabetic separator text (unlikely but valid) does\n // not exclude letters from the per-placeholder token pattern.\n const nonWordSepChars = new Set<string>();\n for (const sep of innerParts) {\n for (const ch of sep) {\n if (!/\\w/.test(ch)) {\n nonWordSepChars.add(ch);\n }\n }\n }\n\n // All intermediate tokens (every placeholder except the last) use the\n // combined exclusion so they cannot cross placeholder boundaries.\n //\n // The LAST token is different: it is terminated by the literal suffix anchor\n // (e.g. `\\]`), so it does not need to exclude inner-separator characters.\n // Dropping that restriction lets placeholder values that legitimately contain\n // a separator character (e.g. an ISO-8601 timestamp `2026-04-10T14:25:07Z`\n // in template `[src:{agent}:{ts}]`) be recognised correctly instead of\n // producing false-negative misses that trigger duplicate citation injection.\n //\n // Only the LAST token is relaxed. Intermediate tokens keep the combined\n // exclusion so that cross-boundary false positives are still rejected\n // (e.g. `[src:foo/bar/extra@2026-04-11]` for `[src:{a}/{b}@{c}]`).\n const interToken = buildTokenPattern(nonWordSepChars);\n // Last token: terminated by suffix anchor — exclude only newlines.\n const lastToken = buildTokenPattern(new Set<string>());\n\n // Reconstruct the interior: interToken sep interToken sep ... sep lastToken\n // (or just lastToken when there are no inner separators at all).\n const middle =\n innerParts.length === 0\n ? lastToken\n : interToken +\n innerParts\n .slice(0, -1)\n .map((sep) => escapeRegExp(sep) + interToken)\n .join(\"\") +\n escapeRegExp(innerParts[innerParts.length - 1]!) +\n lastToken;\n\n const pattern = escapedPrefix + middle + escapedSuffix;\n return new RegExp(pattern, \"i\");\n }\n\n // Placeholder-bounded case: prefix and suffix are both empty.\n const middleLiterals = parts.slice(1, -1);\n const hasNonEmptyMiddle = middleLiterals.some((p) => p.length > 0);\n if (!hasNonEmptyMiddle) {\n // All-placeholder template with no literal content. Impossible to anchor\n // reliably without sentinel markers; signal the caller.\n return null;\n }\n\n // Identifier token: one or more word chars, dots, dashes, or colons.\n // Colons are included to allow timestamp values like \"10:30\" or session\n // keys like \"agent:planner:main\" inside compact placeholder-bounded\n // templates. URL-like fragments (`http://host:80`) are still rejected\n // because the lead anchor requires whitespace or a bracket immediately\n // before the first id-token group (`http` is preceded by `/`).\n const idToken = \"[\\\\w.:-]+\";\n const body =\n idToken +\n middleLiterals.map((lit) => escapeRegExp(lit) + idToken).join(\"\");\n\n const separatorText = middleLiterals.join(\"\");\n if (/\\s/.test(separatorText)) {\n // Separator contains whitespace: the emitted citation looks like ordinary\n // prose (e.g. `planner main`). Require a hard bracket/paren/angle\n // delimiter on both sides to prevent false matches on English text.\n const opener = \"[\\\\[\\\\(\\\\<]\";\n const closer = \"[\\\\]\\\\)\\\\>]\";\n return new RegExp(opener + body + closer, \"i\");\n }\n\n // Separator is compact (no whitespace): `formatCitation` emits a token like\n // `planner:main` without surrounding delimiters. The challenge is that the\n // same token shape also matches ordinary hyphenated or slashed prose words\n // (e.g. `long-term`, `docs/setup`), causing `hasCitationForTemplate` to\n // return true on uncited fact bodies and silently suppress citation injection\n // from `attachCitation`.\n //\n // Fix (Finding 1): tighten the trail anchor so a bare compact token is only\n // accepted when it sits at the very end of the string (possibly followed by\n // optional trailing whitespace or a newline). Since `attachCitation` always\n // appends the citation at the trimmed end of the fact body, a real citation\n // token will always appear at the tail. Prose like `\"long-term solution\"`\n // has `long-term` in the middle of the string (followed by ` solution`), so\n // the end-of-string anchor rejects it — no false positive, no silent drop.\n //\n // The lead anchor still accepts either a bracket opener or a whitespace\n // boundary (or start of string), so `\"Fact. planner:main\"` and standalone\n // `\"planner:main\"` are both detected after the first attachment pass.\n //\n // Bracket-wrapped form (e.g. `[planner:main]`) is also accepted via the\n // opener/closer pair — bracket still takes precedence over end-of-string.\n //\n // Example — why `http://host:80` does NOT match:\n // Trying to match `host:80`: the char before `h` is `/` (non-whitespace,\n // non-bracket), so `(?<=[\\[\\(\\<])` and `(?<!\\S)` both fail ⟹ no match.\n // Trying to match `http:...`: after `http:` the next chars are `//` which\n // are not `[\\w.-]+`, so the second id-token group fails ⟹ no match.\n const leadAnchor = \"(?:(?<=[\\\\[\\\\(\\\\<])|(?<!\\\\S))\";\n // Trail: either a bracket closer (for `[token]` shape) or end-of-string\n // optionally preceded by whitespace. The `(?!\\S)` is deliberately removed\n // so that a compact token in the MIDDLE of a sentence does not match.\n const trailAnchor = \"(?:(?=[\\\\]\\\\)\\\\>])|(?=\\\\s*$))\";\n return new RegExp(leadAnchor + body + trailAnchor, \"i\");\n}\n\n/**\n * Returns true if `text` already carries a citation produced by `template`\n * **or** by the default `[Source: ...]` format (for facts that were tagged\n * before a config change).\n *\n * Use this instead of {@link hasCitation} whenever the caller has access to\n * the configured `inlineSourceAttributionFormat`.\n *\n * All-placeholder templates such as `{agent}{sessionId}` have no literal\n * content to anchor on and therefore cannot be reliably detected without\n * dedicated sentinel markers. In that case the function returns `false` —\n * callers that need idempotent dedup for such templates should either adopt\n * a template with literal delimiters (recommended) or rely on the default\n * `[Source: ...]` marker detection which is always available via\n * {@link hasCitation}.\n */\nexport function hasCitationForTemplate(text: string, template: string): boolean {\n if (typeof text !== \"string\" || text.length === 0) return false;\n // Always accept the default format as a fallback so facts tagged before a\n // configuration change are not double-tagged on reprocessing.\n if (hasCitation(text)) return true;\n // If the configured template matches the default, we're done.\n //\n // Known limitation (Thread 2 — Codex P2): this fast path exits without\n // checking whether the content carries a citation from a DIFFERENT custom\n // template that was active before the config was changed back to the default.\n // Such a fact would be detected by `hasCitation` above only if the prior\n // custom template happened to match the default `[Source: ...]` pattern.\n // In practice, template changes mid-stream are rare, and the false-negative\n // (missing an old custom citation) produces a benign duplicate citation rather\n // than data loss. A full fix would require storing the citation template used\n // at write time in the frontmatter and checking that here.\n if (template === DEFAULT_CITATION_FORMAT) return false;\n\n // Fully-literal template (no placeholders): exact inclusion check.\n if (!PLACEHOLDER_REGEX.test(template)) {\n // Reset lastIndex because PLACEHOLDER_REGEX is declared with /g.\n PLACEHOLDER_REGEX.lastIndex = 0;\n return text.includes(template);\n }\n // Reset lastIndex after the .test() probe above.\n PLACEHOLDER_REGEX.lastIndex = 0;\n\n const matcher = templateMatcher(template);\n if (!matcher) {\n // All-placeholder template: cannot build a reliable matcher. See the\n // docstring — callers should not rely on dedup for this shape.\n return false;\n }\n return matcher.test(text);\n}\n\n/**\n * Attach an inline citation to fact text.\n *\n * If the text already has a citation — either the default `[Source: ...]`\n * marker or one produced by the configured template — it is returned unchanged.\n * Existing provenance is respected and never overwritten. Otherwise the\n * citation is appended to the trimmed text with a single space separator,\n * which keeps the marker visually adjacent to the fact body.\n */\nexport function attachCitation(\n text: string,\n ctx: CitationContext,\n template: string = DEFAULT_CITATION_FORMAT,\n): string {\n if (typeof text !== \"string\") return text as unknown as string;\n if (hasCitationForTemplate(text, template)) return text;\n const trimmedEnd = text.replace(/\\s+$/u, \"\");\n if (trimmedEnd.length === 0) return text;\n const citation = formatCitation(ctx, template);\n // Preserve any trailing newline that callers rely on for markdown rendering.\n const trailing = text.slice(trimmedEnd.length);\n return `${trimmedEnd} ${citation}${trailing}`;\n}\n\n/**\n * Parse a single inline citation from a piece of text. Returns the first\n * citation encountered or `null` when none is present. Malformed citations\n * do not throw — fields that cannot be parsed simply remain `undefined`.\n */\nexport function parseCitation(text: string): ParsedCitation | null {\n if (typeof text !== \"string\" || text.length === 0) return null;\n const matcher = defaultCitationMatcher();\n const match = matcher.exec(text);\n if (!match) return null;\n\n const body = match[1] ?? \"\";\n const raw = match[0] ?? \"\";\n const parsed: ParsedCitation = { raw };\n\n const fields = body\n .split(\",\")\n .map((segment) => segment.trim())\n .filter((segment) => segment.length > 0);\n\n for (const field of fields) {\n const eqIdx = field.indexOf(\"=\");\n if (eqIdx <= 0) continue;\n const key = field.slice(0, eqIdx).trim().toLowerCase();\n const value = field.slice(eqIdx + 1).trim();\n if (value.length === 0) continue;\n switch (key) {\n case \"agent\":\n parsed.agent = value;\n break;\n case \"session\":\n case \"sessionid\":\n parsed.session = value;\n break;\n case \"ts\":\n case \"timestamp\":\n parsed.ts = value;\n break;\n default:\n // Unknown fields are ignored defensively so human edits never crash.\n break;\n }\n }\n\n return parsed;\n}\n\n/**\n * Parse every citation embedded in the text. Always returns an array; empty\n * when none are present.\n */\nexport function parseAllCitations(text: string): ParsedCitation[] {\n if (typeof text !== \"string\" || text.length === 0) return [];\n const matcher = defaultCitationMatcher();\n const results: ParsedCitation[] = [];\n let match: RegExpExecArray | null;\n while ((match = matcher.exec(text)) !== null) {\n const parsed = parseCitation(match[0]);\n if (parsed) results.push(parsed);\n }\n return results;\n}\n\n/**\n * Remove all inline citations from a piece of text.\n *\n * Callers that want the raw fact body (for dedup hashing, display, or\n * comparison) should use this helper instead of hand-rolled regexes so the\n * whole codebase agrees on the citation syntax.\n *\n * Finding 2 fix: when the input contains no citation marker, the input is\n * returned byte-for-byte unchanged. When a citation IS removed, whitespace\n * normalization is applied only at each join seam (the single space between\n * the preceding text and where the citation was), rather than across the\n * entire string. This preserves markdown hard-break spacing, aligned text,\n * and code-like snippets in fact bodies that happen to carry a citation.\n *\n * Implementation: each citation match is replaced by its \"seam fix\" — the\n * content before the match has its trailing whitespace trimmed and then a\n * single space is appended if any text remains, collapsing only the gap\n * left by the removed marker. Whitespace elsewhere in the body is untouched.\n */\nexport function stripCitation(text: string): string {\n if (typeof text !== \"string\" || text.length === 0) return text;\n // Early exit: no citation marker present — return the input unchanged so\n // that callers never lose formatting fidelity on uncited strings.\n if (!hasCitation(text)) return text;\n\n // Walk through all citations and slice them out one by one so that we can\n // normalise ONLY the whitespace at each seam rather than the entire string.\n const matcher = defaultCitationMatcher();\n let result = \"\";\n let lastIndex = 0;\n\n let match: RegExpExecArray | null;\n while ((match = matcher.exec(text)) !== null) {\n // Text before this citation. Trim trailing spaces/tabs at the seam only.\n const before = text.slice(lastIndex, match.index).replace(/[ \\t]+$/, \"\");\n result += before;\n lastIndex = match.index + match[0].length;\n }\n\n // Append any trailing text after the last citation. Trim leading\n // spaces/tabs and trailing whitespace at the join seam.\n const after = text.slice(lastIndex).replace(/^[ \\t]+/, \"\");\n if (after.length > 0) {\n if (result.length > 0) result += \" \";\n result += after;\n }\n\n return result.trimEnd();\n}\n\n/**\n * Strip an inline citation from text using a specific template regex.\n *\n * This is the template-aware counterpart to {@link stripCitation}. When the\n * caller holds the configured `inlineSourceAttributionFormat`, use this\n * function to strip citations produced by that template — including custom\n * templates that differ from the default `[Source: ...]` pattern.\n *\n * Behaviour:\n * - If the text has a **default-format** citation, delegates to\n * {@link stripCitation} (always safe).\n * - If the text has a **custom-template** citation detected by\n * `hasCitationForTemplate`, builds the template regex and removes every\n * occurrence (citations are appended at the end of the fact body by\n * {@link attachCitation}).\n * - All-placeholder templates (no literal prefix/suffix/separator) cannot\n * produce a reliable matcher. `hasCitationForTemplate` already returns\n * `false` for such templates, so this function never attempts to strip an\n * undetectable citation. The text is returned unchanged when no citation\n * is detected.\n * - If no citation is detected for the given template, returns the text\n * unchanged.\n *\n * @returns The stripped text (or the original text when no citation is found).\n */\nexport function stripCitationForTemplate(\n text: string,\n template: string,\n): string {\n if (typeof text !== \"string\" || text.length === 0) return text;\n\n // Fast path: default-format citation — delegate to the existing stripper.\n if (hasCitation(text)) return stripCitation(text);\n\n // No default citation; check whether the custom template produced one.\n // hasCitationForTemplate returns false for all-placeholder templates (no\n // reliable matcher), so those pass through unchanged below.\n if (!hasCitationForTemplate(text, template)) return text;\n\n // Build the template matcher. hasCitationForTemplate already returned true,\n // which means templateMatcher produced a non-null result. The null branch\n // here is a defensive fallback only — delegate to stripCitation.\n const matcher = templateMatcher(template);\n if (!matcher) return stripCitation(text);\n\n // The matcher regex was built without the global flag; add it for exec loop.\n const globalMatcher = new RegExp(\n matcher.source,\n matcher.flags.includes(\"g\") ? matcher.flags : matcher.flags + \"g\",\n );\n let result = \"\";\n let lastIndex = 0;\n let match: RegExpExecArray | null;\n\n while ((match = globalMatcher.exec(text)) !== null) {\n const matchEnd = match.index + match[0].length;\n const enclosure = enclosingDelimiterRange(text, match.index, matchEnd);\n const removalStart = enclosure?.start ?? match.index;\n const removalEnd = enclosure?.end ?? matchEnd;\n const before = text.slice(lastIndex, removalStart).replace(/[ \\t]+$/, \"\");\n result += before;\n lastIndex = removalEnd;\n // Guard against zero-width matches causing an infinite loop.\n if (match[0].length === 0) {\n globalMatcher.lastIndex++;\n }\n }\n\n const after = text.slice(lastIndex).replace(/^[ \\t]+/, \"\");\n if (after.length > 0) {\n if (result.length > 0) result += \" \";\n result += after;\n }\n\n return result.trimEnd();\n}\n\nfunction enclosingDelimiterRange(\n text: string,\n start: number,\n end: number,\n): { start: number; end: number } | undefined {\n if (start <= 0 || end >= text.length) return undefined;\n const opener = text[start - 1];\n const closer = text[end];\n if (\n (opener === \"[\" && closer === \"]\") ||\n (opener === \"(\" && closer === \")\") ||\n (opener === \"<\" && closer === \">\")\n ) {\n return { start: start - 1, end: end + 1 };\n }\n return undefined;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/source-attribution.ts"],"sourcesContent":["/**\n * Inline Source Attribution Protocol (issue #369)\n *\n * Extracted facts carry provenance inline inside the fact body, so the\n * citation survives hostile memory text, copy/paste, and LLM quoting. This\n * complements — never replaces — the YAML frontmatter provenance stored on\n * disk.\n *\n * Default format (matches issue #369 proposal):\n *\n * The foo service uses Redis for rate limiting. [Source: agent=planner, session=abc123, ts=2026-04-10T14:25:07Z]\n *\n * Key properties:\n * - Inline (part of the body, not metadata).\n * - Compact (typically <80 chars of overhead per fact).\n * - Machine-parseable by a single regex.\n * - Opt-in via `inlineSourceAttributionEnabled` config flag (default off\n * for backwards compatibility with existing downstream consumers).\n * - Legacy facts without a citation remain fully readable.\n *\n * The format template is configurable via `inlineSourceAttributionFormat`\n * with supported placeholders:\n *\n * {agent} — principal / agent identifier\n * {session} — full session key (colon-delimited)\n * {sessionId} — short stable session id (trailing component)\n * {ts} — extraction timestamp (ISO 8601)\n * {date} — extraction date (YYYY-MM-DD)\n *\n * Any privacy-sensitive identifiers should be normalized before being passed\n * to `formatCitation` — the helper treats them as opaque strings.\n */\n\n/** Default citation format template (matches issue #369). */\nexport const DEFAULT_CITATION_FORMAT =\n \"[Source: agent={agent}, session={sessionId}, ts={ts}]\";\n\n/** Sentinel value used when a provenance field is missing. */\nexport const CITATION_UNKNOWN = \"unknown\";\n\nexport interface CitationContext {\n /** Principal / agent identifier (e.g. resolved via resolvePrincipal). */\n agent?: string;\n /** Full session key (e.g. \"agent:planner:main\"). */\n session?: string;\n /**\n * Opaque short session id. Derived from the trailing component of the\n * session key when not provided explicitly. Use this for compact formats\n * that do not need the full colon-delimited session key.\n */\n sessionId?: string;\n /** Extraction timestamp as an ISO 8601 string. */\n ts?: string;\n}\n\nexport interface ParsedCitation {\n /** Agent identifier parsed from the citation (never crashes on malformed input). */\n agent?: string;\n /** Session identifier parsed from the citation. */\n session?: string;\n /** Extraction timestamp parsed from the citation. */\n ts?: string;\n /** The full matched citation substring. */\n raw: string;\n}\n\n/**\n * Regex that matches the default `[Source: agent=X, session=Y, ts=Z]` shape\n * as well as human-edited variants (extra whitespace, reordered fields,\n * subset of fields). Matches non-greedily so it can be anchored anywhere in\n * the text. Kept as a getter factory so callers do not share regex state.\n */\nfunction defaultCitationMatcher(): RegExp {\n // Bounded repetition {1,1024} instead of + so the match cannot backtrack\n // polynomially over hostile memory text (CodeQL js/polynomial-redos). A real\n // citation is far shorter than 1024 chars, so this is behavior-preserving for\n // any genuine [Source: …] block; only pathological/oversized input is excluded.\n return /\\[Source:([^\\]\\n]{1,1024})\\]/gi;\n}\n\n// Linear trailing-whitespace trim. Replaces text.replace(/\\s+$/u, \"\"), whose\n// anchored quantifier backtracks polynomially on long inputs (CodeQL\n// js/polynomial-redos). Matches the exact \\s (with u flag) semantics one char\n// at a time, so the trailing-content preservation logic in attachCitation is\n// unaffected.\nfunction trimTrailingWhitespace(text: string): string {\n let end = text.length;\n while (end > 0 && /\\s/u.test(text[end - 1]!)) end--;\n return text.slice(0, end);\n}\n\n/**\n * Derive a short session id from a full session key.\n * Falls back to the raw session string if no colon is present.\n */\nexport function deriveSessionId(session: string | undefined): string | undefined {\n if (!session) return undefined;\n const trimmed = session.trim();\n if (trimmed.length === 0) return undefined;\n const parts = trimmed.split(\":\").filter((p) => p.length > 0);\n if (parts.length === 0) return trimmed;\n return parts[parts.length - 1];\n}\n\n/**\n * Format an inline citation tag using the provided template.\n *\n * Missing context fields fall back to {@link CITATION_UNKNOWN} — the caller\n * should always get a non-empty, parseable tag.\n *\n * Uses a single-pass substitution so that values which themselves contain\n * placeholder syntax (e.g. an agent literally named `\"{ts}\"`) cannot be\n * re-interpreted by subsequent replacement steps. Each placeholder slot\n * receives exactly one lookup and the substituted value is treated as\n * terminal text, not template source.\n */\nexport function formatCitation(\n ctx: CitationContext,\n template: string = DEFAULT_CITATION_FORMAT,\n): string {\n const session = ctx.session ?? \"\";\n const sessionId = ctx.sessionId ?? deriveSessionId(session) ?? CITATION_UNKNOWN;\n const ts = ctx.ts ?? CITATION_UNKNOWN;\n const agent = ctx.agent && ctx.agent.trim().length > 0 ? ctx.agent : CITATION_UNKNOWN;\n const date = ts && ts !== CITATION_UNKNOWN ? ts.slice(0, 10) : CITATION_UNKNOWN;\n const sessionForTemplate = session.trim().length > 0 ? session : CITATION_UNKNOWN;\n\n // Map from recognised placeholder names to their resolved value. Unknown\n // placeholder names are left intact (returning the original `{name}`).\n const values: Record<string, string> = {\n agent,\n session: sessionForTemplate,\n sessionId,\n ts,\n date,\n };\n\n // Single-pass scan: replace every recognised `{name}` in one sweep so that\n // substituted values cannot themselves be treated as template source on a\n // subsequent pass. The replacer-function form also guarantees that `$` /\n // `$&` / `$1` sequences inside values are emitted literally.\n return template.replace(/\\{([a-zA-Z_][\\w]*)\\}/g, (match, name: string) => {\n return Object.prototype.hasOwnProperty.call(values, name)\n ? values[name]!\n : match;\n });\n}\n\n/**\n * Returns true if the text already carries at least one citation marker.\n * Safe to call on any string — never throws.\n */\nexport function hasCitation(text: string): boolean {\n if (typeof text !== \"string\" || text.length === 0) return false;\n return defaultCitationMatcher().test(text);\n}\n\n/**\n * Escape a string for use as a regex literal.\n */\nfunction escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Escape a single character for use INSIDE a character class `[...]`.\n * Special chars inside character classes: `]`, `\\`, `^`, `-`.\n */\nfunction escapeRegExpCharClass(ch: string): string {\n if (ch === \"]\") return \"\\\\]\";\n if (ch === \"\\\\\") return \"\\\\\\\\\";\n if (ch === \"^\") return \"\\\\^\";\n if (ch === \"-\") return \"\\\\-\";\n // Other regex meta chars are NOT special inside [...] but escaping them is safe.\n return escapeRegExp(ch);\n}\n\n/**\n * Build a per-placeholder token pattern that excludes newlines, whitespace,\n * and any punctuation characters used as inner separators in the template.\n *\n * This prevents a placeholder span from consuming separator bytes and\n * matching strings that cross separator boundaries in user-supplied content.\n *\n * @param nonWordSepChars - Set of non-word (non-alphanumeric, non-`_`) chars\n * extracted from the inner separator literals of the template.\n */\nfunction buildTokenPattern(nonWordSepChars: Set<string>): string {\n // Always exclude newlines and whitespace.\n const base = \"\\\\n\\\\s\";\n if (nonWordSepChars.size === 0) {\n // No inner separator punctuation — the placeholder spans the full space\n // between prefix and suffix. Fall back to a generous no-newline match.\n return `[^\\\\n]+?`;\n }\n const escaped = [...nonWordSepChars].map(escapeRegExpCharClass).join(\"\");\n return `[^${base}${escaped}]+?`;\n}\n\n/** Regex that matches a `{placeholder}` token inside a template string. */\nconst PLACEHOLDER_REGEX = /\\{[a-zA-Z_][\\w]*\\}/g;\n\n/**\n * Build a regex that matches a citation produced by the given template.\n *\n * The approach depends on the shape of the template:\n *\n * - **Normal case (non-empty literal prefix or suffix).** Anchor the match\n * on the outer literal frame and reconstruct the interior as\n * `interToken + sep + interToken + sep + ... + sep + lastToken`.\n * All **intermediate** per-placeholder tokens exclude the combined set of\n * non-word separator characters used between any two adjacent placeholders,\n * preventing a value from consuming a separator and crossing placeholder\n * boundaries. The **last** token is only required to avoid newlines because\n * it is terminated by the literal suffix anchor — this lets placeholder\n * values that legitimately contain a separator character be recognised (e.g.\n * an ISO-8601 timestamp `2026-04-10T14:25:07Z` in `[src:{agent}:{ts}]`\n * where `:` is the inter-placeholder separator). A template like\n * `[src:{agent}/{sessionId}@{date}]` emits\n * `\\[src:[^\\n\\s/@]+?\\/[^\\n\\s/@]+?@[^\\n]+?\\]` so that `[src:foo/bar]`\n * is NOT matched (wrong separator count), `[src:foo/bar/extra@2026]`\n * is NOT matched (intermediate token crosses a `/` boundary), and\n * `[src:planner/main@2026-04-10]` IS matched correctly.\n *\n * - **Placeholder-bounded with whitespace separator.** Both prefix and\n * suffix are empty and the separator literal(s) between placeholders\n * contain at least one whitespace character (e.g. `{source}: {content}`,\n * `{agent} {sessionId}`). A whitespace-containing separator produces\n * output that is visually indistinguishable from ordinary prose, so the\n * safe strategy is to require a **hard bracket/paren/angle delimiter** on\n * both sides of the reconstructed match. Prose almost never places\n * `[...]` / `(...)` / `<...>` around a phrase, so this yields clean\n * false-positive rejection.\n *\n * - **Placeholder-bounded with compact (non-whitespace) separator.** Both\n * prefix and suffix are empty and the separator literal(s) contain NO\n * whitespace (e.g. `{agent}:{sessionId}`, `{agent}/{sessionId}`).\n * `formatCitation` emits a compact token like `planner:main` with no\n * surrounding delimiters, so the bracket strategy cannot detect it.\n * Instead, the pattern requires that the entire token is bordered by\n * whitespace or a bracket/paren/angle on each side:\n *\n * `(?:(?<=[\\[\\(\\<])|(?<!\\S))[\\w.-]+<sep>[\\w.-]+(?:(?=[\\]\\)\\>])|(?!\\S))`\n *\n * This accepts `planner:main` when it appears standalone or inside a\n * bracket-wrapped token, and rejects `host:80` embedded inside a URL like\n * `http://host:80` because `host` is immediately preceded by `/`\n * (non-whitespace, non-bracket).\n *\n * - **All-placeholder case (no literals between placeholders either).** No\n * reliable regex can be built — a template like `{agent}{sessionId}`\n * contains no anchor characters. Returns `null`; {@link\n * hasCitationForTemplate} treats this as \"cannot detect\" and returns\n * false, falling back on explicit sentinel/format detection only for the\n * default `[Source: ...]` shape.\n *\n * Returns `null` when the template has no placeholders (fully-literal\n * citation, handled by the string-equality fast path in {@link\n * hasCitationForTemplate}) **or** when the template is entirely placeholder-\n * only with no literal content whatsoever.\n */\nfunction templateMatcher(template: string): RegExp | null {\n // Split around all {placeholder} tokens.\n const parts = template.split(PLACEHOLDER_REGEX);\n if (parts.length <= 1) return null;\n\n const prefix = parts[0] ?? \"\";\n const suffix = parts[parts.length - 1] ?? \"\";\n\n // Normal case: at least one literal frame on the outside.\n // Tighten the per-placeholder token so it cannot consume separator\n // characters and match strings that cross separator boundaries\n // (Finding 3 — Uru3).\n if (prefix.length > 0 || suffix.length > 0) {\n const escapedPrefix = escapeRegExp(prefix);\n const escapedSuffix = escapeRegExp(suffix);\n\n // Inner parts: literal separators that sit between adjacent placeholders.\n // For `[src:{agent}/{sessionId}@{date}]`, parts = [\"[src:\", \"/\", \"@\", \"]\"]\n // so innerParts = [\"/\", \"@\"].\n const innerParts = parts.slice(1, -1);\n\n // Collect only the non-word (punctuation/symbol) characters from each\n // inner separator so alphabetic separator text (unlikely but valid) does\n // not exclude letters from the per-placeholder token pattern.\n const nonWordSepChars = new Set<string>();\n for (const sep of innerParts) {\n for (const ch of sep) {\n if (!/\\w/.test(ch)) {\n nonWordSepChars.add(ch);\n }\n }\n }\n\n // All intermediate tokens (every placeholder except the last) use the\n // combined exclusion so they cannot cross placeholder boundaries.\n //\n // The LAST token is different: it is terminated by the literal suffix anchor\n // (e.g. `\\]`), so it does not need to exclude inner-separator characters.\n // Dropping that restriction lets placeholder values that legitimately contain\n // a separator character (e.g. an ISO-8601 timestamp `2026-04-10T14:25:07Z`\n // in template `[src:{agent}:{ts}]`) be recognised correctly instead of\n // producing false-negative misses that trigger duplicate citation injection.\n //\n // Only the LAST token is relaxed. Intermediate tokens keep the combined\n // exclusion so that cross-boundary false positives are still rejected\n // (e.g. `[src:foo/bar/extra@2026-04-11]` for `[src:{a}/{b}@{c}]`).\n const interToken = buildTokenPattern(nonWordSepChars);\n // Last token: terminated by suffix anchor — exclude only newlines.\n const lastToken = buildTokenPattern(new Set<string>());\n\n // Reconstruct the interior: interToken sep interToken sep ... sep lastToken\n // (or just lastToken when there are no inner separators at all).\n const middle =\n innerParts.length === 0\n ? lastToken\n : interToken +\n innerParts\n .slice(0, -1)\n .map((sep) => escapeRegExp(sep) + interToken)\n .join(\"\") +\n escapeRegExp(innerParts[innerParts.length - 1]!) +\n lastToken;\n\n const pattern = escapedPrefix + middle + escapedSuffix;\n return new RegExp(pattern, \"i\");\n }\n\n // Placeholder-bounded case: prefix and suffix are both empty.\n const middleLiterals = parts.slice(1, -1);\n const hasNonEmptyMiddle = middleLiterals.some((p) => p.length > 0);\n if (!hasNonEmptyMiddle) {\n // All-placeholder template with no literal content. Impossible to anchor\n // reliably without sentinel markers; signal the caller.\n return null;\n }\n\n // Identifier token: one or more word chars, dots, dashes, or colons.\n // Colons are included to allow timestamp values like \"10:30\" or session\n // keys like \"agent:planner:main\" inside compact placeholder-bounded\n // templates. URL-like fragments (`http://host:80`) are still rejected\n // because the lead anchor requires whitespace or a bracket immediately\n // before the first id-token group (`http` is preceded by `/`).\n const idToken = \"[\\\\w.:-]+\";\n const body =\n idToken +\n middleLiterals.map((lit) => escapeRegExp(lit) + idToken).join(\"\");\n\n const separatorText = middleLiterals.join(\"\");\n if (/\\s/.test(separatorText)) {\n // Separator contains whitespace: the emitted citation looks like ordinary\n // prose (e.g. `planner main`). Require a hard bracket/paren/angle\n // delimiter on both sides to prevent false matches on English text.\n const opener = \"[\\\\[\\\\(\\\\<]\";\n const closer = \"[\\\\]\\\\)\\\\>]\";\n return new RegExp(opener + body + closer, \"i\");\n }\n\n // Separator is compact (no whitespace): `formatCitation` emits a token like\n // `planner:main` without surrounding delimiters. The challenge is that the\n // same token shape also matches ordinary hyphenated or slashed prose words\n // (e.g. `long-term`, `docs/setup`), causing `hasCitationForTemplate` to\n // return true on uncited fact bodies and silently suppress citation injection\n // from `attachCitation`.\n //\n // Fix (Finding 1): tighten the trail anchor so a bare compact token is only\n // accepted when it sits at the very end of the string (possibly followed by\n // optional trailing whitespace or a newline). Since `attachCitation` always\n // appends the citation at the trimmed end of the fact body, a real citation\n // token will always appear at the tail. Prose like `\"long-term solution\"`\n // has `long-term` in the middle of the string (followed by ` solution`), so\n // the end-of-string anchor rejects it — no false positive, no silent drop.\n //\n // The lead anchor still accepts either a bracket opener or a whitespace\n // boundary (or start of string), so `\"Fact. planner:main\"` and standalone\n // `\"planner:main\"` are both detected after the first attachment pass.\n //\n // Bracket-wrapped form (e.g. `[planner:main]`) is also accepted via the\n // opener/closer pair — bracket still takes precedence over end-of-string.\n //\n // Example — why `http://host:80` does NOT match:\n // Trying to match `host:80`: the char before `h` is `/` (non-whitespace,\n // non-bracket), so `(?<=[\\[\\(\\<])` and `(?<!\\S)` both fail ⟹ no match.\n // Trying to match `http:...`: after `http:` the next chars are `//` which\n // are not `[\\w.-]+`, so the second id-token group fails ⟹ no match.\n const leadAnchor = \"(?:(?<=[\\\\[\\\\(\\\\<])|(?<!\\\\S))\";\n // Trail: either a bracket closer (for `[token]` shape) or end-of-string\n // optionally preceded by whitespace. The `(?!\\S)` is deliberately removed\n // so that a compact token in the MIDDLE of a sentence does not match.\n const trailAnchor = \"(?:(?=[\\\\]\\\\)\\\\>])|(?=\\\\s*$))\";\n return new RegExp(leadAnchor + body + trailAnchor, \"i\");\n}\n\n/**\n * Returns true if `text` already carries a citation produced by `template`\n * **or** by the default `[Source: ...]` format (for facts that were tagged\n * before a config change).\n *\n * Use this instead of {@link hasCitation} whenever the caller has access to\n * the configured `inlineSourceAttributionFormat`.\n *\n * All-placeholder templates such as `{agent}{sessionId}` have no literal\n * content to anchor on and therefore cannot be reliably detected without\n * dedicated sentinel markers. In that case the function returns `false` —\n * callers that need idempotent dedup for such templates should either adopt\n * a template with literal delimiters (recommended) or rely on the default\n * `[Source: ...]` marker detection which is always available via\n * {@link hasCitation}.\n */\nexport function hasCitationForTemplate(text: string, template: string): boolean {\n if (typeof text !== \"string\" || text.length === 0) return false;\n // Always accept the default format as a fallback so facts tagged before a\n // configuration change are not double-tagged on reprocessing.\n if (hasCitation(text)) return true;\n // If the configured template matches the default, we're done.\n //\n // Known limitation (Thread 2 — Codex P2): this fast path exits without\n // checking whether the content carries a citation from a DIFFERENT custom\n // template that was active before the config was changed back to the default.\n // Such a fact would be detected by `hasCitation` above only if the prior\n // custom template happened to match the default `[Source: ...]` pattern.\n // In practice, template changes mid-stream are rare, and the false-negative\n // (missing an old custom citation) produces a benign duplicate citation rather\n // than data loss. A full fix would require storing the citation template used\n // at write time in the frontmatter and checking that here.\n if (template === DEFAULT_CITATION_FORMAT) return false;\n\n // Fully-literal template (no placeholders): exact inclusion check.\n if (!PLACEHOLDER_REGEX.test(template)) {\n // Reset lastIndex because PLACEHOLDER_REGEX is declared with /g.\n PLACEHOLDER_REGEX.lastIndex = 0;\n return text.includes(template);\n }\n // Reset lastIndex after the .test() probe above.\n PLACEHOLDER_REGEX.lastIndex = 0;\n\n const matcher = templateMatcher(template);\n if (!matcher) {\n // All-placeholder template: cannot build a reliable matcher. See the\n // docstring — callers should not rely on dedup for this shape.\n return false;\n }\n return matcher.test(text);\n}\n\n/**\n * Attach an inline citation to fact text.\n *\n * If the text already has a citation — either the default `[Source: ...]`\n * marker or one produced by the configured template — it is returned unchanged.\n * Existing provenance is respected and never overwritten. Otherwise the\n * citation is appended to the trimmed text with a single space separator,\n * which keeps the marker visually adjacent to the fact body.\n */\nexport function attachCitation(\n text: string,\n ctx: CitationContext,\n template: string = DEFAULT_CITATION_FORMAT,\n): string {\n if (typeof text !== \"string\") return text as unknown as string;\n if (hasCitationForTemplate(text, template)) return text;\n const trimmedEnd = trimTrailingWhitespace(text);\n if (trimmedEnd.length === 0) return text;\n const citation = formatCitation(ctx, template);\n // Preserve any trailing newline that callers rely on for markdown rendering.\n const trailing = text.slice(trimmedEnd.length);\n return `${trimmedEnd} ${citation}${trailing}`;\n}\n\n/**\n * Parse a single inline citation from a piece of text. Returns the first\n * citation encountered or `null` when none is present. Malformed citations\n * do not throw — fields that cannot be parsed simply remain `undefined`.\n */\nexport function parseCitation(text: string): ParsedCitation | null {\n if (typeof text !== \"string\" || text.length === 0) return null;\n const matcher = defaultCitationMatcher();\n const match = matcher.exec(text);\n if (!match) return null;\n\n const body = match[1] ?? \"\";\n const raw = match[0] ?? \"\";\n const parsed: ParsedCitation = { raw };\n\n const fields = body\n .split(\",\")\n .map((segment) => segment.trim())\n .filter((segment) => segment.length > 0);\n\n for (const field of fields) {\n const eqIdx = field.indexOf(\"=\");\n if (eqIdx <= 0) continue;\n const key = field.slice(0, eqIdx).trim().toLowerCase();\n const value = field.slice(eqIdx + 1).trim();\n if (value.length === 0) continue;\n switch (key) {\n case \"agent\":\n parsed.agent = value;\n break;\n case \"session\":\n case \"sessionid\":\n parsed.session = value;\n break;\n case \"ts\":\n case \"timestamp\":\n parsed.ts = value;\n break;\n default:\n // Unknown fields are ignored defensively so human edits never crash.\n break;\n }\n }\n\n return parsed;\n}\n\n/**\n * Parse every citation embedded in the text. Always returns an array; empty\n * when none are present.\n */\nexport function parseAllCitations(text: string): ParsedCitation[] {\n if (typeof text !== \"string\" || text.length === 0) return [];\n const matcher = defaultCitationMatcher();\n const results: ParsedCitation[] = [];\n let match: RegExpExecArray | null;\n while ((match = matcher.exec(text)) !== null) {\n const parsed = parseCitation(match[0]);\n if (parsed) results.push(parsed);\n }\n return results;\n}\n\n/**\n * Remove all inline citations from a piece of text.\n *\n * Callers that want the raw fact body (for dedup hashing, display, or\n * comparison) should use this helper instead of hand-rolled regexes so the\n * whole codebase agrees on the citation syntax.\n *\n * Finding 2 fix: when the input contains no citation marker, the input is\n * returned byte-for-byte unchanged. When a citation IS removed, whitespace\n * normalization is applied only at each join seam (the single space between\n * the preceding text and where the citation was), rather than across the\n * entire string. This preserves markdown hard-break spacing, aligned text,\n * and code-like snippets in fact bodies that happen to carry a citation.\n *\n * Implementation: each citation match is replaced by its \"seam fix\" — the\n * content before the match has its trailing whitespace trimmed and then a\n * single space is appended if any text remains, collapsing only the gap\n * left by the removed marker. Whitespace elsewhere in the body is untouched.\n */\nexport function stripCitation(text: string): string {\n if (typeof text !== \"string\" || text.length === 0) return text;\n // Early exit: no citation marker present — return the input unchanged so\n // that callers never lose formatting fidelity on uncited strings.\n if (!hasCitation(text)) return text;\n\n // Walk through all citations and slice them out one by one so that we can\n // normalise ONLY the whitespace at each seam rather than the entire string.\n const matcher = defaultCitationMatcher();\n let result = \"\";\n let lastIndex = 0;\n\n let match: RegExpExecArray | null;\n while ((match = matcher.exec(text)) !== null) {\n // Text before this citation. Trim trailing spaces/tabs at the seam only.\n const before = text.slice(lastIndex, match.index).replace(/[ \\t]+$/, \"\");\n result += before;\n lastIndex = match.index + match[0].length;\n }\n\n // Append any trailing text after the last citation. Trim leading\n // spaces/tabs and trailing whitespace at the join seam.\n const after = text.slice(lastIndex).replace(/^[ \\t]+/, \"\");\n if (after.length > 0) {\n if (result.length > 0) result += \" \";\n result += after;\n }\n\n return result.trimEnd();\n}\n\n/**\n * Strip an inline citation from text using a specific template regex.\n *\n * This is the template-aware counterpart to {@link stripCitation}. When the\n * caller holds the configured `inlineSourceAttributionFormat`, use this\n * function to strip citations produced by that template — including custom\n * templates that differ from the default `[Source: ...]` pattern.\n *\n * Behaviour:\n * - If the text has a **default-format** citation, delegates to\n * {@link stripCitation} (always safe).\n * - If the text has a **custom-template** citation detected by\n * `hasCitationForTemplate`, builds the template regex and removes every\n * occurrence (citations are appended at the end of the fact body by\n * {@link attachCitation}).\n * - All-placeholder templates (no literal prefix/suffix/separator) cannot\n * produce a reliable matcher. `hasCitationForTemplate` already returns\n * `false` for such templates, so this function never attempts to strip an\n * undetectable citation. The text is returned unchanged when no citation\n * is detected.\n * - If no citation is detected for the given template, returns the text\n * unchanged.\n *\n * @returns The stripped text (or the original text when no citation is found).\n */\nexport function stripCitationForTemplate(\n text: string,\n template: string,\n): string {\n if (typeof text !== \"string\" || text.length === 0) return text;\n\n // Fast path: default-format citation — delegate to the existing stripper.\n if (hasCitation(text)) return stripCitation(text);\n\n // No default citation; check whether the custom template produced one.\n // hasCitationForTemplate returns false for all-placeholder templates (no\n // reliable matcher), so those pass through unchanged below.\n if (!hasCitationForTemplate(text, template)) return text;\n\n // Build the template matcher. hasCitationForTemplate already returned true,\n // which means templateMatcher produced a non-null result. The null branch\n // here is a defensive fallback only — delegate to stripCitation.\n const matcher = templateMatcher(template);\n if (!matcher) return stripCitation(text);\n\n // The matcher regex was built without the global flag; add it for exec loop.\n const globalMatcher = new RegExp(\n matcher.source,\n matcher.flags.includes(\"g\") ? matcher.flags : matcher.flags + \"g\",\n );\n let result = \"\";\n let lastIndex = 0;\n let match: RegExpExecArray | null;\n\n while ((match = globalMatcher.exec(text)) !== null) {\n const matchEnd = match.index + match[0].length;\n const enclosure = enclosingDelimiterRange(text, match.index, matchEnd);\n const removalStart = enclosure?.start ?? match.index;\n const removalEnd = enclosure?.end ?? matchEnd;\n const before = text.slice(lastIndex, removalStart).replace(/[ \\t]+$/, \"\");\n result += before;\n lastIndex = removalEnd;\n // Guard against zero-width matches causing an infinite loop.\n if (match[0].length === 0) {\n globalMatcher.lastIndex++;\n }\n }\n\n const after = text.slice(lastIndex).replace(/^[ \\t]+/, \"\");\n if (after.length > 0) {\n if (result.length > 0) result += \" \";\n result += after;\n }\n\n return result.trimEnd();\n}\n\nfunction enclosingDelimiterRange(\n text: string,\n start: number,\n end: number,\n): { start: number; end: number } | undefined {\n if (start <= 0 || end >= text.length) return undefined;\n const opener = text[start - 1];\n const closer = text[end];\n if (\n (opener === \"[\" && closer === \"]\") ||\n (opener === \"(\" && closer === \")\") ||\n (opener === \"<\" && closer === \">\")\n ) {\n return { start: start - 1, end: end + 1 };\n }\n return undefined;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from "./chunk-L2EXJQJP.js";
|
|
13
13
|
import {
|
|
14
14
|
extractJsonCandidates
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-RGMVMVMF.js";
|
|
16
16
|
import {
|
|
17
17
|
callCodexCliFallback
|
|
18
18
|
} from "./chunk-RK6F44Y6.js";
|
|
@@ -692,4 +692,4 @@ export {
|
|
|
692
692
|
fallbackLlmRuntimeContextFromConfig,
|
|
693
693
|
FallbackLlmClient
|
|
694
694
|
};
|
|
695
|
-
//# sourceMappingURL=chunk-
|
|
695
|
+
//# sourceMappingURL=chunk-KGIGRNR6.js.map
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
listMemoryGovernanceRuns,
|
|
28
28
|
readMemoryGovernanceRunArtifact,
|
|
29
29
|
runMemoryGovernance
|
|
30
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-GYTVOLNX.js";
|
|
31
31
|
import {
|
|
32
32
|
clusterByKey,
|
|
33
33
|
combineNamespaces,
|
|
@@ -73,7 +73,7 @@ import {
|
|
|
73
73
|
persistExplicitCapture,
|
|
74
74
|
queueExplicitCaptureForReview,
|
|
75
75
|
validateExplicitCaptureInput
|
|
76
|
-
} from "./chunk-
|
|
76
|
+
} from "./chunk-H3PHZLMF.js";
|
|
77
77
|
import {
|
|
78
78
|
CrossNamespaceBudget
|
|
79
79
|
} from "./chunk-ODPLEWB6.js";
|
|
@@ -82,10 +82,10 @@ import {
|
|
|
82
82
|
buildBriefing,
|
|
83
83
|
parseBriefingFocus,
|
|
84
84
|
parseBriefingWindow
|
|
85
|
-
} from "./chunk-
|
|
85
|
+
} from "./chunk-OZXVGYGZ.js";
|
|
86
86
|
import {
|
|
87
87
|
parseEntityFile
|
|
88
|
-
} from "./chunk-
|
|
88
|
+
} from "./chunk-PJGB7XRR.js";
|
|
89
89
|
import {
|
|
90
90
|
DEFAULT_RECALL_DISCLOSURE,
|
|
91
91
|
isRecallDisclosure
|
|
@@ -4349,4 +4349,4 @@ export {
|
|
|
4349
4349
|
shapeMemorySummary,
|
|
4350
4350
|
EngramAccessService
|
|
4351
4351
|
};
|
|
4352
|
-
//# sourceMappingURL=chunk-
|
|
4352
|
+
//# sourceMappingURL=chunk-KQFQ3IS5.js.map
|
|
@@ -151,7 +151,7 @@ function splitLoopMarkdown(raw) {
|
|
|
151
151
|
const sections = [];
|
|
152
152
|
let current = null;
|
|
153
153
|
for (const line of lines) {
|
|
154
|
-
const sectionMatch = line.match(/^##\s
|
|
154
|
+
const sectionMatch = line.match(/^##\s(.+)$/);
|
|
155
155
|
if (sectionMatch) {
|
|
156
156
|
if (current) sections.push({ title: current.title, body: current.body.trimEnd() });
|
|
157
157
|
current = { title: sectionMatch[1].trim(), body: "" };
|
|
@@ -265,4 +265,4 @@ export {
|
|
|
265
265
|
upsertContinuityLoopInMarkdown,
|
|
266
266
|
reviewContinuityLoopInMarkdown
|
|
267
267
|
};
|
|
268
|
-
//# sourceMappingURL=chunk-
|
|
268
|
+
//# sourceMappingURL=chunk-LDXUBPMO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/identity-continuity.ts"],"sourcesContent":["import type {\n ContinuityIncidentCloseInput,\n ContinuityIncidentOpenInput,\n ContinuityImprovementLoop,\n ContinuityLoopCadence,\n ContinuityLoopReviewInput,\n ContinuityLoopStatus,\n ContinuityLoopUpsertInput,\n ContinuityIncidentRecord,\n ContinuityIncidentState,\n} from \"./types.js\";\n\nfunction parseFrontmatterValue(raw: string): unknown {\n try {\n return JSON.parse(raw);\n } catch {\n return raw;\n }\n}\n\nfunction parseFrontmatter(raw: string): Record<string, unknown> {\n const parsed: Record<string, unknown> = {};\n for (const line of raw.split(\"\\n\")) {\n const idx = line.indexOf(\":\");\n if (idx <= 0) continue;\n const key = line.slice(0, idx).trim();\n const value = line.slice(idx + 1).trim();\n parsed[key] = parseFrontmatterValue(value);\n }\n return parsed;\n}\n\nfunction emitSection(lines: string[], title: string, value?: string): void {\n if (!value || value.trim().length === 0) return;\n lines.push(`## ${title}`, \"\", value.trim(), \"\");\n}\n\nfunction parseSection(body: string, title: string): string | undefined {\n const escaped = title.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const re = new RegExp(`## ${escaped}\\\\n\\\\n([\\\\s\\\\S]*?)(?=\\\\n## |$)`);\n const match = body.match(re);\n if (!match) return undefined;\n const value = match[1].trim();\n return value.length > 0 ? value : undefined;\n}\n\nexport function serializeContinuityIncident(incident: ContinuityIncidentRecord): string {\n const lines = [\n \"---\",\n `id: ${JSON.stringify(incident.id)}`,\n `state: ${JSON.stringify(incident.state)}`,\n `openedAt: ${JSON.stringify(incident.openedAt)}`,\n `updatedAt: ${JSON.stringify(incident.updatedAt)}`,\n ];\n if (incident.closedAt) lines.push(`closedAt: ${JSON.stringify(incident.closedAt)}`);\n if (incident.triggerWindow) lines.push(`triggerWindow: ${JSON.stringify(incident.triggerWindow)}`);\n lines.push(\"---\", \"\");\n\n emitSection(lines, \"Symptom\", incident.symptom);\n emitSection(lines, \"Suspected Cause\", incident.suspectedCause);\n emitSection(lines, \"Fix Applied\", incident.fixApplied);\n emitSection(lines, \"Verification Result\", incident.verificationResult);\n emitSection(lines, \"Preventive Rule\", incident.preventiveRule);\n\n return lines.join(\"\\n\").trimEnd() + \"\\n\";\n}\n\nexport function parseContinuityIncident(raw: string): ContinuityIncidentRecord | null {\n const match = raw.match(/^---\\n([\\s\\S]*?)\\n---\\n?([\\s\\S]*)$/);\n if (!match) return null;\n const frontmatter = parseFrontmatter(match[1]);\n const body = match[2] ?? \"\";\n\n const id = typeof frontmatter.id === \"string\" ? frontmatter.id : \"\";\n const stateRaw = frontmatter.state;\n const state: ContinuityIncidentState = stateRaw === \"closed\" ? \"closed\" : \"open\";\n const openedAt = typeof frontmatter.openedAt === \"string\" ? frontmatter.openedAt : \"\";\n const updatedAt = typeof frontmatter.updatedAt === \"string\" ? frontmatter.updatedAt : openedAt;\n const symptom = parseSection(body, \"Symptom\");\n\n if (!id || !openedAt || !updatedAt || !symptom) return null;\n\n return {\n id,\n state,\n openedAt,\n updatedAt,\n triggerWindow: typeof frontmatter.triggerWindow === \"string\" ? frontmatter.triggerWindow : undefined,\n symptom,\n suspectedCause: parseSection(body, \"Suspected Cause\"),\n fixApplied: parseSection(body, \"Fix Applied\"),\n verificationResult: parseSection(body, \"Verification Result\"),\n preventiveRule: parseSection(body, \"Preventive Rule\"),\n closedAt: typeof frontmatter.closedAt === \"string\" ? frontmatter.closedAt : undefined,\n };\n}\n\nexport function createContinuityIncidentRecord(\n id: string,\n input: ContinuityIncidentOpenInput,\n nowIso: string,\n): ContinuityIncidentRecord {\n return {\n id,\n state: \"open\",\n openedAt: nowIso,\n updatedAt: nowIso,\n triggerWindow: input.triggerWindow?.trim() || undefined,\n symptom: input.symptom.trim(),\n suspectedCause: input.suspectedCause?.trim() || undefined,\n };\n}\n\nexport function closeContinuityIncidentRecord(\n incident: ContinuityIncidentRecord,\n closure: ContinuityIncidentCloseInput,\n nowIso: string,\n): ContinuityIncidentRecord {\n return {\n ...incident,\n state: \"closed\",\n updatedAt: nowIso,\n closedAt: nowIso,\n fixApplied: closure.fixApplied.trim(),\n verificationResult: closure.verificationResult.trim(),\n preventiveRule: closure.preventiveRule?.trim() || incident.preventiveRule,\n };\n}\n\nconst LOOP_HEADER = \"# Continuity Improvement Loops\";\nconst LOOP_CADENCES = new Set<ContinuityLoopCadence>([\"daily\", \"weekly\", \"monthly\", \"quarterly\"]);\nconst LOOP_STATUSES = new Set<ContinuityLoopStatus>([\"active\", \"paused\", \"retired\"]);\nconst STALE_LAST_REVIEWED_FALLBACK = \"1970-01-01T00:00:00.000Z\";\n\nfunction normalizeLoopField(value: string | undefined): string | undefined {\n if (typeof value !== \"string\") return undefined;\n const trimmed = value.trim();\n if (trimmed.length === 0) return undefined;\n return trimmed.replace(/\\s+/g, \" \");\n}\n\nfunction isValidIso(value: string): boolean {\n const ts = Date.parse(value);\n return Number.isFinite(ts);\n}\n\nfunction normalizeContinuityLoop(\n input: ContinuityLoopUpsertInput | ContinuityImprovementLoop,\n nowIso: string,\n): ContinuityImprovementLoop | null {\n const id = normalizeLoopField(input.id);\n const cadence = normalizeLoopField(input.cadence) as ContinuityLoopCadence | undefined;\n const status = normalizeLoopField(input.status) as ContinuityLoopStatus | undefined;\n const purpose = normalizeLoopField(input.purpose);\n const killCondition = normalizeLoopField(input.killCondition);\n const notes = normalizeLoopField(input.notes);\n const lastReviewedRaw =\n \"lastReviewed\" in input && typeof input.lastReviewed === \"string\" ? input.lastReviewed : undefined;\n const lastReviewed = normalizeLoopField(lastReviewedRaw) ?? nowIso;\n\n if (!id || !cadence || !status || !purpose || !killCondition) return null;\n if (!LOOP_CADENCES.has(cadence)) return null;\n if (!LOOP_STATUSES.has(status)) return null;\n if (!isValidIso(lastReviewed)) return null;\n\n return {\n id,\n cadence,\n purpose,\n status,\n killCondition,\n lastReviewed,\n notes,\n };\n}\n\nfunction serializeContinuityLoopSection(loop: ContinuityImprovementLoop): string {\n const lines = [\n `## ${loop.id}`,\n `cadence: ${loop.cadence}`,\n `purpose: ${loop.purpose}`,\n `status: ${loop.status}`,\n `killCondition: ${loop.killCondition}`,\n `lastReviewed: ${loop.lastReviewed}`,\n ];\n if (loop.notes) lines.push(`notes: ${loop.notes}`);\n return lines.join(\"\\n\");\n}\n\ntype MarkdownSection = {\n title: string;\n body: string;\n};\n\nfunction splitLoopMarkdown(raw: string | null): { header: string; sections: MarkdownSection[] } {\n const text = (raw ?? \"\").replace(/\\r/g, \"\");\n const lines = text.split(\"\\n\");\n const headerLines: string[] = [];\n const sections: MarkdownSection[] = [];\n let current: MarkdownSection | null = null;\n\n for (const line of lines) {\n // /^##\\s(.+)$/ + the trim below is exactly equivalent to the original\n // /^##\\s+(.+?)\\s*$/ (same match/no-match and same trimmed title across all\n // inputs, including \"## \" → no-match and \"## \" → empty title) but has no\n // adjacent overlapping quantifiers, so it cannot backtrack polynomially\n // (CodeQL js/polynomial-redos). \\s matches a single fixed-width char and\n // .+ runs greedily to the line end — no \\s+/.* overlap.\n const sectionMatch = line.match(/^##\\s(.+)$/);\n if (sectionMatch) {\n if (current) sections.push({ title: current.title, body: current.body.trimEnd() });\n current = { title: sectionMatch[1].trim(), body: \"\" };\n continue;\n }\n if (!current) {\n headerLines.push(line);\n continue;\n }\n current.body += current.body.length > 0 ? `\\n${line}` : line;\n }\n if (current) sections.push({ title: current.title, body: current.body.trimEnd() });\n\n const headerRaw = headerLines.join(\"\\n\").trim();\n const header = headerRaw.length > 0 ? headerRaw : LOOP_HEADER;\n return { header, sections };\n}\n\nfunction parseLoopFromSection(section: MarkdownSection, nowIso: string): ContinuityImprovementLoop | null {\n const fields: Record<string, string> = {};\n for (const line of section.body.split(\"\\n\")) {\n const kv = line.match(/^([A-Za-z][A-Za-z0-9_]*):\\s*(.+?)\\s*$/);\n if (!kv) continue;\n fields[kv[1]] = kv[2];\n }\n const parsedLastReviewed = normalizeLoopField(fields.lastReviewed);\n const safeLastReviewed =\n parsedLastReviewed && isValidIso(parsedLastReviewed) ? parsedLastReviewed : STALE_LAST_REVIEWED_FALLBACK;\n return normalizeContinuityLoop(\n {\n id: section.title,\n cadence: (fields.cadence ?? \"\") as ContinuityLoopCadence,\n purpose: fields.purpose ?? \"\",\n status: (fields.status ?? \"\") as ContinuityLoopStatus,\n killCondition: fields.killCondition ?? \"\",\n lastReviewed: safeLastReviewed,\n notes: fields.notes,\n },\n nowIso,\n );\n}\n\nfunction joinLoopMarkdown(header: string, sections: MarkdownSection[]): string {\n const lines: string[] = [header.trim(), \"\"];\n for (const section of sections) {\n lines.push(`## ${section.title}`);\n if (section.body.trim().length > 0) {\n lines.push(section.body.trimEnd());\n }\n lines.push(\"\");\n }\n return lines.join(\"\\n\").replace(/\\n{3,}/g, \"\\n\\n\").trimEnd() + \"\\n\";\n}\n\nexport function parseContinuityImprovementLoops(raw: string): ContinuityImprovementLoop[] {\n const parsed = splitLoopMarkdown(raw);\n const nowIso = new Date().toISOString();\n return parsed.sections\n .map((section) => parseLoopFromSection(section, nowIso))\n .filter((loop): loop is ContinuityImprovementLoop => loop !== null);\n}\n\nexport function upsertContinuityLoopInMarkdown(\n raw: string | null,\n input: ContinuityLoopUpsertInput,\n nowIso: string,\n): { markdown: string; loop: ContinuityImprovementLoop } {\n const normalized = normalizeContinuityLoop(input, nowIso);\n if (!normalized) {\n throw new Error(\"Invalid continuity loop input\");\n }\n\n const parsed = splitLoopMarkdown(raw);\n let replaced = false;\n const nextSections = parsed.sections.map((section) => {\n if (normalizeLoopField(section.title) !== normalized.id) return section;\n replaced = true;\n return { title: normalized.id, body: serializeContinuityLoopSection(normalized).split(\"\\n\").slice(1).join(\"\\n\") };\n });\n\n if (!replaced) {\n nextSections.push({\n title: normalized.id,\n body: serializeContinuityLoopSection(normalized).split(\"\\n\").slice(1).join(\"\\n\"),\n });\n }\n\n return { markdown: joinLoopMarkdown(parsed.header, nextSections), loop: normalized };\n}\n\nexport function reviewContinuityLoopInMarkdown(\n raw: string | null,\n id: string,\n input: ContinuityLoopReviewInput,\n nowIso: string,\n): { markdown: string; loop: ContinuityImprovementLoop | null } {\n const parsed = splitLoopMarkdown(raw);\n const normalizedId = normalizeLoopField(id);\n if (!normalizedId) {\n return { markdown: joinLoopMarkdown(parsed.header, parsed.sections), loop: null };\n }\n let updatedLoop: ContinuityImprovementLoop | null = null;\n const nextSections = parsed.sections.map((section) => {\n if (normalizeLoopField(section.title) !== normalizedId) return section;\n const existing = parseLoopFromSection(section, nowIso);\n if (!existing) return section;\n const reviewed = applyContinuityLoopReview(existing, input, nowIso);\n updatedLoop = reviewed;\n return { title: reviewed.id, body: serializeContinuityLoopSection(reviewed).split(\"\\n\").slice(1).join(\"\\n\") };\n });\n\n return { markdown: joinLoopMarkdown(parsed.header, nextSections), loop: updatedLoop };\n}\n\nfunction applyContinuityLoopReview(\n existing: ContinuityImprovementLoop,\n input: ContinuityLoopReviewInput,\n nowIso: string,\n): ContinuityImprovementLoop {\n const nextStatus = normalizeLoopField(input.status) as ContinuityLoopStatus | undefined;\n const nextNotes = normalizeLoopField(input.notes);\n const reviewedAt = normalizeLoopField(input.reviewedAt) ?? nowIso;\n\n return {\n ...existing,\n status: nextStatus && LOOP_STATUSES.has(nextStatus) ? nextStatus : existing.status,\n notes: nextNotes ?? existing.notes,\n lastReviewed: isValidIso(reviewedAt) ? reviewedAt : nowIso,\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -268,7 +268,11 @@ function renderMemorySummary(ctx) {
|
|
|
268
268
|
}
|
|
269
269
|
lines.push("");
|
|
270
270
|
}
|
|
271
|
-
const
|
|
271
|
+
const joined = lines.join("\n");
|
|
272
|
+
let joinedEnd = joined.length;
|
|
273
|
+
while (joinedEnd > 0 && joined[joinedEnd - 1] === "\n") joinedEnd--;
|
|
274
|
+
const full = joinedEnd === joined.length ? joined : `${joined.slice(0, joinedEnd)}
|
|
275
|
+
`;
|
|
272
276
|
return truncateToTokenBudget(full, ctx.maxTokens);
|
|
273
277
|
}
|
|
274
278
|
function renderMemoryMd(ctx) {
|
|
@@ -690,4 +694,4 @@ export {
|
|
|
690
694
|
truncateToTokenBudget,
|
|
691
695
|
describeMemoriesDir
|
|
692
696
|
};
|
|
693
|
-
//# sourceMappingURL=chunk-
|
|
697
|
+
//# sourceMappingURL=chunk-LN4YGHTM.js.map
|