@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,169 @@
|
|
|
1
|
+
import { applicabilitySchema, withApplicability, sameApplicability, applicabilityKey, inferApplicability } from "./applicability.js";
|
|
2
|
+
import { assertCompatibleProject } from "./storage.js";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import { sortableId } from "./ids.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { listKnowledge, renderKnowledge } from "./memory.js";
|
|
7
|
+
import { listProjectSkills, renderSkill, skillInputSchema } from "./skills.js";
|
|
8
|
+
import { readConfig, sha256, listCheckpoints } from "./storage.js";
|
|
9
|
+
import { checkedPath, hasSecret, readSource, safeRelative, snapshotImportState } from "./plugin-files.js";
|
|
10
|
+
import { commitSyncTransaction, recoverSyncTransaction, withSyncLock } from "./sync-transaction.js";
|
|
11
|
+
import { buildIndex } from "./index.js";
|
|
12
|
+
import { estimateTokens, knowledgeText, MAX_CANONICAL_ITEM_TOKENS } from "./telemetry.js";
|
|
13
|
+
import { assertEnglishProse } from "./language.js";
|
|
14
|
+
const hash = z.string().regex(/^[a-f0-9]{64}$/);
|
|
15
|
+
const text = z.string().trim().min(1);
|
|
16
|
+
export const importEvidenceSchema = z.object({ path: text.max(400), sha256: hash, startLine: z.number().int().positive(), endLine: z.number().int().positive() }).strict();
|
|
17
|
+
export const importItemSchema = z.object({
|
|
18
|
+
applicability: applicabilitySchema.optional(), scopeKind: z.enum(["path", "topic"]).optional(),
|
|
19
|
+
key: text.max(100), destination: z.enum(["project_instruction", "knowledge", "skill"]),
|
|
20
|
+
title: text.max(200).regex(/^[^\r\n]+$/), content: text.max(8000),
|
|
21
|
+
scope: z.enum(["global", "module", "task"]), scopeRef: text.max(300).nullable(),
|
|
22
|
+
category: z.enum(["principle", "invariant", "decision", "constraint", "lesson", "state", "instruction"]),
|
|
23
|
+
evidence: z.array(importEvidenceSchema).min(1).max(12), reason: text.max(1000),
|
|
24
|
+
persistEvidence: z.boolean().optional(),
|
|
25
|
+
skill: skillInputSchema.strict().nullable(), activateSkill: z.boolean(),
|
|
26
|
+
}).strict();
|
|
27
|
+
export const importBatchSchema = z.object({
|
|
28
|
+
version: z.literal(1), planId: hash, approvedPlanId: hash,
|
|
29
|
+
stateFingerprint: hash, items: z.array(importItemSchema).max(100),
|
|
30
|
+
}).strict();
|
|
31
|
+
export async function readImportState(root) {
|
|
32
|
+
if (!(await readConfig(root)))
|
|
33
|
+
throw new Error("Initialize Prodero before importing knowledge");
|
|
34
|
+
const files = await snapshotImportState(root);
|
|
35
|
+
return { knowledge: await listKnowledge(root), skills: await listProjectSkills(root), fingerprint: sha256(JSON.stringify(files)), files, importedSources: (await listCheckpoints(root)).flatMap(c => c.importReceipt ? [...c.importReceipt.created, ...c.importReceipt.duplicates].flatMap(i => i.source ?? []) : []), importedKeys: (await listCheckpoints(root)).flatMap(c => c.importReceipt ? [...c.importReceipt.created, ...c.importReceipt.duplicates].map(i => i.key) : []) };
|
|
36
|
+
}
|
|
37
|
+
const normalized = (content) => content.trim().replace(/\s+/g, " ").toLowerCase();
|
|
38
|
+
/** Read-only preflight is shared by proposal review and the durable apply boundary. */
|
|
39
|
+
export async function validateImportItems(root, raw) {
|
|
40
|
+
await assertCompatibleProject(root);
|
|
41
|
+
const items = z.array(importItemSchema).max(100).parse(raw);
|
|
42
|
+
if (new Set(items.map(i => i.key)).size !== items.length)
|
|
43
|
+
throw new Error("Duplicate import item key");
|
|
44
|
+
if (hasSecret(JSON.stringify(items)))
|
|
45
|
+
throw new Error("Potential secret in import proposal");
|
|
46
|
+
// Validate every proposal and evidence before taking the writer lock or touching durable state.
|
|
47
|
+
for (const item of items) {
|
|
48
|
+
assertEnglishProse([item.title, item.content, item.reason, item.skill?.title, item.skill?.description, item.skill?.scope, ...(item.skill?.triggers ?? []), item.skill?.content], "Imported Prodero knowledge");
|
|
49
|
+
if (item.destination !== "skill" && estimateTokens(knowledgeText(item)) > MAX_CANONICAL_ITEM_TOKENS)
|
|
50
|
+
throw new Error("Knowledge exceeds canonical token budget");
|
|
51
|
+
if ((item.scope === "global") !== (item.scopeRef === null))
|
|
52
|
+
throw new Error("Invalid scope boundary");
|
|
53
|
+
if (item.destination === "project_instruction" && item.category !== "instruction")
|
|
54
|
+
throw new Error("Project rules require instruction category");
|
|
55
|
+
if (item.destination === "skill" ? !item.skill : item.skill !== null || item.activateSkill)
|
|
56
|
+
throw new Error("Invalid skill destination");
|
|
57
|
+
if (item.skill && (item.title !== item.skill.title || item.content !== item.skill.content || item.skill.relatedKnowledge.length))
|
|
58
|
+
throw new Error("Skill proposal must match its reviewed content and use evidence references");
|
|
59
|
+
if (item.skill && estimateTokens(renderSkill({ ...item.skill, id: "validation", status: "candidate", origin: "legacy", createdAt: "", updatedAt: "", sourceSessions: [], derivedFrom: [], proposedReason: item.reason, reviewedAt: null, reviewReason: null, path: "skills/validation.md" })) > 1500)
|
|
60
|
+
throw new Error("Skill exceeds context budget; distill the procedure");
|
|
61
|
+
for (const reference of item.skill?.references ?? [])
|
|
62
|
+
safeRelative(reference);
|
|
63
|
+
for (const evidence of item.evidence) {
|
|
64
|
+
safeRelative(evidence.path);
|
|
65
|
+
const source = await readSource(root, evidence.path);
|
|
66
|
+
if (source.sha256 !== evidence.sha256 || evidence.endLine < evidence.startLine || evidence.endLine > source.content.split("\n").length)
|
|
67
|
+
throw new Error("Source evidence changed or line range invalid");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** No plugin-supplied write paths, overwrites or model execution. Exact approved input only. */
|
|
72
|
+
export async function applyImport(root, plugin, raw) {
|
|
73
|
+
const batch = importBatchSchema.parse(raw);
|
|
74
|
+
if (batch.approvedPlanId !== batch.planId)
|
|
75
|
+
throw new Error("Exact plan approval required");
|
|
76
|
+
await validateImportItems(root, batch.items);
|
|
77
|
+
const batchHash = sha256(JSON.stringify(batch));
|
|
78
|
+
return withSyncLock(root, async () => {
|
|
79
|
+
await recoverSyncTransaction(root);
|
|
80
|
+
const checkpoints = await listCheckpoints(root);
|
|
81
|
+
const previousImport = checkpoints.find(c => c.importReceipt?.plugin === plugin.id && c.importReceipt?.planId === batch.planId);
|
|
82
|
+
if (previousImport?.importReceipt) {
|
|
83
|
+
const receipt = previousImport.importReceipt;
|
|
84
|
+
if (receipt.batchHash !== batchHash)
|
|
85
|
+
throw new Error("Applied plan differs from requested batch");
|
|
86
|
+
await checkedPath(root, ".prodero/index.md");
|
|
87
|
+
await buildIndex(root);
|
|
88
|
+
return receipt;
|
|
89
|
+
}
|
|
90
|
+
// Reuse Core's chronological ID contract; the receipt is the stable replay key.
|
|
91
|
+
const checkpoint = sortableId("chk");
|
|
92
|
+
const state = await readImportState(root);
|
|
93
|
+
if (state.fingerprint !== batch.stateFingerprint)
|
|
94
|
+
throw new Error("Prodero state changed since audit; create a fresh reviewed plan");
|
|
95
|
+
const writes = [];
|
|
96
|
+
const now = new Date().toISOString();
|
|
97
|
+
const receipt = { version: 1, plugin: plugin.id, pluginVersion: plugin.version, planId: batch.planId, batchHash, checkpoint, created: [], duplicates: [], before: state.files };
|
|
98
|
+
const knowledge = [...state.knowledge], skills = [...state.skills];
|
|
99
|
+
for (const item of batch.items) {
|
|
100
|
+
const applicability = item.applicability ?? (item.scopeRef ? { kind: "scoped", ...(item.scopeKind === "path" || /[/.]/.test(item.scopeRef) ? { paths: [item.scopeRef] } : { subsystems: [item.scopeRef] }) } : inferApplicability(item.content));
|
|
101
|
+
const signature = item.destination === "skill" ? `skill:${item.skill.scope}:${normalized(item.content)}` : `${item.destination}:${applicabilityKey({ ...item, applicability })}:${item.category}:${normalized(item.content)}`;
|
|
102
|
+
const id = `${item.destination === "skill" ? "skill" : "mem"}_import_${sha256(signature).slice(0, 32)}`;
|
|
103
|
+
const provenance = item.persistEvidence !== false ? item.evidence.map(e => `${e.path}#L${e.startLine}-L${e.endLine}@${e.sha256}`) : [];
|
|
104
|
+
const derivedFrom = [...provenance, `plugin:${plugin.id}@${plugin.version}/plan:${batch.planId}`];
|
|
105
|
+
let target, after;
|
|
106
|
+
if (item.destination === "skill") {
|
|
107
|
+
const prior = skills.find(s => s.status !== "superseded" && normalized(s.content) === normalized(item.content) && s.scope === item.skill.scope);
|
|
108
|
+
if (prior) {
|
|
109
|
+
const p = `.prodero/${prior.path}`;
|
|
110
|
+
receipt.duplicates.push({ key: item.key, path: p, sha256: sha256(await fs.readFile(await checkedPath(root, p), "utf8").catch(() => writes.find(w => `.prodero/${w.path}` === p).after)), destination: item.destination, source: item.evidence });
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const skill = { ...item.skill, id, status: item.activateSkill ? "active" : "candidate", origin: "legacy", createdAt: now, updatedAt: now, sourceSessions: [], derivedFrom, proposedReason: item.reason, reviewedAt: item.activateSkill ? now : null, reviewReason: item.activateSkill ? `Approved plan ${batch.planId}` : null, path: `skills/${id}.md` };
|
|
114
|
+
after = renderSkill(skill);
|
|
115
|
+
if (estimateTokens(after) > 1500)
|
|
116
|
+
throw new Error("Skill with provenance exceeds context budget");
|
|
117
|
+
target = skill.path;
|
|
118
|
+
skills.push(skill);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
const prior = knowledge.find(k => k.status === "active" && sameApplicability(k, { ...item, applicability }) && k.category === item.category && normalized(k.content) === normalized(item.content));
|
|
122
|
+
if (prior) {
|
|
123
|
+
const p = `.prodero/memory/${prior.id}.md`;
|
|
124
|
+
receipt.duplicates.push({ key: item.key, path: p, sha256: state.files[p] ?? sha256(writes.find(w => `.prodero/${w.path}` === p).after), destination: item.destination, source: item.evidence });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const knowledgeItem = withApplicability({ version: 3, contextPolicy: "relevant", authority: "imported", id, scope: item.scope, scopeRef: item.scopeRef, category: item.category, priority: "normal", status: "active", title: item.title, content: item.content, createdAt: now, updatedAt: now, sourceSessions: [], derivedFrom, supersedes: [], related: [], evidence: provenance.map(ref => ({ kind: "source_file", ref })) }, applicability);
|
|
128
|
+
if (estimateTokens(knowledgeText(knowledgeItem)) > MAX_CANONICAL_ITEM_TOKENS)
|
|
129
|
+
throw new Error("Knowledge exceeds canonical token budget");
|
|
130
|
+
after = renderKnowledge(knowledgeItem);
|
|
131
|
+
target = `memory/${id}.md`;
|
|
132
|
+
knowledge.push(knowledgeItem);
|
|
133
|
+
}
|
|
134
|
+
writes.push({ path: target, before: null, after });
|
|
135
|
+
receipt.created.push({ key: item.key, path: `.prodero/${target}`, sha256: sha256(after), destination: item.destination, source: item.evidence });
|
|
136
|
+
}
|
|
137
|
+
await checkedPath(root, ".prodero/index.md");
|
|
138
|
+
const record = { id: checkpoint, createdAt: now, previousCheckpoint: checkpoints.at(-1)?.id ?? null, sessions: [], consolidation: "not-performed", createdMemory: receipt.created.filter(i => i.destination !== "skill").map(i => i.path.split("/").at(-1).slice(0, -3)), createdSkills: receipt.created.filter(i => i.destination === "skill").map(i => i.path.split("/").at(-1).slice(0, -3)), updatedMemory: [], supersededMemory: [], duplicateMemory: [], conflicts: [], summary: { sessions: 0, candidates: batch.items.length, created: receipt.created.length, consolidated: 0, duplicates: receipt.duplicates.length, superseded: 0, conflicts: 0 }, importReceipt: receipt };
|
|
139
|
+
try {
|
|
140
|
+
await commitSyncTransaction(root, [...writes, { path: `checkpoints/${checkpoint}.json`, before: null, after: JSON.stringify(record, null, 2) + "\n" }]);
|
|
141
|
+
await buildIndex(root);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
throw new Error("Import interrupted and may be partially applied. Core retains its recovery journal/checkpoint; retry this exact approved plan after resolving the filesystem or concurrent-write error.");
|
|
145
|
+
}
|
|
146
|
+
return receipt;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
export async function verifyImport(root, receipt) {
|
|
150
|
+
await assertCompatibleProject(root);
|
|
151
|
+
const issues = [];
|
|
152
|
+
if (!/^chk_[0-9]{17}_[a-f0-9]{12}$/.test(receipt.checkpoint))
|
|
153
|
+
throw new Error("Invalid receipt checkpoint");
|
|
154
|
+
const record = JSON.parse(await fs.readFile(await checkedPath(root, `.prodero/checkpoints/${receipt.checkpoint}.json`), "utf8"));
|
|
155
|
+
if (JSON.stringify(record.importReceipt) !== JSON.stringify(receipt))
|
|
156
|
+
throw new Error("Receipt does not match durable checkpoint");
|
|
157
|
+
const state = await readImportState(root);
|
|
158
|
+
for (const item of [...receipt.created, ...receipt.duplicates])
|
|
159
|
+
if (state.files[item.path] !== item.sha256)
|
|
160
|
+
issues.push(`Imported item missing or changed: ${item.key}`);
|
|
161
|
+
const allowed = new Set([...receipt.created.map(i => i.path), `.prodero/checkpoints/${receipt.checkpoint}.json`, ".prodero/index.md"]);
|
|
162
|
+
for (const p of new Set([...Object.keys(receipt.before), ...Object.keys(state.files)]))
|
|
163
|
+
if (!allowed.has(p) && receipt.before[p] !== state.files[p])
|
|
164
|
+
issues.push(`Other canonical state changed: ${p}`);
|
|
165
|
+
if (new Set(receipt.created.map(i => i.path)).size !== receipt.created.length)
|
|
166
|
+
issues.push("Duplicate created destination");
|
|
167
|
+
return { valid: issues.length === 0, issues };
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=imports.js.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { PRODERO_SCHEMA_VERSION } from "./constants.js";
|
|
2
|
+
import { loadMemoryCatalog } from "./memory-index.js";
|
|
3
|
+
import { loadKnowledgeGraph } from "./knowledge-graph.js";
|
|
4
|
+
import { checkedDataPath, ensureRuntimeDirectories, writeFileAtomic } from "./storage.js";
|
|
5
|
+
import { listProjectSkills, skillContext } from "./skills.js";
|
|
6
|
+
function line(item) { return `- [${item.title}](memory/${item.filename})`; }
|
|
7
|
+
export const MAX_INDEX_REQUIRED_ITEMS = 16;
|
|
8
|
+
export const MAX_INDEX_CONTEXTUAL_ITEMS = 128;
|
|
9
|
+
function diverseContextual(items, limit) {
|
|
10
|
+
const groups = new Map();
|
|
11
|
+
for (const item of items) {
|
|
12
|
+
const key = item.scopeRef ?? item.scope;
|
|
13
|
+
const group = groups.get(key) ?? [];
|
|
14
|
+
group.push(item);
|
|
15
|
+
groups.set(key, group);
|
|
16
|
+
}
|
|
17
|
+
const orderedGroups = [...groups.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
18
|
+
const selected = [];
|
|
19
|
+
for (let offset = 0; selected.length < limit; offset += 1) {
|
|
20
|
+
let added = false;
|
|
21
|
+
for (const [, group] of orderedGroups) {
|
|
22
|
+
if (group[offset]) {
|
|
23
|
+
selected.push(group[offset]);
|
|
24
|
+
added = true;
|
|
25
|
+
}
|
|
26
|
+
if (selected.length >= limit)
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
if (!added)
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
return selected;
|
|
33
|
+
}
|
|
34
|
+
async function skillReferences(root) {
|
|
35
|
+
return (await listProjectSkills(root)).filter((skill) => skill.status === "active").map(skillContext);
|
|
36
|
+
}
|
|
37
|
+
export async function buildIndex(root) {
|
|
38
|
+
await ensureRuntimeDirectories(root);
|
|
39
|
+
const catalog = await loadMemoryCatalog(root);
|
|
40
|
+
const active = catalog.summaries.filter((item) => item.status === "active" && item.layer === "versioned");
|
|
41
|
+
const local = catalog.summaries.filter((item) => item.status === "active" && item.layer === "local");
|
|
42
|
+
const allRequired = active
|
|
43
|
+
.filter((item) => item.scope === "global" && item.contextPolicy === "required")
|
|
44
|
+
.sort((a, b) => (a.priority === b.priority ? 0 : a.priority === "critical" ? -1 : 1) || b.updatedAt.localeCompare(a.updatedAt) || a.title.localeCompare(b.title) || a.id.localeCompare(b.id));
|
|
45
|
+
const required = allRequired.slice(0, MAX_INDEX_REQUIRED_ITEMS);
|
|
46
|
+
const allRequiredIds = new Set(allRequired.map((item) => item.id));
|
|
47
|
+
const allContextual = active
|
|
48
|
+
.filter((item) => !allRequiredIds.has(item.id))
|
|
49
|
+
.sort((a, b) => (a.scopeRef ?? a.scope).localeCompare(b.scopeRef ?? b.scope) || a.title.localeCompare(b.title) || a.id.localeCompare(b.id));
|
|
50
|
+
const contextual = diverseContextual(allContextual, MAX_INDEX_CONTEXTUAL_ITEMS);
|
|
51
|
+
const skills = await skillReferences(root);
|
|
52
|
+
const text = `---
|
|
53
|
+
prodero_schema: ${PRODERO_SCHEMA_VERSION}
|
|
54
|
+
kind: project-index
|
|
55
|
+
generated: true
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
# Prodero · Project Intelligence
|
|
59
|
+
|
|
60
|
+
> Compact router for progressive context loading. Open only references relevant to the current task.
|
|
61
|
+
|
|
62
|
+
## Required Global Context
|
|
63
|
+
|
|
64
|
+
${required.length ? required.map(line).join("\n") : "_No required global knowledge._"}${allRequired.length > required.length ? `\n\n_${allRequired.length - required.length} additional required item(s) are available through bounded context resolution._` : ""}
|
|
65
|
+
|
|
66
|
+
## Contextual Knowledge
|
|
67
|
+
|
|
68
|
+
${contextual.length ? contextual.map((item) => `${line(item)} — ${item.scope}${item.scopeRef ? `: ${item.scopeRef}` : ""}; ${item.category}; ${item.priority}`).join("\n") : "_No contextual knowledge._"}${allContextual.length > contextual.length ? `\n\n_${allContextual.length - contextual.length} additional contextual item(s) are available through query-ranked resolution._` : ""}
|
|
69
|
+
|
|
70
|
+
## Local Knowledge
|
|
71
|
+
|
|
72
|
+
- [Developer-local knowledge](local/index.md) — ignored by Git and retrieved together with Versioned Knowledge.
|
|
73
|
+
|
|
74
|
+
## Project Skills
|
|
75
|
+
|
|
76
|
+
${skills.length ? skills.slice(0, 32).map((skill) => `- [${skill.title}](${skill.path})`).join("\n") : "_No project skills._"}
|
|
77
|
+
|
|
78
|
+
## Loading Rules
|
|
79
|
+
|
|
80
|
+
1. Use bounded context resolution; required global knowledge is priority-ranked and budgeted.
|
|
81
|
+
2. Follow only bounded graph relationships selected by context resolution; never recursively load the graph or unrelated scopes.
|
|
82
|
+
3. Load relevant project skills; do not recursively load \`.prodero/\`.
|
|
83
|
+
4. Use session memory as the write path. Sync produces Git-ignored Local Knowledge.
|
|
84
|
+
5. Use the promotion review workflow for Versioned Knowledge. Review repository changes before committing or pushing; Prodero does neither.
|
|
85
|
+
`;
|
|
86
|
+
await writeFileAtomic(await checkedDataPath(root, "index.md"), text);
|
|
87
|
+
const localText = `# Prodero · Local Knowledge\n\n> Developer-local canonical knowledge. This directory is ignored by Git.\n\n${local.length ? local.sort((a, b) => a.title.localeCompare(b.title) || a.id.localeCompare(b.id)).map((item) => `- [${item.title}](memory/${item.filename}) — ${item.scope}${item.scopeRef ? `: ${item.scopeRef}` : ""}; ${item.category}; ${item.priority}`).join("\n") : "_No active local knowledge._"}\n`;
|
|
88
|
+
await writeFileAtomic(await checkedDataPath(root, "local", "index.md"), localText);
|
|
89
|
+
await loadKnowledgeGraph(root, catalog);
|
|
90
|
+
return text;
|
|
91
|
+
}
|
|
92
|
+
export { skillReferences };
|
|
93
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { applicabilityKey } from "./applicability.js";
|
|
2
|
+
import { loadMemoryCatalog } from './memory-index.js';
|
|
3
|
+
import { planAssistedSync } from './knowledge-engine.js';
|
|
4
|
+
import { exactKnowledge } from './semantic-safety.js';
|
|
5
|
+
import { INTELLIGENCE_LIMITS } from './intelligence.js';
|
|
6
|
+
import { withSyncLock, recoverSyncTransaction, commitSyncTransaction, fileHash, safeProjectPath } from './sync-transaction.js';
|
|
7
|
+
import { listCheckpoints } from './storage.js';
|
|
8
|
+
import { sortableId } from './ids.js';
|
|
9
|
+
import { parseKnowledge } from './memory.js';
|
|
10
|
+
import { buildIndex } from './index.js';
|
|
11
|
+
import { estimateTokens, knowledgeText } from './telemetry.js';
|
|
12
|
+
export async function reviewOmittedInstructions(selection, offset = 0, criticalOnly = false) {
|
|
13
|
+
const catalog = await loadMemoryCatalog(selection.root);
|
|
14
|
+
const omitted = selection.omitted.filter(i => !criticalOnly || i.priority === 'critical');
|
|
15
|
+
const page = omitted.slice(offset, offset + 12);
|
|
16
|
+
const items = await Promise.all(page.map(async (selected) => {
|
|
17
|
+
const summary = catalog.summaries.find(i => i.id === selected.id);
|
|
18
|
+
if (!summary)
|
|
19
|
+
return { selection: selected, item: null, missing: true };
|
|
20
|
+
const item = await catalog.load(summary.id);
|
|
21
|
+
return { selection: selected, item: { ...item, layer: summary.layer, revision: catalog.originalHashes.get(item.id) }, missing: false };
|
|
22
|
+
}));
|
|
23
|
+
return { root: selection.root, sessionId: selection.sessionId, task: selection.task, items, offset, total: omitted.length, nextOffset: offset + page.length < omitted.length ? offset + page.length : null, criticalOnly,
|
|
24
|
+
counts: { critical: selection.omitted.filter(i => i.priority === 'critical').length, normal: selection.omitted.filter(i => i.priority !== 'critical').length } };
|
|
25
|
+
}
|
|
26
|
+
// Offline optimization only removes exact duplicates. It uses the same structured
|
|
27
|
+
// action validator as assisted instruction policy consolidation, without inference.
|
|
28
|
+
const duplicateProvider = { name: 'deterministic-duplicates',
|
|
29
|
+
distill: async () => ({ output: { candidates: [] } }), reconcile: async () => { throw Error('Not used'); },
|
|
30
|
+
audit: async ({ items }) => {
|
|
31
|
+
const groups = new Map();
|
|
32
|
+
for (const m of items) {
|
|
33
|
+
const key = JSON.stringify([applicabilityKey(m), m.contextPolicy, m.authority, m.priority, exactKnowledge(m.content)]);
|
|
34
|
+
groups.set(key, [...(groups.get(key) ?? []), m.id]);
|
|
35
|
+
}
|
|
36
|
+
const actions = [...groups.values()].flatMap(ids => ids.slice(1).map(id => ({ action: 'NO_CHANGE', sources: [ids[0], id], canonical: null, reason: 'Exact duplicate instruction with identical authority and scope.' })));
|
|
37
|
+
return { output: { status: actions.length ? 'changes_proposed' : 'clean', issues: [], actions } };
|
|
38
|
+
} };
|
|
39
|
+
export async function previewInstructionOptimization(selection, provider) {
|
|
40
|
+
return withSyncLock(selection.root, async () => {
|
|
41
|
+
await recoverSyncTransaction(selection.root);
|
|
42
|
+
const catalog = await loadMemoryCatalog(selection.root);
|
|
43
|
+
const shadowed = new Set(catalog.summaries.filter(i => ['active', 'disputed'].includes(i.status)).flatMap(i => i.supersedes));
|
|
44
|
+
const ids = [...new Set([...selection.omitted.map(i => i.id), ...selection.selectedIds])];
|
|
45
|
+
const eligible = ids.map(id => catalog.summaries.find(m => m.id === id)).filter(m => m?.category === 'instruction' && m.status === 'active' && !shadowed.has(m.id) && !m.relations.some(r => r.type === 'contradicts'));
|
|
46
|
+
// One bounded same-authority area per click, including loaded policies when useful.
|
|
47
|
+
const groups = new Map();
|
|
48
|
+
for (const m of eligible)
|
|
49
|
+
if (m) {
|
|
50
|
+
const key = JSON.stringify([m.layer, applicabilityKey(m), m.contextPolicy, m.authority, m.priority]);
|
|
51
|
+
groups.set(key, [...(groups.get(key) ?? []), m]);
|
|
52
|
+
}
|
|
53
|
+
const group = [...groups.values()].filter(g => g.length > 1).sort((a, b) => Number(b[0].priority === 'critical') - Number(a[0].priority === 'critical') || b.length - a.length)[0] ?? [];
|
|
54
|
+
const area = group.slice(0, INTELLIGENCE_LIMITS.auditItems);
|
|
55
|
+
const checks = await Promise.all(area.map(async (m) => ({ path: m.relativePath, hash: await fileHash(await safeProjectPath(selection.root, m.relativePath)) })));
|
|
56
|
+
const plan = await planAssistedSync(selection.root, [], provider ?? duplicateProvider, new Date(), new Set(), { instructionIds: area.map(m => m.id) });
|
|
57
|
+
const after = plan.writes.map(w => ({ ...parseKnowledge(w.after, w.path), path: w.path }));
|
|
58
|
+
const before = plan.knowledgeBefore;
|
|
59
|
+
const beforeTokens = before.reduce((n, m) => n + estimateTokens(knowledgeText(m)), 0);
|
|
60
|
+
const afterTokens = after.filter(m => m.status === 'active').reduce((n, m) => n + estimateTokens(knowledgeText(m)), 0);
|
|
61
|
+
if (plan.writes.length && (afterTokens > beforeTokens || after.filter(m => m.status === 'active').length >= before.length))
|
|
62
|
+
throw new Error('Optimization did not reduce instruction pressure. No changes were saved.');
|
|
63
|
+
return { root: selection.root, plan, checks, view: { before, after, beforeTokens, afterTokens, reducedBy: before.length - after.filter(m => m.status === 'active').length,
|
|
64
|
+
repository: after.some(m => m.path.startsWith('memory/')), mode: provider ? 'assisted' : 'deterministic',
|
|
65
|
+
notice: plan.writes.length ? 'Review the preserved clauses and scope before applying.' : provider ? 'No safe consolidation found in this bounded area. Edit obsolete or overly broad instructions individually.' : 'No exact duplicates found. Semantic policy consolidation requires MCP sampling support; individual editing remains available.',
|
|
66
|
+
inspected: area.length, remaining: Math.max(0, eligible.length - area.length) } };
|
|
67
|
+
}, { waitMs: 5000 });
|
|
68
|
+
}
|
|
69
|
+
export async function applyInstructionOptimization(preview) {
|
|
70
|
+
return withSyncLock(preview.root, async () => {
|
|
71
|
+
await recoverSyncTransaction(preview.root);
|
|
72
|
+
for (const check of preview.checks)
|
|
73
|
+
if (await fileHash(await safeProjectPath(preview.root, check.path)) !== check.hash)
|
|
74
|
+
throw new Error('Instructions changed. Optimize again before applying.');
|
|
75
|
+
const plan = preview.plan;
|
|
76
|
+
if (!plan.writes.length)
|
|
77
|
+
throw new Error('No consolidation changes to apply.');
|
|
78
|
+
const id = sortableId('chk');
|
|
79
|
+
const r = plan.result;
|
|
80
|
+
const checkpoint = { id, createdAt: new Date().toISOString(), previousCheckpoint: (await listCheckpoints(preview.root)).at(-1)?.id ?? null, sessions: [], consolidation: 'assisted-v1', intelligence: plan.intelligence, reasoning: plan.reasoning, knowledgeBefore: plan.knowledgeBefore, createdMemory: [], updatedMemory: r.updated, supersededMemory: r.superseded, duplicateMemory: r.duplicates, conflicts: r.conflicts, summary: { sessions: 0, candidates: 0, created: 0, consolidated: r.consolidated.length, duplicates: r.duplicates.length, superseded: 0, conflicts: 0 }, note: 'Explicit reviewed instruction optimization' };
|
|
81
|
+
await commitSyncTransaction(preview.root, [...plan.writes, { path: `checkpoints/${id}.json`, before: null, after: JSON.stringify(checkpoint, null, 2) + '\n' }], { policyMode: 'exact' });
|
|
82
|
+
await buildIndex(preview.root);
|
|
83
|
+
return { checkpoint: id, reducedBy: preview.view.reducedBy };
|
|
84
|
+
}, { waitMs: 5000 });
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=instruction-review.js.map
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { checkedPath, safeRelative, secretPath } from './plugin-files.js';
|
|
7
|
+
export const INTEGRITY_LIMITS = Object.freeze({ files: 20_000, bytes: 100_000_000, depth: 32, entries: 40_000, timeoutMs: 30_000 });
|
|
8
|
+
const skipped = new Set(['.git', 'node_modules', '.cache', '.transactions', 'dist', 'dist-ui']);
|
|
9
|
+
const privateStores = /(^|\/)\.prodero\/(?:local|sessions|checkpoints|\.cache|\.transactions|\.backups|\.migrations)(?:\/|$)/;
|
|
10
|
+
const canonicalStores = ['config.json', 'index.md', 'memory', 'skills', 'local/memory', 'local/skills', 'local/archive', 'archive', 'sessions', 'checkpoints'];
|
|
11
|
+
export async function integritySnapshot(root, relative, options = {}, canonical = false) {
|
|
12
|
+
const limits = { ...INTEGRITY_LIMITS, ...options.limits };
|
|
13
|
+
for (const key of Object.keys(INTEGRITY_LIMITS)) {
|
|
14
|
+
if (!Number.isInteger(limits[key]) || limits[key] < 1 || limits[key] > INTEGRITY_LIMITS[key])
|
|
15
|
+
throw new Error('Invalid integrity limit; limits may only be lowered');
|
|
16
|
+
}
|
|
17
|
+
const exclusions = new Set((options.exclude ?? []).map(safeRelative));
|
|
18
|
+
const controller = new AbortController();
|
|
19
|
+
const cancel = () => controller.abort();
|
|
20
|
+
options.signal?.addEventListener('abort', cancel, { once: true });
|
|
21
|
+
if (options.signal?.aborted)
|
|
22
|
+
cancel();
|
|
23
|
+
const timer = setTimeout(cancel, limits.timeoutMs);
|
|
24
|
+
const started = performance.now();
|
|
25
|
+
const check = () => { if (controller.signal.aborted || performance.now() - started >= limits.timeoutMs)
|
|
26
|
+
throw new Error('INTEGRITY_CANCELLED: digest cancelled or time budget exceeded; no complete digest produced'); };
|
|
27
|
+
const fail = (kind) => { throw new Error(`INTEGRITY_LIMIT: ${kind} budget exceeded; narrow the import tree or remove out-of-scope artifacts and retry. No complete digest produced.`); };
|
|
28
|
+
let entries = 0, bytes = 0, directories = 0, files = 0;
|
|
29
|
+
const digest = crypto.createHash('sha256').update(`curator-integrity-v2:${canonical ? 'canonical' : 'external'}\0`);
|
|
30
|
+
const hashes = Object.create(null);
|
|
31
|
+
const changed = () => new Error('Source tree changed while creating digest; retry audit');
|
|
32
|
+
const stable = (a, b) => a.dev === b.dev && a.ino === b.ino && a.size === b.size && a.mtimeNs === b.mtimeNs && a.ctimeNs === b.ctimeNs;
|
|
33
|
+
async function git(args, input = '') {
|
|
34
|
+
check();
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const child = spawn('git', ['-C', root, ...args], { stdio: ['pipe', 'pipe', 'pipe'], signal: controller.signal });
|
|
37
|
+
let output = '', errorText = '', error = false;
|
|
38
|
+
child.stdout.on('data', chunk => { output += chunk; if (Buffer.byteLength(output) > 8_000_000) {
|
|
39
|
+
error = true;
|
|
40
|
+
child.kill();
|
|
41
|
+
} });
|
|
42
|
+
child.stderr.on('data', chunk => { errorText = (errorText + chunk).slice(0, 4096); });
|
|
43
|
+
child.stdin.on('error', () => { });
|
|
44
|
+
child.on('error', () => reject(new Error('INTEGRITY_GIT: Git ignore inspection unavailable or cancelled; no complete digest produced')));
|
|
45
|
+
child.on('close', code => error ? reject(new Error('INTEGRITY_LIMIT: Git ignore output budget exceeded')) : resolve({ code, output, errorText }));
|
|
46
|
+
child.stdin.end(input);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
let gitRepository = false;
|
|
50
|
+
async function ignored(names) {
|
|
51
|
+
if (!gitRepository || !names.length)
|
|
52
|
+
return new Set();
|
|
53
|
+
if (Buffer.byteLength(names.join('\0')) > 4_000_000)
|
|
54
|
+
fail('Git path-input');
|
|
55
|
+
const result = await git(['check-ignore', '--no-index', '-z', '--stdin'], names.join('\0') + '\0');
|
|
56
|
+
if (result.code !== 0 && result.code !== 1)
|
|
57
|
+
throw new Error('INTEGRITY_GIT: Git ignore inspection failed; no complete digest produced');
|
|
58
|
+
return new Set(result.output.split('\0').filter(Boolean));
|
|
59
|
+
}
|
|
60
|
+
function excluded(rel, within) {
|
|
61
|
+
if (secretPath(rel))
|
|
62
|
+
return true;
|
|
63
|
+
if (canonical) {
|
|
64
|
+
if (rel.split('/').some(part => part.includes('.tmp-')))
|
|
65
|
+
return true;
|
|
66
|
+
if (rel === ".prodero")
|
|
67
|
+
return false;
|
|
68
|
+
const name = rel.slice('.prodero/'.length);
|
|
69
|
+
return !canonicalStores.some(store => name === store || name.startsWith(store + '/') || store.startsWith(name + '/'));
|
|
70
|
+
}
|
|
71
|
+
if (privateStores.test(rel) || rel.split('/').some(p => skipped.has(p)))
|
|
72
|
+
return true;
|
|
73
|
+
if (relative === '.' && rel.split('/')[0] === '.prodero')
|
|
74
|
+
return true;
|
|
75
|
+
return [...exclusions].some(p => within === p || within.startsWith(p + '/'));
|
|
76
|
+
}
|
|
77
|
+
async function hashFile(target, rel, stat) {
|
|
78
|
+
if (++files > limits.files)
|
|
79
|
+
fail('file-count');
|
|
80
|
+
if (stat.size > BigInt(limits.bytes - bytes))
|
|
81
|
+
fail('total-byte');
|
|
82
|
+
check();
|
|
83
|
+
const handle = await fs.open(target, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
84
|
+
try {
|
|
85
|
+
const before = await handle.stat({ bigint: true });
|
|
86
|
+
if (!before.isFile() || !stable(stat, before))
|
|
87
|
+
throw changed();
|
|
88
|
+
const value = crypto.createHash('sha256');
|
|
89
|
+
let read = 0;
|
|
90
|
+
for await (const chunk of handle.createReadStream({ autoClose: false, highWaterMark: 64 * 1024, signal: controller.signal })) {
|
|
91
|
+
check();
|
|
92
|
+
read += chunk.length;
|
|
93
|
+
if (read > limits.bytes - bytes)
|
|
94
|
+
fail('total-byte');
|
|
95
|
+
value.update(chunk);
|
|
96
|
+
}
|
|
97
|
+
const after = await handle.stat({ bigint: true });
|
|
98
|
+
const current = await fs.lstat(target, { bigint: true });
|
|
99
|
+
if (!stable(before, after) || !stable(after, current) || BigInt(read) !== before.size)
|
|
100
|
+
throw changed();
|
|
101
|
+
bytes += read;
|
|
102
|
+
hashes[rel] = value.digest('hex');
|
|
103
|
+
return { hash: hashes[rel], bytes: read };
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
await handle.close();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function visit(target, rel, within, depth) {
|
|
110
|
+
check();
|
|
111
|
+
if (excluded(rel, within))
|
|
112
|
+
return;
|
|
113
|
+
const before = await fs.lstat(target, { bigint: true });
|
|
114
|
+
if (before.isSymbolicLink())
|
|
115
|
+
throw new Error('Symlink in source tree rejected');
|
|
116
|
+
if (depth > limits.depth)
|
|
117
|
+
fail('traversal-depth');
|
|
118
|
+
if (before.isDirectory()) {
|
|
119
|
+
if (within) {
|
|
120
|
+
directories++;
|
|
121
|
+
digest.update(`D\0${within}\0`);
|
|
122
|
+
}
|
|
123
|
+
const names = [];
|
|
124
|
+
for await (const entry of await fs.opendir(target)) {
|
|
125
|
+
check();
|
|
126
|
+
if (++entries > limits.entries)
|
|
127
|
+
fail('entry-count');
|
|
128
|
+
const nextRel = rel === '.' ? entry.name : `${rel}/${entry.name}`;
|
|
129
|
+
const nextWithin = within ? `${within}/${entry.name}` : entry.name;
|
|
130
|
+
if (!excluded(nextRel, nextWithin))
|
|
131
|
+
names.push(entry.name);
|
|
132
|
+
}
|
|
133
|
+
// Batch Git checks and prune ignored directories before traversing them.
|
|
134
|
+
const paths = names.map(n => rel === '.' ? n : `${rel}/${n}`);
|
|
135
|
+
const ignores = await ignored(paths);
|
|
136
|
+
for (const name of names.sort()) {
|
|
137
|
+
const nextRel = rel === '.' ? name : `${rel}/${name}`;
|
|
138
|
+
if (!ignores.has(nextRel))
|
|
139
|
+
await visit(path.join(target, name), nextRel, within ? `${within}/${name}` : name, depth + 1);
|
|
140
|
+
}
|
|
141
|
+
if (!stable(before, await fs.lstat(target, { bigint: true })))
|
|
142
|
+
throw changed();
|
|
143
|
+
}
|
|
144
|
+
else if (before.isFile()) {
|
|
145
|
+
const value = await hashFile(target, rel, before);
|
|
146
|
+
digest.update(`F\0${within}\0${value.bytes}\0${value.hash}\0`);
|
|
147
|
+
}
|
|
148
|
+
else
|
|
149
|
+
throw new Error('Special source file rejected');
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
check();
|
|
153
|
+
const base = await checkedPath(root, relative);
|
|
154
|
+
if (!canonical) {
|
|
155
|
+
const result = await git(['rev-parse', '--is-inside-work-tree']);
|
|
156
|
+
if (result.code !== 0 && !(result.code === 128 && /not a git repository/i.test(result.errorText)))
|
|
157
|
+
throw new Error('INTEGRITY_GIT: Unable to determine Git ignore scope');
|
|
158
|
+
gitRepository = result.code === 0 && result.output.trim() === 'true';
|
|
159
|
+
}
|
|
160
|
+
const stat = await fs.lstat(base).catch(error => { if (error.code === 'ENOENT')
|
|
161
|
+
return null; throw error; });
|
|
162
|
+
if (stat && !stat.isDirectory())
|
|
163
|
+
throw new Error('Source is not a directory');
|
|
164
|
+
if (stat && !(await ignored([relative])).has(relative))
|
|
165
|
+
await visit(base, relative, '', 0);
|
|
166
|
+
check();
|
|
167
|
+
return { hashes: Object.fromEntries(Object.entries(hashes)), digest: { version: 2, algorithm: 'sha256-tree-v2', sha256: digest.digest('hex'), files, directories, bytes } };
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
clearTimeout(timer);
|
|
171
|
+
options.signal?.removeEventListener('abort', cancel);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=integrity-files.js.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { applicabilitySchema } from "./applicability.js";
|
|
2
|
+
import { assertNoSecrets } from "./secrets.js";
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { estimateTokens } from './telemetry.js';
|
|
5
|
+
/** Central limits are local estimates, never provider billing units. */
|
|
6
|
+
export const INTELLIGENCE_LIMITS = Object.freeze({
|
|
7
|
+
distillInputTokens: 4_000, candidatesPerBatch: 12, observationsPerBatch: 12,
|
|
8
|
+
relatedMemories: 12, reconcileInputTokens: 8_000,
|
|
9
|
+
auditItems: 16, auditInputTokens: 12_000, auditTriggerItems: 3,
|
|
10
|
+
outputTokens: 4_000, outputBytes: 32_000, timeoutMs: 45_000,
|
|
11
|
+
});
|
|
12
|
+
const text = z.string().trim().min(1);
|
|
13
|
+
const id = text.max(240);
|
|
14
|
+
export const canonicalProposalSchema = z.object({
|
|
15
|
+
applicability: applicabilitySchema.optional(),
|
|
16
|
+
title: text.max(200), content: text.max(8_000),
|
|
17
|
+
category: z.enum(['principle', 'invariant', 'decision', 'constraint', 'lesson', 'state', 'instruction']),
|
|
18
|
+
scope: z.enum(['global', 'module', 'task']), scopeRef: z.string().max(300).nullable(), scopeKind: z.enum(['path', 'topic']).optional(),
|
|
19
|
+
priority: z.enum(['critical', 'important', 'normal', 'contextual']),
|
|
20
|
+
}).strict();
|
|
21
|
+
export const candidateSchema = canonicalProposalSchema.extend({
|
|
22
|
+
id, kind: z.enum(['knowledge', 'skill_candidate', 'instruction_candidate']),
|
|
23
|
+
sources: z.array(id).min(1).max(24),
|
|
24
|
+
evidence: z.array(z.object({ source: id, quote: text.max(8_000) }).strict()).min(1).max(24),
|
|
25
|
+
supersedes: z.array(id).max(12),
|
|
26
|
+
}).strict();
|
|
27
|
+
export const distillSchema = z.object({ candidates: z.array(candidateSchema).max(INTELLIGENCE_LIMITS.candidatesPerBatch) }).strict();
|
|
28
|
+
export const reconciliationCategorySchema = z.enum(['state_transition', 'refinement', 'duplicate', 'supporting', 'potential_conflict', 'unresolved', 'independent']);
|
|
29
|
+
export const actionSchema = z.object({
|
|
30
|
+
action: z.enum(['CREATE', 'MERGE', 'SUPERSEDE', 'CONFLICT', 'NO_CHANGE', 'LINK', 'UNCERTAIN']),
|
|
31
|
+
sources: z.array(id).min(1).max(16),
|
|
32
|
+
canonical: canonicalProposalSchema.nullable(),
|
|
33
|
+
reason: text.max(500),
|
|
34
|
+
classification: reconciliationCategorySchema.optional(),
|
|
35
|
+
// Optional for backward-compatible providers; required for inferred replacement.
|
|
36
|
+
replacements: z.array(z.object({
|
|
37
|
+
target: id, previousQuote: text.max(8_000), replacementQuote: text.max(8_000),
|
|
38
|
+
transitionQuote: text.max(8_000), sameFact: z.literal(true), completeReplacement: z.literal(true),
|
|
39
|
+
basis: z.literal('explicit_transition'),
|
|
40
|
+
}).strict()).min(1).max(12).optional(),
|
|
41
|
+
}).strict();
|
|
42
|
+
export const reconcileSchema = z.object({ actions: z.array(actionSchema).min(1).max(1) }).strict();
|
|
43
|
+
export const auditSchema = z.object({
|
|
44
|
+
status: z.enum(['clean', 'changes_proposed', 'uncertain']),
|
|
45
|
+
issues: z.array(z.object({ kind: z.enum(['contradiction', 'duplicate', 'obsolete', 'unsafe_merge', 'fragmentation', 'constraint_loss', 'uncertain']), sources: z.array(id).min(1).max(16), reason: text.max(500) }).strict()).max(16),
|
|
46
|
+
actions: z.array(actionSchema).max(16),
|
|
47
|
+
}).strict();
|
|
48
|
+
export function intelligenceTelemetry(provider, fallbackReason) {
|
|
49
|
+
return { mode: provider ? 'assisted' : 'deterministic', provider: provider?.name ?? null, ...(fallbackReason ? { fallbackReason } : {}),
|
|
50
|
+
distillation: { calls: 0, sessions: 0, observations: 0, candidates: 0, inputTokens: 0, outputTokens: 0 },
|
|
51
|
+
reconciliation: { calls: 0, comparisons: 0, inputTokens: 0, outputTokens: 0, actions: { CREATE: 0, MERGE: 0, SUPERSEDE: 0, CONFLICT: 0, NO_CHANGE: 0, LINK: 0, UNCERTAIN: 0 } },
|
|
52
|
+
audit: { triggered: false, items: 0, truncated: false, issues: 0, proposed: 0, applied: 0, inputTokens: 0, outputTokens: 0, status: 'skipped' }, providerUsage: [] };
|
|
53
|
+
}
|
|
54
|
+
export class IntelligenceError extends Error {
|
|
55
|
+
phase;
|
|
56
|
+
constructor(phase, detail) {
|
|
57
|
+
super(`Knowledge engine ${phase} failed: ${detail}. No semantic plan was committed; sessions remain pending.`);
|
|
58
|
+
this.phase = phase;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export async function requestIntelligence(provider, phase, input, telemetry) {
|
|
62
|
+
assertNoSecrets(input);
|
|
63
|
+
const limit = phase === 'distill' ? INTELLIGENCE_LIMITS.distillInputTokens : phase === 'reconcile' ? INTELLIGENCE_LIMITS.reconcileInputTokens : INTELLIGENCE_LIMITS.auditInputTokens;
|
|
64
|
+
const inputTokens = estimateTokens(JSON.stringify(input));
|
|
65
|
+
if (inputTokens > limit)
|
|
66
|
+
throw new IntelligenceError(phase, 'input budget exceeded');
|
|
67
|
+
const controller = new AbortController();
|
|
68
|
+
let timer;
|
|
69
|
+
try {
|
|
70
|
+
const response = await Promise.race([
|
|
71
|
+
provider[phase].call(provider, structuredClone(input), controller.signal),
|
|
72
|
+
new Promise((_, reject) => { timer = setTimeout(() => { controller.abort(); reject(new Error('provider timed out')); }, INTELLIGENCE_LIMITS.timeoutMs); }),
|
|
73
|
+
]);
|
|
74
|
+
assertNoSecrets(response.output);
|
|
75
|
+
const encoded = JSON.stringify(response.output);
|
|
76
|
+
if (!encoded || Buffer.byteLength(encoded) > INTELLIGENCE_LIMITS.outputBytes)
|
|
77
|
+
throw new Error('output budget exceeded');
|
|
78
|
+
const output = (phase === 'distill' ? distillSchema : phase === 'reconcile' ? reconcileSchema : auditSchema).parse(response.output);
|
|
79
|
+
const target = phase === 'distill' ? telemetry.distillation : phase === 'reconcile' ? telemetry.reconciliation : telemetry.audit;
|
|
80
|
+
target.inputTokens += inputTokens;
|
|
81
|
+
target.outputTokens += estimateTokens(encoded);
|
|
82
|
+
if ('calls' in target)
|
|
83
|
+
target.calls++;
|
|
84
|
+
if (response.usage)
|
|
85
|
+
telemetry.providerUsage.push(response.usage);
|
|
86
|
+
return output;
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
throw new IntelligenceError(phase, error instanceof z.ZodError ? 'invalid structured output' : 'provider failed or returned unsafe content');
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=intelligence.js.map
|