@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 +935 -99
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +469 -52
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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
|
|
5589
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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> <span style="color:' + t.relDrift + '">\\u0394 drifted</span> '
|
|
6827
|
+
+ '<span style="color:' + t.relBad + '">\\u2715 incompatible</span> <span style="color:' + t.relUnknown + '">? unavailable (dotted)</span>'
|
|
6828
|
+
+ (at ? ' \\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 ' + sw(t.proxyOut) + '\\u21E2 out-port \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> <span style="color:' + t.warn + '">\\u2190 used by</span>' +
|
|
6676
|
-
(state.dataCoupling ? ' \xB7 <span style="color:' + t.typeV.stroke + '">- - \\u25B8 uses models</span>' : '')
|
|
6850
|
+
(state.dataCoupling ? ' \xB7 <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 = {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
41079
|
-
|
|
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, ...
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
50298
|
-
|
|
50299
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
-
//
|
|
57252
|
-
|
|
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
|
-
|
|
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
|
|
57823
|
-
const
|
|
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
|
-
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
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 || !
|
|
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
|
|
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
|
-
|
|
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,
|