@wairon/cli 5.1.1-dev.91 → 5.1.1-dev.93

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/index.js CHANGED
@@ -65,7 +65,7 @@ var init_defaults = __esm({
65
65
  copilot: ".github/prompts",
66
66
  codex: ".codex/agents"
67
67
  };
68
- WAIRON_VERSION = "5.1.1-dev.91";
68
+ WAIRON_VERSION = "5.1.1-dev.93";
69
69
  GITHUB_REPO = "SYW-Apps/Waffle-AIron";
70
70
  SUPPORTED_ALIASES = ["wai"];
71
71
  SCAN_EXCLUDE_DIRS = /* @__PURE__ */ new Set([
@@ -212,6 +212,7 @@ __export(fs_exports, {
212
212
  getProjectRootOverride: () => getProjectRootOverride,
213
213
  getRequestParentReach: () => getRequestParentReach,
214
214
  getRequestProjectRoot: () => getRequestProjectRoot,
215
+ getWriteReach: () => getWriteReach,
215
216
  isOutsideRoot: () => isOutsideRoot,
216
217
  listFiles: () => listFiles,
217
218
  listFilesRecursive: () => listFilesRecursive,
@@ -222,7 +223,9 @@ __export(fs_exports, {
222
223
  runWithProjectRoot: () => runWithProjectRoot,
223
224
  setProjectRoot: () => setProjectRoot,
224
225
  writeFile: () => writeFile,
225
- writeFileIfChanged: () => writeFileIfChanged
226
+ writeFileIfChanged: () => writeFileIfChanged,
227
+ writeReachDenials: () => writeReachDenials,
228
+ writeReachPermits: () => writeReachPermits
226
229
  });
227
230
  function ensureDir(dirPath) {
228
231
  fs3.mkdirSync(dirPath, { recursive: true });
@@ -277,6 +280,17 @@ function listFilesRecursive(dirPath, ext) {
277
280
  }
278
281
  return files;
279
282
  }
283
+ function writeReachPermits(reach2, root, subsystemId) {
284
+ const own = subsystemId === null ? void 0 : reach2.subsystems.find((s) => s.subsystemId === subsystemId && sameRoot(s.root, root));
285
+ if (own) return own.writable;
286
+ return !reach2.unwritableRoots.some((r) => sameRoot(r, root));
287
+ }
288
+ function writeReachDenials(reach2, root) {
289
+ return reach2.subsystems.filter((s) => !s.writable && sameRoot(s.root, root));
290
+ }
291
+ function getWriteReach() {
292
+ return requestRootStore.getStore()?.writeReach ?? null;
293
+ }
280
294
  function currentRootBinding() {
281
295
  return requestRootStore.getStore();
282
296
  }
@@ -291,7 +305,8 @@ function runWithProjectBinding(dir, reach2, fn) {
291
305
  parentReach: reach2.parentReach,
292
306
  ...reach2.narrowed ? { narrowed: true } : {},
293
307
  ...reach2.unwritableRoots ? { unwritableRoots: reach2.unwritableRoots.map((r) => path4.resolve(r)) } : {},
294
- ...reach2.hostedLookup ? { hostedLookup: reach2.hostedLookup } : {}
308
+ ...reach2.hostedLookup ? { hostedLookup: reach2.hostedLookup } : {},
309
+ ...reach2.writeReach ? { writeReach: reach2.writeReach } : {}
295
310
  },
296
311
  fn
297
312
  );
@@ -369,7 +384,7 @@ function aiDirAt(root, ...segments) {
369
384
  const base = !fs3.existsSync(waiPath) && fs3.existsSync(waironPath) ? waironPath : waiPath;
370
385
  return path4.join(base, ...segments);
371
386
  }
372
- var fs3, path4, import_node_async_hooks, projectRootOverride, requestRootStore;
387
+ var fs3, path4, import_node_async_hooks, projectRootOverride, sameRoot, requestRootStore;
373
388
  var init_fs = __esm({
374
389
  "src/utils/fs.ts"() {
375
390
  "use strict";
@@ -377,6 +392,7 @@ var init_fs = __esm({
377
392
  path4 = __toESM(require("path"));
378
393
  import_node_async_hooks = require("async_hooks");
379
394
  projectRootOverride = null;
395
+ sameRoot = (a, b) => path4.resolve(a) === path4.resolve(b);
380
396
  requestRootStore = new import_node_async_hooks.AsyncLocalStorage();
381
397
  }
382
398
  });
@@ -3229,6 +3245,12 @@ function typeSourceFiles(type) {
3229
3245
  for (const method2 of type.methods ?? []) add2(method2.sourcePath);
3230
3246
  return files;
3231
3247
  }
3248
+ function relationHealth(status2) {
3249
+ if (status2.uses.some((u) => u.state === "changed" || u.state === "removed")) return "incompatible";
3250
+ if (!status2.reachable || status2.outOfReach || status2.uses.some((u) => u.state === "unavailable" || u.state === "unlocked")) return "unavailable";
3251
+ if (status2.drifted) return "drifted";
3252
+ return "ok";
3253
+ }
3232
3254
  var import_zod8, SpecIdSchema, SpecStatusSchema, BoundaryItemSchema, RequirementItemSchema, DatabaseSpecSchema, DiagramConfigSchema, SURFACE_AUDIENCES, SurfaceAudienceSchema, SystemPublicInterfaceSchema, SystemSpecSchema, PublicInterfaceTypeSchema, PublicInterfaceSchema, TrustedLinkSchema, LintAllowSchema, LintConfigSchema, ExtDataSchema, LifecycleEntrypointSchema, SubsystemSpecSchema, ComponentTypeSchema, PATTERN_TYPES, RETIRED_STEREOTYPES, PortalTypeSchema, DispatchBindingSchema, DurabilitySchema, DependencyClassSchema, PatternRefSchema, EventBindingSchema, ExternalLinkTypeSchema, ExternalLinkSchema, PortalAuthSchemeSchema, PortalAuthSchema, PortalMountSchema, ComponentSpecSchema, BLOCK_NOUNS, HttpMethodSchema, TransportSchema, EndpointSchema, SEMANTIC_GUARANTEES, GuaranteeSchema, MethodParamSchema, FindingDeclarationSchema, MethodSignatureSchema, INTENT_FLOOR_MIN_CHARS, InterfaceSpecSchema, NarrativeStepTypeSchema, LoopKindSchema, SwitchCaseSchema, CatchClauseSchema, ParallelBranchSchema, NarrativeStepSchema, NarrativeDetailSchema, ConformanceTierSchema, MethodImplementationSchema, TechnologySchema, ImplementationSpecSchema, TypeKindSchema, TypeFieldSchema, TypeMethodSchema, InvariantSchema, TypeSpecSchema, SurfaceOriginSchema, SurfaceTypeDefSchema, SurfaceTypeExportSchema, SurfaceContractEntrySchema, SurfaceSnapshotSchema, ExternalLockEntrySchema, ExternalsLockSchema, ParentExcerptSchema, NamedOpenApiSpecSchema, GroupSpecSchema;
3233
3255
  var init_specs = __esm({
3234
3256
  "src/models/specs.ts"() {
@@ -5585,12 +5607,16 @@ function memberProjectNodes() {
5585
5607
  trustedLinks: [],
5586
5608
  project: true
5587
5609
  }));
5588
- const referenced = (family.nodes.find((n) => n.namespace === "")?.externals ?? []).filter((e) => e.role === "member").map((e) => ({ id: e.alias, name: e.alias, description: "", trustedLinks: [], project: true, storage: storageBadge(e.sourceKind === "family" ? "contained" : e.sourceKind, e.directory, e.commit) }));
5589
- return [...contained, ...referenced];
5610
+ const rootExternals = family.nodes.find((n) => n.namespace === "")?.externals ?? [];
5611
+ const referenced = rootExternals.filter((e) => e.role === "member").map((e) => ({ id: e.alias, name: e.alias, description: "", trustedLinks: [], project: true, storage: storageBadge(e.sourceKind === "family" ? "contained" : e.sourceKind, e.directory, e.commit) }));
5612
+ const taken = new Set([...contained, ...referenced].map((n) => n.id));
5613
+ const external = rootExternals.filter((e) => e.role !== "member" && e.sourceKind !== "family" && !taken.has(e.alias)).map((e) => ({ id: e.alias, name: e.alias, description: e.project === e.alias ? "" : `project ${e.project}`, trustedLinks: [], project: true, external: true, storage: storageBadge(e.sourceKind, e.directory, e.commit) }));
5614
+ return [...contained, ...referenced, ...external];
5590
5615
  }
5591
5616
  function storageBadge(storage, directory, commit3) {
5592
5617
  if (storage === "git") return `git ${commit3 ? commit3.slice(0, 7) : ""}`.trim();
5593
5618
  if (storage === "hosted") return "hosted";
5619
+ if (storage === "unresolved") return "unresolved";
5594
5620
  if (storage === "path") return directory ? `../${directory.split(/[\\/]/).pop()}` : "sibling";
5595
5621
  return "contained";
5596
5622
  }
@@ -5602,7 +5628,60 @@ function partBadges() {
5602
5628
  }
5603
5629
  return out;
5604
5630
  }
5605
- function buildCanvasModel(issues = []) {
5631
+ function useLabel(use) {
5632
+ if (use.publicName === void 0) return use.member ?? "(the external)";
5633
+ return use.member === void 0 || use.member === "type" ? use.publicName : `${use.publicName}.${use.member}`;
5634
+ }
5635
+ function relationFrom(id, consumer, producer, status2, comparedAt) {
5636
+ const changed = status2.uses.filter((u) => u.state !== "unchanged");
5637
+ const health = relationHealth(status2);
5638
+ return {
5639
+ id,
5640
+ consumer,
5641
+ producer,
5642
+ health,
5643
+ uses: changed.map((u) => ({ name: useLabel(u), state: u.state, ...u.detail !== void 0 ? { detail: u.detail } : {} })),
5644
+ unchanged: status2.uses.length - changed.length,
5645
+ ...status2.pinnedCommit !== void 0 ? { pinnedCommit: status2.pinnedCommit } : {},
5646
+ ...status2.pinnedDigest !== void 0 ? { pinnedDigest: status2.pinnedDigest } : {},
5647
+ comparedAt,
5648
+ ...health === "unavailable" && status2.detail !== void 0 ? { detail: status2.detail } : {}
5649
+ };
5650
+ }
5651
+ function statusFor(entry, consumer, producer) {
5652
+ const { alias, node } = producer;
5653
+ return entry.externals.find((s) => alias !== void 0 && s.alias === alias || node !== void 0 && (node.parent === consumer && s.alias === node.mountAlias || s.sourceKind === "family" && node.id !== void 0 && s.project === node.id));
5654
+ }
5655
+ function nameRelations(edges, relations, outsideIds, ownerOf4) {
5656
+ const family = graph();
5657
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5658
+ const out = /* @__PURE__ */ new Map();
5659
+ for (const edge of edges) {
5660
+ if (!edge.consumption) continue;
5661
+ const consumer = ownerOf4(edge.from);
5662
+ const node = outsideIds.has(edge.to) ? void 0 : familyNode(family, ownerOf4(edge.to)) ?? void 0;
5663
+ const alias = outsideIds.has(edge.to) ? edge.to : void 0;
5664
+ const entry = relations.find((r) => r.project === consumer);
5665
+ const status2 = entry && !entry.detail ? statusFor(entry, consumer, { alias, node }) : void 0;
5666
+ const producer = status2?.alias ?? alias ?? (node?.parent === consumer ? node.mountAlias : void 0) ?? node?.id ?? ownerOf4(edge.to);
5667
+ const id = `${consumer}\u2192${producer}`;
5668
+ if (!out.has(id)) {
5669
+ out.set(id, status2 ? relationFrom(id, consumer, producer, status2, entry.comparedAt) : {
5670
+ id,
5671
+ consumer,
5672
+ producer,
5673
+ health: "unavailable",
5674
+ uses: [],
5675
+ unchanged: 0,
5676
+ comparedAt: entry?.comparedAt ?? now,
5677
+ detail: entry?.detail ?? (entry ? `the consumer's statuses name no producer "${producer}"` : "the consumer's statuses were not read")
5678
+ });
5679
+ }
5680
+ edge.relation = id;
5681
+ }
5682
+ return [...out.values()];
5683
+ }
5684
+ function buildCanvasModel(issues = [], relations) {
5606
5685
  const system = loadSystemSpec();
5607
5686
  const subsystems = loadSubsystemSpecs();
5608
5687
  const components = loadComponentSpecs();
@@ -5710,22 +5789,26 @@ function buildCanvasModel(issues = []) {
5710
5789
  const badges = partBadges();
5711
5790
  const componentSub = new Map(components.map((c) => [c.id, c.subsystem]));
5712
5791
  const edges = [];
5713
- const referencedIds = new Set(projectNodes.filter((p) => p.storage !== void 0).map((p) => p.id));
5792
+ const outsideIds = new Set(projectNodes.filter((p) => p.storage !== void 0).map((p) => p.id));
5793
+ const owners = graph().owners;
5794
+ const ownerOfSpec = (id) => owners.get(id) ?? "";
5714
5795
  for (const comp of components) {
5715
5796
  for (const depId of comp.dependsOn) {
5716
5797
  const alias = depId.includes("::") ? depId.split("::")[0] : "";
5717
- if (referencedIds.has(alias) && !componentIds.has(depId)) {
5718
- edges.push({ from: comp.id, to: alias, cross: true });
5798
+ if (outsideIds.has(alias) && !componentIds.has(depId)) {
5799
+ edges.push({ from: comp.id, to: alias, cross: true, consumption: true });
5719
5800
  continue;
5720
5801
  }
5721
5802
  if (!componentIds.has(depId)) continue;
5722
5803
  edges.push({
5723
5804
  from: comp.id,
5724
5805
  to: depId,
5725
- cross: componentSub.get(depId) !== comp.subsystem
5806
+ cross: componentSub.get(depId) !== comp.subsystem,
5807
+ ...ownerOfSpec(depId) !== ownerOfSpec(comp.id) ? { consumption: true } : {}
5726
5808
  });
5727
5809
  }
5728
5810
  }
5811
+ const canvasRelations2 = relations === void 0 ? void 0 : nameRelations(edges, relations, outsideIds, ownerOfSpec);
5729
5812
  const typeSpecs = loadTypeSpecs();
5730
5813
  const usedByFor = (t) => {
5731
5814
  const qualified = t.subsystem && !t.id.startsWith(`${t.subsystem}::`) ? `${t.subsystem}::${t.id}` : t.id;
@@ -5847,6 +5930,7 @@ function buildCanvasModel(issues = []) {
5847
5930
  ],
5848
5931
  components: modelComponents,
5849
5932
  edges,
5933
+ ...canvasRelations2 !== void 0 ? { relations: canvasRelations2 } : {},
5850
5934
  types: modelTypes,
5851
5935
  typeEdges,
5852
5936
  dataEdges,
@@ -6029,6 +6113,7 @@ body[data-theme="light"] .selectControl { color-scheme:light; }
6029
6113
  #cy { position:absolute; inset:0; }
6030
6114
  .legend { position:absolute; left:12px; bottom:12px; background:var(--chrome); border:1px solid var(--chrome-border); border-radius:10px; padding:8px 12px; font-size:11px; color:var(--dim); z-index:5; pointer-events:none; }
6031
6115
  .legend .sw { display:inline-block; width:10px; height:10px; border-radius:3px; margin-right:4px; vertical-align:-1px; border:1.5px solid; }
6116
+ .reltip { position:absolute; display:none; background:var(--chrome); border:1px solid var(--chrome-border); border-radius:8px; padding:6px 10px; font-size:11px; color:var(--dim); white-space:pre; z-index:6; pointer-events:none; max-width:520px; overflow:hidden; }
6032
6117
  /* Stage overlays clear the floating header (52px + 10px top + 10px gap). The
6033
6118
  header hides in presentation mode, where they return to the top edge. */
6034
6119
  .viewhint { position:absolute; top:72px; left:12px; color:var(--dim); font-size:11px; background:var(--chrome); border:1px solid var(--chrome-border); border-radius:9px; padding:5px 10px; z-index:5; pointer-events:none; }
@@ -6214,6 +6299,7 @@ body.presentation #exitPresent, body.presentation #presentDetails { display:bloc
6214
6299
  <div class="viewhint" id="viewHint"></div>
6215
6300
  <div id="typesWarn"></div>
6216
6301
  <div class="legend" id="legend"></div>
6302
+ <div class="reltip" id="relTip"></div>
6217
6303
  </div>
6218
6304
  <div id="panelResizer" title="Drag to resize details sidebar"></div>
6219
6305
  <div id="panel"></div>
@@ -6541,6 +6627,7 @@ var MODEL = __MODEL_JSON__;
6541
6627
  retired: { fill: '#ebe8e4', stroke: '#857b73', text: '#35302b' },
6542
6628
  },
6543
6629
  issue: '#b3261e', selGlow: '#3465b4', bgLabel: '#f2f5f8', png: '#f2f5f8',
6630
+ relOk: '#2e7d4f', relDrift: '#8a6116', relBad: '#b3261e', relUnknown: '#5f6b78',
6544
6631
  },
6545
6632
  syw: {
6546
6633
  pageEdge: '#7c8ca3', edgeText: '#aebdd2', cross: '#ff6b81', ink: '#eef2f8',
@@ -6562,6 +6649,7 @@ var MODEL = __MODEL_JSON__;
6562
6649
  retired: { fill: '#24211f', stroke: '#a8a29e', text: '#e7e5e4' },
6563
6650
  },
6564
6651
  issue: '#ff6b81', selGlow: '#22ddff', bgLabel: '#0a0a0f', png: '#0a0a0f',
6652
+ relOk: '#34d399', relDrift: '#f59e0b', relBad: '#ff6b81', relUnknown: '#93a1b8',
6565
6653
  },
6566
6654
  };
6567
6655
 
@@ -6624,6 +6712,15 @@ var MODEL = __MODEL_JSON__;
6624
6712
  { selector: 'edge.bundle', style: { width: 4.5, opacity: 0.7 } },
6625
6713
  { selector: 'edge.toghost', style: { 'line-style': 'dashed', opacity: 0.75 } },
6626
6714
  { selector: 'edge.inneredge', style: { width: 1.1, 'arrow-scale': 0.6, opacity: 0.8 } },
6715
+ // Relation health on consumption edges: the health colour replaces the
6716
+ // boundary-hop colour, and unavailable is dotted as well as grey, so the
6717
+ // state never rests on colour alone (the glyph is the edge's label).
6718
+ { selector: 'edge.rel-unchecked', style: { 'line-color': t.pageEdge, 'target-arrow-color': t.pageEdge } },
6719
+ { selector: 'edge.rel-ok', style: { 'line-color': t.relOk, 'target-arrow-color': t.relOk } },
6720
+ { selector: 'edge.rel-drifted', style: { 'line-color': t.relDrift, 'target-arrow-color': t.relDrift } },
6721
+ { selector: 'edge.rel-incompatible', style: { 'line-color': t.relBad, 'target-arrow-color': t.relBad } },
6722
+ { selector: 'edge.rel-unavailable', style: { 'line-color': t.relUnknown, 'target-arrow-color': t.relUnknown, 'line-style': 'dotted' } },
6723
+ { selector: '.extProject', style: { 'border-style': 'dashed' } },
6627
6724
  { selector: 'edge.stubHover', style: { 'line-color': t.selGlow, 'target-arrow-color': t.selGlow, width: 2.6, opacity: 1, 'z-compound-depth': 'top' } },
6628
6725
  { selector: '.dimmed', style: { opacity: 0.13 } },
6629
6726
  { selector: '.hasIssue', style: { 'border-color': t.issue, 'border-style': 'dashed', 'border-width': 3 } },
@@ -6660,6 +6757,83 @@ var MODEL = __MODEL_JSON__;
6660
6757
  ];
6661
6758
  }
6662
6759
 
6760
+ // ---- relation health (consumption edges) ----------------------------------
6761
+ // The model carries relations only when health was computed; without them a
6762
+ // consumption edge is drawn neutral and the legend says it was not checked \u2014
6763
+ // never green for something nobody compared.
6764
+ var HAS_RELATIONS = Array.isArray(MODEL.relations);
6765
+ var REL_BY_ID = {};
6766
+ (MODEL.relations || []).forEach(function (r) { REL_BY_ID[r.id] = r; });
6767
+ var HEALTH_RANK = { ok: 1, drifted: 2, unavailable: 3, incompatible: 4 };
6768
+ var HEALTH_GLYPH = { ok: '\\u2713', drifted: '\\u0394', incompatible: '\\u2715', unavailable: '?' };
6769
+ var HAS_CONSUMPTION = MODEL.edges.some(function (e) { return !!e.consumption; });
6770
+ function edgeHealth(edge) {
6771
+ var r = edge.relation ? REL_BY_ID[edge.relation] : null;
6772
+ return r ? r.health : (HAS_RELATIONS ? 'unavailable' : null);
6773
+ }
6774
+ // Where edges fold into one, the worst health wins.
6775
+ function worseHealth(a, b) {
6776
+ if (!a) return b;
6777
+ if (!b) return a;
6778
+ return HEALTH_RANK[b] > HEALTH_RANK[a] ? b : a;
6779
+ }
6780
+ function relationsComparedAt() {
6781
+ var at = null;
6782
+ (MODEL.relations || []).forEach(function (r) { if (!at || r.comparedAt < at) at = r.comparedAt; });
6783
+ return at;
6784
+ }
6785
+ function shortPin(r) {
6786
+ if (r.pinnedCommit) return r.pinnedCommit.slice(0, 7);
6787
+ return r.pinnedDigest ? r.pinnedDigest.replace(/^[a-z0-9-]+:/, '').slice(0, 12) : '';
6788
+ }
6789
+ function relationName(r) { return (r.consumer || 'this project') + ' \\u2192 ' + r.producer; }
6790
+ function relationText(r) {
6791
+ var lines = [relationName(r) + ': ' + HEALTH_GLYPH[r.health] + ' ' + r.health];
6792
+ r.uses.forEach(function (u) { lines.push(' ' + u.name + ' \\u2014 ' + u.state + (u.detail ? ': ' + u.detail : '')); });
6793
+ if (r.unchanged) lines.push(' ' + r.unchanged + ' use' + (r.unchanged === 1 ? '' : 's') + ' unchanged');
6794
+ if (r.detail) lines.push(' ' + r.detail);
6795
+ var pin = shortPin(r);
6796
+ lines.push((pin ? 'pinned ' + pin + ' \\u00B7 ' : '') + 'compared ' + r.comparedAt);
6797
+ return lines.join('\\n');
6798
+ }
6799
+ function relationHtml(r) {
6800
+ var pin = shortPin(r);
6801
+ return '<div class="method"><div class="mname">' + esc(relationName(r)) + '<span class="grow"></span><span class="chip">' + HEALTH_GLYPH[r.health] + ' ' + esc(r.health) + '</span></div>'
6802
+ + r.uses.map(function (u) { return '<div class="mdesc">' + esc(u.name) + ' \\u2014 ' + esc(u.state) + (u.detail ? ': ' + esc(u.detail) : '') + '</div>'; }).join('')
6803
+ + (r.unchanged ? '<div class="mdesc">' + r.unchanged + ' use' + (r.unchanged === 1 ? '' : 's') + ' unchanged</div>' : '')
6804
+ + (r.detail ? '<div class="mdesc">' + esc(r.detail) + '</div>' : '')
6805
+ + '<div class="mdesc">' + (pin ? 'pinned ' + esc(pin) + ' \\u00B7 ' : '') + 'compared ' + esc(r.comparedAt) + '</div></div>';
6806
+ }
6807
+ // The relations whose consumption edges start or land inside a node.
6808
+ function relationsTouching(sid) {
6809
+ var seen = {}, out = [];
6810
+ var inside = function (x) {
6811
+ var c = compById[x];
6812
+ return x === sid || x.indexOf(sid + '::') === 0 || !!(c && (c.subsystem === sid || c.subsystem.indexOf(sid + '::') === 0));
6813
+ };
6814
+ MODEL.edges.forEach(function (e) {
6815
+ if (!e.relation || seen[e.relation] || !(inside(e.to) || inside(e.from))) return;
6816
+ seen[e.relation] = true;
6817
+ if (REL_BY_ID[e.relation]) out.push(REL_BY_ID[e.relation]);
6818
+ });
6819
+ return out;
6820
+ }
6821
+ // The one legend row, only when the model has a consumption edge.
6822
+ function relationLegend(t) {
6823
+ if (!HAS_CONSUMPTION) return '';
6824
+ if (!HAS_RELATIONS) return '<br>relations: <span style="color:' + t.pageEdge + '">health not checked</span>';
6825
+ var at = relationsComparedAt();
6826
+ return '<br>relations: <span style="color:' + t.relOk + '">\\u2713 ok</span>&nbsp; <span style="color:' + t.relDrift + '">\\u0394 drifted</span>&nbsp; '
6827
+ + '<span style="color:' + t.relBad + '">\\u2715 incompatible</span>&nbsp; <span style="color:' + t.relUnknown + '">? unavailable (dotted)</span>'
6828
+ + (at ? '&nbsp; \\u00B7 as of ' + esc(at) : '');
6829
+ }
6830
+ // A project's own page, only through the host's open-project hook (the web
6831
+ // app): never in a standalone file or on a shared page, which have none.
6832
+ function openProjectButton(s) {
6833
+ if (!s.recordId || typeof opts === 'undefined' || !opts || typeof opts.onOpenProject !== 'function') return '';
6834
+ return '<div class="openbtn"><button class="tbtn" data-openproject-id="' + esc(s.recordId) + '">Open project \\u2197</button></div>';
6835
+ }
6836
+
6663
6837
  function renderLegend() {
6664
6838
  var t = THEMES[state.theme];
6665
6839
  var sw = function (c) { return '<span class="sw" style="background:' + c.fill + ';border-color:' + c.stroke + '"></span>'; };
@@ -6673,7 +6847,8 @@ var MODEL = __MODEL_JSON__;
6673
6847
  sw(t.proxyIn) + '\\u21E0 in-port&nbsp; ' + sw(t.proxyOut) + '\\u21E2 out-port&nbsp; \u2014 bold border = published \xB7 ' +
6674
6848
  '<span style="color:' + t.cross + '">red</span> = boundary hop \xB7 double-click = open<br>' +
6675
6849
  'on select: <span style="color:' + t.selGlow + '">\\u2192 depends on</span>&nbsp; <span style="color:' + t.warn + '">\\u2190 used by</span>' +
6676
- (state.dataCoupling ? '&nbsp; \xB7 &nbsp;<span style="color:' + t.typeV.stroke + '">- - \\u25B8 uses models</span>' : '');
6850
+ (state.dataCoupling ? '&nbsp; \xB7 &nbsp;<span style="color:' + t.typeV.stroke + '">- - \\u25B8 uses models</span>' : '') +
6851
+ relationLegend(t);
6677
6852
  }
6678
6853
 
6679
6854
  // ---- view layout ---------------------------------------------------------------
@@ -7140,6 +7315,14 @@ var MODEL = __MODEL_JSON__;
7140
7315
  if (!agg[key]) agg[key] = { src: src, tgt: tgt, n: 0, cross: false, ghost: ghost };
7141
7316
  agg[key].n++;
7142
7317
  if (edge.cross) agg[key].cross = true;
7318
+ if (edge.consumption) {
7319
+ agg[key].consumption = true;
7320
+ agg[key].health = worseHealth(agg[key].health, edgeHealth(edge));
7321
+ if (edge.relation) {
7322
+ var rels = agg[key].rels || (agg[key].rels = []);
7323
+ if (rels.indexOf(edge.relation) < 0) rels.push(edge.relation);
7324
+ }
7325
+ }
7143
7326
  });
7144
7327
  return { agg: agg, ghosts: ghosts };
7145
7328
  }
@@ -7700,7 +7883,7 @@ var MODEL = __MODEL_JSON__;
7700
7883
  var classes, label;
7701
7884
  var isPub = e.kind === 'component' && compById[e.id] && compById[e.id].public;
7702
7885
  if (e.kind === 'subsystem') {
7703
- classes = 'subsysBox';
7886
+ classes = 'subsysBox' + (subById[e.id] && subById[e.id].external ? ' extProject' : '');
7704
7887
  label = nameOf(e) + (e.hasKids && !inner ? '\\n\\u25B8 open' : '');
7705
7888
  } else {
7706
7889
  var c = compById[e.id];
@@ -7908,9 +8091,13 @@ var MODEL = __MODEL_JSON__;
7908
8091
  var bundle = e.n > 1;
7909
8092
  var dim = state.query && (dimmedAnchors[e.src] || dimmedAnchors[e.tgt]);
7910
8093
  var route = routeData(e.src, e.tgt, key);
8094
+ // A consumption edge carries its relation's health: the glyph at its middle,
8095
+ // its colour (and line, dotted when unavailable) by class.
8096
+ var glyph = e.health ? HEALTH_GLYPH[e.health] : '';
8097
+ var relCls = e.consumption ? 'rel rel-' + (e.health || 'unchecked') + ' ' : '';
7911
8098
  eles.push({
7912
- data: { id: 'e' + (i++), source: e.src, target: e.tgt, lbl: bundle ? e.n + ' links' : '', cpDist: route.cpDist, cpWeight: route.cpWeight, taxiTurn: route.taxiTurn },
7913
- classes: 'routed ' + (e.cross ? 'cross ' : '') + (bundle ? 'bundle ' : '') + (e.ghost ? 'toghost ' : '') + (dim ? 'dimmed' : ''),
8099
+ data: { id: 'e' + (i++), source: e.src, target: e.tgt, lbl: (bundle ? e.n + ' links' : '') + (glyph ? (bundle ? ' ' : '') + glyph : ''), rels: (e.rels || []).join('|'), cpDist: route.cpDist, cpWeight: route.cpWeight, taxiTurn: route.taxiTurn },
8100
+ classes: 'routed ' + (e.cross ? 'cross ' : '') + (bundle ? 'bundle ' : '') + (e.ghost ? 'toghost ' : '') + relCls + (dim ? 'dimmed' : ''),
7914
8101
  });
7915
8102
  });
