@prodero/curator 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/ARCHITECTURE.md +298 -0
  2. package/CHANGELOG.md +33 -0
  3. package/CONTRIBUTING.md +9 -0
  4. package/LICENSE +21 -0
  5. package/README.md +93 -2
  6. package/SECURITY.md +7 -0
  7. package/bin/prodero +7 -0
  8. package/dist/cli.js +266 -0
  9. package/dist/context-instructions.js +2 -0
  10. package/dist/contracts.js +51 -0
  11. package/dist/conversation-presentation.js +297 -0
  12. package/dist/core/agent-instructions.js +52 -0
  13. package/dist/core/applicability-label.js +11 -0
  14. package/dist/core/applicability.js +143 -0
  15. package/dist/core/bootstrap.js +129 -0
  16. package/dist/core/consolidation.js +315 -0
  17. package/dist/core/constants.js +21 -0
  18. package/dist/core/context-allocation.js +54 -0
  19. package/dist/core/context-snapshot.js +12 -0
  20. package/dist/core/context.js +489 -0
  21. package/dist/core/curation-clauses.js +68 -0
  22. package/dist/core/curation-evidence.js +98 -0
  23. package/dist/core/curation-graphical.js +117 -0
  24. package/dist/core/curation-question.js +28 -0
  25. package/dist/core/curation-review-presentation.js +4 -0
  26. package/dist/core/curation-review-state.js +62 -0
  27. package/dist/core/curation-review.js +573 -0
  28. package/dist/core/curation-revision.js +106 -0
  29. package/dist/core/curation-semantic-contract.js +242 -0
  30. package/dist/core/curation-semantic-jobs.js +340 -0
  31. package/dist/core/diagnostics.js +17 -0
  32. package/dist/core/format.js +114 -0
  33. package/dist/core/ids.js +6 -0
  34. package/dist/core/imports.js +169 -0
  35. package/dist/core/index.js +93 -0
  36. package/dist/core/instruction-review.js +86 -0
  37. package/dist/core/integrity-files.js +174 -0
  38. package/dist/core/intelligence.js +95 -0
  39. package/dist/core/knowledge-analysis.js +46 -0
  40. package/dist/core/knowledge-authority.js +143 -0
  41. package/dist/core/knowledge-curation.js +316 -0
  42. package/dist/core/knowledge-engine.js +475 -0
  43. package/dist/core/knowledge-graph.js +192 -0
  44. package/dist/core/knowledge-lifecycle.js +212 -0
  45. package/dist/core/knowledge-management.js +113 -0
  46. package/dist/core/knowledge-migration.js +84 -0
  47. package/dist/core/knowledge-policy.js +38 -0
  48. package/dist/core/knowledge-provenance.js +90 -0
  49. package/dist/core/knowledge-write-policy.js +85 -0
  50. package/dist/core/language.js +14 -0
  51. package/dist/core/local-preferences.js +76 -0
  52. package/dist/core/matching.js +6 -0
  53. package/dist/core/memory-index.js +241 -0
  54. package/dist/core/memory-intelligence.js +288 -0
  55. package/dist/core/memory-link-repair.js +70 -0
  56. package/dist/core/memory-operations.js +122 -0
  57. package/dist/core/memory-pressure.js +212 -0
  58. package/dist/core/memory-sync-curator.js +82 -0
  59. package/dist/core/memory-sync.js +558 -0
  60. package/dist/core/memory-transformation-bundle.js +163 -0
  61. package/dist/core/memory-transformation-proposal.js +135 -0
  62. package/dist/core/memory-transformation.js +108 -0
  63. package/dist/core/memory.js +186 -0
  64. package/dist/core/migrations.js +569 -0
  65. package/dist/core/npm-update.js +289 -0
  66. package/dist/core/path-scope.js +27 -0
  67. package/dist/core/plugin-files.js +116 -0
  68. package/dist/core/project-root.js +39 -0
  69. package/dist/core/project.js +634 -0
  70. package/dist/core/promotion.js +279 -0
  71. package/dist/core/reconciliation.js +61 -0
  72. package/dist/core/secrets.js +67 -0
  73. package/dist/core/semantic-safety.js +33 -0
  74. package/dist/core/session-index.js +116 -0
  75. package/dist/core/session-review.js +150 -0
  76. package/dist/core/sessions.js +289 -0
  77. package/dist/core/settings.js +94 -0
  78. package/dist/core/skill-curation.js +239 -0
  79. package/dist/core/skill-polish.js +41 -0
  80. package/dist/core/skills.js +387 -0
  81. package/dist/core/storage.js +462 -0
  82. package/dist/core/sync-transaction.js +290 -0
  83. package/dist/core/telemetry.js +64 -0
  84. package/dist/core/templates.js +53 -0
  85. package/dist/core/types.js +2 -0
  86. package/dist/core/update.js +326 -0
  87. package/dist/curation-review-adapter.js +92 -0
  88. package/dist/diagnostic-boundary.js +55 -0
  89. package/dist/filesystem/permissions.js +39 -0
  90. package/dist/hosts/adapters.js +119 -0
  91. package/dist/hosts/files.js +133 -0
  92. package/dist/hosts/manager.js +150 -0
  93. package/dist/hosts/public-entry.js +47 -0
  94. package/dist/hosts/runtime-command.js +66 -0
  95. package/dist/hosts/runtime.js +139 -0
  96. package/dist/hosts/wizard.js +114 -0
  97. package/dist/knowledge-table.js +38 -0
  98. package/dist/mcp-development.js +69 -0
  99. package/dist/mcp-workspace.js +141 -0
  100. package/dist/memory-presentation.js +182 -0
  101. package/dist/memory-sync-operation.js +88 -0
  102. package/dist/plugins/api.js +2 -0
  103. package/dist/plugins/context.js +20 -0
  104. package/dist/plugins/mcp.js +72 -0
  105. package/dist/plugins/registry.js +106 -0
  106. package/dist/presentation-cards.js +32 -0
  107. package/dist/presentation-server.js +94 -0
  108. package/dist/presentation-trace.js +174 -0
  109. package/dist/presentation.js +443 -0
  110. package/dist/providers/mcp-sampling.js +45 -0
  111. package/dist/server.js +664 -0
  112. package/dist/settings-definition.js +22 -0
  113. package/dist/skill-instructions.js +2 -0
  114. package/dist/status-presentation.js +20 -0
  115. package/dist/text-layout.js +278 -0
  116. package/dist/update-command.js +53 -0
  117. package/dist/update-instructions.js +4 -0
  118. package/dist-ui/index.html +216 -0
  119. package/docs/APPLICABILITY.md +151 -0
  120. package/docs/COMPATIBILITY.md +68 -0
  121. package/docs/CONTEXT-SELECTION-V2.md +61 -0
  122. package/docs/CURATION-APP-REVIEW.md +156 -0
  123. package/docs/CURATION-CLAUSE-REFINEMENT.md +45 -0
  124. package/docs/IMPORTER-CURATION.md +142 -0
  125. package/docs/KNOWLEDGE-AUTHORITY.md +165 -0
  126. package/docs/KNOWLEDGE-SCHEMA-V2.md +150 -0
  127. package/docs/MIGRATIONS.md +103 -0
  128. package/docs/PRIVACY.md +28 -0
  129. package/docs/RECOVERY.md +58 -0
  130. package/docs/RELEASING.md +98 -0
  131. package/docs/hosts.md +152 -0
  132. package/docs/images/prodero-context-ready-cropped.png +0 -0
  133. package/docs/images/prodero-overview-cropped.png +0 -0
  134. package/docs/images/prodero-session-learned-cropped.png +0 -0
  135. package/docs/plugins.md +83 -0
  136. package/docs/ui-themes.md +49 -0
  137. package/instructions/AGENTS.md +39 -0
  138. package/package.json +92 -4
  139. package/plugins/migrate_ai_studio/README.md +101 -0
  140. package/plugins/migrate_ai_studio/index.cjs +396 -0
  141. package/plugins/migrate_ai_studio/instructions/audit.md +25 -0
  142. package/plugins/migrate_ai_studio/instructions/classify.md +41 -0
  143. package/plugins/migrate_ai_studio/instructions/migrate.md +34 -0
  144. package/plugins/migrate_ai_studio/instructions/verify.md +20 -0
  145. package/plugins/migrate_ai_studio/plugin.json +11 -0
  146. package/plugins/migrate_ai_studio/schema.cjs +20 -0
  147. package/plugins/migrate_ai_studio/ui/dist/index.html +77 -0
  148. package/prodero.release.json +5 -0
