@phyzess/pappra 0.0.0-stage → 0.1.0-beta.10
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/LICENSE +21 -0
- package/README.md +99 -2
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/assets/integrations/skills/pappra/SKILL.md +71 -0
- package/assets/integrations/skills/pappra/references/hosts.md +40 -0
- package/assets/integrations/skills/pappra/references/lifecycle.md +41 -0
- package/assets/integrations/skills/pappra/references/operations.md +27 -0
- package/assets/manifest.json +4 -0
- package/dist/app/collaboration.d.ts +170 -0
- package/dist/app/collaboration.js +1643 -0
- package/dist/app/doctor.d.ts +9 -0
- package/dist/app/doctor.js +1030 -0
- package/dist/app/init.d.ts +171 -0
- package/dist/app/init.js +1442 -0
- package/dist/cli/commands/code.d.ts +16 -0
- package/dist/cli/commands/code.js +127 -0
- package/dist/cli/commands/collaboration.d.ts +13 -0
- package/dist/cli/commands/collaboration.js +186 -0
- package/dist/cli/commands/continuity.d.ts +29 -0
- package/dist/cli/commands/continuity.js +133 -0
- package/dist/cli/commands/docs.d.ts +12 -0
- package/dist/cli/commands/docs.js +234 -0
- package/dist/cli/commands/doctor.d.ts +14 -0
- package/dist/cli/commands/doctor.js +57 -0
- package/dist/cli/commands/host.d.ts +17 -0
- package/dist/cli/commands/host.js +199 -0
- package/dist/cli/commands/init.d.ts +59 -0
- package/dist/cli/commands/init.js +845 -0
- package/dist/cli/commands/runtime.d.ts +14 -0
- package/dist/cli/commands/runtime.js +263 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +10 -0
- package/dist/cli/output.d.ts +4 -0
- package/dist/cli/output.js +1 -0
- package/dist/cli/run.d.ts +2 -0
- package/dist/cli/run.js +749 -0
- package/dist/code/budget.d.ts +14 -0
- package/dist/code/budget.js +39 -0
- package/dist/code/exec.d.ts +23 -0
- package/dist/code/exec.js +133 -0
- package/dist/code/identity.d.ts +13 -0
- package/dist/code/identity.js +214 -0
- package/dist/code/index.d.ts +6 -0
- package/dist/code/index.js +3 -0
- package/dist/code/providers/codegraph.d.ts +59 -0
- package/dist/code/providers/codegraph.js +567 -0
- package/dist/code/registration.d.ts +9 -0
- package/dist/code/registration.js +293 -0
- package/dist/code/service.d.ts +53 -0
- package/dist/code/service.js +633 -0
- package/dist/code/text.d.ts +3 -0
- package/dist/code/text.js +202 -0
- package/dist/collab/admission.d.ts +450 -0
- package/dist/collab/admission.js +797 -0
- package/dist/collab/canonical.d.ts +26 -0
- package/dist/collab/canonical.js +136 -0
- package/dist/collab/contracts.d.ts +73 -0
- package/dist/collab/contracts.js +76 -0
- package/dist/collab/envelope.d.ts +52 -0
- package/dist/collab/envelope.js +241 -0
- package/dist/collab/fingerprint.d.ts +7 -0
- package/dist/collab/fingerprint.js +58 -0
- package/dist/collab/handoff.d.ts +350 -0
- package/dist/collab/handoff.js +513 -0
- package/dist/collab/index.d.ts +9 -0
- package/dist/collab/index.js +8 -0
- package/dist/collab/lifecycle.d.ts +449 -0
- package/dist/collab/lifecycle.js +534 -0
- package/dist/collab/messages.d.ts +148 -0
- package/dist/collab/messages.js +583 -0
- package/dist/collab/notify.d.ts +47 -0
- package/dist/collab/notify.js +140 -0
- package/dist/collab/preflight.d.ts +31 -0
- package/dist/collab/preflight.js +271 -0
- package/dist/collab/resources.d.ts +119 -0
- package/dist/collab/resources.js +451 -0
- package/dist/collab/store.d.ts +143 -0
- package/dist/collab/store.js +606 -0
- package/dist/collab/types.d.ts +354 -0
- package/dist/collab/types.js +1 -0
- package/dist/collab/validation.d.ts +10 -0
- package/dist/collab/validation.js +303 -0
- package/dist/continuity/context.d.ts +24 -0
- package/dist/continuity/context.js +179 -0
- package/dist/continuity/index.d.ts +3 -0
- package/dist/continuity/index.js +1 -0
- package/dist/continuity/paths.d.ts +37 -0
- package/dist/continuity/paths.js +258 -0
- package/dist/continuity/service.d.ts +59 -0
- package/dist/continuity/service.js +1258 -0
- package/dist/hosts/claude/native.d.ts +79 -0
- package/dist/hosts/claude/native.js +853 -0
- package/dist/hosts/codex/native.d.ts +345 -0
- package/dist/hosts/codex/native.js +1528 -0
- package/dist/hosts/codex/protocol.d.ts +51 -0
- package/dist/hosts/codex/protocol.js +42 -0
- package/dist/hosts/detect.d.ts +29 -0
- package/dist/hosts/detect.js +73 -0
- package/dist/hosts/index.d.ts +10 -0
- package/dist/hosts/index.js +10 -0
- package/dist/hosts/integration/bundle-export.d.ts +24 -0
- package/dist/hosts/integration/bundle-export.js +128 -0
- package/dist/hosts/integration/bundle.d.ts +19 -0
- package/dist/hosts/integration/bundle.js +99 -0
- package/dist/hosts/integration/hook-entry.d.ts +1 -0
- package/dist/hosts/integration/hook-entry.js +68 -0
- package/dist/hosts/integration/hook.d.ts +32 -0
- package/dist/hosts/integration/hook.js +781 -0
- package/dist/hosts/integration/pappra-extension-runtime.mjs +24610 -0
- package/dist/hosts/integration/pappra-hook.mjs +24728 -0
- package/dist/hosts/integration/pi-extension-runtime.d.ts +18 -0
- package/dist/hosts/integration/pi-extension-runtime.js +89 -0
- package/dist/hosts/integration/pi-template.d.ts +3 -0
- package/dist/hosts/integration/pi-template.js +5 -0
- package/dist/hosts/integration/service.d.ts +49 -0
- package/dist/hosts/integration/service.js +1421 -0
- package/dist/hosts/integration/support.d.ts +59 -0
- package/dist/hosts/integration/support.js +66 -0
- package/dist/hosts/integration/templates.d.ts +22 -0
- package/dist/hosts/integration/templates.js +75 -0
- package/dist/hosts/pi/protocol.d.ts +22 -0
- package/dist/hosts/pi/protocol.js +28 -0
- package/dist/hosts/pi/query-result.d.ts +22 -0
- package/dist/hosts/pi/query-result.js +710 -0
- package/dist/hosts/pi/rpc.d.ts +939 -0
- package/dist/hosts/pi/rpc.js +1659 -0
- package/dist/hosts/pilot.d.ts +485 -0
- package/dist/hosts/pilot.js +593 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/infra/fs.d.ts +27 -0
- package/dist/infra/fs.js +130 -0
- package/dist/infra/git.d.ts +60 -0
- package/dist/infra/git.js +358 -0
- package/dist/infra/owned-process-group.d.ts +173 -0
- package/dist/infra/owned-process-group.js +571 -0
- package/dist/install/apply.d.ts +65 -0
- package/dist/install/apply.js +663 -0
- package/dist/install/index.d.ts +5 -0
- package/dist/install/index.js +5 -0
- package/dist/install/ownership.d.ts +85 -0
- package/dist/install/ownership.js +430 -0
- package/dist/install/plan.d.ts +39 -0
- package/dist/install/plan.js +97 -0
- package/dist/install/rollback.d.ts +18 -0
- package/dist/install/rollback.js +544 -0
- package/dist/install/runtime.d.ts +76 -0
- package/dist/install/runtime.js +1252 -0
- package/dist/knowledge/catalog.d.ts +47 -0
- package/dist/knowledge/catalog.js +308 -0
- package/dist/knowledge/context.d.ts +28 -0
- package/dist/knowledge/context.js +61 -0
- package/dist/knowledge/index.d.ts +3 -0
- package/dist/knowledge/index.js +1 -0
- package/dist/knowledge/journal.d.ts +16 -0
- package/dist/knowledge/journal.js +109 -0
- package/dist/knowledge/lifecycle.d.ts +8 -0
- package/dist/knowledge/lifecycle.js +1001 -0
- package/dist/knowledge/paths.d.ts +37 -0
- package/dist/knowledge/paths.js +236 -0
- package/dist/knowledge/refs.d.ts +36 -0
- package/dist/knowledge/refs.js +267 -0
- package/dist/knowledge/service.d.ts +21 -0
- package/dist/knowledge/service.js +393 -0
- package/dist/knowledge/types.d.ts +171 -0
- package/dist/knowledge/types.js +1 -0
- package/dist/knowledge/write.d.ts +8 -0
- package/dist/knowledge/write.js +1223 -0
- package/dist/package-info.d.ts +8 -0
- package/dist/package-info.js +25 -0
- package/dist/schemas/code.d.ts +377 -0
- package/dist/schemas/code.js +145 -0
- package/dist/schemas/collaboration.d.ts +115 -0
- package/dist/schemas/collaboration.js +106 -0
- package/dist/schemas/continuity.d.ts +315 -0
- package/dist/schemas/continuity.js +199 -0
- package/dist/schemas/diagnostics.d.ts +28 -0
- package/dist/schemas/diagnostics.js +27 -0
- package/dist/schemas/doctor.d.ts +157 -0
- package/dist/schemas/doctor.js +75 -0
- package/dist/schemas/document.d.ts +644 -0
- package/dist/schemas/document.js +296 -0
- package/dist/schemas/errors.d.ts +177 -0
- package/dist/schemas/errors.js +176 -0
- package/dist/schemas/evidence.d.ts +24 -0
- package/dist/schemas/evidence.js +16 -0
- package/dist/schemas/host.d.ts +436 -0
- package/dist/schemas/host.js +203 -0
- package/dist/schemas/index.d.ts +15 -0
- package/dist/schemas/index.js +15 -0
- package/dist/schemas/init.d.ts +121 -0
- package/dist/schemas/init.js +94 -0
- package/dist/schemas/install.d.ts +227 -0
- package/dist/schemas/install.js +147 -0
- package/dist/schemas/machine.d.ts +43 -0
- package/dist/schemas/machine.js +96 -0
- package/dist/schemas/outcome.d.ts +23 -0
- package/dist/schemas/outcome.js +17 -0
- package/dist/schemas/paths.d.ts +19 -0
- package/dist/schemas/paths.js +63 -0
- package/dist/schemas/workspace.d.ts +120 -0
- package/dist/schemas/workspace.js +201 -0
- package/dist/workspace/config-file.d.ts +36 -0
- package/dist/workspace/config-file.js +237 -0
- package/dist/workspace/config.d.ts +28 -0
- package/dist/workspace/config.js +79 -0
- package/dist/workspace/discover.d.ts +34 -0
- package/dist/workspace/discover.js +195 -0
- package/dist/workspace/documents.d.ts +21 -0
- package/dist/workspace/documents.js +395 -0
- package/dist/workspace/ignore.d.ts +10 -0
- package/dist/workspace/ignore.js +48 -0
- package/dist/workspace/index.d.ts +7 -0
- package/dist/workspace/index.js +3 -0
- package/dist/workspace/repositories.d.ts +24 -0
- package/dist/workspace/repositories.js +411 -0
- package/dist/workspace/resolve.d.ts +24 -0
- package/dist/workspace/resolve.js +268 -0
- package/dist/workspace/types.d.ts +71 -0
- package/dist/workspace/types.js +1 -0
- package/dist/workspace/validation.d.ts +18 -0
- package/dist/workspace/validation.js +197 -0
- package/package.json +111 -4
- package/skills/pappra/SKILL.md +71 -0
- package/skills/pappra/references/hosts.md +40 -0
- package/skills/pappra/references/lifecycle.md +41 -0
- package/skills/pappra/references/operations.md +27 -0
|
@@ -0,0 +1,1001 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { unlink } from 'node:fs/promises';
|
|
3
|
+
import { createDiagnostic } from '../schemas/diagnostics.js';
|
|
4
|
+
import { diagnosticCodes } from '../schemas/errors.js';
|
|
5
|
+
import { knowledgeDeletePlanSchema, } from '../schemas/document.js';
|
|
6
|
+
import { observePath } from '../infra/fs.js';
|
|
7
|
+
import { loadKnowledgeContext, sameRootIdentity } from './context.js';
|
|
8
|
+
import { catalogDigestFor, catalogFor, loadKnowledgeCatalogs, recordScope, removeCatalogRecord, setCatalogRecord, writeCatalogFile, } from './catalog.js';
|
|
9
|
+
import { acquireKnowledgeLock, knowledgeJournalPath, releaseKnowledgeLock, writeKnowledgeJournal, } from './journal.js';
|
|
10
|
+
import { readKnowledgeFile, resolveKnowledgeLocation } from './paths.js';
|
|
11
|
+
import { findCanonicalAlias, findRecord, findRegisteredReferences, } from './refs.js';
|
|
12
|
+
const MAX_DOCUMENT_BYTES = 8 * 1024 * 1024;
|
|
13
|
+
const DELETE_PLAN_MAX_AGE_MS = 15 * 60 * 1000;
|
|
14
|
+
function diag(code, message, source, hint) {
|
|
15
|
+
return createDiagnostic({ code, severity: 'error', message, source, hint });
|
|
16
|
+
}
|
|
17
|
+
function outcomeDiagnostic(failure, source) {
|
|
18
|
+
return createDiagnostic({
|
|
19
|
+
code: failure.code,
|
|
20
|
+
severity: 'error',
|
|
21
|
+
message: failure.message,
|
|
22
|
+
source,
|
|
23
|
+
hint: failure.nextAction,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
async function withLock(context, operation) {
|
|
27
|
+
const lock = await acquireKnowledgeLock(context, operation);
|
|
28
|
+
if (!lock.ok || lock.lock === null) {
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
status: lock.status === 'locked' ? 'locked' : 'failed',
|
|
32
|
+
diagnostics: [
|
|
33
|
+
diag(lock.status === 'locked'
|
|
34
|
+
? diagnosticCodes.document_locked
|
|
35
|
+
: diagnosticCodes.document_write_conflict, lock.message ?? 'Knowledge state is locked.', lock.path, 'Retry after the current knowledge operation finishes.'),
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return { ok: true, lock: lock.lock, path: lock.path };
|
|
40
|
+
}
|
|
41
|
+
export async function archiveDocument(context, documentId) {
|
|
42
|
+
const catalogs = await loadKnowledgeCatalogs(context);
|
|
43
|
+
if (!catalogs.ok) {
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
status: 'conflict',
|
|
47
|
+
record: null,
|
|
48
|
+
diagnostics: [...catalogs.diagnostics],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const record = findRecord(catalogs, documentId);
|
|
52
|
+
if (record === null) {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
status: 'not-found',
|
|
56
|
+
record: null,
|
|
57
|
+
diagnostics: [
|
|
58
|
+
diag(diagnosticCodes.document_not_found, `Document is not registered or is ambiguous: ${documentId}`, context.configPath, 'Read the catalog before retrying.'),
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (record.lifecycle === 'archived') {
|
|
63
|
+
return { ok: true, status: 'unchanged', record, diagnostics: [] };
|
|
64
|
+
}
|
|
65
|
+
if (record.lifecycle === 'superseded') {
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
status: 'conflict',
|
|
69
|
+
record,
|
|
70
|
+
diagnostics: [
|
|
71
|
+
diag(diagnosticCodes.document_supersede_invalid, 'A superseded document cannot be archived with the archive operation.', context.configPath, 'Preserve the supersession chain; archive only active documents.'),
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const lock = await withLock(context, 'archive-document');
|
|
76
|
+
if (!lock.ok) {
|
|
77
|
+
return {
|
|
78
|
+
ok: false,
|
|
79
|
+
status: lock.status,
|
|
80
|
+
record: null,
|
|
81
|
+
diagnostics: [...lock.diagnostics],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const heldLock = lock.lock;
|
|
85
|
+
const archiveResult = await (async () => {
|
|
86
|
+
const reloaded = await loadKnowledgeContext(context.root);
|
|
87
|
+
if (!reloaded.ok || reloaded.context === null) {
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
status: 'failed',
|
|
91
|
+
record: null,
|
|
92
|
+
diagnostics: [...reloaded.diagnostics],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const fresh = await loadKnowledgeCatalogs(reloaded.context);
|
|
96
|
+
const current = fresh.ok ? findRecord(fresh, documentId) : null;
|
|
97
|
+
if (!fresh.ok || current === null) {
|
|
98
|
+
return {
|
|
99
|
+
ok: false,
|
|
100
|
+
status: 'conflict',
|
|
101
|
+
record: null,
|
|
102
|
+
diagnostics: fresh.ok
|
|
103
|
+
? [
|
|
104
|
+
diag(diagnosticCodes.document_not_found, 'Document disappeared before archive.', reloaded.context.configPath, 'Read the catalog again.'),
|
|
105
|
+
]
|
|
106
|
+
: [...fresh.diagnostics],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (current.lifecycle !== 'active') {
|
|
110
|
+
return {
|
|
111
|
+
ok: true,
|
|
112
|
+
status: 'unchanged',
|
|
113
|
+
record: current,
|
|
114
|
+
diagnostics: [...fresh.diagnostics],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const next = { ...current, lifecycle: 'archived' };
|
|
118
|
+
const changed = setCatalogRecord(reloaded.context, fresh, next);
|
|
119
|
+
const written = await writeCatalogFile(reloaded.context, changed.scope, changed.catalog, catalogDigestFor(fresh, changed.scope));
|
|
120
|
+
if (!written.ok) {
|
|
121
|
+
return {
|
|
122
|
+
ok: false,
|
|
123
|
+
status: 'failed',
|
|
124
|
+
record: null,
|
|
125
|
+
diagnostics: [
|
|
126
|
+
...fresh.diagnostics,
|
|
127
|
+
diag(written.code, written.message, written.nextAction ?? reloaded.context.configPath, 'The document file and path were not changed.'),
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
ok: true,
|
|
133
|
+
status: 'archived',
|
|
134
|
+
record: next,
|
|
135
|
+
diagnostics: [...fresh.diagnostics],
|
|
136
|
+
};
|
|
137
|
+
})();
|
|
138
|
+
const release = await releaseKnowledgeLock(heldLock, lock.path);
|
|
139
|
+
if (!release.ok) {
|
|
140
|
+
return {
|
|
141
|
+
...archiveResult,
|
|
142
|
+
ok: false,
|
|
143
|
+
status: 'failed',
|
|
144
|
+
diagnostics: [
|
|
145
|
+
...archiveResult.diagnostics,
|
|
146
|
+
outcomeDiagnostic(release, lock.path),
|
|
147
|
+
],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return archiveResult;
|
|
151
|
+
}
|
|
152
|
+
export async function supersedeDocument(context, input) {
|
|
153
|
+
const catalogs = await loadKnowledgeCatalogs(context);
|
|
154
|
+
if (!catalogs.ok) {
|
|
155
|
+
return {
|
|
156
|
+
ok: false,
|
|
157
|
+
status: 'conflict',
|
|
158
|
+
record: null,
|
|
159
|
+
diagnostics: [...catalogs.diagnostics],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const source = findRecord(catalogs, input.documentId);
|
|
163
|
+
const target = findRecord(catalogs, input.supersededBy);
|
|
164
|
+
if (source === null || target === null) {
|
|
165
|
+
return {
|
|
166
|
+
ok: false,
|
|
167
|
+
status: 'not-found',
|
|
168
|
+
record: null,
|
|
169
|
+
diagnostics: [
|
|
170
|
+
diag(diagnosticCodes.document_supersede_invalid, 'Both the source and replacement document must exist.', context.configPath, 'Register the replacement first; supersede never creates a dangling target.'),
|
|
171
|
+
],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const validate = async (currentCatalogs) => {
|
|
175
|
+
const currentSource = findRecord(currentCatalogs, input.documentId);
|
|
176
|
+
const currentTarget = findRecord(currentCatalogs, input.supersededBy);
|
|
177
|
+
if (currentSource === null || currentTarget === null) {
|
|
178
|
+
return diag(diagnosticCodes.document_supersede_invalid, 'A supersede endpoint disappeared before the operation.', context.configPath, 'Read the catalog again.');
|
|
179
|
+
}
|
|
180
|
+
if (input.documentId === input.supersededBy) {
|
|
181
|
+
return diag(diagnosticCodes.document_supersede_invalid, 'A document cannot supersede itself.', context.configPath, 'Choose a different replacement document.');
|
|
182
|
+
}
|
|
183
|
+
if (currentSource.visibility === 'shareable' &&
|
|
184
|
+
currentTarget.visibility === 'private') {
|
|
185
|
+
return diag(diagnosticCodes.document_visibility_invalid, 'A shareable document cannot point to a private replacement.', context.configPath, 'Keep the supersession chain within the same visibility boundary.');
|
|
186
|
+
}
|
|
187
|
+
if (currentSource.lifecycle === 'archived') {
|
|
188
|
+
return diag(diagnosticCodes.document_supersede_invalid, 'An archived document cannot be superseded.', context.configPath, 'Preserve the archived record; supersede active documents.');
|
|
189
|
+
}
|
|
190
|
+
let current = currentTarget;
|
|
191
|
+
const seen = new Set([currentSource.documentId]);
|
|
192
|
+
while (current !== null) {
|
|
193
|
+
if (seen.has(current.documentId)) {
|
|
194
|
+
return diag(diagnosticCodes.document_supersede_invalid, 'The proposed replacement chain contains a cycle or returns to the source.', context.configPath, 'Choose a replacement outside the existing chain.');
|
|
195
|
+
}
|
|
196
|
+
seen.add(current.documentId);
|
|
197
|
+
if (current.supersededBy === null)
|
|
198
|
+
break;
|
|
199
|
+
current = findRecord(currentCatalogs, current.supersededBy);
|
|
200
|
+
if (current === null) {
|
|
201
|
+
return diag(diagnosticCodes.document_supersede_invalid, 'The existing supersession chain has a dangling replacement.', context.configPath, 'Repair the dangling chain before adding another supersession.');
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return { source: currentSource, target: currentTarget };
|
|
205
|
+
};
|
|
206
|
+
const initial = await validate(catalogs);
|
|
207
|
+
if ('code' in initial) {
|
|
208
|
+
return {
|
|
209
|
+
ok: false,
|
|
210
|
+
status: 'conflict',
|
|
211
|
+
record: null,
|
|
212
|
+
diagnostics: [initial],
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
if (initial.source.lifecycle === 'superseded' &&
|
|
216
|
+
initial.source.supersededBy === input.supersededBy) {
|
|
217
|
+
return {
|
|
218
|
+
ok: true,
|
|
219
|
+
status: 'unchanged',
|
|
220
|
+
record: initial.source,
|
|
221
|
+
diagnostics: [],
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
if (initial.source.lifecycle === 'superseded') {
|
|
225
|
+
return {
|
|
226
|
+
ok: false,
|
|
227
|
+
status: 'conflict',
|
|
228
|
+
record: initial.source,
|
|
229
|
+
diagnostics: [
|
|
230
|
+
diag(diagnosticCodes.document_supersede_invalid, 'The document already points to a different replacement.', context.configPath, 'Preserve the existing chain; do not repoint historical supersessions.'),
|
|
231
|
+
],
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
const lock = await withLock(context, 'supersede-document');
|
|
235
|
+
if (!lock.ok) {
|
|
236
|
+
return {
|
|
237
|
+
ok: false,
|
|
238
|
+
status: lock.status,
|
|
239
|
+
record: null,
|
|
240
|
+
diagnostics: [...lock.diagnostics],
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
const heldLock = lock.lock;
|
|
244
|
+
const supersedeResult = await (async () => {
|
|
245
|
+
const reloaded = await loadKnowledgeContext(context.root);
|
|
246
|
+
if (!reloaded.ok || reloaded.context === null) {
|
|
247
|
+
return {
|
|
248
|
+
ok: false,
|
|
249
|
+
status: 'failed',
|
|
250
|
+
record: null,
|
|
251
|
+
diagnostics: [...reloaded.diagnostics],
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
const fresh = await loadKnowledgeCatalogs(reloaded.context);
|
|
255
|
+
if (!fresh.ok) {
|
|
256
|
+
return {
|
|
257
|
+
ok: false,
|
|
258
|
+
status: 'conflict',
|
|
259
|
+
record: null,
|
|
260
|
+
diagnostics: [...fresh.diagnostics],
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
const current = await validate(fresh);
|
|
264
|
+
if ('code' in current) {
|
|
265
|
+
return {
|
|
266
|
+
ok: false,
|
|
267
|
+
status: 'conflict',
|
|
268
|
+
record: null,
|
|
269
|
+
diagnostics: [current],
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
const next = {
|
|
273
|
+
...current.source,
|
|
274
|
+
lifecycle: 'superseded',
|
|
275
|
+
supersededBy: current.target.documentId,
|
|
276
|
+
};
|
|
277
|
+
const changed = setCatalogRecord(reloaded.context, fresh, next);
|
|
278
|
+
const written = await writeCatalogFile(reloaded.context, changed.scope, changed.catalog, catalogDigestFor(fresh, changed.scope));
|
|
279
|
+
if (!written.ok) {
|
|
280
|
+
return {
|
|
281
|
+
ok: false,
|
|
282
|
+
status: 'failed',
|
|
283
|
+
record: null,
|
|
284
|
+
diagnostics: [
|
|
285
|
+
...fresh.diagnostics,
|
|
286
|
+
outcomeDiagnostic(written, reloaded.context.configPath),
|
|
287
|
+
],
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
ok: true,
|
|
292
|
+
status: 'superseded',
|
|
293
|
+
record: next,
|
|
294
|
+
diagnostics: [...fresh.diagnostics],
|
|
295
|
+
};
|
|
296
|
+
})();
|
|
297
|
+
const release = await releaseKnowledgeLock(heldLock, lock.path);
|
|
298
|
+
if (!release.ok) {
|
|
299
|
+
return {
|
|
300
|
+
...supersedeResult,
|
|
301
|
+
ok: false,
|
|
302
|
+
status: 'failed',
|
|
303
|
+
diagnostics: [
|
|
304
|
+
...supersedeResult.diagnostics,
|
|
305
|
+
outcomeDiagnostic(release, lock.path),
|
|
306
|
+
],
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
return supersedeResult;
|
|
310
|
+
}
|
|
311
|
+
export async function shareDocument(context, documentId) {
|
|
312
|
+
const catalogs = await loadKnowledgeCatalogs(context);
|
|
313
|
+
if (!catalogs.ok) {
|
|
314
|
+
return {
|
|
315
|
+
ok: false,
|
|
316
|
+
status: 'conflict',
|
|
317
|
+
record: null,
|
|
318
|
+
diagnostics: [...catalogs.diagnostics],
|
|
319
|
+
residualPaths: [],
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
const record = findRecord(catalogs, documentId);
|
|
323
|
+
if (record === null) {
|
|
324
|
+
return {
|
|
325
|
+
ok: false,
|
|
326
|
+
status: 'not-found',
|
|
327
|
+
record: null,
|
|
328
|
+
diagnostics: [
|
|
329
|
+
diag(diagnosticCodes.document_not_found, `Document is not registered or is ambiguous: ${documentId}`, context.configPath, 'Read the catalog before retrying.'),
|
|
330
|
+
],
|
|
331
|
+
residualPaths: [],
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
if (record.visibility === 'shareable') {
|
|
335
|
+
return {
|
|
336
|
+
ok: true,
|
|
337
|
+
status: 'unchanged',
|
|
338
|
+
record,
|
|
339
|
+
diagnostics: [],
|
|
340
|
+
residualPaths: [],
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
if (record.taskId !== null) {
|
|
344
|
+
return {
|
|
345
|
+
ok: false,
|
|
346
|
+
status: 'conflict',
|
|
347
|
+
record,
|
|
348
|
+
diagnostics: [
|
|
349
|
+
diag(diagnosticCodes.document_task_state_unknown, 'A private task-linked document cannot be moved to the shared catalog automatically.', context.configPath, 'Review task ownership and remove the private task link explicitly before sharing.'),
|
|
350
|
+
],
|
|
351
|
+
residualPaths: [],
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
const lock = await withLock(context, 'share-document');
|
|
355
|
+
if (!lock.ok) {
|
|
356
|
+
return {
|
|
357
|
+
ok: false,
|
|
358
|
+
status: lock.status,
|
|
359
|
+
record: null,
|
|
360
|
+
diagnostics: [...lock.diagnostics],
|
|
361
|
+
residualPaths: [],
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
const heldLock = lock.lock;
|
|
365
|
+
const shareResult = await (async () => {
|
|
366
|
+
const reloaded = await loadKnowledgeContext(context.root);
|
|
367
|
+
if (!reloaded.ok || reloaded.context === null) {
|
|
368
|
+
return {
|
|
369
|
+
ok: false,
|
|
370
|
+
status: 'failed',
|
|
371
|
+
record: null,
|
|
372
|
+
diagnostics: [...reloaded.diagnostics],
|
|
373
|
+
residualPaths: [],
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
const fresh = await loadKnowledgeCatalogs(reloaded.context);
|
|
377
|
+
const current = fresh.ok ? findRecord(fresh, documentId) : null;
|
|
378
|
+
if (!fresh.ok || current === null) {
|
|
379
|
+
return {
|
|
380
|
+
ok: false,
|
|
381
|
+
status: 'conflict',
|
|
382
|
+
record: null,
|
|
383
|
+
diagnostics: fresh.ok
|
|
384
|
+
? [
|
|
385
|
+
diag(diagnosticCodes.document_not_found, 'Document disappeared before sharing.', reloaded.context.configPath, 'Read the catalog again.'),
|
|
386
|
+
]
|
|
387
|
+
: [...fresh.diagnostics],
|
|
388
|
+
residualPaths: [],
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
if (current.taskId !== null) {
|
|
392
|
+
return {
|
|
393
|
+
ok: false,
|
|
394
|
+
status: 'conflict',
|
|
395
|
+
record: current,
|
|
396
|
+
diagnostics: [
|
|
397
|
+
diag(diagnosticCodes.document_task_state_unknown, 'Task-linked documents require explicit task ownership review before sharing.', reloaded.context.configPath, 'No catalog or file was changed.'),
|
|
398
|
+
],
|
|
399
|
+
residualPaths: [],
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
const supersessionTarget = current.supersededBy === null
|
|
403
|
+
? null
|
|
404
|
+
: findRecord(fresh, current.supersededBy);
|
|
405
|
+
if (current.supersededBy !== null &&
|
|
406
|
+
(supersessionTarget === null ||
|
|
407
|
+
supersessionTarget.visibility === 'private')) {
|
|
408
|
+
return {
|
|
409
|
+
ok: false,
|
|
410
|
+
status: 'conflict',
|
|
411
|
+
record: current,
|
|
412
|
+
diagnostics: [
|
|
413
|
+
diag(diagnosticCodes.document_visibility_invalid, 'A private supersession target would enter the shared catalog.', reloaded.context.configPath, 'Review or share the replacement document explicitly first.'),
|
|
414
|
+
],
|
|
415
|
+
residualPaths: [],
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
const privateReference = current.sources.find((source) => source.kind === 'document' &&
|
|
419
|
+
findRecord(fresh, source.documentId)?.visibility === 'private');
|
|
420
|
+
if (privateReference !== undefined) {
|
|
421
|
+
return {
|
|
422
|
+
ok: false,
|
|
423
|
+
status: 'conflict',
|
|
424
|
+
record: current,
|
|
425
|
+
diagnostics: [
|
|
426
|
+
diag(diagnosticCodes.document_visibility_invalid, 'A private source reference would enter the shared catalog.', reloaded.context.configPath, 'Review the referenced documents and share them explicitly first.'),
|
|
427
|
+
],
|
|
428
|
+
residualPaths: [],
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
const next = { ...current, visibility: 'shareable' };
|
|
432
|
+
const sharedCatalog = catalogFor(fresh, 'shared', reloaded.context);
|
|
433
|
+
const sharedWrite = await writeCatalogFile(reloaded.context, 'shared', {
|
|
434
|
+
...sharedCatalog,
|
|
435
|
+
updatedAt: new Date().toISOString(),
|
|
436
|
+
records: [...sharedCatalog.records, next].sort((left, right) => left.documentId.localeCompare(right.documentId)),
|
|
437
|
+
}, catalogDigestFor(fresh, 'shared'));
|
|
438
|
+
if (!sharedWrite.ok) {
|
|
439
|
+
return {
|
|
440
|
+
ok: false,
|
|
441
|
+
status: 'failed',
|
|
442
|
+
record: null,
|
|
443
|
+
diagnostics: [
|
|
444
|
+
...fresh.diagnostics,
|
|
445
|
+
outcomeDiagnostic(sharedWrite, reloaded.context.configPath),
|
|
446
|
+
],
|
|
447
|
+
residualPaths: [],
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
const privateCatalog = catalogFor(fresh, 'private', reloaded.context);
|
|
451
|
+
const privateWrite = await writeCatalogFile(reloaded.context, 'private', {
|
|
452
|
+
...privateCatalog,
|
|
453
|
+
updatedAt: new Date().toISOString(),
|
|
454
|
+
records: privateCatalog.records.filter((entry) => entry.documentId !== documentId),
|
|
455
|
+
}, catalogDigestFor(fresh, 'private'));
|
|
456
|
+
if (!privateWrite.ok) {
|
|
457
|
+
const rollback = await writeCatalogFile(reloaded.context, 'shared', sharedCatalog, sharedWrite.value.digest);
|
|
458
|
+
const privatePath = reloaded.context.root + '/.pappra/local/knowledge/catalog.json';
|
|
459
|
+
if (rollback.ok) {
|
|
460
|
+
return {
|
|
461
|
+
ok: false,
|
|
462
|
+
status: 'failed',
|
|
463
|
+
record: null,
|
|
464
|
+
diagnostics: [
|
|
465
|
+
...fresh.diagnostics,
|
|
466
|
+
outcomeDiagnostic(privateWrite, privatePath),
|
|
467
|
+
diag(diagnosticCodes.document_visibility_invalid, 'The shared catalog update was rolled back because the private catalog could not be updated.', reloaded.context.configPath, 'The record remains private; retry the explicit share operation.'),
|
|
468
|
+
],
|
|
469
|
+
residualPaths: [],
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
return {
|
|
473
|
+
ok: false,
|
|
474
|
+
status: 'partial',
|
|
475
|
+
record: null,
|
|
476
|
+
diagnostics: [
|
|
477
|
+
...fresh.diagnostics,
|
|
478
|
+
outcomeDiagnostic(privateWrite, privatePath),
|
|
479
|
+
diag(diagnosticCodes.document_visibility_invalid, 'Sharing is partially applied and the private catalog could not be updated; duplicate-id conflict is possible.', reloaded.context.configPath, 'Inspect both catalogs and the journal state manually; no auto-repair is attempted.'),
|
|
480
|
+
],
|
|
481
|
+
residualPaths: [privatePath],
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
ok: true,
|
|
486
|
+
status: 'shared',
|
|
487
|
+
record: next,
|
|
488
|
+
diagnostics: [...fresh.diagnostics],
|
|
489
|
+
residualPaths: [],
|
|
490
|
+
};
|
|
491
|
+
})();
|
|
492
|
+
const release = await releaseKnowledgeLock(heldLock, lock.path);
|
|
493
|
+
if (!release.ok) {
|
|
494
|
+
return {
|
|
495
|
+
...shareResult,
|
|
496
|
+
ok: false,
|
|
497
|
+
status: shareResult.ok ? 'partial' : shareResult.status,
|
|
498
|
+
diagnostics: [
|
|
499
|
+
...shareResult.diagnostics,
|
|
500
|
+
outcomeDiagnostic(release, lock.path),
|
|
501
|
+
],
|
|
502
|
+
residualPaths: [...shareResult.residualPaths, lock.path],
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
return shareResult;
|
|
506
|
+
}
|
|
507
|
+
async function inspectDelete(context, input) {
|
|
508
|
+
const catalogs = await loadKnowledgeCatalogs(context);
|
|
509
|
+
if (!catalogs.ok) {
|
|
510
|
+
return {
|
|
511
|
+
ok: false,
|
|
512
|
+
status: 'failed',
|
|
513
|
+
plan: null,
|
|
514
|
+
diagnostics: [...catalogs.diagnostics],
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
const record = findRecord(catalogs, input.documentId);
|
|
518
|
+
if (record === null) {
|
|
519
|
+
return {
|
|
520
|
+
ok: false,
|
|
521
|
+
status: 'not-found',
|
|
522
|
+
plan: null,
|
|
523
|
+
diagnostics: [
|
|
524
|
+
diag(diagnosticCodes.document_not_found, `Document is not registered or is ambiguous: ${input.documentId}`, context.configPath, 'Read the catalog before deleting.'),
|
|
525
|
+
],
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
const blockers = [];
|
|
529
|
+
const references = findRegisteredReferences(catalogs, record.documentId);
|
|
530
|
+
for (const reference of references) {
|
|
531
|
+
blockers.push({
|
|
532
|
+
code: diagnosticCodes.document_delete_blocked,
|
|
533
|
+
message: `Document is referenced by ${reference.documentId} (${reference.relation}).`,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
if (record.taskId !== null) {
|
|
537
|
+
blockers.push({
|
|
538
|
+
code: diagnosticCodes.document_task_state_unknown,
|
|
539
|
+
message: `Document is linked to task ${record.taskId}; execution state is unknown without P07/P10 evidence.`,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
const recordLocation = await resolveKnowledgeLocation(context, record.location);
|
|
543
|
+
if (recordLocation.ok) {
|
|
544
|
+
const alias = await findCanonicalAlias(context, catalogs, recordLocation.value, record.documentId);
|
|
545
|
+
if (alias !== null) {
|
|
546
|
+
blockers.push({
|
|
547
|
+
code: diagnosticCodes.document_location_conflict,
|
|
548
|
+
message: `Canonical document path is also registered by ${alias.documentId}.`,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
let expectedFileDigest = null;
|
|
553
|
+
let expectedFileIdentity = null;
|
|
554
|
+
if (input.removeFile) {
|
|
555
|
+
const resolved = await resolveKnowledgeLocation(context, record.location);
|
|
556
|
+
if (!resolved.ok) {
|
|
557
|
+
blockers.push({
|
|
558
|
+
code: resolved.code,
|
|
559
|
+
message: resolved.message,
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
if (resolved.value.access !== 'write') {
|
|
564
|
+
blockers.push({
|
|
565
|
+
code: diagnosticCodes.document_access_denied,
|
|
566
|
+
message: 'Document file belongs to a read-only repository.',
|
|
567
|
+
});
|
|
568
|
+
expectedFileDigest = null;
|
|
569
|
+
expectedFileIdentity = null;
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
const file = await readKnowledgeFile(resolved.value.absolutePath, MAX_DOCUMENT_BYTES);
|
|
573
|
+
if (!file.ok) {
|
|
574
|
+
blockers.push({ code: file.code, message: file.message });
|
|
575
|
+
}
|
|
576
|
+
else if (file.value.digest !== record.digest) {
|
|
577
|
+
blockers.push({
|
|
578
|
+
code: diagnosticCodes.document_file_changed,
|
|
579
|
+
message: 'Document bytes no longer match the registered digest.',
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
const observation = await observePath(resolved.value.absolutePath);
|
|
584
|
+
if (observation.stats === null) {
|
|
585
|
+
blockers.push({
|
|
586
|
+
code: diagnosticCodes.document_path_unsafe,
|
|
587
|
+
message: 'Cannot observe document file identity.',
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
expectedFileDigest = file.value.digest;
|
|
592
|
+
expectedFileIdentity = {
|
|
593
|
+
dev: observation.stats.dev,
|
|
594
|
+
ino: observation.stats.ino,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
const plan = {
|
|
602
|
+
schema: 'pappra/knowledge-delete-plan/1',
|
|
603
|
+
planId: randomUUID(),
|
|
604
|
+
createdAt: new Date().toISOString(),
|
|
605
|
+
documentId: record.documentId,
|
|
606
|
+
removeFile: input.removeFile,
|
|
607
|
+
location: record.location,
|
|
608
|
+
expectedFileDigest,
|
|
609
|
+
expectedFileIdentity,
|
|
610
|
+
catalogScope: recordScope(catalogs, record),
|
|
611
|
+
catalogDigest: catalogs.catalogDigest,
|
|
612
|
+
rootIdentity: context.rootIdentity,
|
|
613
|
+
references: [...references],
|
|
614
|
+
pathReferences: 'unchecked',
|
|
615
|
+
coverage: 'catalog-only',
|
|
616
|
+
taskState: record.taskId === null ? 'not-linked' : 'unknown',
|
|
617
|
+
blockers,
|
|
618
|
+
deletable: blockers.length === 0,
|
|
619
|
+
};
|
|
620
|
+
const parsed = knowledgeDeletePlanSchema.safeParse(plan);
|
|
621
|
+
if (!parsed.success) {
|
|
622
|
+
return {
|
|
623
|
+
ok: false,
|
|
624
|
+
status: 'failed',
|
|
625
|
+
plan: null,
|
|
626
|
+
diagnostics: [
|
|
627
|
+
diag(diagnosticCodes.document_delete_blocked, `Delete plan is invalid: ${parsed.error.issues[0]?.message ?? 'unknown issue'}`, context.configPath, 'Inspect the record; no file was deleted.'),
|
|
628
|
+
],
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
const diagnostics = [...catalogs.diagnostics];
|
|
632
|
+
for (const blocker of blockers) {
|
|
633
|
+
diagnostics.push({
|
|
634
|
+
code: diagnosticCodes.document_delete_blocked,
|
|
635
|
+
severity: 'warning',
|
|
636
|
+
message: blocker.message,
|
|
637
|
+
source: context.configPath,
|
|
638
|
+
pointer: null,
|
|
639
|
+
hint: 'Delete is blocked; resolve the reference/task/file state explicitly.',
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
return {
|
|
643
|
+
ok: true,
|
|
644
|
+
status: plan.deletable ? 'ready' : 'blocked',
|
|
645
|
+
plan: parsed.data,
|
|
646
|
+
diagnostics,
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
export async function previewDelete(context, input) {
|
|
650
|
+
return inspectDelete(context, input);
|
|
651
|
+
}
|
|
652
|
+
function sameFileIdentity(left, right) {
|
|
653
|
+
if (left === null || right === null)
|
|
654
|
+
return left === right;
|
|
655
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
656
|
+
}
|
|
657
|
+
function planMismatch(plan, next) {
|
|
658
|
+
return (plan.documentId !== next.documentId ||
|
|
659
|
+
plan.removeFile !== next.removeFile ||
|
|
660
|
+
JSON.stringify(plan.location) !== JSON.stringify(next.location) ||
|
|
661
|
+
plan.catalogDigest !== next.catalogDigest ||
|
|
662
|
+
plan.expectedFileDigest !== next.expectedFileDigest ||
|
|
663
|
+
!sameFileIdentity(plan.expectedFileIdentity, next.expectedFileIdentity) ||
|
|
664
|
+
plan.catalogScope !== next.catalogScope ||
|
|
665
|
+
plan.taskState !== next.taskState ||
|
|
666
|
+
plan.deletable !== next.deletable ||
|
|
667
|
+
plan.references.length !== next.references.length);
|
|
668
|
+
}
|
|
669
|
+
export async function deleteExplicit(context, plan) {
|
|
670
|
+
const parsedPlan = knowledgeDeletePlanSchema.safeParse(plan);
|
|
671
|
+
if (!parsedPlan.success) {
|
|
672
|
+
return {
|
|
673
|
+
ok: false,
|
|
674
|
+
status: 'stale',
|
|
675
|
+
documentId: null,
|
|
676
|
+
diagnostics: [
|
|
677
|
+
diag(diagnosticCodes.document_delete_stale, `Delete plan is invalid: ${parsedPlan.error.issues[0]?.message ?? 'unknown issue'}`, context.configPath, 'Generate a fresh preview.'),
|
|
678
|
+
],
|
|
679
|
+
residualPaths: [],
|
|
680
|
+
journalPath: null,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
const planAge = Date.now() - Date.parse(plan.createdAt);
|
|
684
|
+
if (!Number.isFinite(planAge) ||
|
|
685
|
+
planAge < 0 ||
|
|
686
|
+
planAge > DELETE_PLAN_MAX_AGE_MS) {
|
|
687
|
+
return {
|
|
688
|
+
ok: false,
|
|
689
|
+
status: 'stale',
|
|
690
|
+
documentId: plan.documentId,
|
|
691
|
+
diagnostics: [
|
|
692
|
+
diag(diagnosticCodes.document_delete_stale, 'Delete preview has expired; generate and confirm a fresh plan.', context.configPath, 'No file or catalog was removed.'),
|
|
693
|
+
],
|
|
694
|
+
residualPaths: [],
|
|
695
|
+
journalPath: null,
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
if (!plan.deletable || plan.blockers.length > 0) {
|
|
699
|
+
return {
|
|
700
|
+
ok: false,
|
|
701
|
+
status: 'blocked',
|
|
702
|
+
documentId: plan.documentId,
|
|
703
|
+
diagnostics: [
|
|
704
|
+
diag(diagnosticCodes.document_delete_blocked, 'Delete plan has unresolved blockers.', context.configPath, 'Resolve references/task/file blockers and preview again.'),
|
|
705
|
+
],
|
|
706
|
+
residualPaths: [],
|
|
707
|
+
journalPath: null,
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
const lock = await acquireKnowledgeLock(context, 'delete-document');
|
|
711
|
+
if (!lock.ok || lock.lock === null) {
|
|
712
|
+
return {
|
|
713
|
+
ok: false,
|
|
714
|
+
status: lock.status === 'locked' ? 'locked' : 'failed',
|
|
715
|
+
documentId: plan.documentId,
|
|
716
|
+
diagnostics: [
|
|
717
|
+
diag(lock.status === 'locked'
|
|
718
|
+
? diagnosticCodes.document_locked
|
|
719
|
+
: diagnosticCodes.document_delete_blocked, lock.message ?? 'Knowledge state is locked.', lock.path, 'Retry after the current knowledge operation finishes.'),
|
|
720
|
+
],
|
|
721
|
+
residualPaths: [],
|
|
722
|
+
journalPath: null,
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
const operationId = randomUUID();
|
|
726
|
+
const journalPath = knowledgeJournalPath(context, operationId);
|
|
727
|
+
let residualPaths = [];
|
|
728
|
+
const deleteResult = await (async () => {
|
|
729
|
+
const reloaded = await loadKnowledgeContext(context.root);
|
|
730
|
+
if (!reloaded.ok || reloaded.context === null) {
|
|
731
|
+
return {
|
|
732
|
+
ok: false,
|
|
733
|
+
status: 'failed',
|
|
734
|
+
documentId: plan.documentId,
|
|
735
|
+
diagnostics: [...reloaded.diagnostics],
|
|
736
|
+
residualPaths: [],
|
|
737
|
+
journalPath,
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
const currentContext = reloaded.context;
|
|
741
|
+
if (!sameRootIdentity(currentContext, plan.rootIdentity) ||
|
|
742
|
+
plan.planId.length === 0) {
|
|
743
|
+
return {
|
|
744
|
+
ok: false,
|
|
745
|
+
status: 'stale',
|
|
746
|
+
documentId: plan.documentId,
|
|
747
|
+
diagnostics: [
|
|
748
|
+
diag(diagnosticCodes.document_delete_stale, 'Workspace root identity changed since the delete preview.', currentContext.configPath, 'Generate a fresh preview.'),
|
|
749
|
+
],
|
|
750
|
+
residualPaths: [],
|
|
751
|
+
journalPath,
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
const fresh = await inspectDelete(currentContext, {
|
|
755
|
+
documentId: plan.documentId,
|
|
756
|
+
removeFile: plan.removeFile,
|
|
757
|
+
});
|
|
758
|
+
if (!fresh.ok ||
|
|
759
|
+
fresh.plan === null ||
|
|
760
|
+
!fresh.plan.deletable ||
|
|
761
|
+
planMismatch(plan, fresh.plan) ||
|
|
762
|
+
fresh.plan.catalogDigest !== plan.catalogDigest) {
|
|
763
|
+
return {
|
|
764
|
+
ok: false,
|
|
765
|
+
status: 'stale',
|
|
766
|
+
documentId: plan.documentId,
|
|
767
|
+
diagnostics: [
|
|
768
|
+
...fresh.diagnostics,
|
|
769
|
+
diag(diagnosticCodes.document_delete_stale, 'Delete preview is no longer current; no file or catalog was removed.', currentContext.configPath, 'Generate a fresh preview and confirm it again.'),
|
|
770
|
+
],
|
|
771
|
+
residualPaths: [],
|
|
772
|
+
journalPath,
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
const catalogs = await loadKnowledgeCatalogs(currentContext);
|
|
776
|
+
if (!catalogs.ok || catalogs.catalogDigest !== plan.catalogDigest) {
|
|
777
|
+
return {
|
|
778
|
+
ok: false,
|
|
779
|
+
status: 'stale',
|
|
780
|
+
documentId: plan.documentId,
|
|
781
|
+
diagnostics: [
|
|
782
|
+
...catalogs.diagnostics,
|
|
783
|
+
diag(diagnosticCodes.document_delete_stale, 'Catalog changed after the delete preview; no file or catalog was removed.', currentContext.configPath, 'Generate a fresh preview and confirm it again.'),
|
|
784
|
+
],
|
|
785
|
+
residualPaths: [],
|
|
786
|
+
journalPath,
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
const record = findRecord(catalogs, plan.documentId);
|
|
790
|
+
if (record === null) {
|
|
791
|
+
return {
|
|
792
|
+
ok: false,
|
|
793
|
+
status: 'stale',
|
|
794
|
+
documentId: plan.documentId,
|
|
795
|
+
diagnostics: [
|
|
796
|
+
diag(diagnosticCodes.document_delete_stale, 'Document disappeared before delete.', currentContext.configPath, 'No file or catalog was removed.'),
|
|
797
|
+
],
|
|
798
|
+
residualPaths: [],
|
|
799
|
+
journalPath,
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
const removed = removeCatalogRecord(currentContext, catalogs, record);
|
|
803
|
+
const initialJournal = {
|
|
804
|
+
schema: 'pappra/knowledge-journal/1',
|
|
805
|
+
operationId,
|
|
806
|
+
operation: 'delete-document',
|
|
807
|
+
stage: 'prepared',
|
|
808
|
+
documentId: record.documentId,
|
|
809
|
+
targetPath: null,
|
|
810
|
+
catalogScope: removed.scope,
|
|
811
|
+
expectedDigest: plan.expectedFileDigest,
|
|
812
|
+
fileDigest: plan.expectedFileDigest,
|
|
813
|
+
catalogDigest: catalogs.catalogDigest,
|
|
814
|
+
createdAt: plan.createdAt,
|
|
815
|
+
updatedAt: new Date().toISOString(),
|
|
816
|
+
residualPaths: [],
|
|
817
|
+
detail: plan.removeFile
|
|
818
|
+
? 'Deleting document file and record.'
|
|
819
|
+
: 'Removing catalog record only.',
|
|
820
|
+
};
|
|
821
|
+
const journalWritten = await writeKnowledgeJournal(currentContext, initialJournal);
|
|
822
|
+
if (!journalWritten.ok) {
|
|
823
|
+
return {
|
|
824
|
+
ok: false,
|
|
825
|
+
status: 'failed',
|
|
826
|
+
documentId: record.documentId,
|
|
827
|
+
diagnostics: [outcomeDiagnostic(journalWritten, journalPath)],
|
|
828
|
+
residualPaths: [],
|
|
829
|
+
journalPath,
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
const catalogWritten = await writeCatalogFile(currentContext, removed.scope, removed.catalog, catalogDigestFor(catalogs, removed.scope));
|
|
833
|
+
if (!catalogWritten.ok) {
|
|
834
|
+
return {
|
|
835
|
+
ok: false,
|
|
836
|
+
status: 'failed',
|
|
837
|
+
documentId: record.documentId,
|
|
838
|
+
diagnostics: [outcomeDiagnostic(catalogWritten, journalPath)],
|
|
839
|
+
residualPaths: [],
|
|
840
|
+
journalPath,
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
const catalogStage = {
|
|
844
|
+
...initialJournal,
|
|
845
|
+
stage: 'catalog-written',
|
|
846
|
+
updatedAt: new Date().toISOString(),
|
|
847
|
+
detail: plan.removeFile
|
|
848
|
+
? 'Catalog record removed; file deletion is pending.'
|
|
849
|
+
: 'Catalog record removed; file retained.',
|
|
850
|
+
};
|
|
851
|
+
const catalogStageWritten = await writeKnowledgeJournal(currentContext, catalogStage);
|
|
852
|
+
if (!catalogStageWritten.ok) {
|
|
853
|
+
return {
|
|
854
|
+
ok: false,
|
|
855
|
+
status: 'partial',
|
|
856
|
+
documentId: record.documentId,
|
|
857
|
+
diagnostics: [
|
|
858
|
+
...currentContext.diagnostics,
|
|
859
|
+
outcomeDiagnostic(catalogStageWritten, journalPath),
|
|
860
|
+
],
|
|
861
|
+
residualPaths: [journalPath],
|
|
862
|
+
journalPath,
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
if (!plan.removeFile) {
|
|
866
|
+
const complete = await writeKnowledgeJournal(currentContext, {
|
|
867
|
+
...catalogStage,
|
|
868
|
+
stage: 'complete',
|
|
869
|
+
updatedAt: new Date().toISOString(),
|
|
870
|
+
detail: 'Catalog record removed; file retained by explicit plan.',
|
|
871
|
+
});
|
|
872
|
+
if (!complete.ok) {
|
|
873
|
+
return {
|
|
874
|
+
ok: false,
|
|
875
|
+
status: 'partial',
|
|
876
|
+
documentId: record.documentId,
|
|
877
|
+
diagnostics: [
|
|
878
|
+
...currentContext.diagnostics,
|
|
879
|
+
outcomeDiagnostic(complete, journalPath),
|
|
880
|
+
],
|
|
881
|
+
residualPaths: [journalPath],
|
|
882
|
+
journalPath,
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
return {
|
|
886
|
+
ok: true,
|
|
887
|
+
status: 'unregistered',
|
|
888
|
+
documentId: record.documentId,
|
|
889
|
+
diagnostics: [...currentContext.diagnostics],
|
|
890
|
+
residualPaths: [],
|
|
891
|
+
journalPath,
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
const resolved = await resolveKnowledgeLocation(currentContext, record.location, { forWrite: plan.removeFile });
|
|
895
|
+
if (!resolved.ok) {
|
|
896
|
+
residualPaths = [];
|
|
897
|
+
return {
|
|
898
|
+
ok: false,
|
|
899
|
+
status: 'partial',
|
|
900
|
+
documentId: record.documentId,
|
|
901
|
+
diagnostics: [
|
|
902
|
+
diag(resolved.code, resolved.message, resolved.nextAction ?? currentContext.configPath, 'Catalog record was removed; the file path could not be resolved and was preserved.'),
|
|
903
|
+
],
|
|
904
|
+
residualPaths,
|
|
905
|
+
journalPath,
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
residualPaths = [resolved.value.absolutePath];
|
|
909
|
+
const file = await readKnowledgeFile(resolved.value.absolutePath, MAX_DOCUMENT_BYTES);
|
|
910
|
+
const observation = await observePath(resolved.value.absolutePath);
|
|
911
|
+
if (!file.ok ||
|
|
912
|
+
file.value.digest !== plan.expectedFileDigest ||
|
|
913
|
+
observation.stats === null ||
|
|
914
|
+
plan.expectedFileIdentity === null ||
|
|
915
|
+
observation.stats.dev !== plan.expectedFileIdentity.dev ||
|
|
916
|
+
observation.stats.ino !== plan.expectedFileIdentity.ino) {
|
|
917
|
+
await writeKnowledgeJournal(currentContext, {
|
|
918
|
+
...catalogStage,
|
|
919
|
+
stage: 'failed',
|
|
920
|
+
residualPaths,
|
|
921
|
+
updatedAt: new Date().toISOString(),
|
|
922
|
+
detail: 'Catalog record removed, but the document file changed; file preserved.',
|
|
923
|
+
});
|
|
924
|
+
return {
|
|
925
|
+
ok: false,
|
|
926
|
+
status: 'partial',
|
|
927
|
+
documentId: record.documentId,
|
|
928
|
+
diagnostics: [
|
|
929
|
+
diag(diagnosticCodes.document_delete_stale, 'Document file changed after preview; catalog was removed but the file was preserved.', resolved.value.absolutePath, 'Inspect the journal and re-register or remove the file explicitly.'),
|
|
930
|
+
],
|
|
931
|
+
residualPaths,
|
|
932
|
+
journalPath,
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
try {
|
|
936
|
+
await unlink(resolved.value.absolutePath);
|
|
937
|
+
}
|
|
938
|
+
catch (error) {
|
|
939
|
+
await writeKnowledgeJournal(currentContext, {
|
|
940
|
+
...catalogStage,
|
|
941
|
+
stage: 'failed',
|
|
942
|
+
residualPaths,
|
|
943
|
+
updatedAt: new Date().toISOString(),
|
|
944
|
+
detail: 'Catalog record removed, but file deletion failed; file preserved.',
|
|
945
|
+
});
|
|
946
|
+
return {
|
|
947
|
+
ok: false,
|
|
948
|
+
status: 'partial',
|
|
949
|
+
documentId: record.documentId,
|
|
950
|
+
diagnostics: [
|
|
951
|
+
diag(diagnosticCodes.document_delete_stale, `Catalog record removed, but file deletion failed: ${error instanceof Error ? error.message : String(error)}`, resolved.value.absolutePath, 'Inspect the journal and permissions; the file was preserved.'),
|
|
952
|
+
],
|
|
953
|
+
residualPaths,
|
|
954
|
+
journalPath,
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
residualPaths = [];
|
|
958
|
+
const complete = await writeKnowledgeJournal(currentContext, {
|
|
959
|
+
...catalogStage,
|
|
960
|
+
stage: 'complete',
|
|
961
|
+
residualPaths: [],
|
|
962
|
+
updatedAt: new Date().toISOString(),
|
|
963
|
+
detail: 'Catalog record and document file removed by explicit plan.',
|
|
964
|
+
});
|
|
965
|
+
if (!complete.ok) {
|
|
966
|
+
return {
|
|
967
|
+
ok: false,
|
|
968
|
+
status: 'partial',
|
|
969
|
+
documentId: record.documentId,
|
|
970
|
+
diagnostics: [
|
|
971
|
+
...currentContext.diagnostics,
|
|
972
|
+
outcomeDiagnostic(complete, journalPath),
|
|
973
|
+
],
|
|
974
|
+
residualPaths: [journalPath],
|
|
975
|
+
journalPath,
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
return {
|
|
979
|
+
ok: true,
|
|
980
|
+
status: 'deleted',
|
|
981
|
+
documentId: record.documentId,
|
|
982
|
+
diagnostics: [...currentContext.diagnostics],
|
|
983
|
+
residualPaths: [],
|
|
984
|
+
journalPath,
|
|
985
|
+
};
|
|
986
|
+
})();
|
|
987
|
+
const release = await releaseKnowledgeLock(lock.lock, lock.path);
|
|
988
|
+
if (!release.ok) {
|
|
989
|
+
return {
|
|
990
|
+
...deleteResult,
|
|
991
|
+
ok: false,
|
|
992
|
+
status: deleteResult.ok ? 'partial' : deleteResult.status,
|
|
993
|
+
diagnostics: [
|
|
994
|
+
...deleteResult.diagnostics,
|
|
995
|
+
outcomeDiagnostic(release, lock.path),
|
|
996
|
+
],
|
|
997
|
+
residualPaths: [...deleteResult.residualPaths, lock.path],
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
return deleteResult;
|
|
1001
|
+
}
|