@dzhechkov/harness-core 0.8.6 → 0.8.11
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/.dz-manifest.json +361 -97
- package/README.md +48 -1
- package/dist/amendment-trace.d.ts.map +1 -1
- package/dist/amendment-trace.js +12 -1
- package/dist/amendment-trace.js.map +1 -1
- package/dist/codex-invoke.d.ts +73 -0
- package/dist/codex-invoke.d.ts.map +1 -0
- package/dist/codex-invoke.js +80 -0
- package/dist/codex-invoke.js.map +1 -0
- package/dist/compounding.d.ts +54 -0
- package/dist/compounding.d.ts.map +1 -1
- package/dist/compounding.js +221 -1
- package/dist/compounding.js.map +1 -1
- package/dist/discrimination-gate.d.ts +63 -3
- package/dist/discrimination-gate.d.ts.map +1 -1
- package/dist/discrimination-gate.js +113 -16
- package/dist/discrimination-gate.js.map +1 -1
- package/dist/eta.d.ts +92 -0
- package/dist/eta.d.ts.map +1 -0
- package/dist/eta.js +488 -0
- package/dist/eta.js.map +1 -0
- package/dist/event-chain.d.ts +30 -0
- package/dist/event-chain.d.ts.map +1 -1
- package/dist/event-chain.js +24 -0
- package/dist/event-chain.js.map +1 -1
- package/dist/feature-adr-checkpoints.js +1 -1
- package/dist/feature-adr-decision-recall.d.ts +167 -0
- package/dist/feature-adr-decision-recall.d.ts.map +1 -0
- package/dist/feature-adr-decision-recall.js +519 -0
- package/dist/feature-adr-decision-recall.js.map +1 -0
- package/dist/feature-adr-landing.d.ts +37 -0
- package/dist/feature-adr-landing.d.ts.map +1 -0
- package/dist/feature-adr-landing.js +59 -0
- package/dist/feature-adr-landing.js.map +1 -0
- package/dist/feature-adr-routing.d.ts +2 -2
- package/dist/feature-adr-routing.d.ts.map +1 -1
- package/dist/feature-adr-routing.js +7 -11
- package/dist/feature-adr-routing.js.map +1 -1
- package/dist/guard-promotion.d.ts +41 -0
- package/dist/guard-promotion.d.ts.map +1 -1
- package/dist/guard-promotion.js +218 -4
- package/dist/guard-promotion.js.map +1 -1
- package/dist/guard-volume.d.ts +108 -0
- package/dist/guard-volume.d.ts.map +1 -0
- package/dist/guard-volume.js +536 -0
- package/dist/guard-volume.js.map +1 -0
- package/dist/guard.d.ts +17 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +92 -4
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +24 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -5
- package/dist/index.js.map +1 -1
- package/dist/integration-apply.d.ts +25 -0
- package/dist/integration-apply.d.ts.map +1 -0
- package/dist/integration-apply.js +299 -0
- package/dist/integration-apply.js.map +1 -0
- package/dist/integration-evidence.d.ts +46 -0
- package/dist/integration-evidence.d.ts.map +1 -0
- package/dist/integration-evidence.js +44 -0
- package/dist/integration-evidence.js.map +1 -0
- package/dist/integration-probe-worker.d.ts +22 -0
- package/dist/integration-probe-worker.d.ts.map +1 -0
- package/dist/integration-probe-worker.js +334 -0
- package/dist/integration-probe-worker.js.map +1 -0
- package/dist/integrations-verify.d.ts +60 -0
- package/dist/integrations-verify.d.ts.map +1 -0
- package/dist/integrations-verify.js +194 -0
- package/dist/integrations-verify.js.map +1 -0
- package/dist/lesson-generalization.d.ts +29 -0
- package/dist/lesson-generalization.d.ts.map +1 -0
- package/dist/lesson-generalization.js +84 -0
- package/dist/lesson-generalization.js.map +1 -0
- package/dist/mutation-gate.d.ts +39 -36
- package/dist/mutation-gate.d.ts.map +1 -1
- package/dist/mutation-gate.js +111 -5
- package/dist/mutation-gate.js.map +1 -1
- package/dist/operations.d.ts +27 -0
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +186 -7
- package/dist/operations.js.map +1 -1
- package/dist/patterns.d.ts +27 -1
- package/dist/patterns.d.ts.map +1 -1
- package/dist/patterns.js +211 -45
- package/dist/patterns.js.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +27 -5
- package/dist/plugin.js.map +1 -1
- package/dist/recommend.d.ts +4 -5
- package/dist/recommend.d.ts.map +1 -1
- package/dist/recommend.js +110 -45
- package/dist/recommend.js.map +1 -1
- package/dist/registry.d.ts +32 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +165 -9
- package/dist/registry.js.map +1 -1
- package/dist/run-records.d.ts +3 -0
- package/dist/run-records.d.ts.map +1 -1
- package/dist/run-records.js +18 -0
- package/dist/run-records.js.map +1 -1
- package/dist/score.d.ts +95 -0
- package/dist/score.d.ts.map +1 -1
- package/dist/score.js +274 -2
- package/dist/score.js.map +1 -1
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +20 -17
- package/dist/setup.js.map +1 -1
- package/dist/skill-selection.d.ts +72 -0
- package/dist/skill-selection.d.ts.map +1 -0
- package/dist/skill-selection.js +76 -0
- package/dist/skill-selection.js.map +1 -0
- package/dist/stem.d.ts +12 -0
- package/dist/stem.d.ts.map +1 -0
- package/dist/stem.js +89 -0
- package/dist/stem.js.map +1 -0
- package/dist/target-integrations.d.ts +65 -0
- package/dist/target-integrations.d.ts.map +1 -0
- package/dist/target-integrations.js +152 -0
- package/dist/target-integrations.js.map +1 -0
- package/dist/telemetry-vocabulary.d.ts +7 -0
- package/dist/telemetry-vocabulary.d.ts.map +1 -1
- package/dist/telemetry-vocabulary.js +29 -0
- package/dist/telemetry-vocabulary.js.map +1 -1
- package/dist/vector-tier.d.ts +6 -1
- package/dist/vector-tier.d.ts.map +1 -1
- package/dist/vector-tier.js +32 -7
- package/dist/vector-tier.js.map +1 -1
- package/package.json +7 -6
- package/sbom.json +772 -112
- package/src/amendment-trace.ts +12 -1
- package/src/codex-invoke.ts +138 -0
- package/src/compounding.ts +300 -1
- package/src/discrimination-gate.ts +183 -19
- package/src/eta.ts +590 -0
- package/src/event-chain.ts +41 -0
- package/src/feature-adr-checkpoints.ts +1 -1
- package/src/feature-adr-decision-recall.ts +652 -0
- package/src/feature-adr-landing.ts +109 -0
- package/src/feature-adr-routing.ts +7 -11
- package/src/guard-promotion.ts +245 -4
- package/src/guard-volume.ts +752 -0
- package/src/guard.ts +110 -4
- package/src/index.ts +73 -6
- package/src/integration-apply.ts +332 -0
- package/src/integration-evidence.ts +89 -0
- package/src/integration-probe-worker.ts +310 -0
- package/src/integration-receipts/claude-code/mcp/2.1.235.json +35 -0
- package/src/integrations-verify.ts +258 -0
- package/src/lesson-generalization.ts +115 -0
- package/src/mutation-gate.ts +165 -5
- package/src/operations.ts +207 -7
- package/src/patterns.ts +252 -43
- package/src/plugin.ts +27 -5
- package/src/recommend.ts +116 -46
- package/src/registry.ts +144 -11
- package/src/run-records.ts +23 -0
- package/src/score.ts +361 -3
- package/src/setup.ts +20 -17
- package/src/skill-selection.ts +111 -0
- package/src/stem.ts +87 -0
- package/src/target-integrations.ts +225 -0
- package/src/telemetry-vocabulary.ts +36 -0
- package/src/vector-tier.ts +44 -14
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/** Journaled, ownership-aware project carrier transaction. */
|
|
2
|
+
|
|
3
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
4
|
+
import {
|
|
5
|
+
closeSync,
|
|
6
|
+
constants,
|
|
7
|
+
existsSync,
|
|
8
|
+
fsyncSync,
|
|
9
|
+
lstatSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
openSync,
|
|
12
|
+
readFileSync,
|
|
13
|
+
realpathSync,
|
|
14
|
+
renameSync,
|
|
15
|
+
statSync,
|
|
16
|
+
unlinkSync,
|
|
17
|
+
writeFileSync,
|
|
18
|
+
} from 'node:fs';
|
|
19
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
20
|
+
|
|
21
|
+
import { canonicalIntegrationJson, parseStrictJson, type CarrierFragment, type IntegrationReasonCode } from '@dzhechkov/core';
|
|
22
|
+
|
|
23
|
+
import { withNamedLockSync } from './named-lock.js';
|
|
24
|
+
|
|
25
|
+
const MAX_CARRIER_BYTES = 1024 * 1024;
|
|
26
|
+
const JOURNAL_REL = '.dz/integrations-ownership.json';
|
|
27
|
+
|
|
28
|
+
type JournalState = 'pending' | 'committed';
|
|
29
|
+
|
|
30
|
+
interface JournalEntry {
|
|
31
|
+
readonly state: JournalState;
|
|
32
|
+
readonly carrierPath: string;
|
|
33
|
+
readonly entryId: string;
|
|
34
|
+
readonly previousCarrierDigest: string;
|
|
35
|
+
readonly desiredCarrierDigest: string;
|
|
36
|
+
readonly desiredContentHash: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface JournalV1 {
|
|
40
|
+
readonly version: 1;
|
|
41
|
+
readonly entries: Record<string, JournalEntry>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type IntegrationApplyFault = 'after-pending' | 'after-carrier' | 'after-committed';
|
|
45
|
+
|
|
46
|
+
export interface ApplyIntegrationOptions {
|
|
47
|
+
readonly projectRoot: string;
|
|
48
|
+
readonly fragments: readonly CarrierFragment[];
|
|
49
|
+
/** Test-only fault seam; production callers omit it. */
|
|
50
|
+
readonly injectFault?: IntegrationApplyFault;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface IntegrationApplyReport {
|
|
54
|
+
readonly written: readonly string[];
|
|
55
|
+
readonly alreadyCurrent: readonly string[];
|
|
56
|
+
readonly journalPath: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class IntegrationApplyError extends Error {
|
|
60
|
+
constructor(
|
|
61
|
+
readonly reasonCode: IntegrationReasonCode,
|
|
62
|
+
message: string,
|
|
63
|
+
/** Carrier bytes may already be durable while ownership remains unverified. */
|
|
64
|
+
readonly applied = false,
|
|
65
|
+
) {
|
|
66
|
+
super(message);
|
|
67
|
+
this.name = 'IntegrationApplyError';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function sha(value: string | Buffer): string {
|
|
72
|
+
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function absentOrDigest(bytes: string | undefined): string {
|
|
76
|
+
return bytes === undefined ? 'sha256:absent' : sha(bytes);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function assertPlainObject(value: unknown, label: string): Record<string, unknown> {
|
|
80
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
81
|
+
throw new IntegrationApplyError('CONFIG_MALFORMED', `${label} must contain a JSON object`);
|
|
82
|
+
}
|
|
83
|
+
return value as Record<string, unknown>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function readBoundedJson(path: string, label: string): { bytes: string; value: Record<string, unknown> } | undefined {
|
|
87
|
+
if (!existsSync(path)) return undefined;
|
|
88
|
+
const st = lstatSync(path);
|
|
89
|
+
if (st.isSymbolicLink() || !st.isFile()) throw new IntegrationApplyError('UNSAFE_PATH', `${label} must be a regular non-symlink file`);
|
|
90
|
+
if (st.size > MAX_CARRIER_BYTES) throw new IntegrationApplyError('CONFIG_MALFORMED', `${label} exceeds ${MAX_CARRIER_BYTES} bytes`);
|
|
91
|
+
const bytes = readFileSync(path, 'utf8');
|
|
92
|
+
try {
|
|
93
|
+
return { bytes, value: assertPlainObject(parseStrictJson(bytes, { label, maxBytes: MAX_CARRIER_BYTES }), label) };
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error instanceof IntegrationApplyError) throw error;
|
|
96
|
+
throw new IntegrationApplyError('CONFIG_MALFORMED', error instanceof Error ? error.message : String(error));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function journalFrom(value: Record<string, unknown>): JournalV1 {
|
|
101
|
+
if (value['version'] !== 1) throw new IntegrationApplyError('CONFIG_MALFORMED', 'integration journal version must be 1');
|
|
102
|
+
const rawEntries = assertPlainObject(value['entries'], 'integration journal entries');
|
|
103
|
+
const entries = Object.create(null) as Record<string, JournalEntry>;
|
|
104
|
+
for (const [key, raw] of Object.entries(rawEntries)) {
|
|
105
|
+
const row = assertPlainObject(raw, `integration journal entry ${key}`);
|
|
106
|
+
const state = row['state'];
|
|
107
|
+
if (state !== 'pending' && state !== 'committed') throw new IntegrationApplyError('CONFIG_MALFORMED', `integration journal entry ${key} has invalid state`);
|
|
108
|
+
for (const field of ['carrierPath', 'entryId', 'previousCarrierDigest', 'desiredCarrierDigest', 'desiredContentHash'] as const) {
|
|
109
|
+
if (typeof row[field] !== 'string') throw new IntegrationApplyError('CONFIG_MALFORMED', `integration journal entry ${key} lacks ${field}`);
|
|
110
|
+
}
|
|
111
|
+
entries[key] = {
|
|
112
|
+
state,
|
|
113
|
+
carrierPath: row['carrierPath'] as string,
|
|
114
|
+
entryId: row['entryId'] as string,
|
|
115
|
+
previousCarrierDigest: row['previousCarrierDigest'] as string,
|
|
116
|
+
desiredCarrierDigest: row['desiredCarrierDigest'] as string,
|
|
117
|
+
desiredContentHash: row['desiredContentHash'] as string,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return { version: 1, entries };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function ensureSafeProjectRoot(projectRoot: string): string {
|
|
124
|
+
mkdirSync(projectRoot, { recursive: true });
|
|
125
|
+
const lexical = resolve(projectRoot);
|
|
126
|
+
const actual = realpathSync(projectRoot);
|
|
127
|
+
if (lexical !== actual || lstatSync(projectRoot).isSymbolicLink()) {
|
|
128
|
+
throw new IntegrationApplyError('UNSAFE_PATH', 'project root must be a real non-symlink directory');
|
|
129
|
+
}
|
|
130
|
+
const dz = join(actual, '.dz');
|
|
131
|
+
if (!existsSync(dz)) mkdirSync(dz, { mode: 0o700 });
|
|
132
|
+
const dzStat = lstatSync(dz);
|
|
133
|
+
if (dzStat.isSymbolicLink() || !dzStat.isDirectory() || realpathSync(dz) !== dz) {
|
|
134
|
+
throw new IntegrationApplyError('UNSAFE_PATH', '.dz must be a real non-symlink directory');
|
|
135
|
+
}
|
|
136
|
+
return actual;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function confinedPath(root: string, relativePath: string): string {
|
|
140
|
+
if (relativePath.startsWith('/') || relativePath.split('/').includes('..')) {
|
|
141
|
+
throw new IntegrationApplyError('UNSAFE_PATH', `unsafe carrier path ${JSON.stringify(relativePath)}`);
|
|
142
|
+
}
|
|
143
|
+
const absolute = resolve(root, ...relativePath.split('/'));
|
|
144
|
+
const rel = relative(root, absolute);
|
|
145
|
+
if (rel === '..' || rel.startsWith(`..${sep}`) || resolve(root, rel) !== absolute) {
|
|
146
|
+
throw new IntegrationApplyError('UNSAFE_PATH', `carrier escapes project root: ${relativePath}`);
|
|
147
|
+
}
|
|
148
|
+
const parent = dirname(absolute);
|
|
149
|
+
if (parent !== root) {
|
|
150
|
+
mkdirSync(parent, { recursive: true });
|
|
151
|
+
let walk = root;
|
|
152
|
+
for (const part of relative(root, parent).split(sep).filter(Boolean)) {
|
|
153
|
+
walk = join(walk, part);
|
|
154
|
+
const st = lstatSync(walk);
|
|
155
|
+
if (st.isSymbolicLink() || !st.isDirectory()) throw new IntegrationApplyError('UNSAFE_PATH', `carrier parent is not a real directory: ${relativePath}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (existsSync(absolute) && lstatSync(absolute).isSymbolicLink()) {
|
|
159
|
+
throw new IntegrationApplyError('UNSAFE_PATH', `carrier must not be a symlink: ${relativePath}`);
|
|
160
|
+
}
|
|
161
|
+
return absolute;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function fsyncDirectory(path: string): void {
|
|
165
|
+
let fd: number | undefined;
|
|
166
|
+
try {
|
|
167
|
+
fd = openSync(path, constants.O_RDONLY);
|
|
168
|
+
fsyncSync(fd);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
171
|
+
if (!['EINVAL', 'ENOTSUP', 'EISDIR', 'EPERM'].includes(code ?? '')) throw error;
|
|
172
|
+
} finally {
|
|
173
|
+
if (fd !== undefined) closeSync(fd);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function durableReplace(path: string, bytes: string): void {
|
|
178
|
+
const parent = dirname(path);
|
|
179
|
+
const temp = join(parent, `.${relative(parent, path)}.dz-${process.pid}-${randomBytes(8).toString('hex')}.tmp`);
|
|
180
|
+
let fd: number | undefined;
|
|
181
|
+
try {
|
|
182
|
+
const noFollow = typeof constants.O_NOFOLLOW === 'number' ? constants.O_NOFOLLOW : 0;
|
|
183
|
+
fd = openSync(temp, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | noFollow, 0o600);
|
|
184
|
+
writeFileSync(fd, bytes, 'utf8');
|
|
185
|
+
fsyncSync(fd);
|
|
186
|
+
closeSync(fd); fd = undefined;
|
|
187
|
+
renameSync(temp, path);
|
|
188
|
+
const reread = readFileSync(path, 'utf8');
|
|
189
|
+
if (reread !== bytes) throw new IntegrationApplyError('APPLY_FAILED', `durable reread mismatch for ${path}`);
|
|
190
|
+
fsyncDirectory(parent);
|
|
191
|
+
} finally {
|
|
192
|
+
if (fd !== undefined) closeSync(fd);
|
|
193
|
+
if (existsSync(temp)) unlinkSync(temp);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function entryHash(value: unknown): string {
|
|
198
|
+
return sha(canonicalIntegrationJson(value));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function isKnownAgentDbLegacy(id: string, value: unknown, root: string): boolean {
|
|
202
|
+
if (id !== 'mcpServers.agentdb' || value === null || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
203
|
+
const row = value as Record<string, unknown>;
|
|
204
|
+
if (!Object.keys(row).every((key) => ['command', 'args', 'env'].includes(key))) return false;
|
|
205
|
+
if (row['command'] !== 'npx' || !Array.isArray(row['args'])) return false;
|
|
206
|
+
const args = row['args'];
|
|
207
|
+
if (args.length !== 3 || typeof args[0] !== 'string' || !/^agentdb@(?:latest|[^\s/]+)$/.test(args[0]) || args[1] !== 'mcp' || args[2] !== 'start') return false;
|
|
208
|
+
if (row['env'] === undefined) return true;
|
|
209
|
+
const env = row['env'];
|
|
210
|
+
if (env === null || typeof env !== 'object' || Array.isArray(env) || Object.keys(env).length !== 1) return false;
|
|
211
|
+
const path = (env as Record<string, unknown>)['AGENTDB_PATH'];
|
|
212
|
+
return path === join(root, '.dz', 'agentdb.db') || path === join(root, '.dz', 'agentdb-mcp.db');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Apply all project JSON fragments under one short project-journal lock. */
|
|
216
|
+
export function applyIntegrationFragments(options: ApplyIntegrationOptions): IntegrationApplyReport {
|
|
217
|
+
const root = ensureSafeProjectRoot(options.projectRoot);
|
|
218
|
+
const journalPath = confinedPath(root, JOURNAL_REL);
|
|
219
|
+
if (options.fragments.some((fragment) => fragment.scope !== 'project' || fragment.format !== 'json')) {
|
|
220
|
+
throw new IntegrationApplyError('INTENT_NOT_EXPRESSIBLE', 'initial transaction supports project JSON carriers only');
|
|
221
|
+
}
|
|
222
|
+
const groups = new Map<string, CarrierFragment[]>();
|
|
223
|
+
for (const fragment of options.fragments) {
|
|
224
|
+
const rows = groups.get(fragment.carrierPath) ?? [];
|
|
225
|
+
rows.push(fragment); groups.set(fragment.carrierPath, rows);
|
|
226
|
+
}
|
|
227
|
+
if (groups.size === 0) return { written: [], alreadyCurrent: [], journalPath: JOURNAL_REL };
|
|
228
|
+
|
|
229
|
+
return withNamedLockSync(root, 'integrations', () => {
|
|
230
|
+
const rootBefore = statSync(root);
|
|
231
|
+
const dzBefore = statSync(join(root, '.dz'));
|
|
232
|
+
const journalRead = readBoundedJson(journalPath, JOURNAL_REL);
|
|
233
|
+
const journal: JournalV1 = journalRead === undefined ? { version: 1, entries: Object.create(null) } : journalFrom(journalRead.value);
|
|
234
|
+
const candidates: { rel: string; abs: string; before: string | undefined; bytes: string; tuples: string[] }[] = [];
|
|
235
|
+
const pendingEntries: Record<string, JournalEntry> = { ...journal.entries };
|
|
236
|
+
|
|
237
|
+
for (const [carrierRel, fragments] of [...groups.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
238
|
+
const carrierAbs = confinedPath(root, carrierRel);
|
|
239
|
+
const existing = readBoundedJson(carrierAbs, carrierRel);
|
|
240
|
+
const rootObject = existing?.value ?? Object.create(null) as Record<string, unknown>;
|
|
241
|
+
const candidate = { ...rootObject } as Record<string, unknown>;
|
|
242
|
+
const tuples: string[] = [];
|
|
243
|
+
for (const fragment of fragments) {
|
|
244
|
+
const rootValue = candidate[fragment.rootKey];
|
|
245
|
+
const bucket = rootValue === undefined ? Object.create(null) as Record<string, unknown> : assertPlainObject(rootValue, `${carrierRel}.${fragment.rootKey}`);
|
|
246
|
+
const nextBucket = { ...bucket };
|
|
247
|
+
for (const [id, desired] of Object.entries(fragment.entries)) {
|
|
248
|
+
const tuple = `${carrierRel}::${fragment.rootKey}.${id}`;
|
|
249
|
+
const current = bucket[id];
|
|
250
|
+
const ledger = journal.entries[tuple];
|
|
251
|
+
const desiredHash = entryHash(desired);
|
|
252
|
+
if (current !== undefined) {
|
|
253
|
+
const currentHash = entryHash(current);
|
|
254
|
+
const recoverablePending = ledger?.state === 'pending' &&
|
|
255
|
+
currentHash === ledger.desiredContentHash &&
|
|
256
|
+
desiredHash === ledger.desiredContentHash;
|
|
257
|
+
const committedCurrent = ledger?.state === 'committed' && currentHash === ledger.desiredContentHash;
|
|
258
|
+
const legacy = isKnownAgentDbLegacy(`${fragment.rootKey}.${id}`, current, root);
|
|
259
|
+
if (!recoverablePending && !committedCurrent && !legacy) {
|
|
260
|
+
throw new IntegrationApplyError('OWNERSHIP_AMBIGUOUS', `${tuple} already exists without matching dz ownership; original bytes preserved`);
|
|
261
|
+
}
|
|
262
|
+
} else if (ledger?.state === 'pending') {
|
|
263
|
+
// Explicitly recoverable: pending publication may have stopped before carrier rename.
|
|
264
|
+
} else if (ledger?.state === 'committed') {
|
|
265
|
+
throw new IntegrationApplyError('OWNERSHIP_AMBIGUOUS', `${tuple} is absent but journal is committed; refuse implicit recreation`);
|
|
266
|
+
}
|
|
267
|
+
nextBucket[id] = desired;
|
|
268
|
+
tuples.push(tuple);
|
|
269
|
+
pendingEntries[tuple] = {
|
|
270
|
+
state: 'pending',
|
|
271
|
+
carrierPath: carrierRel,
|
|
272
|
+
entryId: `${fragment.rootKey}.${id}`,
|
|
273
|
+
previousCarrierDigest: absentOrDigest(existing?.bytes),
|
|
274
|
+
desiredCarrierDigest: '',
|
|
275
|
+
desiredContentHash: desiredHash,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
candidate[fragment.rootKey] = nextBucket;
|
|
279
|
+
}
|
|
280
|
+
const bytes = `${JSON.stringify(candidate, null, 2)}\n`;
|
|
281
|
+
const desiredCarrierDigest = sha(bytes);
|
|
282
|
+
for (const tuple of tuples) pendingEntries[tuple] = { ...pendingEntries[tuple]!, desiredCarrierDigest };
|
|
283
|
+
candidates.push({ rel: carrierRel, abs: carrierAbs, before: existing?.bytes, bytes, tuples });
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (statSync(root).dev !== rootBefore.dev || statSync(root).ino !== rootBefore.ino ||
|
|
287
|
+
statSync(join(root, '.dz')).dev !== dzBefore.dev || statSync(join(root, '.dz')).ino !== dzBefore.ino) {
|
|
288
|
+
throw new IntegrationApplyError('CONCURRENT_MODIFICATION', 'project root or .dz inode changed during planning');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const pendingBytes = `${JSON.stringify({ version: 1, entries: pendingEntries }, null, 2)}\n`;
|
|
292
|
+
durableReplace(journalPath, pendingBytes);
|
|
293
|
+
if (options.injectFault === 'after-pending') throw new IntegrationApplyError('APPLY_FAILED', 'injected fault after pending journal publication');
|
|
294
|
+
|
|
295
|
+
const written: string[] = [];
|
|
296
|
+
const alreadyCurrent: string[] = [];
|
|
297
|
+
let carrierPublished = false;
|
|
298
|
+
try {
|
|
299
|
+
for (const candidate of candidates) {
|
|
300
|
+
const current = readBoundedJson(candidate.abs, candidate.rel)?.bytes;
|
|
301
|
+
if (current !== candidate.before) throw new IntegrationApplyError('CONCURRENT_MODIFICATION', `${candidate.rel} changed after planning`);
|
|
302
|
+
if (current === candidate.bytes) alreadyCurrent.push(candidate.rel);
|
|
303
|
+
else {
|
|
304
|
+
durableReplace(candidate.abs, candidate.bytes);
|
|
305
|
+
written.push(candidate.rel);
|
|
306
|
+
carrierPublished = true;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (options.injectFault === 'after-carrier') {
|
|
310
|
+
throw new IntegrationApplyError('APPLY_FAILED', 'carrier was written but committed ownership journal was not verified', true);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const committedEntries: Record<string, JournalEntry> = { ...pendingEntries };
|
|
314
|
+
for (const candidate of candidates) {
|
|
315
|
+
for (const tuple of candidate.tuples) committedEntries[tuple] = { ...committedEntries[tuple]!, state: 'committed' };
|
|
316
|
+
}
|
|
317
|
+
durableReplace(journalPath, `${JSON.stringify({ version: 1, entries: committedEntries }, null, 2)}\n`);
|
|
318
|
+
if (options.injectFault === 'after-committed') throw new IntegrationApplyError('APPLY_FAILED', 'injected fault after committed journal publication', true);
|
|
319
|
+
return { written, alreadyCurrent, journalPath: JOURNAL_REL };
|
|
320
|
+
} catch (error) {
|
|
321
|
+
if (error instanceof IntegrationApplyError && error.applied) throw error;
|
|
322
|
+
if (carrierPublished) {
|
|
323
|
+
throw new IntegrationApplyError(
|
|
324
|
+
'APPLY_FAILED',
|
|
325
|
+
`carrier was written but committed ownership journal was not verified: ${error instanceof Error ? error.message : String(error)}`,
|
|
326
|
+
true,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
throw error;
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/** Exact-version receipt and evidence classifiers. */
|
|
2
|
+
|
|
3
|
+
import type { IntegrationComponent, IntegrationReasonCode } from '@dzhechkov/core';
|
|
4
|
+
|
|
5
|
+
import type { TargetName } from './targets.js';
|
|
6
|
+
|
|
7
|
+
export interface ProbeReceipt {
|
|
8
|
+
readonly schemaVersion: 1;
|
|
9
|
+
readonly target: TargetName;
|
|
10
|
+
readonly component: IntegrationComponent;
|
|
11
|
+
readonly productSurface: string;
|
|
12
|
+
readonly runtimeVersion: string;
|
|
13
|
+
readonly scope: 'project' | 'user' | 'plugin';
|
|
14
|
+
readonly carrierPath: string;
|
|
15
|
+
readonly serializerId: string;
|
|
16
|
+
readonly probeCommandId: string;
|
|
17
|
+
readonly probeCommand: readonly string[];
|
|
18
|
+
readonly timeoutMs: number;
|
|
19
|
+
readonly observedExitCode: number;
|
|
20
|
+
readonly transcriptSha256: `sha256:${string}`;
|
|
21
|
+
readonly parsedObservation: {
|
|
22
|
+
readonly registrationId: string;
|
|
23
|
+
readonly scope: 'project';
|
|
24
|
+
readonly status: string;
|
|
25
|
+
readonly approval: 'pending' | 'approved' | 'unknown';
|
|
26
|
+
readonly ready: boolean;
|
|
27
|
+
};
|
|
28
|
+
readonly wrongPathNegativeControl: {
|
|
29
|
+
readonly carrierPath: string;
|
|
30
|
+
readonly probeCommand: readonly string[];
|
|
31
|
+
readonly timeoutMs: number;
|
|
32
|
+
readonly observedExitCode: number;
|
|
33
|
+
readonly transcriptSha256: `sha256:${string}`;
|
|
34
|
+
readonly registrationObserved: false;
|
|
35
|
+
readonly reasonCode: 'POST_WRITE_REGISTRATION_NOT_OBSERVED';
|
|
36
|
+
};
|
|
37
|
+
readonly transportFamily: 'stdio-local' | 'http';
|
|
38
|
+
readonly fieldFamilies: readonly string[];
|
|
39
|
+
readonly observedAt: string;
|
|
40
|
+
readonly documentation: readonly string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const CLAUDE_MCP_RECEIPT: ProbeReceipt = {
|
|
44
|
+
schemaVersion: 1,
|
|
45
|
+
target: 'claude-code',
|
|
46
|
+
component: 'mcp',
|
|
47
|
+
productSurface: 'claude-code-cli',
|
|
48
|
+
runtimeVersion: '2.1.235',
|
|
49
|
+
scope: 'project',
|
|
50
|
+
carrierPath: '.mcp.json',
|
|
51
|
+
serializerId: 'claude-project-mcp-json-v1',
|
|
52
|
+
probeCommandId: 'claude-project-mcp-get-v1',
|
|
53
|
+
probeCommand: ['claude', 'mcp', 'get', 'dz-registration-probe'],
|
|
54
|
+
timeoutMs: 30_000,
|
|
55
|
+
observedExitCode: 0,
|
|
56
|
+
transcriptSha256: 'sha256:0e30c79de6381c7e7b93a39cc644cb282b6c95ceaeb4a0f5a94fbfecb5c71549',
|
|
57
|
+
parsedObservation: {
|
|
58
|
+
registrationId: 'dz-registration-probe',
|
|
59
|
+
scope: 'project',
|
|
60
|
+
status: 'Pending approval',
|
|
61
|
+
approval: 'pending',
|
|
62
|
+
ready: false,
|
|
63
|
+
},
|
|
64
|
+
wrongPathNegativeControl: {
|
|
65
|
+
carrierPath: '.claude/.mcp.json',
|
|
66
|
+
probeCommand: ['claude', 'mcp', 'get', 'dz-wrong-path-probe'],
|
|
67
|
+
timeoutMs: 30_000,
|
|
68
|
+
observedExitCode: 1,
|
|
69
|
+
transcriptSha256: 'sha256:aba424ed140ee785947f314942566ae4bccc7b7c44ca9be785efbf36be0f62d5',
|
|
70
|
+
registrationObserved: false,
|
|
71
|
+
reasonCode: 'POST_WRITE_REGISTRATION_NOT_OBSERVED',
|
|
72
|
+
},
|
|
73
|
+
transportFamily: 'stdio-local',
|
|
74
|
+
fieldFamilies: ['type', 'command', 'args'],
|
|
75
|
+
observedAt: '2026-08-30T19:22:15.366Z',
|
|
76
|
+
documentation: ['https://code.claude.com/docs/en/mcp'],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export interface EvidenceAssessment {
|
|
80
|
+
readonly eligible: boolean;
|
|
81
|
+
readonly reasonCode?: IntegrationReasonCode;
|
|
82
|
+
readonly receipt?: ProbeReceipt;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function assessClaudeMcpEvidence(runtimeVersion: string | undefined): EvidenceAssessment {
|
|
86
|
+
if (runtimeVersion === undefined) return { eligible: false, reasonCode: 'TARGET_BINARY_UNAVAILABLE' };
|
|
87
|
+
if (runtimeVersion !== CLAUDE_MCP_RECEIPT.runtimeVersion) return { eligible: false, reasonCode: 'RECEIPT_STALE' };
|
|
88
|
+
return { eligible: true, receipt: CLAUDE_MCP_RECEIPT };
|
|
89
|
+
}
|