@phyzess/pappra 0.1.0-beta.13 → 0.1.0-beta.18
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/README.md +134 -15
- package/assets/integrations/skills/pappra/SKILL.md +28 -7
- package/assets/integrations/skills/pappra/references/collaboration.md +633 -0
- package/assets/integrations/skills/pappra/references/environment.md +360 -0
- package/assets/integrations/skills/pappra/references/hosts.md +20 -0
- package/assets/integrations/skills/pappra/references/operations.md +36 -5
- package/dist/adapters/pi/recovery-extension.d.ts +65 -0
- package/dist/adapters/pi/recovery-extension.js +238 -0
- package/dist/adapters/pi/session-transition-extension.d.ts +30 -0
- package/dist/adapters/pi/session-transition-extension.js +265 -0
- package/dist/app/automatic-handoff-events.d.ts +26 -0
- package/dist/app/automatic-handoff-events.js +134 -0
- package/dist/app/bounded-file.d.ts +2 -0
- package/dist/app/bounded-file.js +2 -0
- package/dist/app/capability-evidence/claude.d.ts +37 -0
- package/dist/app/capability-evidence/claude.js +206 -0
- package/dist/app/capability-evidence/codex.d.ts +35 -0
- package/dist/app/capability-evidence/codex.js +193 -0
- package/dist/app/capability-evidence/index.d.ts +33 -0
- package/dist/app/capability-evidence/index.js +117 -0
- package/dist/app/capability-evidence/pi.d.ts +73 -0
- package/dist/app/capability-evidence/pi.js +669 -0
- package/dist/app/capability-evidence/shared.d.ts +69 -0
- package/dist/app/capability-evidence/shared.js +195 -0
- package/dist/app/collab-auto-handoff.d.ts +55 -0
- package/dist/app/collab-auto-handoff.js +384 -0
- package/dist/app/collab-bindings.d.ts +57 -0
- package/dist/app/collab-bindings.js +1006 -0
- package/dist/app/collab-bootstrap-transport.d.ts +163 -0
- package/dist/app/collab-bootstrap-transport.js +769 -0
- package/dist/app/collab-bootstrap.d.ts +137 -0
- package/dist/app/collab-bootstrap.js +3455 -0
- package/dist/app/collab-handoff.d.ts +12 -0
- package/dist/app/collab-handoff.js +511 -0
- package/dist/app/collab-messages.d.ts +2 -0
- package/dist/app/collab-messages.js +2 -0
- package/dist/app/collab-notify.d.ts +156 -0
- package/dist/app/collab-notify.js +1208 -0
- package/dist/app/collab-preflight.d.ts +34 -0
- package/dist/app/collab-preflight.js +96 -0
- package/dist/app/collab-recovery-executor.d.ts +161 -0
- package/dist/app/collab-recovery-executor.js +1153 -0
- package/dist/app/collab-recovery-projection.d.ts +103 -0
- package/dist/app/collab-recovery-projection.js +659 -0
- package/dist/app/collab-recovery.d.ts +25 -0
- package/dist/app/collab-recovery.js +429 -0
- package/dist/app/collab-resources.d.ts +16 -0
- package/dist/app/collab-resources.js +278 -0
- package/dist/app/collab-role-events.d.ts +22 -0
- package/dist/app/collab-role-events.js +329 -0
- package/dist/app/collab-role-orchestration.d.ts +53 -0
- package/dist/app/collab-role-orchestration.js +601 -0
- package/dist/app/collab-role-plan.d.ts +81 -0
- package/dist/app/collab-role-plan.js +504 -0
- package/dist/app/collab-role-repair.d.ts +109 -0
- package/dist/app/collab-role-repair.js +821 -0
- package/dist/app/collab-rollover.d.ts +263 -0
- package/dist/app/collab-rollover.js +1112 -0
- package/dist/app/collab-route.d.ts +64 -0
- package/dist/app/collab-route.js +240 -0
- package/dist/app/collab-session-transition.d.ts +221 -0
- package/dist/app/collab-session-transition.js +3334 -0
- package/dist/app/collab-state.d.ts +7 -0
- package/dist/app/collab-state.js +162 -0
- package/dist/app/collab-task-store.d.ts +22 -0
- package/dist/app/collab-task-store.js +204 -0
- package/dist/app/collab-work-execution.d.ts +127 -0
- package/dist/app/collab-work-execution.js +1027 -0
- package/dist/app/collab-work-plan.d.ts +87 -0
- package/dist/app/collab-work-plan.js +437 -0
- package/dist/app/collaboration.d.ts +8 -2
- package/dist/app/collaboration.js +101 -12
- package/dist/app/doctor.js +108 -5
- package/dist/app/host-mcp.js +34 -8
- package/dist/app/host-probe.js +4 -4
- package/dist/app/init.d.ts +58 -4
- package/dist/app/init.js +768 -155
- package/dist/app/managed-pi-bootstrap-admission.d.ts +14 -0
- package/dist/app/managed-pi-bootstrap-admission.js +104 -0
- package/dist/app/managed-pi-bootstrap-refresh.d.ts +10 -0
- package/dist/app/managed-pi-bootstrap-refresh.js +78 -0
- package/dist/app/managed-pi-delivery.d.ts +8 -0
- package/dist/app/managed-pi-delivery.js +110 -0
- package/dist/app/managed-pi-handoff-receiver.d.ts +15 -0
- package/dist/app/managed-pi-handoff-receiver.js +263 -0
- package/dist/app/managed-pi-handoff-reference.d.ts +3 -0
- package/dist/app/managed-pi-handoff-reference.js +60 -0
- package/dist/app/managed-pi-herdr.d.ts +21 -0
- package/dist/app/managed-pi-herdr.js +185 -0
- package/dist/app/managed-pi-initial-admission.d.ts +28 -0
- package/dist/app/managed-pi-initial-admission.js +416 -0
- package/dist/app/managed-pi-initial-launch.d.ts +5 -0
- package/dist/app/managed-pi-initial-launch.js +61 -0
- package/dist/app/managed-pi-native-observation.d.ts +36 -0
- package/dist/app/managed-pi-native-observation.js +191 -0
- package/dist/app/managed-pi-readiness.d.ts +29 -0
- package/dist/app/managed-pi-readiness.js +288 -0
- package/dist/app/managed-pi-repair-stop.d.ts +21 -0
- package/dist/app/managed-pi-repair-stop.js +371 -0
- package/dist/app/managed-pi-resume-admission.d.ts +32 -0
- package/dist/app/managed-pi-resume-admission.js +618 -0
- package/dist/app/managed-pi-retirement.d.ts +28 -0
- package/dist/app/managed-pi-retirement.js +112 -0
- package/dist/app/managed-pi-sdk-control.d.ts +7 -0
- package/dist/app/managed-pi-sdk-control.js +169 -0
- package/dist/app/managed-pi-sdk-runtime.d.ts +3 -0
- package/dist/app/managed-pi-sdk-runtime.js +483 -0
- package/dist/app/managed-pi-source-admission.d.ts +14 -0
- package/dist/app/managed-pi-source-admission.js +323 -0
- package/dist/app/managed-pi-transition-host.d.ts +36 -0
- package/dist/app/managed-pi-transition-host.js +292 -0
- package/dist/app/managed-pi.d.ts +54 -0
- package/dist/app/managed-pi.js +248 -0
- package/dist/app/shell-readiness.d.ts +66 -0
- package/dist/app/shell-readiness.js +368 -0
- package/dist/app/task-pin.d.ts +2 -23
- package/dist/app/task-pin.js +2 -113
- package/dist/cli/commands/collaboration-binding.d.ts +13 -0
- package/dist/cli/commands/collaboration-binding.js +118 -0
- package/dist/cli/commands/collaboration-bootstrap.d.ts +10 -0
- package/dist/cli/commands/collaboration-bootstrap.js +84 -0
- package/dist/cli/commands/collaboration-capabilities.d.ts +13 -0
- package/dist/cli/commands/collaboration-capabilities.js +127 -0
- package/dist/cli/commands/collaboration-handoff.d.ts +13 -0
- package/dist/cli/commands/collaboration-handoff.js +102 -0
- package/dist/cli/commands/collaboration-message.d.ts +17 -0
- package/dist/cli/commands/collaboration-message.js +177 -0
- package/dist/cli/commands/collaboration-plan.d.ts +8 -0
- package/dist/cli/commands/collaboration-plan.js +108 -0
- package/dist/cli/commands/collaboration-preflight.d.ts +10 -0
- package/dist/cli/commands/collaboration-preflight.js +167 -0
- package/dist/cli/commands/collaboration-recovery-run.d.ts +46 -0
- package/dist/cli/commands/collaboration-recovery-run.js +224 -0
- package/dist/cli/commands/collaboration-recovery.d.ts +11 -0
- package/dist/cli/commands/collaboration-recovery.js +104 -0
- package/dist/cli/commands/collaboration-resources.d.ts +11 -0
- package/dist/cli/commands/collaboration-resources.js +96 -0
- package/dist/cli/commands/collaboration-role-plan.d.ts +9 -0
- package/dist/cli/commands/collaboration-role-plan.js +69 -0
- package/dist/cli/commands/collaboration-role-run.d.ts +11 -0
- package/dist/cli/commands/collaboration-role-run.js +128 -0
- package/dist/cli/commands/collaboration-rollover.d.ts +10 -0
- package/dist/cli/commands/collaboration-rollover.js +103 -0
- package/dist/cli/commands/collaboration-state.d.ts +10 -0
- package/dist/cli/commands/collaboration-state.js +96 -0
- package/dist/cli/commands/collaboration-transition.d.ts +10 -0
- package/dist/cli/commands/collaboration-transition.js +75 -0
- package/dist/cli/commands/collaboration-work-run.d.ts +34 -0
- package/dist/cli/commands/collaboration-work-run.js +178 -0
- package/dist/cli/commands/collaboration.d.ts +4 -0
- package/dist/cli/commands/collaboration.js +39 -4
- package/dist/cli/commands/continuity.js +2 -6
- package/dist/cli/commands/docs.d.ts +2 -0
- package/dist/cli/commands/docs.js +89 -15
- package/dist/cli/commands/host.d.ts +7 -1
- package/dist/cli/commands/host.js +122 -7
- package/dist/cli/commands/init.d.ts +20 -3
- package/dist/cli/commands/init.js +897 -232
- package/dist/cli/commands/mcp.d.ts +1 -0
- package/dist/cli/commands/mcp.js +11 -5
- package/dist/cli/commands/repair.d.ts +12 -0
- package/dist/cli/commands/repair.js +41 -0
- package/dist/cli/commands/runtime.d.ts +1 -1
- package/dist/cli/commands/runtime.js +139 -38
- package/dist/cli/commands/session.d.ts +7 -0
- package/dist/cli/commands/session.js +82 -0
- package/dist/cli/commands/task.js +19 -5
- package/dist/cli/run.js +866 -24
- package/dist/code/identity.d.ts +7 -0
- package/dist/code/identity.js +7 -3
- package/dist/code/registration.js +7 -0
- package/dist/code/service.d.ts +9 -0
- package/dist/code/service.js +16 -3
- package/dist/code/text.js +19 -7
- package/dist/collab/admission.d.ts +91 -66
- package/dist/collab/admission.js +21 -6
- package/dist/collab/canonical.d.ts +1 -1
- package/dist/collab/canonical.js +44 -3
- package/dist/collab/envelope.d.ts +2 -2
- package/dist/collab/envelope.js +12 -12
- package/dist/collab/first-work.d.ts +3 -0
- package/dist/collab/first-work.js +15 -0
- package/dist/collab/handoff.d.ts +16 -0
- package/dist/collab/handoff.js +39 -5
- package/dist/collab/index.d.ts +97 -1
- package/dist/collab/index.js +9 -1
- package/dist/collab/messages.js +126 -3
- package/dist/collab/persisted-messages.d.ts +123 -0
- package/dist/collab/persisted-messages.js +496 -0
- package/dist/collab/preflight.d.ts +1 -1
- package/dist/collab/resources.d.ts +13 -3
- package/dist/collab/resources.js +37 -6
- package/dist/collab/rollover.d.ts +311 -0
- package/dist/collab/rollover.js +1024 -0
- package/dist/collab/store.d.ts +1 -0
- package/dist/collab/store.js +1 -4
- package/dist/collab/types.d.ts +2 -354
- package/dist/collab/validation.d.ts +2 -10
- package/dist/collab/validation.js +2 -303
- package/dist/collab/work-plan.d.ts +105 -0
- package/dist/collab/work-plan.js +843 -0
- package/dist/continuity/context.d.ts +11 -2
- package/dist/continuity/context.js +100 -13
- package/dist/continuity/handoff-bundle.d.ts +296 -0
- package/dist/continuity/handoff-bundle.js +477 -0
- package/dist/continuity/index.d.ts +2 -0
- package/dist/continuity/index.js +1 -0
- package/dist/continuity/paths.js +10 -2
- package/dist/continuity/service.js +91 -15
- package/dist/continuity/task-pin.d.ts +24 -0
- package/dist/continuity/task-pin.js +590 -0
- package/dist/doctor.d.ts +1 -0
- package/dist/doctor.js +6 -0
- package/dist/experimental/index.d.ts +13 -0
- package/dist/experimental/index.js +25 -0
- package/dist/experimental/pi.d.ts +4 -0
- package/dist/experimental/pi.js +12 -0
- package/dist/hosts/claude/native.js +1 -1
- package/dist/hosts/codex/native.js +1 -1
- package/dist/hosts/codex/protocol.js +1 -1
- package/dist/hosts/integration/bootstrap-config.d.ts +58 -0
- package/dist/hosts/integration/bootstrap-config.js +254 -0
- package/dist/hosts/integration/bundle-export.d.ts +44 -0
- package/dist/hosts/integration/bundle-export.js +366 -3
- package/dist/hosts/integration/bundle.js +3 -0
- package/dist/hosts/integration/hook.js +36 -9
- package/dist/hosts/integration/pappra-bootstrap-extension.mjs +80 -0
- package/dist/hosts/integration/pappra-extension-runtime.mjs +59 -24631
- package/dist/hosts/integration/pappra-hook.mjs +61 -24749
- package/dist/hosts/integration/pappra-recovery-extension.mjs +259 -0
- package/dist/hosts/integration/pappra-session-transition-extension.mjs +276 -0
- package/dist/hosts/integration/pi-bootstrap-extension.d.ts +36 -0
- package/dist/hosts/integration/pi-bootstrap-extension.js +101 -0
- package/dist/hosts/integration/pi-bootstrap-guard.d.ts +125 -0
- package/dist/hosts/integration/pi-bootstrap-guard.js +438 -0
- package/dist/hosts/integration/pi-bootstrap-runtime.d.ts +63 -0
- package/dist/hosts/integration/pi-bootstrap-runtime.js +223 -0
- package/dist/hosts/integration/pi-native-events.d.ts +112 -0
- package/dist/hosts/integration/pi-native-events.js +348 -0
- package/dist/hosts/integration/pi-rollover-extension.d.ts +166 -0
- package/dist/hosts/integration/pi-rollover-extension.js +431 -0
- package/dist/hosts/integration/recovery-config.d.ts +48 -0
- package/dist/hosts/integration/recovery-config.js +117 -0
- package/dist/hosts/integration/service.d.ts +6 -1
- package/dist/hosts/integration/service.js +485 -35
- package/dist/hosts/integration/support.d.ts +7 -0
- package/dist/hosts/integration/support.js +9 -1
- package/dist/hosts/integration/templates.d.ts +3 -0
- package/dist/hosts/integration/templates.js +26 -3
- package/dist/hosts/pi/managed-host-identity.d.ts +2 -0
- package/dist/hosts/pi/managed-host-identity.js +40 -0
- package/dist/hosts/pi/managed-provider-diagnostics.d.ts +6 -0
- package/dist/hosts/pi/managed-provider-diagnostics.js +184 -0
- package/dist/hosts/pi/managed-sdk-session.d.ts +335 -0
- package/dist/hosts/pi/managed-sdk-session.js +1639 -0
- package/dist/hosts/pi/managed-session-extension.d.ts +20 -0
- package/dist/hosts/pi/managed-session-extension.js +84 -0
- package/dist/hosts/pi/managed-session.d.ts +177 -0
- package/dist/hosts/pi/managed-session.js +1557 -0
- package/dist/hosts/pi/protocol.js +1 -1
- package/dist/hosts/pi/query-result.js +1 -1
- package/dist/hosts/pi/resume-capability.d.ts +41 -0
- package/dist/hosts/pi/resume-capability.js +33 -0
- package/dist/hosts/pi/rpc.d.ts +182 -134
- package/dist/hosts/pi/rpc.js +230 -37
- package/dist/hosts/pilot.d.ts +97 -74
- package/dist/hosts/pilot.js +5 -1
- package/dist/hosts/version.d.ts +3 -0
- package/dist/hosts/version.js +18 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/infra/bounded-file.d.ts +19 -0
- package/dist/infra/bounded-file.js +66 -0
- package/dist/infra/fs.js +10 -2
- package/dist/infra/git.d.ts +29 -0
- package/dist/infra/git.js +45 -9
- package/dist/infra/json-property.d.ts +2 -0
- package/dist/infra/json-property.js +69 -0
- package/dist/infra/json.d.ts +4 -0
- package/dist/infra/json.js +24 -0
- package/dist/infra/owned-process-group.js +1 -1
- package/dist/infra/private-lock.d.ts +16 -0
- package/dist/infra/private-lock.js +148 -0
- package/dist/infra/private-state.d.ts +7 -0
- package/dist/infra/private-state.js +146 -0
- package/dist/infra/process-identity.d.ts +12 -0
- package/dist/infra/process-identity.js +142 -0
- package/dist/infra/validation.d.ts +5 -0
- package/dist/infra/validation.js +15 -0
- package/dist/init.d.ts +1 -0
- package/dist/init.js +6 -0
- package/dist/install/apply.d.ts +9 -1
- package/dist/install/apply.js +176 -12
- package/dist/install/dependencies.d.ts +38 -0
- package/dist/install/dependencies.js +282 -0
- package/dist/install/ownership.js +30 -1
- package/dist/install/references.d.ts +31 -0
- package/dist/install/references.js +208 -0
- package/dist/install/repair.d.ts +37 -0
- package/dist/install/repair.js +798 -0
- package/dist/install/reset-local.d.ts +16 -0
- package/dist/install/reset-local.js +304 -0
- package/dist/install/rollback.d.ts +3 -0
- package/dist/install/rollback.js +9 -3
- package/dist/install/runtime.d.ts +10 -6
- package/dist/install/runtime.js +299 -59
- package/dist/install/source-fingerprint.d.ts +2 -0
- package/dist/install/source-fingerprint.js +103 -0
- package/dist/knowledge/catalog.d.ts +1 -0
- package/dist/knowledge/catalog.js +143 -10
- package/dist/knowledge/journal.js +17 -7
- package/dist/knowledge/paths.js +2 -1
- package/dist/knowledge/refs.js +4 -0
- package/dist/knowledge/register-batch.d.ts +4 -0
- package/dist/knowledge/register-batch.js +225 -0
- package/dist/knowledge/relocate.d.ts +5 -0
- package/dist/knowledge/relocate.js +92 -0
- package/dist/knowledge/scan.js +103 -47
- package/dist/knowledge/service.d.ts +3 -0
- package/dist/knowledge/service.js +33 -1
- package/dist/knowledge/write.d.ts +4 -1
- package/dist/knowledge/write.js +1 -1
- package/dist/mcp/server.d.ts +4 -2
- package/dist/mcp/server.js +87 -136
- package/dist/schemas/collaboration-state-validation.d.ts +6 -0
- package/dist/schemas/collaboration-state-validation.js +342 -0
- package/dist/schemas/collaboration-state.d.ts +436 -0
- package/dist/schemas/collaboration-state.js +1 -0
- package/dist/schemas/collaboration.d.ts +335 -0
- package/dist/schemas/collaboration.js +272 -0
- package/dist/schemas/continuity.d.ts +8 -0
- package/dist/schemas/continuity.js +13 -3
- package/dist/schemas/doctor.d.ts +1 -0
- package/dist/schemas/doctor.js +1 -0
- package/dist/schemas/document.d.ts +61 -0
- package/dist/schemas/document.js +9 -0
- package/dist/schemas/errors.d.ts +1 -0
- package/dist/schemas/errors.js +1 -0
- package/dist/schemas/host.d.ts +24 -0
- package/dist/schemas/host.js +19 -0
- package/dist/schemas/install.d.ts +8 -2
- package/dist/schemas/install.js +11 -1
- package/dist/schemas/managed-pi-execution.d.ts +16 -0
- package/dist/schemas/managed-pi-execution.js +2 -0
- package/dist/schemas/outcome.d.ts +9 -5
- package/dist/workspace/config-file.js +1 -3
- package/dist/workspace/documents.js +12 -2
- package/dist/workspace/repositories.d.ts +7 -0
- package/dist/workspace/repositories.js +14 -5
- package/package.json +35 -35
- package/skills/pappra/SKILL.md +28 -7
- package/skills/pappra/references/collaboration.md +633 -0
- package/skills/pappra/references/environment.md +360 -0
- package/skills/pappra/references/hosts.md +20 -0
- package/skills/pappra/references/operations.md +36 -5
- package/dist/app/collab-from.d.ts +0 -35
- package/dist/app/collab-from.js +0 -84
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { mkdir, open, rename, rm } from 'node:fs/promises';
|
|
3
|
-
import {
|
|
2
|
+
import { mkdir, open, readdir, rename, rm, unlink } from 'node:fs/promises';
|
|
3
|
+
import { ensurePrivateState } from '../infra/private-state.js';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
4
5
|
import { createDiagnostic } from '../schemas/diagnostics.js';
|
|
5
6
|
import { diagnosticCodes } from '../schemas/errors.js';
|
|
6
|
-
import { DOCUMENT_CATALOG_SCHEMA, documentCatalogSchema, } from '../schemas/document.js';
|
|
7
|
+
import { DOCUMENT_CATALOG_SCHEMA, documentCatalogSchema, documentRecordFileSchema, } from '../schemas/document.js';
|
|
7
8
|
import { failed, succeeded } from '../schemas/outcome.js';
|
|
8
9
|
import { observePath, readFileBounded, readTextFileBounded, sha256Digest, } from '../infra/fs.js';
|
|
9
10
|
import { ensureKnowledgePathBoundary, knowledgePaths, } from './paths.js';
|
|
@@ -34,6 +35,8 @@ function isUnknownRecord(value) {
|
|
|
34
35
|
return typeof value === 'object' && value !== null;
|
|
35
36
|
}
|
|
36
37
|
async function readCatalogFile(context, scope, path) {
|
|
38
|
+
if (scope === 'shared')
|
|
39
|
+
return readSharedRecords(context, path);
|
|
37
40
|
let observation;
|
|
38
41
|
try {
|
|
39
42
|
observation = await observePath(path);
|
|
@@ -156,6 +159,85 @@ async function readCatalogFile(context, scope, path) {
|
|
|
156
159
|
valid: diagnostics.length === 0,
|
|
157
160
|
};
|
|
158
161
|
}
|
|
162
|
+
async function readSharedRecords(context, path) {
|
|
163
|
+
const diagnostics = [];
|
|
164
|
+
const invalid = (message) => ({
|
|
165
|
+
scope: 'shared',
|
|
166
|
+
path,
|
|
167
|
+
exists: true,
|
|
168
|
+
digest: null,
|
|
169
|
+
catalog: null,
|
|
170
|
+
diagnostics: [
|
|
171
|
+
catalogDiagnostic(diagnosticCodes.document_catalog_invalid, message, path, 'Inspect the per-document records; malformed state is preserved. Legacy catalog.json requires an explicit reset, not an automatic migration.'),
|
|
172
|
+
],
|
|
173
|
+
valid: false,
|
|
174
|
+
});
|
|
175
|
+
try {
|
|
176
|
+
const legacy = await observePath(join(dirname(path), 'catalog.json'));
|
|
177
|
+
if (legacy.kind !== 'missing')
|
|
178
|
+
return invalid('Legacy shared catalog.json is unsupported; preserve or export it and explicitly reset the shared catalog before using per-document records.');
|
|
179
|
+
const observation = await observePath(path);
|
|
180
|
+
if (observation.kind === 'missing')
|
|
181
|
+
return {
|
|
182
|
+
scope: 'shared',
|
|
183
|
+
path,
|
|
184
|
+
exists: false,
|
|
185
|
+
digest: null,
|
|
186
|
+
catalog: null,
|
|
187
|
+
diagnostics: [],
|
|
188
|
+
valid: true,
|
|
189
|
+
};
|
|
190
|
+
if (observation.kind !== 'directory')
|
|
191
|
+
return invalid('Shared document records must be a real directory.');
|
|
192
|
+
const names = (await readdir(path))
|
|
193
|
+
.filter((name) => name !== '.DS_Store' && name !== 'Thumbs.db')
|
|
194
|
+
.sort();
|
|
195
|
+
if (names.length > 2048)
|
|
196
|
+
return invalid('Shared document record limit exceeded (2048).');
|
|
197
|
+
const records = [];
|
|
198
|
+
const digests = [];
|
|
199
|
+
const recordDigests = new Map();
|
|
200
|
+
let totalBytes = 0;
|
|
201
|
+
for (const name of names) {
|
|
202
|
+
if (/^[0-9a-f-]{36}\.json\.tmp-[0-9a-f-]{36}$/i.test(name))
|
|
203
|
+
continue;
|
|
204
|
+
if (!/^[0-9a-f-]{36}\.json$/i.test(name))
|
|
205
|
+
return invalid(`Unexpected file in shared records directory: ${name}`);
|
|
206
|
+
const file = join(path, name);
|
|
207
|
+
const read = await readTextFileBounded(file, DEFAULT_CATALOG_MAX_BYTES);
|
|
208
|
+
if (!read.ok)
|
|
209
|
+
return invalid(`Cannot safely read shared document record: ${name}`);
|
|
210
|
+
totalBytes += Buffer.byteLength(read.value.text);
|
|
211
|
+
if (totalBytes > DEFAULT_CATALOG_MAX_BYTES)
|
|
212
|
+
return invalid('Shared document records exceed the catalog byte budget.');
|
|
213
|
+
const parsed = documentRecordFileSchema.safeParse(JSON.parse(read.value.text));
|
|
214
|
+
if (!parsed.success ||
|
|
215
|
+
`${parsed.data.record.documentId}.json` !== name ||
|
|
216
|
+
parsed.data.workspaceId !== context.config.workspaceId ||
|
|
217
|
+
parsed.data.record.visibility !== 'shareable')
|
|
218
|
+
return invalid(`Invalid document identity, scope or schema in shared record: ${name}`);
|
|
219
|
+
records.push(parsed.data.record);
|
|
220
|
+
digests.push(`${name}:${read.value.digest}`);
|
|
221
|
+
recordDigests.set(parsed.data.record.documentId, read.value.digest);
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
scope: 'shared',
|
|
225
|
+
path,
|
|
226
|
+
exists: true,
|
|
227
|
+
digest: sha256Digest(digests.join('\n')),
|
|
228
|
+
catalog: {
|
|
229
|
+
...emptyCatalog(context, '1970-01-01T00:00:00.000Z'),
|
|
230
|
+
records,
|
|
231
|
+
},
|
|
232
|
+
diagnostics,
|
|
233
|
+
valid: true,
|
|
234
|
+
recordDigests,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
return invalid(`Cannot read shared document records: ${error instanceof Error ? error.message : String(error)}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
159
241
|
function combineDiagnostics(shared, privateState) {
|
|
160
242
|
return [...shared.diagnostics, ...privateState.diagnostics];
|
|
161
243
|
}
|
|
@@ -180,6 +262,8 @@ export async function loadKnowledgeCatalogs(context) {
|
|
|
180
262
|
else {
|
|
181
263
|
records.set(record.documentId, record);
|
|
182
264
|
}
|
|
265
|
+
if (record.lifecycle === 'archived')
|
|
266
|
+
continue;
|
|
183
267
|
const key = locationKey(record.location);
|
|
184
268
|
const owner = locations.get(key);
|
|
185
269
|
if (owner !== undefined && owner !== record.documentId) {
|
|
@@ -248,6 +332,58 @@ export async function writeCatalogFile(context, scope, catalog, expectedDigest)
|
|
|
248
332
|
const path = scope === 'shared'
|
|
249
333
|
? knowledgePaths(context.root).sharedCatalog
|
|
250
334
|
: knowledgePaths(context.root).privateCatalog;
|
|
335
|
+
const parsed = documentCatalogSchema.safeParse(catalog);
|
|
336
|
+
if (!parsed.success || catalog.workspaceId !== context.config.workspaceId)
|
|
337
|
+
return failed(diagnosticCodes.document_record_invalid, 'Refusing to write an invalid catalog or foreign workspace identity.');
|
|
338
|
+
if (scope === 'shared') {
|
|
339
|
+
try {
|
|
340
|
+
return await writeSharedRecords(context, path, parsed.data, expectedDigest);
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
return failed(diagnosticCodes.fileUnreadable, `Cannot commit shared document records: ${error instanceof Error ? error.message : String(error)}`, {
|
|
344
|
+
nextAction: 'Inspect permissions and any individually committed records; rerun scan to reconcile without replacing document content.',
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return writeCatalogBytes(context, path, Buffer.from(`${JSON.stringify(parsed.data, null, 2)}\n`), expectedDigest, scope);
|
|
349
|
+
}
|
|
350
|
+
async function writeSharedRecords(context, path, catalog, expectedDigest) {
|
|
351
|
+
const boundary = await ensureKnowledgePathBoundary(context.root, path);
|
|
352
|
+
if (!boundary.ok)
|
|
353
|
+
return boundary;
|
|
354
|
+
const before = await readSharedRecords(context, path);
|
|
355
|
+
if (!before.valid || before.digest !== expectedDigest)
|
|
356
|
+
return failed(diagnosticCodes.document_catalog_conflict, 'Shared document records changed before commit; reload before retrying.');
|
|
357
|
+
await mkdir(path, { recursive: true });
|
|
358
|
+
const previous = new Map(before.catalog?.records.map((record) => [record.documentId, record]) ?? []);
|
|
359
|
+
for (const record of catalog.records) {
|
|
360
|
+
const old = previous.get(record.documentId);
|
|
361
|
+
previous.delete(record.documentId);
|
|
362
|
+
if (old && JSON.stringify(old) === JSON.stringify(record))
|
|
363
|
+
continue;
|
|
364
|
+
const recordPath = join(path, `${record.documentId}.json`);
|
|
365
|
+
const bytes = Buffer.from(`${JSON.stringify({ schema: 'pappra/document-record-file/1', workspaceId: context.config.workspaceId, record }, null, 2)}\n`);
|
|
366
|
+
const written = await writeCatalogBytes(context, recordPath, bytes, before.recordDigests?.get(record.documentId) ?? null, 'shared');
|
|
367
|
+
if (!written.ok)
|
|
368
|
+
return written;
|
|
369
|
+
}
|
|
370
|
+
for (const record of previous.values()) {
|
|
371
|
+
const file = join(path, `${record.documentId}.json`);
|
|
372
|
+
const boundary = await ensureKnowledgePathBoundary(context.root, file);
|
|
373
|
+
if (!boundary.ok)
|
|
374
|
+
return boundary;
|
|
375
|
+
const current = await readTextFileBounded(file, DEFAULT_CATALOG_MAX_BYTES);
|
|
376
|
+
if (!current.ok ||
|
|
377
|
+
JSON.stringify(JSON.parse(current.value.text).record) !== JSON.stringify(record))
|
|
378
|
+
return failed(diagnosticCodes.document_catalog_conflict, 'Shared document record changed before removal.');
|
|
379
|
+
await unlink(file);
|
|
380
|
+
}
|
|
381
|
+
const after = await readSharedRecords(context, path);
|
|
382
|
+
return after.valid && after.digest !== null
|
|
383
|
+
? succeeded({ path, digest: after.digest })
|
|
384
|
+
: failed(diagnosticCodes.document_catalog_conflict, 'Shared catalog commit is incomplete; inspect the per-document files and retry.');
|
|
385
|
+
}
|
|
386
|
+
async function writeCatalogBytes(context, path, bytes, expectedDigest, scope) {
|
|
251
387
|
const boundary = await ensureKnowledgePathBoundary(context.root, path);
|
|
252
388
|
if (!boundary.ok)
|
|
253
389
|
return boundary;
|
|
@@ -255,14 +391,11 @@ export async function writeCatalogFile(context, scope, catalog, expectedDigest)
|
|
|
255
391
|
if (current.kind !== 'missing' && current.kind !== 'file') {
|
|
256
392
|
return failed(diagnosticCodes.document_path_unsafe, `Catalog path is not a regular file: ${path}`, { nextAction: 'Inspect the catalog path; it was not replaced.' });
|
|
257
393
|
}
|
|
258
|
-
if (
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
if (!parsed.success) {
|
|
263
|
-
return failed(diagnosticCodes.document_record_invalid, `Refusing to write an invalid catalog: ${parsed.error.issues[0]?.message ?? 'unknown issue'}`, { nextAction: 'Fix the record; no catalog bytes were written.' });
|
|
394
|
+
if (scope === 'private') {
|
|
395
|
+
const prepared = await ensurePrivateState(context.root);
|
|
396
|
+
if (!prepared.ok)
|
|
397
|
+
return prepared;
|
|
264
398
|
}
|
|
265
|
-
const bytes = Buffer.from(`${JSON.stringify(parsed.data, null, 2)}\n`);
|
|
266
399
|
const temporary = `${path}.tmp-${randomUUID()}`;
|
|
267
400
|
let handle;
|
|
268
401
|
try {
|
|
@@ -5,6 +5,9 @@ import { diagnosticCodes } from '../schemas/errors.js';
|
|
|
5
5
|
import { errorMessage, failed, succeeded } from '../schemas/outcome.js';
|
|
6
6
|
import { knowledgeJournalSchema, knowledgeLockSchema, } from '../schemas/document.js';
|
|
7
7
|
import { readTextFileBounded } from '../infra/fs.js';
|
|
8
|
+
import { ensurePrivateState } from '../infra/private-state.js';
|
|
9
|
+
import { publishPrivateLock } from '../infra/private-lock.js';
|
|
10
|
+
import { captureProcessIdentity } from '../infra/process-identity.js';
|
|
8
11
|
import { ensureKnowledgePathBoundary, knowledgePaths } from './paths.js';
|
|
9
12
|
function lockFailure(path, status, message) {
|
|
10
13
|
return { ok: false, status, lock: null, path, message };
|
|
@@ -15,6 +18,9 @@ export async function acquireKnowledgeLock(context, operation) {
|
|
|
15
18
|
if (!boundary.ok) {
|
|
16
19
|
return lockFailure(path, 'failed', boundary.message);
|
|
17
20
|
}
|
|
21
|
+
const prepared = await ensurePrivateState(context.root);
|
|
22
|
+
if (!prepared.ok)
|
|
23
|
+
return lockFailure(path, 'failed', prepared.message);
|
|
18
24
|
try {
|
|
19
25
|
await mkdir(dirname(path), { recursive: true });
|
|
20
26
|
}
|
|
@@ -26,19 +32,20 @@ export async function acquireKnowledgeLock(context, operation) {
|
|
|
26
32
|
token: randomUUID(),
|
|
27
33
|
operation,
|
|
28
34
|
pid: process.pid,
|
|
35
|
+
...(captureProcessIdentity() === null
|
|
36
|
+
? {}
|
|
37
|
+
: { process_identity: captureProcessIdentity() }),
|
|
29
38
|
createdAt: new Date().toISOString(),
|
|
30
39
|
};
|
|
31
|
-
let handle;
|
|
32
40
|
try {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
41
|
+
await publishPrivateLock(path, Buffer.from(`${JSON.stringify(lock, null, 2)}\n`), async () => {
|
|
42
|
+
const current = await ensureKnowledgePathBoundary(context.root, path);
|
|
43
|
+
if (!current.ok)
|
|
44
|
+
throw new Error(current.message);
|
|
45
|
+
});
|
|
38
46
|
return { ok: true, status: 'acquired', lock, path, message: null };
|
|
39
47
|
}
|
|
40
48
|
catch (error) {
|
|
41
|
-
await handle?.close().catch(() => undefined);
|
|
42
49
|
const code = typeof error === 'object' &&
|
|
43
50
|
error !== null &&
|
|
44
51
|
'code' in error &&
|
|
@@ -88,6 +95,9 @@ export async function writeKnowledgeJournal(context, journal) {
|
|
|
88
95
|
const boundary = await ensureKnowledgePathBoundary(context.root, path);
|
|
89
96
|
if (!boundary.ok)
|
|
90
97
|
return boundary;
|
|
98
|
+
const prepared = await ensurePrivateState(context.root);
|
|
99
|
+
if (!prepared.ok)
|
|
100
|
+
return prepared;
|
|
91
101
|
const temporary = `${path}.tmp-${randomUUID()}`;
|
|
92
102
|
const bytes = Buffer.from(`${JSON.stringify(parsed.data, null, 2)}\n`);
|
|
93
103
|
let handle;
|
package/dist/knowledge/paths.js
CHANGED
|
@@ -6,7 +6,7 @@ import { observePath, readFileBounded, resolveRealpath } from '../infra/fs.js';
|
|
|
6
6
|
export function knowledgePaths(root) {
|
|
7
7
|
const localDir = join(root, '.pappra', 'local', 'knowledge');
|
|
8
8
|
return {
|
|
9
|
-
sharedCatalog: join(root, '.pappra', 'knowledge', '
|
|
9
|
+
sharedCatalog: join(root, '.pappra', 'knowledge', 'records'),
|
|
10
10
|
privateCatalog: join(localDir, 'catalog.json'),
|
|
11
11
|
localDir,
|
|
12
12
|
lockPath: join(localDir, 'apply.lock'),
|
|
@@ -17,6 +17,7 @@ const managedStatePrefixes = [
|
|
|
17
17
|
'.pappra/local',
|
|
18
18
|
'.pappra/generated',
|
|
19
19
|
'.pappra/knowledge/catalog.json',
|
|
20
|
+
'.pappra/knowledge/records',
|
|
20
21
|
];
|
|
21
22
|
/**
|
|
22
23
|
* Rejects a path if its final component or any existing ancestor resolves
|
package/dist/knowledge/refs.js
CHANGED
|
@@ -10,6 +10,8 @@ export function findRecord(catalogs, documentId) {
|
|
|
10
10
|
}
|
|
11
11
|
export async function findCanonicalAlias(context, catalogs, target, ignoreDocumentId = null) {
|
|
12
12
|
for (const record of catalogs.records.values()) {
|
|
13
|
+
if (record.lifecycle === 'archived')
|
|
14
|
+
continue;
|
|
13
15
|
if (record.documentId === ignoreDocumentId)
|
|
14
16
|
continue;
|
|
15
17
|
const resolved = await resolveKnowledgeLocation(context, record.location);
|
|
@@ -24,6 +26,8 @@ export function findRecordByLocation(catalogs, key) {
|
|
|
24
26
|
if (catalogs.locationConflicts.has(key))
|
|
25
27
|
return null;
|
|
26
28
|
for (const record of catalogs.records.values()) {
|
|
29
|
+
if (record.lifecycle === 'archived')
|
|
30
|
+
continue;
|
|
27
31
|
const recordKey = record.location.kind === 'workspace'
|
|
28
32
|
? `workspace:${record.location.path}`
|
|
29
33
|
: `source:${record.location.sourceId}:${record.location.path}`;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type KnowledgeContext } from './context.js';
|
|
2
|
+
import type { RegisterDocumentInput, RegisterDocumentResult } from './types.js';
|
|
3
|
+
/** One context, catalog snapshot and lock for a bounded discovery batch. */
|
|
4
|
+
export declare function registerDocumentBatch(context: KnowledgeContext, inputs: readonly RegisterDocumentInput[]): Promise<readonly RegisterDocumentResult[]>;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { createDiagnostic } from '../schemas/diagnostics.js';
|
|
3
|
+
import { diagnosticCodes } from '../schemas/errors.js';
|
|
4
|
+
import { samePath } from '../schemas/paths.js';
|
|
5
|
+
import { observePath } from '../infra/fs.js';
|
|
6
|
+
import { loadKnowledgeContext, sameRootIdentity, } from './context.js';
|
|
7
|
+
import { catalogDigestFor, catalogFor, catalogScopeFor, loadKnowledgeCatalogs, locationKey, writeCatalogFile, } from './catalog.js';
|
|
8
|
+
import { acquireKnowledgeLock, releaseKnowledgeLock, writeKnowledgeJournal, } from './journal.js';
|
|
9
|
+
import { readKnowledgeFile, resolveKnowledgeLocation } from './paths.js';
|
|
10
|
+
import { validateSources } from './refs.js';
|
|
11
|
+
import { validateAndBuildRecord } from './write.js';
|
|
12
|
+
/** One context, catalog snapshot and lock for a bounded discovery batch. */
|
|
13
|
+
export async function registerDocumentBatch(context, inputs) {
|
|
14
|
+
if (inputs.length === 0)
|
|
15
|
+
return [];
|
|
16
|
+
const fail = (message, diagnostics = []) => inputs.map(() => ({
|
|
17
|
+
ok: false,
|
|
18
|
+
status: 'failed',
|
|
19
|
+
record: null,
|
|
20
|
+
diagnostics: [
|
|
21
|
+
...diagnostics,
|
|
22
|
+
createDiagnostic({
|
|
23
|
+
code: diagnosticCodes.document_write_conflict,
|
|
24
|
+
severity: 'error',
|
|
25
|
+
message,
|
|
26
|
+
source: context.configPath,
|
|
27
|
+
}),
|
|
28
|
+
],
|
|
29
|
+
}));
|
|
30
|
+
const held = await acquireKnowledgeLock(context, 'write-document');
|
|
31
|
+
if (!held.ok || !held.lock)
|
|
32
|
+
return fail(held.message ?? 'Knowledge lock is unavailable.');
|
|
33
|
+
try {
|
|
34
|
+
const freshContext = await loadKnowledgeContext(context.root);
|
|
35
|
+
if (!freshContext.ok ||
|
|
36
|
+
!freshContext.context ||
|
|
37
|
+
!sameRootIdentity(freshContext.context, context.rootIdentity))
|
|
38
|
+
return fail('Workspace identity changed before batch registration.', freshContext.diagnostics);
|
|
39
|
+
context = freshContext.context;
|
|
40
|
+
const catalogs = await loadKnowledgeCatalogs(context);
|
|
41
|
+
if (!catalogs.ok)
|
|
42
|
+
return fail('Catalog conflicts prevent registration.', catalogs.diagnostics);
|
|
43
|
+
const records = new Map(catalogs.records);
|
|
44
|
+
const resolvedRecords = [];
|
|
45
|
+
for (const record of records.values()) {
|
|
46
|
+
if (record.lifecycle === 'archived')
|
|
47
|
+
continue;
|
|
48
|
+
const resolved = await resolveKnowledgeLocation(context, record.location);
|
|
49
|
+
const observation = resolved.ok
|
|
50
|
+
? await observePath(resolved.value.absolutePath).catch(() => null)
|
|
51
|
+
: null;
|
|
52
|
+
resolvedRecords.push({
|
|
53
|
+
record,
|
|
54
|
+
canonical: resolved.ok && observation?.kind === 'file'
|
|
55
|
+
? resolved.value.canonicalPath
|
|
56
|
+
: null,
|
|
57
|
+
missing: (!resolved.ok &&
|
|
58
|
+
resolved.code === diagnosticCodes.document_not_found) ||
|
|
59
|
+
observation?.kind === 'missing',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const results = [];
|
|
63
|
+
for (const input of inputs) {
|
|
64
|
+
const diagnostics = [];
|
|
65
|
+
const conflict = (message) => ({
|
|
66
|
+
ok: false,
|
|
67
|
+
status: 'conflict',
|
|
68
|
+
record: null,
|
|
69
|
+
diagnostics: [
|
|
70
|
+
createDiagnostic({
|
|
71
|
+
code: diagnosticCodes.document_location_conflict,
|
|
72
|
+
severity: 'error',
|
|
73
|
+
message,
|
|
74
|
+
source: input.location.path,
|
|
75
|
+
}),
|
|
76
|
+
],
|
|
77
|
+
});
|
|
78
|
+
const resolved = await resolveKnowledgeLocation(context, input.location);
|
|
79
|
+
if (!resolved.ok) {
|
|
80
|
+
results.push(conflict(resolved.message));
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const file = await readKnowledgeFile(resolved.value.absolutePath, 8 * 1024 * 1024);
|
|
84
|
+
if (!file.ok) {
|
|
85
|
+
results.push(conflict(file.message));
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const existing = [...records.values()].find((record) => record.lifecycle !== 'archived' &&
|
|
89
|
+
locationKey(record.location) === locationKey(input.location));
|
|
90
|
+
if (existing) {
|
|
91
|
+
results.push(existing.digest === file.value.digest &&
|
|
92
|
+
(input.documentId === undefined ||
|
|
93
|
+
input.documentId === existing.documentId)
|
|
94
|
+
? { ok: true, status: 'existing', record: existing, diagnostics }
|
|
95
|
+
: conflict('An active registration exists with different content; review and update it explicitly.'));
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const aliases = resolvedRecords.filter((entry) => entry.canonical !== null &&
|
|
99
|
+
samePath(entry.canonical, resolved.value.canonicalPath));
|
|
100
|
+
const moved = aliases.length === 1 ? aliases[0] : undefined;
|
|
101
|
+
const byDigest = resolvedRecords.filter((entry) => entry.missing && entry.record.digest === file.value.digest);
|
|
102
|
+
const previous = moved ?? (byDigest.length === 1 ? byDigest[0] : undefined);
|
|
103
|
+
if (aliases.length > 1 || byDigest.length > 1) {
|
|
104
|
+
results.push(conflict('Several registrations could identify this path; use docs relocate with an explicit document id.'));
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (previous) {
|
|
108
|
+
if (previous.record.digest !== file.value.digest ||
|
|
109
|
+
(input.documentId !== undefined &&
|
|
110
|
+
input.documentId !== previous.record.documentId)) {
|
|
111
|
+
results.push(conflict('The canonical document changed; review it before relocation.'));
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const record = {
|
|
115
|
+
...previous.record,
|
|
116
|
+
location: resolved.value.location,
|
|
117
|
+
};
|
|
118
|
+
records.set(record.documentId, record);
|
|
119
|
+
previous.record = record;
|
|
120
|
+
previous.canonical = resolved.value.canonicalPath;
|
|
121
|
+
previous.missing = false;
|
|
122
|
+
results.push({ ok: true, status: 'existing', record, diagnostics });
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const built = await validateAndBuildRecord(context, input, resolved.value);
|
|
126
|
+
if (!built.ok) {
|
|
127
|
+
results.push(conflict(built.message));
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const record = { ...built.value, digest: file.value.digest };
|
|
131
|
+
if (records.has(record.documentId)) {
|
|
132
|
+
results.push(conflict('Document id is already registered; batch discovery never replaces another document identity.'));
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
diagnostics.push(...validateSources(context, catalogs, record.documentId, record.visibility, record.sources));
|
|
136
|
+
if (diagnostics.some((entry) => entry.severity === 'error')) {
|
|
137
|
+
results.push({
|
|
138
|
+
ok: false,
|
|
139
|
+
status: 'failed',
|
|
140
|
+
record: null,
|
|
141
|
+
diagnostics,
|
|
142
|
+
});
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
records.set(record.documentId, record);
|
|
146
|
+
resolvedRecords.push({
|
|
147
|
+
record,
|
|
148
|
+
canonical: resolved.value.canonicalPath,
|
|
149
|
+
missing: false,
|
|
150
|
+
});
|
|
151
|
+
results.push({ ok: true, status: 'registered', record, diagnostics });
|
|
152
|
+
}
|
|
153
|
+
const changedScopes = ['shared', 'private'].filter((scope) => {
|
|
154
|
+
const previous = catalogFor(catalogs, scope, context);
|
|
155
|
+
const next = [...records.values()]
|
|
156
|
+
.filter((record) => catalogScopeFor(record) === scope)
|
|
157
|
+
.sort((a, b) => a.documentId < b.documentId
|
|
158
|
+
? -1
|
|
159
|
+
: a.documentId > b.documentId
|
|
160
|
+
? 1
|
|
161
|
+
: 0);
|
|
162
|
+
return JSON.stringify(previous.records) !== JSON.stringify(next);
|
|
163
|
+
});
|
|
164
|
+
if (changedScopes.length === 0)
|
|
165
|
+
return results;
|
|
166
|
+
const timestamp = new Date().toISOString();
|
|
167
|
+
const journal = {
|
|
168
|
+
schema: 'pappra/knowledge-journal/1',
|
|
169
|
+
operationId: randomUUID(),
|
|
170
|
+
operation: 'write-document',
|
|
171
|
+
stage: 'prepared',
|
|
172
|
+
documentId: null,
|
|
173
|
+
targetPath: null,
|
|
174
|
+
catalogScope: null,
|
|
175
|
+
expectedDigest: null,
|
|
176
|
+
fileDigest: null,
|
|
177
|
+
catalogDigest: catalogs.catalogDigest,
|
|
178
|
+
createdAt: timestamp,
|
|
179
|
+
updatedAt: timestamp,
|
|
180
|
+
residualPaths: [],
|
|
181
|
+
detail: `Batch registration of ${inputs.length} discovered documents. Document content is preserved; rerun scan after interruption to reconcile committed metadata.`,
|
|
182
|
+
};
|
|
183
|
+
const prepared = await writeKnowledgeJournal(context, journal);
|
|
184
|
+
if (!prepared.ok)
|
|
185
|
+
return fail(`Cannot prepare batch registration journal: ${prepared.message}`);
|
|
186
|
+
for (const scope of changedScopes) {
|
|
187
|
+
const previous = catalogFor(catalogs, scope, context);
|
|
188
|
+
const next = [...records.values()]
|
|
189
|
+
.filter((record) => catalogScopeFor(record) === scope)
|
|
190
|
+
.sort((a, b) => a.documentId < b.documentId
|
|
191
|
+
? -1
|
|
192
|
+
: a.documentId > b.documentId
|
|
193
|
+
? 1
|
|
194
|
+
: 0);
|
|
195
|
+
if (JSON.stringify(previous.records) === JSON.stringify(next))
|
|
196
|
+
continue;
|
|
197
|
+
const written = await writeCatalogFile(context, scope, { ...previous, records: next }, catalogDigestFor(catalogs, scope));
|
|
198
|
+
if (!written.ok) {
|
|
199
|
+
await writeKnowledgeJournal(context, {
|
|
200
|
+
...journal,
|
|
201
|
+
stage: 'failed',
|
|
202
|
+
updatedAt: new Date().toISOString(),
|
|
203
|
+
residualPaths: [
|
|
204
|
+
catalogs.paths.sharedCatalog,
|
|
205
|
+
catalogs.paths.privateCatalog,
|
|
206
|
+
],
|
|
207
|
+
});
|
|
208
|
+
return fail(`Batch registration was interrupted: ${written.message}. Rerun scan to reconcile already committed records; document files were not changed.`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const completed = await writeKnowledgeJournal(context, {
|
|
212
|
+
...journal,
|
|
213
|
+
stage: 'complete',
|
|
214
|
+
updatedAt: new Date().toISOString(),
|
|
215
|
+
});
|
|
216
|
+
if (!completed.ok)
|
|
217
|
+
return fail(`Registrations committed but their completion journal could not be saved: ${completed.message}. Rerun scan before treating coverage as complete.`);
|
|
218
|
+
return results;
|
|
219
|
+
}
|
|
220
|
+
finally {
|
|
221
|
+
const released = await releaseKnowledgeLock(held.lock, held.path);
|
|
222
|
+
if (!released.ok)
|
|
223
|
+
return fail('Registration may have committed, but its lock could not be released; inspect the catalog before retrying.');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { DocumentLocation } from '../schemas/document.js';
|
|
2
|
+
import type { KnowledgeContext } from './context.js';
|
|
3
|
+
import { type CatalogScope } from './catalog.js';
|
|
4
|
+
import type { RegisterDocumentResult } from './types.js';
|
|
5
|
+
export declare function relocateOrForgetDocument(context: KnowledgeContext, documentId: string, location: DocumentLocation | null, scope?: CatalogScope): Promise<RegisterDocumentResult>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { createDiagnostic } from '../schemas/diagnostics.js';
|
|
2
|
+
import { diagnosticCodes } from '../schemas/errors.js';
|
|
3
|
+
import { loadKnowledgeContext, sameRootIdentity } from './context.js';
|
|
4
|
+
import { catalogDigestFor, catalogFor, loadKnowledgeCatalogs, recordScope, recordsForCatalog, setCatalogRecord, writeCatalogFile, } from './catalog.js';
|
|
5
|
+
import { acquireKnowledgeLock, releaseKnowledgeLock } from './journal.js';
|
|
6
|
+
import { findCanonicalAlias, findRecord, findRegisteredReferences, } from './refs.js';
|
|
7
|
+
import { readKnowledgeFile, resolveKnowledgeLocation } from './paths.js';
|
|
8
|
+
export async function relocateOrForgetDocument(context, documentId, location, scope) {
|
|
9
|
+
const failure = (message) => ({
|
|
10
|
+
ok: false,
|
|
11
|
+
status: 'conflict',
|
|
12
|
+
record: null,
|
|
13
|
+
diagnostics: [
|
|
14
|
+
createDiagnostic({
|
|
15
|
+
code: diagnosticCodes.document_write_conflict,
|
|
16
|
+
severity: 'error',
|
|
17
|
+
message,
|
|
18
|
+
source: context.configPath,
|
|
19
|
+
hint: 'No document content is changed by relocate or forget.',
|
|
20
|
+
}),
|
|
21
|
+
],
|
|
22
|
+
});
|
|
23
|
+
if (scope !== undefined &&
|
|
24
|
+
(!['shared', 'private'].includes(scope) || location !== null))
|
|
25
|
+
return failure('An explicit shared/private scope is accepted only for forget.');
|
|
26
|
+
const held = await acquireKnowledgeLock(context, 'write-document');
|
|
27
|
+
if (!held.ok || !held.lock)
|
|
28
|
+
return failure(held.message ?? 'Knowledge state is locked.');
|
|
29
|
+
try {
|
|
30
|
+
const loaded = await loadKnowledgeContext(context.root);
|
|
31
|
+
if (!loaded.ok ||
|
|
32
|
+
!loaded.context ||
|
|
33
|
+
!sameRootIdentity(loaded.context, context.rootIdentity))
|
|
34
|
+
return failure('Workspace identity changed.');
|
|
35
|
+
context = loaded.context;
|
|
36
|
+
const catalogs = await loadKnowledgeCatalogs(context);
|
|
37
|
+
if (!catalogs.shared.valid ||
|
|
38
|
+
!catalogs.private.valid ||
|
|
39
|
+
(scope === undefined && catalogs.idConflicts.size > 0))
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
status: 'conflict',
|
|
43
|
+
record: null,
|
|
44
|
+
diagnostics: [...catalogs.diagnostics],
|
|
45
|
+
};
|
|
46
|
+
const scoped = scope === undefined
|
|
47
|
+
? null
|
|
48
|
+
: catalogFor(catalogs, scope, context).records.filter((entry) => entry.documentId === documentId);
|
|
49
|
+
const record = scoped === null
|
|
50
|
+
? findRecord(catalogs, documentId)
|
|
51
|
+
: scoped.length === 1
|
|
52
|
+
? scoped[0]
|
|
53
|
+
: null;
|
|
54
|
+
if (!record)
|
|
55
|
+
return failure('Document id is not registered or is ambiguous.');
|
|
56
|
+
if (record.taskId !== null)
|
|
57
|
+
return failure('Task-linked records require explicit task ownership review before relocating or forgetting.');
|
|
58
|
+
if (location === null) {
|
|
59
|
+
const retainedInOtherScope = scope === 'private' &&
|
|
60
|
+
catalogFor(catalogs, 'shared', context).records.some((entry) => entry.documentId === documentId);
|
|
61
|
+
if (!retainedInOtherScope &&
|
|
62
|
+
findRegisteredReferences(catalogs, documentId).length > 0)
|
|
63
|
+
return failure('Other documents reference this id; update their references before forgetting it.');
|
|
64
|
+
const removedScope = scope ?? recordScope(catalogs, record);
|
|
65
|
+
const catalog = catalogFor(catalogs, removedScope, context);
|
|
66
|
+
const written = await writeCatalogFile(context, removedScope, { ...catalog, records: recordsForCatalog(catalog, documentId, null) }, catalogDigestFor(catalogs, removedScope));
|
|
67
|
+
return written.ok
|
|
68
|
+
? { ok: true, status: 'existing', record, diagnostics: [] }
|
|
69
|
+
: failure(written.message);
|
|
70
|
+
}
|
|
71
|
+
const resolved = await resolveKnowledgeLocation(context, location);
|
|
72
|
+
if (!resolved.ok)
|
|
73
|
+
return failure(resolved.message);
|
|
74
|
+
const file = await readKnowledgeFile(resolved.value.absolutePath, 8 * 1024 * 1024);
|
|
75
|
+
if (!file.ok || file.value.digest !== record.digest)
|
|
76
|
+
return failure('Relocation target does not match the registered content digest.');
|
|
77
|
+
const alias = await findCanonicalAlias(context, catalogs, resolved.value, record.documentId);
|
|
78
|
+
if (alias)
|
|
79
|
+
return failure('Relocation target is owned by another active document.');
|
|
80
|
+
const next = { ...record, location: resolved.value.location };
|
|
81
|
+
const update = setCatalogRecord(context, catalogs, next);
|
|
82
|
+
const written = await writeCatalogFile(context, update.scope, update.catalog, catalogDigestFor(catalogs, update.scope));
|
|
83
|
+
return written.ok
|
|
84
|
+
? { ok: true, status: 'existing', record: next, diagnostics: [] }
|
|
85
|
+
: failure(written.message);
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
const released = await releaseKnowledgeLock(held.lock, held.path);
|
|
89
|
+
if (!released.ok)
|
|
90
|
+
return failure('Metadata may have committed, but its lock could not be released; inspect the catalog before retrying.');
|
|
91
|
+
}
|
|
92
|
+
}
|