@stacksjs/defaults 0.75.361 → 0.75.366
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/ai/skills/stacks-ai/SKILL.md +22 -0
- package/ai/skills/stacks-native/CATALOG.md +5 -5
- package/ai/skills/stacks-payments/SKILL.md +10 -0
- package/ai/skills/stacks-technical-diagrams/LICENSE +202 -22
- package/ai/skills/stacks-technical-diagrams/LICENSE-MIT +22 -0
- package/ai/skills/stacks-technical-diagrams/NOTICE +26 -0
- package/ai/skills/stacks-technical-diagrams/SKILL.md +106 -266
- package/ai/skills/stacks-technical-diagrams/THIRD_PARTY_NOTICES.md +11 -2
- package/ai/skills/stacks-technical-diagrams/UPSTREAM.json +12 -0
- package/ai/skills/stacks-technical-diagrams/agents/openai.yaml +2 -2
- package/ai/skills/stacks-technical-diagrams/assets/template.html +3655 -3798
- package/ai/skills/stacks-technical-diagrams/bin/archify.mjs +7290 -0
- package/ai/skills/stacks-technical-diagrams/bin/delivery-update.mjs +7 -0
- package/ai/skills/stacks-technical-diagrams/bin/diagrams +1 -0
- package/ai/skills/stacks-technical-diagrams/bin/finalize.mjs +1056 -0
- package/ai/skills/stacks-technical-diagrams/bin/open-artifact.mjs +72 -12
- package/ai/skills/stacks-technical-diagrams/bin/preview.mjs +348 -31
- package/ai/skills/stacks-technical-diagrams/bin/recover-output.mjs +41 -0
- package/ai/skills/stacks-technical-diagrams/bin/technical-diagrams.mjs +3 -2097
- package/ai/skills/stacks-technical-diagrams/bin/visual-check.mjs +2142 -157
- package/ai/skills/stacks-technical-diagrams/brand-marks/README.md +3 -3
- package/ai/skills/stacks-technical-diagrams/delta/architecture-delta.mjs +130 -33
- package/ai/skills/stacks-technical-diagrams/examples/agent-run.lifecycle.json +25 -30
- package/ai/skills/stacks-technical-diagrams/examples/agent-tool-call.workflow.json +0 -20
- package/ai/skills/stacks-technical-diagrams/examples/archify-dev-activity.timeline.json +176 -0
- package/ai/skills/stacks-technical-diagrams/examples/archify-repository.tree.json +197 -0
- package/ai/skills/stacks-technical-diagrams/examples/async-job-roundtrip.sequence.json +1 -6
- package/ai/skills/stacks-technical-diagrams/examples/brand-aware-delivery.architecture.json +3 -16
- package/ai/skills/stacks-technical-diagrams/examples/cache-miss-request.sequence.json +2 -7
- package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.base.architecture.json +2 -1
- package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.head.architecture.json +2 -1
- package/ai/skills/stacks-technical-diagrams/examples/checkout-request.waterfall.json +20 -0
- package/ai/skills/stacks-technical-diagrams/examples/class-payment-processors-rendered.html +14987 -0
- package/ai/skills/stacks-technical-diagrams/examples/class-payments-rendered.html +14787 -0
- package/ai/skills/stacks-technical-diagrams/examples/dataflow-product-analytics.html +14880 -0
- package/ai/skills/stacks-technical-diagrams/examples/delivery.explainer.json +155 -0
- package/ai/skills/stacks-technical-diagrams/examples/deployment-release.lifecycle.json +57 -34
- package/ai/skills/stacks-technical-diagrams/examples/erd-orders-rendered.html +15112 -0
- package/ai/skills/stacks-technical-diagrams/examples/event-stream.dataflow.json +1 -6
- package/ai/skills/stacks-technical-diagrams/examples/example-rebuild.waterfall.json +212 -0
- package/ai/skills/stacks-technical-diagrams/examples/holiday-planning.workflow.json +143 -0
- package/ai/skills/stacks-technical-diagrams/examples/incident-response.workflow.json +1 -6
- package/ai/skills/stacks-technical-diagrams/examples/lifecycle-agent-run.html +14892 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/de.json +463 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/fr.json +463 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/fr.partial.json +22 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/it.json +463 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/ja.json +463 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/pt.json +463 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/ru.json +463 -0
- package/ai/skills/stacks-technical-diagrams/examples/orders.erd.json +337 -0
- package/ai/skills/stacks-technical-diagrams/examples/payment-incident.timeline.json +24 -0
- package/ai/skills/stacks-technical-diagrams/examples/payment-platform.tree.json +22 -0
- package/ai/skills/stacks-technical-diagrams/examples/payment-processors.class.json +177 -0
- package/ai/skills/stacks-technical-diagrams/examples/payments.class.json +85 -0
- package/ai/skills/stacks-technical-diagrams/examples/product-analytics.dataflow.json +1 -6
- package/ai/skills/stacks-technical-diagrams/examples/production-deployment.architecture.json +3 -23
- package/ai/skills/stacks-technical-diagrams/examples/release-delivery.workflow.json +1 -6
- package/ai/skills/stacks-technical-diagrams/examples/sequence-cache-miss-request.html +14895 -0
- package/ai/skills/stacks-technical-diagrams/examples/subscription-billing-rendered.html +15369 -0
- package/ai/skills/stacks-technical-diagrams/examples/subscription-billing.erd.json +548 -0
- package/ai/skills/stacks-technical-diagrams/examples/timeline-archify-dev-activity-rendered.html +15002 -0
- package/ai/skills/stacks-technical-diagrams/examples/timeline-payment-incident-rendered.html +14772 -0
- package/ai/skills/stacks-technical-diagrams/examples/tree-archify-repository-rendered.html +15071 -0
- package/ai/skills/stacks-technical-diagrams/examples/tree-payment-platform-rendered.html +14810 -0
- package/ai/skills/stacks-technical-diagrams/examples/waterfall-checkout-request-rendered.html +14754 -0
- package/ai/skills/stacks-technical-diagrams/examples/waterfall-example-rebuild-rendered.html +14940 -0
- package/ai/skills/stacks-technical-diagrams/examples/web-app-rendered.html +14889 -0
- package/ai/skills/stacks-technical-diagrams/examples/web-app.architecture.json +1 -6
- package/ai/skills/stacks-technical-diagrams/examples/workflow-agent-tool-call-rendered.html +14907 -0
- package/ai/skills/stacks-technical-diagrams/explainer/browser-check.mjs +208 -0
- package/ai/skills/stacks-technical-diagrams/explainer/charts.mjs +70 -0
- package/ai/skills/stacks-technical-diagrams/explainer/check.mjs +168 -0
- package/ai/skills/stacks-technical-diagrams/explainer/diagrams.mjs +518 -0
- package/ai/skills/stacks-technical-diagrams/explainer/diff.mjs +124 -0
- package/ai/skills/stacks-technical-diagrams/explainer/inline.mjs +155 -0
- package/ai/skills/stacks-technical-diagrams/explainer/layout.mjs +35 -0
- package/ai/skills/stacks-technical-diagrams/explainer/page-bundle.mjs +16 -0
- package/ai/skills/stacks-technical-diagrams/explainer/parse.mjs +300 -0
- package/ai/skills/stacks-technical-diagrams/explainer/patch.mjs +117 -0
- package/ai/skills/stacks-technical-diagrams/explainer/prepare.mjs +131 -0
- package/ai/skills/stacks-technical-diagrams/explainer/reader.css +472 -0
- package/ai/skills/stacks-technical-diagrams/explainer/reader.js +667 -0
- package/ai/skills/stacks-technical-diagrams/explainer/render.mjs +224 -0
- package/ai/skills/stacks-technical-diagrams/explainer/revision.mjs +20 -0
- package/ai/skills/stacks-technical-diagrams/explainer/sources.mjs +318 -0
- package/ai/skills/stacks-technical-diagrams/explainer/validate.mjs +138 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/LICENSE +40 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/README.md +5 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/3DKXF73K.js +4 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/42UTT654.js +3 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/DKTLEL5E.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/EEIX5ZYH.js +2 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/EJ5KOXYD.js +25 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/GUJTEFZK.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/IZOKQ2GT.js +4 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/L4LY553T.js +3 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/MSKIJHB2.js +2 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/RVARI57O.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/UKSIWDDN.js +2 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/UPTXNAXN.js +2 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/XTDUIGUI.js +9 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/YAYTP6L5.js +9 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/core.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/pie.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/quadrantChart.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/radar.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/sankey.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/venn.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/xychart.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/writing.mjs +139 -0
- package/ai/skills/stacks-technical-diagrams/locales/README.md +40 -0
- package/ai/skills/stacks-technical-diagrams/locales/ar.json +488 -0
- package/ai/skills/stacks-technical-diagrams/locales/en.json +488 -0
- package/ai/skills/stacks-technical-diagrams/locales/es.json +488 -0
- package/ai/skills/stacks-technical-diagrams/locales/ko.json +488 -0
- package/ai/skills/stacks-technical-diagrams/locales/manifest.json +10 -0
- package/ai/skills/stacks-technical-diagrams/locales/zh-CN.json +488 -0
- package/ai/skills/stacks-technical-diagrams/locales/zh-TW.json +488 -0
- package/ai/skills/stacks-technical-diagrams/migrations/workflow-v2.mjs +1 -0
- package/ai/skills/stacks-technical-diagrams/recipes/scenarios.mjs +115 -51
- package/ai/skills/stacks-technical-diagrams/references/architecture-layout-repair.md +18 -0
- package/ai/skills/stacks-technical-diagrams/references/authoring-contract.md +453 -51
- package/ai/skills/stacks-technical-diagrams/references/authoring-defaults.md +57 -0
- package/ai/skills/stacks-technical-diagrams/references/brand-marks.md +57 -3
- package/ai/skills/stacks-technical-diagrams/references/delivery-contract.md +603 -58
- package/ai/skills/stacks-technical-diagrams/references/explainer-authoring.md +274 -0
- package/ai/skills/stacks-technical-diagrams/references/repository-authoring.md +126 -0
- package/ai/skills/stacks-technical-diagrams/references/update-awareness.md +13 -0
- package/ai/skills/stacks-technical-diagrams/references/viewer-runtime.md +18 -11
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/grid.mjs +1 -1
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/labels.mjs +2 -0
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/render-architecture.mjs +550 -424
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/routing.mjs +1469 -0
- package/ai/skills/stacks-technical-diagrams/renderers/class/README.md +129 -0
- package/ai/skills/stacks-technical-diagrams/renderers/class/render-class.mjs +1095 -0
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/README.md +89 -17
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/dataflow-compiler.mjs +1605 -0
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/joint-label-routing.mjs +165 -0
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/label-capacity.mjs +81 -0
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/render-dataflow.mjs +14 -474
- package/ai/skills/stacks-technical-diagrams/renderers/erd/README.md +246 -0
- package/ai/skills/stacks-technical-diagrams/renderers/erd/grid.mjs +150 -0
- package/ai/skills/stacks-technical-diagrams/renderers/erd/render-erd.mjs +1588 -0
- package/ai/skills/stacks-technical-diagrams/renderers/explainer/render-explainer.mjs +42 -0
- package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/README.md +83 -94
- package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/render-lifecycle.mjs +990 -417
- package/ai/skills/stacks-technical-diagrams/renderers/sequence/README.md +49 -17
- package/ai/skills/stacks-technical-diagrams/renderers/sequence/render-sequence.mjs +735 -94
- package/ai/skills/stacks-technical-diagrams/renderers/shared/atomic-output.mjs +1982 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/automatic-labels.mjs +290 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/brand-marks.mjs +169 -26
- package/ai/skills/stacks-technical-diagrams/renderers/shared/cli.mjs +266 -54
- package/ai/skills/stacks-technical-diagrams/renderers/shared/desktop-readability.mjs +106 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/diagnostics.mjs +0 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/generated-validators.mjs +268 -2
- package/ai/skills/stacks-technical-diagrams/renderers/shared/geometry.mjs +774 -124
- package/ai/skills/stacks-technical-diagrams/renderers/shared/i18n.mjs +289 -556
- package/ai/skills/stacks-technical-diagrams/renderers/shared/layout-report.mjs +18 -1
- package/ai/skills/stacks-technical-diagrams/renderers/shared/legend.mjs +16 -6
- package/ai/skills/stacks-technical-diagrams/renderers/shared/node-card-style.mjs +32 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/output-path.mjs +329 -229
- package/ai/skills/stacks-technical-diagrams/renderers/shared/owned-temp-directory.mjs +82 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/path-semantics.mjs +1017 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/portable-path.mjs +408 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-evidence.mjs +235 -79
- package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-location.mjs +17 -4
- package/ai/skills/stacks-technical-diagrams/renderers/shared/route-quality.mjs +670 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/sidecar-path.mjs +38 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/software-notices.mjs +6 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/spatial-grid.mjs +66 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/svg-path-analysis.mjs +135 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/text-fit.mjs +59 -3
- package/ai/skills/stacks-technical-diagrams/renderers/shared/utils.mjs +63 -35
- package/ai/skills/stacks-technical-diagrams/renderers/shared/validator.mjs +2 -5
- package/ai/skills/stacks-technical-diagrams/renderers/timeline/README.md +76 -0
- package/ai/skills/stacks-technical-diagrams/renderers/timeline/render-timeline.mjs +576 -0
- package/ai/skills/stacks-technical-diagrams/renderers/tree/README.md +96 -0
- package/ai/skills/stacks-technical-diagrams/renderers/tree/render-tree.mjs +573 -0
- package/ai/skills/stacks-technical-diagrams/renderers/waterfall/README.md +73 -0
- package/ai/skills/stacks-technical-diagrams/renderers/waterfall/render-waterfall.mjs +413 -0
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/README.md +137 -23
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/render-workflow.mjs +18 -6
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-compiler.mjs +2230 -710
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-migration-geometry.mjs +17 -0
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-placement-repair.mjs +94 -0
- package/ai/skills/stacks-technical-diagrams/schemas/README.md +87 -40
- package/ai/skills/stacks-technical-diagrams/schemas/architecture.schema.json +7 -33
- package/ai/skills/stacks-technical-diagrams/schemas/class.schema.json +345 -0
- package/ai/skills/stacks-technical-diagrams/schemas/common.schema.json +68 -2
- package/ai/skills/stacks-technical-diagrams/schemas/dataflow.schema.json +14 -3
- package/ai/skills/stacks-technical-diagrams/schemas/erd.schema.json +381 -0
- package/ai/skills/stacks-technical-diagrams/schemas/explainer.schema.json +927 -0
- package/ai/skills/stacks-technical-diagrams/schemas/lifecycle.schema.json +23 -110
- package/ai/skills/stacks-technical-diagrams/schemas/sequence.schema.json +15 -4
- package/ai/skills/stacks-technical-diagrams/schemas/timeline.schema.json +160 -0
- package/ai/skills/stacks-technical-diagrams/schemas/tree.schema.json +141 -0
- package/ai/skills/stacks-technical-diagrams/schemas/waterfall.schema.json +147 -0
- package/ai/skills/stacks-technical-diagrams/schemas/workflow.schema.json +14 -3
- package/ai/skills/stacks-technical-diagrams/scripts/check-render-output.mjs +876 -243
- package/ai/skills/stacks-technical-diagrams/scripts/check-update.mjs +1717 -0
- package/ai/skills/stacks-technical-diagrams/scripts/delivery-update-child.mjs +26 -0
- package/ai/skills/stacks-technical-diagrams/scripts/generate-brand-marks.mjs +141 -0
- package/ai/skills/stacks-technical-diagrams/scripts/generate-validators.mjs +105 -0
- package/ai/skills/stacks-technical-diagrams/scripts/render-examples.mjs +11 -1
- package/ai/skills/stacks-technical-diagrams/scripts/update-contract.mjs +187 -0
- package/ai/skills/stacks-technical-diagrams/skill-release.json +10 -0
- package/ai/skills/stacks-testing/SKILL.md +8 -1
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import { runtimeArgs } from '../renderers/shared/bun-runtime.mjs';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { execFile } from 'node:child_process';
|
|
7
|
+
import { promisify } from 'node:util';
|
|
8
|
+
import { validateSchema } from '../renderers/shared/validator.mjs';
|
|
9
|
+
import { textUnits, SEMANTIC_SIGIL_FOOTPRINT } from '../renderers/shared/utils.mjs';
|
|
10
|
+
import { minimumNodeTextWidth } from '../renderers/shared/text-fit.mjs';
|
|
11
|
+
import { throwDiagnosticError } from '../renderers/shared/diagnostics.mjs';
|
|
12
|
+
import { createOwnedTempDirectory } from '../renderers/shared/owned-temp-directory.mjs';
|
|
13
|
+
|
|
14
|
+
const runFile = promisify(execFile);
|
|
15
|
+
const skillRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
16
|
+
const TYPES = new Set(['architecture', 'workflow', 'sequence', 'dataflow', 'lifecycle', 'erd', 'class', 'tree', 'timeline', 'waterfall']);
|
|
17
|
+
const MAX_SPEC_BYTES = 4 * 1024 * 1024;
|
|
18
|
+
const MAX_DIAGRAM_BYTES = 32 * 1024 * 1024;
|
|
19
|
+
const ID = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
|
|
20
|
+
const hash = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
21
|
+
const freezeBytes = bytes => ({ sha256: hash(bytes), bytes: bytes.length, contentBase64: bytes.toString('base64') });
|
|
22
|
+
|
|
23
|
+
function subject(context, pointer, extra = {}) {
|
|
24
|
+
const map = context.sourceMap;
|
|
25
|
+
let lookup = pointer, location;
|
|
26
|
+
while (lookup && !location) {
|
|
27
|
+
location = map instanceof Map ? map.get(lookup) : map?.[lookup];
|
|
28
|
+
lookup = lookup.slice(0, lookup.lastIndexOf('/'));
|
|
29
|
+
}
|
|
30
|
+
location ||= {};
|
|
31
|
+
return { component: 'diagram', line: location.line || context.line || 1, path: pointer,
|
|
32
|
+
...(location.column ? { column: location.column } : {}), ...(context.sourcePath ? { input: context.sourcePath } : {}),
|
|
33
|
+
...(context.panelId ? { panelId: context.panelId } : {}), ...extra };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function fail(context, reason, message, example, fixes = [], cause) {
|
|
37
|
+
throwDiagnosticError(message, [{ code: `explainer/${reason}`, severity: 'error', message,
|
|
38
|
+
subject: subject(context, context.pointer || '/', context.subject),
|
|
39
|
+
evidence: { reason, example: example || 'Use a native diagram spec with matching diagram_type.', ...(cause ? { native: cause } : {}) },
|
|
40
|
+
supportedFixes: fixes.length ? fixes : [`Use this form: ${example || 'native JSON'}`] }]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function safeId(value, context) {
|
|
44
|
+
if (!ID.test(value || '')) fail(context, 'diagram-id', 'Diagram identities must use the native identifier syntax.', 'api | API | backend');
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function inferredId(label) {
|
|
49
|
+
const slug = label.normalize('NFKC').toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
50
|
+
return ID.test(slug) ? slug : `actor-${hash(label).slice(0, 12)}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function rowParts(text) {
|
|
54
|
+
let value = text.trim();
|
|
55
|
+
if (value.startsWith('|') && value.endsWith('|')) value = value.slice(1, -1);
|
|
56
|
+
return value.split('|').map(part => part.trim());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function duplicateCheck(items, context, kind) {
|
|
60
|
+
const seen = new Set();
|
|
61
|
+
for (const item of items) {
|
|
62
|
+
if (seen.has(item.id)) fail(context, 'diagram-id-duplicate', `Duplicate ${kind} identity ${item.id}.`, 'Use a unique id for every declaration.');
|
|
63
|
+
seen.add(item.id);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Estimated native workflow canvas width for a column assignment: the widest node per column plus the
|
|
68
|
+
// column gaps, which widen for edge labels. Measured within about 10% of real canvases. The native gate
|
|
69
|
+
// projects 8px edge labels onto a 930px reader, so 1240px is the hard limit; this keeps a margin.
|
|
70
|
+
const READABLE_WORKFLOW_WIDTH = 1100;
|
|
71
|
+
function workflowWidth(nodes, edges) {
|
|
72
|
+
const columns = Math.max(...nodes.map(node => node.col)) + 1;
|
|
73
|
+
const widest = Array.from({ length: columns }, (_, col) => Math.max(92, ...nodes.filter(node => node.col === col).map(node => node.width || 0)));
|
|
74
|
+
const colOf = new Map(nodes.map(node => [node.id, node.col]));
|
|
75
|
+
const gaps = Array.from({ length: columns - 1 }, (_, col) => Math.max(34, ...edges.filter(edge => colOf.get(edge.from) === col && colOf.get(edge.to) === col + 1).map(edge => (edge.label ? textUnits(edge.label) * 5.4 : 0) + 16)));
|
|
76
|
+
return widest.reduce((sum, width) => sum + width, 0) + gaps.reduce((sum, gap) => sum + gap, 0) + 80;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Placement supplies native grid/lane hints only. Native renderers own sizing,
|
|
80
|
+
// ports, routes, labels, and all quality validation.
|
|
81
|
+
function rankedPlacement(nodes, edges) {
|
|
82
|
+
const ids = new Set(nodes.map(node => node.id));
|
|
83
|
+
const incoming = new Map(nodes.map(node => [node.id, 0]));
|
|
84
|
+
for (const edge of edges) if (ids.has(edge.from) && ids.has(edge.to) && edge.from !== edge.to) incoming.set(edge.to, incoming.get(edge.to) + 1);
|
|
85
|
+
const rank = new Map();
|
|
86
|
+
const queue = nodes.filter(node => incoming.get(node.id) === 0).map(node => node.id);
|
|
87
|
+
const visit = id => {
|
|
88
|
+
if (!rank.has(id)) rank.set(id, 0);
|
|
89
|
+
for (const edge of edges.filter(edge => edge.from === id && edge.to !== id)) {
|
|
90
|
+
if (!ids.has(edge.to)) continue;
|
|
91
|
+
incoming.set(edge.to, incoming.get(edge.to) - 1);
|
|
92
|
+
rank.set(edge.to, Math.max(rank.get(edge.to) || 0, rank.get(id) + 1));
|
|
93
|
+
if (incoming.get(edge.to) === 0) queue.push(edge.to);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
for (let cursor = 0; cursor < queue.length; cursor++) visit(queue[cursor]);
|
|
97
|
+
// A cyclic remainder stays a deterministic separate row; no authored edge is
|
|
98
|
+
// dropped or converted into an invented forward relation.
|
|
99
|
+
const nextRow = new Map();
|
|
100
|
+
return nodes.map(node => {
|
|
101
|
+
const col = rank.has(node.id) && incoming.get(node.id) === 0 ? rank.get(node.id) : 0;
|
|
102
|
+
const row = nextRow.get(col) || 0;
|
|
103
|
+
nextRow.set(col, row + 1);
|
|
104
|
+
return { ...node, row, col };
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Compile a small author shorthand into an existing native Archify model. */
|
|
109
|
+
export function compileExplainerDiagram(diagramType, rows, { title, id, line = 1, quality = 'showcase' } = {}) {
|
|
110
|
+
const context = { line, pointer: '/', id, diagramType, rowMap: {}, subject: { diagram: id, diagramType } };
|
|
111
|
+
if (!TYPES.has(diagramType)) fail(context, 'diagram-type', 'Unknown native diagram type.', 'sequence');
|
|
112
|
+
if (!Array.isArray(rows) || !rows.length) fail(context, 'diagram-rows', 'A shorthand diagram needs at least one row.', 'Client | API | 请求记录');
|
|
113
|
+
const entries = rows.map((row, index) => {
|
|
114
|
+
const ctx = { ...context, line: Number.isInteger(row?.line) ? row.line : line + index };
|
|
115
|
+
const cells = Array.isArray(row) ? row : row?.cells;
|
|
116
|
+
if (cells !== undefined) {
|
|
117
|
+
if (!Array.isArray(cells) || !cells.length || cells.some(cell => typeof cell !== 'string')) fail(ctx, 'diagram-row', 'Each diagram row needs text cells.', 'Client | API | 请求记录');
|
|
118
|
+
return { parts: cells.map(cell => cell.trim()), context: ctx };
|
|
119
|
+
}
|
|
120
|
+
const text = typeof row === 'string' ? row : row?.text;
|
|
121
|
+
if (typeof text !== 'string' || !text.trim()) fail(ctx, 'diagram-row', 'Each diagram row needs text.', 'Client | API | 请求记录');
|
|
122
|
+
return { parts: rowParts(text), context: ctx };
|
|
123
|
+
});
|
|
124
|
+
const spec = { schema_version: diagramType === 'workflow' ? 2 : 1, diagram_type: diagramType,
|
|
125
|
+
meta: { title: title || id || diagramType, output: `${id && ID.test(id) ? id : 'diagram'}.html`, quality_profile: quality } };
|
|
126
|
+
const expect = (entry, min, max, example) => {
|
|
127
|
+
if (entry.parts.length < min || entry.parts.length > max || entry.parts.slice(0, min).some(part => !part)) {
|
|
128
|
+
fail(entry.context, 'diagram-row', `Expected ${min === max ? min : `${min}–${max}`} pipe-separated fields.`, example);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
const edgeCounts = new Map();
|
|
132
|
+
const edgeId = (from, to) => {
|
|
133
|
+
const base = `${from}-to-${to}`;
|
|
134
|
+
const count = (edgeCounts.get(base) || 0) + 1;
|
|
135
|
+
edgeCounts.set(base, count);
|
|
136
|
+
return `${base}-${count}`;
|
|
137
|
+
};
|
|
138
|
+
if (diagramType === 'sequence') {
|
|
139
|
+
spec.participants = [];
|
|
140
|
+
spec.messages = [];
|
|
141
|
+
const aliases = new Map();
|
|
142
|
+
const byId = new Map();
|
|
143
|
+
for (const entry of entries.filter(entry => entry.parts[0].startsWith('actor '))) {
|
|
144
|
+
expect(entry, 2, 3, 'actor api | API | backend');
|
|
145
|
+
const actor = { id: safeId(entry.parts[0].slice(6).trim(), entry.context), label: entry.parts[1], type: entry.parts[2] || 'backend' };
|
|
146
|
+
context.rowMap[`/participants/${spec.participants.length}`] = entry.context.line;
|
|
147
|
+
spec.participants.push(actor); byId.set(actor.id, actor); aliases.set(actor.label, actor.id); aliases.set(actor.id, actor.id);
|
|
148
|
+
}
|
|
149
|
+
const actorId = (label, ctx) => {
|
|
150
|
+
if (aliases.has(label)) return aliases.get(label);
|
|
151
|
+
const actor = { id: inferredId(label), label, type: 'external' };
|
|
152
|
+
if (byId.has(actor.id)) fail(ctx, 'diagram-actor-collision', 'Actor labels resolve to the same id; declare explicit actor identities.', 'actor client | Client | frontend');
|
|
153
|
+
spec.participants.push(actor); byId.set(actor.id, actor); aliases.set(label, actor.id);
|
|
154
|
+
return actor.id;
|
|
155
|
+
};
|
|
156
|
+
// "phase | label" rows open a labelled band over the messages that follow, up to the next phase.
|
|
157
|
+
const phases = [];
|
|
158
|
+
for (const entry of entries.filter(entry => !entry.parts[0].startsWith('actor '))) {
|
|
159
|
+
if (entry.parts[0] === 'phase') { expect(entry, 2, 2, 'phase | TCP handshake'); phases.push({ label: entry.parts[1], first: spec.messages.length, context: entry.context }); continue; }
|
|
160
|
+
expect(entry, 3, 4, 'Client | API | 请求记录 | return');
|
|
161
|
+
const [fromLabel, toLabel, label, variant] = entry.parts;
|
|
162
|
+
const from = actorId(fromLabel, entry.context), to = actorId(toLabel, entry.context);
|
|
163
|
+
context.rowMap[`/messages/${spec.messages.length}`] = entry.context.line;
|
|
164
|
+
spec.messages.push({ id: edgeId(from, to), from, to, label, y: 160 + spec.messages.length * 48, ...(variant ? { variant } : {}) });
|
|
165
|
+
}
|
|
166
|
+
phases.forEach((phase, index) => {
|
|
167
|
+
const last = (phases[index + 1]?.first ?? spec.messages.length) - 1;
|
|
168
|
+
if (last < phase.first) fail(phase.context, 'diagram-row', 'A phase needs at least one message after it.', 'phase | TCP handshake\n| browser | server | SYN |');
|
|
169
|
+
(spec.segments ||= []).push({ from: spec.messages[phase.first].y - 22, to: spec.messages[last].y + 16, label: phase.label });
|
|
170
|
+
});
|
|
171
|
+
duplicateCheck(spec.participants, context, 'actor');
|
|
172
|
+
if (!spec.messages.length) fail(context, 'diagram-rows', 'Sequence shorthand needs a message.', 'client | api | 请求记录');
|
|
173
|
+
} else if (diagramType === 'architecture' || diagramType === 'workflow') {
|
|
174
|
+
const nodes = [], edges = [], lanes = new Map(), nodeLanes = new Map();
|
|
175
|
+
for (const entry of entries) {
|
|
176
|
+
const route = entry.parts[0].match(/^([a-zA-Z][a-zA-Z0-9_-]*)\s*>\s*([a-zA-Z][a-zA-Z0-9_-]*)$/);
|
|
177
|
+
if (diagramType === 'workflow' && entry.parts[0].startsWith('lane ')) {
|
|
178
|
+
expect(entry, 2, 2, 'lane locate | Locate the cause');
|
|
179
|
+
const laneId = safeId(entry.parts[0].slice(5).trim(), entry.context);
|
|
180
|
+
if (lanes.has(laneId)) fail(entry.context, 'diagram-id-duplicate', 'Workflow lane ids must be unique.', 'lane locate | Locate the cause');
|
|
181
|
+
lanes.set(laneId, { id: laneId, label: entry.parts[1] });
|
|
182
|
+
} else if (route) {
|
|
183
|
+
if (entry.parts.length > 3) fail(entry.context, 'diagram-row', 'An edge supports a label and optional native variant.', 'api > redis | 查询 | dashed');
|
|
184
|
+
context.rowMap[`/${diagramType === 'architecture' ? 'connections' : 'edges'}/${edges.length}`] = entry.context.line;
|
|
185
|
+
edges.push({ id: edgeId(route[1], route[2]), from: route[1], to: route[2], ...(entry.parts[1] ? { label: entry.parts[1] } : {}), ...(entry.parts[2] ? { variant: entry.parts[2] } : {}) });
|
|
186
|
+
} else {
|
|
187
|
+
if (diagramType === 'workflow') expect(entry, 2, 4, 'trace | Compare slow traces | backend | locate');
|
|
188
|
+
else expect(entry, 2, 3, 'api | API | backend');
|
|
189
|
+
context.rowMap[`/${diagramType === 'architecture' ? 'components' : 'nodes'}/${nodes.length}`] = entry.context.line;
|
|
190
|
+
const node = { id: safeId(entry.parts[0], entry.context), label: entry.parts[1], type: entry.parts[2] || 'backend' };
|
|
191
|
+
// Wide enough that the label stays centred between the corner icon and the opposite edge;
|
|
192
|
+
// a narrower node drops the label below the icon, out of line with its neighbours.
|
|
193
|
+
const labelWidth = Math.ceil(Math.max(textUnits(node.label) * 6.8 + 12, minimumNodeTextWidth(node.label, 11) + 2 * (SEMANTIC_SIGIL_FOOTPRINT + 2)));
|
|
194
|
+
if (diagramType === 'workflow' && labelWidth > 92) node.width = labelWidth;
|
|
195
|
+
// A question is a decision point: the native decision mark tells it apart from a step.
|
|
196
|
+
if (diagramType === 'workflow' && /[??]$/u.test(node.label)) node.icon = 'decision';
|
|
197
|
+
if (entry.parts[3]) nodeLanes.set(node.id, { lane: safeId(entry.parts[3], entry.context), context: entry.context });
|
|
198
|
+
nodes.push(node);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
duplicateCheck(nodes, context, 'node');
|
|
202
|
+
const placed = rankedPlacement(nodes, edges);
|
|
203
|
+
if (diagramType === 'architecture') {
|
|
204
|
+
const cols = Math.max(1, ...placed.map(node => node.col + 1));
|
|
205
|
+
if (cols > 12) fail(context, 'diagram-shorthand-limit', 'This architecture needs more than twelve native grid columns.', 'Use native JSON with explicit component positions.');
|
|
206
|
+
const fitWidth = node => Math.ceil(textUnits(node.label) * 6.6 + 16);
|
|
207
|
+
// Order each column by where its upstream nodes sit, then centre it against the tallest column,
|
|
208
|
+
// so a hub sits between the nodes it connects instead of on the first row.
|
|
209
|
+
const height = Math.max(...Array.from({ length: cols }, (_, col) => placed.filter(node => node.col === col).length));
|
|
210
|
+
const rowOf = new Map();
|
|
211
|
+
for (let col = 0; col < cols; col++) {
|
|
212
|
+
const column = placed.filter(node => node.col === col);
|
|
213
|
+
const upstream = node => { const rows = edges.filter(edge => edge.to === node.id && rowOf.has(edge.from)).map(edge => rowOf.get(edge.from)); return rows.length ? rows.reduce((sum, row) => sum + row, 0) / rows.length : node.row; };
|
|
214
|
+
column.sort((a, b) => upstream(a) - upstream(b) || a.row - b.row)
|
|
215
|
+
.forEach((node, i) => rowOf.set(node.id, Math.floor((height - column.length) / 2) + i));
|
|
216
|
+
}
|
|
217
|
+
spec.layout = { mode: 'grid', cols, cellW: Math.max(176, ...placed.map(fitWidth)), cellH: 76, gapX: 100, gapY: 110 };
|
|
218
|
+
spec.components = placed.map(node => ({ ...node, row: rowOf.get(node.id), ...(fitWidth(node) > 120 ? { size: [fitWidth(node), 60] } : {}) }));
|
|
219
|
+
spec.connections = edges;
|
|
220
|
+
} else if (lanes.size) {
|
|
221
|
+
for (const [id, { lane, context: ctx }] of nodeLanes) if (!lanes.has(lane)) fail(ctx, 'diagram-lane', `Node ${id} names an undeclared lane.`, 'lane locate | Locate the cause');
|
|
222
|
+
const first = lanes.keys().next().value;
|
|
223
|
+
const order = [...lanes.keys()];
|
|
224
|
+
const ranked = [...placed].sort((a, b) => a.col - b.col || a.row - b.row).map(({ row, col, ...node }) => ({ ...node, lane: nodeLanes.get(node.id)?.lane || first }));
|
|
225
|
+
// Prefer, in order: one row with phase labels above it; phase rows that step down so each hand-off
|
|
226
|
+
// edge drops straight; phase rows that each restart at the left. Take the first that stays within
|
|
227
|
+
// the readable width, since a wider canvas shrinks its edge labels below the native text floor.
|
|
228
|
+
const row = [...ranked].sort((a, b) => order.indexOf(a.lane) - order.indexOf(b.lane));
|
|
229
|
+
const used = [...lanes.values()].filter(lane => ranked.some(node => node.lane === lane.id));
|
|
230
|
+
const sizes = used.map(lane => ranked.filter(node => node.lane === lane.id).length);
|
|
231
|
+
const wrapped = stair => {
|
|
232
|
+
const starts = sizes.map((_, i) => stair ? sizes.slice(0, i).reduce((sum, size) => sum + size - 1, 0) : 0);
|
|
233
|
+
const nextCol = new Map(used.map((lane, i) => [lane.id, starts[i]]));
|
|
234
|
+
return ranked.map(node => { const col = nextCol.get(node.lane); nextCol.set(node.lane, col + 1); return { ...node, col }; });
|
|
235
|
+
};
|
|
236
|
+
const single = row.map(({ lane, ...node }, col) => ({ ...node, lane: 'flow', col }));
|
|
237
|
+
// Grouping by phase keeps the flow order only when each phase is one stretch of the flow.
|
|
238
|
+
const inOrder = ranked.every((node, i) => i === 0 || order.indexOf(node.lane) >= order.indexOf(ranked[i - 1].lane));
|
|
239
|
+
const candidates = [
|
|
240
|
+
...(inOrder && ranked.length <= 6 && placed.every(node => node.row === 0) ? [{ nodes: single, single: true }] : []),
|
|
241
|
+
...(inOrder && sizes.reduce((sum, size) => sum + size, 0) - (sizes.length - 1) <= 6 ? [{ nodes: wrapped(true) }] : []),
|
|
242
|
+
{ nodes: wrapped(false) },
|
|
243
|
+
];
|
|
244
|
+
const chosen = candidates.find(candidate => workflowWidth(candidate.nodes, edges) <= READABLE_WORKFLOW_WIDTH) || candidates.at(-1);
|
|
245
|
+
spec.nodes = chosen.nodes;
|
|
246
|
+
if (chosen.single) {
|
|
247
|
+
spec.lanes = [{ id: 'flow', label: spec.meta.title }];
|
|
248
|
+
spec.phases = order.map(id => ({ id, label: lanes.get(id).label, cols: row.flatMap((node, col) => node.lane === id ? [col] : []) })).filter(phase => phase.cols.length).map(({ cols, ...phase }) => ({ ...phase, fromCol: cols[0], toCol: cols.at(-1) }));
|
|
249
|
+
} else spec.lanes = used;
|
|
250
|
+
if (spec.nodes.some(node => node.col > 5)) fail(context, 'diagram-shorthand-limit', 'A workflow lane holds at most six nodes.', 'Split the process into another lane.');
|
|
251
|
+
spec.edges = edges;
|
|
252
|
+
} else {
|
|
253
|
+
// A process too wide for one readable row wraps by columns into stacked bands, keeping each
|
|
254
|
+
// band's branch rows, and steps each band down under the end of the previous one where six
|
|
255
|
+
// columns allow, the same way declared phases do.
|
|
256
|
+
const fits = laid => laid.every(node => node.col <= 5) && workflowWidth(laid, edges) <= READABLE_WORKFLOW_WIDTH;
|
|
257
|
+
let wrapped;
|
|
258
|
+
if (!fits(placed)) {
|
|
259
|
+
const branchRows = Math.max(...placed.map(node => node.row)) + 1;
|
|
260
|
+
for (let size = 5; size >= 2 && !wrapped; size--) {
|
|
261
|
+
for (const stair of [true, false]) {
|
|
262
|
+
const laid = placed.map(node => { const band = Math.floor(node.col / size); return { ...node, row: band * branchRows + node.row, col: (stair ? band * (size - 1) : 0) + node.col - band * size }; });
|
|
263
|
+
const used = [...new Set(laid.map(node => node.row))].sort((a, b) => a - b);
|
|
264
|
+
const compact = laid.map(node => ({ ...node, row: used.indexOf(node.row) }));
|
|
265
|
+
if (fits(compact)) { wrapped = compact; break; }
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const rows = wrapped || placed;
|
|
270
|
+
if (rows.some(node => node.col > 5)) fail(context, 'diagram-shorthand-limit', 'Native workflow columns are limited to 0–5.', 'Declare phase lanes with "lane <id> | label" rows and assign nodes to them.');
|
|
271
|
+
spec.lanes = Array.from({ length: Math.max(1, ...rows.map(node => node.row + 1)) }, (_, row) => ({ id: `lane-${row}`, label: wrapped ? `${spec.meta.title} · ${row + 1}` : row ? `分支 ${row}` : spec.meta.title }));
|
|
272
|
+
spec.nodes = rows.map(({ row, ...node }) => ({ ...node, lane: `lane-${row}` }));
|
|
273
|
+
spec.edges = edges;
|
|
274
|
+
}
|
|
275
|
+
} else if (diagramType === 'tree') {
|
|
276
|
+
spec.nodes = entries.map((entry, index) => {
|
|
277
|
+
expect(entry, 2, 3, 'api | API 服务 | root');
|
|
278
|
+
context.rowMap[`/nodes/${index}`] = entry.context.line;
|
|
279
|
+
const [nodeId, label, parent] = entry.parts;
|
|
280
|
+
return { id: safeId(nodeId, entry.context), label, ...(parent && parent !== '-' ? { parent: safeId(parent, entry.context) } : {}) };
|
|
281
|
+
});
|
|
282
|
+
duplicateCheck(spec.nodes, context, 'node');
|
|
283
|
+
} else if (diagramType === 'timeline') {
|
|
284
|
+
spec.meta.evidence = 'illustrative';
|
|
285
|
+
const lanes = new Map(); spec.events = [];
|
|
286
|
+
for (const entry of entries) {
|
|
287
|
+
if (entry.parts[0].startsWith('lane ')) {
|
|
288
|
+
expect(entry, 2, 2, 'lane release | 发布');
|
|
289
|
+
const laneId = safeId(entry.parts[0].slice(5).trim(), entry.context);
|
|
290
|
+
if (lanes.has(laneId)) fail(entry.context, 'diagram-id-duplicate', 'Timeline lane ids must be unique.', 'lane release | 发布');
|
|
291
|
+
lanes.set(laneId, { id: laneId, label: entry.parts[1] });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
for (const entry of entries.filter(entry => !entry.parts[0].startsWith('lane '))) {
|
|
295
|
+
expect(entry, 3, 5, 'deploy | 2026-10-01T10:00:00+08:00 | 发布 v2 | release | change');
|
|
296
|
+
const [eventId, at, eventTitle, authoredLane, kind] = entry.parts;
|
|
297
|
+
const lane = authoredLane || 'events'; safeId(lane, entry.context);
|
|
298
|
+
if (!lanes.has(lane)) lanes.set(lane, { id: lane, label: lane });
|
|
299
|
+
context.rowMap[`/events/${spec.events.length}`] = entry.context.line;
|
|
300
|
+
spec.events.push({ id: safeId(eventId, entry.context), at, title: eventTitle, lane, ...(kind ? { kind } : {}) });
|
|
301
|
+
}
|
|
302
|
+
spec.lanes = [...lanes.values()]; duplicateCheck(spec.events, context, 'event');
|
|
303
|
+
} else if (diagramType === 'erd') {
|
|
304
|
+
const entities = new Map(); spec.relationships = [];
|
|
305
|
+
for (const entry of entries.filter(entry => entry.parts[0].startsWith('entity '))) {
|
|
306
|
+
expect(entry, 2, 2, 'entity user | 用户');
|
|
307
|
+
const entityId = safeId(entry.parts[0].slice(7).trim(), entry.context);
|
|
308
|
+
if (entities.has(entityId)) fail(entry.context, 'diagram-id-duplicate', 'Entity ids must be unique.', 'entity user | 用户');
|
|
309
|
+
entities.set(entityId, { id: entityId, label: entry.parts[1], row: 0, col: entities.size, attributes: [] });
|
|
310
|
+
}
|
|
311
|
+
for (const entry of entries.filter(entry => !entry.parts[0].startsWith('entity '))) {
|
|
312
|
+
const route = entry.parts[0].match(/^([a-zA-Z][a-zA-Z0-9_-]*)\s*>\s*([a-zA-Z][a-zA-Z0-9_-]*)$/);
|
|
313
|
+
if (route) {
|
|
314
|
+
expect(entry, 2, 3, 'order > user | many:one | 下单者');
|
|
315
|
+
const cardinality = entry.parts[1].match(/^(one|many):(one|many)$/);
|
|
316
|
+
if (!cardinality) fail(entry.context, 'diagram-cardinality', 'Relationship cardinality must be explicit.', 'order > user | many:one | 下单者');
|
|
317
|
+
spec.relationships.push({ id: edgeId(route[1], route[2]), from: route[1], to: route[2], fromCardinality: cardinality[1], toCardinality: cardinality[2], ...(entry.parts[2] ? { label: entry.parts[2] } : {}) });
|
|
318
|
+
} else {
|
|
319
|
+
expect(entry, 2, 4, 'order.user_id | bigint | fk | user.id');
|
|
320
|
+
const field = entry.parts[0].match(/^([a-zA-Z][a-zA-Z0-9_-]*)\.([a-zA-Z][a-zA-Z0-9_]*)$/);
|
|
321
|
+
if (!field || !entities.has(field[1])) fail(entry.context, 'diagram-field', 'A field must name an explicitly declared entity.', 'entity user | 用户\nuser.id | bigint | pk');
|
|
322
|
+
const roles = entry.parts[2]?.split(',').map(value => value.trim()).filter(Boolean);
|
|
323
|
+
entities.get(field[1]).attributes.push({ name: field[2], type: entry.parts[1], ...(roles?.length ? { key: roles.length === 1 ? roles[0] : roles } : {}), ...(entry.parts[3] ? { references: entry.parts[3] } : {}) });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
spec.layout = { mode: 'grid', gapX: 96, gapY: 64 }; spec.entities = [...entities.values()];
|
|
327
|
+
} else {
|
|
328
|
+
fail(context, 'diagram-shorthand-unsupported', `The ${diagramType} type uses native JSON instead of shorthand rows.`, `Use spec with diagram_type: ${diagramType}, or source: diagram.${diagramType}.json.`);
|
|
329
|
+
}
|
|
330
|
+
try { validateSchema(diagramType, spec); } catch (error) { rethrowNative(error, context, 'diagram-schema'); }
|
|
331
|
+
return spec;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function rethrowNative(error, context, reason) {
|
|
335
|
+
let details = error.archifyDiagnostics || error.diagnostics || [];
|
|
336
|
+
if (!details.length && Array.isArray(error.checks)) {
|
|
337
|
+
details = error.checks.filter(check => !check.ok).flatMap(check => (check.details?.length ? check.details : [`Native check ${check.name} failed.`])
|
|
338
|
+
.map(message => ({ code: `check/${check.name}`, severity: 'error', message, evidence: check })));
|
|
339
|
+
if (!details.length) details = (error.composition?.issues || []).filter(issue => issue.severity === 'error')
|
|
340
|
+
.map(issue => ({ ...issue, message: issue.code === 'composition/desktop-readability' && issue.projectedFontPx
|
|
341
|
+
? `Text "${issue.text}" on ${issue.owner?.kind || 'element'} ${issue.owner?.id || ''} would show at ${issue.projectedFontPx.toFixed(1)}px, below ${issue.minimumProjectedFontPx}px: the diagram is ${Math.round(issue.viewBoxWidth)}px wide and shrinks to ${Math.round(issue.scale * 100)}% on the page. Make it narrower: at most four or five nodes per row, shorter node labels, or phases.`
|
|
342
|
+
: issue.detail || `Native quality check ${issue.code} failed.` }));
|
|
343
|
+
}
|
|
344
|
+
if (details.length) throwDiagnosticError('Native diagram validation failed.', details.map(entry => ({
|
|
345
|
+
code: `explainer/${reason}`, severity: entry.severity || 'error', message: entry.message,
|
|
346
|
+
subject: subject({ ...context, line: context.rowMap?.[entry.subject?.path?.match(/^\/[^/]+\/\d+/)?.[0]] || context.line },
|
|
347
|
+
(context.pointer && entry.subject?.path && !context.rowMap) ? `${context.pointer}/spec${entry.subject.path}` : context.pointer || '/',
|
|
348
|
+
{ diagramType: context.diagramType, diagram: context.id }),
|
|
349
|
+
evidence: { reason, example: `Use a valid native ${context.diagramType || 'diagram'} JSON specification.`, native: entry },
|
|
350
|
+
supportedFixes: entry.supportedFixes?.length ? entry.supportedFixes : ['Repair the native diagnostic and render again.'],
|
|
351
|
+
})));
|
|
352
|
+
fail(context, reason, 'The native diagram toolchain could not complete this diagram.', `Use a valid native ${context.diagramType || 'diagram'} JSON specification.`, [], { message: String(error.message || error).slice(0, 1000) });
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function readSpec(baseDir, relative, context) {
|
|
356
|
+
if (!baseDir || typeof relative !== 'string' || !relative || path.isAbsolute(relative) || /[\\:\u0000-\u001f?#]/.test(relative)
|
|
357
|
+
|| relative.split('/').some(part => !part || part === '.' || part === '..' || part.toLowerCase() === '.git') || !/\.json$/i.test(relative)) {
|
|
358
|
+
fail(context, 'diagram-source-path', 'Diagram source must be a relative JSON file inside the input directory.', 'diagrams/request.sequence.json');
|
|
359
|
+
}
|
|
360
|
+
let fd;
|
|
361
|
+
try {
|
|
362
|
+
const root = fs.realpathSync(baseDir), ancestors = [[root, fs.lstatSync(root)]];
|
|
363
|
+
let candidate = root;
|
|
364
|
+
for (const part of relative.split('/')) {
|
|
365
|
+
candidate = path.join(candidate, part); const stat = fs.lstatSync(candidate);
|
|
366
|
+
if (stat.isSymbolicLink()) fail(context, 'diagram-source-symlink', 'Diagram sources may not traverse symbolic links.', 'request.sequence.json');
|
|
367
|
+
ancestors.push([candidate, stat]);
|
|
368
|
+
}
|
|
369
|
+
const expected = ancestors.at(-1)[1];
|
|
370
|
+
if (!expected.isFile() || expected.size > MAX_SPEC_BYTES) fail(context, 'diagram-source-size', 'Diagram source must be a regular JSON file smaller than 4 MiB.', 'request.sequence.json');
|
|
371
|
+
fd = fs.openSync(candidate, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
372
|
+
const before = fs.fstatSync(fd);
|
|
373
|
+
const same = (a, b) => a.dev === b.dev && a.ino === b.ino && a.size === b.size && a.mtimeMs === b.mtimeMs;
|
|
374
|
+
if (!same(before, expected)) fail(context, 'diagram-source-changed', 'Diagram source changed while being frozen.', 'Finish edits before rendering.');
|
|
375
|
+
const bytes = Buffer.alloc(before.size);
|
|
376
|
+
let offset = 0;
|
|
377
|
+
while (offset < bytes.length) {
|
|
378
|
+
const count = fs.readSync(fd, bytes, offset, bytes.length - offset, offset);
|
|
379
|
+
if (!count) break;
|
|
380
|
+
offset += count;
|
|
381
|
+
}
|
|
382
|
+
if (offset !== bytes.length || !same(before, fs.fstatSync(fd)) || !ancestors.every(([name, stat]) => { const current = fs.lstatSync(name); return !current.isSymbolicLink() && same(current, stat); })) {
|
|
383
|
+
fail(context, 'diagram-source-changed', 'Diagram source changed while being frozen.', 'Finish edits before rendering.');
|
|
384
|
+
}
|
|
385
|
+
return bytes;
|
|
386
|
+
} catch (error) {
|
|
387
|
+
if (error.archifyDiagnostics) throw error;
|
|
388
|
+
fail(context, 'diagram-source-read', 'Diagram source cannot be read safely inside the input directory.', 'request.sequence.json');
|
|
389
|
+
} finally { if (fd !== undefined) fs.closeSync(fd); }
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function decode(value) {
|
|
393
|
+
return value.replace(/&(?:quot|apos|lt|gt|amp|#\d+|#x[\da-f]+);/gi, entity => {
|
|
394
|
+
const named = { '"': '"', ''': "'", '<': '<', '>': '>', '&': '&' };
|
|
395
|
+
if (Object.hasOwn(named, entity)) return named[entity];
|
|
396
|
+
const code = entity.startsWith('&#x') ? parseInt(entity.slice(3, -1), 16) : parseInt(entity.slice(2, -1), 10);
|
|
397
|
+
return Number.isFinite(code) && code <= 0x10ffff ? String.fromCodePoint(code) : entity;
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function attrs(tag) {
|
|
402
|
+
return Object.fromEntries([...tag.matchAll(/([\w:-]+)="([^"]*)"/g)].map(match => [match[1], decode(match[2])]));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function semanticOf(svg) {
|
|
406
|
+
const nodes = new Map(), edges = new Map();
|
|
407
|
+
for (const match of svg.matchAll(/<[a-zA-Z][^>]*>/g)) {
|
|
408
|
+
const values = attrs(match[0]);
|
|
409
|
+
if (values['data-node-id']) nodes.set(values['data-node-id'], { id: values['data-node-id'], label: values['data-node-label'] || values['data-node-id'], svgId: values.id });
|
|
410
|
+
if (values['data-edge-from'] && values['data-edge-to']) {
|
|
411
|
+
const id = values['data-edge-id'] || values['data-edge-key'] || `${values['data-edge-from']}~${values['data-edge-to']}`;
|
|
412
|
+
edges.set(id, { id, from: values['data-edge-from'], to: values['data-edge-to'], label: values['data-edge-label'] || '',
|
|
413
|
+
...(values['data-edge-key'] ? { key: values['data-edge-key'] } : {}) });
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return { nodes: [...nodes.values()], edges: [...edges.values()] };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function changesPreview(svg, changes, semantic, context) {
|
|
420
|
+
if (changes !== undefined && !Array.isArray(changes)) fail(context, 'diagram-change', 'Structural changes must be an array of explicit target marks.', '[{target:"api",change:"modified"}]');
|
|
421
|
+
const nodes = new Set(semantic.nodes.map(node => node.id)), edges = new Set(semantic.edges.map(edge => edge.id));
|
|
422
|
+
const byTarget = new Map();
|
|
423
|
+
for (const change of changes || []) {
|
|
424
|
+
if (!change || !['added', 'removed', 'modified'].includes(change.change) || typeof change.target !== 'string') fail(context, 'diagram-change', 'A structural change needs an explicit target and change type.', '{target:"api",change:"modified"}');
|
|
425
|
+
if (!nodes.has(change.target) && !edges.has(change.target)) fail(context, 'diagram-change-target', 'The change target is absent from the authored native topology.', '{target:"api",change:"modified"}');
|
|
426
|
+
if (byTarget.has(change.target)) fail(context, 'diagram-change-duplicate', 'A target may have only one structural change mark.', '{target:"api",change:"modified"}');
|
|
427
|
+
byTarget.set(change.target, change.change);
|
|
428
|
+
}
|
|
429
|
+
return svg.replace(/<[a-zA-Z][^>]*>/g, tag => {
|
|
430
|
+
const values = attrs(tag), change = byTarget.get(values['data-node-id']) || byTarget.get(values['data-edge-id']);
|
|
431
|
+
return change ? tag.replace(/(\/?>)$/, ` data-explainer-change="${change}"$1`) : tag;
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async function nativeProcess(script, args, options, context, reason) {
|
|
436
|
+
try {
|
|
437
|
+
const result = await runFile(process.execPath, runtimeArgs([script, ...args]), { cwd: options.cwd || skillRoot, maxBuffer: MAX_DIAGRAM_BYTES,
|
|
438
|
+
timeout: 120000, env: { ...process.env, ARCHIFY_DIAGNOSTIC_FORMAT: 'json', ARCHIFY_QUALITY_PROFILE: options.quality,
|
|
439
|
+
...(options.repoRoot ? { ARCHIFY_REPO_ROOT: path.resolve(options.repoRoot) } : {}) } });
|
|
440
|
+
return result;
|
|
441
|
+
} catch (error) {
|
|
442
|
+
let native;
|
|
443
|
+
for (const output of [error.stdout, error.stderr]) { try { native = JSON.parse(output); if (native.diagnostics) break; } catch {} }
|
|
444
|
+
rethrowNative(native || error, context, reason);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** Freeze native diagrams, validate, render, and check without a per-child browser gate. */
|
|
449
|
+
export async function prepareExplainerDiagrams(document, { repoRoot, baseDir, quality = 'showcase', sourceMap, sourcePath } = {}) {
|
|
450
|
+
const diagrams = Object.create(null), dependencies = [], blocks = [];
|
|
451
|
+
for (const [panelIndex, panel] of (document.panels || []).entries()) {
|
|
452
|
+
for (const [blockIndex, block] of (panel.blocks || []).entries()) if (block.type === 'diagram') blocks.push({ block, pointer: `/panels/${panelIndex}/blocks/${blockIndex}`, panelId: panel.id });
|
|
453
|
+
}
|
|
454
|
+
// Legacy flat draft shapes are accepted only as adapter inputs; canonical
|
|
455
|
+
// explainer validation remains the parent pipeline's responsibility.
|
|
456
|
+
for (const [blockIndex, block] of (document.blocks || []).entries()) if (block.type === 'diagram') blocks.push({ block, pointer: `/blocks/${blockIndex}` });
|
|
457
|
+
for (const entry of blocks) {
|
|
458
|
+
const { block, pointer, panelId } = entry, context = { pointer, panelId, sourceMap, sourcePath, id: block.id, diagramType: block.diagramType };
|
|
459
|
+
if (typeof block.id !== 'string' || !block.id || Object.hasOwn(diagrams, block.id)) fail(context, 'diagram-id', 'Diagram blocks need unique stable identities.', '{id:"request",type:"diagram",diagramType:"sequence",spec:{...}}');
|
|
460
|
+
if (!TYPES.has(block.diagramType)) fail(context, 'diagram-type', 'Unknown native diagram type.', 'sequence');
|
|
461
|
+
if ((block.spec !== undefined) === (block.source !== undefined)) fail(context, 'diagram-input', 'Provide exactly one inline spec or relative source.', 'source: request.sequence.json');
|
|
462
|
+
let originalBytes;
|
|
463
|
+
if (block.source !== undefined) originalBytes = readSpec(baseDir, block.source, { ...context, pointer: `${pointer}/source` });
|
|
464
|
+
else {
|
|
465
|
+
try { originalBytes = Buffer.from(JSON.stringify(block.spec)); } catch { fail(context, 'diagram-spec', 'Inline native JSON must be serializable.', 'spec: {schema_version:1,diagram_type:"tree",meta:{title:"Tree"},nodes:[...]}'); }
|
|
466
|
+
if (originalBytes.length > MAX_SPEC_BYTES) fail(context, 'diagram-source-size', 'Inline diagram JSON exceeds 4 MiB.', 'Use a smaller native JSON specification.');
|
|
467
|
+
}
|
|
468
|
+
let original;
|
|
469
|
+
try { original = JSON.parse(originalBytes.toString('utf8')); } catch { fail(context, 'diagram-json', 'Diagram source is not valid JSON.', '{"schema_version":1,"diagram_type":"sequence","meta":{"title":"Request"},"participants":[],"messages":[]}'); }
|
|
470
|
+
if (!original || typeof original !== 'object' || Array.isArray(original) || original.diagram_type !== block.diagramType) fail(context, 'diagram-type-mismatch', 'Native specification diagram_type must match its explainer block.', `diagram_type: ${block.diagramType}`);
|
|
471
|
+
dependencies.push({ kind: 'diagram-spec', id: block.id, ...(block.source ? { path: path.resolve(baseDir, block.source), relativePath: block.source } : { inline: true }), ...freezeBytes(originalBytes) });
|
|
472
|
+
const temporary = createOwnedTempDirectory('archify-explainer-diagram-');
|
|
473
|
+
try {
|
|
474
|
+
const output = path.join(temporary.path, 'diagram.html'), input = path.join(temporary.path, 'diagram.json');
|
|
475
|
+
// Inline diagrams belong to the explainer's reading context. Keep the
|
|
476
|
+
// original author bytes frozen; apply inheritance only to native render
|
|
477
|
+
// input. Referenced standalone specs retain their own locale defaults.
|
|
478
|
+
const inheritedLocale = block.spec !== undefined && original.meta?.locale === undefined && document.meta?.locale !== undefined
|
|
479
|
+
? { locale: document.meta.locale } : {};
|
|
480
|
+
const spec = { ...original, meta: { ...original.meta, ...inheritedLocale, output: 'diagram.html', quality_profile: quality } };
|
|
481
|
+
try { validateSchema(block.diagramType, spec); } catch (error) { rethrowNative(error, context, 'diagram-schema'); }
|
|
482
|
+
fs.writeFileSync(input, JSON.stringify(spec), { flag: 'wx' });
|
|
483
|
+
await nativeProcess(path.join(skillRoot, 'renderers', block.diagramType, `render-${block.diagramType}.mjs`), [input, output], { repoRoot, quality, cwd: temporary.path }, context, 'diagram-render');
|
|
484
|
+
const checked = await nativeProcess(path.join(skillRoot, 'scripts/check-render-output.mjs'), [output], { repoRoot, quality, cwd: temporary.path }, context, 'diagram-check');
|
|
485
|
+
let check;
|
|
486
|
+
try { check = JSON.parse(checked.stdout); } catch { fail(context, 'diagram-check-receipt', 'Native checker returned no structured receipt.', 'Rerun the native diagram check.'); }
|
|
487
|
+
if (check.ok === false) rethrowNative(check, context, 'diagram-check');
|
|
488
|
+
const bytes = fs.readFileSync(output);
|
|
489
|
+
if (bytes.length > MAX_DIAGRAM_BYTES) fail(context, 'diagram-artifact-size', 'The native diagram artifact exceeds 32 MiB.', 'Use a smaller native diagram.');
|
|
490
|
+
const html = bytes.toString('utf8'), svgMatch = html.match(/<svg\b[^>]*\bdata-quality-profile="[^"]*"[^>]*>[\s\S]*?<\/svg>/);
|
|
491
|
+
if (!svgMatch) fail(context, 'diagram-svg', 'The native renderer produced no primary semantic SVG.', 'Use a supported native diagram renderer.');
|
|
492
|
+
const canonicalSvg = svgMatch[0], semantic = semanticOf(canonicalSvg);
|
|
493
|
+
const svg = changesPreview(canonicalSvg, block.changes, semantic, context);
|
|
494
|
+
// Preview styles are derived from html at render time; storing them again would double the page.
|
|
495
|
+
diagrams[block.id] = { diagramType: block.diagramType, spec: original, html, svg, sha256: hash(bytes), bytes: bytes.length,
|
|
496
|
+
specification: freezeBytes(originalBytes), checks: { validate: { ok: true }, render: { ok: true }, check }, semantic,
|
|
497
|
+
...(block.changes ? { changes: block.changes.map(change => ({ ...change })) } : {}) };
|
|
498
|
+
} finally { await temporary.cleanup(); }
|
|
499
|
+
}
|
|
500
|
+
return { diagrams, dependencies };
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** Resolve a reference against the actual native SVG graph, including real routes. */
|
|
504
|
+
export function validateExplainerDiagramTarget(diagrams, target, { sourceMap, sourcePath, pointer = '/', line = 1 } = {}) {
|
|
505
|
+
const context = { sourceMap, sourcePath, pointer, line };
|
|
506
|
+
const diagram = diagrams?.[target?.diagram];
|
|
507
|
+
if (!diagram) fail(context, 'diagram-reference', 'The reference names an unknown diagram block.', '{diagram:"request",node:"api"}');
|
|
508
|
+
const nodes = new Set(diagram.semantic.nodes.map(node => node.id)), edges = diagram.semantic.edges;
|
|
509
|
+
if (target.node !== undefined && !nodes.has(target.node)) fail(context, 'diagram-node-reference', 'The reference names a node absent from the native diagram.', `{diagram:"${target.diagram}",node:"${diagram.semantic.nodes[0]?.id || 'api'}"}`);
|
|
510
|
+
if (target.edges !== undefined && (!Array.isArray(target.edges) || !target.edges.length || target.edges.some(id => !edges.some(edge => edge.id === id)))) fail(context, 'diagram-edge-reference', 'The reference names an edge absent from the native diagram.', `{diagram:"${target.diagram}",edges:["${edges[0]?.id || 'api-to-db-1'}"]}`);
|
|
511
|
+
if (target.route !== undefined) {
|
|
512
|
+
if (!Array.isArray(target.route) || target.route.length !== 2 || !target.route.every(id => nodes.has(id))) fail(context, 'diagram-route-reference', 'A route needs two real native node identities.', `{diagram:"${target.diagram}",route:["api","db"]}`);
|
|
513
|
+
const [from, to] = target.route, visited = new Set([from]), queue = [from];
|
|
514
|
+
for (let index = 0; index < queue.length; index++) for (const edge of edges.filter(edge => edge.from === queue[index])) if (!visited.has(edge.to)) { visited.add(edge.to); queue.push(edge.to); }
|
|
515
|
+
if (!visited.has(to)) fail(context, 'diagram-route-reference', 'No directed route connects these nodes in the native diagram.', `{diagram:"${target.diagram}",route:["${edges[0]?.from || 'api'}","${edges[0]?.to || 'db'}"]}`);
|
|
516
|
+
}
|
|
517
|
+
return true;
|
|
518
|
+
}
|