@bragi-gmbh/codebus 1.6.5 → 1.6.7
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/commands/graph-pull.js +37 -6
- package/dist/commands/graph-pull.js.map +1 -1
- package/dist/commands/graph-versions-cmd.js +67 -0
- package/dist/commands/graph-versions-cmd.js.map +1 -0
- package/dist/commands/graph.js +122 -2
- package/dist/commands/graph.js.map +1 -1
- package/dist/commands/teams-link.js +24 -0
- package/dist/commands/teams-link.js.map +1 -0
- package/dist/graph-remote.js +151 -8
- package/dist/graph-remote.js.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/kit/graph-publish.mjs +359 -13
- package/package.json +1 -1
|
@@ -79,15 +79,20 @@
|
|
|
79
79
|
* against a fixture, per the build ticket).
|
|
80
80
|
* --skip-build — same effect, reads graphify-out/graph.json
|
|
81
81
|
* as-is without re-running graphify.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
* --dry-run — do everything except the final
|
|
83
|
+
* graph_ingest_with_token call (still reports
|
|
84
|
+
* the head and still validates). Reports the
|
|
85
|
+
* post-compaction size against the 25 MB
|
|
86
|
+
* ingest cap (PASS/FAIL) so pre-builds know.
|
|
87
|
+
* --no-compact — disable artifact compaction (compaction is
|
|
88
|
+
* ON by default).
|
|
85
89
|
*/
|
|
86
90
|
|
|
87
91
|
import { execFileSync } from "node:child_process";
|
|
88
92
|
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
89
93
|
import { join, resolve, relative, isAbsolute } from "node:path";
|
|
90
94
|
import { pathToFileURL } from "node:url";
|
|
95
|
+
import { createHash } from "node:crypto";
|
|
91
96
|
import { createClient } from "@supabase/supabase-js";
|
|
92
97
|
|
|
93
98
|
const args = process.argv.slice(2);
|
|
@@ -97,7 +102,233 @@ const opt = (name) => {
|
|
|
97
102
|
return i >= 0 ? args[i + 1] : undefined;
|
|
98
103
|
};
|
|
99
104
|
|
|
105
|
+
// Artifact size cap + compaction (25 MB ingest cap, 0050
|
|
106
|
+
// graph_ingest_artifact: octet_length(p_artifact::text) > 25MB rejects).
|
|
107
|
+
// EXACT stored/read field map, derived from the RPC bodies — the compactor
|
|
108
|
+
// below must keep every one of these byte-identical and may drop anything
|
|
109
|
+
// else:
|
|
110
|
+
//
|
|
111
|
+
// 0050 graph_ingest_validate: nodes[] needs id/label/file_type (+optional
|
|
112
|
+
// tier); links[] (or edges[]) needs source/target/relation/confidence
|
|
113
|
+
// (+owner_source when relation=owned_by); hyperedges[] needs
|
|
114
|
+
// id/label/relation/confidence + non-empty nodes[] (+optional tier via
|
|
115
|
+
// confidence). Top level needs nodes + links/edges arrays (hyperedges
|
|
116
|
+
// optional).
|
|
117
|
+
// 0063 graph_content_sync (the ONLY projection into graph_nodes/graph_edges,
|
|
118
|
+
// full-replace on each promoted ingest): nodes keep id, label, file_type,
|
|
119
|
+
// source_file, source_location, community, norm_label, _origin, tier,
|
|
120
|
+
// metadata; edges keep source, target, relation, confidence,
|
|
121
|
+
// confidence_score, weight, source_file, context, owner_source.
|
|
122
|
+
// Hyperedges are NOT projected into tables but ARE persisted verbatim in
|
|
123
|
+
// the content-addressed graph_artifacts row — kept whole.
|
|
124
|
+
// 20260922230000 graph_read_contract_v1 (signature/doc derivation): reads
|
|
125
|
+
// ONLY meta->>signature/def and meta->>doc/docstring/description, i.e.
|
|
126
|
+
// keys INSIDE the node's metadata object — metadata is preserved whole,
|
|
127
|
+
// so the derivation is unaffected.
|
|
128
|
+
//
|
|
129
|
+
// Everything else observed in real artifacts is dropped: top-level extras
|
|
130
|
+
// (directed, multigraph, graph — which DUPLICATES hyperedges —,
|
|
131
|
+
// built_at_commit), stray per-node keys (e.g. source_url/captured_at/
|
|
132
|
+
// author/contributor), per-edge source_location (never read or stored).
|
|
133
|
+
// Minification comes free via JSON.stringify (no whitespace).
|
|
134
|
+
//
|
|
135
|
+
// Exported for scripts/test/graph-compact.validate.test.mjs (same
|
|
136
|
+
// import.meta.url guard convention as resolveRepoRoot above).
|
|
137
|
+
export const GRAPH_ARTIFACT_SIZE_CAP_BYTES = 25 * 1024 * 1024;
|
|
138
|
+
export const GRAPH_COMPACT_KEEP_NODE_FIELDS = new Set([
|
|
139
|
+
"id", "label", "file_type", "source_file", "source_location",
|
|
140
|
+
"community", "norm_label", "_origin", "tier", "metadata",
|
|
141
|
+
]);
|
|
142
|
+
export const GRAPH_COMPACT_KEEP_EDGE_FIELDS = new Set([
|
|
143
|
+
"source", "target", "relation", "confidence", "confidence_score",
|
|
144
|
+
"weight", "source_file", "context", "owner_source",
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
export function compactArtifact(artifact) {
|
|
148
|
+
const removedBytes = {};
|
|
149
|
+
const dropStr = (obj, keepSet, section) => {
|
|
150
|
+
const out = {};
|
|
151
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
152
|
+
if (keepSet.has(k)) {
|
|
153
|
+
out[k] = v;
|
|
154
|
+
} else {
|
|
155
|
+
const b = Buffer.byteLength(JSON.stringify({ [k]: v }), "utf-8");
|
|
156
|
+
removedBytes[`${section}.${k}`] = (removedBytes[`${section}.${k}`] ?? 0) + b;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
};
|
|
161
|
+
const compacted = {};
|
|
162
|
+
// Top level: keep exactly what the RPCs read. Prefer `links` (graphify's
|
|
163
|
+
// real key); keep `edges` too if present so a future rename is not
|
|
164
|
+
// penalized — same acceptance rule as graph_ingest_validate.
|
|
165
|
+
for (const [k, v] of Object.entries(artifact)) {
|
|
166
|
+
if (k === "nodes" || k === "links" || k === "edges" || k === "hyperedges") {
|
|
167
|
+
compacted[k] = v;
|
|
168
|
+
} else {
|
|
169
|
+
const b = Buffer.byteLength(JSON.stringify({ [k]: v }), "utf-8");
|
|
170
|
+
removedBytes[`top.${k}`] = (removedBytes[`top.${k}`] ?? 0) + b;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (Array.isArray(compacted.nodes)) {
|
|
174
|
+
compacted.nodes = compacted.nodes.map((n) => dropStr(n, GRAPH_COMPACT_KEEP_NODE_FIELDS, "node"));
|
|
175
|
+
}
|
|
176
|
+
for (const key of ["links", "edges"]) {
|
|
177
|
+
if (Array.isArray(compacted[key])) {
|
|
178
|
+
compacted[key] = compacted[key].map((e) => dropStr(e, GRAPH_COMPACT_KEEP_EDGE_FIELDS, "edge"));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Hyperedges are persisted verbatim (content-addressed artifact) — never
|
|
182
|
+
// stripped, only minified by the final stringify.
|
|
183
|
+
return { compacted, removedBytes };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function artifactBytes(obj) {
|
|
187
|
+
return Buffer.byteLength(JSON.stringify(obj), "utf-8");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export const GRAPH_CHUNK_PART_MAX_BYTES = 8 * 1024 * 1024;
|
|
191
|
+
|
|
192
|
+
// buildCanonicalText: THE canonical serialization for chunked ingest.
|
|
193
|
+
// Returns { head, chunks, str, sha256, counts, meta } where
|
|
194
|
+
// str = head + chunks.join("") + '}'
|
|
195
|
+
// byte-identically to what graph_ingest_commit_with_token reassembles
|
|
196
|
+
// (head + verbatim-concatenated textchunk payloads + '}'):
|
|
197
|
+
// head = {"directed":<d>,"multigraph":<m>,"graph":<g>,
|
|
198
|
+
// chunks are verbatim slices of the body
|
|
199
|
+
// '"nodes":[...],"links":[...],"hyperedges":[...]' (WITHOUT the trailing
|
|
200
|
+
// brace), split only at element boundaries (never mid-codepoint: element
|
|
201
|
+
// granularity via JSON.stringify per element). Inter-element and
|
|
202
|
+
// inter-section commas live INSIDE the chunk bytes — no join separator is
|
|
203
|
+
// added on either side. sha256 is over str. The commit RPC verifies sha
|
|
204
|
+
// BEFORE parsing, so these bytes round-trip untouched (no jsonb ::text
|
|
205
|
+
// re-serialization involved).
|
|
206
|
+
//
|
|
207
|
+
// Number-format contract: JSON.stringify emits integral floats as "1" (not
|
|
208
|
+
// "1.0"), matching Postgres jsonb numeric output; the server never
|
|
209
|
+
// re-serializes before hashing, so client bytes are authoritative.
|
|
210
|
+
export function buildCanonicalText(artifact, maxBytes = GRAPH_CHUNK_PART_MAX_BYTES) {
|
|
211
|
+
const links = Array.isArray(artifact.links) ? artifact.links
|
|
212
|
+
: Array.isArray(artifact.edges) ? artifact.edges : [];
|
|
213
|
+
const nodes = Array.isArray(artifact.nodes) ? artifact.nodes : [];
|
|
214
|
+
const hyperedges = Array.isArray(artifact.hyperedges) ? artifact.hyperedges : [];
|
|
215
|
+
const head =
|
|
216
|
+
'{"directed":' + JSON.stringify(artifact.directed ?? false) +
|
|
217
|
+
',"multigraph":' + JSON.stringify(artifact.multigraph ?? false) +
|
|
218
|
+
',"graph":' + JSON.stringify(artifact.graph ?? {}) + ',';
|
|
219
|
+
// Split one JSON array's elements into verbatim slices <= maxBytes.
|
|
220
|
+
// MIDDLE slices (neither first nor last of their array) are bare element
|
|
221
|
+
// runs with NO key prefix and NO closing bracket — they are only valid
|
|
222
|
+
// concatenated between their siblings. The FIRST slice carries the
|
|
223
|
+
// '"name":[' prefix; the LAST slice carries the ']' suffix; a single-slice
|
|
224
|
+
// array carries both. Slices therefore must NEVER merge across arrays:
|
|
225
|
+
// each array's slices form one ordered group, groups join with ','.
|
|
226
|
+
const splitArray = (name, elements) => {
|
|
227
|
+
const prefix = '"' + name + '":[';
|
|
228
|
+
if (elements.length === 0) return [prefix + "]"];
|
|
229
|
+
const slices = [];
|
|
230
|
+
let cur = prefix, first = true;
|
|
231
|
+
for (const el of elements) {
|
|
232
|
+
const piece = (first ? "" : ",") + JSON.stringify(el);
|
|
233
|
+
if (!first && Buffer.byteLength(cur + piece + "]", "utf-8") > maxBytes) {
|
|
234
|
+
slices.push(cur);
|
|
235
|
+
cur = JSON.stringify(el);
|
|
236
|
+
first = false;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
cur += piece;
|
|
240
|
+
first = false;
|
|
241
|
+
}
|
|
242
|
+
slices.push(cur + "]");
|
|
243
|
+
return slices;
|
|
244
|
+
};
|
|
245
|
+
// Groups join with ',' — the comma is OUTSIDE the chunk bytes (it is the
|
|
246
|
+
// join separator on both sides: server comma-joins too)... NO — verbatim
|
|
247
|
+
// protocol says no separators are added. Resolve: keep commas inside by
|
|
248
|
+
// attaching the trailing comma to each group except the last: group text
|
|
249
|
+
// is slices.join(",") and groups are concatenated directly.
|
|
250
|
+
const groups = [splitArray("nodes", nodes), splitArray("links", links), splitArray("hyperedges", hyperedges)];
|
|
251
|
+
const parts = [];
|
|
252
|
+
groups.forEach((slices, gi) => {
|
|
253
|
+
const lastGroup = gi === groups.length - 1;
|
|
254
|
+
slices.forEach((sl, i) => {
|
|
255
|
+
const lastSlice = i === slices.length - 1;
|
|
256
|
+
// Attach inter-group comma to the group's final slice (except the
|
|
257
|
+
// very last slice overall); inter-slice commas within a group attach
|
|
258
|
+
// to each slice except the group's last.
|
|
259
|
+
let chunk = sl;
|
|
260
|
+
if (!lastSlice) chunk = chunk + ",";
|
|
261
|
+
else if (!lastGroup) chunk = chunk + ",";
|
|
262
|
+
parts.push(chunk);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
// Merge SMALL adjacent chunks (verbatim concat stays valid: commas are
|
|
266
|
+
// inside the bytes, so plain concatenation preserves the text).
|
|
267
|
+
const merged = [];
|
|
268
|
+
for (const c of parts) {
|
|
269
|
+
if (merged.length > 0 && Buffer.byteLength(merged[merged.length - 1] + c, "utf-8") <= maxBytes) {
|
|
270
|
+
merged[merged.length - 1] += c;
|
|
271
|
+
} else merged.push(c);
|
|
272
|
+
}
|
|
273
|
+
const body = merged.join("");
|
|
274
|
+
const str = head + body + "}";
|
|
275
|
+
return {
|
|
276
|
+
head,
|
|
277
|
+
meta: { directed: artifact.directed ?? false, multigraph: artifact.multigraph ?? false, graph: artifact.graph ?? {} },
|
|
278
|
+
chunks: merged,
|
|
279
|
+
str,
|
|
280
|
+
sha256: createHash("sha256").update(str, "utf-8").digest("hex"),
|
|
281
|
+
counts: { nodes: nodes.length, links: links.length, hyperedges: hyperedges.length },
|
|
282
|
+
};
|
|
283
|
+
const sha256 = createHash("sha256").update(str, "utf-8").digest("hex");
|
|
284
|
+
return {
|
|
285
|
+
head,
|
|
286
|
+
meta: { directed: artifact.directed ?? false, multigraph: artifact.multigraph ?? false, graph: artifact.graph ?? {} },
|
|
287
|
+
chunks: parts,
|
|
288
|
+
str,
|
|
289
|
+
sha256,
|
|
290
|
+
counts: { nodes: nodes.length, links: links.length, hyperedges: hyperedges.length },
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function chunkArray(elements, maxBytes = GRAPH_CHUNK_PART_MAX_BYTES) {
|
|
295
|
+
const parts = [];
|
|
296
|
+
let cur = [];
|
|
297
|
+
let curBytes = 2; // "[]"
|
|
298
|
+
for (const el of elements) {
|
|
299
|
+
const elBytes = Buffer.byteLength(JSON.stringify(el), "utf-8");
|
|
300
|
+
const add = (cur.length === 0 ? elBytes : elBytes + 1);
|
|
301
|
+
if (cur.length > 0 && curBytes + add > maxBytes) {
|
|
302
|
+
parts.push(cur);
|
|
303
|
+
cur = [];
|
|
304
|
+
curBytes = 2;
|
|
305
|
+
}
|
|
306
|
+
cur.push(el);
|
|
307
|
+
curBytes += cur.length === 1 ? elBytes : elBytes + 1;
|
|
308
|
+
}
|
|
309
|
+
if (cur.length > 0) parts.push(cur);
|
|
310
|
+
return parts;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function buildCanonical(artifact) {
|
|
314
|
+
const links = Array.isArray(artifact.links) ? artifact.links
|
|
315
|
+
: Array.isArray(artifact.edges) ? artifact.edges : [];
|
|
316
|
+
const canonical = {
|
|
317
|
+
directed: artifact.directed ?? false,
|
|
318
|
+
multigraph: artifact.multigraph ?? false,
|
|
319
|
+
graph: artifact.graph ?? {},
|
|
320
|
+
nodes: Array.isArray(artifact.nodes) ? artifact.nodes : [],
|
|
321
|
+
links,
|
|
322
|
+
hyperedges: Array.isArray(artifact.hyperedges) ? artifact.hyperedges : [],
|
|
323
|
+
};
|
|
324
|
+
const str = JSON.stringify(canonical);
|
|
325
|
+
const sha256 = createHash("sha256").update(str, "utf-8").digest("hex");
|
|
326
|
+
return { canonical, str, sha256 };
|
|
327
|
+
}
|
|
328
|
+
|
|
100
329
|
const DRY_RUN = flag("--dry-run");
|
|
330
|
+
const FORCE_CHUNKED = flag("--force-chunked");
|
|
331
|
+
const COMPACT = !flag("--no-compact");
|
|
101
332
|
const SKIP_BUILD = flag("--skip-build") || !!opt("--artifact-path");
|
|
102
333
|
const ARTIFACT_PATH_OVERRIDE = opt("--artifact-path");
|
|
103
334
|
|
|
@@ -194,7 +425,12 @@ async function main() {
|
|
|
194
425
|
// §B.2 preamble).
|
|
195
426
|
if (!supabaseUrl) die("SUPABASE_URL is required");
|
|
196
427
|
if (!anonKey) die("SUPABASE_ANON_KEY is required");
|
|
197
|
-
|
|
428
|
+
// --dry-run for a repo that is not chartered yet (pre-build extraction before
|
|
429
|
+
// the owner has minted a token) is legitimate: build + validate need no
|
|
430
|
+
// token. Everything that DOES need one (head report, ingest) is skipped
|
|
431
|
+
// below with an explicit line, never silently.
|
|
432
|
+
if (!ingestToken && !DRY_RUN) die("GRAPH_INGEST_TOKEN is required (this repo's ingest-scoped token — mint via graph_ingest_token_mint)");
|
|
433
|
+
if (!ingestToken && DRY_RUN) console.log("dry-run without GRAPH_INGEST_TOKEN: head report will be SKIPPED (token-less pre-build)");
|
|
198
434
|
|
|
199
435
|
let commitSha = process.env.GITHUB_SHA;
|
|
200
436
|
if (!commitSha) {
|
|
@@ -220,17 +456,22 @@ async function main() {
|
|
|
220
456
|
// work happens. This is what makes freshness honest even when step 2
|
|
221
457
|
// fails below — the hub already knows main advanced. Token-scoped: no
|
|
222
458
|
// repo_id is sent, it's resolved server-side from GRAPH_INGEST_TOKEN.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
459
|
+
if (!ingestToken && DRY_RUN) {
|
|
460
|
+
section("Step 1: graph_head_report_with_token — SKIPPED (dry-run, no token)");
|
|
461
|
+
} else {
|
|
462
|
+
section("Step 1: graph_head_report_with_token (authoritative tip, before build)");
|
|
463
|
+
{
|
|
464
|
+
const { error } = await supabase.rpc("graph_head_report_with_token", {
|
|
465
|
+
p_token: ingestToken,
|
|
466
|
+
p_sha: commitSha,
|
|
467
|
+
p_run_id: String(runId),
|
|
468
|
+
});
|
|
469
|
+
if (error) die(`graph_head_report_with_token failed: ${error.message} (nothing downstream can be trusted if the tip itself was never recorded)`);
|
|
470
|
+
console.log(`✓ head reported: sha=${commitSha}`);
|
|
471
|
+
}
|
|
232
472
|
}
|
|
233
473
|
|
|
474
|
+
|
|
234
475
|
// ── Step 2: build (unless testing against a fixture/existing artifact).
|
|
235
476
|
section("Step 2: graphify build");
|
|
236
477
|
const graphJsonPath = join(repoRoot, "graphify-out", "graph.json");
|
|
@@ -263,11 +504,69 @@ async function main() {
|
|
|
263
504
|
} catch (err) {
|
|
264
505
|
die(`failed to parse artifact JSON at ${artifactPath}: ${err.message}`);
|
|
265
506
|
}
|
|
507
|
+
// ── Step 2b: deterministic augmentation (SQL functions, gateway
|
|
508
|
+
// routes, contract schemas as EXTRACTED nodes). Runs after the graphify
|
|
509
|
+
// build, before validation, so the SAME augmented artifact is validated
|
|
510
|
+
// (Step 4) and ingested (Step 5). Uses only vocab relations/file_types —
|
|
511
|
+
// no vocabulary change required. Persisted back to the artifact file.
|
|
512
|
+
section("Step 2b: deterministic augmentation (graph-augment.mjs)");
|
|
513
|
+
{
|
|
514
|
+
try {
|
|
515
|
+
const { augmentArtifact } =
|
|
516
|
+
await import(join(repoRoot, "scripts", "graph-augment.mjs"));
|
|
517
|
+
const res = augmentArtifact(artifact, repoRoot);
|
|
518
|
+
const { writeFileSync } = await import("node:fs");
|
|
519
|
+
writeFileSync(artifactPath, JSON.stringify(res.artifact, null, 2));
|
|
520
|
+
artifact = res.artifact;
|
|
521
|
+
console.log(`✓ augment: +${res.addedNodes} nodes +${res.addedEdges} edges ` +
|
|
522
|
+
`(sql ${res.per.sql.nodes}n/${res.per.sql.edges}e, ` +
|
|
523
|
+
`routes ${res.per.routes.nodes}n/${res.per.routes.edges}e, ` +
|
|
524
|
+
`schemas ${res.per.schemas.nodes}n/${res.per.schemas.edges}e)`);
|
|
525
|
+
} catch (err) {
|
|
526
|
+
die(`augmentation failed: ${err.message}`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
266
530
|
const nodeCount = artifact.nodes?.length ?? 0;
|
|
267
531
|
const edgeCount = (artifact.links ?? artifact.edges ?? []).length;
|
|
268
532
|
const hyperedgeCount = (artifact.hyperedges ?? artifact.graph?.hyperedges ?? []).length;
|
|
533
|
+
const rawBytes = artifactBytes(artifact);
|
|
269
534
|
console.log(`artifact: ${artifactPath}`);
|
|
270
535
|
console.log(` nodes=${nodeCount} edges=${edgeCount} hyperedges=${hyperedgeCount}`);
|
|
536
|
+
console.log(` raw size: ${rawBytes} bytes (${(rawBytes / 1048576).toFixed(2)} MB)`);
|
|
537
|
+
|
|
538
|
+
// ── Step 2b: compaction (default ON, --no-compact disables). Removes
|
|
539
|
+
// every field no RPC reads or stores (see the map above) + minifies.
|
|
540
|
+
// Counts and all stored fields are preserved byte-identical.
|
|
541
|
+
if (COMPACT) {
|
|
542
|
+
const { compacted, removedBytes } = compactArtifact(artifact);
|
|
543
|
+
const compactBytes = artifactBytes(compacted);
|
|
544
|
+
const cn = compacted.nodes?.length ?? 0;
|
|
545
|
+
const ce = (compacted.links ?? compacted.edges ?? []).length;
|
|
546
|
+
const ch = (compacted.hyperedges ?? []).length;
|
|
547
|
+
if (cn !== nodeCount || ce !== edgeCount) {
|
|
548
|
+
die(`compaction changed counts (nodes ${nodeCount}->${cn}, edges ${edgeCount}->${ce}) — refusing to publish a compacted artifact that disagrees with the build`);
|
|
549
|
+
}
|
|
550
|
+
console.log(` compacted size: ${compactBytes} bytes (${(compactBytes / 1048576).toFixed(2)} MB)`);
|
|
551
|
+
const entries = Object.entries(removedBytes).sort((a, b) => b[1] - a[1]);
|
|
552
|
+
if (entries.length > 0) {
|
|
553
|
+
console.log(` removed fields:`);
|
|
554
|
+
for (const [k, b] of entries) {
|
|
555
|
+
console.log(` ${k}: ${b} bytes`);
|
|
556
|
+
}
|
|
557
|
+
} else {
|
|
558
|
+
console.log(` (compaction removed nothing — artifact already minimal)`);
|
|
559
|
+
}
|
|
560
|
+
artifact = compacted;
|
|
561
|
+
} else {
|
|
562
|
+
console.log(` (--no-compact given: skipping compaction)`);
|
|
563
|
+
}
|
|
564
|
+
const finalBytes = artifactBytes(artifact);
|
|
565
|
+
const capState = finalBytes > GRAPH_ARTIFACT_SIZE_CAP_BYTES ? "FAIL" : "PASS";
|
|
566
|
+
console.log(` ingest size check (${(GRAPH_ARTIFACT_SIZE_CAP_BYTES / 1048576).toFixed(0)} MB cap): ${finalBytes} bytes — ${capState}`);
|
|
567
|
+
if (capState === "FAIL") {
|
|
568
|
+
console.log(` (artifact exceeds the 25 MB ingest cap even after compaction — ingest WILL be rejected; options: raise the cap via migration, or shard by top-level directory)`);
|
|
569
|
+
}
|
|
271
570
|
|
|
272
571
|
// ── Step 3: resolve the current vocab version (never hardcode — the
|
|
273
572
|
// vocabulary is the versioned artifact, per B.2.3). graph_vocab_current()
|
|
@@ -314,7 +613,54 @@ async function main() {
|
|
|
314
613
|
// last-good serving completely untouched (0050's own guarantee), and
|
|
315
614
|
// (finding 2) the exact validated artifact is persisted content-addressed
|
|
316
615
|
// in the SAME transaction as promotion.
|
|
616
|
+
//
|
|
617
|
+
// Chunked path: when the minified artifact exceeds the 25 MB cap (or
|
|
618
|
+
// --force-chunked), canonicalize + split into <=8MB parts and ingest via
|
|
619
|
+
// graph_ingest_begin_with_token / graph_ingest_append_with_token /
|
|
620
|
+
// graph_ingest_commit_with_token. Single-call path unchanged otherwise.
|
|
317
621
|
section("Step 5: graph_ingest_with_token (publish)");
|
|
622
|
+
const useChunked = finalBytes > GRAPH_ARTIFACT_SIZE_CAP_BYTES || FORCE_CHUNKED;
|
|
623
|
+
if (useChunked) {
|
|
624
|
+
const minified = JSON.parse(JSON.stringify(artifact));
|
|
625
|
+
const { meta, chunks, str, sha256, counts } = buildCanonicalText(minified);
|
|
626
|
+
const totalBytes = Buffer.byteLength(str, "utf-8");
|
|
627
|
+
console.log(`chunked ingest: sha=${sha256} textchunks=${chunks.length} nodes=${counts.nodes} links=${counts.links} hyperedges=${counts.hyperedges} total=${totalBytes} bytes`);
|
|
628
|
+
if (DRY_RUN) { console.log("=== DRY-RUN: skipping chunked ingest RPCs ==="); process.exit(0); }
|
|
629
|
+
const { data: beginData, error: beginErr } = await supabase.rpc("graph_ingest_begin_with_token", {
|
|
630
|
+
p_token: ingestToken, p_source_sha: commitSha,
|
|
631
|
+
p_run_id: String(runId), p_workflow_ref: workflowRef,
|
|
632
|
+
p_meta: { vocab_version: vocabVersion, compacted: COMPACT },
|
|
633
|
+
p_declared_node_count: counts.nodes,
|
|
634
|
+
p_declared_link_count: counts.links,
|
|
635
|
+
p_declared_hyperedge_count: counts.hyperedges,
|
|
636
|
+
p_sha256: sha256,
|
|
637
|
+
});
|
|
638
|
+
if (beginErr) die(`CHUNKED BEGIN FAILED: ${beginErr.message}`);
|
|
639
|
+
const uploadId = beginData.upload_id ?? beginData;
|
|
640
|
+
let partNo = 0;
|
|
641
|
+
const append = async (kind, part) => {
|
|
642
|
+
partNo++;
|
|
643
|
+
const bytes = Buffer.byteLength(JSON.stringify(part), "utf-8");
|
|
644
|
+
const { error } = await supabase.rpc("graph_ingest_append_with_token", {
|
|
645
|
+
p_token: ingestToken, p_upload_id: uploadId, p_part_no: partNo, p_kind: kind, p_payload: part,
|
|
646
|
+
});
|
|
647
|
+
if (error) die(`CHUNKED APPEND FAILED part ${partNo} (${kind}): ${error.message}`);
|
|
648
|
+
console.log(` part ${partNo}: kind=${kind} bytes=${bytes}`);
|
|
649
|
+
};
|
|
650
|
+
await append("meta", meta);
|
|
651
|
+
for (const c of chunks) await append("textchunk", c);
|
|
652
|
+
// NOTE: append() JSON-encodes the chunk string as a jsonb string payload;
|
|
653
|
+
// the server concatenates chunk text verbatim (payload #>> '{}').
|
|
654
|
+
const { data: commitData, error: commitErr } = await supabase.rpc("graph_ingest_commit_with_token", {
|
|
655
|
+
p_token: ingestToken, p_upload_id: uploadId,
|
|
656
|
+
});
|
|
657
|
+
if (commitErr) die(`CHUNKED COMMIT FAILED: ${commitErr.message}`);
|
|
658
|
+
console.log(`chunked ingest committed: parts=${partNo} total=${totalBytes} bytes`);
|
|
659
|
+
console.log(JSON.stringify(commitData));
|
|
660
|
+
console.log("");
|
|
661
|
+
console.log("=== SUCCESS ===");
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
318
664
|
{
|
|
319
665
|
const { data, error } = await supabase.rpc("graph_ingest_with_token", {
|
|
320
666
|
p_token: ingestToken,
|