@bovinphang/craft-common 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +12 -2
- package/dist/content/catalog.d.ts +4 -0
- package/dist/content/resources.d.ts +2 -0
- package/dist/content/types.d.ts +21 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +668 -0
- package/dist/install/identity.d.ts +5 -0
- package/dist/install/planner.d.ts +6 -0
- package/dist/install/transaction.d.ts +7 -0
- package/dist/install/types.d.ts +44 -0
- package/docs/consuming-common.md +33 -0
- package/localized/zh-CN/methods/debug.md +13 -0
- package/localized/zh-CN/methods/doc-sync.md +12 -0
- package/localized/zh-CN/methods/refactoring.md +16 -0
- package/localized/zh-CN/methods/source-driven-development.md +19 -0
- package/localized/zh-CN/methods/tdd.md +13 -0
- package/localized/zh-CN/skills/cc-debug-methodology/SKILL.md +8 -0
- package/localized/zh-CN/skills/cc-debug-methodology/references/method.md +13 -0
- package/localized/zh-CN/skills/cc-doc-sync-core/SKILL.md +8 -0
- package/localized/zh-CN/skills/cc-doc-sync-core/references/method.md +12 -0
- package/localized/zh-CN/skills/cc-drawio-studio/SKILL.md +73 -0
- package/localized/zh-CN/skills/cc-drawio-studio/data/THIRD_PARTY_NOTICES.md +10 -0
- package/localized/zh-CN/skills/cc-drawio-studio/data/brand-icons.json +878 -0
- package/localized/zh-CN/skills/cc-drawio-studio/data/shape-index.json +83570 -0
- package/localized/zh-CN/skills/cc-drawio-studio/references/data-residency.md +27 -0
- package/localized/zh-CN/skills/cc-drawio-studio/references/diagram-patterns.md +60 -0
- package/localized/zh-CN/skills/cc-drawio-studio/references/flowchart-quality.md +49 -0
- package/localized/zh-CN/skills/cc-drawio-studio/references/layout-and-export.md +72 -0
- package/localized/zh-CN/skills/cc-drawio-studio/references/troubleshooting.md +29 -0
- package/localized/zh-CN/skills/cc-drawio-studio/references/xml-and-mermaid.md +58 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/brand-symbols.mjs +99 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/diagram-lint.mjs +205 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/diagram-url.mjs +102 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/drawio-export.mjs +134 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/layout-graph.mjs +107 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/png-embed-fix.mjs +26 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/scan-go-packages.mjs +36 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/scan-js-modules.mjs +52 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/scan-python-classes.mjs +30 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/scan-python-modules.mjs +55 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/scan-rust-modules.mjs +35 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/scan-ts-modules.mjs +7 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/shape-query.mjs +121 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/studio-core.mjs +139 -0
- package/localized/zh-CN/skills/cc-drawio-studio/styles/built-in/corporate.json +49 -0
- package/localized/zh-CN/skills/cc-drawio-studio/styles/built-in/default.json +49 -0
- package/localized/zh-CN/skills/cc-drawio-studio/styles/built-in/handdrawn.json +49 -0
- package/localized/zh-CN/skills/cc-drawio-studio/styles/schema.json +112 -0
- package/localized/zh-CN/skills/cc-refactoring-core/SKILL.md +8 -0
- package/localized/zh-CN/skills/cc-refactoring-core/references/method.md +16 -0
- package/localized/zh-CN/skills/cc-source-driven-development/SKILL.md +8 -0
- package/localized/zh-CN/skills/cc-source-driven-development/references/method.md +19 -0
- package/localized/zh-CN/skills/cc-tdd-methodology/SKILL.md +8 -0
- package/localized/zh-CN/skills/cc-tdd-methodology/references/method.md +13 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/SKILL.md +34 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/assets/interactive-diagram.html +371 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/README.md +14 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/architecture.json +121 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/invalid-explicit.json +41 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/sequence.json +53 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/sequence.mmd +21 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/workflow.json +98 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/workflow.mmd +7 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/references/diagram-workflows.md +81 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/references/html-technical-diagrams.md +243 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/references/png-qa-autofix.md +37 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/scripts/diagram-browser.mjs +153 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/scripts/diagram-layout.mjs +176 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/scripts/export-diagram.mjs +332 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/scripts/interactive-diagram-server.mjs +654 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/scripts/mermaid-render.mjs +64 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/scripts/png-qa.mjs +462 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/scripts/tech-diagram-render.mjs +1537 -0
- package/localized/zh-CN/skills/eval_queries.json +160 -0
- package/localized/zh-CN/skills/metadata.json +223 -0
- package/localized/zh-CN/skills/relations.json +53 -0
- package/methods/debug.md +13 -0
- package/methods/doc-sync.md +12 -0
- package/methods/refactoring.md +16 -0
- package/methods/source-driven-development.md +19 -0
- package/methods/tdd.md +13 -0
- package/package.json +51 -4
- package/skills/cc-debug-methodology/SKILL.md +8 -0
- package/skills/cc-debug-methodology/references/method.md +13 -0
- package/skills/cc-doc-sync-core/SKILL.md +8 -0
- package/skills/cc-doc-sync-core/references/method.md +12 -0
- package/skills/cc-drawio-studio/SKILL.md +73 -0
- package/skills/cc-drawio-studio/data/THIRD_PARTY_NOTICES.md +10 -0
- package/skills/cc-drawio-studio/data/brand-icons.json +878 -0
- package/skills/cc-drawio-studio/data/shape-index.json +83570 -0
- package/skills/cc-drawio-studio/references/data-residency.md +27 -0
- package/skills/cc-drawio-studio/references/diagram-patterns.md +60 -0
- package/skills/cc-drawio-studio/references/flowchart-quality.md +49 -0
- package/skills/cc-drawio-studio/references/layout-and-export.md +72 -0
- package/skills/cc-drawio-studio/references/troubleshooting.md +29 -0
- package/skills/cc-drawio-studio/references/xml-and-mermaid.md +58 -0
- package/skills/cc-drawio-studio/scripts/brand-symbols.mjs +99 -0
- package/skills/cc-drawio-studio/scripts/diagram-lint.mjs +205 -0
- package/skills/cc-drawio-studio/scripts/diagram-url.mjs +102 -0
- package/skills/cc-drawio-studio/scripts/drawio-export.mjs +134 -0
- package/skills/cc-drawio-studio/scripts/layout-graph.mjs +107 -0
- package/skills/cc-drawio-studio/scripts/png-embed-fix.mjs +26 -0
- package/skills/cc-drawio-studio/scripts/scan-go-packages.mjs +36 -0
- package/skills/cc-drawio-studio/scripts/scan-js-modules.mjs +52 -0
- package/skills/cc-drawio-studio/scripts/scan-python-classes.mjs +30 -0
- package/skills/cc-drawio-studio/scripts/scan-python-modules.mjs +55 -0
- package/skills/cc-drawio-studio/scripts/scan-rust-modules.mjs +35 -0
- package/skills/cc-drawio-studio/scripts/scan-ts-modules.mjs +7 -0
- package/skills/cc-drawio-studio/scripts/shape-query.mjs +121 -0
- package/skills/cc-drawio-studio/scripts/studio-core.mjs +139 -0
- package/skills/cc-drawio-studio/styles/built-in/corporate.json +49 -0
- package/skills/cc-drawio-studio/styles/built-in/default.json +49 -0
- package/skills/cc-drawio-studio/styles/built-in/handdrawn.json +49 -0
- package/skills/cc-drawio-studio/styles/schema.json +112 -0
- package/skills/cc-refactoring-core/SKILL.md +8 -0
- package/skills/cc-refactoring-core/references/method.md +16 -0
- package/skills/cc-source-driven-development/SKILL.md +8 -0
- package/skills/cc-source-driven-development/references/method.md +19 -0
- package/skills/cc-tdd-methodology/SKILL.md +8 -0
- package/skills/cc-tdd-methodology/references/method.md +13 -0
- package/skills/cc-technical-diagrams/SKILL.md +34 -0
- package/skills/cc-technical-diagrams/assets/interactive-diagram.html +371 -0
- package/skills/cc-technical-diagrams/assets/quality-examples/README.md +14 -0
- package/skills/cc-technical-diagrams/assets/quality-examples/architecture.json +121 -0
- package/skills/cc-technical-diagrams/assets/quality-examples/invalid-explicit.json +41 -0
- package/skills/cc-technical-diagrams/assets/quality-examples/sequence.json +53 -0
- package/skills/cc-technical-diagrams/assets/quality-examples/sequence.mmd +21 -0
- package/skills/cc-technical-diagrams/assets/quality-examples/workflow.json +98 -0
- package/skills/cc-technical-diagrams/assets/quality-examples/workflow.mmd +7 -0
- package/skills/cc-technical-diagrams/references/diagram-workflows.md +81 -0
- package/skills/cc-technical-diagrams/references/html-technical-diagrams.md +243 -0
- package/skills/cc-technical-diagrams/references/png-qa-autofix.md +37 -0
- package/skills/cc-technical-diagrams/scripts/diagram-browser.mjs +153 -0
- package/skills/cc-technical-diagrams/scripts/diagram-layout.mjs +176 -0
- package/skills/cc-technical-diagrams/scripts/export-diagram.mjs +332 -0
- package/skills/cc-technical-diagrams/scripts/interactive-diagram-server.mjs +654 -0
- package/skills/cc-technical-diagrams/scripts/mermaid-render.mjs +64 -0
- package/skills/cc-technical-diagrams/scripts/png-qa.mjs +462 -0
- package/skills/cc-technical-diagrams/scripts/tech-diagram-render.mjs +1537 -0
- package/skills/eval_queries.json +160 -0
- package/skills/metadata.json +223 -0
- package/skills/relations.json +53 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
// Use installed tools only. Never invoke npx or a shell that could download packages.
|
|
9
|
+
try {
|
|
10
|
+
/** @type {Record<string,string>} */
|
|
11
|
+
const args = {};
|
|
12
|
+
for (let i = 2; i < process.argv.length; i += 2) {
|
|
13
|
+
const key = process.argv[i];
|
|
14
|
+
if (!key.startsWith("--") || !process.argv[i + 1]) throw new Error("Expected --input source.mmd --output image.svg [--cli path] [--theme default|dark|neutral|forest] [--timeout 30000] [--report file.json]");
|
|
15
|
+
if (!["input", "output", "cli", "theme", "timeout", "report", "format"].includes(key.slice(2))) throw new Error(`Unknown argument: ${key}`);
|
|
16
|
+
args[key.slice(2)] = process.argv[i + 1];
|
|
17
|
+
}
|
|
18
|
+
if (!args.input || !args.output) throw new Error("--input and --output are required.");
|
|
19
|
+
const input = path.resolve(args.input), output = path.resolve(args.output);
|
|
20
|
+
if (!fs.existsSync(input)) throw new Error(`Input does not exist: ${input}`);
|
|
21
|
+
if (!/\.(svg|png|pdf)$/i.test(output)) throw new Error("Output must end in .svg, .png or .pdf.");
|
|
22
|
+
const theme = args.theme === "light" ? "default" : args.theme ?? "default";
|
|
23
|
+
if (args.format && !["json","markdown"].includes(args.format)) throw new Error("--format must be json or markdown");
|
|
24
|
+
if (!["default", "dark", "neutral", "forest"].includes(theme)) throw new Error("Unsupported Mermaid theme.");
|
|
25
|
+
const timeout = Number(args.timeout ?? 30000);
|
|
26
|
+
if (!Number.isFinite(timeout) || timeout < 100 || timeout > 300000) throw new Error("--timeout must be between 100 and 300000 milliseconds.");
|
|
27
|
+
/** @type {string[]} */
|
|
28
|
+
const candidates = args.cli ? [path.resolve(args.cli)] : (process.env.PATH ?? "").split(path.delimiter).flatMap((dir) => [path.join(dir, "mmdc"), path.join(dir, "mmdc.cmd")]);
|
|
29
|
+
let cli = candidates.find((candidate) => fs.existsSync(candidate));
|
|
30
|
+
if (cli?.endsWith(".cmd")) {
|
|
31
|
+
// npm Windows shims are shell scripts. Execute their installed JS entry directly.
|
|
32
|
+
const entry = path.join(path.dirname(cli), "node_modules", "@mermaid-js", "mermaid-cli", "src", "cli.js");
|
|
33
|
+
cli = fs.existsSync(entry) ? entry : undefined;
|
|
34
|
+
}
|
|
35
|
+
if (!cli) throw new Error("Mermaid CLI unavailable. Use the JSON/HTML renderer or provide an installed mmdc JS entry with --cli. No packages were installed.");
|
|
36
|
+
const js = /\.[cm]?js$/i.test(cli);
|
|
37
|
+
const command = js ? process.execPath : cli;
|
|
38
|
+
const prefix = js ? [cli] : [];
|
|
39
|
+
const version = spawnSync(command, [...prefix, "--version"], { encoding: "utf8", timeout, windowsHide: true });
|
|
40
|
+
if (version.error || version.status !== 0) throw new Error(`Mermaid CLI unavailable: ${version.error?.message ?? version.stderr}`);
|
|
41
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fec-mermaid-"));
|
|
42
|
+
try {
|
|
43
|
+
const artifact = path.join(dir, `diagram${path.extname(output)}`);
|
|
44
|
+
const result = spawnSync(command, [...prefix, "-i", input, "-o", artifact, "-t", theme], { encoding: "utf8", timeout, windowsHide: true });
|
|
45
|
+
if (result.error || result.status !== 0) throw new Error(`Mermaid render failed: ${result.error?.message ?? result.stderr}`);
|
|
46
|
+
if (!fs.existsSync(artifact) || fs.statSync(artifact).size === 0) throw new Error("Mermaid render failed: no fresh artifact produced.");
|
|
47
|
+
const bytes = fs.readFileSync(artifact);
|
|
48
|
+
if (output.endsWith(".svg") && !bytes.toString("utf8").includes("<svg")) throw new Error("Mermaid render failed: invalid SVG.");
|
|
49
|
+
if (output.endsWith(".png") && bytes.subarray(0, 8).toString("hex") !== "89504e470d0a1a0a") throw new Error("Mermaid render failed: invalid PNG.");
|
|
50
|
+
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
51
|
+
fs.copyFileSync(artifact, output);
|
|
52
|
+
if (args.report) {
|
|
53
|
+
fs.mkdirSync(path.dirname(path.resolve(args.report)), { recursive: true });
|
|
54
|
+
fs.writeFileSync(args.report, JSON.stringify({ ok:true,engine:"mermaid",format:path.extname(output).slice(1),verified:false,route: "mermaid", version: version.stdout.trim(), input, output, theme, status: "rendered", visualQA: "pending" }, null, 2) + "\n");
|
|
55
|
+
}
|
|
56
|
+
const report = {ok:true,engine:"mermaid",version:version.stdout.trim(),input,output,format:path.extname(output).slice(1),verified:false,reason:"Final image visual QA is pending."};
|
|
57
|
+
console.log(args.format === "json" ? JSON.stringify(report) : `Rendered Mermaid diagram: ${output}\nVisual QA: pending`);
|
|
58
|
+
} finally {
|
|
59
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
}
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import zlib from "node:zlib";
|
|
6
|
+
import { segmentHits } from "./diagram-layout.mjs";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
8
|
+
|
|
9
|
+
if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) {
|
|
10
|
+
const args = parseArgs(process.argv.slice(2));
|
|
11
|
+
|
|
12
|
+
if (!args.png || args.help) {
|
|
13
|
+
printHelp();
|
|
14
|
+
process.exit(args.help ? 0 : 1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const image = readPng(args.png);
|
|
19
|
+
const manifest = args.manifest ? readManifest(args.manifest) : undefined;
|
|
20
|
+
const report = analyze(image, manifest, Boolean(args.strict));
|
|
21
|
+
if ((args.format ?? "markdown") === "json") {
|
|
22
|
+
console.log(JSON.stringify(report, null, 2));
|
|
23
|
+
} else {
|
|
24
|
+
console.log(renderMarkdown(report));
|
|
25
|
+
}
|
|
26
|
+
process.exit(report.ok ? 0 : 2);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseArgs(values) {
|
|
35
|
+
const parsed = { format: "markdown", strict: false, help: false };
|
|
36
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
37
|
+
const value = values[index];
|
|
38
|
+
if (value === "--help" || value === "-h") parsed.help = true;
|
|
39
|
+
else if (value === "--strict") parsed.strict = true;
|
|
40
|
+
else if (value === "--png") parsed.png = values[++index];
|
|
41
|
+
else if (value === "--manifest") parsed.manifest = values[++index];
|
|
42
|
+
else if (value === "--format") parsed.format = values[++index];
|
|
43
|
+
else throw new Error(`Unknown argument: ${value}`);
|
|
44
|
+
}
|
|
45
|
+
if (parsed.format !== "json" && parsed.format !== "markdown") {
|
|
46
|
+
throw new Error("--format must be json or markdown");
|
|
47
|
+
}
|
|
48
|
+
return parsed;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function printHelp() {
|
|
52
|
+
console.log(`Usage: node skills/cc-technical-diagrams/scripts/png-qa.mjs --png <file> [--manifest <layout.json>] [--format json|markdown] [--strict]`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readManifest(filePath) {
|
|
56
|
+
const manifest = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
57
|
+
return {
|
|
58
|
+
...manifest,
|
|
59
|
+
canvas: manifest.canvas,
|
|
60
|
+
boxes: Array.isArray(manifest.boxes) ? manifest.boxes : [],
|
|
61
|
+
connectors: Array.isArray(manifest.connectors) ? manifest.connectors : [],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readPng(filePath) {
|
|
66
|
+
const buffer = fs.readFileSync(filePath);
|
|
67
|
+
const signature = "89504e470d0a1a0a";
|
|
68
|
+
if (buffer.subarray(0, 8).toString("hex") !== signature) {
|
|
69
|
+
throw new Error(`Not a PNG file: ${filePath}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let offset = 8;
|
|
73
|
+
let ihdr;
|
|
74
|
+
const idat = [];
|
|
75
|
+
while (offset < buffer.length) {
|
|
76
|
+
const length = buffer.readUInt32BE(offset);
|
|
77
|
+
const type = buffer.subarray(offset + 4, offset + 8).toString("ascii");
|
|
78
|
+
const data = buffer.subarray(offset + 8, offset + 8 + length);
|
|
79
|
+
offset += 12 + length;
|
|
80
|
+
if (type === "IHDR") {
|
|
81
|
+
ihdr = {
|
|
82
|
+
width: data.readUInt32BE(0),
|
|
83
|
+
height: data.readUInt32BE(4),
|
|
84
|
+
bitDepth: data[8],
|
|
85
|
+
colorType: data[9],
|
|
86
|
+
compression: data[10],
|
|
87
|
+
filter: data[11],
|
|
88
|
+
interlace: data[12],
|
|
89
|
+
};
|
|
90
|
+
} else if (type === "IDAT") {
|
|
91
|
+
idat.push(data);
|
|
92
|
+
} else if (type === "IEND") {
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!ihdr) throw new Error("PNG is missing IHDR");
|
|
98
|
+
if (ihdr.bitDepth !== 8) throw new Error(`Unsupported PNG bit depth: ${ihdr.bitDepth}`);
|
|
99
|
+
if (ihdr.compression !== 0 || ihdr.filter !== 0 || ihdr.interlace !== 0) {
|
|
100
|
+
throw new Error("Unsupported PNG compression/filter/interlace settings");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const channels = channelsForColorType(ihdr.colorType);
|
|
104
|
+
const inflated = zlib.inflateSync(Buffer.concat(idat));
|
|
105
|
+
const pixels = unfilter(inflated, ihdr.width, ihdr.height, channels);
|
|
106
|
+
return {
|
|
107
|
+
path: path.resolve(filePath),
|
|
108
|
+
width: ihdr.width,
|
|
109
|
+
height: ihdr.height,
|
|
110
|
+
colorType: ihdr.colorType,
|
|
111
|
+
channels,
|
|
112
|
+
pixels,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function channelsForColorType(colorType) {
|
|
117
|
+
if (colorType === 0) return 1;
|
|
118
|
+
if (colorType === 2) return 3;
|
|
119
|
+
if (colorType === 4) return 2;
|
|
120
|
+
if (colorType === 6) return 4;
|
|
121
|
+
throw new Error(`Unsupported PNG color type: ${colorType}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function unfilter(data, width, height, channels) {
|
|
125
|
+
const stride = width * channels;
|
|
126
|
+
const output = Buffer.alloc(stride * height);
|
|
127
|
+
let sourceOffset = 0;
|
|
128
|
+
|
|
129
|
+
for (let y = 0; y < height; y += 1) {
|
|
130
|
+
const filterType = data[sourceOffset];
|
|
131
|
+
sourceOffset += 1;
|
|
132
|
+
const rowOffset = y * stride;
|
|
133
|
+
const previousRowOffset = (y - 1) * stride;
|
|
134
|
+
|
|
135
|
+
for (let x = 0; x < stride; x += 1) {
|
|
136
|
+
const raw = data[sourceOffset + x];
|
|
137
|
+
const left = x >= channels ? output[rowOffset + x - channels] : 0;
|
|
138
|
+
const up = y > 0 ? output[previousRowOffset + x] : 0;
|
|
139
|
+
const upLeft = y > 0 && x >= channels ? output[previousRowOffset + x - channels] : 0;
|
|
140
|
+
let value;
|
|
141
|
+
if (filterType === 0) value = raw;
|
|
142
|
+
else if (filterType === 1) value = raw + left;
|
|
143
|
+
else if (filterType === 2) value = raw + up;
|
|
144
|
+
else if (filterType === 3) value = raw + Math.floor((left + up) / 2);
|
|
145
|
+
else if (filterType === 4) value = raw + paeth(left, up, upLeft);
|
|
146
|
+
else throw new Error(`Unsupported PNG filter type: ${filterType}`);
|
|
147
|
+
output[rowOffset + x] = value & 0xff;
|
|
148
|
+
}
|
|
149
|
+
sourceOffset += stride;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return output;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function paeth(left, up, upLeft) {
|
|
156
|
+
const estimate = left + up - upLeft;
|
|
157
|
+
const leftDistance = Math.abs(estimate - left);
|
|
158
|
+
const upDistance = Math.abs(estimate - up);
|
|
159
|
+
const upLeftDistance = Math.abs(estimate - upLeft);
|
|
160
|
+
if (leftDistance <= upDistance && leftDistance <= upLeftDistance) return left;
|
|
161
|
+
if (upDistance <= upLeftDistance) return up;
|
|
162
|
+
return upLeft;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function analyze(image, manifest, strict) {
|
|
166
|
+
const issues = [];
|
|
167
|
+
const nextActions = new Set();
|
|
168
|
+
const ink = measureInk(image, strict ? 10 : 18);
|
|
169
|
+
|
|
170
|
+
if (image.width < 64 || image.height < 64) {
|
|
171
|
+
addIssue(issues, nextActions, "image-too-small", "PNG is too small for reliable visual QA.", "Export a larger PNG before review.");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (ink.coverage < (strict ? 0.001 : 0.0002)) {
|
|
175
|
+
addIssue(issues, nextActions, "blank-image", "PNG appears blank or nearly blank.", "Check the renderer/export target and regenerate the image.");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (ink.bounds) {
|
|
179
|
+
const pad = strict ? 6 : 3;
|
|
180
|
+
if (ink.bounds.minX <= pad || ink.bounds.minY <= pad || image.width - ink.bounds.maxX <= pad || image.height - ink.bounds.maxY <= pad) {
|
|
181
|
+
addIssue(issues, nextActions, "edge-clipping", "Visible content is very close to the PNG edge.", "Increase canvas/viewBox padding or reduce subject scale.");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (manifest) analyzeManifest(image, manifest, issues, nextActions, strict);
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
ok: issues.length === 0,
|
|
189
|
+
image: {
|
|
190
|
+
path: image.path,
|
|
191
|
+
width: image.width,
|
|
192
|
+
height: image.height,
|
|
193
|
+
colorType: image.colorType,
|
|
194
|
+
inkCoverage: Number(ink.coverage.toFixed(6)),
|
|
195
|
+
inkBounds: ink.bounds,
|
|
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" },
|
|
199
|
+
issues,
|
|
200
|
+
nextActions: [...nextActions],
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function measureInk(image, threshold) {
|
|
205
|
+
const first = samplePixel(image, 0, 0);
|
|
206
|
+
let inkPixels = 0;
|
|
207
|
+
let minX = image.width;
|
|
208
|
+
let minY = image.height;
|
|
209
|
+
let maxX = -1;
|
|
210
|
+
let maxY = -1;
|
|
211
|
+
|
|
212
|
+
for (let y = 0; y < image.height; y += 1) {
|
|
213
|
+
for (let x = 0; x < image.width; x += 1) {
|
|
214
|
+
const pixel = samplePixel(image, x, y);
|
|
215
|
+
const differs = colorDistance(pixel, first) > threshold && pixel.a > 8;
|
|
216
|
+
if (differs) {
|
|
217
|
+
inkPixels += 1;
|
|
218
|
+
minX = Math.min(minX, x);
|
|
219
|
+
minY = Math.min(minY, y);
|
|
220
|
+
maxX = Math.max(maxX, x);
|
|
221
|
+
maxY = Math.max(maxY, y);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
coverage: inkPixels / (image.width * image.height),
|
|
228
|
+
bounds: inkPixels > 0 ? { minX, minY, maxX, maxY } : null,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function samplePixel(image, x, y) {
|
|
233
|
+
const offset = (y * image.width + x) * image.channels;
|
|
234
|
+
if (image.channels === 1) {
|
|
235
|
+
const value = image.pixels[offset];
|
|
236
|
+
return { r: value, g: value, b: value, a: 255 };
|
|
237
|
+
}
|
|
238
|
+
if (image.channels === 2) {
|
|
239
|
+
const value = image.pixels[offset];
|
|
240
|
+
return { r: value, g: value, b: value, a: image.pixels[offset + 1] };
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
r: image.pixels[offset],
|
|
244
|
+
g: image.pixels[offset + 1],
|
|
245
|
+
b: image.pixels[offset + 2],
|
|
246
|
+
a: image.channels === 4 ? image.pixels[offset + 3] : 255,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function colorDistance(left, right) {
|
|
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);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function analyzeManifest(image, manifest, issues, nextActions, strict) {
|
|
255
|
+
if (manifest.canvas && (manifest.canvas.width !== image.width || manifest.canvas.height !== image.height)) {
|
|
256
|
+
addIssue(
|
|
257
|
+
issues,
|
|
258
|
+
nextActions,
|
|
259
|
+
"manifest-canvas-mismatch",
|
|
260
|
+
`Manifest canvas ${manifest.canvas.width}x${manifest.canvas.height} does not match PNG ${image.width}x${image.height}.`,
|
|
261
|
+
"Regenerate the manifest from the final export dimensions.",
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
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.");
|
|
280
|
+
for (const box of boxes) {
|
|
281
|
+
if (box.x < 0 || box.y < 0 || box.x + box.width > image.width || box.y + box.height > image.height) {
|
|
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.");
|
|
283
|
+
}
|
|
284
|
+
if (!labels.length && box.label && estimateLabelOverflow(box, strict)) {
|
|
285
|
+
addIssue(issues, nextActions, "label-overflow", `Box "${box.id}" label is likely to overflow.`, "Wrap or shorten labels, widen boxes, or increase export scale.");
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
for (let i = 0; i < boxes.length; i += 1) {
|
|
290
|
+
for (let j = i + 1; j < boxes.length; j += 1) {
|
|
291
|
+
const overlap = overlapArea(boxes[i], boxes[j]);
|
|
292
|
+
if (overlap > 0) {
|
|
293
|
+
addIssue(issues, nextActions, "box-overlap", `Boxes "${boxes[i].id}" and "${boxes[j].id}" overlap by ${Math.round(overlap)}px^2.`, "Increase node spacing or split dense groups.");
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const boxById = new Map(boxes.map((box) => [box.id, box]));
|
|
299
|
+
const segments = [];
|
|
300
|
+
for (const connector of manifest.connectors) {
|
|
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.");
|
|
304
|
+
for (let index = 0; index < points.length - 1; index += 1) {
|
|
305
|
+
const segment = { connector, a: points[index], b: points[index + 1] };
|
|
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.");
|
|
308
|
+
for (const box of boxes) {
|
|
309
|
+
if (box.id === connector.from || box.id === connector.to) continue;
|
|
310
|
+
if (segmentIntersectsBox(segment, box)) {
|
|
311
|
+
addIssue(issues, nextActions, "connector-through-label", `Connector "${connector.id ?? "unnamed"}" crosses box "${box.id}".`, "Add waypoints around labels or use orthogonal routing.");
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if ((connector.from && !boxById.has(connector.from)) || (connector.to && !boxById.has(connector.to))) {
|
|
316
|
+
addIssue(issues, nextActions, "connector-endpoint-missing", `Connector "${connector.id ?? "unnamed"}" references a missing endpoint.`, "Update connector endpoints to match manifest box ids.");
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
for (let i = 0; i < segments.length; i += 1) {
|
|
321
|
+
for (let j = i + 1; j < segments.length; j += 1) {
|
|
322
|
+
if (segments[i].connector === segments[j].connector) continue;
|
|
323
|
+
if (segmentsStack(segments[i], segments[j], strict ? 4 : 2)) {
|
|
324
|
+
addIssue(issues, nextActions, "connector-stacking", "Two connector segments are stacked or nearly stacked.", "Offset parallel connectors or bundle them into a labeled shared route.");
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function normalizeBox(box) {
|
|
331
|
+
if (!box || typeof box.id !== "string") return null;
|
|
332
|
+
const normalized = {
|
|
333
|
+
id: box.id,
|
|
334
|
+
kind: box.kind,
|
|
335
|
+
x: Number(box.x),
|
|
336
|
+
y: Number(box.y),
|
|
337
|
+
width: Number(box.width),
|
|
338
|
+
height: Number(box.height),
|
|
339
|
+
label: typeof box.label === "string" ? box.label : "",
|
|
340
|
+
};
|
|
341
|
+
if ([normalized.x, normalized.y, normalized.width, normalized.height].some((value) => !Number.isFinite(value))) return null;
|
|
342
|
+
return normalized;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function normalizePoints(points) {
|
|
346
|
+
if (!Array.isArray(points)) return [];
|
|
347
|
+
return points
|
|
348
|
+
.map((point) => {
|
|
349
|
+
if (Array.isArray(point)) return { x: Number(point[0]), y: Number(point[1]) };
|
|
350
|
+
return { x: Number(point?.x), y: Number(point?.y) };
|
|
351
|
+
})
|
|
352
|
+
.filter((point) => Number.isFinite(point.x) && Number.isFinite(point.y));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function estimateLabelOverflow(box, strict) {
|
|
356
|
+
const label = box.label.trim();
|
|
357
|
+
if (!label) return false;
|
|
358
|
+
const lines = label.split(/\r?\n/);
|
|
359
|
+
const maxLine = Math.max(...lines.map((line) => visualLength(line)));
|
|
360
|
+
const estimatedWidth = maxLine * (strict ? 7.5 : 7);
|
|
361
|
+
const estimatedHeight = lines.length * (strict ? 18 : 16);
|
|
362
|
+
return estimatedWidth > Math.max(0, box.width - 12) || estimatedHeight > Math.max(0, box.height - 8);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function visualLength(value) {
|
|
366
|
+
return [...value].reduce((total, char) => total + (/[\u4e00-\u9fff]/.test(char) ? 2 : 1), 0);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function overlapArea(left, right) {
|
|
370
|
+
const width = Math.max(0, Math.min(left.x + left.width, right.x + right.width) - Math.max(left.x, right.x));
|
|
371
|
+
const height = Math.max(0, Math.min(left.y + left.height, right.y + right.height) - Math.max(left.y, right.y));
|
|
372
|
+
return width * height;
|
|
373
|
+
}
|
|
374
|
+
|
|
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) {
|
|
380
|
+
if (pointInsideBox(segment.a, box) || pointInsideBox(segment.b, box)) return true;
|
|
381
|
+
const edges = [
|
|
382
|
+
[{ x: box.x, y: box.y }, { x: box.x + box.width, y: box.y }],
|
|
383
|
+
[{ x: box.x + box.width, y: box.y }, { x: box.x + box.width, y: box.y + box.height }],
|
|
384
|
+
[{ x: box.x + box.width, y: box.y + box.height }, { x: box.x, y: box.y + box.height }],
|
|
385
|
+
[{ x: box.x, y: box.y + box.height }, { x: box.x, y: box.y }],
|
|
386
|
+
];
|
|
387
|
+
return edges.some(([a, b]) => segmentsIntersect(segment.a, segment.b, a, b));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function pointInsideBox(point, box) {
|
|
391
|
+
return point.x >= box.x && point.x <= box.x + box.width && point.y >= box.y && point.y <= box.y + box.height;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function segmentsIntersect(a, b, c, d) {
|
|
395
|
+
const o1 = orientation(a, b, c);
|
|
396
|
+
const o2 = orientation(a, b, d);
|
|
397
|
+
const o3 = orientation(c, d, a);
|
|
398
|
+
const o4 = orientation(c, d, b);
|
|
399
|
+
if (o1 !== o2 && o3 !== o4) return true;
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function orientation(a, b, c) {
|
|
404
|
+
const value = (b.y - a.y) * (c.x - b.x) - (b.x - a.x) * (c.y - b.y);
|
|
405
|
+
if (Math.abs(value) < 0.0001) return 0;
|
|
406
|
+
return value > 0 ? 1 : 2;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function segmentsStack(left, right, tolerance) {
|
|
410
|
+
const leftHorizontal = Math.abs(left.a.y - left.b.y) <= tolerance;
|
|
411
|
+
const rightHorizontal = Math.abs(right.a.y - right.b.y) <= tolerance;
|
|
412
|
+
const leftVertical = Math.abs(left.a.x - left.b.x) <= tolerance;
|
|
413
|
+
const rightVertical = Math.abs(right.a.x - right.b.x) <= tolerance;
|
|
414
|
+
|
|
415
|
+
if (leftHorizontal && rightHorizontal && Math.abs(left.a.y - right.a.y) <= tolerance) {
|
|
416
|
+
return rangesOverlap([left.a.x, left.b.x], [right.a.x, right.b.x]) > tolerance;
|
|
417
|
+
}
|
|
418
|
+
if (leftVertical && rightVertical && Math.abs(left.a.x - right.a.x) <= tolerance) {
|
|
419
|
+
return rangesOverlap([left.a.y, left.b.y], [right.a.y, right.b.y]) > tolerance;
|
|
420
|
+
}
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function rangesOverlap(left, right) {
|
|
425
|
+
const a = [Math.min(...left), Math.max(...left)];
|
|
426
|
+
const b = [Math.min(...right), Math.max(...right)];
|
|
427
|
+
return Math.max(0, Math.min(a[1], b[1]) - Math.max(a[0], b[0]));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function addIssue(issues, nextActions, code, message, action) {
|
|
431
|
+
issues.push({ code, message, action });
|
|
432
|
+
if (typeof nextActions.add === "function") nextActions.add(action);
|
|
433
|
+
else if (!nextActions.includes(action)) nextActions.push(action);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function renderMarkdown(report) {
|
|
437
|
+
const lines = [
|
|
438
|
+
"# PNG QA Report",
|
|
439
|
+
"",
|
|
440
|
+
`- Status: ${report.ok ? "ok" : "issues-found"}`,
|
|
441
|
+
`- Image: ${report.image.width}x${report.image.height}`,
|
|
442
|
+
`- Ink coverage: ${report.image.inkCoverage}`,
|
|
443
|
+
];
|
|
444
|
+
if (report.image.inkBounds) {
|
|
445
|
+
lines.push(`- Ink bounds: ${JSON.stringify(report.image.inkBounds)}`);
|
|
446
|
+
}
|
|
447
|
+
lines.push("", "## Issues");
|
|
448
|
+
if (report.issues.length === 0) {
|
|
449
|
+
lines.push("- None");
|
|
450
|
+
} else {
|
|
451
|
+
for (const issue of report.issues) {
|
|
452
|
+
lines.push(`- ${issue.code}: ${issue.message}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
lines.push("", "## Next Actions");
|
|
456
|
+
if (report.nextActions.length === 0) {
|
|
457
|
+
lines.push("- None");
|
|
458
|
+
} else {
|
|
459
|
+
for (const action of report.nextActions) lines.push(`- ${action}`);
|
|
460
|
+
}
|
|
461
|
+
return `${lines.join("\n")}\n`;
|
|
462
|
+
}
|