@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,98 @@
|
|
|
1
|
+
/** Bounded, read-only repository evidence. Never executes project code. */
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { sha256 } from './storage.js';
|
|
5
|
+
import { secretPath } from './plugin-files.js';
|
|
6
|
+
import { hasSecret, assertNoSecrets } from './secrets.js';
|
|
7
|
+
const sensitiveEvidenceReason = 'Sensitive evidence excluded';
|
|
8
|
+
// Use the same source policy as importer/plugin reads; screen whole files before slicing.
|
|
9
|
+
function sensitiveSource(file, content) { return secretPath(file) || hasSecret(file) || hasSecret(content); }
|
|
10
|
+
// Persisted reviews/jobs predate collection screening. Fail closed before reusing them.
|
|
11
|
+
export function assertSafeCurationData(value) {
|
|
12
|
+
assertNoSecrets(value);
|
|
13
|
+
function visit(v) {
|
|
14
|
+
if (!v || typeof v !== 'object')
|
|
15
|
+
return;
|
|
16
|
+
const record = v;
|
|
17
|
+
if (typeof record.path === 'string' && typeof record.excerpt === 'string' && sensitiveSource(record.path, record.excerpt))
|
|
18
|
+
throw new Error(sensitiveEvidenceReason);
|
|
19
|
+
for (const child of Object.values(record))
|
|
20
|
+
visit(child);
|
|
21
|
+
}
|
|
22
|
+
visit(value);
|
|
23
|
+
}
|
|
24
|
+
const ignored = new Set(['.git', '.prodero', 'node_modules', 'vendor', 'bower_components']);
|
|
25
|
+
export async function collectCurationEvidence(root) {
|
|
26
|
+
const base = await fs.realpath(root), files = [];
|
|
27
|
+
let visited = 0, truncated = false, sensitiveExcluded = 0;
|
|
28
|
+
async function walk(dir, depth) {
|
|
29
|
+
if (depth > 9 || visited >= 2500) {
|
|
30
|
+
truncated = true;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
let entries;
|
|
34
|
+
try {
|
|
35
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
41
|
+
if (++visited > 2500) {
|
|
42
|
+
truncated = true;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (e.isSymbolicLink() || ignored.has(e.name))
|
|
46
|
+
continue;
|
|
47
|
+
const absolute = path.join(dir, e.name), relative = path.relative(base, absolute).split(path.sep).join('/');
|
|
48
|
+
if (secretPath(relative) || hasSecret(relative)) {
|
|
49
|
+
sensitiveExcluded++;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
// Hidden tool state is not repository implementation evidence.
|
|
53
|
+
if (e.isDirectory() && e.name.startsWith('.') && e.name !== '.github')
|
|
54
|
+
continue;
|
|
55
|
+
if (e.isDirectory()) {
|
|
56
|
+
await walk(absolute, depth + 1);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (!/\.(?:php|js|cjs|mjs|jsx|ts|tsx|sh|md|xml|py|rb|go|rs|java|kt|c|h|cpp|cs|swift|sql|html|css|vue|svelte|json|ya?ml|toml)$/i.test(e.name))
|
|
60
|
+
continue;
|
|
61
|
+
const st = await fs.lstat(absolute);
|
|
62
|
+
if (!st.isFile() || st.size > 300000)
|
|
63
|
+
continue;
|
|
64
|
+
// Recheck real containment: referenced symlinks may not escape the project.
|
|
65
|
+
const real = await fs.realpath(absolute);
|
|
66
|
+
if (!real.startsWith(base + path.sep))
|
|
67
|
+
continue;
|
|
68
|
+
const content = await fs.readFile(real, 'utf8');
|
|
69
|
+
if (sensitiveSource(relative, content)) {
|
|
70
|
+
sensitiveExcluded++;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
files.push({ path: relative, revision: sha256(content), content });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// One bounded traversal treats repository layouts uniformly.
|
|
77
|
+
await walk(base, 0);
|
|
78
|
+
return { root: base, files, truncated, sensitiveExcluded };
|
|
79
|
+
}
|
|
80
|
+
export function evidenceFor(content, inventory) {
|
|
81
|
+
const symbols = new Set((content.match(/\b[A-Za-z_][A-Za-z0-9_]*\b/g) ?? []).filter(s => s.length > 4).map(s => s.toLowerCase()));
|
|
82
|
+
const results = [];
|
|
83
|
+
for (const f of inventory.files) {
|
|
84
|
+
if (sensitiveSource(f.path, f.content))
|
|
85
|
+
continue;
|
|
86
|
+
const lines = f.content.split('\n');
|
|
87
|
+
for (let i = 0; i < lines.length; i++)
|
|
88
|
+
if ((lines[i].match(/\b[A-Za-z_][A-Za-z0-9_]*\b/g) ?? []).some(s => symbols.has(s.toLowerCase()))) {
|
|
89
|
+
results.push({ path: f.path, revision: f.revision, line: i + 1, excerpt: lines[i].trim().slice(0, 300), kind: /test/i.test(f.path) ? 'test' : /grunt|deploy/i.test(f.path) ? 'build' : 'implementation' });
|
|
90
|
+
if (results.length >= 30 || results.filter(r => r.path === f.path).length >= 3)
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
if (results.length >= 30)
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
return results;
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=curation-evidence.js.map
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/** First-party App orchestration. Local review state only; never grants or writes knowledge. */
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { withSyncLock, ownsSyncLock } from './sync-transaction.js';
|
|
4
|
+
import { loadCurationReview, curationReviewView, stageCurationDecisions, stageEvent, assertCurationHead, CurationReviewError } from './curation-review.js';
|
|
5
|
+
import { curationAnswers, appendCurationAnswer, semanticJobs, semanticJobStatus, semanticJobSuspended, semanticRetryState, prepareSemanticCuration } from './curation-semantic-jobs.js';
|
|
6
|
+
import { revisionIssues, selectionProblems, deterministicBaseline } from './curation-revision.js';
|
|
7
|
+
import { planKnowledgeCuration } from './knowledge-curation.js';
|
|
8
|
+
import { semanticHash } from './curation-semantic-contract.js';
|
|
9
|
+
const identity = { reviewId: z.string(), reviewRevisionId: z.string(), planHash: z.string(), decisionRevision: z.number().int().nonnegative(), answerLogHash: z.string() };
|
|
10
|
+
export const graphicalActionSchema = z.object({ ...identity, action: z.enum(['summary', 'analyze', 'answer', 'question_later', 'question_reopen', 'retry_analysis']), questionId: z.string().optional(), questionVersion: z.number().int().positive().optional(), choiceId: z.string().optional(), rawText: z.string().max(8192).optional(), requestId: z.string().min(1).max(100).optional(), replacesAnswerId: z.string().optional() }).strict();
|
|
11
|
+
const pending = (status) => ['prepared', 'awaiting_reasoning', 'submitted'].includes(status);
|
|
12
|
+
export async function graphicalReviewState(root, review, projection) {
|
|
13
|
+
const revision = review.snapshot.revision;
|
|
14
|
+
if (!revision)
|
|
15
|
+
return undefined;
|
|
16
|
+
const answers = await curationAnswers(root, revision.lineageId), allJobs = await semanticJobs(root);
|
|
17
|
+
const questions = projection.clarificationCandidates.map(q => {
|
|
18
|
+
const units = projection.items.filter(u => u.proposalIds.some(id => q.sourceIds.includes(id)));
|
|
19
|
+
const answer = [...answers].reverse().find(a => a.questionId === q.question.id && a.basisHash === q.basisHash && !answers.some(next => next.replacesAnswerId === a.id));
|
|
20
|
+
return { ...q, unitIds: units.map(u => u.id), componentId: revision.dependencies.groups.find(g => g.proposalIds.some(id => q.sourceIds.includes(id)))?.component, answer };
|
|
21
|
+
});
|
|
22
|
+
const jobs = allJobs.filter(j => j.input.reviewRevisionId === revision.id && !allJobs.some(next => next.input.previousJobId === j.input.jobId)).map(j => {
|
|
23
|
+
const event = j.events.at(-1);
|
|
24
|
+
const suspended = semanticJobSuspended(j, review);
|
|
25
|
+
const status = semanticJobStatus(j, review);
|
|
26
|
+
return { id: j.input.jobId, componentId: j.input.bundle.componentId, status, reason: event.reason, suspended, ...semanticRetryState(j, review, allJobs) };
|
|
27
|
+
});
|
|
28
|
+
const actionable = jobs.filter(j => !j.suspended && j.status !== 'validated');
|
|
29
|
+
const unansweredComponents = revision.dependencies.groups.filter(g => projection.items.some(u => u.id === g.id && u.needsInput) && !questions.some(q => q.componentId === g.component));
|
|
30
|
+
const awaiting = questions.find(q => q.state === 'answered_pending_analysis');
|
|
31
|
+
const componentId = awaiting?.componentId ?? unansweredComponents[0]?.component;
|
|
32
|
+
const activeQuestions = questions.filter(q => q.needsInput);
|
|
33
|
+
const answered = new Set(revision.answerBindings.map(a => a.answerEventId));
|
|
34
|
+
const clarifiedWithYou = new Set(answers.filter(a => answered.has(a.id) && !revisionIssues(revision).some(i => i.sourceIds.some(id => a.sourceIds.includes(id)))).map(a => a.issueId)).size;
|
|
35
|
+
return { version: 1, questions: questions.map(q => ({ ...q,
|
|
36
|
+
// Deferral requires every affected unit to become deferred/kept; reopening
|
|
37
|
+
// any unit can reactivate an overlapping question. These effects differ.
|
|
38
|
+
relatedQuestions: questions.filter(other => other.state !== 'deferred' && other.unitIds.some(id => q.unitIds.includes(id)) && other.unitIds.every(id => q.unitIds.includes(id) || projection.items.some(u => u.id === id && (u.issue?.state === 'deferred' || u.disposition === 'keep_current')))).map(other => ({ id: other.question.id, version: other.question.version, title: other.question.title })),
|
|
39
|
+
reopenedQuestions: questions.filter(other => (other.state === 'deferred' || other.disposition === 'keep_current') && other.unitIds.some(id => q.unitIds.includes(id))).map(other => ({ id: other.question.id, version: other.question.version, title: other.question.title })),
|
|
40
|
+
position: activeQuestions.findIndex(a => a.question.id === q.question.id) + 1 })), activeQuestionIds: activeQuestions.map(q => q.question.id), activeQuestionCount: activeQuestions.length, deferredQuestionIds: questions.filter(q => q.state === 'deferred').map(q => q.question.id), answerLogHash: semanticHash(answers), clarifiedWithYou,
|
|
41
|
+
jobs, continuationJobId: actionable.find(j => pending(j.status))?.id, analysisFailed: actionable.some(j => !pending(j.status)), analysisPending: actionable.some(j => pending(j.status)) || !!awaiting && !actionable.some(j => !pending(j.status)),
|
|
42
|
+
canRetryAnalysis: actionable.some(j => j.canRetry || j.canResume),
|
|
43
|
+
nextAnalysisComponent: componentId, needsPreparation: !!componentId && !actionable.some(j => j.componentId === componentId),
|
|
44
|
+
filters: { ready: projection.items.filter(i => i.ready).map(i => i.id), clarification: projection.items.filter(i => i.outcome === 'unresolved').map(i => i.id), deferred: projection.items.filter(i => i.disposition === 'deferred').map(i => i.id), unchanged: projection.items.filter(i => i.outcome === 'unchanged').map(i => i.id), applied: projection.items.filter(i => i.execution === 'applied').map(i => i.id) },
|
|
45
|
+
repositoryChanges: projection.items.filter(i => i.ready && i.proposalIds.some(id => !review.snapshot.plan.proposals.find(p => p.id === id).path.startsWith('local/'))).length, policyEffectChanges: projection.items.filter(i => i.ready && i.proposalIds.some(id => review.snapshot.plan.proposals.find(p => p.id === id).changes.some(c => c.startsWith('applicability-')))).length,
|
|
46
|
+
packageReady: projection.readyChangeGroups === projection.included && projection.canApply, packageCount: projection.included,
|
|
47
|
+
remaining: projection.unresolvedIssueCount > 0 || projection.deferredChanges > 0 || projection.readyChangeGroups > 0 };
|
|
48
|
+
}
|
|
49
|
+
export async function graphicalReviewPayload(root, review) {
|
|
50
|
+
const view = await curationReviewView(root, review.snapshot.plan, review);
|
|
51
|
+
return { action: 'curation', root, plan: review.snapshot.plan, ...view, graphical: await graphicalReviewState(root, view.review, view.projection), appReviewAvailable: true };
|
|
52
|
+
}
|
|
53
|
+
export async function graphicalReviewAction(root, raw, answerOrigin = 'app') {
|
|
54
|
+
const input = graphicalActionSchema.parse(raw);
|
|
55
|
+
const operation = async () => {
|
|
56
|
+
let review = await loadCurationReview(root, input.reviewId);
|
|
57
|
+
const revision = review.snapshot.revision;
|
|
58
|
+
await assertCurationHead(root, review);
|
|
59
|
+
if (!revision || revision.id !== input.reviewRevisionId || review.snapshot.planHash !== input.planHash || review.state.revision !== input.decisionRevision)
|
|
60
|
+
throw new CurationReviewError('review_changed', 'This review changed. Reload saved decisions.');
|
|
61
|
+
const answers = await curationAnswers(root, revision.lineageId);
|
|
62
|
+
const replay = input.action === 'answer' && answers.find(a => a.requestId === input.requestId);
|
|
63
|
+
if (!replay && semanticHash(answers) !== input.answerLogHash)
|
|
64
|
+
throw new CurationReviewError('review_changed', 'Another answer was saved. Reload this review.');
|
|
65
|
+
let view = await graphicalReviewPayload(root, review), g = view.graphical;
|
|
66
|
+
const question = g.questions.find(q => q.question.id === input.questionId && q.question.version === input.questionVersion);
|
|
67
|
+
if (['answer', 'question_later', 'question_reopen'].includes(input.action) && !question)
|
|
68
|
+
throw new CurationReviewError('review_changed', 'This question changed. Reload the review.');
|
|
69
|
+
if (input.action === 'summary') {
|
|
70
|
+
const selected = view.projection.items.filter(i => i.ready).flatMap(i => i.proposalIds);
|
|
71
|
+
if (selectionProblems(revision.dependencies, selected).length)
|
|
72
|
+
throw new CurationReviewError('dependency_blocked', 'Some changes depend on unresolved information.');
|
|
73
|
+
const decisions = Object.fromEntries(view.projection.items.filter(i => i.ready && i.disposition !== 'included').map(i => [i.id, 'suggested']));
|
|
74
|
+
if (Object.keys(decisions).length)
|
|
75
|
+
review = await stageCurationDecisions(root, { ...input, decisions });
|
|
76
|
+
}
|
|
77
|
+
else if (input.action === 'question_later' || input.action === 'question_reopen') {
|
|
78
|
+
const baseline = deterministicBaseline(revision);
|
|
79
|
+
if ((await planKnowledgeCuration(root, { memorySyncSources: baseline.memorySyncSources })).planId !== baseline.planId)
|
|
80
|
+
throw new CurationReviewError('semantic_stale', 'Knowledge or evidence changed. Review the current question.');
|
|
81
|
+
if (input.action === 'question_reopen' && question.state !== 'deferred')
|
|
82
|
+
throw new CurationReviewError('review_changed', 'This question is not deferred. Reload the saved review.');
|
|
83
|
+
review = await stageEvent(root, input, input.action === 'question_later' ? 'defer_issue' : 'reopen_issue', Object.fromEntries(question.unitIds.map(id => [id, 'deferred'])));
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
if (input.action === 'answer') {
|
|
87
|
+
if (!input.requestId)
|
|
88
|
+
throw new CurationReviewError('semantic_invalid', 'Answer request identity is missing.');
|
|
89
|
+
if (!replay && (!question.needsInput || g.analysisPending))
|
|
90
|
+
throw new CurationReviewError('review_changed', 'This answer is already being reviewed.');
|
|
91
|
+
await appendCurationAnswer(root, { reviewId: input.reviewId, reviewRevisionId: input.reviewRevisionId, decisionRevision: input.decisionRevision, answerLogHash: input.answerLogHash, questionId: input.questionId, questionVersion: input.questionVersion, requestId: input.requestId, origin: answerOrigin, choiceId: input.choiceId, rawText: input.rawText, replacesAnswerId: input.replacesAnswerId });
|
|
92
|
+
// Answer is durable before job preparation. A capacity/dispatch failure cannot erase it.
|
|
93
|
+
view = await graphicalReviewPayload(root, review);
|
|
94
|
+
g = view.graphical;
|
|
95
|
+
}
|
|
96
|
+
const failed = g.jobs.find(j => !j.suspended && (j.canRetry || j.canResume)), component = input.action === 'answer' ? question.componentId : input.action === 'retry_analysis' ? failed?.componentId : g.nextAnalysisComponent;
|
|
97
|
+
if (!component) {
|
|
98
|
+
if (g.continuationJobId)
|
|
99
|
+
return view;
|
|
100
|
+
throw new CurationReviewError('semantic_limit', 'No further automatic analysis is available. Review details or answer later.');
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const job = await prepareSemanticCuration(root, { reviewId: review.snapshot.reviewId, reviewRevisionId: revision.id, componentId: component, requestId: input.requestId ?? `app_${semanticHash([revision.id, review.state.revision, g.answerLogHash, component, input.action, ...(input.action === 'retry_analysis' ? [failed.id] : [])])}`, reason: input.action === 'retry_analysis' ? (failed.canRetry ? 'validation_correction' : 'resume') : input.action === 'answer' ? 'answer' : g.questions.some(q => q.state === 'answered_pending_analysis') ? 'resume' : 'initial', ...(input.action === 'retry_analysis' ? failed.canRetry ? { correctionForJobId: failed.id } : { resumeForJobId: failed.id } : {}) });
|
|
104
|
+
view = await graphicalReviewPayload(root, review);
|
|
105
|
+
return { ...view, continuationJobId: pending(job.status) ? job.jobId : undefined, analysisNotice: pending(job.status) ? undefined : 'No further progress was possible with the current information.' };
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (!(error instanceof CurationReviewError))
|
|
109
|
+
throw error;
|
|
110
|
+
return { ...await graphicalReviewPayload(root, review), analysisNotice: input.action === 'answer' ? 'Answer saved. Analysis needs more information or has reached its limit.' : error.message };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return graphicalReviewPayload(root, review);
|
|
114
|
+
};
|
|
115
|
+
return ownsSyncLock(root) ? operation() : withSyncLock(root, operation, { recoverLifecycle: false, waitMs: 2000 });
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=curation-graphical.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Model-visible Sync question adapter. Staging only; no selection or Apply. */
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { graphicalActionSchema, graphicalReviewAction } from './curation-graphical.js';
|
|
4
|
+
import { syncCurationReview } from './memory-sync.js';
|
|
5
|
+
import { withSyncLock } from './sync-transaction.js';
|
|
6
|
+
import { curationAnswers } from './curation-semantic-jobs.js';
|
|
7
|
+
const owner = { workflowId: z.string().uuid(), candidateId: z.string().regex(/^area_[a-f0-9]{20}$/) };
|
|
8
|
+
const actions = z.enum(['analyze', 'answer', 'question_later', 'question_reopen', 'retry_analysis']);
|
|
9
|
+
export const curationQuestionSchema = z.union([
|
|
10
|
+
graphicalActionSchema.extend({ ...owner, action: actions }).strict(),
|
|
11
|
+
z.object({ ...owner, action: z.literal('inspect'), reviewId: z.string().regex(/^review_[a-zA-Z0-9_]+$/) }).strict(),
|
|
12
|
+
]);
|
|
13
|
+
// MCP requires an object-root schema. The strict action-specific union is
|
|
14
|
+
// checked inside the same operation before ownership or journal mutations.
|
|
15
|
+
export const curationQuestionToolSchema = graphicalActionSchema.extend({ ...owner, action: z.enum(['inspect', ...actions.options]), reviewRevisionId: graphicalActionSchema.shape.reviewRevisionId.optional(), planHash: graphicalActionSchema.shape.planHash.optional(), decisionRevision: graphicalActionSchema.shape.decisionRevision.optional(), answerLogHash: graphicalActionSchema.shape.answerLogHash.optional() }).strict();
|
|
16
|
+
export async function runCurationQuestion(root, raw) {
|
|
17
|
+
const input = curationQuestionSchema.parse(raw);
|
|
18
|
+
return withSyncLock(root, async () => {
|
|
19
|
+
const { workflowId, candidateId, ...request } = input;
|
|
20
|
+
const saved = await syncCurationReview(root, workflowId, candidateId, request.reviewId);
|
|
21
|
+
const payload = request.action === 'inspect' ? saved : await graphicalReviewAction(root, request, 'agent_relay');
|
|
22
|
+
const answer = request.action === 'answer' ? (await curationAnswers(root, payload.review.snapshot.revision.lineageId)).find(a => a.requestId === request.requestId) : undefined;
|
|
23
|
+
return { ...payload, action: 'curation-question', syncWorkflowId: workflowId, candidateId, appReviewAvailable: false,
|
|
24
|
+
...(answer ? { answerAcknowledgement: { answerId: answer.id, origin: answer.origin, saved: true, message: 'Relayed chat answer saved. It is evidence for staged analysis, not authenticated human intent or approval.' } } : {}),
|
|
25
|
+
next: { tool: 'prodero_memory_sync', arguments: { projectRoot: root, request: { action: 'inspect', workflowId } }, instructions: 'Inspect this saved Sync workflow after the bounded reasoning continuation. Relayed answers remain evidence, not approval; unresolved facts may still need clarification or deliberate deferral. Never Apply, promote or publish on the strength of an answer.' } };
|
|
26
|
+
}, { recoverLifecycle: false, waitMs: 2000 });
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=curation-question.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export function renderReviewProjection(p) {
|
|
2
|
+
return `${p.analyzedSourceCount} knowledge items · ${p.readyChangeGroups} proposed change groups · ${p.needsInputNow} need your input · ${p.deferredQuestions} deferred questions · ${p.deferredChanges} deferred changes · ${p.unchanged} unchanged${p.blockedGroups ? ` · ${p.blockedGroups} blocked change groups` : ''}${p.applied ? ` · ${p.applied} applied` : ''}${p.semanticAnalysisPending ? ` · ${p.semanticAnalysisPending} semantic analysis pending` : ''}${p.semanticAnalysisFailed ? ` · ${p.semanticAnalysisFailed} semantic analysis failed` : ''}${p.semanticProposalsReady ? ` · ${p.semanticProposalsReady} staged semantic candidates (structural validation only)` : ''}${p.additionalEvidenceRequired?.length ? ' · Additional evidence required' : ''}${p.paused ? ' · Review paused' : ''}`;
|
|
3
|
+
}
|
|
4
|
+
//# sourceMappingURL=curation-review-presentation.js.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { deriveDependencies, selectionProblems, curationSelectionHash, revisionIssues } from './curation-revision.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export const clarificationStateSchema = z.enum(['open', 'answered_pending_analysis', 'resolved', 'deferred', 'superseded']);
|
|
4
|
+
export function analysisOutcome(plan, unit) {
|
|
5
|
+
const proposals = unit.proposalIds.map(id => plan.proposals.find(p => p.id === id));
|
|
6
|
+
if (unit.attention || proposals.some(p => p.outputs.some(o => o.applicability?.kind === 'unresolved')))
|
|
7
|
+
return 'unresolved';
|
|
8
|
+
return unit.applicable ? 'proposed' : 'unchanged';
|
|
9
|
+
}
|
|
10
|
+
/** The one attention/count/lifecycle policy. UI and text render this result verbatim. */
|
|
11
|
+
export function projectCurationReview(plan, units, state, options = {}) {
|
|
12
|
+
const dependencies = options.legacy ? undefined : options.revision?.dependencies ?? deriveDependencies(plan, units);
|
|
13
|
+
const consumed = !!options.revision && Object.values(state.items).some(i => i.execution === 'applied');
|
|
14
|
+
const semanticIssues = revisionIssues(options.revision);
|
|
15
|
+
const items = units.map(unit => {
|
|
16
|
+
const persisted = state.items[unit.id];
|
|
17
|
+
const answerPending = semanticIssues.some(issue => options.semantic?.answeredIssueIds?.includes(issue.id) && unit.proposalIds.some(id => issue.sourceIds.includes(id)));
|
|
18
|
+
const item = answerPending && persisted.issue ? { ...persisted, issue: { ...persisted.issue, state: 'answered_pending_analysis' } } : persisted;
|
|
19
|
+
const needsInput = item.outcome === 'unresolved' && item.issue?.state === 'open' && item.disposition !== 'keep_current' && item.disposition !== 'deferred' && item.execution !== 'applied';
|
|
20
|
+
const dependency = dependencies?.groups.find(g => g.id === unit.id);
|
|
21
|
+
const blocked = !!options.semantic?.blockedGroupIds.includes(unit.id) || !!dependency && (dependency.blockedByIssueIds.length > 0 || dependency.incomplete.length > 0 || dependency.requires.some(id => ['keep_current', 'deferred'].includes(state.items[id]?.disposition ?? '')));
|
|
22
|
+
const ready = !consumed && !options.semantic?.blockedGroupIds.includes(unit.id) && !blocked && item.outcome === 'proposed' && item.disposition !== 'keep_current' && item.disposition !== 'deferred' && item.execution !== 'applied';
|
|
23
|
+
return { ...unit, ...item, blocked: item.outcome === 'proposed' && blocked, blockedReason: blocked ? 'Depends on unresolved information or incomplete evidence.' : null, needsInput: needsInput && !state.paused, ready: ready && !state.paused && !options.stale,
|
|
24
|
+
pendingDecision: item.execution !== 'applied' && item.outcome === 'proposed' && item.disposition === null };
|
|
25
|
+
});
|
|
26
|
+
const count = (predicate) => items.filter(predicate).length;
|
|
27
|
+
const analyzedSourceCount = options.revision?.analyzedSourceIds.length ?? new Set(plan.proposals.map(p => p.sourceId)).size;
|
|
28
|
+
const unchanged = plan.proposals.filter(p => !p.changes.length && !p.attention.length && !p.outputs.some(o => o.applicability?.kind === 'unresolved')).length;
|
|
29
|
+
let needsInputNow = count(i => i.needsInput);
|
|
30
|
+
let deferredQuestions = count(i => i.outcome === 'unresolved' && i.issue?.state === 'deferred');
|
|
31
|
+
const deferredChanges = count(i => i.outcome === 'proposed' && i.disposition === 'deferred');
|
|
32
|
+
const keptCurrent = count(i => i.disposition === 'keep_current');
|
|
33
|
+
const proposed = count(i => i.outcome === 'proposed');
|
|
34
|
+
const readyChangeGroups = count(i => i.ready);
|
|
35
|
+
const applied = count(i => i.execution === 'applied') + (options.revision?.appliedHistory.length ?? 0);
|
|
36
|
+
const semanticAnalysisPending = options.semantic?.pending ?? 0, semanticAnalysisFailed = options.semantic?.failed ?? 0;
|
|
37
|
+
const clarificationCandidates = semanticIssues.map(issue => {
|
|
38
|
+
const affected = items.filter(i => i.proposalIds.some(id => issue.sourceIds.includes(id)));
|
|
39
|
+
const kept = affected.length > 0 && affected.every(i => i.disposition === 'keep_current');
|
|
40
|
+
const deferred = !kept && affected.length > 0 && affected.every(i => i.issue?.state === 'deferred' || i.disposition === 'keep_current');
|
|
41
|
+
const questionState = options.semantic?.answeredIssueIds?.includes(issue.id) ? 'answered_pending_analysis' : deferred ? 'deferred' : 'open';
|
|
42
|
+
return { ...issue, state: questionState, disposition: kept ? 'keep_current' : deferred ? 'deferred' : null, needsInput: !state.paused && !kept && questionState === 'open' };
|
|
43
|
+
});
|
|
44
|
+
if (semanticIssues.length) {
|
|
45
|
+
const uncovered = items.filter(i => !semanticIssues.some(issue => i.proposalIds.some(id => issue.sourceIds.includes(id))));
|
|
46
|
+
needsInputNow = clarificationCandidates.filter(i => i.needsInput).length + uncovered.filter(i => i.needsInput).length;
|
|
47
|
+
deferredQuestions = clarificationCandidates.filter(i => i.state === 'deferred').length + uncovered.filter(i => i.issue?.state === 'deferred').length;
|
|
48
|
+
}
|
|
49
|
+
const analysisPending = (state.analysis === 'pending' ? 1 : count(i => i.issue?.state === 'answered_pending_analysis' && !options.semantic?.answeredIssueIds?.length)) + semanticAnalysisPending;
|
|
50
|
+
const analysisFailed = (state.analysis === 'failed' ? 1 : 0) + semanticAnalysisFailed;
|
|
51
|
+
const selectedIds = items.filter(i => i.ready && i.disposition === 'included').flatMap(i => i.proposalIds).sort();
|
|
52
|
+
const selectionIssues = dependencies ? selectionProblems(dependencies, selectedIds) : [];
|
|
53
|
+
const included = count(i => i.ready && i.disposition === 'included');
|
|
54
|
+
const outstanding = count(i => i.execution !== 'applied' && i.outcome !== 'unchanged');
|
|
55
|
+
const lifecycle = options.stale ? 'stale' : state.paused ? 'paused' : analysisFailed ? 'analysis_failed' : analysisPending ? 'analyzing' : applied ? (outstanding ? 'partially_applied' : 'completed') : needsInputNow ? (readyChangeGroups ? 'partially_ready' : 'awaiting_input') : readyChangeGroups ? 'ready' : count(i => i.blocked) ? 'awaiting_input' : deferredQuestions || deferredChanges ? 'ready' : 'completed';
|
|
56
|
+
return { version: 1, semanticAnalysisPending, semanticAnalysisFailed, clarificationCandidates, semanticProposalsReady: options.revision?.typedPlan.kind === 'semantic-v2' ? items.filter(i => i.ready && plan.proposals.some(p => i.proposalIds.includes(p.id) && p.changes.includes('semantic-candidate'))).length : 0, additionalEvidenceRequired: options.revision?.typedPlan.kind === 'semantic-v2' ? options.revision.typedPlan.analyses.flatMap((a, index, all) => a.proposal.additionalEvidenceRequired.filter(missing => !all.slice(index + 1).some(next => missing.sourceIds.some(id => next.bundle.expectedSources.some(s => s.id === id))))) : [], semanticValidation: options.revision?.typedPlan.kind === 'semantic-v2' ? 'structurally_validated_candidate' : undefined, unresolvedIssueCount: clarificationCandidates.length + items.filter(i => i.outcome === 'unresolved' && !semanticIssues.some(issue => i.proposalIds.some(id => issue.sourceIds.includes(id)))).length, reviewRevisionId: options.revision?.id, parentRevisionId: options.revision?.parentRevisionId, lineageId: options.revision?.lineageId, selectionHash: options.revision ? curationSelectionHash(options.revision.id, state.revision, selectedIds) : undefined, blockedGroups: count(i => i.blocked), selectionIssues, analyzedSourceCount, unchanged, proposed, readyChangeGroups, needsInputNow, deferredQuestions, keptCurrent, deferredChanges, analysisPending, analysisFailed, applied, included,
|
|
57
|
+
pendingDecisions: count(i => i.pendingDecision), unresolved: count(i => i.outcome === 'unresolved'),
|
|
58
|
+
paused: state.paused, lifecycle, selectedIds, canApply: selectedIds.length > 0 && state.analysis === 'complete' && !selectionIssues.length,
|
|
59
|
+
attentionUnitIds: items.filter(i => i.needsInput).map(i => i.id), items };
|
|
60
|
+
}
|
|
61
|
+
export { renderReviewProjection } from './curation-review-presentation.js';
|
|
62
|
+
//# sourceMappingURL=curation-review-state.js.map
|