@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,143 @@
1
+ import { z } from 'zod';
2
+ import { tokens } from './matching.js';
3
+ import { isPathScope, pathApplies, pathScopeRelevant } from './path-scope.js';
4
+ export const TASK_KINDS = ['implementation', 'testing', 'migration', 'deployment', 'documentation', 'review', 'debugging'];
5
+ const names = z.array(z.string().trim().min(1).max(160)).min(1).max(16);
6
+ const paths = z.array(z.string().trim().min(1).max(300)).min(1).max(16).refine(a => a.every(p => !/[\0\r\n]/.test(p) && !p.startsWith('/') && !p.includes('\\') && !p.split('/').includes('..') && !p.includes('//') && p !== '.' && !p.replace(/\/\*\*$/, '').includes('*')), 'Use repository-relative paths with optional terminal /**');
7
+ export const applicabilitySchema = z.discriminatedUnion('kind', [
8
+ z.object({ kind: z.literal('legacy') }).strict(),
9
+ z.object({ kind: z.literal('project') }).strict(),
10
+ z.object({ kind: z.literal('unresolved') }).strict(),
11
+ z.object({ kind: z.literal('scoped'), paths: paths.optional(), subsystems: names.optional(), taskKinds: z.array(z.enum(TASK_KINDS)).min(1).max(7).optional() }).strict(),
12
+ ]);
13
+ export const taskContextInputSchema = z.object({ paths: paths.optional(), subsystems: names.optional(), taskKinds: z.array(z.enum(TASK_KINDS)).min(1).max(7).optional(), explicitTopics: names.optional() }).strict();
14
+ const uniq = (v) => [...new Set(v)].sort();
15
+ const lower = (s) => s.toLowerCase().trim();
16
+ const cleanPath = (s) => s.replace(/\/\*\*$/, '').replace(/^\.\//, '').replace(/\/$/, '');
17
+ export function validateApplicability(value, policy) {
18
+ const a = applicabilitySchema.parse(value);
19
+ if (policy === 'required' && a.kind === 'unresolved')
20
+ throw new Error('Required knowledge needs an explicit applicability boundary');
21
+ if (a.kind === 'scoped' && !a.paths?.length && !a.subsystems?.length && !a.taskKinds?.length)
22
+ throw new Error('Scoped applicability needs at least one restriction');
23
+ return a;
24
+ }
25
+ export function applicabilityOf(r) { return r.applicability ?? { kind: 'legacy' }; }
26
+ export function scopeProjection(a) {
27
+ if (a.kind === 'project')
28
+ return { scope: 'global', scopeRef: null };
29
+ if (a.kind === 'unresolved')
30
+ return { scope: 'task', scopeRef: 'unresolved' };
31
+ if (a.kind === 'legacy')
32
+ throw new Error('Legacy applicability retains its original scope');
33
+ if (a.paths?.length)
34
+ return { scope: 'module', scopeRef: cleanPath(a.paths[0]), scopeKind: 'path' };
35
+ if (a.subsystems?.length)
36
+ return { scope: 'module', scopeRef: a.subsystems[0], scopeKind: 'topic' };
37
+ return { scope: 'task', scopeRef: a.taskKinds.join(' ') };
38
+ }
39
+ export function withApplicability(r, a) {
40
+ a = validateApplicability(a, r.contextPolicy);
41
+ return { ...r, ...(a.kind === 'legacy' ? {} : { scopeKind: undefined, ...scopeProjection(a) }), applicability: a };
42
+ }
43
+ export function applicabilityKey(r) {
44
+ const a = applicabilityOf(r);
45
+ return a.kind === 'legacy' ? (r.scope === 'global' ? JSON.stringify({ kind: 'project' }) : JSON.stringify(['legacy', r.scope, r.scopeRef, r.scopeKind ?? null])) : JSON.stringify(a.kind !== 'scoped' ? a : { kind: a.kind, paths: a.paths ? uniq(a.paths.map(cleanPath)) : [], subsystems: a.subsystems ? uniq(a.subsystems.map(lower)) : [], taskKinds: a.taskKinds ? uniq(a.taskKinds) : [] });
46
+ }
47
+ export const sameApplicability = (a, b) => applicabilityKey(a) === applicabilityKey(b);
48
+ /** Subset proof only; incomparable boundaries are not guessed equivalent. */
49
+ export function narrowsApplicability(before, after) {
50
+ if (sameApplicability(before, after))
51
+ return true;
52
+ const b = applicabilityOf(before), a = applicabilityOf(after);
53
+ if (a.kind === 'legacy')
54
+ return false;
55
+ if (b.kind === 'project' || b.kind === 'unresolved')
56
+ return a.kind === 'scoped';
57
+ if (b.kind === 'legacy')
58
+ return before.scope === 'global' && a.kind === 'scoped';
59
+ if (a.kind !== 'scoped')
60
+ return false;
61
+ return (!b.paths || !!a.paths && a.paths.every(p => b.paths.some(q => pathApplies(cleanPath(q), cleanPath(p))))) &&
62
+ (!b.subsystems || !!a.subsystems && a.subsystems.every(s => b.subsystems.map(lower).includes(lower(s)))) &&
63
+ (!b.taskKinds || !!a.taskKinds && a.taskKinds.every(k => b.taskKinds.includes(k)));
64
+ }
65
+ const kindPatterns = { implementation: /\b(?:implement|implementation|develop|update|fix|add|refactor)\b/i, testing: /\b(?:test|tests|testing|unit|spec|harness)\b|тест/i, migration: /\bmigrat\w*\b|миграц/i, deployment: /\b(?:deploy\w*|release|rollout)\b|депло/i, documentation: /\b(?:readme|documentation|docs|document)\b|документ/i, review: /\b(?:review|audit)\b|аудит|ревью/i, debugging: /\b(?:debug\w*|investigate|diagnos\w*|troubleshoot)\b|диагност|расслед/i };
66
+ export function buildTaskContext(taskText = '', input = {}, knownSubsystems = [], knownPaths = []) {
67
+ input = taskContextInputSchema.parse(input);
68
+ const signals = [];
69
+ const add = (dimension, value, source, basis) => signals.push({ dimension, value, source, ...(basis ? { basis } : {}) });
70
+ for (const [dimension, values] of Object.entries(input))
71
+ for (const v of values ?? [])
72
+ add(dimension === 'explicitTopics' ? 'subsystems' : dimension, v, 'argument');
73
+ const mentions = [...taskText.matchAll(/`([^`]+)`|"([^"\n]+)"|(?:^|\s)([\w@.-]+\/[\w@./*-]+|[\w.-]+\.(?:md|ts|tsx|js|php|py|json|sql|html|css)\b)/g)].map(m => m[1] ?? m[2] ?? m[3]);
74
+ for (const p of mentions)
75
+ if (paths.safeParse([p]).success)
76
+ add('paths', cleanPath(p), 'task-text');
77
+ for (const kind of TASK_KINDS)
78
+ if (kindPatterns[kind].test(taskText))
79
+ add('taskKinds', kind, 'task-text');
80
+ for (const p of knownPaths)
81
+ if (cleanPath(taskText.trim()) === cleanPath(p))
82
+ add('paths', cleanPath(p), 'task-text');
83
+ const queryTokens = tokens(taskText);
84
+ for (const name of knownSubsystems)
85
+ if ([...tokens(name)].length && [...tokens(name)].every(t => queryTokens.has(t)))
86
+ add('subsystems', name, 'task-text', 'vocabulary');
87
+ // Technical identifiers are usable narrow topics without a hardcoded project dictionary.
88
+ for (const m of taskText.matchAll(/\b(?:[A-Z][a-z]+(?:[A-Z][A-Za-z0-9]*)+|[A-Z]{2,}|[a-z]+_[a-z_]+)\b/g))
89
+ if (!['README', 'SQL', 'API', 'UI'].includes(m[0]))
90
+ add('subsystems', m[0], 'task-text', 'identifier');
91
+ const values = (dimension) => uniq(signals.filter(s => s.dimension === dimension).map(s => s.value));
92
+ if (signals.some(s => s.dimension === 'taskKinds' && s.value === 'documentation') && !/\b(?:implement|implementation|code|refactor)\b/i.test(taskText)) {
93
+ for (let i = signals.length - 1; i >= 0; i--)
94
+ if (signals[i].source === 'task-text' && signals[i].dimension === 'taskKinds' && signals[i].value === 'implementation')
95
+ signals.splice(i, 1);
96
+ }
97
+ const kinds = values('taskKinds');
98
+ const subsystems = values('subsystems');
99
+ return { taskText, paths: values('paths'), subsystems, taskKinds: kinds, signals, documentationOnly: kinds.includes('documentation') && !kinds.some(k => ['testing', 'deployment', 'migration', 'debugging'].includes(k)) && !subsystems.length };
100
+ }
101
+ export function evaluateApplicability(record, context, use = 'required') {
102
+ const a = applicabilityOf(record), yes = (why) => ({ state: 'applicable', reasons: [why], matched: 1 });
103
+ if (a.kind === 'project')
104
+ return yes('Applies throughout this project');
105
+ if (a.kind === 'unresolved')
106
+ return { state: 'unknown', reasons: ['Knowledge has no resolved work boundary'], matched: 0 };
107
+ if (a.kind === 'legacy') {
108
+ // Exact pre-v3 rules, including different historical Required/Relevant filtering.
109
+ if (record.scope === 'global')
110
+ return yes('Legacy global scope preserved');
111
+ if (isPathScope(record))
112
+ return { state: pathScopeRelevant(context.taskText, record) ? 'applicable' : 'not_applicable', reasons: ['Legacy path matching preserved'], matched: 0 };
113
+ if (use === 'relevant')
114
+ return yes('Legacy topic relevance retrieval preserved');
115
+ const query = lower(context.taskText).replace(/^\.\//, '').replace(/\/$/, ''), ref = lower(record.scopeRef ?? '').replace(/^\.\//, '').replace(/\/$/, '');
116
+ const matched = !!query && !!ref && (query === ref || query.startsWith(ref + '/') || record.scope === 'task' && [...tokens(query)].some(t => tokens(ref).has(t)));
117
+ return { state: matched ? 'applicable' : 'not_applicable', reasons: ['Legacy scoped Required matching preserved'], matched: Number(matched) };
118
+ }
119
+ const results = [];
120
+ if (a.paths) {
121
+ const match = a.paths.some(p => context.paths.some(q => pathApplies(cleanPath(p), cleanPath(q))));
122
+ results.push({ state: match ? 'applicable' : context.paths.length ? 'not_applicable' : 'unknown', reason: match ? 'Repository path matched' : context.paths.length ? 'Paths are outside the configured boundary' : `Task has no resolved path; needs ${a.paths.join(' or ')}` });
123
+ }
124
+ if (a.subsystems) {
125
+ const otherResolved = context.signals.some(s => s.dimension === 'subsystems' && (s.source === 'argument' || s.basis === 'vocabulary'));
126
+ const match = a.subsystems.some(s => context.subsystems.some(q => lower(s) === lower(q)));
127
+ results.push({ state: match ? 'applicable' : otherResolved || context.documentationOnly ? 'not_applicable' : 'unknown', reason: match ? `Subsystem matched: ${a.subsystems.filter(s => context.subsystems.some(q => lower(q) === lower(s))).join(', ')}` : otherResolved || context.documentationOnly ? 'Task focus does not match the subsystem' : `Task has no resolved subsystem; needs ${a.subsystems.join(' or ')}` });
128
+ }
129
+ if (a.taskKinds) {
130
+ const match = a.taskKinds.some(k => context.taskKinds.includes(k));
131
+ results.push({ state: match ? 'applicable' : context.taskKinds.length ? 'not_applicable' : 'unknown', reason: match ? `Task kind matched: ${a.taskKinds.filter(k => context.taskKinds.includes(k)).join(', ')}` : context.taskKinds.length ? 'Task kind is outside the configured boundary' : `Task kind is unresolved; needs ${a.taskKinds.join(' or ')}` });
132
+ }
133
+ return { state: results.some(r => r.state === 'not_applicable') ? 'not_applicable' : results.some(r => r.state === 'unknown') ? 'unknown' : 'applicable', reasons: results.map(r => r.reason), matched: results.filter(r => r.state === 'applicable').length };
134
+ }
135
+ export function inferApplicability(content, context) {
136
+ const own = buildTaskContext(content);
137
+ const c = own.paths.length || own.subsystems.length ? own : context ?? own;
138
+ const a = { kind: 'scoped', ...(c.paths.length ? { paths: c.paths.slice(0, 16) } : {}), ...(c.subsystems.length ? { subsystems: c.subsystems.slice(0, 16) } : {}), ...(c.taskKinds.length ? { taskKinds: c.taskKinds } : {}) };
139
+ // No invented mandatory/global claim: unlocated ordinary knowledge stays task-bound.
140
+ return a.paths || a.subsystems || a.taskKinds ? a : { kind: 'unresolved' };
141
+ }
142
+ export { applicabilityLabel } from "./applicability-label.js";
143
+ //# sourceMappingURL=applicability.js.map
@@ -0,0 +1,129 @@
1
+ import { applicabilityKey, buildTaskContext, evaluateApplicability, applicabilityLabel } from "./applicability.js";
2
+ import { loadMemoryCatalog } from "./memory-index.js";
3
+ import { TOKEN_COUNT_METHOD } from "./telemetry.js";
4
+ export const MAX_BOOTSTRAP_INSTRUCTIONS = 12;
5
+ export const DEFAULT_BOOTSTRAP_BUDGET_TOKENS = 1_000;
6
+ const PRIORITY_SCORE = { critical: 80, important: 40, normal: 20, contextual: 10 };
7
+ function normalizedScope(value) {
8
+ const scope = value?.trim().toLowerCase().replace(/^\.\//, "").replace(/\/$/, "");
9
+ return scope || null;
10
+ }
11
+ export function instructionApplies(item, scope) {
12
+ return item.contextPolicy === 'required' && evaluateApplicability(item, buildTaskContext(scope, {}, item.applicability?.kind === 'scoped' ? item.applicability.subsystems : [], item.applicability?.kind === 'scoped' ? item.applicability.paths : [])).state === 'applicable';
13
+ }
14
+ function orderInstructions(a, b) {
15
+ const aSpecific = a.scope === "global" ? 0 : 1;
16
+ const bSpecific = b.scope === "global" ? 0 : 1;
17
+ return Number(b.priority === "critical") - Number(a.priority === "critical")
18
+ || bSpecific - aSpecific
19
+ || PRIORITY_SCORE[b.priority] - PRIORITY_SCORE[a.priority]
20
+ || a.title.localeCompare(b.title)
21
+ || a.id.localeCompare(b.id);
22
+ }
23
+ function instructionView(item, summary) {
24
+ return {
25
+ id: item.id,
26
+ title: item.title,
27
+ content: item.content,
28
+ applicability: item.applicability,
29
+ appliesTo: applicabilityLabel(item),
30
+ scope: item.scope,
31
+ scopeRef: item.scopeRef,
32
+ priority: item.priority,
33
+ status: item.status === "disputed" ? "disputed" : "active",
34
+ layer: summary.layer,
35
+ kind: "instruction",
36
+ contextPolicy: "required",
37
+ authority: item.authority,
38
+ enforcement: "required",
39
+ estimatedTokens: summary.estimatedTokens,
40
+ };
41
+ }
42
+ export function conflictWarnings(candidates) {
43
+ const byId = new Map(candidates.map((item) => [item.id, item]));
44
+ const pairs = new Map();
45
+ const addPair = (left, right) => {
46
+ if (left.id === right.id)
47
+ return;
48
+ const [first, second] = [left, right].sort((a, b) => a.id.localeCompare(b.id));
49
+ pairs.set(`${first.id}:${second.id}`, [first, second]);
50
+ };
51
+ for (const item of candidates) {
52
+ for (const relation of item.relations) {
53
+ if (relation.type === "contradicts" && byId.has(relation.target))
54
+ addPair(item, byId.get(relation.target));
55
+ }
56
+ if (item.status === "disputed") {
57
+ for (const related of item.related)
58
+ if (byId.get(related)?.status === "disputed")
59
+ addPair(item, byId.get(related));
60
+ }
61
+ }
62
+ const boundaries = new Map();
63
+ for (const item of candidates) {
64
+ const key = `${applicabilityKey(item)}:${item.normalizedTitle}`;
65
+ const group = boundaries.get(key) ?? [];
66
+ group.push(item);
67
+ boundaries.set(key, group);
68
+ }
69
+ for (const group of boundaries.values()) {
70
+ for (let left = 0; left < group.length; left += 1)
71
+ for (let right = left + 1; right < group.length; right += 1) {
72
+ if (group[left].normalizedContent !== group[right].normalizedContent)
73
+ addPair(group[left], group[right]);
74
+ }
75
+ }
76
+ const paired = new Set([...pairs.values()].flatMap(([left, right]) => [left.id, right.id]));
77
+ const warnings = [...pairs.values()]
78
+ .sort(([a], [b]) => a.id.localeCompare(b.id))
79
+ .map(([left, right]) => `Conflicting required instructions: ${left.title} (${left.id}) vs ${right.title} (${right.id}).`);
80
+ for (const item of candidates.filter((candidate) => candidate.status === "disputed" && !paired.has(candidate.id)).sort(orderInstructions)) {
81
+ warnings.push(`Disputed required instruction requires review: ${item.title} (${item.id}).`);
82
+ }
83
+ return warnings;
84
+ }
85
+ export async function getBootstrapContext(root, scope) {
86
+ const catalog = await loadMemoryCatalog(root);
87
+ const shadowed = new Set(catalog.summaries.filter((item) => item.status === "active" || item.status === "disputed").flatMap((item) => item.supersedes));
88
+ const candidates = catalog.summaries
89
+ .filter((item) => (item.status === "active" || item.status === "disputed") && !shadowed.has(item.id) && instructionApplies(item, scope))
90
+ .sort(orderInstructions);
91
+ const warnings = conflictWarnings(candidates);
92
+ const selected = [];
93
+ let selectedTokens = 0;
94
+ for (const item of candidates) {
95
+ if (selected.length >= MAX_BOOTSTRAP_INSTRUCTIONS || selectedTokens + item.estimatedTokens > DEFAULT_BOOTSTRAP_BUDGET_TOKENS)
96
+ continue;
97
+ selected.push(item);
98
+ selectedTokens += item.estimatedTokens;
99
+ }
100
+ const selectedIds = new Set(selected.map((item) => item.id));
101
+ const omitted = candidates.filter((item) => !selectedIds.has(item.id));
102
+ const omittedCritical = omitted.filter(item => item.priority === "critical").length;
103
+ if (omittedCritical)
104
+ warnings.push(`${omittedCritical} critical required instruction(s) were not loaded because Bootstrap Context is bounded to ${MAX_BOOTSTRAP_INSTRUCTIONS} items and ${DEFAULT_BOOTSTRAP_BUDGET_TOKENS} estimated tokens.`);
105
+ const instructions = await Promise.all(selected.map(async (item) => instructionView(await catalog.load(item.id), item)));
106
+ return {
107
+ version: 1,
108
+ projectRoot: root,
109
+ scope: normalizedScope(scope),
110
+ loaded: true,
111
+ instructions,
112
+ warnings,
113
+ metrics: { scopedRequiredPolicies: catalog.summaries.filter(i => i.contextPolicy === "required" && ["active", "disputed"].includes(i.status) && !shadowed.has(i.id) && !instructionApplies(i)).length, candidateInstructions: candidates.length, loadedInstructions: instructions.length, omittedInstructions: omitted.length, estimatedTokens: selectedTokens, tokenCountMethod: TOKEN_COUNT_METHOD, estimated: true },
114
+ limits: { maxInstructions: MAX_BOOTSTRAP_INSTRUCTIONS, budgetTokens: DEFAULT_BOOTSTRAP_BUDGET_TOKENS },
115
+ };
116
+ }
117
+ export function emptyBootstrapContext(root, scope) {
118
+ return {
119
+ version: 1,
120
+ projectRoot: root,
121
+ scope: normalizedScope(scope),
122
+ loaded: false,
123
+ instructions: [],
124
+ warnings: [],
125
+ metrics: { candidateInstructions: 0, loadedInstructions: 0, omittedInstructions: 0, estimatedTokens: 0, tokenCountMethod: TOKEN_COUNT_METHOD, estimated: true },
126
+ limits: { maxInstructions: MAX_BOOTSTRAP_INSTRUCTIONS, budgetTokens: DEFAULT_BOOTSTRAP_BUDGET_TOKENS },
127
+ };
128
+ }
129
+ //# sourceMappingURL=bootstrap.js.map
@@ -0,0 +1,315 @@
1
+ import { parseObservationAnnotations as annotations } from "./format.js";
2
+ import { validateApplicability, withApplicability, sameApplicability } from "./applicability.js";
3
+ import { inferredKnowledge } from './knowledge-authority.js';
4
+ import { inheritKnowledgePolicy } from "./knowledge-policy.js";
5
+ import { SyncEvidenceCollector, pressureRecord } from "./memory-pressure.js";
6
+ import { deterministicClassification, reconciliationSignal } from "./reconciliation.js";
7
+ import { assertCompatibleProject } from "./storage.js";
8
+ import { assertNoSecrets } from "./secrets.js";
9
+ import { exactKnowledge, semanticRisk, evidenceSentences } from "./semantic-safety.js";
10
+ import { createSkillCandidate } from "./skills.js";
11
+ import { sortableId } from "./ids.js";
12
+ import { tokens } from "./matching.js";
13
+ import { loadMemoryCatalog, MAX_RELATED_MEMORY_CANDIDATES } from "./memory-index.js";
14
+ import { renderKnowledge, parseKnowledge } from "./memory.js";
15
+ import { safeProjectPath, fileHash } from "./sync-transaction.js";
16
+ import { estimateTokens, knowledgeText } from "./telemetry.js";
17
+ const SECTION_DEFAULTS = {
18
+ Decisions: { category: "decision", priority: "important", scope: "global" },
19
+ Lessons: { category: "lesson", priority: "normal", scope: "task" },
20
+ Discoveries: { category: "state", priority: "normal", scope: "task" },
21
+ "Project State Changes": { category: "state", priority: "normal", scope: "global" },
22
+ "Global Instruction Candidates": { category: "instruction", priority: "important", scope: "global" },
23
+ "Skill Candidates": { category: "instruction", priority: "contextual", scope: "task" },
24
+ };
25
+ function similarity(a, b) {
26
+ const left = tokens(a);
27
+ const right = tokens(b);
28
+ if (!left.size || !right.size)
29
+ return { jaccard: 0, coverage: 0 };
30
+ const intersection = [...left].filter((token) => right.has(token)).length;
31
+ return { jaccard: intersection / (left.size + right.size - intersection), coverage: intersection / Math.min(left.size, right.size) };
32
+ }
33
+ function asScope(value, fallback) { return value === "module" || value === "task" || value === "global" ? value : fallback; }
34
+ function asPriority(value, fallback) { return value === "critical" || value === "important" || value === "normal" || value === "contextual" ? value : fallback; }
35
+ function asCategory(value, fallback) {
36
+ return value === "principle" || value === "invariant" || value === "decision" || value === "constraint" || value === "lesson" || value === "state" || value === "instruction" ? value : fallback;
37
+ }
38
+ function titleFrom(content) {
39
+ const clean = content.replace(/[`*_]/g, "").replace(/\s+/g, " ").trim();
40
+ const sentence = clean.match(/^(.{1,100}?[.!?])(?:\s|$)/)?.[1] ?? clean;
41
+ return sentence.length > 100 ? `${sentence.slice(0, 97).trim()}…` : sentence;
42
+ }
43
+ export function sessionCandidates(session) {
44
+ assertNoSecrets(session);
45
+ const candidates = [];
46
+ for (const [section, items] of Object.entries(session.sections)) {
47
+ const defaults = SECTION_DEFAULTS[section];
48
+ if (!defaults)
49
+ continue;
50
+ items.forEach((raw, index) => {
51
+ const parsed = annotations(raw);
52
+ if (!parsed.content)
53
+ return;
54
+ const explicitFromText = [...parsed.content.matchAll(/\b(?:replaces|supersedes)\s+(mem_[a-zA-Z0-9_]+)/gi)].map((match) => match[1]);
55
+ let triggers = [];
56
+ try {
57
+ const value = JSON.parse(parsed.metadata.triggers ?? "[]");
58
+ if (Array.isArray(value))
59
+ triggers = value.filter((v) => typeof v === "string");
60
+ }
61
+ catch { /* Legacy observations have no structured triggers. */ }
62
+ let relations = [];
63
+ try {
64
+ const value = JSON.parse(parsed.metadata.relations ?? "[]");
65
+ if (Array.isArray(value))
66
+ relations = value.filter((v) => Boolean(v && typeof v === "object" && typeof v.type === "string" && typeof v.target === "string"));
67
+ }
68
+ catch { /* Legacy observations have no structured relationships. */ }
69
+ candidates.push({
70
+ ...(parsed.metadata.applicability ? { applicability: validateApplicability(JSON.parse(parsed.metadata.applicability)) } : {}),
71
+ instructionCandidate: section === "Global Instruction Candidates",
72
+ skill: section === "Skill Candidates",
73
+ description: parsed.metadata.description,
74
+ triggers,
75
+ proposedReason: parsed.metadata.proposed_reason,
76
+ scope: asScope(parsed.metadata.scope, defaults.scope),
77
+ scopeRef: parsed.metadata.scope_ref ?? null,
78
+ ...(parsed.metadata.scope_kind ? { scopeKind: parsed.metadata.scope_kind } : {}),
79
+ category: asCategory(parsed.metadata.category, defaults.category),
80
+ priority: asPriority(parsed.metadata.priority, defaults.priority),
81
+ title: parsed.metadata.title ?? titleFrom(parsed.content),
82
+ content: parsed.content,
83
+ sessionId: session.id,
84
+ derivedFrom: `${session.id}#${parsed.metadata.id ?? `${section.replace(/\s+/g, "-").toLowerCase()}-${index + 1}`}`,
85
+ supersedes: unique([...(parsed.metadata.supersedes?.split(",").map((value) => value.trim()).filter(Boolean) ?? []), ...explicitFromText]),
86
+ relations,
87
+ });
88
+ });
89
+ }
90
+ return candidates.map(c => c.applicability && !c.skill && !c.instructionCandidate ? withApplicability(c, c.applicability) : c);
91
+ }
92
+ function unique(values) { return [...new Set(values)].sort(); }
93
+ function uniqueRelations(values) {
94
+ return [...new Map(values.map((relation) => [`${relation.type}:${relation.target}`, relation])).values()]
95
+ .sort((a, b) => a.type.localeCompare(b.type) || a.target.localeCompare(b.target));
96
+ }
97
+ function explicitSuperseded(candidate, active) {
98
+ return active.filter((item) => candidate.supersedes.includes(item.id) || new RegExp(`\\b(?:replaces|supersedes)\\s+${item.id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i").test(candidate.content));
99
+ }
100
+ /** Core-owned signals; similarity and merge guards are never conflict verdicts. */
101
+ export function candidateReviewSignals(candidate, related) {
102
+ const signals = [];
103
+ if (candidate.instructionCandidate)
104
+ signals.push({ kind: 'instruction-candidate', detail: 'Proposed instruction; not an active required policy.', severity: 'info', requiresReview: true });
105
+ if (candidate.skill)
106
+ signals.push({ kind: 'skill-candidate', detail: 'Proposed skill; activation requires separate review.', severity: 'info', requiresReview: true });
107
+ for (const old of related) {
108
+ const category = deterministicClassification(candidate, old);
109
+ // Retrieval can include loose neighbors; keep unrelated observations quiet.
110
+ if (category === 'unresolved' && similarity(candidate.content, old.content).coverage < 0.3)
111
+ continue;
112
+ signals.push(reconciliationSignal(category, old));
113
+ }
114
+ return signals;
115
+ }
116
+ function mergeEligible(candidate, old) {
117
+ if (semanticRisk(candidate.content, old.content))
118
+ return false;
119
+ const incoming = evidenceSentences(candidate.content);
120
+ const existing = evidenceSentences(old.content);
121
+ // Only an unchanged sentence prefix plus complementary complete sentences is safe without reasoning.
122
+ const smaller = incoming.length < existing.length ? incoming : existing;
123
+ const larger = incoming.length < existing.length ? existing : incoming;
124
+ return smaller.length > 0 && smaller.every((sentence, index) => sentence === larger[index])
125
+ && estimateTokens(`${old.title}\n${old.content}\n${candidate.content}`) <= 2_000;
126
+ }
127
+ function segments(value) {
128
+ return value.split(/(?:\r?\n){2,}|(?<=[.!?])\s+(?=[\p{Lu}\p{N}`])/u).map((part) => part.trim()).filter(Boolean);
129
+ }
130
+ function compactContribution(existing, incoming) {
131
+ const kept = segments(existing);
132
+ const seen = new Set(kept.map(exactKnowledge));
133
+ const added = [];
134
+ let removed = 0;
135
+ for (const segment of segments(incoming)) {
136
+ const key = exactKnowledge(segment);
137
+ if (!key || seen.has(key)) {
138
+ removed += 1;
139
+ continue;
140
+ }
141
+ seen.add(key);
142
+ kept.push(segment);
143
+ added.push(segment);
144
+ }
145
+ return { content: kept.join("\n\n"), added: added.join("\n\n"), removed };
146
+ }
147
+ async function deterministicConsolidate(root, sessions, now = new Date(), processedSources = new Set()) {
148
+ const candidates = sessions.flatMap(sessionCandidates).filter((candidate) => !processedSources.has(candidate.derivedFrom));
149
+ const catalog = await loadMemoryCatalog(root);
150
+ const writes = new Map();
151
+ const pressure = new SyncEvidenceCollector();
152
+ const changedRecords = new Map();
153
+ const stageKnowledge = async (item) => {
154
+ const relative = `local/memory/${item.id}.md`;
155
+ const after = renderKnowledge(item);
156
+ parseKnowledge(after, `${item.id}.md`);
157
+ const existing = writes.get(relative);
158
+ const before = existing ? existing.before : catalog.originalHashes.get(item.id) ?? await fileHash(await safeProjectPath(root, relative));
159
+ writes.set(relative, { path: relative, before, after });
160
+ catalog.stage(item, "local");
161
+ changedRecords.set(item.id, pressureRecord({ ...item, layer: "local", estimatedTokens: estimateTokens(knowledgeText(item)), relations: item.relations ?? [] }));
162
+ };
163
+ const result = {
164
+ candidates: 0, created: [], createdSkills: [], skillCandidateTokensAdded: 0, updated: [], consolidated: [], superseded: [], duplicates: [], conflicts: [], conflictCount: 0,
165
+ memoryFilesScanned: catalog.filesScanned, memoryBodiesIndexed: catalog.bodiesIndexed, memoryBodiesLoaded: 0, relatedCandidatesSelectedMax: 0,
166
+ observationsProcessed: 0, rawSessionTokens: 0, rawKnowledgeTokens: 0, rawSkillCandidateTokens: 0, canonicalTokensAdded: 0,
167
+ compactedSegments: 0, conservativeMergesSkipped: 0,
168
+ };
169
+ result.candidates = candidates.length;
170
+ result.observationsProcessed = candidates.length;
171
+ result.rawKnowledgeTokens = candidates.filter((candidate) => !candidate.skill).reduce((total, candidate) => total + estimateTokens(candidate.content), 0);
172
+ result.rawSkillCandidateTokens = candidates.filter((candidate) => candidate.skill).reduce((total, candidate) => total + estimateTokens(candidate.content), 0);
173
+ result.rawSessionTokens = candidates.reduce((total, candidate) => total + estimateTokens(candidate.content), 0);
174
+ for (const candidate of candidates) {
175
+ if (candidate.instructionCandidate) {
176
+ (result.instructionCandidates ??= []).push({ ...candidate, id: `candidate_${result.instructionCandidates?.length ?? 0}`, kind: "instruction_candidate", sources: [candidate.derivedFrom], evidence: [{ source: candidate.derivedFrom, quote: candidate.content }] });
177
+ continue;
178
+ }
179
+ if (candidate.skill) {
180
+ const saved = await createSkillCandidate(root, {
181
+ title: candidate.title,
182
+ description: candidate.description ?? candidate.content.replace(/\s+/g, " ").slice(0, 300),
183
+ content: candidate.content,
184
+ scope: candidate.scopeRef ?? "project",
185
+ triggers: candidate.triggers,
186
+ }, { sessionId: candidate.sessionId, derivedFrom: candidate.derivedFrom, proposedReason: candidate.proposedReason }, now, writes);
187
+ if (saved.created) {
188
+ result.createdSkills.push(saved.skill.id);
189
+ result.skillCandidateTokensAdded += estimateTokens(saved.skill.content);
190
+ }
191
+ continue;
192
+ }
193
+ const selected = catalog.selectRelated({
194
+ scope: candidate.scope,
195
+ scopeRef: candidate.scopeRef,
196
+ scopeKind: candidate.scopeKind,
197
+ applicability: candidate.applicability ?? { kind: "legacy" },
198
+ category: candidate.category,
199
+ title: candidate.title,
200
+ content: candidate.content,
201
+ explicitIds: candidate.supersedes,
202
+ }, MAX_RELATED_MEMORY_CANDIDATES);
203
+ // An explicit replacement must survive the complete write path. Never silently
204
+ // drop a missing/historical target and save an unrelated active record instead.
205
+ const unavailable = candidate.supersedes.filter(id => !selected.some(item => item.id === id));
206
+ if (unavailable.length)
207
+ throw new Error(`Explicit supersedes target is missing or no longer current: ${unavailable.join(", ")}. Session remains pending; use current memory IDs.`);
208
+ result.relatedCandidatesSelectedMax = Math.max(result.relatedCandidatesSelectedMax, selected.length);
209
+ const active = await Promise.all(selected.map((entry) => catalog.load(entry.id)));
210
+ for (const signal of candidateReviewSignals(candidate, active)) {
211
+ const target = selected.find(item => item.id === signal.memory?.id);
212
+ if (target)
213
+ pressure.observe(candidate.derivedFrom, pressureRecord(target), signal.kind);
214
+ }
215
+ const localIds = new Set(selected.filter((entry) => entry.layer === "local").map((entry) => entry.id));
216
+ const exact = active.find((item) => item.scope === candidate.scope && item.scopeRef === candidate.scopeRef && item.scopeKind === candidate.scopeKind && item.category === candidate.category && sameApplicability(item, candidate) && exactKnowledge(item.content) === exactKnowledge(candidate.content));
217
+ if (exact && !candidate.supersedes.length) {
218
+ if (localIds.has(exact.id)) {
219
+ exact.sourceSessions = unique([...exact.sourceSessions, candidate.sessionId]);
220
+ exact.derivedFrom = unique([...exact.derivedFrom, candidate.derivedFrom]);
221
+ exact.relations = uniqueRelations([...(exact.relations ?? []), ...candidate.relations]);
222
+ exact.updatedAt = now.toISOString();
223
+ await stageKnowledge(exact);
224
+ result.updated.push(exact.id);
225
+ }
226
+ result.duplicates.push(exact.id);
227
+ continue;
228
+ }
229
+ const supersededTargets = explicitSuperseded(candidate, active);
230
+ const replaced = supersededTargets.filter((item) => localIds.has(item.id));
231
+ const id = sortableId("mem", now);
232
+ const item = {
233
+ id,
234
+ scope: candidate.scope,
235
+ scopeRef: candidate.scopeRef,
236
+ scopeKind: candidate.scopeKind,
237
+ applicability: candidate.applicability ?? { kind: "legacy" },
238
+ version: 3,
239
+ category: candidate.category,
240
+ priority: candidate.priority,
241
+ status: "active",
242
+ title: candidate.title,
243
+ content: candidate.content,
244
+ createdAt: now.toISOString(),
245
+ updatedAt: now.toISOString(),
246
+ sourceSessions: [candidate.sessionId],
247
+ derivedFrom: [candidate.derivedFrom],
248
+ supersedes: unique(supersededTargets.flatMap((old) => [old.id, ...old.supersedes])),
249
+ related: [],
250
+ relations: uniqueRelations(candidate.relations),
251
+ };
252
+ Object.assign(item, withApplicability(inferredKnowledge(item), item.applicability));
253
+ inheritKnowledgePolicy(item, supersededTargets);
254
+ if (supersededTargets.length) {
255
+ for (const old of replaced) {
256
+ old.status = "superseded";
257
+ old.updatedAt = now.toISOString();
258
+ old.related = unique([...old.related, id]);
259
+ await stageKnowledge(old);
260
+ result.superseded.push(old.id);
261
+ result.updated.push(old.id);
262
+ }
263
+ }
264
+ else {
265
+ const sameBoundary = active.filter((old) => localIds.has(old.id) && old.scope === candidate.scope && old.scopeRef === candidate.scopeRef && old.scopeKind === candidate.scopeKind && old.category === candidate.category && sameApplicability(old, candidate));
266
+ const overlapping = sameBoundary.find((old) => mergeEligible(candidate, old));
267
+ if (overlapping) {
268
+ const compacted = compactContribution(overlapping.content, candidate.content);
269
+ overlapping.content = compacted.content;
270
+ result.compactedSegments += compacted.removed;
271
+ result.canonicalTokensAdded += estimateTokens(compacted.added);
272
+ overlapping.sourceSessions = unique([...overlapping.sourceSessions, candidate.sessionId]);
273
+ overlapping.derivedFrom = unique([...overlapping.derivedFrom, candidate.derivedFrom]);
274
+ overlapping.relations = uniqueRelations([...(overlapping.relations ?? []), ...candidate.relations]);
275
+ overlapping.updatedAt = now.toISOString();
276
+ await stageKnowledge(overlapping);
277
+ result.updated.push(overlapping.id);
278
+ if (compacted.added)
279
+ result.consolidated.push(overlapping.id);
280
+ else
281
+ result.duplicates.push(overlapping.id);
282
+ continue;
283
+ }
284
+ if (sameBoundary.some((old) => {
285
+ const score = similarity(`${candidate.title} ${candidate.content}`, `${old.title} ${old.content}`);
286
+ return score.jaccard >= 0.35 || score.coverage >= 0.5;
287
+ }))
288
+ result.conservativeMergesSkipped += 1;
289
+ // Similarity and protected-token changes only limit safe merging. Without
290
+ // semantic evidence, keep both records active for later reconciliation.
291
+ }
292
+ await stageKnowledge(item);
293
+ result.created.push(id);
294
+ result.canonicalTokensAdded += estimateTokens(knowledgeText(item));
295
+ }
296
+ result.created = unique(result.created);
297
+ result.updated = unique(result.updated);
298
+ result.consolidated = unique(result.consolidated);
299
+ result.superseded = unique(result.superseded);
300
+ result.duplicates = unique(result.duplicates);
301
+ result.conflicts = unique(result.conflicts);
302
+ result.memoryBodiesLoaded = catalog.bodiesLoaded;
303
+ result.writes = [...writes.values()];
304
+ result.memoryPressure = pressure.finish([...changedRecords.values()], result);
305
+ return result;
306
+ }
307
+ export const deterministicConsolidator = {
308
+ name: "deterministic-v1",
309
+ consolidate: deterministicConsolidate,
310
+ };
311
+ export async function consolidateSessions(root, sessions, now = new Date(), consolidator = deterministicConsolidator, processedSources = new Set()) {
312
+ await assertCompatibleProject(root);
313
+ return consolidator.consolidate(root, sessions, now, processedSources);
314
+ }
315
+ //# sourceMappingURL=consolidation.js.map
@@ -0,0 +1,21 @@
1
+ import { readFileSync } from "node:fs";
2
+ const packageIdentity = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
3
+ const releaseIdentity = JSON.parse(readFileSync(new URL("../../prodero.release.json", import.meta.url), "utf8"));
4
+ if (releaseIdentity.formatVersion !== 1 || !Number.isInteger(releaseIdentity.schemaVersion) || releaseIdentity.schemaVersion < 1 || !Number.isInteger(releaseIdentity.knowledgeSchemaVersion) || releaseIdentity.knowledgeSchemaVersion < 1)
5
+ throw new Error("Invalid prodero.release.json");
6
+ export const PRODERO_VERSION = packageIdentity.version;
7
+ export const PRODERO_SCHEMA_VERSION = releaseIdentity.schemaVersion;
8
+ export const PRODERO_KNOWLEDGE_SCHEMA_VERSION = releaseIdentity.knowledgeSchemaVersion;
9
+ export const PRODERO_PACKAGE_NAME = packageIdentity.name;
10
+ export const PRODERO_DIRECTORY = ".prodero";
11
+ export const MANAGED_START = "<!-- prodero:start -->";
12
+ export const MANAGED_END = "<!-- prodero:end -->";
13
+ export const PROJECT_DIRECTORIES = [
14
+ "memory",
15
+ "sessions",
16
+ "skills",
17
+ "checkpoints",
18
+ "local/memory",
19
+ ".cache",
20
+ ];
21
+ //# sourceMappingURL=constants.js.map