7916
8103
 
@@ -8410,6 +8597,27 @@ var MODEL = __MODEL_JSON__;
8410
8597
  cy.on('mouseover', 'node', function (ev) { setHover(ev.target); });
8411
8598
  cy.on('mouseout', 'node', function (ev) { if (hoveredNode && hoveredNode.id() === ev.target.id()) setHover(null); });
8412
8599
 
8600
+ // Relation health: hovering a consumption edge shows its relation(s) \u2014 which
8601
+ // uses changed or were removed, why one could not be compared, the pin and
8602
+ // when it was compared; tapping it shows the same in the details panel.
8603
+ function relationsOfEdge(edge) {
8604
+ return (edge.data('rels') || '').split('|').filter(Boolean).map(function (rid) { return REL_BY_ID[rid]; }).filter(Boolean);
8605
+ }
8606
+ cy.on('mouseover', 'edge.rel', function (ev) {
8607
+ var tip = document.getElementById('relTip');
8608
+ var rels = relationsOfEdge(ev.target);
8609
+ tip.textContent = rels.length ? rels.map(relationText).join('\\n\\n') : 'relation health not checked';
8610
+ var at = ev.renderedPosition || { x: 0, y: 0 };
8611
+ tip.style.left = (at.x + 12) + 'px';
8612
+ tip.style.top = (at.y + 12) + 'px';
8613
+ tip.style.display = 'block';
8614
+ });
8615
+ cy.on('mouseout', 'edge.rel', function () { document.getElementById('relTip').style.display = 'none'; });
8616
+ cy.on('tap', 'edge.rel', function (ev) {
8617
+ var ids = ev.target.data('rels') || '';
8618
+ if (ids) select('relation', ids, false);
8619
+ });
8620
+
8413
8621
  cy.on('tap', 'node', function (ev) {
8414
8622
  var t = idOf(ev.target);
8415
8623
  if (t.group) return;
@@ -8467,7 +8675,15 @@ var MODEL = __MODEL_JSON__;
8467
8675
  var hasKids = t.kind === 'subsystem'
8468
8676
  ? (childSubsOf(t.id).length + childCompsOf(t.id).length) > 0
8469
8677
  : memberCompsOf(t.id).length > 0;
8470
- if (hasKids) navigateTo(t.kind, t.id);
8678
+ if (hasKids) { navigateTo(t.kind, t.id); return; }
8679
+ // A project node with nothing to drill into (a referenced or external one):
8680
+ // its own page through the host's open-project hook when it carries a
8681
+ // record id, else its details panel \u2014 its storage and its relations.
8682
+ var ps = t.kind === 'subsystem' ? subById[t.id] : null;
8683
+ if (ps && ps.project) {
8684
+ if (ps.recordId && typeof opts !== 'undefined' && opts && typeof opts.onOpenProject === 'function') { opts.onOpenProject(ps.recordId); return; }
8685
+ select('subsystem', t.id, false);
8686
+ }
8471
8687
  });
8472
8688
  cy.on('dragfree', 'node', function () { harvestPositions(); });
8473
8689
 
@@ -9704,7 +9920,7 @@ var MODEL = __MODEL_JSON__;
9704
9920
  cy.nodes().removeClass('sel');
9705
9921
  cy.edges().removeClass('fieldhl');
9706
9922
  if (id) {
9707
- var node = nodeForRef(kind, id);
9923
+ var node = kind === 'relation' ? cy.collection() : nodeForRef(kind, id);
9708
9924
  if (node.length) {
9709
9925
  node.addClass('sel');
9710
9926
  applyFocus(node);
@@ -9874,6 +10090,10 @@ var MODEL = __MODEL_JSON__;
9874
10090
  via.map(function (v) { return chip(v.label, v.kind, v.id); }).join(''), true);
9875
10091
  }
9876
10092
  }
