@openclawbrain/cli 0.4.34 → 0.4.36
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/dist/src/cli.d.ts +69 -1
- package/dist/src/cli.js +817 -4
- package/dist/src/graphify-compiled-artifacts.d.ts +127 -0
- package/dist/src/graphify-compiled-artifacts.js +1185 -0
- package/dist/src/graphify-import-slice.js +1091 -0
- package/dist/src/graphify-lints.js +977 -0
- package/dist/src/graphify-maintenance-diff.d.ts +167 -0
- package/dist/src/graphify-maintenance-diff.js +1288 -0
- package/dist/src/graphify-runner.js +745 -0
- package/dist/src/import-export.d.ts +127 -0
- package/dist/src/import-export.js +938 -26
- package/dist/src/index.js +4 -2
- package/dist/src/session-store.js +37 -0
- package/dist/src/session-tail.js +111 -2
- package/package.json +9 -9
|
@@ -0,0 +1,1288 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
+
import { canonicalJson } from "@openclawbrain/contracts";
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
const defaultRepoRoot = path.resolve(__dirname, "../../../..");
|
|
12
|
+
const defaultWorkspaceRoot = path.resolve(defaultRepoRoot, "..");
|
|
13
|
+
const defaultGraphifyRoot = path.join(defaultRepoRoot, "artifacts");
|
|
14
|
+
const defaultOcbRoot = defaultRepoRoot;
|
|
15
|
+
const defaultOutputRoot = path.join(defaultWorkspaceRoot, "artifacts", "graphify-maintenance-diff");
|
|
16
|
+
|
|
17
|
+
export const GRAPHIFY_MAINTENANCE_DIFF_LAYOUT_V1 = {
|
|
18
|
+
maintenanceDiff: "maintenance-diff.json",
|
|
19
|
+
summary: "summary.md",
|
|
20
|
+
proposalSuggestion: "proposal-suggestion.json",
|
|
21
|
+
verdict: "verdict.json",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function stableJson(value) {
|
|
25
|
+
return canonicalJson(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sha256Text(text) {
|
|
29
|
+
return `sha256:${createHash("sha256").update(String(text ?? ""), "utf8").digest("hex")}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function ensureDir(dirPath) {
|
|
33
|
+
mkdirSync(dirPath, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function writeJson(filePath, value) {
|
|
37
|
+
writeFileSync(filePath, `${stableJson(value)}\n`, "utf8");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function writeText(filePath, value) {
|
|
41
|
+
writeFileSync(filePath, `${value}\n`, "utf8");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readJsonIfExists(filePath) {
|
|
45
|
+
if (!existsSync(filePath)) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readTextIfExists(filePath) {
|
|
52
|
+
return existsSync(filePath) ? readFileSync(filePath, "utf8") : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizeText(value) {
|
|
56
|
+
if (typeof value !== "string") {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const trimmed = value.trim();
|
|
60
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function slugify(value) {
|
|
64
|
+
return String(value ?? "")
|
|
65
|
+
.toLowerCase()
|
|
66
|
+
.replace(/[^a-z0-9]+/gu, "-")
|
|
67
|
+
.replace(/^-+|-+$/gu, "")
|
|
68
|
+
.replace(/-{2,}/gu, "-") || "bundle";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function timestampToken(value = new Date().toISOString()) {
|
|
72
|
+
return String(value).replace(/[:]/g, "-");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function relativeWorkspacePath(absPath, workspaceRoot) {
|
|
76
|
+
const resolvedPath = path.resolve(absPath);
|
|
77
|
+
const relative = path.relative(workspaceRoot, resolvedPath);
|
|
78
|
+
return relative.startsWith("..") ? resolvedPath : relative.replace(/\\/g, "/");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function uniqueBy(items, keyFn) {
|
|
82
|
+
const seen = new Set();
|
|
83
|
+
const result = [];
|
|
84
|
+
for (const item of items) {
|
|
85
|
+
const key = keyFn(item);
|
|
86
|
+
if (seen.has(key)) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
seen.add(key);
|
|
90
|
+
result.push(item);
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function tryReadDir(dirPath) {
|
|
96
|
+
try {
|
|
97
|
+
return readdirSync(dirPath, { withFileTypes: true });
|
|
98
|
+
} catch {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function containsBundleMarkers(root) {
|
|
104
|
+
if (!existsSync(root)) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
const markers = [
|
|
108
|
+
"pack.manifest.json",
|
|
109
|
+
"import-slice.json",
|
|
110
|
+
"candidate-pack-input.json",
|
|
111
|
+
"graph.json",
|
|
112
|
+
"graphify-summary.json",
|
|
113
|
+
"surface-map.json",
|
|
114
|
+
"proposal-report.json",
|
|
115
|
+
"verdict.json",
|
|
116
|
+
];
|
|
117
|
+
return markers.some((marker) => existsSync(path.join(root, marker)));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function discoverBundleRootsFromContainer(containerRoot) {
|
|
121
|
+
const roots = [];
|
|
122
|
+
if (!existsSync(containerRoot)) {
|
|
123
|
+
return roots;
|
|
124
|
+
}
|
|
125
|
+
for (const entry of tryReadDir(containerRoot)) {
|
|
126
|
+
if (!entry.isDirectory()) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const childRoot = path.join(containerRoot, entry.name);
|
|
130
|
+
if (containsBundleMarkers(childRoot)) {
|
|
131
|
+
roots.push(childRoot);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return roots;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function discoverCurrentBundleRoots(graphifyRoot) {
|
|
138
|
+
const resolvedRoot = path.resolve(graphifyRoot);
|
|
139
|
+
const roots = [];
|
|
140
|
+
if (containsBundleMarkers(resolvedRoot)) {
|
|
141
|
+
roots.push(resolvedRoot);
|
|
142
|
+
}
|
|
143
|
+
const directChildren = [
|
|
144
|
+
"compiled",
|
|
145
|
+
"import",
|
|
146
|
+
"run",
|
|
147
|
+
"teacher-v3-proof",
|
|
148
|
+
];
|
|
149
|
+
for (const child of directChildren) {
|
|
150
|
+
const childRoot = path.join(resolvedRoot, child);
|
|
151
|
+
if (containsBundleMarkers(childRoot)) {
|
|
152
|
+
roots.push(childRoot);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
roots.push(...discoverBundleRootsFromContainer(path.join(resolvedRoot, "graphify-runs")));
|
|
156
|
+
roots.push(...discoverBundleRootsFromContainer(path.join(resolvedRoot, "graphify-imports")));
|
|
157
|
+
roots.push(...discoverBundleRootsFromContainer(path.join(resolvedRoot, "graphify-source-bundles")));
|
|
158
|
+
return uniqueBy(roots, (value) => value);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function discoverOcbBundleRoots(ocbRoot) {
|
|
162
|
+
const resolvedRoot = path.resolve(ocbRoot);
|
|
163
|
+
const roots = [];
|
|
164
|
+
if (containsBundleMarkers(resolvedRoot)) {
|
|
165
|
+
roots.push(resolvedRoot);
|
|
166
|
+
}
|
|
167
|
+
const directChildren = [
|
|
168
|
+
"candidate",
|
|
169
|
+
"compiled",
|
|
170
|
+
"promoted",
|
|
171
|
+
"teacher-v3-shadow-examples",
|
|
172
|
+
"teacher-v3-promotable-examples",
|
|
173
|
+
"teacher-v3-proof",
|
|
174
|
+
];
|
|
175
|
+
for (const child of directChildren) {
|
|
176
|
+
const childRoot = path.join(resolvedRoot, child);
|
|
177
|
+
if (containsBundleMarkers(childRoot)) {
|
|
178
|
+
roots.push(childRoot);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const explicitFixture = path.join(resolvedRoot, "artifacts", "fixtures", "compiled-artifacts", "target-state-scaffold");
|
|
182
|
+
if (containsBundleMarkers(explicitFixture)) {
|
|
183
|
+
roots.push(explicitFixture);
|
|
184
|
+
}
|
|
185
|
+
return uniqueBy(roots, (value) => value);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function loadArtifactMeta(bundleRoot, artifactSummary) {
|
|
189
|
+
const metaPath = typeof artifactSummary.metaPath === "string" && artifactSummary.metaPath.trim().length > 0
|
|
190
|
+
? path.resolve(bundleRoot, artifactSummary.metaPath)
|
|
191
|
+
: path.join(bundleRoot, "artifacts", artifactSummary.artifactId, "artifact.meta.json");
|
|
192
|
+
const markdownPath = typeof artifactSummary.markdownPath === "string" && artifactSummary.markdownPath.trim().length > 0
|
|
193
|
+
? path.resolve(bundleRoot, artifactSummary.markdownPath)
|
|
194
|
+
: path.join(bundleRoot, "artifacts", artifactSummary.artifactId, "artifact.md");
|
|
195
|
+
const meta = readJsonIfExists(metaPath) ?? {};
|
|
196
|
+
const markdownText = readTextIfExists(markdownPath);
|
|
197
|
+
return {
|
|
198
|
+
...meta,
|
|
199
|
+
artifactId: artifactSummary.artifactId,
|
|
200
|
+
kind: meta.kind ?? artifactSummary.kind ?? "unknown",
|
|
201
|
+
title: meta.title ?? artifactSummary.title ?? artifactSummary.artifactId,
|
|
202
|
+
markdownPath,
|
|
203
|
+
metaPath,
|
|
204
|
+
markdownText,
|
|
205
|
+
bundleRoot,
|
|
206
|
+
artifactSummary,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function loadBundleSnapshot(bundleRoot, role) {
|
|
211
|
+
const resolvedRoot = path.resolve(bundleRoot);
|
|
212
|
+
const packManifest = readJsonIfExists(path.join(resolvedRoot, "pack.manifest.json"));
|
|
213
|
+
const importSlice = readJsonIfExists(path.join(resolvedRoot, "import-slice.json"));
|
|
214
|
+
const candidatePackInput = readJsonIfExists(path.join(resolvedRoot, "candidate-pack-input.json"));
|
|
215
|
+
const graph = readJsonIfExists(path.join(resolvedRoot, "graph.json"));
|
|
216
|
+
const graphSummary = readJsonIfExists(path.join(resolvedRoot, "graphify-summary.json"));
|
|
217
|
+
const surfaceMap = readJsonIfExists(path.join(resolvedRoot, "surface-map.json"));
|
|
218
|
+
const proposalReport = readJsonIfExists(path.join(resolvedRoot, "proposal-report.json"));
|
|
219
|
+
const verdict = readJsonIfExists(path.join(resolvedRoot, "verdict.json"));
|
|
220
|
+
const labels = readJsonIfExists(path.join(resolvedRoot, "labels.json"));
|
|
221
|
+
const evidencePointers = readJsonIfExists(path.join(resolvedRoot, "evidence-pointers.json"));
|
|
222
|
+
const rationalePointers = readJsonIfExists(path.join(resolvedRoot, "rationale-pointers.json"));
|
|
223
|
+
|
|
224
|
+
const artifactRecords = [];
|
|
225
|
+
const artifactSummaries = Array.isArray(packManifest?.artifacts) ? packManifest.artifacts : [];
|
|
226
|
+
for (const summary of artifactSummaries) {
|
|
227
|
+
artifactRecords.push(loadArtifactMeta(resolvedRoot, summary));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const sourceBundle = graphSummary?.sourceBundle ?? packManifest?.graphifyRun?.sourceBundleId ?? null;
|
|
231
|
+
return {
|
|
232
|
+
role,
|
|
233
|
+
bundleRoot: resolvedRoot,
|
|
234
|
+
packManifest,
|
|
235
|
+
importSlice,
|
|
236
|
+
candidatePackInput,
|
|
237
|
+
graph,
|
|
238
|
+
graphSummary,
|
|
239
|
+
surfaceMap,
|
|
240
|
+
proposalReport,
|
|
241
|
+
verdict,
|
|
242
|
+
labels,
|
|
243
|
+
evidencePointers,
|
|
244
|
+
rationalePointers,
|
|
245
|
+
artifacts: artifactRecords,
|
|
246
|
+
sourceBundle,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function toEvidenceRef(sourceId, excerpt) {
|
|
251
|
+
return {
|
|
252
|
+
sourceKind: "file",
|
|
253
|
+
sourceId,
|
|
254
|
+
authority: "raw_source",
|
|
255
|
+
derivation: "teacher_lint",
|
|
256
|
+
excerpt,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function baseEvidenceRefs(currentGraphifyRoots, ocbRoots) {
|
|
261
|
+
const refs = [
|
|
262
|
+
toEvidenceRef("docs/architecture/graphify-bridge.md", "Graphify stays off the serve path and below stronger truth layers."),
|
|
263
|
+
toEvidenceRef("docs/architecture/teacher-v3.md", "Teacher v3 is an off-path compiler of graph structure and compiled artifacts, not an arbiter of current truth."),
|
|
264
|
+
toEvidenceRef("docs/architecture/teacher-v3-proof.md", "Teacher v3 surfaces must remain bounded and labeled by truth layer."),
|
|
265
|
+
];
|
|
266
|
+
for (const root of currentGraphifyRoots.slice(0, 2)) {
|
|
267
|
+
refs.push(toEvidenceRef(relativeWorkspacePath(path.join(root.bundleRoot, "pack.manifest.json"), defaultWorkspaceRoot), `Current Graphify surface root: ${root.role}`));
|
|
268
|
+
}
|
|
269
|
+
for (const root of ocbRoots.slice(0, 2)) {
|
|
270
|
+
refs.push(toEvidenceRef(relativeWorkspacePath(path.join(root.bundleRoot, "pack.manifest.json"), defaultWorkspaceRoot), `OCB inspectable surface root: ${root.role}`));
|
|
271
|
+
}
|
|
272
|
+
return refs;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function surfaceKey(record) {
|
|
276
|
+
return `${record.surfaceId}::${record.kind}`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function rolePriority(role) {
|
|
280
|
+
if (typeof role !== "string") {
|
|
281
|
+
return 0;
|
|
282
|
+
}
|
|
283
|
+
if (role.startsWith("ocb_promoted") || role === "promoted") {
|
|
284
|
+
return 3;
|
|
285
|
+
}
|
|
286
|
+
if (role.startsWith("ocb_compiled") || role === "compiled") {
|
|
287
|
+
return 2;
|
|
288
|
+
}
|
|
289
|
+
if (role.startsWith("ocb_candidate") || role === "candidate") {
|
|
290
|
+
return 1;
|
|
291
|
+
}
|
|
292
|
+
return 0;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function choosePreferredRecordMap(records) {
|
|
296
|
+
const map = new Map();
|
|
297
|
+
for (const record of records) {
|
|
298
|
+
const existing = map.get(record.surfaceId) ?? null;
|
|
299
|
+
if (existing === null || rolePriority(record.bundleRole) >= rolePriority(existing.bundleRole)) {
|
|
300
|
+
map.set(record.surfaceId, record);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return map;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function buildCurrentSurfaceIndex(snapshot) {
|
|
307
|
+
const current = [];
|
|
308
|
+
|
|
309
|
+
for (const artifact of snapshot.artifacts) {
|
|
310
|
+
current.push({
|
|
311
|
+
origin: "current",
|
|
312
|
+
bundleRole: snapshot.role,
|
|
313
|
+
bundleRoot: snapshot.bundleRoot,
|
|
314
|
+
surfaceId: artifact.artifactId,
|
|
315
|
+
kind: artifact.kind,
|
|
316
|
+
state: artifact.status ?? "current",
|
|
317
|
+
title: artifact.title,
|
|
318
|
+
subjectIds: Array.isArray(artifact.subjectIds) ? [...artifact.subjectIds] : [],
|
|
319
|
+
evidence: Array.isArray(artifact.evidence) ? [...artifact.evidence] : [],
|
|
320
|
+
claims: Array.isArray(artifact.claims) ? [...artifact.claims] : [],
|
|
321
|
+
sourceRoots: Array.isArray(artifact.provenance?.sourceRoots) ? [...artifact.provenance.sourceRoots] : [],
|
|
322
|
+
sourceBundleId: artifact.provenance?.sourceBundleId ?? null,
|
|
323
|
+
sourceBundleHash: artifact.provenance?.sourceBundleHash ?? null,
|
|
324
|
+
graphHash: artifact.provenance?.graphHash ?? null,
|
|
325
|
+
contentHash: artifact.contentHash ?? null,
|
|
326
|
+
markdownPath: artifact.markdownPath,
|
|
327
|
+
metaPath: artifact.metaPath,
|
|
328
|
+
sourceArtifactId: artifact.artifactId,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (snapshot.importSlice) {
|
|
333
|
+
const hubPriors = Array.isArray(snapshot.importSlice.hubPriors) ? snapshot.importSlice.hubPriors : [];
|
|
334
|
+
const neighborhoodPriors = Array.isArray(snapshot.importSlice.neighborhoodPriors) ? snapshot.importSlice.neighborhoodPriors : [];
|
|
335
|
+
for (const prior of [...hubPriors, ...neighborhoodPriors]) {
|
|
336
|
+
current.push({
|
|
337
|
+
origin: "current",
|
|
338
|
+
bundleRole: snapshot.role,
|
|
339
|
+
bundleRoot: snapshot.bundleRoot,
|
|
340
|
+
surfaceId: prior.priorId,
|
|
341
|
+
kind: prior.kind ?? "prior",
|
|
342
|
+
state: "current",
|
|
343
|
+
title: prior.title ?? prior.label ?? prior.priorId,
|
|
344
|
+
label: prior.label ?? prior.title ?? prior.priorId,
|
|
345
|
+
subjectIds: Array.isArray(prior.subjectIds) ? [...prior.subjectIds] : [],
|
|
346
|
+
evidencePointerIds: Array.isArray(prior.evidencePointerIds) ? [...prior.evidencePointerIds] : [],
|
|
347
|
+
rationalePointerIds: Array.isArray(prior.rationalePointerIds) ? [...prior.rationalePointerIds] : [],
|
|
348
|
+
sourceRoots: Array.isArray(prior.sourceRoots) ? [...prior.sourceRoots] : [],
|
|
349
|
+
sourceBundleId: prior.sourceBundleId ?? snapshot.importSlice.sourceBundleId ?? null,
|
|
350
|
+
sourceBundleHash: prior.sourceBundleHash ?? snapshot.importSlice.sourceBundleHash ?? null,
|
|
351
|
+
sourceArtifactId: prior.sourceArtifactId ?? null,
|
|
352
|
+
sourceArtifactPath: prior.sourceArtifactPath ?? null,
|
|
353
|
+
sourceMetaPath: prior.sourceMetaPath ?? null,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (snapshot.surfaceMap) {
|
|
359
|
+
const controlSurfaces = Array.isArray(snapshot.surfaceMap.controlSurfaces) ? snapshot.surfaceMap.controlSurfaces : [];
|
|
360
|
+
const sourceTruthAnchors = Array.isArray(snapshot.surfaceMap.sourceTruthAnchors) ? snapshot.surfaceMap.sourceTruthAnchors : [];
|
|
361
|
+
for (const surface of controlSurfaces) {
|
|
362
|
+
current.push({
|
|
363
|
+
origin: "current",
|
|
364
|
+
bundleRole: snapshot.role,
|
|
365
|
+
bundleRoot: snapshot.bundleRoot,
|
|
366
|
+
surfaceId: surface.id,
|
|
367
|
+
kind: surface.kind ?? "proposal_truth",
|
|
368
|
+
state: surface.state ?? "current",
|
|
369
|
+
title: surface.note ?? surface.id,
|
|
370
|
+
source: surface.source ?? null,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
for (const anchor of sourceTruthAnchors) {
|
|
374
|
+
current.push({
|
|
375
|
+
origin: "current",
|
|
376
|
+
bundleRole: snapshot.role,
|
|
377
|
+
bundleRoot: snapshot.bundleRoot,
|
|
378
|
+
surfaceId: anchor.id,
|
|
379
|
+
kind: anchor.kind ?? "source_truth_anchor",
|
|
380
|
+
state: anchor.state ?? "current",
|
|
381
|
+
title: anchor.source ?? anchor.id,
|
|
382
|
+
source: anchor.source ?? null,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return uniqueBy(current, surfaceKey);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function buildOcbSurfaceIndex(snapshot) {
|
|
391
|
+
const ocb = [];
|
|
392
|
+
for (const artifact of snapshot.artifacts) {
|
|
393
|
+
ocb.push({
|
|
394
|
+
origin: "ocb",
|
|
395
|
+
bundleRole: snapshot.role,
|
|
396
|
+
bundleRoot: snapshot.bundleRoot,
|
|
397
|
+
surfaceId: artifact.artifactId,
|
|
398
|
+
kind: artifact.kind,
|
|
399
|
+
state: artifact.status ?? "current",
|
|
400
|
+
title: artifact.title,
|
|
401
|
+
subjectIds: Array.isArray(artifact.subjectIds) ? [...artifact.subjectIds] : [],
|
|
402
|
+
evidence: Array.isArray(artifact.evidence) ? [...artifact.evidence] : [],
|
|
403
|
+
claims: Array.isArray(artifact.claims) ? [...artifact.claims] : [],
|
|
404
|
+
sourceRoots: Array.isArray(artifact.provenance?.sourceRoots) ? [...artifact.provenance.sourceRoots] : [],
|
|
405
|
+
sourceBundleId: artifact.provenance?.sourceBundleId ?? null,
|
|
406
|
+
sourceBundleHash: artifact.provenance?.sourceBundleHash ?? null,
|
|
407
|
+
graphHash: artifact.provenance?.graphHash ?? null,
|
|
408
|
+
contentHash: artifact.contentHash ?? null,
|
|
409
|
+
markdownPath: artifact.markdownPath,
|
|
410
|
+
metaPath: artifact.metaPath,
|
|
411
|
+
sourceArtifactId: artifact.artifactId,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
if (snapshot.importSlice) {
|
|
415
|
+
const hubPriors = Array.isArray(snapshot.importSlice.hubPriors) ? snapshot.importSlice.hubPriors : [];
|
|
416
|
+
const neighborhoodPriors = Array.isArray(snapshot.importSlice.neighborhoodPriors) ? snapshot.importSlice.neighborhoodPriors : [];
|
|
417
|
+
for (const prior of [...hubPriors, ...neighborhoodPriors]) {
|
|
418
|
+
ocb.push({
|
|
419
|
+
origin: "ocb",
|
|
420
|
+
bundleRole: snapshot.role,
|
|
421
|
+
bundleRoot: snapshot.bundleRoot,
|
|
422
|
+
surfaceId: prior.priorId,
|
|
423
|
+
kind: prior.kind ?? "prior",
|
|
424
|
+
state: "current",
|
|
425
|
+
title: prior.title ?? prior.label ?? prior.priorId,
|
|
426
|
+
label: prior.label ?? prior.title ?? prior.priorId,
|
|
427
|
+
subjectIds: Array.isArray(prior.subjectIds) ? [...prior.subjectIds] : [],
|
|
428
|
+
evidencePointerIds: Array.isArray(prior.evidencePointerIds) ? [...prior.evidencePointerIds] : [],
|
|
429
|
+
rationalePointerIds: Array.isArray(prior.rationalePointerIds) ? [...prior.rationalePointerIds] : [],
|
|
430
|
+
sourceRoots: Array.isArray(prior.sourceRoots) ? [...prior.sourceRoots] : [],
|
|
431
|
+
sourceBundleId: prior.sourceBundleId ?? snapshot.importSlice.sourceBundleId ?? null,
|
|
432
|
+
sourceBundleHash: prior.sourceBundleHash ?? snapshot.importSlice.sourceBundleHash ?? null,
|
|
433
|
+
sourceArtifactId: prior.sourceArtifactId ?? null,
|
|
434
|
+
sourceArtifactPath: prior.sourceArtifactPath ?? null,
|
|
435
|
+
sourceMetaPath: prior.sourceMetaPath ?? null,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (snapshot.surfaceMap) {
|
|
440
|
+
const controlSurfaces = Array.isArray(snapshot.surfaceMap.controlSurfaces) ? snapshot.surfaceMap.controlSurfaces : [];
|
|
441
|
+
const sourceTruthAnchors = Array.isArray(snapshot.surfaceMap.sourceTruthAnchors) ? snapshot.surfaceMap.sourceTruthAnchors : [];
|
|
442
|
+
for (const surface of controlSurfaces) {
|
|
443
|
+
ocb.push({
|
|
444
|
+
origin: "ocb",
|
|
445
|
+
bundleRole: snapshot.role,
|
|
446
|
+
bundleRoot: snapshot.bundleRoot,
|
|
447
|
+
surfaceId: surface.id,
|
|
448
|
+
kind: surface.kind ?? "proposal_truth",
|
|
449
|
+
state: surface.state ?? "current",
|
|
450
|
+
title: surface.note ?? surface.id,
|
|
451
|
+
source: surface.source ?? null,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
for (const anchor of sourceTruthAnchors) {
|
|
455
|
+
ocb.push({
|
|
456
|
+
origin: "ocb",
|
|
457
|
+
bundleRole: snapshot.role,
|
|
458
|
+
bundleRoot: snapshot.bundleRoot,
|
|
459
|
+
surfaceId: anchor.id,
|
|
460
|
+
kind: anchor.kind ?? "source_truth_anchor",
|
|
461
|
+
state: anchor.state ?? "current",
|
|
462
|
+
title: anchor.source ?? anchor.id,
|
|
463
|
+
source: anchor.source ?? null,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return uniqueBy(ocb, surfaceKey);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function buildCurrentGraphRootsSummary(currentRoots) {
|
|
471
|
+
return currentRoots.map((root) => ({
|
|
472
|
+
role: root.role,
|
|
473
|
+
bundleRoot: root.bundleRoot,
|
|
474
|
+
relativePath: relativeWorkspacePath(root.bundleRoot, defaultWorkspaceRoot),
|
|
475
|
+
}));
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function buildOcbGraphRootsSummary(ocbRoots) {
|
|
479
|
+
return ocbRoots.map((root) => ({
|
|
480
|
+
role: root.role,
|
|
481
|
+
bundleRoot: root.bundleRoot,
|
|
482
|
+
relativePath: relativeWorkspacePath(root.bundleRoot, defaultWorkspaceRoot),
|
|
483
|
+
}));
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function matchingOcbRecord(currentRecord, ocbById) {
|
|
487
|
+
return ocbById.get(currentRecord.surfaceId) ?? null;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function recordEvidenceSupport(record) {
|
|
491
|
+
const evidenceIds = Array.isArray(record.evidence) ? record.evidence.map((item) => item?.evidenceId).filter((value) => typeof value === "string" && value.trim().length > 0) : [];
|
|
492
|
+
const evidenceWithSourceHash = Array.isArray(record.evidence)
|
|
493
|
+
? record.evidence.filter((item) => item && typeof item === "object" && typeof item.sourceHash === "string" && item.sourceHash.trim().length > 0).length
|
|
494
|
+
: 0;
|
|
495
|
+
return { evidenceIds, evidenceWithSourceHash };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function buildMissingFromOcbFindings(currentRecords, ocbById, currentRootSummaries, ocbRootSummaries) {
|
|
499
|
+
const findings = [];
|
|
500
|
+
for (const record of currentRecords) {
|
|
501
|
+
const ocbRecord = matchingOcbRecord(record, ocbById);
|
|
502
|
+
if (ocbRecord !== null) {
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
findings.push({
|
|
506
|
+
surfaceId: record.surfaceId,
|
|
507
|
+
kind: record.kind,
|
|
508
|
+
title: record.title ?? record.label ?? record.surfaceId,
|
|
509
|
+
reason: `current ${record.kind} surface is not represented in the OCB inspectable surfaces`,
|
|
510
|
+
sourcePaths: [record.markdownPath, record.metaPath, record.sourceArtifactPath, record.sourceMetaPath].filter((value) => typeof value === "string" && value.trim().length > 0),
|
|
511
|
+
sourceBundleId: record.sourceBundleId ?? null,
|
|
512
|
+
sourceBundleHash: record.sourceBundleHash ?? null,
|
|
513
|
+
bundleRole: record.bundleRole,
|
|
514
|
+
currentRoots: currentRootSummaries,
|
|
515
|
+
ocbRoots: ocbRootSummaries,
|
|
516
|
+
evidenceRefs: [
|
|
517
|
+
toEvidenceRef(relativeWorkspacePath(record.metaPath ?? record.markdownPath ?? record.sourceMetaPath ?? record.sourceArtifactPath ?? `${record.surfaceId}`, defaultWorkspaceRoot), `Current surface ${record.surfaceId}`),
|
|
518
|
+
toEvidenceRef("docs/architecture/graphify-bridge.md", "Graphify outputs are derived and review-only."),
|
|
519
|
+
],
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
return findings;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function buildStaleInOcbFindings(currentRecords, ocbRecords, currentById, ocbRootSummaries, currentRootSummaries) {
|
|
526
|
+
const findings = [];
|
|
527
|
+
for (const ocbRecord of ocbRecords) {
|
|
528
|
+
const currentRecord = currentById.get(ocbRecord.surfaceId) ?? null;
|
|
529
|
+
if (currentRecord === null) {
|
|
530
|
+
findings.push({
|
|
531
|
+
surfaceId: ocbRecord.surfaceId,
|
|
532
|
+
kind: ocbRecord.kind,
|
|
533
|
+
title: ocbRecord.title ?? ocbRecord.label ?? ocbRecord.surfaceId,
|
|
534
|
+
reason: `OCB surface ${ocbRecord.surfaceId} is absent from the current Graphify surfaces`,
|
|
535
|
+
sourcePaths: [ocbRecord.markdownPath, ocbRecord.metaPath, ocbRecord.sourceArtifactPath, ocbRecord.sourceMetaPath].filter((value) => typeof value === "string" && value.trim().length > 0),
|
|
536
|
+
sourceBundleId: ocbRecord.sourceBundleId ?? null,
|
|
537
|
+
sourceBundleHash: ocbRecord.sourceBundleHash ?? null,
|
|
538
|
+
bundleRole: ocbRecord.bundleRole,
|
|
539
|
+
ocbRoots: ocbRootSummaries,
|
|
540
|
+
currentRoots: currentRootSummaries,
|
|
541
|
+
evidenceRefs: [
|
|
542
|
+
toEvidenceRef(relativeWorkspacePath(ocbRecord.metaPath ?? ocbRecord.markdownPath ?? ocbRecord.sourceMetaPath ?? ocbRecord.sourceArtifactPath ?? `${ocbRecord.surfaceId}`, defaultWorkspaceRoot), `OCB surface ${ocbRecord.surfaceId}`),
|
|
543
|
+
toEvidenceRef("docs/architecture/teacher-v3.md", "OCB inspectable surfaces remain below runtime/proof/docs truth."),
|
|
544
|
+
],
|
|
545
|
+
});
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
const hashMismatch = normalizeText(currentRecord.contentHash) !== normalizeText(ocbRecord.contentHash)
|
|
549
|
+
|| normalizeText(currentRecord.sourceBundleHash) !== normalizeText(ocbRecord.sourceBundleHash)
|
|
550
|
+
|| normalizeText(currentRecord.graphHash) !== normalizeText(ocbRecord.graphHash)
|
|
551
|
+
|| JSON.stringify(currentRecord.subjectIds ?? []) !== JSON.stringify(ocbRecord.subjectIds ?? []);
|
|
552
|
+
if (hashMismatch) {
|
|
553
|
+
findings.push({
|
|
554
|
+
surfaceId: ocbRecord.surfaceId,
|
|
555
|
+
kind: ocbRecord.kind,
|
|
556
|
+
title: ocbRecord.title ?? ocbRecord.label ?? ocbRecord.surfaceId,
|
|
557
|
+
reason: `OCB surface ${ocbRecord.surfaceId} is stale relative to current Graphify metadata or content`,
|
|
558
|
+
sourcePaths: [ocbRecord.metaPath, ocbRecord.markdownPath, currentRecord.metaPath, currentRecord.markdownPath].filter((value) => typeof value === "string" && value.trim().length > 0),
|
|
559
|
+
currentContentHash: currentRecord.contentHash ?? null,
|
|
560
|
+
ocbContentHash: ocbRecord.contentHash ?? null,
|
|
561
|
+
currentSourceBundleHash: currentRecord.sourceBundleHash ?? null,
|
|
562
|
+
ocbSourceBundleHash: ocbRecord.sourceBundleHash ?? null,
|
|
563
|
+
bundleRole: ocbRecord.bundleRole,
|
|
564
|
+
ocbRoots: ocbRootSummaries,
|
|
565
|
+
currentRoots: currentRootSummaries,
|
|
566
|
+
evidenceRefs: [
|
|
567
|
+
toEvidenceRef(relativeWorkspacePath(ocbRecord.metaPath ?? ocbRecord.markdownPath ?? `${ocbRecord.surfaceId}`, defaultWorkspaceRoot), `OCB stale surface ${ocbRecord.surfaceId}`),
|
|
568
|
+
toEvidenceRef(relativeWorkspacePath(currentRecord.metaPath ?? currentRecord.markdownPath ?? `${currentRecord.surfaceId}`, defaultWorkspaceRoot), `Current Graphify surface ${currentRecord.surfaceId}`),
|
|
569
|
+
],
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return findings;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function buildCandidateOnlyEdgeFindings(currentRecords) {
|
|
577
|
+
const findings = [];
|
|
578
|
+
for (const record of currentRecords) {
|
|
579
|
+
if (!Array.isArray(record.claims) || record.claims.length === 0) {
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
for (const claim of record.claims) {
|
|
583
|
+
const claimEvidenceIds = Array.isArray(claim?.evidenceIds)
|
|
584
|
+
? claim.evidenceIds.filter((value) => typeof value === "string" && value.trim().length > 0)
|
|
585
|
+
: [];
|
|
586
|
+
if (claimEvidenceIds.length === 0) {
|
|
587
|
+
findings.push({
|
|
588
|
+
edgeId: `${record.surfaceId}:${claim?.claimId ?? "claim"}`,
|
|
589
|
+
edgeKind: "claim_to_evidence",
|
|
590
|
+
sourceSurfaceId: record.surfaceId,
|
|
591
|
+
targetSurfaceId: claim?.claimId ?? null,
|
|
592
|
+
title: claim?.text ?? claim?.claimId ?? record.surfaceId,
|
|
593
|
+
reason: `claim ${claim?.claimId ?? "unknown"} carries no evidence support`,
|
|
594
|
+
sourcePaths: [record.metaPath, record.markdownPath].filter((value) => typeof value === "string" && value.trim().length > 0),
|
|
595
|
+
evidenceRefs: [
|
|
596
|
+
toEvidenceRef(relativeWorkspacePath(record.metaPath ?? `${record.surfaceId}`, defaultWorkspaceRoot), `Claim without evidence support on ${record.surfaceId}`),
|
|
597
|
+
toEvidenceRef("docs/architecture/compiled-artifacts.md", "Compiled artifacts must keep evidence refs explicit."),
|
|
598
|
+
],
|
|
599
|
+
});
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
const evidenceById = new Map(Array.isArray(record.evidence) ? record.evidence.map((item) => [item?.evidenceId ?? item?.sourceId, item]) : []);
|
|
603
|
+
for (const evidenceId of claimEvidenceIds) {
|
|
604
|
+
const evidence = evidenceById.get(evidenceId) ?? null;
|
|
605
|
+
if (evidence === null || !normalizeText(evidence.sourceHash)) {
|
|
606
|
+
findings.push({
|
|
607
|
+
edgeId: `${record.surfaceId}:${claim?.claimId ?? "claim"}:${evidenceId}`,
|
|
608
|
+
edgeKind: "claim_to_evidence",
|
|
609
|
+
sourceSurfaceId: record.surfaceId,
|
|
610
|
+
targetSurfaceId: evidenceId,
|
|
611
|
+
title: claim?.text ?? claim?.claimId ?? record.surfaceId,
|
|
612
|
+
reason: `claim ${claim?.claimId ?? "unknown"} points at unsupported evidence ${evidenceId}`,
|
|
613
|
+
sourcePaths: [record.metaPath, record.markdownPath].filter((value) => typeof value === "string" && value.trim().length > 0),
|
|
614
|
+
evidenceRefs: [
|
|
615
|
+
toEvidenceRef(relativeWorkspacePath(record.metaPath ?? `${record.surfaceId}`, defaultWorkspaceRoot), `Unsupported claim edge on ${record.surfaceId}`),
|
|
616
|
+
toEvidenceRef("docs/architecture/teacher-v3.md", "Teacher v3 keeps derivation below authority and requires bounded evidence."),
|
|
617
|
+
],
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (Array.isArray(record.evidence)) {
|
|
624
|
+
for (const evidence of record.evidence) {
|
|
625
|
+
const evidenceId = evidence?.evidenceId ?? evidence?.sourceId ?? null;
|
|
626
|
+
if (evidenceId === null || !normalizeText(evidence.sourceHash) || !normalizeText(evidence.sourceId)) {
|
|
627
|
+
findings.push({
|
|
628
|
+
edgeId: `${record.surfaceId}:${evidenceId ?? "evidence"}`,
|
|
629
|
+
edgeKind: "artifact_to_evidence",
|
|
630
|
+
sourceSurfaceId: record.surfaceId,
|
|
631
|
+
targetSurfaceId: evidenceId,
|
|
632
|
+
title: record.title ?? record.surfaceId,
|
|
633
|
+
reason: `evidence ref on ${record.surfaceId} is missing source support`,
|
|
634
|
+
sourcePaths: [record.metaPath, record.markdownPath].filter((value) => typeof value === "string" && value.trim().length > 0),
|
|
635
|
+
evidenceRefs: [
|
|
636
|
+
toEvidenceRef(relativeWorkspacePath(record.metaPath ?? `${record.surfaceId}`, defaultWorkspaceRoot), `Unsupported evidence on ${record.surfaceId}`),
|
|
637
|
+
toEvidenceRef("docs/architecture/graphify-bridge.md", "Graphify outputs stay derived and provenance-first."),
|
|
638
|
+
],
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
}
|
|
645
|
+
return findings;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function overlapRatio(left, right) {
|
|
649
|
+
const leftSet = new Set((left ?? []).filter((value) => typeof value === "string" && value.trim().length > 0));
|
|
650
|
+
const rightSet = new Set((right ?? []).filter((value) => typeof value === "string" && value.trim().length > 0));
|
|
651
|
+
if (leftSet.size === 0 || rightSet.size === 0) {
|
|
652
|
+
return 0;
|
|
653
|
+
}
|
|
654
|
+
let overlap = 0;
|
|
655
|
+
for (const value of leftSet) {
|
|
656
|
+
if (rightSet.has(value)) {
|
|
657
|
+
overlap += 1;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return overlap / Math.min(leftSet.size, rightSet.size);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function buildCurrentSourceHubFindings(currentRecords, ocbRecords, currentRootSummaries, ocbRootSummaries) {
|
|
664
|
+
const ocbSurfaceIds = new Set(ocbRecords.map((record) => record.surfaceId));
|
|
665
|
+
const ocbSubjects = new Set(ocbRecords.flatMap((record) => Array.isArray(record.subjectIds) ? record.subjectIds : []));
|
|
666
|
+
const findings = [];
|
|
667
|
+
for (const record of currentRecords) {
|
|
668
|
+
const isHubLike = ["hub_prior", "map_of_territory", "concept_page"].includes(record.kind);
|
|
669
|
+
if (!isHubLike) {
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
const subjectIds = Array.isArray(record.subjectIds) ? record.subjectIds : [];
|
|
673
|
+
const isNewSubjectSpace = subjectIds.some((subjectId) => !ocbSubjects.has(subjectId));
|
|
674
|
+
if (isNewSubjectSpace || !ocbSurfaceIds.has(record.surfaceId)) {
|
|
675
|
+
findings.push({
|
|
676
|
+
surfaceId: record.surfaceId,
|
|
677
|
+
kind: record.kind,
|
|
678
|
+
title: record.title ?? record.label ?? record.surfaceId,
|
|
679
|
+
reason: `current source hub ${record.surfaceId} is new relative to OCB subject/surface coverage`,
|
|
680
|
+
subjectIds,
|
|
681
|
+
sourcePaths: [record.metaPath, record.markdownPath, record.sourceArtifactPath, record.sourceMetaPath].filter((value) => typeof value === "string" && value.trim().length > 0),
|
|
682
|
+
currentRoots: currentRootSummaries,
|
|
683
|
+
ocbRoots: ocbRootSummaries,
|
|
684
|
+
evidenceRefs: [
|
|
685
|
+
toEvidenceRef(relativeWorkspacePath(record.metaPath ?? record.sourceMetaPath ?? record.markdownPath ?? `${record.surfaceId}`, defaultWorkspaceRoot), `Current source hub ${record.surfaceId}`),
|
|
686
|
+
toEvidenceRef("docs/architecture/teacher-v3.md", "Teacher v3 keeps derived hubs subordinate to stronger truth layers."),
|
|
687
|
+
],
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return findings;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function buildProvenanceGapFindings(currentRecords, ocbRecords) {
|
|
695
|
+
const findings = [];
|
|
696
|
+
for (const record of [...currentRecords, ...ocbRecords]) {
|
|
697
|
+
const provenanceMissing = ["map_of_territory", "concept_page", "neighborhood_summary", "provenance_gap_report", "hub_prior", "neighborhood_prior", "candidate_pack_input"].includes(record.kind)
|
|
698
|
+
&& (!normalizeText(record.sourceBundleHash) || (!normalizeText(record.sourceBundleId) && !normalizeText(record.sourceArtifactId)));
|
|
699
|
+
const evidenceMissingHash = Array.isArray(record.evidence) && record.evidence.some((evidence) => !normalizeText(evidence?.sourceHash) || !normalizeText(evidence?.sourceId));
|
|
700
|
+
const pointerMissingHash = Array.isArray(record.evidencePointerIds) && record.evidencePointerIds.length > 0 && !normalizeText(record.sourceBundleHash);
|
|
701
|
+
if (!(provenanceMissing || evidenceMissingHash || pointerMissingHash)) {
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
findings.push({
|
|
705
|
+
surfaceId: record.surfaceId,
|
|
706
|
+
kind: record.kind,
|
|
707
|
+
title: record.title ?? record.label ?? record.surfaceId,
|
|
708
|
+
reason: provenanceMissing
|
|
709
|
+
? `surface ${record.surfaceId} is missing provenance fields`
|
|
710
|
+
: `surface ${record.surfaceId} has evidence or pointer entries without source hashes`,
|
|
711
|
+
sourcePaths: [record.metaPath, record.markdownPath, record.sourceMetaPath, record.sourceArtifactPath].filter((value) => typeof value === "string" && value.trim().length > 0),
|
|
712
|
+
sourceBundleId: record.sourceBundleId ?? null,
|
|
713
|
+
sourceBundleHash: record.sourceBundleHash ?? null,
|
|
714
|
+
evidenceRefs: [
|
|
715
|
+
toEvidenceRef(relativeWorkspacePath(record.metaPath ?? record.sourceMetaPath ?? record.markdownPath ?? `${record.surfaceId}`, defaultWorkspaceRoot), `Provenance gap candidate ${record.surfaceId}`),
|
|
716
|
+
toEvidenceRef("docs/architecture/compiled-artifacts.md", "Compiled artifacts require explicit provenance metadata."),
|
|
717
|
+
],
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
return findings;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function buildMergeSplitHints(currentRecords, ocbRecords) {
|
|
724
|
+
const hints = [];
|
|
725
|
+
const hubLikeCurrent = currentRecords.filter((record) => ["hub_prior", "map_of_territory", "concept_page"].includes(record.kind));
|
|
726
|
+
for (let leftIndex = 0; leftIndex < hubLikeCurrent.length; leftIndex += 1) {
|
|
727
|
+
for (let rightIndex = leftIndex + 1; rightIndex < hubLikeCurrent.length; rightIndex += 1) {
|
|
728
|
+
const left = hubLikeCurrent[leftIndex];
|
|
729
|
+
const right = hubLikeCurrent[rightIndex];
|
|
730
|
+
const ratio = overlapRatio(left.subjectIds, right.subjectIds);
|
|
731
|
+
const titleMatch = normalizeText(left.title) && normalizeText(right.title)
|
|
732
|
+
? left.title.toLowerCase().split(/\s+/u)[0] === right.title.toLowerCase().split(/\s+/u)[0]
|
|
733
|
+
: false;
|
|
734
|
+
if (ratio >= 0.5 || (ratio > 0 && titleMatch)) {
|
|
735
|
+
hints.push({
|
|
736
|
+
hintId: `merge:${left.surfaceId}:${right.surfaceId}`,
|
|
737
|
+
hintKind: "merge",
|
|
738
|
+
leftSurfaceId: left.surfaceId,
|
|
739
|
+
rightSurfaceId: right.surfaceId,
|
|
740
|
+
summary: `Consider merging ${left.surfaceId} and ${right.surfaceId} because their subject coverage overlaps`,
|
|
741
|
+
overlapRatio: Number(ratio.toFixed(3)),
|
|
742
|
+
sourcePaths: [left.metaPath, right.metaPath, left.markdownPath, right.markdownPath].filter((value) => typeof value === "string" && value.trim().length > 0),
|
|
743
|
+
evidenceRefs: [
|
|
744
|
+
toEvidenceRef(relativeWorkspacePath(left.metaPath ?? `${left.surfaceId}`, defaultWorkspaceRoot), `Merge hint left ${left.surfaceId}`),
|
|
745
|
+
toEvidenceRef(relativeWorkspacePath(right.metaPath ?? `${right.surfaceId}`, defaultWorkspaceRoot), `Merge hint right ${right.surfaceId}`),
|
|
746
|
+
toEvidenceRef("docs/architecture/graphify-bridge.md", "Graphify review surfaces can propose merges and splits without mutating live state."),
|
|
747
|
+
],
|
|
748
|
+
});
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const ocbHubLike = ocbRecords.filter((record) => ["hub_prior", "map_of_territory", "concept_page"].includes(record.kind));
|
|
755
|
+
for (const ocbRecord of ocbHubLike) {
|
|
756
|
+
const overlaps = hubLikeCurrent.filter((current) => overlapRatio(current.subjectIds, ocbRecord.subjectIds) > 0.25);
|
|
757
|
+
if (overlaps.length >= 2) {
|
|
758
|
+
hints.push({
|
|
759
|
+
hintId: `split:${ocbRecord.surfaceId}`,
|
|
760
|
+
hintKind: "split",
|
|
761
|
+
surfaceId: ocbRecord.surfaceId,
|
|
762
|
+
summary: `Consider splitting ${ocbRecord.surfaceId} because multiple current hubs overlap its subject space`,
|
|
763
|
+
overlapSurfaceIds: overlaps.map((record) => record.surfaceId),
|
|
764
|
+
sourcePaths: [ocbRecord.metaPath, ocbRecord.markdownPath].filter((value) => typeof value === "string" && value.trim().length > 0),
|
|
765
|
+
evidenceRefs: [
|
|
766
|
+
toEvidenceRef(relativeWorkspacePath(ocbRecord.metaPath ?? `${ocbRecord.surfaceId}`, defaultWorkspaceRoot), `Split hint on ${ocbRecord.surfaceId}`),
|
|
767
|
+
toEvidenceRef("docs/architecture/teacher-v3.md", "Structural proposals can review merge and split candidates off-path."),
|
|
768
|
+
],
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return hints;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function capFindingList(list, limit = 8) {
|
|
776
|
+
return {
|
|
777
|
+
items: list.slice(0, limit),
|
|
778
|
+
truncated: list.length > limit,
|
|
779
|
+
total: list.length,
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function buildSummaryMarkdown(report) {
|
|
784
|
+
const lines = [
|
|
785
|
+
"# Graphify × OCB maintenance diff",
|
|
786
|
+
"",
|
|
787
|
+
`- diff id: \`${report.diffId}\``,
|
|
788
|
+
`- proposal id: \`${report.proposalId}\``,
|
|
789
|
+
`- graphify root: \`${report.graphifyRoot}\``,
|
|
790
|
+
`- OCB root: \`${report.ocbRoot}\``,
|
|
791
|
+
`- current roots: ${report.currentBundleRoots.length}`,
|
|
792
|
+
`- OCB roots: ${report.ocbBundleRoots.length}`,
|
|
793
|
+
`- verdict: **${report.verdict.verdict}** (${report.verdict.severity})`,
|
|
794
|
+
`- current surfaces: ${report.counts.currentSurfaceCount}`,
|
|
795
|
+
`- OCB surfaces: ${report.counts.ocbSurfaceCount}`,
|
|
796
|
+
"",
|
|
797
|
+
"## Output classes",
|
|
798
|
+
`- missing_from_ocb: ${report.counts.missing_from_ocb}`,
|
|
799
|
+
`- stale_in_ocb: ${report.counts.stale_in_ocb}`,
|
|
800
|
+
`- candidate_only_edges_without_source_support: ${report.counts.candidate_only_edges_without_source_support}`,
|
|
801
|
+
`- new_current_source_hubs: ${report.counts.new_current_source_hubs}`,
|
|
802
|
+
`- provenance_gap_candidates: ${report.counts.provenance_gap_candidates}`,
|
|
803
|
+
`- possible merge/split review hints: ${report.counts.possible_merge_split_review_hints}`,
|
|
804
|
+
"",
|
|
805
|
+
];
|
|
806
|
+
|
|
807
|
+
const classSections = [
|
|
808
|
+
["missing_from_ocb", report.findings.missing_from_ocb.items],
|
|
809
|
+
["stale_in_ocb", report.findings.stale_in_ocb.items],
|
|
810
|
+
["candidate_only_edges_without_source_support", report.findings.candidate_only_edges_without_source_support.items],
|
|
811
|
+
["new_current_source_hubs", report.findings.new_current_source_hubs.items],
|
|
812
|
+
["provenance_gap_candidates", report.findings.provenance_gap_candidates.items],
|
|
813
|
+
["possible merge/split review hints", report.findings.possible_merge_split_review_hints.items],
|
|
814
|
+
];
|
|
815
|
+
for (const [label, items] of classSections) {
|
|
816
|
+
lines.push(`## ${label}`);
|
|
817
|
+
if (items.length === 0) {
|
|
818
|
+
lines.push("- none");
|
|
819
|
+
} else {
|
|
820
|
+
for (const item of items.slice(0, 6)) {
|
|
821
|
+
const id = item.surfaceId ?? item.edgeId ?? item.hintId ?? "unknown";
|
|
822
|
+
const title = item.title ?? item.summary ?? item.reason ?? id;
|
|
823
|
+
lines.push(`- \`${id}\` — ${title}`);
|
|
824
|
+
}
|
|
825
|
+
if (items.length > 6) {
|
|
826
|
+
lines.push(`- … ${items.length - 6} more`);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
lines.push("");
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
lines.push("## Proposal-suggestion posture");
|
|
833
|
+
lines.push("This lane only emits bounded diagnostics and proposal suggestions. It does not mutate live or candidate graph state.");
|
|
834
|
+
lines.push("");
|
|
835
|
+
return lines.join("\n");
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function buildProposalSuggestion(report) {
|
|
839
|
+
const suggestions = [];
|
|
840
|
+
const suggestionSpecs = [
|
|
841
|
+
["missing_from_ocb", "Add or explicitly exclude missing current surfaces from OCB inspectable bundles."],
|
|
842
|
+
["stale_in_ocb", "Refresh OCB inspectable bundles or correct the stale promotion surface."],
|
|
843
|
+
["candidate_only_edges_without_source_support", "Bind the candidate edge to source evidence or downgrade it to review-only."],
|
|
844
|
+
["new_current_source_hubs", "Review new current source hubs for promotion or merge into an existing hub."],
|
|
845
|
+
["provenance_gap_candidates", "Fill provenance fields before any later promotion or import claim."],
|
|
846
|
+
["possible_merge_split_review_hints", "Review overlapping hubs for merge/split before widening the surface."],
|
|
847
|
+
];
|
|
848
|
+
for (const [code, summary] of suggestionSpecs) {
|
|
849
|
+
const items = report.findings[code]?.items ?? [];
|
|
850
|
+
if (items.length === 0) {
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
853
|
+
const sample = items.slice(0, 3).map((item) => item.surfaceId ?? item.edgeId ?? item.hintId ?? "unknown");
|
|
854
|
+
suggestions.push({
|
|
855
|
+
suggestionId: `suggest:${code}:${slugify(report.diffId)}`,
|
|
856
|
+
code,
|
|
857
|
+
summary,
|
|
858
|
+
rationale: items[0].reason ?? items[0].summary ?? summary,
|
|
859
|
+
sampleIds: sample,
|
|
860
|
+
confidence: code === "provenance_gap_candidates" ? 0.96 : 0.9,
|
|
861
|
+
rollbackKey: report.rollbackKey,
|
|
862
|
+
reviewMode: "proposal_only",
|
|
863
|
+
targetStateOnly: true,
|
|
864
|
+
evidenceRefs: items[0].evidenceRefs ?? [],
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
return {
|
|
869
|
+
contract: "graphify_ocb_maintenance_diff_proposal_suggestion.v1",
|
|
870
|
+
diffId: report.diffId,
|
|
871
|
+
proposalId: report.proposalId,
|
|
872
|
+
rollbackKey: report.rollbackKey,
|
|
873
|
+
reviewMode: "proposal_only",
|
|
874
|
+
status: Object.values(report.counts).some((value) => Number(value ?? 0) > 0) ? "needs_review" : "clear",
|
|
875
|
+
summary: suggestions.length === 0
|
|
876
|
+
? "No maintenance suggestions were generated; current Graphify and OCB inspectable surfaces are aligned within the bounded checks used here."
|
|
877
|
+
: `${suggestions.length} proposal suggestions generated from bounded maintenance diff findings.`,
|
|
878
|
+
suggestionCount: suggestions.length,
|
|
879
|
+
suggestions,
|
|
880
|
+
counts: report.counts,
|
|
881
|
+
currentBundleRoots: report.currentBundleRoots,
|
|
882
|
+
ocbBundleRoots: report.ocbBundleRoots,
|
|
883
|
+
createdAt: report.createdAt,
|
|
884
|
+
updatedAt: report.updatedAt,
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function buildVerdict(report) {
|
|
889
|
+
const findingCount = Object.values(report.counts).reduce((total, value) => total + Number(value ?? 0), 0);
|
|
890
|
+
return {
|
|
891
|
+
contract: "graphify_ocb_maintenance_diff_verdict.v1",
|
|
892
|
+
diffId: report.diffId,
|
|
893
|
+
proposalId: report.proposalId,
|
|
894
|
+
verdict: findingCount > 0 ? "needs_review" : "clear",
|
|
895
|
+
severity: findingCount > 0 ? "warn" : "info",
|
|
896
|
+
findingCount,
|
|
897
|
+
proposalSuggestionCount: report.proposalSuggestion.suggestionCount,
|
|
898
|
+
currentSurfaceCount: report.counts.currentSurfaceCount,
|
|
899
|
+
ocbSurfaceCount: report.counts.ocbSurfaceCount,
|
|
900
|
+
why: findingCount > 0
|
|
901
|
+
? "bounded maintenance diagnostics identified current-vs-OCB surface drift"
|
|
902
|
+
: "bounded maintenance diagnostics found no surface drift requiring operator attention",
|
|
903
|
+
reviewMode: "proposal_only",
|
|
904
|
+
targetStateOnly: true,
|
|
905
|
+
rollbackKey: report.rollbackKey,
|
|
906
|
+
createdAt: report.createdAt,
|
|
907
|
+
updatedAt: report.updatedAt,
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function buildBundleDigest(files) {
|
|
912
|
+
const entries = Object.entries(files).sort(([left], [right]) => left.localeCompare(right));
|
|
913
|
+
const digest = createHash("sha256");
|
|
914
|
+
const fileHashes = {};
|
|
915
|
+
for (const [name, text] of entries) {
|
|
916
|
+
const hash = sha256Text(text);
|
|
917
|
+
digest.update(`${name}\0${hash}\n`);
|
|
918
|
+
fileHashes[name] = hash;
|
|
919
|
+
}
|
|
920
|
+
return {
|
|
921
|
+
bundleHash: `sha256:${digest.digest("hex")}`,
|
|
922
|
+
fileCount: entries.length,
|
|
923
|
+
files: fileHashes,
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function buildReportPayload(options) {
|
|
928
|
+
const repoRoot = path.resolve(options.repoRoot ?? defaultRepoRoot);
|
|
929
|
+
const workspaceRoot = path.resolve(options.workspaceRoot ?? defaultWorkspaceRoot);
|
|
930
|
+
const graphifyRoot = path.resolve(options.graphifyRoot ?? defaultGraphifyRoot);
|
|
931
|
+
const ocbRoot = path.resolve(options.ocbRoot ?? defaultOcbRoot);
|
|
932
|
+
|
|
933
|
+
const currentBundleRoots = discoverCurrentBundleRoots(graphifyRoot).map((root, index) => loadBundleSnapshot(root, `current_${index + 1}`));
|
|
934
|
+
const ocbBundleRoots = discoverOcbBundleRoots(ocbRoot).map((root, index) => loadBundleSnapshot(root, `ocb_${index + 1}`));
|
|
935
|
+
|
|
936
|
+
if (currentBundleRoots.length === 0) {
|
|
937
|
+
throw new Error(`graphify maintenance diff found no recognizable current Graphify bundle roots under ${graphifyRoot}`);
|
|
938
|
+
}
|
|
939
|
+
if (ocbBundleRoots.length === 0) {
|
|
940
|
+
throw new Error(`graphify maintenance diff found no recognizable OCB inspectable bundle roots under ${ocbRoot}`);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const currentRecords = uniqueBy(currentBundleRoots.flatMap((snapshot) => buildCurrentSurfaceIndex(snapshot)), surfaceKey);
|
|
944
|
+
const ocbRecords = ocbBundleRoots.flatMap((snapshot) => buildOcbSurfaceIndex(snapshot));
|
|
945
|
+
|
|
946
|
+
const currentById = choosePreferredRecordMap(currentRecords);
|
|
947
|
+
const ocbById = choosePreferredRecordMap(ocbRecords);
|
|
948
|
+
|
|
949
|
+
const currentRootSummaries = buildCurrentGraphRootsSummary(currentBundleRoots);
|
|
950
|
+
const ocbRootSummaries = buildOcbGraphRootsSummary(ocbBundleRoots);
|
|
951
|
+
|
|
952
|
+
const missingFromOcb = buildMissingFromOcbFindings(currentRecords, ocbById, currentRootSummaries, ocbRootSummaries);
|
|
953
|
+
const staleInOcb = buildStaleInOcbFindings(currentRecords, ocbRecords, currentById, ocbRootSummaries, currentRootSummaries);
|
|
954
|
+
const candidateOnlyEdgesWithoutSourceSupport = buildCandidateOnlyEdgeFindings(currentRecords);
|
|
955
|
+
const newCurrentSourceHubs = buildCurrentSourceHubFindings(currentRecords, ocbRecords, currentRootSummaries, ocbRootSummaries);
|
|
956
|
+
const provenanceGapCandidates = buildProvenanceGapFindings(currentRecords, ocbRecords);
|
|
957
|
+
const possibleMergeSplitReviewHints = buildMergeSplitHints(currentRecords, ocbRecords);
|
|
958
|
+
|
|
959
|
+
const report = {
|
|
960
|
+
contract: "graphify_ocb_maintenance_diff.v1",
|
|
961
|
+
diffId: normalizeText(options.diffId) ?? `graphify-maintenance-diff-${slugify(options.runId ?? timestampToken(new Date().toISOString()))}`,
|
|
962
|
+
proposalId: normalizeText(options.proposalId) ?? `prop_graphify_maintenance_diff_${slugify(options.runId ?? timestampToken(new Date().toISOString()))}`,
|
|
963
|
+
rollbackKey: normalizeText(options.rollbackKey) ?? `rollback:graphify-maintenance-diff:${slugify(options.runId ?? timestampToken(new Date().toISOString()))}`,
|
|
964
|
+
graphifyRoot: relativeWorkspacePath(graphifyRoot, workspaceRoot),
|
|
965
|
+
ocbRoot: relativeWorkspacePath(ocbRoot, workspaceRoot),
|
|
966
|
+
repoRoot: relativeWorkspacePath(repoRoot, workspaceRoot),
|
|
967
|
+
workspaceRoot: relativeWorkspacePath(workspaceRoot, workspaceRoot),
|
|
968
|
+
currentBundleRoots: currentRootSummaries,
|
|
969
|
+
ocbBundleRoots: ocbRootSummaries,
|
|
970
|
+
counts: {
|
|
971
|
+
currentSurfaceCount: currentRecords.length,
|
|
972
|
+
ocbSurfaceCount: ocbRecords.length,
|
|
973
|
+
missing_from_ocb: missingFromOcb.length,
|
|
974
|
+
stale_in_ocb: staleInOcb.length,
|
|
975
|
+
candidate_only_edges_without_source_support: candidateOnlyEdgesWithoutSourceSupport.length,
|
|
976
|
+
new_current_source_hubs: newCurrentSourceHubs.length,
|
|
977
|
+
provenance_gap_candidates: provenanceGapCandidates.length,
|
|
978
|
+
possible_merge_split_review_hints: possibleMergeSplitReviewHints.length,
|
|
979
|
+
},
|
|
980
|
+
findings: {
|
|
981
|
+
missing_from_ocb: capFindingList(missingFromOcb),
|
|
982
|
+
stale_in_ocb: capFindingList(staleInOcb),
|
|
983
|
+
candidate_only_edges_without_source_support: capFindingList(candidateOnlyEdgesWithoutSourceSupport),
|
|
984
|
+
new_current_source_hubs: capFindingList(newCurrentSourceHubs),
|
|
985
|
+
provenance_gap_candidates: capFindingList(provenanceGapCandidates),
|
|
986
|
+
possible_merge_split_review_hints: capFindingList(possibleMergeSplitReviewHints),
|
|
987
|
+
},
|
|
988
|
+
evidenceRefs: baseEvidenceRefs(currentBundleRoots, ocbBundleRoots),
|
|
989
|
+
createdAt: new Date().toISOString(),
|
|
990
|
+
updatedAt: new Date().toISOString(),
|
|
991
|
+
sourceUniverse: {
|
|
992
|
+
currentSurfaceIds: currentRecords.slice(0, 40).map((record) => record.surfaceId),
|
|
993
|
+
ocbSurfaceIds: ocbRecords.slice(0, 40).map((record) => record.surfaceId),
|
|
994
|
+
},
|
|
995
|
+
};
|
|
996
|
+
|
|
997
|
+
const proposalSuggestion = buildProposalSuggestion(report);
|
|
998
|
+
const verdict = buildVerdict({ ...report, proposalSuggestion });
|
|
999
|
+
const finalReport = {
|
|
1000
|
+
...report,
|
|
1001
|
+
proposalSuggestion,
|
|
1002
|
+
verdict,
|
|
1003
|
+
};
|
|
1004
|
+
finalReport.summary = buildSummaryMarkdown(finalReport);
|
|
1005
|
+
return {
|
|
1006
|
+
report: finalReport,
|
|
1007
|
+
proposalSuggestion,
|
|
1008
|
+
verdict,
|
|
1009
|
+
currentRecords,
|
|
1010
|
+
ocbRecords,
|
|
1011
|
+
currentBundleRoots,
|
|
1012
|
+
ocbBundleRoots,
|
|
1013
|
+
currentRootSummaries,
|
|
1014
|
+
ocbRootSummaries,
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function buildFilesFromPayload(payload) {
|
|
1019
|
+
const summary = buildSummaryMarkdown(payload.report);
|
|
1020
|
+
payload.report.summary = summary;
|
|
1021
|
+
return {
|
|
1022
|
+
[GRAPHIFY_MAINTENANCE_DIFF_LAYOUT_V1.maintenanceDiff]: stableJson(payload.report),
|
|
1023
|
+
[GRAPHIFY_MAINTENANCE_DIFF_LAYOUT_V1.summary]: summary,
|
|
1024
|
+
[GRAPHIFY_MAINTENANCE_DIFF_LAYOUT_V1.proposalSuggestion]: stableJson(payload.proposalSuggestion),
|
|
1025
|
+
[GRAPHIFY_MAINTENANCE_DIFF_LAYOUT_V1.verdict]: stableJson(payload.verdict),
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
export function buildGraphifyMaintenanceDiffBundle(options = {}) {
|
|
1030
|
+
const repoRoot = path.resolve(options.repoRoot ?? defaultRepoRoot);
|
|
1031
|
+
const workspaceRoot = path.resolve(options.workspaceRoot ?? defaultWorkspaceRoot);
|
|
1032
|
+
const graphifyRoot = path.resolve(options.graphifyRoot ?? defaultGraphifyRoot);
|
|
1033
|
+
const ocbRoot = path.resolve(options.ocbRoot ?? defaultOcbRoot);
|
|
1034
|
+
const outputRoot = path.resolve(options.outputRoot ?? defaultOutputRoot);
|
|
1035
|
+
const runId = normalizeText(options.runId) ?? `graphify-maintenance-diff-${timestampToken(new Date().toISOString())}`;
|
|
1036
|
+
const outputDir = path.join(outputRoot, runId);
|
|
1037
|
+
const payload = buildReportPayload({
|
|
1038
|
+
...options,
|
|
1039
|
+
repoRoot,
|
|
1040
|
+
workspaceRoot,
|
|
1041
|
+
graphifyRoot,
|
|
1042
|
+
ocbRoot,
|
|
1043
|
+
runId,
|
|
1044
|
+
});
|
|
1045
|
+
const files = buildFilesFromPayload(payload);
|
|
1046
|
+
const digest = buildBundleDigest(files);
|
|
1047
|
+
payload.report.bundleHash = digest.bundleHash;
|
|
1048
|
+
payload.proposalSuggestion.bundleHash = digest.bundleHash;
|
|
1049
|
+
payload.verdict.bundleHash = digest.bundleHash;
|
|
1050
|
+
const finalFiles = buildFilesFromPayload(payload);
|
|
1051
|
+
const finalDigest = buildBundleDigest(finalFiles);
|
|
1052
|
+
const paths = {
|
|
1053
|
+
maintenanceDiff: path.join(outputDir, GRAPHIFY_MAINTENANCE_DIFF_LAYOUT_V1.maintenanceDiff),
|
|
1054
|
+
summary: path.join(outputDir, GRAPHIFY_MAINTENANCE_DIFF_LAYOUT_V1.summary),
|
|
1055
|
+
proposalSuggestion: path.join(outputDir, GRAPHIFY_MAINTENANCE_DIFF_LAYOUT_V1.proposalSuggestion),
|
|
1056
|
+
verdict: path.join(outputDir, GRAPHIFY_MAINTENANCE_DIFF_LAYOUT_V1.verdict),
|
|
1057
|
+
};
|
|
1058
|
+
return {
|
|
1059
|
+
ok: true,
|
|
1060
|
+
runId,
|
|
1061
|
+
diffId: payload.report.diffId,
|
|
1062
|
+
proposalId: payload.report.proposalId,
|
|
1063
|
+
rollbackKey: payload.report.rollbackKey,
|
|
1064
|
+
repoRoot,
|
|
1065
|
+
workspaceRoot,
|
|
1066
|
+
graphifyRoot,
|
|
1067
|
+
ocbRoot,
|
|
1068
|
+
outputRoot,
|
|
1069
|
+
outputDir,
|
|
1070
|
+
report: payload.report,
|
|
1071
|
+
proposalSuggestion: payload.proposalSuggestion,
|
|
1072
|
+
verdict: payload.verdict,
|
|
1073
|
+
summary: payload.report.summary,
|
|
1074
|
+
files: finalFiles,
|
|
1075
|
+
paths,
|
|
1076
|
+
digest: finalDigest,
|
|
1077
|
+
currentRecords: payload.currentRecords,
|
|
1078
|
+
ocbRecords: payload.ocbRecords,
|
|
1079
|
+
currentBundleRoots: payload.currentRootSummaries,
|
|
1080
|
+
ocbBundleRoots: payload.ocbRootSummaries,
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
export function writeGraphifyMaintenanceDiffBundle(outputDir, bundle) {
|
|
1085
|
+
rmSync(outputDir, { recursive: true, force: true });
|
|
1086
|
+
ensureDir(outputDir);
|
|
1087
|
+
const writtenFiles = [];
|
|
1088
|
+
for (const [name, text] of Object.entries(bundle.files)) {
|
|
1089
|
+
const filePath = path.join(outputDir, name);
|
|
1090
|
+
writeText(filePath, text);
|
|
1091
|
+
writtenFiles.push(filePath);
|
|
1092
|
+
}
|
|
1093
|
+
return {
|
|
1094
|
+
writtenFiles,
|
|
1095
|
+
fileCount: writtenFiles.length,
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
export function parseGraphifyMaintenanceDiffCliArgs(argv) {
|
|
1100
|
+
let graphifyRoot = defaultGraphifyRoot;
|
|
1101
|
+
let ocbRoot = defaultOcbRoot;
|
|
1102
|
+
let repoRoot = defaultRepoRoot;
|
|
1103
|
+
let workspaceRoot = defaultWorkspaceRoot;
|
|
1104
|
+
let outputRoot = null;
|
|
1105
|
+
let runId = null;
|
|
1106
|
+
let help = false;
|
|
1107
|
+
let json = false;
|
|
1108
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
1109
|
+
const arg = argv[index];
|
|
1110
|
+
if (arg === "--help" || arg === "-h") {
|
|
1111
|
+
help = true;
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
if (arg === "--json") {
|
|
1115
|
+
json = true;
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
if (arg === "--graphify-root" || arg === "--bundle-root") {
|
|
1119
|
+
const next = argv[index + 1];
|
|
1120
|
+
if (next === undefined) {
|
|
1121
|
+
throw new Error(`${arg} requires a value`);
|
|
1122
|
+
}
|
|
1123
|
+
graphifyRoot = next;
|
|
1124
|
+
index += 1;
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
if (arg === "--ocb-root") {
|
|
1128
|
+
const next = argv[index + 1];
|
|
1129
|
+
if (next === undefined) {
|
|
1130
|
+
throw new Error("--ocb-root requires a value");
|
|
1131
|
+
}
|
|
1132
|
+
ocbRoot = next;
|
|
1133
|
+
index += 1;
|
|
1134
|
+
continue;
|
|
1135
|
+
}
|
|
1136
|
+
if (arg === "--repo-root") {
|
|
1137
|
+
const next = argv[index + 1];
|
|
1138
|
+
if (next === undefined) {
|
|
1139
|
+
throw new Error("--repo-root requires a value");
|
|
1140
|
+
}
|
|
1141
|
+
repoRoot = next;
|
|
1142
|
+
index += 1;
|
|
1143
|
+
continue;
|
|
1144
|
+
}
|
|
1145
|
+
if (arg === "--workspace-root") {
|
|
1146
|
+
const next = argv[index + 1];
|
|
1147
|
+
if (next === undefined) {
|
|
1148
|
+
throw new Error("--workspace-root requires a value");
|
|
1149
|
+
}
|
|
1150
|
+
workspaceRoot = next;
|
|
1151
|
+
index += 1;
|
|
1152
|
+
continue;
|
|
1153
|
+
}
|
|
1154
|
+
if (arg === "--output-root") {
|
|
1155
|
+
const next = argv[index + 1];
|
|
1156
|
+
if (next === undefined) {
|
|
1157
|
+
throw new Error("--output-root requires a value");
|
|
1158
|
+
}
|
|
1159
|
+
outputRoot = next;
|
|
1160
|
+
index += 1;
|
|
1161
|
+
continue;
|
|
1162
|
+
}
|
|
1163
|
+
if (arg === "--run-id") {
|
|
1164
|
+
const next = argv[index + 1];
|
|
1165
|
+
if (next === undefined) {
|
|
1166
|
+
throw new Error("--run-id requires a value");
|
|
1167
|
+
}
|
|
1168
|
+
runId = next;
|
|
1169
|
+
index += 1;
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1172
|
+
throw new Error(`unknown argument for graphify-maintenance-diff: ${arg}`);
|
|
1173
|
+
}
|
|
1174
|
+
return {
|
|
1175
|
+
command: "graphify-maintenance-diff",
|
|
1176
|
+
graphifyRoot: path.resolve(graphifyRoot),
|
|
1177
|
+
ocbRoot: path.resolve(ocbRoot),
|
|
1178
|
+
repoRoot: path.resolve(repoRoot),
|
|
1179
|
+
workspaceRoot: path.resolve(workspaceRoot),
|
|
1180
|
+
outputRoot: outputRoot === null ? null : path.resolve(outputRoot),
|
|
1181
|
+
runId,
|
|
1182
|
+
json,
|
|
1183
|
+
help,
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
export function formatGraphifyMaintenanceDiffSummary(result) {
|
|
1188
|
+
const lines = [
|
|
1189
|
+
"GRAPHIFY MAINTENANCE DIFF ok",
|
|
1190
|
+
` Diff id: ${result.report.diffId}`,
|
|
1191
|
+
` Proposal: ${result.report.proposalId}`,
|
|
1192
|
+
` Graphify root: ${result.report.graphifyRoot}`,
|
|
1193
|
+
` OCB root: ${result.report.ocbRoot}`,
|
|
1194
|
+
` Current roots: ${result.currentBundleRoots.length}`,
|
|
1195
|
+
` OCB roots: ${result.ocbBundleRoots.length}`,
|
|
1196
|
+
` Verdict: ${result.verdict.verdict} (${result.verdict.severity})`,
|
|
1197
|
+
` Current surfaces: ${result.report.counts.currentSurfaceCount}`,
|
|
1198
|
+
` OCB surfaces: ${result.report.counts.ocbSurfaceCount}`,
|
|
1199
|
+
` Output root: ${result.outputRoot}`,
|
|
1200
|
+
` Report: ${result.paths.maintenanceDiff}`,
|
|
1201
|
+
` Proposal suggestion: ${result.paths.proposalSuggestion}`,
|
|
1202
|
+
` Verdict file: ${result.paths.verdict}`,
|
|
1203
|
+
" Findings:",
|
|
1204
|
+
];
|
|
1205
|
+
for (const [code, count] of Object.entries(result.report.counts)) {
|
|
1206
|
+
if (code === "currentSurfaceCount" || code === "ocbSurfaceCount") {
|
|
1207
|
+
continue;
|
|
1208
|
+
}
|
|
1209
|
+
lines.push(` - ${code}: ${count}`);
|
|
1210
|
+
}
|
|
1211
|
+
const topFindingLines = [
|
|
1212
|
+
["missing_from_ocb", result.report.findings.missing_from_ocb.items],
|
|
1213
|
+
["stale_in_ocb", result.report.findings.stale_in_ocb.items],
|
|
1214
|
+
["candidate_only_edges_without_source_support", result.report.findings.candidate_only_edges_without_source_support.items],
|
|
1215
|
+
["new_current_source_hubs", result.report.findings.new_current_source_hubs.items],
|
|
1216
|
+
["provenance_gap_candidates", result.report.findings.provenance_gap_candidates.items],
|
|
1217
|
+
["possible merge/split review hints", result.report.findings.possible_merge_split_review_hints.items],
|
|
1218
|
+
];
|
|
1219
|
+
for (const [label, items] of topFindingLines) {
|
|
1220
|
+
if (items.length === 0) {
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
lines.push(` ${label}: ${items[0].surfaceId ?? items[0].edgeId ?? items[0].hintId ?? "unknown"}`);
|
|
1224
|
+
}
|
|
1225
|
+
return `${lines.join("\n")}\n`;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
export function runGraphifyMaintenanceDiff(argvOrOptions = {}) {
|
|
1229
|
+
const parsed = Array.isArray(argvOrOptions)
|
|
1230
|
+
? parseGraphifyMaintenanceDiffCliArgs(argvOrOptions)
|
|
1231
|
+
: { command: "graphify-maintenance-diff", json: false, help: false, ...argvOrOptions };
|
|
1232
|
+
if (parsed.help) {
|
|
1233
|
+
return {
|
|
1234
|
+
ok: true,
|
|
1235
|
+
help: true,
|
|
1236
|
+
summary: "",
|
|
1237
|
+
report: null,
|
|
1238
|
+
proposalSuggestion: null,
|
|
1239
|
+
verdict: null,
|
|
1240
|
+
paths: null,
|
|
1241
|
+
outputRoot: null,
|
|
1242
|
+
outputDir: null,
|
|
1243
|
+
graphifyRoot: null,
|
|
1244
|
+
ocbRoot: null,
|
|
1245
|
+
repoRoot: null,
|
|
1246
|
+
workspaceRoot: null,
|
|
1247
|
+
runId: null,
|
|
1248
|
+
diffId: null,
|
|
1249
|
+
proposalId: null,
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
const result = buildGraphifyMaintenanceDiffBundle({
|
|
1253
|
+
graphifyRoot: parsed.graphifyRoot,
|
|
1254
|
+
ocbRoot: parsed.ocbRoot,
|
|
1255
|
+
repoRoot: parsed.repoRoot,
|
|
1256
|
+
workspaceRoot: parsed.workspaceRoot,
|
|
1257
|
+
outputRoot: parsed.outputRoot ?? undefined,
|
|
1258
|
+
runId: parsed.runId ?? undefined,
|
|
1259
|
+
});
|
|
1260
|
+
writeGraphifyMaintenanceDiffBundle(result.outputDir, result);
|
|
1261
|
+
return {
|
|
1262
|
+
...result,
|
|
1263
|
+
json: Boolean(parsed.json),
|
|
1264
|
+
summary: formatGraphifyMaintenanceDiffSummary(result),
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
function main() {
|
|
1269
|
+
const result = runGraphifyMaintenanceDiff(process.argv.slice(2));
|
|
1270
|
+
if (result.help) {
|
|
1271
|
+
process.stdout.write([
|
|
1272
|
+
"Usage:",
|
|
1273
|
+
" node scripts/graphify-maintenance-diff.mjs --graphify-root <path> --ocb-root <path> [--repo-root <path>] [--workspace-root <path>] [--output-root <path>] [--run-id <id>] [--json]",
|
|
1274
|
+
"",
|
|
1275
|
+
"This maintenance diff lane emits bounded operator diagnostics and proposal suggestions only.",
|
|
1276
|
+
].join("\n") + "\n");
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
if (result.json) {
|
|
1280
|
+
process.stdout.write(`${stableJson({ ok: result.ok, report: result.report, proposalSuggestion: result.proposalSuggestion, verdict: result.verdict, paths: result.paths })}\n`);
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
process.stdout.write(result.summary);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
1287
|
+
main();
|
|
1288
|
+
}
|