@intentius/behold 0.10.1 → 0.11.0
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/cli.js +297 -58
- package/example-argo-estate/package-lock.json +133 -590
- package/example-carve/README.md +7 -5
- package/example-carve/app/package-lock.json +117 -574
- package/example-carve/app/package.json +2 -2
- package/example-flux-estate/package-lock.json +133 -590
- package/example-k8s/package-lock.json +151 -608
- package/example-writes/package-lock.json +142 -599
- package/package.json +3 -3
- package/web/app.js +68 -55
- package/web/carve-steps.js +53 -2
- package/web/carve-steps.test.js +31 -0
- package/web/layout-store.js +4 -4
package/dist/cli.js
CHANGED
|
@@ -1121,7 +1121,7 @@ import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
|
1121
1121
|
import { execFile as execFile3 } from "node:child_process";
|
|
1122
1122
|
import { promisify as promisify3 } from "node:util";
|
|
1123
1123
|
import { existsSync as existsSync13, readFileSync as readFileSync13 } from "node:fs";
|
|
1124
|
-
import { dirname as dirname4, join as join16, relative as relative3, resolve as resolve5, sep as sep4 } from "node:path";
|
|
1124
|
+
import { basename, dirname as dirname4, join as join16, relative as relative3, resolve as resolve5, sep as sep4 } from "node:path";
|
|
1125
1125
|
|
|
1126
1126
|
// src/recents.ts
|
|
1127
1127
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
@@ -1552,8 +1552,15 @@ function parseYAMLArray(lines, startIndex, baseIndent) {
|
|
|
1552
1552
|
result.push(obj);
|
|
1553
1553
|
i = j;
|
|
1554
1554
|
} else {
|
|
1555
|
-
|
|
1556
|
-
|
|
1555
|
+
const header = blockScalarHeader(itemValue);
|
|
1556
|
+
if (header) {
|
|
1557
|
+
const block = parseBlockScalar(lines, i + 1, indent, header);
|
|
1558
|
+
result.push(block.value);
|
|
1559
|
+
i = block.endIndex;
|
|
1560
|
+
} else {
|
|
1561
|
+
result.push(parseScalar(itemValue));
|
|
1562
|
+
i++;
|
|
1563
|
+
}
|
|
1557
1564
|
}
|
|
1558
1565
|
} else {
|
|
1559
1566
|
break;
|
|
@@ -4187,6 +4194,7 @@ function renderGraph(ir, opts = {}) {
|
|
|
4187
4194
|
const layout = layoutIr(ir, { fit: true, ...boxes ? { groups: boxes } : {} });
|
|
4188
4195
|
if (opts.radial && !boxes) radializeLayout(layout, groupKeyByNode(ir), footprints(ir));
|
|
4189
4196
|
else if (!boxes) packComponents(layout, ir);
|
|
4197
|
+
else if (boxKey === "byStack" && boxes) packMemberBoxes(layout, ir, boxes);
|
|
4190
4198
|
const svg = renderSvg(ir, layout, {
|
|
4191
4199
|
fit: true,
|
|
4192
4200
|
hideTitle: true,
|
|
@@ -4409,6 +4417,92 @@ function packComponents(layout, ir) {
|
|
|
4409
4417
|
layout.width = Math.max(...nodes.map((n) => n.x + halfW(n))) + pad;
|
|
4410
4418
|
layout.height = Math.max(...nodes.map((n) => n.y + halfH(n))) + pad;
|
|
4411
4419
|
}
|
|
4420
|
+
function packMemberBoxes(layout, ir, members) {
|
|
4421
|
+
const boxes = layout.groups ?? [];
|
|
4422
|
+
if (boxes.length < 4) return;
|
|
4423
|
+
const nodes = layout.nodes;
|
|
4424
|
+
const size = footprints(ir);
|
|
4425
|
+
const halfW = (n) => (size.get(n.id)?.w ?? NODE_W) / 2;
|
|
4426
|
+
const halfH = (n) => (size.get(n.id)?.h ?? NODE_H) / 2;
|
|
4427
|
+
for (const n of nodes) n.y = layout.height - n.y;
|
|
4428
|
+
for (const b of boxes) b.y = layout.height - b.y;
|
|
4429
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
4430
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
4431
|
+
const blocks = boxes.map((box) => {
|
|
4432
|
+
const ns = (members[box.id ?? box.title] ?? members[box.title] ?? []).flatMap((id) => byId.get(id) ?? []);
|
|
4433
|
+
for (const n of ns) claimed.add(n.id);
|
|
4434
|
+
let minX = box.x - box.w / 2;
|
|
4435
|
+
let maxX2 = box.x + box.w / 2;
|
|
4436
|
+
let minY = box.y - box.h / 2;
|
|
4437
|
+
let maxY2 = box.y + box.h / 2;
|
|
4438
|
+
for (const n of ns) {
|
|
4439
|
+
minX = Math.min(minX, n.x - halfW(n));
|
|
4440
|
+
maxX2 = Math.max(maxX2, n.x + halfW(n));
|
|
4441
|
+
minY = Math.min(minY, n.y - halfH(n));
|
|
4442
|
+
maxY2 = Math.max(maxY2, n.y + halfH(n));
|
|
4443
|
+
}
|
|
4444
|
+
return { ns, box, minX, minY, w: maxX2 - minX, h: maxY2 - minY };
|
|
4445
|
+
});
|
|
4446
|
+
const rest = nodes.filter((n) => !claimed.has(n.id));
|
|
4447
|
+
if (rest.length) {
|
|
4448
|
+
const idxOf = new Map(rest.map((n, i) => [n.id, i]));
|
|
4449
|
+
const parent = rest.map((_, i) => i);
|
|
4450
|
+
const find = (x) => {
|
|
4451
|
+
while (parent[x] !== x) x = parent[x] = parent[parent[x]];
|
|
4452
|
+
return x;
|
|
4453
|
+
};
|
|
4454
|
+
for (const e of ir.edges) {
|
|
4455
|
+
const a = idxOf.get(e.from);
|
|
4456
|
+
const b = idxOf.get(e.to);
|
|
4457
|
+
if (a != null && b != null) parent[find(a)] = find(b);
|
|
4458
|
+
}
|
|
4459
|
+
const comps = /* @__PURE__ */ new Map();
|
|
4460
|
+
rest.forEach((n, i) => (comps.get(find(i)) ?? comps.set(find(i), []).get(find(i))).push(n));
|
|
4461
|
+
for (const ns of comps.values()) {
|
|
4462
|
+
const minX = Math.min(...ns.map((n) => n.x - halfW(n)));
|
|
4463
|
+
const minY = Math.min(...ns.map((n) => n.y - halfH(n)));
|
|
4464
|
+
blocks.push({
|
|
4465
|
+
ns,
|
|
4466
|
+
minX,
|
|
4467
|
+
minY,
|
|
4468
|
+
w: Math.max(...ns.map((n) => n.x + halfW(n))) - minX,
|
|
4469
|
+
h: Math.max(...ns.map((n) => n.y + halfH(n))) - minY
|
|
4470
|
+
});
|
|
4471
|
+
}
|
|
4472
|
+
}
|
|
4473
|
+
const totalArea = blocks.reduce((s, b) => s + (b.w + MEMBER_GAP) * (b.h + MEMBER_GAP), 0);
|
|
4474
|
+
const targetW = Math.max(Math.max(...blocks.map((b) => b.w)), Math.sqrt(totalArea) * 1.3);
|
|
4475
|
+
let shelfX = 0;
|
|
4476
|
+
let shelfY = 0;
|
|
4477
|
+
let shelfH = 0;
|
|
4478
|
+
let maxX = 0;
|
|
4479
|
+
let maxY = 0;
|
|
4480
|
+
for (const b of blocks) {
|
|
4481
|
+
if (shelfX > 0 && shelfX + b.w > targetW) {
|
|
4482
|
+
shelfX = 0;
|
|
4483
|
+
shelfY += shelfH + MEMBER_GAP;
|
|
4484
|
+
shelfH = 0;
|
|
4485
|
+
}
|
|
4486
|
+
const dx = shelfX - b.minX;
|
|
4487
|
+
const dy = shelfY - b.minY;
|
|
4488
|
+
for (const n of b.ns) {
|
|
4489
|
+
n.x += dx;
|
|
4490
|
+
n.y += dy;
|
|
4491
|
+
}
|
|
4492
|
+
if (b.box) {
|
|
4493
|
+
b.box.x += dx;
|
|
4494
|
+
b.box.y += dy;
|
|
4495
|
+
}
|
|
4496
|
+
maxX = Math.max(maxX, shelfX + b.w);
|
|
4497
|
+
maxY = Math.max(maxY, shelfY + b.h);
|
|
4498
|
+
shelfX += b.w + MEMBER_GAP;
|
|
4499
|
+
shelfH = Math.max(shelfH, b.h);
|
|
4500
|
+
}
|
|
4501
|
+
layout.width = maxX;
|
|
4502
|
+
layout.height = maxY;
|
|
4503
|
+
for (const n of nodes) n.y = maxY - n.y;
|
|
4504
|
+
for (const b of boxes) b.y = maxY - b.y;
|
|
4505
|
+
}
|
|
4412
4506
|
function groupKeyByNode(ir) {
|
|
4413
4507
|
const out = /* @__PURE__ */ new Map();
|
|
4414
4508
|
for (const n of ir.nodes) {
|
|
@@ -4796,6 +4890,67 @@ async function runCarvePlan(demo) {
|
|
|
4796
4890
|
noDestroy: r.noDestroy
|
|
4797
4891
|
};
|
|
4798
4892
|
}
|
|
4893
|
+
async function runCarveObserve(demo, select) {
|
|
4894
|
+
if (!demo.live) {
|
|
4895
|
+
return refuse2(
|
|
4896
|
+
"carve-action",
|
|
4897
|
+
"the observe beat reads the scratch Floci, and this server isn't serving one",
|
|
4898
|
+
"run `behold demo carve --live` (needs docker + terraform)"
|
|
4899
|
+
);
|
|
4900
|
+
}
|
|
4901
|
+
if (!existsSync8(join10(demo.out, "src"))) {
|
|
4902
|
+
return refuse2("carve-action", "nothing has been emitted yet", "run Emit first \u2014 observe reads the carveout emit writes");
|
|
4903
|
+
}
|
|
4904
|
+
const args = ["lifecycle", "diff", "prod", "--live", "--json"];
|
|
4905
|
+
const run3 = await runChantRaw(args, demo.out, { AWS_ENDPOINT_URL: demo.live.endpoint }).catch((err) => ({
|
|
4906
|
+
code: 127,
|
|
4907
|
+
stdout: "",
|
|
4908
|
+
stderr: err instanceof Error ? err.message : String(err)
|
|
4909
|
+
}));
|
|
4910
|
+
if (run3.code !== 0) {
|
|
4911
|
+
return refuse2(
|
|
4912
|
+
"carve-action",
|
|
4913
|
+
`chant lifecycle diff exited ${run3.code}: ${merge(run3, demo.root) || "(no output)"}`,
|
|
4914
|
+
"Is the scratch Floci still up? `docker ps` should list " + demo.live.container + "."
|
|
4915
|
+
);
|
|
4916
|
+
}
|
|
4917
|
+
let aws;
|
|
4918
|
+
try {
|
|
4919
|
+
aws = JSON.parse(run3.stdout).lexicons?.aws ?? {};
|
|
4920
|
+
} catch {
|
|
4921
|
+
return refuse2("carve-action", "chant lifecycle diff answered something that isn't JSON", "re-run \u2014 a partial read is not a verdict");
|
|
4922
|
+
}
|
|
4923
|
+
const entity = select.split(".").pop() ?? select;
|
|
4924
|
+
const command = `AWS_ENDPOINT_URL=${demo.live.endpoint} chant ${args.join(" ")} # in ${shortenIn(demo.out, demo.root)}`;
|
|
4925
|
+
const queried = aws.resources?.queried?.[entity];
|
|
4926
|
+
const meta = aws.observed?.[entity];
|
|
4927
|
+
if (meta) {
|
|
4928
|
+
return {
|
|
4929
|
+
ok: true,
|
|
4930
|
+
select,
|
|
4931
|
+
command,
|
|
4932
|
+
entity,
|
|
4933
|
+
verdict: "observed",
|
|
4934
|
+
...meta.status ? { status: meta.status } : {},
|
|
4935
|
+
...meta.ownership ? { ownership: meta.ownership } : {},
|
|
4936
|
+
...meta.physicalId ? { physicalId: meta.physicalId } : {},
|
|
4937
|
+
...queried ? { queried } : {}
|
|
4938
|
+
};
|
|
4939
|
+
}
|
|
4940
|
+
const hole = aws.resources?.unobserved?.find((u) => u.name === entity);
|
|
4941
|
+
if (hole) {
|
|
4942
|
+
return {
|
|
4943
|
+
ok: true,
|
|
4944
|
+
select,
|
|
4945
|
+
command,
|
|
4946
|
+
entity,
|
|
4947
|
+
verdict: "unobserved",
|
|
4948
|
+
...hole.detail || hole.reason ? { detail: [hole.reason, hole.detail].filter(Boolean).join(": ") } : {},
|
|
4949
|
+
...queried ? { queried } : {}
|
|
4950
|
+
};
|
|
4951
|
+
}
|
|
4952
|
+
return { ok: true, select, command, entity, verdict: "missing", ...queried ? { queried } : {} };
|
|
4953
|
+
}
|
|
4799
4954
|
var MAX_ARTIFACT_BYTES = 64 * 1024;
|
|
4800
4955
|
var MAX_ARTIFACTS = 24;
|
|
4801
4956
|
var MAX_OUTPUT_BYTES = 32 * 1024;
|
|
@@ -5126,6 +5281,11 @@ function statusTree(o) {
|
|
|
5126
5281
|
if (!attrs) return void 0;
|
|
5127
5282
|
return rec2(attrs.status) ?? attrs;
|
|
5128
5283
|
}
|
|
5284
|
+
function specTree(o) {
|
|
5285
|
+
const attrs = rec2(o.attributes);
|
|
5286
|
+
if (!attrs) return void 0;
|
|
5287
|
+
return rec2(attrs.spec) ?? attrs;
|
|
5288
|
+
}
|
|
5129
5289
|
var ARGO_HEALTH = {
|
|
5130
5290
|
Healthy: "healthy",
|
|
5131
5291
|
Degraded: "degraded",
|
|
@@ -5226,6 +5386,7 @@ function revisionVerdict(tree) {
|
|
|
5226
5386
|
return { health: "progressing", detail: `applied ${applied}, attempted ${attempted}` };
|
|
5227
5387
|
}
|
|
5228
5388
|
var FLUX_TYPE_PREFIX = "K8s::Flux::";
|
|
5389
|
+
var FLUX_HELMREPOSITORY_TYPE = "K8s::Flux::HelmRepository";
|
|
5229
5390
|
function fluxVerdict(o) {
|
|
5230
5391
|
if (!o.type?.startsWith(FLUX_TYPE_PREFIX)) return void 0;
|
|
5231
5392
|
const tree = statusTree(o);
|
|
@@ -5233,6 +5394,9 @@ function fluxVerdict(o) {
|
|
|
5233
5394
|
if (ready?.status === "False") return { health: "degraded", detail: describeReady(ready) };
|
|
5234
5395
|
if (ready?.status === "Unknown") return { health: "progressing", detail: describeReady(ready) };
|
|
5235
5396
|
if (ready?.status === "True") return revisionVerdict(tree) ?? { health: "healthy", detail: describeReady(ready) };
|
|
5397
|
+
if (o.type === FLUX_HELMREPOSITORY_TYPE && str2(specTree(o)?.type) === "oci") {
|
|
5398
|
+
return { health: "healthy", detail: "spec.type=oci (nothing to reconcile)" };
|
|
5399
|
+
}
|
|
5236
5400
|
const conditions = tree?.conditions;
|
|
5237
5401
|
const reconciling = Array.isArray(conditions) && conditions.some((c) => {
|
|
5238
5402
|
const cond = rec2(c);
|
|
@@ -5244,9 +5408,41 @@ function fluxVerdict(o) {
|
|
|
5244
5408
|
}
|
|
5245
5409
|
return void 0;
|
|
5246
5410
|
}
|
|
5411
|
+
var DEPLOYMENT_TYPE = "K8s::Apps::Deployment";
|
|
5412
|
+
function unhappyCondition(o, type) {
|
|
5413
|
+
const fromStrings = rec2(o.attributes)?.conditions;
|
|
5414
|
+
if (Array.isArray(fromStrings)) {
|
|
5415
|
+
for (const c of fromStrings) {
|
|
5416
|
+
if (typeof c === "string" && c.startsWith(type)) return c;
|
|
5417
|
+
}
|
|
5418
|
+
}
|
|
5419
|
+
const tree = statusTree(o);
|
|
5420
|
+
const raw = tree?.conditions;
|
|
5421
|
+
if (Array.isArray(raw)) {
|
|
5422
|
+
for (const c of raw) {
|
|
5423
|
+
const cond = rec2(c);
|
|
5424
|
+
if (!cond || cond.type !== type) continue;
|
|
5425
|
+
const status = str2(cond.status);
|
|
5426
|
+
const unhappy = type === "ReplicaFailure" ? status === "True" : status === "False";
|
|
5427
|
+
if (!unhappy) continue;
|
|
5428
|
+
const reason = str2(cond.reason);
|
|
5429
|
+
const message = str2(cond.message);
|
|
5430
|
+
return `${type}${reason ? `=${reason}` : ""}${message ? `: ${message}` : ""}`;
|
|
5431
|
+
}
|
|
5432
|
+
}
|
|
5433
|
+
return void 0;
|
|
5434
|
+
}
|
|
5435
|
+
function deploymentVerdict(o) {
|
|
5436
|
+
if (o.type !== DEPLOYMENT_TYPE) return void 0;
|
|
5437
|
+
const damage = unhappyCondition(o, "ReplicaFailure") ?? unhappyCondition(o, "Progressing");
|
|
5438
|
+
if (damage) return { health: "degraded", detail: damage };
|
|
5439
|
+
const available = unhappyCondition(o, "Available");
|
|
5440
|
+
if (available) return { health: "progressing", detail: available };
|
|
5441
|
+
return void 0;
|
|
5442
|
+
}
|
|
5247
5443
|
function classifyObservedHealth(observed) {
|
|
5248
5444
|
if (!observed) return { health: "unknown" };
|
|
5249
|
-
return argoVerdict(observed) ?? fluxVerdict(observed) ?? { health: classifyHealth(observed.status) };
|
|
5445
|
+
return argoVerdict(observed) ?? fluxVerdict(observed) ?? deploymentVerdict(observed) ?? { health: classifyHealth(observed.status) };
|
|
5250
5446
|
}
|
|
5251
5447
|
|
|
5252
5448
|
// src/apply.ts
|
|
@@ -5377,9 +5573,14 @@ var OpRunner = class {
|
|
|
5377
5573
|
* Start `chant run <name>` unless one is already running (the Sync/Adopt/auto-
|
|
5378
5574
|
* sync path). `cwd` is the Op's own project dir (#31 multi-estate); defaults to
|
|
5379
5575
|
* the primary. Returns true if it started, false if busy.
|
|
5576
|
+
*
|
|
5577
|
+
* `temporal` — pass `--temporal` so the run gets the durable runtime. Callers
|
|
5578
|
+
* set it from the Op's declared gate: chant refuses a gated Op outright in
|
|
5579
|
+
* local mode ("gates and schedules need a durable runtime"), so without the
|
|
5580
|
+
* flag a gated Op could never run from behold at all.
|
|
5380
5581
|
*/
|
|
5381
|
-
trigger(name, opEnv, cwd) {
|
|
5382
|
-
return this.start(["run", name], name, opEnv, cwd);
|
|
5582
|
+
trigger(name, opEnv, cwd, temporal) {
|
|
5583
|
+
return this.start(temporal ? ["run", name, "--temporal"] : ["run", name], name, opEnv, cwd);
|
|
5383
5584
|
}
|
|
5384
5585
|
/**
|
|
5385
5586
|
* Run an arbitrary `chant` invocation through the same guard/stream/PR/capture
|
|
@@ -5772,13 +5973,31 @@ async function openRollbackBranches(projectDir, env) {
|
|
|
5772
5973
|
|
|
5773
5974
|
// src/estate.ts
|
|
5774
5975
|
import { statSync as statSync3 } from "node:fs";
|
|
5976
|
+
import { availableParallelism } from "node:os";
|
|
5775
5977
|
import { join as join13, resolve as resolve3, sep as sep2 } from "node:path";
|
|
5776
5978
|
import { composeStacks, shortStackNames } from "@intentius/pinhole";
|
|
5979
|
+
function estateReadPool(members, env = process.env) {
|
|
5980
|
+
const override = Number.parseInt(env.BEHOLD_ESTATE_CONCURRENCY ?? "", 10);
|
|
5981
|
+
const cap = Number.isInteger(override) && override >= 1 ? override : Math.min(4, availableParallelism());
|
|
5982
|
+
return Math.max(1, Math.min(cap, members));
|
|
5983
|
+
}
|
|
5984
|
+
async function mapPool(items, width, fn) {
|
|
5985
|
+
const results = new Array(items.length);
|
|
5986
|
+
let next = 0;
|
|
5987
|
+
const worker = async () => {
|
|
5988
|
+
for (let i = next++; i < items.length; i = next++) {
|
|
5989
|
+
results[i] = await fn(items[i], i);
|
|
5990
|
+
}
|
|
5991
|
+
};
|
|
5992
|
+
await Promise.all(Array.from({ length: Math.max(1, Math.min(width, items.length)) }, () => worker()));
|
|
5993
|
+
return results;
|
|
5994
|
+
}
|
|
5777
5995
|
async function composeEstate(projectDirs, opts = {}) {
|
|
5778
5996
|
const names = shortStackNames(projectDirs);
|
|
5779
|
-
const stacks = await
|
|
5780
|
-
|
|
5781
|
-
|
|
5997
|
+
const stacks = await mapPool(projectDirs, estateReadPool(projectDirs.length), async (dir, i) => ({
|
|
5998
|
+
name: names[i],
|
|
5999
|
+
ir: await graphIr(dir, opts)
|
|
6000
|
+
}));
|
|
5782
6001
|
return composeStacks(stacks);
|
|
5783
6002
|
}
|
|
5784
6003
|
var firstLine2 = (e) => {
|
|
@@ -5872,7 +6091,11 @@ function joinNamespaceBindings(members, isDir = realIsDir) {
|
|
|
5872
6091
|
async function estateNamespaceScopes(projectDirs, opts, isDir = realIsDir) {
|
|
5873
6092
|
if (projectDirs.length < 2) return /* @__PURE__ */ new Map();
|
|
5874
6093
|
const { live: _live, overlay: _overlay, namespace: _namespace, ...src } = opts;
|
|
5875
|
-
const irs = await
|
|
6094
|
+
const irs = await mapPool(
|
|
6095
|
+
projectDirs,
|
|
6096
|
+
estateReadPool(projectDirs.length),
|
|
6097
|
+
(dir) => graphIr(dir, { ...src, detail: 3 }).catch(() => void 0)
|
|
6098
|
+
);
|
|
5876
6099
|
const scopes = /* @__PURE__ */ new Map();
|
|
5877
6100
|
for (const j of joinNamespaceBindings(projectDirs.map((dir, i) => ({ dir, ir: irs[i] })), isDir)) {
|
|
5878
6101
|
if (!meetsFloor(resolveChant(j.dir).version, NAMESPACE_JOIN_FLOOR)) continue;
|
|
@@ -5891,28 +6114,26 @@ async function composeEstateOverlay(projectDirs, opts, classify) {
|
|
|
5891
6114
|
const joined = [];
|
|
5892
6115
|
const stacks = new Array(projectDirs.length);
|
|
5893
6116
|
const scopes = await estateNamespaceScopes(projectDirs, opts);
|
|
5894
|
-
await
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
6117
|
+
await mapPool(projectDirs, estateReadPool(projectDirs.length), async (dir, i) => {
|
|
6118
|
+
const name = names[i];
|
|
6119
|
+
const namespace = scopes.get(dir);
|
|
6120
|
+
try {
|
|
6121
|
+
const live = { ...opts, live: true, overlay: true, ...namespace ? { namespace } : {} };
|
|
6122
|
+
stacks[i] = { name, ir: namespaceRuntimeOwners(name, classify(await graphIr(dir, live))) };
|
|
6123
|
+
if (namespace) joined.push({ name, namespace });
|
|
6124
|
+
} catch (err) {
|
|
6125
|
+
const reason = firstLine2(err);
|
|
5898
6126
|
try {
|
|
5899
|
-
const
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
const src = await graphIr(dir, srcOpts);
|
|
5907
|
-
for (const n of src.nodes) n.attrs = { ...n.attrs, _status: "neutral", _unobserved: reason };
|
|
5908
|
-
stacks[i] = { name, ir: src };
|
|
5909
|
-
unobserved.push({ name, reason });
|
|
5910
|
-
} catch (err2) {
|
|
5911
|
-
dropped.push({ name, reason: firstLine2(err2) });
|
|
5912
|
-
}
|
|
6127
|
+
const { env: _env, live: _live, overlay: _overlay, ...srcOpts } = opts;
|
|
6128
|
+
const src = await graphIr(dir, srcOpts);
|
|
6129
|
+
for (const n of src.nodes) n.attrs = { ...n.attrs, _status: "neutral", _unobserved: reason };
|
|
6130
|
+
stacks[i] = { name, ir: src };
|
|
6131
|
+
unobserved.push({ name, reason });
|
|
6132
|
+
} catch (err2) {
|
|
6133
|
+
dropped.push({ name, reason: firstLine2(err2) });
|
|
5913
6134
|
}
|
|
5914
|
-
}
|
|
5915
|
-
);
|
|
6135
|
+
}
|
|
6136
|
+
});
|
|
5916
6137
|
const present = stacks.filter((s) => !!s);
|
|
5917
6138
|
return {
|
|
5918
6139
|
ir: composeStacks(present),
|
|
@@ -5959,6 +6180,12 @@ function watchSource(projectDir, onChange, debounceMs = 200) {
|
|
|
5959
6180
|
watcher.close();
|
|
5960
6181
|
};
|
|
5961
6182
|
}
|
|
6183
|
+
function watchSources(projectDirs, onChange, debounceMs = 200) {
|
|
6184
|
+
const stops = projectDirs.map((dir) => watchSource(dir, () => onChange(dir), debounceMs));
|
|
6185
|
+
return () => {
|
|
6186
|
+
for (const stop of stops) stop();
|
|
6187
|
+
};
|
|
6188
|
+
}
|
|
5962
6189
|
|
|
5963
6190
|
// src/poll.ts
|
|
5964
6191
|
function digestOf(nodes) {
|
|
@@ -5986,18 +6213,22 @@ function changedLexicons(prev, next) {
|
|
|
5986
6213
|
}
|
|
5987
6214
|
function startDriftPoll(opts) {
|
|
5988
6215
|
let stopped = false;
|
|
5989
|
-
|
|
6216
|
+
const last = /* @__PURE__ */ new Map();
|
|
5990
6217
|
let timer;
|
|
5991
6218
|
const tick = async () => {
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
const
|
|
5996
|
-
|
|
6219
|
+
for (const member of opts.members) {
|
|
6220
|
+
if (stopped) return;
|
|
6221
|
+
try {
|
|
6222
|
+
const digests = driftDigestsByLexicon(await member.query());
|
|
6223
|
+
const prev = last.get(member.dir);
|
|
6224
|
+
if (prev !== void 0) {
|
|
6225
|
+
const moved = changedLexicons(prev, digests);
|
|
6226
|
+
if (moved.length) opts.onChange(member.dir, moved);
|
|
6227
|
+
}
|
|
6228
|
+
last.set(member.dir, digests);
|
|
6229
|
+
} catch (err) {
|
|
6230
|
+
opts.onError?.(member.dir, err);
|
|
5997
6231
|
}
|
|
5998
|
-
last = digests;
|
|
5999
|
-
} catch (err) {
|
|
6000
|
-
opts.onError?.(err);
|
|
6001
6232
|
}
|
|
6002
6233
|
if (!stopped) timer = setTimeout(tick, opts.intervalMs);
|
|
6003
6234
|
};
|
|
@@ -6571,6 +6802,7 @@ function carveRoutes(app, reportPath, demo) {
|
|
|
6571
6802
|
};
|
|
6572
6803
|
app.post("/api/carve/emit", (c) => runStep2(c, runCarveEmit));
|
|
6573
6804
|
app.post("/api/carve/bridge", (c) => runStep2(c, runCarveBridge));
|
|
6805
|
+
app.post("/api/carve/observe", (c) => runStep2(c, runCarveObserve));
|
|
6574
6806
|
app.post("/api/carve/plan", async (c) => {
|
|
6575
6807
|
const block = demoBlock();
|
|
6576
6808
|
if (block) {
|
|
@@ -6647,7 +6879,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
6647
6879
|
if (!info) {
|
|
6648
6880
|
return c.json({ error: `no Op named "${name}" in the estate` }, 404);
|
|
6649
6881
|
}
|
|
6650
|
-
if (!runner.trigger(name, info.env, info.dir)) {
|
|
6882
|
+
if (!runner.trigger(name, info.env, info.dir, Boolean(info.gate))) {
|
|
6651
6883
|
return c.json({ error: `an Op is already running (${runner.running})` }, 409);
|
|
6652
6884
|
}
|
|
6653
6885
|
return c.json({ started: true, name });
|
|
@@ -6692,7 +6924,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
6692
6924
|
const { name, gate } = c.req.param();
|
|
6693
6925
|
const info = estateOps().find((o) => o.name === name);
|
|
6694
6926
|
broadcaster.emit("op", `\u270E signal ${name} ${gate}`);
|
|
6695
|
-
const { code, stderr } = await runChantRaw(["run", "signal", name, gate], info?.dir ?? cfg.projectDir);
|
|
6927
|
+
const { code, stderr } = await runChantRaw(["run", "signal", name, gate, "--temporal"], info?.dir ?? cfg.projectDir);
|
|
6696
6928
|
if (code !== 0) return c.json({ error: stderr.trim() || `signal exited ${code}` }, 500);
|
|
6697
6929
|
return c.json({ signalled: true });
|
|
6698
6930
|
});
|
|
@@ -7434,17 +7666,18 @@ async function startServer(cfg) {
|
|
|
7434
7666
|
const app = createApp(cfg, broadcaster, frames, runner);
|
|
7435
7667
|
const autoSync = cfg.autoSync ?? "off";
|
|
7436
7668
|
const capture2 = () => captureFrame(cfg.projectDir, cfg.env, frames, broadcaster);
|
|
7437
|
-
const onEstateChange = () => {
|
|
7438
|
-
broadcaster.emit("changed");
|
|
7669
|
+
const onEstateChange = (memberDir) => {
|
|
7670
|
+
broadcaster.emit("changed", memberDir ?? "");
|
|
7439
7671
|
void capture2();
|
|
7440
7672
|
};
|
|
7441
|
-
const
|
|
7442
|
-
|
|
7673
|
+
const memberTag = (dir) => (cfg.projectDirs?.length ?? 0) > 1 ? ` [${basename(dir)}]` : "";
|
|
7674
|
+
const onPollDrift = (dir, movedLexicons) => {
|
|
7675
|
+
onEstateChange(dir);
|
|
7443
7676
|
if (autoSync === "off") return;
|
|
7444
|
-
void routeAutoSync(movedLexicons);
|
|
7677
|
+
void routeAutoSync(dir, movedLexicons);
|
|
7445
7678
|
};
|
|
7446
|
-
const routeAutoSync = async (movedLexicons) => {
|
|
7447
|
-
const suspended = autoSync === "pull-request" ? suspendedByRollback(await openRollbackBranches(
|
|
7679
|
+
const routeAutoSync = async (dir, movedLexicons) => {
|
|
7680
|
+
const suspended = autoSync === "pull-request" ? suspendedByRollback(await openRollbackBranches(dir, cfg.env), movedLexicons) : /* @__PURE__ */ new Set();
|
|
7448
7681
|
const { picks, declined } = pickAutoSyncOps(
|
|
7449
7682
|
autoSync,
|
|
7450
7683
|
discoverEstateOps(cfg.projectDirs ?? [cfg.projectDir]),
|
|
@@ -7453,31 +7686,37 @@ async function startServer(cfg) {
|
|
|
7453
7686
|
suspended
|
|
7454
7687
|
);
|
|
7455
7688
|
for (const d of declined) {
|
|
7456
|
-
broadcaster.emit("op", `\u27F3 auto-sync (${autoSync}) declined ${d.lexicon}: ${d.reason}`);
|
|
7689
|
+
broadcaster.emit("op", `\u27F3 auto-sync (${autoSync})${memberTag(dir)} declined ${d.lexicon}: ${d.reason}`);
|
|
7457
7690
|
}
|
|
7458
7691
|
for (const { op, lexicons } of picks) {
|
|
7459
7692
|
const scope = lexicons.join("+");
|
|
7460
|
-
if (runner.trigger(op.name, op.env, op.dir)) {
|
|
7461
|
-
broadcaster.emit("op", `\u27F3 auto-sync (${autoSync}) ${scope} \u2192 ${op.name}`);
|
|
7693
|
+
if (runner.trigger(op.name, op.env, op.dir, Boolean(op.gate))) {
|
|
7694
|
+
broadcaster.emit("op", `\u27F3 auto-sync (${autoSync})${memberTag(dir)} ${scope} \u2192 ${op.name}`);
|
|
7462
7695
|
} else {
|
|
7463
|
-
broadcaster.emit("op", `\u27F3 auto-sync (${autoSync}) ${scope} \u2192 ${op.name} waiting \u2014 ${runner.running} is running`);
|
|
7696
|
+
broadcaster.emit("op", `\u27F3 auto-sync (${autoSync})${memberTag(dir)} ${scope} \u2192 ${op.name} waiting \u2014 ${runner.running} is running`);
|
|
7464
7697
|
}
|
|
7465
7698
|
}
|
|
7466
7699
|
};
|
|
7467
7700
|
const carve = !!cfg.carveReport;
|
|
7468
7701
|
let stopWatch = carve ? () => {
|
|
7469
|
-
} :
|
|
7702
|
+
} : watchSources(cfg.projectDirs ?? [cfg.projectDir], onEstateChange);
|
|
7470
7703
|
let stopPoll = !carve && cfg.env && cfg.pollSecs ? startDriftPoll({
|
|
7471
7704
|
intervalMs: cfg.pollSecs * 1e3,
|
|
7472
|
-
|
|
7705
|
+
// Per-member queries, swept sequentially inside the poll (#297): a
|
|
7706
|
+
// member read can take seconds (#295), so an estate-wide tick must
|
|
7707
|
+
// stretch, not stampede N describes at once.
|
|
7708
|
+
members: (cfg.projectDirs ?? [cfg.projectDir]).map((dir) => ({
|
|
7709
|
+
dir,
|
|
7710
|
+
query: () => graphIr(dir, { live: true, overlay: true, env: cfg.env })
|
|
7711
|
+
})),
|
|
7473
7712
|
onChange: onPollDrift,
|
|
7474
|
-
onError: (err) => process.stderr.write(`poll: ${err instanceof Error ? err.message : String(err)}
|
|
7713
|
+
onError: (dir, err) => process.stderr.write(`poll${memberTag(dir)}: ${err instanceof Error ? err.message : String(err)}
|
|
7475
7714
|
`)
|
|
7476
7715
|
}) : () => {
|
|
7477
7716
|
};
|
|
7478
7717
|
cfg.onProjectSwitch = (dir) => {
|
|
7479
7718
|
stopWatch();
|
|
7480
|
-
stopWatch =
|
|
7719
|
+
stopWatch = watchSources(cfg.projectDirs ?? [dir], onEstateChange);
|
|
7481
7720
|
stopPoll();
|
|
7482
7721
|
stopPoll = () => {
|
|
7483
7722
|
};
|
|
@@ -7551,7 +7790,7 @@ async function startServer(cfg) {
|
|
|
7551
7790
|
|
|
7552
7791
|
// src/export.ts
|
|
7553
7792
|
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, copyFileSync, readFileSync as readFileSync14, readdirSync as readdirSync5 } from "node:fs";
|
|
7554
|
-
import { join as join17, dirname as dirname5, basename } from "node:path";
|
|
7793
|
+
import { join as join17, dirname as dirname5, basename as basename2 } from "node:path";
|
|
7555
7794
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
7556
7795
|
var LENS_PARAMS = ["components", "detail", "env", "logical", "radial", "tier"];
|
|
7557
7796
|
function canonicalKey(path, params) {
|
|
@@ -7602,7 +7841,7 @@ function webDir() {
|
|
|
7602
7841
|
return join17(dirname5(fileURLToPath4(import.meta.url)), "..", "web");
|
|
7603
7842
|
}
|
|
7604
7843
|
function workerName(project, override) {
|
|
7605
|
-
const raw = override ?? `behold-${
|
|
7844
|
+
const raw = override ?? `behold-${basename2(project)}`;
|
|
7606
7845
|
const name = raw.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63);
|
|
7607
7846
|
return name || "behold-export";
|
|
7608
7847
|
}
|