10093
+ } else if (focusKind === 'relation') {
10094
+ var rl = String(focusId).split('|').map(function (rid) { return REL_BY_ID[rid]; }).filter(Boolean);
10095
+ head = '<h2>' + esc(rl.length === 1 ? relationName(rl[0]) : rl.length + ' relations') + '</h2>' + staticChip('relation');
10096
+ body += rl.map(relationHtml).join('');
9877
10097
  } else if (focusKind === 'type') {
9878
10098
  var ty = null;
9879
10099
  MODEL.types.forEach(function (t2) { if (t2.id === focusId) ty = t2; });
@@ -9957,8 +10177,14 @@ var MODEL = __MODEL_JSON__;
9957
10177
  + (scopeFocus ? staticChip('current view') : '')
9958
10178
  + (scopeFocus ? '' : openViewButton('subsystem', s.id, subKids > 0))
9959
10179
  + openApiButton(subsystemExposesApi(s.id), '')
9960
- + openSpecButton('subsystem', s.id);
10180
+ + openSpecButton('subsystem', s.id)
10181
+ + (s.project ? staticChip(s.external ? 'external project' : 'project') : '')
10182
+ + (s.storage ? staticChip(s.storage) : '')
10183
+ + openProjectButton(s);
9961
10184
  body += '<p class="desc">' + esc(s.description) + '</p>';
10185
+ var nodeRels = relationsTouching(s.id);
10186
+ if (nodeRels.length) body += section('Relations', nodeRels.length, nodeRels.map(relationHtml).join(''), true);
10187
+ else if (s.project && HAS_CONSUMPTION && !HAS_RELATIONS) body += section('Relations', null, '<span class="desc">relation health was not checked</span>', true);
9962
10188
  if (s.trustedLinks.length) {
9963
10189
  body += section('Trusted links (fast lanes)', s.trustedLinks.length, s.trustedLinks.map(function (t2) {
9964
10190
  return '<div class="method">' + chip(t2.subsystem, 'subsystem', t2.subsystem) + '<div class="mdesc">' + esc(t2.reason) + '</div></div>';
@@ -10025,6 +10251,14 @@ var MODEL = __MODEL_JSON__;
10025
10251
  });
10026
10252
  })(ospecs[si]);
10027
10253
  }
10254
+ var oprojs = panel.querySelectorAll('[data-openproject-id]');
10255
+ for (var opi = 0; opi < oprojs.length; opi++) {
10256
+ (function (b) {
10257
+ b.addEventListener('click', function () {
10258
+ if (typeof opts !== 'undefined' && opts && typeof opts.onOpenProject === 'function') opts.onOpenProject(b.getAttribute('data-openproject-id'));
10259
+ });
10260
+ })(oprojs[opi]);
10261
+ }
10028
10262
  var flows = panel.querySelectorAll('[data-flow-comp]');
