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.
- package/README.md +204 -2
- package/cordis.patch.yml +99 -0
- package/dist/client.js +2261 -0
- package/dist/client.js.map +1 -0
- package/docs/CONTAINER.md +197 -0
- package/docs/DESIGN.md +905 -0
- package/docs/HANDOVER.md +391 -0
- package/docs/PUBLISH.md +107 -0
- package/icon.svg +12 -0
- package/lib/host/archify-skill.js +116 -0
- package/lib/host/archify-skill.js.map +1 -0
- package/lib/host/config.js +95 -0
- package/lib/host/config.js.map +1 -0
- package/lib/host/context.js +28 -0
- package/lib/host/context.js.map +1 -0
- package/lib/host/git.js +185 -0
- package/lib/host/git.js.map +1 -0
- package/lib/host/http.js +184 -0
- package/lib/host/http.js.map +1 -0
- package/lib/host/index.js +90 -0
- package/lib/host/index.js.map +1 -0
- package/lib/host/policy.js +124 -0
- package/lib/host/policy.js.map +1 -0
- package/lib/host/routes.js +667 -0
- package/lib/host/routes.js.map +1 -0
- package/lib/host/runner.js +913 -0
- package/lib/host/runner.js.map +1 -0
- package/lib/host/store.js +370 -0
- package/lib/host/store.js.map +1 -0
- package/lib/host/workspace.js +65 -0
- package/lib/host/workspace.js.map +1 -0
- package/lib/shared/format.js +138 -0
- package/lib/shared/format.js.map +1 -0
- package/lib/shared/prompt.js +257 -0
- package/lib/shared/prompt.js.map +1 -0
- package/lib/shared/protocol.js +31 -0
- package/lib/shared/protocol.js.map +1 -0
- package/lib/shared/types.js +173 -0
- package/lib/shared/types.js.map +1 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +98 -4
- package/skills/archify/LICENSE +22 -0
- package/skills/archify/SKILL.md +109 -0
- package/skills/archify/THIRD_PARTY_NOTICES.md +69 -0
- package/skills/archify/assets/JetBrainsMono-OFL.txt +93 -0
- package/skills/archify/assets/template.html +13709 -0
- package/skills/archify/bin/archify.mjs +6936 -0
- package/skills/archify/bin/delivery-update.mjs +91 -0
- package/skills/archify/bin/finalize.mjs +895 -0
- package/skills/archify/bin/open-artifact.mjs +146 -0
- package/skills/archify/bin/preview.mjs +969 -0
- package/skills/archify/bin/recover-output.mjs +41 -0
- package/skills/archify/bin/visual-check.mjs +2667 -0
- package/skills/archify/brand-marks/README.md +31 -0
- package/skills/archify/brand-marks/catalog.json +131 -0
- package/skills/archify/delta/architecture-delta.mjs +1313 -0
- package/skills/archify/examples/agent-run.lifecycle.json +55 -0
- package/skills/archify/examples/agent-tool-call.workflow.json +74 -0
- package/skills/archify/examples/async-job-roundtrip.sequence.json +56 -0
- package/skills/archify/examples/brand-aware-delivery.architecture.json +34 -0
- package/skills/archify/examples/cache-miss-request.sequence.json +77 -0
- package/skills/archify/examples/checkout-platform.base.architecture.json +32 -0
- package/skills/archify/examples/checkout-platform.head.architecture.json +32 -0
- package/skills/archify/examples/dataflow-product-analytics.html +13820 -0
- package/skills/archify/examples/deployment-release.lifecycle.json +48 -0
- package/skills/archify/examples/event-stream.dataflow.json +52 -0
- package/skills/archify/examples/holiday-planning.workflow.json +143 -0
- package/skills/archify/examples/incident-response.workflow.json +59 -0
- package/skills/archify/examples/lifecycle-agent-run.html +13765 -0
- package/skills/archify/examples/locales/de.json +425 -0
- package/skills/archify/examples/locales/es.json +440 -0
- package/skills/archify/examples/locales/fr.json +425 -0
- package/skills/archify/examples/locales/fr.partial.json +22 -0
- package/skills/archify/examples/locales/it.json +425 -0
- package/skills/archify/examples/locales/ja.json +425 -0
- package/skills/archify/examples/locales/ko.json +440 -0
- package/skills/archify/examples/locales/pt.json +425 -0
- package/skills/archify/examples/locales/ru.json +425 -0
- package/skills/archify/examples/product-analytics.dataflow.json +71 -0
- package/skills/archify/examples/production-deployment.architecture.json +51 -0
- package/skills/archify/examples/release-delivery.workflow.json +57 -0
- package/skills/archify/examples/sequence-cache-miss-request.html +13835 -0
- package/skills/archify/examples/web-app-rendered.html +13808 -0
- package/skills/archify/examples/web-app.architecture.json +41 -0
- package/skills/archify/examples/workflow-agent-tool-call-rendered.html +13826 -0
- package/skills/archify/migrations/workflow-v2.mjs +280 -0
- package/skills/archify/package.json +17 -0
- package/skills/archify/recipes/scenarios.mjs +430 -0
- package/skills/archify/references/architecture-layout-repair.md +18 -0
- package/skills/archify/references/authoring-contract.md +419 -0
- package/skills/archify/references/authoring-defaults.md +41 -0
- package/skills/archify/references/brand-marks.md +82 -0
- package/skills/archify/references/delivery-contract.md +588 -0
- package/skills/archify/references/repository-authoring.md +105 -0
- package/skills/archify/references/update-awareness.md +12 -0
- package/skills/archify/references/viewer-runtime.md +35 -0
- package/skills/archify/renderers/architecture/grid.mjs +62 -0
- package/skills/archify/renderers/architecture/labels.mjs +215 -0
- package/skills/archify/renderers/architecture/render-architecture.mjs +1001 -0
- package/skills/archify/renderers/architecture/routing.mjs +1157 -0
- package/skills/archify/renderers/dataflow/README.md +105 -0
- package/skills/archify/renderers/dataflow/render-dataflow.mjs +544 -0
- package/skills/archify/renderers/lifecycle/README.md +171 -0
- package/skills/archify/renderers/lifecycle/grid-routing.mjs +429 -0
- package/skills/archify/renderers/lifecycle/render-lifecycle.mjs +938 -0
- package/skills/archify/renderers/sequence/README.md +129 -0
- package/skills/archify/renderers/sequence/render-sequence.mjs +526 -0
- package/skills/archify/renderers/shared/atomic-output.mjs +1982 -0
- package/skills/archify/renderers/shared/brand-marks.mjs +670 -0
- package/skills/archify/renderers/shared/cli.mjs +451 -0
- package/skills/archify/renderers/shared/desktop-readability.mjs +132 -0
- package/skills/archify/renderers/shared/diagnostics.mjs +173 -0
- package/skills/archify/renderers/shared/engineering-profiles.mjs +157 -0
- package/skills/archify/renderers/shared/generated-brand-marks.mjs +2003 -0
- package/skills/archify/renderers/shared/generated-validators.mjs +134 -0
- package/skills/archify/renderers/shared/geometry.mjs +1941 -0
- package/skills/archify/renderers/shared/i18n.mjs +603 -0
- package/skills/archify/renderers/shared/layout-report.mjs +42 -0
- package/skills/archify/renderers/shared/legend.mjs +217 -0
- package/skills/archify/renderers/shared/output-path.mjs +440 -0
- package/skills/archify/renderers/shared/path-semantics.mjs +1017 -0
- package/skills/archify/renderers/shared/portable-path.mjs +408 -0
- package/skills/archify/renderers/shared/repository-evidence.mjs +352 -0
- package/skills/archify/renderers/shared/repository-location.mjs +59 -0
- package/skills/archify/renderers/shared/route-quality.mjs +614 -0
- package/skills/archify/renderers/shared/sidecar-path.mjs +38 -0
- package/skills/archify/renderers/shared/spatial-grid.mjs +66 -0
- package/skills/archify/renderers/shared/text-fit.mjs +105 -0
- package/skills/archify/renderers/shared/utils.mjs +254 -0
- package/skills/archify/renderers/shared/validator.mjs +86 -0
- package/skills/archify/renderers/workflow/README.md +301 -0
- package/skills/archify/renderers/workflow/render-workflow.mjs +37 -0
- package/skills/archify/renderers/workflow/workflow-compiler.mjs +4913 -0
- package/skills/archify/renderers/workflow/workflow-migration-geometry.mjs +144 -0
- package/skills/archify/schemas/README.md +256 -0
- package/skills/archify/schemas/architecture.schema.json +152 -0
- package/skills/archify/schemas/common.schema.json +181 -0
- package/skills/archify/schemas/dataflow.schema.json +254 -0
- package/skills/archify/schemas/lifecycle.schema.json +280 -0
- package/skills/archify/schemas/sequence.schema.json +234 -0
- package/skills/archify/schemas/workflow.schema.json +439 -0
- package/skills/archify/scripts/check-render-output.mjs +1388 -0
- package/skills/archify/scripts/check-update.mjs +1710 -0
- package/skills/archify/scripts/delivery-update-child.mjs +23 -0
- package/skills/archify/scripts/render-examples.mjs +26 -0
- package/skills/archify/scripts/update-contract.mjs +182 -0
- 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
|
+
}
|