@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,489 @@
1
+ import { ContextAllocation, marginalValue, REQUIRED_HARD_ITEMS, REQUIRED_HARD_TOKENS, RELEVANT_HARD_ITEMS } from './context-allocation.js';
2
+ import { buildTaskContext, evaluateApplicability, applicabilityOf } from "./applicability.js";
3
+ import { RetrievalEvidenceCollector } from "./memory-pressure.js";
4
+ import { readConfig } from "./storage.js";
5
+ import { pathScopeRelevant, isPathScope } from "./path-scope.js";
6
+ import { tokens } from "./matching.js";
7
+ import { conflictWarnings } from "./bootstrap.js";
8
+ import { listProjectSkills, skillContext, scoreSkill } from "./skills.js";
9
+ import { isStaleSummary, loadMemoryCatalog, memoryHealth } from "./memory-index.js";
10
+ import { loadKnowledgeGraph } from "./knowledge-graph.js";
11
+ import { contextPressure, DEFAULT_CONTEXT_BUDGET_TOKENS, DEFAULT_REQUIRED_CONTEXT_BUDGET_TOKENS, safeRatio, TOKEN_COUNT_METHOD } from "./telemetry.js";
12
+ export const MAX_CONTEXT_REQUIRED_ITEMS = REQUIRED_HARD_ITEMS;
13
+ export const MAX_CONTEXT_RELEVANT_ITEMS = 8;
14
+ export const MAX_CONFIGURED_CONTEXT_ITEMS = 128;
15
+ export const MAX_CONTEXT_SKILLS = 4;
16
+ export const MAX_OMITTED_CONTEXT_DETAILS = 32;
17
+ export const MAX_GRAPH_EXPANSIONS = 4;
18
+ export const MAX_GRAPH_DEPTH = 2;
19
+ export const MAX_GRAPH_TRAVERSED_EDGES = 32;
20
+ export const DEFAULT_CONTEXT_BUDGET_ESTIMATED_TOKENS = DEFAULT_CONTEXT_BUDGET_TOKENS;
21
+ export const DEFAULT_REQUIRED_CONTEXT_BUDGET_ESTIMATED_TOKENS = DEFAULT_REQUIRED_CONTEXT_BUDGET_TOKENS;
22
+ const PRIORITY_SCORE = { critical: 80, important: 40, normal: 20, contextual: 10 };
23
+ const RELEVANCE_PRIORITY_SCORE = { critical: 8, important: 4, normal: 2, contextual: 1 };
24
+ const CATEGORY_ORDER = ["instruction", "principle", "invariant", "constraint", "decision", "lesson", "state"];
25
+ function lexicalScore(queryTokens, item) {
26
+ if (!queryTokens.size)
27
+ return { score: 0, signals: [] };
28
+ const searchable = new Set(item.tokens);
29
+ const titleTokens = tokens(item.title);
30
+ const scopeTokens = tokens(item.scopeRef ?? "");
31
+ const matched = [];
32
+ const titleMatched = [];
33
+ const scopeMatched = [];
34
+ let queryScore = 0;
35
+ for (const token of queryTokens) {
36
+ if (!searchable.has(token))
37
+ continue;
38
+ matched.push(token);
39
+ queryScore += token.length > 6 ? 6 : 4;
40
+ if (titleTokens.has(token))
41
+ titleMatched.push(token);
42
+ if (scopeTokens.has(token))
43
+ scopeMatched.push(token);
44
+ }
45
+ if (!matched.length)
46
+ return { score: 0, signals: [] };
47
+ const signals = [
48
+ { kind: "query-token", label: `Query token overlap: ${matched.join(", ")}`, score: queryScore },
49
+ ];
50
+ if (titleMatched.length)
51
+ signals.push({ kind: "title-token", label: `Title token match: ${titleMatched.join(", ")}`, score: titleMatched.length * 8 });
52
+ if (scopeMatched.length)
53
+ signals.push({ kind: "scope-token", label: `Scope token match: ${scopeMatched.join(", ")}`, score: scopeMatched.length * 10 });
54
+ signals.push({ kind: "priority", label: `${item.priority[0].toUpperCase()}${item.priority.slice(1)} priority`, score: RELEVANCE_PRIORITY_SCORE[item.priority] });
55
+ return { score: signals.reduce((total, signal) => total + signal.score, 0), signals };
56
+ }
57
+ /** Storage freshness is a small ranking hint, never evidence of semantic replacement.
58
+ * UTC-day precision keeps identical daily snapshots stable across reads/cache rebuilds.
59
+ */
60
+ function recencySignal(item, now) {
61
+ const dayMs = 86_400_000;
62
+ const updatedDay = Math.floor(Date.parse(item.updatedAt) / dayMs);
63
+ const today = Math.floor(now.getTime() / dayMs);
64
+ if (!Number.isFinite(updatedDay) || !Number.isFinite(today))
65
+ return { kind: "recency", label: "Storage freshness unavailable", score: 0 };
66
+ const ageDays = Math.max(0, today - updatedDay);
67
+ const score = Math.round(0.5 * 30 / (30 + ageDays) * 1_000_000) / 1_000_000;
68
+ return { kind: "recency", label: `Storage freshness: updated ${ageDays} UTC day(s) ago (${item.updatedAt}); not a fact-effective date`, score };
69
+ }
70
+ function requiredOrder(a, b) {
71
+ return Number(b.contextPolicy === "required") - Number(a.contextPolicy === "required")
72
+ || PRIORITY_SCORE[b.priority] - PRIORITY_SCORE[a.priority]
73
+ || CATEGORY_ORDER.indexOf(a.category) - CATEGORY_ORDER.indexOf(b.category)
74
+ || a.title.localeCompare(b.title)
75
+ || a.id.localeCompare(b.id);
76
+ }
77
+ const recencyTie = (a, b) => (b.recencyTime ?? 0) - (a.recencyTime ?? 0);
78
+ function* diverseRank(candidates, limit) {
79
+ // Within each scope/category group the diversity penalty is identical. Sort
80
+ // once, then compare group heads instead of repeatedly sorting the corpus.
81
+ const groups = new Map();
82
+ for (const candidate of candidates) {
83
+ const key = JSON.stringify([candidate.item.scopeRef ?? candidate.item.scope, candidate.item.category]);
84
+ if (!groups.has(key))
85
+ groups.set(key, { items: [], cursor: 0 });
86
+ groups.get(key).items.push(candidate);
87
+ }
88
+ const tie = (a, b) => b.score - a.score || recencyTie(a, b) || a.item.title.localeCompare(b.item.title) || a.item.id.localeCompare(b.item.id);
89
+ for (const group of groups.values())
90
+ group.items.sort(tie);
91
+ const scopeCounts = new Map();
92
+ const categoryCounts = new Map();
93
+ const utility = (c) => c.score * 100 - (scopeCounts.get(c.item.scopeRef ?? c.item.scope) ?? 0) * 120 - (categoryCounts.get(c.item.category) ?? 0) * 30;
94
+ let visited = 0;
95
+ while (groups.size && visited++ < limit) {
96
+ let bestKey = '', best;
97
+ for (const [key, group] of groups) {
98
+ const head = group.items[group.cursor];
99
+ if (!best || utility(head) > utility(best) || (utility(head) === utility(best) && tie(head, best) < 0)) {
100
+ best = head;
101
+ bestKey = key;
102
+ }
103
+ }
104
+ const group = groups.get(bestKey);
105
+ const next = group.items[group.cursor++];
106
+ if (group.cursor === group.items.length)
107
+ groups.delete(bestKey);
108
+ const scope = next.item.scopeRef ?? next.item.scope;
109
+ const scopePenalty = (scopeCounts.get(scope) ?? 0) * 1.2;
110
+ const categoryPenalty = (categoryCounts.get(next.item.category) ?? 0) * 0.3;
111
+ const signals = [...next.signals];
112
+ if (scopePenalty)
113
+ signals.push({ kind: "scope-diversity", label: `Scope diversity penalty: ${scope}`, score: -scopePenalty });
114
+ if (categoryPenalty)
115
+ signals.push({ kind: "category-diversity", label: `Category diversity penalty: ${next.item.category}`, score: -categoryPenalty });
116
+ const ranked = { ...next, signals, rankScore: next.score - scopePenalty - categoryPenalty };
117
+ yield ranked;
118
+ scopeCounts.set(scope, (scopeCounts.get(scope) ?? 0) + 1);
119
+ categoryCounts.set(next.item.category, (categoryCounts.get(next.item.category) ?? 0) + 1);
120
+ }
121
+ }
122
+ export async function getContext(root, query, options = {}) {
123
+ const evidence = options.onRetrievalEvidence ? new RetrievalEvidenceCollector() : undefined;
124
+ const catalog = await loadMemoryCatalog(root, options);
125
+ const graph = await loadKnowledgeGraph(root, catalog, options);
126
+ const now = options.now ?? new Date();
127
+ const knownSubsystems = catalog.summaries.flatMap(i => i.applicability?.kind === 'scoped' ? i.applicability.subsystems ?? [] : []);
128
+ const taskContext = buildTaskContext(query, options.taskContext, knownSubsystems, catalog.summaries.flatMap(i => i.applicability?.kind === 'scoped' ? i.applicability.paths ?? [] : []));
129
+ const decisions = new Map(catalog.summaries.map(i => [i.id, evaluateApplicability(i, taskContext, i.contextPolicy === 'required' ? 'required' : 'relevant')]));
130
+ const applies = (i) => i.contextPolicy === 'required' && decisions.get(i.id).state === 'applicable';
131
+ const relevantApplies = (i) => applicabilityOf(i).kind === 'legacy' ? evaluateApplicability(i, taskContext, 'relevant').state === 'applicable' : decisions.get(i.id).state !== 'not_applicable' && (i.contextPolicy !== 'required' || decisions.get(i.id).state === 'applicable');
132
+ const shadowed = new Set(catalog.summaries.filter((item) => item.status === "active" || item.status === "disputed").flatMap((item) => item.supersedes));
133
+ const active = catalog.summaries.filter((item) => {
134
+ const eligible = item.status === "active" && !shadowed.has(item.id);
135
+ if (!eligible)
136
+ evidence?.observe(item, "filtered", shadowed.has(item.id) || item.status === "superseded" ? "superseded" : "inactive");
137
+ return eligible;
138
+ });
139
+ const health = memoryHealth(catalog.summaries, now);
140
+ const contextBudgetTokens = Math.max(1, Math.floor(options.contextBudgetTokens ?? DEFAULT_CONTEXT_BUDGET_TOKENS));
141
+ const requiredContextBudgetTokens = Math.min(contextBudgetTokens, Math.max(0, Math.floor(options.requiredContextBudgetTokens ?? DEFAULT_REQUIRED_CONTEXT_BUDGET_TOKENS)));
142
+ const maxContextItems = Math.min(MAX_CONFIGURED_CONTEXT_ITEMS, Math.max(1, Math.floor(options.maxContextItems ?? MAX_CONTEXT_RELEVANT_ITEMS)));
143
+ const allocation = new ContextAllocation(contextBudgetTokens, requiredContextBudgetTokens);
144
+ const queryTokens = tokens(query ?? "");
145
+ const requiredCandidates = active
146
+ .filter((item) => applies(item))
147
+ .sort((a, b) => Number(b.priority === "critical") - Number(a.priority === "critical")
148
+ || lexicalScore(queryTokens, b).score - lexicalScore(queryTokens, a).score || requiredOrder(a, b));
149
+ const requiredGlobalCandidateTokens = requiredCandidates.reduce((total, item) => total + item.estimatedTokens, 0);
150
+ const requiredSummaries = [];
151
+ const omittedRequired = [];
152
+ let requiredGlobalTokens = 0;
153
+ let requiredGlobalOmittedByBudget = 0;
154
+ let requiredGlobalOmittedByBudgetTokens = 0;
155
+ let requiredGlobalOmittedByItemLimit = 0;
156
+ let requiredGlobalOmittedByItemLimitTokens = 0;
157
+ let omittedCritical = 0;
158
+ for (const item of requiredCandidates) {
159
+ let reason = null;
160
+ const constraint = allocation.take('required', item.estimatedTokens);
161
+ if (constraint)
162
+ reason = constraint === 'required-item-ceiling' ? 'item-limit' : 'budget';
163
+ if (reason) {
164
+ if (item.priority === "critical")
165
+ omittedCritical += 1;
166
+ if (reason === "budget") {
167
+ requiredGlobalOmittedByBudget += 1;
168
+ requiredGlobalOmittedByBudgetTokens += item.estimatedTokens;
169
+ }
170
+ else {
171
+ requiredGlobalOmittedByItemLimit += 1;
172
+ requiredGlobalOmittedByItemLimitTokens += item.estimatedTokens;
173
+ }
174
+ evidence?.observe(item, "required", reason === "budget" ? "required-budget" : "item-limit", PRIORITY_SCORE[item.priority], PRIORITY_SCORE[item.priority]);
175
+ omittedRequired.push({ id: item.id, title: item.title, category: item.category, scope: item.scope, scopeRef: item.scopeRef, priority: item.priority, estimatedTokens: item.estimatedTokens, score: PRIORITY_SCORE[item.priority], rankScore: PRIORITY_SCORE[item.priority], signals: [{ kind: "priority", label: `${item.priority[0].toUpperCase()}${item.priority.slice(1)} priority`, score: PRIORITY_SCORE[item.priority] }], reason, constraint: constraint });
176
+ continue;
177
+ }
178
+ requiredSummaries.push(item);
179
+ requiredGlobalTokens += item.estimatedTokens;
180
+ }
181
+ options.onRequiredOmitted?.(omittedRequired);
182
+ const required = await Promise.all(requiredSummaries.map((item) => catalog.load(item.id)));
183
+ const requiredSelection = Object.fromEntries(requiredSummaries.map((item) => [item.id, { estimatedTokens: item.estimatedTokens, applicability: decisions.get(item.id), ...lexicalScore(queryTokens, item), rankScore: lexicalScore(queryTokens, item).score }]));
184
+ for (const item of requiredSummaries) {
185
+ const selected = requiredSelection[item.id];
186
+ evidence?.observe(item, "required", "selected", selected.score, selected.rankScore, selected.signals);
187
+ }
188
+ const requiredIds = new Set(requiredCandidates.map((item) => item.id));
189
+ const requiredGlobalExcludedItems = requiredCandidates.length - requiredSummaries.length;
190
+ const requiredGlobalExcludedTokens = requiredGlobalCandidateTokens - requiredGlobalTokens;
191
+ let contextTokens = requiredGlobalTokens;
192
+ const lexicalMatches = active
193
+ .filter((item) => !requiredIds.has(item.id))
194
+ .filter((item) => { const matches = relevantApplies(item); if (!matches)
195
+ evidence?.observe(item, "filtered", "scope-mismatch"); return matches; })
196
+ .map((item) => {
197
+ const lexical = lexicalScore(queryTokens, item);
198
+ const applicability = decisions.get(item.id);
199
+ if (applicabilityOf(item).kind === 'scoped' && applicability.state === 'applicable') {
200
+ lexical.score += 10 * applicability.matched;
201
+ lexical.signals.push({ kind: 'scope-token', label: applicability.reasons.join('; '), score: 10 * applicability.matched });
202
+ }
203
+ if (isPathScope(item) && applicability.state === 'applicable') {
204
+ lexical.score += 10;
205
+ lexical.signals.push({ kind: "scope-token", label: "Exact or descendant repository path", score: 10 });
206
+ }
207
+ return { item, ...lexical, stale: isStaleSummary(item, now) };
208
+ })
209
+ .filter(({ item, score, signals }) => { if (score <= 0)
210
+ evidence?.observe(item, "filtered", "no-match", score, null, signals); return score > 0; });
211
+ const requiredSeedMatches = requiredSummaries
212
+ .map((item) => ({ item, ...lexicalScore(queryTokens, item), stale: isStaleSummary(item, now) }))
213
+ .filter(({ score }) => score > 0);
214
+ const matchingRelevant = [...lexicalMatches];
215
+ const graphCandidates = new Set([...matchingRelevant, ...requiredSeedMatches].map(({ item }) => item.id));
216
+ const activeById = new Map(active.map((item) => [item.id, item]));
217
+ const traversalSeeds = [...lexicalMatches, ...requiredSeedMatches].sort((a, b) => b.score - a.score || a.item.id.localeCompare(b.item.id)).slice(0, maxContextItems);
218
+ const queue = traversalSeeds.map((seed) => ({ id: seed.item.id, depth: 0, seed }));
219
+ let traversedEdges = 0;
220
+ let expandedItems = 0;
221
+ for (let cursor = 0; cursor < queue.length && expandedItems < MAX_GRAPH_EXPANSIONS && traversedEdges < MAX_GRAPH_TRAVERSED_EDGES; cursor += 1) {
222
+ const current = queue[cursor];
223
+ if (current.depth >= MAX_GRAPH_DEPTH)
224
+ continue;
225
+ const graphPathPriority = (id) => {
226
+ const item = activeById.get(id);
227
+ return item && isPathScope(item) && pathScopeRelevant(query ?? "", item) ? 1 : 0;
228
+ };
229
+ const edges = graph.neighbors(current.id).filter((edge) => edge.weight > 0)
230
+ .sort((a, b) => graphPathPriority(b.to) - graphPathPriority(a.to) || b.weight - a.weight || a.type.localeCompare(b.type) || a.to.localeCompare(b.to));
231
+ for (const edge of edges) {
232
+ if (traversedEdges >= MAX_GRAPH_TRAVERSED_EDGES || expandedItems >= MAX_GRAPH_EXPANSIONS)
233
+ break;
234
+ traversedEdges += 1;
235
+ const item = activeById.get(edge.to);
236
+ if (!item || requiredIds.has(item.id) || graphCandidates.has(item.id) || !relevantApplies(item) || (applicabilityOf(item).kind === 'scoped' && decisions.get(item.id).state === 'unknown' && lexicalScore(queryTokens, item).score <= 0))
237
+ continue;
238
+ const graphScore = Math.max(1, current.seed.score * 0.25 + edge.weight / (current.depth + 1));
239
+ const priorityScore = RELEVANCE_PRIORITY_SCORE[item.priority];
240
+ const score = graphScore + priorityScore;
241
+ matchingRelevant.push({
242
+ item,
243
+ score,
244
+ stale: isStaleSummary(item, now),
245
+ signals: [
246
+ { kind: "graph-edge", label: `${edge.type} from ${current.seed.item.title} at depth ${current.depth + 1}`, score: graphScore },
247
+ { kind: "priority", label: `${item.priority[0].toUpperCase()}${item.priority.slice(1)} priority`, score: priorityScore },
248
+ ],
249
+ });
250
+ graphCandidates.add(item.id);
251
+ expandedItems += 1;
252
+ queue.push({ id: item.id, depth: current.depth + 1, seed: current.seed });
253
+ }
254
+ }
255
+ // Purpose adjusts only already-retrieved candidates; it never admits unrelated knowledge.
256
+ if (options.purpose === "explain") {
257
+ for (const candidate of matchingRelevant) {
258
+ if (!["decision", "lesson", "state"].includes(candidate.item.category))
259
+ continue;
260
+ candidate.score += 2;
261
+ candidate.signals.push({ kind: "purpose", label: "Explain: decisions, lessons and state history", score: 2 });
262
+ }
263
+ }
264
+ // Apply only after lexical/path/graph eligibility and purpose scoring. Freshness
265
+ // never admits unrelated records, alters graph expansion or ranks required rules.
266
+ for (const candidate of matchingRelevant) {
267
+ if (requiredIds.has(candidate.item.id))
268
+ continue;
269
+ const recency = recencySignal(candidate.item, now);
270
+ candidate.score += recency.score;
271
+ candidate.signals.push(recency);
272
+ const updated = Date.parse(candidate.item.updatedAt);
273
+ // Same-day evidence still orders by update time; clock skew gets no future advantage.
274
+ candidate.recencyTime = Number.isFinite(updated) ? Math.min(updated, now.getTime()) : 0;
275
+ }
276
+ const activeSkills = (await listProjectSkills(root)).filter((skill) => skill.status === "active");
277
+ const projectSkillsTokens = activeSkills.reduce((total, skill) => total + skillContext(skill).estimatedTokens, 0);
278
+ const matchingSkills = activeSkills.map((skill) => ({ skill, ...scoreSkill(query ?? "", skill) }))
279
+ .filter(({ score }) => score > 0)
280
+ .sort((a, b) => b.score - a.score || a.skill.id.localeCompare(b.skill.id));
281
+ const skills = [];
282
+ const omittedSkills = [];
283
+ let selectedSkillsTokens = 0;
284
+ let omittedSkillsByBudget = 0;
285
+ let omittedSkillsByItemLimit = 0;
286
+ const skillBudgetTokens = Math.min(1_500, allocation.remaining);
287
+ const strongSkill = (s) => s.score >= 14;
288
+ function selectSkills(candidates) {
289
+ for (const { skill, score, signals } of candidates) {
290
+ const context = skillContext(skill);
291
+ const constraint = allocation.take('skill', context.estimatedTokens);
292
+ if (constraint) {
293
+ if (constraint === 'skill-item-ceiling')
294
+ omittedSkillsByItemLimit++;
295
+ else
296
+ omittedSkillsByBudget++;
297
+ omittedSkills.push({ id: skill.id, reason: constraint, estimatedTokens: context.estimatedTokens });
298
+ continue;
299
+ }
300
+ skills.push({ ...context, score, signals });
301
+ selectedSkillsTokens += context.estimatedTokens;
302
+ contextTokens += context.estimatedTokens;
303
+ }
304
+ }
305
+ selectSkills(matchingSkills.filter(strongSkill));
306
+ const protectedSkillTokens = selectedSkillsTokens;
307
+ const rankedIds = new Set();
308
+ const ranked = diverseRank(matchingRelevant, matchingRelevant.length);
309
+ const relevant = [];
310
+ const relevantSelection = {};
311
+ const omittedRelevant = [];
312
+ let omittedRelevantByBudget = 0;
313
+ let omittedRelevantByItemLimit = 0;
314
+ let omittedRelevantByMarginalValue = 0;
315
+ const suppliedTerms = new Set(requiredSummaries.flatMap(i => i.tokens));
316
+ const peakScore = matchingRelevant.reduce((peak, c) => Math.max(peak, c.score), 0);
317
+ let outsideReason = 'item-limit';
318
+ const minimumRelevantCost = matchingRelevant.reduce((n, c) => Math.min(n, c.item.estimatedTokens), Infinity);
319
+ let selectedContextualTokens = 0;
320
+ let staleRelevantItems = 0;
321
+ for (const candidate of ranked) {
322
+ rankedIds.add(candidate.item.id);
323
+ let reason = null;
324
+ const marginal = marginalValue(candidate.item.tokens, queryTokens, suppliedTerms, candidate.rankScore, peakScore);
325
+ let constraint = null;
326
+ if (candidate.item.estimatedTokens > allocation.remaining) {
327
+ reason = 'budget';
328
+ omittedRelevantByBudget++;
329
+ constraint = 'total-capacity';
330
+ }
331
+ else if (relevant.length >= maxContextItems && !marginal.useful) {
332
+ reason = 'marginal-value';
333
+ omittedRelevantByMarginalValue++;
334
+ }
335
+ else {
336
+ constraint = allocation.take('relevant', candidate.item.estimatedTokens);
337
+ if (constraint) {
338
+ reason = constraint === 'relevant-item-ceiling' ? 'item-limit' : 'budget';
339
+ if (reason === 'item-limit')
340
+ omittedRelevantByItemLimit++;
341
+ else
342
+ omittedRelevantByBudget++;
343
+ }
344
+ }
345
+ if (relevant.length >= maxContextItems)
346
+ candidate.signals.push({ kind: 'marginal-value', label: `New information terms: ${marginal.novelTerms}; fraction ${marginal.novelFraction.toFixed(2)}`, score: 0 });
347
+ if (reason) {
348
+ evidence?.observe(candidate.item, "contextual", reason === "budget" ? "context-budget" : reason === "marginal-value" ? "marginal-value" : "item-limit", candidate.score, candidate.rankScore, candidate.signals);
349
+ if (omittedRelevant.length < MAX_OMITTED_CONTEXT_DETAILS)
350
+ omittedRelevant.push({ id: candidate.item.id, title: candidate.item.title, category: candidate.item.category, scope: candidate.item.scope, scopeRef: candidate.item.scopeRef, priority: candidate.item.priority, estimatedTokens: candidate.item.estimatedTokens, score: candidate.score, rankScore: candidate.rankScore, signals: candidate.signals, reason, ...(constraint ? { constraint } : {}) });
351
+ if (allocation.remaining < minimumRelevantCost) {
352
+ outsideReason = 'budget';
353
+ break;
354
+ }
355
+ if (relevant.length >= maxContextItems && candidate.rankScore < Math.max(12, peakScore * 0.3)) {
356
+ outsideReason = 'marginal-value';
357
+ break;
358
+ }
359
+ continue;
360
+ }
361
+ evidence?.observe(candidate.item, "contextual", "selected", candidate.score, candidate.rankScore, candidate.signals);
362
+ relevant.push(await catalog.load(candidate.item.id));
363
+ relevantSelection[candidate.item.id] = { applicability: decisions.get(candidate.item.id), estimatedTokens: candidate.item.estimatedTokens, score: candidate.score, rankScore: candidate.rankScore, signals: candidate.signals };
364
+ selectedContextualTokens += candidate.item.estimatedTokens;
365
+ contextTokens += candidate.item.estimatedTokens;
366
+ if (candidate.stale)
367
+ staleRelevantItems += 1;
368
+ for (const token of candidate.item.tokens)
369
+ suppliedTerms.add(token);
370
+ if (relevant.length >= RELEVANT_HARD_ITEMS)
371
+ break;
372
+ }
373
+ const outsideRankWindow = matchingRelevant.length - rankedIds.size;
374
+ if (outsideReason === 'budget')
375
+ omittedRelevantByBudget += outsideRankWindow;
376
+ else if (outsideReason === 'marginal-value')
377
+ omittedRelevantByMarginalValue += outsideRankWindow;
378
+ else
379
+ omittedRelevantByItemLimit += outsideRankWindow;
380
+ for (const candidate of matchingRelevant)
381
+ if (!rankedIds.has(candidate.item.id))
382
+ evidence?.observe(candidate.item, "contextual", outsideReason === 'budget' ? 'context-budget' : outsideReason, candidate.score, null, candidate.signals);
383
+ for (const candidate of matchingRelevant
384
+ .filter(({ item }) => !rankedIds.has(item.id))
385
+ .sort((a, b) => b.score - a.score || recencyTie(a, b) || a.item.title.localeCompare(b.item.title) || a.item.id.localeCompare(b.item.id))) {
386
+ if (omittedRelevant.length >= MAX_OMITTED_CONTEXT_DETAILS)
387
+ break;
388
+ omittedRelevant.push({ id: candidate.item.id, title: candidate.item.title, category: candidate.item.category, scope: candidate.item.scope, scopeRef: candidate.item.scopeRef, priority: candidate.item.priority, estimatedTokens: candidate.item.estimatedTokens, score: candidate.score, rankScore: candidate.score, signals: candidate.signals, reason: outsideReason, ...(outsideReason === 'item-limit' ? { constraint: 'relevant-item-ceiling' } : outsideReason === 'budget' ? { constraint: 'total-capacity' } : {}) });
389
+ }
390
+ const omittedRelevantTotal = omittedRelevantByBudget + omittedRelevantByItemLimit + omittedRelevantByMarginalValue;
391
+ selectSkills(matchingSkills.filter(s => !strongSkill(s)));
392
+ const projectKnowledgeTokens = active.reduce((total, item) => total + item.estimatedTokens, 0);
393
+ const pressure = contextPressure(requiredGlobalCandidateTokens, requiredContextBudgetTokens);
394
+ const attention = [];
395
+ const relevantDisputes = catalog.summaries.filter((item) => item.status === "disputed" && !shadowed.has(item.id) && relevantApplies(item) && (applies(item) || lexicalScore(queryTokens, item).score > 0));
396
+ if (relevantDisputes.length)
397
+ attention.push({ kind: "conflict", severity: "critical", title: `${relevantDisputes.length} disputed knowledge item(s) match this task`, detail: relevantDisputes.slice(0, 2).map((item) => item.title).join(" vs ") });
398
+ const instructionConflicts = conflictWarnings(catalog.summaries.filter(item => !shadowed.has(item.id) && ["active", "disputed"].includes(item.status) && applies(item)));
399
+ const applicableRequired = catalog.summaries.filter(item => !shadowed.has(item.id) && ['active', 'disputed'].includes(item.status) && applies(item));
400
+ const applicableRequiredIds = new Set(applicableRequired.map(i => i.id));
401
+ const blockingConflict = applicableRequired.some(i => i.status === 'disputed' || i.relations.some(r => r.type === 'contradicts' && applicableRequiredIds.has(r.target)));
402
+ const unknownRequired = catalog.summaries.filter(i => !shadowed.has(i.id) && ['active', 'disputed'].includes(i.status) && i.contextPolicy === 'required' && decisions.get(i.id).state === 'unknown');
403
+ const policyReadiness = { state: blockingConflict ? 'review-required' : unknownRequired.length ? 'context-needed' : requiredGlobalExcludedItems ? 'required-incomplete' : 'ready', blocking: blockingConflict || unknownRequired.length > 0 || requiredGlobalExcludedItems > 0 };
404
+ if (unknownRequired.length)
405
+ attention.push({ kind: 'memory-health', severity: 'important', title: 'Resolve task context before continuing', detail: `${unknownRequired.length} scoped required policies need task paths, subsystem or activity signals. The agent can enrich Task Context and retry Prepare; no knowledge approval is requested.` });
406
+ if (blockingConflict)
407
+ attention.push({ kind: 'conflict', severity: 'critical', title: 'Resolve required policies before continuing', detail: 'Applicable required knowledge has an explicit conflict or unresolved dispute. Keep the original task and retry Prepare after the policy is resolved.' });
408
+ if (instructionConflicts.length && !relevantDisputes.length)
409
+ attention.push({ kind: "conflict", severity: "critical", title: "Required instruction conflicts", detail: instructionConflicts.join(" ") });
410
+ if (requiredGlobalExcludedItems)
411
+ attention.push({ kind: "context-budget", severity: "critical", title: "Required context is incomplete", detail: `${requiredGlobalExcludedItems} applicable required records cannot be delivered within total capacity or safety ceilings. Do not proceed as if mandatory context were complete; inspect omissions and retry with a suitably scoped task or explicit capacity change.` });
412
+ if (evidence)
413
+ options.onRetrievalEvidence?.(evidence.finish(query ?? "", options.purpose ?? "work", { contextBudgetTokens, requiredContextBudgetTokens, maxRequiredItems: MAX_CONTEXT_REQUIRED_ITEMS, maxContextItems }));
414
+ return {
415
+ projectRoot: root,
416
+ allocation: { version: 2, required: { demandItems: requiredCandidates.length, demandTokens: requiredGlobalCandidateTokens, selectedItems: required.length, selectedTokens: requiredGlobalTokens, reserveTokens: requiredContextBudgetTokens, borrowedTokens: Math.max(0, requiredGlobalTokens - requiredContextBudgetTokens), complete: requiredGlobalExcludedItems === 0 && unknownRequired.length === 0 && !blockingConflict, hardTokens: REQUIRED_HARD_TOKENS, hardItems: REQUIRED_HARD_ITEMS }, relevant: { candidates: matchingRelevant.length, selected: relevant.length, softTarget: maxContextItems, hardItems: RELEVANT_HARD_ITEMS, beyondTarget: Math.max(0, relevant.length - maxContextItems), omissions: { capacity: omittedRelevantByBudget, safety: omittedRelevantByItemLimit, marginalValue: omittedRelevantByMarginalValue }, stopReason: omittedRelevantByItemLimit ? 'safety-ceiling' : omittedRelevantByBudget ? 'capacity' : omittedRelevantByMarginalValue ? 'marginal-value' : 'candidates-exhausted' }, skills: { candidates: matchingSkills.length, selected: skills.length, tokens: selectedSkillsTokens, protectedTokens: protectedSkillTokens, omitted: omittedSkills }, unusedTokens: allocation.remaining },
417
+ policyReadiness,
418
+ taskContext,
419
+ applicability: { unknownRequired: unknownRequired.length, decisions: Object.fromEntries([...required, ...relevant, ...unknownRequired.slice(0, 32), ...active.filter(i => !requiredIds.has(i.id)).slice(0, 32)].map(i => [i.id, decisions.get(i.id)])), truncated: Math.max(0, unknownRequired.length - 32) },
420
+ purpose: options.purpose ?? "work",
421
+ query: query?.trim() || null,
422
+ required,
423
+ relevant,
424
+ skills,
425
+ snapshot: { version: 1, sessionId: null, task: query?.trim() || null },
426
+ selection: {
427
+ required: requiredSelection,
428
+ relevant: relevantSelection,
429
+ omittedRequired: omittedRequired.slice(0, MAX_OMITTED_CONTEXT_DETAILS),
430
+ omittedRelevant,
431
+ omittedRelevantByMarginalValue,
432
+ omittedRelevantByBudget,
433
+ omittedRelevantByItemLimit,
434
+ omittedRelevantDetailsTruncated: Math.max(0, omittedRelevantTotal - omittedRelevant.length),
435
+ omittedSkillsByBudget,
436
+ omittedSkillsByItemLimit,
437
+ loadedKnowledgeItems: required.length + relevant.length,
438
+ graphTraversal: { seedCount: traversalSeeds.length, traversedEdges, expandedItems, maxDepth: MAX_GRAPH_DEPTH, expansionLimit: MAX_GRAPH_EXPANSIONS },
439
+ },
440
+ warnings: [
441
+ ...(options.pendingSessions ? [`${options.pendingSessions} session file(s) contain unsynchronized changes.`] : []),
442
+ ...(requiredGlobalExcludedItems ? [`Required context was truncated (required-incomplete): ${requiredGlobalExcludedItems} applicable records exceeded total capacity or a hard safety ceiling.`] : []),
443
+ ],
444
+ attention,
445
+ memoryHealth: health,
446
+ knowledgeGraph: graph.diagnostics,
447
+ metrics: {
448
+ tokenCountMethod: TOKEN_COUNT_METHOD,
449
+ estimated: true,
450
+ projectKnowledgeTokens,
451
+ requiredGlobalTokens,
452
+ requiredGlobalCandidateTokens,
453
+ requiredGlobalExcludedItems,
454
+ requiredCriticalOmitted: omittedCritical,
455
+ requiredNormalOmitted: requiredGlobalExcludedItems - omittedCritical,
456
+ requiredGlobalExcludedTokens,
457
+ requiredGlobalOmittedByBudget,
458
+ requiredGlobalOmittedByBudgetTokens,
459
+ requiredGlobalOmittedByItemLimit,
460
+ requiredGlobalOmittedByItemLimitTokens,
461
+ requiredContextBudgetTokens,
462
+ selectedContextualTokens,
463
+ selectedSkillsTokens,
464
+ projectSkillsTokens,
465
+ skillSelectivity: safeRatio(selectedSkillsTokens, projectSkillsTokens),
466
+ contextTokens,
467
+ contextBudgetTokens,
468
+ contextSelectivity: safeRatio(requiredGlobalTokens + selectedContextualTokens, projectKnowledgeTokens),
469
+ requiredGlobalPressure: pressure,
470
+ staleRelevantItems,
471
+ },
472
+ agentUsage: null,
473
+ limits: { maxSkills: MAX_CONTEXT_SKILLS, skillBudgetTokens, maxRequiredItems: MAX_CONTEXT_REQUIRED_ITEMS, maxRelevantItems: maxContextItems, contextBudgetTokens, requiredContextBudgetTokens, tokenCountMethod: TOKEN_COUNT_METHOD, estimated: true },
474
+ };
475
+ }
476
+ /** Shared read-only Context Engine entry point. Lifecycle owns sync and session association. */
477
+ export async function prepareContext(root, options = {}) {
478
+ const config = await readConfig(root);
479
+ if (!config)
480
+ throw new Error("Prodero is not initialized for this project.");
481
+ return getContext(root, options.query, {
482
+ ...options,
483
+ persistCache: false,
484
+ contextBudgetTokens: config.context?.max_tokens ?? config.context?.budgetTokens,
485
+ maxContextItems: config.context?.max_items,
486
+ requiredContextBudgetTokens: config.context?.required_max_tokens ?? config.context?.requiredBudgetTokens,
487
+ });
488
+ }
489
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1,68 @@
1
+ /** Curation-only semantic roles. These are proposals, never authorization. */
2
+ import { analyzeKnowledgeText } from './knowledge-analysis.js';
3
+ const labels = /^(Decision|Impact|Constraint|Correction|Guard Pattern|Workflow Note|Test-Quality Note|State|Lesson|Principle|Invariant):\s*$/gmi;
4
+ const norm = /\b(?:must|should|never|always|required|only|cannot|do not|keep|preserve)\b/i;
5
+ function role(label, body) {
6
+ // Explicit supporting fields do not gain authority from imperative grammar.
7
+ if (/test-quality|lesson|correction/.test(label))
8
+ return 'lesson';
9
+ if (/workflow/.test(label))
10
+ return 'procedure';
11
+ if (!/\b(?:must|required|only)\b/i.test(body) && (/^(?:Run\b|For .*\b(?:run|coverage),?\b)/i.test(body.trim()) || /^`?\.\/vendor\/bin\//m.test(body)))
12
+ return 'command';
13
+ if (label === 'state' || /\b(?:currently fails|remains? blocked|planned work owns|temporary workaround)\b/i.test(body) && !/\b(?:must|should|only|unless|never)\b/i.test(body))
14
+ return 'history';
15
+ if (label === 'invariant' || /\btransactional invariant\b/i.test(body))
16
+ return 'invariant';
17
+ if (norm.test(body))
18
+ return /constraint|guard/.test(label) || /\b(?:must not|never|cannot|fail.closed)\b/i.test(body) ? 'constraint' : 'convention';
19
+ if (label === 'impact')
20
+ return 'rationale';
21
+ return 'unresolved';
22
+ }
23
+ export function refineCurationClauses(title, content) {
24
+ const fields = [...content.matchAll(labels)];
25
+ // Unstructured authored rules stay intact; no speculative policy demotion.
26
+ if (!fields.length)
27
+ return analyzeKnowledgeText(title, content).map(p => ({ ...p, role: 'unresolved', normative: true }));
28
+ const fieldsWithBounds = fields.map((m, i) => ({ start: i === 0 ? 0 : m.index, end: fields[i + 1]?.index ?? content.length, label: m[1].toLowerCase(), bodyStart: m.index + m[0].length }));
29
+ const chunks = fieldsWithBounds.flatMap(c => {
30
+ // Split only at complete sentence boundaries, then immediately coalesce equal
31
+ // roles. Exact offsets include labels/whitespace, so nothing disappears.
32
+ const breaks = [...content.slice(c.bodyStart, c.end).matchAll(/(?<=[.!?])\s+(?=[A-Z`])/g)].map(m => c.bodyStart + m.index + m[0].length);
33
+ const starts = [c.start, ...breaks];
34
+ return starts.map((start, i) => ({ ...c, start, end: starts[i + 1] ?? c.end, bodyStart: i === 0 ? c.bodyStart : start }));
35
+ });
36
+ const parts = [];
37
+ for (const c of chunks) {
38
+ const r = role(c.label, content.slice(c.bodyStart, c.end));
39
+ const p = analyzeKnowledgeText(title, content.slice(c.start, c.end))[0];
40
+ const normative = ['constraint', 'invariant', 'convention'].includes(r);
41
+ const previous = parts.at(-1);
42
+ // Adjacent normative clauses form one coherent constraint; adjacent support of
43
+ // the same lifecycle/role stays together. Never discard the original labels.
44
+ if (previous && (previous.normative && normative || previous.role === r)) {
45
+ previous.end = c.end;
46
+ previous.content = content.slice(previous.start, c.end).trim();
47
+ previous.evidence.push(`Source field ${c.label}: ${r}`);
48
+ continue;
49
+ }
50
+ parts.push({ ...p, start: c.start, end: c.end, content: content.slice(c.start, c.end).trim(), role: r, normative,
51
+ category: r === 'history' ? 'state' : r === 'lesson' ? 'lesson' : p.category,
52
+ transient: r === 'history', evidence: [`Source characters ${c.start}–${c.end}`, `Semantic role: ${r}; ${normative ? 'source-supported normative clause' : 'supporting knowledge, not independently mandatory'}`] });
53
+ }
54
+ const grouped = [];
55
+ for (const p of parts) {
56
+ const prior = grouped.find(g => (g.normative && p.normative || g.role === p.role) && JSON.stringify(g.applicability) === JSON.stringify(p.applicability));
57
+ if (prior) {
58
+ prior.segments ??= [{ start: prior.start, end: prior.end }];
59
+ prior.segments.push({ start: p.start, end: p.end });
60
+ prior.content += '\n\n' + p.content;
61
+ prior.evidence.push(...p.evidence);
62
+ }
63
+ else
64
+ grouped.push({ ...p });
65
+ }
66
+ return grouped.map((p, i) => ({ ...p, title: grouped.length === 1 ? title : `${title.slice(0, 155)} · ${p.role} ${i + 1}` }));
67
+ }
68
+ //# sourceMappingURL=curation-clauses.js.map