10029
10263
  for (var j = 0; j < flows.length; j++) {
10030
10264
  (function (b) {
@@ -11077,7 +11311,14 @@ function externalStatus(binding, lock) {
11077
11311
  const { external } = binding;
11078
11312
  const entry = lock?.externals[external.alias];
11079
11313
  const pinned = entry !== void 0 && externalsRepository.readSnapshot(external.alias) !== null;
11080
- const base = { alias: external.alias, project: external.project, sourceKind: external.sourceKind, pinned };
11314
+ const base = {
11315
+ alias: external.alias,
11316
+ project: external.project,
11317
+ sourceKind: external.sourceKind,
11318
+ pinned,
11319
+ ...entry !== void 0 ? { pinnedDigest: entry.digest } : {},
11320
+ ...entry?.commit !== void 0 ? { pinnedCommit: entry.commit } : {}
11321
+ };
11081
11322
  const unreadable = external.sourceKind === "unresolved" ? hostedOnly(external) ? external.problem : `the external does not resolve: ${external.problem}` : producerUnreadable(external);
11082
11323
  let live = null;
11083
11324
  let reason = unreadable;
@@ -11361,7 +11602,8 @@ function reachCeiling(root, family) {
11361
11602
  }
11362
11603
  function within2(dir, ceiling, fn) {
11363
11604
  if (ceiling === null) return runWithProjectRoot(dir, fn);
11364
- return runWithProjectBinding(dir, { topRoot: ceiling.topRoot, parentReach: true, narrowed: ceiling.narrowed }, fn);
11605
+ const lookup2 = getHostedLookup();
11606
+ return runWithProjectBinding(dir, { topRoot: ceiling.topRoot, parentReach: true, narrowed: ceiling.narrowed, ...lookup2 ? { hostedLookup: lookup2 } : {} }, fn);
11365
11607
  }
11366
11608
  function depthOf(family, node) {
11367
11609
  let depth = 0;
@@ -11412,7 +11654,8 @@ function referencedOf(nodes) {
11412
11654
  return out;
11413
11655
  }
11414
11656
  function atReferenced(ref, fn) {
11415
- return runWithProjectBinding(ref.directory, { topRoot: ref.directory, parentReach: true, narrowed: true }, fn);
11657
+ const lookup2 = getHostedLookup();
11658
+ return runWithProjectBinding(ref.directory, { topRoot: ref.directory, parentReach: true, narrowed: true, ...lookup2 ? { hostedLookup: lookup2 } : {} }, fn);
11416
11659
  }
11417
11660
  function narrowed(family, selected) {
11418
11661
  const keys = /* @__PURE__ */ new Set(["", ...selected.map((s) => s.node.namespace)]);
@@ -11725,6 +11968,86 @@ function projectApprovals(depth) {
11725
11968
  }
11726
11969
  return entries;
11727
11970
  }
11971
+ function relationReach(root) {
11972
+ const granted = getRequestParentReach();
11973
+ if (!granted) return null;
11974
+ const lookup2 = getHostedLookup();
11975
+ const base = granted.parentReach ? { topRoot: granted.topRoot ?? root, parentReach: true, ...granted.narrowed ? { narrowed: true } : {} } : { topRoot: root, parentReach: true, narrowed: true };
11976
+ return { ...base, ...lookup2 ? { hostedLookup: lookup2 } : {} };
11977
+ }
11978
+ function outsideReach(node) {
11979
+ const lookup2 = getHostedLookup();
11980
+ if (!lookup2 || node.namespace === "") return null;
11981
+ return lookup2(node.id ?? node.namespace) === null ? `the project "${node.id ?? node.namespace}" lies outside this request's reach` : null;
11982
+ }
11983
+ function referenceUse(ref) {
11984
+ return ref.publicName ?? ref.resolved.split("::").pop() ?? ref.authored;
11985
+ }
11986
+ function failureAt(ref, failures) {
11987
+ const site = `${ref.specId} (${ref.position})`;
11988
+ const last = ref.resolved.split("::").pop() ?? "";
11989
+ return failures.find((i) => i.resolution.callSite === site && (i.resolution.canonicalTarget === ref.authored || (i.resolution.canonicalTarget ?? "").endsWith(`::${last}`)));
11990
+ }
11991
+ function containedStatus(child, refs, failures) {
11992
+ const base = { alias: child.mountAlias, project: child.id ?? child.namespace, sourceKind: "family", pinned: false };
11993
+ const unreachable = outsideReach(child);
11994
+ if (unreachable) {
11995
+ const uses2 = [...new Set(refs.map(referenceUse))].map((name) => ({ publicName: name, state: "unavailable", code: "EXTERNAL_CHECK_UNAVAILABLE", detail: unreachable }));
11996
+ return { ...base, reachable: false, stale: false, outOfReach: true, uses: uses2, detail: unreachable };
11997
+ }
11998
+ const uses = [];
11999
+ for (const ref of refs) {
12000
+ const failed = failureAt(ref, failures);
12001
+ const use = failed ? { publicName: referenceUse(ref), state: "removed", code: failed.code, detail: failed.resolution.reason } : { publicName: referenceUse(ref), state: "unchanged" };
12002
+ if (!uses.some((u) => u.publicName === use.publicName && u.state === use.state)) uses.push(use);
12003
+ }
12004
+ return { ...base, reachable: true, stale: uses.some((u) => u.state === "removed"), uses, detail: "a contained member, read live \u2014 nothing is pinned" };
12005
+ }
12006
+ function containedRelations(answered) {
12007
+ const own = graph();
12008
+ const consumed = own.nodes.filter((n) => n.parent === "" && n.mountAlias !== void 0 && !answered.some((s) => s.alias === n.mountAlias)).map((child) => ({
12009
+ child,
12010
+ refs: own.authoredReferences.filter((r) => (own.owners.get(r.specId) ?? "") === "" && r.producer === child.namespace && r.binding !== "local")
12011
+ })).filter((c) => c.refs.length > 0);
12012
+ if (consumed.length === 0) return [];
12013
+ const config = configOrNull();
12014
+ const failures = validateProject({ rules: config?.rules, projectType: config?.projectType }).issues.filter((i) => i.resolution !== void 0 && i.resolution.outcome !== "resolved");
12015
+ return consumed.map(({ child, refs }) => containedStatus(child, refs, failures));
12016
+ }
12017
+ function relationsAt(key, dir, reach2) {
12018
+ const bound2 = (fn) => reach2 === null ? runWithProjectRoot(dir, fn) : runWithProjectBinding(dir, reach2, fn);
12019
+ return bound2(() => {
12020
+ const statuses = getExternalsStatus2();
12021
+ const contained = containedRelations(statuses);
12022
+ return { project: key, externals: [...statuses, ...contained], comparedAt: (/* @__PURE__ */ new Date()).toISOString() };
12023
+ });
12024
+ }
12025
+ function unopened(key, detail) {
12026
+ return { project: key, externals: [], comparedAt: (/* @__PURE__ */ new Date()).toISOString(), detail };
12027
+ }
12028
+ function familyRelations() {
12029
+ const family = graph();
12030
+ const root = path17.resolve(getProjectRoot());
12031
+ const reach2 = relationReach(root);
12032
+ const out = [];
12033
+ for (const node of family.nodes) {
12034
+ const unreachable = outsideReach(node);
12035
+ if (unreachable || !fs12.existsSync(node.directory)) {
12036
+ out.push(unopened(node.namespace, unreachable ?? `its root ${node.directory} does not exist`));
12037
+ continue;
12038
+ }
12039
+ out.push(relationsAt(node.namespace, node.directory, reach2));
12040
+ }
12041
+ for (const ref of referencedOf(family.nodes)) {
12042
+ if (ref.directory === void 0) {
12043
+ out.push(unopened(ref.key, ref.problem ?? "its root could not be located"));
12044
+ continue;
12045
+ }
12046
+ const lookup2 = getHostedLookup();
12047
+ out.push(relationsAt(ref.key, ref.directory, { topRoot: ref.directory, parentReach: true, narrowed: true, ...lookup2 ? { hostedLookup: lookup2 } : {} }));
12048
+ }
12049
+ return out;
12050
+ }
11728
12051
  function requiredBy(key) {
11729
12052
  return key === "" ? "the family root" : `"${key}"`;
11730
12053
  }
@@ -23340,6 +23663,9 @@ function directMemberSubjects() {
23340
23663
  function familyApprovals(depth) {
23341
23664
  return projectApprovals(depth);
23342
23665
  }
23666
+ function familyRelations2() {
23667
+ return familyRelations();
23668
+ }
23343
23669
  var EXCERPT_EPOCH, GRADE_ORDER, NEVER_SUBJECT;
23344
23670
  var init_validation = __esm({
23345
23671
  "src/core/validation.ts"() {
@@ -23367,10 +23693,11 @@ var init_validation = __esm({
23367
23693
  });
23368
23694
 
23369
23695
  // src/core/diagram.ts
23370
- function renderDiagram(format) {
23696
+ function renderDiagram(format, relations) {
23371
23697
  switch (format) {
23698
+ // Only the canvas carries relation health; the other formats ignore it.
23372
23699
  case "canvas":
23373
- return renderCanvasHtml(buildCanvasModel(diagramIssues()));
23700
+ return renderCanvasHtml(buildCanvasModel(diagramIssues(), relations));
23374
23701
  case "mermaid":
23375
23702
  return generateComponentDiagram();
23376
23703
  case "drawio":
@@ -23381,8 +23708,8 @@ function renderDiagram(format) {
23381
23708
  throw new Error(`Unsupported diagram format "${format}" (canvas | mermaid | drawio | excalidraw).`);
23382
23709
  }
23383
23710
  }
23384
- function buildCanvasDataModel() {
23385
- return buildCanvasModel(diagramIssues());
23711
+ function buildCanvasDataModel(relations) {
23712
+ return buildCanvasModel(diagramIssues(), relations);
23386
23713
  }
23387
23714
  function diagramIssues() {
23388
23715
  try {
@@ -24659,6 +24986,28 @@ var init_exports2 = __esm({
24659
24986
  });
24660
24987
 
24661
24988
  // src/core/specs.ts
24989
+ function deniedOwners(reach2, owners) {
24990
+ const seen = /* @__PURE__ */ new Set();
24991
+ const denied = [];
24992
+ for (const o of owners) {
24993
+ const key = `${path23.resolve(o.root)}|${o.subsystemId ?? ""}`;
24994
+ if (seen.has(key)) continue;
24995
+ seen.add(key);
24996
+ if (writeReachPermits(reach2, o.root, o.subsystemId)) continue;
24997
+ const rule = o.subsystemId === null ? void 0 : reach2.subsystems.find((s) => s.subsystemId === o.subsystemId && path23.resolve(s.root) === path23.resolve(o.root));
24998
+ denied.push(o.subsystemId === null ? `project "${o.project}" itself (a spec of no subsystem, decided at the project rung)` : `subsystem "${o.subsystemId}" of project "${o.project}" (decided by ${rule?.decidedBy ?? "the project rung"})`);
24999
+ }
25000
+ return denied;
25001
+ }
25002
+ function assertSpecWritesPermitted(owners, what) {
25003
+ const reach2 = getWriteReach();
25004
+ if (!reach2) return;
25005
+ const denied = deniedOwners(reach2, owners);
25006
+ if (denied.length === 0) return;
25007
+ throw new SubsystemWriteDenied(
25008
+ `SubsystemWriteDenied: ${what} would change ${denied.length === 1 ? "a spec" : "specs"} this request may not change \u2014 ${denied.join("; ")}. Nothing was written.`
25009
+ );
25010
+ }
24662
25011
  function unknownKeysIn(raw, parsed, at, out) {
24663
25012
  if (Array.isArray(raw)) {
24664
25013
  if (!Array.isArray(parsed) || parsed.length !== raw.length) return;
@@ -26375,10 +26724,18 @@ function normalizeReferences(kind, id) {
26375
26724
  function rewriteReferences(kind, id, edits) {
26376
26725
  return current().respellReferences(kind, id, edits);
26377
26726
  }
26727
+ function assertSpecsInReach(targets, what, projectRung = false) {
26728
+ if (!getWriteReach()) return;
26729
+ const ws = current();
26730
+ const writable = ["system", "subsystem", "component", "interface", "implementation", "type"];
26731
+ const owners = targets.map((t) => ws.specOwner(t.kind, t.kind === "system" ? null : (writable.includes(t.kind) ? ws.load(t.kind, t.id) : null) ?? { id: t.id }));
26732
+ if (projectRung) owners.push(ws.specOwner("system", null));
26733
+ assertSpecWritesPermitted(owners, what);
26734
+ }
26378
26735
  function moveMethods(from, to, methods, hooks, dryRun) {
26379
26736
  return current().moveMethods(from, to, methods, hooks, dryRun);
26380
26737
  }
26381
- var fs16, path23, crypto5, PartReadOnly, PART_FORBIDDEN_FIELDS, IMPORTABLE_POSITIONS, SIGNATURE_TTL_MS, COMPONENT_AUTH_SOURCE, STEP_JUMP_FIELDS, STEP_JUMP_LISTS, DELTA_VERBS, JUMP_LABEL_TWINS, LABEL_TWIN_CONTAINERS, SpecWorkspace, DEFAULT_DEPENDENCY_LIMIT, PROSE_FIELDS, DELTA_JUMP_PINS, workspaces, lastServedBinding;
26738
+ var fs16, path23, crypto5, PartReadOnly, SubsystemWriteDenied, PART_FORBIDDEN_FIELDS, IMPORTABLE_POSITIONS, SIGNATURE_TTL_MS, COMPONENT_AUTH_SOURCE, STEP_JUMP_FIELDS, STEP_JUMP_LISTS, DELTA_VERBS, JUMP_LABEL_TWINS, LABEL_TWIN_CONTAINERS, SpecWorkspace, DEFAULT_DEPENDENCY_LIMIT, PROSE_FIELDS, DELTA_JUMP_PINS, workspaces, lastServedBinding;
26382
26739
  var init_specs2 = __esm({
26383
26740
  "src/core/specs.ts"() {
26384
26741
  "use strict";
@@ -26411,6 +26768,12 @@ var init_specs2 = __esm({
26411
26768
  this.name = "PartReadOnly";
26412
26769
  }
26413
26770
  };
26771
+ SubsystemWriteDenied = class extends WaironError {
26772
+ constructor(message) {
26773
+ super(message);
26774
+ this.name = "SubsystemWriteDenied";
26775
+ }
26776
+ };
26414
26777
  PART_FORBIDDEN_FIELDS = ["members", "externals", "rules", "extensions", "composition", "targets"];
26415
26778
  IMPORTABLE_POSITIONS = /* @__PURE__ */ new Set(["dependsOn", "dispatch", "mounts", "narrative"]);
26416
26779
  SIGNATURE_TTL_MS = 2e3;
@@ -27086,6 +27449,92 @@ var init_specs2 = __esm({
27086
27449
  `The spec file ${file} belongs to the part "${held.part.alias}", fetched from git (${held.source?.git ?? "its repository"} at ${held.part.commit ?? "its pinned commit"}): a git part is read-only here. Edit it in its own repository, commit it there, and take it in with \`wairon member update ${held.part.alias}\`. Nothing was written.`
27087
27450
  );
27088
27451
  }
27452
+ /**
27453
+ * spec_registry.save step 5 / delete step 2: inside a hosted request, may the
27454
+ * request change this spec? Judged against the request's write reach with the
27455
+ * home's project root and the spec's owning subsystem — the document written
27456
+ * and, when a stored document is replaced or removed, the one stored (a
27457
+ * component moved out of a subsystem changes that subsystem too). Outside a
27458
+ * hosted request nothing is judged.
27459
+ */
27460
+ assertHomeInReach(file, kind, document) {
27461
+ if (!getWriteReach()) return;
27462
+ const raw = this.homeRootOf(file);
27463
+ const owners = [];
27464
+ if (document !== null) owners.push(this.documentOwner(raw, kind, document));
27465
+ if (pathExists(file)) {
27466
+ try {
27467
+ owners.push(this.documentOwner(raw, kind, readSpecFile(file)));
27468
+ } catch {
27469
+ }
27470
+ }
27471
+ assertSpecWritesPermitted(owners, `writing ${kind} spec ${path23.basename(path23.dirname(file))}/${path23.basename(file)}`);
27472
+ }
27473
+ /** The project root a spec file is homed under: the root whose specs folder, or one of whose parts' folders, holds it (deepest wins). */
27474
+ homeRootOf(file) {
27475
+ this.scanAll();
27476
+ let best;
27477
+ let depth = -1;
27478
+ for (const raw of this.cachedRawRoots) {
27479
+ const homes = [aiPathsAt(raw.dir).specsDir(), ...raw.record.parts.map((p) => p.directory).filter((d) => d !== void 0)];
27480
+ for (const home of homes) {
27481
+ if (isWithin(home, file) && path23.resolve(home).length > depth) {
27482
+ best = raw;
27483
+ depth = path23.resolve(home).length;
27484
+ }
27485
+ }
27486
+ }
27487
+ if (!best) return { dir: this.rootDir, namespace: "", project: path23.basename(this.rootDir) };
27488
+ return { dir: best.dir, namespace: best.record.namespace, project: idOf(best) ?? (best.record.namespace || path23.basename(best.dir)) };
27489
+ }
27490
+ /**
27491
+ * The owner of one spec DOCUMENT as stored in its root (its references local
27492
+ * to that root): a subsystem is its own, a component or type names its
27493
+ * subsystem, an interface or implementation follows its component; the L0, a
27494
+ * system-level type and a type group belong to no subsystem.
27495
+ */
27496
+ documentOwner(root, kind, document) {
27497
+ const doc = document ?? {};
27498
+ const local = (v) => typeof v === "string" && v ? splitNamespace(v).localId : null;
27499
+ const key = (v) => typeof v === "string" && v ? keyIn(root.namespace, splitNamespace(v).localId) : null;
27500
+ let subsystemId = null;
27501
+ if (kind === "subsystem") subsystemId = local(doc.id);
27502
+ else if (kind === "component" || kind === "type") subsystemId = local(doc.subsystem);
27503
+ else if (kind === "interface") subsystemId = this.componentSubsystem(key(doc.component));
27504
+ else if (kind === "implementation") {
27505
+ const contract = key(doc.contract);
27506
+ subsystemId = this.componentSubsystem(contract ? this.loadInterfaceSpec(contract)?.component ?? null : null);
27507
+ }
27508
+ return { root: root.dir, project: root.project, subsystemId };
27509
+ }
27510
+ /** The local id of a component's subsystem, by the component's key; null when no such component is stored. */
27511
+ componentSubsystem(componentKey) {
27512
+ if (!componentKey) return null;
27513
+ const subsystem = this.loadComponentSpec(componentKey)?.subsystem;
27514
+ return subsystem ? splitNamespace(subsystem).localId : null;
27515
+ }
27516
+ /**
27517
+ * The owner of one IN-MEMORY spec (its references keys, as the loader binds
27518
+ * them) — what a multi-spec write (a rename, a method move) judges before its
27519
+ * first write. `staged` answers a spec the write itself creates.
27520
+ */
27521
+ specOwner(kind, spec, staged) {
27522
+ this.scanAll();
27523
+ const id = kind === "system" ? "" : String(spec?.id ?? "");
27524
+ const namespace = (kind === "system" ? "" : this.ownerKeyOf(id)) ?? "";
27525
+ const raw = this.cachedRawRoots.find((r) => r.record.namespace === namespace);
27526
+ const root = raw ? { dir: raw.dir, project: idOf(raw) ?? (namespace || path23.basename(raw.dir)) } : { dir: this.rootDir, project: path23.basename(this.rootDir) };
27527
+ const localOfKey = (v) => v ? splitNamespace(v).localId : null;
27528
+ let subsystemId = null;
27529
+ if (kind === "subsystem") subsystemId = localOfKey(id);
27530
+ else if (kind === "component" || kind === "type") subsystemId = localOfKey(spec?.subsystem);
27531
+ else if (kind === "interface") subsystemId = this.componentSubsystem(spec?.component ?? null);
27532
+ else if (kind === "implementation" && spec?.contract) {
27533
+ const component = this.loadInterfaceSpec(spec.contract)?.component ?? staged?.("interface", spec.contract)?.component ?? null;
27534
+ subsystemId = this.componentSubsystem(component);
27535
+ }
27536
+ return { root: root.dir, project: root.project, subsystemId };
27537
+ }
27089
27538
  /**
27090
27539
  * The key one spec file is approved under (stage 8): its path from the bound
27091
27540
  * root, POSIX — or, for a part's file, `members/<alias>/<path inside the
@@ -27096,13 +27545,15 @@ var init_specs2 = __esm({
27096
27545
  return approvalKeyIn(file, this.rootDir, this.cachedRoots[0]?.parts ?? []);
27097
27546
  }
27098
27547
  /** The file store's write, at a home the writer resolved — a part's folder included, a git part's refused. */
27099
- writeHome(file, document) {
27548
+ writeHome(file, document, kind) {
27100
27549
  this.assertHomeWritable(file);
27550
+ this.assertHomeInReach(file, kind, document);
27101
27551
  writeSpecFile(file, document);
27102
27552
  }
27103
27553
  /** The file store's delete, pruning up to the specs folder the file lives in (its part's, for a part's file). */
27104
- removeHome(file) {
27554
+ removeHome(file, kind) {
27105
27555
  this.assertHomeWritable(file);
27556
+ this.assertHomeInReach(file, kind, null);
27106
27557
  const part = this.partHolding(file)?.part;
27107
27558
  return removeSpecFile(file, part?.directory ? aiPathsAt(part.directory).specsDir() : this.paths.specsDir());
27108
27559
  }
@@ -27453,6 +27904,7 @@ var init_specs2 = __esm({
27453
27904
  }
27454
27905
  saveSystemSpec(spec) {
27455
27906
  const p = this.paths.specsSystem();
27907
+ this.assertHomeInReach(p, "system", spec);
27456
27908
  ensureDir(path23.dirname(p));
27457
27909
  writeSpecFile(p, parseOrThrow(SystemSpecSchema, spec, "system", spec.name));
27458
27910
  invalidateSpecCache();
@@ -27603,12 +28055,12 @@ var init_specs2 = __esm({
27603
28055
  }
27604
28056
  }
27605
28057
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
27606
- this.writeHome(p, parseOrThrow(SubsystemSpecSchema, specToWrite, "subsystem", spec.id));
28058
+ this.writeHome(p, parseOrThrow(SubsystemSpecSchema, specToWrite, "subsystem", spec.id), "subsystem");
27607
28059
  invalidateSpecCache();
27608
28060
  }
27609
28061
  deleteSubsystemSpec(id) {
27610
28062
  const p = this.getSubsystemPath(id);
27611
- if (!this.removeHome(p)) return false;
28063
+ if (!this.removeHome(p, "subsystem")) return false;
27612
28064
  invalidateSpecCache();
27613
28065
  return true;
27614
28066
  }
@@ -27663,14 +28115,14 @@ var init_specs2 = __esm({
27663
28115
  );
27664
28116
  }
27665
28117
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
27666
- this.writeHome(p, parseOrThrow(ComponentSpecSchema, specToWrite, "component", spec.id));
28118
+ this.writeHome(p, parseOrThrow(ComponentSpecSchema, specToWrite, "component", spec.id), "component");
27667
28119
  invalidateSpecCache();
27668
28120
  this.normalizeComponentLayout();
27669
28121
  return notices;
27670
28122
  }
27671
28123
  deleteComponentSpec(id) {
27672
28124
  const p = this.getComponentPath(id);
27673
- if (!this.removeHome(p)) return false;
28125
+ if (!this.removeHome(p, "component")) return false;
27674
28126
  invalidateSpecCache();
27675
28127
  return true;
27676
28128
  }
@@ -27792,13 +28244,13 @@ var init_specs2 = __esm({
27792
28244
  }
27793
28245
  }
27794
28246
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
27795
- this.writeHome(p, parseOrThrow(InterfaceSpecSchema, specToWrite, "interface", spec.id));
28247
+ this.writeHome(p, parseOrThrow(InterfaceSpecSchema, specToWrite, "interface", spec.id), "interface");
27796
28248
  invalidateSpecCache();
27797
28249
  return notices;
27798
28250
  }
27799
28251
  deleteInterfaceSpec(id) {
27800
28252
  const p = this.getInterfacePath(id);
27801
- if (!this.removeHome(p)) return false;
28253
+ if (!this.removeHome(p, "interface")) return false;
27802
28254
  invalidateSpecCache();
27803
28255
  return true;
27804
28256
  }
@@ -27848,13 +28300,13 @@ var init_specs2 = __esm({
27848
28300
  }
27849
28301
  }
27850
28302
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
27851
- this.writeHome(p, parseOrThrow(ImplementationSpecSchema, specToWrite, "implementation", spec.id));
28303
+ this.writeHome(p, parseOrThrow(ImplementationSpecSchema, specToWrite, "implementation", spec.id), "implementation");
27852
28304
  invalidateSpecCache();
27853
28305
  return notices;
27854
28306
  }
27855
28307
  deleteImplementationSpec(id) {
27856
28308
  const p = this.getImplementationPath(id);
27857
- if (!this.removeHome(p)) return false;
28309
+ if (!this.removeHome(p, "implementation")) return false;
27858
28310
  invalidateSpecCache();
27859
28311
  return true;
27860
28312
  }
@@ -27900,14 +28352,14 @@ var init_specs2 = __esm({
27900
28352
  }
27901
28353
  }
27902
28354
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
27903
- this.writeHome(p, parseOrThrow(TypeSpecSchema, specToWrite, "type", spec.id));
28355
+ this.writeHome(p, parseOrThrow(TypeSpecSchema, specToWrite, "type", spec.id), "type");
27904
28356
  invalidateSpecCache();
27905
28357
  return notices;
27906
28358
  }
27907
28359
  deleteTypeSpec(id) {
27908
28360
  const spec = this.loadTypeSpec(id);
27909
28361
  const p = this.getTypePath(id, spec?.subsystem, spec?.group);
27910
- if (!this.removeHome(p)) return false;
28362
+ if (!this.removeHome(p, "type")) return false;
27911
28363
  invalidateSpecCache();
27912
28364
  return true;
27913
28365
  }
@@ -27930,12 +28382,12 @@ var init_specs2 = __esm({
27930
28382
  specToWrite.createdAt = existing.createdAt;
27931
28383
  }
27932
28384
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
27933
- this.writeHome(p, parseOrThrow(GroupSpecSchema, specToWrite, "group", spec.id));
28385
+ this.writeHome(p, parseOrThrow(GroupSpecSchema, specToWrite, "group", spec.id), "group");
27934
28386
  invalidateSpecCache();
27935
28387
  }
27936
28388
  deleteGroupSpec(id) {
27937
28389
  const p = this.getGroupPath(id);
27938
- if (!this.removeHome(p)) return false;
28390
+ if (!this.removeHome(p, "group")) return false;
27939
28391
  invalidateSpecCache();
27940
28392
  return true;
27941
28393
  }
@@ -28133,7 +28585,7 @@ var init_specs2 = __esm({
28133
28585
  );
28134
28586
  }
28135
28587
  if (JSON.stringify(document) === JSON.stringify(stored)) return false;
28136
- this.writeHome(file, document);
28588
+ this.writeHome(file, document, kind);
28137
28589
  invalidateSpecCache();
28138
28590
  return true;
28139
28591
  }
@@ -29047,6 +29499,7 @@ var init_specs2 = __esm({
29047
29499
  }
29048
29500
  }
29049
29501
  const carried = projectConfigRepository.rekeyCarried(rename, true);
29502
+ this.assertMoveInReach(from, to, plan7, carried.length > 0);
29050
29503
  if (dryRun) {
29051
29504
  const edits2 = moveChangeReports([...plan7.edits.values()], true);
29052
29505
  return {
@@ -29086,6 +29539,14 @@ var init_specs2 = __esm({
29086
29539
  carried
29087
29540
  };
29088
29541
  }
29542
+ /** Refuse a method move, naming every subsystem it may not change, before anything is written. */
29543
+ assertMoveInReach(from, to, plan7, rekeysRegister) {
29544
+ if (!getWriteReach()) return;
29545
+ const staged = (kind, id) => plan7.edits.get(`${kind}:${id}`)?.after;
29546
+ const owners = [...plan7.edits.values()].flatMap((e) => [e.before, e.after].filter((s) => s !== null && s !== void 0).map((s) => this.specOwner(e.kind, s, staged)));
29547
+ if (rekeysRegister) owners.push(this.specOwner("system", null));
29548
+ assertSpecWritesPermitted(owners, `moving methods from "${from}" to "${to}"`);
29549
+ }
29089
29550
  /**
29090
29551
  * Rekey every lint allow keyed on a moved method, staging each spec it
29091
29552
  * changes like any other reference the move follows. An allow anchored at a
@@ -31035,7 +31496,7 @@ function restateReExports(from, to) {
31035
31496
  publicInterfaces: entries.map((e) => e.from === from ? { ...e, from: to } : e)
31036
31497
  });
31037
31498
  }
31038
- function rewriteRefFields(specsDir, remap, excludeDir) {
31499
+ function rewriteRefFields(specsDir, remap, excludeDir, dryRun = false) {
31039
31500
  const rewritten = [];
31040
31501
  for (const file of listFilesRecursive(specsDir, ".yaml")) {
31041
31502
  if (excludeDir && isWithinDir2(excludeDir, file)) continue;
@@ -31048,12 +31509,12 @@ function rewriteRefFields(specsDir, remap, excludeDir) {
31048
31509
  if (!raw || typeof raw !== "object") continue;
31049
31510
  const kind = specKind(raw);
31050
31511
  if (!rewriteSpecRefs(raw, remap) || !kind) continue;
31051
- writeYamlFile(file, raw);
31512
+ if (!dryRun) writeYamlFile(file, raw);
31052
31513
  rewritten.push({ kind, id: kind === "system" ? "system" : String(raw.id) });
31053
31514
  }
31054
31515
  return rewritten;
31055
31516
  }
31056
- function rekeyLintAllows(specsDir, rename) {
31517
+ function rekeyLintAllows(specsDir, rename, dryRun = false) {
31057
31518
  const rewritten = [];
31058
31519
  for (const file of listFilesRecursive(specsDir, ".yaml")) {
31059
31520
  let raw;
@@ -31084,7 +31545,7 @@ function rekeyLintAllows(specsDir, rename) {
31084
31545
  }
31085
31546
  }
31086
31547
  if (!changed) continue;
31087
- writeYamlFile(file, raw);
31548
+ if (!dryRun) writeYamlFile(file, raw);
31088
31549
  rewritten.push({ kind, id: specId });
31089
31550
  }
31090
31551
  return rewritten;
@@ -31162,15 +31623,22 @@ function renameComponent(componentId, newId) {
31162
31623
  specs: renamed.map(({ from, to }) => ({ from, to })),
31163
31624
  components: [{ from: componentId, to: newId }]
31164
31625
  };
31165
- projectConfigRepository.rekeyCarried(rename, true);
31166
- const rewrittenRefs = rewriteRefFields(aiPathsAt(getProjectRoot()).specsDir(), (ref, position) => {
31626
+ const registerEdits = projectConfigRepository.rekeyCarried(rename, true);
31627
+ const remap = (ref, position) => {
31167
31628
  if (position === "component" || position === "entity-class" || position === "auth-source") {
31168
31629
  return ref === componentId ? newId : ref;
31169
31630
  }
31170
31631
  if (position === "interface" && ref === movingInterface?.id) return interfaceId;
31171
31632
  return ref;
31172
- });
31173
- const allowsRekeyed = rekeyLintAllows(aiPathsAt(getProjectRoot()).specsDir(), rename);
31633
+ };
31634
+ const specsDir = aiPathsAt(getProjectRoot()).specsDir();
31635
+ assertSpecsInReach(
31636
+ [...renamed.map(({ kind, from }) => ({ kind, id: from })), ...rewriteRefFields(specsDir, remap, void 0, true), ...rekeyLintAllows(specsDir, rename, true)],
31637
+ `renaming component "${componentId}"`,
31638
+ Array.isArray(registerEdits) && registerEdits.length > 0
31639
+ );
31640
+ const rewrittenRefs = rewriteRefFields(specsDir, remap);
31641
+ const allowsRekeyed = rekeyLintAllows(specsDir, rename);
31174
31642
  const rewritten = [...new Set([...rewrittenRefs, ...allowsRekeyed].filter((spec) => !renamed.some((moved) => moved.kind === spec.kind && moved.from === spec.id)).map((spec) => spec.id))];
31175
31643
  invalidateSpecCache();
31176
31644
  moveRenamedSpecs(componentId, newId, component, movingInterface, movingImplementation);
@@ -31248,7 +31716,18 @@ function renameMethod(componentId, methodName, newName, pinSymbol) {
31248
31716
  ]
31249
31717
  }]
31250
31718
  };
31251
- projectConfigRepository.rekeyCarried(rename, true);
31719
+ const registerEdits = projectConfigRepository.rekeyCarried(rename, true);
31720
+ const methodRemap = (ref, position, owner) => position === "method" && owner === componentId && ref === methodName ? newName : ref;
31721
+ const reachDir = aiPathsAt(getProjectRoot()).specsDir();
31722
+ assertSpecsInReach(
31723
+ [
31724
+ ...(rename.methods?.[0]?.specs ?? []).map((id) => ({ kind: moving.some((i) => i.id === id) ? "interface" : "implementation", id })),
31725
+ ...rewriteRefFields(reachDir, methodRemap, void 0, true),
31726
+ ...rekeyLintAllows(reachDir, rename, true)
31727
+ ],
31728
+ `renaming method "${componentId}.${methodName}"`,
31729
+ Array.isArray(registerEdits) && registerEdits.length > 0
31730
+ );
31252
31731
  for (const contract of moving) {
31253
31732
  saveInterfaceSpec({
31254
31733
  ...contract,
@@ -31271,7 +31750,7 @@ function renameMethod(componentId, methodName, newName, pinSymbol) {
31271
31750
  }
31272
31751
  invalidateSpecCache();
31273
31752
  const specsDir = aiPathsAt(getProjectRoot()).specsDir();
31274
- const retargeted = rewriteRefFields(specsDir, (ref, position, owner) => position === "method" && owner === componentId && ref === methodName ? newName : ref);
31753
+ const retargeted = rewriteRefFields(specsDir, methodRemap);
31275
31754
  const allowsRekeyed = rekeyLintAllows(specsDir, rename);
31276
31755
  const rewritten = [...new Set([...retargeted, ...allowsRekeyed].map((spec) => spec.id))];
31277
31756
  const mentions = collectMentions(specsDir, methodName, moving);
@@ -41075,10 +41554,13 @@ function rehearse2(plan7) {
41075
41554
  return { ...plan7, refusals: [...plan7.refusals, { code: `${plan7.request.verb}-refused`, project: "", detail }] };
41076
41555
  }
41077
41556
  const changes = diff(rehearsal);
41078
- const outside = unwritableOwners(changes);
41079
- if (outside.length > 0) {
41557
+ const refused2 = [
41558
+ ...unwritableOwners(changes).map((o) => ({ code: "family-partial", project: o, detail: `the plan must write ${o}, which this request may not write` })),
41559
+ ...deniedSubsystems(changes)
41560
+ ];
41561
+ if (refused2.length > 0) {
41080
41562
  discard(rehearsal);
41081
- return { ...plan7, refusals: [...plan7.refusals, ...outside.map((o) => ({ code: "family-partial", project: o, detail: `the plan must write ${o}, which this request may not write` }))] };
41563
+ return { ...plan7, refusals: [...plan7.refusals, ...refused2] };
41082
41564
  }
41083
41565
  return { ...plan7, rehearsal, changes, relock: relockOf(plan7, changes) };
41084
41566
  }
@@ -41122,6 +41604,16 @@ function unwritableOwners(changes) {
41122
41604
  const owners = [...new Set(changes.map((c) => path48.resolve(c.project)))];
41123
41605
  return owners.filter((o) => denied.includes(o));
41124
41606
  }
41607
+ function deniedSubsystems(changes) {
41608
+ const reach2 = getWriteReach();
41609
+ if (!reach2) return [];
41610
+ const owners = [...new Set(changes.map((c) => path48.resolve(c.project)))];
41611
+ return owners.flatMap((o) => writeReachDenials(reach2, o).map((s) => ({
41612
+ code: "subsystem-denied",
41613
+ project: o,
41614
+ detail: `the plan must change ${o}, which holds subsystem "${s.subsystemId}" this request may not change (decided by ${s.decidedBy}); a family migration reshapes its projects whole, so it needs write on every subsystem of every project it changes`
41615
+ })));
41616
+ }
41125
41617
  function pathProducers(plan7, family) {
41126
41618
  const inside = (dir) => {
41127
41619
  const rel2 = path48.relative(plan7.familyRoot, dir);
@@ -43240,7 +43732,10 @@ var init_server = __esm({
43240
43732
  stale: import_zod11.z.boolean(),
43241
43733
  drifted: import_zod11.z.boolean().optional(),
43242
43734
  uses: import_zod11.z.array(import_zod11.z.record(import_zod11.z.unknown())),
43243
- detail: import_zod11.z.string().optional()
43735
+ detail: import_zod11.z.string().optional(),
43736
+ outOfReach: import_zod11.z.boolean().optional(),
43737
+ pinnedDigest: import_zod11.z.string().optional().describe("The content digest the lock recorded; provenance, never compared here."),
43738
+ pinnedCommit: import_zod11.z.string().optional().describe("The producer commit the lock recorded, for one in another repository; provenance only.")
43244
43739
  })).describe("One status per declared external, in declaration order; EXTERNAL_CHECK_UNAVAILABLE marks what could not be compared."),
43245
43740
  ...staleServerOutput
43246
43741
  };
@@ -47113,12 +47608,16 @@ function parseSequenceRef(ref) {
47113
47608
  }
47114
47609
  return { component: ref.slice(0, sep18), method: ref.slice(sep18 + 1) };
47115
47610
  }
47611
+ function canvasRelations(options) {
47612
+ if (options.health === false) return void 0;
47613
+ return familyRelations2();
47614
+ }
47116
47615
  async function runDiagram(rawOptions = {}) {
47117
47616
  assertProjectInitialized();
47118
47617
  const options = applyFormat(rawOptions);
47119
47618
  if (options.canvas && !options.all) {
47120
47619
  const dest2 = options.out ?? path58.join(AI_PATHS.docsDir(), "diagrams", "canvas.html");
47121
- writeArtifact(dest2, renderDiagram("canvas"));
47620
+ writeArtifact(dest2, renderDiagram("canvas", canvasRelations(options)));
47122
47621
  logger.success(`Interactive canvas written to ${dest2}`);
47123
47622
  logger.info("Open it in a browser \u2014 fully self-contained (works offline).");
47124
47623
  return;
@@ -47140,7 +47639,7 @@ async function runDiagram(rawOptions = {}) {
47140
47639
  const wantsMermaid = options.format?.toLowerCase().startsWith("mermaid") || !!options.subsystem || !!options.sequence;
47141
47640
  if (!options.all && !options.sequence && !wantsMermaid) {
47142
47641
  const dest2 = options.out ?? path58.join(AI_PATHS.docsDir(), "diagrams", "canvas.html");
47143
- writeArtifact(dest2, renderDiagram("canvas"));
47642
+ writeArtifact(dest2, renderDiagram("canvas", canvasRelations(options)));
47144
47643
  logger.success(`Interactive canvas written to ${dest2}`);
47145
47644
  logger.info("Open it in a browser \u2014 fully self-contained (works offline). Other formats: --format mermaid|drawio|excalidraw.");
47146
47645
  return;
@@ -47155,7 +47654,7 @@ async function runDiagram(rawOptions = {}) {
47155
47654
  for (const file of files) {
47156
47655
  writeArtifact(path58.join(outDir, file.relPath), toMarkdown(file));
47157
47656
  }
47158
- writeArtifact(path58.join(outDir, "canvas.html"), renderDiagram("canvas"));
47657
+ writeArtifact(path58.join(outDir, "canvas.html"), renderDiagram("canvas", canvasRelations(options)));
47159
47658
  writeArtifact(path58.join(outDir, "architecture.drawio"), renderDiagram("drawio"));
47160
47659
  writeArtifact(path58.join(outDir, "architecture.excalidraw"), renderDiagram("excalidraw"));
47161
47660
  const graph2 = loadSpecGraph();
@@ -48147,6 +48646,27 @@ function existingProjectRoot(dataDir, id) {
48147
48646
  const rec = records.find((r) => r.id === id);
48148
48647
  return rec ? derivedRoot(records, rec) : null;
48149
48648
  }
48649
+ function declaredSubsystemIds(dataDir, id) {
48650
+ const root = rootOnDisk(dataDir, id);
48651
+ return root ? subsystemIdsAt(root) : null;
48652
+ }
48653
+ function rootOnDisk(dataDir, id) {
48654
+ if (!isValidProjectId(id)) return null;
48655
+ const records = load5(dataDir);
48656
+ const rec = records.find((r) => r.id === id);
48657
+ const root = rec ? derivedRoot(records, rec) : null;
48658
+ return root && fs39.existsSync(root) ? root : null;
48659
+ }
48660
+ function subsystemIdsAt(dir) {
48661
+ return runWithProjectRoot(dir, () => loadSubsystemSpecs()).map((s) => s.id).filter((key) => !key.includes("::"));
48662
+ }
48663
+ function partSubsystemIds(dataDir, id, alias) {
48664
+ const root = rootOnDisk(dataDir, id);
48665
+ const decl = root ? memberDeclarationsOn(root).find((d) => d.alias === alias && d.path && !d.hosted && !d.refused) : void 0;
48666
+ if (!root || !decl?.path) return null;
48667
+ const dir = assertContainedProjectPath(root, decl.path);
48668
+ return fs39.existsSync(dir) ? subsystemIdsAt(dir) : null;
48669
+ }
48150
48670
  function createProjectRecord(dataDir, id) {
48151
48671
  if (!isValidProjectId(id)) {
48152
48672
  throw new Error(`Invalid project id "${id}" (allowed: lowercase letters, digits, hyphen).`);
@@ -49064,6 +49584,23 @@ var fs42 = __toESM(require("fs"));
49064
49584
  var path64 = __toESM(require("path"));
49065
49585
 
49066
49586
  // src/server/types.ts
49587
+ var SUBSYSTEM_SCOPE_SEPARATOR = "/";
49588
+ var SUBSYSTEM_NOT_FOUND = "subsystem not found";
49589
+ function parseSubsystemScope(scopeId) {
49590
+ const at = scopeId.indexOf(SUBSYSTEM_SCOPE_SEPARATOR);
49591
+ if (at <= 0 || at === scopeId.length - 1) return null;
49592
+ return { projectId: scopeId.slice(0, at), subsystemId: scopeId.slice(at + 1) };
49593
+ }
49594
+ function describeDecision(p) {
49595
+ const setting = { "instance-admin": "the instance-admin bypass", user: "a user setting", role: "a role", "everyone-default": "the everyone-default", "instance-default": "the instance default" }[p.source];
49596
+ if (p.source === "instance-admin" || p.source === "instance-default") return setting;
49597
+ if (p.decidedScopeKind === "subsystem" && p.decidedScopeId) {
49598
+ const parsed = parseSubsystemScope(p.decidedScopeId);
49599
+ return `${setting} at subsystem ${parsed?.subsystemId ?? p.decidedScopeId} of project ${parsed?.projectId ?? ""}`.trim();
49600
+ }
49601
+ if (p.decidedScopeKind && p.decidedScopeKind !== "instance") return `${setting} at ${p.decidedScopeKind} ${p.decidedScopeId ?? ""}`.trim();
49602
+ return `${setting} at the instance`;
49603
+ }
49067
49604
  var SSO_ADMIN_ROLE_ID = "sso-admin";
49068
49605
  var UNAUTHENTICATED = {
49069
49606
  tokenId: "",
@@ -49173,6 +49710,7 @@ function listRoles(dataDir) {
49173
49710
  }
49174
49711
 
49175
49712
  // src/server/permission-rules.ts
49713
+ var SUBSYSTEM_CAPABILITY = "project:write";
49176
49714
  var INSTANCE_ROOT = { kind: "instance" };
49177
49715
  var scopeKey = (kind, id) => `${kind}:${id ?? ""}`;
49178
49716
  function isDefinitive(value) {
@@ -49210,7 +49748,13 @@ function familyRootUnits(projectId, world) {
49210
49748
  const placement = world.placements.find((p) => p.projectId === familyRoot);
49211
49749
  return placement ? unitChain(placement.unitId, world) : [];
49212
49750
  }
49213
- function ancestorChain(targetScopeKind, targetScopeId, world) {
49751
+ function ancestorChain(capability, targetScopeKind, targetScopeId, world) {
49752
+ if (targetScopeKind === "subsystem") {
49753
+ const parsed = parseSubsystemScope(targetScopeId);
49754
+ if (!parsed) return [INSTANCE_ROOT];
49755
+ const own = capability === SUBSYSTEM_CAPABILITY ? [{ kind: "subsystem", id: targetScopeId }] : [];
49756
+ return [...own, ...ancestorChain(capability, "project", parsed.projectId, world)];
49757
+ }
49214
49758
  if (targetScopeKind === "project") {
49215
49759
  const projects = projectChain(targetScopeId, world).map((id) => ({ kind: "project", id }));
49216
49760
  return [...projects, ...familyRootUnits(targetScopeId, world), INSTANCE_ROOT];
@@ -49252,7 +49796,8 @@ function subjectIdMatches(subject, assignmentSubjectId) {
49252
49796
  if (assignmentSubjectId === subject.subjectId) return true;
49253
49797
  return (subject.aliasSubjectIds ?? []).includes(assignmentSubjectId);
49254
49798
  }
49255
- function decideScope(settings) {
49799
+ function decideScope(settings, kind) {
49800
+ if (kind === "subsystem") return decideSubsystemScope(settings);
49256
49801
  if (isDefinitive(settings.user)) return { value: settings.user, source: "user" };
49257
49802
  const best = settings.roleValues.reduce(
49258
49803
  (acc, v) => acc === void 0 || grantRank(v) > grantRank(acc) ? v : acc,
@@ -49262,14 +49807,21 @@ function decideScope(settings) {
49262
49807
  if (isDefinitive(settings.everyone)) return { value: settings.everyone, source: "everyone-default" };
49263
49808
  return null;
49264
49809
  }
49810
+ function decideSubsystemScope(settings) {
49811
+ const yesOrNo = (v) => v === "yes" || v === "no";
49812
+ if (yesOrNo(settings.user)) return { value: settings.user, source: "user" };
49813
+ if (settings.roleValues.includes("yes")) return { value: "yes", source: "role" };
49814
+ if (yesOrNo(settings.everyone)) return { value: settings.everyone, source: "everyone-default" };
49815
+ return null;
49816
+ }
49265
49817
  function resolvePermission(subject, capability, targetScopeKind, targetScopeId, world) {
49266
49818
  if (subject.instanceAdmin) {
49267
49819
  return { value: "yes", source: "instance-admin" };
49268
49820
  }
49269
- const chain = ancestorChain(targetScopeKind, targetScopeId, world);
49821
+ const chain = ancestorChain(capability, targetScopeKind, targetScopeId, world);
49270
49822
  for (const scope of chain) {
49271
49823
  const settings = settingsAtScope(subject, capability, scope, world);
49272
- const decision = decideScope(settings);
49824
+ const decision = decideScope(settings, scope.kind);
49273
49825
  if (decision) {
49274
49826
  return {
49275
49827
  value: decision.value,
@@ -49288,7 +49840,21 @@ function allScopes(world) {
49288
49840
  ...projectIds.map((id) => ({ kind: "project", id }))
49289
49841
  ];
49290
49842
  }
49843
+ function namedSubsystemScopes(subject, world) {
49844
+ const ids = [
49845
+ ...world.assignments.filter((a) => a.scopeKind === "subsystem" && a.capability === SUBSYSTEM_CAPABILITY && a.scopeId).map((a) => a.scopeId),
49846
+ ...subject.roleBindings.filter((b) => b.scopeKind === "subsystem" && b.scopeId).map((b) => b.scopeId)
49847
+ ];
49848
+ return [...new Set(ids)].filter((id) => parseSubsystemScope(id) !== null);
49849
+ }
49850
+ function judgedScopes(subject, capability, world) {
49851
+ const subsystems = capability === SUBSYSTEM_CAPABILITY ? namedSubsystemScopes(subject, world).map((id) => ({ kind: "subsystem", id })) : [];
49852
+ return [...allScopes(world), ...subsystems];
49853
+ }
49291
49854
  function ancestorUnitsOf(scope, world) {
49855
+ if (scope.kind === "subsystem") {
49856
+ return familyRootUnits(parseSubsystemScope(scope.id)?.projectId ?? "", world).map((s) => s.id);
49857
+ }
49292
49858
  if (scope.kind === "project") return familyRootUnits(scope.id, world).map((s) => s.id);
49293
49859
  return unitChain(scope.id, world).map((s) => s.id).filter((id) => id !== scope.id);
49294
49860
  }
@@ -49303,7 +49869,7 @@ function resolveVisibleScopes(subject, capability, world) {
49303
49869
  }
49304
49870
  const actionable = /* @__PURE__ */ new Map();
49305
49871
  const context = /* @__PURE__ */ new Map();
49306
- for (const scope of allScopes(world)) {
49872
+ for (const scope of judgedScopes(subject, capability, world)) {
49307
49873
  const effective = resolvePermission(subject, capability, scope.kind, scope.id, world);
49308
49874
  if (effective.value === "yes" || effective.value === "approval") {
49309
49875
  actionable.set(scopeKey(scope.kind, scope.id), {
@@ -49312,6 +49878,15 @@ function resolveVisibleScopes(subject, capability, world) {
49312
49878
  value: effective.value,
49313
49879
  context: false
49314
49880
  });
49881
+ const project2 = scope.kind === "subsystem" ? parseSubsystemScope(scope.id)?.projectId : void 0;
49882
+ if (project2 !== void 0 && !context.has(scopeKey("project", project2))) {
49883
+ context.set(scopeKey("project", project2), {
49884
+ scopeKind: "project",
49885
+ scopeId: project2,
49886
+ value: resolvePermission(subject, capability, "project", project2, world).value,
49887
+ context: true
49888
+ });
49889
+ }
49315
49890
  for (const unitId of ancestorUnitsOf(scope, world)) {
49316
49891
  const key = scopeKey("unit", unitId);
49317
49892
  if (context.has(key)) continue;
@@ -49327,6 +49902,26 @@ function resolveVisibleScopes(subject, capability, world) {
49327
49902
  const contextOnly = [...context.entries()].filter(([key]) => !actionable.has(key)).map(([, scope]) => scope);
49328
49903
  return [...actionable.values(), ...contextOnly];
49329
49904
  }
49905
+ function resolveSubsystemReach(subject, projectId, world) {
49906
+ if (subject.instanceAdmin) return [];
49907
+ const prefix = `${projectId}${SUBSYSTEM_SCOPE_SEPARATOR}`;
49908
+ const applies = (a) => a.subjectKind === "everyone" || a.subjectKind === "user" && subjectIdMatches(subject, a.subjectId);
49909
+ const scopeIds = [
49910
+ ...world.assignments.filter((a) => a.scopeKind === "subsystem" && a.scopeId?.startsWith(prefix) && applies(a)).map((a) => a.scopeId),
49911
+ ...subject.roleBindings.filter((b) => b.scopeKind === "subsystem" && b.scopeId?.startsWith(prefix)).map((b) => b.scopeId)
49912
+ ];
49913
+ const reach2 = [];
49914
+ for (const scopeId of new Set(scopeIds)) {
49915
+ const parsed = parseSubsystemScope(scopeId);
49916
+ if (!parsed || parsed.projectId !== projectId) continue;
49917
+ reach2.push({
49918
+ projectId,
49919
+ subsystemId: parsed.subsystemId,
49920
+ permission: resolvePermission(subject, SUBSYSTEM_CAPABILITY, "subsystem", scopeId, world)
49921
+ });
49922
+ }
49923
+ return reach2;
49924
+ }
49330
49925
 
49331
49926
  // src/server/authorization.ts
49332
49927
  function mergeRoles(stored) {
@@ -49347,15 +49942,20 @@ function gatherPermissionWorld(dataDir) {
49347
49942
  return gatherWorld(dataDir);
49348
49943
  }
49349
49944
  function resolveFamilyReach(dataDir, principal, projectIds) {
49350
- const reach2 = { readable: [], writable: [] };
49945
+ const reach2 = { readable: [], writable: [], subsystems: [] };
49351
49946
  if (!principal.authenticated || !principal.permissionSubject) return reach2;
49352
49947
  const world = gatherPermissionWorld(dataDir);
49353
49948
  for (const id of projectIds) {
49354
49949
  if (resolvePermission(principal.permissionSubject, "project:read", "project", id, world).value === "yes") reach2.readable.push(id);
49355
49950
  if (resolvePermission(principal.permissionSubject, "project:write", "project", id, world).value === "yes") reach2.writable.push(id);
49951
+ reach2.subsystems.push(...resolveSubsystemReach(principal.permissionSubject, id, world));
49356
49952
  }
49357
49953
  return reach2;
49358
49954
  }
49955
+ function explain2(dataDir, subject, capability, scopeKind, scopeId) {
49956
+ const world = gatherPermissionWorld(dataDir);
49957
+ return resolvePermission(subject, capability, scopeKind, scopeId, world);
49958
+ }
49359
49959
  function authorize(dataDir, principal, capability, scopeKind, scopeId) {
49360
49960
  if (!principal.authenticated || !principal.permissionSubject) {
49361
49961
  return { value: "no", source: "instance-default" };
@@ -49841,6 +50441,40 @@ function compare5(users, before, beforeId, after, afterId) {
49841
50441
  }
49842
50442
  return rows;
49843
50443
  }
50444
+ var subjectOfSetting = (subjectKind, subjectId) => subjectKind === "everyone" ? EVERYONE_ELSE : subjectId ?? "";
50445
+ function carryPlan(users, world, parentId, memberId, moved) {
50446
+ const scopes = new Set(moved.map((s) => `${parentId}/${s}`));
50447
+ const bySubject = /* @__PURE__ */ new Map();
50448
+ for (const a of world.assignments) {
50449
+ if (a.scopeKind !== "subsystem" || !scopes.has(a.scopeId ?? "") || a.capability !== "project:write") continue;
50450
+ if (a.value !== "yes" && a.value !== "no") continue;
50451
+ const subject = subjectOfSetting(a.subjectKind, a.subjectId);
50452
+ const held = bySubject.get(subject) ?? { scopes: [], values: [] };
50453
+ held.scopes.push(a.scopeId);
50454
+ held.values.push(a.value);
50455
+ bySubject.set(subject, held);
50456
+ }
50457
+ const own = new Set(world.assignments.filter((a) => a.scopeKind === "project" && a.scopeId === memberId && a.capability === "project:write").map((a) => subjectOfSetting(a.subjectKind, a.subjectId)));
50458
+ const rules = [];
50459
+ for (const [subject, held] of bySubject) {
50460
+ if (own.has(subject)) continue;
50461
+ rules.push({
50462
+ from: [...held.scopes].sort(),
50463
+ to: memberId,
50464
+ subject,
50465
+ kind: "assignment",
50466
+ value: held.values.includes("no") ? "no" : "yes",
50467
+ note: held.scopes.length > 1 ? "most restrictive wins" : "carried"
50468
+ });
50469
+ }
50470
+ for (const user of users) {
50471
+ for (const b of user.roleBindings ?? []) {
50472
+ if (b.scopeKind !== "subsystem" || !scopes.has(b.scopeId ?? "")) continue;
50473
+ rules.push({ from: [b.scopeId], to: memberId, subject: user.subject.userId || user.id, kind: "role", value: b.roleId, note: "not carried (can only narrow)" });
50474
+ }
50475
+ }
50476
+ return rules;
50477
+ }
49844
50478
  function narrowing(key, members) {
49845
50479
  if (!key.projects.some((e) => e.includes("::"))) {
49846
50480
  return null;
@@ -50023,6 +50657,7 @@ function configureSync(periodicSyncMinutes, skipIfClean) {
50023
50657
  // src/server/members.ts
50024
50658
  init_fs();
50025
50659
  init_project();
50660
+ var EVERYONE = "everyone";
50026
50661
  var UPGRADE_AREAS = ["projects.json", "auth/credentials.json"];
50027
50662
  var UPGRADE_VERB = "member-upgrade";
50028
50663
  var OPERATOR = { userId: "operator:host-doctor", kind: "service", issuer: "local" };
@@ -50218,7 +50853,7 @@ function recoverData(dataDir, fix) {
50218
50853
  return found;
50219
50854
  }
50220
50855
  var ROOT_RENAME_REFUSED = "renaming a hosted family root's id is not supported; a member can be renamed, or the root recreated";
50221
- function screen(dataDir, binding, tool, alias, path93) {
50856
+ function screen(dataDir, binding, tool, alias, path93, newId) {
50222
50857
  if (path93 !== void 0 && (tool === "sdd_add_member" || tool === "sdd_attach_member")) {
50223
50858
  const storage = parseMemberSource(path93).storage;
50224
50859
  if (storage === "path" || storage === "git") {
@@ -50236,7 +50871,10 @@ function screen(dataDir, binding, tool, alias, path93) {
50236
50871
  const users = listUsers(dataDir);
50237
50872
  const incoming = standIn(binding.projectId, alias, path93);
50238
50873
  const after = relink(world, [{ projectId: incoming, parentProjectId: binding.projectId }], [], [], []);
50239
- return { reachChanges: compare5(users, world, null, after, incoming).filter((r) => !r.equal) };
50874
+ const reachChanges = compare5(users, world, null, after, incoming).filter((r) => !r.equal);
50875
+ if (tool !== "sdd_promote_member" && tool !== "sdd_externalize_subsystem") return { reachChanges };
50876
+ const moved = tool === "sdd_externalize_subsystem" ? [alias] : partSubsystemIds(dataDir, binding.projectId, alias) ?? [];
50877
+ return { reachChanges, carried: carryPlan(users, world, binding.projectId, newId || alias, moved) };
50240
50878
  }
50241
50879
  function screenDemote(dataDir, binding, alias) {
50242
50880
  const declared = planMemberRecords(dataDir, binding.projectId).find((d) => d.record.parentProjectId === binding.projectId && d.qualifier.endsWith(`${SUBPROJECT_SEPARATOR}${alias}`) && d.action !== "unreadable");
@@ -50271,7 +50909,8 @@ function reconcile(dataDir, principal, projectId, touched) {
50271
50909
  setProjectRecordStatus(dataDir, r.id, "disabled");
50272
50910
  changes.push({ kind: "departed", id: r.id, detail: { parent: r.parentProjectId, memberPath: r.memberPath } });
50273
50911
  }
50274
- const reachChanges = reachOf2(users, world, changes);
50912
+ const carried = carrySubsystemRules(dataDir, listChangedLinks(changes.filter((c) => c.kind === "registered" || c.kind === "returned")));
50913
+ const reachChanges = [...reachOf2(users, world, changes, listAssignments(dataDir)), ...carried];
50275
50914
  const ids = [.../* @__PURE__ */ new Set([...touched, ...changes.map((c) => c.id)])];
50276
50915
  for (const id of ids) auditTouched(dataDir, principal, projectId, id, changes.find((c) => c.id === id), reachChanges);
50277
50916
  const commit3 = publishFamily(dataDir, ids, `wairon: reconcile the family of ${projectId} (${ids.join(", ")})`);
@@ -50294,16 +50933,28 @@ function rekey(dataDir, world, users, m) {
50294
50933
  if (refused2) {
50295
50934
  return { kind: "registered", id: newId, detail: { parent: m.record.parentProjectId, memberPath: m.record.memberPath, rekeyRefused: oldId } };
50296
50935
  }
50297
- const moved = { assignments: world.assignments.filter((a) => a.scopeKind === "project" && a.scopeId === oldId).length };
50298
- remapScope(dataDir, [{ oldId, newId }]);
50299
- remapUnitReferences(dataDir, [{ oldId, newId }], []);
50936
+ const subsystemRemap = subsystemScopesOf(world, users, oldId).map((scopeId) => ({ oldId: scopeId, newId: `${newId}${scopeId.slice(oldId.length)}` }));
50937
+ const moved = {
50938
+ assignments: world.assignments.filter((a) => a.scopeKind === "project" && a.scopeId === oldId).length,
50939
+ subsystemRules: subsystemRemap.map((r) => `${r.oldId} -> ${r.newId}`)
50940
+ };
50941
+ remapScope(dataDir, [{ oldId, newId }, ...subsystemRemap]);
50942
+ remapUnitReferences(dataDir, [{ oldId, newId }, ...subsystemRemap], []);
50300
50943
  const keys = listCredentials(dataDir, oldId).filter((k) => k.projects.includes(oldId) && !k.revokedAt);
50301
50944
  for (const k of keys) renarrowCredential(dataDir, k.id, [...new Set(k.projects.map((e) => e === oldId ? newId : e))]);
50302
50945
  removeProjectRecord(dataDir, oldId);
50303
50946
  return { kind: "renamed", id: newId, previousId: oldId, detail: { ...moved, keys: keys.map((k) => k.id) } };
50304
50947
  }
50948
+ function subsystemScopesOf(world, users, id) {
50949
+ const prefix = `${id}/`;
50950
+ const ids = [
50951
+ ...world.assignments.filter((a) => a.scopeKind === "subsystem" && a.scopeId?.startsWith(prefix)).map((a) => a.scopeId),
50952
+ ...users.flatMap((u) => (u.roleBindings ?? []).filter((b) => b.scopeKind === "subsystem" && b.scopeId?.startsWith(prefix)).map((b) => b.scopeId))
50953
+ ];
50954
+ return [...new Set(ids)];
50955
+ }
50305
50956
  function hasOwnSettings(world, users, id) {
50306
- return world.assignments.some((a) => a.scopeKind === "project" && a.scopeId === id) || users.some((u) => (u.roleBindings ?? []).some((b) => b.scopeKind === "project" && b.scopeId === id));
50957
+ return world.assignments.some((a) => a.scopeKind === "project" && a.scopeId === id) || users.some((u) => (u.roleBindings ?? []).some((b) => b.scopeKind === "project" && b.scopeId === id)) || subsystemScopesOf(world, users, id).length > 0;
50307
50958
  }
50308
50959
  function departedRecords(family, declared, changes) {
50309
50960
  const live = new Set(declared.filter((d) => d.action !== "unreadable").map((d) => d.record.id));
@@ -50311,16 +50962,52 @@ function departedRecords(family, declared, changes) {
50311
50962
  const rekeyed = new Set(changes.filter((c) => c.kind === "renamed").map((c) => c.previousId));
50312
50963
  return family.filter((r) => r.parentProjectId !== void 0 && r.status === "active" && !live.has(r.id) && !rekeyed.has(r.id) && !held.includes(`${r.parentProjectId}/${r.memberPath}`));
50313
50964
  }
50314
- function reachOf2(users, world, changes) {
50965
+ function reachOf2(users, world, changes, assignmentsAfter) {
50315
50966
  const links = listChangedLinks(changes);
50316
50967
  const departed = changes.filter((c) => c.kind === "departed" || c.kind === "retired").map((c) => c.id);
50317
- const after = relink(world, links, departed, [], []);
50968
+ const after = { ...relink(world, links, departed, [], []), assignments: assignmentsAfter };
50318
50969
  return changes.filter((c) => c.kind !== "renamed").flatMap((c) => {
50319
50970
  const before = c.kind === "registered" || c.kind === "returned" ? null : c.id;
50320
50971
  const now = c.kind === "departed" ? null : c.kind === "retired" ? String(c.detail.parent ?? "") || null : c.id;
50321
50972
  return compare5(users, world, before, after, now).filter((r) => !r.equal).map((r) => ({ ...r, memberId: c.id }));
50322
50973
  });
50323
50974
  }
50975
+ function carrySubsystemRules(dataDir, members) {
50976
+ const rows = [];
50977
+ for (const { projectId: memberId, parentProjectId: parentId } of members) {
50978
+ const parentHolds = declaredSubsystemIds(dataDir, parentId) ?? [];
50979
+ const moved = (declaredSubsystemIds(dataDir, memberId) ?? []).filter((s) => !parentHolds.includes(s));
50980
+ if (moved.length === 0) continue;
50981
+ const plan7 = carryPlan(listUsers(dataDir), gatherPermissionWorld(dataDir), parentId, memberId, moved);
50982
+ for (const rule of plan7.filter((r) => r.kind === "assignment")) {
50983
+ const everyone = rule.subject === EVERYONE;
50984
+ setAssignment(dataDir, {
50985
+ id: "",
50986
+ subjectKind: everyone ? "everyone" : "user",
50987
+ ...everyone ? {} : { subjectId: rule.subject },
50988
+ scopeKind: "project",
50989
+ scopeId: memberId,
50990
+ capability: "project:write",
50991
+ value: rule.value,
50992
+ createdAt: ""
50993
+ });
50994
+ rows.push(carriedRow(rule));
50995
+ }
50996
+ }
50997
+ return rows;
50998
+ }
50999
+ function carriedRow(rule) {
51000
+ const source = rule.subject === EVERYONE ? "everyone-default" : "user";
51001
+ const value = rule.value;
51002
+ return {
51003
+ memberId: rule.to,
51004
+ subjectId: rule.subject,
51005
+ capability: "project:write",
51006
+ before: { value, source, decidedScopeKind: "subsystem", decidedScopeId: rule.from.join(", ") },
51007
+ after: { value, source, decidedScopeKind: "project", decidedScopeId: rule.to },
51008
+ equal: true
51009
+ };
51010
+ }
50324
51011
  function listChangedLinks(changes) {
50325
51012
  return changes.filter((c) => c.kind === "registered" || c.kind === "returned" || c.kind === "relocated").map((c) => ({ projectId: c.id, parentProjectId: String(c.detail.parent) }));
50326
51013
  }
@@ -51468,6 +52155,7 @@ init_validation();
51468
52155
  init_errors();
51469
52156
  init_validation();
51470
52157
  init_validation();
52158
+ init_validation();
51471
52159
  function validateAsComplete2() {
51472
52160
  const config = loadProjectConfig();
51473
52161
  if (!config) throw new ProjectNotInitializedError();
@@ -51888,7 +52576,17 @@ function generateDiagram(cfg, credential, project2, format) {
51888
52576
  requireAdmin(credential);
51889
52577
  const root = existingProjectRoot(cfg.dataDir, project2);
51890
52578
  if (!root) throw new Error(`Unknown project "${project2}".`);
51891
- return runWithProjectRoot(root, () => renderDiagram(format));
52579
+ const relations = format === "canvas" ? instanceRelations(cfg, project2, root) : void 0;
52580
+ return runWithProjectRoot(root, () => renderDiagram(format, relations));
52581
+ }
52582
+ function instanceRelations(cfg, project2, root) {
52583
+ const familyRoot = listFamilyRecords(cfg.dataDir, project2)[0];
52584
+ const reach2 = {
52585
+ topRoot: familyRoot?.rootPath || root,
52586
+ parentReach: true,
52587
+ hostedLookup: (id) => existingProjectRoot(cfg.dataDir, id)
52588
+ };
52589
+ return runWithProjectBinding(root, reach2, () => familyRelations2());
51892
52590
  }
51893
52591
  function downloadDiagram(cfg, credential, project2, format) {
51894
52592
  return generateDiagram(cfg, credential, project2, format);
@@ -52437,6 +53135,7 @@ function setAssignment2(cfg, credential, assignment) {
52437
53135
  "the instance-admin marker ('*'@instance) is reserved to the built-in admin account"
52438
53136
  );
52439
53137
  }
53138
+ refuseUnfitSubsystemAssignment(assignment);
52440
53139
  requireAdminOverScope(
52441
53140
  cfg,
52442
53141
  principal,
@@ -52444,8 +53143,9 @@ function setAssignment2(cfg, credential, assignment) {
52444
53143
  assignment.scopeId ?? "",
52445
53144
  "setting an assignment requires project:admin over its scope"
52446
53145
  );
53146
+ const { scopeNote: _label, ...unlabelled } = assignment;
52447
53147
  const stored = setAssignment(cfg.dataDir, {
52448
- ...assignment,
53148
+ ...unlabelled,
52449
53149
  // Canonical grid key: user-kind subjects store the subject's userId, so
52450
53150
  // permission rules' primary match (not the legacy-alias path) serves them.
52451
53151
  ...assignment.subjectKind === "user" && assignment.subjectId ? { subjectId: canonicalSubjectId(cfg, assignment.subjectId) } : {},
@@ -52454,6 +53154,27 @@ function setAssignment2(cfg, credential, assignment) {
52454
53154
  tryAppendAudit(cfg, buildAuditEvent(principal, "permission.set", stored.id));
52455
53155
  return stored;
52456
53156
  }
53157
+ var SUBSYSTEM_VALUES = /* @__PURE__ */ new Set(["yes", "no", "inherit"]);
53158
+ function refuseUnfitSubsystemAssignment(assignment) {
53159
+ if (assignment.scopeKind !== "subsystem") return;
53160
+ const fits = String(assignment.capability) === "project:write" && SUBSYSTEM_VALUES.has(String(assignment.value)) && parseSubsystemScope(assignment.scopeId ?? "") !== null;
53161
+ if (!fits) {
53162
+ throw new ForbiddenError(
53163
+ `a subsystem scope carries only project:write, yes or no (or inherit), at <projectId>/<subsystemId> \u2014 got ${assignment.capability}=${assignment.value} at "${assignment.scopeId ?? ""}"`
53164
+ );
53165
+ }
53166
+ }
53167
+ function refuseUnfitSubsystemBinding(scopeKind, scopeId) {
53168
+ if (scopeKind === "subsystem" && parseSubsystemScope(scopeId ?? "") === null) {
53169
+ throw new ForbiddenError(`a subsystem scope id is <projectId>/<subsystemId> \u2014 got "${scopeId ?? ""}"`);
53170
+ }
53171
+ }
53172
+ function subsystemMissing(cfg, scopeId, cache2) {
53173
+ const parsed = parseSubsystemScope(scopeId);
53174
+ if (!parsed) return true;
53175
+ if (!cache2.has(parsed.projectId)) cache2.set(parsed.projectId, declaredSubsystemIds(cfg.dataDir, parsed.projectId));
53176
+ return !(cache2.get(parsed.projectId) ?? []).includes(parsed.subsystemId);
53177
+ }
52457
53178
  function removeAssignment2(cfg, credential, assignmentId) {
52458
53179
  const principal = requirePrincipal3(cfg, credential);
52459
53180
  const existing = getAssignment(cfg.dataDir, assignmentId);
@@ -52482,13 +53203,15 @@ function listAssignments2(cfg, credential, scopeKind, scopeId, subjectKind, subj
52482
53203
  const subjectIds = subjectKind === "user" && subjectId ? [.../* @__PURE__ */ new Set([subjectId, canonicalSubjectId(cfg, subjectId)])] : void 0;
52483
53204
  const assignments = subjectIds ? subjectIds.flatMap((id) => listAssignments(cfg.dataDir, void 0, subjectKind, id)) : listAssignments(cfg.dataDir, void 0, subjectKind, subjectId);
52484
53205
  const seen = /* @__PURE__ */ new Set();
52485
- return assignments.filter((a) => {
53206
+ const listed = assignments.filter((a) => {
52486
53207
  if (seen.has(a.id)) return false;
52487
53208
  seen.add(a.id);
52488
53209
  if (scopeKind !== void 0 && a.scopeKind !== scopeKind) return false;
52489
53210
  if (scopeId !== void 0 && (a.scopeId ?? "") !== scopeId) return false;
52490
53211
  return true;
52491
53212
  });
53213
+ const trees = /* @__PURE__ */ new Map();
53214
+ return listed.map((a) => a.scopeKind === "subsystem" && subsystemMissing(cfg, a.scopeId ?? "", trees) ? { ...a, scopeNote: SUBSYSTEM_NOT_FOUND } : a);
52492
53215
  }
52493
53216
  function bindingScope(scopeKind, scopeId) {
52494
53217
  return { kind: scopeKind ?? "instance", id: scopeId ?? "" };
@@ -52498,6 +53221,7 @@ function sameBinding(a, b) {
52498
53221
  }
52499
53222
  function bindRole(cfg, credential, userId, roleId, scopeKind, scopeId) {
52500
53223
  const principal = requirePrincipal3(cfg, credential);
53224
+ refuseUnfitSubsystemBinding(scopeKind, scopeId);
52501
53225
  const scope = bindingScope(scopeKind, scopeId);
52502
53226
  requireAdminOverScope(cfg, principal, scope.kind, scope.id, "binding a role requires project:admin over the scope");
52503
53227
  const user = getUserById(cfg.dataDir, userId);
@@ -52533,6 +53257,27 @@ function unbindRole(cfg, credential, userId, roleId, scopeKind, scopeId) {
52533
53257
  tryAppendAudit(cfg, buildAuditEvent(principal, "role.unbind", `${userId}:${roleId}`));
52534
53258
  return stored;
52535
53259
  }
53260
+ function subjectOfUser(user) {
53261
+ const subjectId = user.subject.userId || user.id;
53262
+ const aliases = [user.id, user.subject.userId].filter((id) => !!id && id !== subjectId);
53263
+ return {
53264
+ subjectId,
53265
+ ...aliases.length ? { aliasSubjectIds: [...new Set(aliases)] } : {},
53266
+ roleBindings: user.roleBindings ?? [],
53267
+ instanceAdmin: false
53268
+ };
53269
+ }
53270
+ function explain3(cfg, credential, userId, capability, scopeKind, scopeId) {
53271
+ const principal = requirePrincipal3(cfg, credential);
53272
+ requireAdminOverScope(cfg, principal, scopeKind, scopeId, "explaining a permission requires project:admin over its scope");
53273
+ const user = findUserByRecordOrSubjectId(cfg.dataDir, userId);
53274
+ if (!user) throw new Error(`User "${userId}" not found.`);
53275
+ const explained = explain2(cfg.dataDir, subjectOfUser(user), capability, scopeKind, scopeId);
53276
+ if (scopeKind === "subsystem" && subsystemMissing(cfg, scopeId, /* @__PURE__ */ new Map())) {
53277
+ return { ...explained, scopeNote: SUBSYSTEM_NOT_FOUND };
53278
+ }
53279
+ return explained;
53280
+ }
52536
53281
 
52537
53282
  // src/server/identity.ts
52538
53283
  var crypto19 = __toESM(require("crypto"));
@@ -55799,6 +56544,7 @@ function lockTargetOf(cfg, req) {
55799
56544
  function lifecycleAction(cfg, credential, projectId, opts) {
55800
56545
  const principal = requirePrincipal7(cfg, credential);
55801
56546
  const effective = authorize(cfg.dataDir, principal, PROJECT_WRITE_CAPABILITY4, "project", projectId);
56547
+ if (effective.value !== "no") refuseDeniedSubsystems(cfg, principal, projectId);
55802
56548
  switch (effective.value) {
55803
56549
  case "yes": {
55804
56550
  const outcome = opts.execute(hostedApprover(principal.subject));
@@ -55830,6 +56576,13 @@ function lifecycleAction(cfg, credential, projectId, opts) {
55830
56576
  }
55831
56577
  }
55832
56578
  }
56579
+ function refuseDeniedSubsystems(cfg, principal, projectId) {
56580
+ const denied = resolveFamilyReach(cfg.dataDir, principal, [projectId]).subsystems.filter((s) => s.projectId === projectId && s.permission.value === "no");
56581
+ if (denied.length === 0) return;
56582
+ throw new ForbiddenError(
56583
+ "a lock signs the whole tree, and the caller may not change these subsystems: " + denied.map((s) => `${s.subsystemId} (decided by ${describeDecision(s.permission)})`).join(", ")
56584
+ );
56585
+ }
55833
56586
  function getApprovalStatus(cfg, credential, requestId) {
55834
56587
  const principal = requirePrincipal7(cfg, credential);
55835
56588
  expirePendingApprovals(cfg.dataDir, (/* @__PURE__ */ new Date()).toISOString());
@@ -57112,6 +57865,9 @@ function bindRole2(cfg, sessionId, userId, roleId, scopeKind, scopeId) {
57112
57865
  function unbindRole2(cfg, sessionId, userId, roleId, scopeKind, scopeId) {
57113
57866
  return unbindRole(cfg, sessionId, userId, roleId, scopeKind, scopeId);
57114
57867
  }
57868
+ function explainPermission(cfg, sessionId, userId, capability, scopeKind, scopeId) {
57869
+ return explain3(cfg, sessionId, userId, capability, scopeKind, scopeId);
57870
+ }
57115
57871
 
57116
57872
  // src/server/webproject.ts
57117
57873
  function listProjects2(cfg, sessionId) {
@@ -57239,20 +57995,31 @@ function getSnapshot2(dataDir, snapshotId) {
57239
57995
  function getSnapshotArtifact(dataDir, snapshotId, kind, portalId) {
57240
57996
  return new ShareSnapshotIndex(new ShareSnapshotStore(dataDir)).getArtifact(snapshotId, kind, portalId);
57241
57997
  }
57998
+ function creatorReach(dataDir, principal, projectId, root) {
57999
+ const familyRoot = listFamilyRecords(dataDir, projectId)[0];
58000
+ const top = familyRoot !== void 0 ? resolveProjectRoot(dataDir, principal, familyRoot.id) : null;
58001
+ return {
58002
+ topRoot: top ?? root,
58003
+ parentReach: top !== null,
58004
+ hostedLookup: (id) => resolveProjectRoot(dataDir, principal, id)
58005
+ };
58006
+ }
57242
58007
  function captureSnapshot(dataDir, principal, projectId, view, artifacts) {
57243
58008
  const root = resolveProjectRoot(dataDir, principal, projectId);
57244
58009
  if (!root) throw new ForbiddenError("project not authorized or unknown");
58010
+ const relations = runWithProjectBinding(root, creatorReach(dataDir, principal, projectId, root), () => familyRelations2());
57245
58011
  return runWithProjectRoot(root, () => {
57246
58012
  const snapshot = {
57247
58013
  id: "",
57248
58014
  projectId,
57249
58015
  view,
57250
58016
  capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
57251
- // The canvas model is the primary payload the shared view renders.
57252
- canvasModel: JSON.stringify(buildCanvasDataModel())
58017
+ // Step 3: the canvas model is the primary payload the shared view renders,
58018
+ // with those relations on its consumption edges and no member record ids.
58019
+ canvasModel: JSON.stringify(buildCanvasDataModel(relations))
57253
58020
  };
57254
58021
  if (artifacts.includes("html")) {
57255
- snapshot.html = renderDiagram("canvas");
58022
+ snapshot.html = renderDiagram("canvas", relations);
57256
58023
  }
57257
58024
  if (artifacts.includes("openapi")) {
57258
58025
  const result = exportSurface2("project", "openapi");
@@ -57807,7 +58574,9 @@ function getWebProjectCanvas(cfg, sessionId, projectId) {
57807
58574
  if (!root) {
57808
58575
  throw new ForbiddenError("project not authorized or unknown");
57809
58576
  }
57810
- return runWithProjectRoot(root, () => renderDiagram("canvas"));
58577
+ const listed = listProjects2(cfg, sessionId);
58578
+ const relations = runWithProjectBinding(root, readReach(cfg, projectId, root, listed), () => familyRelations2());
58579
+ return runWithProjectRoot(root, () => renderDiagram("canvas", relations));
57811
58580
  }
57812
58581
  function getWebProjectCanvasModel(cfg, sessionId, projectId) {
57813
58582
  const principal = authenticateSession(cfg.dataDir, sessionId);
@@ -57819,17 +58588,34 @@ function getWebProjectCanvasModel(cfg, sessionId, projectId) {
57819
58588
  if (!root) {
57820
58589
  throw new ForbiddenError("project not authorized or unknown");
57821
58590
  }
57822
- const model = runWithProjectRoot(root, () => buildCanvasDataModel());
57823
- const readable = new Set(listProjects2(cfg, sessionId).map((r) => r.id));
58591
+ const listed = listProjects2(cfg, sessionId);
58592
+ const relations = runWithProjectBinding(root, readReach(cfg, projectId, root, listed), () => familyRelations2());
58593
+ const model = runWithProjectRoot(root, () => buildCanvasDataModel(relations));
58594
+ const readable = new Set(listed.map((r) => r.id));
57824
58595
  const members = new Set(listFamilyRecords(cfg.dataDir, projectId).filter((r) => r.parentProjectId !== void 0).map((r) => r.id));
57825
- return withMemberLinks(model, (id) => members.has(id) && readable.has(id));
58596
+ const hostedOf = externalRecords(runWithProjectRoot(root, () => loadProjectConfig()));
58597
+ return withProjectLinks(model, (node) => {
58598
+ const id = node.external ? hostedOf.get(node.id) : node.id;
58599
+ if (id === void 0 || !readable.has(id)) return void 0;
58600
+ return node.external || members.has(id) ? id : void 0;
58601
+ });
58602
+ }
58603
+ function externalRecords(config) {
58604
+ const out = /* @__PURE__ */ new Map();
58605
+ for (const [alias, decl] of Object.entries(config?.externals ?? {})) {
58606
+ if (decl?.source?.hosted !== void 0) out.set(alias, decl.source.hosted);
58607
+ }
58608
+ return out;
57826
58609
  }
57827
- function withMemberLinks(model, linkable) {
58610
+ function withProjectLinks(model, linkOf) {
57828
58611
  const m = model;
57829
58612
  if (!Array.isArray(m.subsystems)) return model;
57830
58613
  return {
57831
58614
  ...model,
57832
- subsystems: m.subsystems.map((s) => s.project && linkable(s.id) ? { ...s, recordId: s.id } : s)
58615
+ subsystems: m.subsystems.map((s) => {
58616
+ const recordId = s.project ? linkOf(s) : void 0;
58617
+ return recordId !== void 0 ? { ...s, recordId } : s;
58618
+ })
57833
58619
  };
57834
58620
  }
57835
58621
  function getWebProjectExternals(cfg, sessionId, projectId) {
@@ -59976,6 +60762,9 @@ function adminUnbindRole(cfg, sessionId, userId, roleId, scopeKind, scopeId) {
59976
60762
  function adminListAssignments(cfg, sessionId, scopeKind, scopeId, subjectKind, subjectId) {
59977
60763
  return listAssignments3(cfg, sessionId, scopeKind, scopeId, subjectKind, subjectId);
59978
60764
  }
60765
+ function adminExplainPermission(cfg, sessionId, userId, capability, scopeKind, scopeId) {
60766
+ return explainPermission(cfg, sessionId, userId, capability, scopeKind, scopeId ?? "");
60767
+ }
59979
60768
  function adminSetAssignment(cfg, sessionId, assignment) {
59980
60769
  return setAssignment3(cfg, sessionId, assignment);
59981
60770
  }
@@ -60477,6 +61266,10 @@ async function handleWebRequest(cfg, req, res, body, url, ctx) {
60477
61266
  )
60478
61267
  });
60479
61268
  }
61269
+ if (req.method === "GET" && parts.length === 4 && parts[2] === "permissions" && parts[3] === "explain") {
61270
+ const q2 = (name) => url.searchParams.get(name) ?? "";
61271
+ return sendJson(res, 200, adminExplainPermission(cfg, sessionId, q2("userId"), q2("capability"), q2("scopeKind") || "instance", q2("scopeId") || void 0));
61272
+ }
60480
61273
  if (req.method === "POST" && parts.length === 3 && parts[2] === "permissions") {
60481
61274
  return sendJson(res, 200, adminSetAssignment(cfg, sessionId, body));
60482
61275
  }
@@ -60862,6 +61655,23 @@ var WRITE_TOOL_PREFIXES = [
60862
61655
  "sdd_promote_",
60863
61656
  "sdd_demote_"
60864
61657
  ];
61658
+ var SUBSYSTEM_SCOPED_WRITE_TOOLS = /* @__PURE__ */ new Set([
61659
+ "sdd_add_subsystem",
61660
+ "sdd_add_component",
61661
+ "sdd_add_type",
61662
+ "sdd_define_interface",
61663
+ "sdd_write_narrative",
61664
+ "sdd_set_endpoints",
61665
+ "sdd_set_public_interfaces",
61666
+ "sdd_update_spec",
61667
+ "sdd_delete_spec",
61668
+ "sdd_rename_component",
61669
+ "sdd_rename_method",
61670
+ "sdd_move_methods"
61671
+ ]);
61672
+ function isSubsystemScopedWrite(toolName) {
61673
+ return SUBSYSTEM_SCOPED_WRITE_TOOLS.has(toolName);
61674
+ }
60865
61675
  var READ_TOOL_PREFIXES = ["sdd_get_", "sdd_validate_"];
60866
61676
  var READ_TOOL_NAMES = /* @__PURE__ */ new Set([
60867
61677
  "listAgents",
@@ -60897,18 +61707,22 @@ function mcpChangeChannels(body, projectId, response) {
60897
61707
  if (MUTATING_HOST_TOOLS.has(name)) return [`project:${projectId}`, "projects"];
60898
61708
  return [];
60899
61709
  }
60900
- function dataPlanePermissionError(cfg, principal, projectId, body) {
61710
+ function dataPlanePermissionError(cfg, principal, projectId, body, reach2) {
60901
61711
  const msg = jsonRpcRequest(body);
60902
61712
  if (!msg || msg.method !== "tools/call") return void 0;
60903
61713
  const name = msg.params?.name;
60904
61714
  if (typeof name !== "string" || name.length === 0) return void 0;
60905
61715
  const needed = requiredDataPlaneCapability(name);
60906
- if (authorize(cfg.dataDir, principal, needed, "project", projectId).value === "yes") return void 0;
61716
+ const decided = authorize(cfg.dataDir, principal, needed, "project", projectId);
61717
+ if (decided.value === "yes") return void 0;
61718
+ if (needed === "project:write" && isSubsystemScopedWrite(name) && reach2.subsystems.some((s) => s.projectId === projectId && s.permission.value === "yes")) {
61719
+ return void 0;
61720
+ }
60907
61721
  return {
60908
61722
  jsonrpc: "2.0",
60909
61723
  id: msg.id ?? null,
60910
61724
  result: {
60911
- content: [{ type: "text", text: `Forbidden \u2014 permission ${needed} required for ${name}` }],
61725
+ content: [{ type: "text", text: `Forbidden \u2014 permission ${needed} required for ${name}, decided at ${describeDecision(decided)}` }],
60912
61726
  isError: true
60913
61727
  }
60914
61728
  };
@@ -61084,7 +61898,7 @@ async function handleMcpRequest(cfg, req, res, body, credential) {
61084
61898
  for (const ch of mcpChangeChannels(body, projectId, dispatchedResponse)) publishChange(ch);
61085
61899
  return;
61086
61900
  }
61087
- const permissionError = dataPlanePermissionError(cfg, principal, projectId, body);
61901
+ const permissionError = dataPlanePermissionError(cfg, principal, projectId, body, reach2.family);
61088
61902
  if (permissionError !== void 0) {
61089
61903
  sendJson(res, 200, permissionError);
61090
61904
  auditToolCall(cfg.dataDir, principal, binding, body, deriveMcpOutcome(permissionError));
@@ -61124,7 +61938,11 @@ async function handleMcpRequest(cfg, req, res, body, credential) {
61124
61938
  for (const ch of mcpChangeChannels(body, projectId, response)) publishChange(ch);
61125
61939
  });
61126
61940
  }
61127
- var MEMBERSHIP_TOOLS = /* @__PURE__ */ new Set(["sdd_attach_member", "sdd_adopt_member", "sdd_detach_member", "sdd_rename_project", "sdd_promote_member", "sdd_demote_member", "sdd_add_member"]);
61941
+ var MEMBERSHIP_TOOLS = /* @__PURE__ */ new Set(["sdd_attach_member", "sdd_adopt_member", "sdd_detach_member", "sdd_rename_project", "sdd_promote_member", "sdd_demote_member", "sdd_add_member", "sdd_externalize_subsystem"]);
61942
+ function isMembershipCall(call) {
61943
+ if (call.name === "sdd_externalize_subsystem") return call.args.as === "project";
61944
+ return MEMBERSHIP_TOOLS.has(call.name);
61945
+ }
61128
61946
  var FAMILY_SHAPE_TOOLS = /* @__PURE__ */ new Set([
61129
61947
  "sdd_add_member",
61130
61948
  "sdd_move_member",
@@ -61146,7 +61964,7 @@ function reachLines(rows) {
61146
61964
  }
61147
61965
  function serveMembershipTool(cfg, principal, binding, body) {
61148
61966
  const call = toolCall(body);
61149
- if (!call || !MEMBERSHIP_TOOLS.has(call.name)) return {};
61967
+ if (!call || !isMembershipCall(call)) return {};
61150
61968
  const apply4 = call.args.dryRun !== true;
61151
61969
  const answer = (result) => ({ response: { jsonrpc: "2.0", id: call.id, result } });
61152
61970
  try {
@@ -61156,9 +61974,10 @@ function serveMembershipTool(cfg, principal, binding, body) {
61156
61974
  case "sdd_adopt_member":
61157
61975
  return answer(relocationResult("adopt", adopt(cfg.dataDir, principal, binding, String(call.args.alias ?? ""), String(call.args.path ?? ""), apply4), apply4));
61158
61976
  default: {
61159
- const named2 = call.name === "sdd_rename_project" ? String(call.args.project ?? "") : String(call.args.alias ?? "");
61977
+ const named2 = call.name === "sdd_rename_project" ? String(call.args.project ?? "") : call.name === "sdd_externalize_subsystem" ? String(call.args.subsystem ?? "") : String(call.args.alias ?? "");
61160
61978
  const located = typeof call.args.source === "string" ? call.args.source : typeof call.args.path === "string" ? call.args.path : void 0;
61161
- const screened = screen(cfg.dataDir, binding, call.name, named2, located);
61979
+ const newId = call.name === "sdd_promote_member" && typeof call.args.id === "string" && call.args.id ? call.args.id : void 0;
61980
+ const screened = screen(cfg.dataDir, binding, call.name, named2, located, newId);
61162
61981
  if (screened.refusal !== void 0) return answer({ content: [{ type: "text", text: screened.refusal }], isError: true });
61163
61982
  return { screen: screened };
61164
61983
  }
@@ -61202,6 +62021,10 @@ function withReachListing(result, screened, body) {
61202
62021
  const lines = reachLines(screened.reachChanges);
61203
62022
  r.content.push({ type: "text", text: lines.length > 0 ? `Reach changes \u2014 who gains access through the new parent:
61204
62023
  ${lines.join("\n")}` : "Reach changes: none \u2014 nobody gains or loses access." });
62024
+ if (!screened.carried) return;
62025
+ const carried = screened.carried.map((c) => `${c.from.join(", ")} -> project ${c.to}: ${c.subject} ` + (c.kind === "role" ? `role ${c.value} \u2014 ${c.note}` : `project:write ${c.value} (${c.note})`));
62026
+ r.content.push({ type: "text", text: carried.length > 0 ? `Subsystem rules carried onto the new project:
62027
+ ${carried.join("\n")}` : "Subsystem rules carried onto the new project: none." });
61205
62028
  }
61206
62029
  function reconcileAfter(cfg, principal, binding, body, response) {
61207
62030
  const call = toolCall(body);
@@ -61244,7 +62067,14 @@ function familyReachOf(cfg, principal, binding) {
61244
62067
  const topRoot = existingProjectRoot(cfg.dataDir, binding.familyRootId) ?? binding.rootPath;
61245
62068
  const parentReach = reach2.readable.includes(binding.familyRootId) && covered(binding.familyRootId);
61246
62069
  const unwritableRoots = family.filter((r) => r.rootPath && !isRetiredPart(r) && !(reach2.writable.includes(r.id) && covered(r.id))).map((r) => r.rootPath);
61247
- return { topRoot, parentReach, unwritableRoots, hostedLookup: hostedRecordLookup(cfg, principal) };
62070
+ const roots = new Map(family.filter((r) => r.rootPath && !isRetiredPart(r)).map((r) => [r.id, r.rootPath]));
62071
+ const subsystems = reach2.subsystems.flatMap((s) => {
62072
+ const root = roots.get(s.projectId);
62073
+ if (!root) return [];
62074
+ return [{ root, subsystemId: s.subsystemId, writable: s.permission.value === "yes" && covered(s.projectId), decidedBy: describeDecision(s.permission) }];
62075
+ });
62076
+ const writeReach = { unwritableRoots, subsystems };
62077
+ return { topRoot, parentReach, unwritableRoots, hostedLookup: hostedRecordLookup(cfg, principal), writeReach, family: reach2 };
61248
62078
  }
61249
62079
  function hostedRecordLookup(cfg, principal) {
61250
62080
  return (recordId) => {
@@ -62786,6 +63616,10 @@ function printMemberUpgrade(upgrade, fix) {
62786
63616
  var CAPABILITIES = ["project:read", "project:create", "project:write", "project:admin", "approval:decide", "share:create"];
62787
63617
  var PERMISSION_VALUES = ["yes", "approval", "no", "inherit"];
62788
63618
  function resolveScopeOptions(options) {
63619
+ if (options.subsystem) {
63620
+ if (!options.project) throw new WaironError("`--subsystem <id>` needs `--project <id>`: a subsystem scope is `<project>/<subsystem>`.");
63621
+ return { kind: "subsystem", id: `${options.project}/${options.subsystem}` };
63622
+ }
62789
63623
  if (options.project) return { kind: "project", id: options.project };
62790
63624
  if (options.unit) return { kind: "unit", id: options.unit };
62791
63625
  return { kind: "instance" };
@@ -62825,7 +63659,7 @@ async function runHostPermission(action, options = {}) {
62825
63659
  const list2 = listAssignments2(
62826
63660
  cfg,
62827
63661
  cred,
62828
- options.project || options.unit || options.instance ? scope.kind : void 0,
63662
+ options.project || options.unit || options.instance || options.subsystem ? scope.kind : void 0,
62829
63663
  scope.id,
62830
63664
  options.user ? "user" : void 0,
62831
63665
  options.user
@@ -62835,7 +63669,7 @@ async function runHostPermission(action, options = {}) {
62835
63669
  } else {
62836
63670
  for (const a of list2) {
62837
63671
  const subject = a.subjectKind === "everyone" ? "everyone" : a.subjectId;
62838
- logger.info(` ${a.id} ${String(subject).padEnd(20)} ${a.capability.padEnd(16)} ${a.value.padEnd(9)} ${a.scopeKind}${a.scopeId ? ` ${a.scopeId}` : ""}`);
63672
+ logger.info(` ${a.id} ${String(subject).padEnd(20)} ${a.capability.padEnd(16)} ${a.value.padEnd(9)} ${a.scopeKind}${a.scopeId ? ` ${a.scopeId}` : ""}${a.scopeNote ? ` (${a.scopeNote})` : ""}`);
62839
63673
  }
62840
63674
  }
62841
63675
  break;
@@ -64200,7 +65034,7 @@ var variantsCmd = program.command("variants").description("Component variants \u
64200
65034
  variantsCmd.command("list").alias("ls").description("List the component variants in the registry (global + project) with their base and guidance").action(async () => {
64201
65035
  await runVariants();
64202
65036
  });
64203
- program.command("diagram").description("Generate architecture diagrams from the spec tree \u2014 interactive canvas by default; Mermaid / draw.io / Excalidraw via --format").option("--subsystem <id>", "scope the component diagram to one subsystem (plus its external neighbors)").option("--sequence <component:method>", "emit a sequence diagram derived from the method's L5 narrative").option("--depth <n>", "max call-expansion depth for sequence diagrams (default 3)", (v) => parseInt(v, 10)).option("--all", "write the full diagram set: system, per-subsystem, entrypoint sequences, and the interactive canvas").option("--canvas", "emit the interactive self-contained HTML canvas (pan/zoom, collapse boundaries, detail panel, issue overlay)").option("--drawio", "emit an editable draw.io / diagrams.net file (same layout as the canvas)").option("--excalidraw", "emit an editable Excalidraw scene (same layout as the canvas)").option("--format <fmt>", "alias for the flags above: mermaid | canvas | drawio | excalidraw").option("--out <path>", "write to a file (or directory with --all; default .wai/docs/diagrams) instead of stdout").action(async (opts) => {
65037
+ program.command("diagram").description("Generate architecture diagrams from the spec tree \u2014 interactive canvas by default; Mermaid / draw.io / Excalidraw via --format").option("--subsystem <id>", "scope the component diagram to one subsystem (plus its external neighbors)").option("--sequence <component:method>", "emit a sequence diagram derived from the method's L5 narrative").option("--depth <n>", "max call-expansion depth for sequence diagrams (default 3)", (v) => parseInt(v, 10)).option("--all", "write the full diagram set: system, per-subsystem, entrypoint sequences, and the interactive canvas").option("--canvas", "emit the interactive self-contained HTML canvas (pan/zoom, collapse boundaries, detail panel, issue overlay)").option("--drawio", "emit an editable draw.io / diagrams.net file (same layout as the canvas)").option("--excalidraw", "emit an editable Excalidraw scene (same layout as the canvas)").option("--format <fmt>", "alias for the flags above: mermaid | canvas | drawio | excalidraw").option("--out <path>", "write to a file (or directory with --all; default .wai/docs/diagrams) instead of stdout").option("--no-health", "skip comparing each consumption relation with its live producer: the canvas draws those edges as not checked").action(async (opts) => {
64204
65038
  await runDiagram({
64205
65039
  subsystem: opts.subsystem,
64206
65040
  sequence: opts.sequence,
@@ -64210,7 +65044,8 @@ program.command("diagram").description("Generate architecture diagrams from the
64210
65044
  drawio: opts.drawio,
64211
65045
  excalidraw: opts.excalidraw,
64212
65046
  format: opts.format,
64213
- out: opts.out
65047
+ out: opts.out,
65048
+ health: opts.health
64214
65049
  });
64215
65050
  });
64216
65051
  program.command("list").alias("ls").description("List all dynamic agents resolved from the spec tree").action(async () => {
@@ -64372,12 +65207,13 @@ hostCmd.command("unit <action>").description("create an organization unit (proje
64372
65207
  hostCmd.command("doctor").description("migrate a hosted data dir to the permission model (grants \u2192 assignments, owners, unit ids, placements); dry-run without --fix").option("--fix", "apply the migration (REQUIRED at rollout \u2014 legacy users/tokens otherwise resolve to zero permissions)").option("--data-dir <path>", "data root").action(async (opts) => {
64373
65208
  await runHostDoctor({ fix: opts.fix, dataDir: opts.dataDir });
64374
65209
  });
64375
- hostCmd.command("permission <action>").description("set | list | remove a permission assignment (the subject\xD7scope\xD7capability grid)").option("--user <userId>", "the subject user id (for set; optional filter for list)").option("--capability <capability>", "project:read | project:create | project:write | project:admin | approval:decide").option("--value <value>", "yes | approval | no | inherit", "yes").option("--project <id>", "anchor the assignment at a project scope").option("--unit <unitId>", "anchor the assignment at an organization-unit scope").option("--instance", "anchor the assignment at the instance scope (the default)").option("--id <assignmentId>", "assignment id (for remove)").option("--data-dir <path>", "data root").action(async (action, opts) => {
65210
+ hostCmd.command("permission <action>").description("set | list | remove a permission assignment (the subject\xD7scope\xD7capability grid)").option("--user <userId>", "the subject user id (for set; optional filter for list)").option("--capability <capability>", "project:read | project:create | project:write | project:admin | approval:decide").option("--value <value>", "yes | approval | no | inherit", "yes").option("--project <id>", "anchor the assignment at a project scope").option("--subsystem <id>", "with --project: anchor at a subsystem of that project (project:write only, yes | no | inherit)").option("--unit <unitId>", "anchor the assignment at an organization-unit scope").option("--instance", "anchor the assignment at the instance scope (the default)").option("--id <assignmentId>", "assignment id (for remove)").option("--data-dir <path>", "data root").action(async (action, opts) => {
64376
65211
  await runHostPermission(action, {
64377
65212
  user: opts.user,
64378
65213
  capability: opts.capability,
64379
65214
  value: opts.value,
64380
65215
  project: opts.project,
65216
+ subsystem: opts.subsystem,
64381
65217
  unit: opts.unit,
64382
65218
  instance: opts.instance,
64383
65219
  id: opts.id,