@bovinphang/frontend-craft 2.8.0 → 2.9.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/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +378 -339
- package/CHANGELOG.zh-CN.md +377 -338
- package/CONTRIBUTING.md +371 -361
- package/CONTRIBUTING.zh-CN.md +371 -361
- package/README.md +545 -493
- package/README.openclaw.md +359 -320
- package/README.openclaw.zh-CN.md +359 -320
- package/README.zh-CN.md +545 -493
- package/agents/fec-code-reviewer.md +24 -3
- package/agents/fec-debugger.md +1 -1
- package/agents/fec-doc-updater.md +1 -1
- package/agents/fec-e2e-runner.md +1 -1
- package/agents/fec-figma-implementer.md +1 -1
- package/agents/fec-performance-optimizer.md +1 -1
- package/agents/fec-refactor-cleaner.md +4 -1
- package/agents/fec-refactoring-expert.md +81 -0
- package/agents/fec-security-reviewer.md +17 -1
- package/agents/fec-test-planner.md +1 -1
- package/agents/fec-typescript-reviewer.md +24 -8
- package/agents/fec-ui-checker.md +11 -11
- package/commands/fec-debug.md +1 -1
- package/commands/fec-refactor-plan.md +17 -0
- package/commands/fec-refactor.md +18 -0
- package/commands/fec-review.md +40 -10
- package/commands/fec-scaffold.md +1 -1
- package/commands/fec-smell.md +17 -0
- package/commands/fec-tdd.md +6 -2
- package/dist/bin/frontend-craft.js +190 -38
- package/dist/hooks/fec-run-tests.js +5 -1
- package/dist/hooks/fec-session-start.js +1 -1
- package/docs/example-prompts.md +157 -140
- package/docs/ja-JP/README.md +534 -493
- package/docs/ko-KR/README.md +534 -493
- package/docs/project-structure.md +151 -131
- package/docs/refactoring/README.md +50 -0
- package/docs/refactoring/catalog.md +105 -0
- package/docs/refactoring/smell-refactoring-matrix.md +41 -0
- package/docs/review-and-diagram-workflows.md +39 -0
- package/docs/runtimes/antigravity.md +18 -10
- package/docs/runtimes/augment.md +18 -10
- package/docs/runtimes/claude.md +191 -177
- package/docs/runtimes/claude.zh-CN.md +189 -175
- package/docs/runtimes/cline.md +18 -10
- package/docs/runtimes/codebuddy.md +18 -10
- package/docs/runtimes/codex.md +18 -10
- package/docs/runtimes/compatibility.md +27 -0
- package/docs/runtimes/copilot.md +18 -10
- package/docs/runtimes/cursor.md +18 -10
- package/docs/runtimes/gemini.md +18 -10
- package/docs/runtimes/kilo.md +18 -10
- package/docs/runtimes/openclaw.md +18 -10
- package/docs/runtimes/opencode.md +18 -10
- package/docs/runtimes/qoder.md +18 -19
- package/docs/runtimes/trae.md +18 -10
- package/docs/runtimes/windsurf.md +18 -10
- package/docs/superpowers/plans/2026-09-26-content-integrity.md +145 -0
- package/docs/superpowers/plans/2026-09-26-diagram-quality.md +170 -0
- package/docs/superpowers/specs/2026-09-26-diagram-quality-design.md +77 -0
- package/docs/superpowers/specs/2026-09-26-project-optimization-design.md +95 -0
- package/docs/zh-CN/example-prompts.md +157 -140
- package/docs/zh-CN/project-structure.md +148 -128
- package/docs/zh-CN/review-and-diagram-workflows.md +39 -0
- package/docs/zh-TW/README.md +534 -493
- package/localized/zh-CN/agents/fec-code-reviewer.md +22 -3
- package/localized/zh-CN/agents/fec-refactor-cleaner.md +3 -0
- package/localized/zh-CN/agents/fec-refactoring-expert.md +53 -0
- package/localized/zh-CN/agents/fec-security-reviewer.md +16 -0
- package/localized/zh-CN/agents/fec-typescript-reviewer.md +23 -7
- package/localized/zh-CN/commands/fec-refactor-plan.md +15 -0
- package/localized/zh-CN/commands/fec-refactor.md +14 -0
- package/localized/zh-CN/commands/fec-review.md +40 -9
- package/localized/zh-CN/commands/fec-smell.md +13 -0
- package/localized/zh-CN/commands/fec-tdd.md +4 -0
- package/localized/zh-CN/skills/eval_queries.json +244 -0
- package/localized/zh-CN/skills/fec-code-review/SKILL.md +20 -1
- package/localized/zh-CN/skills/fec-code-review/references/report-template.md +11 -1
- package/localized/zh-CN/skills/fec-code-smells/SKILL.md +33 -0
- package/localized/zh-CN/skills/fec-code-smells/references/detection-guide.md +25 -0
- package/localized/zh-CN/skills/fec-code-smells/references/smell-catalog.md +817 -0
- package/localized/zh-CN/skills/fec-code-smells/references/smell-to-refactoring-map.md +265 -0
- package/localized/zh-CN/skills/fec-image-generation/SKILL.md +31 -1
- package/localized/zh-CN/skills/fec-image-generation/assets/quality-examples/README.md +14 -0
- package/localized/zh-CN/skills/fec-image-generation/assets/quality-examples/architecture.json +121 -0
- package/localized/zh-CN/skills/fec-image-generation/assets/quality-examples/invalid-explicit.json +41 -0
- package/localized/zh-CN/skills/fec-image-generation/assets/quality-examples/sequence.json +53 -0
- package/localized/zh-CN/skills/fec-image-generation/assets/quality-examples/sequence.mmd +21 -0
- package/localized/zh-CN/skills/fec-image-generation/assets/quality-examples/workflow.json +98 -0
- package/localized/zh-CN/skills/fec-image-generation/assets/quality-examples/workflow.mmd +7 -0
- package/localized/zh-CN/skills/fec-image-generation/references/diagram-workflows.md +10 -3
- package/localized/zh-CN/skills/fec-image-generation/references/html-technical-diagrams.md +8 -1
- package/localized/zh-CN/skills/fec-image-generation/references/png-qa-autofix.md +9 -2
- package/localized/zh-CN/skills/fec-image-generation/scripts/diagram-browser.mjs +90 -0
- package/localized/zh-CN/skills/fec-image-generation/scripts/diagram-layout.mjs +176 -0
- package/localized/zh-CN/skills/fec-image-generation/scripts/export-diagram.mjs +48 -80
- package/localized/zh-CN/skills/fec-image-generation/scripts/mermaid-render.mjs +64 -0
- package/localized/zh-CN/skills/fec-image-generation/scripts/png-qa.mjs +35 -4
- package/localized/zh-CN/skills/fec-image-generation/scripts/tech-diagram-render.mjs +193 -53
- package/localized/zh-CN/skills/fec-refactor-clean/SKILL.md +3 -0
- package/localized/zh-CN/skills/fec-refactoring/SKILL.md +83 -0
- package/localized/zh-CN/skills/fec-refactoring/references/principles.md +43 -0
- package/localized/zh-CN/skills/fec-refactoring/references/refactoring-vs-feature-change.md +25 -0
- package/localized/zh-CN/skills/fec-refactoring/references/when-to-refactor.md +17 -0
- package/localized/zh-CN/skills/fec-refactoring/references/workflow.md +59 -0
- package/localized/zh-CN/skills/fec-refactoring-api/SKILL.md +25 -0
- package/localized/zh-CN/skills/fec-refactoring-api/references/api-refactorings.md +496 -0
- package/localized/zh-CN/skills/fec-refactoring-catalog/SKILL.md +21 -0
- package/localized/zh-CN/skills/fec-refactoring-catalog/references/catalog-index.md +99 -0
- package/localized/zh-CN/skills/fec-refactoring-catalog/references/composition-guide.md +49 -0
- package/localized/zh-CN/skills/fec-refactoring-catalog/references/selection-guide.md +41 -0
- package/localized/zh-CN/skills/fec-refactoring-control/SKILL.md +25 -0
- package/localized/zh-CN/skills/fec-refactoring-control/references/conditional-refactorings.md +299 -0
- package/localized/zh-CN/skills/fec-refactoring-data/SKILL.md +25 -0
- package/localized/zh-CN/skills/fec-refactoring-data/references/data-refactorings.md +248 -0
- package/localized/zh-CN/skills/fec-refactoring-encapsulation/SKILL.md +25 -0
- package/localized/zh-CN/skills/fec-refactoring-encapsulation/references/encapsulation-refactorings.md +452 -0
- package/localized/zh-CN/skills/fec-refactoring-functions/SKILL.md +25 -0
- package/localized/zh-CN/skills/fec-refactoring-functions/references/function-refactorings.md +567 -0
- package/localized/zh-CN/skills/fec-refactoring-inheritance/SKILL.md +25 -0
- package/localized/zh-CN/skills/fec-refactoring-inheritance/references/inheritance-refactorings.md +550 -0
- package/localized/zh-CN/skills/fec-refactoring-move-features/SKILL.md +25 -0
- package/localized/zh-CN/skills/fec-refactoring-move-features/references/moving-refactorings.md +453 -0
- package/localized/zh-CN/skills/fec-refactoring-validation/SKILL.md +52 -0
- package/localized/zh-CN/skills/fec-refactoring-validation/references/behavior-preservation.md +15 -0
- package/localized/zh-CN/skills/fec-refactoring-validation/references/refactoring-test-checklist.md +25 -0
- package/localized/zh-CN/skills/fec-security-review/SKILL.md +16 -0
- package/localized/zh-CN/skills/fec-security-review/references/report-template.md +10 -0
- package/localized/zh-CN/skills/fec-tdd-workflow/SKILL.md +3 -0
- package/localized/zh-CN/skills/metadata.json +408 -50
- package/localized/zh-CN/skills/relations.json +271 -3
- package/localized/zh-CN/templates/shared/rules/fec-agent-workflow.md +1 -0
- package/openclaw.plugin.json +2 -3
- package/package.json +27 -17
- package/skills/eval_queries.json +244 -0
- package/skills/fec-code-review/SKILL.md +20 -1
- package/skills/fec-code-review/references/report-template.md +11 -1
- package/skills/fec-code-smells/SKILL.md +33 -0
- package/skills/fec-code-smells/references/detection-guide.md +25 -0
- package/skills/fec-code-smells/references/smell-catalog.md +861 -0
- package/skills/fec-code-smells/references/smell-to-refactoring-map.md +265 -0
- package/skills/fec-data-fetching/SKILL.md +1 -1
- package/skills/fec-image-generation/SKILL.md +31 -1
- package/skills/fec-image-generation/assets/quality-examples/README.md +14 -0
- package/skills/fec-image-generation/assets/quality-examples/architecture.json +121 -0
- package/skills/fec-image-generation/assets/quality-examples/invalid-explicit.json +41 -0
- package/skills/fec-image-generation/assets/quality-examples/sequence.json +53 -0
- package/skills/fec-image-generation/assets/quality-examples/sequence.mmd +21 -0
- package/skills/fec-image-generation/assets/quality-examples/workflow.json +98 -0
- package/skills/fec-image-generation/assets/quality-examples/workflow.mmd +7 -0
- package/skills/fec-image-generation/references/diagram-workflows.md +10 -3
- package/skills/fec-image-generation/references/html-technical-diagrams.md +8 -1
- package/skills/fec-image-generation/references/png-qa-autofix.md +9 -2
- package/skills/fec-image-generation/scripts/diagram-browser.mjs +90 -0
- package/skills/fec-image-generation/scripts/diagram-layout.mjs +176 -0
- package/skills/fec-image-generation/scripts/export-diagram.mjs +48 -80
- package/skills/fec-image-generation/scripts/mermaid-render.mjs +64 -0
- package/skills/fec-image-generation/scripts/png-qa.mjs +35 -4
- package/skills/fec-image-generation/scripts/tech-diagram-render.mjs +193 -53
- package/skills/fec-legacy-web-standard/SKILL.md +1 -1
- package/skills/fec-refactor-clean/SKILL.md +3 -0
- package/skills/fec-refactoring/SKILL.md +83 -0
- package/skills/fec-refactoring/references/principles.md +60 -0
- package/skills/fec-refactoring/references/refactoring-vs-feature-change.md +27 -0
- package/skills/fec-refactoring/references/when-to-refactor.md +31 -0
- package/skills/fec-refactoring/references/workflow.md +90 -0
- package/skills/fec-refactoring-api/SKILL.md +25 -0
- package/skills/fec-refactoring-api/references/api-refactorings.md +496 -0
- package/skills/fec-refactoring-catalog/SKILL.md +28 -0
- package/skills/fec-refactoring-catalog/references/catalog-index.md +99 -0
- package/skills/fec-refactoring-catalog/references/composition-guide.md +49 -0
- package/skills/fec-refactoring-catalog/references/selection-guide.md +47 -0
- package/skills/fec-refactoring-control/SKILL.md +25 -0
- package/skills/fec-refactoring-control/references/conditional-refactorings.md +299 -0
- package/skills/fec-refactoring-data/SKILL.md +25 -0
- package/skills/fec-refactoring-data/references/data-refactorings.md +248 -0
- package/skills/fec-refactoring-encapsulation/SKILL.md +25 -0
- package/skills/fec-refactoring-encapsulation/references/encapsulation-refactorings.md +452 -0
- package/skills/fec-refactoring-functions/SKILL.md +25 -0
- package/skills/fec-refactoring-functions/references/function-refactorings.md +567 -0
- package/skills/fec-refactoring-inheritance/SKILL.md +25 -0
- package/skills/fec-refactoring-inheritance/references/inheritance-refactorings.md +550 -0
- package/skills/fec-refactoring-move-features/SKILL.md +25 -0
- package/skills/fec-refactoring-move-features/references/moving-refactorings.md +453 -0
- package/skills/fec-refactoring-validation/SKILL.md +59 -0
- package/skills/fec-refactoring-validation/references/behavior-preservation.md +53 -0
- package/skills/fec-refactoring-validation/references/refactoring-test-checklist.md +42 -0
- package/skills/fec-security-review/SKILL.md +16 -0
- package/skills/fec-security-review/references/report-template.md +10 -0
- package/skills/fec-tdd-workflow/SKILL.md +5 -1
- package/skills/metadata.json +434 -71
- package/skills/relations.json +264 -0
- package/templates/shared/rules/fec-agent-workflow.md +1 -0
- package/docs/superpowers/plans/2026-05-29-fec-debug.md +0 -491
- package/docs/superpowers/specs/2026-05-29-fec-debug-design.md +0 -340
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import zlib from "node:zlib";
|
|
6
|
+
import { segmentHits } from "./diagram-layout.mjs";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
6
8
|
|
|
9
|
+
if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) {
|
|
7
10
|
const args = parseArgs(process.argv.slice(2));
|
|
8
11
|
|
|
9
12
|
if (!args.png || args.help) {
|
|
@@ -26,6 +29,8 @@ try {
|
|
|
26
29
|
process.exit(1);
|
|
27
30
|
}
|
|
28
31
|
|
|
32
|
+
}
|
|
33
|
+
|
|
29
34
|
function parseArgs(values) {
|
|
30
35
|
const parsed = { format: "markdown", strict: false, help: false };
|
|
31
36
|
for (let index = 0; index < values.length; index += 1) {
|
|
@@ -50,6 +55,7 @@ function printHelp() {
|
|
|
50
55
|
function readManifest(filePath) {
|
|
51
56
|
const manifest = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
52
57
|
return {
|
|
58
|
+
...manifest,
|
|
53
59
|
canvas: manifest.canvas,
|
|
54
60
|
boxes: Array.isArray(manifest.boxes) ? manifest.boxes : [],
|
|
55
61
|
connectors: Array.isArray(manifest.connectors) ? manifest.connectors : [],
|
|
@@ -188,6 +194,8 @@ function analyze(image, manifest, strict) {
|
|
|
188
194
|
inkCoverage: Number(ink.coverage.toFixed(6)),
|
|
189
195
|
inkBounds: ink.bounds,
|
|
190
196
|
},
|
|
197
|
+
measurement: manifest?.measurement ?? "image-only",
|
|
198
|
+
verification: { image: "checked", geometry: manifest ? "checked" : "unavailable", text: manifest?.measurement === "browser" ? "browser-measured" : "estimated-or-unavailable", visual: "pending" },
|
|
191
199
|
issues,
|
|
192
200
|
nextActions: [...nextActions],
|
|
193
201
|
};
|
|
@@ -243,7 +251,7 @@ function colorDistance(left, right) {
|
|
|
243
251
|
return Math.abs(left.r - right.r) + Math.abs(left.g - right.g) + Math.abs(left.b - right.b) + Math.abs(left.a - right.a);
|
|
244
252
|
}
|
|
245
253
|
|
|
246
|
-
function analyzeManifest(image, manifest, issues, nextActions, strict) {
|
|
254
|
+
export function analyzeManifest(image, manifest, issues, nextActions, strict) {
|
|
247
255
|
if (manifest.canvas && (manifest.canvas.width !== image.width || manifest.canvas.height !== image.height)) {
|
|
248
256
|
addIssue(
|
|
249
257
|
issues,
|
|
@@ -254,12 +262,26 @@ function analyzeManifest(image, manifest, issues, nextActions, strict) {
|
|
|
254
262
|
);
|
|
255
263
|
}
|
|
256
264
|
|
|
257
|
-
const
|
|
265
|
+
for (const box of manifest.boxes) if (![box.x,box.y,box.width,box.height].every(Number.isFinite) || box.width <= 0 || box.height <= 0) addIssue(issues,nextActions,"invalid-box-geometry",`Invalid geometry for "${box.id}".`,"Correct source dimensions.");
|
|
266
|
+
const allBoxes = manifest.boxes.map(normalizeBox).filter(Boolean);
|
|
267
|
+
const boxes = allBoxes.filter((box) => box.kind !== "group");
|
|
268
|
+
for (const issue of manifest.issues ?? []) addIssue(issues, nextActions, issue.code, issue.message, "Fix the source layout and render again.");
|
|
269
|
+
const labels = manifest.labels ?? [];
|
|
270
|
+
for (const label of labels) {
|
|
271
|
+
const owner = allBoxes.find((box) => box.id === label.owner);
|
|
272
|
+
if (label.kind === "node" && !owner) addIssue(issues,nextActions,"label-owner-missing",`Label "${label.id}" has an unknown node owner.`,"Correct label ownership in the manifest.");
|
|
273
|
+
if (![label.x,label.y,label.width,label.height].every(Number.isFinite) || label.width < 0 || label.height < 0) addIssue(issues,nextActions,"invalid-label-geometry",`Label "${label.id}" has invalid bounds.`,"Measure source labels again.");
|
|
274
|
+
if (label.x < 0 || label.y < 0 || label.x + label.width > image.width || label.y + label.height > image.height) addIssue(issues, nextActions, "label-out-of-bounds", `Label "${label.id}" exceeds the canvas.`, "Expand the canvas or adjust the source label position.");
|
|
275
|
+
if (label.kind === "node" && owner && (label.x < owner.x || label.y < owner.y || label.x + label.width > owner.x + owner.width || label.y + label.height > owner.y + owner.height)) addIssue(issues, nextActions, "node-text-overflow", `Label "${label.id}" exceeds node "${owner.id}".`, "Expand the source node or adjust its text layout.");
|
|
276
|
+
if (Array.isArray(label.lines) && label.lines.join("").replace(/\r?\n/g, "") !== String(label.text ?? "").replace(/\r?\n/g, "")) addIssue(issues, nextActions, "label-content-loss", `Label "${label.id}" does not preserve source text.`, "Restore the complete source label.");
|
|
277
|
+
if (label.kind === "edge") for (const box of boxes) if (overlapArea(label, box) > 0) addIssue(issues, nextActions, "edge-label-collision", `Label "${label.id}" overlaps node "${box.id}".`, "Move the edge label to a clear route segment.");
|
|
278
|
+
}
|
|
279
|
+
for (let i = 0; i < labels.length; i++) for (let j = i + 1; j < labels.length; j++) if (overlapArea(labels[i], labels[j]) > 0) addIssue(issues, nextActions, "text-overlap", `Labels "${labels[i].id}" and "${labels[j].id}" overlap.`, "Increase label spacing in the source.");
|
|
258
280
|
for (const box of boxes) {
|
|
259
281
|
if (box.x < 0 || box.y < 0 || box.x + box.width > image.width || box.y + box.height > image.height) {
|
|
260
282
|
addIssue(issues, nextActions, "box-out-of-bounds", `Box "${box.id}" is outside the canvas.`, "Move out-of-bounds nodes inside the canvas or expand the canvas.");
|
|
261
283
|
}
|
|
262
|
-
if (box.label && estimateLabelOverflow(box, strict)) {
|
|
284
|
+
if (!labels.length && box.label && estimateLabelOverflow(box, strict)) {
|
|
263
285
|
addIssue(issues, nextActions, "label-overflow", `Box "${box.id}" label is likely to overflow.`, "Wrap or shorten labels, widen boxes, or increase export scale.");
|
|
264
286
|
}
|
|
265
287
|
}
|
|
@@ -277,9 +299,12 @@ function analyzeManifest(image, manifest, issues, nextActions, strict) {
|
|
|
277
299
|
const segments = [];
|
|
278
300
|
for (const connector of manifest.connectors) {
|
|
279
301
|
const points = normalizePoints(connector.points);
|
|
302
|
+
if(points.some(point=>point.x<0||point.y<0||point.x>image.width||point.y>image.height)) addIssue(issues,nextActions,"connector-out-of-bounds",`Connector "${connector.id}" exceeds the canvas.`,"Correct explicit waypoints or expand the source canvas.");
|
|
303
|
+
if (points.length < 2 || points.every((point) => point.x === points[0].x && point.y === points[0].y)) addIssue(issues, nextActions, "degenerate-connector", `Connector "${connector.id ?? "unnamed"}" has no visible path.`, "Give self calls a loop and other connectors distinct endpoints.");
|
|
280
304
|
for (let index = 0; index < points.length - 1; index += 1) {
|
|
281
305
|
const segment = { connector, a: points[index], b: points[index + 1] };
|
|
282
306
|
segments.push(segment);
|
|
307
|
+
for (const label of labels) if (segmentHits([segment.a.x,segment.a.y],[segment.b.x,segment.b.y],label)) addIssue(issues,nextActions,"connector-through-text",`Connector "${connector.id}" crosses label "${label.id}".`,"Move the source label or route around its text.");
|
|
283
308
|
for (const box of boxes) {
|
|
284
309
|
if (box.id === connector.from || box.id === connector.to) continue;
|
|
285
310
|
if (segmentIntersectsBox(segment, box)) {
|
|
@@ -306,6 +331,7 @@ function normalizeBox(box) {
|
|
|
306
331
|
if (!box || typeof box.id !== "string") return null;
|
|
307
332
|
const normalized = {
|
|
308
333
|
id: box.id,
|
|
334
|
+
kind: box.kind,
|
|
309
335
|
x: Number(box.x),
|
|
310
336
|
y: Number(box.y),
|
|
311
337
|
width: Number(box.width),
|
|
@@ -347,6 +373,10 @@ function overlapArea(left, right) {
|
|
|
347
373
|
}
|
|
348
374
|
|
|
349
375
|
function segmentIntersectsBox(segment, box) {
|
|
376
|
+
return segmentHits([segment.a.x,segment.a.y],[segment.b.x,segment.b.y],box);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function legacySegmentIntersectsBox(segment, box) {
|
|
350
380
|
if (pointInsideBox(segment.a, box) || pointInsideBox(segment.b, box)) return true;
|
|
351
381
|
const edges = [
|
|
352
382
|
[{ x: box.x, y: box.y }, { x: box.x + box.width, y: box.y }],
|
|
@@ -399,7 +429,8 @@ function rangesOverlap(left, right) {
|
|
|
399
429
|
|
|
400
430
|
function addIssue(issues, nextActions, code, message, action) {
|
|
401
431
|
issues.push({ code, message, action });
|
|
402
|
-
nextActions.add(action);
|
|
432
|
+
if (typeof nextActions.add === "function") nextActions.add(action);
|
|
433
|
+
else if (!nextActions.includes(action)) nextActions.push(action);
|
|
403
434
|
}
|
|
404
435
|
|
|
405
436
|
function renderMarkdown(report) {
|
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import { layoutText, fitNodeText, routeConnector, placeEdgeLabel, overlaps } from "./diagram-layout.mjs";
|
|
7
|
+
|
|
8
|
+
/** @type {import("./diagram-layout.mjs").QualityIssue[]} */
|
|
9
|
+
const qualityIssues = [];
|
|
10
|
+
/** @type {Array<{id:string,owner:string,kind:string,text:string,lines:string[],x:number,y:number,width:number,height:number}>} */
|
|
11
|
+
const textBounds = [];
|
|
12
|
+
/** @type {Box[]} */
|
|
13
|
+
let routeObstacles = [];
|
|
14
|
+
/** @type {Array<Array<[number,number]>>} */
|
|
15
|
+
const occupiedRoutes = [];
|
|
16
|
+
/** @type {Array<{id:string,label:string,points:Array<[number,number]>}>} */
|
|
17
|
+
const deferredLabels=[];
|
|
18
|
+
let finalCanvas = { width: 0, height: 0 };
|
|
6
19
|
|
|
7
20
|
const ALLOWED_TYPES = new Set(["workflow", "sequence", "dataflow", "lifecycle", "architecture"]);
|
|
8
21
|
const NODE_TYPES = new Set([
|
|
@@ -34,8 +47,12 @@ try {
|
|
|
34
47
|
/** @type {SummaryCard[]} */
|
|
35
48
|
const summary = "summary" in renderResult ? /** @type {SummaryCard[]} */ (renderResult.summary) : [];
|
|
36
49
|
const style = normalizeVisualStyle(diagram.visual, "/visual");
|
|
37
|
-
const
|
|
50
|
+
const rawHtml = renderHtml(diagram.meta.title, diagram.meta.subtitle ?? "", renderResult.svg, type, summary, style);
|
|
38
51
|
|
|
52
|
+
const pageStyles = rawHtml.match(/<style>([\s\S]*?)<\/style>/)?.[1] ?? "";
|
|
53
|
+
const diagramStyles=(pageStyles.match(/[^{}]+\{[^{}]*\}/g) ?? []).filter(rule=>/^\s*(?:[:][r]oot|\[data-|\.node|\.edge|\.group|\.lane|\.legend|\.flow|\.label|\.sublabel|\.muted|\.actor|\.step|\.lifeline|\.tech-diagram|\.surface|\.stage|marker|#arrow)/.test(rule)).join("\n");
|
|
54
|
+
const svgStyles = diagramStyles.replaceAll(":root", "svg").replaceAll(/\[data-theme=/g, "svg[data-theme=").replaceAll(/\[data-visual-style=/g, "svg[data-visual-style=");
|
|
55
|
+
const html = rawHtml.replace('data-visual-style="default" data-export-root', `data-visual-style="${style}" data-export-root`).replace(/(<svg[^>]*>)/, `$1<style>${svgStyles}\nsvg{font-family:ui-sans-serif,system-ui,"Segoe UI",sans-serif;}svg[data-theme="dark"]{--surface:#172033;--text:#e2e8f0;--muted:#94a3b8;--line:#94a3b8;}svg[data-theme="light"]{--surface:#f8fafc;--text:#0f172a;--muted:#475569;--line:#475569;--frontend:#0369a1;--backend:#047857;--database:#0e7490;--external:#475569;--agent:#0e7490;--model:#7e22ce;--memory:#047857;--gateway:#a16207;--browser:#0369a1;--queue:#c2410c;--flow-control:#b45309;--flow-data:#0369a1;--flow-read:#047857;--flow-write:#15803d;--flow-async:#64748b;--flow-feedback:#7e22ce;}</style>`);
|
|
39
56
|
fs.mkdirSync(path.dirname(path.resolve(outputPath)), { recursive: true });
|
|
40
57
|
fs.writeFileSync(outputPath, html, "utf8");
|
|
41
58
|
|
|
@@ -50,6 +67,7 @@ try {
|
|
|
50
67
|
input: inputPath,
|
|
51
68
|
output: outputPath,
|
|
52
69
|
manifest: args.manifest ?? null,
|
|
70
|
+
quality: { status: qualityIssues.length ? "partial" : "estimated", issues: qualityIssues },
|
|
53
71
|
nodes: renderResult.manifest.boxes.length,
|
|
54
72
|
connectors: renderResult.manifest.connectors.length,
|
|
55
73
|
};
|
|
@@ -165,6 +183,7 @@ function renderArchitecture(diagram) {
|
|
|
165
183
|
label,
|
|
166
184
|
sublabel: "",
|
|
167
185
|
type,
|
|
186
|
+
kind: "group",
|
|
168
187
|
};
|
|
169
188
|
groupBoxes.push(box);
|
|
170
189
|
}
|
|
@@ -187,6 +206,14 @@ function renderArchitecture(diagram) {
|
|
|
187
206
|
});
|
|
188
207
|
}
|
|
189
208
|
|
|
209
|
+
prepareBoxes(boxes, nodes);
|
|
210
|
+
for (const node of nodes) {
|
|
211
|
+
const box = boxes.get(String(node.id));
|
|
212
|
+
const group = groupBoxes.find((candidate) => candidate.id === node.group);
|
|
213
|
+
if (box && group && (box.x < group.x + 12 || box.y < group.y + 32 || box.x + box.width > group.x + group.width - 12 || box.y + box.height > group.y + group.height - 12)) qualityIssues.push({code:"group-containment",id:box.id,message:`Node "${box.id}" exceeds group "${group.id}" content area.`});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
|
|
190
217
|
/** @type {Connector[]} */
|
|
191
218
|
const connectors = [];
|
|
192
219
|
const flowSet = new Set();
|
|
@@ -209,11 +236,13 @@ function renderArchitecture(diagram) {
|
|
|
209
236
|
const contentBottom = Math.max(360, maxBoxExtent(allBoxes, "y") + 64);
|
|
210
237
|
const legend = normalizeLegend(diagram.legend, [...boxes.values()]);
|
|
211
238
|
const flowLegend = normalizeFlowLegend(diagram.flowLegend, [...flowSet]);
|
|
212
|
-
const
|
|
213
|
-
const
|
|
214
|
-
const
|
|
215
|
-
const
|
|
216
|
-
const
|
|
239
|
+
const width = Math.max(860, maxBoxExtent(allBoxes, "x") + 64);
|
|
240
|
+
const columns=Math.max(1,Math.floor((width-88)/200));
|
|
241
|
+
const legendHeight=legend.length ? 30+Math.ceil(legend.length/columns)*72 : 0;
|
|
242
|
+
const flowHeight=flowLegend.length ? 30+Math.ceil(flowLegend.length/columns)*72 : 0;
|
|
243
|
+
const legendSvg = renderArchitectureLegend(legend,44,contentBottom-4,columns);
|
|
244
|
+
const flowLegendSvg = renderFlowLegend(flowLegend,44,contentBottom+legendHeight,columns);
|
|
245
|
+
const height=contentBottom+legendHeight+flowHeight+32;
|
|
217
246
|
const groupSvg = groupBoxes.map(renderGroup).join("\n");
|
|
218
247
|
const boxSvg = [...boxes.values()].map(renderBox).join("\n");
|
|
219
248
|
const svg = wrapSvg(width, height, `${groupSvg}\n${connectionSvg}\n${boxSvg}\n${legendSvg}\n${flowLegendSvg}`);
|
|
@@ -249,8 +278,8 @@ function renderLaneDiagram(diagram, itemKey, edgeKey) {
|
|
|
249
278
|
assertObject(item, `/${itemKey}/${index}`);
|
|
250
279
|
return requireNumber(item, "col", `/${itemKey}/${index}/col`);
|
|
251
280
|
}));
|
|
252
|
-
|
|
253
|
-
|
|
281
|
+
let width = Math.max(780, 190 + (maxCol + 1) * 180);
|
|
282
|
+
let height = Math.max(420, 140 + lanes.length * 145);
|
|
254
283
|
const laneHeight = 118;
|
|
255
284
|
const top = 72;
|
|
256
285
|
const left = 142;
|
|
@@ -285,6 +314,36 @@ function renderLaneDiagram(diagram, itemKey, edgeKey) {
|
|
|
285
314
|
boxes.set(id, box);
|
|
286
315
|
}
|
|
287
316
|
|
|
317
|
+
prepareBoxes(boxes, items);
|
|
318
|
+
let laneTop = 72;
|
|
319
|
+
/** @type {Array<{y:number,height:number}>} */
|
|
320
|
+
const laneGeometry = [];
|
|
321
|
+
const columns = [...new Set(items.map((item) => Number(item.col)))].sort((a, b) => a - b);
|
|
322
|
+
const columnX = new Map();
|
|
323
|
+
let nextX = 160;
|
|
324
|
+
for (const col of columns) {
|
|
325
|
+
columnX.set(col, nextX);
|
|
326
|
+
nextX += Math.max(0, ...items.filter((item) => item.col === col).map((item) => boxes.get(String(item.id))?.width ?? 0)) + 64;
|
|
327
|
+
}
|
|
328
|
+
for (const lane of lanes) {
|
|
329
|
+
const members = items.filter((item) => item.lane === lane.id);
|
|
330
|
+
const columnBottom = new Map();
|
|
331
|
+
for (const item of members) {
|
|
332
|
+
const box = boxes.get(String(item.id));
|
|
333
|
+
if (!box) continue;
|
|
334
|
+
box.x = Number(columnX.get(Number(item.col)));
|
|
335
|
+
const baseline = columnBottom.get(item.col) ?? laneTop + 48;
|
|
336
|
+
box.y = baseline + (typeof item.yOffset === "number" ? item.yOffset : 0);
|
|
337
|
+
columnBottom.set(item.col, box.y + box.height + 32);
|
|
338
|
+
}
|
|
339
|
+
const bottom = Math.max(laneTop + 112, ...members.map((item) => { const b = boxes.get(String(item.id)); return b ? b.y + b.height + 24 : laneTop; }));
|
|
340
|
+
laneGeometry.push({ y: laneTop, height: bottom - laneTop });
|
|
341
|
+
laneTop = bottom + 32;
|
|
342
|
+
}
|
|
343
|
+
width = Math.max(width, nextX + 40);
|
|
344
|
+
height = Math.max(height, laneTop + 40);
|
|
345
|
+
|
|
346
|
+
|
|
288
347
|
/** @type {Connector[]} */
|
|
289
348
|
const connectors = [];
|
|
290
349
|
const edgeSvg = edges.map((edge, index) => {
|
|
@@ -303,8 +362,8 @@ function renderLaneDiagram(diagram, itemKey, edgeKey) {
|
|
|
303
362
|
}).join("\n");
|
|
304
363
|
|
|
305
364
|
const laneSvg = lanes.map((lane, index) => {
|
|
306
|
-
const y =
|
|
307
|
-
return `<g class="lane"><rect x="34" y="${y}" width="${width - 68}" height="${
|
|
365
|
+
const y = laneGeometry[index].y;
|
|
366
|
+
return `<g class="lane"><rect x="34" y="${y}" width="${width - 68}" height="${laneGeometry[index].height}" rx="10"/><text x="52" y="${y + 28}" class="label muted">${escapeHtml(String(lane.label))}</text></g>`;
|
|
308
367
|
}).join("\n");
|
|
309
368
|
const boxSvg = [...boxes.values()].map(renderBox).join("\n");
|
|
310
369
|
const svg = wrapSvg(width, height, `${laneSvg}\n${edgeSvg}\n${boxSvg}`);
|
|
@@ -323,8 +382,8 @@ function renderSequence(diagram) {
|
|
|
323
382
|
const participants = requireObjectArray(diagram.participants, "/participants");
|
|
324
383
|
const messages = requireObjectArray(diagram.messages, "/messages");
|
|
325
384
|
const ids = new Set();
|
|
326
|
-
|
|
327
|
-
|
|
385
|
+
let width = Math.max(760, 160 + participants.length * 170);
|
|
386
|
+
let height = Math.max(420, 170 + messages.length * 82);
|
|
328
387
|
const top = 74;
|
|
329
388
|
const laneGap = (width - 160) / Math.max(1, participants.length - 1);
|
|
330
389
|
/** @type {Map<string, Box>} */
|
|
@@ -349,25 +408,40 @@ function renderSequence(diagram) {
|
|
|
349
408
|
});
|
|
350
409
|
}
|
|
351
410
|
|
|
411
|
+
prepareBoxes(boxes, participants);
|
|
412
|
+
const gap = Math.max(280, ...[...boxes.values()].map(box=>box.width+80), ...messages.map((m) => layoutText(typeof m.label === "string" ? m.label : "", 260, 12, 16).width + 60));
|
|
413
|
+
width = Math.max(width, 160 + participants.length * gap);
|
|
414
|
+
[...boxes.values()].forEach((box, index) => { box.x = 80 + index * gap + gap / 2 - box.width / 2; });
|
|
415
|
+
let messageY = Math.max(...[...boxes.values()].map((box) => box.y + box.height)) + 72;
|
|
416
|
+
height = Math.max(height, messageY + messages.length * 108 + 80);
|
|
417
|
+
|
|
418
|
+
|
|
352
419
|
/** @type {Connector[]} */
|
|
353
420
|
const connectors = [];
|
|
354
|
-
|
|
421
|
+
|
|
355
422
|
const messageSvg = messages.map((message, index) => {
|
|
356
423
|
assertObject(message, `/messages/${index}`);
|
|
424
|
+
if (["activation","activate","deactivate","fragment","alt","loop","par","async"].some((key)=>key in message)) throw usageError(`/messages/${index}`,"Complex sequence semantics require the Mermaid route; do not simplify activation/fragments/async fields.");
|
|
357
425
|
const from = requireId(message, "from", `/messages/${index}/from`);
|
|
358
426
|
const to = requireId(message, "to", `/messages/${index}/to`);
|
|
359
427
|
const fromBox = boxes.get(from);
|
|
360
428
|
const toBox = boxes.get(to);
|
|
361
429
|
if (!fromBox) throw usageError(`/messages/${index}/from`, `Unknown endpoint "${from}".`);
|
|
362
430
|
if (!toBox) throw usageError(`/messages/${index}/to`, `Unknown endpoint "${to}".`);
|
|
363
|
-
const
|
|
431
|
+
const messageText = typeof message.label === "string" ? message.label : "";
|
|
432
|
+
const textLayout = layoutText(messageText, 190, 12, 16);
|
|
433
|
+
const y = messageY;
|
|
434
|
+
messageY += Math.max(72, textLayout.height + 56 + (from === to ? 32 : 0));
|
|
364
435
|
/** @type {Array<[number, number]>} */
|
|
365
|
-
const points = [[centerX(fromBox), y], [centerX(toBox), y]];
|
|
436
|
+
const points = from === to ? [[centerX(fromBox), y], [centerX(fromBox) + 64, y], [centerX(fromBox) + 64, y + 32], [centerX(fromBox), y + 32]] : [[centerX(fromBox), y], [centerX(toBox), y]];
|
|
437
|
+
occupiedRoutes.push(points);
|
|
366
438
|
const variant = normalizeVariant(message.variant, `/messages/${index}/variant`);
|
|
367
|
-
connectors.push({ id: `${from}-${to}-${index}`, from, to, points });
|
|
439
|
+
connectors.push({ id: `${from}-${to}-${index}`, from, to, points, kind: "sequence", self: from === to });
|
|
368
440
|
return renderConnector(points, variant, typeof message.label === "string" ? message.label : "");
|
|
369
441
|
}).join("\n");
|
|
370
442
|
const boxSvg = [...boxes.values()].map(renderBox).join("\n");
|
|
443
|
+
height=Math.max(height,messageY+80);
|
|
444
|
+
const lifelines = [...boxes.values()].map((box) => `<line x1="${centerX(box)}" y1="${box.y + box.height}" x2="${centerX(box)}" y2="${height - 56}" class="lifeline"/>`).join("\n");
|
|
371
445
|
const svg = wrapSvg(width, height, `${lifelines}\n${messageSvg}\n${boxSvg}`);
|
|
372
446
|
|
|
373
447
|
return { svg, manifest: manifestFrom(width, height, [...boxes.values()], connectors) };
|
|
@@ -410,6 +484,8 @@ function renderDataflow(diagram) {
|
|
|
410
484
|
});
|
|
411
485
|
}
|
|
412
486
|
|
|
487
|
+
prepareBoxes(boxes, nodes);
|
|
488
|
+
|
|
413
489
|
/** @type {Connector[]} */
|
|
414
490
|
const connectors = [];
|
|
415
491
|
const stageSvg = stages.map((stage, index) => {
|
|
@@ -463,22 +539,45 @@ function countStageBefore(nodes, endIndex, stage) {
|
|
|
463
539
|
return count;
|
|
464
540
|
}
|
|
465
541
|
|
|
542
|
+
|
|
543
|
+
/** @param {Map<string, Box>} boxes @param {Array<Record<string, unknown>>} sources */
|
|
544
|
+
function prepareBoxes(boxes, sources) {
|
|
545
|
+
for (const [id, box] of boxes) {
|
|
546
|
+
const source = sources.find((item) => item.id === id) ?? {};
|
|
547
|
+
const fitted = fitNodeText(box, { width: typeof source.width === "number", height: typeof source.height === "number" });
|
|
548
|
+
boxes.set(id, fitted.box);
|
|
549
|
+
qualityIssues.push(...fitted.issues);
|
|
550
|
+
}
|
|
551
|
+
routeObstacles = [...boxes.values()];
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** @param {string} id @param {string} owner @param {string} kind @param {string} text @param {import("./diagram-layout.mjs").TextLayout} layout @param {number} cx @param {number} top @param {string} cls */
|
|
555
|
+
function renderText(id, owner, kind, text, layout, cx, top, cls) {
|
|
556
|
+
textBounds.push({ id, owner, kind, text, lines: layout.lines, x: cx - layout.width / 2, y: top, width: layout.width, height: layout.height });
|
|
557
|
+
return `<text data-label-id="${escapeHtml(id)}" x="${cx}" y="${top + layout.fontSize}" class="${cls}" font-size="${layout.fontSize}" text-anchor="middle">${layout.lines.map((line, i) => `<tspan x="${cx}" dy="${i ? layout.lineHeight : 0}">${escapeHtml(line)}</tspan>`).join("")}</text>`;
|
|
558
|
+
}
|
|
559
|
+
|
|
466
560
|
/**
|
|
467
561
|
* @param {Box} box
|
|
468
562
|
*/
|
|
469
563
|
function renderBox(box) {
|
|
470
|
-
const labelLines = wrapLabel(box.label, 18).slice(0, 2);
|
|
471
564
|
const shape = box.shape ?? "box";
|
|
472
565
|
const isSemantic = isSemanticNodeType(box.type);
|
|
473
|
-
const
|
|
474
|
-
const
|
|
475
|
-
const
|
|
476
|
-
const
|
|
477
|
-
const
|
|
566
|
+
const available = shape === "diamond" ? box.width * 0.5 - 12 : (box.type === "queue" ? box.width - 64 : box.width - 32);
|
|
567
|
+
const main = layoutText(box.label, available);
|
|
568
|
+
const sub = layoutText(box.sublabel, available, 12, 16);
|
|
569
|
+
const total = main.height + (box.sublabel ? sub.height + 6 : 0);
|
|
570
|
+
const textTop = shape === "diamond" ? centerY(box) - total / 2 : box.y + (["database", "vectorstore"].includes(box.type) ? 30 : box.type === "queue" ? 20 : isSemantic ? 46 : box.actor ? 40 : 20);
|
|
571
|
+
const labelSvg = renderText(box.id + "-label", box.id, "node", box.label, main, centerX(box), textTop, "label");
|
|
572
|
+
const sublabel = box.sublabel ? renderText(box.id + "-sub", box.id, "node", box.sublabel, sub, centerX(box), textTop + main.height + 6, "sublabel") : "";
|
|
573
|
+
const actorLayout=layoutText(box.actor??"",box.width-32,12,16);
|
|
574
|
+
const actor = box.actor ? renderText(box.id+"-actor",box.id,"group",box.actor,actorLayout,centerX(box),box.y-actorLayout.height-12,"actor") : "";
|
|
575
|
+
const stepLayout=layoutText(box.step??"",Math.max(14,box.width-16),9,12);
|
|
576
|
+
const step = box.step ? `<g class="step-badge"><circle cx="${box.x + 13}" cy="${box.y + 13}" r="${Math.max(10,stepLayout.width/2+4)}"/>${renderText(box.id+"-step",box.id,"group",box.step,stepLayout,box.x+13,box.y+7,"step-label")}</g>` : "";
|
|
478
577
|
const shapeSvg = renderSemanticShape(box) ?? (shape === "diamond"
|
|
479
578
|
? `<polygon points="${centerX(box)},${box.y} ${box.x + box.width},${centerY(box)} ${centerX(box)},${box.y + box.height} ${box.x},${centerY(box)}"/>`
|
|
480
579
|
: `<rect x="${box.x}" y="${box.y}" width="${box.width}" height="${box.height}" rx="${shape === "terminal" ? Math.round(box.height / 2) : 10}"/>`);
|
|
481
|
-
return `<g class="node node-${box.type}">${actor}\n${shapeSvg}\n${step}\n${labelSvg}\n${sublabel}</g>`;
|
|
580
|
+
return `<g data-node-id="${escapeHtml(box.id)}" class="node node-${box.type}">${actor}\n${shapeSvg}\n${step}\n${labelSvg}\n${sublabel}</g>`;
|
|
482
581
|
}
|
|
483
582
|
|
|
484
583
|
/**
|
|
@@ -514,8 +613,9 @@ function renderSemanticShape(box) {
|
|
|
514
613
|
return `<path d="M ${box.x} ${box.y} L ${box.x + box.width - fold} ${box.y} L ${box.x + box.width} ${box.y + fold} L ${box.x + box.width} ${box.y + box.height} L ${box.x} ${box.y + box.height} Z"/><path d="M ${box.x + box.width - fold} ${box.y} L ${box.x + box.width - fold} ${box.y + fold} L ${box.x + box.width} ${box.y + fold}" fill="none"/>`;
|
|
515
614
|
}
|
|
516
615
|
if (box.type === "queue") {
|
|
517
|
-
const ry =
|
|
518
|
-
|
|
616
|
+
const ry = box.height / 2;
|
|
617
|
+
const end = Math.min(18, box.width / 6);
|
|
618
|
+
return `<ellipse cx="${box.x + end}" cy="${cy}" rx="${end}" ry="${ry}"/><rect x="${box.x + end}" y="${cy - ry}" width="${box.width - end * 2}" height="${ry * 2}" stroke="none"/><line x1="${box.x + end}" y1="${cy - ry}" x2="${box.x + box.width - end}" y2="${cy - ry}"/><line x1="${box.x + end}" y1="${cy + ry}" x2="${box.x + box.width - end}" y2="${cy + ry}"/><ellipse cx="${box.x + box.width - end}" cy="${cy}" rx="${end}" ry="${ry}"/>`;
|
|
519
619
|
}
|
|
520
620
|
if (box.type === "browser") {
|
|
521
621
|
return `<rect x="${box.x}" y="${box.y}" width="${box.width}" height="${box.height}" rx="10"/><rect x="${box.x}" y="${box.y}" width="${box.width}" height="20" rx="10" class="node-chrome"/><circle cx="${box.x + 14}" cy="${box.y + 10}" r="3" class="node-dot"/><circle cx="${box.x + 26}" cy="${box.y + 10}" r="3" class="node-dot"/><circle cx="${box.x + 38}" cy="${box.y + 10}" r="3" class="node-dot"/>`;
|
|
@@ -534,7 +634,7 @@ function renderSemanticShape(box) {
|
|
|
534
634
|
* @param {Box} box
|
|
535
635
|
*/
|
|
536
636
|
function renderGroup(box) {
|
|
537
|
-
return `<g class="group group-${box.type}"><rect x="${box.x}" y="${box.y}" width="${box.width}" height="${box.height}" rx="14"
|
|
637
|
+
return `<g class="group group-${box.type}"><rect x="${box.x}" y="${box.y}" width="${box.width}" height="${box.height}" rx="14"/>${renderText(box.id+"-group",box.id,"group",box.label,layoutText(box.label,box.width-32,12,16),box.x+box.width/2,box.y+10,"sublabel")}</g>`;
|
|
538
638
|
}
|
|
539
639
|
|
|
540
640
|
/**
|
|
@@ -544,8 +644,12 @@ function renderGroup(box) {
|
|
|
544
644
|
*/
|
|
545
645
|
function renderConnector(points, variant, label, flow = "") {
|
|
546
646
|
const pathData = points.map(([x, y], index) => `${index === 0 ? "M" : "L"} ${x} ${y}`).join(" ");
|
|
547
|
-
const
|
|
548
|
-
const
|
|
647
|
+
const textLayout = layoutText(label, 190, 12, 16);
|
|
648
|
+
const placed = placeEdgeLabel(points, textLayout, [...routeObstacles, ...textBounds]);
|
|
649
|
+
const id = `edge-label-${textBounds.length}`;
|
|
650
|
+
if (label) qualityIssues.push(...placed.issues.map((issue) => ({ ...issue, id })));
|
|
651
|
+
if(label) deferredLabels.push({id,label,points});
|
|
652
|
+
const labelSvg = label ? renderText(id, id, "edge", label, textLayout, placed.rect.x + placed.rect.width / 2, placed.rect.y, "edge-label") : "";
|
|
549
653
|
const marker = flow ? `flow-${flow}` : (variant === "return" ? "default" : variant);
|
|
550
654
|
return `<g class="edge edge-${variant}${flow ? ` flow-${flow}` : ""}"><path d="${pathData}" marker-end="url(#arrow-${marker})"/>\n${labelSvg}</g>`;
|
|
551
655
|
}
|
|
@@ -556,7 +660,19 @@ function renderConnector(points, variant, label, flow = "") {
|
|
|
556
660
|
* @param {string} body
|
|
557
661
|
*/
|
|
558
662
|
function wrapSvg(width, height, body) {
|
|
559
|
-
|
|
663
|
+
for(let i=textBounds.length-1;i>=0;i--) if(textBounds[i].kind === "edge") textBounds.splice(i,1);
|
|
664
|
+
for(const {id,label,points} of deferredLabels) {
|
|
665
|
+
const layout=layoutText(label,190,12,16);
|
|
666
|
+
const placed=placeEdgeLabel(points,layout,[...routeObstacles,...textBounds],occupiedRoutes);
|
|
667
|
+
qualityIssues.push(...placed.issues);
|
|
668
|
+
const replacement=renderText(id,id,"edge",label,layout,placed.rect.x+placed.rect.width/2,placed.rect.y,"edge-label");
|
|
669
|
+
body=body.replace(new RegExp('<text data-label-id="'+id+'"[^>]*>[\\s\\S]*?</text>'),replacement);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
width = Math.ceil(Math.max(width, ...routeObstacles.map((b) => b.x + b.width + 40), ...textBounds.map((b) => b.x + b.width + 40), ...occupiedRoutes.flat().map((p) => p[0] + 40)));
|
|
673
|
+
height = Math.ceil(Math.max(height, ...routeObstacles.map((b) => b.y + b.height + 40), ...textBounds.map((b) => b.y + b.height + 40), ...occupiedRoutes.flat().map((p) => p[1] + 40)));
|
|
674
|
+
finalCanvas = { width, height };
|
|
675
|
+
return `<svg class="tech-diagram" data-theme="light" data-visual-style="default" data-export-root="1" role="img" viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg">
|
|
560
676
|
<defs>
|
|
561
677
|
<marker id="arrow-default" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z"/></marker>
|
|
562
678
|
<marker id="arrow-emphasis" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z"/></marker>
|
|
@@ -760,8 +876,8 @@ button {
|
|
|
760
876
|
.node-glyph-outline, .node-glyph-line { fill: none; stroke: currentColor; }
|
|
761
877
|
.node-chrome { fill: currentColor; opacity: 0.25; stroke: none; }
|
|
762
878
|
.node-dot { fill: currentColor; stroke: none; opacity: 0.65; }
|
|
763
|
-
.label { fill: var(--text); font-weight: 650; font-size:
|
|
764
|
-
.sublabel, .muted, .actor { fill: var(--muted); font-size:
|
|
879
|
+
.label { fill: var(--text); font-weight: 650; font-size: 14px; }
|
|
880
|
+
.sublabel, .muted, .actor { fill: var(--muted); font-size: 12px; }
|
|
765
881
|
.actor { font-weight: 650; }
|
|
766
882
|
.step-badge circle { fill: var(--panel); stroke: currentColor; stroke-width: 1.3; }
|
|
767
883
|
.step-badge text { fill: var(--text); font-size: 9px; font-weight: 750; }
|
|
@@ -775,7 +891,7 @@ button {
|
|
|
775
891
|
.flow-write path { stroke: var(--flow-write); stroke-dasharray: 5 4; }
|
|
776
892
|
.flow-async path { stroke: var(--flow-async); stroke-dasharray: 4 4; }
|
|
777
893
|
.flow-feedback path { stroke: var(--flow-feedback); stroke-width: 2; }
|
|
778
|
-
.edge-label { fill: var(--muted); font-size:
|
|
894
|
+
.edge-label { fill: var(--muted); font-size: 12px; paint-order: stroke; stroke: var(--surface); stroke-width: 5px; stroke-linejoin: round; }
|
|
779
895
|
.lifeline { stroke: color-mix(in srgb, var(--muted), transparent 48%); stroke-dasharray: 4 5; }
|
|
780
896
|
.summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 10px; margin-top: 12px; }
|
|
781
897
|
.summary-card { background: var(--panel); border: 1px solid color-mix(in srgb, var(--muted), transparent 74%); border-radius: 8px; padding: 12px; }
|
|
@@ -823,16 +939,25 @@ ${summaryHtml}
|
|
|
823
939
|
<script>
|
|
824
940
|
const root = document.documentElement;
|
|
825
941
|
const stored = localStorage.getItem("fec-tech-diagram-theme");
|
|
826
|
-
|
|
942
|
+
const diagramSvg = document.querySelector("[data-export-root]");
|
|
943
|
+
root.dataset.theme = stored === "dark" ? "dark" : "light";
|
|
944
|
+
diagramSvg.dataset.theme = root.dataset.theme;
|
|
945
|
+
diagramSvg.dataset.visualStyle = root.dataset.visualStyle;
|
|
827
946
|
document.querySelector('[data-action="theme"]').addEventListener("click", () => {
|
|
828
947
|
const next = root.dataset.theme === "light" ? "dark" : "light";
|
|
829
948
|
root.dataset.theme = next;
|
|
949
|
+
diagramSvg.dataset.theme = next;
|
|
830
950
|
localStorage.setItem("fec-tech-diagram-theme", next);
|
|
831
951
|
});
|
|
832
952
|
document.querySelector('[data-action="print"]').addEventListener("click", () => window.print());
|
|
833
953
|
document.querySelector('[data-action="svg"]').addEventListener("click", () => {
|
|
834
954
|
const svg = document.querySelector("[data-export-root]");
|
|
835
|
-
const
|
|
955
|
+
const clone = svg.cloneNode(true);
|
|
956
|
+
const originals = [svg, ...svg.querySelectorAll("*")];
|
|
957
|
+
const copies = [clone, ...clone.querySelectorAll("*")];
|
|
958
|
+
const properties = ["fill","stroke","stroke-width","stroke-dasharray","font-family","font-size","font-weight","color","opacity","paint-order","text-anchor"];
|
|
959
|
+
originals.forEach((element,index) => { const computed=getComputedStyle(element); properties.forEach(property=>copies[index].style?.setProperty(property,computed.getPropertyValue(property))); });
|
|
960
|
+
const source = new XMLSerializer().serializeToString(clone);
|
|
836
961
|
const blob = new Blob([source], { type: "image/svg+xml" });
|
|
837
962
|
const link = document.createElement("a");
|
|
838
963
|
link.href = URL.createObjectURL(blob);
|
|
@@ -866,10 +991,20 @@ ${summary.map((card) => ` <article class="summary-card summary-${card.type}">
|
|
|
866
991
|
* @param {Connector[]} connectors
|
|
867
992
|
*/
|
|
868
993
|
function manifestFrom(width, height, boxes, connectors) {
|
|
994
|
+
for(const connector of connectors) if(connector.points.some(([x,y])=>x<0||y<0)) qualityIssues.push({code:"connector-out-of-bounds",id:connector.id,message:`Connector "${connector.id}" has negative explicit coordinates.`});
|
|
995
|
+
const nodes = boxes.filter((box) => box.kind !== "group");
|
|
996
|
+
for (const box of boxes) if (box.x < 0 || box.y < 0) qualityIssues.push({code:"box-out-of-bounds",id:box.id,message:`Box "${box.id}" has negative coordinates.`});
|
|
997
|
+
for (let i=0;i<nodes.length;i++) for (let j=i+1;j<nodes.length;j++) if (overlaps(nodes[i],nodes[j])) qualityIssues.push({code:"box-overlap",id:nodes[i].id,message:`Nodes "${nodes[i].id}" and "${nodes[j].id}" overlap.`});
|
|
869
998
|
return {
|
|
870
|
-
canvas: { width, height },
|
|
871
|
-
boxes: boxes.map(({ id, x, y, width: boxWidth, height: boxHeight, label }) => ({ id, x, y, width: boxWidth, height: boxHeight, label })),
|
|
999
|
+
canvas: finalCanvas.width ? finalCanvas : { width, height },
|
|
1000
|
+
boxes: boxes.map(({ id, x, y, width: boxWidth, height: boxHeight, label, kind }) => ({ id, x, y, width: boxWidth, height: boxHeight, label, kind })),
|
|
1001
|
+
groups: boxes.filter((box) => box.kind === "group").map((box) => ({ ...box })),
|
|
872
1002
|
connectors,
|
|
1003
|
+
labels: textBounds,
|
|
1004
|
+
coordinateSpace: "svg",
|
|
1005
|
+
scale: 1,
|
|
1006
|
+
measurement: "estimated",
|
|
1007
|
+
issues: qualityIssues,
|
|
873
1008
|
};
|
|
874
1009
|
}
|
|
875
1010
|
|
|
@@ -905,14 +1040,15 @@ function routeWithWaypoints(from, to, waypoints, pointer) {
|
|
|
905
1040
|
if (!Array.isArray(waypoints)) throw usageError(pointer, "Expected array of [x, y] waypoints.");
|
|
906
1041
|
/** @type {Array<[number, number]>} */
|
|
907
1042
|
const parsed = waypoints.map((point, index) => {
|
|
908
|
-
if (!Array.isArray(point) || point.length !== 2 || typeof point[0] !== "number" || typeof point[1] !== "number") {
|
|
1043
|
+
if (!Array.isArray(point) || point.length !== 2 || typeof point[0] !== "number" || typeof point[1] !== "number" || !Number.isFinite(point[0]) || !Number.isFinite(point[1])) {
|
|
909
1044
|
throw usageError(`${pointer}/${index}`, "Expected [x, y] number tuple.");
|
|
910
1045
|
}
|
|
911
1046
|
return [point[0], point[1]];
|
|
912
1047
|
});
|
|
913
|
-
const
|
|
914
|
-
|
|
915
|
-
|
|
1048
|
+
const result = routeConnector(from, to, routeObstacles.filter((box) => box.id !== from.id && box.id !== to.id), occupiedRoutes, parsed);
|
|
1049
|
+
qualityIssues.push(...result.issues);
|
|
1050
|
+
occupiedRoutes.push(result.points);
|
|
1051
|
+
return result.points;
|
|
916
1052
|
}
|
|
917
1053
|
|
|
918
1054
|
/**
|
|
@@ -1006,11 +1142,13 @@ function normalizeLegend(legend, boxes) {
|
|
|
1006
1142
|
* @param {number} x
|
|
1007
1143
|
* @param {number} y
|
|
1008
1144
|
*/
|
|
1009
|
-
function renderArchitectureLegend(items, x, y) {
|
|
1145
|
+
function renderArchitectureLegend(items, x, y, columns = 4) {
|
|
1010
1146
|
if (items.length === 0) return "";
|
|
1011
1147
|
const itemSvg = items.map((item, index) => {
|
|
1012
|
-
const itemX = x + index *
|
|
1013
|
-
|
|
1148
|
+
const itemX = x + (index % columns) * 200;
|
|
1149
|
+
const itemY = y + Math.floor(index / columns) * 72;
|
|
1150
|
+
const text = layoutText(item.label,160,12,16);
|
|
1151
|
+
return `<g class="node node-${item.type}"><rect x="${itemX}" y="${itemY + 16}" width="16" height="10" rx="2"/>${renderText("legend-"+index,"legend","group",item.label,text,itemX+24+text.width/2,itemY+14,"sublabel")}</g>`;
|
|
1014
1152
|
}).join("\n");
|
|
1015
1153
|
return `<g class="legend"><text x="${x}" y="${y}" class="label">Legend</text>\n${itemSvg}</g>`;
|
|
1016
1154
|
}
|
|
@@ -1020,11 +1158,13 @@ function renderArchitectureLegend(items, x, y) {
|
|
|
1020
1158
|
* @param {number} x
|
|
1021
1159
|
* @param {number} y
|
|
1022
1160
|
*/
|
|
1023
|
-
function renderFlowLegend(items, x, y) {
|
|
1161
|
+
function renderFlowLegend(items, x, y, columns = 4) {
|
|
1024
1162
|
if (items.length === 0) return "";
|
|
1025
1163
|
const itemSvg = items.map((item, index) => {
|
|
1026
|
-
const itemX = x + index *
|
|
1027
|
-
|
|
1164
|
+
const itemX = x + (index % columns) * 200;
|
|
1165
|
+
const itemY = y + Math.floor(index / columns) * 72;
|
|
1166
|
+
const text = layoutText(item.label,152,12,16);
|
|
1167
|
+
return `<g class="edge flow-${item.type}"><path d="M ${itemX} ${itemY + 22} L ${itemX + 28} ${itemY + 22}" marker-end="url(#arrow-flow-${item.type})"/>${renderText("flow-legend-"+index,"flow-legend","group",item.label,text,itemX+38+text.width/2,itemY+14,"sublabel")}</g>`;
|
|
1028
1168
|
}).join("\n");
|
|
1029
1169
|
return `<g class="flow-legend"><text x="${x}" y="${y}" class="label">Flow Legend</text>\n${itemSvg}</g>`;
|
|
1030
1170
|
}
|
|
@@ -1115,13 +1255,10 @@ function normalizeSummary(summary) {
|
|
|
1115
1255
|
* @returns {Array<[number, number]>}
|
|
1116
1256
|
*/
|
|
1117
1257
|
function routeBoxes(from, to) {
|
|
1118
|
-
const
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
}
|
|
1123
|
-
const midX = Math.round((start[0] + end[0]) / 2);
|
|
1124
|
-
return [[start[0], start[1]], [midX, start[1]], [midX, end[1]], [end[0], end[1]]];
|
|
1258
|
+
const result = routeConnector(from, to, routeObstacles.filter((box) => box.id !== from.id && box.id !== to.id), occupiedRoutes);
|
|
1259
|
+
qualityIssues.push(...result.issues);
|
|
1260
|
+
occupiedRoutes.push(result.points);
|
|
1261
|
+
return result.points;
|
|
1125
1262
|
}
|
|
1126
1263
|
|
|
1127
1264
|
/**
|
|
@@ -1341,6 +1478,7 @@ function printReport(report, format) {
|
|
|
1341
1478
|
* type: string;
|
|
1342
1479
|
* actor?: string;
|
|
1343
1480
|
* step?: string;
|
|
1481
|
+
* kind?: string;
|
|
1344
1482
|
* shape?: "box" | "diamond" | "terminal";
|
|
1345
1483
|
* }} Box
|
|
1346
1484
|
*
|
|
@@ -1348,6 +1486,8 @@ function printReport(report, format) {
|
|
|
1348
1486
|
* id: string;
|
|
1349
1487
|
* from: string;
|
|
1350
1488
|
* to: string;
|
|
1489
|
+
* kind?: string;
|
|
1490
|
+
* self?: boolean;
|
|
1351
1491
|
* points: Array<[number, number]>;
|
|
1352
1492
|
* }} Connector
|
|
1353
1493
|
*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: fec-legacy-web-standard
|
|
3
|
-
description: Use when maintaining or safely modifying existing non-framework frontend code: vanilla JavaScript, jQuery, HTML/CSS, MPA pages, server-rendered templates, legacy plugins, or long-lived code that should stay in its current stack. Use migration skill when planning a move to React/Vue/TypeScript; Chinese triggers include traditional frontend, native JS, jQuery.
|
|
3
|
+
description: "Use when maintaining or safely modifying existing non-framework frontend code: vanilla JavaScript, jQuery, HTML/CSS, MPA pages, server-rendered templates, legacy plugins, or long-lived code that should stay in its current stack. Use migration skill when planning a move to React/Vue/TypeScript; Chinese triggers include traditional frontend, native JS, jQuery."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Traditional front-end project specifications (JS + jQuery + HTML)
|
|
@@ -49,3 +49,6 @@ When writing a cleanup report, load [references/report-template.md](references/r
|
|
|
49
49
|
|
|
50
50
|
- Clean reports are saved as `reports/refactor-clean-YYYY-MM-DD-HHmmss.md`.
|
|
51
51
|
- After the code is cleaned, the relevant verification commands pass, or the cause of the blocking is clearly stated.
|
|
52
|
+
## Structural Refactoring Boundary
|
|
53
|
+
|
|
54
|
+
This workflow owns proven dead/unused code. When code is live but structurally difficult—long functions, ownership problems, mutable/derived data, conditionals, API shape, or hierarchy design—route it to structural refactoring instead of deleting or rewriting it under cleanup.
|