@prodero/curator 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/ARCHITECTURE.md +298 -0
  2. package/CHANGELOG.md +16 -0
  3. package/CONTRIBUTING.md +9 -0
  4. package/LICENSE +21 -0
  5. package/README.md +94 -2
  6. package/SECURITY.md +7 -0
  7. package/bin/prodero +7 -0
  8. package/dist/cli.js +228 -0
  9. package/dist/context-instructions.js +2 -0
  10. package/dist/contracts.js +50 -0
  11. package/dist/conversation-presentation.js +262 -0
  12. package/dist/core/agent-instructions.js +39 -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 +105 -0
  24. package/dist/core/curation-review-presentation.js +4 -0
  25. package/dist/core/curation-review-state.js +62 -0
  26. package/dist/core/curation-review.js +573 -0
  27. package/dist/core/curation-revision.js +106 -0
  28. package/dist/core/curation-semantic-contract.js +239 -0
  29. package/dist/core/curation-semantic-jobs.js +280 -0
  30. package/dist/core/diagnostics.js +17 -0
  31. package/dist/core/format.js +114 -0
  32. package/dist/core/ids.js +6 -0
  33. package/dist/core/imports.js +169 -0
  34. package/dist/core/index.js +93 -0
  35. package/dist/core/instruction-review.js +86 -0
  36. package/dist/core/integrity-files.js +174 -0
  37. package/dist/core/intelligence.js +95 -0
  38. package/dist/core/knowledge-analysis.js +46 -0
  39. package/dist/core/knowledge-authority.js +143 -0
  40. package/dist/core/knowledge-curation.js +316 -0
  41. package/dist/core/knowledge-engine.js +475 -0
  42. package/dist/core/knowledge-graph.js +191 -0
  43. package/dist/core/knowledge-lifecycle.js +212 -0
  44. package/dist/core/knowledge-management.js +113 -0
  45. package/dist/core/knowledge-migration.js +84 -0
  46. package/dist/core/knowledge-policy.js +38 -0
  47. package/dist/core/knowledge-provenance.js +90 -0
  48. package/dist/core/knowledge-write-policy.js +85 -0
  49. package/dist/core/language.js +14 -0
  50. package/dist/core/matching.js +6 -0
  51. package/dist/core/memory-index.js +239 -0
  52. package/dist/core/memory-intelligence.js +288 -0
  53. package/dist/core/memory-link-repair.js +66 -0
  54. package/dist/core/memory-operations.js +122 -0
  55. package/dist/core/memory-pressure.js +212 -0
  56. package/dist/core/memory-sync-curator.js +62 -0
  57. package/dist/core/memory-sync.js +463 -0
  58. package/dist/core/memory-transformation-bundle.js +163 -0
  59. package/dist/core/memory-transformation-proposal.js +135 -0
  60. package/dist/core/memory-transformation.js +108 -0
  61. package/dist/core/memory.js +186 -0
  62. package/dist/core/migrations.js +569 -0
  63. package/dist/core/path-scope.js +27 -0
  64. package/dist/core/plugin-files.js +116 -0
  65. package/dist/core/project-root.js +39 -0
  66. package/dist/core/project.js +634 -0
  67. package/dist/core/promotion.js +279 -0
  68. package/dist/core/reconciliation.js +61 -0
  69. package/dist/core/secrets.js +67 -0
  70. package/dist/core/semantic-safety.js +33 -0
  71. package/dist/core/session-index.js +112 -0
  72. package/dist/core/session-review.js +150 -0
  73. package/dist/core/sessions.js +289 -0
  74. package/dist/core/settings.js +76 -0
  75. package/dist/core/skill-curation.js +239 -0
  76. package/dist/core/skill-polish.js +41 -0
  77. package/dist/core/skills.js +368 -0
  78. package/dist/core/storage.js +408 -0
  79. package/dist/core/sync-transaction.js +254 -0
  80. package/dist/core/telemetry.js +64 -0
  81. package/dist/core/templates.js +53 -0
  82. package/dist/core/types.js +2 -0
  83. package/dist/core/update.js +320 -0
  84. package/dist/curation-review-adapter.js +92 -0
  85. package/dist/diagnostic-boundary.js +54 -0
  86. package/dist/filesystem/permissions.js +39 -0
  87. package/dist/hosts/adapters.js +119 -0
  88. package/dist/hosts/files.js +133 -0
  89. package/dist/hosts/manager.js +150 -0
  90. package/dist/hosts/public-entry.js +47 -0
  91. package/dist/hosts/runtime-command.js +66 -0
  92. package/dist/hosts/runtime.js +139 -0
  93. package/dist/hosts/wizard.js +114 -0
  94. package/dist/knowledge-table.js +38 -0
  95. package/dist/mcp-development.js +69 -0
  96. package/dist/mcp-workspace.js +141 -0
  97. package/dist/memory-presentation.js +172 -0
  98. package/dist/plugins/api.js +2 -0
  99. package/dist/plugins/context.js +20 -0
  100. package/dist/plugins/mcp.js +72 -0
  101. package/dist/plugins/registry.js +106 -0
  102. package/dist/presentation-cards.js +32 -0
  103. package/dist/presentation-server.js +79 -0
  104. package/dist/presentation-trace.js +174 -0
  105. package/dist/presentation.js +412 -0
  106. package/dist/providers/mcp-sampling.js +45 -0
  107. package/dist/server.js +656 -0
  108. package/dist/settings-definition.js +22 -0
  109. package/dist/skill-instructions.js +2 -0
  110. package/dist-ui/index.html +214 -0
  111. package/docs/APPLICABILITY.md +151 -0
  112. package/docs/COMPATIBILITY.md +49 -0
  113. package/docs/CONTEXT-SELECTION-V2.md +61 -0
  114. package/docs/CURATION-APP-REVIEW.md +156 -0
  115. package/docs/CURATION-CLAUSE-REFINEMENT.md +45 -0
  116. package/docs/IMPORTER-CURATION.md +142 -0
  117. package/docs/KNOWLEDGE-AUTHORITY.md +165 -0
  118. package/docs/KNOWLEDGE-SCHEMA-V2.md +150 -0
  119. package/docs/MIGRATIONS.md +99 -0
  120. package/docs/PRIVACY.md +28 -0
  121. package/docs/RECOVERY.md +56 -0
  122. package/docs/RELEASING.md +40 -0
  123. package/docs/hosts.md +134 -0
  124. package/docs/plugins.md +83 -0
  125. package/docs/ui-themes.md +43 -0
  126. package/instructions/AGENTS.md +33 -0
  127. package/package.json +88 -4
  128. package/plugins/migrate_ai_studio/README.md +101 -0
  129. package/plugins/migrate_ai_studio/index.cjs +396 -0
  130. package/plugins/migrate_ai_studio/instructions/audit.md +25 -0
  131. package/plugins/migrate_ai_studio/instructions/classify.md +41 -0
  132. package/plugins/migrate_ai_studio/instructions/migrate.md +34 -0
  133. package/plugins/migrate_ai_studio/instructions/verify.md +20 -0
  134. package/plugins/migrate_ai_studio/plugin.json +11 -0
  135. package/plugins/migrate_ai_studio/schema.cjs +20 -0
  136. package/plugins/migrate_ai_studio/ui/dist/index.html +77 -0
  137. package/prodero.release.json +5 -0
