dsh-archify-projects 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.md +204 -2
  2. package/cordis.patch.yml +99 -0
  3. package/dist/client.js +2261 -0
  4. package/dist/client.js.map +1 -0
  5. package/docs/CONTAINER.md +197 -0
  6. package/docs/DESIGN.md +905 -0
  7. package/docs/HANDOVER.md +391 -0
  8. package/docs/PUBLISH.md +107 -0
  9. package/icon.svg +12 -0
  10. package/lib/host/archify-skill.js +116 -0
  11. package/lib/host/archify-skill.js.map +1 -0
  12. package/lib/host/config.js +95 -0
  13. package/lib/host/config.js.map +1 -0
  14. package/lib/host/context.js +28 -0
  15. package/lib/host/context.js.map +1 -0
  16. package/lib/host/git.js +185 -0
  17. package/lib/host/git.js.map +1 -0
  18. package/lib/host/http.js +184 -0
  19. package/lib/host/http.js.map +1 -0
  20. package/lib/host/index.js +90 -0
  21. package/lib/host/index.js.map +1 -0
  22. package/lib/host/policy.js +124 -0
  23. package/lib/host/policy.js.map +1 -0
  24. package/lib/host/routes.js +667 -0
  25. package/lib/host/routes.js.map +1 -0
  26. package/lib/host/runner.js +913 -0
  27. package/lib/host/runner.js.map +1 -0
  28. package/lib/host/store.js +370 -0
  29. package/lib/host/store.js.map +1 -0
  30. package/lib/host/workspace.js +65 -0
  31. package/lib/host/workspace.js.map +1 -0
  32. package/lib/shared/format.js +138 -0
  33. package/lib/shared/format.js.map +1 -0
  34. package/lib/shared/prompt.js +257 -0
  35. package/lib/shared/prompt.js.map +1 -0
  36. package/lib/shared/protocol.js +31 -0
  37. package/lib/shared/protocol.js.map +1 -0
  38. package/lib/shared/types.js +173 -0
  39. package/lib/shared/types.js.map +1 -0
  40. package/locale/en.json +6 -0
  41. package/locale/zh.json +6 -0
  42. package/package.json +98 -4
  43. package/skills/archify/LICENSE +22 -0
  44. package/skills/archify/SKILL.md +109 -0
  45. package/skills/archify/THIRD_PARTY_NOTICES.md +69 -0
  46. package/skills/archify/assets/JetBrainsMono-OFL.txt +93 -0
  47. package/skills/archify/assets/template.html +13709 -0
  48. package/skills/archify/bin/archify.mjs +6936 -0
  49. package/skills/archify/bin/delivery-update.mjs +91 -0
  50. package/skills/archify/bin/finalize.mjs +895 -0
  51. package/skills/archify/bin/open-artifact.mjs +146 -0
  52. package/skills/archify/bin/preview.mjs +969 -0
  53. package/skills/archify/bin/recover-output.mjs +41 -0
  54. package/skills/archify/bin/visual-check.mjs +2667 -0
  55. package/skills/archify/brand-marks/README.md +31 -0
  56. package/skills/archify/brand-marks/catalog.json +131 -0
  57. package/skills/archify/delta/architecture-delta.mjs +1313 -0
  58. package/skills/archify/examples/agent-run.lifecycle.json +55 -0
  59. package/skills/archify/examples/agent-tool-call.workflow.json +74 -0
  60. package/skills/archify/examples/async-job-roundtrip.sequence.json +56 -0
  61. package/skills/archify/examples/brand-aware-delivery.architecture.json +34 -0
  62. package/skills/archify/examples/cache-miss-request.sequence.json +77 -0
  63. package/skills/archify/examples/checkout-platform.base.architecture.json +32 -0
  64. package/skills/archify/examples/checkout-platform.head.architecture.json +32 -0
  65. package/skills/archify/examples/dataflow-product-analytics.html +13820 -0
  66. package/skills/archify/examples/deployment-release.lifecycle.json +48 -0
  67. package/skills/archify/examples/event-stream.dataflow.json +52 -0
  68. package/skills/archify/examples/holiday-planning.workflow.json +143 -0
  69. package/skills/archify/examples/incident-response.workflow.json +59 -0
  70. package/skills/archify/examples/lifecycle-agent-run.html +13765 -0
  71. package/skills/archify/examples/locales/de.json +425 -0
  72. package/skills/archify/examples/locales/es.json +440 -0
  73. package/skills/archify/examples/locales/fr.json +425 -0
  74. package/skills/archify/examples/locales/fr.partial.json +22 -0
  75. package/skills/archify/examples/locales/it.json +425 -0
  76. package/skills/archify/examples/locales/ja.json +425 -0
  77. package/skills/archify/examples/locales/ko.json +440 -0
  78. package/skills/archify/examples/locales/pt.json +425 -0
  79. package/skills/archify/examples/locales/ru.json +425 -0
  80. package/skills/archify/examples/product-analytics.dataflow.json +71 -0
  81. package/skills/archify/examples/production-deployment.architecture.json +51 -0
  82. package/skills/archify/examples/release-delivery.workflow.json +57 -0
  83. package/skills/archify/examples/sequence-cache-miss-request.html +13835 -0
  84. package/skills/archify/examples/web-app-rendered.html +13808 -0
  85. package/skills/archify/examples/web-app.architecture.json +41 -0
  86. package/skills/archify/examples/workflow-agent-tool-call-rendered.html +13826 -0
  87. package/skills/archify/migrations/workflow-v2.mjs +280 -0
  88. package/skills/archify/package.json +17 -0
  89. package/skills/archify/recipes/scenarios.mjs +430 -0
  90. package/skills/archify/references/architecture-layout-repair.md +18 -0
  91. package/skills/archify/references/authoring-contract.md +419 -0
  92. package/skills/archify/references/authoring-defaults.md +41 -0
  93. package/skills/archify/references/brand-marks.md +82 -0
  94. package/skills/archify/references/delivery-contract.md +588 -0
  95. package/skills/archify/references/repository-authoring.md +105 -0
  96. package/skills/archify/references/update-awareness.md +12 -0
  97. package/skills/archify/references/viewer-runtime.md +35 -0
  98. package/skills/archify/renderers/architecture/grid.mjs +62 -0
  99. package/skills/archify/renderers/architecture/labels.mjs +215 -0
  100. package/skills/archify/renderers/architecture/render-architecture.mjs +1001 -0
  101. package/skills/archify/renderers/architecture/routing.mjs +1157 -0
  102. package/skills/archify/renderers/dataflow/README.md +105 -0
  103. package/skills/archify/renderers/dataflow/render-dataflow.mjs +544 -0
  104. package/skills/archify/renderers/lifecycle/README.md +171 -0
  105. package/skills/archify/renderers/lifecycle/grid-routing.mjs +429 -0
  106. package/skills/archify/renderers/lifecycle/render-lifecycle.mjs +938 -0
  107. package/skills/archify/renderers/sequence/README.md +129 -0
  108. package/skills/archify/renderers/sequence/render-sequence.mjs +526 -0
  109. package/skills/archify/renderers/shared/atomic-output.mjs +1982 -0
  110. package/skills/archify/renderers/shared/brand-marks.mjs +670 -0
  111. package/skills/archify/renderers/shared/cli.mjs +451 -0
  112. package/skills/archify/renderers/shared/desktop-readability.mjs +132 -0
  113. package/skills/archify/renderers/shared/diagnostics.mjs +173 -0
  114. package/skills/archify/renderers/shared/engineering-profiles.mjs +157 -0
  115. package/skills/archify/renderers/shared/generated-brand-marks.mjs +2003 -0
  116. package/skills/archify/renderers/shared/generated-validators.mjs +134 -0
  117. package/skills/archify/renderers/shared/geometry.mjs +1941 -0
  118. package/skills/archify/renderers/shared/i18n.mjs +603 -0
  119. package/skills/archify/renderers/shared/layout-report.mjs +42 -0
  120. package/skills/archify/renderers/shared/legend.mjs +217 -0
  121. package/skills/archify/renderers/shared/output-path.mjs +440 -0
  122. package/skills/archify/renderers/shared/path-semantics.mjs +1017 -0
  123. package/skills/archify/renderers/shared/portable-path.mjs +408 -0
  124. package/skills/archify/renderers/shared/repository-evidence.mjs +352 -0
  125. package/skills/archify/renderers/shared/repository-location.mjs +59 -0
  126. package/skills/archify/renderers/shared/route-quality.mjs +614 -0
  127. package/skills/archify/renderers/shared/sidecar-path.mjs +38 -0
  128. package/skills/archify/renderers/shared/spatial-grid.mjs +66 -0
  129. package/skills/archify/renderers/shared/text-fit.mjs +105 -0
  130. package/skills/archify/renderers/shared/utils.mjs +254 -0
  131. package/skills/archify/renderers/shared/validator.mjs +86 -0
  132. package/skills/archify/renderers/workflow/README.md +301 -0
  133. package/skills/archify/renderers/workflow/render-workflow.mjs +37 -0
  134. package/skills/archify/renderers/workflow/workflow-compiler.mjs +4913 -0
  135. package/skills/archify/renderers/workflow/workflow-migration-geometry.mjs +144 -0
  136. package/skills/archify/schemas/README.md +256 -0
  137. package/skills/archify/schemas/architecture.schema.json +152 -0
  138. package/skills/archify/schemas/common.schema.json +181 -0
  139. package/skills/archify/schemas/dataflow.schema.json +254 -0
  140. package/skills/archify/schemas/lifecycle.schema.json +280 -0
  141. package/skills/archify/schemas/sequence.schema.json +234 -0
  142. package/skills/archify/schemas/workflow.schema.json +439 -0
  143. package/skills/archify/scripts/check-render-output.mjs +1388 -0
  144. package/skills/archify/scripts/check-update.mjs +1710 -0
  145. package/skills/archify/scripts/delivery-update-child.mjs +23 -0
  146. package/skills/archify/scripts/render-examples.mjs +26 -0
  147. package/skills/archify/scripts/update-contract.mjs +182 -0
  148. package/skills/archify/skill-release.json +10 -0
