@intentius/behold 0.10.0 → 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 +303 -58
- package/example-argo-estate/app-a/package.json +2 -2
- package/example-argo-estate/app-b/package.json +2 -2
- package/example-argo-estate/control-plane/package.json +2 -2
- package/example-argo-estate/package-lock.json +139 -596
- 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/app-a/package.json +2 -2
- package/example-flux-estate/app-b/package.json +2 -2
- package/example-flux-estate/control-plane/package.json +2 -2
- package/example-flux-estate/package-lock.json +139 -596
- 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",
|
|
@@ -5140,10 +5300,16 @@ var ARGO_SYNC = {
|
|
|
5140
5300
|
Unknown: "unknown"
|
|
5141
5301
|
};
|
|
5142
5302
|
var ARGO_ERROR_CONDITION = /Error$/;
|
|
5303
|
+
var ARGO_ERROR_STRING = /^([A-Za-z]*Error)(?:=[^:]*)?: (.+)$/;
|
|
5143
5304
|
function argoErrorMessage(tree) {
|
|
5144
5305
|
const conditions = tree?.conditions;
|
|
5145
5306
|
if (!Array.isArray(conditions)) return void 0;
|
|
5146
5307
|
for (const c of conditions) {
|
|
5308
|
+
if (typeof c === "string") {
|
|
5309
|
+
const m = ARGO_ERROR_STRING.exec(c);
|
|
5310
|
+
if (m) return m[2];
|
|
5311
|
+
continue;
|
|
5312
|
+
}
|
|
5147
5313
|
const type = str2(rec2(c)?.type);
|
|
5148
5314
|
if (!type || !ARGO_ERROR_CONDITION.test(type)) continue;
|
|
5149
5315
|
const message = str2(rec2(c)?.message);
|
|
@@ -5220,6 +5386,7 @@ function revisionVerdict(tree) {
|
|
|
5220
5386
|
return { health: "progressing", detail: `applied ${applied}, attempted ${attempted}` };
|
|
5221
5387
|
}
|
|
5222
5388
|
var FLUX_TYPE_PREFIX = "K8s::Flux::";
|
|
5389
|
+
var FLUX_HELMREPOSITORY_TYPE = "K8s::Flux::HelmRepository";
|
|
5223
5390
|
function fluxVerdict(o) {
|
|
5224
5391
|
if (!o.type?.startsWith(FLUX_TYPE_PREFIX)) return void 0;
|
|
5225
5392
|
const tree = statusTree(o);
|
|
@@ -5227,6 +5394,9 @@ function fluxVerdict(o) {
|
|
|
5227
5394
|
if (ready?.status === "False") return { health: "degraded", detail: describeReady(ready) };
|
|
5228
5395
|
if (ready?.status === "Unknown") return { health: "progressing", detail: describeReady(ready) };
|
|
5229
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
|
+
}
|
|
5230
5400
|
const conditions = tree?.conditions;
|
|
5231
5401
|
const reconciling = Array.isArray(conditions) && conditions.some((c) => {
|
|
5232
5402
|
const cond = rec2(c);
|
|
@@ -5238,9 +5408,41 @@ function fluxVerdict(o) {
|
|
|
5238
5408
|
}
|
|
5239
5409
|
return void 0;
|
|
5240
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
|
+
}
|
|
5241
5443
|
function classifyObservedHealth(observed) {
|
|
5242
5444
|
if (!observed) return { health: "unknown" };
|
|
5243
|
-
return argoVerdict(observed) ?? fluxVerdict(observed) ?? { health: classifyHealth(observed.status) };
|
|
5445
|
+
return argoVerdict(observed) ?? fluxVerdict(observed) ?? deploymentVerdict(observed) ?? { health: classifyHealth(observed.status) };
|
|
5244
5446
|
}
|
|
5245
5447
|
|
|
5246
5448
|
// src/apply.ts
|
|
@@ -5371,9 +5573,14 @@ var OpRunner = class {
|
|
|
5371
5573
|
* Start `chant run <name>` unless one is already running (the Sync/Adopt/auto-
|
|
5372
5574
|
* sync path). `cwd` is the Op's own project dir (#31 multi-estate); defaults to
|
|
5373
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.
|
|
5374
5581
|
*/
|
|
5375
|
-
trigger(name, opEnv, cwd) {
|
|
5376
|
-
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);
|
|
5377
5584
|
}
|
|
5378
5585
|
/**
|
|
5379
5586
|
* Run an arbitrary `chant` invocation through the same guard/stream/PR/capture
|
|
@@ -5766,13 +5973,31 @@ async function openRollbackBranches(projectDir, env) {
|
|
|
5766
5973
|
|
|
5767
5974
|
// src/estate.ts
|
|
5768
5975
|
import { statSync as statSync3 } from "node:fs";
|
|
5976
|
+
import { availableParallelism } from "node:os";
|
|
5769
5977
|
import { join as join13, resolve as resolve3, sep as sep2 } from "node:path";
|
|
5770
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
|
+
}
|
|
5771
5995
|
async function composeEstate(projectDirs, opts = {}) {
|
|
5772
5996
|
const names = shortStackNames(projectDirs);
|
|
5773
|
-
const stacks = await
|
|
5774
|
-
|
|
5775
|
-
|
|
5997
|
+
const stacks = await mapPool(projectDirs, estateReadPool(projectDirs.length), async (dir, i) => ({
|
|
5998
|
+
name: names[i],
|
|
5999
|
+
ir: await graphIr(dir, opts)
|
|
6000
|
+
}));
|
|
5776
6001
|
return composeStacks(stacks);
|
|
5777
6002
|
}
|
|
5778
6003
|
var firstLine2 = (e) => {
|
|
@@ -5866,7 +6091,11 @@ function joinNamespaceBindings(members, isDir = realIsDir) {
|
|
|
5866
6091
|
async function estateNamespaceScopes(projectDirs, opts, isDir = realIsDir) {
|
|
5867
6092
|
if (projectDirs.length < 2) return /* @__PURE__ */ new Map();
|
|
5868
6093
|
const { live: _live, overlay: _overlay, namespace: _namespace, ...src } = opts;
|
|
5869
|
-
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
|
+
);
|
|
5870
6099
|
const scopes = /* @__PURE__ */ new Map();
|
|
5871
6100
|
for (const j of joinNamespaceBindings(projectDirs.map((dir, i) => ({ dir, ir: irs[i] })), isDir)) {
|
|
5872
6101
|
if (!meetsFloor(resolveChant(j.dir).version, NAMESPACE_JOIN_FLOOR)) continue;
|
|
@@ -5885,28 +6114,26 @@ async function composeEstateOverlay(projectDirs, opts, classify) {
|
|
|
5885
6114
|
const joined = [];
|
|
5886
6115
|
const stacks = new Array(projectDirs.length);
|
|
5887
6116
|
const scopes = await estateNamespaceScopes(projectDirs, opts);
|
|
5888
|
-
await
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
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);
|
|
5892
6126
|
try {
|
|
5893
|
-
const
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
const src = await graphIr(dir, srcOpts);
|
|
5901
|
-
for (const n of src.nodes) n.attrs = { ...n.attrs, _status: "neutral", _unobserved: reason };
|
|
5902
|
-
stacks[i] = { name, ir: src };
|
|
5903
|
-
unobserved.push({ name, reason });
|
|
5904
|
-
} catch (err2) {
|
|
5905
|
-
dropped.push({ name, reason: firstLine2(err2) });
|
|
5906
|
-
}
|
|
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) });
|
|
5907
6134
|
}
|
|
5908
|
-
}
|
|
5909
|
-
);
|
|
6135
|
+
}
|
|
6136
|
+
});
|
|
5910
6137
|
const present = stacks.filter((s) => !!s);
|
|
5911
6138
|
return {
|
|
5912
6139
|
ir: composeStacks(present),
|
|
@@ -5953,6 +6180,12 @@ function watchSource(projectDir, onChange, debounceMs = 200) {
|
|
|
5953
6180
|
watcher.close();
|
|
5954
6181
|
};
|
|
5955
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
|
+
}
|
|
5956
6189
|
|
|
5957
6190
|
// src/poll.ts
|
|
5958
6191
|
function digestOf(nodes) {
|
|
@@ -5980,18 +6213,22 @@ function changedLexicons(prev, next) {
|
|
|
5980
6213
|
}
|
|
5981
6214
|
function startDriftPoll(opts) {
|
|
5982
6215
|
let stopped = false;
|
|
5983
|
-
|
|
6216
|
+
const last = /* @__PURE__ */ new Map();
|
|
5984
6217
|
let timer;
|
|
5985
6218
|
const tick = async () => {
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
const
|
|
5990
|
-
|
|
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);
|
|
5991
6231
|
}
|
|
5992
|
-
last = digests;
|
|
5993
|
-
} catch (err) {
|
|
5994
|
-
opts.onError?.(err);
|
|
5995
6232
|
}
|
|
5996
6233
|
if (!stopped) timer = setTimeout(tick, opts.intervalMs);
|
|
5997
6234
|
};
|
|
@@ -6565,6 +6802,7 @@ function carveRoutes(app, reportPath, demo) {
|
|
|
6565
6802
|
};
|
|
6566
6803
|
app.post("/api/carve/emit", (c) => runStep2(c, runCarveEmit));
|
|
6567
6804
|
app.post("/api/carve/bridge", (c) => runStep2(c, runCarveBridge));
|
|
6805
|
+
app.post("/api/carve/observe", (c) => runStep2(c, runCarveObserve));
|
|
6568
6806
|
app.post("/api/carve/plan", async (c) => {
|
|
6569
6807
|
const block = demoBlock();
|
|
6570
6808
|
if (block) {
|
|
@@ -6641,7 +6879,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
6641
6879
|
if (!info) {
|
|
6642
6880
|
return c.json({ error: `no Op named "${name}" in the estate` }, 404);
|
|
6643
6881
|
}
|
|
6644
|
-
if (!runner.trigger(name, info.env, info.dir)) {
|
|
6882
|
+
if (!runner.trigger(name, info.env, info.dir, Boolean(info.gate))) {
|
|
6645
6883
|
return c.json({ error: `an Op is already running (${runner.running})` }, 409);
|
|
6646
6884
|
}
|
|
6647
6885
|
return c.json({ started: true, name });
|
|
@@ -6686,7 +6924,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
6686
6924
|
const { name, gate } = c.req.param();
|
|
6687
6925
|
const info = estateOps().find((o) => o.name === name);
|
|
6688
6926
|
broadcaster.emit("op", `\u270E signal ${name} ${gate}`);
|
|
6689
|
-
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);
|
|
6690
6928
|
if (code !== 0) return c.json({ error: stderr.trim() || `signal exited ${code}` }, 500);
|
|
6691
6929
|
return c.json({ signalled: true });
|
|
6692
6930
|
});
|
|
@@ -7428,17 +7666,18 @@ async function startServer(cfg) {
|
|
|
7428
7666
|
const app = createApp(cfg, broadcaster, frames, runner);
|
|
7429
7667
|
const autoSync = cfg.autoSync ?? "off";
|
|
7430
7668
|
const capture2 = () => captureFrame(cfg.projectDir, cfg.env, frames, broadcaster);
|
|
7431
|
-
const onEstateChange = () => {
|
|
7432
|
-
broadcaster.emit("changed");
|
|
7669
|
+
const onEstateChange = (memberDir) => {
|
|
7670
|
+
broadcaster.emit("changed", memberDir ?? "");
|
|
7433
7671
|
void capture2();
|
|
7434
7672
|
};
|
|
7435
|
-
const
|
|
7436
|
-
|
|
7673
|
+
const memberTag = (dir) => (cfg.projectDirs?.length ?? 0) > 1 ? ` [${basename(dir)}]` : "";
|
|
7674
|
+
const onPollDrift = (dir, movedLexicons) => {
|
|
7675
|
+
onEstateChange(dir);
|
|
7437
7676
|
if (autoSync === "off") return;
|
|
7438
|
-
void routeAutoSync(movedLexicons);
|
|
7677
|
+
void routeAutoSync(dir, movedLexicons);
|
|
7439
7678
|
};
|
|
7440
|
-
const routeAutoSync = async (movedLexicons) => {
|
|
7441
|
-
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();
|
|
7442
7681
|
const { picks, declined } = pickAutoSyncOps(
|
|
7443
7682
|
autoSync,
|
|
7444
7683
|
discoverEstateOps(cfg.projectDirs ?? [cfg.projectDir]),
|
|
@@ -7447,31 +7686,37 @@ async function startServer(cfg) {
|
|
|
7447
7686
|
suspended
|
|
7448
7687
|
);
|
|
7449
7688
|
for (const d of declined) {
|
|
7450
|
-
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}`);
|
|
7451
7690
|
}
|
|
7452
7691
|
for (const { op, lexicons } of picks) {
|
|
7453
7692
|
const scope = lexicons.join("+");
|
|
7454
|
-
if (runner.trigger(op.name, op.env, op.dir)) {
|
|
7455
|
-
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}`);
|
|
7456
7695
|
} else {
|
|
7457
|
-
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`);
|
|
7458
7697
|
}
|
|
7459
7698
|
}
|
|
7460
7699
|
};
|
|
7461
7700
|
const carve = !!cfg.carveReport;
|
|
7462
7701
|
let stopWatch = carve ? () => {
|
|
7463
|
-
} :
|
|
7702
|
+
} : watchSources(cfg.projectDirs ?? [cfg.projectDir], onEstateChange);
|
|
7464
7703
|
let stopPoll = !carve && cfg.env && cfg.pollSecs ? startDriftPoll({
|
|
7465
7704
|
intervalMs: cfg.pollSecs * 1e3,
|
|
7466
|
-
|
|
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
|
+
})),
|
|
7467
7712
|
onChange: onPollDrift,
|
|
7468
|
-
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)}
|
|
7469
7714
|
`)
|
|
7470
7715
|
}) : () => {
|
|
7471
7716
|
};
|
|
7472
7717
|
cfg.onProjectSwitch = (dir) => {
|
|
7473
7718
|
stopWatch();
|
|
7474
|
-
stopWatch =
|
|
7719
|
+
stopWatch = watchSources(cfg.projectDirs ?? [dir], onEstateChange);
|
|
7475
7720
|
stopPoll();
|
|
7476
7721
|
stopPoll = () => {
|
|
7477
7722
|
};
|
|
@@ -7545,7 +7790,7 @@ async function startServer(cfg) {
|
|
|
7545
7790
|
|
|
7546
7791
|
// src/export.ts
|
|
7547
7792
|
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, copyFileSync, readFileSync as readFileSync14, readdirSync as readdirSync5 } from "node:fs";
|
|
7548
|
-
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";
|
|
7549
7794
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
7550
7795
|
var LENS_PARAMS = ["components", "detail", "env", "logical", "radial", "tier"];
|
|
7551
7796
|
function canonicalKey(path, params) {
|
|
@@ -7596,7 +7841,7 @@ function webDir() {
|
|
|
7596
7841
|
return join17(dirname5(fileURLToPath4(import.meta.url)), "..", "web");
|
|
7597
7842
|
}
|
|
7598
7843
|
function workerName(project, override) {
|
|
7599
|
-
const raw = override ?? `behold-${
|
|
7844
|
+
const raw = override ?? `behold-${basename2(project)}`;
|
|
7600
7845
|
const name = raw.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63);
|
|
7601
7846
|
return name || "behold-export";
|
|
7602
7847
|
}
|