@@ -0,0 +1,90 @@
1
+ import { readConfig } from './storage.js';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { parseKnowledge } from './memory.js';
5
+ import { assertCompatibleProject, assertLifecycleReadable, knowledgeReadFence, canonicalId, checkedDataPath, exists, sha256 } from './storage.js';
6
+ export const archiveBucket = (at) => {
7
+ if (!Number.isFinite(Date.parse(at)))
8
+ throw new Error('Invalid archive timestamp');
9
+ return new Date(at).toISOString().slice(0, 7).replace('-', '_');
10
+ };
11
+ export const knowledgeArchivePath = (layer, at, id) => {
12
+ canonicalId(id, 'archive');
13
+ return `${layer === 'local' ? 'local/' : ''}archive/${archiveBucket(at)}/${id}.md`;
14
+ };
15
+ /** Enumerate paths only. Ordinary retrieval never loads archive bodies. */
16
+ export async function listArchivePaths(root) {
17
+ await assertLifecycleReadable(root);
18
+ const entries = [];
19
+ for (const layer of ['versioned', 'local']) {
20
+ const base = layer === 'local' ? 'local/archive' : 'archive';
21
+ const directory = await checkedDataPath(root, base);
22
+ if (!await exists(directory))
23
+ continue;
24
+ for (const bucket of (await fs.readdir(directory)).filter(n => /^\d{4}_(0[1-9]|1[0-2])$/.test(n)).sort()) {
25
+ const dir = await checkedDataPath(root, base, bucket);
26
+ for (const name of (await fs.readdir(dir)).filter(n => n.endsWith('.md') && !n.startsWith('.')).sort()) {
27
+ const id = path.basename(name, '.md');
28
+ canonicalId(id, name);
29
+ const relative = `${base}/${bucket}/${name}`;
30
+ await checkedDataPath(root, relative);
31
+ entries.push({ id, path: relative, layer });
32
+ }
33
+ }
34
+ }
35
+ return entries;
36
+ }
37
+ /** Explicit ID/provenance operations, not archive-aware search or ranking. */
38
+ export async function readKnowledgeInventory(root, options = {}) {
39
+ await assertCompatibleProject(root);
40
+ const finishRead = await knowledgeReadFence(root);
41
+ const paths = (await listArchivePaths(root)).map(e => ({ ...e, storage: 'archive' }));
42
+ for (const layer of options.storage === 'archive' ? [] : ['versioned', 'local']) {
43
+ const base = layer === 'local' ? 'local/memory' : 'memory', dir = await checkedDataPath(root, base);
44
+ if (!await exists(dir))
45
+ continue;
46
+ for (const name of (await fs.readdir(dir)).filter(n => n.endsWith('.md') && !n.startsWith('.')).sort())
47
+ paths.push({ path: `${base}/${name}`, layer, id: path.basename(name, '.md'), storage: 'current' });
48
+ }
49
+ const records = [], ids = new Set();
50
+ for (const entry of paths.sort((a, b) => a.path.localeCompare(b.path))) {
51
+ const text = await fs.readFile(await checkedDataPath(root, entry.path), 'utf8'), item = parseKnowledge(text, path.basename(entry.path), (await readConfig(root))?.knowledgeSchemaVersion);
52
+ if (ids.has(item.id))
53
+ throw new Error(`Duplicate knowledge ID across storage locations: ${item.id}`);
54
+ ids.add(item.id);
55
+ if (entry.storage === 'archive' && (!item.archivedAt || knowledgeArchivePath(entry.layer, item.archivedAt, item.id) !== entry.path))
56
+ throw new Error(`Archive metadata/path mismatch: ${item.id}`);
57
+ records.push({ ...entry, item, revision: sha256(text) });
58
+ }
59
+ await finishRead();
60
+ return records;
61
+ }
62
+ export function provenanceEdges(items) {
63
+ const edges = new Map();
64
+ const add = (from, to, type) => edges.set(`${from}\0${type}\0${to}`, { from, to, type });
65
+ for (const item of items) {
66
+ for (const old of item.supersedes)
67
+ add(item.id, old, 'supersedes');
68
+ for (const r of item.relations ?? []) {
69
+ if (r.type === 'supersedes' || r.type === 'consolidates')
70
+ add(item.id, r.target, r.type);
71
+ if (r.type === 'superseded_by')
72
+ add(r.target, item.id, 'supersedes');
73
+ if (r.type === 'consolidated_into')
74
+ add(r.target, item.id, 'consolidates');
75
+ }
76
+ }
77
+ return [...edges.values()].sort((a, b) => a.from.localeCompare(b.from) || a.type.localeCompare(b.type) || a.to.localeCompare(b.to));
78
+ }
79
+ export async function getKnowledgeProvenance(root, id) {
80
+ canonicalId(id, 'knowledge lookup');
81
+ const records = await readKnowledgeInventory(root), record = records.find(r => r.item.id === id);
82
+ if (!record)
83
+ throw new Error(`Knowledge ${id} was not found`);
84
+ const edges = provenanceEdges(records.map(r => r.item)), incoming = edges.filter(e => e.to === id), outgoing = edges.filter(e => e.from === id);
85
+ const supersededBy = incoming.filter(e => e.type === 'supersedes').map(e => e.from), consolidatedInto = incoming.filter(e => e.type === 'consolidates').map(e => e.from);
86
+ return { ...record, current: record.storage === 'current' && record.item.status === 'active' && !incoming.length,
87
+ supersedes: outgoing.filter(e => e.type === 'supersedes').map(e => e.to), superseded_by: supersededBy,
88
+ consolidates: outgoing.filter(e => e.type === 'consolidates').map(e => e.to), consolidated_into: consolidatedInto };
89
+ }
90
+ //# sourceMappingURL=knowledge-provenance.js.map
@@ -0,0 +1,85 @@
1
+ import { sameApplicability } from "./applicability.js";
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { parseKnowledge, renderKnowledge } from './memory.js';
5
+ import { checkedDataPath } from './storage.js';
6
+ import { sha256 } from './storage.js';
7
+ import { evaluateKnowledgeTransition, inferredKnowledge, protectedKnowledge, sameProtection, coversProtection, KnowledgePolicyReviewRequired, summarizePolicyDecisions } from './knowledge-authority.js';
8
+ import { loadMemoryCatalog } from './memory-index.js';
9
+ const canonical = (p) => /^(memory|local\/memory|(local\/)?archive\/\d{4}_\d{2})\//.test(p);
10
+ /** Policy validation precedes the durable journal. Recovery replays the already
11
+ * decided journal; it must not re-authorize a past decision from persisted prose. */
12
+ export async function protectKnowledgeWrites(root, writes, exact = false, moves = []) {
13
+ writes = writes.map(w => { if (!canonical(w.path))
14
+ return w; const item = parseKnowledge(w.after, path.basename(w.path)); return { ...w, after: renderKnowledge({ ...item, version: 3, applicability: item.applicability ?? { kind: 'legacy' } }) }; });
15
+ const proposed = writes.filter(w => canonical(w.path));
16
+ if (!proposed.length)
17
+ return { writes, policy: { ...summarizePolicyDecisions([]), createdIds: undefined, supersededIds: undefined, preservedIds: [] } };
18
+ const parsed = new Map(proposed.map(w => [w.path, parseKnowledge(w.after, path.basename(w.path), 3)]));
19
+ const catalog = await loadMemoryCatalog(root);
20
+ const ids = new Set([...parsed.values()].flatMap(i => [i.id, ...i.supersedes, ...i.related, ...(i.relations ?? []).map(r => r.target)]));
21
+ const current = [];
22
+ for (const id of ids)
23
+ if (catalog.summaries.some(s => s.id === id))
24
+ current.push(await catalog.load(id));
25
+ const byId = new Map(current.map(i => [i.id, i]));
26
+ const decisions = [], safe = new Map(), forks = [];
27
+ const sourcesFor = (i) => current.filter(s => i.supersedes.includes(s.id) || i.related.includes(s.id) || i.id === s.id || (i.relations ?? []).some(r => ['supersedes', 'consolidates'].includes(r.type) && r.target === s.id) || sameProtection(s, i));
28
+ // Decide new records first, so a retirement cannot rely on a rejected replacement.
29
+ for (const w of proposed)
30
+ if (w.before === null) {
31
+ const i = parsed.get(w.path);
32
+ const move = moves.find(m => m.to === w.path);
33
+ const original = move ? parseKnowledge(await fs.readFile(await checkedDataPath(root, move.from), 'utf8'), path.basename(move.from), 3) : undefined;
34
+ const retirementCovered = !!original && [...parsed.values()].some(other => other.id !== i.id && other.status === 'active' && coversProtection(original, other) && evaluateKnowledgeTransition(root, other, byId.get(other.id), { sources: sourcesFor(other) }).decision === 'allowed');
35
+ const d = evaluateKnowledgeTransition(root, i, original, { sources: sourcesFor(i), exact: exact || !!move, retirementCovered });
36
+ decisions.push(d);
37
+ if (d.blocking)
38
+ throw new KnowledgePolicyReviewRequired(d);
39
+ if (d.decision !== 'allowed') {
40
+ d.item.supersedes = d.item.supersedes.filter(id => !protectedKnowledge(byId.get(id) ?? i));
41
+ d.item.relations = (d.item.relations ?? []).filter(r => !['supersedes', 'consolidates', 'consolidated_into', 'superseded_by'].includes(r.type));
42
+ }
43
+ d.item.supersedes = d.item.supersedes.filter(id => { const source = byId.get(id); return !source || !protectedKnowledge(source) || coversProtection(source, d.item) || d.reasons.includes('trusted-current-operation'); });
44
+ d.item.relations = (d.item.relations ?? []).filter(r => { const source = byId.get(r.target); return !['supersedes', 'consolidates'].includes(r.type) || !source || !protectedKnowledge(source) || coversProtection(source, d.item) || d.reasons.includes('trusted-current-operation'); });
45
+ safe.set(w.path, d.item);
46
+ }
47
+ for (const w of proposed)
48
+ if (w.before !== null) {
49
+ const before = parseKnowledge(await fs.readFile(await checkedDataPath(root, w.path), 'utf8'), path.basename(w.path), 3), i = parsed.get(w.path);
50
+ const retirementCovered = [...safe.values(), ...current.filter(c => !proposed.some(w => parsed.get(w.path).id === c.id)), ...proposed.filter(w => w.before !== null).map(w => parsed.get(w.path)).filter(c => evaluateKnowledgeTransition(root, c, byId.get(c.id), { sources: sourcesFor(c) }).decision === 'allowed')].some(other => other.id !== i.id && other.status === 'active' && coversProtection(before, other) && (other.supersedes.includes(i.id) || other.related.includes(i.id) || (i.related ?? []).includes(other.id) || (other.relations ?? []).some(r => r.target === i.id && r.type === 'consolidates')));
51
+ const replacementRequested = [...parsed.values()].some(other => other.id !== i.id && (other.supersedes.includes(i.id) || (other.relations ?? []).some(r => ["supersedes", "consolidates"].includes(r.type) && r.target === i.id)) && !sameApplicability(other, before));
52
+ const d = evaluateKnowledgeTransition(root, i, before, { exact, retirementCovered, replacementRequested, sources: sourcesFor(i) });
53
+ decisions.push(d);
54
+ if (d.blocking)
55
+ throw new KnowledgePolicyReviewRequired(d);
56
+ if (d.decision !== 'allowed' && i.content !== before.content) {
57
+ const fork = evaluateKnowledgeTransition(root, inferredKnowledge({ ...i, id: `mem_policy_${sha256(JSON.stringify(i)).slice(0, 24)}`, status: 'active', supersedes: [], related: [before.id], relations: [{ type: 'related_to', target: before.id }] }), undefined, { sources: [before] }).item;
58
+ forks.push({ path: `local/memory/${fork.id}.md`, before: null, after: renderKnowledge(fork) });
59
+ }
60
+ d.item.supersedes = d.item.supersedes.filter(id => { const source = byId.get(id); return before.supersedes.includes(id) || !source || !protectedKnowledge(source) || coversProtection(source, d.item) || d.reasons.includes('trusted-current-operation'); });
61
+ d.item.relations = (d.item.relations ?? []).filter(r => { const source = byId.get(r.target); return (before.relations ?? []).some(old => old.type === r.type && old.target === r.target) || !['supersedes', 'consolidates'].includes(r.type) || !source || !protectedKnowledge(source) || coversProtection(source, d.item) || d.reasons.includes('trusted-current-operation'); });
62
+ safe.set(w.path, d.item);
63
+ }
64
+ const policy = summarizePolicyDecisions(decisions);
65
+ const preservedIds = decisions.filter(d => d.decision === 'review_recommended').map(d => d.item.id);
66
+ const result = writes.map(w => safe.has(w.path) ? { ...w, after: renderKnowledge(safe.get(w.path)) } : w);
67
+ const checkpoint = result.find(w => w.path.startsWith('checkpoints/'));
68
+ if (checkpoint) {
69
+ const c = JSON.parse(checkpoint.after);
70
+ c.policyTransitions = policy;
71
+ c.updatedMemory = (c.updatedMemory ?? []).filter((id) => !preservedIds.includes(id));
72
+ c.createdMemory = [...new Set([...(c.createdMemory ?? []), ...forks.map(w => path.basename(w.path, '.md'))])];
73
+ c.supersededMemory = (c.supersededMemory ?? []).filter((id) => [...safe.values()].some(i => i.id === id && i.status === 'superseded'));
74
+ if (c.summary) {
75
+ c.summary.consolidated = Math.max(0, (c.summary.consolidated ?? 0) - preservedIds.length);
76
+ c.summary.created = c.createdMemory.length;
77
+ c.summary.superseded = c.supersededMemory.length;
78
+ }
79
+ checkpoint.after = JSON.stringify(c, null, 2) + '\n';
80
+ }
81
+ result.splice(Math.max(0, result.length - 1), 0, ...forks);
82
+ const committed = checkpoint ? JSON.parse(checkpoint.after) : null;
83
+ return { writes: result, policy: { ...policy, preservedIds, createdIds: committed?.createdMemory, supersededIds: committed?.supersededMemory } };
84
+ }
85
+ //# sourceMappingURL=knowledge-write-policy.js.map
@@ -0,0 +1,14 @@
1
+ const CODE = /```[\s\S]*?```|`[^`]*`/g;
2
+ /** Durable prose is English; identifiers and code literals may retain their source spelling. */
3
+ export function assertEnglishProse(values, subject = "Durable Prodero prose") {
4
+ for (const value of values) {
5
+ if (!value)
6
+ continue;
7
+ const prose = value.replace(CODE, "");
8
+ for (const character of prose) {
9
+ if (/\p{L}/u.test(character) && !/\p{Script=Latin}/u.test(character))
10
+ throw new Error(`${subject} must be written in English; wrap exact non-English identifiers or code literals in backticks.`);
11
+ }
12
+ }
13
+ }
14
+ //# sourceMappingURL=language.js.map
@@ -0,0 +1,76 @@
1
+ import fs from 'node:fs/promises';
2
+ import { modeSchema, themeSchema } from '../settings-definition.js';
3
+ import { assertSafePath, projectDataPath, ensureSelfIgnoredPrivateDirectory, writeFileAtomic } from './storage.js';
4
+ import { ownsSyncLock } from './sync-transaction.js';
5
+ export const LOCAL_PREFERENCES_VERSION = 1;
6
+ const relativePath = 'local/preferences.json';
7
+ const object = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
8
+ // Extensions may round-trip, but a local file cannot masquerade as shared project policy.
9
+ const sharedKeys = new Set(['context', 'knowledgeEngine', 'schemaVersion', 'knowledgeSchemaVersion', 'initializedWith', 'lastUpdatedWith', 'projectId', 'createdAt', 'contextPolicy', 'authority', '__proto__', 'constructor', 'prototype']);
10
+ const forbidden = (key) => sharedKeys.has(key.split('.')[0]);
11
+ const diagnostic = (code, cause) => ({ code, cause,
12
+ recovery: 'Preserve .prodero/local/preferences.json; repair it explicitly or use a compatible Curator before saving preferences.' });
13
+ /** Read-only: no creation, legacy copy, permission changes, recovery or cleanup. */
14
+ export async function readLocalPreferences(root) {
15
+ const fallback = { presentation: {}, diagnostics: [] };
16
+ let raw;
17
+ try {
18
+ const target = projectDataPath(root, relativePath);
19
+ await assertSafePath(target);
20
+ const stat = await fs.lstat(target);
21
+ if (!stat.isFile() || stat.nlink !== 1)
22
+ throw new Error('unsafe preference file');
23
+ raw = JSON.parse(await fs.readFile(target, 'utf8'));
24
+ }
25
+ catch (error) {
26
+ if (error.code === 'ENOENT')
27
+ return fallback;
28
+ return { ...fallback, diagnostics: [diagnostic('PRODERO_LOCAL_PREFERENCES_UNAVAILABLE', 'Local preferences could not be safely read. Legacy project defaults are used.')] };
29
+ }
30
+ if (!object(raw) || raw.version !== LOCAL_PREFERENCES_VERSION) {
31
+ const newer = object(raw) && Number.isInteger(raw.version) && Number(raw.version) > LOCAL_PREFERENCES_VERSION;
32
+ return { ...fallback, diagnostics: [diagnostic(newer ? 'PRODERO_LOCAL_PREFERENCES_NEWER' : 'PRODERO_LOCAL_PREFERENCES_INVALID', newer ? 'Local preferences require a newer Curator. Legacy project defaults are used.' : 'Local preferences have an invalid format. Legacy project defaults are used.')] };
33
+ }
34
+ if (Object.keys(raw).some(forbidden) || (raw.presentation !== undefined && (!object(raw.presentation) || Object.keys(raw.presentation).some(forbidden)))) {
35
+ return { ...fallback, diagnostics: [diagnostic('PRODERO_LOCAL_PREFERENCES_INVALID', 'Local preferences must contain presentation overrides only; shared policy or identity overrides are not supported.')] };
36
+ }
37
+ const presentation = {}, diagnostics = [];
38
+ for (const [key, schema] of [['mode', modeSchema], ['theme', themeSchema]]) {
39
+ const value = raw.presentation?.[key];
40
+ if (value === undefined)
41
+ continue;
42
+ const parsed = schema.safeParse(value);
43
+ if (parsed.success)
44
+ Object.assign(presentation, { [key]: parsed.data });
45
+ else
46
+ diagnostics.push(diagnostic('PRODERO_LOCAL_PREFERENCES_INVALID', `Local presentation.${key} is invalid; its legacy project default is used.`));
47
+ }
48
+ // Valid sibling fields can resolve, but writes never overwrite a damaged document.
49
+ return { presentation, diagnostics, ...(diagnostics.length ? {} : { document: raw }) };
50
+ }
51
+ /** Caller owns the shared Sync/migration lock and has checked canonical write gates. */
52
+ export async function saveLocalPreference(root, key, value, reset) {
53
+ if (!ownsSyncLock(root))
54
+ throw new Error('Local preferences require the shared project writer lock.');
55
+ if (key !== 'mode' && key !== 'theme')
56
+ throw new Error('Only presentation mode and theme may be saved as local preferences.');
57
+ if (reset && value !== undefined)
58
+ throw new Error('Reset requires no value.');
59
+ const parsed = reset ? undefined : (key === 'mode' ? modeSchema : themeSchema).parse(value);
60
+ const current = await readLocalPreferences(root);
61
+ if (current.diagnostics.length) {
62
+ const issue = current.diagnostics[0];
63
+ throw new Error(`${issue.code}: ${issue.cause} ${issue.recovery}`);
64
+ }
65
+ if (reset && current.document?.presentation?.[key] === undefined)
66
+ return;
67
+ const document = current.document ?? { version: LOCAL_PREFERENCES_VERSION };
68
+ const presentation = { ...document.presentation };
69
+ if (reset)
70
+ delete presentation[key];
71
+ else
72
+ presentation[key] = parsed;
73
+ await ensureSelfIgnoredPrivateDirectory(root, 'local');
74
+ await writeFileAtomic(projectDataPath(root, relativePath), `${JSON.stringify({ ...document, presentation }, null, 2)}\n`);
75
+ }
76
+ //# sourceMappingURL=local-preferences.js.map
@@ -0,0 +1,6 @@
1
+ const STOP_WORDS = new Set(["a", "an", "and", "are", "as", "at", "be", "for", "from", "in", "is", "it", "must", "of", "on", "or", "the", "to", "use", "with"]);
2
+ export function tokens(value) {
3
+ return new Set(value.toLowerCase().replace(/[`*_]/g, "").match(/[\p{L}\p{N}]+(?:[._/-][\p{L}\p{N}]+)*/gu)?.filter((token) => token.length > 1 && !STOP_WORDS.has(token)) ?? []);
4
+ }
5
+ export function normalized(value) { return [...tokens(value)].sort().join(" "); }
6
+ //# sourceMappingURL=matching.js.map
@@ -0,0 +1,241 @@
1
+ import { applicabilitySchema, validateApplicability, scopeProjection, sameApplicability } from "./applicability.js";
2
+ import { knowledgePolicy, CONTEXT_POLICIES, KNOWLEDGE_AUTHORITIES } from "./knowledge-policy.js";
3
+ import { knowledgeReadFence } from "./storage.js";
4
+ import { assertNoSecrets } from "./secrets.js";
5
+ import { z } from "zod";
6
+ import fs from "node:fs/promises";
7
+ import { normalized, tokens } from "./matching.js";
8
+ import { parseKnowledge } from "./memory.js";
9
+ import { estimateTokens, knowledgeText, MAX_CANONICAL_ITEM_TOKENS, STALE_STATE_AFTER_DAYS } from "./telemetry.js";
10
+ import { ensureDerivedStateRoot, exists, projectDataPath, checkedDataPath, sha256, writeFileAtomic, readConfig } from "./storage.js";
11
+ export const MAX_RELATED_MEMORY_CANDIDATES = 24;
12
+ const DAY_MS = 86_400_000;
13
+ const PRIORITY_SCORE = { critical: 40, important: 30, normal: 20, contextual: 10 };
14
+ export function isStaleSummary(item, now = new Date()) {
15
+ if (item.status !== "active" || item.category !== "state" || item.priority === "critical")
16
+ return false;
17
+ const updated = Date.parse(item.updatedAt);
18
+ return Number.isFinite(updated) && now.getTime() - updated >= STALE_STATE_AFTER_DAYS * DAY_MS;
19
+ }
20
+ export function memoryHealth(summaries, now = new Date()) {
21
+ const active = summaries.filter((item) => item.status === "active");
22
+ const disputedItems = summaries.filter((item) => item.status === "disputed").length;
23
+ const staleActiveItems = active.filter((item) => isStaleSummary(item, now)).length;
24
+ // Compatibility counter: incomplete source/observation metadata, not evidence
25
+ // that source history was deleted. Do not infer or rewrite provenance here.
26
+ const missingProvenanceItems = active.filter((item) => (item.sourceSessionCount === 0 && (item.sourceEvidenceCount ?? 0) === 0) || item.derivedFromCount === 0).length;
27
+ const oversizedActiveItems = active.filter((item) => item.estimatedTokens > MAX_CANONICAL_ITEM_TOKENS).length;
28
+ return {
29
+ activeItems: active.length,
30
+ disputedItems,
31
+ historicalItems: summaries.length - active.length - disputedItems,
32
+ staleActiveItems,
33
+ missingProvenanceItems,
34
+ oversizedActiveItems,
35
+ status: disputedItems || missingProvenanceItems || oversizedActiveItems ? "attention" : "healthy",
36
+ };
37
+ }
38
+ function stamp(stat) { return { size: Number(stat.size), mtimeMs: Number(stat.mtimeMs), ctimeMs: Number(stat.ctimeMs) }; }
39
+ function sameStamp(left, right) { return Boolean(left && left.size === right.size && left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs); }
40
+ function summary(item, filename, relativePath, layer, fileStamp) {
41
+ return {
42
+ ...fileStamp,
43
+ id: item.id,
44
+ filename,
45
+ relativePath,
46
+ layer,
47
+ scope: item.scope,
48
+ scopeRef: item.scopeRef,
49
+ ...(item.scopeKind ? { scopeKind: item.scopeKind } : {}),
50
+ version: item.version ?? 3,
51
+ ...(item.applicability ? { applicability: item.applicability } : {}),
52
+ contextPolicy: knowledgePolicy(item).contextPolicy,
53
+ authority: knowledgePolicy(item).authority,
54
+ category: item.category,
55
+ priority: item.priority,
56
+ status: item.status,
57
+ title: item.title,
58
+ normalizedTitle: normalized(item.title),
59
+ normalizedContent: normalized(item.content),
60
+ tokens: [...tokens(`${item.title} ${item.content} ${item.scopeRef ?? ""}`)].sort(),
61
+ contentChars: item.content.length,
62
+ estimatedTokens: estimateTokens(knowledgeText(item)),
63
+ updatedAt: item.updatedAt,
64
+ sourceSessionCount: item.sourceSessions.length,
65
+ derivedFromCount: item.derivedFrom.length,
66
+ sourceEvidenceCount: item.evidence?.filter(e => e.kind === "source_file" && typeof e.ref === "string" && e.ref.length > 0).length ?? 0,
67
+ related: item.related,
68
+ supersedes: item.supersedes,
69
+ sourceSessions: item.sourceSessions,
70
+ relations: item.relations ?? [],
71
+ };
72
+ }
73
+ const summarySchema = z.object({
74
+ version: z.union([z.literal(1), z.literal(2), z.literal(3)]),
75
+ applicability: applicabilitySchema.optional(),
76
+ contextPolicy: z.enum(CONTEXT_POLICIES), authority: z.enum(KNOWLEDGE_AUTHORITIES),
77
+ size: z.number().nonnegative(), mtimeMs: z.number().finite(), ctimeMs: z.number().finite(),
78
+ id: z.string().regex(/^[a-zA-Z0-9_-]+$/), filename: z.string(), relativePath: z.string(), layer: z.enum(["local", "versioned"]),
79
+ scope: z.enum(["global", "module", "task"]), scopeRef: z.string().nullable(), scopeKind: z.enum(["path", "topic"]).optional(),
80
+ category: z.enum(["principle", "invariant", "decision", "constraint", "lesson", "state", "instruction"]),
81
+ priority: z.enum(["critical", "important", "normal", "contextual"]), status: z.enum(["active", "superseded", "deprecated", "consolidated", "duplicate", "disputed"]),
82
+ title: z.string(), normalizedTitle: z.string(), normalizedContent: z.string(), tokens: z.array(z.string()),
83
+ contentChars: z.number().nonnegative(), estimatedTokens: z.number().nonnegative(), updatedAt: z.string(),
84
+ sourceSessionCount: z.number().nonnegative(), derivedFromCount: z.number().nonnegative(), sourceEvidenceCount: z.number().nonnegative(),
85
+ related: z.array(z.string()), supersedes: z.array(z.string()), sourceSessions: z.array(z.string()),
86
+ relations: z.array(z.object({ type: z.enum(["related_to", "depends_on", "implements", "applies_to", "supersedes", "superseded_by", "consolidates", "consolidated_into", "contradicts", "learned_from"]), target: z.string() })),
87
+ }).superRefine((entry, ctx) => {
88
+ if (entry.version !== 3)
89
+ return;
90
+ try {
91
+ const applicability = validateApplicability(entry.applicability, entry.contextPolicy);
92
+ if (applicability.kind !== 'legacy') {
93
+ const projection = scopeProjection(applicability);
94
+ if (entry.scope !== projection.scope || entry.scopeRef !== projection.scopeRef || entry.scopeKind !== projection.scopeKind)
95
+ throw new Error('Invalid applicability projection');
96
+ }
97
+ }
98
+ catch {
99
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Invalid v3 applicability cache entry' });
100
+ }
101
+ });
102
+ const memoryCacheSchema = z.object({ version: z.literal(10), entries: z.record(summarySchema) });
103
+ async function readCache(root, persistCache) {
104
+ if (persistCache)
105
+ await ensureDerivedStateRoot(root);
106
+ const target = await checkedDataPath(root, ".cache", "memory-index.json");
107
+ if (!(await exists(target)))
108
+ return { cache: { version: 10, entries: {} }, state: "missing" };
109
+ try {
110
+ const parsed = memoryCacheSchema.parse(JSON.parse(await fs.readFile(target, "utf8")));
111
+ assertNoSecrets(parsed);
112
+ if (Object.entries(parsed.entries).every(([key, e]) => key === e.relativePath && key === `${e.layer === "local" ? "local/memory" : "memory"}/${e.filename}` && /^[a-zA-Z0-9_-]+\.md$/.test(e.filename)))
113
+ return { cache: parsed, state: "warm" };
114
+ }
115
+ catch {
116
+ // Disposable derived JSON is rebuilt below; canonical Markdown is never repaired here.
117
+ }
118
+ if (persistCache)
119
+ await fs.rm(target, { force: true });
120
+ return { cache: { version: 10, entries: {} }, state: "invalid" };
121
+ }
122
+ export class MemoryCatalog {
123
+ root;
124
+ summaries;
125
+ filesScanned;
126
+ cacheState;
127
+ bodiesIndexed;
128
+ bodiesLoaded = 0;
129
+ staged = new Map();
130
+ originalHashes = new Map();
131
+ byId;
132
+ constructor(root, summaries, filesScanned, bodiesIndexed, cacheState) {
133
+ this.root = root;
134
+ this.summaries = summaries;
135
+ this.filesScanned = filesScanned;
136
+ this.bodiesIndexed = bodiesIndexed;
137
+ this.cacheState = cacheState;
138
+ this.byId = new Map(summaries.map((entry) => [entry.id, entry]));
139
+ }
140
+ selectRelated(query, limit = MAX_RELATED_MEMORY_CANDIDATES) {
141
+ const queryTokens = tokens(`${query.title} ${query.content} ${query.scopeRef ?? ""}`);
142
+ const explicit = new Set(query.explicitIds);
143
+ const shadowed = new Set(this.summaries.filter(m => m.status === "active" || m.status === "disputed").flatMap(m => m.supersedes));
144
+ return this.summaries
145
+ .filter(entry => explicit.has(entry.id) || !shadowed.has(entry.id))
146
+ .filter((entry) => (entry.status === "active" || entry.status === "disputed") && (explicit.has(entry.id) || (entry.scope === query.scope && entry.scopeRef === query.scopeRef && entry.scopeKind === query.scopeKind && entry.category === query.category && sameApplicability(entry, query))))
147
+ .map((entry) => {
148
+ const intersection = entry.tokens.filter((token) => queryTokens.has(token)).length;
149
+ let score = intersection * 10 + PRIORITY_SCORE[entry.priority];
150
+ if (explicit.has(entry.id))
151
+ score += 10_000;
152
+ const queryTitle = normalized(query.title);
153
+ const queryContent = normalized(query.content);
154
+ if (queryTitle && entry.normalizedTitle === queryTitle)
155
+ score += 1_000;
156
+ if (queryContent && entry.normalizedContent === queryContent)
157
+ score += 2_000;
158
+ if (entry.scopeRef && query.scopeRef === entry.scopeRef)
159
+ score += 100;
160
+ return { entry, score };
161
+ })
162
+ .filter(({ score }) => score > 0)
163
+ .sort((a, b) => b.score - a.score || a.entry.id.localeCompare(b.entry.id))
164
+ .slice(0, Math.max(1, limit))
165
+ .map(({ entry }) => entry);
166
+ }
167
+ async load(id) {
168
+ const entry = this.byId.get(id);
169
+ if (!entry)
170
+ throw new Error(`Canonical Prodero memory ${id} was not found.`);
171
+ this.bodiesLoaded += 1;
172
+ const staged = this.staged.get(id);
173
+ if (staged)
174
+ return structuredClone(staged);
175
+ const finishRead = await knowledgeReadFence(this.root);
176
+ const target = await checkedDataPath(this.root, entry.relativePath);
177
+ if (!sameStamp(entry, stamp(await fs.stat(target))))
178
+ throw new Error("Knowledge changed after context selection; retry");
179
+ const text = await fs.readFile(target, "utf8");
180
+ await finishRead();
181
+ this.originalHashes.set(id, sha256(text));
182
+ return parseKnowledge(text, entry.filename, (await readConfig(this.root))?.knowledgeSchemaVersion ?? 1);
183
+ }
184
+ /** In-memory overlay for a Sync plan; never persist virtual stamps in the derived cache. */
185
+ stage(item, layer = "local") {
186
+ const filename = `${item.id}.md`;
187
+ const relativePath = layer === "versioned" ? `memory/${filename}` : `local/memory/${filename}`;
188
+ const entry = summary(item, filename, relativePath, layer, { size: 0, mtimeMs: 0, ctimeMs: 0 });
189
+ const index = this.summaries.findIndex(candidate => candidate.id === item.id);
190
+ if (index >= 0)
191
+ this.summaries.splice(index, 1, entry);
192
+ else
193
+ this.summaries.push(entry);
194
+ this.byId.set(item.id, entry);
195
+ this.staged.set(item.id, structuredClone({ ...item, ...knowledgePolicy(item) }));
196
+ }
197
+ async save() {
198
+ if (this.staged.size)
199
+ throw new Error("Cannot persist an uncommitted knowledge plan as cache");
200
+ await ensureDerivedStateRoot(this.root);
201
+ const entries = Object.fromEntries(this.summaries.sort((a, b) => a.relativePath.localeCompare(b.relativePath)).map((entry) => [entry.relativePath, entry]));
202
+ await writeFileAtomic(projectDataPath(this.root, ".cache", "memory-index.json"), `${JSON.stringify({ version: 10, entries })}\n`);
203
+ }
204
+ }
205
+ export async function loadMemoryCatalog(root, options = {}) {
206
+ const finishRead = await knowledgeReadFence(root);
207
+ const { cache, state } = await readCache(root, options.persistCache !== false);
208
+ const summaries = [];
209
+ const schema = (await readConfig(root))?.knowledgeSchemaVersion ?? 1;
210
+ let bodiesIndexed = 0;
211
+ for (const layer of ["versioned", "local"]) {
212
+ const relativeDirectory = layer === "versioned" ? "memory" : "local/memory";
213
+ const directory = await checkedDataPath(root, ...relativeDirectory.split("/"));
214
+ if (!(await exists(directory)))
215
+ continue;
216
+ const names = (await fs.readdir(directory)).filter((name) => name.endsWith(".md") && !name.startsWith(".")).sort();
217
+ for (const name of names) {
218
+ const target = await checkedDataPath(root, relativeDirectory, name);
219
+ const stat = await fs.lstat(target);
220
+ if (stat.isSymbolicLink())
221
+ throw new Error(`Symlink in canonical ${layer} memory: ${name}`);
222
+ const fileStamp = stamp(stat);
223
+ const relativePath = `${relativeDirectory}/${name}`;
224
+ let entry = cache.entries[relativePath];
225
+ if (!sameStamp(entry, fileStamp) || (schema >= 2 && entry.version !== schema)) {
226
+ entry = summary(parseKnowledge(await fs.readFile(target, "utf8"), name, schema), name, relativePath, layer, fileStamp);
227
+ bodiesIndexed += 1;
228
+ }
229
+ if (summaries.some((candidate) => candidate.id === entry.id))
230
+ throw new Error(`Knowledge id ${entry.id} exists in both Local and Versioned Knowledge.`);
231
+ summaries.push(entry);
232
+ }
233
+ }
234
+ const cacheState = state === "warm" && bodiesIndexed > 0 ? "stale" : state;
235
+ const catalog = new MemoryCatalog(root, summaries, summaries.length, bodiesIndexed, cacheState);
236
+ await finishRead();
237
+ if (options.persistCache !== false)
238
+ await catalog.save();
239
+ return catalog;
240
+ }
241
+ //# sourceMappingURL=memory-index.js.map