Skill Tracer
Skills › Archify › archify › authoring-contract.md

authoring-contract.md

645 lines · archify/references/authoring-contract.md at a406594

Open on GitHub
1# Authoring contract
2
3Read this reference only after the Fast authoring path calls for more detail. The schemas and examples remain authoritative.
4

Authoring contract

Read this reference only after the Fast authoring path calls for more detail. The schemas and examples remain authoritative.

Raw lines 1 to 4
5## Composition repair
6
7When correcting an authored overview's abstraction, map every affected role, relationship direction, protocol, boundary, condition, and source reference to its surviving node or relationship before regrouping. A startup citation does not prove a message protocol. Preserve each claim's inspected evidence. Keep user-supplied or agreed topology fixed; fewer routes alone do not justify merging. A boundary around one node requires an explicit isolation fact and must not merely repeat its label.
8

Composition repair

When correcting an authored overview's abstraction, map every affected role, relationship direction, protocol, boundary, condition, and source reference to its surviving node or relationship before regrouping. A startup citation does not prove a message protocol. Preserve each claim's inspected evidence. Keep user-supplied or agreed topology fixed; fewer routes alone do not justify merging. A boundary around one node requires an explicit isolation fact and must not merely repeat its label.

Raw lines 5 to 8
9## Label repair
11When a relationship label collides, move the label, adjust the route or spacing, then shorten the wording while preserving meaning. Omit wording only when both endpoints fully imply it and it conveys no protocol, action, direction, synchronous or asynchronous behavior, or cross-boundary mechanism. Spacing means clear gap rather than center distance; measured mask width takes precedence. The first-draft gap budget is in [Layout and routing](authoring-defaults.md#layout-and-routing).
13For a disproportionate sublabel, keep its exact role or protocol concise and place the supplementary fact in a note or card. Preserve every required responsibility, protocol, and boundary fact. Use the first-draft node-width budget in [Layout and routing](authoring-defaults.md#layout-and-routing).

Label repair

When a relationship label collides, move the label, adjust the route or spacing, then shorten the wording while preserving meaning. Omit wording only when both endpoints fully imply it and it conveys no protocol, action, direction, synchronous or asynchronous behavior, or cross-boundary mechanism. Spacing means clear gap rather than center distance; measured mask width takes precedence. The first-draft gap budget is in Layout and routing.

For a disproportionate sublabel, keep its exact role or protocol concise and place the supplementary fact in a note or card. Preserve every required responsibility, protocol, and boundary fact. Use the first-draft node-width budget in Layout and routing.

Raw lines 9 to 14
15## Schema lookup
17Read both the mode schema and `schemas/common.schema.json`. The mode schemas use `$ref`, so the common file is where shared enums live.
19- `componentType`: `frontend`, `backend`, `database`, `cloud`, `security`, `messagebus`, `external`
20- Entity-relationship documents use `entities` and `relationships`; `key` accepts `pk`, `fk`, or `uk`, and `fromCardinality`/`toCardinality` accept `one` or `many`.
21- `variant`: `default`, `emphasis`, `security`, `dashed`
22- Relationship IDs use the shared identifier pattern and must be unique in their collection.
24Do not invent fields. Before writing any new field, enum, or constrained text, read its schema definition, including common `$ref` targets. In particular, check boundary kinds, repository identity, and source-reference shapes. An example demonstrates structure; it does not enumerate every valid value. Author fresh IDs, wording, facts, and layout.

Schema lookup

Read both the mode schema and schemas/common.schema.json. The mode schemas use $ref, so the common file is where shared enums live.

  • componentType: frontend, backend, database, cloud, security, messagebus, external
  • Entity-relationship documents use entities and relationships; key accepts pk, fk, or uk, and fromCardinality/toCardinality accept one or many.
  • variant: default, emphasis, security, dashed
  • Relationship IDs use the shared identifier pattern and must be unique in their collection.

Do not invent fields. Before writing any new field, enum, or constrained text, read its schema definition, including common $ref targets. In particular, check boundary kinds, repository identity, and source-reference shapes. An example demonstrates structure; it does not enumerate every valid value. Author fresh IDs, wording, facts, and layout.

Raw lines 15 to 25
26## Workflow layout contracts
28Use schema v2 for new workflows and keep schema v1 when an existing source must
29retain fixed geometry. In both versions, `col` stays in `0..5` and semantic
30edge labels are never deleted as a spacing repair. Do not change only
31`schema_version` when absolute coordinates exist: follow the canonical
32[migration and layout-receipt contract](../renderers/workflow/README.md#migration-and-layout-receipt).
33The complete normative invariants live in the workflow renderer's
34[layout contracts](../renderers/workflow/README.md#layout-contracts).
35For sequential stages stacked in one container, use one v2 lane and group,
36omit `meta.viewBox`, and center nodes around the lane content with symmetric
37`yOffset` values such as `-90 / 0 / 90`. Keep semantic edge labels and act on
38compiler diagnostics.

Workflow layout contracts

Use schema v2 for new workflows and keep schema v1 when an existing source must retain fixed geometry. In both versions, col stays in 0..5 and semantic edge labels are never deleted as a spacing repair. Do not change only schema_version when absolute coordinates exist: follow the canonical migration and layout-receipt contract. The complete normative invariants live in the workflow renderer's layout contracts. For sequential stages stacked in one container, use one v2 lane and group, omit meta.viewBox, and center nodes around the lane content with symmetric yOffset values such as -90 / 0 / 90. Keep semantic edge labels and act on compiler diagnostics.

Raw lines 26 to 39
40## Legend contract
42Omit `meta.legend` for the truthful default: `auto` lists only semantic kinds
43present in typed IR. Use `mode: "all"` for a renderer reference or
44`mode: "hidden"` to remove the full legend. Under `entries`, only keys listed
45by the selected mode schema are valid; each key accepts `label`, `visible`, or
46both. `visible: true` may show an unused supported convention, while
47`visible: false` hides it. `hidden` cannot be overridden.
49A label override changes reader wording only. Never infer a kind from prose or
50use the legend to compensate for missing nodes, states, messages, or flows.
51Long labels are measured and wrap into deterministic rows. Architecture's
52implicit automatic viewBox grows from that same measured footprint, and an
53automatic ERD canvas is sized the same way so its default legend is always in
54the generated content. For backwards compatibility, a legacy document with no
55`meta.legend` may omit an implicit auto legend that cannot fit its explicit
56viewBox; this never changes its typed topology. An ERD that authors a
57`meta.viewBox` keeps that fixed area and reports the capacity instead of
58dropping the legend. Adding `meta.legend` makes the presentation intentional and
59strict: if its resolved labels cannot fit the authored viewBox, shorten or hide
60them, or widen the viewBox using the emitted diagnostic.

Legend contract

Omit meta.legend for the truthful default: auto lists only semantic kinds present in typed IR. Use mode: "all" for a renderer reference or mode: "hidden" to remove the full legend. Under entries, only keys listed by the selected mode schema are valid; each key accepts label, visible, or both. visible: true may show an unused supported convention, while visible: false hides it. hidden cannot be overridden.

A label override changes reader wording only. Never infer a kind from prose or use the legend to compensate for missing nodes, states, messages, or flows. Long labels are measured and wrap into deterministic rows. Architecture's implicit automatic viewBox grows from that same measured footprint, and an automatic ERD canvas is sized the same way so its default legend is always in the generated content. For backwards compatibility, a legacy document with no meta.legend may omit an implicit auto legend that cannot fit its explicit viewBox; this never changes its typed topology. An ERD that authors a meta.viewBox keeps that fixed area and reports the capacity instead of dropping the legend. Adding meta.legend makes the presentation intentional and strict: if its resolved labels cannot fit the authored viewBox, shorten or hide them, or widen the viewBox using the emitted diagnostic.

Raw lines 40 to 61
62## Language consistency
64Choose one primary authored language. An explicit user choice wins; otherwise
65use the language of the request, or the conversation's dominant language when
66the request itself is language-neutral. Separately choose the Viewer locale.
67Always write the matching `meta.locale` as a well-formed language tag: `"en"`
68for English, `"zh-CN"` for Simplified Chinese, `"es"` for Spanish, or any
69other tag for another language. The renderer consumes the authored locale without inferring language
70from diagram strings. Documents that omit it remain valid and default to
71English.
73`meta.locale` controls only renderer-owned reader surfaces: `<html lang>`, the
74document-title suffix, default SVG description and focus labels, default legend
75labels, and fixed Viewer controls, statuses, accessibility names, and errors.
76It never translates authored content. Apply the primary language separately to
77titles, subtitles, node and relationship copy, boundaries, lanes, groups,
78legend label overrides, and cards. A bilingual diagram still
79chooses one primary locale for the Viewer; follow an explicit primary-language
80request, then prompt order or conversation dominance.
82Bundled Viewer catalogs are enrolled in `locales/manifest.json`: currently
83`en`, `zh-CN`, `zh-TW`, `es`, `ko`, and `ar`. For these, `meta.locale` alone selects the full
84catalog. Tags match case-insensitively (`zh-cn` selects `zh-CN`), but region
85and script variants are distinct: `zh-Hant`, `es-MX`, or `ko-KR` select no
86bundled catalog.
88`meta.translations` is an optional per-key override: an object mapping the
89renderer's canonical message keys (`locales/en.json`, or `catalogKeys()` in
90`renderers/shared/i18n.mjs`) to translated strings whose `{placeholder}` tokens
91match the English source exactly. Each message resolves as: valid
92`meta.translations` value → the selected bundled catalog → English. For a
93bundled locale, supply only the keys whose wording the diagram needs to change;
94the rest of the language is kept. Omitting the field or supplying `{}` means no
95override. For a language without a bundled catalog, supply the catalog here.
96Reuse suitable translations from `examples/locales/` or a previously reviewed
97catalog, translate missing keys, and use the English source to check keys and
98placeholders.
100An unknown key or a value with mismatched placeholders is rejected and keeps
101the lower-priority message; it is reported as `i18n/invalid-translation` on
102stderr and as a warning in the `validate --json`, `deliver`, and `finalize`
103receipt `diagnostics[]`. Keys that still resolve to English in a
104non-English locale are reported as `i18n/translation-coverage`, with the missing
105keys and their English source text; add exactly those keys to repair the gap.
106Coverage describes the final resolved catalog, not the size of the override.
108For a requested language you cannot supply `meta.translations` for, do not
109write a `meta.locale` with no bundled catalog and no translations. Keep every
110reader-facing authored string in the requested language, omit `meta.locale` so
111the renderer safely uses English, and explicitly tell the user that fixed
112Viewer UI and `<html lang>` remain English and the artifact is not fully localized.
113The fallback applies only to renderer-owned surfaces; it never
114permits authored copy to fall back to English. Do not silently substitute
115`zh-CN` for another language or Chinese locale, and do not machine-translate
116`meta.translations` values without disclosing that they are unreviewed.
118Keep exact product names, code identifiers, commands, protocols, API paths, and
119environment names intact. Those terms may remain English inside localized copy,
120but surrounding explanatory prose must still use the selected language.
121Renderer-owned default legend labels follow `meta.locale`; author a
122`meta.legend.entries.*.label` override only when the diagram needs different
123domain wording, and keep that authored override in the primary language.

Language consistency

Choose one primary authored language. An explicit user choice wins; otherwise use the language of the request, or the conversation's dominant language when the request itself is language-neutral. Separately choose the Viewer locale. Always write the matching meta.locale as a well-formed language tag: "en" for English, "zh-CN" for Simplified Chinese, "es" for Spanish, or any other tag for another language. The renderer consumes the authored locale without inferring language from diagram strings. Documents that omit it remain valid and default to English.

meta.locale controls only renderer-owned reader surfaces: <html lang>, the document-title suffix, default SVG description and focus labels, default legend labels, and fixed Viewer controls, statuses, accessibility names, and errors. It never translates authored content. Apply the primary language separately to titles, subtitles, node and relationship copy, boundaries, lanes, groups, legend label overrides, and cards. A bilingual diagram still chooses one primary locale for the Viewer; follow an explicit primary-language request, then prompt order or conversation dominance.

Bundled Viewer catalogs are enrolled in locales/manifest.json: currently en, zh-CN, zh-TW, es, ko, and ar. For these, meta.locale alone selects the full catalog. Tags match case-insensitively (zh-cn selects zh-CN), but region and script variants are distinct: zh-Hant, es-MX, or ko-KR select no bundled catalog.

meta.translations is an optional per-key override: an object mapping the renderer's canonical message keys (locales/en.json, or catalogKeys() in renderers/shared/i18n.mjs) to translated strings whose {placeholder} tokens match the English source exactly. Each message resolves as: valid meta.translations value → the selected bundled catalog → English. For a bundled locale, supply only the keys whose wording the diagram needs to change; the rest of the language is kept. Omitting the field or supplying {} means no override. For a language without a bundled catalog, supply the catalog here. Reuse suitable translations from examples/locales/ or a previously reviewed catalog, translate missing keys, and use the English source to check keys and placeholders.

An unknown key or a value with mismatched placeholders is rejected and keeps the lower-priority message; it is reported as i18n/invalid-translation on stderr and as a warning in the validate --json, deliver, and finalize receipt diagnostics[]. Keys that still resolve to English in a non-English locale are reported as i18n/translation-coverage, with the missing keys and their English source text; add exactly those keys to repair the gap. Coverage describes the final resolved catalog, not the size of the override.

For a requested language you cannot supply meta.translations for, do not write a meta.locale with no bundled catalog and no translations. Keep every reader-facing authored string in the requested language, omit meta.locale so the renderer safely uses English, and explicitly tell the user that fixed Viewer UI and <html lang> remain English and the artifact is not fully localized. The fallback applies only to renderer-owned surfaces; it never permits authored copy to fall back to English. Do not silently substitute zh-CN for another language or Chinese locale, and do not machine-translate meta.translations values without disclosing that they are unreviewed.

Keep exact product names, code identifiers, commands, protocols, API paths, and environment names intact. Those terms may remain English inside localized copy, but surrounding explanatory prose must still use the selected language. Renderer-owned default legend labels follow meta.locale; author a meta.legend.entries.*.label override only when the diagram needs different domain wording, and keep that authored override in the primary language.

Raw lines 62 to 124
125## Visual preset default
127Omit `meta.visual_preset` by default. The renderer then opens the diagram in
128`classic` for both light and dark color modes. Color mode and visual preset are
129independent viewer state: switching Light / Dark must preserve the current
130preset. Author `signal-flow`, `blueprint`, or `editorial` only when the user
131explicitly requests that visual style.

Visual preset default

Omit meta.visual_preset by default. The renderer then opens the diagram in classic for both light and dark color modes. Color mode and visual preset are independent viewer state: switching Light / Dark must preserve the current preset. Author signal-flow, blueprint, or editorial only when the user explicitly requests that visual style.

Raw lines 125 to 132
133## Engineering profile default
135Omit `meta.engineering_profile` for an ordinary system architecture. Region,
136cluster, and security boundary wording do not by themselves enable an
137engineering profile. Enable `deployment-ownership` only when the user
138explicitly asks for a production deployment topology, ownership handoff, or
139fail-closed deployment review and the source facts are known. Once enabled,
140do not remove the engineering profile merely to pass validation; repair the
141authored facts or report the diagnostics truthfully.

Engineering profile default

Omit meta.engineering_profile for an ordinary system architecture. Region, cluster, and security boundary wording do not by themselves enable an engineering profile. Enable deployment-ownership only when the user explicitly asks for a production deployment topology, ownership handoff, or fail-closed deployment review and the source facts are known. Once enabled, do not remove the engineering profile merely to pass validation; repair the authored facts or report the diagnostics truthfully.

Raw lines 133 to 142
143## Title hierarchy
145Use one concise title and let the diagram carry the explanation. Omit
146`meta.subtitle` by default, and never use it to restate the title, nodes, edges,
147or cards. Include one short supporting line only when the user explicitly asks
148for a subtitle; an omitted or blank subtitle must not leave an empty visual row
149in the generated viewer.

Title hierarchy

Use one concise title and let the diagram carry the explanation. Omit meta.subtitle by default, and never use it to restate the title, nodes, edges, or cards. Include one short supporting line only when the user explicitly asks for a subtitle; an omitted or blank subtitle must not leave an empty visual row in the generated viewer.

Raw lines 143 to 150
151## Executable geometry rules
153Generate one responsive artifact for laptops and external displays, preserving the authored SVG/viewBox, proportions, semantic geometry, and normal document flow. Use meaningful content rows and the Reader-declared readable page-scroll behavior when the complete diagram needs more height; viewport fitting does not authorize alternate topology or smaller typography.
155- Node anchors start at side midpoints. `left`/`right` change the horizontal endpoint; `top`/`bottom` change the vertical endpoint. For an automatic Architecture relationship, unobstructed facing ports whose axis offset is under 16px may share one horizontal or vertical axis when both endpoints retain the 16px corner gutter. If exactly one endpoint belongs to a spread group, only its unshared counterpart moves; relationships spread at both endpoints keep their distinct ports and outside bridge unless a reciprocal facing pair can jointly use separate straight lanes while preserving endpoint spacing, labels, and all surrounding route and obstacle clearances.
156- A side is a direction contract. The first and final route segment must be perpendicular and outward/inward in the named direction.
157- In architecture and data-flow diagrams, explicit `route: "straight"` requests one direct segment, which may be diagonal when endpoint sides are not pinned. The artifact checker preserves this intent; explicit sides, opaque-node clearance, and other quality gates still apply. `via` takes precedence and retains existing rules, including data-flow's requirement for orthogonal via segments.
158- Automatic Port Spread is a default renderer behavior for architecture, workflow, and data-flow diagrams. Shared automatic endpoints spread deterministically and symmetrically with a 16px corner gutter. It does not apply to sequence messages, single relationships, or explicit `via`, `channelX`, `channelY`, `labelAt`, or non-`auto` routes.
159- Showcase route rhythm: every nonzero segment must be at least 8px; every interior segment must be at least 16px. When spread ports are nearly parallel, the router uses a 24px endpoint stub and a 16px outside bridge instead of manufacturing a tiny dogleg.
160- Showcase route compactness: an explicit Architecture route fails with `composition/excessive-route-detour` when its orthogonal length is at least 2.5 times an obstacle-aware legal route, adds at least 200px, and sends a control point at least 96px beyond the content envelope. The evidence records both lengths, ratio, excess, bounds, and excursion. Remove an unnecessary `via` or move the diagnosed corridor inward instead of enlarging the canvas. Related relationships that overlap on the same outer corridor by at least 32px are treated as an intentional bus and remain valid.
161- Shared endpoint corridors are allowed only when they remain semantically unambiguous. Unrelated collinear overlap of 8px or more fails showcase.
162- Container borders are intentional pass-through geometry, but a long edge running along a structural border is not.
163- An edge crossing an unrelated opaque node is always a hard failure, independent of quality profile.

Executable geometry rules

Generate one responsive artifact for laptops and external displays, preserving the authored SVG/viewBox, proportions, semantic geometry, and normal document flow. Use meaningful content rows and the Reader-declared readable page-scroll behavior when the complete diagram needs more height; viewport fitting does not authorize alternate topology or smaller typography.

  • Node anchors start at side midpoints. left/right change the horizontal endpoint; top/bottom change the vertical endpoint. For an automatic Architecture relationship, unobstructed facing ports whose axis offset is under 16px may share one horizontal or vertical axis when both endpoints retain the 16px corner gutter. If exactly one endpoint belongs to a spread group, only its unshared counterpart moves; relationships spread at both endpoints keep their distinct ports and outside bridge unless a reciprocal facing pair can jointly use separate straight lanes while preserving endpoint spacing, labels, and all surrounding route and obstacle clearances.
  • A side is a direction contract. The first and final route segment must be perpendicular and outward/inward in the named direction.
  • In architecture and data-flow diagrams, explicit route: "straight" requests one direct segment, which may be diagonal when endpoint sides are not pinned. The artifact checker preserves this intent; explicit sides, opaque-node clearance, and other quality gates still apply. via takes precedence and retains existing rules, including data-flow's requirement for orthogonal via segments.
  • Automatic Port Spread is a default renderer behavior for architecture, workflow, and data-flow diagrams. Shared automatic endpoints spread deterministically and symmetrically with a 16px corner gutter. It does not apply to sequence messages, single relationships, or explicit via, channelX, channelY, labelAt, or non-auto routes.
  • Showcase route rhythm: every nonzero segment must be at least 8px; every interior segment must be at least 16px. When spread ports are nearly parallel, the router uses a 24px endpoint stub and a 16px outside bridge instead of manufacturing a tiny dogleg.
  • Showcase route compactness: an explicit Architecture route fails with composition/excessive-route-detour when its orthogonal length is at least 2.5 times an obstacle-aware legal route, adds at least 200px, and sends a control point at least 96px beyond the content envelope. The evidence records both lengths, ratio, excess, bounds, and excursion. Remove an unnecessary via or move the diagnosed corridor inward instead of enlarging the canvas. Related relationships that overlap on the same outer corridor by at least 32px are treated as an intentional bus and remain valid.
  • Shared endpoint corridors are allowed only when they remain semantically unambiguous. Unrelated collinear overlap of 8px or more fails showcase.
  • Container borders are intentional pass-through geometry, but a long edge running along a structural border is not.
  • An edge crossing an unrelated opaque node is always a hard failure, independent of quality profile.
Raw lines 151 to 164
165### Explicit `via` coordinates
167Use the resolved departure anchor `S = [sx, sy]` and arrival anchor
168`T = [tx, ty]`. Anchors start at side midpoints, but automatic routing and
169Port Spread can move them as described above; do not assume an anchor copied
170from an automatic route is the anchor of a newly authored explicit route.
171Explicit `via` routes do not receive automatic Port Spread.
173For the first waypoint `F = via[0]` and last waypoint `L = via[via.length - 1]`,
174use these alignments and directions (SVG y increases downward):
176| Side | Departure (`fromSide`): `S` → `F` | Arrival (`toSide`): `L` → `T` |
177| --- | --- | --- |
178| `top` | `F[0] === sx`, `F[1] < sy` | `L[0] === tx`, `L[1] < ty` |
179| `bottom` | `F[0] === sx`, `F[1] > sy` | `L[0] === tx`, `L[1] > ty` |
180| `left` | `F[1] === sy`, `F[0] < sx` | `L[1] === ty`, `L[0] < tx` |
181| `right` | `F[1] === sy`, `F[0] > sx` | `L[1] === ty`, `L[0] > tx` |
183For example, given a bottom departure anchor `S = [180, 160]` and a left
184arrival anchor `T = [360, 260]`, this relationship fragment leaves downward
185and enters the target rightward:
187```json
189 "from": "source",
190 "to": "target",
191 "fromSide": "bottom",
192 "toSide": "left",
193 "via": [[180, 200], [300, 200], [300, 260]]
195```
197The full path is `[180, 160] → [180, 200] → [300, 200] → [300, 260] → [360, 260]`.
198Changing only the first waypoint to `[200, 200]` makes the departure diagonal;
199changing it to `[180, 120]` keeps its x aligned but leaves upward through the
200source instead of outward from its bottom. Both violate `fromSide: "bottom"`
201and produce `clean-flow/endpoint-side-direction`. The example establishes
202endpoint direction only: keep the full route clear of unrelated nodes and
203apply the other geometry rules above.

Explicit via coordinates

Use the resolved departure anchor S = [sx, sy] and arrival anchor T = [tx, ty]. Anchors start at side midpoints, but automatic routing and Port Spread can move them as described above; do not assume an anchor copied from an automatic route is the anchor of a newly authored explicit route. Explicit via routes do not receive automatic Port Spread.

For the first waypoint F = via[0] and last waypoint L = via[via.length - 1], use these alignments and directions (SVG y increases downward):

SideDeparture (fromSide): S → FArrival (toSide): L → T
topF[0] === sx, F[1] < syL[0] === tx, L[1] < ty
bottomF[0] === sx, F[1] > syL[0] === tx, L[1] > ty
leftF[1] === sy, F[0] < sxL[1] === ty, L[0] < tx
rightF[1] === sy, F[0] > sxL[1] === ty, L[0] > tx

For example, given a bottom departure anchor S = [180, 160] and a left arrival anchor T = [360, 260], this relationship fragment leaves downward and enters the target rightward:

{
  "from": "source",
  "to": "target",
  "fromSide": "bottom",
  "toSide": "left",
  "via": [[180, 200], [300, 200], [300, 260]]
}

The full path is [180, 160] → [180, 200] → [300, 200] → [300, 260] → [360, 260]. Changing only the first waypoint to [200, 200] makes the departure diagonal; changing it to [180, 120] keeps its x aligned but leaves upward through the source instead of outward from its bottom. Both violate fromSide: "bottom" and produce clean-flow/endpoint-side-direction. The example establishes endpoint direction only: keep the full route clear of unrelated nodes and apply the other geometry rules above.

Raw lines 165 to 204
205### Spacing and labels
207In showcase Architecture, an unpinned connection label keeps its default position
208when clear. If it collides, the renderer tries a bounded set of nearby positions
209along the existing route, avoiding nodes, boundary titles, other labels and
210other routes within the resolved canvas. Explicit `labelAt`, `labelDx`, `labelDy`
211or `labelSegment` (including zero) disables this fallback. Routes and topology
212stay unchanged; if no nearby position is clear, validation reports the original
213collision. Inspect resolved labels with `--layout-json` before adding controls.
214Standard placement retains its existing behavior.
216Spacing recommendations mean clear gap between boxes, not center distance. A 200px center distance between 165px-wide nodes leaves only 35px of clear gap.
218For a relationship label, require:
220```text
221clear gap > label mask width + 8px breathing room
222label mask width ≈ 6.5px × ASCII units + 13px
223CJK characters count as two units
224```
226Relationship labels are semantic data. If the gap is too small, move the label,
227adjust the route or spacing, then shorten the wording while preserving meaning.
228Omit only wording already fully implied by both endpoints and carrying no
229protocol, action, direction, synchronous/asynchronous behavior, or
230cross-boundary mechanism. Preserve every meaningful label.
231Deleting it is not a spacing repair. If a relationship starts unlabeled because
232its endpoints fully imply it, explain why the wording is redundant; this is a
233semantic authoring choice, not a spacing repair. In workflow v2, let the compiler
234allocate its measured mask before applying a diagnosed `labelAt`,
235`labelDx`/`labelDy`, or `labelSegment`. Apply one diagnosed geometry control at
236a time unless several edges share a constrained channel. In that case, plan the smallest coupled change from measured geometry and
237validate it together. Architecture/workflow provide layout evidence through
238`validate <type> <candidate.json> --layout-json`; for other types, use validation
239diagnostics and the rendered SVG geometry.
240Before adding manual routes, check whether unnecessary agent-added controls
241disable automatic port spread; preserve user-required route intent. Use the
242measured clearance rules above rather than guessing coordinates.

Spacing and labels

In showcase Architecture, an unpinned connection label keeps its default position when clear. If it collides, the renderer tries a bounded set of nearby positions along the existing route, avoiding nodes, boundary titles, other labels and other routes within the resolved canvas. Explicit labelAt, labelDx, labelDy or labelSegment (including zero) disables this fallback. Routes and topology stay unchanged; if no nearby position is clear, validation reports the original collision. Inspect resolved labels with --layout-json before adding controls. Standard placement retains its existing behavior.

Spacing recommendations mean clear gap between boxes, not center distance. A 200px center distance between 165px-wide nodes leaves only 35px of clear gap.

For a relationship label, require:

clear gap > label mask width + 8px breathing room
label mask width ≈ 6.5px × ASCII units + 13px
CJK characters count as two units

Relationship labels are semantic data. If the gap is too small, move the label, adjust the route or spacing, then shorten the wording while preserving meaning. Omit only wording already fully implied by both endpoints and carrying no protocol, action, direction, synchronous/asynchronous behavior, or cross-boundary mechanism. Preserve every meaningful label. Deleting it is not a spacing repair. If a relationship starts unlabeled because its endpoints fully imply it, explain why the wording is redundant; this is a semantic authoring choice, not a spacing repair. In workflow v2, let the compiler allocate its measured mask before applying a diagnosed labelAt, labelDx/labelDy, or labelSegment. Apply one diagnosed geometry control at a time unless several edges share a constrained channel. In that case, plan the smallest coupled change from measured geometry and validate it together. Architecture/workflow provide layout evidence through validate <type> <candidate.json> --layout-json; for other types, use validation diagnostics and the rendered SVG geometry. Before adding manual routes, check whether unnecessary agent-added controls disable automatic port spread; preserve user-required route intent. Use the measured clearance rules above rather than guessing coordinates.

Raw lines 205 to 243
244### Repair evidence
246For architecture, `validate architecture <input.json> --layout-json` exposes the
247resolved component boxes, boundary frames, connection points, and label positions.
248A measurable rejected layout also returns these fields, with `ok: false`,
249`contract: "archify-architecture-layout-v1"`, diagnostics, and exit 1. This is
250repair evidence, not artifact acceptance; it writes no HTML. Malformed input or
251an implementation failure retains the ordinary failure receipt without layout.
253Use the measured failing side for `layout/boundary-out-of-bounds`. Left/top
254negative coordinates need an inward move; increasing viewBox width/height only
255addresses right/bottom overflow. Boundaries may wrap members across rows. Keep
256real membership intact and recheck connected routes after moving members.
258Automatic architecture canvases include route points as well as nodes, frames,
259and labels. An authored viewBox remains authoritative. In showcase,
260`layout/route-out-of-bounds` identifies clipped route points; negative coordinates
261need an inward route, while right/bottom overflow can also use a larger authored
262canvas. Recheck desktop readability after enlarging a canvas.
264When several crossing/corridor diagnoses involve the same nodes, consider their
265placement together before adding route controls. Apply one coherent repair and
266validate it; independent label nudges cannot fix a shared layout bottleneck.
267Compare diagnostics by code, subject, and stage instead of total count alone.

Repair evidence

For architecture, validate architecture <input.json> --layout-json exposes the resolved component boxes, boundary frames, connection points, and label positions. A measurable rejected layout also returns these fields, with ok: false, contract: "archify-architecture-layout-v1", diagnostics, and exit 1. This is repair evidence, not artifact acceptance; it writes no HTML. Malformed input or an implementation failure retains the ordinary failure receipt without layout.

Use the measured failing side for layout/boundary-out-of-bounds. Left/top negative coordinates need an inward move; increasing viewBox width/height only addresses right/bottom overflow. Boundaries may wrap members across rows. Keep real membership intact and recheck connected routes after moving members.

Automatic architecture canvases include route points as well as nodes, frames, and labels. An authored viewBox remains authoritative. In showcase, layout/route-out-of-bounds identifies clipped route points; negative coordinates need an inward route, while right/bottom overflow can also use a larger authored canvas. Recheck desktop readability after enlarging a canvas.

When several crossing/corridor diagnoses involve the same nodes, consider their placement together before adding route controls. Apply one coherent repair and validate it; independent label nudges cannot fix a shared layout bottleneck. Compare diagnostics by code, subject, and stage instead of total count alone.

Raw lines 244 to 268
269### Repair order
2711. Fix missing/invalid `meta.quality_profile` and schema errors.
2722. Fix node overlap or out-of-range placement.
2733. Fix edge-through-node and endpoint-direction errors.
2744. Fix crossings, ambiguous corridors, border runs, excessive detours, and route rhythm.
2755. Fix label-to-node, label-to-label, then label-to-route clearance.
2766. Fix labels that leave the canvas: move the label with `labelAt`/`labelDx`/`labelDy`/`labelSegment`, or widen `meta.viewBox`. Suggested `labelDx`/`labelDy` values replace the authored field; they are not added to it.
278After every edit, rerun the complete `finalize` as the delivery contract requires; use `validate` only for focused diagnosis between finalize runs. Consume `diagnostics[]` by stable `code`, exact `subject`, measured `evidence`, and `supportedFixes`. If the diagnostic gives `labelAt`, use that point instead of estimating another offset.

Repair order

  1. Fix missing/invalid meta.quality_profile and schema errors.
  2. Fix node overlap or out-of-range placement.
  3. Fix edge-through-node and endpoint-direction errors.
  4. Fix crossings, ambiguous corridors, border runs, excessive detours, and route rhythm.
  5. Fix label-to-node, label-to-label, then label-to-route clearance.
  6. Fix labels that leave the canvas: move the label with labelAt/labelDx/labelDy/labelSegment, or widen meta.viewBox. Suggested labelDx/labelDy values replace the authored field; they are not added to it.

After every edit, rerun the complete finalize as the delivery contract requires; use validate only for focused diagnosis between finalize runs. Consume diagnostics[] by stable code, exact subject, measured evidence, and supportedFixes. If the diagnostic gives labelAt, use that point instead of estimating another offset.

Raw lines 269 to 279
280## Mode placement

Mode placement

Raw lines 280 to 281
282### Architecture
284Choose overview or mechanism detail using [Composition and meaning](authoring-defaults.md#composition-and-meaning). Use one obvious primary reading path, which may step across meaningful rows when the requested topology needs room. Keep the overview readable at its chosen abstraction; expand implementation details when they answer the reader's question. Group only real ownership, trust, process, or deployment boundaries. Boundaries do not replace relationships.
286Grid placement is preferred when the schema supports it. Free positions are appropriate for a bounded exception, not for prose-level coordinate planning. Keep external actors outside the system boundary when that is factually true. A boundary is drawn as the padded box around all of its members, so place them as one compact axis-aligned cluster (no empty bay that lets a non-member sit inside the padded frame) and keep every non-member outside that box; `layout/boundary-encloses-non-member` reports a component the box swallows in showcase quality. Standard retains its existing acceptance behavior.

Architecture

Choose overview or mechanism detail using Composition and meaning. Use one obvious primary reading path, which may step across meaningful rows when the requested topology needs room. Keep the overview readable at its chosen abstraction; expand implementation details when they answer the reader's question. Group only real ownership, trust, process, or deployment boundaries. Boundaries do not replace relationships.

Grid placement is preferred when the schema supports it. Free positions are appropriate for a bounded exception, not for prose-level coordinate planning. Keep external actors outside the system boundary when that is factually true. A boundary is drawn as the padded box around all of its members, so place them as one compact axis-aligned cluster (no empty bay that lets a non-member sit inside the padded frame) and keep every non-member outside that box; layout/boundary-encloses-non-member reports a component the box swallows in showcase quality. Standard retains its existing acceptance behavior.

Raw lines 282 to 287
288### Workflow
290Lanes express responsibility or phase. Columns `0..5` express logical
291progression. Start new workflows on `readable-v2`; retain `fixed-v1` only for
292legacy geometry compatibility. In readable-v2, nodes may share a `(lane, col)`
293when distinct `yOffset` values leave room for their full rectangles, labels and
294connecting routes. Keep the happy path monotonic, preserve semantic edge labels,
295and route retries and exception returns outside the main lane corridor.
297#### Workflow viewport repair
299When `viewer/viewport-overflow` includes `workflowLanes`, inspect the tallest
300rendered frames and their node span before changing the source. Measurements
301are CSS pixels; space above/below nodes includes lane titles and routing, so it
302is not a removable-space budget. Frame IDs identify rendered lane indices.
304Run `validate workflow <source.json> --layout-json` and match those frames to
305source lanes and nodes. Check whether many steps share the last logical column
306and use large `yOffset` values. Readable-v2 reserves symmetric space around
307offsets. With a same-column vertical stack, an implicit canvas and no absolute
308route or label pins, it measures each lane independently. Other cases retain
309shared-height geometry for compatibility; see the canonical
310[lane measurement contract](../renderers/workflow/README.md#readable-v2).
312Where the source's ownership and explicit geometry permit, redistribute steps
313across logical columns and meaningful lanes, keeping the main path monotonic.
314Preserve every required node, relationship, label and semantic check. If ownership
315or absolute pins prevent reflow, report that constraint instead of merging lanes
316or moving pins automatically. Validate the changed JSON, deliver a fresh HTML,
317then rerun browser checks and inspect the first screen; a static pass alone does
318not settle viewport fit. These are repair directions, not guaranteed coordinates.

Workflow

Lanes express responsibility or phase. Columns 0..5 express logical progression. Start new workflows on readable-v2; retain fixed-v1 only for legacy geometry compatibility. In readable-v2, nodes may share a (lane, col) when distinct yOffset values leave room for their full rectangles, labels and connecting routes. Keep the happy path monotonic, preserve semantic edge labels, and route retries and exception returns outside the main lane corridor.

Workflow viewport repair

When viewer/viewport-overflow includes workflowLanes, inspect the tallest rendered frames and their node span before changing the source. Measurements are CSS pixels; space above/below nodes includes lane titles and routing, so it is not a removable-space budget. Frame IDs identify rendered lane indices.

Run validate workflow <source.json> --layout-json and match those frames to source lanes and nodes. Check whether many steps share the last logical column and use large yOffset values. Readable-v2 reserves symmetric space around offsets. With a same-column vertical stack, an implicit canvas and no absolute route or label pins, it measures each lane independently. Other cases retain shared-height geometry for compatibility; see the canonical lane measurement contract.

Where the source's ownership and explicit geometry permit, redistribute steps across logical columns and meaningful lanes, keeping the main path monotonic. Preserve every required node, relationship, label and semantic check. If ownership or absolute pins prevent reflow, report that constraint instead of merging lanes or moving pins automatically. Validate the changed JSON, deliver a fresh HTML, then rerun browser checks and inspect the first screen; a static pass alone does not settle viewport fit. These are repair directions, not guaranteed coordinates.

Raw lines 288 to 319
320### Sequence
322Participants are ordered by conversation role. Messages own their vertical order. Use return/async/security variants for meaning, not decoration; sequence does not use Automatic Port Spread. A segment's `from`/`to` edges must stay at least 4px off every message arrow (`message.y`): an edge along an arrow leaves the reader unable to tell which phase the message belongs to, and showcase quality reports `sequence/segment-message-border-run`. Standard retains its existing acceptance behavior.
324First-draft shape: every message needs an `id`, `from`, `to`, `label`, and integer `y` at least 160. `from` and `to` must be different participants — self-messages are unsupported (`sequence/self-message-unsupported`). Optional phase bands are `segments[]` with only `from`, `to`, and `label` (no segment `id`). Inventing `pos`/`size` on participants fails schema validation — order alone places them. Omit `meta.viewBox` so automatic height fits the timeline + legend (minimum 327px readable band) instead of a fixed 760px floor, and so 2–3 participant spreads pack below the 920px default when labels fit (4+ stay at 920px; long labels still widen).

Sequence

Participants are ordered by conversation role. Messages own their vertical order. Use return/async/security variants for meaning, not decoration; sequence does not use Automatic Port Spread. A segment's from/to edges must stay at least 4px off every message arrow (message.y): an edge along an arrow leaves the reader unable to tell which phase the message belongs to, and showcase quality reports sequence/segment-message-border-run. Standard retains its existing acceptance behavior.

First-draft shape: every message needs an id, from, to, label, and integer y at least 160. from and to must be different participants — self-messages are unsupported (sequence/self-message-unsupported). Optional phase bands are segments[] with only from, to, and label (no segment id). Inventing pos/size on participants fails schema validation — order alone places them. Omit meta.viewBox so automatic height fits the timeline + legend (minimum 327px readable band) instead of a fixed 760px floor, and so 2–3 participant spreads pack below the 920px default when labels fit (4+ stay at 920px; long labels still widen).

Raw lines 320 to 325
326### Dataflow
328Stages express transformation or custody. Rows separate parallel streams. Give every flow a concise data-contract or movement label; choose its visual convention using the required [Dataflow authoring defaults](authoring-defaults.md#presentation-and-modes).
330First-draft shape: declare `stages[]` (each a `{label}`), then place every node with integer `stage` and `row` — not `pos`/`size`. Flows are `flows[]` with required `from`, `to`, and non-empty `label`.
332Omit `meta.viewBox` to fit canvas width to all stages and nodes in either quality
333profile, including explicit node widths. Width uses content + 24px right padding
334with a 480px minimum (five default stages still need ~1068); left-edge overflow
335still needs a node repair. An authored viewBox stays fixed. Showcase node growth, readable typography, content height, port bridges
336and bounded label placement apply only when the canvas and all node widths are
337omitted. `--quality` overrides `meta.quality_profile`; without it, the renderer
338uses `ARCHIFY_QUALITY_PROFILE` when set, otherwise the JSON profile.

Dataflow

Stages express transformation or custody. Rows separate parallel streams. Give every flow a concise data-contract or movement label; choose its visual convention using the required Dataflow authoring defaults.

First-draft shape: declare stages[] (each a {label}), then place every node with integer stage and row — not pos/size. Flows are flows[] with required from, to, and non-empty label.

Omit meta.viewBox to fit canvas width to all stages and nodes in either quality profile, including explicit node widths. Width uses content + 24px right padding with a 480px minimum (five default stages still need ~1068); left-edge overflow still needs a node repair. An authored viewBox stays fixed. Showcase node growth, readable typography, content height, port bridges and bounded label placement apply only when the canvas and all node widths are omitted. --quality overrides meta.quality_profile; without it, the renderer uses ARCHIFY_QUALITY_PROFILE when set, otherwise the JSON profile.

Raw lines 326 to 339
340### ERD
342Treat a schema ERD as a table catalogue as well as a relationship map. If the
343schema has more than 8 tables or 50 fields, it is a dense full-schema map: start
344with `meta.quality_profile: "standard"` so complete field visibility is not
345traded away for repeated showcase route repairs. Include every physical column in
346`attributes`, keep the source SQL type, and preserve the
347field's Chinese comment in the type string as `SQL_TYPE|中文备注` because the
348schema has no separate comment property. The default viewer shows these rows at
349the `read` level; do not rely on hover/focus or cards to reveal physical fields.
350Cards may explain constraints, inferred relationships, or domain rules, but never
351replace a field list.
353Before placing boxes, classify tables into functional domains using verified table
354names, comments, module paths, and foreign-key meaning. Put the domain name in
355`tag` and give each domain one solid rectangle of grid cells: a single row, a
356single column, or a filled rectangular block — not an L or a diagonal — with
357every cell of the rectangle holding one of its tables.
358All tables with the same tag stay together; do not interleave unrelated tables
359between members. A domain that fills one such rectangle is drawn as a labelled
360band behind its tables, so the
361grouping is visible without reading every box;
362a tag whose tables do not fill one rectangle earns no band, and because the band
363is the only place a domain name is drawn the renderer reports
364`erd/domain-not-drawn` rather than publishing a diagram that cannot name the
365domain. Absolute
366coordinates have no grid cells at all, so a tagged table always needs `row`/`col`.
367Order parent/core tables
368toward the shared boundary and
369place their direct children beside or beneath them. Columns read left to right and
370rows read top to bottom, so a relationship between neighbouring columns is one
371straight corridor. Never place an unrelated entity between two aligned anchors;
372the router can detour around it, but the clear corridor is shorter and reads
373better. A junction table has two parents: put it in a cell beside or between
374both, not beneath one of them with tables stacked between it and the other,
375and keep every table that shares its domain tag in one solid rectangle of cells. A relationship must connect two different entities; a self-reference such as manager_id stays as a foreign-key column (and optionally a card), not an `employees`→`employees` edge.
376Use `row`/`col` for this normal grouped layout, and only use explicit
377`pos`/`via` after a diagnostic identifies a concrete geometry problem.
379State every key role a column carries in `key`, and the real references in
380`references`. A column is often more than one at once — a junction table's
381`tenant_id` is both part of the primary key and a foreign key to `tenant.id` — so
382`key` takes a role or a list (`["pk", "fk"]`), and dropping a role claims less
383than the schema does.
384A many-to-many pair is a real fact about the model, so state it and identify the join
385table when one exists. A relationship has two ends and each one declares its own
386maximum, so a `many`-to-`one` foreign key needs both `fromCardinality` and
387`toCardinality`; `fromOptional`/`toOptional` only lowers that same end's minimum
388from one to zero (an optional `one` end reads zero-or-one, an optional `many` end
389reads zero-or-many), and `identifying: false` draws the non-identifying dashed
390line and owns that dash — the schema rejects a `variant` that would draw the
391relationship solid beside it. The foot opens toward the entity it describes, so the
392drawn glyph states the end's maximum, not a direction of travel.
394A table side only has room for so many ends: the ports spread at most `(side
395extent - 32) / (ends - 1)` apart, and a cardinality glyph is 14 units tall. More
396relationship ends than that room allows is `layout/marker-capacity`, which names
397the table, the side, and the ways out — add fields so the side is taller, spread
398the relationships, or reduce the fan-in. Never fix it by hiding the cardinality.
400Give the domains a column each when the model has to read completely inside a
401laptop viewport. The Reader scales a wide canvas up to the reader width and
402narrows a tall one until the whole page fits, so the same eleven tables cost text
403size when they are stacked as four horizontal domains instead of four vertical
404ones. Leave a whole tile of the grid free where a long relationship has to cross
405a domain: a run that crosses an occupied cell forces a jog, and a jog below the
406interior floor is a `composition/short-interior-segment` failure.
408An automatic canvas is sized so the default legend is always in the generated
409content, and a canvas the author fixes with `meta.viewBox` reports its legend
410capacity instead of dropping the key; see
411[`../renderers/erd/README.md`](../renderers/erd/README.md) for the band, port, and
412reader contract.

ERD

Treat a schema ERD as a table catalogue as well as a relationship map. If the schema has more than 8 tables or 50 fields, it is a dense full-schema map: start with meta.quality_profile: "standard" so complete field visibility is not traded away for repeated showcase route repairs. Include every physical column in attributes, keep the source SQL type, and preserve the field's Chinese comment in the type string as SQL_TYPE|中文备注 because the schema has no separate comment property. The default viewer shows these rows at the read level; do not rely on hover/focus or cards to reveal physical fields. Cards may explain constraints, inferred relationships, or domain rules, but never replace a field list.

Before placing boxes, classify tables into functional domains using verified table names, comments, module paths, and foreign-key meaning. Put the domain name in tag and give each domain one solid rectangle of grid cells: a single row, a single column, or a filled rectangular block — not an L or a diagonal — with every cell of the rectangle holding one of its tables. All tables with the same tag stay together; do not interleave unrelated tables between members. A domain that fills one such rectangle is drawn as a labelled band behind its tables, so the grouping is visible without reading every box; a tag whose tables do not fill one rectangle earns no band, and because the band is the only place a domain name is drawn the renderer reports erd/domain-not-drawn rather than publishing a diagram that cannot name the domain. Absolute coordinates have no grid cells at all, so a tagged table always needs row/col. Order parent/core tables toward the shared boundary and place their direct children beside or beneath them. Columns read left to right and rows read top to bottom, so a relationship between neighbouring columns is one straight corridor. Never place an unrelated entity between two aligned anchors; the router can detour around it, but the clear corridor is shorter and reads better. A junction table has two parents: put it in a cell beside or between both, not beneath one of them with tables stacked between it and the other, and keep every table that shares its domain tag in one solid rectangle of cells. A relationship must connect two different entities; a self-reference such as manager_id stays as a foreign-key column (and optionally a card), not an employees→employees edge. Use row/col for this normal grouped layout, and only use explicit pos/via after a diagnostic identifies a concrete geometry problem.

State every key role a column carries in key, and the real references in references. A column is often more than one at once — a junction table's tenant_id is both part of the primary key and a foreign key to tenant.id — so key takes a role or a list (["pk", "fk"]), and dropping a role claims less than the schema does. A many-to-many pair is a real fact about the model, so state it and identify the join table when one exists. A relationship has two ends and each one declares its own maximum, so a many-to-one foreign key needs both fromCardinality and toCardinality; fromOptional/toOptional only lowers that same end's minimum from one to zero (an optional one end reads zero-or-one, an optional many end reads zero-or-many), and identifying: false draws the non-identifying dashed line and owns that dash — the schema rejects a variant that would draw the relationship solid beside it. The foot opens toward the entity it describes, so the drawn glyph states the end's maximum, not a direction of travel.

A table side only has room for so many ends: the ports spread at most (side extent - 32) / (ends - 1) apart, and a cardinality glyph is 14 units tall. More relationship ends than that room allows is layout/marker-capacity, which names the table, the side, and the ways out — add fields so the side is taller, spread the relationships, or reduce the fan-in. Never fix it by hiding the cardinality.

Give the domains a column each when the model has to read completely inside a laptop viewport. The Reader scales a wide canvas up to the reader width and narrows a tall one until the whole page fits, so the same eleven tables cost text size when they are stacked as four horizontal domains instead of four vertical ones. Leave a whole tile of the grid free where a long relationship has to cross a domain: a run that crosses an occupied cell forces a jog, and a jog below the interior floor is a composition/short-interior-segment failure.

An automatic canvas is sized so the default legend is always in the generated content, and a canvas the author fixes with meta.viewBox reports its legend capacity instead of dropping the key; see [../renderers/erd/README.md](../renderers/erd/README.md) for the band, port, and reader contract.

Raw lines 340 to 413
414### Tree
416A tree answers "how is this decomposed?": one root, and every other node names
417exactly one `parent`. Links mean containment only; do not use a tree for calls,
418data movement, or cross-branch dependencies. Nothing is inferred: a missing
419parent (`tree/missing-parent`), zero or several roots (`tree/root-count`), a
420parent cycle (`tree/cycle`, which also covers every node stranded beneath it),
421and duplicate ids are refused. Children read in declaration order.
423Use `layout.direction: "down"` (default) for a shallow, balanced breakdown and
424`"right"` for a deep or leaf-heavy tree such as a repository layout; a rightward
425tree aligns each parent with its first child and scrolls vertically in the
426reader. Labels wrap between words. `collapsed: true` makes a branch start
427collapsed in the Viewer; it never removes content from the artifact, and every
428export is the complete tree. For a real codebase, ground each node in the
429observed path and attach `sources`. See
430[`../renderers/tree/README.md`](../renderers/tree/README.md).

Tree

A tree answers "how is this decomposed?": one root, and every other node names exactly one parent. Links mean containment only; do not use a tree for calls, data movement, or cross-branch dependencies. Nothing is inferred: a missing parent (tree/missing-parent), zero or several roots (tree/root-count), a parent cycle (tree/cycle, which also covers every node stranded beneath it), and duplicate ids are refused. Children read in declaration order.

Use layout.direction: "down" (default) for a shallow, balanced breakdown and "right" for a deep or leaf-heavy tree such as a repository layout; a rightward tree aligns each parent with its first child and scrolls vertically in the reader. Labels wrap between words. collapsed: true makes a branch start collapsed in the Viewer; it never removes content from the artifact, and every export is the complete tree. For a real codebase, ground each node in the observed path and attach sources. See [../renderers/tree/README.md](../renderers/tree/README.md).

Raw lines 414 to 431
432### Class
434A class diagram explains the contracts inside one module: which types exist,
435which members matter to the explanation, and how the types relate. Choose the
436members the question needs; a type may show none. Each type states its `kind`
437(`class`, `abstract`, `interface`, `enum`, `record`); every kind except `class`
438draws its UML keyword, so an interface never reads as an empty class. For a real
439codebase, ground every type, member, and relationship in repository evidence and
440attach `sources`.
442Every relationship reads `from` -> `to` and its `kind` owns the notation:
443`dependency` (`from` uses `to`, dashed open arrow), `association` (`from` holds a
444`to`, solid open arrow), `inheritance` (`from` extends `to`, solid hollow
445triangle), `realization` (`from` implements interface `to`, dashed hollow
446triangle), `composition` and `aggregation` (`from` is the whole, filled or
447hollow diamond at `from`). Realization must target an interface from a
448non-interface; inheritance must not cross the interface boundary; an
449inheritance cycle is rejected.
451Place types on the `row`/`col` grid with supertypes above their subtypes. Two
452or more automatic generalizations into one supertype draw as one hierarchy bus
453with a single triangle. Members never truncate: a type grows to its widest
454member up to `layout.typeMaxW` and longer members wrap at parameter boundaries.
455See [`../renderers/class/README.md`](../renderers/class/README.md).
457First-draft shape: types use `row`/`col` (not `pos`/`size`), fields go in
458`attributes[]`, operations in `methods[]` with `parameters` as one string such as
459`"msg: Message"` (not an array of objects). Enum members are attributes with only
460`name`. Relationships need an `id`, `from`, `to`, and `kind`.

Class

A class diagram explains the contracts inside one module: which types exist, which members matter to the explanation, and how the types relate. Choose the members the question needs; a type may show none. Each type states its kind (class, abstract, interface, enum, record); every kind except class draws its UML keyword, so an interface never reads as an empty class. For a real codebase, ground every type, member, and relationship in repository evidence and attach sources.

Every relationship reads from -> to and its kind owns the notation: dependency (from uses to, dashed open arrow), association (from holds a to, solid open arrow), inheritance (from extends to, solid hollow triangle), realization (from implements interface to, dashed hollow triangle), composition and aggregation (from is the whole, filled or hollow diamond at from). Realization must target an interface from a non-interface; inheritance must not cross the interface boundary; an inheritance cycle is rejected.

Place types on the row/col grid with supertypes above their subtypes. Two or more automatic generalizations into one supertype draw as one hierarchy bus with a single triangle. Members never truncate: a type grows to its widest member up to layout.typeMaxW and longer members wrap at parameter boundaries. See [../renderers/class/README.md](../renderers/class/README.md).

First-draft shape: types use row/col (not pos/size), fields go in attributes[], operations in methods[] with parameters as one string such as "msg: Message" (not an array of objects). Enum members are attributes with only name. Relationships need an id, from, to, and kind.

Raw lines 432 to 461
462### Timeline
464A timeline answers "when did what happen, and how far apart?". Every event
465needs an ISO 8601 `at` with an explicit `Z` or `±HH:MM` offset; a timestamp
466without one is rejected rather than guessed. `meta.timezone` (IANA, default
467`UTC`) is the display clock for ticks and labels, and the axis caption states
468it. `meta.evidence` is required: `observed` for recorded events (logs, git
469history, pager records) and `illustrative` for explanatory input. Never mark
470invented or approximate times as observed, and never fill gaps with events the
471input does not contain.
473Use `lanes` for sources or categories (release, monitoring, response); every
474event then names one. Each event's visible text is `title` (not `label`). `kind`
475(`default`, `change`, `alert`, `action`, `recovery`) only
476colours the card; inventing values such as `milestone` fails schema validation. Events are drawn in time order whatever the authored order;
477simultaneous and close events stack. By default a quiet period longer than 8×
478the median gap and 10% of the span is drawn as a fixed-width break labelled
479with the omitted duration; `layout.breaks: "none"` keeps one proportional axis.
480See [`../renderers/timeline/README.md`](../renderers/timeline/README.md).

Timeline

A timeline answers "when did what happen, and how far apart?". Every event needs an ISO 8601 at with an explicit Z or ±HH:MM offset; a timestamp without one is rejected rather than guessed. meta.timezone (IANA, default UTC) is the display clock for ticks and labels, and the axis caption states it. meta.evidence is required: observed for recorded events (logs, git history, pager records) and illustrative for explanatory input. Never mark invented or approximate times as observed, and never fill gaps with events the input does not contain.

Use lanes for sources or categories (release, monitoring, response); every event then names one. Each event's visible text is title (not label). kind (default, change, alert, action, recovery) only colours the card; inventing values such as milestone fails schema validation. Events are drawn in time order whatever the authored order; simultaneous and close events stack. By default a quiet period longer than 8× the median gap and 10% of the span is drawn as a fixed-width break labelled with the omitted duration; layout.breaks: "none" keeps one proportional axis. See [../renderers/timeline/README.md](../renderers/timeline/README.md).

Raw lines 462 to 481
482### Waterfall
484A waterfall answers "where did the time go?" for one request, job, or agent
485run. Each span has `id`, `name`, `start`, and `end` or `duration` in
486`meta.unit` (`us`, `ms`, `s`; default `ms`), plus optional `parent`, `status`,
487`service`, and `detail`. Bar position and length come only from these numbers.
488`meta.evidence` is required: `measured` only for recorded timing (trace export,
489logs, profiler, timed run); otherwise `illustrative`. Never infer a duration
490from code structure, and never present estimated numbers as measured.
492A span with no recorded end must be `status: "incomplete"`; it is drawn as an
493open lower bound to the last recorded instant. Contradictory end/duration,
494an end before the start, a missing parent, or a parent cycle is refused.
495Percentages are always "of the wall-clock total"; parent and child durations
496are inclusive and never summed. Do not mark a critical path or waiting time
497unless the input states the dependency. See
498[`../renderers/waterfall/README.md`](../renderers/waterfall/README.md).

Waterfall

A waterfall answers "where did the time go?" for one request, job, or agent run. Each span has id, name, start, and end or duration in meta.unit (us, ms, s; default ms), plus optional parent, status, service, and detail. Bar position and length come only from these numbers. meta.evidence is required: measured only for recorded timing (trace export, logs, profiler, timed run); otherwise illustrative. Never infer a duration from code structure, and never present estimated numbers as measured.

A span with no recorded end must be status: "incomplete"; it is drawn as an open lower bound to the last recorded instant. Contradictory end/duration, an end before the start, a missing parent, or a parent cycle is refused. Percentages are always "of the wall-clock total"; parent and child durations are inclusive and never summed. Do not mark a critical path or waiting time unless the input states the dependency. See [../renderers/waterfall/README.md](../renderers/waterfall/README.md).

Raw lines 482 to 499
500### Lifecycle
502Schema v3 is the only lifecycle contract. `mainPath` lists the happy path from
503the initial state; every consecutive pair needs a transition. The renderer owns
504all geometry: the main path is one row, transitions back to an earlier phase
505(or skipping ahead) become arcs above it, and every other state sits below the
506state it branches from, one row deeper per step away from the main path.
507Equivalent exits share one arrow only when their labels, notes and variants
508match and exactly one group from at least two main-path phases qualifies for
509that target. Consecutive phases receive a composite frame; non-consecutive
510phases share a bus. Other exits stay separate, including distinct notes on
511their original transitions. Do not remove or copy notes to force sharing.
512There are no lanes, columns, sizes, or routing controls. Layout retries retain
513the transition text's minimum gap and stop widening the main-path row when its
514minimum width would exceed the original text readability budget; the final
515width and label-clearance checks still apply.
517Keep the main path to the phases a reader follows, at most about six states,
518and keep transition labels short. A recoverable failure needs a real transition
519back to an active state; a card saying “retry” is not topology.
521Pick each state's `type` by what the reader should notice, since color and the
522corner sigil follow it: `waiting` for a pause on a person, time, or input;
523`decision` for a review or branch point; `success` or `failure` for a good or
524bad outcome; `neutral` for parked states and outcomes that are neither;
525`external` when an outside party holds the work; `active` otherwise. From a
526Mermaid `stateDiagram`, `[*] -->` names the first `mainPath` state, a state
527with `--> [*]` is an outcome (`success`, `failure`, or `neutral`; it is drawn as
528final when it has no other outgoing transition), and `<<choice>>` becomes a
529`decision` state. Flatten a composite `state X { ... }` into its inner states
530and repeat its exit from each of them with one label; consecutive main-path
531phases sharing that exit are framed automatically. Concurrent regions (`--`)
532have no lifecycle form: draw one lifecycle per region.

Lifecycle

Schema v3 is the only lifecycle contract. mainPath lists the happy path from the initial state; every consecutive pair needs a transition. The renderer owns all geometry: the main path is one row, transitions back to an earlier phase (or skipping ahead) become arcs above it, and every other state sits below the state it branches from, one row deeper per step away from the main path. Equivalent exits share one arrow only when their labels, notes and variants match and exactly one group from at least two main-path phases qualifies for that target. Consecutive phases receive a composite frame; non-consecutive phases share a bus. Other exits stay separate, including distinct notes on their original transitions. Do not remove or copy notes to force sharing. There are no lanes, columns, sizes, or routing controls. Layout retries retain the transition text's minimum gap and stop widening the main-path row when its minimum width would exceed the original text readability budget; the final width and label-clearance checks still apply.

Keep the main path to the phases a reader follows, at most about six states, and keep transition labels short. A recoverable failure needs a real transition back to an active state; a card saying “retry” is not topology.

Pick each state's type by what the reader should notice, since color and the corner sigil follow it: waiting for a pause on a person, time, or input; decision for a review or branch point; success or failure for a good or bad outcome; neutral for parked states and outcomes that are neither; external when an outside party holds the work; active otherwise. From a Mermaid stateDiagram, [*] --> names the first mainPath state, a state with --> [*] is an outcome (success, failure, or neutral; it is drawn as final when it has no other outgoing transition), and <<choice>> becomes a decision state. Flatten a composite state X { ... } into its inner states and repeat its exit from each of them with one label; consecutive main-path phases sharing that exit are framed automatically. Concurrent regions (--) have no lifecycle form: draw one lifecycle per region.

Raw lines 500 to 533
534## Repository evidence
536When the diagram must reflect real code, inspect repository entrypoints,
537runtime boundaries, storage, transports, and deployment configuration before
538authoring. Record only evidence you actually verified. `--repo-root <path>` is
539accepted by `render`, `validate`, `deliver`, and `preview` for every diagram
540type, by architecture `compare`, and by workflow `migrate`; every mode verifies `meta.repository` and
541node `sources` the same way. Migrating a source-backed workflow requires the same
542`--repo-root` so its candidate is verified before replacing the destination.
543Never infer runtime causality from file proximity
544or naming alone.
546Declare `meta.repository.url` and one full 40-character `revision`, then attach
547`sources` to the mode's node collection (Architecture `components[]`, Workflow
548and Data Flow `nodes[]`, Sequence `participants[]`, Lifecycle `states[]`) with
549repository-relative `path`, optional `line`, `end_line`, and `label`.
550Verification reads blobs at that commit, independently of working-tree edits.
551Verification ignores local Git replacement refs, including those selected by
552`GIT_REPLACE_REF_BASE`, and always reads the original objects at the pinned SHA.
553It does not change repository configuration or delete replacement refs.
554A matching local origin, available commit, bounded path,
555blob, and valid line range are required in every link mode. Verification is
556local and makes no remote requests; it establishes neither public availability
557nor the current reader's access rights.
559`link_mode` defaults to `web`. GitHub, Gitee, and GitLab HTTPS repository URLs
560generate revision-pinned links; the public hosts github.com, gitee.com, and
561gitlab.com select the provider automatically. Optional `provider: "github"`,
562`"gitee"`, or `"gitlab"` must agree with a public host. A self-managed GitLab
563host declares `provider: "gitlab"`; Archify never contacts the host to detect
564its forge. Existing GitHub declarations and default delivery receipt fields
565remain compatible.
567GitLab URLs may name nested groups (`group/subgroup/project`). Source links use
568`<url>/-/blob/<revision>/<path>#L<line>-<end_line>` and the repository link
569uses `<url>/-/tree/<revision>`. A Markdown source (`.md`, `.markdown`) with a
570line range links to the plain view (`?plain=1`) so the cited lines are
571highlighted instead of the rendered document. GitLab repository paths compare
572case-insensitively, like GitHub.
574```json
576 "url": "https://gitee.com/team/service",
577 "revision": "0123456789abcdef0123456789abcdef01234567",
578 "provider": "gitee"
580```
582```json
584 "url": "https://git.example.com/platform/payments/service",
585 "revision": "0123456789abcdef0123456789abcdef01234567",
586 "provider": "gitlab"
588```
590For an internal or unsupported forge, select `link_mode: "local-only"`. The
591Viewer retains SRC markers, searchable file paths, line ranges, and revision
592labels without repository or source hyperlinks. The evidence receipt adds
593`linkMode: "local-only"`. `url` remains required as the expected origin identity;
594local-only disables links, not identity verification. A repository without an
595origin is not supported.
597```json
599 "url": "http://git.internal:3000/Platform/Services/service",
600 "revision": "0123456789abcdef0123456789abcdef01234567",
601 "link_mode": "local-only"
603```
605Local-only accepts HTTP(S), `git@host:path`, and `ssh://git@host[:port]/path`
606addresses, including nested namespaces. Declare a credential-free address;
607HTTP(S) credentials on the checkout's origin are ignored for identity and
608redacted from diagnostics. Hostnames compare case-insensitively; repository
609paths retain case except for GitHub and GitLab. A trailing slash
610normalizes away. Only GitHub, Gitee, and GitLab (gitlab.com, or a host declared
611with `provider: "gitlab"`) normalize a terminal `.git` and match standard
612HTTPS/443 with Git SSH/22 on the same host; a GitLab SSH endpoint on another host
613or port is not inferred. For other hosts, use the actual clone address:
614transport, port, `.git` suffix, and remote-relative versus absolute paths must
615match. For example, `git@host:Team/repo` differs from
616`ssh://git@host/Team/repo`; `git@host:/Team/repo` matches the latter. SCP-style
617paths preserve literal percent escapes, while URI paths decode them. SSH host
618aliases and forge-specific browse/clone prefixes are not guessed.
619Gitea/Forgejo/Bitbucket web links are not implemented in this version;
620use local-only until a tested link provider is available. Unknown web providers
621fail with a diagnostic rather than emitting a guessed link.

Repository evidence

When the diagram must reflect real code, inspect repository entrypoints, runtime boundaries, storage, transports, and deployment configuration before authoring. Record only evidence you actually verified. --repo-root <path> is accepted by render, validate, deliver, and preview for every diagram type, by architecture compare, and by workflow migrate; every mode verifies meta.repository and node sources the same way. Migrating a source-backed workflow requires the same --repo-root so its candidate is verified before replacing the destination. Never infer runtime causality from file proximity or naming alone.

Declare meta.repository.url and one full 40-character revision, then attach sources to the mode's node collection (Architecture components[], Workflow and Data Flow nodes[], Sequence participants[], Lifecycle states[]) with repository-relative path, optional line, end_line, and label. Verification reads blobs at that commit, independently of working-tree edits. Verification ignores local Git replacement refs, including those selected by GIT_REPLACE_REF_BASE, and always reads the original objects at the pinned SHA. It does not change repository configuration or delete replacement refs. A matching local origin, available commit, bounded path, blob, and valid line range are required in every link mode. Verification is local and makes no remote requests; it establishes neither public availability nor the current reader's access rights.

link_mode defaults to web. GitHub, Gitee, and GitLab HTTPS repository URLs generate revision-pinned links; the public hosts github.com, gitee.com, and gitlab.com select the provider automatically. Optional provider: "github", "gitee", or "gitlab" must agree with a public host. A self-managed GitLab host declares provider: "gitlab"; Archify never contacts the host to detect its forge. Existing GitHub declarations and default delivery receipt fields remain compatible.

GitLab URLs may name nested groups (group/subgroup/project). Source links use <url>/-/blob/<revision>/<path>#L<line>-<end_line> and the repository link uses <url>/-/tree/<revision>. A Markdown source (.md, .markdown) with a line range links to the plain view (?plain=1) so the cited lines are highlighted instead of the rendered document. GitLab repository paths compare case-insensitively, like GitHub.

{
  "url": "https://gitee.com/team/service",
  "revision": "0123456789abcdef0123456789abcdef01234567",
  "provider": "gitee"
}
{
  "url": "https://git.example.com/platform/payments/service",
  "revision": "0123456789abcdef0123456789abcdef01234567",
  "provider": "gitlab"
}

For an internal or unsupported forge, select link_mode: "local-only". The Viewer retains SRC markers, searchable file paths, line ranges, and revision labels without repository or source hyperlinks. The evidence receipt adds linkMode: "local-only". url remains required as the expected origin identity; local-only disables links, not identity verification. A repository without an origin is not supported.

{
  "url": "http://git.internal:3000/Platform/Services/service",
  "revision": "0123456789abcdef0123456789abcdef01234567",
  "link_mode": "local-only"
}

Local-only accepts HTTP(S), git@host:path, and ssh://git@host[:port]/path addresses, including nested namespaces. Declare a credential-free address; HTTP(S) credentials on the checkout's origin are ignored for identity and redacted from diagnostics. Hostnames compare case-insensitively; repository paths retain case except for GitHub and GitLab. A trailing slash normalizes away. Only GitHub, Gitee, and GitLab (gitlab.com, or a host declared with provider: "gitlab") normalize a terminal .git and match standard HTTPS/443 with Git SSH/22 on the same host; a GitLab SSH endpoint on another host or port is not inferred. For other hosts, use the actual clone address: transport, port, .git suffix, and remote-relative versus absolute paths must match. For example, git@host:Team/repo differs from ssh://git@host/Team/repo; git@host:/Team/repo matches the latter. SCP-style paths preserve literal percent escapes, while URI paths decode them. SSH host aliases and forge-specific browse/clone prefixes are not guessed. Gitea/Forgejo/Bitbucket web links are not implemented in this version; use local-only until a tested link provider is available. Unknown web providers fail with a diagnostic rather than emitting a guessed link.

Raw lines 534 to 622
623## Hand-placed fallback
625Use only when no renderer can run. Start from `assets/template.html`, keep semantic CSS classes, preserve the inline SVG/accessibility structure, and run the delivery visual checklist. Never introduce inline literal colors that break dark/light parity.

Hand-placed fallback

Use only when no renderer can run. Start from assets/template.html, keep semantic CSS classes, preserve the inline SVG/accessibility structure, and run the delivery visual checklist. Never introduce inline literal colors that break dark/light parity.

Raw lines 623 to 626
627## Node icons
629For domain-specific diagrams, set an optional `icon` on architecture components,
630workflow/dataflow nodes, sequence participants, or lifecycle states. Choose
631`calendar`, `clock`, `person`, `briefcase`, `flag`, or `moon` for everyday concepts;
632the complete catalog (including existing technical and lifecycle symbols) is
633`common.schema.json#/$defs/nodeIcon`. Use `icon: "none"` to hide the corner symbol.
634Omitting `icon` keeps the type-based default; lifecycle `start`, `active`, and
635`neutral` states default to no symbol. These inline SVG symbols are
636renderer-owned and export with the diagram; URLs and raw SVG are not accepted.
638Icon selection changes only the corner symbol. The node's type still determines
639color and semantic grouping; brand marks remain independent. For a holiday
640workflow, pair `type: "backend", icon: "calendar"` with
641`meta.legend.entries.backend.label: "假期"`, and use `icon: "briefcase"` plus
642an appropriate legend label for make-up work. Keep the node label meaningful:
643icons are decorative and are hidden from assistive technology.
645See [holiday planning](../examples/holiday-planning.workflow.json) for a complete workflow example.

Node icons

For domain-specific diagrams, set an optional icon on architecture components, workflow/dataflow nodes, sequence participants, or lifecycle states. Choose calendar, clock, person, briefcase, flag, or moon for everyday concepts; the complete catalog (including existing technical and lifecycle symbols) is common.schema.json#/$defs/nodeIcon. Use icon: "none" to hide the corner symbol. Omitting icon keeps the type-based default; lifecycle start, active, and neutral states default to no symbol. These inline SVG symbols are renderer-owned and export with the diagram; URLs and raw SVG are not accepted.

Icon selection changes only the corner symbol. The node's type still determines color and semantic grouping; brand marks remain independent. For a holiday workflow, pair type: "backend", icon: "calendar" with meta.legend.entries.backend.label: "假期", and use icon: "briefcase" plus an appropriate legend label for make-up work. Keep the node label meaningful: icons are decorative and are hidden from assistive technology.

See holiday planning for a complete workflow example.

Raw lines 627 to 645