@@ -0,0 +1,173 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ const DIAGNOSTIC_MODE = process.env.ARCHIFY_DIAGNOSTIC_FORMAT === 'json';
5
+ const recorded = [];
6
+ const recordedMessages = new Set();
7
+ const boundaryKey = Symbol.for('archify.renderer-diagnostic-boundary');
8
+ let recordingSuppressionDepth = 0;
9
+
10
+ function plainObject(value) {
11
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
12
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
13
+ }
14
+
15
+ function normalizedDiagnostic(diagnostic) {
16
+ const message = String(diagnostic?.message || 'Archify could not classify this failure.').trim();
17
+ return {
18
+ code: String(diagnostic?.code || 'internal/unclassified'),
19
+ severity: diagnostic?.severity === 'warning' ? 'warning' : 'error',
20
+ message,
21
+ subject: plainObject(diagnostic?.subject),
22
+ evidence: plainObject(diagnostic?.evidence),
23
+ supportedFixes: Array.isArray(diagnostic?.supportedFixes)
24
+ ? [...new Set(diagnostic.supportedFixes.map((fix) => String(fix).trim()).filter(Boolean))]
25
+ : [],
26
+ ...(Array.isArray(diagnostic?.suppresses) ? {
27
+ suppresses: [...new Set(diagnostic.suppresses.map((code) => String(code).trim()).filter(Boolean))],
28
+ } : {}),
29
+ };
30
+ }
31
+
32
+ export function recordDiagnostic(diagnostic) {
33
+ if (!DIAGNOSTIC_MODE || recordingSuppressionDepth > 0) return;
34
+ const normalized = normalizedDiagnostic(diagnostic);
35
+ if (recordedMessages.has(normalized.message)) return;
36
+ recordedMessages.add(normalized.message);
37
+ recorded.push(normalized);
38
+ }
39
+
40
+ export function withDiagnosticRecordingSuppressed(callback) {
41
+ recordingSuppressionDepth += 1;
42
+ try {
43
+ return callback();
44
+ } finally {
45
+ recordingSuppressionDepth -= 1;
46
+ }
47
+ }
48
+
49
+ export function throwDiagnosticError(message, diagnostics) {
50
+ for (const diagnostic of diagnostics || []) recordDiagnostic(diagnostic);
51
+ const error = new Error(message);
52
+ error.archifyDiagnostics = (diagnostics || []).map(normalizedDiagnostic);
53
+ throw error;
54
+ }
55
+
56
+ export function throwDiagnosticProblems(prefix, problems, { code = 'layout/constraint', subject = {}, diagnostics: details = [] } = {}) {
57
+ const messages = (problems || []).map((problem) => String(problem));
58
+ const byMessage = new Map(details.map((entry) => [entry.message, entry]));
59
+ const diagnostics = messages.map((message) => normalizedDiagnostic(byMessage.get(message) || {
60
+ code,
61
+ severity: 'error',
62
+ message,
63
+ subject,
64
+ evidence: {},
65
+ supportedFixes: [],
66
+ }));
67
+ throwDiagnosticError(`${prefix}:\n- ${messages.join('\n- ')}`, diagnostics);
68
+ }
69
+
70
+ function fallbackDiagnostic(error) {
71
+ const input = process.argv[2] ? path.resolve(process.argv[2]) : undefined;
72
+ return normalizedDiagnostic({
73
+ code: 'internal/unclassified',
74
+ severity: 'error',
75
+ message: error?.message || 'Renderer failed without a diagnostic.',
76
+ subject: { input },
77
+ evidence: { errorName: error?.name || 'Error' },
78
+ supportedFixes: [],
79
+ });
80
+ }
81
+ export function rendererFailure(error) {
82
+ const attached = Array.isArray(error?.archifyDiagnostics)
83
+ ? error.archifyDiagnostics.map(normalizedDiagnostic)
84
+ : [];
85
+ // Earlier diagnostics do not classify a later, unrelated implementation error.
86
+ const diagnostics = attached.length
87
+ ? (recorded.length ? recorded : attached)
88
+ : [fallbackDiagnostic(error)];
89
+ return {
90
+ schemaVersion: 1,
91
+ ok: false,
92
+ source: 'renderer',
93
+ error: error?.message || 'Renderer failed without a diagnostic.',
94
+ diagnostics,
95
+ };
96
+ }
97
+
98
+ // Match the public CLI's text format without making its standalone doctor
99
+ // bootstrap depend on this renderer runtime being present.
100
+ function formatDiagnostics(error, diagnostics = []) {
101
+ if (!diagnostics.length) return error;
102
+ return [
103
+ error,
104
+ ...diagnostics.map((entry) => {
105
+ const fix = entry.supportedFixes?.length ? ` Fix: ${entry.supportedFixes.join('; ')}.` : '';
106
+ return `[${entry.code}] ${entry.message}${fix}`;
107
+ }),
108
+ ].join('\n');
109
+ }
110
+
111
+ const readerSignal = new Int32Array(new SharedArrayBuffer(4));
112
+
113
+ function waitForReader() {
114
+ // Sleep instead of spinning on EAGAIN. A retry budget looks like a safeguard
115
+ // and behaves like a truncation gate: a spinning loop burns thousands of
116
+ // attempts in a few milliseconds, so a reader that is merely slow to start
117
+ // exhausts it and loses the tail of the receipt. Waiting costs nothing while
118
+ // the reader catches up, and a reader that goes away raises EPIPE, which the
119
+ // caller already treats as a real write failure.
120
+ Atomics.wait(readerSignal, 0, 0, 1);
121
+ }
122
+
123
+ export function installRendererDiagnosticBoundary() {
124
+ if (globalThis[boundaryKey]) return;
125
+ globalThis[boundaryKey] = true;
126
+ if (!DIAGNOSTIC_MODE) {
127
+ process.once('uncaughtException', (error) => {
128
+ // Only errors classified at their operation boundary are author-facing.
129
+ // Preserve Node's debugging information for unexpected implementation errors.
130
+ if (!error?.archifyDiagnostics?.length) {
131
+ // The once-listener is already removed. Rethrow outside the exception
132
+ // handler so Node retains its normal stack and exit code (not code 7).
133
+ process.nextTick(() => { throw error; });
134
+ return;
135
+ }
136
+ const payload = `${formatDiagnostics(error.message, error.archifyDiagnostics)}\n`;
137
+ process.stderr.once('error', () => process.exit(1));
138
+ process.stderr.write(payload, () => process.exit(1));
139
+ });
140
+ return;
141
+ }
142
+ process.on('uncaughtException', (error) => {
143
+ const payload = `${JSON.stringify(rendererFailure(error))}\n`;
144
+ try {
145
+ // stderr may be a pipe. fs.writeSync performs a PARTIAL write once the
146
+ // payload exceeds the OS pipe buffer (8KB on macOS) and returns the byte
147
+ // count actually written. Ignoring that return value silently truncated
148
+ // large diagnostic payloads mid-JSON, so the parent CLI's JSON.parse
149
+ // failed and the fail-closed boundary reported internal/unclassified
150
+ // instead of the diagnostics we had already computed. Loop until drained.
151
+ // A full pipe also makes writeSync throw EAGAIN; wait for the reader
152
+ // rather than treat it as a stream failure, otherwise the tail is
153
+ // dropped just the same.
154
+ const buffer = Buffer.from(payload, 'utf8');
155
+ let written = 0;
156
+ while (written < buffer.length) {
157
+ try {
158
+ written += fs.writeSync(process.stderr.fd, buffer, written, buffer.length - written);
159
+ } catch (writeError) {
160
+ if (writeError?.code === 'EAGAIN') {
161
+ waitForReader();
162
+ continue;
163
+ }
164
+ throw writeError;
165
+ }
166
+ }
167
+ } catch {
168
+ // The renderer is already failing. Avoid replacing its real error with a
169
+ // secondary stream failure; the parent CLI still has the exit status.
170
+ }
171
+ process.exit(1);
172
+ });
173
+ }
@@ -0,0 +1,157 @@
1
+ import { throwDiagnosticError } from './diagnostics.mjs';
2
+
3
+ const DEPLOYMENT_PROFILE = 'deployment-ownership';
4
+ const DEPLOYMENT_BOUNDARY_KINDS = new Set(['region', 'security-group']);
5
+ const PRIVATE_STATE_TYPES = new Set(['database']);
6
+
7
+ function subject(collection, index, item = {}) {
8
+ return {
9
+ diagramType: 'architecture',
10
+ profile: DEPLOYMENT_PROFILE,
11
+ collection,
12
+ index,
13
+ ...(item.id ? { id: item.id } : {}),
14
+ };
15
+ }
16
+
17
+ function membership(boundaries, componentId, kind) {
18
+ return boundaries
19
+ .map((boundary, index) => ({ boundary, index }))
20
+ .filter(({ boundary }) => boundary.kind === kind && boundary.wraps.includes(componentId));
21
+ }
22
+
23
+ export function deploymentOwnershipDiagnostics(diagram) {
24
+ const components = Array.isArray(diagram.components) ? diagram.components : [];
25
+ const boundaries = (Array.isArray(diagram.boundaries) ? diagram.boundaries : [])
26
+ .map((boundary) => ({ ...boundary, wraps: Array.isArray(boundary.wraps) ? boundary.wraps : [] }));
27
+ const connections = Array.isArray(diagram.connections) ? diagram.connections : [];
28
+ const diagnostics = [];
29
+
30
+ for (const kind of DEPLOYMENT_BOUNDARY_KINDS) {
31
+ const count = boundaries.filter((boundary) => boundary.kind === kind).length;
32
+ if (count > 0) continue;
33
+ diagnostics.push({
34
+ code: 'engineering/deployment-boundary-kind',
35
+ severity: 'error',
36
+ message: `Deployment ownership requires at least one ${kind} boundary.`,
37
+ subject: subject('boundaries', -1),
38
+ evidence: { requiredKind: kind, found: count },
39
+ supportedFixes: [`add one ${kind} boundary with an explicit wraps list`],
40
+ });
41
+ }
42
+
43
+ components.forEach((component, index) => {
44
+ if (component.type === 'external') return;
45
+ if (typeof component.tag !== 'string' || component.tag.trim() === '') {
46
+ diagnostics.push({
47
+ code: 'engineering/deployment-owner-missing',
48
+ severity: 'error',
49
+ message: `Deployment component ${JSON.stringify(component.id)} does not name its owner in tag.`,
50
+ subject: subject('components', index, component),
51
+ evidence: { componentType: component.type, ownerField: 'tag' },
52
+ supportedFixes: [`set /components/${index}/tag to the responsible team or owner`],
53
+ });
54
+ }
55
+
56
+ const regions = membership(boundaries, component.id, 'region');
57
+ if (regions.length === 0) {
58
+ diagnostics.push({
59
+ code: 'engineering/deployment-region-scope',
60
+ severity: 'error',
61
+ message: `Deployment component ${JSON.stringify(component.id)} is not assigned to a region boundary.`,
62
+ subject: subject('components', index, component),
63
+ evidence: { componentType: component.type, regionMemberships: 0 },
64
+ supportedFixes: ['add the component id to the real region boundary wraps list'],
65
+ });
66
+ } else if (regions.length > 1) {
67
+ diagnostics.push({
68
+ code: 'engineering/deployment-region-ambiguous',
69
+ severity: 'error',
70
+ message: `Deployment component ${JSON.stringify(component.id)} belongs to more than one region boundary.`,
71
+ subject: subject('components', index, component),
72
+ evidence: {
73
+ componentType: component.type,
74
+ regions: regions.map(({ boundary, index: boundaryIndex }) => ({ boundaryIndex, label: boundary.label })),
75
+ },
76
+ supportedFixes: ['keep the component id in exactly one real region boundary wraps list'],
77
+ });
78
+ }
79
+
80
+ if (PRIVATE_STATE_TYPES.has(component.type)) {
81
+ const privateScopes = membership(boundaries, component.id, 'security-group');
82
+ if (privateScopes.length === 0) {
83
+ diagnostics.push({
84
+ code: 'engineering/deployment-private-state',
85
+ severity: 'error',
86
+ message: `Stateful component ${JSON.stringify(component.id)} is not assigned to a private security-group boundary.`,
87
+ subject: subject('components', index, component),
88
+ evidence: { componentType: component.type, privateMemberships: 0 },
89
+ supportedFixes: ['add the component id to the real private security-group boundary wraps list'],
90
+ });
91
+ }
92
+ }
93
+ });
94
+
95
+ boundaries.forEach((boundary, index) => {
96
+ if (boundary.kind !== 'security-group') return;
97
+ const members = boundary.wraps.map((id) => ({
98
+ id,
99
+ regions: membership(boundaries, id, 'region').map(({ boundary: region, index: boundaryIndex }) => ({
100
+ boundaryIndex,
101
+ label: region.label,
102
+ })),
103
+ }));
104
+ const regionIndexes = new Set(members.flatMap((member) => member.regions.map((region) => region.boundaryIndex)));
105
+ const consistent = members.length > 0
106
+ && members.every((member) => member.regions.length === 1)
107
+ && regionIndexes.size === 1;
108
+ if (consistent) return;
109
+ diagnostics.push({
110
+ code: 'engineering/deployment-private-region-consistency',
111
+ severity: 'error',
112
+ message: `Private boundary ${JSON.stringify(boundary.label)} must contain components from exactly one shared region.`,
113
+ subject: subject('boundaries', index, boundary),
114
+ evidence: { boundaryKind: boundary.kind, members },
115
+ supportedFixes: ['assign every private-boundary component to exactly one shared region boundary'],
116
+ });
117
+ });
118
+
119
+ connections.forEach((connection, index) => {
120
+ const crossedBoundaries = boundaries
121
+ .map((boundary, boundaryIndex) => ({
122
+ boundaryIndex,
123
+ kind: boundary.kind,
124
+ label: boundary.label,
125
+ fromInside: boundary.wraps.includes(connection.from),
126
+ toInside: boundary.wraps.includes(connection.to),
127
+ }))
128
+ .filter((boundary) => DEPLOYMENT_BOUNDARY_KINDS.has(boundary.kind) && boundary.fromInside !== boundary.toInside);
129
+ if (crossedBoundaries.length === 0 || (typeof connection.label === 'string' && connection.label.trim() !== '')) return;
130
+ diagnostics.push({
131
+ code: 'engineering/deployment-crossing-mechanism',
132
+ severity: 'error',
133
+ message: `Cross-boundary connection ${JSON.stringify(connection.id || `${connection.from}->${connection.to}`)} does not name its mechanism.`,
134
+ subject: subject('connections', index, connection),
135
+ evidence: {
136
+ from: connection.from,
137
+ to: connection.to,
138
+ crossedBoundaries: crossedBoundaries.map(({ boundaryIndex, kind, label }) => ({ boundaryIndex, kind, label })),
139
+ },
140
+ supportedFixes: [`set /connections/${index}/label to the real cross-boundary mechanism`],
141
+ });
142
+ });
143
+
144
+ return diagnostics;
145
+ }
146
+
147
+ export function validateEngineeringProfile(diagramType, diagram) {
148
+ const profile = diagram.meta?.engineering_profile;
149
+ if (!profile) return;
150
+ if (diagramType !== 'architecture' || profile !== DEPLOYMENT_PROFILE) return;
151
+ const diagnostics = deploymentOwnershipDiagnostics(diagram);
152
+ if (!diagnostics.length) return;
153
+ throwDiagnosticError(
154
+ `Engineering profile ${JSON.stringify(profile)} failed:\n${diagnostics.map((entry) => `- ${entry.message}`).join('\n')}`,
155
+ diagnostics,
156
+ );
157
+ }