@aiwg/cli 2026.10.5 → 2026.10.6
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/agentic/code/addons/composition-engine/lib/runtime.mjs +1401 -0
- package/agentic/code/addons/composition-engine/lib/validator.mjs +513 -0
- package/agentic/code/addons/composition-engine/schemas/flow-graph.schema.json +372 -0
- package/agentic/code/providers/model-capabilities.v1.json +74 -50
- package/bin/aiwg.mjs +3 -1
- package/dist/src/artifacts/browser-export.js +1 -0
- package/dist/src/artifacts/citation-parser.js +8 -1
- package/dist/src/artifacts/cli.js +57 -6
- package/dist/src/artifacts/fortemi-core-query-adapter.js +2 -0
- package/dist/src/artifacts/fortemi-core-sync.js +26 -8
- package/dist/src/artifacts/graph-query.js +18 -8
- package/dist/src/artifacts/index-adapters.js +339 -0
- package/dist/src/artifacts/index-builder.js +143 -14
- package/dist/src/artifacts/index-files.js +15 -4
- package/dist/src/artifacts/index-reader.js +44 -0
- package/dist/src/artifacts/index-refresh.js +114 -55
- package/dist/src/artifacts/index-status.js +20 -5
- package/dist/src/artifacts/memory-maintenance.js +503 -0
- package/dist/src/artifacts/prebuilt-relations.js +3 -1
- package/dist/src/artifacts/query-engine.js +263 -108
- package/dist/src/artifacts/relations-builder.js +2 -2
- package/dist/src/artifacts/source-graph.js +2 -2
- package/dist/src/artifacts/types.js +9 -1
- package/dist/src/cli/handlers/decision.js +19 -1
- package/dist/src/cli/handlers/guided-setup.js +470 -0
- package/dist/src/cli/handlers/help.js +2 -2
- package/dist/src/cli/handlers/housekeeper.js +1110 -0
- package/dist/src/cli/handlers/index.js +4 -2
- package/dist/src/cli/handlers/init.js +41 -6
- package/dist/src/cli/handlers/local-executor.js +5 -3
- package/dist/src/cli/handlers/mc.js +509 -53
- package/dist/src/cli/handlers/models.js +32 -2
- package/dist/src/cli/handlers/ralph-launcher.js +76 -4
- package/dist/src/cli/handlers/rules.js +1 -1
- package/dist/src/cli/handlers/script-runner.js +73 -2
- package/dist/src/cli/handlers/serve.js +5 -3
- package/dist/src/cli/handlers/sessions.js +1 -0
- package/dist/src/cli/handlers/setup.js +428 -1
- package/dist/src/cli/handlers/skill-lint.js +1 -1
- package/dist/src/cli/handlers/subcommands.js +118 -8
- package/dist/src/cli/handlers/use.js +279 -93
- package/dist/src/cli/handlers/workspace.js +3 -1
- package/dist/src/cli/prompt-utils.js +8 -0
- package/dist/src/cli/router.js +16 -1
- package/dist/src/config/aiwg-config.js +42 -0
- package/dist/src/config/cli.js +7 -3
- package/dist/src/config/decision-config.js +14 -1
- package/dist/src/config/native-sandbox-warnings.js +25 -0
- package/dist/src/contributors/validation.js +6 -5
- package/dist/src/decision/admission.js +7 -3
- package/dist/src/decision/benchmark.js +469 -0
- package/dist/src/decision/ledger.js +22 -3
- package/dist/src/decision/pricing.js +147 -0
- package/dist/src/decision/qualification/gate-evidence.js +18 -15
- package/dist/src/decision/qualification/load-staged.js +539 -0
- package/dist/src/decision/qualification/load.js +109 -33
- package/dist/src/decision/qualification/runner.js +5 -2
- package/dist/src/decision/review/service.js +6 -4
- package/dist/src/decision/review/validate.js +23 -2
- package/dist/src/decision/savings.js +8 -3
- package/dist/src/discovery/index.js +492 -0
- package/dist/src/discovery/interview.js +233 -0
- package/dist/src/discovery/plan-apply.js +571 -0
- package/dist/src/discovery/plan-store.js +171 -0
- package/dist/src/discovery/plan-types.js +2 -0
- package/dist/src/discovery/plan-validation.js +139 -0
- package/dist/src/discovery/types.js +2 -0
- package/dist/src/extensions/commands/definitions.js +49 -9
- package/dist/src/extensions/manifest.js +71 -44
- package/dist/src/extensions/project-local-discovery.js +1 -1
- package/dist/src/extensions/project-quickref.js +18 -17
- package/dist/src/extensions/validation.js +32 -29
- package/dist/src/housekeeper/daemon-config.js +148 -0
- package/dist/src/housekeeper/ipc.js +84 -0
- package/dist/src/housekeeper/lifecycle.js +148 -0
- package/dist/src/housekeeper/platform.js +35 -0
- package/dist/src/housekeeper/status.js +70 -0
- package/dist/src/housekeeper/units.js +114 -0
- package/dist/src/jobs/flow.js +22 -21
- package/dist/src/lint/rule-activation.js +20 -5
- package/dist/src/lint/runner.js +1 -1
- package/dist/src/mcp/cli.mjs +5 -6
- package/dist/src/mcp/config-file.mjs +6 -2
- package/dist/src/mcp/config-layers.mjs +23 -7
- package/dist/src/mcp/profiles.js +18 -31
- package/dist/src/mcp/profiles.mjs +18 -32
- package/dist/src/mcp/registry.js +7 -27
- package/dist/src/mcp/registry.mjs +7 -28
- package/dist/src/mcp/server.mjs +34 -1
- package/dist/src/mcp/tools/agentic-sandbox.mjs +8 -7
- package/dist/src/mcp/tools/command-run.mjs +3 -2
- package/dist/src/mcp/tools/decision.mjs +49 -48
- package/dist/src/mcp/tools/discovery.mjs +46 -21
- package/dist/src/mcp/tools/orchestration.mjs +22 -22
- package/dist/src/mcp/tools/subsystems.mjs +9 -8
- package/dist/src/mcp/tools/worker-invoke.mjs +17 -4
- package/dist/src/mcp/tools/writer-profiles.mjs +1 -1
- package/dist/src/memory/audit.js +719 -0
- package/dist/src/memory/canonical-context.js +392 -13
- package/dist/src/memory/canonical-maintenance.js +95 -0
- package/dist/src/memory/cli-result.js +22 -0
- package/dist/src/memory/cli.js +176 -0
- package/dist/src/memory/context-pack.js +57 -6
- package/dist/src/memory/cost.js +512 -0
- package/dist/src/memory/firewall-classification.d.mts +7 -0
- package/dist/src/memory/firewall-classification.mjs +32 -0
- package/dist/src/memory/index.js +8 -0
- package/dist/src/memory/line-maintenance.js +68 -0
- package/dist/src/memory/maintenance-cli.js +286 -0
- package/dist/src/memory/maintenance-contract.js +136 -0
- package/dist/src/memory/maintenance-index.js +18 -0
- package/dist/src/memory/maintenance-owner.js +2 -0
- package/dist/src/memory/maintenance-record.js +138 -0
- package/dist/src/memory/maintenance.js +254 -0
- package/dist/src/memory/provider-memory-adapter.js +154 -0
- package/dist/src/memory/recommend-cli.js +83 -0
- package/dist/src/memory/recommend.js +269 -0
- package/dist/src/memory/scheduled.js +81 -0
- package/dist/src/memory/taxonomy.js +44 -0
- package/dist/src/mission-protocol/codecs.js +19 -9
- package/dist/src/models/config-loader.js +11 -1
- package/dist/src/models/model-capabilities.v1.json +74 -50
- package/dist/src/models/provider-policy.js +125 -62
- package/dist/src/models/resolver.js +45 -8
- package/dist/src/models/worker-installation.d.mts +7 -0
- package/dist/src/models/worker-installation.mjs +169 -37
- package/dist/src/models/wrapper-route.js +1 -1
- package/dist/src/output-modes/registry.js +8 -7
- package/dist/src/plugin/metadata-validator.js +1 -1
- package/dist/src/providers/deploy-version.d.mts +1 -0
- package/dist/src/providers/deploy-version.mjs +17 -0
- package/dist/src/providers/provider-definitions.js +19 -16
- package/dist/src/providers/skill-ownership.d.mts +11 -2
- package/dist/src/providers/skill-ownership.mjs +35 -17
- package/dist/src/rules/activation.d.mts +11 -2
- package/dist/src/rules/activation.mjs +82 -6
- package/dist/src/rules/inventory.js +1 -1
- package/dist/src/rules/rule-frontmatter.schema.json +2 -0
- package/dist/src/schema/zod-validation.d.mts +6 -0
- package/dist/src/schema/zod-validation.mjs +50 -0
- package/dist/src/serve/executor-registry.js +297 -20
- package/dist/src/serve/executor-token.d.mts +1 -0
- package/dist/src/serve/executor-token.mjs +8 -0
- package/dist/src/serve/shared-host-scheduler.js +1102 -31
- package/dist/src/serve/worker-invocation.js +114 -15
- package/dist/src/sessions/adapters/claude.js +19 -18
- package/dist/src/sessions/adapters/codex.js +10 -9
- package/dist/src/sessions/adapters/copilot.js +14 -13
- package/dist/src/sessions/adapters/cursor.js +19 -18
- package/dist/src/sessions/adapters/deepseek-harness.js +9 -8
- package/dist/src/sessions/adapters/factory.js +11 -10
- package/dist/src/sessions/adapters/generic.js +32 -23
- package/dist/src/sessions/adapters/grokbot.js +35 -35
- package/dist/src/sessions/adapters/hermes.js +20 -19
- package/dist/src/sessions/adapters/muse.js +50 -49
- package/dist/src/sessions/adapters/omp.js +5 -4
- package/dist/src/sessions/adapters/openclaw.js +30 -29
- package/dist/src/sessions/adapters/opencode.js +26 -26
- package/dist/src/sessions/adapters/openhuman.js +24 -23
- package/dist/src/sessions/adapters/pi.js +5 -4
- package/dist/src/sessions/adapters/windsurf.js +8 -8
- package/dist/src/sessions/analytics.js +10 -9
- package/dist/src/sessions/candidates.js +7 -6
- package/dist/src/sessions/contracts.js +63 -53
- package/dist/src/sessions/output-registration.js +11 -10
- package/dist/src/sessions/workspace-discovery.js +3 -4
- package/dist/src/smiths/context-pipeline/aiwg-md.js +18 -1
- package/dist/src/smiths/context-pipeline/claude-hook.js +42 -6
- package/dist/src/smiths/context-pipeline/parallelism-section.js +72 -5
- package/dist/src/smiths/context-pipeline/workspace-context.js +106 -10
- package/dist/src/update/checker.d.mts +4 -0
- package/dist/src/update/checker.mjs +14 -0
- package/dist/src/writing/exemplar-selection.js +6 -6
- package/dist/src/writing/voice-evaluation.js +24 -23
- package/dist/src/writing/writer-migration.js +14 -13
- package/dist/src/writing/writer-profile.js +19 -18
- package/dist/src/writing/writing-brief.js +18 -17
- package/dist/src/writing/writing-receipt.js +47 -46
- package/package.json +5 -2
- package/schemas/decision/DecisionReview.schema.json +78 -1
- package/schemas/mission-v1.schema.json +3 -0
- package/tools/agents/deploy-agents.mjs +67 -39
- package/tools/agents/providers/antigravity.mjs +6 -1
- package/tools/agents/providers/base.mjs +19 -13
- package/tools/agents/providers/claude.mjs +1 -1
- package/tools/agents/providers/codex.mjs +50 -56
- package/tools/agents/providers/copilot.mjs +8 -2
- package/tools/agents/providers/cursor.mjs +1 -1
- package/tools/agents/providers/deepseek-harness.mjs +1 -1
- package/tools/agents/providers/factory.mjs +7 -1
- package/tools/agents/providers/grok-build.mjs +18 -2
- package/tools/agents/providers/grokbot.mjs +1 -1
- package/tools/agents/providers/hermes.mjs +7 -13
- package/tools/agents/providers/muse-hooks.mjs +21 -14
- package/tools/agents/providers/muse.mjs +7 -2
- package/tools/agents/providers/omp.mjs +28 -4
- package/tools/agents/providers/opencode.mjs +14 -5
- package/tools/agents/providers/openhuman.mjs +1 -1
- package/tools/agents/providers/pi.mjs +1 -1
- package/tools/agents/providers/provider-prepare-coordinator.mjs +29 -9
- package/tools/agents/providers/rule-delivery.mjs +263 -71
- package/tools/agents/providers/windsurf.mjs +7 -0
- package/tools/cli/workspace-status.mjs +5 -0
- package/tools/commands/deploy-prompts-codex.mjs +11 -8
- package/tools/ralph-external/lib/codex-adapter.mjs +6 -2
- package/tools/ralph-external/lib/muse-adapter.mjs +12 -2
- package/tools/ralph-external/lib/worker-health.mjs +1 -1
- package/tools/ralph-external/lib/worker-isolation.mjs +65 -9
- package/tools/ralph-external/process-monitor.mjs +7 -4
- package/tools/ralph-external/session-launcher.mjs +280 -32
- package/tools/skills/deploy-skills-codex.mjs +11 -7
- package/vscode-extension/schemas/aiwg.config.v1.json +11 -1
|
@@ -1,14 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
1
|
+
import { existsSync, mkdirSync, linkSync, statSync, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { dirname, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { hostname } from 'node:os';
|
|
5
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
4
6
|
import { z } from 'zod';
|
|
7
|
+
import { dateTime } from '../schema/zod-validation.mjs';
|
|
8
|
+
import { validateMaintenancePlan } from './maintenance-contract.js';
|
|
9
|
+
import { loadMemoryRecord, memoryRecordId } from './maintenance-record.js';
|
|
10
|
+
import { projectAiwgPath, resolveProjectAiwgDir, resolveProjectAiwgDirForWrite, isProjectArtifactRootExternal } from '../config/project-artifacts.js';
|
|
5
11
|
const DigestSchema = z.string().regex(/^sha256:[0-9a-f]{64}$/);
|
|
6
12
|
const LocatorSchema = z.string().min(1).max(512).refine(value => !/[\r\n\0]/.test(value));
|
|
7
13
|
export const CanonicalContextTargetSchema = z.enum([
|
|
8
14
|
'project-fact', 'preference', 'goal', 'project', 'person', 'decision', 'operating-rule',
|
|
9
15
|
]);
|
|
10
16
|
export const CanonicalContextClassificationSchema = z.enum(['public', 'internal']);
|
|
11
|
-
export const CanonicalContextProposalSchema = z.
|
|
17
|
+
export const CanonicalContextProposalSchema = z.strictObject({
|
|
12
18
|
target: CanonicalContextTargetSchema,
|
|
13
19
|
key: z.string().min(1).max(128).regex(/^[a-z0-9][a-z0-9._-]*$/),
|
|
14
20
|
value: z.string().min(1).max(8192),
|
|
@@ -18,9 +24,9 @@ export const CanonicalContextProposalSchema = z.object({
|
|
|
18
24
|
reason: z.string().min(1).max(1024),
|
|
19
25
|
scope: z.string().min(1).max(128),
|
|
20
26
|
classification: CanonicalContextClassificationSchema,
|
|
21
|
-
reviewAt:
|
|
22
|
-
expiresAt:
|
|
23
|
-
})
|
|
27
|
+
reviewAt: dateTime().nullable().default(null),
|
|
28
|
+
expiresAt: dateTime().nullable().default(null),
|
|
29
|
+
});
|
|
24
30
|
function sha256(value) {
|
|
25
31
|
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
26
32
|
}
|
|
@@ -54,17 +60,20 @@ function assertSafeProposal(proposal) {
|
|
|
54
60
|
}
|
|
55
61
|
}
|
|
56
62
|
function ensureStorageInsideProject(projectRoot, target) {
|
|
57
|
-
|
|
63
|
+
let rootAncestor = resolve(projectRoot);
|
|
64
|
+
while (!existsSync(rootAncestor))
|
|
65
|
+
rootAncestor = dirname(rootAncestor);
|
|
66
|
+
const root = resolve(realpathSync(rootAncestor), relative(rootAncestor, resolve(projectRoot)));
|
|
58
67
|
const lexicalRoot = resolve(projectRoot);
|
|
59
68
|
const candidate = resolve(target);
|
|
60
69
|
if (candidate !== lexicalRoot && !candidate.startsWith(`${lexicalRoot}${sep}`)) {
|
|
61
|
-
throw new Error('canonical context storage must remain inside the
|
|
70
|
+
throw new Error('canonical context storage must remain inside the resolved root');
|
|
62
71
|
}
|
|
63
72
|
let ancestor = candidate;
|
|
64
73
|
while (!existsSync(ancestor))
|
|
65
74
|
ancestor = dirname(ancestor);
|
|
66
75
|
const actual = realpathSync(ancestor);
|
|
67
|
-
if (actual !== root && !actual.startsWith(`${root}${sep}`)) {
|
|
76
|
+
if (existsSync(projectRoot) && actual !== root && !actual.startsWith(`${root}${sep}`)) {
|
|
68
77
|
throw new Error('canonical context storage cannot traverse a link outside the project');
|
|
69
78
|
}
|
|
70
79
|
}
|
|
@@ -107,18 +116,73 @@ function proposalFromEntry(entry) {
|
|
|
107
116
|
function storeDigest(store) {
|
|
108
117
|
return sha256(JSON.stringify(store));
|
|
109
118
|
}
|
|
119
|
+
/** Project the canonical owner data without creating a second record store. */
|
|
120
|
+
export function canonicalMemoryRecord(entry) {
|
|
121
|
+
const sourcePath = `.aiwg/context/compound-memory/context.json#${entry.entryId}`;
|
|
122
|
+
const digest = sha256(entry.value);
|
|
123
|
+
const id = memoryRecordId(sourcePath, digest);
|
|
124
|
+
const lifecycle = entry.maintenance?.lifecycle
|
|
125
|
+
?? (entry.status === 'revoked' ? 'retired' : entry.status);
|
|
126
|
+
const tombstone = lifecycle === 'active' ? undefined : entry.maintenance?.tombstone ?? {
|
|
127
|
+
id, digest, disposition: lifecycle,
|
|
128
|
+
reason: entry.disposition?.reason ?? entry.reason,
|
|
129
|
+
reviewer: entry.disposition?.reviewer ?? entry.reviewer,
|
|
130
|
+
at: entry.disposition?.recordedAt ?? entry.updatedAt,
|
|
131
|
+
};
|
|
132
|
+
return loadMemoryRecord({
|
|
133
|
+
schemaVersion: 'aiwg.memory-record.v1', id, sourcePath, digest,
|
|
134
|
+
version: entry.maintenance?.version ?? 1,
|
|
135
|
+
aliases: [...new Set([entry.entryId, ...(entry.maintenance?.aliases ?? [])])],
|
|
136
|
+
supersedes: entry.maintenance?.supersedes ?? [],
|
|
137
|
+
mergedFrom: entry.maintenance?.mergedFrom ?? [],
|
|
138
|
+
splitFrom: entry.maintenance?.splitFrom ?? [],
|
|
139
|
+
scope: entry.maintenance?.scope ?? 'project',
|
|
140
|
+
placement: entry.maintenance?.placement ?? 'resident', lifecycle,
|
|
141
|
+
authority: entry.maintenance?.authority ?? {
|
|
142
|
+
owner: 'compound-memory', source: 'reviewer', protection: 'unprotected',
|
|
143
|
+
},
|
|
144
|
+
observation: {
|
|
145
|
+
status: 'on-disk', evidence: {
|
|
146
|
+
source: sourcePath, at: entry.updatedAt, session: null, provider: null, version: null,
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
...(entry.importedFromWorkspace ? { sourceWorkspaceId: entry.importedFromWorkspace } : {}),
|
|
150
|
+
...(tombstone ? { tombstone } : {}),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
function maintenanceMetadata(record, category, prior, recordedAt) {
|
|
154
|
+
const { version, scope, placement, lifecycle, authority, aliases, supersedes, mergedFrom, splitFrom, tombstone } = record;
|
|
155
|
+
return { version, scope, placement, lifecycle, authority, aliases, supersedes, mergedFrom, splitFrom,
|
|
156
|
+
...(tombstone ? { tombstone } : {}), ...(category ? { category } : {}),
|
|
157
|
+
...(prior ? { history: [...(prior.maintenance?.history ?? []), {
|
|
158
|
+
record: canonicalMemoryRecord(prior), disposition: prior.disposition,
|
|
159
|
+
recordedAt: recordedAt ?? prior.updatedAt,
|
|
160
|
+
...(prior.maintenance?.category ? { category: prior.maintenance.category } : {}),
|
|
161
|
+
}] } : {}),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
110
164
|
export class CanonicalContextRepository {
|
|
165
|
+
projectRoot;
|
|
111
166
|
root;
|
|
112
167
|
statePath;
|
|
113
168
|
receiptRoot;
|
|
114
169
|
lockPath;
|
|
115
170
|
workspaceId;
|
|
116
171
|
constructor(projectRoot) {
|
|
117
|
-
this.
|
|
172
|
+
this.projectRoot = projectRoot;
|
|
173
|
+
this.root = projectAiwgPath(projectRoot, 'context/compound-memory');
|
|
118
174
|
this.statePath = resolve(this.root, 'context.json');
|
|
119
175
|
this.receiptRoot = resolve(this.root, 'receipts');
|
|
120
176
|
this.lockPath = resolve(this.root, '.lock');
|
|
121
|
-
|
|
177
|
+
if (isProjectArtifactRootExternal(projectRoot)) {
|
|
178
|
+
// Before artifact routing the store was always repository-local. Never
|
|
179
|
+
// silently hide that history behind an empty external store.
|
|
180
|
+
const legacy = resolve(projectRoot, '.aiwg/context/compound-memory');
|
|
181
|
+
if (existsSync(resolve(legacy, 'context.json')) && !existsSync(this.statePath)) {
|
|
182
|
+
throw new Error(`Canonical context exists at the repository-local path ${legacy}, but the configured artifact root is external. Move that directory to ${this.root} before using canonical context.`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
ensureStorageInsideProject(isProjectArtifactRootExternal(projectRoot) ? resolveProjectAiwgDir(projectRoot) : projectRoot, this.root);
|
|
122
186
|
this.workspaceId = workspaceIdentity(projectRoot);
|
|
123
187
|
}
|
|
124
188
|
read() {
|
|
@@ -140,6 +204,292 @@ export class CanonicalContextRepository {
|
|
|
140
204
|
}
|
|
141
205
|
return store;
|
|
142
206
|
}
|
|
207
|
+
snapshot() { return storeDigest(this.read()); }
|
|
208
|
+
/** Input to regenerated resident context. History remains available through export(). */
|
|
209
|
+
residentEntries() {
|
|
210
|
+
return Object.values(this.read().entries).filter(entry => {
|
|
211
|
+
const record = canonicalMemoryRecord(entry);
|
|
212
|
+
return record.lifecycle === 'active' && record.placement === 'resident';
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
maintenanceReceipt(key, operation) {
|
|
216
|
+
const receipt = this.read().maintenanceReceipts?.[key];
|
|
217
|
+
if (receipt && operation && receipt.operationId !== sha256(JSON.stringify({ operation, snapshot: operation.owner?.snapshot }))) {
|
|
218
|
+
throw new Error('Maintenance receipt key conflicts with another operation');
|
|
219
|
+
}
|
|
220
|
+
return receipt;
|
|
221
|
+
}
|
|
222
|
+
previewMaintenance(operation) {
|
|
223
|
+
const store = this.read();
|
|
224
|
+
if (!operation.owner || operation.owner.name !== 'compound-memory' || !operation.review) {
|
|
225
|
+
throw new Error('Canonical maintenance requires an owner binding and explicit review');
|
|
226
|
+
}
|
|
227
|
+
if (operation.owner.snapshot !== storeDigest(store))
|
|
228
|
+
throw new Error('Stale canonical owner snapshot');
|
|
229
|
+
const entry = store.entries[operation.owner.handle];
|
|
230
|
+
if (!entry)
|
|
231
|
+
throw new Error('Canonical maintenance entry was not found');
|
|
232
|
+
const record = canonicalMemoryRecord(entry);
|
|
233
|
+
if (record.authority.owner !== 'compound-memory')
|
|
234
|
+
throw new Error('Canonical maintenance cannot mutate another authority owner');
|
|
235
|
+
if (operation.target.id !== record.id && !record.aliases.includes(operation.target.id)) {
|
|
236
|
+
throw new Error('Canonical maintenance target does not match its owner handle');
|
|
237
|
+
}
|
|
238
|
+
validateMaintenancePlan({ schemaVersion: 'aiwg.maintenance-plan.v1', operations: [operation] }, Object.values(store.entries).map(canonicalMemoryRecord), { retirementBasis: operation.review.retirementBasis });
|
|
239
|
+
if (operation.sessionEffect !== 'next-session') {
|
|
240
|
+
throw new Error('Canonical maintenance can only promise next-session effect');
|
|
241
|
+
}
|
|
242
|
+
if (operation.op !== 'merge' && operation.related?.length)
|
|
243
|
+
throw new Error('Only merge may bind additional sources');
|
|
244
|
+
if (operation.op === 'restore' && (record.lifecycle === 'active'
|
|
245
|
+
|| Object.values(store.entries).some(candidate => candidate.entryId !== entry.entryId
|
|
246
|
+
&& candidate.target === entry.target && candidate.key === entry.key && candidate.status === 'active'))) {
|
|
247
|
+
throw new Error('Restore requires inactive history without an active conflicting entry');
|
|
248
|
+
}
|
|
249
|
+
if (operation.op === 'recategorize' && !operation.category)
|
|
250
|
+
throw new Error('Recategorize requires category');
|
|
251
|
+
if (operation.op === 'promote' || operation.op === 'demote') {
|
|
252
|
+
const ranks = { archived: 0, indexed: 1, 'task-context': 2, resident: 3 };
|
|
253
|
+
const direction = ranks[operation.to.placement] - ranks[operation.from.placement];
|
|
254
|
+
if ((operation.op === 'promote' && direction <= 0) || (operation.op === 'demote' && direction >= 0)
|
|
255
|
+
|| operation.to.lifecycle !== operation.from.lifecycle) {
|
|
256
|
+
throw new Error('Placement maintenance requires the requested promotion or demotion');
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
else if (operation.to.placement !== operation.from.placement) {
|
|
260
|
+
throw new Error('Lifecycle and category maintenance cannot implicitly change placement');
|
|
261
|
+
}
|
|
262
|
+
if (operation.op === 'recategorize' && operation.to.lifecycle !== record.lifecycle) {
|
|
263
|
+
throw new Error('Recategorize cannot implicitly change lifecycle');
|
|
264
|
+
}
|
|
265
|
+
const replacements = operation.replacements ?? [];
|
|
266
|
+
if (['supersede', 'merge', 'split'].includes(operation.op)) {
|
|
267
|
+
if (operation.to.lifecycle !== 'superseded')
|
|
268
|
+
throw new Error('Replacement requires superseded lifecycle');
|
|
269
|
+
if ((operation.op === 'split' && replacements.length < 2)
|
|
270
|
+
|| (operation.op !== 'split' && replacements.length !== 1)) {
|
|
271
|
+
throw new Error('Replacement operation requires the exact replacement count');
|
|
272
|
+
}
|
|
273
|
+
const keys = new Set();
|
|
274
|
+
for (const replacement of replacements) {
|
|
275
|
+
const proposal = CanonicalContextProposalSchema.parse({
|
|
276
|
+
...proposalFromEntry(entry), key: replacement.key, value: replacement.value,
|
|
277
|
+
sourceRef: canonicalLocator(replacement.sourcePath ?? entry.sourceRef),
|
|
278
|
+
sourceDigest: replacement.sourcePath ? null : entry.sourceDigest,
|
|
279
|
+
reviewer: operation.review.reviewer, reason: operation.rationale,
|
|
280
|
+
});
|
|
281
|
+
assertSafeProposal(proposal);
|
|
282
|
+
const replacementId = stableEntryId(proposal);
|
|
283
|
+
if (keys.has(replacement.key) || store.entries[replacementId]) {
|
|
284
|
+
throw new Error('Replacement must have a unique new identity');
|
|
285
|
+
}
|
|
286
|
+
keys.add(replacement.key);
|
|
287
|
+
if (Object.values(store.entries).some(candidate => candidate.status === 'active'
|
|
288
|
+
&& candidate.entryId !== entry.entryId && candidate.key === replacement.key
|
|
289
|
+
&& candidate.target === entry.target && !(operation.related ?? []).some(binding => binding.id === candidate.entryId
|
|
290
|
+
|| binding.id === canonicalMemoryRecord(candidate).id))) {
|
|
291
|
+
throw new Error('Replacement conflicts with an unreviewed active canonical entry');
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
else if (replacements.length)
|
|
296
|
+
throw new Error('This operation cannot replace canonical values');
|
|
297
|
+
if (operation.op === 'merge' && this.mergeEntries(store, operation).length < 2) {
|
|
298
|
+
throw new Error('Merge requires additional reviewed canonical sources');
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
schemaVersion: 'aiwg.canonical-context-preview.v1', operation: 'maintenance',
|
|
302
|
+
operationId: sha256(JSON.stringify({ operation, snapshot: storeDigest(store) })),
|
|
303
|
+
workspaceId: this.workspaceId, storeDigest: storeDigest(store), duplicate: false,
|
|
304
|
+
confirmationRequired: true, conflicts: [], proposedEntryId: entry.entryId,
|
|
305
|
+
importCount: 0, diff: [{ field: entry.entryId, before: JSON.stringify(operation.from), after: JSON.stringify(operation.to) }],
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
mergeEntries(store, operation) {
|
|
309
|
+
const target = store.entries[operation.owner.handle];
|
|
310
|
+
const related = operation.related ?? [];
|
|
311
|
+
const sources = [target];
|
|
312
|
+
for (const binding of related) {
|
|
313
|
+
const source = Object.values(store.entries).find(candidate => {
|
|
314
|
+
const record = canonicalMemoryRecord(candidate);
|
|
315
|
+
return record.id === binding.id || record.aliases.includes(binding.id);
|
|
316
|
+
});
|
|
317
|
+
if (!source || sources.includes(source))
|
|
318
|
+
throw new Error('Merge source is missing or duplicated');
|
|
319
|
+
const record = canonicalMemoryRecord(source);
|
|
320
|
+
if (record.digest !== binding.digest || record.version !== binding.version)
|
|
321
|
+
throw new Error('Stale merge source digest or version');
|
|
322
|
+
sources.push(source);
|
|
323
|
+
}
|
|
324
|
+
for (const entry of sources) {
|
|
325
|
+
const record = canonicalMemoryRecord(entry);
|
|
326
|
+
const base = canonicalMemoryRecord(target);
|
|
327
|
+
if (record.lifecycle !== 'active' || record.scope !== base.scope
|
|
328
|
+
|| !isDeepStrictEqual(record.authority, base.authority)
|
|
329
|
+
|| record.placement !== base.placement || entry.scope !== target.scope || entry.classification !== target.classification) {
|
|
330
|
+
throw new Error('Merge sources must preserve scope, authority, placement and active lifecycle');
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return sources;
|
|
334
|
+
}
|
|
335
|
+
publishOwnerLock(lockPath) {
|
|
336
|
+
// Publish complete owner identity with one link syscall. A crash before this
|
|
337
|
+
// point leaves only an unrelated temporary file, never an empty owner lock.
|
|
338
|
+
const temporary = `${lockPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
339
|
+
try {
|
|
340
|
+
writeFileSync(temporary, JSON.stringify({ pid: process.pid, host: hostname() }), { flag: 'wx', mode: 0o600 });
|
|
341
|
+
linkSync(temporary, lockPath);
|
|
342
|
+
}
|
|
343
|
+
finally {
|
|
344
|
+
if (existsSync(temporary))
|
|
345
|
+
unlinkSync(temporary);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
openOwnerLock(lockPath = this.lockPath, depth = 0) {
|
|
349
|
+
try {
|
|
350
|
+
return this.publishOwnerLock(lockPath);
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
if (error.code !== 'EEXIST' || depth >= 8)
|
|
354
|
+
throw error;
|
|
355
|
+
// The same atomic lease protocol protects recovery for sync legacy and
|
|
356
|
+
// async maintenance callers, including recovery after a dead reclaimer.
|
|
357
|
+
const recoveryPath = `${lockPath}.recovery`;
|
|
358
|
+
this.openOwnerLock(recoveryPath, depth + 1);
|
|
359
|
+
try {
|
|
360
|
+
let owner;
|
|
361
|
+
try {
|
|
362
|
+
owner = JSON.parse(readFileSync(lockPath, 'utf8'));
|
|
363
|
+
}
|
|
364
|
+
catch (readError) {
|
|
365
|
+
if (readError.code === 'ENOENT')
|
|
366
|
+
return this.publishOwnerLock(lockPath);
|
|
367
|
+
}
|
|
368
|
+
if (!owner || typeof owner !== 'object' || Array.isArray(owner)
|
|
369
|
+
|| typeof owner.host !== 'string' || !owner.host
|
|
370
|
+
|| !Number.isSafeInteger(owner.pid) || owner.pid < 1) {
|
|
371
|
+
// A legacy writer could crash after exclusive create and before writing.
|
|
372
|
+
// Give fresh/partially written locks a bounded grace period.
|
|
373
|
+
if (Date.now() - statSync(lockPath).mtimeMs < 60_000)
|
|
374
|
+
throw error;
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
if (owner.host !== hostname())
|
|
378
|
+
throw error;
|
|
379
|
+
try {
|
|
380
|
+
process.kill(owner.pid, 0);
|
|
381
|
+
throw error;
|
|
382
|
+
}
|
|
383
|
+
catch (probe) {
|
|
384
|
+
if (probe.code !== 'ESRCH')
|
|
385
|
+
throw probe;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
unlinkSync(lockPath);
|
|
389
|
+
return this.publishOwnerLock(lockPath);
|
|
390
|
+
}
|
|
391
|
+
finally {
|
|
392
|
+
unlinkSync(recoveryPath);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
async confirmMaintenance(input) {
|
|
397
|
+
resolveProjectAiwgDirForWrite(this.projectRoot);
|
|
398
|
+
mkdirSync(this.root, { recursive: true });
|
|
399
|
+
this.openOwnerLock();
|
|
400
|
+
try {
|
|
401
|
+
const store = this.read();
|
|
402
|
+
const prior = store.maintenanceReceipts?.[input.key];
|
|
403
|
+
if (prior) {
|
|
404
|
+
if (prior.operationId !== input.preview.operationId)
|
|
405
|
+
throw new Error('Maintenance receipt key conflicts with another operation');
|
|
406
|
+
return prior;
|
|
407
|
+
}
|
|
408
|
+
const preview = this.previewMaintenance(input.operation);
|
|
409
|
+
if (JSON.stringify(preview) !== JSON.stringify(input.preview)) {
|
|
410
|
+
throw new Error('Confirmation requires the exact current canonical maintenance preview');
|
|
411
|
+
}
|
|
412
|
+
const removesResident = input.operation.from.placement === 'resident'
|
|
413
|
+
&& (input.operation.to.placement !== 'resident' || input.operation.to.lifecycle !== 'active');
|
|
414
|
+
if (removesResident && !input.verifyRetrieval) {
|
|
415
|
+
throw new Error('compound-memory resident removal is unsupported: no owner-native indexed retrieval projection is available');
|
|
416
|
+
}
|
|
417
|
+
if (removesResident && !await input.verifyRetrieval(input.operation)) {
|
|
418
|
+
throw new Error('Indexed retrieval must be verified before resident removal');
|
|
419
|
+
}
|
|
420
|
+
if (this.snapshot() !== preview.storeDigest)
|
|
421
|
+
throw new Error('Stale canonical owner snapshot after retrieval verification');
|
|
422
|
+
const now = new Date().toISOString();
|
|
423
|
+
const entry = store.entries[input.operation.owner.handle];
|
|
424
|
+
const original = canonicalMemoryRecord(entry);
|
|
425
|
+
const sources = input.operation.op === 'merge' ? this.mergeEntries(store, input.operation) : [entry];
|
|
426
|
+
const newIds = [];
|
|
427
|
+
for (const replacement of input.operation.replacements ?? []) {
|
|
428
|
+
const proposal = CanonicalContextProposalSchema.parse({
|
|
429
|
+
...proposalFromEntry(entry), key: replacement.key, value: replacement.value,
|
|
430
|
+
sourceRef: canonicalLocator(replacement.sourcePath ?? entry.sourceRef),
|
|
431
|
+
sourceDigest: replacement.sourcePath ? null : entry.sourceDigest,
|
|
432
|
+
reviewer: input.operation.review.reviewer, reason: input.operation.rationale,
|
|
433
|
+
});
|
|
434
|
+
const entryId = stableEntryId(proposal);
|
|
435
|
+
const next = {
|
|
436
|
+
...proposal, entryId, status: 'active', createdAt: now, updatedAt: now,
|
|
437
|
+
supersedes: entry.entryId, disposition: null, importedFromWorkspace: entry.importedFromWorkspace,
|
|
438
|
+
};
|
|
439
|
+
const nextRecord = canonicalMemoryRecord(next);
|
|
440
|
+
nextRecord.scope = original.scope;
|
|
441
|
+
nextRecord.authority = { ...original.authority };
|
|
442
|
+
nextRecord.placement = original.placement;
|
|
443
|
+
nextRecord.aliases = [...new Set([...nextRecord.aliases, ...sources.map(source => canonicalMemoryRecord(source).id)])];
|
|
444
|
+
const lineage = sources.map(source => canonicalMemoryRecord(source).id);
|
|
445
|
+
if (input.operation.op === 'merge')
|
|
446
|
+
nextRecord.mergedFrom = lineage;
|
|
447
|
+
else if (input.operation.op === 'split')
|
|
448
|
+
nextRecord.splitFrom = lineage;
|
|
449
|
+
else
|
|
450
|
+
nextRecord.supersedes = lineage;
|
|
451
|
+
next.maintenance = maintenanceMetadata(nextRecord, replacement.category ?? entry.maintenance?.category);
|
|
452
|
+
store.entries[entryId] = next;
|
|
453
|
+
newIds.push(entryId);
|
|
454
|
+
}
|
|
455
|
+
for (const source of sources) {
|
|
456
|
+
const record = canonicalMemoryRecord(source);
|
|
457
|
+
record.version += 1;
|
|
458
|
+
record.placement = input.operation.to.placement;
|
|
459
|
+
record.lifecycle = input.operation.to.lifecycle;
|
|
460
|
+
if (record.lifecycle !== 'active') {
|
|
461
|
+
record.tombstone = {
|
|
462
|
+
id: record.id, digest: record.digest, disposition: record.lifecycle,
|
|
463
|
+
reviewer: input.operation.review.reviewer, reason: input.operation.rationale, at: now,
|
|
464
|
+
...(newIds[0] ? { successor: canonicalMemoryRecord(store.entries[newIds[0]]).id } : {}),
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
else
|
|
468
|
+
delete record.tombstone;
|
|
469
|
+
source.maintenance = maintenanceMetadata(record, input.operation.category ?? source.maintenance?.category, source, now);
|
|
470
|
+
source.status = record.lifecycle === 'active' ? 'active' : record.lifecycle === 'superseded' ? 'superseded' : 'revoked';
|
|
471
|
+
source.updatedAt = now;
|
|
472
|
+
source.disposition = record.lifecycle === 'active' ? null : {
|
|
473
|
+
reviewer: input.operation.review.reviewer, reason: input.operation.rationale, recordedAt: now,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
store.revision += 1;
|
|
477
|
+
const receipt = {
|
|
478
|
+
schemaVersion: 'aiwg.canonical-context-receipt.v1',
|
|
479
|
+
receiptId: sha256(`${preview.operationId}\0${store.revision}`), operationId: preview.operationId,
|
|
480
|
+
operation: 'maintenance', workspaceId: this.workspaceId,
|
|
481
|
+
entryIds: [...sources.map(source => source.entryId), ...newIds],
|
|
482
|
+
revision: store.revision, duplicate: false, completedAt: now,
|
|
483
|
+
};
|
|
484
|
+
store.maintenanceReceipts = { ...store.maintenanceReceipts, [input.key]: receipt };
|
|
485
|
+
// Mutation and recovery receipt are a single atomic owner commit.
|
|
486
|
+
writeJsonAtomic(this.statePath, store);
|
|
487
|
+
return receipt;
|
|
488
|
+
}
|
|
489
|
+
finally {
|
|
490
|
+
unlinkSync(this.lockPath);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
143
493
|
previewUpsert(raw) {
|
|
144
494
|
const proposal = CanonicalContextProposalSchema.parse({
|
|
145
495
|
...raw,
|
|
@@ -149,6 +499,11 @@ export class CanonicalContextRepository {
|
|
|
149
499
|
const store = this.read();
|
|
150
500
|
const entryId = stableEntryId(proposal);
|
|
151
501
|
const active = Object.values(store.entries).find(entry => (entry.status === 'active' && entry.target === proposal.target && entry.key === proposal.key));
|
|
502
|
+
const prior = Object.values(store.entries).filter(entry => entry.target === proposal.target && entry.key === proposal.key);
|
|
503
|
+
if ((!active && prior.some(entry => canonicalMemoryRecord(entry).lifecycle === 'retired'))
|
|
504
|
+
|| (active && canonicalMemoryRecord(active).placement !== 'resident' && active.entryId !== entryId)) {
|
|
505
|
+
throw new Error('Canonical retired or demoted content requires reviewed maintenance or restore');
|
|
506
|
+
}
|
|
152
507
|
const duplicate = active?.entryId === entryId;
|
|
153
508
|
const conflicts = active && !duplicate
|
|
154
509
|
? [{ entryId: active.entryId, valueDigest: sha256(active.value) }]
|
|
@@ -241,8 +596,9 @@ export class CanonicalContextRepository {
|
|
|
241
596
|
};
|
|
242
597
|
}
|
|
243
598
|
confirm(input) {
|
|
599
|
+
resolveProjectAiwgDirForWrite(this.projectRoot);
|
|
244
600
|
mkdirSync(this.root, { recursive: true });
|
|
245
|
-
|
|
601
|
+
this.openOwnerLock();
|
|
246
602
|
try {
|
|
247
603
|
const requestedReceiptPath = resolve(this.receiptRoot, `${input.preview.operationId.replace(':', '_')}.json`);
|
|
248
604
|
if (existsSync(requestedReceiptPath)) {
|
|
@@ -273,6 +629,12 @@ export class CanonicalContextRepository {
|
|
|
273
629
|
if (!current.duplicate) {
|
|
274
630
|
const active = Object.values(store.entries).find(entry => (entry.status === 'active' && entry.target === proposal.target && entry.key === proposal.key));
|
|
275
631
|
if (active) {
|
|
632
|
+
const record = canonicalMemoryRecord(active);
|
|
633
|
+
record.version += 1;
|
|
634
|
+
record.lifecycle = 'superseded';
|
|
635
|
+
record.tombstone = { id: record.id, digest: record.digest, disposition: 'superseded',
|
|
636
|
+
reviewer: proposal.reviewer, reason: proposal.reason, at: now };
|
|
637
|
+
active.maintenance = maintenanceMetadata(record, active.maintenance?.category, active, now);
|
|
276
638
|
active.status = 'superseded';
|
|
277
639
|
active.updatedAt = now;
|
|
278
640
|
active.disposition = { reviewer: proposal.reviewer, reason: proposal.reason, recordedAt: now };
|
|
@@ -288,12 +650,30 @@ export class CanonicalContextRepository {
|
|
|
288
650
|
disposition: null,
|
|
289
651
|
importedFromWorkspace: null,
|
|
290
652
|
};
|
|
653
|
+
if (active?.maintenance) {
|
|
654
|
+
const successor = canonicalMemoryRecord(store.entries[current.proposedEntryId]);
|
|
655
|
+
const predecessor = canonicalMemoryRecord(active);
|
|
656
|
+
successor.scope = predecessor.scope;
|
|
657
|
+
successor.authority = { ...predecessor.authority };
|
|
658
|
+
successor.placement = predecessor.placement;
|
|
659
|
+
successor.supersedes = [predecessor.id];
|
|
660
|
+
successor.aliases.push(predecessor.id);
|
|
661
|
+
store.entries[current.proposedEntryId].maintenance = maintenanceMetadata(successor, active.maintenance.category);
|
|
662
|
+
if (active.maintenance.tombstone)
|
|
663
|
+
active.maintenance.tombstone.successor = successor.id;
|
|
664
|
+
}
|
|
291
665
|
changed.push(current.proposedEntryId);
|
|
292
666
|
}
|
|
293
667
|
}
|
|
294
668
|
else if (current.operation === 'revoke' && input.revoke) {
|
|
295
669
|
const entry = store.entries[input.revoke.entryId];
|
|
296
670
|
if (!current.duplicate) {
|
|
671
|
+
const record = canonicalMemoryRecord(entry);
|
|
672
|
+
record.version += 1;
|
|
673
|
+
record.lifecycle = 'retired';
|
|
674
|
+
record.tombstone = { id: record.id, digest: record.digest, disposition: 'retired',
|
|
675
|
+
reviewer: input.revoke.reviewer, reason: input.revoke.reason, at: now };
|
|
676
|
+
entry.maintenance = maintenanceMetadata(record, entry.maintenance?.category, entry, now);
|
|
297
677
|
entry.status = 'revoked';
|
|
298
678
|
entry.updatedAt = now;
|
|
299
679
|
entry.disposition = {
|
|
@@ -334,7 +714,6 @@ export class CanonicalContextRepository {
|
|
|
334
714
|
return receipt;
|
|
335
715
|
}
|
|
336
716
|
finally {
|
|
337
|
-
closeSync(lock);
|
|
338
717
|
unlinkSync(this.lockPath);
|
|
339
718
|
}
|
|
340
719
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { refreshMaintenanceIndexes } from './maintenance-index.js';
|
|
2
|
+
import { CanonicalContextRepository, canonicalMemoryRecord } from './canonical-context.js';
|
|
3
|
+
import { sha256 } from './context-pack.js';
|
|
4
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
5
|
+
/** Routes reviewed placement and lifecycle changes through the canonical owner. */
|
|
6
|
+
export class CanonicalMaintenanceOwner {
|
|
7
|
+
name = 'compound-memory';
|
|
8
|
+
repository;
|
|
9
|
+
projectRoot;
|
|
10
|
+
options;
|
|
11
|
+
constructor(projectRoot, options = {}) {
|
|
12
|
+
this.options = options;
|
|
13
|
+
this.projectRoot = projectRoot;
|
|
14
|
+
this.repository = new CanonicalContextRepository(projectRoot);
|
|
15
|
+
}
|
|
16
|
+
async records() {
|
|
17
|
+
return Object.values(this.repository.read().entries).map(canonicalMemoryRecord);
|
|
18
|
+
}
|
|
19
|
+
async snapshot() { return this.repository.snapshot(); }
|
|
20
|
+
async validate(operation) {
|
|
21
|
+
this.repository.previewMaintenance(operation);
|
|
22
|
+
}
|
|
23
|
+
async apply(operation, key) {
|
|
24
|
+
const prior = this.repository.maintenanceReceipt(key, operation);
|
|
25
|
+
if (prior) {
|
|
26
|
+
await this.refreshIndex();
|
|
27
|
+
return prior;
|
|
28
|
+
}
|
|
29
|
+
const preview = this.repository.previewMaintenance(operation);
|
|
30
|
+
const receipt = await this.repository.confirmMaintenance({
|
|
31
|
+
preview, operation, key, verifyRetrieval: operation => this.verifyRetrieval(operation),
|
|
32
|
+
});
|
|
33
|
+
await this.refreshIndex();
|
|
34
|
+
return receipt;
|
|
35
|
+
}
|
|
36
|
+
async receipt(key, options = {}) {
|
|
37
|
+
const receipt = this.repository.maintenanceReceipt(key);
|
|
38
|
+
if (receipt !== undefined && !options.readOnly)
|
|
39
|
+
await this.refreshIndex();
|
|
40
|
+
return receipt;
|
|
41
|
+
}
|
|
42
|
+
async verifyEndState(operation, key) {
|
|
43
|
+
const state = this.repository.read();
|
|
44
|
+
const receipt = this.repository.maintenanceReceipt(key);
|
|
45
|
+
const findings = [];
|
|
46
|
+
if (receipt && (receipt.operation !== 'maintenance'
|
|
47
|
+
|| receipt.operationId !== sha256(JSON.stringify({ operation, snapshot: operation.owner?.snapshot })))) {
|
|
48
|
+
findings.push('Owner receipt does not bind the exact reviewed operation');
|
|
49
|
+
}
|
|
50
|
+
for (const target of [operation.target, ...(operation.related ?? [])]) {
|
|
51
|
+
const entry = Object.values(state.entries).find(item => canonicalMemoryRecord(item).id === target.id);
|
|
52
|
+
if (!entry)
|
|
53
|
+
continue;
|
|
54
|
+
const history = entry.maintenance?.history;
|
|
55
|
+
if (history !== undefined && !Array.isArray(history))
|
|
56
|
+
throw new Error('Invalid canonical maintenance history');
|
|
57
|
+
const prior = history?.find(item => item.record.id === target.id && item.record.version === target.version
|
|
58
|
+
&& item.record.digest === target.digest && item.record.placement === operation.from.placement
|
|
59
|
+
&& item.record.lifecycle === operation.from.lifecycle);
|
|
60
|
+
if (!prior)
|
|
61
|
+
findings.push(`Reviewed source history is missing: ${target.id}`);
|
|
62
|
+
if (operation.category && entry.maintenance?.category !== operation.category)
|
|
63
|
+
findings.push(`Reviewed category differs: ${target.id}`);
|
|
64
|
+
if (prior && !isDeepStrictEqual(prior.record.authority, canonicalMemoryRecord(entry).authority)) {
|
|
65
|
+
findings.push(`Source authority changed: ${target.id}`);
|
|
66
|
+
}
|
|
67
|
+
if (operation.op === 'restore' && !prior?.record.tombstone)
|
|
68
|
+
findings.push(`Recovery tombstone history is missing: ${target.id}`);
|
|
69
|
+
if (canonicalMemoryRecord(entry).lifecycle !== 'active' && !entry.maintenance?.tombstone)
|
|
70
|
+
findings.push(`Retained owner tombstone is missing: ${target.id}`);
|
|
71
|
+
if (receipt && !receipt.entryIds.includes(entry.entryId))
|
|
72
|
+
findings.push(`Receipt omits retained source: ${target.id}`);
|
|
73
|
+
}
|
|
74
|
+
for (const replacement of operation.replacements ?? []) {
|
|
75
|
+
const entry = Object.values(state.entries).find(item => item.key === replacement.key
|
|
76
|
+
&& receipt?.entryIds.includes(item.entryId));
|
|
77
|
+
if (!entry || sha256(entry.value) !== sha256(replacement.value))
|
|
78
|
+
findings.push(`Receipt replacement differs: ${replacement.key}`);
|
|
79
|
+
if (replacement.category && entry?.maintenance?.category !== replacement.category)
|
|
80
|
+
findings.push(`Replacement category differs: ${replacement.key}`);
|
|
81
|
+
}
|
|
82
|
+
return findings;
|
|
83
|
+
}
|
|
84
|
+
async refreshIndex() {
|
|
85
|
+
await refreshMaintenanceIndexes(this.projectRoot);
|
|
86
|
+
}
|
|
87
|
+
async verifyRetrieval(operation) {
|
|
88
|
+
if (operation.owner?.name !== this.name)
|
|
89
|
+
return false;
|
|
90
|
+
if (this.options.verifyRetrieval)
|
|
91
|
+
return this.options.verifyRetrieval(operation);
|
|
92
|
+
throw new Error('compound-memory resident removal is unsupported: no owner-native indexed retrieval projection is available');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=canonical-maintenance.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { AiwgError } from '../cli/errors.js';
|
|
2
|
+
/** Public maintenance command statuses; legacy storage commands retain their contract. */
|
|
3
|
+
export const MEMORY_EXIT_CODES = { success: 0, findings: 1, usage: 2, failed: 3 };
|
|
4
|
+
export const MEMORY_EXIT_USAGE = 'Exit codes: 0 success/no findings/help; 1 findings; 2 usage or malformed input; 3 stale or failed operation.';
|
|
5
|
+
export function memoryUsage(read) {
|
|
6
|
+
try {
|
|
7
|
+
return read();
|
|
8
|
+
}
|
|
9
|
+
catch (cause) {
|
|
10
|
+
throw new AiwgError({ code: 'ERR_USAGE_MEMORY', exitCode: MEMORY_EXIT_CODES.usage,
|
|
11
|
+
message: cause instanceof Error ? cause.message : String(cause), cause });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export async function memoryInput(read) {
|
|
15
|
+
try {
|
|
16
|
+
return await read();
|
|
17
|
+
}
|
|
18
|
+
catch (cause) {
|
|
19
|
+
return memoryUsage(() => { throw cause; });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=cli-result.js.map
|