@orchestrator-claude/definitions 3.38.0 → 3.40.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/agents/agentic-architect.md +1 -1
- package/agents/architecture-mediator.md +2 -1
- package/agents/backend-architect.md +1 -1
- package/agents/contract-architect.md +1 -1
- package/agents/datamodel-architect.md +1 -1
- package/agents/frontend-architect.md +1 -1
- package/agents/harness-architect.md +1 -1
- package/agents/infra-architect.md +1 -1
- package/manifest.json +20 -12
- package/package.json +1 -1
- package/skills/archify/LICENSE +22 -0
- package/skills/archify/SKILL.md +278 -0
- package/skills/archify/VENDOR.txt +70 -0
- package/skills/archify/assets/template.html +1261 -0
- package/skills/archify/bin/archify.mjs +271 -0
- package/skills/archify/examples/agent-run.lifecycle.json +64 -0
- package/skills/archify/examples/agent-tool-call.workflow.json +88 -0
- package/skills/archify/examples/cache-miss-request.sequence.json +75 -0
- package/skills/archify/examples/product-analytics.dataflow.json +70 -0
- package/skills/archify/examples/web-app.architecture.json +41 -0
- package/skills/archify/package-lock.json +80 -0
- package/skills/archify/package.json +23 -0
- package/skills/archify/renderers/architecture/grid.mjs +62 -0
- package/skills/archify/renderers/architecture/render-architecture.mjs +371 -0
- package/skills/archify/renderers/dataflow/README.md +82 -0
- package/skills/archify/renderers/dataflow/render-dataflow.mjs +295 -0
- package/skills/archify/renderers/lifecycle/README.md +91 -0
- package/skills/archify/renderers/lifecycle/render-lifecycle.mjs +384 -0
- package/skills/archify/renderers/sequence/README.md +84 -0
- package/skills/archify/renderers/sequence/render-sequence.mjs +257 -0
- package/skills/archify/renderers/shared/cli.mjs +44 -0
- package/skills/archify/renderers/shared/generated-validators.mjs +13 -0
- package/skills/archify/renderers/shared/geometry.mjs +230 -0
- package/skills/archify/renderers/shared/layout-report.mjs +40 -0
- package/skills/archify/renderers/shared/utils.mjs +88 -0
- package/skills/archify/renderers/shared/validator.mjs +40 -0
- package/skills/archify/renderers/workflow/README.md +103 -0
- package/skills/archify/renderers/workflow/render-workflow.mjs +481 -0
- package/skills/archify/schemas/README.md +72 -0
- package/skills/archify/schemas/architecture.schema.json +121 -0
- package/skills/archify/schemas/common.schema.json +43 -0
- package/skills/archify/schemas/dataflow.schema.json +221 -0
- package/skills/archify/schemas/lifecycle.schema.json +241 -0
- package/skills/archify/schemas/sequence.schema.json +186 -0
- package/skills/archify/schemas/workflow.schema.json +332 -0
- package/skills/archify/scripts/check-render-output.mjs +297 -0
- package/skills/archify/scripts/generate-validators.mjs +66 -0
- package/skills/archify/scripts/render-examples.mjs +26 -0
- package/skills/archify/test/animation.test.mjs +53 -0
- package/skills/archify/test/cli.test.mjs +206 -0
- package/skills/archify/test/degraded.test.mjs +157 -0
- package/skills/archify/test/generate-validators.test.mjs +40 -0
- package/skills/archify/test/geometry.test.mjs +186 -0
- package/skills/archify/test/golden.mjs +167 -0
- package/skills/archify/test/grid.test.mjs +47 -0
- package/skills/archify/test/layout-rules.test.mjs +193 -0
- package/skills/archify/test/render-output-checks.test.mjs +75 -0
- package/skills/arquitetura-mediada-arquiteto/SKILL.md +20 -3
- package/skills/kb-api-pattern/SKILL.md +1 -1
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Golden-file harness for the archify renderers. No test framework needed:
|
|
2
|
+
// renderers are deterministic, so fresh renders must match the checked-in
|
|
3
|
+
// example HTML aside from platform checkout line endings. Also covers schema enforcement (negative cases),
|
|
4
|
+
// template freshness of the architecture-mode example, and version sync.
|
|
5
|
+
//
|
|
6
|
+
// Run from the skill folder: npm test
|
|
7
|
+
|
|
8
|
+
import { execFileSync } from 'node:child_process';
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
|
|
14
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const skillRoot = path.resolve(__dirname, '..');
|
|
16
|
+
const repoRoot = path.resolve(skillRoot, '..');
|
|
17
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-test-'));
|
|
18
|
+
|
|
19
|
+
let failures = 0;
|
|
20
|
+
|
|
21
|
+
function check(name, ok, detail) {
|
|
22
|
+
if (ok) {
|
|
23
|
+
console.log(` ok ${name}`);
|
|
24
|
+
} else {
|
|
25
|
+
failures += 1;
|
|
26
|
+
console.error(` FAIL ${name}${detail ? ` — ${detail}` : ''}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function render(mode, inputPath, outPath) {
|
|
31
|
+
execFileSync('node', [
|
|
32
|
+
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
|
|
33
|
+
inputPath,
|
|
34
|
+
outPath,
|
|
35
|
+
], { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeNewlines(text) {
|
|
39
|
+
return text.replace(/\r\n?/g, '\n');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
console.log('golden renders (renderer output must match checked-in examples)');
|
|
44
|
+
|
|
45
|
+
const GOLDEN = [
|
|
46
|
+
['workflow', 'agent-tool-call.workflow.json', 'workflow-agent-tool-call-rendered.html'],
|
|
47
|
+
['sequence', 'cache-miss-request.sequence.json', 'sequence-cache-miss-request.html'],
|
|
48
|
+
['dataflow', 'product-analytics.dataflow.json', 'dataflow-product-analytics.html'],
|
|
49
|
+
['lifecycle', 'agent-run.lifecycle.json', 'lifecycle-agent-run.html'],
|
|
50
|
+
['architecture', 'web-app.architecture.json', 'web-app-rendered.html'],
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
for (const [mode, input, golden] of GOLDEN) {
|
|
54
|
+
const out = path.join(tmp, golden);
|
|
55
|
+
try {
|
|
56
|
+
render(mode, path.join(skillRoot, 'examples', input), out);
|
|
57
|
+
const fresh = fs.readFileSync(out, 'utf8');
|
|
58
|
+
const checked = fs.readFileSync(path.join(repoRoot, 'examples', golden), 'utf8');
|
|
59
|
+
check(`${mode}: ${golden}`, normalizeNewlines(fresh) === normalizeNewlines(checked),
|
|
60
|
+
`fresh render differs from examples/${golden}; if the change is intentional, re-render the examples and commit them`);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
check(`${mode}: ${golden}`, false, String(err.stderr || err.message).slice(0, 300));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
console.log('schema enforcement (invalid JSON must fail with a path-prefixed message)');
|
|
68
|
+
|
|
69
|
+
function expectFailure(name, mode, mutate, expectInMessage) {
|
|
70
|
+
const base = JSON.parse(fs.readFileSync(
|
|
71
|
+
path.join(skillRoot, 'examples', GOLDEN.find(([m]) => m === mode)[1]), 'utf8'));
|
|
72
|
+
mutate(base);
|
|
73
|
+
const input = path.join(tmp, `neg-${name.replace(/[^a-z0-9]+/gi, '-')}.json`);
|
|
74
|
+
fs.writeFileSync(input, JSON.stringify(base));
|
|
75
|
+
try {
|
|
76
|
+
render(mode, input, path.join(tmp, 'neg-out.html'));
|
|
77
|
+
check(name, false, 'renderer exited 0 on invalid input');
|
|
78
|
+
} catch (err) {
|
|
79
|
+
const message = String(err.stderr || err.message);
|
|
80
|
+
check(name, message.includes(expectInMessage),
|
|
81
|
+
`expected "${expectInMessage}" in:\n${message.slice(0, 300)}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
expectFailure('card dot outside enum', 'workflow',
|
|
86
|
+
(d) => { d.cards[0].dot = 'pink'; }, '/cards/0/dot');
|
|
87
|
+
expectFailure('node id starting with a digit', 'workflow',
|
|
88
|
+
(d) => { d.nodes[0].id = '1user'; }, 'pattern');
|
|
89
|
+
expectFailure('extra property rejected', 'workflow',
|
|
90
|
+
(d) => { d.nodes[0].colour = 'red'; }, 'additional properties');
|
|
91
|
+
expectFailure('column beyond layout maximum', 'workflow',
|
|
92
|
+
(d) => { d.nodes[0].col = 7; }, '<= 5');
|
|
93
|
+
expectFailure('missing schema_version', 'sequence',
|
|
94
|
+
(d) => { delete d.schema_version; }, 'schema_version');
|
|
95
|
+
expectFailure('cross-lane state overlap', 'lifecycle',
|
|
96
|
+
(d) => {
|
|
97
|
+
const approval = d.states.find((s) => s.id === 'approval');
|
|
98
|
+
const failed = d.states.find((s) => s.id === 'failed');
|
|
99
|
+
delete failed.yOffset;
|
|
100
|
+
failed.col = approval.col;
|
|
101
|
+
}, 'less than 10px apart');
|
|
102
|
+
expectFailure('zero component width rejected by schema', 'architecture',
|
|
103
|
+
(d) => { d.components[0].size = [0, 60]; }, '/components/0/size/0');
|
|
104
|
+
expectFailure('zero component height rejected by schema', 'architecture',
|
|
105
|
+
(d) => { d.components[0].size = [120, 0]; }, '/components/0/size/1');
|
|
106
|
+
expectFailure('negative component width rejected by schema', 'architecture',
|
|
107
|
+
(d) => { d.components[0].size = [-1, 60]; }, '/components/0/size/0');
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
console.log('template freshness (architecture example must carry the current template)');
|
|
111
|
+
|
|
112
|
+
function blocks(html, tag) {
|
|
113
|
+
const re = new RegExp(`<${tag}[^>]*>[\\s\\S]*?<\\/${tag}>`, 'g');
|
|
114
|
+
return html.match(re) || [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const template = fs.readFileSync(path.join(skillRoot, 'assets/template.html'), 'utf8');
|
|
118
|
+
const webApp = fs.readFileSync(path.join(repoRoot, 'examples/web-app.html'), 'utf8');
|
|
119
|
+
// <style> and <script> blocks pass through applyTemplate untouched, so the
|
|
120
|
+
// architecture-mode example must contain them verbatim or it has drifted.
|
|
121
|
+
for (const tag of ['style', 'script']) {
|
|
122
|
+
const t = blocks(template, tag).filter((b) => !b.includes('[PROJECT NAME]'));
|
|
123
|
+
const w = blocks(webApp, tag).filter((b) => !b.includes('Sample Web App'));
|
|
124
|
+
check(`web-app.html ${tag} blocks match template`,
|
|
125
|
+
JSON.stringify(t) === JSON.stringify(w),
|
|
126
|
+
'examples/web-app.html was generated from a stale template — re-derive it');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
console.log('version sync');
|
|
131
|
+
|
|
132
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(skillRoot, 'package.json'), 'utf8'));
|
|
133
|
+
check('template generator meta matches package.json version',
|
|
134
|
+
template.includes(`<meta name="generator" content="archify ${pkg.version}">`),
|
|
135
|
+
`package.json says ${pkg.version}`);
|
|
136
|
+
|
|
137
|
+
const lock = JSON.parse(fs.readFileSync(path.join(skillRoot, 'package-lock.json'), 'utf8'));
|
|
138
|
+
check('package-lock.json version matches package.json',
|
|
139
|
+
lock.version === pkg.version && lock.packages?.['']?.version === pkg.version,
|
|
140
|
+
`lockfile says ${lock.version} — run npm install and rebuild the zip`);
|
|
141
|
+
|
|
142
|
+
const skillMd = fs.readFileSync(path.join(skillRoot, 'SKILL.md'), 'utf8');
|
|
143
|
+
const skillVersion = (skillMd.match(/^\s*version:\s*"([^"]+)"/m) || [])[1];
|
|
144
|
+
check('SKILL.md metadata version matches package.json major.minor',
|
|
145
|
+
!!skillVersion && pkg.version.startsWith(skillVersion),
|
|
146
|
+
`SKILL.md says ${skillVersion}, package.json says ${pkg.version}`);
|
|
147
|
+
|
|
148
|
+
for (const readmeName of ['README.md', 'README_EN.md', 'README_ZH.md']) {
|
|
149
|
+
const readme = fs.readFileSync(path.join(repoRoot, readmeName), 'utf8');
|
|
150
|
+
check(`${readmeName} badge matches package.json version`,
|
|
151
|
+
readme.includes(`/badge/version-${pkg.version}-`),
|
|
152
|
+
`${readmeName} does not advertise ${pkg.version}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const landingPage = fs.readFileSync(path.join(repoRoot, 'docs/index.html'), 'utf8');
|
|
156
|
+
const landingVersions = [...landingPage.matchAll(/\bv\d+\.\d+\.\d+\b/g)].map((m) => m[0]);
|
|
157
|
+
check('GitHub Pages version labels match package.json',
|
|
158
|
+
landingVersions.length > 0 && landingVersions.every((v) => v === `v${pkg.version}`),
|
|
159
|
+
`landing page says ${[...new Set(landingVersions)].join(', ') || '(no version)'}`);
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
163
|
+
if (failures) {
|
|
164
|
+
console.error(`\n${failures} check(s) failed`);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
console.log('\nall checks passed');
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { gridLayout, resolveComponentPos, validateGridPlacement } from '../renderers/architecture/grid.mjs';
|
|
4
|
+
|
|
5
|
+
test('gridLayout returns null for free placement', () => {
|
|
6
|
+
assert.equal(gridLayout({}), null);
|
|
7
|
+
assert.equal(gridLayout({ layout: undefined }), null);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test('resolveComponentPos prefers explicit pos over row/col', () => {
|
|
11
|
+
const grid = gridLayout({ layout: { mode: 'grid' } });
|
|
12
|
+
assert.deepEqual(resolveComponentPos({ pos: [9, 8], row: 0, col: 0 }, grid), [9, 8]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('resolveComponentPos maps row/col to pixel origin', () => {
|
|
16
|
+
const grid = gridLayout({
|
|
17
|
+
layout: { mode: 'grid', origin: [40, 80], gapX: 30, gapY: 40, cellW: 130, cellH: 64 },
|
|
18
|
+
});
|
|
19
|
+
assert.deepEqual(resolveComponentPos({ row: 0, col: 0 }, grid), [40, 80]);
|
|
20
|
+
assert.deepEqual(resolveComponentPos({ row: 1, col: 2 }, grid), [40 + 2 * 160, 80 + 104]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('validateGridPlacement rejects duplicate cells and missing row/col', () => {
|
|
24
|
+
const grid = gridLayout({ layout: { mode: 'grid', cols: 4 } });
|
|
25
|
+
const problems = [];
|
|
26
|
+
validateGridPlacement({
|
|
27
|
+
components: [
|
|
28
|
+
{ id: 'a', row: 0, col: 0 },
|
|
29
|
+
{ id: 'b', row: 0, col: 0 },
|
|
30
|
+
{ id: 'c' },
|
|
31
|
+
],
|
|
32
|
+
}, grid, problems);
|
|
33
|
+
assert.ok(problems.some((p) => p.includes('share grid cell')));
|
|
34
|
+
assert.ok(problems.some((p) => p.includes('"c" needs pos')));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('validateGridPlacement ignores explicit pos overrides without row/col', () => {
|
|
38
|
+
const grid = gridLayout({ layout: { mode: 'grid', cols: 4 } });
|
|
39
|
+
const problems = [];
|
|
40
|
+
validateGridPlacement({
|
|
41
|
+
components: [
|
|
42
|
+
{ id: 'a', pos: [40, 80] },
|
|
43
|
+
{ id: 'b', pos: [200, 80] },
|
|
44
|
+
],
|
|
45
|
+
}, grid, problems);
|
|
46
|
+
assert.deepEqual(problems, []);
|
|
47
|
+
});
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// Per-rule coverage for the renderers' layout validators. The golden suite's
|
|
2
|
+
// negative cases mostly trip ajv SCHEMA rules; this file targets the hand-
|
|
3
|
+
// written LAYOUT rules (the `problems.push(...)` checks) — the layer that has
|
|
4
|
+
// regressed before — by mutating a valid example into exactly one violation
|
|
5
|
+
// and asserting the renderer exits non-zero with the expected message.
|
|
6
|
+
//
|
|
7
|
+
// It also locks the error-message CONTRACT: representative messages must carry
|
|
8
|
+
// both the numeric threshold and a remediation hint, since the consumer is an
|
|
9
|
+
// LLM that fixes the JSON from the message alone.
|
|
10
|
+
//
|
|
11
|
+
// node --test test/*.test.mjs
|
|
12
|
+
|
|
13
|
+
import { test } from 'node:test';
|
|
14
|
+
import assert from 'node:assert/strict';
|
|
15
|
+
import { execFileSync } from 'node:child_process';
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import os from 'node:os';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
|
|
21
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const skillRoot = path.resolve(__dirname, '..');
|
|
23
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-rules-'));
|
|
24
|
+
|
|
25
|
+
const EXAMPLES = {
|
|
26
|
+
workflow: 'agent-tool-call.workflow.json',
|
|
27
|
+
sequence: 'cache-miss-request.sequence.json',
|
|
28
|
+
dataflow: 'product-analytics.dataflow.json',
|
|
29
|
+
lifecycle: 'agent-run.lifecycle.json',
|
|
30
|
+
architecture: 'web-app.architecture.json',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function load(mode) {
|
|
34
|
+
return JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples', EXAMPLES[mode]), 'utf8'));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Returns { code, stderr }. Never throws on non-zero exit.
|
|
38
|
+
function render(mode, doc) {
|
|
39
|
+
const input = path.join(tmp, `${mode}-${Math.abs(hash(JSON.stringify(doc)))}.json`);
|
|
40
|
+
const outPath = path.join(tmp, `${mode}-${Math.abs(hash(JSON.stringify(doc)))}.html`);
|
|
41
|
+
fs.writeFileSync(input, JSON.stringify(doc));
|
|
42
|
+
try {
|
|
43
|
+
execFileSync('node', [
|
|
44
|
+
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
|
|
45
|
+
input,
|
|
46
|
+
outPath,
|
|
47
|
+
], { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
48
|
+
return { code: 0, stderr: '', outPath };
|
|
49
|
+
} catch (err) {
|
|
50
|
+
return { code: err.status ?? 1, stderr: String(err.stderr || ''), outPath };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function hash(s) {
|
|
55
|
+
let h = 0;
|
|
56
|
+
for (let i = 0; i < s.length; i += 1) h = (h * 31 + s.charCodeAt(i)) | 0;
|
|
57
|
+
return h;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// [name, mode, mutate(doc), expectedSubstrings[]] — every mutation introduces
|
|
61
|
+
// exactly one layout violation. Each expected substring must appear in stderr.
|
|
62
|
+
const CASES = [
|
|
63
|
+
// ---- workflow layout rules ----
|
|
64
|
+
['workflow: unknown lane', 'workflow', (d) => { d.nodes[0].lane = 'ghost'; }, ['unknown lane "ghost"']],
|
|
65
|
+
['workflow: node label wider than box', 'workflow',
|
|
66
|
+
(d) => { d.nodes[0].label = 'An Extremely Long Node Label That Overflows'; }, ['wider than node', 'shorten the label']],
|
|
67
|
+
['workflow: viewBox width below schema min', 'workflow',
|
|
68
|
+
(d) => { d.meta.viewBox = [699, 900]; }, ['700']],
|
|
69
|
+
['workflow: nodes too close in a lane', 'workflow',
|
|
70
|
+
(d) => { d.nodes.push({ ...d.nodes[0], id: 'dupe', col: d.nodes[0].col }); }, ['less than 8px apart']],
|
|
71
|
+
['workflow: empty group', 'workflow',
|
|
72
|
+
(d) => { d.groups = [{ id: 'empty', label: 'Empty group', lane: 'ui', fromCol: 3, toCol: 4 }]; }, ['does not contain any nodes']],
|
|
73
|
+
['workflow: mainPath missing edge', 'workflow',
|
|
74
|
+
(d) => { d.mainPath = ['user', 'planner']; }, ['mainPath step "user" -> "planner" has no matching edge']],
|
|
75
|
+
['workflow: mainPath moves backward', 'workflow',
|
|
76
|
+
(d) => { d.mainPath = ['external', 'trace']; }, ['moves backward from col']],
|
|
77
|
+
|
|
78
|
+
// ---- sequence layout rules ----
|
|
79
|
+
['sequence: message references unknown participant', 'sequence',
|
|
80
|
+
(d) => { d.messages[0].from = 'ghost'; }, ['unknown source "ghost"']],
|
|
81
|
+
['sequence: message y outside timeline', 'sequence',
|
|
82
|
+
(d) => { d.messages[0].y = 9000; }, ['outside the readable timeline', 'keep y between']],
|
|
83
|
+
['sequence: segment to <= from', 'sequence',
|
|
84
|
+
(d) => { d.segments = [{ from: 400, to: 300, label: 'bad' }]; }, ['invalid y range', 'greater than']],
|
|
85
|
+
|
|
86
|
+
// ---- dataflow layout rules ----
|
|
87
|
+
['dataflow: flow missing label', 'dataflow',
|
|
88
|
+
(d) => { delete d.flows[0].label; }, ['label']],
|
|
89
|
+
['dataflow: flow references unknown node', 'dataflow',
|
|
90
|
+
(d) => { d.flows[0].to = 'ghost'; }, ['unknown target "ghost"']],
|
|
91
|
+
|
|
92
|
+
// ---- lifecycle layout rules ----
|
|
93
|
+
['lifecycle: missing reserved main lane', 'lifecycle',
|
|
94
|
+
(d) => {
|
|
95
|
+
d.lanes = d.lanes.map((l) => (l.id === 'main' ? { ...l, id: 'primary' } : l));
|
|
96
|
+
d.states = d.states.map((s) => (s.lane === 'main' ? { ...s, lane: 'primary' } : s));
|
|
97
|
+
}, ['"main"', 'reserved']],
|
|
98
|
+
['lifecycle: cross-lane state overlap', 'lifecycle',
|
|
99
|
+
(d) => {
|
|
100
|
+
const approval = d.states.find((s) => s.id === 'approval');
|
|
101
|
+
const failed = d.states.find((s) => s.id === 'failed');
|
|
102
|
+
delete failed.yOffset;
|
|
103
|
+
failed.col = approval.col;
|
|
104
|
+
}, ['less than 10px apart']],
|
|
105
|
+
['lifecycle: viewBox height below schema min', 'lifecycle',
|
|
106
|
+
(d) => { d.meta.viewBox = [980, 565]; }, ['566']],
|
|
107
|
+
|
|
108
|
+
// ---- architecture layout rules ----
|
|
109
|
+
['architecture: components overlap', 'architecture',
|
|
110
|
+
(d) => { d.components[1].pos = [...d.components[0].pos]; }, ['less than 8px apart']],
|
|
111
|
+
['architecture: connection references unknown component', 'architecture',
|
|
112
|
+
(d) => { d.connections[0].to = 'ghost'; }, ['unknown target "ghost"']],
|
|
113
|
+
['architecture: boundary wraps unknown component', 'architecture',
|
|
114
|
+
(d) => { d.boundaries[0].wraps.push('ghost'); }, ['wraps unknown component "ghost"']],
|
|
115
|
+
['architecture: label wider than component', 'architecture',
|
|
116
|
+
(d) => { d.components[0].label = 'An Extremely Long Component Label Overflow'; }, ['wider than component', 'shorten the label']],
|
|
117
|
+
['architecture: component overlap suggests fix', 'architecture',
|
|
118
|
+
(d) => { d.components[1].pos = [...d.components[0].pos]; }, ['Suggested fix', 'move "']],
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
for (const [name, mode, mutate, expected] of CASES) {
|
|
122
|
+
test(name, () => {
|
|
123
|
+
const doc = load(mode);
|
|
124
|
+
mutate(doc);
|
|
125
|
+
const { code, stderr } = render(mode, doc);
|
|
126
|
+
assert.notEqual(code, 0, `expected non-zero exit; stderr:\n${stderr}`);
|
|
127
|
+
assert.doesNotMatch(stderr, /TypeError|is not a function|Cannot read/, `crashed instead of reporting:\n${stderr}`);
|
|
128
|
+
for (const sub of expected) {
|
|
129
|
+
assert.ok(stderr.includes(sub), `expected "${sub}" in stderr:\n${stderr}`);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ---- error-message contract: threshold + remediation, not just a path ----
|
|
135
|
+
test('contract: short-edge message carries both the px minimum and a fix verb', () => {
|
|
136
|
+
const d = load('workflow');
|
|
137
|
+
// Force a too-short labeled edge between adjacent same-lane columns.
|
|
138
|
+
d.nodes.push({ id: 'a1', lane: d.nodes[0].lane, col: 0, type: 'backend', label: 'A' });
|
|
139
|
+
d.nodes.push({ id: 'a2', lane: d.nodes[0].lane, col: 0, type: 'backend', label: 'B', yOffset: 30 });
|
|
140
|
+
d.edges.push({ from: 'a1', to: 'a2', label: 'x', route: 'straight' });
|
|
141
|
+
const { stderr } = render('workflow', d);
|
|
142
|
+
// Whatever rule fires, the messages must remain actionable (threshold + verb).
|
|
143
|
+
assert.match(stderr, /\d+px|at least \d+|0\.\.\d+|less than/);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('contract: ajv path errors are annotated with the element id', () => {
|
|
147
|
+
const d = load('workflow');
|
|
148
|
+
d.nodes[3].colour = 'red'; // unknown property → ajv additionalProperties
|
|
149
|
+
const { stderr } = render('workflow', d);
|
|
150
|
+
// Only meaningful when ajv is installed; skip the assertion in degraded mode.
|
|
151
|
+
if (!/schema validation failed/.test(stderr)) return;
|
|
152
|
+
assert.match(stderr, /id\/label:/);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('workflow: same-lane offset auto edge stays orthogonal', () => {
|
|
156
|
+
const d = {
|
|
157
|
+
schema_version: 1,
|
|
158
|
+
diagram_type: 'workflow',
|
|
159
|
+
meta: { title: 'Same-lane offset route' },
|
|
160
|
+
lanes: [{ id: 'main', label: 'Main lane' }],
|
|
161
|
+
nodes: [
|
|
162
|
+
{ id: 'left', lane: 'main', col: 1, type: 'backend', label: 'A', width: 32, height: 38, yOffset: -14 },
|
|
163
|
+
{ id: 'right', lane: 'main', col: 2, type: 'backend', label: 'B', width: 32, height: 38, yOffset: 14 },
|
|
164
|
+
],
|
|
165
|
+
edges: [{ from: 'left', to: 'right' }],
|
|
166
|
+
};
|
|
167
|
+
const { code, stderr, outPath } = render('workflow', d);
|
|
168
|
+
assert.equal(code, 0, stderr);
|
|
169
|
+
const html = fs.readFileSync(outPath, 'utf8');
|
|
170
|
+
assert.doesNotMatch(html, /M 236 105 L 284 133/);
|
|
171
|
+
assert.match(html, /M 236 105 L 260 105 L 260 133 L 284 133/);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('workflow: edge crossing a non-endpoint node is rejected', () => {
|
|
175
|
+
const d = {
|
|
176
|
+
schema_version: 1,
|
|
177
|
+
diagram_type: 'workflow',
|
|
178
|
+
meta: { title: 'Crossing edge route' },
|
|
179
|
+
lanes: [{ id: 'main', label: 'Main lane' }],
|
|
180
|
+
nodes: [
|
|
181
|
+
{ id: 'left', lane: 'main', col: 0, type: 'backend', label: 'Left', width: 60 },
|
|
182
|
+
{ id: 'middle', lane: 'main', col: 2, type: 'database', label: 'Middle', width: 70 },
|
|
183
|
+
{ id: 'right', lane: 'main', col: 4, type: 'backend', label: 'Right', width: 60 },
|
|
184
|
+
],
|
|
185
|
+
edges: [{ from: 'left', to: 'right', route: 'straight' }],
|
|
186
|
+
};
|
|
187
|
+
const { code, stderr } = render('workflow', d);
|
|
188
|
+
assert.notEqual(code, 0, `expected non-zero exit; stderr:\n${stderr}`);
|
|
189
|
+
assert.match(stderr, /crosses node "middle"/);
|
|
190
|
+
assert.match(stderr, /fromSide\/toSide|channel|lane\/column/);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const skillRoot = path.resolve(__dirname, '..');
|
|
11
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-output-checks-'));
|
|
12
|
+
const checker = path.join(skillRoot, 'scripts/check-render-output.mjs');
|
|
13
|
+
|
|
14
|
+
function checkHtml(name, svgBody) {
|
|
15
|
+
const htmlPath = path.join(tmp, `${name}.html`);
|
|
16
|
+
fs.writeFileSync(htmlPath, `<!doctype html><html><body><svg viewBox="0 0 240 160">${svgBody}</svg></body></html>`);
|
|
17
|
+
try {
|
|
18
|
+
const stdout = execFileSync('node', [checker, htmlPath], { encoding: 'utf8' });
|
|
19
|
+
return { code: 0, result: JSON.parse(stdout) };
|
|
20
|
+
} catch (err) {
|
|
21
|
+
return { code: err.status ?? 1, result: JSON.parse(String(err.stdout || '{}')) };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('render output check: accepts orthogonal arrows away from legend', () => {
|
|
26
|
+
const { code, result } = checkHtml('clean', `
|
|
27
|
+
<path d="M 20 20 L 120 20 L 120 60" class="a-default" stroke-width="1.4" marker-end="url(#arrowhead)"/>
|
|
28
|
+
<!-- Legend -->
|
|
29
|
+
<text x="40" y="120" class="t-primary" font-size="10">Legend</text>
|
|
30
|
+
<rect x="40" y="132" width="14" height="9" class="c-backend"/>
|
|
31
|
+
<text x="60" y="140" class="t-muted" font-size="7">Backend</text>
|
|
32
|
+
`);
|
|
33
|
+
assert.equal(code, 0);
|
|
34
|
+
assert.equal(result.ok, true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('render output check: rejects two-point diagonal arrows', () => {
|
|
38
|
+
const { code, result } = checkHtml('diagonal', `
|
|
39
|
+
<path d="M 20 20 L 120 80" class="a-default" stroke-width="1.4" marker-end="url(#arrowhead)"/>
|
|
40
|
+
<!-- Legend -->
|
|
41
|
+
<text x="40" y="120" class="t-primary" font-size="10">Legend</text>
|
|
42
|
+
`);
|
|
43
|
+
assert.notEqual(code, 0);
|
|
44
|
+
const check = result.checks.find((item) => item.name === 'orthogonal_arrows');
|
|
45
|
+
assert.equal(check.ok, false);
|
|
46
|
+
assert.match(check.details[0], /path 1/);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('render output check: rejects arrows crossing legend text', () => {
|
|
50
|
+
const { code, result } = checkHtml('legend-crossing', `
|
|
51
|
+
<path d="M 20 112 L 180 112" class="a-dashed" stroke-width="1.4" marker-end="url(#arrowhead-dashed)"/>
|
|
52
|
+
<!-- Legend -->
|
|
53
|
+
<text x="40" y="120" class="t-primary" font-size="10">Legend</text>
|
|
54
|
+
<rect x="40" y="132" width="14" height="9" class="c-backend"/>
|
|
55
|
+
<text x="60" y="140" class="t-muted" font-size="7">Backend</text>
|
|
56
|
+
`);
|
|
57
|
+
assert.notEqual(code, 0);
|
|
58
|
+
const check = result.checks.find((item) => item.name === 'legend_clearance');
|
|
59
|
+
assert.equal(check.ok, false);
|
|
60
|
+
assert.match(check.details[0], /Legend/);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('render output check: ignores unmarked sequence lifelines near legend', () => {
|
|
64
|
+
const { code, result } = checkHtml('lifeline-near-legend', `
|
|
65
|
+
<path d="M 60 20 L 60 126" class="a-default" stroke-width="0.8" stroke-dasharray="3,7"/>
|
|
66
|
+
<!-- Legend -->
|
|
67
|
+
<text x="40" y="120" class="t-primary" font-size="10">Legend</text>
|
|
68
|
+
<path d="M 120 136 L 154 136" class="a-default" stroke-width="1.4" stroke-dasharray="3,5" marker-end="url(#arrowhead)"/>
|
|
69
|
+
<text x="163" y="139" class="t-muted" font-size="8">return</text>
|
|
70
|
+
`);
|
|
71
|
+
assert.equal(code, 0);
|
|
72
|
+
assert.equal(result.ok, true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
@@ -136,9 +136,23 @@ saber sem decidir, o que já é o resultado.
|
|
|
136
136
|
|
|
137
137
|
### 6.5 O desenho não é a verdade
|
|
138
138
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
**Todo mapa que você produz sai também desenhado, em Archify (HTML).** Mapa de topologia, mapa de dead
|
|
140
|
+
code, mapa de dual path: os três. A tabela do relatório continua sendo o mapa; o desenho é **uma
|
|
141
|
+
segunda forma dele**, obrigatória, e não um substituto (D-183, S533: a primeira rodada produziu zero
|
|
142
|
+
diagramas porque esta seção os deixava opcionais). Fora dos mapas, diagrama só quando **decide** alguma
|
|
143
|
+
coisa.
|
|
144
|
+
|
|
145
|
+
Toda figura carrega **de que medição foi derivada** (o comando, o `arquivo:linha` ou o sha) e **em que
|
|
146
|
+
data**, e isso vai **dentro do desenho**: num elemento do próprio SVG (um nó ou rótulo de
|
|
147
|
+
proveniência), não só no `meta.subtitle`, num card ou num comentário. Na Archify, subtítulo e cards são
|
|
148
|
+
HTML **fora** do `<svg>`, e a exportação serializa **só** o SVG (`assets/template.html`,
|
|
149
|
+
`XMLSerializer` sobre o clone do `<svg>`): o PNG ou o SVG exportado perderia a proveniência. Sem ela a
|
|
150
|
+
figura envelhece em silêncio e continua convincente, e em duas sessões vira a fonte que alguém cita no
|
|
151
|
+
lugar do código.
|
|
152
|
+
|
|
153
|
+
**O desenho não é a verdade.** Ele é derivado do registro e do relatório, e nunca o contrário. Quando
|
|
154
|
+
os dois divergem, **o registro e o relatório vencem**, e o desenho é que está velho: regenere-o a
|
|
155
|
+
partir deles, sem ajustar a tabela para caber na figura.
|
|
142
156
|
|
|
143
157
|
### 6.6 Verificar — e desconfiar do instrumento que concorda
|
|
144
158
|
|
|
@@ -181,6 +195,9 @@ DEAD CODE (se houve varredura)
|
|
|
181
195
|
DUAL PATH (se houve varredura)
|
|
182
196
|
| contrato | caminho A | caminho B | quem chama A | quem chama B | divergiram desde | qual está certo |
|
|
183
197
|
|
|
198
|
+
MAPAS ARCHIFY (um por mapa acima: topologia, dead code, dual path — §6.5)
|
|
199
|
+
| mapa | caminho do .html | sha256 do .html | medição de origem (dentro do desenho) | data |
|
|
200
|
+
|
|
184
201
|
<as seções do seu substrato — o seu charter diz quais>
|
|
185
202
|
|
|
186
203
|
DECISÃO PROPOSTA
|
|
@@ -8,7 +8,7 @@ description: Search and apply API patterns from project KB. Use before implement
|
|
|
8
8
|
Before implementing, find the canonical pattern:
|
|
9
9
|
|
|
10
10
|
### Step 1: Search KB
|
|
11
|
-
|
|
11
|
+
Grep the codebase for existing implementations:
|
|
12
12
|
```bash
|
|
13
13
|
grep -rl "class.*Controller" src/presentation/http/controllers/ | head -3
|
|
14
14
|
```
|