archctx 0.9.0 → 0.9.2

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/bin/archctx.mjs CHANGED
@@ -53,6 +53,9 @@ function stableYaml(value, indent = 0) {
53
53
  return `[]
54
54
  `;
55
55
  return value.map((item) => {
56
+ if (Array.isArray(item))
57
+ return `${pad}- ${canonicalize(item)}
58
+ `;
56
59
  if (item !== null && typeof item === "object") {
57
60
  return `${pad}- ${stableYaml(item, indent + 2).trimStart()}`;
58
61
  }
@@ -2374,8 +2377,9 @@ var init_failure = __esm(() => {
2374
2377
  "model-missing": { code: "AC_MODEL_ADOPTION_REQUIRED", message: "Architecture debt requires an adopted model.", fields: ["checks"] },
2375
2378
  "changed-path-invalid": { code: "AC_SCHEMA_INVALID", message: "Changed paths must be an array of repository-relative POSIX paths.", fields: ["checks"] },
2376
2379
  "debt-report-invalid": { code: "AC_SCHEMA_INVALID", message: "Architecture debt evidence violates its contract.", fields: ["checks"] },
2377
- "projection-manifest-upgrade-required": { code: "AC_PRECONDITION_FAILED", message: "The committed projection manifest requires supported re-baselining." },
2380
+ "projection-manifest-upgrade-required": { code: "AC_PRECONDITION_FAILED", message: "The committed projection manifest requires supported re-baselining. First restore the original model if missing; the manifest digest cannot recover it. With the reviewed model available, follow docs/runbooks/architecture-documentation-projections.md before running archctx docs apply --approved --profile <default|repo-harness/v1>." },
2378
2381
  "projection-manifest-invalid": { code: "AC_PRECONDITION_FAILED", message: "The committed projection manifest violates its contract." },
2382
+ "architecture-model-ignored": { code: "AC_PRECONDITION_FAILED", action: "commit-architecture-model", message: "Git ignore rules exclude architecture model paths, so the model cannot be committed and a manifest digest cannot rebuild it. Remove or narrow the rule, commit the model, then retry adoption. The diagnostic command prints each deciding rule.", facts: { pathCount: "uint", paths: "optional:diagnosticPaths", commands: "optional:diagnosticCommands", omittedPathCount: "uint" } },
2379
2383
  "projection-footprint-uncovered": { code: "AC_PRECONDITION_FAILED", message: "Declared source footprints lack a projection target." },
2380
2384
  "index-missing": { code: "AC_CODE_FACTS_UNAVAILABLE", message: "Required code facts have no available index. Run codegraph init." },
2381
2385
  "code-facts-not-ready": { code: "AC_CODE_FACTS_NOT_READY", message: "Repository code facts are not ready. Run archctx doctor --json and follow the reported action.", facts: { issues: "codeFactsIssues" } },
@@ -2813,6 +2817,89 @@ var init_failure = __esm(() => {
2813
2817
  };
2814
2818
  });
2815
2819
 
2820
+ // packages/contracts/src/runtime-topology.ts
2821
+ function isRuntimeTopologyNodeKind(kind) {
2822
+ return typeof kind === "string" && RUNTIME_TOPOLOGY_NODE_KINDS.includes(kind);
2823
+ }
2824
+ function runtimeTopologyModelIssues(model) {
2825
+ const issues = [];
2826
+ const nodesById = new Map;
2827
+ for (const node of model.nodes)
2828
+ if (typeof node.id === "string")
2829
+ nodesById.set(node.id, node);
2830
+ const namespacesByProcess = new Map;
2831
+ const mountersByMount = new Map;
2832
+ for (const relation of model.relations) {
2833
+ if (typeof relation.id !== "string" || typeof relation.kind !== "string" || !Object.hasOwn(RUNTIME_TOPOLOGY_RELATION_ENDPOINT_KINDS, relation.kind))
2834
+ continue;
2835
+ const rule = RUNTIME_TOPOLOGY_RELATION_ENDPOINT_KINDS[relation.kind];
2836
+ let endpointsValid = true;
2837
+ for (const field of ["source", "target"]) {
2838
+ const endpoint = relation[field];
2839
+ const node = typeof endpoint === "string" ? nodesById.get(endpoint) : undefined;
2840
+ if (!node) {
2841
+ issues.push({ entityId: relation.id, field, invariant: "topology-relation-endpoint-missing" });
2842
+ endpointsValid = false;
2843
+ } else if (!rule[field].includes(node.kind)) {
2844
+ issues.push({ entityId: relation.id, field, invariant: field === "source" ? "topology-relation-source-kind-invalid" : "topology-relation-target-kind-invalid" });
2845
+ endpointsValid = false;
2846
+ }
2847
+ }
2848
+ if (!endpointsValid)
2849
+ continue;
2850
+ const source = relation.source;
2851
+ const target = relation.target;
2852
+ if (relation.kind === "runs-in")
2853
+ namespacesByProcess.set(source, [...namespacesByProcess.get(source) ?? [], target]);
2854
+ if (relation.kind === "mounts")
2855
+ mountersByMount.set(target, [...mountersByMount.get(target) ?? [], source]);
2856
+ }
2857
+ for (const [processId, namespaces] of namespacesByProcess) {
2858
+ if (new Set(namespaces).size > 1)
2859
+ issues.push({ entityId: processId, field: "runs-in", invariant: "topology-process-namespace-ambiguous" });
2860
+ }
2861
+ for (const [mountId, mounters] of mountersByMount) {
2862
+ if (new Set(mounters).size > 1)
2863
+ issues.push({ entityId: mountId, field: "mounts", invariant: "topology-mount-mounter-ambiguous" });
2864
+ }
2865
+ for (const [id, node] of nodesById) {
2866
+ if (node.kind !== "namespace" || node.parent === undefined)
2867
+ continue;
2868
+ const parent = typeof node.parent === "string" ? nodesById.get(node.parent) : undefined;
2869
+ if (!parent) {
2870
+ issues.push({ entityId: id, field: "parent", invariant: "topology-namespace-parent-missing" });
2871
+ continue;
2872
+ }
2873
+ if (parent.kind !== "namespace") {
2874
+ issues.push({ entityId: id, field: "parent", invariant: "topology-namespace-parent-kind-invalid" });
2875
+ continue;
2876
+ }
2877
+ const seen = new Set([id]);
2878
+ let cursor = parent;
2879
+ while (cursor && cursor.kind === "namespace" && typeof cursor.id === "string") {
2880
+ if (seen.has(cursor.id)) {
2881
+ issues.push({ entityId: id, field: "parent", invariant: "topology-namespace-parent-cycle" });
2882
+ break;
2883
+ }
2884
+ seen.add(cursor.id);
2885
+ cursor = typeof cursor.parent === "string" ? nodesById.get(cursor.parent) : undefined;
2886
+ }
2887
+ }
2888
+ return issues.sort((left, right) => compare(left.entityId, right.entityId) || compare(left.field, right.field) || compare(left.invariant, right.invariant));
2889
+ }
2890
+ function compare(left, right) {
2891
+ return left < right ? -1 : left > right ? 1 : 0;
2892
+ }
2893
+ var RUNTIME_TOPOLOGY_NODE_KINDS, RUNTIME_TOPOLOGY_RELATION_ENDPOINT_KINDS;
2894
+ var init_runtime_topology = __esm(() => {
2895
+ RUNTIME_TOPOLOGY_NODE_KINDS = ["process", "namespace", "mount"];
2896
+ RUNTIME_TOPOLOGY_RELATION_ENDPOINT_KINDS = {
2897
+ spawns: { source: ["process"], target: ["process"] },
2898
+ "runs-in": { source: ["process"], target: ["namespace"] },
2899
+ mounts: { source: ["process", "namespace"], target: ["mount"] }
2900
+ };
2901
+ });
2902
+
2816
2903
  // packages/contracts/src/validator.ts
2817
2904
  function jsonSchemaResolver(documents) {
2818
2905
  const byId = new Map;
@@ -3156,6 +3243,10 @@ function nativeContextModelIssues(model, schemas, recordPaths) {
3156
3243
  if (relation[field] !== undefined && !nodes.has(relation[field]))
3157
3244
  add(relation, field, "relation-node-missing");
3158
3245
  }
3246
+ const relations = new Map(model.relations.map((relation) => [relation.id, relation]));
3247
+ for (const issue of runtimeTopologyModelIssues(model)) {
3248
+ add(relations.get(issue.entityId) ?? nodes.get(issue.entityId) ?? issue.entityId, issue.field, issue.invariant);
3249
+ }
3159
3250
  for (const flow of model.flows) {
3160
3251
  if (nodes.get(flow.capabilityId)?.kind !== "capability")
3161
3252
  add(flow, "capabilityId", "flow-capability-missing");
@@ -3198,6 +3289,7 @@ var NATIVE_CONTEXT_SCHEMA_VERSION = "archcontext.native-context-export/v1", NATI
3198
3289
  var init_native_context = __esm(() => {
3199
3290
  init_failure();
3200
3291
  init_schema();
3292
+ init_runtime_topology();
3201
3293
  init_validator();
3202
3294
  NATIVE_CONTEXT_LIMITS = Object.freeze({
3203
3295
  inputFiles: 1e4,
@@ -3781,6 +3873,8 @@ var init_projection_manifest = __esm(() => {
3781
3873
  "diagram-mermaid": true,
3782
3874
  "diagram-structurizr": true,
3783
3875
  "diagram-likec4": true,
3876
+ "diagram-runtime-topology": true,
3877
+ "runtime-topology-summary": true,
3784
3878
  "agent-context": true
3785
3879
  };
3786
3880
  SCOPE_KINDS = { repository: true, entity: true, relation: true, decision: true, diagram: true, changelog: true };
@@ -3873,7 +3967,7 @@ function compareVersionTriples(left, right) {
3873
3967
  }
3874
3968
  return 0;
3875
3969
  }
3876
- var ARCHCONTEXT_PRODUCT_NAME = "archctx", ARCHCONTEXT_PRODUCT_VERSION = "0.9.0", ARCHCONTEXT_PACKAGE_MANAGER = "bun@1.4.3", ARCHCONTEXT_NODE_RANGE = ">=22.22 <26", LOCAL_RUNTIME_RPC_SCHEMA_VERSION = "archcontext.runtime-rpc/v2", ARCHCONTEXT_SCHEMA_SET_VERSION = "2026-07-11.explorer-view-v2";
3970
+ var ARCHCONTEXT_PRODUCT_NAME = "archctx", ARCHCONTEXT_PRODUCT_VERSION = "0.9.2", ARCHCONTEXT_PACKAGE_MANAGER = "bun@1.4.3", ARCHCONTEXT_NODE_RANGE = ">=22.22 <26", LOCAL_RUNTIME_RPC_SCHEMA_VERSION = "archcontext.runtime-rpc/v2", ARCHCONTEXT_SCHEMA_SET_VERSION = "2026-07-11.explorer-view-v2";
3877
3971
 
3878
3972
  // packages/contracts/src/refactor.ts
3879
3973
  function refactorNamespaceIssues(value, label = "product.id") {
@@ -4978,6 +5072,7 @@ var init_src = __esm(() => {
4978
5072
  init_projection_writer();
4979
5073
  init_projection_manifest();
4980
5074
  init_refactor();
5075
+ init_runtime_topology();
4981
5076
  init_schema();
4982
5077
  init_failure();
4983
5078
  init_validator();
@@ -17691,6 +17786,15 @@ var FIXED_TARGETS = [
17691
17786
  layoutTarget("projection_target.diagram.structurizr", "diagram-structurizr", { kind: "diagram", id: "architecture" }, "docs/architecture/diagrams/architecture.structurizr.json", "generated", "structurizr-json"),
17692
17787
  layoutTarget("projection_target.diagram.likec4", "diagram-likec4", { kind: "diagram", id: "architecture" }, "docs/architecture/diagrams/architecture.likec4", "generated", "likec4")
17693
17788
  ];
17789
+ var RUNTIME_TOPOLOGY_DIAGRAM_PATH = "docs/architecture/diagrams/runtime-topology.mmd";
17790
+ var RUNTIME_TOPOLOGY_SUMMARY_PATH = "docs/architecture/runtime-topology.md";
17791
+ var RUNTIME_TOPOLOGY_TARGETS = [
17792
+ layoutTarget("projection_target.diagram.runtime-topology", "diagram-runtime-topology", { kind: "diagram", id: "runtime-topology" }, RUNTIME_TOPOLOGY_DIAGRAM_PATH, "generated", "mermaid"),
17793
+ layoutTarget("projection_target.runtime-topology.summary", "runtime-topology-summary", { kind: "diagram", id: "runtime-topology" }, RUNTIME_TOPOLOGY_SUMMARY_PATH, "generated", "markdown")
17794
+ ];
17795
+ function hasRuntimeTopologyNodes(nodes) {
17796
+ return nodes.some((node) => isRuntimeTopologyNodeKind(node.kind));
17797
+ }
17694
17798
  function parseRepoHarnessNodeProfile(node) {
17695
17799
  const match = /^capability\.([a-z0-9]+(?:-[a-z0-9]+)*)\.([a-z0-9]+(?:-[a-z0-9]+)*)$/.exec(node.id);
17696
17800
  if (!match || node.kind !== "capability") {
@@ -17718,7 +17822,7 @@ function parseRepoHarnessNodeProfile(node) {
17718
17822
  };
17719
17823
  }
17720
17824
  function entitySummaryTargetNodes(nodes, profile) {
17721
- return profile === REPO_HARNESS_PROJECTION_PROFILE ? nodes.filter((node) => node.kind === "capability") : [...nodes];
17825
+ return profile === REPO_HARNESS_PROJECTION_PROFILE ? nodes.filter((node) => node.kind === "capability") : nodes.filter((node) => !isRuntimeTopologyNodeKind(node.kind));
17722
17826
  }
17723
17827
  function resolveArchitectureDocumentationLayout(input) {
17724
17828
  const profile = input.profile ?? "default";
@@ -17728,7 +17832,8 @@ function resolveArchitectureDocumentationLayout(input) {
17728
17832
  return layoutTarget(`projection_target.entity.${stableId(node.id)}`, "entity-summary", { kind: "entity", id: node.id, entityKind: node.kind }, path, "mixed", "markdown");
17729
17833
  });
17730
17834
  const relationTargets = (profile === REPO_HARNESS_PROJECTION_PROFILE ? [] : input.relations).map((relation) => layoutTarget(`projection_target.relation.${stableId(relation.id)}`, "relation-summary", { kind: "relation", id: relation.id }, `docs/architecture/relations/${pathSegment(relation.id)}.md`, "mixed", "markdown"));
17731
- const targets = [FIXED_TARGETS[0], ...entityTargets, ...relationTargets, ...FIXED_TARGETS.slice(1)].map((target) => ({ ...target, path: requiredPath(target.path, target.targetId) }));
17835
+ const topologyTargets = hasRuntimeTopologyNodes(input.nodes) ? RUNTIME_TOPOLOGY_TARGETS : [];
17836
+ const targets = [FIXED_TARGETS[0], ...entityTargets, ...relationTargets, ...FIXED_TARGETS.slice(1), ...topologyTargets].map((target) => ({ ...target, path: requiredPath(target.path, target.targetId) }));
17732
17837
  assertUniqueTargets(targets);
17733
17838
  return {
17734
17839
  profile,
@@ -20100,12 +20205,52 @@ function normalizeNativeModel(model) {
20100
20205
  flows: [...model.flows ?? []].sort((a, b) => a.id.localeCompare(b.id))
20101
20206
  };
20102
20207
  }
20208
+ var ARCHITECTURE_NODE_LAYER_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
20209
+ function nativeNodeLayer(node) {
20210
+ const layer = node.layer;
20211
+ if (layer === undefined)
20212
+ return;
20213
+ if (typeof layer !== "string" || !ARCHITECTURE_NODE_LAYER_PATTERN.test(layer)) {
20214
+ throw new Error(`architecture-node-layer-invalid: ${node.id} (${JSON.stringify(layer)})`);
20215
+ }
20216
+ return layer;
20217
+ }
20218
+ function architectureLayerGroups(model) {
20219
+ const nodesByLayer = new Map;
20220
+ const ungroupedNodes = [];
20221
+ const layerByNodeId = new Map;
20222
+ for (const node of [...model.nodes].sort((a, b) => a.id.localeCompare(b.id))) {
20223
+ const layer = nativeNodeLayer(node);
20224
+ if (layer === undefined) {
20225
+ ungroupedNodes.push(node);
20226
+ continue;
20227
+ }
20228
+ const members = nodesByLayer.get(layer);
20229
+ if (members)
20230
+ members.push(node);
20231
+ else
20232
+ nodesByLayer.set(layer, [node]);
20233
+ layerByNodeId.set(node.id, layer);
20234
+ }
20235
+ const layers = [...nodesByLayer.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([layer, nodes]) => ({ layer, nodes }));
20236
+ return { layers, ungroupedNodes, layerByNodeId };
20237
+ }
20238
+ function mermaidLayerId(layer) {
20239
+ return `layer__${layer.replace(/-/g, "_")}`;
20240
+ }
20103
20241
  function exportMermaidModel(model) {
20104
20242
  const normalized = normalizeNativeModel(model);
20243
+ const grouping = architectureLayerGroups(normalized);
20244
+ const nodeLine = (indent, node) => `${indent}${mermaidId(node.id)}["${escapeMermaid(node.name)}"]`;
20105
20245
  const lines = ["%% Generated by ArchContext. Native model remains source of truth.", "flowchart LR"];
20106
- for (const node of normalized.nodes) {
20107
- lines.push(` ${mermaidId(node.id)}["${escapeMermaid(node.name)}"]`);
20246
+ for (const group of grouping.layers) {
20247
+ lines.push(` subgraph ${mermaidLayerId(group.layer)}["${escapeMermaid(group.layer)}"]`);
20248
+ for (const node of group.nodes)
20249
+ lines.push(nodeLine(" ", node));
20250
+ lines.push(" end");
20108
20251
  }
20252
+ for (const node of grouping.ungroupedNodes)
20253
+ lines.push(nodeLine(" ", node));
20109
20254
  for (const relation of normalized.relations) {
20110
20255
  lines.push(` ${mermaidId(relation.source)} -->|"${escapeMermaid(relation.kind)}"| ${mermaidId(relation.target)}`);
20111
20256
  }
@@ -20122,22 +20267,15 @@ function exportDocumentationLikeC4Model(model) {
20122
20267
  const normalized = normalizeNativeModel(model);
20123
20268
  const lines = [
20124
20269
  "// Generated by ArchContext. Native model remains source of truth.",
20125
- "specification {",
20126
- " element capability",
20127
- " element module",
20128
- " element component",
20129
- " element interface",
20130
- " element datastore",
20131
- " element external_system",
20132
- "}",
20270
+ ...likeC4SpecificationLines(normalized.nodes),
20133
20271
  "",
20134
20272
  "model {"
20135
20273
  ];
20136
20274
  for (const node of normalized.nodes)
20137
- lines.push(` ${mermaidId(node.id)} = ${documentationLikeC4Kind(node.kind)} "${escapeDsl(node.name)}"`);
20275
+ lines.push(` ${mermaidId(node.id)} = ${likeC4ElementKind(node.kind)} "${escapeDsl(node.name)}"`);
20138
20276
  for (const relation of normalized.relations)
20139
20277
  lines.push(` ${mermaidId(relation.source)} -> ${mermaidId(relation.target)} "${escapeDsl(relation.kind)}"`);
20140
- lines.push("}", "", "views {", " view index {", " include *", " autoLayout TopBottom", " }", "}");
20278
+ lines.push("}", "", ...likeC4IndexViewLines(architectureLayerGroups(normalized), mermaidId));
20141
20279
  const content = `${lines.join(`
20142
20280
  `)}
20143
20281
  `;
@@ -20147,18 +20285,73 @@ function exportDocumentationLikeC4Model(model) {
20147
20285
  files: [{ path: "docs/architecture/diagrams/architecture.likec4", content }]
20148
20286
  };
20149
20287
  }
20288
+ function likeC4SpecificationLines(nodes) {
20289
+ const topologyKinds = RUNTIME_TOPOLOGY_NODE_KINDS.filter((kind) => nodes.some((node) => node.kind === kind));
20290
+ return [
20291
+ "specification {",
20292
+ " element capability",
20293
+ " element module",
20294
+ " element component",
20295
+ " element interface",
20296
+ " element datastore",
20297
+ " element external_system",
20298
+ ...topologyKinds.map((kind) => ` element ${kind}`),
20299
+ "}"
20300
+ ];
20301
+ }
20302
+ function likeC4ElementKind(kind) {
20303
+ if (kind === "external-system")
20304
+ return "external_system";
20305
+ if (kind === "datastore")
20306
+ return "datastore";
20307
+ if (["capability", "module", "component", "interface"].includes(kind))
20308
+ return kind;
20309
+ if (isRuntimeTopologyNodeKind(kind))
20310
+ return kind;
20311
+ return "component";
20312
+ }
20313
+ function architectureStructurizrElementType(kind) {
20314
+ if (kind === "capability")
20315
+ return "Software System";
20316
+ if (kind === "module")
20317
+ return "Container";
20318
+ if (kind === "datastore")
20319
+ return "Database";
20320
+ if (kind === "external-system")
20321
+ return "External System";
20322
+ if (kind === "namespace")
20323
+ return "Deployment Node";
20324
+ if (kind === "process")
20325
+ return "Container Instance";
20326
+ if (kind === "mount")
20327
+ return "Infrastructure Node";
20328
+ return "Component";
20329
+ }
20330
+ function likeC4IndexViewLines(grouping, elementId) {
20331
+ const groupLines = grouping.layers.flatMap((group) => [
20332
+ ` group "${escapeDsl(group.layer)}" {`,
20333
+ ...group.nodes.map((node) => ` include ${elementId(node.id)}`),
20334
+ " }"
20335
+ ]);
20336
+ return ["views {", " view index {", ...groupLines, " include *", " autoLayout TopBottom", " }", "}"];
20337
+ }
20150
20338
  function exportDocumentationStructurizrWorkspace(model) {
20151
20339
  const normalized = normalizeNativeModel(model);
20340
+ const { layerByNodeId } = architectureLayerGroups(normalized);
20152
20341
  const workspace = {
20153
20342
  schemaVersion: "archcontext.structurizr-export/v1",
20154
20343
  name: "ArchContext Architecture",
20155
20344
  model: {
20156
- elements: normalized.nodes.map((node) => ({
20157
- id: node.id,
20158
- name: node.name,
20159
- type: documentationStructurizrElementType(node.kind),
20160
- tags: [node.kind]
20161
- })),
20345
+ elements: normalized.nodes.map((node) => {
20346
+ const layer = layerByNodeId.get(node.id);
20347
+ return {
20348
+ id: node.id,
20349
+ name: node.name,
20350
+ type: architectureStructurizrElementType(node.kind),
20351
+ tags: [node.kind],
20352
+ ...layer === undefined ? {} : { group: layer }
20353
+ };
20354
+ }),
20162
20355
  relationships: normalized.relations.map((relation) => ({
20163
20356
  id: relation.id,
20164
20357
  sourceId: relation.source,
@@ -20181,6 +20374,209 @@ function exportDocumentationStructurizrWorkspace(model) {
20181
20374
  files: [{ path: "docs/architecture/diagrams/architecture.structurizr.json", content }]
20182
20375
  };
20183
20376
  }
20377
+ var RUNTIME_TOPOLOGY_NAMESPACE_TYPES = ["host", "container", "pod", "kubernetes", "pid", "mount", "net", "user"];
20378
+ var RUNTIME_TOPOLOGY_MOUNT_TARGET_PATTERN = /^\/([^/\u0000]+(\/[^/\u0000]+)*)?$/;
20379
+ function runtimeTopologyView(model) {
20380
+ const issues = runtimeTopologyModelIssues(model);
20381
+ if (issues.length > 0) {
20382
+ throw new Error(`runtime-topology-model-invalid: ${issues.map((issue) => `${issue.entityId} ${issue.field} ${issue.invariant}`).join("; ")}`);
20383
+ }
20384
+ const byId = (left, right) => compareStrings3(left.id, right.id);
20385
+ const nodes = [...model.nodes].sort(byId);
20386
+ const relations = [...model.relations].sort(byId);
20387
+ const nodesById = new Map(nodes.map((node) => [node.id, node]));
20388
+ const namespaces = nodes.filter((node) => node.kind === "namespace").map((node) => ({
20389
+ node,
20390
+ type: runtimeTopologyNamespaceType(node),
20391
+ ...typeof node.parent === "string" ? { parentId: node.parent } : {},
20392
+ namespaceIds: [],
20393
+ processIds: []
20394
+ }));
20395
+ const namespaceById = new Map(namespaces.map((namespace) => [namespace.node.id, namespace]));
20396
+ for (const namespace of namespaces) {
20397
+ if (namespace.parentId !== undefined)
20398
+ namespaceById.get(namespace.parentId).namespaceIds.push(namespace.node.id);
20399
+ }
20400
+ const namespaceByProcessId = new Map(relations.filter((relation) => relation.kind === "runs-in").map((relation) => [relation.source, relation.target]));
20401
+ const processes = nodes.filter((node) => node.kind === "process").map((node) => {
20402
+ const namespaceId = namespaceByProcessId.get(node.id);
20403
+ return namespaceId === undefined ? { node } : { node, namespaceId };
20404
+ });
20405
+ for (const topologyProcess of processes) {
20406
+ if (topologyProcess.namespaceId !== undefined)
20407
+ namespaceById.get(topologyProcess.namespaceId).processIds.push(topologyProcess.node.id);
20408
+ }
20409
+ const mounts = nodes.filter((node) => node.kind === "mount").map(runtimeTopologyMount);
20410
+ const mounterByMountId = new Map(relations.filter((relation) => relation.kind === "mounts").map((relation) => [relation.target, relation.source]));
20411
+ const stacksByMounterId = new Map;
20412
+ for (const mount of mounts) {
20413
+ const mounterId = mounterByMountId.get(mount.node.id);
20414
+ if (mounterId === undefined)
20415
+ continue;
20416
+ stacksByMounterId.set(mounterId, [...stacksByMounterId.get(mounterId) ?? [], mount]);
20417
+ }
20418
+ const mountStacks = [...stacksByMounterId.entries()].sort(([left], [right]) => compareStrings3(left, right)).map(([mounterId, stack]) => ({
20419
+ mounterId,
20420
+ mounts: stack.sort((left, right) => left.order - right.order || compareStrings3(left.node.id, right.node.id))
20421
+ }));
20422
+ const deployments = relations.filter((relation) => relation.kind === "deployed-on" && isRuntimeTopologyNodeKind(nodesById.get(relation.target)?.kind));
20423
+ const linkedNodeIds = new Set;
20424
+ for (const relation of deployments) {
20425
+ const source = nodesById.get(relation.source);
20426
+ if (!source)
20427
+ throw new Error(`runtime-topology-relation-endpoint-missing: ${relation.id} (${relation.source})`);
20428
+ if (!isRuntimeTopologyNodeKind(source.kind))
20429
+ linkedNodeIds.add(source.id);
20430
+ }
20431
+ return {
20432
+ nodesById,
20433
+ namespaces,
20434
+ rootNamespaceIds: namespaces.filter((namespace) => namespace.parentId === undefined).map((namespace) => namespace.node.id),
20435
+ processes,
20436
+ mountStacks,
20437
+ unmountedMounts: mounts.filter((mount) => !mounterByMountId.has(mount.node.id)),
20438
+ spawns: relations.filter((relation) => relation.kind === "spawns"),
20439
+ mounts: relations.filter((relation) => relation.kind === "mounts"),
20440
+ deployments,
20441
+ linkedNodes: nodes.filter((node) => linkedNodeIds.has(node.id))
20442
+ };
20443
+ }
20444
+ function runtimeTopologyNamespaceType(node) {
20445
+ const value = node.namespace;
20446
+ const type = value !== null && typeof value === "object" && !Array.isArray(value) ? value.type : undefined;
20447
+ if (typeof type !== "string" || !RUNTIME_TOPOLOGY_NAMESPACE_TYPES.includes(type)) {
20448
+ throw new Error(`runtime-topology-namespace-invalid: ${node.id}`);
20449
+ }
20450
+ return type;
20451
+ }
20452
+ function runtimeTopologyMount(node) {
20453
+ const value = node.mount;
20454
+ if (value === null || typeof value !== "object" || Array.isArray(value))
20455
+ throw new Error(`runtime-topology-mount-invalid: ${node.id}`);
20456
+ const { source, target, mode, order } = value;
20457
+ if (source !== undefined && (typeof source !== "string" || source === "") || typeof target !== "string" || !RUNTIME_TOPOLOGY_MOUNT_TARGET_PATTERN.test(target) || mode !== "ro" && mode !== "rw" || typeof order !== "number" || !Number.isInteger(order) || order < 0) {
20458
+ throw new Error(`runtime-topology-mount-invalid: ${node.id}`);
20459
+ }
20460
+ return { node, ...source === undefined ? {} : { source }, target, mode, order };
20461
+ }
20462
+ function runtimeTopologyMountStackId(mounterId) {
20463
+ return `mounts__${mermaidId(mounterId)}`;
20464
+ }
20465
+ function runtimeTopologyMountLabel(mount) {
20466
+ return `${mount.node.name} (${mount.target}, ${mount.mode}, order ${mount.order})`;
20467
+ }
20468
+ function exportRuntimeTopologyMermaid(model) {
20469
+ const view = runtimeTopologyView(model);
20470
+ const namespaceById = new Map(view.namespaces.map((namespace) => [namespace.node.id, namespace]));
20471
+ const nodeLine = (indent, node, label = node.name) => `${indent}${mermaidId(node.id)}["${escapeMermaid(label)}"]`;
20472
+ const lines = ["%% Generated by ArchContext. Native model remains source of truth.", "flowchart TB"];
20473
+ const pushNamespace = (namespaceId, depth) => {
20474
+ const namespace = namespaceById.get(namespaceId);
20475
+ const indent = " ".repeat(depth);
20476
+ lines.push(`${indent}subgraph ${mermaidId(namespaceId)}["${escapeMermaid(`${namespace.node.name} (${namespace.type})`)}"]`);
20477
+ for (const processId of namespace.processIds)
20478
+ lines.push(nodeLine(`${indent} `, view.nodesById.get(processId)));
20479
+ for (const childId of namespace.namespaceIds)
20480
+ pushNamespace(childId, depth + 1);
20481
+ lines.push(`${indent}end`);
20482
+ };
20483
+ for (const namespaceId of view.rootNamespaceIds)
20484
+ pushNamespace(namespaceId, 1);
20485
+ for (const topologyProcess of view.processes)
20486
+ if (topologyProcess.namespaceId === undefined)
20487
+ lines.push(nodeLine(" ", topologyProcess.node));
20488
+ for (const stack of view.mountStacks) {
20489
+ const mounter = view.nodesById.get(stack.mounterId);
20490
+ lines.push(` subgraph ${runtimeTopologyMountStackId(stack.mounterId)}["${escapeMermaid(`Mounts: ${mounter.name}`)}"]`, " direction TB");
20491
+ for (const mount of stack.mounts)
20492
+ lines.push(nodeLine(" ", mount.node, runtimeTopologyMountLabel(mount)));
20493
+ for (let index = 1;index < stack.mounts.length; index += 1) {
20494
+ lines.push(` ${mermaidId(stack.mounts[index - 1].node.id)} ~~~ ${mermaidId(stack.mounts[index].node.id)}`);
20495
+ }
20496
+ lines.push(" end");
20497
+ }
20498
+ for (const mount of view.unmountedMounts)
20499
+ lines.push(nodeLine(" ", mount.node, runtimeTopologyMountLabel(mount)));
20500
+ for (const node of view.linkedNodes)
20501
+ lines.push(nodeLine(" ", node));
20502
+ for (const relation of [...view.spawns, ...view.mounts, ...view.deployments].sort((left, right) => compareStrings3(left.id, right.id))) {
20503
+ lines.push(` ${mermaidId(relation.source)} -->|"${escapeMermaid(relation.kind)}"| ${mermaidId(relation.target)}`);
20504
+ }
20505
+ const content = `${lines.join(`
20506
+ `)}
20507
+ `;
20508
+ return {
20509
+ format: "mermaid",
20510
+ digest: digestJson({ format: "mermaid", content }),
20511
+ files: [{ path: RUNTIME_TOPOLOGY_DIAGRAM_PATH, content }]
20512
+ };
20513
+ }
20514
+ function renderRuntimeTopologySummary(model, summaryPath) {
20515
+ const view = runtimeTopologyView(model);
20516
+ const cell = (value) => value.replaceAll("|", "\\|");
20517
+ const named = (id) => {
20518
+ const node = view.nodesById.get(id);
20519
+ return `\`${id}\`${node ? ` ${cell(node.name)}` : ""}`;
20520
+ };
20521
+ const list = (ids) => ids.length === 0 ? "none" : ids.map((id) => `\`${id}\``).join(", ");
20522
+ const lines = [
20523
+ "# Runtime Topology",
20524
+ "",
20525
+ "The architecture model declares this topology. No code facts verify it.",
20526
+ "",
20527
+ `- Diagram: [Mermaid](${posix.relative(posix.dirname(summaryPath), RUNTIME_TOPOLOGY_DIAGRAM_PATH)})`,
20528
+ "",
20529
+ "## Namespaces",
20530
+ ""
20531
+ ];
20532
+ if (view.namespaces.length === 0)
20533
+ lines.push("- No namespaces recorded.");
20534
+ else {
20535
+ lines.push("| Namespace | Type | Parent | Namespaces | Processes |", "| --- | --- | --- | --- | --- |");
20536
+ for (const namespace of view.namespaces) {
20537
+ lines.push(`| ${named(namespace.node.id)} | ${namespace.type} | ${namespace.parentId === undefined ? "none" : `\`${namespace.parentId}\``} | ${list(namespace.namespaceIds)} | ${list(namespace.processIds)} |`);
20538
+ }
20539
+ }
20540
+ lines.push("", "## Processes", "");
20541
+ if (view.processes.length === 0)
20542
+ lines.push("- No processes recorded.");
20543
+ else {
20544
+ lines.push("| Process | Namespace | Spawns | Deployed here |", "| --- | --- | --- | --- |");
20545
+ for (const topologyProcess of view.processes) {
20546
+ const id = topologyProcess.node.id;
20547
+ const spawned = view.spawns.filter((relation) => relation.source === id).map((relation) => relation.target);
20548
+ const deployed = view.deployments.filter((relation) => relation.target === id).map((relation) => relation.source);
20549
+ lines.push(`| ${named(id)} | ${topologyProcess.namespaceId === undefined ? "none" : `\`${topologyProcess.namespaceId}\``} | ${list([...new Set(spawned)].sort(compareStrings3))} | ${list([...new Set(deployed)].sort(compareStrings3))} |`);
20550
+ }
20551
+ }
20552
+ lines.push("", "## Mount Stacks", "");
20553
+ const mountRows = (stack) => [
20554
+ "| Order | Mount | Source | Target | Mode |",
20555
+ "| --- | --- | --- | --- | --- |",
20556
+ ...stack.map((mount) => `| ${mount.order} | ${named(mount.node.id)} | ${mount.source === undefined ? "none" : `\`${cell(mount.source)}\``} | \`${cell(mount.target)}\` | ${mount.mode} |`)
20557
+ ];
20558
+ if (view.mountStacks.length === 0 && view.unmountedMounts.length === 0)
20559
+ lines.push("- No mounts recorded.");
20560
+ for (const [index, stack] of view.mountStacks.entries()) {
20561
+ if (index > 0)
20562
+ lines.push("");
20563
+ lines.push(`### ${named(stack.mounterId)}`, "", ...mountRows(stack.mounts));
20564
+ }
20565
+ if (view.unmountedMounts.length > 0) {
20566
+ if (view.mountStacks.length > 0)
20567
+ lines.push("");
20568
+ lines.push("### Not mounted", "", ...mountRows(view.unmountedMounts));
20569
+ }
20570
+ const otherDeployments = view.deployments.filter((relation) => view.nodesById.get(relation.target)?.kind !== "process");
20571
+ if (otherDeployments.length > 0) {
20572
+ lines.push("", "## Other Deployments", "");
20573
+ for (const relation of otherDeployments)
20574
+ lines.push(`- \`${relation.source}\` -> \`${relation.target}\` — ${relation.intent}`);
20575
+ }
20576
+ return `${lines.join(`
20577
+ `)}
20578
+ `;
20579
+ }
20184
20580
  function loadNativeModelFromArchContext(root) {
20185
20581
  return nativeModelFromYamlObjects({
20186
20582
  nodes: readYamlObjects(resolve6(root, ".archcontext/model/nodes")),
@@ -20199,6 +20595,7 @@ function nativeModelFromYamlObjects(input) {
20199
20595
  throw new Error(`architecture-node-schema-version-unsupported: ${node.id || "unknown"} (${String(node.schemaVersion)})`);
20200
20596
  }
20201
20597
  assertArchitectureNodeSourceV2(node);
20598
+ nativeNodeLayer(node);
20202
20599
  }
20203
20600
  const flows = input.flows;
20204
20601
  for (const flow of flows)
@@ -20383,8 +20780,15 @@ function renderTargetGeneratedBody(target, model, input) {
20383
20780
  return exportDocumentationStructurizrWorkspace(model).files[0].content;
20384
20781
  if (target.type === "diagram-likec4")
20385
20782
  return exportDocumentationLikeC4Model(model).files[0].content;
20783
+ if (target.type === "diagram-runtime-topology")
20784
+ return exportRuntimeTopologyMermaid(model).files[0].content;
20785
+ if (target.type === "runtime-topology-summary")
20786
+ return renderRuntimeTopologySummary(model, target.path);
20386
20787
  return "";
20387
20788
  }
20789
+ function architectureIndexLink(path) {
20790
+ return path.replace(/^docs\/architecture\//, "");
20791
+ }
20388
20792
  function renderArchitectureIndex(model, layout2) {
20389
20793
  const lines = [
20390
20794
  "# Architecture Index",
@@ -20409,6 +20813,7 @@ function renderArchitectureIndex(model, layout2) {
20409
20813
  "- [Mermaid](diagrams/architecture.mmd)",
20410
20814
  "- [Structurizr JSON](diagrams/architecture.structurizr.json)",
20411
20815
  "- [LikeC4](diagrams/architecture.likec4)",
20816
+ ...layout2.targets.flatMap((target) => target.type === "diagram-runtime-topology" ? [`- [Runtime topology Mermaid](${architectureIndexLink(target.path)})`] : target.type === "runtime-topology-summary" ? [`- [Runtime topology](${architectureIndexLink(target.path)})`] : []),
20412
20817
  "- [Decision index](decisions/index.md)",
20413
20818
  "- [Architecture changelog](changelog.md)"
20414
20819
  ];
@@ -20862,26 +21267,6 @@ function isManagedArchitectureDocumentationMarkdownPath(path) {
20862
21267
  function pathSegment2(id) {
20863
21268
  return stableId(id).replace(/\./g, "-");
20864
21269
  }
20865
- function documentationLikeC4Kind(kind) {
20866
- if (kind === "external-system")
20867
- return "external_system";
20868
- if (kind === "datastore")
20869
- return "datastore";
20870
- if (["capability", "module", "component", "interface"].includes(kind))
20871
- return kind;
20872
- return "component";
20873
- }
20874
- function documentationStructurizrElementType(kind) {
20875
- if (kind === "capability")
20876
- return "Software System";
20877
- if (kind === "module")
20878
- return "Container";
20879
- if (kind === "datastore")
20880
- return "Database";
20881
- if (kind === "external-system")
20882
- return "External System";
20883
- return "Component";
20884
- }
20885
21270
  function escapeDsl(value) {
20886
21271
  return value.replace(/"/g, "'");
20887
21272
  }
@@ -21545,7 +21930,7 @@ function sameOwners(left, right) {
21545
21930
 
21546
21931
  // packages/core/module-statistics/src/index.ts
21547
21932
  function evaluateDependencyConstraints(input) {
21548
- const constraints = [...input.constraints].sort((left, right) => compare(left.id, right.id));
21933
+ const constraints = [...input.constraints].sort((left, right) => compare2(left.id, right.id));
21549
21934
  const certification = certifyCodeFacts(input.codeFacts, input.worktreeDigest, input.truncated, input.importNodeLimitExceeded ?? false);
21550
21935
  if (constraints.length === 0) {
21551
21936
  return {
@@ -21602,7 +21987,7 @@ function evaluateDependencyConstraints(input) {
21602
21987
  reasonCodes.add("unresolved-import");
21603
21988
  if (input.workspacePackagesResolved === false)
21604
21989
  reasonCodes.add("workspace-resolution-failed");
21605
- const sortedViolations = [...violations.values()].sort((left, right) => compare(left.constraintId, right.constraintId) || compare(left.fromPath, right.fromPath) || compare(left.toPath, right.toPath));
21990
+ const sortedViolations = [...violations.values()].sort((left, right) => compare2(left.constraintId, right.constraintId) || compare2(left.fromPath, right.fromPath) || compare2(left.toPath, right.toPath));
21606
21991
  return {
21607
21992
  schemaVersion: "archcontext.dependency-constraint-evaluation/v1",
21608
21993
  status: sortedViolations.length > 0 ? "violated" : reasonCodes.size > 0 ? "undetermined" : "pass",
@@ -21679,7 +22064,7 @@ function measureCapabilityDependencies(input) {
21679
22064
  const cellKey = JSON.stringify([from.capability, to.capability]);
21680
22065
  let cell = cells.get(cellKey);
21681
22066
  if (cell === undefined) {
21682
- const relationIds = [...new Set(declaringRelations.get(cellKey) ?? [])].sort(compare);
22067
+ const relationIds = [...new Set(declaringRelations.get(cellKey) ?? [])].sort(compare2);
21683
22068
  cell = { from: from.capability, to: to.capability, imports: 0, calls: null, declared: from.capability === to.capability || relationIds.length > 0, relationIds };
21684
22069
  cells.set(cellKey, cell);
21685
22070
  }
@@ -21687,14 +22072,14 @@ function measureCapabilityDependencies(input) {
21687
22072
  attributedEdges.push({ fromPath: edge.from, toPath: edge.to, fromCapability: from.capability, toCapability: to.capability });
21688
22073
  }
21689
22074
  const edgeLimit = input.edgeLimit ?? CAPABILITY_DEPENDENCIES_EDGE_LIMIT;
21690
- attributedEdges.sort((left, right) => compare(left.fromPath, right.fromPath) || compare(left.toPath, right.toPath));
22075
+ attributedEdges.sort((left, right) => compare2(left.fromPath, right.fromPath) || compare2(left.toPath, right.toPath));
21691
22076
  const listed = attributedEdges.slice(0, edgeLimit);
21692
22077
  return {
21693
22078
  schemaVersion: CAPABILITY_DEPENDENCIES_SCHEMA_VERSION,
21694
22079
  ownershipRule: "parent-chain",
21695
22080
  filter: { from: input.from ?? null, into: input.into ?? null },
21696
- capabilities: input.nodes.filter((node) => node.kind === "capability").map((node) => node.id).sort(compare),
21697
- matrix: [...cells.values()].sort((left, right) => compare(left.from, right.from) || compare(left.to, right.to)),
22081
+ capabilities: input.nodes.filter((node) => node.kind === "capability").map((node) => node.id).sort(compare2),
22082
+ matrix: [...cells.values()].sort((left, right) => compare2(left.from, right.from) || compare2(left.to, right.to)),
21698
22083
  imports: {
21699
22084
  coverage: certification.coverage,
21700
22085
  reasonCodes: [...certification.reasonCodes].sort(),
@@ -21730,7 +22115,7 @@ function isRepositoryLocalSpecifier(specifier, workspacePackages) {
21730
22115
  return true;
21731
22116
  return workspacePackages.some((workspacePackage) => specifier === workspacePackage.name || specifier.startsWith(`${workspacePackage.name}/`));
21732
22117
  }
21733
- function compare(left, right) {
22118
+ function compare2(left, right) {
21734
22119
  return left < right ? -1 : left > right ? 1 : 0;
21735
22120
  }
21736
22121
  function measureModuleStatistics(input) {
@@ -21815,7 +22200,7 @@ function modelDigest(model, constraints) {
21815
22200
  nodes: [...model.nodes].sort((left, right) => left.id < right.id ? -1 : left.id > right.id ? 1 : 0),
21816
22201
  relations: [...model.relations].sort((left, right) => left.id < right.id ? -1 : left.id > right.id ? 1 : 0),
21817
22202
  flows: [...model.flows ?? []].sort((left, right) => left.id < right.id ? -1 : left.id > right.id ? 1 : 0),
21818
- ...constraints.length === 0 ? {} : { constraints: [...constraints].sort((left, right) => compare(left.id, right.id)) }
22203
+ ...constraints.length === 0 ? {} : { constraints: [...constraints].sort((left, right) => compare2(left.id, right.id)) }
21819
22204
  });
21820
22205
  }
21821
22206
  function countViolationsByNode(evaluation) {
@@ -22712,6 +23097,29 @@ function listGitVisibleFiles(root, pathspec) {
22712
23097
  }
22713
23098
  }).sort((left, right) => left.localeCompare(right));
22714
23099
  }
23100
+ function ignoredRepositoryPaths(root, paths) {
23101
+ if (paths.length === 0)
23102
+ return [];
23103
+ const result = spawnSync2("git", ["check-ignore", "--verbose", "--no-index", "-z", "--stdin"], {
23104
+ cwd: root,
23105
+ encoding: "utf8",
23106
+ input: paths.map((path) => `${path}\x00`).join(""),
23107
+ stdio: ["pipe", "pipe", "pipe"]
23108
+ });
23109
+ if (result.status !== 0 && result.status !== 1) {
23110
+ const detail = result.error?.message ?? (result.stderr.trim() || `git check-ignore exited ${result.status}`);
23111
+ throw new GitVisibleFilesUnavailableError(root, detail);
23112
+ }
23113
+ const fields = result.stdout.split("\x00");
23114
+ const ignored = [];
23115
+ for (let index = 0;index + 3 < fields.length; index += 4) {
23116
+ const [source, line, pattern, path] = fields.slice(index, index + 4);
23117
+ if (pattern.startsWith("!"))
23118
+ continue;
23119
+ ignored.push({ path, source, line: Number(line), pattern });
23120
+ }
23121
+ return ignored;
23122
+ }
22715
23123
  function readHeadSha(root) {
22716
23124
  try {
22717
23125
  return execFileSync("git", ["rev-parse", "HEAD"], {
@@ -26080,7 +26488,7 @@ function buildObservations(snapshot, structure, model) {
26080
26488
  evidence: evidence.evidenceGap()
26081
26489
  });
26082
26490
  }
26083
- return drafts.map((draft, index) => ({ draft, order: draft.kind === "cycle" ? index : 0 })).sort((left, right) => compare2(left.draft.kind, right.draft.kind) || left.order - right.order || compare2(left.draft.subjectSelectorId, right.draft.subjectSelectorId)).map(({ draft }) => ({
26491
+ return drafts.map((draft, index) => ({ draft, order: draft.kind === "cycle" ? index : 0 })).sort((left, right) => compare3(left.draft.kind, right.draft.kind) || left.order - right.order || compare3(left.draft.subjectSelectorId, right.draft.subjectSelectorId)).map(({ draft }) => ({
26084
26492
  kind: draft.kind,
26085
26493
  subjectSelectorId: draft.subjectSelectorId,
26086
26494
  affectedNodeIds: draft.affectedNodeIds,
@@ -26270,13 +26678,13 @@ function signalIdFor(draft) {
26270
26678
  }
26271
26679
  function modelDigest2(model, constraints) {
26272
26680
  return digestJson({
26273
- nodes: [...model.nodes].sort((left, right) => compare2(left.id, right.id)),
26274
- relations: [...model.relations].sort((left, right) => compare2(left.id, right.id)),
26275
- flows: [...model.flows ?? []].sort((left, right) => compare2(left.id, right.id)),
26276
- ...constraints.length === 0 ? {} : { constraints: [...constraints].sort((left, right) => compare2(left.id, right.id)) }
26681
+ nodes: [...model.nodes].sort((left, right) => compare3(left.id, right.id)),
26682
+ relations: [...model.relations].sort((left, right) => compare3(left.id, right.id)),
26683
+ flows: [...model.flows ?? []].sort((left, right) => compare3(left.id, right.id)),
26684
+ ...constraints.length === 0 ? {} : { constraints: [...constraints].sort((left, right) => compare3(left.id, right.id)) }
26277
26685
  });
26278
26686
  }
26279
- function compare2(left, right) {
26687
+ function compare3(left, right) {
26280
26688
  return left < right ? -1 : left > right ? 1 : 0;
26281
26689
  }
26282
26690
  function isNumber(value) {
@@ -26298,17 +26706,49 @@ var architecture_node_schema_default = {
26298
26706
  type: "object",
26299
26707
  additionalProperties: false,
26300
26708
  required: ["schemaVersion", "id", "kind", "name", "status", "summary"],
26709
+ allOf: [
26710
+ {
26711
+ if: { properties: { kind: { const: "namespace" } }, required: ["kind"] },
26712
+ then: { required: ["namespace"] },
26713
+ else: { not: { required: ["namespace"] } }
26714
+ },
26715
+ {
26716
+ if: { properties: { kind: { const: "mount" } }, required: ["kind"] },
26717
+ then: { required: ["mount"] },
26718
+ else: { not: { required: ["mount"] } }
26719
+ }
26720
+ ],
26301
26721
  properties: {
26302
26722
  schemaVersion: { const: "archcontext.node/v2" },
26303
26723
  id: { type: "string", pattern: "^([a-z][a-z0-9]*(\\.[a-z0-9][a-z0-9-]*)*::)?[a-z][a-z0-9]*(\\.[a-z0-9][a-z0-9-]*)+$" },
26304
26724
  kind: {
26305
26725
  type: "string",
26306
- enum: ["capability", "module", "component", "interface", "event", "datastore", "external-system", "deployment"]
26726
+ enum: ["capability", "module", "component", "interface", "event", "datastore", "external-system", "deployment", "process", "namespace", "mount"]
26307
26727
  },
26308
26728
  name: { type: "string" },
26309
26729
  status: { type: "string", enum: ["active", "planned", "deprecated", "removed"] },
26310
26730
  parent: { type: "string", pattern: "^([a-z][a-z0-9]*(\\.[a-z0-9][a-z0-9-]*)*::)?[a-z][a-z0-9]*(\\.[a-z0-9][a-z0-9-]*)+$" },
26731
+ layer: { type: "string", pattern: "^[a-z][a-z0-9]*(-[a-z0-9]+)*$" },
26311
26732
  summary: { type: "string" },
26733
+ namespace: {
26734
+ type: "object",
26735
+ additionalProperties: false,
26736
+ required: ["type"],
26737
+ properties: {
26738
+ type: { type: "string", enum: ["host", "container", "pod", "kubernetes", "pid", "mount", "net", "user"] }
26739
+ }
26740
+ },
26741
+ mount: {
26742
+ type: "object",
26743
+ additionalProperties: false,
26744
+ required: ["target", "mode", "order"],
26745
+ properties: {
26746
+ source: { type: "string", minLength: 1 },
26747
+ target: { type: "string", pattern: "^/([^/\\u0000]+(/[^/\\u0000]+)*)?$" },
26748
+ mode: { type: "string", enum: ["ro", "rw"] },
26749
+ order: { type: "integer", minimum: 0 }
26750
+ }
26751
+ },
26312
26752
  responsibilities: { type: "array", items: { type: "string" }, minItems: 1 },
26313
26753
  source: {
26314
26754
  type: "object",
@@ -26397,7 +26837,7 @@ var architecture_relation_schema_default = {
26397
26837
  id: { type: "string", pattern: "^([a-z][a-z0-9]*(\\.[a-z0-9][a-z0-9-]*)*::)?[a-z][a-z0-9]*(\\.[a-z0-9][a-z0-9-]*)+$" },
26398
26838
  kind: {
26399
26839
  type: "string",
26400
- enum: ["depends-on", "calls", "publishes", "subscribes", "reads", "writes", "implements", "deployed-on"]
26840
+ enum: ["depends-on", "calls", "publishes", "subscribes", "reads", "writes", "implements", "deployed-on", "spawns", "runs-in", "mounts"]
26401
26841
  },
26402
26842
  source: { type: "string", pattern: "^([a-z][a-z0-9]*(\\.[a-z0-9][a-z0-9-]*)*::)?[a-z][a-z0-9]*(\\.[a-z0-9][a-z0-9-]*)+$" },
26403
26843
  target: { type: "string", pattern: "^([a-z][a-z0-9]*(\\.[a-z0-9][a-z0-9-]*)*::)?[a-z][a-z0-9]*(\\.[a-z0-9][a-z0-9-]*)+$" },
@@ -27105,6 +27545,22 @@ function createDefaultProjectionTargetManifest() {
27105
27545
  ownership: "generated",
27106
27546
  format: "likec4"
27107
27547
  },
27548
+ {
27549
+ id: "projection_rule.diagram.runtime-topology",
27550
+ targetType: "diagram-runtime-topology",
27551
+ scope: { kind: "diagram", id: "runtime-topology" },
27552
+ pathTemplate: "docs/architecture/diagrams/runtime-topology.mmd",
27553
+ ownership: "generated",
27554
+ format: "mermaid"
27555
+ },
27556
+ {
27557
+ id: "projection_rule.runtime-topology.summary",
27558
+ targetType: "runtime-topology-summary",
27559
+ scope: { kind: "diagram", id: "runtime-topology" },
27560
+ pathTemplate: "docs/architecture/runtime-topology.md",
27561
+ ownership: "generated",
27562
+ format: "markdown"
27563
+ },
27108
27564
  {
27109
27565
  id: "projection_rule.agent-context.claude",
27110
27566
  targetType: "agent-context",
@@ -27250,10 +27706,19 @@ class YamlModelStore {
27250
27706
  }
27251
27707
  }
27252
27708
  const files = listModelFiles(workspace.root);
27709
+ const topologyRecords = { nodes: [], relations: [] };
27710
+ const topologyRecordPaths = new Map;
27253
27711
  for (const file of files) {
27254
27712
  if (file.path.startsWith(".archcontext/model/") && /\.ya?ml$/.test(file.path)) {
27255
27713
  try {
27256
27714
  const value = parseJsonOrStableYaml(file.body, file.path);
27715
+ for (const [directory, records] of [["nodes", topologyRecords.nodes], ["relations", topologyRecords.relations]]) {
27716
+ if (!file.path.startsWith(`.archcontext/model/${directory}/`) || !isJsonRecord2(value))
27717
+ continue;
27718
+ records.push(value);
27719
+ if (typeof value.id === "string")
27720
+ topologyRecordPaths.set(value.id, file.path);
27721
+ }
27257
27722
  if (file.path.startsWith(".archcontext/model/nodes/")) {
27258
27723
  const { issues } = validateJsonSchema(architecture_node_schema_default, value);
27259
27724
  for (const issue of issues) {
@@ -27274,6 +27739,9 @@ class YamlModelStore {
27274
27739
  errors.push(`${file.path}: expected archcontext.flow/v1, got ${file.schemaVersion || "missing"}`);
27275
27740
  }
27276
27741
  }
27742
+ for (const issue of runtimeTopologyModelIssues(topologyRecords)) {
27743
+ errors.push(`${topologyRecordPaths.get(issue.entityId) ?? issue.entityId}: ${issue.field}: ${issue.invariant}`);
27744
+ }
27277
27745
  const adr = validateAdrAppliesTo(files);
27278
27746
  const constraints = readDependencyConstraints(files);
27279
27747
  const reviewPolicy = readReviewPolicy(files);
@@ -27294,6 +27762,21 @@ class YamlModelStore {
27294
27762
  return { digest: digestJson(changeSet), summary: "ChangeSet preview generated" };
27295
27763
  }
27296
27764
  }
27765
+ var ARCHITECTURE_MODEL_COMMITTED_PATHS = Object.freeze([
27766
+ ".archcontext/manifest.yaml",
27767
+ ".archcontext/product.yaml",
27768
+ ".archcontext/model/nodes/",
27769
+ ".archcontext/model/relations/",
27770
+ ".archcontext/model/flows/",
27771
+ ".archcontext/decisions/",
27772
+ ".archcontext/policies/",
27773
+ ".archcontext/practices/",
27774
+ ".archcontext/projections/"
27775
+ ]);
27776
+ function architectureModelIgnoreProbePaths(root) {
27777
+ const directories = ARCHITECTURE_MODEL_COMMITTED_PATHS.filter((path) => path.endsWith("/")).map((path) => path.slice(0, -1));
27778
+ return [...new Set([...ARCHITECTURE_MODEL_COMMITTED_PATHS, ...collectArchContextFiles(root, directories)])];
27779
+ }
27297
27780
  function listModelFiles(root) {
27298
27781
  const paths = collectArchContextFiles(root, [
27299
27782
  ".archcontext/manifest.yaml",
@@ -27311,7 +27794,7 @@ function listModelFiles(root) {
27311
27794
  return {
27312
27795
  path,
27313
27796
  body,
27314
- schemaVersion: extractSchemaVersion(body),
27797
+ schemaVersion: extractSchemaVersion(body, path),
27315
27798
  digest: digestJson({ path, body })
27316
27799
  };
27317
27800
  });
@@ -27357,7 +27840,15 @@ function readdirOrFile(path) {
27357
27840
  return "file";
27358
27841
  }
27359
27842
  }
27360
- function extractSchemaVersion(body) {
27843
+ function extractSchemaVersion(body, path) {
27844
+ if (path.startsWith(".archcontext/model/") && /\.ya?ml$/.test(path)) {
27845
+ try {
27846
+ const parsed = parseJsonOrStableYaml(body, path);
27847
+ return typeof parsed?.schemaVersion === "string" ? parsed.schemaVersion : "";
27848
+ } catch {
27849
+ return "";
27850
+ }
27851
+ }
27361
27852
  const match = body.match(/schemaVersion:\s*"?([^"\n]+)"?/);
27362
27853
  if (match)
27363
27854
  return match[1].trim();
@@ -27370,6 +27861,9 @@ function extractSchemaVersion(body) {
27370
27861
  return "archcontext.generated/v1";
27371
27862
  return "";
27372
27863
  }
27864
+ function isJsonRecord2(value) {
27865
+ return value !== null && typeof value === "object" && !Array.isArray(value);
27866
+ }
27373
27867
  function withTrailingNewline(body) {
27374
27868
  return body.endsWith(`
27375
27869
  `) ? body : `${body}
@@ -31307,13 +31801,10 @@ class HttpContext7Transport {
31307
31801
  url.searchParams.set("libraryName", input.libraryName);
31308
31802
  url.searchParams.set("query", input.query);
31309
31803
  url.searchParams.set("fast", input.fast ? "true" : "false");
31310
- const response = await fetchWithTimeout(url, {
31804
+ const { payload } = await fetchJsonWithTimeout(url, {
31311
31805
  method: "GET",
31312
31806
  headers: authHeaders(input.apiKey)
31313
- }, input.timeoutMs);
31314
- if (!response.ok)
31315
- throw httpStatusError("Context7 search failed", response);
31316
- const { payload } = await parseJsonResponse(response, "Context7 search");
31807
+ }, input.timeoutMs, "Context7 search");
31317
31808
  return parseSearchResponse(payload);
31318
31809
  }
31319
31810
  async getContext(input) {
@@ -31322,13 +31813,10 @@ class HttpContext7Transport {
31322
31813
  url.searchParams.set("query", input.query);
31323
31814
  url.searchParams.set("type", "json");
31324
31815
  url.searchParams.set("fast", "true");
31325
- const response = await fetchWithTimeout(url, {
31816
+ const { payload } = await fetchJsonWithTimeout(url, {
31326
31817
  method: "GET",
31327
31818
  headers: authHeaders(input.apiKey)
31328
- }, input.timeoutMs);
31329
- if (!response.ok)
31330
- throw httpStatusError("Context7 context fetch failed", response);
31331
- const { payload } = await parseJsonResponse(response, "Context7 context fetch");
31819
+ }, input.timeoutMs, "Context7 context fetch");
31332
31820
  return parseDocumentationResponse(payload).slice(0, input.maxResults);
31333
31821
  }
31334
31822
  }
@@ -31426,11 +31914,14 @@ function normalizeSourceUri(value) {
31426
31914
  return;
31427
31915
  }
31428
31916
  }
31429
- async function fetchWithTimeout(input, init, timeoutMs) {
31917
+ async function fetchJsonWithTimeout(input, init, timeoutMs, label) {
31430
31918
  const controller = new AbortController;
31431
31919
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
31432
31920
  try {
31433
- return await withLocalEgress2("context7", () => fetch(input, { ...init, redirect: "error", signal: controller.signal }));
31921
+ const response = await withLocalEgress2("context7", () => fetch(input, { ...init, redirect: "error", signal: controller.signal }));
31922
+ if (!response.ok)
31923
+ throw httpStatusError(`${label} failed`, response);
31924
+ return await parseJsonResponse(response, label);
31434
31925
  } catch (error) {
31435
31926
  if (isAbortError(error)) {
31436
31927
  throw new Context7ProviderError("timeout", "Context7 provider request timed out", { retryable: true, cause: error });
@@ -32663,6 +33154,11 @@ async function runArchitectureDocsProjectionCommand(input, cwd, daemon) {
32663
33154
  try {
32664
33155
  const subcommand = input.action ?? "plan";
32665
33156
  const root = findRepositoryRoot(cwd);
33157
+ if (subcommand === "adopt") {
33158
+ const ignored = architectureModelIgnoredEnvelope("docs.adopt", root);
33159
+ if (ignored)
33160
+ return ignored;
33161
+ }
32666
33162
  let profile;
32667
33163
  let projection;
32668
33164
  let acceptedChange;
@@ -32778,6 +33274,23 @@ async function runArchitectureDocsProjectionCommand(input, cwd, daemon) {
32778
33274
  throw publicOperationFailure(error);
32779
33275
  }
32780
33276
  }
33277
+ function ignoredArchitectureModelPaths(root) {
33278
+ const ignored = ignoredRepositoryPaths(root, architectureModelIgnoreProbePaths(root));
33279
+ const directories = ignored.filter((entry) => entry.path.endsWith("/")).map((entry) => entry.path);
33280
+ return ignored.filter((entry) => entry.path.endsWith("/") || !directories.some((directory) => entry.path.startsWith(directory)));
33281
+ }
33282
+ function architectureModelIgnoredEnvelope(requestId, root) {
33283
+ const ignored = ignoredArchitectureModelPaths(root);
33284
+ if (ignored.length === 0)
33285
+ return;
33286
+ const paths = ignored.map((entry) => entry.path).slice(0, 8);
33287
+ return publicFailureEnvelope(requestId, "AC_PRECONDITION_FAILED", "architecture-model-ignored", {
33288
+ pathCount: ignored.length,
33289
+ paths,
33290
+ commands: [`git check-ignore --verbose --no-index -- ${paths.join(" ")}`],
33291
+ omittedPathCount: ignored.length - paths.length
33292
+ });
33293
+ }
32781
33294
  function committedArchitectureDocsProfile(root) {
32782
33295
  return existsSync9(resolve13(root, ARCHITECTURE_DOCS_PROJECTION_MANIFEST_PATH)) ? loadArchitectureDocumentationProfile(root) : "default";
32783
33296
  }
@@ -33088,6 +33601,11 @@ async function runProjectionProtocolCommand(invocation, cwd, daemon) {
33088
33601
  if (replay)
33089
33602
  return replay;
33090
33603
  }
33604
+ if (request.mode === "adopt") {
33605
+ const ignored = architectureModelIgnoredEnvelope("projection.run", root);
33606
+ if (ignored)
33607
+ return ignored;
33608
+ }
33091
33609
  let observed;
33092
33610
  let acceptedChange = request.acceptedChange;
33093
33611
  let declinedUnprovableNodeIds;
@@ -34652,6 +35170,7 @@ init_projection_recovery();
34652
35170
  init_projection_writer();
34653
35171
  init_projection_manifest();
34654
35172
  init_refactor();
35173
+ init_runtime_topology();
34655
35174
  init_schema();
34656
35175
  init_failure();
34657
35176
  init_validator();
@@ -39648,6 +40167,7 @@ class AgentJobService {
39648
40167
  supersededByJobId: input.supersededByJobId,
39649
40168
  now: input.now ?? this.context.clock()
39650
40169
  });
40170
+ this.context.abortOwnedAudit(job.job.jobId);
39651
40171
  return okEnvelope("jobs.cancel", { job });
39652
40172
  }
39653
40173
  async runtimeAgentJobInScope(scope, jobId) {
@@ -47060,7 +47580,7 @@ function upgradeExistingLocalStoreTarget2(paths, integrityCheck) {
47060
47580
  var RUNTIME_AGENT_JOB_STATUSES = ["queued", "running", "succeeded", "failed", "cancelled", "superseded", "expired"];
47061
47581
  var CHANGESET_MODEL_TRANSITION_SCHEMA_VERSION = "archcontext.changeset-model-transition/v1";
47062
47582
  function assertChangeSetModelTransition(value, label = "changeset-model-transition-invalid") {
47063
- if (!isJsonRecord2(value) || Object.keys(value).sort().join(",") !== "after,before,schemaVersion" || value.schemaVersion !== CHANGESET_MODEL_TRANSITION_SCHEMA_VERSION || typeof value.before !== "string" || !/^sha256:[a-f0-9]{64}$/.test(value.before) || typeof value.after !== "string" || !/^sha256:[a-f0-9]{64}$/.test(value.after)) {
47583
+ if (!isJsonRecord3(value) || Object.keys(value).sort().join(",") !== "after,before,schemaVersion" || value.schemaVersion !== CHANGESET_MODEL_TRANSITION_SCHEMA_VERSION || typeof value.before !== "string" || !/^sha256:[a-f0-9]{64}$/.test(value.before) || typeof value.after !== "string" || !/^sha256:[a-f0-9]{64}$/.test(value.after)) {
47064
47584
  throw new Error(label);
47065
47585
  }
47066
47586
  return { schemaVersion: CHANGESET_MODEL_TRANSITION_SCHEMA_VERSION, before: value.before, after: value.after };
@@ -51348,10 +51868,10 @@ function readChangeSetJournalTaskSessionId(metadataJson, journalId) {
51348
51868
  } catch {
51349
51869
  throw new Error(`changeset-journal-metadata-malformed: ${journalId}`);
51350
51870
  }
51351
- if (!isJsonRecord2(metadata))
51871
+ if (!isJsonRecord3(metadata))
51352
51872
  throw new Error(`changeset-journal-metadata-malformed: ${journalId}`);
51353
51873
  const reason = metadata.reason;
51354
- if (!isJsonRecord2(reason) || typeof reason.taskSessionId !== "string" || reason.taskSessionId === "") {
51874
+ if (!isJsonRecord3(reason) || typeof reason.taskSessionId !== "string" || reason.taskSessionId === "") {
51355
51875
  throw new Error(`changeset-journal-reason-malformed: ${journalId}`);
51356
51876
  }
51357
51877
  return reason.taskSessionId;
@@ -51384,7 +51904,7 @@ function committedChangeSetJournalFiles(filesJson, journalId) {
51384
51904
  if (!Array.isArray(parsed))
51385
51905
  throw new Error(`changeset-journal-files-malformed: ${journalId}`);
51386
51906
  return parsed.map((entry) => {
51387
- if (!isJsonRecord2(entry) || typeof entry.path !== "string" || typeof entry.operation !== "string") {
51907
+ if (!isJsonRecord3(entry) || typeof entry.path !== "string" || typeof entry.operation !== "string") {
51388
51908
  throw new Error(`changeset-journal-file-malformed: ${journalId}`);
51389
51909
  }
51390
51910
  if (typeof entry.bodyHash !== "string" || entry.bodyHash === "") {
@@ -51404,7 +51924,7 @@ function committedChangeSetProjectionOwned(metadataJson, journalId) {
51404
51924
  } catch {
51405
51925
  throw new Error(`changeset-journal-metadata-malformed: ${journalId}`);
51406
51926
  }
51407
- return isJsonRecord2(metadata) && metadata.projectionOwned === true;
51927
+ return isJsonRecord3(metadata) && metadata.projectionOwned === true;
51408
51928
  }
51409
51929
  function committedChangeSetModelTransition(metadataJson, journalId) {
51410
51930
  let metadata;
@@ -51413,7 +51933,7 @@ function committedChangeSetModelTransition(metadataJson, journalId) {
51413
51933
  } catch {
51414
51934
  throw new Error(`changeset-journal-metadata-malformed: ${journalId}`);
51415
51935
  }
51416
- if (!isJsonRecord2(metadata))
51936
+ if (!isJsonRecord3(metadata))
51417
51937
  throw new Error(`changeset-journal-metadata-malformed: ${journalId}`);
51418
51938
  if (metadata.modelTransition === undefined)
51419
51939
  return;
@@ -51427,7 +51947,7 @@ function readChangeSetJournalMetadata(db, journalId) {
51427
51947
  }
51428
51948
  function withChangeSetJournalArchitectureLedger(metadata, patch) {
51429
51949
  const existing = metadata.architectureLedger;
51430
- const architectureLedger = isJsonRecord2(existing) ? { ...existing, ...patch } : {
51950
+ const architectureLedger = isJsonRecord3(existing) ? { ...existing, ...patch } : {
51431
51951
  schemaVersion: "archcontext.changeset-ledger-recovery/v1",
51432
51952
  ...patch
51433
51953
  };
@@ -51449,12 +51969,12 @@ function changeSetJournalPlannedLedgerEvent(metadata) {
51449
51969
  const architectureLedger = metadata.architectureLedger;
51450
51970
  if (architectureLedger === undefined)
51451
51971
  return;
51452
- if (!isJsonRecord2(architectureLedger))
51972
+ if (!isJsonRecord3(architectureLedger))
51453
51973
  throw new Error("changeset-ledger-recovery-metadata-malformed");
51454
51974
  const plannedEvent = architectureLedger.plannedEvent;
51455
51975
  if (plannedEvent === undefined)
51456
51976
  return;
51457
- if (!isJsonRecord2(plannedEvent))
51977
+ if (!isJsonRecord3(plannedEvent))
51458
51978
  throw new Error("changeset-ledger-recovery-planned-event-malformed");
51459
51979
  const event = plannedEvent;
51460
51980
  try {
@@ -51464,7 +51984,7 @@ function changeSetJournalPlannedLedgerEvent(metadata) {
51464
51984
  }
51465
51985
  return event;
51466
51986
  }
51467
- function isJsonRecord2(value) {
51987
+ function isJsonRecord3(value) {
51468
51988
  return typeof value === "object" && value !== null && !Array.isArray(value);
51469
51989
  }
51470
51990
  function recoverJournalFiles(root, files) {
@@ -54391,7 +54911,7 @@ class ProjectionWriterFailure extends Error {
54391
54911
  function projectionWriterPaths(repositoryRoot, remote, base, env = process.env) {
54392
54912
  const key = createHash13("sha256").update(`${remote}\x00${base}`, "utf8").digest("hex").slice(0, 16);
54393
54913
  const dir = join23(runtimeStatePaths(repositoryRoot, env).repositoryStateDir, "projection-writer", key);
54394
- return { dir, worktree: join23(dir, "worktree"), state: join23(dir, "state.json"), lock: join23(dir, "lock") };
54914
+ return { dir, baseRef: `refs/archctx/projection-writer/${key}/base`, worktree: join23(dir, "worktree"), state: join23(dir, "state.json"), lock: join23(dir, "lock") };
54395
54915
  }
54396
54916
  async function runProjectionPublishCommand(options) {
54397
54917
  const env = options.env ?? process.env;
@@ -54483,10 +55003,21 @@ async function runLockedProjectionPublish(input) {
54483
55003
  const failed = (failure, fields = {}, extra = {}) => finish("failed", { ...fields, error: writerError(failure) }, extra);
54484
55004
  try {
54485
55005
  const pushRemote = projectionWriterPushUrl(repositoryRoot, remote);
55006
+ const fetchUrls = git(repositoryRoot, ["remote", "get-url", "--all", "--", remote]).trim().split(`
55007
+ `).filter(Boolean);
55008
+ const tracksPublication = fetchUrls.length === 1 && fetchUrls[0] === pushRemote;
54486
55009
  while (iterations < PROJECTION_WRITER_MAX_ITERATIONS) {
54487
55010
  iterations += 1;
54488
- remoteGit(repositoryRoot, ["fetch", "--no-tags", remote, `+refs/heads/${base}:refs/remotes/${remote}/${base}`], env);
54489
- baseSha = git(repositoryRoot, ["rev-parse", "--verify", `refs/remotes/${remote}/${base}^{commit}`]).trim();
55011
+ remoteGit(repositoryRoot, [
55012
+ "fetch",
55013
+ "--no-tags",
55014
+ "--no-write-fetch-head",
55015
+ "--",
55016
+ pushRemote,
55017
+ `+refs/heads/${base}:${paths.baseRef}`,
55018
+ ...tracksPublication ? [`+refs/heads/${base}:refs/remotes/${remote}/${base}`] : []
55019
+ ], env);
55020
+ baseSha = git(repositoryRoot, ["rev-parse", "--verify", `${paths.baseRef}^{commit}`]).trim();
54490
55021
  const successMatches = state.lastSuccessfulBranch === branch && state.lastSuccessfulSha === baseSha;
54491
55022
  const pending = state.publisherPending?.branch === branch && state.publisherPending.baseSha === baseSha ? state.publisherPending : successMatches && state.lastSuccessfulCommit && state.lastSuccessfulPublisher !== publisherIdentity ? { branch, baseSha, commitSha: state.lastSuccessfulCommit, files: state.lastSuccessfulFiles } : null;
54492
55023
  if (!pending && successMatches && state.lastSuccessfulPublisher === publisherIdentity && (state.lastSuccessfulCommit === null || lsRemoteBranch(repositoryRoot, pushRemote, branch, env) === state.lastSuccessfulCommit)) {
@@ -54535,8 +55066,15 @@ async function runLockedProjectionPublish(input) {
54535
55066
  throw new ProjectionWriterFailure("AC_PRECONDITION_FAILED", "projection-apply-status-unsupported", `projection apply ended with status ${projectionStatus}; the writer publishes only noop, applied and applied-partial results`);
54536
55067
  }
54537
55068
  const changed = worktreeChangedPaths(paths.worktree);
54538
- if (changed.length === 0)
55069
+ if (changed.length === 0) {
55070
+ const tip = lsRemoteBranch(repositoryRoot, pushRemote, base, env);
55071
+ if (tip === null) {
55072
+ return failed(new ProjectionWriterFailure("AC_PRECONDITION_FAILED", "projection-writer-base-missing", `${remote} has no branch ${base}`), { projectionStatus });
55073
+ }
55074
+ if (tip !== baseSha)
55075
+ continue;
54539
55076
  return finish("up-to-date", { projectionStatus });
55077
+ }
54540
55078
  const written = new Set(result.files.filter((file) => file.action !== "unchanged").map((file) => file.path));
54541
55079
  const offending = changed.filter((path) => !written.has(path) || !isProjectionOwnedPath(path, ownedPatterns));
54542
55080
  if (offending.length > 0) {
@@ -54551,7 +55089,7 @@ async function runLockedProjectionPublish(input) {
54551
55089
  return failed(new ProjectionWriterFailure("AC_PRECONDITION_FAILED", "projection-writer-commit-failed", `git commit failed in the writer worktree; configure a git identity (user.name, user.email) for the repository or set GIT_AUTHOR_*/GIT_COMMITTER_*: ${errorMessage2(error)}`), { projectionStatus, files: changed });
54552
55090
  }
54553
55091
  const commitSha = git(paths.worktree, ["rev-parse", "HEAD"]).trim();
54554
- const tip = lsRemoteBranch(repositoryRoot, remote, base, env);
55092
+ const tip = lsRemoteBranch(repositoryRoot, pushRemote, base, env);
54555
55093
  if (tip === null) {
54556
55094
  return failed(new ProjectionWriterFailure("AC_PRECONDITION_FAILED", "projection-writer-base-missing", `${remote} has no branch ${base}`), { projectionStatus, files: changed });
54557
55095
  }
@@ -54900,8 +55438,8 @@ function createGhProjectionPublisher(options = {}) {
54900
55438
  const env = { ...stringEnv(process.env), GH_PROMPT_DISABLED: "1" };
54901
55439
  delete env.GH_REPO;
54902
55440
  const gh = (args) => withLocalEgress2("projection-publishing", () => exec("gh", [...args, "--repo", repository], { cwd: input.worktree, env, timeoutMs }), admissionEnv);
54903
- const listed = await gh(["pr", "list", "--head", input.branch, "--base", input.base, "--state", "open", "--json", "number,url", "--limit", "1"]);
54904
- const open = parseGhPullRequestList(listed.stdout);
55441
+ const listed = await gh(["pr", "list", "--head", input.branch, "--base", input.base, "--state", "open", "--json", "number,url,isCrossRepository,headRefName,baseRefName", "--limit", "100"]);
55442
+ const open = parseGhPullRequestList(listed.stdout, input.branch, input.base);
54905
55443
  if (open) {
54906
55444
  await gh(["pr", "edit", String(open.number), "--title", input.title, "--body-file", input.bodyFile]);
54907
55445
  return { number: open.number, ...open.url ? { url: open.url } : {} };
@@ -55049,16 +55587,24 @@ function readCommandFirstLine(command) {
55049
55587
  closeSync9(fd);
55050
55588
  }
55051
55589
  }
55052
- function parseGhPullRequestList(stdout) {
55053
- const parsed = JSON.parse(stdout.trim() === "" ? "[]" : stdout);
55590
+ function parseGhPullRequestList(stdout, branch, base) {
55591
+ const parsed = JSON.parse(stdout);
55054
55592
  if (!Array.isArray(parsed))
55055
55593
  throw new Error("gh pr list returned an unexpected shape");
55056
- const first = parsed[0];
55057
- if (!first)
55058
- return;
55059
- if (typeof first.number !== "number" || !Number.isInteger(first.number) || first.number <= 0)
55060
- throw new Error("gh pr list returned a pull request without a number");
55061
- return { number: first.number, ...typeof first.url === "string" && first.url !== "" ? { url: first.url } : {} };
55594
+ if (parsed.length >= 100)
55595
+ throw new Error("gh pr list reached the 100-result limit; resolve rolling PR ambiguity manually");
55596
+ const matches = [];
55597
+ for (const row of parsed) {
55598
+ if (!row || typeof row !== "object" || Array.isArray(row) || typeof row.isCrossRepository !== "boolean" || row.headRefName !== branch || row.baseRefName !== base || typeof row.number !== "number" || !Number.isInteger(row.number) || row.number <= 0) {
55599
+ throw new Error("gh pr list returned a pull request without verified identity");
55600
+ }
55601
+ if (!row.isCrossRepository) {
55602
+ matches.push({ number: row.number, ...typeof row.url === "string" && row.url !== "" ? { url: row.url } : {} });
55603
+ }
55604
+ }
55605
+ if (matches.length > 1)
55606
+ throw new Error("gh pr list returned multiple same-repository rolling pull requests");
55607
+ return matches[0];
55062
55608
  }
55063
55609
  function parsePublisherCommandOutput(stdout) {
55064
55610
  let parsed;
@@ -57954,23 +58500,31 @@ class ArchctxRuntimeRpcServer {
57954
58500
  this.server = undefined;
57955
58501
  const connection = this.connection;
57956
58502
  this.connection = undefined;
58503
+ const errors = [];
58504
+ const cleanup = async (action) => {
58505
+ try {
58506
+ await action();
58507
+ } catch (error) {
58508
+ errors.push(error);
58509
+ }
58510
+ };
57957
58511
  if (server) {
57958
- await new Promise((resolveClose, rejectClose) => {
58512
+ await cleanup(() => new Promise((resolveClose, rejectClose) => {
57959
58513
  server.close((error) => error ? rejectClose(error) : resolveClose());
57960
- });
57961
- }
57962
- await this.daemon.stop();
57963
- try {
57964
- if (connection)
57965
- await removePrivateControlFile(connection.connectionPath);
57966
- } finally {
57967
- if (this.lockFd !== undefined)
57968
- closeSync11(this.lockFd);
57969
- this.lockFd = undefined;
58514
+ }));
57970
58515
  }
58516
+ await cleanup(() => this.daemon.stop());
57971
58517
  if (connection)
57972
- await removePrivateControlFile(connection.lockPath);
57973
- this.options.onStop?.();
58518
+ await cleanup(() => removePrivateControlFile(connection.connectionPath));
58519
+ const lockFd = this.lockFd;
58520
+ this.lockFd = undefined;
58521
+ if (lockFd !== undefined)
58522
+ await cleanup(() => closeSync11(lockFd));
58523
+ if (connection)
58524
+ await cleanup(() => removePrivateControlFile(connection.lockPath));
58525
+ await cleanup(() => this.options.onStop?.());
58526
+ if (errors.length > 0)
58527
+ throw errors[0];
57974
58528
  }
57975
58529
  armIdleTimer() {
57976
58530
  if (this.idleTimeoutMs <= 0 || !this.server)
@@ -58486,6 +59040,7 @@ class ArchctxDaemon {
58486
59040
  localStore: this.localStore
58487
59041
  });
58488
59042
  this.agentJobs = new AgentJobService({
59043
+ abortOwnedAudit: (jobId) => this.auditRunAbortControllers.get(jobId)?.abort(),
58489
59044
  assertRunning: () => this.assertRunning(),
58490
59045
  openSession: (root) => this.openSession(root),
58491
59046
  architectureLedgerScope: (root) => this.architectureLedgerScope(root),
@@ -59312,11 +59867,13 @@ class ArchctxDaemon {
59312
59867
  return okEnvelope("status", status);
59313
59868
  const repositoryId = repositoryFingerprint(root);
59314
59869
  const session = this.sessions.get(repositoryId);
59870
+ const ignoredModelPaths = ignoredArchitectureModelPaths(root);
59315
59871
  return okEnvelope("status", {
59316
59872
  ...status,
59317
59873
  repositoryId,
59318
59874
  headSha: session?.workspace.headSha ?? readHeadSha(root),
59319
- worktreeDigest: computeWorktreeDigest(root, listGitVisibleFiles(root))
59875
+ worktreeDigest: computeWorktreeDigest(root, listGitVisibleFiles(root)),
59876
+ ...ignoredModelPaths.length === 0 ? {} : { architectureModelIgnoredPaths: ignoredModelPaths }
59320
59877
  });
59321
59878
  } catch (error) {
59322
59879
  throw publicOperationFailure(error);
@@ -60031,26 +60588,19 @@ function exportLikeC4Model(model) {
60031
60588
  const normalized = normalizeNativeModel(model);
60032
60589
  const lines = [
60033
60590
  "// Generated by ArchContext. Native model remains source of truth.",
60034
- "specification {",
60035
- " element capability",
60036
- " element module",
60037
- " element component",
60038
- " element interface",
60039
- " element datastore",
60040
- " element external_system",
60041
- "}",
60591
+ ...likeC4SpecificationLines(normalized.nodes),
60042
60592
  "",
60043
60593
  "model {"
60044
60594
  ];
60045
60595
  for (const node of normalized.nodes) {
60046
60596
  lines.push(` // archctx-node ${JSON.stringify(node)}`);
60047
- lines.push(` ${likec4Id(node.id)} = ${likec4Kind(node.kind)} "${escapeDsl2(node.name)}"`);
60597
+ lines.push(` ${likec4Id(node.id)} = ${likeC4ElementKind(node.kind)} "${escapeDsl2(node.name)}"`);
60048
60598
  }
60049
60599
  for (const relation of normalized.relations) {
60050
60600
  lines.push(` // archctx-relation ${JSON.stringify(relation)}`);
60051
60601
  lines.push(` ${likec4Id(relation.source)} -> ${likec4Id(relation.target)} "${escapeDsl2(relation.kind)}"`);
60052
60602
  }
60053
- lines.push("}", "", "views {", " view index {", " include *", " autoLayout TopBottom", " }", "}");
60603
+ lines.push("}", "", ...likeC4IndexViewLines(architectureLayerGroups(normalized), likec4Id));
60054
60604
  const content = `${lines.join(`
60055
60605
  `)}
60056
60606
  `;
@@ -60080,15 +60630,6 @@ function importLikeC4InitialModel(content) {
60080
60630
  function likec4Id(id) {
60081
60631
  return stableId(id).replace(/-/g, "_").replace(/\./g, "_").replace(/::/g, "__");
60082
60632
  }
60083
- function likec4Kind(kind) {
60084
- if (kind === "external-system")
60085
- return "external_system";
60086
- if (kind === "datastore")
60087
- return "datastore";
60088
- if (["capability", "module", "component", "interface"].includes(kind))
60089
- return kind;
60090
- return "component";
60091
- }
60092
60633
  function escapeDsl2(value) {
60093
60634
  return value.replace(/"/g, "'");
60094
60635
  }
@@ -60097,17 +60638,22 @@ function escapeDsl2(value) {
60097
60638
  init_src();
60098
60639
  function exportStructurizrWorkspace(model, name = "ArchContext Export") {
60099
60640
  const normalized = normalizeNativeModel(model);
60641
+ const { layerByNodeId } = architectureLayerGroups(normalized);
60100
60642
  const workspace = {
60101
60643
  schemaVersion: "archcontext.structurizr-export/v1",
60102
60644
  name,
60103
60645
  model: {
60104
- elements: normalized.nodes.map((node) => ({
60105
- id: node.id,
60106
- name: node.name,
60107
- type: structurizrElementType(node.kind),
60108
- tags: [node.kind],
60109
- archcontextNative: node
60110
- })),
60646
+ elements: normalized.nodes.map((node) => {
60647
+ const layer = layerByNodeId.get(node.id);
60648
+ return {
60649
+ id: node.id,
60650
+ name: node.name,
60651
+ type: architectureStructurizrElementType(node.kind),
60652
+ tags: [node.kind],
60653
+ ...layer === undefined ? {} : { group: layer },
60654
+ archcontextNative: node
60655
+ };
60656
+ }),
60111
60657
  relationships: normalized.relations.map((relation) => ({
60112
60658
  id: relation.id,
60113
60659
  sourceId: relation.source,
@@ -60151,17 +60697,6 @@ function importStructurizrInitialModel(content) {
60151
60697
  warnings: ["Structurizr import is initialization-only; Native ArchContext remains source of truth."]
60152
60698
  };
60153
60699
  }
60154
- function structurizrElementType(kind) {
60155
- if (kind === "capability")
60156
- return "Software System";
60157
- if (kind === "module")
60158
- return "Container";
60159
- if (kind === "datastore")
60160
- return "Database";
60161
- if (kind === "external-system")
60162
- return "External System";
60163
- return "Component";
60164
- }
60165
60700
 
60166
60701
  // packages/surfaces/mcp-local/src/tool-schemas.ts
60167
60702
  var text2 = { type: "string", minLength: 1 };
@@ -60735,17 +61270,49 @@ var architecture_node_schema_default2 = {
60735
61270
  type: "object",
60736
61271
  additionalProperties: false,
60737
61272
  required: ["schemaVersion", "id", "kind", "name", "status", "summary"],
61273
+ allOf: [
61274
+ {
61275
+ if: { properties: { kind: { const: "namespace" } }, required: ["kind"] },
61276
+ then: { required: ["namespace"] },
61277
+ else: { not: { required: ["namespace"] } }
61278
+ },
61279
+ {
61280
+ if: { properties: { kind: { const: "mount" } }, required: ["kind"] },
61281
+ then: { required: ["mount"] },
61282
+ else: { not: { required: ["mount"] } }
61283
+ }
61284
+ ],
60738
61285
  properties: {
60739
61286
  schemaVersion: { const: "archcontext.node/v2" },
60740
61287
  id: { type: "string", pattern: "^([a-z][a-z0-9]*(\\.[a-z0-9][a-z0-9-]*)*::)?[a-z][a-z0-9]*(\\.[a-z0-9][a-z0-9-]*)+$" },
60741
61288
  kind: {
60742
61289
  type: "string",
60743
- enum: ["capability", "module", "component", "interface", "event", "datastore", "external-system", "deployment"]
61290
+ enum: ["capability", "module", "component", "interface", "event", "datastore", "external-system", "deployment", "process", "namespace", "mount"]
60744
61291
  },
60745
61292
  name: { type: "string" },
60746
61293
  status: { type: "string", enum: ["active", "planned", "deprecated", "removed"] },
60747
61294
  parent: { type: "string", pattern: "^([a-z][a-z0-9]*(\\.[a-z0-9][a-z0-9-]*)*::)?[a-z][a-z0-9]*(\\.[a-z0-9][a-z0-9-]*)+$" },
61295
+ layer: { type: "string", pattern: "^[a-z][a-z0-9]*(-[a-z0-9]+)*$" },
60748
61296
  summary: { type: "string" },
61297
+ namespace: {
61298
+ type: "object",
61299
+ additionalProperties: false,
61300
+ required: ["type"],
61301
+ properties: {
61302
+ type: { type: "string", enum: ["host", "container", "pod", "kubernetes", "pid", "mount", "net", "user"] }
61303
+ }
61304
+ },
61305
+ mount: {
61306
+ type: "object",
61307
+ additionalProperties: false,
61308
+ required: ["target", "mode", "order"],
61309
+ properties: {
61310
+ source: { type: "string", minLength: 1 },
61311
+ target: { type: "string", pattern: "^/([^/\\u0000]+(/[^/\\u0000]+)*)?$" },
61312
+ mode: { type: "string", enum: ["ro", "rw"] },
61313
+ order: { type: "integer", minimum: 0 }
61314
+ }
61315
+ },
60749
61316
  responsibilities: { type: "array", items: { type: "string" }, minItems: 1 },
60750
61317
  source: {
60751
61318
  type: "object",