@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.
- package/ARCHITECTURE.md +298 -0
- package/CHANGELOG.md +33 -0
- package/CONTRIBUTING.md +9 -0
- package/LICENSE +21 -0
- package/README.md +93 -2
- package/SECURITY.md +7 -0
- package/bin/prodero +7 -0
- package/dist/cli.js +266 -0
- package/dist/context-instructions.js +2 -0
- package/dist/contracts.js +51 -0
- package/dist/conversation-presentation.js +297 -0
- package/dist/core/agent-instructions.js +52 -0
- package/dist/core/applicability-label.js +11 -0
- package/dist/core/applicability.js +143 -0
- package/dist/core/bootstrap.js +129 -0
- package/dist/core/consolidation.js +315 -0
- package/dist/core/constants.js +21 -0
- package/dist/core/context-allocation.js +54 -0
- package/dist/core/context-snapshot.js +12 -0
- package/dist/core/context.js +489 -0
- package/dist/core/curation-clauses.js +68 -0
- package/dist/core/curation-evidence.js +98 -0
- package/dist/core/curation-graphical.js +117 -0
- package/dist/core/curation-question.js +28 -0
- package/dist/core/curation-review-presentation.js +4 -0
- package/dist/core/curation-review-state.js +62 -0
- package/dist/core/curation-review.js +573 -0
- package/dist/core/curation-revision.js +106 -0
- package/dist/core/curation-semantic-contract.js +242 -0
- package/dist/core/curation-semantic-jobs.js +340 -0
- package/dist/core/diagnostics.js +17 -0
- package/dist/core/format.js +114 -0
- package/dist/core/ids.js +6 -0
- package/dist/core/imports.js +169 -0
- package/dist/core/index.js +93 -0
- package/dist/core/instruction-review.js +86 -0
- package/dist/core/integrity-files.js +174 -0
- package/dist/core/intelligence.js +95 -0
- package/dist/core/knowledge-analysis.js +46 -0
- package/dist/core/knowledge-authority.js +143 -0
- package/dist/core/knowledge-curation.js +316 -0
- package/dist/core/knowledge-engine.js +475 -0
- package/dist/core/knowledge-graph.js +192 -0
- package/dist/core/knowledge-lifecycle.js +212 -0
- package/dist/core/knowledge-management.js +113 -0
- package/dist/core/knowledge-migration.js +84 -0
- package/dist/core/knowledge-policy.js +38 -0
- package/dist/core/knowledge-provenance.js +90 -0
- package/dist/core/knowledge-write-policy.js +85 -0
- package/dist/core/language.js +14 -0
- package/dist/core/local-preferences.js +76 -0
- package/dist/core/matching.js +6 -0
- package/dist/core/memory-index.js +241 -0
- package/dist/core/memory-intelligence.js +288 -0
- package/dist/core/memory-link-repair.js +70 -0
- package/dist/core/memory-operations.js +122 -0
- package/dist/core/memory-pressure.js +212 -0
- package/dist/core/memory-sync-curator.js +82 -0
- package/dist/core/memory-sync.js +558 -0
- package/dist/core/memory-transformation-bundle.js +163 -0
- package/dist/core/memory-transformation-proposal.js +135 -0
- package/dist/core/memory-transformation.js +108 -0
- package/dist/core/memory.js +186 -0
- package/dist/core/migrations.js +569 -0
- package/dist/core/npm-update.js +289 -0
- package/dist/core/path-scope.js +27 -0
- package/dist/core/plugin-files.js +116 -0
- package/dist/core/project-root.js +39 -0
- package/dist/core/project.js +634 -0
- package/dist/core/promotion.js +279 -0
- package/dist/core/reconciliation.js +61 -0
- package/dist/core/secrets.js +67 -0
- package/dist/core/semantic-safety.js +33 -0
- package/dist/core/session-index.js +116 -0
- package/dist/core/session-review.js +150 -0
- package/dist/core/sessions.js +289 -0
- package/dist/core/settings.js +94 -0
- package/dist/core/skill-curation.js +239 -0
- package/dist/core/skill-polish.js +41 -0
- package/dist/core/skills.js +387 -0
- package/dist/core/storage.js +462 -0
- package/dist/core/sync-transaction.js +290 -0
- package/dist/core/telemetry.js +64 -0
- package/dist/core/templates.js +53 -0
- package/dist/core/types.js +2 -0
- package/dist/core/update.js +326 -0
- package/dist/curation-review-adapter.js +92 -0
- package/dist/diagnostic-boundary.js +55 -0
- package/dist/filesystem/permissions.js +39 -0
- package/dist/hosts/adapters.js +119 -0
- package/dist/hosts/files.js +133 -0
- package/dist/hosts/manager.js +150 -0
- package/dist/hosts/public-entry.js +47 -0
- package/dist/hosts/runtime-command.js +66 -0
- package/dist/hosts/runtime.js +139 -0
- package/dist/hosts/wizard.js +114 -0
- package/dist/knowledge-table.js +38 -0
- package/dist/mcp-development.js +69 -0
- package/dist/mcp-workspace.js +141 -0
- package/dist/memory-presentation.js +182 -0
- package/dist/memory-sync-operation.js +88 -0
- package/dist/plugins/api.js +2 -0
- package/dist/plugins/context.js +20 -0
- package/dist/plugins/mcp.js +72 -0
- package/dist/plugins/registry.js +106 -0
- package/dist/presentation-cards.js +32 -0
- package/dist/presentation-server.js +94 -0
- package/dist/presentation-trace.js +174 -0
- package/dist/presentation.js +443 -0
- package/dist/providers/mcp-sampling.js +45 -0
- package/dist/server.js +664 -0
- package/dist/settings-definition.js +22 -0
- package/dist/skill-instructions.js +2 -0
- package/dist/status-presentation.js +20 -0
- package/dist/text-layout.js +278 -0
- package/dist/update-command.js +53 -0
- package/dist/update-instructions.js +4 -0
- package/dist-ui/index.html +216 -0
- package/docs/APPLICABILITY.md +151 -0
- package/docs/COMPATIBILITY.md +68 -0
- package/docs/CONTEXT-SELECTION-V2.md +61 -0
- package/docs/CURATION-APP-REVIEW.md +156 -0
- package/docs/CURATION-CLAUSE-REFINEMENT.md +45 -0
- package/docs/IMPORTER-CURATION.md +142 -0
- package/docs/KNOWLEDGE-AUTHORITY.md +165 -0
- package/docs/KNOWLEDGE-SCHEMA-V2.md +150 -0
- package/docs/MIGRATIONS.md +103 -0
- package/docs/PRIVACY.md +28 -0
- package/docs/RECOVERY.md +58 -0
- package/docs/RELEASING.md +98 -0
- package/docs/hosts.md +152 -0
- package/docs/images/prodero-context-ready-cropped.png +0 -0
- package/docs/images/prodero-overview-cropped.png +0 -0
- package/docs/images/prodero-session-learned-cropped.png +0 -0
- package/docs/plugins.md +83 -0
- package/docs/ui-themes.md +49 -0
- package/instructions/AGENTS.md +39 -0
- package/package.json +92 -4
- package/plugins/migrate_ai_studio/README.md +101 -0
- package/plugins/migrate_ai_studio/index.cjs +396 -0
- package/plugins/migrate_ai_studio/instructions/audit.md +25 -0
- package/plugins/migrate_ai_studio/instructions/classify.md +41 -0
- package/plugins/migrate_ai_studio/instructions/migrate.md +34 -0
- package/plugins/migrate_ai_studio/instructions/verify.md +20 -0
- package/plugins/migrate_ai_studio/plugin.json +11 -0
- package/plugins/migrate_ai_studio/schema.cjs +20 -0
- package/plugins/migrate_ai_studio/ui/dist/index.html +77 -0
- package/prodero.release.json +5 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Evidence-first proposals only. Classification never establishes authority. */
|
|
2
|
+
import { inferApplicability, buildTaskContext } from './applicability.js';
|
|
3
|
+
export const KNOWLEDGE_ANALYSIS_VERSION = 'evidence-3';
|
|
4
|
+
const transient = /\b(?:currently (?:fails?|blocked|broken)|temporary workaround|remains? blocked|planned work owns|discovery.*fails?)\b/i;
|
|
5
|
+
/** Keep exact text slices: no paraphrasing, dropped clauses or lexical authority. */
|
|
6
|
+
export function analyzeKnowledgeText(title, content) {
|
|
7
|
+
const boundaries = [...content.matchAll(/^(?:Decision|Impact|Constraint|Correction|Guard Pattern|Workflow Note|Test-Quality Note|State|Lesson|Principle|Invariant):\s*$/gmi)];
|
|
8
|
+
const chunks = boundaries.length > 1 ? boundaries.map((m, i) => ({ start: i === 0 ? 0 : m.index, end: boundaries[i + 1]?.index ?? content.length, label: m[0].trim().slice(0, -1).toLowerCase() })) : [{ start: 0, end: content.length, label: boundaries[0]?.[0].trim().slice(0, -1).toLowerCase() ?? '' }];
|
|
9
|
+
// Impact stays with its decision; split only lifecycle/category boundaries.
|
|
10
|
+
const groups = [];
|
|
11
|
+
for (const c of chunks) {
|
|
12
|
+
if ((c.label === 'impact' || c.label === 'constraint' && /\b(?:must|should|never|always|required|only|cannot|do not)\b/i.test(content.slice(groups.at(-1)?.start ?? 0, c.start))) && groups.length && !transient.test(content.slice(c.start, c.end)))
|
|
13
|
+
groups.at(-1).end = c.end;
|
|
14
|
+
else
|
|
15
|
+
groups.push({ ...c });
|
|
16
|
+
}
|
|
17
|
+
const wholeContext = buildTaskContext(`${title}\n${content}`);
|
|
18
|
+
const titleContext = buildTaskContext(title);
|
|
19
|
+
const domain = titleContext.subsystems.length ? titleContext.subsystems : wholeContext.subsystems;
|
|
20
|
+
return groups.filter(c => content.slice(c.start, c.end).trim()).map((c, i) => {
|
|
21
|
+
const body = content.slice(c.start, c.end).trim();
|
|
22
|
+
const historical = transient.test(body);
|
|
23
|
+
const normative = /\b(?:must|should|never|always|required|only|cannot|do not|prohibit)\b/i.test(body);
|
|
24
|
+
const category = historical ? 'state' : c.label === 'principle' ? 'principle' : c.label === 'invariant' ? 'invariant' : c.label === 'constraint' || c.label === 'guard pattern' ? 'constraint' : c.label === 'lesson' || c.label === 'correction' ? 'lesson' : c.label === 'decision' ? (normative ? 'instruction' : 'decision') : c.label === 'workflow note' || /^(?:Run |Use |Always |Never |Do not )/.test(body) ? 'instruction' : /\b(?:invariant|transactional invariant)\b/i.test(body) ? 'invariant' : /\b(?:must not|cannot|never persist|prohibit)\b/i.test(body) ? 'constraint' : /\b(?:uses?|chosen|canonical|decided|adopted)\b/i.test(body) ? 'decision' : 'state';
|
|
25
|
+
let applicability = inferApplicability(body);
|
|
26
|
+
// Named domain is stronger than incidental code identifiers. Preserve concrete paths
|
|
27
|
+
// when no domain is established; avoid AND-ing incidental paths with a subsystem.
|
|
28
|
+
// Titles are authored source metadata, not a built-in domain dictionary.
|
|
29
|
+
// A single explicit identifier may name the work boundary; ambiguous titles
|
|
30
|
+
// remain unresolved rather than inventing a preferred subsystem.
|
|
31
|
+
const named = [...new Set(domain.filter(n => !['AI', 'PHP', 'CLI', 'HTTP', 'HTML', 'JSON', 'DB', 'SQL'].includes(n)))];
|
|
32
|
+
if (named.length === 1)
|
|
33
|
+
applicability = { kind: 'scoped', subsystems: named, ...(/\b(?:tests?|testing|harness)\b/i.test(title) ? { taskKinds: ['testing'] } : {}) };
|
|
34
|
+
if (named.length > 1)
|
|
35
|
+
applicability = { kind: 'unresolved' };
|
|
36
|
+
const evidence = [`Source characters ${c.start}–${c.end}`, ...(c.label ? [`Explicit source field: ${c.label}`] : []), ...(historical ? ['Time-sensitive wording; retained as historical state'] : [])];
|
|
37
|
+
if (applicability.kind === 'scoped')
|
|
38
|
+
evidence.push(`Explicit work signals: ${JSON.stringify(applicability)}`);
|
|
39
|
+
return { title: groups.length === 1 ? title : `${title.slice(0, 155)} · ${c.label || `part ${i + 1}`}`, content: body, category, applicability, evidence, start: c.start, end: c.end, transient: historical };
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export function importPreviewSummary(items) {
|
|
43
|
+
const selected = items.filter(i => i.include !== false);
|
|
44
|
+
return { records: selected.length, categories: Object.fromEntries([...new Set(selected.map(i => i.category))].sort().map(c => [c, selected.filter(i => i.category === c).length])), required: 0, relevant: selected.length, projectWide: selected.filter(i => i.applicability?.kind === 'project').length, scoped: selected.filter(i => i.applicability?.kind === 'scoped').length, unresolved: selected.filter(i => !i.applicability || i.applicability.kind === 'unresolved').length };
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=knowledge-analysis.js.map
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { sameApplicability, applicabilityKey, narrowsApplicability, applicabilityOf, withApplicability } from "./applicability.js";
|
|
2
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { knowledgePolicy } from './knowledge-policy.js';
|
|
5
|
+
import { evidenceSentences } from './semantic-safety.js';
|
|
6
|
+
const authorityContext = new AsyncLocalStorage();
|
|
7
|
+
const shape = (i) => JSON.stringify([i.category, i.scope, i.scopeRef, i.scopeKind, i.priority, i.content, applicabilityKey(i)]);
|
|
8
|
+
/** Internal application capability, not an MCP input or persisted permission.
|
|
9
|
+
* Entry points bind either host-attested intent or the explicit chat-rule command;
|
|
10
|
+
* the latter is model-mediated and retains distinct provenance.
|
|
11
|
+
* Scope is invocation-local; neither serialized evidence nor authority is a capability. */
|
|
12
|
+
export async function withTrustedKnowledgeIntent(root, proposed, proof, operation) {
|
|
13
|
+
if (!proof.evidence.trim())
|
|
14
|
+
throw new Error('Trusted policy intent requires an auditable source reference');
|
|
15
|
+
const grant = { active: true, root: path.resolve(root), proposed: structuredClone(proposed), evidence: proof.evidence, authority: proof.authority, before: proof.before ? JSON.stringify(proof.before) : undefined };
|
|
16
|
+
try {
|
|
17
|
+
return await authorityContext.run(grant, operation);
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
grant.active = false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export const protectedKnowledge = (i) => i.contextPolicy === 'required' || i.priority === 'critical' || ['explicit_user', 'reviewed'].includes(i.authority ?? '');
|
|
24
|
+
const sameScope = (a, b) => sameApplicability(a, b);
|
|
25
|
+
/** Conservative proof: whole clauses, including order, must survive exactly. No
|
|
26
|
+
* semantic equivalence is inferred from an LLM assertion or bag of words. */
|
|
27
|
+
export const sameProtection = (a, b) => a.category === b.category && sameScope(a, b) && a.priority === b.priority && a.contextPolicy === b.contextPolicy && a.authority === b.authority && JSON.stringify(evidenceSentences(a.content)) === JSON.stringify(evidenceSentences(b.content));
|
|
28
|
+
const samePolicyBoundary = (a, b) => a.category === b.category && sameScope(a, b) && a.priority === b.priority && a.contextPolicy === b.contextPolicy && a.authority === b.authority;
|
|
29
|
+
function clausesCovered(before, after) {
|
|
30
|
+
const output = evidenceSentences(after);
|
|
31
|
+
let cursor = 0;
|
|
32
|
+
return evidenceSentences(before).every(clause => { const index = output.indexOf(clause, cursor); cursor = index + 1; return index >= 0; });
|
|
33
|
+
}
|
|
34
|
+
export const coversProtection = (before, after) => samePolicyBoundary(before, after) && clausesCovered(before.content, after.content);
|
|
35
|
+
function equivalentSources(proposed, sources) {
|
|
36
|
+
if (!sources.length || !sources.every(s => samePolicyBoundary(s, proposed)))
|
|
37
|
+
return false;
|
|
38
|
+
const clauses = new Set(sources.flatMap(s => evidenceSentences(s.content))), out = new Set(evidenceSentences(proposed.content));
|
|
39
|
+
return clauses.size === out.size && [...clauses].every(c => out.has(c)) && sources.every(s => clausesCovered(s.content, proposed.content));
|
|
40
|
+
}
|
|
41
|
+
export function inferredKnowledge(item, origin = 'inferred') {
|
|
42
|
+
return { ...item, ...knowledgePolicy({ ...item, version: item.version === 3 ? 3 : 2, contextPolicy: 'relevant', authority: origin }) };
|
|
43
|
+
}
|
|
44
|
+
/** The single semantic authorization boundary, shared by every canonical transaction. */
|
|
45
|
+
export function evaluateKnowledgeTransition(root, proposed, before, options = {}) {
|
|
46
|
+
try {
|
|
47
|
+
knowledgePolicy(proposed);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return { decision: 'forbidden', item: before ?? proposed, reasons: ['invalid-policy-combination'], blocking: true };
|
|
51
|
+
}
|
|
52
|
+
if (reviewedBatchAllows(root, proposed, before))
|
|
53
|
+
return { decision: "allowed", item: proposed, reasons: ["trusted-current-operation"], blocking: false };
|
|
54
|
+
const grant = authorityContext.getStore();
|
|
55
|
+
if (grant?.active && grant.proposed.status === proposed.status && grant.root === path.resolve(root) && shape(grant.proposed) === shape(proposed) && (!grant.before || grant.before === JSON.stringify(before) || (!before && options.sources?.some(s => JSON.stringify(s) === grant.before)))) {
|
|
56
|
+
const item = { ...proposed, ...knowledgePolicy({ ...proposed, version: proposed.version === 3 ? 3 : 2, contextPolicy: grant.proposed.contextPolicy, authority: grant.authority }), evidence: [...(proposed.evidence ?? []), { kind: 'user_instruction', ref: grant.evidence }] };
|
|
57
|
+
return { decision: 'allowed', item, reasons: ['trusted-current-operation'], blocking: false };
|
|
58
|
+
}
|
|
59
|
+
if (!before) {
|
|
60
|
+
// Publication and equivalent replacement retain existing policy, never gain it.
|
|
61
|
+
const source = options.sources?.find(s => sameProtection(s, proposed));
|
|
62
|
+
if ((source && source.status === proposed.status) || (proposed.status === 'active' && equivalentSources(proposed, (options.sources ?? []).filter(s => s.status === 'active'))))
|
|
63
|
+
return { decision: 'allowed', item: proposed, reasons: ['existing-policy-preserved'], blocking: false };
|
|
64
|
+
const item = inferredKnowledge(proposed, proposed.authority === 'imported' ? 'imported' : 'inferred');
|
|
65
|
+
if (item.priority === 'critical')
|
|
66
|
+
item.priority = 'important';
|
|
67
|
+
const sources = options.sources ?? [];
|
|
68
|
+
// Never union incomparable source boundaries. A useful inferred fork can
|
|
69
|
+
// keep the first source boundary; uncovered originals remain active.
|
|
70
|
+
if (sources.length && !sources.some(s => sameScope(item, s))) {
|
|
71
|
+
item.scope = sources[0].scope;
|
|
72
|
+
item.scopeRef = sources[0].scopeRef;
|
|
73
|
+
item.scopeKind = sources[0].scopeKind;
|
|
74
|
+
Object.assign(item, withApplicability(item, applicabilityOf(sources[0])));
|
|
75
|
+
}
|
|
76
|
+
const changed = item.contextPolicy !== proposed.contextPolicy || item.authority !== proposed.authority || item.priority !== proposed.priority || !sameScope(item, proposed);
|
|
77
|
+
return { decision: changed ? (options.exact ? 'review_required' : 'allowed_with_safe_fallback') : 'allowed', item, reasons: changed ? ['unverified-promotion-saved-as-relevant'] : [], blocking: changed && !!options.exact };
|
|
78
|
+
}
|
|
79
|
+
const reasons = [];
|
|
80
|
+
if (options.replacementRequested && before.status !== proposed.status && !options.retirementCovered)
|
|
81
|
+
reasons.push("replacement-does-not-cover-source");
|
|
82
|
+
if (before.contextPolicy !== proposed.contextPolicy)
|
|
83
|
+
reasons.push('delivery-policy-change');
|
|
84
|
+
if (before.authority !== proposed.authority)
|
|
85
|
+
reasons.push('authority-change');
|
|
86
|
+
if (proposed.priority === 'critical' && before.priority !== 'critical')
|
|
87
|
+
reasons.push('criticality-increase');
|
|
88
|
+
if (protectedKnowledge(before)) {
|
|
89
|
+
if (!sameScope(before, proposed))
|
|
90
|
+
reasons.push('protected-applicability-change');
|
|
91
|
+
if (before.priority !== proposed.priority)
|
|
92
|
+
reasons.push('protected-priority-change');
|
|
93
|
+
if ((before.category !== proposed.category || JSON.stringify(evidenceSentences(before.content)) !== JSON.stringify(evidenceSentences(proposed.content))) && !equivalentSources(proposed, options.sources ?? [before]))
|
|
94
|
+
reasons.push('protected-meaning-change');
|
|
95
|
+
if ((proposed.relations ?? []).some(r => ['superseded_by', 'consolidated_into', 'contradicts'].includes(r.type) && !(before.relations ?? []).some(old => old.type === r.type && old.target === r.target)))
|
|
96
|
+
reasons.push('protected-topology-change');
|
|
97
|
+
if (before.status !== proposed.status && !options.retirementCovered)
|
|
98
|
+
reasons.push('protected-retirement');
|
|
99
|
+
}
|
|
100
|
+
// Ordinary inference may refine relevant knowledge but cannot broaden its scope.
|
|
101
|
+
if (!sameScope(before, proposed) && !(before.contextPolicy === 'relevant' && before.authority === 'inferred' && proposed.derivedFrom.length > 0 && narrowsApplicability(before, proposed)))
|
|
102
|
+
reasons.push('applicability-change');
|
|
103
|
+
if (!reasons.length)
|
|
104
|
+
return { decision: 'allowed', item: proposed, reasons: [], blocking: false };
|
|
105
|
+
return { decision: options.exact ? 'review_required' : 'review_recommended', item: structuredClone(before), reasons: [...new Set(reasons)], blocking: !!options.exact };
|
|
106
|
+
}
|
|
107
|
+
export class KnowledgePolicyReviewRequired extends Error {
|
|
108
|
+
transition;
|
|
109
|
+
code = 'POLICY_REVIEW_REQUIRED';
|
|
110
|
+
constructor(transition) {
|
|
111
|
+
super('POLICY_REVIEW_REQUIRED: The requested rule change needs trusted user intent or an exact policy review. Existing knowledge was preserved.');
|
|
112
|
+
this.transition = transition;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** Optional diagnostics, not an approval queue and never a Prepare blocker. */
|
|
116
|
+
export function summarizePolicyDecisions(decisions) {
|
|
117
|
+
return { blocking: decisions.some(d => d.blocking), recommendations: decisions.filter(d => d.decision === 'review_recommended').length,
|
|
118
|
+
safeFallbacks: decisions.filter(d => d.decision === 'allowed_with_safe_fallback').length, reasons: [...new Set(decisions.flatMap(d => d.reasons))], interactivePrompts: 0 };
|
|
119
|
+
}
|
|
120
|
+
const batchReviewContext = new AsyncLocalStorage();
|
|
121
|
+
export async function withTrustedKnowledgeBatch(root, entries, evidence, operation) {
|
|
122
|
+
if (!evidence.trim() || !entries.length)
|
|
123
|
+
throw new Error('Exact trusted batch review required');
|
|
124
|
+
const grant = { active: true, root: path.resolve(root), entries: structuredClone(entries), evidence };
|
|
125
|
+
try {
|
|
126
|
+
return await batchReviewContext.run(grant, operation);
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
grant.active = false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function reviewedBatchAllows(root, proposed, before) {
|
|
133
|
+
const g = batchReviewContext.getStore();
|
|
134
|
+
// Canonical normalization eliminates presentation aliases, not semantic metadata.
|
|
135
|
+
const canonical = (i) => JSON.stringify(Object.fromEntries(Object.entries(i).filter(([k, v]) => v !== undefined && !['kind', 'enforcement'].includes(k)).sort(([a], [b]) => a.localeCompare(b))));
|
|
136
|
+
return !!g?.active && g.root === path.resolve(root) && g.entries.some(e => canonical(e.after) === canonical(proposed) && (!e.before ? !before : !!before && canonical(e.before) === canonical(before)));
|
|
137
|
+
}
|
|
138
|
+
/** Diagnostic reference only; never grants permission when replayed from history. */
|
|
139
|
+
export function trustedKnowledgeBatchEvidence(root) {
|
|
140
|
+
const grant = batchReviewContext.getStore();
|
|
141
|
+
return grant?.active && grant.root === path.resolve(root) ? grant.evidence : null;
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=knowledge-authority.js.map
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { copyStorageTree, ensureStorageDirectory, ensureDataDirectory } from "./storage.js";
|
|
2
|
+
import { PRIVATE_FILE_MODE } from "../filesystem/permissions.js";
|
|
3
|
+
import { validateRevision, revisionHash } from './curation-revision.js';
|
|
4
|
+
import { renderReviewProjection } from './curation-review-state.js';
|
|
5
|
+
import fs from 'node:fs/promises';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { loadMemoryCatalog } from './memory-index.js';
|
|
8
|
+
import { readConfig, checkedDataPath, sha256, listCheckpoints } from './storage.js';
|
|
9
|
+
import { parseKnowledge, renderKnowledge } from './memory.js';
|
|
10
|
+
import { KNOWLEDGE_ANALYSIS_VERSION } from './knowledge-analysis.js';
|
|
11
|
+
import { assertSafeCurationData, collectCurationEvidence, evidenceFor } from './curation-evidence.js';
|
|
12
|
+
import { refineCurationClauses } from './curation-clauses.js';
|
|
13
|
+
const CURATION_ENGINE = KNOWLEDGE_ANALYSIS_VERSION + '/clauses-2';
|
|
14
|
+
import { withApplicability, sameApplicability, narrowsApplicability, applicabilityLabel } from './applicability.js';
|
|
15
|
+
import { evaluateKnowledgeTransition, KnowledgePolicyReviewRequired, trustedKnowledgeBatchEvidence } from './knowledge-authority.js';
|
|
16
|
+
import { withSyncLock, ownsSyncLock, recoverSyncTransaction, commitSyncTransaction } from './sync-transaction.js';
|
|
17
|
+
import { buildIndex } from './index.js';
|
|
18
|
+
import { prepareContext } from './context.js';
|
|
19
|
+
import { sortableId } from './ids.js';
|
|
20
|
+
import { estimateTokens, knowledgeText } from './telemetry.js';
|
|
21
|
+
const digest = (value) => sha256(JSON.stringify(value));
|
|
22
|
+
const seal = (plan) => ({ ...plan, planId: digest(plan) });
|
|
23
|
+
const active = (items) => items.filter(i => i.status === 'active');
|
|
24
|
+
function stats(items) { const a = active([...new Map(items.map(i => [i.id, i])).values()]), required = a.filter(i => i.contextPolicy === 'required'); return { records: a.length, required: required.length, projectWideRequired: required.filter(i => i.applicability?.kind === 'project' || i.applicability?.kind === 'legacy' && i.scope === 'global').length, scopedRequired: required.filter(i => i.applicability?.kind === 'scoped').length, relevant: a.filter(i => i.contextPolicy === 'relevant').length, unresolved: a.filter(i => i.applicability?.kind === 'unresolved').length, requiredTokens: required.reduce((n, i) => n + estimateTokens(knowledgeText(i)), 0) }; }
|
|
25
|
+
export function corpusSummary(proposals) { return { consolidated: new Set(proposals.flatMap(p => p.outputs.filter(o => o.id.startsWith('mem_curate_shared_')).map(o => o.id))).size, before: stats(proposals.map(p => p.before)), proposed: stats(proposals.flatMap(p => p.attention.length ? p.before : p.outputs)), decomposed: proposals.filter(p => p.outputs.length > 1 && !p.attention.length).length, attention: proposals.filter(p => p.attention.length).length, unchanged: proposals.filter(p => !p.changes.length || p.attention.length).length, safe: proposals.filter(p => p.changes.length && !p.attention.length).length }; }
|
|
26
|
+
/** Pure read phase: cache rebuilds stay in memory, no plan/history persistence. */
|
|
27
|
+
export async function planKnowledgeCuration(root, options = {}) {
|
|
28
|
+
const config = await readConfig(root);
|
|
29
|
+
if (!config || config.knowledgeSchemaVersion !== 3)
|
|
30
|
+
throw new Error('Update knowledge format before curation');
|
|
31
|
+
const catalog = await loadMemoryCatalog(root, { persistCache: false });
|
|
32
|
+
const all = await Promise.all(catalog.summaries.filter(s => !options.memorySyncSources || options.memorySyncSources.includes(s.id)).map(s => catalog.load(s.id)));
|
|
33
|
+
if (options.memorySyncSources) {
|
|
34
|
+
const ids = [...new Set(options.memorySyncSources)].sort();
|
|
35
|
+
if (!ids.length || ids.length > 24 || ids.length !== options.memorySyncSources.length)
|
|
36
|
+
throw new Error('Invalid Sync analysis scope');
|
|
37
|
+
const proposals = ids.map(id => {
|
|
38
|
+
const before = all.find(i => i.id === id && ['active', 'disputed'].includes(i.status)), summary = catalog.summaries.find(s => s.id === id);
|
|
39
|
+
if (!before || !summary)
|
|
40
|
+
throw new Error('Sync analysis source changed');
|
|
41
|
+
return { id, sourceId: id, revision: catalog.originalHashes.get(id), path: summary.relativePath, before, outputs: [before], group: 'Memory', changes: [], evidence: [], attention: [], confidence: 'high', mapping: [{ start: 0, end: before.content.length, outputId: id }], consolidationSources: ids };
|
|
42
|
+
});
|
|
43
|
+
const canonicalRoot = await fs.realpath(root);
|
|
44
|
+
const plan = seal({ version: 1, engine: CURATION_ENGINE, project: canonicalRoot, knowledgeSchema: 3, memorySyncSources: ids, fingerprint: digest(ids.map(id => [id, catalog.originalHashes.get(id)])), proposals, summary: corpusSummary(proposals), accountedSources: ids.length, evidenceRoot: canonicalRoot, evidenceTruncated: false });
|
|
45
|
+
validateCurationPlan(plan);
|
|
46
|
+
return plan;
|
|
47
|
+
}
|
|
48
|
+
const inventory = await collectCurationEvidence(options.evidenceRoot ?? root);
|
|
49
|
+
const proposals = [];
|
|
50
|
+
for (const before of all.filter(i => i.authority === 'legacy_inherited' && !i.derivedFrom.some(ref => ref.startsWith('curation-source:')) && i.contextPolicy === 'required' && ['active', 'disputed'].includes(i.status))) {
|
|
51
|
+
const summary = catalog.summaries.find(s => s.id === before.id);
|
|
52
|
+
const parts = refineCurationClauses(before.title, before.content);
|
|
53
|
+
const projectEvidence = evidenceFor(before.content, inventory);
|
|
54
|
+
const structured = /^Decision:/m.test(before.content);
|
|
55
|
+
const attention = [];
|
|
56
|
+
const conflicts = (before.relations ?? []).filter(r => r.type === 'contradicts');
|
|
57
|
+
if (before.status === 'disputed' || conflicts.length)
|
|
58
|
+
attention.push(`Existing conflict: ${conflicts.map(r => r.target).join(', ') || 'disputed lifecycle state'}`);
|
|
59
|
+
if (parts.some(p => p.transient))
|
|
60
|
+
attention.push('Contains potentially stale historical state; verify current validity before changing protection.');
|
|
61
|
+
const outputs = parts.map((p, n) => {
|
|
62
|
+
// Parent policy is a ceiling: only a source-supported normative core inherits it.
|
|
63
|
+
const policy = before.contextPolicy === 'required' && p.normative ? 'required' : 'relevant';
|
|
64
|
+
const category = policy === 'required' ? 'instruction' : p.category;
|
|
65
|
+
const app = before.applicability && before.applicability.kind !== 'legacy' && before.applicability.kind !== 'unresolved' ? before.applicability : !structured || p.applicability.kind === 'unresolved' ? (before.applicability ?? { kind: 'legacy' }) : p.applicability;
|
|
66
|
+
return withApplicability({ ...before, id: parts.length === 1 ? before.id : `mem_curate_${digest([before.id, catalog.originalHashes.get(before.id), n]).slice(0, 24)}`, category, contextPolicy: policy, authority: 'legacy_inherited', title: p.title, content: p.content, derivedFrom: [...before.derivedFrom, ...(p.segments ?? [{ start: p.start, end: p.end }]).map(m => `curation-source:${before.id}@${catalog.originalHashes.get(before.id)}#${m.start}-${m.end}`)], related: [...new Set([...before.related, ...(parts.length > 1 ? [before.id] : [])])] }, app);
|
|
67
|
+
});
|
|
68
|
+
// Contradiction metadata is evidence of unresolved policy, never permission to demote it.
|
|
69
|
+
if (before.status === 'disputed' || conflicts.length)
|
|
70
|
+
outputs.splice(0, outputs.length, structuredClone(before));
|
|
71
|
+
if (structured && parts.some(p => p.applicability.kind === 'unresolved'))
|
|
72
|
+
attention.push('Applicability remains unresolved; review the intended project boundary.');
|
|
73
|
+
if (outputs.some(o => !sameApplicability(before, o) && !narrowsApplicability(before, o)))
|
|
74
|
+
attention.push('Proposed applicability is not provably narrower; keep the source until reviewed.');
|
|
75
|
+
const changes = [];
|
|
76
|
+
if (outputs.length > 1)
|
|
77
|
+
changes.push('decomposition');
|
|
78
|
+
if (outputs.some(i => !sameApplicability(before, i)))
|
|
79
|
+
changes.push('applicability-narrowing');
|
|
80
|
+
if (outputs.some(i => i.contextPolicy !== before.contextPolicy))
|
|
81
|
+
changes.push('policy-change');
|
|
82
|
+
if (outputs.some(i => i.category !== before.category))
|
|
83
|
+
changes.push('category-change');
|
|
84
|
+
if (!changes.length)
|
|
85
|
+
outputs.splice(0, outputs.length, structuredClone(before));
|
|
86
|
+
if (changes.length && outputs.some(i => i.contextPolicy === 'required' && (i.applicability?.kind === 'legacy' && i.scope === 'global')))
|
|
87
|
+
attention.push('Required project-wide boundary remains uncertain.');
|
|
88
|
+
const group = outputs.flatMap(i => i.applicability?.kind === 'scoped' ? i.applicability.subsystems ?? i.applicability.paths ?? i.applicability.taskKinds ?? [] : [])[0] ?? 'Project-wide';
|
|
89
|
+
if (outputs.some(o => o.applicability?.kind === 'scoped' && o.applicability.subsystems?.some(n => /^(?:DB|SQL|JSON|HTML|PHP|AI)$/.test(n) || /(?:Harness|Helper)$/.test(n))))
|
|
90
|
+
attention.push('Applicability currently names a generic technology or helper rather than a resolved subsystem; review its real work boundary.');
|
|
91
|
+
const hadAttention = attention.length > 0;
|
|
92
|
+
const investigation = hadAttention || projectEvidence.length ? {
|
|
93
|
+
outcome: attention.length ? (parts.length > 1 ? 'reduced-ambiguity' : 'needs-decision') : parts.every(p => p.transient) ? 'historical-only' : 'resolved-proposal',
|
|
94
|
+
state: 'cannot-determine',
|
|
95
|
+
evidence: projectEvidence,
|
|
96
|
+
explanation: attention.length ? 'Evidence informs the proposal but cannot authorize choosing between protected meanings.' : 'Supporting history remains traceable; static evidence does not prove test execution or production rollout. Exact batch review is still required.'
|
|
97
|
+
} : undefined;
|
|
98
|
+
const issueSourceIds = new Set([before.id, ...conflicts.map(r => r.target)]);
|
|
99
|
+
const issueBasis = [...issueSourceIds].sort().map(id => ({ id, revision: catalog.originalHashes.get(id) ?? 'missing' }));
|
|
100
|
+
proposals.push({ issueBasis, investigation, id: before.id, sourceId: before.id, revision: catalog.originalHashes.get(before.id), path: summary.relativePath, before, outputs, group, changes, evidence: [...parts.flatMap(p => p.evidence), ...projectEvidence.map(e => `${e.kind}: ${e.path}:${e.line} (${e.revision.slice(0, 12)}) ${e.excerpt}`)], attention, confidence: attention.length ? 'limited' : 'high', mapping: before.status === 'disputed' || conflicts.length ? [{ start: 0, end: before.content.length, outputId: before.id }] : parts.flatMap((p, n) => (p.segments ?? [{ start: p.start, end: p.end }]).map(m => ({ ...m, outputId: outputs[n]?.id ?? before.id }))).sort((a, b) => a.start - b.start) });
|
|
101
|
+
}
|
|
102
|
+
// Only source-identical normative cores with equal applicability/authority can
|
|
103
|
+
// share an output. Similar vocabulary is never a semantic equivalence proof.
|
|
104
|
+
const cores = new Map();
|
|
105
|
+
for (const p of proposals.filter(p => !p.attention.length))
|
|
106
|
+
for (const o of p.outputs.filter(o => o.contextPolicy === 'required')) {
|
|
107
|
+
const key = digest([o.content, o.applicability, o.scope, o.scopeRef, o.authority, o.priority, p.path.startsWith('local/')]);
|
|
108
|
+
cores.set(key, [...(cores.get(key) ?? []), { proposal: p, output: o }]);
|
|
109
|
+
}
|
|
110
|
+
for (const [key, group] of cores)
|
|
111
|
+
if (new Set(group.map(g => g.proposal.id)).size > 1) {
|
|
112
|
+
const shared = { ...group[0].output, id: `mem_curate_shared_${key.slice(0, 20)}` };
|
|
113
|
+
for (const field of ['sourceSessions', 'derivedFrom', 'supersedes', 'related'])
|
|
114
|
+
shared[field] = [...new Set(group.flatMap(g => g.output[field]))];
|
|
115
|
+
shared.related = [...new Set([...shared.related, ...group.map(g => g.proposal.id)])];
|
|
116
|
+
shared.evidence = Array.from(new Map(group.flatMap(g => (g.output.evidence ?? []).map(e => [JSON.stringify(e), e]))).values());
|
|
117
|
+
shared.relations = Array.from(new Map(group.flatMap(g => (g.output.relations ?? []).map(e => [JSON.stringify(e), e]))).values());
|
|
118
|
+
for (const g of group) {
|
|
119
|
+
const old = g.output.id;
|
|
120
|
+
g.proposal.outputs = g.proposal.outputs.map(o => o.id === old ? structuredClone(shared) : o);
|
|
121
|
+
g.proposal.mapping.forEach(m => { if (m.outputId === old)
|
|
122
|
+
m.outputId = shared.id; });
|
|
123
|
+
g.proposal.consolidationSources = group.map(g => g.proposal.id);
|
|
124
|
+
g.proposal.changes.push('normative-consolidation');
|
|
125
|
+
g.proposal.evidence.push('Identical normative core, applicability, authority and priority; all source provenance retained.');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const fingerprint = digest(catalog.summaries.map(s => [s.id, catalog.originalHashes.get(s.id)]).sort());
|
|
129
|
+
const plan = seal({ version: 1, engine: CURATION_ENGINE, project: path.resolve(root), knowledgeSchema: 3, fingerprint, proposals, summary: corpusSummary(proposals), accountedSources: proposals.length, evidenceRoot: inventory.root, evidenceTruncated: inventory.truncated, ...(inventory.sensitiveExcluded ? { evidenceExclusions: [{ reason: 'Sensitive evidence excluded', count: inventory.sensitiveExcluded }] } : {}) });
|
|
130
|
+
validateCurationPlan(plan);
|
|
131
|
+
return plan;
|
|
132
|
+
}
|
|
133
|
+
export function validateCurationPlan(plan) {
|
|
134
|
+
assertSafeCurationData(plan);
|
|
135
|
+
const { planId, ...body } = plan;
|
|
136
|
+
if (digest(body) !== planId)
|
|
137
|
+
throw new Error('Curation plan changed');
|
|
138
|
+
if (plan.engine !== CURATION_ENGINE || plan.knowledgeSchema !== 3 || new Set(plan.proposals.map(p => p.sourceId)).size !== plan.proposals.length || plan.accountedSources !== plan.proposals.length)
|
|
139
|
+
throw new Error('Invalid curation accounting');
|
|
140
|
+
for (const p of plan.proposals) {
|
|
141
|
+
if (!p.outputs.length || p.before.id !== p.sourceId)
|
|
142
|
+
throw new Error('Unaccounted curation source');
|
|
143
|
+
for (const o of p.outputs) {
|
|
144
|
+
const parsed = parseKnowledge(renderKnowledge(o), `${o.id}.md`, 3);
|
|
145
|
+
if (parsed.category !== o.category || parsed.status !== o.status || parsed.content !== o.content)
|
|
146
|
+
throw new Error("Invalid canonical proposal");
|
|
147
|
+
if (!p.before.sourceSessions.every(v => o.sourceSessions.includes(v)) || !p.before.derivedFrom.every(v => o.derivedFrom.includes(v)) || !(p.before.evidence ?? []).every(v => (o.evidence ?? []).some(e => JSON.stringify(e) === JSON.stringify(v))) || !p.before.related.every(v => o.related.includes(v)) || !p.before.supersedes.every(v => o.supersedes.includes(v)))
|
|
148
|
+
throw new Error('Curation provenance or relationships were lost');
|
|
149
|
+
if (o.authority !== p.before.authority || o.priority !== p.before.priority)
|
|
150
|
+
throw new Error('Curation cannot infer authority or criticality');
|
|
151
|
+
}
|
|
152
|
+
if (p.outputs.length === 1 && p.outputs[0].content.trim() !== p.before.content.trim())
|
|
153
|
+
throw new Error("Unaccounted source meaning");
|
|
154
|
+
if (p.outputs.length > 1) {
|
|
155
|
+
const reproduced = p.mapping.map(m => p.before.content.slice(m.start, m.end)).join('');
|
|
156
|
+
if (reproduced.replace(/\s+/g, ' ') !== p.before.content.trim().replace(/\s+/g, ' '))
|
|
157
|
+
throw new Error('Unaccounted source clauses');
|
|
158
|
+
for (const output of p.outputs) {
|
|
159
|
+
if (output.content !== p.mapping.filter(m => m.outputId === output.id).map(m => p.before.content.slice(m.start, m.end).trim()).join('\n\n'))
|
|
160
|
+
throw new Error('Invalid clause mapping');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function selected(plan, ids, revision) {
|
|
166
|
+
if (revision) {
|
|
167
|
+
validateRevision(revision);
|
|
168
|
+
if (revisionHash(revision.typedPlan.plan) !== revisionHash(plan))
|
|
169
|
+
throw new Error('Revision plan binding changed');
|
|
170
|
+
}
|
|
171
|
+
else
|
|
172
|
+
validateCurationPlan(plan);
|
|
173
|
+
if (!ids.length || new Set(ids).size !== ids.length)
|
|
174
|
+
throw new Error('Select a non-empty exact batch');
|
|
175
|
+
return ids.map(id => {
|
|
176
|
+
const p = plan.proposals.find(p => p.id === id);
|
|
177
|
+
if (!p || p.attention.length || !p.changes.length)
|
|
178
|
+
throw new Error(`Proposal needs individual analysis or has no change: ${id}`);
|
|
179
|
+
return p;
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/** Exact review material for a trusted host adapter; never itself a capability. */
|
|
183
|
+
function proposalEntries(p) { return p.outputs.length === 1 && p.outputs[0].id === p.sourceId ? [{ before: p.before, after: p.outputs[0] }] : [...p.outputs.map(after => ({ before: undefined, after })), { before: p.before, after: { ...p.before, status: 'superseded', related: [...new Set([...p.before.related, ...p.outputs.map(i => i.id)])] } }]; }
|
|
184
|
+
export function curationReviewEntries(plan, ids, revision) { const chosen = selected(plan, ids, revision); for (const p of chosen)
|
|
185
|
+
if (p.consolidationSources?.some(id => !ids.includes(id)))
|
|
186
|
+
throw new Error('Select the complete normative consolidation group'); return [...new Map(chosen.flatMap(proposalEntries).map(e => [e.after.id, e])).values()]; }
|
|
187
|
+
/** A pre-commit source/evidence conflict; no curation writes have started. */
|
|
188
|
+
export class StaleCurationPlanError extends Error {
|
|
189
|
+
}
|
|
190
|
+
export async function applyKnowledgeCuration(root, plan, ids, receipt) {
|
|
191
|
+
if (plan.memorySyncSources)
|
|
192
|
+
throw new Error('Sync analysis cannot grant curation Apply authority');
|
|
193
|
+
if (path.resolve(root) !== plan.project)
|
|
194
|
+
throw new Error('Curation project mismatch');
|
|
195
|
+
const chosen = selected(plan, ids, receipt?.reviewSnapshot?.revision), entries = curationReviewEntries(plan, ids, receipt?.reviewSnapshot?.revision);
|
|
196
|
+
// No approved boolean can mint this capability. Fail before mutation/lock.
|
|
197
|
+
for (const e of entries) {
|
|
198
|
+
const d = evaluateKnowledgeTransition(root, e.after, e.before, { exact: true });
|
|
199
|
+
if (d.decision !== 'allowed')
|
|
200
|
+
throw new KnowledgePolicyReviewRequired(d);
|
|
201
|
+
}
|
|
202
|
+
const commit = async () => {
|
|
203
|
+
await recoverSyncTransaction(root);
|
|
204
|
+
const prior = (await listCheckpoints(root)).find(c => receipt?.reviewRevisionId ? c.curation?.review?.operationId === receipt.operationId : c.curation?.planId === plan.planId && digest(c.curation.selected) === digest(ids));
|
|
205
|
+
if (prior) {
|
|
206
|
+
await buildIndex(root);
|
|
207
|
+
return { checkpoint: prior.id, repeated: true };
|
|
208
|
+
}
|
|
209
|
+
for (const e of chosen.flatMap(p => p.investigation?.evidence ?? [])) {
|
|
210
|
+
const absolute = path.resolve(plan.evidenceRoot, e.path);
|
|
211
|
+
try {
|
|
212
|
+
if (!absolute.startsWith(path.resolve(plan.evidenceRoot) + path.sep) || await fs.realpath(absolute) !== absolute || sha256(await fs.readFile(absolute, 'utf8')) !== e.revision)
|
|
213
|
+
throw new StaleCurationPlanError(`Stale project evidence: ${e.path}`);
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
if (error.code === 'ENOENT')
|
|
217
|
+
throw new StaleCurationPlanError(`Missing project evidence: ${e.path}`);
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const writes = [];
|
|
222
|
+
const preimages = [];
|
|
223
|
+
for (const p of chosen) {
|
|
224
|
+
const current = await fs.readFile(await checkedDataPath(root, p.path), 'utf8').catch(error => {
|
|
225
|
+
if (error.code === 'ENOENT')
|
|
226
|
+
throw new StaleCurationPlanError(`Missing curation source: ${p.sourceId}`);
|
|
227
|
+
throw error;
|
|
228
|
+
});
|
|
229
|
+
if (sha256(current) !== p.revision)
|
|
230
|
+
throw new StaleCurationPlanError(`Stale curation source: ${p.sourceId}`);
|
|
231
|
+
preimages.push({ path: p.path, revision: p.revision, content: current });
|
|
232
|
+
}
|
|
233
|
+
for (const e of entries) {
|
|
234
|
+
const p = chosen.find(p => p.sourceId === e.before?.id || p.outputs.some(i => i.id === e.after.id));
|
|
235
|
+
writes.push({ path: e.before ? p.path : `${p.path.startsWith('local/') ? 'local/' : ''}memory/${e.after.id}.md`, before: e.before ? p.revision : null, after: renderKnowledge(e.after) });
|
|
236
|
+
}
|
|
237
|
+
const id = sortableId('chk');
|
|
238
|
+
writes.push({ path: `checkpoints/${id}.json`, before: null, after: JSON.stringify({ id, createdAt: new Date().toISOString(), previousCheckpoint: null, sessions: [], createdMemory: entries.filter(e => !e.before).map(e => e.after.id), updatedMemory: ids, supersededMemory: entries.filter(e => e.after.status === 'superseded').map(e => e.after.id), duplicateMemory: [], conflicts: [], curation: { ...(receipt ? { review: receipt } : {}), reviewEvidence: trustedKnowledgeBatchEvidence(root), planId: plan.planId, engine: plan.engine, selected: ids, leftUnchanged: plan.proposals.filter(p => !ids.includes(p.id)).map(p => p.id), plan, preimages, postimages: writes.map(w => ({ path: w.path, revision: sha256(w.after), content: w.after })) } }, null, 2) + '\n' });
|
|
239
|
+
await commitSyncTransaction(root, writes, { policyMode: 'exact' });
|
|
240
|
+
await buildIndex(root);
|
|
241
|
+
return { checkpoint: id, repeated: false };
|
|
242
|
+
};
|
|
243
|
+
return ownsSyncLock(root) ? commit() : withSyncLock(root, commit);
|
|
244
|
+
}
|
|
245
|
+
export async function curationRecommendation(root) {
|
|
246
|
+
const plan = await planKnowledgeCuration(root);
|
|
247
|
+
const { curationReviewView } = await import('./curation-review.js');
|
|
248
|
+
const { review, projection } = await curationReviewView(root, plan);
|
|
249
|
+
return { planId: plan.planId, reviewId: review?.snapshot.reviewId, state: projection.lifecycle,
|
|
250
|
+
prominent: !projection.paused && (projection.needsInputNow > 0 || projection.readyChangeGroups > 0), projection };
|
|
251
|
+
}
|
|
252
|
+
export async function deferKnowledgeCuration(root, planId) {
|
|
253
|
+
const plan = await planKnowledgeCuration(root);
|
|
254
|
+
if (plan.planId !== planId)
|
|
255
|
+
throw new Error('Curation changed; refresh');
|
|
256
|
+
return withSyncLock(root, async () => { await recoverSyncTransaction(root); const id = sortableId('chk'); await commitSyncTransaction(root, [{ path: `checkpoints/${id}.json`, before: null, after: JSON.stringify({ id, createdAt: new Date().toISOString(), sessions: [], curationDefer: planId }) + '\n' }]); return { state: 'deferred' }; });
|
|
257
|
+
}
|
|
258
|
+
// Explicitly labelled examples, not a claim of representative project coverage.
|
|
259
|
+
export const CURATION_TASKS = ['Synthetic example: review documentation', 'Synthetic example: run unit tests', 'Synthetic example: review deployment'];
|
|
260
|
+
function contextMetrics(c) { return { unknownRequired: c.applicability?.unknownRequired ?? 0, applicabilityDecisions: c.applicability?.decisions, requiredIds: c.required.map(i => i.id), requiredCandidates: c.required.length + c.metrics.requiredGlobalExcludedItems, requiredCandidateTokens: c.metrics.requiredGlobalCandidateTokens, requiredSelected: c.required.length, requiredSelectedTokens: c.metrics.requiredGlobalTokens, requiredOmissions: c.metrics.requiredGlobalExcludedItems, relevantSelected: c.relevant.length, relevantOmittedByItemLimit: c.selection.omittedRelevantByItemLimit, relevantOmittedByMarginalValue: c.selection.omittedRelevantByMarginalValue ?? 0, allocation: c.allocation, totalContextTokens: c.metrics.contextTokens, pressure: c.metrics.requiredGlobalPressure, contextNeeded: c.policyReadiness?.state === 'context-needed', readiness: c.policyReadiness, limits: c.limits }; }
|
|
261
|
+
/** Real selector over an isolated canonical corpus; no lifecycle Prepare/Sync. */
|
|
262
|
+
export async function simulateKnowledgeCuration(root, plan, tasks = CURATION_TASKS, mode = 'safe-batch', selectedIds, revision) {
|
|
263
|
+
if (revision) {
|
|
264
|
+
validateRevision(revision);
|
|
265
|
+
if (revision.typedPlan.plan.planId !== plan.planId)
|
|
266
|
+
throw new Error('Simulation revision mismatch');
|
|
267
|
+
}
|
|
268
|
+
else
|
|
269
|
+
validateCurationPlan(plan);
|
|
270
|
+
if (path.resolve(root) !== plan.project)
|
|
271
|
+
throw new Error('Simulation project mismatch');
|
|
272
|
+
if (selectedIds?.length)
|
|
273
|
+
curationReviewEntries(plan, selectedIds, revision);
|
|
274
|
+
const temp = await fs.mkdtemp(path.join(await ensureDataDirectory(root, '.cache'), 'prodero-curation-simulation-'));
|
|
275
|
+
try {
|
|
276
|
+
await ensureStorageDirectory(path.join(temp, '.prodero'), true);
|
|
277
|
+
const config = await readConfig(root);
|
|
278
|
+
await fs.writeFile(path.join(temp, '.prodero/config.json'), JSON.stringify(config), { mode: PRIVATE_FILE_MODE });
|
|
279
|
+
// Copy only canonical stores consumed by the selector, never caches or sessions.
|
|
280
|
+
for (const dir of ['memory', 'local/memory', 'skills', 'local/skills']) {
|
|
281
|
+
const from = await checkedDataPath(root, dir);
|
|
282
|
+
try {
|
|
283
|
+
await copyStorageTree(from, path.join(temp, '.prodero', dir), true);
|
|
284
|
+
}
|
|
285
|
+
catch (e) {
|
|
286
|
+
if (e.code !== 'ENOENT')
|
|
287
|
+
throw e;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const copied = await loadMemoryCatalog(temp, { persistCache: false });
|
|
291
|
+
await Promise.all(copied.summaries.map(i => copied.load(i.id)));
|
|
292
|
+
if (digest(copied.summaries.map(i => [i.id, copied.originalHashes.get(i.id)]).sort()) !== plan.fingerprint)
|
|
293
|
+
throw new Error('Simulation corpus changed since planning');
|
|
294
|
+
const before = await Promise.all(tasks.map(async (task) => ({ task, before: contextMetrics(await prepareContext(temp, { query: task })) })));
|
|
295
|
+
for (const p of plan.proposals) {
|
|
296
|
+
if (sha256(await fs.readFile(await checkedDataPath(temp, p.path), 'utf8')) !== p.revision)
|
|
297
|
+
throw new Error(`Stale simulation source: ${p.id}`);
|
|
298
|
+
if ((mode === 'safe-batch' && p.attention.length) || !p.changes.length || (selectedIds && !selectedIds.includes(p.id)))
|
|
299
|
+
continue;
|
|
300
|
+
for (const entry of proposalEntries(p))
|
|
301
|
+
await fs.writeFile(await checkedDataPath(temp, entry.before ? p.path : `${p.path.startsWith('local/') ? 'local/' : ''}memory/${entry.after.id}.md`), renderKnowledge(entry.after), { mode: PRIVATE_FILE_MODE });
|
|
302
|
+
}
|
|
303
|
+
return await Promise.all(before.map(async (row) => ({ ...row, after: contextMetrics(await prepareContext(temp, { query: row.task })) })));
|
|
304
|
+
}
|
|
305
|
+
finally {
|
|
306
|
+
await fs.rm(temp, { recursive: true, force: true });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
export function renderCurationText(plan, projection) {
|
|
310
|
+
return `Knowledge cleanup\n${renderReviewProjection(projection)}.\nNo knowledge is removed automatically.\n` + plan.proposals.map(p => {
|
|
311
|
+
const item = projection.items.find(i => i.proposalIds.includes(p.id));
|
|
312
|
+
const status = item.issue?.state === 'deferred' ? 'Deferred clarification' : item.disposition === 'keep_current' ? 'Keep current' : item.disposition === 'deferred' && item.outcome === 'proposed' ? 'Deferred change' : item.outcome === 'unresolved' ? 'Needs clarification' : item.outcome === 'proposed' ? 'Proposed change' : 'Unchanged';
|
|
313
|
+
return `${p.group} · ${p.before.title}\n${status}\nCurrently: ${p.before.contextPolicy} · ${applicabilityLabel(p.before)}\n${item.outcome === 'unresolved' ? 'No actionable suggestion is available yet.' : `Suggested: ${p.outputs.map(i => `${i.category}, ${i.contextPolicy}, ${applicabilityLabel(i)}`).join('; ')}`}\nWhy: ${p.evidence.join('; ')}`;
|
|
314
|
+
}).join('\n\n');
|
|
315
|
+
}
|
|
316
|
+
//# sourceMappingURL=knowledge-curation.js.map
|