@intentius/behold 0.27.0 → 0.29.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 CHANGED
@@ -5828,10 +5828,10 @@ var require_utils = __commonJS({
5828
5828
  REGEX_SPECIAL_CHARS_GLOBAL
5829
5829
  } = require_constants();
5830
5830
  exports.isObject = (val) => val !== null && typeof val === "object" && !Array.isArray(val);
5831
- exports.hasRegexChars = (str15) => REGEX_SPECIAL_CHARS.test(str15);
5832
- exports.isRegexChar = (str15) => str15.length === 1 && exports.hasRegexChars(str15);
5833
- exports.escapeRegex = (str15) => str15.replace(REGEX_SPECIAL_CHARS_GLOBAL, "\\$1");
5834
- exports.toPosixSlashes = (str15) => str15.replace(REGEX_BACKSLASH, "/");
5831
+ exports.hasRegexChars = (str16) => REGEX_SPECIAL_CHARS.test(str16);
5832
+ exports.isRegexChar = (str16) => str16.length === 1 && exports.hasRegexChars(str16);
5833
+ exports.escapeRegex = (str16) => str16.replace(REGEX_SPECIAL_CHARS_GLOBAL, "\\$1");
5834
+ exports.toPosixSlashes = (str16) => str16.replace(REGEX_BACKSLASH, "/");
5835
5835
  exports.isWindows = () => {
5836
5836
  if (typeof navigator !== "undefined" && navigator.platform) {
5837
5837
  const platform2 = navigator.platform.toLowerCase();
@@ -5842,8 +5842,8 @@ var require_utils = __commonJS({
5842
5842
  }
5843
5843
  return false;
5844
5844
  };
5845
- exports.removeBackslashes = (str15) => {
5846
- return str15.replace(REGEX_REMOVE_BACKSLASH, (match) => {
5845
+ exports.removeBackslashes = (str16) => {
5846
+ return str16.replace(REGEX_REMOVE_BACKSLASH, (match) => {
5847
5847
  return match === "\\" ? "" : match;
5848
5848
  });
5849
5849
  };
@@ -5933,7 +5933,7 @@ var require_scan = __commonJS({
5933
5933
  const slashes = [];
5934
5934
  const tokens = [];
5935
5935
  const parts = [];
5936
- let str15 = input;
5936
+ let str16 = input;
5937
5937
  let index = -1;
5938
5938
  let start = 0;
5939
5939
  let lastIndex = 0;
@@ -5952,10 +5952,10 @@ var require_scan = __commonJS({
5952
5952
  let code;
5953
5953
  let token = { value: "", depth: 0, isGlob: false };
5954
5954
  const eos = () => index >= length;
5955
- const peek = () => str15.charCodeAt(index + 1);
5955
+ const peek = () => str16.charCodeAt(index + 1);
5956
5956
  const advance = () => {
5957
5957
  prev = code;
5958
- return str15.charCodeAt(++index);
5958
+ return str16.charCodeAt(++index);
5959
5959
  };
5960
5960
  while (index < length) {
5961
5961
  code = advance();
@@ -6123,24 +6123,24 @@ var require_scan = __commonJS({
6123
6123
  isExtglob = false;
6124
6124
  isGlob = false;
6125
6125
  }
6126
- let base = str15;
6126
+ let base = str16;
6127
6127
  let prefix = "";
6128
6128
  let glob = "";
6129
6129
  if (start > 0) {
6130
- prefix = str15.slice(0, start);
6131
- str15 = str15.slice(start);
6130
+ prefix = str16.slice(0, start);
6131
+ str16 = str16.slice(start);
6132
6132
  lastIndex -= start;
6133
6133
  }
6134
6134
  if (base && isGlob === true && lastIndex > 0) {
6135
- base = str15.slice(0, lastIndex);
6136
- glob = str15.slice(lastIndex);
6135
+ base = str16.slice(0, lastIndex);
6136
+ glob = str16.slice(lastIndex);
6137
6137
  } else if (isGlob === true) {
6138
6138
  base = "";
6139
- glob = str15;
6139
+ glob = str16;
6140
6140
  } else {
6141
- base = str15;
6141
+ base = str16;
6142
6142
  }
6143
- if (base && base !== "" && base !== "/" && base !== str15) {
6143
+ if (base && base !== "" && base !== "/" && base !== str16) {
6144
6144
  if (isPathSeparator(base.charCodeAt(base.length - 1))) {
6145
6145
  base = base.slice(0, -1);
6146
6146
  }
@@ -7187,8 +7187,8 @@ var require_parse = __commonJS({
7187
7187
  if (opts2.noglobstar === true) return star;
7188
7188
  return `(${capture2}(?:(?!${START_ANCHOR}${opts2.dot ? DOTS_SLASH : DOT_LITERAL}).)*?)`;
7189
7189
  };
7190
- const create = (str15) => {
7191
- switch (str15) {
7190
+ const create = (str16) => {
7191
+ switch (str16) {
7192
7192
  case "*":
7193
7193
  return `${nodot}${ONE_CHAR}${star}`;
7194
7194
  case ".*":
@@ -7206,7 +7206,7 @@ var require_parse = __commonJS({
7206
7206
  case "**/.*":
7207
7207
  return `(?:${nodot}${globstar(opts)}${SLASH_LITERAL})?${DOT_LITERAL}${ONE_CHAR}${star}`;
7208
7208
  default: {
7209
- const match = /^(.*?)\.(\w+)$/.exec(str15);
7209
+ const match = /^(.*?)\.(\w+)$/.exec(str16);
7210
7210
  if (!match) return;
7211
7211
  const source3 = create(match[1]);
7212
7212
  if (!source3) return;
@@ -7237,9 +7237,9 @@ var require_picomatch = __commonJS({
7237
7237
  var picomatch3 = (glob, options2, returnState = false) => {
7238
7238
  if (Array.isArray(glob)) {
7239
7239
  const fns = glob.map((input) => picomatch3(input, options2, returnState));
7240
- const arrayMatcher = (str15) => {
7240
+ const arrayMatcher = (str16) => {
7241
7241
  for (const isMatch of fns) {
7242
- const state2 = isMatch(str15);
7242
+ const state2 = isMatch(str16);
7243
7243
  if (state2) return state2;
7244
7244
  }
7245
7245
  return false;
@@ -7315,7 +7315,7 @@ var require_picomatch = __commonJS({
7315
7315
  const regex = glob instanceof RegExp ? glob : picomatch3.makeRe(glob, options2);
7316
7316
  return regex.test(utils.basename(input, { windows: posix5 }));
7317
7317
  };
7318
- picomatch3.isMatch = (str15, patterns, options2) => picomatch3(patterns, options2)(str15);
7318
+ picomatch3.isMatch = (str16, patterns, options2) => picomatch3(patterns, options2)(str16);
7319
7319
  picomatch3.parse = (pattern, options2) => {
7320
7320
  if (Array.isArray(pattern)) return pattern.map((p) => picomatch3.parse(p, options2));
7321
7321
  return parse(pattern, { ...options2, fastpaths: false });
@@ -8904,8 +8904,8 @@ __export(tree_exports, {
8904
8904
  workingTree: () => workingTree
8905
8905
  });
8906
8906
  import { execFileSync as execFileSync2 } from "node:child_process";
8907
- import { readdirSync as readdirSync10, readFileSync as readFileSync18, statSync as statSync8 } from "node:fs";
8908
- import { join as join25 } from "node:path";
8907
+ import { readdirSync as readdirSync10, readFileSync as readFileSync19, statSync as statSync8 } from "node:fs";
8908
+ import { join as join26 } from "node:path";
8909
8909
  function joinPath(...parts) {
8910
8910
  return parts.filter((p) => p !== "" && p !== ".").join("/");
8911
8911
  }
@@ -8914,7 +8914,7 @@ function workingTree(root) {
8914
8914
  label: "",
8915
8915
  stat(path) {
8916
8916
  try {
8917
- const s = statSync8(join25(root, path));
8917
+ const s = statSync8(join26(root, path));
8918
8918
  return s.isDirectory() ? "dir" : s.isFile() ? "file" : void 0;
8919
8919
  } catch {
8920
8920
  return void 0;
@@ -8923,7 +8923,7 @@ function workingTree(root) {
8923
8923
  list(path) {
8924
8924
  let dirents;
8925
8925
  try {
8926
- dirents = readdirSync10(join25(root, path), { withFileTypes: true });
8926
+ dirents = readdirSync10(join26(root, path), { withFileTypes: true });
8927
8927
  } catch {
8928
8928
  return void 0;
8929
8929
  }
@@ -8932,7 +8932,7 @@ function workingTree(root) {
8932
8932
  let type = d.isDirectory() ? "dir" : d.isFile() ? "file" : void 0;
8933
8933
  if (!type && d.isSymbolicLink()) {
8934
8934
  try {
8935
- const s = statSync8(join25(root, path, d.name));
8935
+ const s = statSync8(join26(root, path, d.name));
8936
8936
  type = s.isDirectory() ? "dir" : s.isFile() ? "file" : void 0;
8937
8937
  } catch {
8938
8938
  }
@@ -8942,10 +8942,10 @@ function workingTree(root) {
8942
8942
  return out;
8943
8943
  },
8944
8944
  read(path) {
8945
- return readFileSync18(join25(root, path), "utf-8");
8945
+ return readFileSync19(join26(root, path), "utf-8");
8946
8946
  },
8947
8947
  bytes(path) {
8948
- return readFileSync18(join25(root, path));
8948
+ return readFileSync19(join26(root, path));
8949
8949
  }
8950
8950
  };
8951
8951
  }
@@ -9133,6 +9133,11 @@ var init_workspace_kinds_schema = __esm({
9133
9133
  },
9134
9134
  fields: {
9135
9135
  $ref: "#/$defs/fields"
9136
+ },
9137
+ reader: {
9138
+ type: "string",
9139
+ pattern: "^[a-z][a-z0-9-]{0,39}$",
9140
+ description: "A link reader chant ships, by name, that derives a member's links from the member's own files (#3932): dbt-project reads a dbt project's sources. The reader is chant's code and runs offline, parsing files as data; the package supplies only the name. A name this chant doesn't have keeps the kind without a reader, and the load reports it. A chant older than this field fails a kinds file that uses it."
9136
9141
  }
9137
9142
  },
9138
9143
  patternProperties: {
@@ -9542,12 +9547,12 @@ var init_identity_gate_rule = __esm({
9542
9547
  });
9543
9548
 
9544
9549
  // node_modules/@intentius/chant/src/workspace/class-data.ts
9545
- import { existsSync as existsSync17, readFileSync as readFileSync19, statSync as statSync9 } from "node:fs";
9546
- import { dirname as dirname7, join as join26, posix, resolve as resolve14 } from "node:path";
9550
+ import { existsSync as existsSync17, readFileSync as readFileSync20, statSync as statSync9 } from "node:fs";
9551
+ import { dirname as dirname7, join as join27, posix, resolve as resolve14 } from "node:path";
9547
9552
  function diskFiles(dir) {
9548
9553
  return (path) => {
9549
9554
  try {
9550
- return readFileSync19(join26(dir, ...path.split("/")), "utf-8");
9555
+ return readFileSync20(join27(dir, ...path.split("/")), "utf-8");
9551
9556
  } catch {
9552
9557
  return void 0;
9553
9558
  }
@@ -9555,9 +9560,9 @@ function diskFiles(dir) {
9555
9560
  }
9556
9561
  function findInstalledPackage(name, fromDir) {
9557
9562
  for (let dir = resolve14(fromDir); ; dir = dirname7(dir)) {
9558
- const candidate = join26(dir, "node_modules", ...name.split("/"));
9563
+ const candidate = join27(dir, "node_modules", ...name.split("/"));
9559
9564
  try {
9560
- if (statSync9(candidate).isDirectory() && existsSync17(join26(candidate, "package.json"))) return candidate;
9565
+ if (statSync9(candidate).isDirectory() && existsSync17(join27(candidate, "package.json"))) return candidate;
9561
9566
  } catch {
9562
9567
  }
9563
9568
  if (dirname7(dir) === dir) return void 0;
@@ -10007,10 +10012,10 @@ var init_jsonc = __esm({
10007
10012
  });
10008
10013
 
10009
10014
  // node_modules/@intentius/chant/src/package-version.ts
10010
- import { readFileSync as readFileSync20 } from "node:fs";
10015
+ import { readFileSync as readFileSync21 } from "node:fs";
10011
10016
  import { fileURLToPath as fileURLToPath4 } from "node:url";
10012
10017
  function readChantPackageVersion() {
10013
- return JSON.parse(readFileSync20(fileURLToPath4(new URL("../package.json", import.meta.url)), "utf-8")).version;
10018
+ return JSON.parse(readFileSync21(fileURLToPath4(new URL("../package.json", import.meta.url)), "utf-8")).version;
10014
10019
  }
10015
10020
  var init_package_version = __esm({
10016
10021
  "node_modules/@intentius/chant/src/package-version.ts"() {
@@ -10809,8 +10814,8 @@ var init_declaration = __esm({
10809
10814
  });
10810
10815
 
10811
10816
  // src/cli.ts
10812
- import { resolve as resolve33, dirname as dirname21, join as join48 } from "node:path";
10813
- import { realpathSync as realpathSync3, existsSync as existsSync30, readFileSync as readFileSync36, rmSync as rmSync4 } from "node:fs";
10817
+ import { resolve as resolve33, dirname as dirname21, join as join50 } from "node:path";
10818
+ import { realpathSync as realpathSync3, existsSync as existsSync30, readFileSync as readFileSync38, rmSync as rmSync4 } from "node:fs";
10814
10819
  import { fileURLToPath as fileURLToPath12 } from "node:url";
10815
10820
  import { spawn as spawn9, spawnSync as spawnSync4 } from "node:child_process";
10816
10821
 
@@ -10868,8 +10873,8 @@ import { serve } from "@hono/node-server";
10868
10873
  import { fileURLToPath as fileURLToPath10 } from "node:url";
10869
10874
  import { execFile as execFile5 } from "node:child_process";
10870
10875
  import { promisify as promisify3 } from "node:util";
10871
- import { existsSync as existsSync27, readFileSync as readFileSync32 } from "node:fs";
10872
- import { basename as basename5, dirname as dirname18, join as join44, relative as relative10, resolve as resolve30, sep as sep9 } from "node:path";
10876
+ import { existsSync as existsSync27, readFileSync as readFileSync34 } from "node:fs";
10877
+ import { basename as basename5, dirname as dirname18, join as join46, relative as relative10, resolve as resolve30, sep as sep9 } from "node:path";
10873
10878
 
10874
10879
  // src/recents.ts
10875
10880
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
@@ -13275,10 +13280,10 @@ function joinNamespaceBindings(members, isDir = realIsDir) {
13275
13280
  if (!winner || tied) continue;
13276
13281
  const prev = best.get(winner.dir);
13277
13282
  if (prev === "ambiguous") continue;
13278
- const join49 = { dir: winner.dir, namespace: b.namespace, path: b.path, declaredBy: b.declaredBy };
13279
- if (!prev) best.set(winner.dir, { score: winner.score, join: join49 });
13283
+ const join51 = { dir: winner.dir, namespace: b.namespace, path: b.path, declaredBy: b.declaredBy };
13284
+ if (!prev) best.set(winner.dir, { score: winner.score, join: join51 });
13280
13285
  else if (prev.join.namespace !== b.namespace) best.set(winner.dir, "ambiguous");
13281
- else if (winner.score > prev.score) best.set(winner.dir, { score: winner.score, join: join49 });
13286
+ else if (winner.score > prev.score) best.set(winner.dir, { score: winner.score, join: join51 });
13282
13287
  }
13283
13288
  return [...best.values()].filter((v) => v !== "ambiguous").map((v) => v.join);
13284
13289
  }
@@ -15776,7 +15781,9 @@ function estateFace(irIn, boxesIn, opts = {}) {
15776
15781
  const face = cardFaces(irIn, boxesIn, opts.prefixes ? { prefixes: opts.prefixes } : {});
15777
15782
  const ir = face.ir;
15778
15783
  if (Object.keys(face.overrides).length) faceOverrides.set(ir, face.overrides);
15779
- const boxes = boxesIn ? Object.fromEntries(Object.entries(boxesIn).map(([key, ids]) => [key, ids.map((id) => face.display.get(id) ?? id)])) : void 0;
15784
+ const toDisplay = (record) => record ? Object.fromEntries(Object.entries(record).map(([key, ids]) => [key, ids.map((id) => face.display.get(id) ?? id)])) : void 0;
15785
+ const boxes = toDisplay(boxesIn);
15786
+ const subgroups = boxes ? toDisplay(opts.subgroups) : void 0;
15780
15787
  const realBand = moduleBandOf(irIn);
15781
15788
  const toReal = new Map(ir.nodes.map((n, i) => [n.id, irIn.nodes[i].id]));
15782
15789
  const bandOf = realBand ? (id) => realBand(toReal.get(id) ?? id) : void 0;
@@ -15786,13 +15793,14 @@ function estateFace(irIn, boxesIn, opts = {}) {
15786
15793
  ...opts.nodesep !== void 0 ? { nodesep: opts.nodesep } : {},
15787
15794
  ...opts.ranksep !== void 0 ? { ranksep: opts.ranksep } : {}
15788
15795
  }) : { ir, grids: /* @__PURE__ */ new Map() };
15789
- return { ir, overrides: face.overrides, boxes, display: face.display, bandOf, chains, restore: face.restore };
15796
+ return { ir, overrides: face.overrides, boxes, subgroups, display: face.display, bandOf, chains, restore: face.restore };
15790
15797
  }
15791
15798
  function packEstateBoxes(layout, face, opts = {}) {
15792
15799
  const boxes = face.boxes;
15793
15800
  if (!boxes) return;
15794
- const boxGrids = packBoxComponents(layout, face.ir, boxes, face.bandOf);
15795
- if (opts.members) packMemberBoxes(layout, face.ir, boxes);
15801
+ const boxGrids = packBoxComponents(layout, face.ir, boxes, face.bandOf, face.subgroups);
15802
+ if (opts.members) packMemberBoxes(layout, face.ir, { ...topLevelSubgroups(layout.groups, face.subgroups), ...boxes });
15803
+ if (face.subgroups) fitNestedBoxes(layout, face.ir, boxes, face.subgroups);
15796
15804
  addBandBoxes(layout, new Map([...boxGrids, ...face.chains.grids]), footprints(face.ir, face.overrides));
15797
15805
  applyBadges(layout.groups, opts.badges, opts.marks);
15798
15806
  }
@@ -15826,17 +15834,27 @@ function renderGraph(irIn, opts = {}) {
15826
15834
  const groupsIn = irIn.groups;
15827
15835
  const pipelines = groupsIn.byPipeline && Object.keys(groupsIn.byPipeline).length ? groupsIn.byPipeline : void 0;
15828
15836
  const boxKey = groupsIn.byWave ? "byWave" : opts.boxes ?? (pipelines ? "byPipeline" : void 0);
15829
- const face = estateFace(irIn, boxKey ? groupsIn[boxKey] : void 0);
15837
+ const nested = boxKey === "byStack" ? pipelines : void 0;
15838
+ const face = estateFace(irIn, boxKey ? groupsIn[boxKey] : void 0, nested ? { subgroups: nested } : {});
15830
15839
  const ir = face.ir;
15831
15840
  const boxes = face.boxes;
15832
15841
  const chains = face.chains;
15833
- const layout = layoutIr(chains.ir, { fit: true, overrides: face.overrides, ...boxes ? { groups: boxes } : {} });
15842
+ const layout = layoutIr(chains.ir, {
15843
+ fit: true,
15844
+ overrides: face.overrides,
15845
+ ...boxes ? { groups: boxes } : {},
15846
+ ...face.subgroups ? { subgroups: face.subgroups } : {}
15847
+ });
15834
15848
  if (opts.radial && !boxes) radializeLayout(layout, groupKeyByNode(ir), footprints(ir, face.overrides));
15835
15849
  else if (!boxes) packComponents(layout, ir);
15836
15850
  packEstateBoxes(layout, face, { members: boxKey === "byStack", ...opts.groupBadges ? { badges: opts.groupBadges } : {} });
15837
- if (boxKey === "byPipeline" && pipelines) {
15851
+ if ((boxKey === "byPipeline" || nested) && pipelines) {
15838
15852
  const titles = pipelineTitles(irIn, pipelines);
15839
- for (const box of layout.groups ?? []) if (box.id !== void 0 && titles[box.id] !== void 0) box.title = titles[box.id];
15853
+ for (const box of layout.groups ?? []) {
15854
+ if (box.id === void 0 || titles[box.id] === void 0) continue;
15855
+ const title = titles[box.id];
15856
+ box.title = title === box.id && box.parent !== void 0 && title.startsWith(`${box.parent}/`) ? title.slice(box.parent.length + 1) : title;
15857
+ }
15840
15858
  }
15841
15859
  const svg = renderSvg(ir, layout, {
15842
15860
  fit: true,
@@ -15849,9 +15867,16 @@ function renderGraph(irIn, opts = {}) {
15849
15867
  }
15850
15868
  function pipelineTitles(ir, byPipeline) {
15851
15869
  const nodes = new Map(ir.nodes.map((n) => [n.id, n]));
15870
+ const workflowOf = (ids) => {
15871
+ const workflows = ids.flatMap((id) => {
15872
+ const n = nodes.get(id);
15873
+ return n && /::Workflow$/.test(n.kind) ? [n] : [];
15874
+ });
15875
+ return workflows.length === 1 ? workflows[0] : void 0;
15876
+ };
15852
15877
  return Object.fromEntries(
15853
- Object.keys(byPipeline).map((key) => {
15854
- const name = nodes.get(key)?.attrs?.["name"];
15878
+ Object.entries(byPipeline).map(([key, ids]) => {
15879
+ const name = (nodes.get(key) ?? workflowOf(ids))?.attrs?.["name"];
15855
15880
  return [key, typeof name === "string" && name.trim() ? name : key];
15856
15881
  })
15857
15882
  );
@@ -15901,6 +15926,39 @@ function addBandBoxes(layout, grids, sizes) {
15901
15926
  boxes.push(...added);
15902
15927
  }
15903
15928
  var BAND_PAD = 14;
15929
+ var NEST_PAD_X = 50;
15930
+ var NEST_PAD_Y = 36;
15931
+ var NEST_OUTER = 36;
15932
+ function topLevelSubgroups(boxes, subgroups) {
15933
+ if (!subgroups) return {};
15934
+ return Object.fromEntries(
15935
+ (boxes ?? []).flatMap((b) => b.parent === void 0 && b.id !== void 0 && subgroups[b.id] ? [[b.id, subgroups[b.id]]] : [])
15936
+ );
15937
+ }
15938
+ function fitNestedBoxes(layout, ir, boxes, subgroups, only2) {
15939
+ const at2 = new Map(layout.nodes.map((n) => [n.id, n]));
15940
+ const ownerOf2 = /* @__PURE__ */ new Map();
15941
+ for (const [key, ids] of Object.entries(boxes)) for (const id of ids) if (at2.has(id)) ownerOf2.set(id, key);
15942
+ const sizes = footprints(ir);
15943
+ for (const box of layout.groups ?? []) {
15944
+ if (box.parent === void 0 || box.id === void 0 || only2 !== void 0 && box.parent !== only2) continue;
15945
+ const cards = (subgroups[box.id] ?? []).flatMap((id) => {
15946
+ const p = at2.get(id);
15947
+ if (!p || ownerOf2.get(id) !== box.parent) return [];
15948
+ const s = sizes.get(id) ?? { w: NODE_W, h: NODE_H };
15949
+ return [{ x: p.x, y: p.y, w: s.w, h: s.h }];
15950
+ });
15951
+ if (!cards.length) continue;
15952
+ const x0 = Math.min(...cards.map((c) => c.x - c.w / 2)) - NEST_PAD_X;
15953
+ const x1 = Math.max(...cards.map((c) => c.x + c.w / 2)) + NEST_PAD_X;
15954
+ const y0 = Math.min(...cards.map((c) => c.y - c.h / 2)) - NEST_PAD_Y;
15955
+ const y1 = Math.max(...cards.map((c) => c.y + c.h / 2)) + NEST_PAD_Y;
15956
+ box.x = (x0 + x1) / 2;
15957
+ box.y = (y0 + y1) / 2;
15958
+ box.w = x1 - x0;
15959
+ box.h = y1 - y0;
15960
+ }
15961
+ }
15904
15962
  function clampInside(r, box) {
15905
15963
  const inset = 6;
15906
15964
  const left = box.x - box.w / 2 + inset;
@@ -16216,7 +16274,8 @@ function packComponents(layout, ir) {
16216
16274
  layout.height = Math.max(...nodes.map((n) => n.y + halfH(n))) + pad;
16217
16275
  }
16218
16276
  function packMemberBoxes(layout, ir, members) {
16219
- const boxes = layout.groups ?? [];
16277
+ const allBoxes = layout.groups ?? [];
16278
+ const boxes = allBoxes.filter((b) => b.parent === void 0);
16220
16279
  if (boxes.length < 4) return;
16221
16280
  const nodes = layout.nodes;
16222
16281
  const size = footprints(ir);
@@ -16332,7 +16391,7 @@ function gridComponent(one, sizes) {
16332
16391
  const rows = Math.ceil(order.length / cols);
16333
16392
  return { local, w: Math.min(cols, order.length) * (cell.w + sep12.x) - sep12.x, h: rows * (cell.h + sep12.y) - sep12.y };
16334
16393
  }
16335
- function packBoxComponents(layout, ir, boxes, bandOf) {
16394
+ function packBoxComponents(layout, ir, boxes, bandOf, subgroups) {
16336
16395
  const grids = /* @__PURE__ */ new Map();
16337
16396
  const groupBoxes = layout.groups ?? [];
16338
16397
  const nodes = layout.nodes;
@@ -16363,6 +16422,10 @@ function packBoxComponents(layout, ir, boxes, bandOf) {
16363
16422
  const b = idxOf.get(e.to);
16364
16423
  if (a !== void 0 && b !== void 0) parent[find(a)] = find(b);
16365
16424
  }
16425
+ for (const ids2 of Object.values(subgroups ?? {})) {
16426
+ const inside = ids2.flatMap((id) => idxOf.get(id) ?? []);
16427
+ for (const i of inside.slice(1)) parent[find(i)] = find(inside[0]);
16428
+ }
16366
16429
  const comps = /* @__PURE__ */ new Map();
16367
16430
  own.forEach((n, i) => (comps.get(find(i)) ?? comps.set(find(i), []).get(find(i))).push(n));
16368
16431
  if (comps.size < 2) continue;
@@ -16375,12 +16438,19 @@ function packBoxComponents(layout, ir, boxes, bandOf) {
16375
16438
  edges: ir.edges.filter((e) => inside.has(e.from) && inside.has(e.to)),
16376
16439
  groups: {}
16377
16440
  };
16378
- let one = layoutIr(sub, { fit: true });
16441
+ const nest = Object.fromEntries(
16442
+ Object.entries(subgroups ?? {}).flatMap(([k, ids2]) => {
16443
+ const mine = ids2.filter((id) => inside.has(id));
16444
+ return mine.length ? [[k, mine]] : [];
16445
+ })
16446
+ );
16447
+ const nests = Object.keys(nest).length > 0;
16448
+ let one = layoutIr(sub, { fit: true, ...nests ? { groups: nest } : {} });
16379
16449
  if (one.width > one.height * BOX_ASPECT_MAX) {
16380
- const sideways = layoutIr(sub, { fit: true, rankdir: "LR" });
16450
+ const sideways = layoutIr(sub, { fit: true, rankdir: "LR", ...nests ? { groups: nest } : {} });
16381
16451
  if (offAspect(sideways) < offAspect(one)) one = sideways;
16382
16452
  }
16383
- const placed = offAspect(one) > GRID_COMPONENT ? gridComponent(one, size) : { local: new Map(one.nodes.map((n) => [n.id, { x: n.x, y: one.height - n.y }])), w: one.width, h: one.height };
16453
+ const placed = !nests && offAspect(one) > GRID_COMPONENT ? gridComponent(one, size) : { local: new Map(one.nodes.map((n) => [n.id, { x: n.x, y: one.height - n.y }])), w: one.width, h: one.height };
16384
16454
  laid.set(root, { ...placed, ns });
16385
16455
  }
16386
16456
  const order = [void 0];
@@ -16396,8 +16466,9 @@ function packBoxComponents(layout, ir, boxes, bandOf) {
16396
16466
  const all = [...laid.values()];
16397
16467
  const area = all.reduce((s, c) => s + (c.w + COMPONENT_GAP) * (c.h + COMPONENT_GAP), 0);
16398
16468
  const targetW = Math.max(Math.max(...all.map((c) => c.w)), Math.sqrt(area * ROW_ASPECT));
16399
- const originX = box.x - box.w / 2 + BAND_PAD;
16400
- const originY = box.y - box.h / 2 + TITLE;
16469
+ const holdsNested = groupBoxes.some((b) => b.parent === key);
16470
+ const originX = box.x - box.w / 2 + (holdsNested ? NEST_OUTER : BAND_PAD);
16471
+ const originY = box.y - box.h / 2 + (holdsNested ? NEST_OUTER : TITLE);
16401
16472
  let shelfX = 0;
16402
16473
  let shelfY = 0;
16403
16474
  let shelfH = 0;
@@ -16429,10 +16500,18 @@ function packBoxComponents(layout, ir, boxes, bandOf) {
16429
16500
  }
16430
16501
  bands.push({ key: band, ids: ids2 });
16431
16502
  }
16432
- const x0 = Math.min(...own.map((n) => n.x - halfW(n))) - BAND_PAD;
16433
- const x1 = Math.max(...own.map((n) => n.x + halfW(n))) + BAND_PAD;
16434
- const y0 = Math.min(...own.map((n) => n.y - halfH(n))) - TITLE;
16435
- const y1 = Math.max(...own.map((n) => n.y + halfH(n))) + BAND_PAD;
16503
+ if (subgroups) fitNestedBoxes(layout, ir, boxes, subgroups, key);
16504
+ const inner = groupBoxes.filter((b) => b.parent === key);
16505
+ let x0 = Math.min(...own.map((n) => n.x - halfW(n))) - BAND_PAD;
16506
+ let x1 = Math.max(...own.map((n) => n.x + halfW(n))) + BAND_PAD;
16507
+ let y0 = Math.min(...own.map((n) => n.y - halfH(n))) - TITLE;
16508
+ let y1 = Math.max(...own.map((n) => n.y + halfH(n))) + BAND_PAD;
16509
+ for (const b of inner) {
16510
+ x0 = Math.min(x0, b.x - b.w / 2 - NEST_OUTER);
16511
+ x1 = Math.max(x1, b.x + b.w / 2 + NEST_OUTER);
16512
+ y0 = Math.min(y0, b.y - b.h / 2 - NEST_OUTER);
16513
+ y1 = Math.max(y1, b.y + b.h / 2 + NEST_OUTER);
16514
+ }
16436
16515
  box.x = (x0 + x1) / 2;
16437
16516
  box.y = (y0 + y1) / 2;
16438
16517
  box.w = x1 - x0;
@@ -17411,11 +17490,11 @@ var RAN = { ok: "ok", fail: "failed", skipped: "skipped" };
17411
17490
  function tookLabel(ms) {
17412
17491
  return ms < 1e3 ? `${ms}ms` : `${(ms / 1e3).toFixed(1)}s`;
17413
17492
  }
17414
- function paintPlayhead(ir, join49, gate) {
17415
- if (!join49.byNode.size && !join49.at) return ir;
17493
+ function paintPlayhead(ir, join51, gate) {
17494
+ if (!join51.byNode.size && !join51.at) return ir;
17416
17495
  const nodes = ir.nodes.map((node) => {
17417
- const run3 = join49.byNode.get(node.id);
17418
- const here = join49.at === node.id;
17496
+ const run3 = join51.byNode.get(node.id);
17497
+ const here = join51.at === node.id;
17419
17498
  if (!run3 && !here) return node;
17420
17499
  const isGate = node.attrs?._step === "gate";
17421
17500
  const attrs = { ...node.attrs };
@@ -17430,7 +17509,7 @@ function paintPlayhead(ir, join49, gate) {
17430
17509
  if (here) {
17431
17510
  attrs._playhead = true;
17432
17511
  attrs._status = isGate && gate ? "warn" : "accent";
17433
- attrs.run = isGate && gate ? `waiting for approval since ${gate.since}` : join49.atReason ?? "here";
17512
+ attrs.run = isGate && gate ? `waiting for approval since ${gate.since}` : join51.atReason ?? "here";
17434
17513
  if (isGate && gate?.description) attrs.gateDescription = gate.description;
17435
17514
  }
17436
17515
  return { ...node, attrs };
@@ -17452,11 +17531,11 @@ function pendingGateCard(op, gate, ir) {
17452
17531
  }
17453
17532
  };
17454
17533
  }
17455
- function playheadNote(state, join49) {
17534
+ function playheadNote(state, join51) {
17456
17535
  if (!state.op) {
17457
17536
  return "No run state: nothing has been run from this behold session, so every step paints as declared.";
17458
17537
  }
17459
- const settled = join49 ? join49.byNode.size : state.records.length;
17538
+ const settled = join51 ? join51.byNode.size : state.records.length;
17460
17539
  const bits = [];
17461
17540
  const head = state.status === "idle" ? `Run state for ${state.op}: no per-step records \u2014 this behold session didn't start the run, so only the gate query answers` : `Run: ${state.op} (${state.mode === "temporal" ? "durable" : "local executor"}) \u2014 ${settled} step${settled === 1 ? "" : "s"} settled`;
17462
17541
  if (state.status === "running") {
@@ -17472,9 +17551,9 @@ function playheadNote(state, join49) {
17472
17551
  } else if (state.gateNote) {
17473
17552
  bits.push(`gate state unavailable \u2014 ${state.gateNote}`);
17474
17553
  }
17475
- if (join49?.unplaced.length) {
17554
+ if (join51?.unplaced.length) {
17476
17555
  bits.push(
17477
- `${join49.unplaced.length} record${join49.unplaced.length === 1 ? "" : "s"} matched no declared step and ${join49.unplaced.length === 1 ? "is" : "are"} not painted`
17556
+ `${join51.unplaced.length} record${join51.unplaced.length === 1 ? "" : "s"} matched no declared step and ${join51.unplaced.length === 1 ? "is" : "are"} not painted`
17478
17557
  );
17479
17558
  }
17480
17559
  if (!bits.length) return `${head}.`;
@@ -18144,10 +18223,90 @@ var OpRunner = class {
18144
18223
  }
18145
18224
  };
18146
18225
 
18226
+ // src/public-mode.ts
18227
+ import { readFileSync as readFileSync15 } from "node:fs";
18228
+ import { join as join20 } from "node:path";
18229
+ var ReadOnlyRunner = class {
18230
+ running = null;
18231
+ applyProgress = initialApplyProgress;
18232
+ runState = initialRunState;
18233
+ operatorState = initialOperatorState;
18234
+ retarget() {
18235
+ }
18236
+ noteGate() {
18237
+ return initialRunState;
18238
+ }
18239
+ noteDeclaredConvergeOps() {
18240
+ return initialOperatorState;
18241
+ }
18242
+ noteOperator() {
18243
+ return initialOperatorState;
18244
+ }
18245
+ trigger() {
18246
+ return false;
18247
+ }
18248
+ run() {
18249
+ return false;
18250
+ }
18251
+ apply() {
18252
+ return false;
18253
+ }
18254
+ pipeline() {
18255
+ return false;
18256
+ }
18257
+ bringUp() {
18258
+ return false;
18259
+ }
18260
+ track() {
18261
+ return false;
18262
+ }
18263
+ };
18264
+ var REFUSED_READS = [
18265
+ [/^\/api\/ops\/[^/]+\/status$/, "the op tools are off"],
18266
+ [/^\/api\/operator(\/|$)/, "the op tools are off"],
18267
+ [/^\/api\/overlay$/, "it reads the live estate with behold's own credentials"],
18268
+ [/^\/api\/reconcile$/, "it plans against the live estate with behold's own credentials"],
18269
+ [/^\/api\/diff(\/|$)/, "it reads the live estate with behold's own credentials"],
18270
+ [/^\/api\/components\/compare$/, "it reads the live estate with behold's own credentials"],
18271
+ [/^\/api\/resources$/, "it runs chant against the project"],
18272
+ [/^\/api\/ci(\/|$)/, "it reads the pipeline and the forge run"],
18273
+ [/^\/api\/doctor$/, "it probes this machine's tools"],
18274
+ [/^\/api\/substrates$/, "it probes this machine's tools"],
18275
+ [/^\/api\/history$/, "it runs git against the checkout"],
18276
+ [/^\/api\/workspace\/(gates|why)$/, "it runs chant against the workspace"],
18277
+ [/^\/api\/choudoufu\/moves$/, "it runs choudoufu"],
18278
+ [/^\/choudoufu\/morph$/, "it runs choudoufu"],
18279
+ [/^\/api\/sql-yodeler\/lifecycle$/, "a public view shows a terragucci repo"]
18280
+ ];
18281
+ var LIVE_PARAMS = ["env", "target", "consistent", "plan", "live"];
18282
+ function publicReadRefusal(path, query) {
18283
+ for (const [re, why] of REFUSED_READS) if (re.test(path)) return why;
18284
+ if (path === "/api/graph" && LIVE_PARAMS.some((p) => query.has(p))) {
18285
+ return "a public view draws the source graph only; a live read uses behold's own credentials";
18286
+ }
18287
+ return void 0;
18288
+ }
18289
+ var PUBLIC_FLAG = "<script>window.__BEHOLD_PUBLIC__ = true;</script>";
18290
+ function publicPage(webRoot2) {
18291
+ const html = readFileSync15(join20(webRoot2, "index.html"), "utf8");
18292
+ return html.replace("</head>", ` ${PUBLIC_FLAG}
18293
+ </head>`);
18294
+ }
18295
+ function publicRoutes(app, webRoot2) {
18296
+ app.use("*", async (c, next) => {
18297
+ const why = publicReadRefusal(c.req.path, new URL(c.req.url).searchParams);
18298
+ if (why) return c.json({ error: `behold is serving a public, read-only view: ${c.req.path} is refused, because ${why}`, code: "public" }, 403);
18299
+ await next();
18300
+ });
18301
+ const page = () => new Response(publicPage(webRoot2), { headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-cache" } });
18302
+ app.get("/", () => page());
18303
+ app.get("/index.html", () => page());
18304
+ }
18305
+
18147
18306
  // src/substrates.ts
18148
18307
  import { spawn as spawn5 } from "node:child_process";
18149
- import { existsSync as existsSync14, readFileSync as readFileSync15 } from "node:fs";
18150
- import { join as join20 } from "node:path";
18308
+ import { existsSync as existsSync14, readFileSync as readFileSync16 } from "node:fs";
18309
+ import { join as join21 } from "node:path";
18151
18310
  import { platform } from "node:os";
18152
18311
  var DOCKER_DOWN_DETAIL = "docker is down";
18153
18312
  function probe(cmd, args) {
@@ -18176,11 +18335,11 @@ async function dockerRunning(run3, nameFilter) {
18176
18335
  return out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
18177
18336
  }
18178
18337
  function scriptBringUp(projectDir, relPath, label) {
18179
- return existsSync14(join20(projectDir, relPath)) ? { label, cmd: "bash", args: [relPath] } : void 0;
18338
+ return existsSync14(join21(projectDir, relPath)) ? { label, cmd: "bash", args: [relPath] } : void 0;
18180
18339
  }
18181
18340
  function projectLexicons(projectDir) {
18182
18341
  try {
18183
- const src = readFileSync15(join20(projectDir, "chant.config.ts"), "utf-8");
18342
+ const src = readFileSync16(join21(projectDir, "chant.config.ts"), "utf-8");
18184
18343
  const m = src.match(/lexicons\s*:\s*\[([^\]]*)\]/);
18185
18344
  if (!m) return [];
18186
18345
  return [...m[1].matchAll(/["']([^"']+)["']/g)].map((x) => x[1]);
@@ -18232,7 +18391,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
18232
18391
  ["forgejo", "Forgejo", ".forgejo", "test/forgejo-runtime-e2e.sh"]
18233
18392
  ];
18234
18393
  for (const [name, label, marker, script] of forges) {
18235
- if (!existsSync14(join20(projectDir, marker))) continue;
18394
+ if (!existsSync14(join21(projectDir, marker))) continue;
18236
18395
  const c = docker ? await dockerRunning(run3, name) : [];
18237
18396
  const d = dep(c.length > 0, "container up", "on-demand (pipeline run)");
18238
18397
  subs.push({
@@ -18266,7 +18425,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
18266
18425
  detail: endpoint ? `targeting ${endpoint}` : "real Fly (FLY_FLAPS_BASE_URL unset)"
18267
18426
  });
18268
18427
  }
18269
- if (existsSync14(join20(projectDir, ".github", "workflows"))) {
18428
+ if (existsSync14(join21(projectDir, ".github", "workflows"))) {
18270
18429
  const gh = await run3("gh", ["auth", "status"]);
18271
18430
  const ready = gh.code === 0;
18272
18431
  subs.push({
@@ -18279,7 +18438,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
18279
18438
  if (lexicons.includes("temporal")) {
18280
18439
  let hasProfiles = false;
18281
18440
  try {
18282
- hasProfiles = /temporal\s*:\s*\{[\s\S]{0,400}?profiles\s*:/.test(readFileSync15(join20(projectDir, "chant.config.ts"), "utf-8"));
18441
+ hasProfiles = /temporal\s*:\s*\{[\s\S]{0,400}?profiles\s*:/.test(readFileSync16(join21(projectDir, "chant.config.ts"), "utf-8"));
18283
18442
  } catch {
18284
18443
  }
18285
18444
  subs.push({
@@ -18422,8 +18581,8 @@ function statusVocabulary(kinds) {
18422
18581
  }
18423
18582
 
18424
18583
  // src/behaviour.ts
18425
- import { readFileSync as readFileSync16 } from "node:fs";
18426
- import { join as join21 } from "node:path";
18584
+ import { readFileSync as readFileSync17 } from "node:fs";
18585
+ import { join as join22 } from "node:path";
18427
18586
  var rec5 = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
18428
18587
  var str6 = (v) => typeof v === "string" && v.trim().length > 0 ? v : void 0;
18429
18588
  var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
@@ -18530,7 +18689,7 @@ var FIRST_TOTAL_SHOWN = "two members stated an estate total; the first is shown"
18530
18689
  var behaviourFileName = (env) => `behaviour.${env}.json`;
18531
18690
  var realRead = (path) => {
18532
18691
  try {
18533
- return readFileSync16(path, "utf8");
18692
+ return readFileSync17(path, "utf8");
18534
18693
  } catch {
18535
18694
  return void 0;
18536
18695
  }
@@ -18588,7 +18747,7 @@ function attachBehaviour(ir, members, env, readFile2 = realRead) {
18588
18747
  }
18589
18748
  const file = behaviourFileName(env);
18590
18749
  looked.push(`${label}/${file}`);
18591
- const raw = readFile2(join21(m.dir, file));
18750
+ const raw = readFile2(join22(m.dir, file));
18592
18751
  if (raw === void 0) {
18593
18752
  sources.set(label, "none");
18594
18753
  continue;
@@ -19024,7 +19183,7 @@ init_choudoufu_live();
19024
19183
  init_choudoufu_member();
19025
19184
  import { mkdtempSync, rmSync as rmSync3 } from "node:fs";
19026
19185
  import { tmpdir as tmpdir2 } from "node:os";
19027
- import { join as join22 } from "node:path";
19186
+ import { join as join23 } from "node:path";
19028
19187
  var isRecord10 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
19029
19188
  var refuse7 = (error, remedy) => ({ ok: false, refusal: { error, code: "choudoufu-live-check", remedy } });
19030
19189
  var CREDS2 = "Give behold the ambient read-only AWS credentials the estate's `plan` needs (AWS_PROFILE, or AWS_ENDPOINT_URL for an emulator), then re-check live with plan.";
@@ -19098,8 +19257,8 @@ var planArgv = (out) => ({
19098
19257
  show: ["show", "-json", out]
19099
19258
  });
19100
19259
  async function readChoudoufuPlan(dir, run3 = captureChoudoufu) {
19101
- const scratch = mkdtempSync(join22(tmpdir2(), "behold-choudoufu-plan-"));
19102
- const file = join22(scratch, "plan.tfplan");
19260
+ const scratch = mkdtempSync(join23(tmpdir2(), "behold-choudoufu-plan-"));
19261
+ const file = join23(scratch, "plan.tfplan");
19103
19262
  const argv = planArgv(file);
19104
19263
  try {
19105
19264
  const planRun = await run3(argv.plan, dir);
@@ -19131,8 +19290,8 @@ init_choudoufu_refs();
19131
19290
  // src/choudoufu-moves.ts
19132
19291
  init_choudoufu_member();
19133
19292
  init_choudoufu_live();
19134
- import { existsSync as existsSync15, readFileSync as readFileSync17 } from "node:fs";
19135
- import { join as join23, relative as relative5 } from "node:path";
19293
+ import { existsSync as existsSync15, readFileSync as readFileSync18 } from "node:fs";
19294
+ import { join as join24, relative as relative5 } from "node:path";
19136
19295
  var isRecord11 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
19137
19296
  var refuse8 = (error, remedy) => ({ ok: false, refusal: { error, code: "choudoufu-live-check", remedy } });
19138
19297
  var HOW_TO_GET_ONE2 = "A plan is the live-mv workbench's carve.json: `{from, estates[], moves[{address, from, to}]}` \u2014 the workbench's plan phase writes one, or write it by hand.";
@@ -19152,7 +19311,7 @@ function parseCarvePlan(value) {
19152
19311
  }
19153
19312
  };
19154
19313
  }
19155
- function readCarvePlan(path, readFile2 = (p) => readFileSync17(p, "utf8")) {
19314
+ function readCarvePlan(path, readFile2 = (p) => readFileSync18(p, "utf8")) {
19156
19315
  let text;
19157
19316
  try {
19158
19317
  text = readFile2(path);
@@ -19168,7 +19327,7 @@ function readCarvePlan(path, readFile2 = (p) => readFileSync17(p, "utf8")) {
19168
19327
  function discoverCarvePlans(memberDirs, base) {
19169
19328
  const out = [];
19170
19329
  for (const dir of memberDirs) {
19171
- const p = join23(dir, "carve.json");
19330
+ const p = join24(dir, "carve.json");
19172
19331
  if (existsSync15(p)) out.push(relative5(base, p));
19173
19332
  }
19174
19333
  return out;
@@ -19399,24 +19558,24 @@ function getRuntime() {
19399
19558
 
19400
19559
  // node_modules/@intentius/chant/src/lifecycle/member-ledger.ts
19401
19560
  import { existsSync as existsSync18 } from "node:fs";
19402
- import { dirname as dirname8, join as join27, relative as relative6, resolve as resolve15, sep as sep6 } from "node:path";
19561
+ import { dirname as dirname8, join as join28, relative as relative6, resolve as resolve15, sep as sep6 } from "node:path";
19403
19562
 
19404
19563
  // node_modules/@intentius/chant/src/project-root.ts
19405
19564
  import { existsSync as existsSync16 } from "fs";
19406
- import { dirname as dirname6, join as join24, resolve as resolve13 } from "path";
19565
+ import { dirname as dirname6, join as join25, resolve as resolve13 } from "path";
19407
19566
  var WORKSPACE_DECLARATION_FILES = ["chant.workspace.json", "chant.workspace.jsonc"];
19408
19567
  function findWorkspaceRoot(startDir) {
19409
19568
  const start = resolve13(startDir);
19410
19569
  let top;
19411
19570
  for (let dir = start; ; dir = dirname6(dir)) {
19412
- if (existsSync16(join24(dir, ".git"))) {
19571
+ if (existsSync16(join25(dir, ".git"))) {
19413
19572
  top = dir;
19414
19573
  break;
19415
19574
  }
19416
19575
  if (dirname6(dir) === dir) break;
19417
19576
  }
19418
19577
  for (let dir = start; ; dir = dirname6(dir)) {
19419
- const [json2, jsonc] = WORKSPACE_DECLARATION_FILES.map((name) => join24(dir, name));
19578
+ const [json2, jsonc] = WORKSPACE_DECLARATION_FILES.map((name) => join25(dir, name));
19420
19579
  const hasJson = existsSync16(json2);
19421
19580
  const hasJsonc = existsSync16(jsonc);
19422
19581
  if (hasJson && hasJsonc) return { dir, file: json2, conflicting: jsonc };
@@ -19478,7 +19637,7 @@ async function ledgerIn(root, dir) {
19478
19637
  }
19479
19638
  let outer = FLAT;
19480
19639
  const parent = dirname8(root);
19481
- if (parent !== root && !existsSync18(join27(root, ".git"))) {
19640
+ if (parent !== root && !existsSync18(join28(root, ".git"))) {
19482
19641
  const outerFound = findWorkspaceRoot(parent);
19483
19642
  if (outerFound) outer = await ledgerIn(outerFound.dir, root);
19484
19643
  }
@@ -19486,6 +19645,17 @@ async function ledgerIn(root, dir) {
19486
19645
  return { members, prefix: memberLedgerPrefix(members) };
19487
19646
  }
19488
19647
 
19648
+ // node_modules/@intentius/chant/src/lifecycle/no-record.ts
19649
+ var NO_RECORD_ENV = "CHANT_NO_RECORD";
19650
+ function ledgerRecordingOff(env = process.env) {
19651
+ const value = env[NO_RECORD_ENV]?.trim().toLowerCase();
19652
+ return value !== void 0 && value !== "" && value !== "0" && value !== "false";
19653
+ }
19654
+ var overlays = /* @__PURE__ */ new Map();
19655
+ function overlayTip(key) {
19656
+ return overlays.get(key)?.tip;
19657
+ }
19658
+
19489
19659
  // node_modules/@intentius/chant/src/lifecycle/git.ts
19490
19660
  var STATE_BRANCH = "chant/lifecycle";
19491
19661
  async function ledgerDir(dir, opts) {
@@ -19502,7 +19672,7 @@ async function readBlobFromPath(environment, filename, opts) {
19502
19672
  const rt = getRuntime();
19503
19673
  const dir = await ledgerDir(environment, opts);
19504
19674
  const result = await rt.spawn(
19505
- ["git", "show", `${STATE_BRANCH}:${dir}/${filename}`],
19675
+ ["git", "show", `${await ledgerRevision(opts?.cwd)}:${dir}/${filename}`],
19506
19676
  { cwd: opts?.cwd }
19507
19677
  );
19508
19678
  if (result.exitCode !== 0) return null;
@@ -19527,7 +19697,7 @@ async function listLedgerTrees(opts) {
19527
19697
  const rt = getRuntime();
19528
19698
  const base = await ledgerDir("", opts);
19529
19699
  const rootResult = await rt.spawn(
19530
- ["git", "ls-tree", "--full-tree", base === "" ? STATE_BRANCH : `${STATE_BRANCH}:${base}`],
19700
+ ["git", "ls-tree", "--full-tree", base === "" ? tip : `${tip}:${base}`],
19531
19701
  { cwd: opts?.cwd }
19532
19702
  );
19533
19703
  if (rootResult.exitCode !== 0) return [];
@@ -19538,7 +19708,12 @@ async function listLedgerTrees(opts) {
19538
19708
  }
19539
19709
  return trees;
19540
19710
  }
19711
+ var C_LOCALE_ENV = { LC_ALL: "C", LANGUAGE: "" };
19541
19712
  async function getStateBranchTip(cwd) {
19713
+ if (ledgerRecordingOff()) {
19714
+ const tip = overlayTip(await overlayKey(cwd));
19715
+ if (tip !== void 0) return tip;
19716
+ }
19542
19717
  const rt = getRuntime();
19543
19718
  const result = await rt.spawn(
19544
19719
  ["git", "rev-parse", "--verify", `refs/heads/${STATE_BRANCH}`],
@@ -19547,6 +19722,20 @@ async function getStateBranchTip(cwd) {
19547
19722
  if (result.exitCode !== 0) return null;
19548
19723
  return result.stdout.trim();
19549
19724
  }
19725
+ async function ledgerRevision(cwd) {
19726
+ if (!ledgerRecordingOff()) return STATE_BRANCH;
19727
+ return overlayTip(await overlayKey(cwd)) ?? STATE_BRANCH;
19728
+ }
19729
+ var overlayKeys = /* @__PURE__ */ new Map();
19730
+ async function overlayKey(cwd) {
19731
+ const from = cwd ?? process.cwd();
19732
+ const cached = overlayKeys.get(from);
19733
+ if (cached !== void 0) return cached;
19734
+ const result = await getRuntime().spawn(["git", "rev-parse", "--path-format=absolute", "--git-common-dir"], { cwd: from, env: C_LOCALE_ENV });
19735
+ const key = result.exitCode === 0 && result.stdout.trim() !== "" ? result.stdout.trim() : from;
19736
+ overlayKeys.set(from, key);
19737
+ return key;
19738
+ }
19550
19739
 
19551
19740
  // node_modules/@intentius/chant/src/content-digest.ts
19552
19741
  import { createHash as createHash6 } from "node:crypto";
@@ -20003,7 +20192,7 @@ function decideOverride(input) {
20003
20192
 
20004
20193
  // src/terragucci-repo.ts
20005
20194
  import { existsSync as existsSync19 } from "node:fs";
20006
- import { dirname as dirname9, join as join28, resolve as resolve16 } from "node:path";
20195
+ import { dirname as dirname9, join as join29, resolve as resolve16 } from "node:path";
20007
20196
  var TERRAGUCCI_CONFIG_NAMES = ["terragucci.yml", "terragucci.yaml", "terragucci.json", "terragucci.ts"];
20008
20197
  function terragucciConfig(dir) {
20009
20198
  return walkUp(dir, TERRAGUCCI_CONFIG_NAMES);
@@ -20012,10 +20201,10 @@ function walkUp(dir, names) {
20012
20201
  let at2 = resolve16(dir);
20013
20202
  for (; ; ) {
20014
20203
  for (const name of names) {
20015
- const file = join28(at2, name);
20204
+ const file = join29(at2, name);
20016
20205
  if (existsSync19(file)) return file;
20017
20206
  }
20018
- if (existsSync19(join28(at2, ".git"))) return void 0;
20207
+ if (existsSync19(join29(at2, ".git"))) return void 0;
20019
20208
  const up = dirname9(at2);
20020
20209
  if (up === at2) return void 0;
20021
20210
  at2 = up;
@@ -20420,6 +20609,9 @@ function guardRequest(req, cfg) {
20420
20609
  return { ok: false, status: 421, error: `behold doesn't answer to host ${name}. Start it with --allow-host ${name} if a proxy of yours sends that name.` };
20421
20610
  }
20422
20611
  if (SAFE.has(req.method.toUpperCase())) return { ok: true };
20612
+ if (cfg.public) {
20613
+ return { ok: false, status: 405, error: `behold is serving a public, read-only view: a ${req.method.toUpperCase()} is refused (GET, HEAD and OPTIONS only)` };
20614
+ }
20423
20615
  if ((req.secFetchSite || "").toLowerCase() === "cross-site") {
20424
20616
  return { ok: false, status: 403, error: "a cross-site page can't write to behold" };
20425
20617
  }
@@ -20815,7 +21007,7 @@ init_foreign();
20815
21007
 
20816
21008
  // src/events.ts
20817
21009
  import { watch, existsSync as existsSync20 } from "node:fs";
20818
- import { join as join29 } from "node:path";
21010
+ import { join as join30 } from "node:path";
20819
21011
  var Broadcaster = class {
20820
21012
  listeners = /* @__PURE__ */ new Set();
20821
21013
  subscribe(fn) {
@@ -20833,7 +21025,7 @@ var Broadcaster = class {
20833
21025
  };
20834
21026
  var IGNORE = /(^|[\\/])(node_modules|dist|\.git)([\\/]|$)/;
20835
21027
  function watchSource(projectDir, onChange, debounceMs = 200) {
20836
- const dir = existsSync20(join29(projectDir, "src")) ? join29(projectDir, "src") : projectDir;
21028
+ const dir = existsSync20(join30(projectDir, "src")) ? join30(projectDir, "src") : projectDir;
20837
21029
  let timer;
20838
21030
  const watcher = watch(dir, { recursive: true }, (_event, file) => {
20839
21031
  const name = typeof file === "string" ? file : "";
@@ -21136,13 +21328,13 @@ async function emulatorDown(projectDir) {
21136
21328
 
21137
21329
  // src/terragucci-view.ts
21138
21330
  import { execFileSync as execFileSync3 } from "node:child_process";
21139
- import { dirname as dirname13, join as join37, relative as relative8, sep as sep7 } from "node:path";
21331
+ import { dirname as dirname13, join as join39, relative as relative8, sep as sep7 } from "node:path";
21140
21332
  init_terraform_member();
21141
21333
 
21142
21334
  // src/terragucci-reports.ts
21143
- import { readFileSync as readFileSync22, statSync as statSync10 } from "node:fs";
21335
+ import { readFileSync as readFileSync23, statSync as statSync10 } from "node:fs";
21144
21336
  import { createRequire as createRequire4 } from "node:module";
21145
- import { dirname as dirname10, join as join32, resolve as resolve20 } from "node:path";
21337
+ import { dirname as dirname10, join as join33, resolve as resolve20 } from "node:path";
21146
21338
  import { fileURLToPath as fileURLToPath5 } from "node:url";
21147
21339
  import { Ajv2020 } from "ajv/dist/2020.js";
21148
21340
 
@@ -21151,12 +21343,12 @@ import { execFile as execFile4 } from "node:child_process";
21151
21343
  import { createHash as createHash8 } from "node:crypto";
21152
21344
  import { existsSync as existsSync21, mkdirSync as mkdirSync6, readdirSync as readdirSync11 } from "node:fs";
21153
21345
  import { tmpdir as tmpdir3 } from "node:os";
21154
- import { basename as basename3, isAbsolute as isAbsolute4, join as join31, resolve as resolve19 } from "node:path";
21346
+ import { basename as basename3, isAbsolute as isAbsolute4, join as join32, resolve as resolve19 } from "node:path";
21155
21347
 
21156
21348
  // node_modules/@intentius/chant/src/gate-decision.ts
21157
21349
  import { spawnSync as spawnSync2 } from "node:child_process";
21158
- import { readFileSync as readFileSync21 } from "node:fs";
21159
- import { join as join30, posix as posix3 } from "node:path";
21350
+ import { readFileSync as readFileSync22 } from "node:fs";
21351
+ import { join as join31, posix as posix3 } from "node:path";
21160
21352
 
21161
21353
  // node_modules/@intentius/chant/src/lifecycle/gate-applied.ts
21162
21354
  function appliedGatePath(op) {
@@ -21265,7 +21457,7 @@ function pinnedClassesAt(repo, base, pins, packages) {
21265
21457
  }
21266
21458
  let installed2;
21267
21459
  try {
21268
- installed2 = JSON.parse(readFileSync21(join30(dir, "package.json"), "utf-8")).version;
21460
+ installed2 = JSON.parse(readFileSync22(join31(dir, "package.json"), "utf-8")).version;
21269
21461
  } catch {
21270
21462
  installed2 = void 0;
21271
21463
  }
@@ -21860,7 +22052,7 @@ var realGit = (args, o = {}) => new Promise((res) => {
21860
22052
  });
21861
22053
  var redact = (s) => s.replace(/(\b[a-z][a-z0-9+.-]*:\/\/)[^/@\s]+@/gi, "$1");
21862
22054
  function cacheDir(origin, root = tmpdir3()) {
21863
- return join31(root, `${CACHE_PREFIX}${createHash8("sha256").update(origin).digest("hex").slice(0, 16)}`);
22055
+ return join32(root, `${CACHE_PREFIX}${createHash8("sha256").update(origin).digest("hex").slice(0, 16)}`);
21864
22056
  }
21865
22057
  var inflight = /* @__PURE__ */ new Map();
21866
22058
  function serially(key, fn) {
@@ -21879,7 +22071,7 @@ function fetchable(url, checkout) {
21879
22071
  async function fetchIntoCache(git3, origin, cache6) {
21880
22072
  assertScratch(basename3(cache6));
21881
22073
  return serially(cache6, async () => {
21882
- if (!existsSync21(join31(cache6, "HEAD"))) {
22074
+ if (!existsSync21(join32(cache6, "HEAD"))) {
21883
22075
  if (existsSync21(cache6) && readdirSync11(cache6).length > 0) return { ok: false, error: `${cache6} exists and is not behold's lifecycle cache; behold leaves it alone` };
21884
22076
  mkdirSync6(cache6, { recursive: true });
21885
22077
  const init = await git3(["init", "--bare", "-q", cache6]);
@@ -22067,7 +22259,7 @@ function structural(name, doc) {
22067
22259
  var compiledSchemas = /* @__PURE__ */ new Map();
22068
22260
  function schemaStamp(dist) {
22069
22261
  try {
22070
- const files = [join32(dist, "..", "package.json"), ...Object.values(SCHEMA_FILES).map((f) => join32(dist, f))];
22262
+ const files = [join33(dist, "..", "package.json"), ...Object.values(SCHEMA_FILES).map((f) => join33(dist, f))];
22071
22263
  return [dist, ...files.map((f) => {
22072
22264
  const st = statSync10(f);
22073
22265
  return `${st.mtimeMs}:${st.size}`;
@@ -22077,10 +22269,10 @@ function schemaStamp(dist) {
22077
22269
  }
22078
22270
  }
22079
22271
  function compileSchemas(dist) {
22080
- const version = JSON.parse(readFileSync22(join32(dist, "..", "package.json"), "utf8")).version ?? "";
22272
+ const version = JSON.parse(readFileSync23(join33(dist, "..", "package.json"), "utf8")).version ?? "";
22081
22273
  const ajv = new Ajv2020({ strict: false, allErrors: false });
22082
22274
  const compiled4 = Object.fromEntries(
22083
- Object.keys(SCHEMA_FILES).map((n) => [n, ajv.compile(JSON.parse(readFileSync22(join32(dist, SCHEMA_FILES[n]), "utf8")))])
22275
+ Object.keys(SCHEMA_FILES).map((n) => [n, ajv.compile(JSON.parse(readFileSync23(join33(dist, SCHEMA_FILES[n]), "utf8")))])
22084
22276
  );
22085
22277
  return {
22086
22278
  validation: { by: "schema", terragucci: version },
@@ -22097,7 +22289,7 @@ function terragucciValidator(dirs = []) {
22097
22289
  for (const from of [...dirs, dirname10(fileURLToPath5(import.meta.url))]) {
22098
22290
  let entry;
22099
22291
  try {
22100
- entry = createRequire4(join32(resolve20(from), "noop.js")).resolve("@intentius/terragucci/report.schema.json");
22292
+ entry = createRequire4(join33(resolve20(from), "noop.js")).resolve("@intentius/terragucci/report.schema.json");
22101
22293
  } catch {
22102
22294
  continue;
22103
22295
  }
@@ -22221,6 +22413,10 @@ async function readTerragucci(source2, opts = {}) {
22221
22413
  }
22222
22414
  }
22223
22415
  }
22416
+ let pullRequest;
22417
+ const prRow = rows.find((r) => r.stage === "tf-plan" && r.pull_request);
22418
+ const prDoc = prRow ? await report(prRow) : void 0;
22419
+ if (prRow && prDoc) pullRequest = pullRequestPlan(prDoc, under(prRow.path, "report.html"));
22224
22420
  const estateDoc = await readDoc(source2, "estate.json", "estate", v);
22225
22421
  const estateProject = estateDoc?.projects.find((p) => p.project === project);
22226
22422
  const waiting = [];
@@ -22266,6 +22462,7 @@ async function readTerragucci(source2, opts = {}) {
22266
22462
  reports: cache6.size,
22267
22463
  roots,
22268
22464
  waiting,
22465
+ ...pullRequest ? { pull_request: pullRequest } : {},
22269
22466
  ...estateDoc && estateProject ? {
22270
22467
  estate: {
22271
22468
  generated: estateDoc.generated,
@@ -22278,6 +22475,57 @@ async function readTerragucci(source2, opts = {}) {
22278
22475
  } : {}
22279
22476
  };
22280
22477
  }
22478
+ function pullRequestPlan(doc, report) {
22479
+ return {
22480
+ run: {
22481
+ commit: doc.run.commit,
22482
+ finished: doc.run.finished,
22483
+ pull_request: doc.run.pull_request ?? "",
22484
+ ...doc.run.pull_request_url ? { pull_request_url: doc.run.pull_request_url } : {},
22485
+ ...doc.run.job_url ? { job_url: doc.run.job_url } : {},
22486
+ report
22487
+ },
22488
+ reads: doc.roots.flatMap(
22489
+ (r) => (r.reads ?? []).map((x) => ({
22490
+ root: r.path,
22491
+ upstream: x.upstream,
22492
+ data: x.data,
22493
+ outputs: x.outputs,
22494
+ ...x.unknown?.length ? { unknown: [...x.unknown] } : {},
22495
+ ...x.why ? { why: x.why } : {},
22496
+ ...x.composed ? { composed: true } : {}
22497
+ }))
22498
+ ),
22499
+ roots: doc.roots.map((r) => r.path),
22500
+ waves: doc.waves.map((w) => ({
22501
+ number: w.number,
22502
+ roots: w.roots,
22503
+ ...w.state ? { state: w.state } : {},
22504
+ // A wave that plans again has no digest a review binds until it does.
22505
+ set_digest: w.replans_after?.length ? null : w.set_digest,
22506
+ ...w.review_digest !== void 0 ? { review_digest: w.replans_after?.length ? null : w.review_digest } : {},
22507
+ ...w.waits !== void 0 ? { waits: w.waits } : {},
22508
+ ...w.reads?.length ? { reads: w.reads } : {},
22509
+ ...w.replans_after?.length ? { replans_after: w.replans_after } : {},
22510
+ ...w.cost ? { cost: w.cost } : {},
22511
+ // #568 step 3: each member's changes come from its own slice, under its own name.
22512
+ ...w.composed ? {
22513
+ composed: {
22514
+ ...w.composed,
22515
+ members: w.composed.members.map((m) => {
22516
+ const counts = doc.roots.find((r) => r.path === m.root)?.counts;
22517
+ return { ...m, ...counts ? { counts } : {} };
22518
+ })
22519
+ }
22520
+ } : {}
22521
+ })),
22522
+ ...doc.compose ? { compose: doc.compose } : {},
22523
+ ...doc.totals ? { totals: doc.totals } : {},
22524
+ ...doc.totals_roots !== void 0 ? { totals_roots: doc.totals_roots } : {},
22525
+ ...doc.cost ? { cost: doc.cost } : {},
22526
+ ...doc.blast ? { blast: doc.blast } : {}
22527
+ };
22528
+ }
22281
22529
 
22282
22530
  // src/terragucci-overlay.ts
22283
22531
  function blockPath(address) {
@@ -22382,11 +22630,11 @@ function joinTerragucci(read, nodes, roots) {
22382
22630
  // src/terragucci-source.ts
22383
22631
  import { spawn as spawn7 } from "node:child_process";
22384
22632
  import { readFile } from "node:fs/promises";
22385
- import { join as join33, resolve as resolve21 } from "node:path";
22633
+ import { join as join34, resolve as resolve21 } from "node:path";
22386
22634
 
22387
22635
  // src/s3-object.ts
22388
22636
  import { createHash as createHash9, createHmac } from "node:crypto";
22389
- import { readFileSync as readFileSync23 } from "node:fs";
22637
+ import { readFileSync as readFileSync24 } from "node:fs";
22390
22638
  var S3Error = class extends Error {
22391
22639
  /** The HTTP status S3 answered, when it answered. */
22392
22640
  status;
@@ -22433,7 +22681,7 @@ function s3FromEnv(bucket, env = process.env) {
22433
22681
  async function assumeRole(w, fetchFn) {
22434
22682
  let token;
22435
22683
  try {
22436
- token = readFileSync23(w.tokenFile, "utf8").trim();
22684
+ token = readFileSync24(w.tokenFile, "utf8").trim();
22437
22685
  } catch (e) {
22438
22686
  throw new S3Error(`cannot read the OIDC token in ${w.tokenFile}: ${e.message}`);
22439
22687
  }
@@ -22630,7 +22878,7 @@ function terragucciSource(spec2, opts = {}) {
22630
22878
  location: dir,
22631
22879
  async read(key) {
22632
22880
  try {
22633
- return await readFile(join33(dir, ...trim(key).split("/")), "utf8");
22881
+ return await readFile(join34(dir, ...trim(key).split("/")), "utf8");
22634
22882
  } catch (e) {
22635
22883
  if (e.code === "ENOENT") return void 0;
22636
22884
  throw e;
@@ -22640,9 +22888,9 @@ function terragucciSource(spec2, opts = {}) {
22640
22888
  }
22641
22889
 
22642
22890
  // src/terragucci-timeline.ts
22643
- import { readFileSync as readFileSync24, statSync as statSync11 } from "node:fs";
22891
+ import { readFileSync as readFileSync25, statSync as statSync11 } from "node:fs";
22644
22892
  import { createRequire as createRequire5 } from "node:module";
22645
- import { dirname as dirname11, join as join34, resolve as resolve22 } from "node:path";
22893
+ import { dirname as dirname11, join as join35, resolve as resolve22 } from "node:path";
22646
22894
  import { fileURLToPath as fileURLToPath6 } from "node:url";
22647
22895
  import { Ajv2020 as Ajv20202 } from "ajv/dist/2020.js";
22648
22896
  var AUDIT_SCHEMA = "terragucci.audit/v1";
@@ -22683,12 +22931,12 @@ function auditValidator(dirs = []) {
22683
22931
  for (const from of [...dirs, dirname11(fileURLToPath6(import.meta.url))]) {
22684
22932
  let dist;
22685
22933
  try {
22686
- dist = dirname11(createRequire5(join34(resolve22(from), "noop.js")).resolve("@intentius/terragucci/report.schema.json"));
22934
+ dist = dirname11(createRequire5(join35(resolve22(from), "noop.js")).resolve("@intentius/terragucci/report.schema.json"));
22687
22935
  } catch {
22688
22936
  continue;
22689
22937
  }
22690
- const pkg = join34(dist, "..", "package.json");
22691
- const file = join34(dist, AUDIT_SCHEMA_FILE);
22938
+ const pkg = join35(dist, "..", "package.json");
22939
+ const file = join35(dist, AUDIT_SCHEMA_FILE);
22692
22940
  let stamp;
22693
22941
  try {
22694
22942
  stamp = [dist, ...[pkg, file].map((f) => {
@@ -22702,8 +22950,8 @@ function auditValidator(dirs = []) {
22702
22950
  if (hit) return hit;
22703
22951
  let v;
22704
22952
  try {
22705
- const version = JSON.parse(readFileSync24(pkg, "utf8")).version ?? "";
22706
- v = compileAuditSchema(JSON.parse(readFileSync24(file, "utf8")), version);
22953
+ const version = JSON.parse(readFileSync25(pkg, "utf8")).version ?? "";
22954
+ v = compileAuditSchema(JSON.parse(readFileSync25(file, "utf8")), version);
22707
22955
  } catch {
22708
22956
  continue;
22709
22957
  }
@@ -22899,7 +23147,7 @@ function terragucciTimelineRoute(app, source2, opts = {}) {
22899
23147
  // src/terragucci-poll.ts
22900
23148
  import { createHash as createHash10 } from "node:crypto";
22901
23149
  import { stat } from "node:fs/promises";
22902
- import { join as join35, resolve as resolve23 } from "node:path";
23150
+ import { join as join36, resolve as resolve23 } from "node:path";
22903
23151
  import { streamSSE } from "hono/streaming";
22904
23152
  var TERRAGUCCI_POLL_SECS = 30;
22905
23153
  var INDEX_KEY = "index.json";
@@ -22950,7 +23198,7 @@ function indexProbe(spec2, opts = {}) {
22950
23198
  return verdict2(prev, etag);
22951
23199
  };
22952
23200
  }
22953
- const file = join35(resolve23(opts.cwd ?? process.cwd(), spec2), name);
23201
+ const file = join36(resolve23(opts.cwd ?? process.cwd(), spec2), name);
22954
23202
  return async (prev) => {
22955
23203
  try {
22956
23204
  const s = await stat(file);
@@ -23105,9 +23353,9 @@ function terragucciLaneRoutes(app, o, base, forget) {
23105
23353
  }
23106
23354
 
23107
23355
  // src/terragucci-progress.ts
23108
- import { readFileSync as readFileSync25, statSync as statSync12 } from "node:fs";
23356
+ import { readFileSync as readFileSync26, statSync as statSync12 } from "node:fs";
23109
23357
  import { createRequire as createRequire6 } from "node:module";
23110
- import { dirname as dirname12, join as join36, resolve as resolve24 } from "node:path";
23358
+ import { dirname as dirname12, join as join37, resolve as resolve24 } from "node:path";
23111
23359
  import { fileURLToPath as fileURLToPath7 } from "node:url";
23112
23360
  import { Ajv2020 as Ajv20203 } from "ajv/dist/2020.js";
23113
23361
  var RUN_SCHEMA = "terragucci.run/v1";
@@ -23152,12 +23400,12 @@ function runValidator(dirs = []) {
23152
23400
  for (const from of [...dirs, dirname12(fileURLToPath7(import.meta.url))]) {
23153
23401
  let dist;
23154
23402
  try {
23155
- dist = dirname12(createRequire6(join36(resolve24(from), "noop.js")).resolve("@intentius/terragucci/report.schema.json"));
23403
+ dist = dirname12(createRequire6(join37(resolve24(from), "noop.js")).resolve("@intentius/terragucci/report.schema.json"));
23156
23404
  } catch {
23157
23405
  continue;
23158
23406
  }
23159
- const pkg = join36(dist, "..", "package.json");
23160
- const file = join36(dist, RUN_SCHEMA_FILE);
23407
+ const pkg = join37(dist, "..", "package.json");
23408
+ const file = join37(dist, RUN_SCHEMA_FILE);
23161
23409
  let stamp;
23162
23410
  try {
23163
23411
  stamp = [dist, ...[pkg, file].map((f) => {
@@ -23171,8 +23419,8 @@ function runValidator(dirs = []) {
23171
23419
  if (hit) return hit;
23172
23420
  let v;
23173
23421
  try {
23174
- const version = JSON.parse(readFileSync25(pkg, "utf8")).version ?? "";
23175
- v = compileRunSchema(JSON.parse(readFileSync25(file, "utf8")), version);
23422
+ const version = JSON.parse(readFileSync26(pkg, "utf8")).version ?? "";
23423
+ v = compileRunSchema(JSON.parse(readFileSync26(file, "utf8")), version);
23176
23424
  } catch {
23177
23425
  continue;
23178
23426
  }
@@ -23198,7 +23446,10 @@ function runKey(row) {
23198
23446
  var zero = () => ({ done: 0, "in-flight": 0, waiting: 0, "not-applied": 0 });
23199
23447
  var MARK_ORDER = ["not-applied", "in-flight", "waiting", "done"];
23200
23448
  function unsettled(run3) {
23201
- return run3.waves.some((w) => w.state === "applying" || (w.progress?.resources ?? []).some((r) => r.status === "in-flight" || r.status === "waiting"));
23449
+ if (run3.waves.some((w) => w.state === "applying" || (w.progress?.resources ?? []).some((r) => r.status === "in-flight" || r.status === "waiting"))) return true;
23450
+ if (run3.waves.some((w) => w.state === "waiting")) return true;
23451
+ if (run3.waves.some((w) => w.state === "failed" || w.state === "refused")) return false;
23452
+ return run3.waves.some((w) => w.state === "applied") && run3.waves.some((w) => w.state === "not-started");
23202
23453
  }
23203
23454
  function joinProgress(run3, nodes, roots) {
23204
23455
  const byRoot = /* @__PURE__ */ new Map();
@@ -23241,12 +23492,36 @@ function joinProgress(run3, nodes, roots) {
23241
23492
  }
23242
23493
  waves.push({ number: w.number, state: w.state, gate: w.gate, read: w.progress.read, counts, resources });
23243
23494
  }
23244
- return { waves, cards, unmatched, watching: unsettled(run3) };
23495
+ const states = run3.waves.map((w) => ({
23496
+ number: w.number,
23497
+ state: w.state,
23498
+ gate: w.gate,
23499
+ roots: [...w.roots],
23500
+ reads: [...w.reads ?? []],
23501
+ ...w.approval ? { approval: w.approval } : {},
23502
+ ...w.digest !== void 0 ? { digest: w.digest } : {},
23503
+ // The run view writes the approve line while the wave waits; behold's own form when it does not.
23504
+ ...w.state === "waiting" ? { command: w.command ?? terragucciApproveCommand(w.number, w.digest ?? null) } : {},
23505
+ ...w.report ? { report: `${w.report}/report.html` } : {},
23506
+ ...w.changed ? { changed: [...w.changed] } : {},
23507
+ ...w.totals ? { totals: { ...w.totals } } : {},
23508
+ ...w.totals_roots !== void 0 ? { totals_roots: w.totals_roots } : {},
23509
+ ...w.composed ? {
23510
+ composed: {
23511
+ roots: [...w.composed.roots],
23512
+ ...w.composed.digest !== void 0 ? { digest: w.composed.digest } : {},
23513
+ ...w.composed.unknown ? { unknown: [...w.composed.unknown] } : {},
23514
+ ...w.composed.members ? { members: w.composed.members.map((m) => ({ ...m })) } : {}
23515
+ }
23516
+ } : {}
23517
+ }));
23518
+ const reads2 = (run3.roots ?? []).map((r) => ({ root: r.root, wave: r.wave, reads: [...r.reads ?? []], ...r.composed ? { composed: { ...r.composed } } : {} }));
23519
+ return { states, reads: reads2, waves, cards, unmatched, watching: unsettled(run3) };
23245
23520
  }
23246
23521
  var ProgressGraphError = class extends Error {
23247
23522
  };
23248
23523
  var FILES = "/api/terragucci/file?key=";
23249
- var progressKey = (a) => JSON.stringify([a.run, a.waves, a.unmatched]);
23524
+ var progressKey = (a) => JSON.stringify([a.run, a.waves, a.unmatched, a.states, a.reads]);
23250
23525
  var TerragucciProgress = class {
23251
23526
  constructor(o) {
23252
23527
  this.o = o;
@@ -23301,6 +23576,8 @@ var TerragucciProgress = class {
23301
23576
  ...this.base(project, v?.run.validation),
23302
23577
  run: null,
23303
23578
  absent: `index.json lists no tf-apply run of ${project} yet, so there is no run view to read.`,
23579
+ states: [],
23580
+ reads: [],
23304
23581
  waves: [],
23305
23582
  cards: {},
23306
23583
  unmatched: [],
@@ -23337,6 +23614,8 @@ var TerragucciProgress = class {
23337
23614
  run: { commit, key, present: false },
23338
23615
  ...unreadable ? { unreadable: true } : {},
23339
23616
  absent: unreadable ?? `${src.location} has no ${key}: terragucci writes the run view when reports.bucket is set, and a wave's progress per resource only for a choudoufu wave (terragucci 0.4.7 and newer). This says nothing about whether the apply moved.`,
23617
+ states: [],
23618
+ reads: [],
23340
23619
  waves: [],
23341
23620
  cards: {},
23342
23621
  unmatched: [],
@@ -23407,6 +23686,90 @@ function terragucciProgressRoute(app, progress) {
23407
23686
  });
23408
23687
  }
23409
23688
 
23689
+ // src/terragucci-demo.ts
23690
+ import { readFileSync as readFileSync27 } from "node:fs";
23691
+ import { join as join38 } from "node:path";
23692
+ var DEMO_KEY = "demo.json";
23693
+ var DEMO_READ_TTL_MS = 2e3;
23694
+ var DEMO_STEPS = ["pull-request", "planned", "merged", "applying", "waiting", "approved", "failed", "resumed", "applied", "reset", "stalled"];
23695
+ var UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;
23696
+ var isRecord17 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
23697
+ var str13 = (v) => typeof v === "string";
23698
+ var int = (v) => typeof v === "number" && Number.isInteger(v);
23699
+ function demoProblem(doc) {
23700
+ if (!isRecord17(doc)) return "it is not an object";
23701
+ if (!int(doc.cycle)) return "cycle is not an integer";
23702
+ if (!str13(doc.scenario) || !doc.scenario) return "scenario is not a string";
23703
+ if (!str13(doc.title)) return "title is not a string";
23704
+ if (!str13(doc.step) || !DEMO_STEPS.includes(doc.step)) return `step is not one of ${DEMO_STEPS.join(", ")}`;
23705
+ if (!str13(doc.label)) return "label is not a string";
23706
+ if (!str13(doc.started_at) || !UTC.test(doc.started_at)) return "started_at is not a UTC time (YYYY-MM-DDTHH:MM:SSZ)";
23707
+ if (doc.next !== null) {
23708
+ if (!isRecord17(doc.next)) return "next is neither an object nor null";
23709
+ if (!str13(doc.next.action)) return "next.action is not a string";
23710
+ if (doc.next.at !== null && !(str13(doc.next.at) && UTC.test(doc.next.at))) return "next.at is neither a UTC time nor null";
23711
+ }
23712
+ if (doc.pull !== null && !int(doc.pull)) return "pull is neither an integer nor null";
23713
+ if (doc.commit !== null && !str13(doc.commit)) return "commit is neither a string nor null";
23714
+ return void 0;
23715
+ }
23716
+ async function readDemo(source2, now = /* @__PURE__ */ new Date()) {
23717
+ const base = { source: source2.location, read: now.toISOString() };
23718
+ let body;
23719
+ try {
23720
+ body = await source2.read(DEMO_KEY);
23721
+ } catch (e) {
23722
+ if (e instanceof TerragucciDenied) return { ...base, demo: null, error: deniedWords(e, "the demo's narration"), code: "terragucci-demo" };
23723
+ return { ...base, demo: null, error: `behold could not read ${DEMO_KEY} from ${source2.location}: ${e instanceof Error ? e.message : String(e)}`, code: "terragucci-demo" };
23724
+ }
23725
+ if (body === void 0) {
23726
+ return { ...base, demo: null, absent: `${source2.location} has no ${DEMO_KEY}: the demo's loop driver writes it at every step (terragucci's stack/demo.sh), beside index.json.` };
23727
+ }
23728
+ let doc;
23729
+ try {
23730
+ doc = JSON.parse(body);
23731
+ } catch {
23732
+ return { ...base, demo: null, error: `${DEMO_KEY} in ${source2.location} is not JSON.`, code: "terragucci-demo" };
23733
+ }
23734
+ const problem = demoProblem(doc);
23735
+ if (problem) return { ...base, demo: null, error: `${DEMO_KEY} in ${source2.location} is not the demo's format: ${problem}.`, code: "terragucci-demo" };
23736
+ const d = doc;
23737
+ return {
23738
+ ...base,
23739
+ demo: {
23740
+ cycle: d.cycle,
23741
+ scenario: d.scenario,
23742
+ title: d.title,
23743
+ step: d.step,
23744
+ label: d.label,
23745
+ started_at: d.started_at,
23746
+ next: d.next ? { action: d.next.action, at: d.next.at } : null,
23747
+ pull: d.pull,
23748
+ commit: d.commit
23749
+ }
23750
+ };
23751
+ }
23752
+ function terragucciDemoRoute(app, source2, opts = {}) {
23753
+ let cached;
23754
+ app.get("/api/terragucci/demo", async (c) => {
23755
+ const at2 = Date.now();
23756
+ if (!cached || at2 - cached.at >= DEMO_READ_TTL_MS) cached = { at: at2, read: readDemo(source2, opts.now ? opts.now() : /* @__PURE__ */ new Date()) };
23757
+ const a = await cached.read;
23758
+ return c.json(a, "error" in a ? 502 : 200, { "cache-control": "no-store" });
23759
+ });
23760
+ }
23761
+ function kioskRoute(app, webRoot2, enabled) {
23762
+ app.get("/kiosk", (c) => {
23763
+ if (!enabled) {
23764
+ return c.html(
23765
+ `<!doctype html><meta charset=utf-8><title>behold kiosk</title><script type="module">import { initTheme } from "/theme.js"; initTheme();</script><body style="font:16px system-ui;background:var(--bg,#1e1e2e);color:var(--fg,#cdd6f4);padding:2rem"><h3>The kiosk follows a terragucci demo</h3><p style="color:var(--muted,#777a8c)">Start behold with <code>--terragucci &lt;reports source&gt;</code>; the kiosk reads <code>demo.json</code> there.</p></body>`,
23766
+ 404
23767
+ );
23768
+ }
23769
+ return new Response(readFileSync27(join38(webRoot2, "kiosk.html"), "utf8"), { headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-cache" } });
23770
+ });
23771
+ }
23772
+
23410
23773
  // src/terragucci-view.ts
23411
23774
  var TERRAGUCCI_READ_TTL_MS = 3e4;
23412
23775
  function repoBase(dirs) {
@@ -23445,7 +23808,7 @@ function checkoutRoots(dirs, base = repoBase(dirs)) {
23445
23808
  for (const m of estateMembers(dirs)) {
23446
23809
  if (m.kind !== "terraform") continue;
23447
23810
  for (const r of discoverTerraformRoots(m.dir).roots) {
23448
- const path = relative8(base, join37(m.dir, r.dir)).split(sep7).join("/") || ".";
23811
+ const path = relative8(base, join39(m.dir, r.dir)).split(sep7).join("/") || ".";
23449
23812
  out.push({ member: m.name, name: r.name, path });
23450
23813
  }
23451
23814
  }
@@ -23521,9 +23884,12 @@ function terragucciRoutes(app, opts, dirs) {
23521
23884
  // Where a key opens: this server, which reads it from the same source.
23522
23885
  files: "/api/terragucci/file?key=",
23523
23886
  ...r.estate ? { estate: r.estate } : {},
23887
+ // #568: the pull request stage, read from the newest tf-plan of a pull request.
23888
+ ...r.pull_request ? { pull_request: r.pull_request } : {},
23524
23889
  ...marks
23525
23890
  });
23526
23891
  });
23892
+ terragucciDemoRoute(app, source2, opts.now ? { now: opts.now } : {});
23527
23893
  terragucciTimelineRoute(app, source2, {
23528
23894
  ...opts.project ? { project: opts.project } : {},
23529
23895
  checkoutProject: () => checkoutProject(repoBase(dirs())),
@@ -23588,7 +23954,7 @@ var heldByOf = (v) => {
23588
23954
  return h && typeof h === "object" && typeof h.hook === "string" && typeof h.moment === "string" ? { hook: h.hook, moment: h.moment, ...typeof h.target === "string" ? { target: h.target } : {} } : void 0;
23589
23955
  };
23590
23956
  var modeOf = (v) => v === "ledger" || v === "pr-review" || v === "sealed" ? v : void 0;
23591
- var str13 = (v) => typeof v === "string" && v !== "" ? v : void 0;
23957
+ var str14 = (v) => typeof v === "string" && v !== "" ? v : void 0;
23592
23958
  var strings3 = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : void 0;
23593
23959
  var time3 = (iso) => Date.parse(iso) || 0;
23594
23960
  function rowOf(line) {
@@ -23604,14 +23970,14 @@ function rowOf(line) {
23604
23970
  }
23605
23971
  function sealOf(r) {
23606
23972
  const s = r.seal;
23607
- const signed = s && s.signature ? { sealed: true, ...str13(s.signer) ? { signer: str13(s.signer) } : {} } : void 0;
23973
+ const signed = s && s.signature ? { sealed: true, ...str14(s.signer) ? { signer: str14(s.signer) } : {} } : void 0;
23608
23974
  if (r.review || r.via === "pr-review") return { seal: "review", ...signed };
23609
23975
  return signed ? { seal: "sealed", ...signed } : { seal: "unsigned" };
23610
23976
  }
23611
23977
  function reviewOf(r) {
23612
23978
  const rv = r.review ?? void 0;
23613
23979
  const pr = typeof rv?.pr === "number" ? rv.pr : typeof r.pr === "number" ? r.pr : void 0;
23614
- const head = str13(rv?.head) ?? str13(r.head);
23980
+ const head = str14(rv?.head) ?? str14(r.head);
23615
23981
  const approvers = strings3(rv?.approvers) ?? strings3(r.reviewers);
23616
23982
  if (pr === void 0 && head === void 0 && approvers === void 0) return void 0;
23617
23983
  return { ...pr !== void 0 ? { pr } : {}, ...head ? { head } : {}, ...approvers ? { approvers } : {} };
@@ -23637,7 +24003,7 @@ function decisionOf(d) {
23637
24003
  const r = v;
23638
24004
  if (!r || typeof r !== "object" || typeof r.wave !== "number" || typeof r.name !== "string" || typeof r.op !== "string" || typeof r.gate !== "string") return void 0;
23639
24005
  const outcomes2 = Array.isArray(r.outcomes) ? r.outcomes.filter((o) => !!o && typeof o === "object" && typeof o.moment === "string" && typeof o.name === "string" && typeof o.exit === "number" && typeof o.outcome === "string") : void 0;
23640
- return { wave: r.wave, name: r.name, op: r.op, gate: r.gate, ...str13(r.digest) ? { digest: str13(r.digest) } : {}, ...strings3(r.heldBy) ? { heldBy: strings3(r.heldBy) } : {}, ...r.heldOn !== void 0 ? { heldOn: r.heldOn } : {}, ...outcomes2 ? { outcomes: outcomes2 } : {} };
24006
+ return { wave: r.wave, name: r.name, op: r.op, gate: r.gate, ...str14(r.digest) ? { digest: str14(r.digest) } : {}, ...strings3(r.heldBy) ? { heldBy: strings3(r.heldBy) } : {}, ...r.heldOn !== void 0 ? { heldOn: r.heldOn } : {}, ...outcomes2 ? { outcomes: outcomes2 } : {} };
23641
24007
  }
23642
24008
  function deriveGateAudit(input) {
23643
24009
  const isOverride = input.isOverride ?? isOverrideGate2;
@@ -23662,17 +24028,17 @@ function deriveGateAudit(input) {
23662
24028
  const requests = [];
23663
24029
  for (const p of parsed) {
23664
24030
  if (!p.pending || p.line.removed) continue;
23665
- const digest = str13(p.r.planDigest);
24031
+ const digest = str14(p.r.planDigest);
23666
24032
  denials.set(`${String(p.r.gate)}
23667
24033
  ${digest ?? ""}`, p.r);
23668
- requests.push({ op: ledger.op, gate: String(p.r.gate), at: time3(String(p.r.timestamp)), ...digest ? { digest } : {}, ...str13(p.r.environment) ? { environment: str13(p.r.environment) } : {} });
24034
+ requests.push({ op: ledger.op, gate: String(p.r.gate), at: time3(String(p.r.timestamp)), ...digest ? { digest } : {}, ...str14(p.r.environment) ? { environment: str14(p.r.environment) } : {} });
23669
24035
  }
23670
24036
  for (const { line, n, r, pending } of parsed) {
23671
24037
  const gate = String(r.gate);
23672
24038
  const override = isOverride(ledger.op, gate);
23673
24039
  const removed = line.removed;
23674
24040
  if (pending && removed) continue;
23675
- const digest = str13(r.planDigest);
24041
+ const digest = str14(r.planDigest);
23676
24042
  const author = removed ?? line.added;
23677
24043
  const source2 = {
23678
24044
  path,
@@ -23691,7 +24057,7 @@ ${digest ?? ""}`, p.r);
23691
24057
  ...base,
23692
24058
  kind: override ? "override-requested" : "approval-requested",
23693
24059
  at: String(r.timestamp),
23694
- ...drop({ runId: str13(r.runId), expiresAt: str13(r.expiresAt), description: str13(r.description), url: str13(r.url), rules: strings3(r.rules), wave: waveOf(r.wave), heldBy: heldByOf(r.heldBy), approvalMode: modeOf(r.approvalMode) }),
24060
+ ...drop({ runId: str14(r.runId), expiresAt: str14(r.expiresAt), description: str14(r.description), url: str14(r.url), rules: strings3(r.rules), wave: waveOf(r.wave), heldBy: heldByOf(r.heldBy), approvalMode: modeOf(r.approvalMode) }),
23695
24061
  detail: extra2
23696
24062
  }));
23697
24063
  continue;
@@ -23719,13 +24085,13 @@ ${digest ?? ""}`) : void 0;
23719
24085
  at: String(r.timestamp),
23720
24086
  by: String(r.resolvedBy),
23721
24087
  seal,
23722
- ...drop({ sealed, signer, committedBy: line.added?.by, relayedBy: str13(r.relayedBy), approver: approverOf(r), note: str13(r.note), url: str13(r.url), review: reviewOf(r), rules }),
24088
+ ...drop({ sealed, signer, committedBy: line.added?.by, relayedBy: str14(r.relayedBy), approver: approverOf(r), note: str14(r.note), url: str14(r.url), review: reviewOf(r), rules }),
23723
24089
  detail: extra2
23724
24090
  });
23725
24091
  entries.push(entry);
23726
24092
  if (!override) {
23727
24093
  const at2 = time3(entry.at);
23728
- const env = str13(r.environment);
24094
+ const env = str14(r.environment);
23729
24095
  const asked = requests.some((q) => q.gate === gate && q.at <= at2 && compatible(q.digest, digest) && (q.environment === void 0 || env === void 0 || q.environment === env));
23730
24096
  if (!asked) {
23731
24097
  gaps.push({ kind: "approval-without-request", op: ledger.op, gate, ...digest ? { digest } : {}, detail: `${path} holds an approval of ${gate} by ${entry.by} at ${entry.at} with no pending fact of that gate${digest ? " and digest" : ""} before it` });
@@ -24000,8 +24366,8 @@ function lifecycleHistoryRoute(app, base, opts = {}) {
24000
24366
  }
24001
24367
 
24002
24368
  // src/lifecycle-releases.ts
24003
- import { existsSync as existsSync22, readFileSync as readFileSync26 } from "node:fs";
24004
- import { join as join38 } from "node:path";
24369
+ import { existsSync as existsSync22, readFileSync as readFileSync28 } from "node:fs";
24370
+ import { join as join40 } from "node:path";
24005
24371
 
24006
24372
  // node_modules/@intentius/chant/src/lifecycle/legacy-digest.ts
24007
24373
  import { resolve as resolve25 } from "node:path";
@@ -24114,10 +24480,10 @@ function memberNote(l) {
24114
24480
  // src/lifecycle-releases.ts
24115
24481
  var WAVES_SPEC_FILE = "yodel-waves.json";
24116
24482
  function waveOrder(checkout) {
24117
- const p = join38(checkout, WAVES_SPEC_FILE);
24483
+ const p = join40(checkout, WAVES_SPEC_FILE);
24118
24484
  if (!existsSync22(p)) return void 0;
24119
24485
  try {
24120
- const doc = JSON.parse(readFileSync26(p, "utf8"));
24486
+ const doc = JSON.parse(readFileSync28(p, "utf8"));
24121
24487
  const names = (doc.waves ?? []).map((w) => w?.name).filter((n) => typeof n === "string" && n.length > 0);
24122
24488
  return names.length ? [...new Set(names)] : void 0;
24123
24489
  } catch {
@@ -24125,7 +24491,7 @@ function waveOrder(checkout) {
24125
24491
  }
24126
24492
  }
24127
24493
  function hasReleaseLedgerHint(checkout) {
24128
- return existsSync22(join38(checkout, "yodel.config.ts")) || existsSync22(join38(checkout, WAVES_SPEC_FILE));
24494
+ return existsSync22(join40(checkout, "yodel.config.ts")) || existsSync22(join40(checkout, WAVES_SPEC_FILE));
24129
24495
  }
24130
24496
  function orderEnvironments(envs, waves) {
24131
24497
  const seen = new Set(envs);
@@ -24167,7 +24533,7 @@ function releaseCells(ledgers, order, sources) {
24167
24533
  }
24168
24534
  async function copyCheckoutRef(git3, checkout, cache6) {
24169
24535
  assertScratch(cache6.split(/[\\/]/).pop());
24170
- if (!existsSync22(join38(cache6, "HEAD"))) {
24536
+ if (!existsSync22(join40(cache6, "HEAD"))) {
24171
24537
  const init = await git3(["init", "--bare", "-q", cache6]);
24172
24538
  if (init.code !== 0) return false;
24173
24539
  }
@@ -24270,8 +24636,8 @@ function releasesRoutes(app, base, opts = {}) {
24270
24636
  }
24271
24637
 
24272
24638
  // src/sql-yodeler-lane.ts
24273
- import { existsSync as existsSync23, readdirSync as readdirSync12, readFileSync as readFileSync27 } from "node:fs";
24274
- import { join as join39 } from "node:path";
24639
+ import { existsSync as existsSync23, readdirSync as readdirSync12, readFileSync as readFileSync29 } from "node:fs";
24640
+ import { join as join41 } from "node:path";
24275
24641
  var YODEL_APPLY_OP = "yodel-apply";
24276
24642
  var YODEL_HEAD_PLANS_DIR = headPlansPath(YODEL_APPLY_OP, "").dir;
24277
24643
  function parseYodelWaves(text, path = WAVES_SPEC_FILE) {
@@ -24303,16 +24669,16 @@ function parseYodelWaves(text, path = WAVES_SPEC_FILE) {
24303
24669
  return { path, waves };
24304
24670
  }
24305
24671
  function readYodelWaves(checkout) {
24306
- const p = join39(checkout, WAVES_SPEC_FILE);
24672
+ const p = join41(checkout, WAVES_SPEC_FILE);
24307
24673
  if (!existsSync23(p)) return void 0;
24308
24674
  try {
24309
- return parseYodelWaves(readFileSync27(p, "utf8"));
24675
+ return parseYodelWaves(readFileSync29(p, "utf8"));
24310
24676
  } catch (e) {
24311
24677
  return { path: WAVES_SPEC_FILE, waves: [], error: e instanceof Error ? e.message : String(e) };
24312
24678
  }
24313
24679
  }
24314
24680
  function migrateOpEnvironments(checkout) {
24315
- const dir = join39(checkout, "ops");
24681
+ const dir = join41(checkout, "ops");
24316
24682
  if (!existsSync23(dir)) return [];
24317
24683
  try {
24318
24684
  return readdirSync12(dir).map((f) => /^migrate-(.+)\.op\.ts$/.exec(f)?.[1]).filter((e) => !!e).sort();
@@ -24435,16 +24801,16 @@ function sqlYodelerLaneRoute(app, checkout, opts = {}) {
24435
24801
 
24436
24802
  // src/terragucci-estate.ts
24437
24803
  init_yaml();
24438
- import { existsSync as existsSync24, readFileSync as readFileSync28, statSync as statSync13 } from "node:fs";
24804
+ import { existsSync as existsSync24, readFileSync as readFileSync30, statSync as statSync13 } from "node:fs";
24439
24805
  import { createRequire as createRequire7 } from "node:module";
24440
- import { dirname as dirname14, extname, join as join40, resolve as resolve26 } from "node:path";
24806
+ import { dirname as dirname14, extname, join as join42, resolve as resolve26 } from "node:path";
24441
24807
  import { fileURLToPath as fileURLToPath8 } from "node:url";
24442
24808
  import { Ajv2020 as Ajv20204 } from "ajv/dist/2020.js";
24443
24809
  var RUN_SCHEMA2 = "terragucci.run/v1";
24444
24810
  function configProjects(file) {
24445
24811
  let text;
24446
24812
  try {
24447
- text = readFileSync28(file, "utf8");
24813
+ text = readFileSync30(file, "utf8");
24448
24814
  } catch {
24449
24815
  return void 0;
24450
24816
  }
@@ -24472,21 +24838,21 @@ function controlRepo(dirs, roots = () => checkoutRoots(dirs).length) {
24472
24838
  }
24473
24839
  return n === 0 ? { config, projects } : void 0;
24474
24840
  }
24475
- var isRecord17 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
24476
- var str14 = (v) => typeof v === "string";
24477
- var int = (v) => typeof v === "number" && Number.isInteger(v);
24841
+ var isRecord18 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
24842
+ var str15 = (v) => typeof v === "string";
24843
+ var int2 = (v) => typeof v === "number" && Number.isInteger(v);
24478
24844
  function structuralRun2(doc) {
24479
- if (!isRecord17(doc)) return "it is not a JSON object";
24845
+ if (!isRecord18(doc)) return "it is not a JSON object";
24480
24846
  if (doc.schema !== RUN_SCHEMA2) return `its schema is ${JSON.stringify(doc.schema)}, not ${RUN_SCHEMA2}`;
24481
- if (!str14(doc.project) || !str14(doc.commit) || !str14(doc.updated)) return "it lacks project, commit or updated";
24847
+ if (!str15(doc.project) || !str15(doc.commit) || !str15(doc.updated)) return "it lacks project, commit or updated";
24482
24848
  if (!Array.isArray(doc.roots) || !Array.isArray(doc.waves)) return "it has no roots or waves array";
24483
24849
  for (const [i, r] of doc.roots.entries()) {
24484
- if (!isRecord17(r) || !str14(r.root) || !int(r.wave) || !Array.isArray(r.reads) || !r.reads.every(str14)) return `root ${i} lacks root, wave or reads`;
24485
- if (r.state !== void 0 && !(isRecord17(r.state) && str14(r.state.key))) return `root ${String(r.root)}'s state has no key`;
24486
- if (r.external !== void 0 && !(Array.isArray(r.external) && r.external.every((x) => isRecord17(x) && str14(x.data) && str14(x.key)))) return `root ${String(r.root)}'s external reads lack data or key`;
24850
+ if (!isRecord18(r) || !str15(r.root) || !int2(r.wave) || !Array.isArray(r.reads) || !r.reads.every(str15)) return `root ${i} lacks root, wave or reads`;
24851
+ if (r.state !== void 0 && !(isRecord18(r.state) && str15(r.state.key))) return `root ${String(r.root)}'s state has no key`;
24852
+ if (r.external !== void 0 && !(Array.isArray(r.external) && r.external.every((x) => isRecord18(x) && str15(x.data) && str15(x.key)))) return `root ${String(r.root)}'s external reads lack data or key`;
24487
24853
  }
24488
24854
  for (const [i, w] of doc.waves.entries()) {
24489
- if (!isRecord17(w) || !int(w.number) || !Array.isArray(w.roots) || !Array.isArray(w.reads) || !str14(w.state) || !str14(w.gate)) return `wave ${i} lacks number, roots, reads, state or gate`;
24855
+ if (!isRecord18(w) || !int2(w.number) || !Array.isArray(w.roots) || !Array.isArray(w.reads) || !str15(w.state) || !str15(w.gate)) return `wave ${i} lacks number, roots, reads, state or gate`;
24490
24856
  }
24491
24857
  return void 0;
24492
24858
  }
@@ -24495,16 +24861,16 @@ function runViewValidator(dirs = []) {
24495
24861
  for (const from of [...dirs, dirname14(fileURLToPath8(import.meta.url))]) {
24496
24862
  let entry;
24497
24863
  try {
24498
- entry = createRequire7(join40(resolve26(from), "noop.js")).resolve("@intentius/terragucci/report.schema.json");
24864
+ entry = createRequire7(join42(resolve26(from), "noop.js")).resolve("@intentius/terragucci/report.schema.json");
24499
24865
  } catch {
24500
24866
  continue;
24501
24867
  }
24502
24868
  const dist = dirname14(entry);
24503
- const schema = join40(dist, "run.schema.json");
24869
+ const schema = join42(dist, "run.schema.json");
24504
24870
  let stamp;
24505
24871
  try {
24506
24872
  const st = statSync13(schema);
24507
- const pkg = statSync13(join40(dist, "..", "package.json"));
24873
+ const pkg = statSync13(join42(dist, "..", "package.json"));
24508
24874
  stamp = `${schema}\0${st.mtimeMs}:${st.size}\0${pkg.mtimeMs}`;
24509
24875
  } catch {
24510
24876
  continue;
@@ -24513,12 +24879,12 @@ function runViewValidator(dirs = []) {
24513
24879
  if (hit) return hit;
24514
24880
  let v;
24515
24881
  try {
24516
- const version = JSON.parse(readFileSync28(join40(dist, "..", "package.json"), "utf8")).version ?? "";
24517
- const validate = new Ajv20204({ strict: false, allErrors: false }).compile(JSON.parse(readFileSync28(schema, "utf8")));
24882
+ const version = JSON.parse(readFileSync30(join42(dist, "..", "package.json"), "utf8")).version ?? "";
24883
+ const validate = new Ajv20204({ strict: false, allErrors: false }).compile(JSON.parse(readFileSync30(schema, "utf8")));
24518
24884
  v = {
24519
24885
  validation: { by: "schema", terragucci: version },
24520
24886
  check(doc) {
24521
- const id = isRecord17(doc) ? doc.schema : void 0;
24887
+ const id = isRecord18(doc) ? doc.schema : void 0;
24522
24888
  if (id !== RUN_SCHEMA2) return `its schema is ${JSON.stringify(id)}, not ${RUN_SCHEMA2}`;
24523
24889
  if (validate(doc)) return void 0;
24524
24890
  const e = validate.errors?.[0];
@@ -24569,8 +24935,8 @@ function controlEdges(views) {
24569
24935
  function siblingCheckout(project, base, projectOf = checkoutProject) {
24570
24936
  const name = project.split("/").filter(Boolean).pop();
24571
24937
  if (!name) return void 0;
24572
- const dir = join40(dirname14(resolve26(base)), name);
24573
- if (dir === resolve26(base) || !existsSync24(join40(dir, ".git"))) return void 0;
24938
+ const dir = join42(dirname14(resolve26(base)), name);
24939
+ if (dir === resolve26(base) || !existsSync24(join42(dir, ".git"))) return void 0;
24574
24940
  return projectOf(dir) === project ? dir : void 0;
24575
24941
  }
24576
24942
  var shellWord = (s) => /^[A-Za-z0-9_./:@%+=,-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`;
@@ -24777,9 +25143,9 @@ function controlRepoInfo(dirs) {
24777
25143
  }
24778
25144
 
24779
25145
  // src/yodel-report.ts
24780
- import { readdirSync as readdirSync13, readFileSync as readFileSync29, statSync as statSync14 } from "node:fs";
25146
+ import { readdirSync as readdirSync13, readFileSync as readFileSync31, statSync as statSync14 } from "node:fs";
24781
25147
  import { createRequire as createRequire8 } from "node:module";
24782
- import { basename as basename4, dirname as dirname15, join as join41, resolve as resolve27 } from "node:path";
25148
+ import { basename as basename4, dirname as dirname15, join as join43, resolve as resolve27 } from "node:path";
24783
25149
  import { fileURLToPath as fileURLToPath9 } from "node:url";
24784
25150
  import { Ajv2020 as Ajv20205 } from "ajv/dist/2020.js";
24785
25151
 
@@ -25208,13 +25574,13 @@ var installed = /* @__PURE__ */ new Map();
25208
25574
  function packageRoot(from) {
25209
25575
  let entry;
25210
25576
  try {
25211
- entry = createRequire8(join41(resolve27(from), "noop.js")).resolve("@intentius/sql-yodeler");
25577
+ entry = createRequire8(join43(resolve27(from), "noop.js")).resolve("@intentius/sql-yodeler");
25212
25578
  } catch {
25213
25579
  return void 0;
25214
25580
  }
25215
25581
  for (let d = dirname15(entry); d !== dirname15(d); d = dirname15(d)) {
25216
25582
  try {
25217
- const pkg = JSON.parse(readFileSync29(join41(d, "package.json"), "utf8"));
25583
+ const pkg = JSON.parse(readFileSync31(join43(d, "package.json"), "utf8"));
25218
25584
  if (pkg.name === "@intentius/sql-yodeler") return d;
25219
25585
  } catch {
25220
25586
  }
@@ -25226,11 +25592,11 @@ function yodelValidator(dirs = []) {
25226
25592
  const root = packageRoot(from);
25227
25593
  if (!root) continue;
25228
25594
  try {
25229
- const version = JSON.parse(readFileSync29(join41(root, "package.json"), "utf8")).version ?? "";
25595
+ const version = JSON.parse(readFileSync31(join43(root, "package.json"), "utf8")).version ?? "";
25230
25596
  const key = `${root}\0${version}`;
25231
25597
  let v = installed.get(key);
25232
25598
  if (!v) {
25233
- const load2 = (n) => JSON.parse(readFileSync29(join41(root, "schemas", `${n}.schema.json`), "utf8"));
25599
+ const load2 = (n) => JSON.parse(readFileSync31(join43(root, "schemas", `${n}.schema.json`), "utf8"));
25234
25600
  v = compile({ common: load2("common"), report: load2("report"), drift: load2("drift") }, { by: "schema", from: "package", sqlYodeler: version });
25235
25601
  installed.set(key, v);
25236
25602
  }
@@ -25241,9 +25607,9 @@ function yodelValidator(dirs = []) {
25241
25607
  pinned ??= compile({ common: common_schema_default, report: report_schema_default, drift: drift_schema_default }, { by: "schema", from: "pinned", commit: PINNED_COMMIT.slice(0, 7) });
25242
25608
  return pinned;
25243
25609
  }
25244
- var isRecord18 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
25245
- var looksLikeReport = (d) => isRecord18(d) && "generatedAt" in d && "environments" in d;
25246
- var looksLikeDrift = (d) => isRecord18(d) && "environment" in d && ("drifted" in d || "changed" in d);
25610
+ var isRecord19 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
25611
+ var looksLikeReport = (d) => isRecord19(d) && "generatedAt" in d && "environments" in d;
25612
+ var looksLikeDrift = (d) => isRecord19(d) && "environment" in d && ("drifted" in d || "changed" in d);
25247
25613
  function filesOf(source2) {
25248
25614
  let st;
25249
25615
  try {
@@ -25252,7 +25618,7 @@ function filesOf(source2) {
25252
25618
  return refuse14(`--yodel-report ${source2}: no such file or directory`, "Point --yodel-report at the file `yodel report --json` wrote, or the directory holding it.");
25253
25619
  }
25254
25620
  const dir = st.isDirectory() ? source2 : dirname15(source2);
25255
- const files = readdirSync13(dir).filter((f) => f.endsWith(".json")).sort().map((f) => join41(dir, f));
25621
+ const files = readdirSync13(dir).filter((f) => f.endsWith(".json")).sort().map((f) => join43(dir, f));
25256
25622
  return st.isDirectory() ? { dir, files } : { dir, files, named: resolve27(source2) };
25257
25623
  }
25258
25624
  function readYodelReport(source2, validator2) {
@@ -25264,7 +25630,7 @@ function readYodelReport(source2, validator2) {
25264
25630
  for (const file of files) {
25265
25631
  let doc;
25266
25632
  try {
25267
- doc = JSON.parse(readFileSync29(file, "utf8"));
25633
+ doc = JSON.parse(readFileSync31(file, "utf8"));
25268
25634
  } catch (e) {
25269
25635
  if (file === named) refuse14(`${basename4(file)} is not JSON: ${e instanceof Error ? e.message : String(e)}`, remedy);
25270
25636
  ignored.push(basename4(file));
@@ -25351,24 +25717,24 @@ function yodelReportRoute(app, source2, dirs) {
25351
25717
 
25352
25718
  // src/demo-pins.ts
25353
25719
  init_terraform_member();
25354
- import { existsSync as existsSync25, lstatSync as lstatSync2, mkdirSync as mkdirSync7, readFileSync as readFileSync30, readlinkSync as readlinkSync2, symlinkSync as symlinkSync2, writeFileSync as writeFileSync6 } from "node:fs";
25355
- import { dirname as dirname16, join as join42, resolve as resolve28 } from "node:path";
25356
- var isRecord19 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
25720
+ import { existsSync as existsSync25, lstatSync as lstatSync2, mkdirSync as mkdirSync7, readFileSync as readFileSync32, readlinkSync as readlinkSync2, symlinkSync as symlinkSync2, writeFileSync as writeFileSync6 } from "node:fs";
25721
+ import { dirname as dirname16, join as join44, resolve as resolve28 } from "node:path";
25722
+ var isRecord20 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
25357
25723
  function linkDemoPins(work, probes = {}) {
25358
- const file = join42(work, "chant.workspace.json");
25724
+ const file = join44(work, "chant.workspace.json");
25359
25725
  if (!existsSync25(file)) return { ok: true, linked: [] };
25360
25726
  let decl;
25361
25727
  try {
25362
- decl = JSON.parse(readFileSync30(file, "utf8"));
25728
+ decl = JSON.parse(readFileSync32(file, "utf8"));
25363
25729
  } catch {
25364
25730
  return { ok: true, linked: [] };
25365
25731
  }
25366
- if (!isRecord19(decl) || !Array.isArray(decl.pins)) return { ok: true, linked: [] };
25732
+ if (!isRecord20(decl) || !Array.isArray(decl.pins)) return { ok: true, linked: [] };
25367
25733
  const find = probes.resolve ?? ((pkg) => resolvedPackage(pkg));
25368
25734
  const linked = [];
25369
25735
  let rewrite = false;
25370
25736
  for (const pin of decl.pins) {
25371
- if (!isRecord19(pin) || typeof pin.package !== "string") continue;
25737
+ if (!isRecord20(pin) || typeof pin.package !== "string") continue;
25372
25738
  const pkg = pin.package;
25373
25739
  if (pkg === TERRAFORM_LEXICON_PKG) {
25374
25740
  const refusal2 = (probes.reader ?? terraformReaderState)().refusal;
@@ -25382,7 +25748,7 @@ function linkDemoPins(work, probes = {}) {
25382
25748
  remedy: `Install ${pkg} beside behold, then load the demo again.`
25383
25749
  };
25384
25750
  }
25385
- const link = join42(work, "node_modules", ...pkg.split("/"));
25751
+ const link = join44(work, "node_modules", ...pkg.split("/"));
25386
25752
  if (!present(link)) {
25387
25753
  mkdirSync7(dirname16(link), { recursive: true });
25388
25754
  symlinkSync2(found.dir, link, "junction");
@@ -25417,13 +25783,13 @@ function linksTo(link, target) {
25417
25783
  }
25418
25784
 
25419
25785
  // src/demos.ts
25420
- import { readFileSync as readFileSync31, existsSync as existsSync26, cpSync, mkdirSync as mkdirSync8 } from "node:fs";
25421
- import { dirname as dirname17, isAbsolute as isAbsolute5, join as join43, relative as relative9, resolve as resolve29, sep as sep8 } from "node:path";
25786
+ import { readFileSync as readFileSync33, existsSync as existsSync26, cpSync, mkdirSync as mkdirSync8 } from "node:fs";
25787
+ import { dirname as dirname17, isAbsolute as isAbsolute5, join as join45, relative as relative9, resolve as resolve29, sep as sep8 } from "node:path";
25422
25788
  import { spawn as spawn8, spawnSync as spawnSync3 } from "node:child_process";
25423
25789
  function readCatalog(file, catalog) {
25424
25790
  let raw;
25425
25791
  try {
25426
- raw = JSON.parse(readFileSync31(file, "utf8"));
25792
+ raw = JSON.parse(readFileSync33(file, "utf8"));
25427
25793
  } catch {
25428
25794
  return [];
25429
25795
  }
@@ -25463,10 +25829,10 @@ function validEntry(e) {
25463
25829
  return true;
25464
25830
  }
25465
25831
  function loadDemoRegistry(pkgRoot3) {
25466
- const merged = readCatalog(join43(pkgRoot3, "demos.json"), "demos");
25832
+ const merged = readCatalog(join45(pkgRoot3, "demos.json"), "demos");
25467
25833
  const seen = new Set(merged.map((e) => e.name));
25468
25834
  const named = process.env.BEHOLD_WORKBENCH;
25469
- for (const e of [...readCatalog(join43(pkgRoot3, "workbench.json"), "workbench"), ...named ? readCatalog(resolve29(named), "workbench") : []]) {
25835
+ for (const e of [...readCatalog(join45(pkgRoot3, "workbench.json"), "workbench"), ...named ? readCatalog(resolve29(named), "workbench") : []]) {
25470
25836
  if (seen.has(e.name)) {
25471
25837
  process.stderr.write(`behold: workbench entry "${e.name}" collides with a demo already in the catalog \u2014 dropped
25472
25838
  `);
@@ -25513,7 +25879,7 @@ async function loadDemo(entry, opts) {
25513
25879
  say(`serving ${work} where it is \u2014 nothing copied`);
25514
25880
  } else if (!existsSync26(target)) {
25515
25881
  if (entry.source === "bundled" || entry.source === "local" && local) {
25516
- const from = entry.source === "bundled" ? join43(pkgRoot3, entry.dir) : local;
25882
+ const from = entry.source === "bundled" ? join45(pkgRoot3, entry.dir) : local;
25517
25883
  if (!existsSync26(from)) return { ok: false, error: entry.source === "bundled" ? `this install has no bundled ${entry.dir}` : `${entry.path} is not checked out (looked in ${from})` };
25518
25884
  say(`copying to ${target} (it's yours \u2014 edit it)`);
25519
25885
  try {
@@ -25545,7 +25911,7 @@ async function loadDemo(entry, opts) {
25545
25911
  if (!pins.ok) return { ok: false, error: `${pins.error} ${pins.remedy}` };
25546
25912
  for (const p of pins.linked) say(`linked ${p.pkg} ${p.version} into the copy${p.was ? ` (the declaration pinned ${p.was}; now ${p.version})` : ""}`);
25547
25913
  }
25548
- if (!inPlace && existsSync26(join43(work, "package.json")) && !existsSync26(join43(work, "node_modules"))) {
25914
+ if (!inPlace && existsSync26(join45(work, "package.json")) && !existsSync26(join45(work, "node_modules"))) {
25549
25915
  say("npm install\u2026");
25550
25916
  if (await runStep("npm", ["install"], { cwd: work, shell: process.platform === "win32" }) !== 0) {
25551
25917
  return { ok: false, error: `npm install failed in ${work}` };
@@ -25562,7 +25928,7 @@ async function loadDemo(entry, opts) {
25562
25928
  return { ok: false, error: `setup failed (${entry.setup})` };
25563
25929
  }
25564
25930
  }
25565
- return { ok: true, serveDirs: entry.serve.dirs?.length ? entry.serve.dirs.map((d) => join43(work, d)) : [work] };
25931
+ return { ok: true, serveDirs: entry.serve.dirs?.length ? entry.serve.dirs.map((d) => join45(work, d)) : [work] };
25566
25932
  }
25567
25933
  function runStep(cmd, args, opts = {}) {
25568
25934
  return new Promise((res) => {
@@ -25573,8 +25939,8 @@ function runStep(cmd, args, opts = {}) {
25573
25939
  }
25574
25940
 
25575
25941
  // src/server.ts
25576
- var pkgRoot = join44(dirname18(fileURLToPath10(import.meta.url)), "..");
25577
- var webRoot = join44(pkgRoot, "web");
25942
+ var pkgRoot = join46(dirname18(fileURLToPath10(import.meta.url)), "..");
25943
+ var webRoot = join46(pkgRoot, "web");
25578
25944
  var execFileP = async (cmd, args) => (await promisify3(execFile5)(cmd, args, { encoding: "utf8", timeout: 1e4 })).stdout;
25579
25945
  function optsFromQuery(url, tierEnvVar, projectDir) {
25580
25946
  const q = url.searchParams;
@@ -25625,7 +25991,7 @@ function tierFailure(tier, message2) {
25625
25991
  function beholdVersion() {
25626
25992
  try {
25627
25993
  const here = dirname18(fileURLToPath10(import.meta.url));
25628
- return JSON.parse(readFileSync32(join44(here, "..", "package.json"), "utf8")).version ?? "unknown";
25994
+ return JSON.parse(readFileSync34(join46(here, "..", "package.json"), "utf8")).version ?? "unknown";
25629
25995
  } catch {
25630
25996
  return "unknown";
25631
25997
  }
@@ -25672,7 +26038,7 @@ async function captureFrame(projectDir, env, frames, broadcaster) {
25672
26038
  }
25673
26039
  }
25674
26040
  function carveRoutes(app, reportPath, demo) {
25675
- const load2 = () => readCarveReport(reportPath, (p) => readFileSync32(p, "utf8"));
26041
+ const load2 = () => readCarveReport(reportPath, (p) => readFileSync34(p, "utf8"));
25676
26042
  const carveDirs = [dirname18(reportPath), ...demo ? [demo.out] : []];
25677
26043
  const carveStates = () => readCarveStates(carveDirs, nodeCarveIo, 0);
25678
26044
  const emittedNodes = async (states) => {
@@ -25908,13 +26274,18 @@ function cardPrefixesOf(ir) {
25908
26274
  const byStack = ir.groups.byStack;
25909
26275
  return byStack && Object.keys(byStack).length ? byStack : void 0;
25910
26276
  }
25911
- function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffer(), runner = new OpRunner({
25912
- projectDir: cfg.projectDir,
25913
- broadcaster,
25914
- onDone: (opEnv) => captureFrame(cfg.projectDir, opEnv ?? cfg.env, frames, broadcaster)
25915
- })) {
26277
+ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffer(), given) {
26278
+ const runner = cfg.public ? new ReadOnlyRunner() : given ?? new OpRunner({
26279
+ projectDir: cfg.projectDir,
26280
+ broadcaster,
26281
+ onDone: (opEnv) => captureFrame(cfg.projectDir, opEnv ?? cfg.env, frames, broadcaster)
26282
+ });
25916
26283
  const app = new Hono();
25917
- const guard = { allowedHosts: allowedHostsFrom(cfg.allowedHosts ?? []), ...cfg.host ? { boundHost: cfg.host } : {} };
26284
+ const guard = {
26285
+ allowedHosts: allowedHostsFrom(cfg.allowedHosts ?? []),
26286
+ ...cfg.host ? { boundHost: cfg.host } : {},
26287
+ ...cfg.public ? { public: true } : {}
26288
+ };
25918
26289
  app.use("*", async (c, next) => {
25919
26290
  const verdict3 = guardRequest(
25920
26291
  {
@@ -25925,9 +26296,13 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
25925
26296
  },
25926
26297
  guard
25927
26298
  );
25928
- if (!verdict3.ok) return c.json({ error: verdict3.error, code: "request-refused" }, verdict3.status);
26299
+ if (!verdict3.ok) {
26300
+ if (verdict3.status === 405) c.header("allow", "GET, HEAD, OPTIONS");
26301
+ return c.json({ error: verdict3.error, code: verdict3.status === 405 ? "public" : "request-refused" }, verdict3.status);
26302
+ }
25929
26303
  await next();
25930
26304
  });
26305
+ if (cfg.public) publicRoutes(app, webRoot);
25931
26306
  setChoudoufuRunner(cfg.choudoufu?.run);
25932
26307
  setEstateLexiconRead(cfg.choudoufu ? cfg.choudoufu.lexicon ?? null : void 0);
25933
26308
  setServedWorkspace(cfg.workspace);
@@ -25985,6 +26360,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
25985
26360
  };
25986
26361
  const estateSourceRoots = async (opts) => (await Promise.all((cfg.projectDirs ?? []).map(async (dir) => [await graphPath(dir, opts), dir]))).flat();
25987
26362
  app.get("/healthz", (c) => c.json({ ok: true, projectDir: cfg.projectDir, env: cfg.env ?? null, frames: frames.size }));
26363
+ kioskRoute(app, webRoot, !!cfg.terragucci);
25988
26364
  app.get("/api/frames", (c) => c.json({ frames: frames.summaries() }));
25989
26365
  app.get("/lanes", (c) => {
25990
26366
  const all = frames.all();
@@ -26068,8 +26444,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
26068
26444
  return c.json({ started: true, name, ran: label });
26069
26445
  });
26070
26446
  app.post("/api/local/reset", (c) => {
26071
- const down = join44(cfg.projectDir, "scripts/local/local-down.sh");
26072
- const up = join44(cfg.projectDir, "scripts/local/local-up.sh");
26447
+ const down = join46(cfg.projectDir, "scripts/local/local-down.sh");
26448
+ const up = join46(cfg.projectDir, "scripts/local/local-up.sh");
26073
26449
  if (!existsSync27(down) || !existsSync27(up)) {
26074
26450
  return c.json({ error: "no local-down.sh / local-up.sh in scripts/local \u2014 reset is only for local emulator projects" }, 400);
26075
26451
  }
@@ -26091,7 +26467,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
26091
26467
  const dirs = [];
26092
26468
  const declared = [];
26093
26469
  for (const f of discoverOpIrs(estateDirs)) {
26094
- const parsed = readOpIr(f.path, (p) => readFileSync32(p, "utf8"));
26470
+ const parsed = readOpIr(f.path, (p) => readFileSync34(p, "utf8"));
26095
26471
  if (!parsed.ok) continue;
26096
26472
  const mine = declaredConvergeOps([parsed.ir]);
26097
26473
  if (!mine.length) continue;
@@ -26271,7 +26647,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
26271
26647
  });
26272
26648
  app.get("/api/project", async (c) => {
26273
26649
  const { environments: declaredEnvs, lexicons, stacks, k8sProfiles } = await detectProject(cfg.projectDir);
26274
- const environments = declaredEnvs.length === 0 && (cfg.projectDirs ?? [cfg.projectDir]).some((d) => memberKindOf(d) === "choudoufu") ? ["live"] : declaredEnvs;
26650
+ const environments = cfg.public ? [] : declaredEnvs.length === 0 && (cfg.projectDirs ?? [cfg.projectDir]).some((d) => memberKindOf(d) === "choudoufu") ? ["live"] : declaredEnvs;
26275
26651
  const k8sTarget = lexicons.includes("k8s") ? resolveK8sTarget(k8sProfiles, cfg.env, await loadKubeconfig()) : void 0;
26276
26652
  const axes = deployAxes(tierEnvVar, lexicons, k8sTarget);
26277
26653
  const emittedOps = discoverOpIrs(estateDirs).length;
@@ -26305,6 +26681,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
26305
26681
  currentEnv: cfg.env ?? null,
26306
26682
  // v0.1.0 preview: the SPA hides git/PR ops + arbitrary-project affordances.
26307
26683
  ...cfg.previewMode ? { previewMode: true } : {},
26684
+ // #569: a public view; the SPA hides every control that writes.
26685
+ ...cfg.public ? { publicMode: true } : {},
26308
26686
  // #489: a terragucci repo is applied by its pipeline; the SPA offers no
26309
26687
  // write at all, and the write routes refuse with the same words.
26310
26688
  ...tgConfig() ? { terragucci: { config: tgConfig(), ...terragucciRefusal(tgConfig(), "deploying from behold") } } : {},
@@ -26369,7 +26747,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
26369
26747
  // true is a flag the SPA can read as "absent means no".
26370
26748
  memberKinds,
26371
26749
  ...runtimeCapable ? { runtimeCapable: true } : {},
26372
- targets: deployTargets(lexicons, k8sTarget),
26750
+ // #569: a public view offers no target: `?target=` is refused there.
26751
+ targets: cfg.public ? [] : deployTargets(lexicons, k8sTarget),
26373
26752
  // Where the k8s binding came from, so the SPA never implies behold chose
26374
26753
  // it (#106). Absent for a project with no k8s lexicon or no resolvable
26375
26754
  // context.
@@ -26382,7 +26761,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
26382
26761
  const body = await c.req.json().catch(() => ({}));
26383
26762
  const dir = typeof body.dir === "string" && body.dir.trim() ? resolve30(body.dir.trim()) : "";
26384
26763
  if (!dir || !existsSync27(dir)) return c.json({ error: `no such directory: ${dir || "(no dir given)"}` }, 400);
26385
- if (!existsSync27(join44(dir, "chant.config.ts"))) {
26764
+ if (!existsSync27(join46(dir, "chant.config.ts"))) {
26386
26765
  return c.json({ error: `${dir} doesn't look like a chant project \u2014 no chant.config.ts` }, 400);
26387
26766
  }
26388
26767
  switchServedProject([dir]);
@@ -26495,6 +26874,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
26495
26874
  const layoutDir = () => cfg.workspace?.root ?? cfg.projectDir;
26496
26875
  const layoutWriteBlock = () => {
26497
26876
  if (cfg.previewMode) return "the layout sidecar is read-only in preview mode";
26877
+ if (cfg.public) return "a public view doesn't write to the project";
26498
26878
  if (cfg.carveReport) return "a carve report isn't a project \u2014 there's nowhere to keep a hand layout";
26499
26879
  if (cfg.layoutWrites === false) return "a static export captures a snapshot \u2014 it doesn't write to the project";
26500
26880
  return unwritableReason(layoutDir());
@@ -26685,7 +27065,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
26685
27065
  }
26686
27066
  const parsedOps = [];
26687
27067
  for (const f of files) {
26688
- const parsed = readOpIr(f.path, (p) => readFileSync32(p, "utf8"));
27068
+ const parsed = readOpIr(f.path, (p) => readFileSync34(p, "utf8"));
26689
27069
  if (!parsed.ok) return c.json(parsed.refusal, 422);
26690
27070
  parsedOps.push(parsed.ir);
26691
27071
  }
@@ -26698,8 +27078,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
26698
27078
  }
26699
27079
  const runState = runner.runState;
26700
27080
  const ranOp = runState.op ? parsedOps.find((o) => o.name === runState.op) : void 0;
26701
- const join49 = ranOp ? joinRun(ranOp, runState.records, runState.gate) : null;
26702
- const opsIr = join49 ? paintPlayhead(declaredIr, join49, runState.gate) : declaredIr;
27081
+ const join51 = ranOp ? joinRun(ranOp, runState.records, runState.gate) : null;
27082
+ const opsIr = join51 ? paintPlayhead(declaredIr, join51, runState.gate) : declaredIr;
26703
27083
  const { svg: svg2 } = renderGraph(opsIr, { boxes: "byStack" });
26704
27084
  const operator = runner.noteDeclaredConvergeOps(declaredConvergeOps(parsedOps));
26705
27085
  return c.json({
@@ -26712,7 +27092,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
26712
27092
  target: null,
26713
27093
  mode: "ops",
26714
27094
  ops: parsedOps.length,
26715
- note: `${opsNote(parsedOps, opsIr)} ${playheadNote(runState, join49)} ${operatorNote(operator)}`,
27095
+ note: `${opsNote(parsedOps, opsIr)} ${playheadNote(runState, join51)} ${operatorNote(operator)}`,
26716
27096
  // The operator strip's own model: the ConvergeOps this project
26717
27097
  // declares, plus whatever the last status read said. `declared`
26718
27098
  // without a `strip` is the honest starting state — a loop exists and
@@ -26823,7 +27203,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
26823
27203
  srcCompositeEdgesAttached = 0;
26824
27204
  }
26825
27205
  }
26826
- if (!multi && !components && !logical && !opts.lens && !ownKindVia && ir.nodes.length === 0 && !existsSync27(join44(cfg.projectDir, "chant.config.ts"))) {
27206
+ if (!multi && !components && !logical && !opts.lens && !ownKindVia && ir.nodes.length === 0 && !existsSync27(join46(cfg.projectDir, "chant.config.ts"))) {
26827
27207
  return c.json(noProjectError(cfg.projectDir), 404);
26828
27208
  }
26829
27209
  const radial = new URL(c.req.url).searchParams.get("radial") === "1";
@@ -27460,7 +27840,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
27460
27840
  if (!c.req.path.startsWith("/api/") && c.res.ok && !c.res.headers.has("cache-control")) c.res.headers.set("cache-control", "no-cache");
27461
27841
  });
27462
27842
  app.use("/*", serveStatic({ root: rel }));
27463
- app.get("/", serveStatic({ path: join44(rel, "index.html") }));
27843
+ app.get("/", serveStatic({ path: join46(rel, "index.html") }));
27464
27844
  return app;
27465
27845
  }
27466
27846
  async function bootLocalEmulators(dir, who) {
@@ -27489,11 +27869,11 @@ async function bootLocalEmulators(dir, who) {
27489
27869
  async function startServer(cfg) {
27490
27870
  if (cfg.local) cfg.emulators = await bootLocalEmulators(cfg.projectDir, "behold serve --local");
27491
27871
  for (const dir of cfg.projectDirs ?? [cfg.projectDir]) {
27492
- if (existsSync27(join44(dir, "chant.config.ts"))) addRecent(dir);
27872
+ if (existsSync27(join46(dir, "chant.config.ts"))) addRecent(dir);
27493
27873
  }
27494
27874
  const broadcaster = new Broadcaster();
27495
27875
  const frames = new FrameBuffer();
27496
- const runner = new OpRunner({
27876
+ const runner = cfg.public ? new ReadOnlyRunner() : new OpRunner({
27497
27877
  projectDir: cfg.projectDir,
27498
27878
  broadcaster,
27499
27879
  onDone: (opEnv) => captureFrame(cfg.projectDir, opEnv ?? cfg.env, frames, broadcaster)
@@ -27501,7 +27881,7 @@ async function startServer(cfg) {
27501
27881
  if (!cfg.host && process.env.BEHOLD_HOST) cfg.host = process.env.BEHOLD_HOST;
27502
27882
  const app = createApp(cfg, broadcaster, frames, runner);
27503
27883
  const autoSync = cfg.autoSync ?? "off";
27504
- if (!cfg.carveReport && !servedProductRepo(cfg.projectDirs ?? [cfg.projectDir])) {
27884
+ if (!cfg.public && !cfg.carveReport && !servedProductRepo(cfg.projectDirs ?? [cfg.projectDir])) {
27505
27885
  void readoptDispatchedRun(cfg, runner).then((result) => {
27506
27886
  if (result.outcome === "readopted") {
27507
27887
  process.stdout.write(` re-adopted dispatched run ${result.run.runId} (${result.run.workflow}) \u2014 following via gh
@@ -27525,7 +27905,7 @@ async function startServer(cfg) {
27525
27905
  const memberTag = (dir) => (cfg.projectDirs?.length ?? 0) > 1 ? ` [${basename5(dir)}]` : "";
27526
27906
  const onPollDrift = (dir, movedLexicons) => {
27527
27907
  broadcaster.emit("changed", dir);
27528
- if (autoSync === "off") return;
27908
+ if (autoSync === "off" || cfg.public) return;
27529
27909
  const guarded = servedProductRepo(cfg.projectDirs ?? [cfg.projectDir]);
27530
27910
  if (guarded) {
27531
27911
  broadcaster.emit("op", `\u27F3 auto-sync (${autoSync})${memberTag(dir)} declined: ${productRefusal(guarded, "auto-sync").error}`);
@@ -27634,7 +28014,8 @@ async function startServer(cfg) {
27634
28014
  `behold \u2192 http://localhost:${info.port}
27635
28015
  project: ${cfg.projectDir}${cfg.env ? ` env: ${cfg.env}` : ""}${auto}${localTag}
27636
28016
  read-only, watching for edits${poll}. lanes: /lanes. Ctrl-C to stop.
27637
- `
28017
+ ` + (cfg.public ? ` public: GET, HEAD and OPTIONS only; no op runner; answers ${["localhost", ...allowedHostsFrom(cfg.allowedHosts ?? [])].join(", ")}.
28018
+ ` : "")
27638
28019
  );
27639
28020
  void detectProject(cfg.projectDir).then(({ environments, lexicons }) => {
27640
28021
  const envs = environments.length ? environments.join(", ") : "(none declared \u2014 env picker shows only source)";
@@ -27662,8 +28043,8 @@ init_chant();
27662
28043
 
27663
28044
  // src/workspace-convert.ts
27664
28045
  init_chant();
27665
- import { existsSync as existsSync28, mkdirSync as mkdirSync9, readFileSync as readFileSync33, renameSync as renameSync2, writeFileSync as writeFileSync7 } from "node:fs";
27666
- import { basename as basename6, dirname as dirname19, join as join45, relative as relative11, resolve as resolve31, sep as sep10 } from "node:path";
28046
+ import { existsSync as existsSync28, mkdirSync as mkdirSync9, readFileSync as readFileSync35, renameSync as renameSync2, writeFileSync as writeFileSync7 } from "node:fs";
28047
+ import { basename as basename6, dirname as dirname19, join as join47, relative as relative11, resolve as resolve31, sep as sep10 } from "node:path";
27667
28048
  import { shortStackNames as shortStackNames2 } from "@intentius/pinhole";
27668
28049
  init_project();
27669
28050
  init_terraform_member();
@@ -27681,10 +28062,10 @@ function unique(name, taken) {
27681
28062
  var posix4 = (p) => p.split(sep10).join("/");
27682
28063
  function installedVersion(pkg, dir) {
27683
28064
  for (let at2 = resolve31(dir); ; at2 = dirname19(at2)) {
27684
- const manifest = join45(at2, "node_modules", ...pkg.split("/"), "package.json");
28065
+ const manifest = join47(at2, "node_modules", ...pkg.split("/"), "package.json");
27685
28066
  if (existsSync28(manifest)) {
27686
28067
  try {
27687
- const v = JSON.parse(readFileSync33(manifest, "utf8")).version;
28068
+ const v = JSON.parse(readFileSync35(manifest, "utf8")).version;
27688
28069
  return typeof v === "string" ? v : void 0;
27689
28070
  } catch {
27690
28071
  return void 0;
@@ -27695,7 +28076,7 @@ function installedVersion(pkg, dir) {
27695
28076
  }
27696
28077
  function planConversion(rootArg) {
27697
28078
  const root = resolve31(rootArg);
27698
- const existing = DECLARATION_FILES.find((f) => existsSync28(join45(root, f)));
28079
+ const existing = DECLARATION_FILES.find((f) => existsSync28(join47(root, f)));
27699
28080
  if (existing) return { ok: false, error: `${root} already has ${existing}; there is nothing to convert.` };
27700
28081
  const shape = detectProjectShape(root);
27701
28082
  if (shape.kind !== "estate" || !shape.members?.length || shape.membersFrom === "itself") {
@@ -27716,10 +28097,10 @@ function planConversion(rootArg) {
27716
28097
  const scan = discoverTerraformRoots(abs[i]);
27717
28098
  for (const r of scan.roots) {
27718
28099
  const name2 = unique(declarationName(r.name), taken);
27719
- members.push({ name: name2, dir: r.dir === "." ? dir : posix4(join45(dir, r.dir)), kind: "terraform" });
28100
+ members.push({ name: name2, dir: r.dir === "." ? dir : posix4(join47(dir, r.dir)), kind: "terraform" });
27720
28101
  moves.push({ from: `${short}/${r.name}/`, to: `${name2}/${name2}/` });
27721
28102
  }
27722
- for (const s of scan.skipped) notes.push(`${posix4(join45(dir, s.dir))} is not declared: ${s.why}.`);
28103
+ for (const s of scan.skipped) notes.push(`${posix4(join47(dir, s.dir))} is not declared: ${s.why}.`);
27723
28104
  if (scan.roots.length > 1) notes.push(`${dir} held ${scan.roots.length} Terraform roots; each is its own member now, since a chant terraform member is one root.`);
27724
28105
  return;
27725
28106
  }
@@ -27762,7 +28143,7 @@ function moveLayoutIds(file, moves) {
27762
28143
  return { version: 1, lenses };
27763
28144
  }
27764
28145
  function applyConversion(plan) {
27765
- const declarationFile = join45(plan.root, DECLARATION_FILES[0]);
28146
+ const declarationFile = join47(plan.root, DECLARATION_FILES[0]);
27766
28147
  if (existsSync28(declarationFile)) throw new Error(`${declarationFile} appeared while converting; nothing was written`);
27767
28148
  writeFileSync7(declarationFile, `${JSON.stringify(plan.declaration, null, 2)}
27768
28149
  `);
@@ -27773,7 +28154,7 @@ function applyConversion(plan) {
27773
28154
  else if (existsSync28(target)) layout = { skipped: `${target} already exists; the layout in ${plan.layoutFrom} was left alone` };
27774
28155
  else {
27775
28156
  const moved = moveLayoutIds(readLayoutFile(dirname19(dirname19(plan.layoutFrom))), plan.moves);
27776
- mkdirSync9(join45(plan.root, LAYOUT_DIR), { recursive: true });
28157
+ mkdirSync9(join47(plan.root, LAYOUT_DIR), { recursive: true });
27777
28158
  const tmp = `${target}.tmp`;
27778
28159
  writeFileSync7(tmp, `${JSON.stringify(moved, null, 2)}
27779
28160
  `);
@@ -27784,9 +28165,9 @@ function applyConversion(plan) {
27784
28165
  }
27785
28166
 
27786
28167
  // src/export.ts
27787
- import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync8, copyFileSync, cpSync as cpSync2, readFileSync as readFileSync34, readdirSync as readdirSync14, existsSync as existsSync29, realpathSync as realpathSync2 } from "node:fs";
28168
+ import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync8, copyFileSync, cpSync as cpSync2, readFileSync as readFileSync36, readdirSync as readdirSync14, existsSync as existsSync29, realpathSync as realpathSync2 } from "node:fs";
27788
28169
  import { homedir as homedir3 } from "node:os";
27789
- import { join as join46, dirname as dirname20, basename as basename7, relative as relative12, resolve as resolve32, sep as sep11 } from "node:path";
28170
+ import { join as join48, dirname as dirname20, basename as basename7, relative as relative12, resolve as resolve32, sep as sep11 } from "node:path";
27790
28171
  import { createHash as createHash11 } from "node:crypto";
27791
28172
  import { fileURLToPath as fileURLToPath11 } from "node:url";
27792
28173
  var LENS_PARAMS = ["collapse", "components", "detail", "env", "logical", "ops", "radial", "stacks", "tier"];
@@ -27843,24 +28224,24 @@ function captureKeys(axes) {
27843
28224
  return [...keys];
27844
28225
  }
27845
28226
  function webDir() {
27846
- return join46(dirname20(fileURLToPath11(import.meta.url)), "..", "web");
28227
+ return join48(dirname20(fileURLToPath11(import.meta.url)), "..", "web");
27847
28228
  }
27848
28229
  function pkgRoot2() {
27849
- return join46(dirname20(fileURLToPath11(import.meta.url)), "..");
28230
+ return join48(dirname20(fileURLToPath11(import.meta.url)), "..");
27850
28231
  }
27851
28232
  var NOTICE_FILES = ["LICENSE", "THIRD_PARTY.md"];
27852
28233
  function copyNotices(outDir, root = pkgRoot2()) {
27853
28234
  const copied = [];
27854
28235
  for (const f of NOTICE_FILES) {
27855
- if (!existsSync29(join46(root, f))) continue;
27856
- copyFileSync(join46(root, f), join46(outDir, f));
28236
+ if (!existsSync29(join48(root, f))) continue;
28237
+ copyFileSync(join48(root, f), join48(outDir, f));
27857
28238
  copied.push(f);
27858
28239
  }
27859
- const lic = join46(root, "licenses");
28240
+ const lic = join48(root, "licenses");
27860
28241
  if (existsSync29(lic)) {
27861
- mkdirSync10(join46(outDir, "licenses"), { recursive: true });
28242
+ mkdirSync10(join48(outDir, "licenses"), { recursive: true });
27862
28243
  for (const f of readdirSync14(lic)) {
27863
- copyFileSync(join46(lic, f), join46(outDir, "licenses", f));
28244
+ copyFileSync(join48(lic, f), join48(outDir, "licenses", f));
27864
28245
  copied.push(`licenses/${f}`);
27865
28246
  }
27866
28247
  }
@@ -27885,7 +28266,7 @@ async function runExport(cfg, outDir, opts = {}) {
27885
28266
  ...proj.sqlYodeler ? { sqlYodeler: true } : {}
27886
28267
  };
27887
28268
  const scrub = exportScrubber(cfg);
27888
- const snapDir = join46(outDir, "snapshots");
28269
+ const snapDir = join48(outDir, "snapshots");
27889
28270
  mkdirSync10(snapDir, { recursive: true });
27890
28271
  const keyToFile = {};
27891
28272
  let ok = 0;
@@ -27894,7 +28275,7 @@ async function runExport(cfg, outDir, opts = {}) {
27894
28275
  const res = await app.request(`${key}${key.includes("?") ? "&" : "?"}layout=1`);
27895
28276
  const body = scrub(shapeSnapshot(key, await res.text(), opts));
27896
28277
  const file = slug3(key, body);
27897
- writeFileSync8(join46(snapDir, file), body);
28278
+ writeFileSync8(join48(snapDir, file), body);
27898
28279
  keyToFile[key] = `snapshots/${file}`;
27899
28280
  if (res.ok) ok++;
27900
28281
  else failed++;
@@ -27916,29 +28297,29 @@ async function runExport(cfg, outDir, opts = {}) {
27916
28297
  axes,
27917
28298
  keyToFile
27918
28299
  };
27919
- writeFileSync8(join46(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
27920
- const html = readFileSync34(join46(webDir(), "index.html"), "utf8").replace(
28300
+ writeFileSync8(join48(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
28301
+ const html = readFileSync36(join48(webDir(), "index.html"), "utf8").replace(
27921
28302
  /<\/head>/i,
27922
28303
  ` <script>window.__BEHOLD_STATIC__ = true;</script>
27923
28304
  </head>`
27924
28305
  );
27925
- writeFileSync8(join46(outDir, "index.html"), html);
28306
+ writeFileSync8(join48(outDir, "index.html"), html);
27926
28307
  for (const f of readdirSync14(webDir())) {
27927
28308
  if (f === "index.html" || f.endsWith(".test.js")) continue;
27928
- cpSync2(join46(webDir(), f), join46(outDir, f), { recursive: true });
28309
+ cpSync2(join48(webDir(), f), join48(outDir, f), { recursive: true });
27929
28310
  }
27930
- writeFileSync8(join46(outDir, "README.md"), BUNDLE_README);
28311
+ writeFileSync8(join48(outDir, "README.md"), BUNDLE_README);
27931
28312
  copyNotices(outDir);
27932
28313
  const name = workerName(cfg.projectDir, opts.name);
27933
28314
  writeFileSync8(
27934
- join46(outDir, "wrangler.jsonc"),
28315
+ join48(outDir, "wrangler.jsonc"),
27935
28316
  JSON.stringify(
27936
28317
  { $schema: "node_modules/wrangler/config-schema.json", name, compatibility_date: "2025-06-01", assets: { directory: "." } },
27937
28318
  null,
27938
28319
  2
27939
28320
  ) + "\n"
27940
28321
  );
27941
- writeFileSync8(join46(outDir, "manifest.json"), JSON.stringify({ ...manifest, files: bundleFiles(outDir).filter((f) => f !== "manifest.json") }, null, 2));
28322
+ writeFileSync8(join48(outDir, "manifest.json"), JSON.stringify({ ...manifest, files: bundleFiles(outDir).filter((f) => f !== "manifest.json") }, null, 2));
27942
28323
  process.stdout.write(
27943
28324
  `behold export \u2192 ${outDir}
27944
28325
  ${ok} snapshots${failed ? ` (${failed} endpoint error(s) captured as-is)` : ""}
@@ -27947,11 +28328,11 @@ async function runExport(cfg, outDir, opts = {}) {
27947
28328
  `
27948
28329
  );
27949
28330
  }
27950
- var isRecord20 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
28331
+ var isRecord21 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
27951
28332
  function dropSource(v) {
27952
28333
  if (Array.isArray(v)) return v.forEach(dropSource);
27953
- if (!isRecord20(v)) return;
27954
- if (isRecord20(v.attrs)) delete v.attrs.source;
28334
+ if (!isRecord21(v)) return;
28335
+ if (isRecord21(v.attrs)) delete v.attrs.source;
27955
28336
  for (const x of Object.values(v)) dropSource(x);
27956
28337
  }
27957
28338
  function shapeSnapshot(key, body, opts = {}) {
@@ -27961,7 +28342,7 @@ function shapeSnapshot(key, body, opts = {}) {
27961
28342
  } catch {
27962
28343
  return body;
27963
28344
  }
27964
- if (!isRecord20(doc)) return body;
28345
+ if (!isRecord21(doc)) return body;
27965
28346
  if (key === "/api/project") {
27966
28347
  delete doc.recents;
27967
28348
  delete doc.approver;
@@ -28031,11 +28412,11 @@ function manifestFiles(text) {
28031
28412
  } catch {
28032
28413
  return [];
28033
28414
  }
28034
- const map = isRecord20(doc) && isRecord20(doc.keyToFile) ? doc.keyToFile : {};
28415
+ const map = isRecord21(doc) && isRecord21(doc.keyToFile) ? doc.keyToFile : {};
28035
28416
  return Object.values(map).filter((f) => typeof f === "string" && /^snapshots\/[A-Za-z0-9=_.-]+$/.test(f) && !f.includes(".."));
28036
28417
  }
28037
28418
  function bundleFiles(outDir) {
28038
- return readdirSync14(outDir, { recursive: true, withFileTypes: true }).filter((d) => d.isFile()).map((d) => relative12(outDir, join46(d.parentPath, d.name)).split(sep11).join("/")).sort();
28419
+ return readdirSync14(outDir, { recursive: true, withFileTypes: true }).filter((d) => d.isFile()).map((d) => relative12(outDir, join48(d.parentPath, d.name)).split(sep11).join("/")).sort();
28039
28420
  }
28040
28421
  async function publishBundle(outDir, target, client) {
28041
28422
  const files = bundleFiles(outDir);
@@ -28051,7 +28432,7 @@ async function publishBundle(outDir, target, client) {
28051
28432
  }
28052
28433
  const last = ["manifest.json", "index.html"];
28053
28434
  const order = [...files.filter((f) => !last.includes(f)), ...last.filter((f) => files.includes(f))];
28054
- for (const f of order) await client.put(at2(f), readFileSync34(join46(outDir, f)), contentType(f), CONTENT_ADDRESSED.test(f) ? IMMUTABLE : NO_CACHE);
28435
+ for (const f of order) await client.put(at2(f), readFileSync36(join48(outDir, f)), contentType(f), CONTENT_ADDRESSED.test(f) ? IMMUTABLE : NO_CACHE);
28055
28436
  const wrote = new Set(files);
28056
28437
  const removed = [];
28057
28438
  for (const f of previous) {
@@ -28103,8 +28484,8 @@ It's just files \u2014 GitHub Pages, S3, nginx, or Cloudflare Pages
28103
28484
 
28104
28485
  // src/publish-snapshots.ts
28105
28486
  import { execFileSync as execFileSync4 } from "node:child_process";
28106
- import { readFileSync as readFileSync35 } from "node:fs";
28107
- import { join as join47 } from "node:path";
28487
+ import { readFileSync as readFileSync37 } from "node:fs";
28488
+ import { join as join49 } from "node:path";
28108
28489
  var DEFAULT_KEEP = 30;
28109
28490
  var SNAPSHOT_REPORTS_BASE = "../../../";
28110
28491
  var COMMIT = /^[0-9a-f]{7,64}$/;
@@ -28129,12 +28510,12 @@ function resolveCommit2(dir, given) {
28129
28510
  return { sha };
28130
28511
  }
28131
28512
  }
28132
- var isRecord21 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
28513
+ var isRecord22 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
28133
28514
  function parseHistory(text) {
28134
28515
  const doc = JSON.parse(text);
28135
28516
  if (!Array.isArray(doc)) throw new Error("history.json is not an array");
28136
- const str15 = (v) => typeof v === "string" ? v : "";
28137
- return doc.filter((e) => isRecord21(e) && typeof e.commit === "string" && COMMIT.test(e.commit)).map((e) => ({ commit: e.commit, at: str15(e.at), project: str15(e.project), generated: str15(e.generated) }));
28517
+ const str16 = (v) => typeof v === "string" ? v : "";
28518
+ return doc.filter((e) => isRecord22(e) && typeof e.commit === "string" && COMMIT.test(e.commit)).map((e) => ({ commit: e.commit, at: str16(e.at), project: str16(e.project), generated: str16(e.generated) }));
28138
28519
  }
28139
28520
  function nextHistory(old, entry, keep) {
28140
28521
  const all = [entry, ...old.filter((e) => e.commit !== entry.commit)];
@@ -28146,9 +28527,9 @@ var BUNDLE_PATH = /^[A-Za-z0-9=_.@+-]+(\/[A-Za-z0-9=_.@+-]+)*$/;
28146
28527
  var insideBundle = (f) => typeof f === "string" && BUNDLE_PATH.test(f) && !f.split("/").some((p) => p === "." || p === "..");
28147
28528
  function commitFiles(manifestText) {
28148
28529
  const doc = JSON.parse(manifestText);
28149
- if (!isRecord21(doc)) return ["manifest.json"];
28530
+ if (!isRecord22(doc)) return ["manifest.json"];
28150
28531
  const listed = Array.isArray(doc.files) ? doc.files.filter(insideBundle) : [];
28151
- const snaps = isRecord21(doc.keyToFile) ? Object.values(doc.keyToFile).filter(insideBundle) : [];
28532
+ const snaps = isRecord22(doc.keyToFile) ? Object.values(doc.keyToFile).filter(insideBundle) : [];
28152
28533
  const rest = [.../* @__PURE__ */ new Set([...listed, ...snaps])].filter((f) => f !== "index.html" && f !== "manifest.json").sort();
28153
28534
  return ["index.html", ...rest, "manifest.json"];
28154
28535
  }
@@ -28192,7 +28573,7 @@ async function publishSnapshot(outDir, target, commit, client, opts = {}) {
28192
28573
  const keep = opts.keep ?? DEFAULT_KEEP;
28193
28574
  if (!Number.isInteger(keep) || keep < 1) throw new Error(`--keep takes a whole number of at least 1, not ${keep}`);
28194
28575
  const at2 = (f) => `${target.prefix}/${f}`;
28195
- const local = JSON.parse(readFileSync35(join47(outDir, "manifest.json"), "utf8"));
28576
+ const local = JSON.parse(readFileSync37(join49(outDir, "manifest.json"), "utf8"));
28196
28577
  const generated = local.capturedAt ?? (/* @__PURE__ */ new Date()).toISOString();
28197
28578
  const entry = { commit: sha, at: commit.at ?? generated, project: opts.project ?? local.projectDir ?? "", generated };
28198
28579
  const bundle = await publishBundle(outDir, { bucket: target.bucket, prefix: at2(sha) }, client);
@@ -28260,6 +28641,7 @@ Usage:
28260
28641
  [--yodel-report <file|dir>]
28261
28642
  [--publish s3://<bucket>/<prefix>/views/<name> [--commit <sha>] [--keep <n>]]
28262
28643
  behold serve <project-dir\u2026> [--port <n>] [--host <addr>] [--allow-host <name,\u2026>] [--hud <url>] [--env <name>] [--poll <secs>] [--local]
28644
+ [--public] [--terragucci <src> [--terragucci-project <p>] [--terragucci-poll <secs>]]
28263
28645
  [--yodel-report <file|dir>]
28264
28646
  behold carve <report.json> [--port <n>]
28265
28647
 
@@ -28325,6 +28707,19 @@ Options:
28325
28707
  --allow-host <n,\u2026> serve only: names besides loopback behold answers to, and
28326
28708
  whose pages may write (BEHOLD_ALLOWED_HOSTS too). For a
28327
28709
  proxy that frames behold, or --host 0.0.0.0.
28710
+ --public serve only: a read-only view anyone may load, for a
28711
+ public host (#569). behold builds no op runner, refuses
28712
+ every method but GET, HEAD and OPTIONS (405), and refuses
28713
+ (403) every read that runs a command or uses your
28714
+ credentials and isn't part of the view: the op tools,
28715
+ the live overlay and its plan, diff, reconcile, CI,
28716
+ doctor, substrates, history, workspace gates and why,
28717
+ choudoufu moves. The page shows no control that writes
28718
+ or asks for those. One project directory only; not with
28719
+ --env, --poll, --local or --auto-sync. It still binds
28720
+ 127.0.0.1: put a proxy in front and pass the public name
28721
+ with --allow-host. With --terragucci, the poll defaults
28722
+ to 10 s.
28328
28723
  --hud <url> serve only: where hud reviews this workspace's records
28329
28724
  (BEHOLD_HUD_URL too). A proposed decision in a member's
28330
28725
  or a card's "why" links to its review there (#471).
@@ -28343,7 +28738,8 @@ Options:
28343
28738
  serve with --terragucci only: while a page is open, ask
28344
28739
  whether index.json changed (If-None-Match, or its mtime)
28345
28740
  and fetch chant/lifecycle (the gates and locks) every
28346
- <secs> (default 30), and push what moved to the page.
28741
+ <secs> (default 30, or 10 with --public), and push what
28742
+ moved to the page.
28347
28743
  0 turns it off. The fetch goes into behold's own cache
28348
28744
  (<tmpdir>/behold-lifecycle-*), never into the checkout.
28349
28745
  --yodel-report <file|dir>
@@ -28464,6 +28860,7 @@ ${USAGE}`);
28464
28860
  let terragucciProject;
28465
28861
  let terragucciPoll;
28466
28862
  let yodelReport;
28863
+ let isPublic = false;
28467
28864
  const allowHosts = [];
28468
28865
  for (let i = 0; i < rest.length; i++) {
28469
28866
  const a = rest[i];
@@ -28478,6 +28875,7 @@ ${USAGE}`);
28478
28875
  else if (a === "--env") env = rest[++i];
28479
28876
  else if (a === "--poll") pollSecs = Number(rest[++i]);
28480
28877
  else if (a === "--local") local = true;
28878
+ else if (a === "--public") isPublic = true;
28481
28879
  else if (a === "--auto-sync") {
28482
28880
  const m = rest[++i];
28483
28881
  if (!m || !isAutoSyncMode(m)) {
@@ -28537,6 +28935,23 @@ ${USAGE}`);
28537
28935
  process.stderr.write("behold serve: --terragucci-poll needs --terragucci\n");
28538
28936
  process.exit(2);
28539
28937
  }
28938
+ if (isPublic && projectDirs.length > 1) {
28939
+ process.stderr.write("behold serve: --public serves one project directory\n");
28940
+ process.exit(2);
28941
+ }
28942
+ if (isPublic && local) {
28943
+ process.stderr.write("behold serve: --public can't --local (it boots emulators and deploys to them)\n");
28944
+ process.exit(2);
28945
+ }
28946
+ if (isPublic && (env || pollSecs !== void 0)) {
28947
+ process.stderr.write("behold serve: --public can't --env or --poll (a live read uses your credentials)\n");
28948
+ process.exit(2);
28949
+ }
28950
+ if (isPublic && autoSync !== "off") {
28951
+ process.stderr.write("behold serve: --public can't --auto-sync (it runs Ops)\n");
28952
+ process.exit(2);
28953
+ }
28954
+ if (isPublic && terragucci !== void 0 && terragucciPoll === void 0) terragucciPoll = PUBLIC_TERRAGUCCI_POLL_SECS;
28540
28955
  if (yodelReport !== void 0 && !yodelReport) {
28541
28956
  process.stderr.write("behold serve: --yodel-report needs the file `yodel report --json` wrote, or the directory holding it\n");
28542
28957
  process.exit(2);
@@ -28552,11 +28967,13 @@ ${USAGE}`);
28552
28967
  ...local ? { local: true } : {},
28553
28968
  ...host ? { host } : {},
28554
28969
  ...allowHosts.length ? { allowedHosts: allowHosts } : {},
28970
+ ...isPublic ? { public: true, layoutWrites: false } : {},
28555
28971
  ...target.workspace && hud ? { hud } : {},
28556
28972
  ...yodelReport ? { yodelReport: resolve33(yodelReport) } : {},
28557
28973
  ...terragucci ? { terragucci: { source: terragucci, ...terragucciProject ? { project: terragucciProject } : {}, ...terragucciPoll !== void 0 ? { pollSecs: terragucciPoll } : {} } } : {}
28558
28974
  });
28559
28975
  }
28976
+ var PUBLIC_TERRAGUCCI_POLL_SECS = 10;
28560
28977
  async function serveTarget(dirs, verb) {
28561
28978
  const workspace = dirs.length === 1 && hasDeclaration(dirs[0]) ? await declaredWorkspace(dirs[0], verb) : void 0;
28562
28979
  if (workspace && workspace.members.length > 0) {
@@ -28608,7 +29025,7 @@ function warnIfNotChantProject(dir) {
28608
29025
  if (shape.kind === "estate") {
28609
29026
  process.stderr.write(
28610
29027
  `behold: warning \u2014 ${dir} is an estate root, not a chant project itself.
28611
- Serve its members composed: behold serve ${shape.members.map((m) => join48(dir, m.dir)).join(" ")}
29028
+ Serve its members composed: behold serve ${shape.members.map((m) => join50(dir, m.dir)).join(" ")}
28612
29029
  `
28613
29030
  );
28614
29031
  return;
@@ -28644,7 +29061,7 @@ async function runCarve(rest) {
28644
29061
  process.exit(2);
28645
29062
  }
28646
29063
  const reportPath = resolve33(fileArg);
28647
- const parsed = readCarveReport(reportPath, (p) => readFileSync36(p, "utf8"));
29064
+ const parsed = readCarveReport(reportPath, (p) => readFileSync38(p, "utf8"));
28648
29065
  if (!parsed.ok) {
28649
29066
  process.stderr.write(`behold carve: ${parsed.refusal.error}
28650
29067
  ${parsed.refusal.remedy}
@@ -28710,7 +29127,7 @@ ${check.stdout || check.stderr}
28710
29127
  if (check.code !== 0) process.exitCode = 1;
28711
29128
  }
28712
29129
  async function runDemo(rest) {
28713
- const pkgRoot3 = join48(dirname21(fileURLToPath12(import.meta.url)), "..");
29130
+ const pkgRoot3 = join50(dirname21(fileURLToPath12(import.meta.url)), "..");
28714
29131
  const registry2 = loadDemoRegistry(pkgRoot3);
28715
29132
  let port = 4600;
28716
29133
  let live2 = false;
@@ -28845,7 +29262,7 @@ async function serveCarveDemo(target, carve, port, live2 = false) {
28845
29262
  };
28846
29263
  process.stdout.write("behold demo carve --live \u2192 the tfstate is real now; the advisor reads what terraform wrote.\n");
28847
29264
  }
28848
- if (existsSync30(join48(project, "package.json")) && !existsSync30(join48(project, "node_modules"))) {
29265
+ if (existsSync30(join50(project, "package.json")) && !existsSync30(join50(project, "node_modules"))) {
28849
29266
  process.stdout.write(`behold demo carve \u2192 npm install in ${carve.project}/ (the chant this walkthrough shells)\u2026
28850
29267
  `);
28851
29268
  const code = await spawnStep("npm", ["install"], project);
@@ -28856,7 +29273,7 @@ async function serveCarveDemo(target, carve, port, live2 = false) {
28856
29273
  }
28857
29274
  }
28858
29275
  let degraded;
28859
- if (!existsSync30(join48(target, "node_modules", ...HCL_PARSER_PKG.split("/")))) {
29276
+ if (!existsSync30(join50(target, "node_modules", ...HCL_PARSER_PKG.split("/")))) {
28860
29277
  process.stdout.write(`behold demo carve \u2192 npm install ${HCL_PARSER_PKG} (chant's HCL parser, ~2MB, once)\u2026
28861
29278
  `);
28862
29279
  const code = await spawnStep("npm", ["install", "--no-save", "--no-package-lock", HCL_PARSER_PKG], target);