@@ -0,0 +1,191 @@
1
+ import { listArchivePaths } from "./knowledge-provenance.js";
2
+ import { assertCompatibleProject } from "./storage.js";
3
+ import { isPathScope } from "./path-scope.js";
4
+ import fs from "node:fs/promises";
5
+ import { loadMemoryCatalog } from "./memory-index.js";
6
+ import { sha256, exists, projectDataPath, writeFileAtomic } from "./storage.js";
7
+ const MEMORY_ID = /^mem_[a-zA-Z0-9_]+$/;
8
+ const EDGE_WEIGHT = {
9
+ depends_on: 24,
10
+ implements: 20,
11
+ related_to: 12,
12
+ supersedes: 8,
13
+ superseded_by: 0,
14
+ consolidates: 0,
15
+ consolidated_into: 0,
16
+ contradicts: 0,
17
+ applies_to: 0,
18
+ learned_from: 0,
19
+ };
20
+ const MAX_INFERRED_RELATED_PER_NODE = 3;
21
+ const MAX_TOKEN_POSTINGS = 128;
22
+ const RELATION_TYPES = new Set(["related_to", "depends_on", "implements", "applies_to", "supersedes", "superseded_by", "consolidates", "consolidated_into", "contradicts", "learned_from"]);
23
+ function validCache(value, digest) {
24
+ if (!value || typeof value !== "object")
25
+ return false;
26
+ const cache = value;
27
+ return cache.version === 1 && cache.sourceDigest === digest
28
+ && Array.isArray(cache.nodes) && cache.nodes.every((node) => typeof node === "string")
29
+ && Array.isArray(cache.brokenReferences) && cache.brokenReferences.every((entry) => typeof entry === "string")
30
+ && Array.isArray(cache.edges) && cache.edges.every((edge) => edge && typeof edge.from === "string" && typeof edge.to === "string"
31
+ && RELATION_TYPES.has(edge.type) && typeof edge.inferred === "boolean" && typeof edge.weight === "number" && Number.isFinite(edge.weight));
32
+ }
33
+ function graphSourceDigest(summaries, archives = []) {
34
+ return sha256(JSON.stringify([archives, summaries.map((item) => ({
35
+ id: item.id, status: item.status, scope: item.scope, scopeRef: item.scopeRef, scopeKind: item.scopeKind, category: item.category,
36
+ title: item.normalizedTitle, content: item.normalizedContent, updatedAt: item.updatedAt,
37
+ related: item.related, supersedes: item.supersedes, sourceSessions: item.sourceSessions, relations: item.relations,
38
+ }))]));
39
+ }
40
+ const pathLike = isPathScope;
41
+ function compatibleScope(left, right) {
42
+ if (pathLike(left) || pathLike(right))
43
+ return left.scope === right.scope && left.scopeRef === right.scopeRef && left.scopeKind === right.scopeKind;
44
+ if (left.scope === "module" || right.scope === "module")
45
+ return left.scope === right.scope && left.scopeRef === right.scopeRef && left.scopeKind === right.scopeKind;
46
+ return true;
47
+ }
48
+ function canonicalPair(from, to, type) {
49
+ return type === "related_to" || type === "contradicts" ? [from, to].sort() : [from, to];
50
+ }
51
+ function build(summaries, archives) {
52
+ const nodes = [...summaries.map((item) => item.id), ...archives].sort();
53
+ const knownIds = new Set(nodes);
54
+ const byId = new Map(summaries.map((item) => [item.id, item]));
55
+ const edges = new Map();
56
+ const broken = new Set();
57
+ const add = (from, to, type, inferred = false) => {
58
+ const needsMemoryTarget = type === "related_to" || type === "depends_on" || type === "implements" || type === "supersedes" || type === "superseded_by" || type === "consolidates" || type === "consolidated_into" || type === "contradicts";
59
+ if (needsMemoryTarget && (!MEMORY_ID.test(to) || !knownIds.has(to))) {
60
+ broken.add(`${from}:${type}:${to}`);
61
+ return;
62
+ }
63
+ const [edgeFrom, edgeTo] = canonicalPair(from, to, type);
64
+ const key = `${edgeFrom}\0${type}\0${edgeTo}`;
65
+ if (!edges.has(key) || !inferred)
66
+ edges.set(key, { from: edgeFrom, to: edgeTo, type, inferred, weight: EDGE_WEIGHT[type] });
67
+ };
68
+ for (const item of summaries) {
69
+ for (const target of item.supersedes)
70
+ add(item.id, target, "supersedes");
71
+ for (const target of item.related) {
72
+ const other = byId.get(target);
73
+ add(item.id, target, item.status === "disputed" && other?.status === "disputed" ? "contradicts" : "related_to");
74
+ }
75
+ for (const relation of item.relations)
76
+ add(item.id, relation.target, relation.type);
77
+ if (item.scopeRef)
78
+ add(item.id, `scope:${item.scopeRef}`, "applies_to");
79
+ for (const session of item.sourceSessions)
80
+ add(item.id, `session:${session}`, "learned_from");
81
+ }
82
+ const successors = new Map();
83
+ for (const item of summaries.filter((candidate) => candidate.status === "active" || candidate.status === "disputed"))
84
+ for (const target of item.supersedes) {
85
+ const ids = successors.get(target) ?? [];
86
+ ids.push(item.id);
87
+ successors.set(target, ids);
88
+ }
89
+ for (const edge of [...edges.values()]) {
90
+ const successor = successors.get(edge.to);
91
+ if (successor?.length === 1 && edge.type !== "supersedes" && edge.type !== "contradicts")
92
+ add(edge.from, successor[0], edge.type, true);
93
+ }
94
+ const current = summaries.filter((item) => item.status === "active" || item.status === "disputed");
95
+ const inverted = new Map();
96
+ for (const item of current)
97
+ for (const token of item.tokens.filter((value) => value.length >= 4)) {
98
+ const ids = inverted.get(token) ?? [];
99
+ ids.push(item.id);
100
+ inverted.set(token, ids);
101
+ }
102
+ for (const item of current) {
103
+ const overlap = new Map();
104
+ for (const token of item.tokens.filter((value) => value.length >= 4)) {
105
+ const postings = inverted.get(token) ?? [];
106
+ if (postings.length > MAX_TOKEN_POSTINGS)
107
+ continue;
108
+ for (const id of postings)
109
+ if (id !== item.id)
110
+ overlap.set(id, (overlap.get(id) ?? 0) + 1);
111
+ }
112
+ const candidates = [...overlap].flatMap(([id, shared]) => {
113
+ const target = byId.get(id);
114
+ if (!compatibleScope(item, target))
115
+ return [];
116
+ const coverage = shared / Math.min(item.tokens.length || 1, target.tokens.length || 1);
117
+ if (shared < 2 || coverage < 0.3)
118
+ return [];
119
+ const targetTitle = target.normalizedTitle.split(" ").filter(Boolean);
120
+ const mentionsTitle = targetTitle.length > 0 && targetTitle.every((token) => item.tokens.includes(token));
121
+ const words = new Set(item.tokens);
122
+ const type = mentionsTitle && (words.has("depends") || words.has("dependency")) ? "depends_on"
123
+ : mentionsTitle && (words.has("implements") || words.has("implementation")) ? "implements" : "related_to";
124
+ return [{ target, shared, coverage, type }];
125
+ }).sort((a, b) => b.shared - a.shared || b.coverage - a.coverage || a.target.id.localeCompare(b.target.id));
126
+ for (const candidate of candidates.slice(0, MAX_INFERRED_RELATED_PER_NODE))
127
+ add(item.id, candidate.target.id, candidate.type, true);
128
+ }
129
+ return {
130
+ version: 1,
131
+ sourceDigest: graphSourceDigest(summaries, archives),
132
+ nodes,
133
+ edges: [...edges.values()].sort((a, b) => a.from.localeCompare(b.from) || a.type.localeCompare(b.type) || a.to.localeCompare(b.to) || Number(a.inferred) - Number(b.inferred)),
134
+ brokenReferences: [...broken].sort(),
135
+ };
136
+ }
137
+ function diagnostics(cache, cacheState) {
138
+ return {
139
+ cacheState,
140
+ nodeCount: cache.nodes.length,
141
+ edgeCount: cache.edges.length,
142
+ brokenReferences: cache.brokenReferences.length,
143
+ conflictEdges: cache.edges.filter((edge) => edge.type === "contradicts").length,
144
+ };
145
+ }
146
+ export class KnowledgeGraph {
147
+ adjacency = new Map();
148
+ diagnostics;
149
+ edges;
150
+ brokenReferences;
151
+ constructor(cache, cacheState) {
152
+ this.edges = cache.edges;
153
+ this.brokenReferences = cache.brokenReferences;
154
+ this.diagnostics = diagnostics(cache, cacheState);
155
+ for (const edge of cache.edges) {
156
+ const outgoing = this.adjacency.get(edge.from) ?? [];
157
+ outgoing.push(edge);
158
+ this.adjacency.set(edge.from, outgoing);
159
+ if (edge.type === "related_to" || edge.type === "contradicts") {
160
+ const incoming = this.adjacency.get(edge.to) ?? [];
161
+ incoming.push({ ...edge, from: edge.to, to: edge.from });
162
+ this.adjacency.set(edge.to, incoming);
163
+ }
164
+ }
165
+ }
166
+ neighbors(id) { return this.adjacency.get(id) ?? []; }
167
+ }
168
+ export async function loadKnowledgeGraph(root, suppliedCatalog, options = {}) {
169
+ await assertCompatibleProject(root);
170
+ const catalog = suppliedCatalog ?? await loadMemoryCatalog(root, options);
171
+ const target = projectDataPath(root, ".cache", "knowledge-graph.json");
172
+ const archives = (await listArchivePaths(root)).map(entry => entry.id).sort();
173
+ const digest = graphSourceDigest(catalog.summaries, archives);
174
+ let state = "missing";
175
+ if (await exists(target)) {
176
+ try {
177
+ const cached = JSON.parse(await fs.readFile(target, "utf8"));
178
+ if (validCache(cached, digest))
179
+ return new KnowledgeGraph(cached, "warm");
180
+ state = "stale";
181
+ }
182
+ catch {
183
+ state = "invalid";
184
+ }
185
+ }
186
+ const rebuilt = build(catalog.summaries, archives);
187
+ if (options.persistCache !== false)
188
+ await writeFileAtomic(target, `${JSON.stringify(rebuilt)}\n`);
189
+ return new KnowledgeGraph(rebuilt, state);
190
+ }
191
+ //# sourceMappingURL=knowledge-graph.js.map
@@ -0,0 +1,212 @@
1
+ import { applicabilitySchema, applicabilityOf, sameApplicability, withApplicability } from "./applicability.js";
2
+ import { inferredKnowledge, evaluateKnowledgeTransition, protectedKnowledge, coversProtection } from './knowledge-authority.js';
3
+ import { inheritKnowledgePolicy } from './knowledge-policy.js';
4
+ import { z } from 'zod';
5
+ import { sortableId } from './ids.js';
6
+ import { assertEnglishProse } from './language.js';
7
+ import { parseKnowledge, renderKnowledge } from './memory.js';
8
+ import { knowledgeArchivePath, provenanceEdges, readKnowledgeInventory } from './knowledge-provenance.js';
9
+ import { assertCompatibleProject, listCheckpoints, sha256 } from './storage.js';
10
+ import { commitSyncTransaction, recoverSyncTransaction, withSyncLock, ownsSyncLock } from './sync-transaction.js';
11
+ const id = z.string().regex(/^[a-zA-Z0-9_-]+$/);
12
+ const proposalSchema = z.object({
13
+ operationId: z.string().regex(/^[a-zA-Z0-9_-]{1,100}$/),
14
+ expectedRevisions: z.record(id, z.string().regex(/^[a-f0-9]{64}$/)).default({}),
15
+ create: z.array(z.object({
16
+ applicability: applicabilitySchema.optional(),
17
+ id: z.string().regex(/^mem_[a-zA-Z0-9_]+$/), layer: z.enum(['local', 'versioned']), title: z.string().trim().min(1).max(240), content: z.string().trim().min(1).max(10000),
18
+ scope: z.enum(['global', 'module', 'task']), scopeRef: z.string().nullable(), scopeKind: z.enum(['path', 'topic']).optional(),
19
+ category: z.enum(['instruction', 'principle', 'invariant', 'constraint', 'decision', 'lesson', 'state']),
20
+ priority: z.enum(['critical', 'important', 'normal', 'contextual']).default('normal'),
21
+ related: z.array(id).max(32).optional(), sourceSessions: z.array(id).optional(), derivedFrom: z.array(z.string()).optional(),
22
+ }).strict()).max(100).default([]),
23
+ links: z.array(z.object({ from: id, to: id, type: z.enum(['supersedes', 'consolidates']) }).strict()).max(1000).default([]),
24
+ archive: z.array(id).max(1000).default([]),
25
+ }).strict();
26
+ const digest = (value) => sha256(JSON.stringify(value));
27
+ const inventoryHash = (records) => digest(records.map(r => [r.item.id, r.path, r.revision]));
28
+ function validateGraph(items) {
29
+ const ids = new Set(items.map(i => i.id)), edges = provenanceEdges(items), adjacency = new Map();
30
+ for (const item of items)
31
+ for (const target of [...item.related, ...(item.relations ?? []).filter(r => !['applies_to', 'learned_from'].includes(r.type)).map(r => r.target)]) {
32
+ if (!ids.has(target))
33
+ throw new Error(`Missing relationship target: ${target}`);
34
+ }
35
+ for (const e of edges) {
36
+ if (!ids.has(e.from) || !ids.has(e.to))
37
+ throw new Error(`Missing provenance target: ${e.from} -> ${e.to}`);
38
+ if (e.from === e.to)
39
+ throw new Error('Self supersession/consolidation is forbidden');
40
+ const targets = adjacency.get(e.from) ?? [];
41
+ targets.push(e.to);
42
+ adjacency.set(e.from, targets);
43
+ }
44
+ // Iterative topological validation also handles long historical chains safely.
45
+ const indegree = new Map([...ids].map(id => [id, 0]));
46
+ for (const targets of adjacency.values())
47
+ for (const target of targets)
48
+ indegree.set(target, indegree.get(target) + 1);
49
+ const ready = [...ids].filter(id => indegree.get(id) === 0);
50
+ let visited = 0;
51
+ for (let i = 0; i < ready.length; i++) {
52
+ visited++;
53
+ for (const target of adjacency.get(ready[i]) ?? []) {
54
+ indegree.set(target, indegree.get(target) - 1);
55
+ if (indegree.get(target) === 0)
56
+ ready.push(target);
57
+ }
58
+ }
59
+ if (visited !== ids.size)
60
+ throw new Error('Knowledge provenance cycle');
61
+ }
62
+ function plan(root, records, proposal, createdAt) {
63
+ if (!Number.isFinite(Date.parse(createdAt)))
64
+ throw new Error('Invalid lifecycle timestamp');
65
+ const before = new Map(records.map(r => [r.item.id, r]));
66
+ for (const [id, revision] of Object.entries(proposal.expectedRevisions)) {
67
+ if (before.get(id)?.revision !== revision)
68
+ throw new Error(`Stale source revision: ${id}`);
69
+ }
70
+ // Incomparable source boundaries cannot be replaced by one broader record.
71
+ const skipped = new Set(proposal.create.filter(input => { const sources = proposal.links.filter(l => l.from === input.id).flatMap(l => before.has(l.to) ? [before.get(l.to).item] : []); return sources.length > 1 && !sources.every(s => sameApplicability(s, sources[0])); }).map(i => i.id));
72
+ const preserved = new Set(proposal.links.filter(l => skipped.has(l.from)).map(l => l.to));
73
+ const after = new Map(records.map(r => [r.item.id, structuredClone(r)]));
74
+ for (const input of proposal.create) {
75
+ if (skipped.has(input.id))
76
+ continue;
77
+ if (after.has(input.id))
78
+ throw new Error(`Knowledge ID already exists: ${input.id}`);
79
+ assertEnglishProse([input.title, input.content], 'Lifecycle knowledge');
80
+ const { layer, ...fields } = input;
81
+ const item = { ...fields, status: 'active', createdAt, updatedAt: createdAt, sourceSessions: input.sourceSessions ?? [], derivedFrom: input.derivedFrom ?? [], supersedes: [], related: input.related ?? [], relations: [] };
82
+ const sources = proposal.links.filter(l => l.from === item.id).flatMap(l => before.has(l.to) ? [before.get(l.to).item] : []);
83
+ Object.assign(item, withApplicability(inferredKnowledge(item), input.applicability ?? (sources[0] ? applicabilityOf(sources[0]) : { kind: 'legacy' })), { version: 3 });
84
+ inheritKnowledgePolicy(item, proposal.links.filter(link => link.from === item.id).flatMap(link => before.has(link.to) ? [before.get(link.to).item] : []));
85
+ Object.assign(item, evaluateKnowledgeTransition(root, item, undefined, { sources: proposal.links.filter(link => link.from === item.id).flatMap(link => before.has(link.to) ? [before.get(link.to).item] : []) }).item);
86
+ const parsed = parseKnowledge(renderKnowledge(item), `${item.id}.md`);
87
+ if (parsed.title !== item.title || parsed.content !== item.content)
88
+ throw new Error('Lifecycle formatting would alter semantic content; revise title/body explicitly');
89
+ after.set(item.id, { item: parsed, layer, path: `${layer === 'local' ? 'local/' : ''}memory/${item.id}.md`, revision: '', storage: 'current' });
90
+ }
91
+ for (const link of proposal.links) {
92
+ if (skipped.has(link.from))
93
+ continue;
94
+ const newer = after.get(link.from), older = after.get(link.to);
95
+ if (!newer || !older)
96
+ throw new Error('Missing knowledge ID in lifecycle link');
97
+ if (link.from === link.to)
98
+ throw new Error('Self supersession/consolidation is forbidden');
99
+ const exists = provenanceEdges([newer.item, older.item]).some(e => e.from === link.from && e.to === link.to && e.type === link.type);
100
+ if (exists)
101
+ continue;
102
+ if (newer.storage === 'archive')
103
+ throw new Error('Archived records are immutable provenance sources');
104
+ if (protectedKnowledge(older.item) && !coversProtection(older.item, newer.item))
105
+ continue;
106
+ if (link.type === 'supersedes')
107
+ newer.item.supersedes.push(link.to);
108
+ else
109
+ (newer.item.relations ??= []).push({ type: 'consolidates', target: link.to });
110
+ }
111
+ validateGraph([...after.values()].map(r => r.item));
112
+ const edges = provenanceEdges([...after.values()].map(r => r.item));
113
+ const affected = new Set(proposal.links.map(l => l.to));
114
+ const archiving = new Set(proposal.archive);
115
+ for (const id of new Set([...affected, ...archiving])) {
116
+ if (preserved.has(id))
117
+ continue;
118
+ const record = after.get(id);
119
+ if (!record)
120
+ throw new Error(`Missing knowledge ID: ${id}`);
121
+ if (record.storage === 'archive')
122
+ continue;
123
+ const incoming = edges.filter(e => e.to === id);
124
+ // Local provenance may reference Versioned sources without publishing a mutation to them.
125
+ if (incoming.length && (record.layer === 'local' || archiving.has(id))) {
126
+ if (incoming.some(e => e.type === 'supersedes'))
127
+ record.item.status = 'superseded';
128
+ else if (record.item.status === 'active')
129
+ record.item.status = 'consolidated';
130
+ // Consolidation must not erase an existing supersession, deprecation or dispute.
131
+ }
132
+ if (archiving.has(id)) {
133
+ if (protectedKnowledge(record.item) && (record.item.status === 'active' || record.item.status === 'disputed'))
134
+ continue;
135
+ if (record.item.status === 'active' || record.item.status === 'disputed')
136
+ throw new Error('Only historical knowledge can be archived');
137
+ record.item.archivedAt = createdAt;
138
+ record.path = knowledgeArchivePath(record.layer, createdAt, id);
139
+ record.storage = 'archive';
140
+ }
141
+ }
142
+ const changes = [];
143
+ for (const [id, record] of after) {
144
+ const original = before.get(id);
145
+ if (original && original.path === record.path && JSON.stringify(original.item) === JSON.stringify(record.item))
146
+ continue;
147
+ record.item.updatedAt = createdAt;
148
+ const text = renderKnowledge(record.item);
149
+ parseKnowledge(text, `${id}.md`);
150
+ changes.push({ id, layer: record.layer, from: original?.path ?? null, to: record.path, before: original?.revision ?? null, after: text });
151
+ }
152
+ changes.sort((a, b) => a.id.localeCompare(b.id));
153
+ const value = { version: 1, proposal, createdAt, inventoryHash: inventoryHash(records), changes, versionedChanges: changes.filter(c => c.layer === 'versioned').map(c => c.id) };
154
+ return { ...value, planId: digest(value) };
155
+ }
156
+ const withLifecycleLock = (root, operation) => ownsSyncLock(root) ? operation() : withSyncLock(root, operation);
157
+ /** Recover pending work under the writer lock, then prepare a plan without applying it. */
158
+ export async function previewKnowledgeLifecycle(root, input, createdAt = new Date().toISOString()) {
159
+ const proposal = proposalSchema.parse(input);
160
+ return withLifecycleLock(root, async () => {
161
+ await assertCompatibleProject(root);
162
+ await recoverSyncTransaction(root);
163
+ return plan(root, await readKnowledgeInventory(root), proposal, createdAt);
164
+ });
165
+ }
166
+ /** Only Core chooses paths and writes. Versioned writes require the exact reviewed preview. */
167
+ export async function applyKnowledgeLifecycle(root, preview, review = {}) {
168
+ return withLifecycleLock(root, async () => {
169
+ await assertCompatibleProject(root);
170
+ await recoverSyncTransaction(root);
171
+ const proposal = proposalSchema.parse(preview.proposal), proposalHash = digest(proposal);
172
+ const { planId, ...value } = preview;
173
+ if (preview.version !== 1 || digest(value) !== planId)
174
+ throw new Error('Lifecycle preview was modified');
175
+ const checkpoints = await listCheckpoints(root);
176
+ const receipt = checkpoints.find(c => c.lifecycle?.operationId === proposal.operationId);
177
+ if (receipt) {
178
+ if (receipt.lifecycle?.planId !== planId || receipt.lifecycle.proposalHash !== proposalHash || receipt.lifecycle.transformationPreviewId !== review.transformationPreviewId)
179
+ throw new Error('Lifecycle operation ID already used by a different plan');
180
+ return { checkpoint: receipt.id, alreadyApplied: true, changed: receipt.createdMemory.length + receipt.updatedMemory.length };
181
+ }
182
+ const records = await readKnowledgeInventory(root);
183
+ if (inventoryHash(records) !== preview.inventoryHash)
184
+ throw new Error('Stale lifecycle preview: knowledge changed; prepare a new preview');
185
+ if (proposal.archive.length && preview.createdAt.slice(0, 7) !== new Date().toISOString().slice(0, 7))
186
+ throw new Error('Stale archive month: prepare a new lifecycle preview');
187
+ const computed = plan(root, records, proposal, preview.createdAt);
188
+ if (computed.planId !== planId)
189
+ throw new Error('Lifecycle preview does not match the validated proposal');
190
+ if (computed.versionedChanges.length && (!review.approvedVersioned || review.reviewedPlanId !== planId))
191
+ throw new Error('Versioned lifecycle changes require approval of this exact reviewed preview');
192
+ const writes = [], moves = [];
193
+ for (const c of computed.changes) {
194
+ const moving = c.from !== null && c.from !== c.to;
195
+ writes.push({ path: c.to, before: moving ? null : c.before, after: c.after });
196
+ if (moving)
197
+ moves.push({ from: c.from, to: c.to, before: c.before });
198
+ }
199
+ const checkpoint = {
200
+ id: sortableId('chk'), createdAt: new Date().toISOString(),
201
+ previousCheckpoint: checkpoints.at(-1)?.id ?? null, sessions: [], consolidation: 'not-performed',
202
+ createdMemory: computed.changes.filter(c => !c.from).map(c => c.id), updatedMemory: computed.changes.filter(c => c.from).map(c => c.id),
203
+ supersededMemory: computed.changes.filter(c => parseKnowledge(c.after, `${c.id}.md`).status === 'superseded').map(c => c.id), duplicateMemory: [], conflicts: [],
204
+ summary: { sessions: 0, candidates: 0, created: computed.changes.filter(c => !c.from).length, consolidated: 0, duplicates: 0, superseded: 0, conflicts: 0 },
205
+ lifecycle: { operationId: proposal.operationId, planId, proposalHash, ...(review.transformationPreviewId ? { transformationPreviewId: review.transformationPreviewId } : {}) },
206
+ };
207
+ writes.push({ path: `checkpoints/${checkpoint.id}.json`, before: null, after: JSON.stringify(checkpoint, null, 2) + '\n' });
208
+ await commitSyncTransaction(root, writes, { moves });
209
+ return { checkpoint: checkpoint.id, alreadyApplied: false, changed: computed.changes.length };
210
+ });
211
+ }
212
+ //# sourceMappingURL=knowledge-lifecycle.js.map
@@ -0,0 +1,113 @@
1
+ import { applicabilitySchema, taskContextInputSchema, withApplicability, applicabilityLabel, buildTaskContext, evaluateApplicability } from "./applicability.js";
2
+ import { safeRelative } from "./plugin-files.js";
3
+ import { z } from 'zod';
4
+ import { loadMemoryCatalog } from './memory-index.js';
5
+ import { searchMemoryPage, forgetMemory } from './memory-operations.js';
6
+ import { promotionRecommendations } from './promotion.js';
7
+ import { assertNoSecrets } from './secrets.js';
8
+ import { assertEnglishProse } from './language.js';
9
+ import { renderKnowledge, parseKnowledge } from './memory.js';
10
+ import { buildIndex } from './index.js';
11
+ import { commitSyncTransaction, withSyncLock, recoverSyncTransaction } from './sync-transaction.js';
12
+ import { listCheckpoints } from './storage.js';
13
+ import { sortableId } from './ids.js';
14
+ export const knowledgeQuerySchema = z.object({
15
+ task: z.string().max(2000).optional(), taskContext: taskContextInputSchema.optional(),
16
+ query: z.string().max(1000).default(''), layer: z.enum(['local', 'versioned']).optional(),
17
+ category: z.enum(['principle', 'invariant', 'decision', 'constraint', 'lesson', 'state', 'instruction']).optional(),
18
+ scope: z.enum(['global', 'project', 'path']).optional(), path: z.string().max(300).optional(),
19
+ includeHistorical: z.boolean().default(false), offset: z.number().int().min(0).default(0), limit: z.number().int().min(1).max(20).default(12),
20
+ });
21
+ export const knowledgeMutationSchema = z.object({
22
+ applicability: applicabilitySchema.optional(),
23
+ id: z.string().regex(/^[a-zA-Z0-9_-]+$/), revision: z.string().regex(/^[a-f0-9]{64}$/),
24
+ priority: z.enum(['critical', 'important', 'normal', 'contextual']).optional(),
25
+ scope: z.enum(['global', 'module', 'task']).optional(), scopeRef: z.string().trim().max(300).nullable().optional(), scopeKind: z.enum(['path', 'topic']).nullable().optional(),
26
+ action: z.enum(['edit', 'retire']), mode: z.enum(['local', 'repository-maintenance']).default('local'), approved: z.literal(true),
27
+ title: z.string().trim().min(1).max(200).regex(/^[^\r\n]+$/).optional(), content: z.string().trim().min(1).max(10000).optional(), reason: z.string().trim().max(1000).optional(),
28
+ }).strict();
29
+ export async function knowledgeView(root, raw = {}, id) {
30
+ const options = knowledgeQuerySchema.parse(raw);
31
+ if (options.scope === 'path') {
32
+ if (!options.path?.trim())
33
+ throw new Error('Path filtering requires a repository-relative path.');
34
+ options.path = safeRelative(options.path.trim().replace(/^\.\//, '').replace(/\/$/, ''));
35
+ }
36
+ const catalog = await loadMemoryCatalog(root);
37
+ const current = catalog.summaries.filter(i => ['active', 'disputed'].includes(i.status));
38
+ const recommendations = await promotionRecommendations(root);
39
+ const page = await searchMemoryPage(root, options.query, { ...options, includeShadowed: true });
40
+ const detailSummary = id ? catalog.summaries.find(i => i.id === id) : undefined;
41
+ if (id && !detailSummary)
42
+ throw new Error('Knowledge was not found. Refresh the list.');
43
+ const detail = detailSummary ? { ...await catalog.load(detailSummary.id), layer: detailSummary.layer, revision: catalog.originalHashes.get(detailSummary.id), recommendation: recommendations.find(r => r.id === id) } : null;
44
+ if (detail) {
45
+ Object.assign(detail, { appliesTo: applicabilityLabel(detail), ...(options.task || options.taskContext ? { applicabilityDecision: evaluateApplicability(detail, buildTaskContext(options.task, options.taskContext, catalog.summaries.flatMap(i => i.applicability?.kind === 'scoped' ? i.applicability.subsystems ?? [] : []), catalog.summaries.flatMap(i => i.applicability?.kind === 'scoped' ? i.applicability.paths ?? [] : [])), detail.contextPolicy === 'required' ? 'required' : 'relevant') } : {}) });
46
+ }
47
+ return { root, ...page, options, detail, counts: { local: current.filter(i => i.layer === 'local').length, versioned: current.filter(i => i.layer === 'versioned').length, recommended: recommendations.filter(r => r.recommended).length },
48
+ matches: page.matches.map(({ content, ...item }) => ({ ...item, contentPreview: content.slice(0, 240), recommendation: recommendations.find(r => r.id === item.id) })) };
49
+ }
50
+ /** Explicit editorial maintenance, preserving identity/topology and existing checkpoint history. */
51
+ export async function mutateKnowledge(root, raw) {
52
+ assertNoSecrets(raw);
53
+ const input = knowledgeMutationSchema.parse(raw);
54
+ assertEnglishProse([input.title, input.content, input.reason], 'Knowledge maintenance');
55
+ if (input.action !== 'edit' && [input.priority, input.scope, input.scopeRef, input.scopeKind, input.applicability].some(v => v !== undefined))
56
+ throw new Error('Classification changes require an instruction edit.');
57
+ if (input.mode === 'local' && input.action === 'retire')
58
+ return forgetMemory(root, { id: input.id, revision: input.revision, reason: input.reason });
59
+ return withSyncLock(root, async () => {
60
+ await recoverSyncTransaction(root);
61
+ const catalog = await loadMemoryCatalog(root);
62
+ const summary = catalog.summaries.find(i => i.id === input.id);
63
+ if (!summary)
64
+ throw new Error('Knowledge was not found. Refresh the list.');
65
+ if ((summary.layer === 'versioned') !== (input.mode === 'repository-maintenance'))
66
+ throw new Error('Versioned Knowledge requires explicit repository-maintenance approval. Local operations cannot modify repository knowledge.');
67
+ const item = await catalog.load(input.id);
68
+ const before = catalog.originalHashes.get(input.id);
69
+ if (before !== input.revision)
70
+ throw new Error('Knowledge changed since it was opened. Reload and review the current revision.');
71
+ if (!['active', 'disputed'].includes(item.status))
72
+ throw new Error('Historical knowledge cannot be edited or reopened.');
73
+ if (input.action === 'edit' && (input.title === undefined || input.content === undefined))
74
+ throw new Error('Editing requires a title and complete statement.');
75
+ const classification = { priority: item.priority, scope: item.scope, scopeRef: item.scopeRef, scopeKind: item.scopeKind };
76
+ if (input.priority !== undefined || input.scope !== undefined || input.scopeRef !== undefined || input.scopeKind !== undefined) {
77
+ if (item.category !== 'instruction' || input.action !== 'edit')
78
+ throw new Error('Classification edits apply only to instruction edits.');
79
+ classification.priority = input.priority ?? item.priority;
80
+ classification.scope = input.scope ?? item.scope;
81
+ classification.scopeRef = input.scopeRef === undefined ? item.scopeRef : input.scopeRef;
82
+ classification.scopeKind = input.scopeKind === undefined ? item.scopeKind : input.scopeKind ?? undefined;
83
+ if (classification.scope === 'global') {
84
+ classification.scopeRef = null;
85
+ classification.scopeKind = undefined;
86
+ }
87
+ else if (!classification.scopeRef)
88
+ throw new Error('Scoped instructions require a scope reference.');
89
+ if (classification.scopeKind === 'path')
90
+ classification.scopeRef = safeRelative(classification.scopeRef);
91
+ }
92
+ if (input.action === 'edit' && !input.applicability && item.title === input.title && item.content === input.content && Object.entries(classification).every(([k, v]) => item[k] === v))
93
+ return { item, unchanged: true };
94
+ const now = new Date();
95
+ const checkpointId = sortableId('chk', now);
96
+ const updated = { ...item, ...classification, title: input.action === 'edit' ? input.title : item.title, content: input.action === 'edit' ? input.content : item.content,
97
+ status: input.action === 'retire' ? 'deprecated' : item.status, updatedAt: now.toISOString(),
98
+ derivedFrom: [...new Set([...item.derivedFrom, `user:maintenance:${checkpointId}`])],
99
+ evidence: [...(item.evidence ?? []), { kind: 'user_instruction', ref: input.reason || `Explicit ${input.mode} ${input.action}` }], };
100
+ if (input.applicability)
101
+ Object.assign(updated, withApplicability(updated, input.applicability), { version: 3 });
102
+ if (!input.applicability && item.applicability?.kind !== 'legacy' && [input.scope, input.scopeRef, input.scopeKind].some(v => v !== undefined) && (classification.scope !== item.scope || classification.scopeRef !== item.scopeRef || classification.scopeKind !== item.scopeKind)) {
103
+ throw new Error('Use structured applicability to change this record’s work boundary; its legacy scope fields are a projection.');
104
+ }
105
+ const after = renderKnowledge(updated);
106
+ parseKnowledge(after, summary.filename);
107
+ const checkpoint = { id: checkpointId, createdAt: now.toISOString(), previousCheckpoint: (await listCheckpoints(root)).at(-1)?.id ?? null, sessions: [], consolidation: 'not-performed', knowledgeBefore: [item], createdMemory: [], updatedMemory: [item.id], supersededMemory: [], duplicateMemory: [], conflicts: [], summary: { sessions: 0, candidates: 0, created: 0, consolidated: 0, duplicates: 0, superseded: 0, conflicts: 0 }, note: `Explicit ${input.mode} ${input.action}: ${item.id}` };
108
+ await commitSyncTransaction(root, [{ path: summary.relativePath, before, after }, { path: `checkpoints/${checkpointId}.json`, before: null, after: JSON.stringify(checkpoint, null, 2) + '\n' }], { policyMode: 'exact' });
109
+ await buildIndex(root);
110
+ return { item: await (await loadMemoryCatalog(root)).load(updated.id), checkpoint: checkpointId, layer: summary.layer };
111
+ });
112
+ }
113
+ //# sourceMappingURL=knowledge-management.js.map
@@ -0,0 +1,84 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { checkedDataPath, exists, writeFileAtomic } from './storage.js';
4
+ import { parseFrontmatter, frontmatter } from './format.js';
5
+ import { parseKnowledge, renderKnowledge } from './memory.js';
6
+ /** Current and historical canonical stores, in deterministic order. Checkpoint
7
+ * preimages remain historical evidence and are not rewritten as current records. */
8
+ export async function knowledgeFiles(root) {
9
+ const directories = ['memory', 'local/memory'];
10
+ for (const archive of ['archive', 'local/archive']) {
11
+ const directory = await checkedDataPath(root, archive);
12
+ if (await exists(directory))
13
+ for (const month of (await fs.readdir(directory)).sort()) {
14
+ if (/^\d{4}_\d{2}$/.test(month))
15
+ directories.push(`${archive}/${month}`);
16
+ }
17
+ }
18
+ const files = [];
19
+ for (const directory of directories) {
20
+ const target = await checkedDataPath(root, directory);
21
+ if (await exists(target))
22
+ for (const name of (await fs.readdir(target)).sort()) {
23
+ if (name.endsWith('.md') && !name.startsWith('.'))
24
+ files.push(`${directory}/${name}`);
25
+ }
26
+ }
27
+ return files;
28
+ }
29
+ /** Mechanical compatibility conversion only: no lexical rules, scope inference,
30
+ * model calls, status filtering or authority promotion. */
31
+ export async function migrateKnowledgeV1ToV2(root) {
32
+ for (const relative of await knowledgeFiles(root)) {
33
+ const target = await checkedDataPath(root, relative), filename = path.basename(relative);
34
+ const before = await fs.readFile(target, 'utf8');
35
+ const old = parseKnowledge(before, filename);
36
+ if (old.version === 2)
37
+ continue;
38
+ const { metadata, body } = parseFrontmatter(before, filename);
39
+ let after;
40
+ if (metadata.kind === 'knowledge') {
41
+ // Preserve bodies, provenance and unrelated metadata byte-for-byte. The old
42
+ // enforcement field was never canonical; remove that ignored draft alias.
43
+ const eol = before.startsWith('---\r\n') ? '\r\n' : '\n';
44
+ const end = before.indexOf(`${eol}---${eol}`, 3 + eol.length);
45
+ const header = before.slice(0, end).replace(/^enforcement:.*(?:\r?\n|$)/m, '');
46
+ after = header.replace(/^---\r?\n/, `---${eol}version: 2${eol}context_policy: ${old.contextPolicy}${eol}authority: legacy_inherited${eol}`) + before.slice(end);
47
+ }
48
+ else {
49
+ // Legacy unstructured Markdown acquires explicit structural metadata while
50
+ // keeping precisely the same effective title/content/defaults.
51
+ const canonical = parseFrontmatter(renderKnowledge({ ...old, version: 2 }), filename).metadata;
52
+ const { enforcement: _ignored, ...preserved } = metadata;
53
+ after = frontmatter({ ...preserved, ...canonical }, body);
54
+ }
55
+ const next = parseKnowledge(after, filename, 2);
56
+ if (JSON.stringify({ ...old, version: 2 }) !== JSON.stringify(next))
57
+ throw new Error(`Knowledge policy migration changed effective knowledge: ${relative}`);
58
+ await writeFileAtomic(target, after);
59
+ }
60
+ }
61
+ export async function validateKnowledgeV2(root) {
62
+ for (const relative of await knowledgeFiles(root)) {
63
+ parseKnowledge(await fs.readFile(await checkedDataPath(root, relative), 'utf8'), path.basename(relative), 2);
64
+ }
65
+ }
66
+ /** Preserve exact old selector semantics; never infer scope from prose. */
67
+ export async function migrateKnowledgeV2ToV3(root) {
68
+ for (const relative of await knowledgeFiles(root)) {
69
+ const target = await checkedDataPath(root, relative), filename = path.basename(relative), before = await fs.readFile(target, 'utf8');
70
+ const old = parseKnowledge(before, filename, 2);
71
+ const eol = before.startsWith('---\r\n') ? '\r\n' : '\n';
72
+ const after = before.replace(/^version: 2\r?$/m, `version: 3${eol}applicability: {"kind":"legacy"}`);
73
+ const next = parseKnowledge(after, filename, 3);
74
+ const { applicability, ...rest } = next;
75
+ if (JSON.stringify({ ...old, version: 3 }) !== JSON.stringify(rest))
76
+ throw new Error(`Applicability migration changed canonical knowledge: ${relative}`);
77
+ await writeFileAtomic(target, after);
78
+ }
79
+ }
80
+ export async function validateKnowledgeV3(root) {
81
+ for (const relative of await knowledgeFiles(root))
82
+ parseKnowledge(await fs.readFile(await checkedDataPath(root, relative), 'utf8'), path.basename(relative), 3);
83
+ }
84
+ //# sourceMappingURL=knowledge-migration.js.map
@@ -0,0 +1,38 @@
1
+ export const CONTEXT_POLICIES = ['required', 'relevant'];
2
+ export const KNOWLEDGE_AUTHORITIES = ['legacy_inherited', 'inferred', 'imported', 'explicit_user', 'reviewed'];
3
+ /** Classification is not proof of user intent. Evidence remains in the existing
4
+ * evidence/sourceSessions/derivedFrom fields. No Phase 1A writer grants stronger
5
+ * authority merely on an agent's assertion or on publication approval. */
6
+ export function validateKnowledgePolicy(category, policy, authority) {
7
+ if (!CONTEXT_POLICIES.includes(policy))
8
+ throw new Error('Invalid canonical knowledge contextPolicy');
9
+ if (!KNOWLEDGE_AUTHORITIES.includes(authority))
10
+ throw new Error('Invalid canonical knowledge authority');
11
+ if (policy === 'required' && category !== 'instruction')
12
+ throw new Error('Required contextPolicy is supported only for instruction records in Knowledge Schema v2');
13
+ }
14
+ /** Strict canonical v2/v3 policy plus a v1/trusted-draft compatibility adapter.
15
+ * Inference writers explicitly use inferredKnowledge and the transaction gate;
16
+ * unversioned draft defaults are not authorization to create mandatory knowledge. */
17
+ export function knowledgePolicy(item) {
18
+ const contextPolicy = (item.version ?? 0) >= 2 ? item.contextPolicy : item.contextPolicy ?? (item.category === 'instruction' ? 'required' : 'relevant');
19
+ const authority = (item.version ?? 0) >= 2 ? item.authority : item.authority ?? (item.version === 1 ? 'legacy_inherited' : 'inferred');
20
+ validateKnowledgePolicy(item.category, contextPolicy, authority);
21
+ return { version: item.version === 3 ? 3 : 2, contextPolicy: contextPolicy, authority: authority,
22
+ ...(item.category === 'instruction' ? { kind: 'instruction', enforcement: contextPolicy } : {}) };
23
+ }
24
+ /** Preserve policy when editing/replacing canonical sources of the same category.
25
+ * Ambiguous source policies need a separate reviewed operation, not a guessed default. */
26
+ export function inheritKnowledgePolicy(item, sources) {
27
+ const matching = sources.filter(source => source.category === item.category);
28
+ if (!matching.length)
29
+ return;
30
+ const policies = matching.map(knowledgePolicy);
31
+ if (new Set(policies.map(p => p.contextPolicy)).size !== 1) {
32
+ Object.assign(item, knowledgePolicy({ ...item, version: item.version === 3 ? 3 : 2, contextPolicy: 'relevant', authority: 'inferred' }));
33
+ return;
34
+ }
35
+ const authority = new Set(policies.map(p => p.authority)).size === 1 ? policies[0].authority : 'inferred';
36
+ Object.assign(item, knowledgePolicy({ ...item, version: item.version === 3 ? 3 : 2, contextPolicy: policies[0].contextPolicy, authority }));
37
+ }
38
+ //# sourceMappingURL=knowledge-policy.js.map