@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.
Files changed (59) hide show
  1. package/agents/agentic-architect.md +1 -1
  2. package/agents/architecture-mediator.md +2 -1
  3. package/agents/backend-architect.md +1 -1
  4. package/agents/contract-architect.md +1 -1
  5. package/agents/datamodel-architect.md +1 -1
  6. package/agents/frontend-architect.md +1 -1
  7. package/agents/harness-architect.md +1 -1
  8. package/agents/infra-architect.md +1 -1
  9. package/manifest.json +20 -12
  10. package/package.json +1 -1
  11. package/skills/archify/LICENSE +22 -0
  12. package/skills/archify/SKILL.md +278 -0
  13. package/skills/archify/VENDOR.txt +70 -0
  14. package/skills/archify/assets/template.html +1261 -0
  15. package/skills/archify/bin/archify.mjs +271 -0
  16. package/skills/archify/examples/agent-run.lifecycle.json +64 -0
  17. package/skills/archify/examples/agent-tool-call.workflow.json +88 -0
  18. package/skills/archify/examples/cache-miss-request.sequence.json +75 -0
  19. package/skills/archify/examples/product-analytics.dataflow.json +70 -0
  20. package/skills/archify/examples/web-app.architecture.json +41 -0
  21. package/skills/archify/package-lock.json +80 -0
  22. package/skills/archify/package.json +23 -0
  23. package/skills/archify/renderers/architecture/grid.mjs +62 -0
  24. package/skills/archify/renderers/architecture/render-architecture.mjs +371 -0
  25. package/skills/archify/renderers/dataflow/README.md +82 -0
  26. package/skills/archify/renderers/dataflow/render-dataflow.mjs +295 -0
  27. package/skills/archify/renderers/lifecycle/README.md +91 -0
  28. package/skills/archify/renderers/lifecycle/render-lifecycle.mjs +384 -0
  29. package/skills/archify/renderers/sequence/README.md +84 -0
  30. package/skills/archify/renderers/sequence/render-sequence.mjs +257 -0
  31. package/skills/archify/renderers/shared/cli.mjs +44 -0
  32. package/skills/archify/renderers/shared/generated-validators.mjs +13 -0
  33. package/skills/archify/renderers/shared/geometry.mjs +230 -0
  34. package/skills/archify/renderers/shared/layout-report.mjs +40 -0
  35. package/skills/archify/renderers/shared/utils.mjs +88 -0
  36. package/skills/archify/renderers/shared/validator.mjs +40 -0
  37. package/skills/archify/renderers/workflow/README.md +103 -0
  38. package/skills/archify/renderers/workflow/render-workflow.mjs +481 -0
  39. package/skills/archify/schemas/README.md +72 -0
  40. package/skills/archify/schemas/architecture.schema.json +121 -0
  41. package/skills/archify/schemas/common.schema.json +43 -0
  42. package/skills/archify/schemas/dataflow.schema.json +221 -0
  43. package/skills/archify/schemas/lifecycle.schema.json +241 -0
  44. package/skills/archify/schemas/sequence.schema.json +186 -0
  45. package/skills/archify/schemas/workflow.schema.json +332 -0
  46. package/skills/archify/scripts/check-render-output.mjs +297 -0
  47. package/skills/archify/scripts/generate-validators.mjs +66 -0
  48. package/skills/archify/scripts/render-examples.mjs +26 -0
  49. package/skills/archify/test/animation.test.mjs +53 -0
  50. package/skills/archify/test/cli.test.mjs +206 -0
  51. package/skills/archify/test/degraded.test.mjs +157 -0
  52. package/skills/archify/test/generate-validators.test.mjs +40 -0
  53. package/skills/archify/test/geometry.test.mjs +186 -0
  54. package/skills/archify/test/golden.mjs +167 -0
  55. package/skills/archify/test/grid.test.mjs +47 -0
  56. package/skills/archify/test/layout-rules.test.mjs +193 -0
  57. package/skills/archify/test/render-output-checks.test.mjs +75 -0
  58. package/skills/arquitetura-mediada-arquiteto/SKILL.md +20 -3
  59. 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
- Diagrama (Archify ou outro) só quando **decide** alguma coisa. Toda figura carrega, no próprio
140
- artefato, **de que medição foi derivada** e **em que data**. Sem isso ela envelhece em silêncio e
141
- continua convincente — e vira, em duas sessões, a fonte que alguém cita no lugar do código.
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
- Use MCP tool kbSearch or grep the codebase for existing implementations:
11
+ Grep the codebase for existing implementations:
12
12
  ```bash
13
13
  grep -rl "class.*Controller" src/presentation/http/controllers/ | head -3
14
14
  ```