boundry 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,7 +4,88 @@ All notable changes to Boundry are documented here.
4
4
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
5
5
  versioning follows [semver](https://semver.org/).
6
6
 
7
- ## [0.2.0] — unreleased
7
+ ## [0.4.0] — 2026-07-17
8
+
9
+ Makes the "one file can be both the communication diagram and the enforcement
10
+ model" promise hold for deep, nested C4 trees, and makes a proposed change
11
+ highlight itself — deterministically, on every LikeC4 surface.
12
+
13
+ ### Added
14
+
15
+ - **Accepted-state lock + `annotate` (prototype).** `approve` now records the
16
+ accepted model to `boundry.lock` beside the diagram — a baseline Boundry
17
+ **owns**, rather than inferring it from whatever git happens to hold, so
18
+ "approved" is decoupled from "committed". `boundry annotate` diffs the diagram
19
+ against that lock and rewrites every undeclared addition — a bare new edge (a
20
+ self-grant) or box — into an explicit `#proposed` proposal in the source,
21
+ deterministically and idempotently. Turns silent drift into a reviewable,
22
+ colourable proposal. (Additive only; deletions are reported, not round-tripped
23
+ back into the DSL — re-materialising a removed box would resurrect it as an
24
+ enforced module.)
25
+ - **Cross-surface highlighting** ([#5]). `annotate` also paints each marker with
26
+ an intrinsic `style { color amber }` (`#proposed`) / `style { color red }`
27
+ (`#proposal-delete`) on the element itself. Intrinsic style is the only styling
28
+ LikeC4 renders on **every** surface — base views *and* the "relationships of X"
29
+ panel — so a proposal is highlighted wherever a reviewer looks, not just inside
30
+ the diff views (whose view-scoped rules stop at the view boundary). Idempotent,
31
+ and `approve` strips the styling back out with the marker: a `#proposed` edge
32
+ returns to bare, a `#proposed` box loses its colour, a `#proposal-delete` is
33
+ removed outright. Needs LikeC4 ≥ 1.58 to render.
34
+ - **Per-layer diff views — `boundry diff` (prototype).** Generates a focused
35
+ LikeC4 view for every layer that holds a pending change — an edge or box tagged
36
+ `#proposed` or `#proposal-delete` — scoped to the tightest element that draws
37
+ it, into a derived `boundry.diff.likec4`. This matters because a proposal nested
38
+ inside a box is invisible once that box collapses at a wider zoom, so a single
39
+ all-up view hides it; one view per layer surfaces every change in the scope
40
+ where it renders. It reads the diagram's own markers, not the lock, so it frames
41
+ whatever `annotate` or a human marked. The file is derived — overwritten each
42
+ run, removed when nothing is proposed — so it always reflects the current
43
+ diagram; regenerate after `approve`.
44
+ - **Deterministic highlighting** ([#4]). `diff` emits the LikeC4 style rules
45
+ into the derived file, so a `#proposed` box fills amber and its edge goes
46
+ amber + solid, a `#proposal-delete` red — with no hand-styling. Boxes are
47
+ styled in place (`style element.tag`, never force-included), so a nested
48
+ proposal stays in its layer; the rules live only in the generated views, so
49
+ user-authored views are untouched. Rules are emitted only for marker tags
50
+ actually in use (referencing an undeclared tag is a hard LikeC4 error).
51
+ - **`#proposal-delete` — the deletion half of the approval lifecycle.** Tag an
52
+ edge or a box `#proposal-delete` to propose retiring it. The marker colours it
53
+ red; a pending deletion changes nothing (the edge stays allowed, the box stays
54
+ enforced), so proposing a removal never breaks the build. On `approve`, the
55
+ marked edge or box is removed from the diagram outright — deterministically, by
56
+ splicing the LikeC4 CST — where `#proposed` only strips its own marker and
57
+ leaves the edge behind. `#proposed` (amber, additive) and `#proposal-delete`
58
+ (red, subtractive) are now the two halves of every change.
59
+ - **File-level mapping — `metadata { file 'src/x.ts' }`** ([#3]). An element maps
60
+ to a single file instead of a folder. A file module owns exactly its file; a
61
+ folder module owns its subtree minus any mapped descendants — nested folders
62
+ and file leaves alike.
63
+ - This is what lets a deep nested diagram stay legal *and* enforceable. A
64
+ guarded contract or port that sits beside sibling sub-folders can be a file
65
+ leaf rather than collapsing into its parent folder. Collapsed, its edges
66
+ become ancestor↔descendant, which LikeC4 rejects with
67
+ `Invalid parent-child relationship`; as a leaf they stay sibling-to-sibling.
68
+ - Declaring both `folder` and `file` on one element is an error.
69
+
70
+ ### Changed
71
+
72
+ - **`Module.folder: string` → `Module.path: string` + `Module.kind: 'folder' | 'file'`.**
73
+ SDK-breaking; the diagram surface and CLI are unaffected.
74
+ - **`likec4` floor raised to `^1.58.0`.** The diff-view style-rule syntax Boundry
75
+ emits is verified against LikeC4 1.58; rendering the coloured views needs a
76
+ renderer at least that new.
77
+
78
+ [#3]: https://github.com/makspiechota/boundry/issues/3
79
+ [#4]: https://github.com/makspiechota/boundry/issues/4
80
+ [#5]: https://github.com/makspiechota/boundry/issues/5
81
+
82
+ ## [0.3.0] — 2026-07-16 — superseded by 0.4.0
83
+
84
+ Published early from a pre-feature snapshot (≈ 0.2.0 plus the `#anything`
85
+ wildcard) and never carried the work now listed under 0.4.0. npm versions are
86
+ immutable, so it stays on the registry; use **0.4.0**.
87
+
88
+ ## [0.2.0] — 2026-07-16
8
89
 
9
90
  The release that makes the diagram a *governed* artifact rather than just a
10
91
  source of rules. An agent can now be handed the diagram without being handed the
package/README.md CHANGED
@@ -18,8 +18,9 @@ diagram (LikeC4) ──► boundary model ──► dependency-cruiser rules
18
18
 
19
19
  ## How it works
20
20
 
21
- 1. You annotate each element in your diagram with the folder it owns:
22
- `metadata { folder 'src/domain' }`.
21
+ 1. You annotate each element in your diagram with the source it owns — a folder,
22
+ `metadata { folder 'src/domain' }`, or a single file,
23
+ `metadata { file 'src/ports/contract.ts' }`.
23
24
  2. Every relationship you draw (`a -> b`) is an **allowed** dependency.
24
25
  Anything you don't draw is **forbidden**.
25
26
  3. Boundry lifts the diagram into a source-agnostic boundary model, compiles a
@@ -51,6 +52,41 @@ A mapped folder claims its **entire subtree**, so the abstraction level stays
51
52
  yours: one box on `src/domain` covers everything beneath it. You add finer boxes
52
53
  where you want finer *rules*, not to satisfy the coverage check.
53
54
 
55
+ ### Mapping a single file (deep nested diagrams)
56
+
57
+ A rich C4 model nests: `application` › `metrics` › `ports` › `story-points-read`
58
+ › `stub`, with a drill-down view at each level. Sometimes a *file* is the guarded
59
+ thing — a contract, a port, a single repository — sitting beside its sibling
60
+ sub-folders. Map it to a file instead of a folder:
61
+
62
+ ```likec4
63
+ component ports 'ports' {
64
+ metadata { folder 'src/ports' }
65
+
66
+ component store 'in-memory-store' {
67
+ metadata { file 'src/ports/in-memory-store.ts' } // a leaf, not a folder
68
+ }
69
+ component read 'story-points-read' {
70
+ metadata { folder 'src/ports/story-points-read' }
71
+ component stub 'stub' {
72
+ metadata { folder 'src/ports/story-points-read/stub' }
73
+ }
74
+ }
75
+ }
76
+
77
+ stub -> store // a legal cross-subtree edge
78
+ ```
79
+
80
+ This is what lets a deep tree be **both** the documentation and the enforcement
81
+ model. Collapse that file into its parent folder and the edge becomes
82
+ `stub -> ports` — a descendant importing its ancestor, which LikeC4 rejects with
83
+ `Invalid parent-child relationship`. As a file leaf, `store` is a sibling, so the
84
+ edge is legal *and* Boundry governs the file exactly: only `stub` may import it,
85
+ and the surrounding `ports` folder no longer owns it.
86
+
87
+ A file module owns exactly its file; a folder module owns its subtree minus any
88
+ mapped descendants — nested folders and file leaves alike.
89
+
54
90
  ### Exempting test files and ambient declarations
55
91
 
56
92
  Every file under a mapped folder is governed as a rule *source* by default —
@@ -201,10 +237,86 @@ the allow-list, the newly-allowed set is exactly the set of self-approvals — n
201
237
  diff engine required. `approve --base` runs the same gate first, so it won't
202
238
  launder a self-granted edge into an approved one.
203
239
 
240
+ **To retire an edge or a box, propose its removal** with `#proposal-delete`
241
+ instead of deleting it. The marker colours it red; a pending deletion changes
242
+ nothing (the edge stays allowed, the box stays enforced) so it never breaks the
243
+ build. Then `approve` removes the marked edge or box outright:
244
+
245
+ ```likec4
246
+ module legacy 'Legacy' {
247
+ #proposal-delete
248
+ metadata { folder 'src/legacy' }
249
+ }
250
+ api -> legacy #proposal-delete
251
+ ```
252
+
253
+ So `#proposed` and `#proposal-delete` are the two halves of a change — an amber
254
+ addition that `approve` makes permanent, and a red removal that `approve` takes
255
+ away — each a visible mark on the diagram until a human acts.
256
+
204
257
  Point your agents at
205
258
  [`.claude/skills/define-architecture-boundaries`](.claude/skills/define-architecture-boundaries/SKILL.md)
206
259
  and they'll follow this protocol.
207
260
 
261
+ ### Catching drift — the lock and `annotate` (prototype)
262
+
263
+ `verify --base` trusts git for the baseline, which conflates *approved* with
264
+ *committed*. If you'd rather Boundry own that baseline: `approve` records the
265
+ accepted model to **`boundry.lock`** beside the diagram, and `annotate` compares
266
+ against it — no git ref required.
267
+
268
+ ```bash
269
+ boundry approve --arch arch # enact proposals AND write boundry.lock
270
+ boundry annotate --arch arch # rewrite undeclared additions as #proposed
271
+ ```
272
+
273
+ `annotate` finds every edge or box that drifted past the lock without a marker —
274
+ a self-grant — and rewrites it in place as a `#proposed` proposal. That's not
275
+ cosmetic: a `#proposed` edge leaves the allow-list, so the silent grant becomes a
276
+ red-again check and a highlighted box on the diagram, awaiting a real approval. It
277
+ handles additions only; a removal is reported, not re-drawn (re-adding a deleted
278
+ box would resurrect it as an enforced module).
279
+
280
+ It also **paints the markers** — every `#proposed` edge/box gets an intrinsic
281
+ `style { color amber }`, every `#proposal-delete` a `style { color red }`, written
282
+ onto the element itself. Intrinsic style is the one styling LikeC4 renders on
283
+ *every* surface — base views **and the "relationships of X" panel** — so a
284
+ proposal shows up highlighted wherever a reviewer looks, not only in the generated
285
+ diff views (whose view-scoped rules stop at the view boundary). `approve` strips
286
+ this styling back out with the marker: a `#proposed` edge returns to bare, a
287
+ `#proposed` box stays but loses its colour, a `#proposal-delete` is removed
288
+ outright. Requires **LikeC4 ≥ 1.58** to render.
289
+
290
+ ### Reviewing a proposal — per-layer diff views (prototype)
291
+
292
+ A proposal nested inside a box is invisible at a wider zoom: at the top level the
293
+ box collapses and its inner `#proposed` edge disappears. So `diff` generates a
294
+ **focused view for every layer that holds a pending change** — the tightest scope
295
+ that actually draws it — into a derived `boundry.diff.likec4`:
296
+
297
+ ```bash
298
+ boundry diff --arch arch # (re)write boundry.diff.likec4
299
+ likec4 serve arch # review each layer, changes coloured
300
+ ```
301
+
302
+ The highlighting is **generated, not hand-styled** — `diff` emits the LikeC4
303
+ style rules into the derived file, so every `#proposed` box fills amber and edge
304
+ goes amber + solid, every `#proposal-delete` red, deterministically. That closes
305
+ the last manual seam: `annotate` marks, `diff` colours, no agent-dependent
306
+ styling step. Boxes are styled *in place* (never force-included), so a nested
307
+ proposal stays in its own layer; unchanged elements keep their defaults; and the
308
+ rules live only in the generated views, so your own views are untouched.
309
+
310
+ The file is a **derived artifact**: overwritten every run, removed when nothing is
311
+ proposed, so it always matches the current diagram — regenerate it after
312
+ `approve`. It reads the diagram's own markers (not the lock), so it frames
313
+ whatever `annotate` or a human has marked. Being derived, it's a `.gitignore`
314
+ candidate (`boundry.diff.likec4`).
315
+
316
+ > Rendering the coloured diff views needs **LikeC4 ≥ 1.58** (the style-rule syntax
317
+ > Boundry emits). Boundry itself depends on that floor; the tool you review with
318
+ > (`likec4 serve`, the CLI, or the IDE extension) has to meet it too.
319
+
208
320
  ## CLI
209
321
 
210
322
  ```
@@ -212,6 +324,8 @@ boundry check [--arch <dir>] [--cwd <dir>] [sources...]
212
324
  boundry generate [--arch <dir>] [--cwd <dir>] [--out <file>]
213
325
  boundry verify [--arch <dir>] [--cwd <dir>] --base <git-ref>
214
326
  boundry approve [--arch <dir>] [--cwd <dir>] [--base <git-ref>]
327
+ boundry annotate [--arch <dir>]
328
+ boundry diff [--arch <dir>]
215
329
  ```
216
330
 
217
331
  | Flag | Meaning |
@@ -226,7 +340,12 @@ boundry approve [--arch <dir>] [--cwd <dir>] [--base <git-ref>]
226
340
  - **`generate`** just emits the dependency-cruiser config so you can commit it or
227
341
  run `depcruise` yourself.
228
342
  - **`verify`** rejects dependencies granted without a proposal.
229
- - **`approve`** strips `#proposed` markers. For humans, not agents.
343
+ - **`approve`** enacts markers (strips `#proposed`, removes `#proposal-delete`) and
344
+ writes `boundry.lock`. For humans, not agents.
345
+ - **`annotate`** rewrites undeclared additions as `#proposed`, diffing against
346
+ `boundry.lock`.
347
+ - **`diff`** generates a focused, colour-coded review view per layer that holds a
348
+ pending change, into a derived `boundry.diff.likec4`.
230
349
 
231
350
  Boundry warns (but does not fail) when a mapped folder matches **zero** files, and
232
351
  **fails outright** when a check analysed no files at all — so a passing check can
@@ -284,7 +403,7 @@ without touching the core.
284
403
 
285
404
  Current limitations:
286
405
 
287
- - One element maps to exactly one folder.
406
+ - One element maps to exactly one path — a folder or a single file.
288
407
  - Nesting is supported: you can map a parent folder *and* its children. A
289
408
  parent's edges govern only the parent's own files — a child never inherits
290
409
  them and must be permitted explicitly.
@@ -1,18 +1,35 @@
1
1
  import * as dependencyCruiser from 'dependency-cruiser';
2
- import { unconstrainedModules } from '../../core/model/boundary-model.js';
2
+ import { unconstrainedModules, } from '../../core/model/boundary-model.js';
3
3
  // dependency-cruiser ships CommonJS; reach `cruise` through either interop shape.
4
4
  const cruise = dependencyCruiser.cruise ?? dependencyCruiser.default?.cruise;
5
- function folderPrefix(folder) {
6
- return folder.replace(/\/+$/, '');
5
+ function normalizePath(path) {
6
+ return path.replace(/\/+$/, '');
7
7
  }
8
- function folderToRegex(folder) {
9
- const escaped = folderPrefix(folder).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
10
- return `^${escaped}/`;
8
+ function escapeRegex(path) {
9
+ return normalizePath(path).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
11
10
  }
12
- /** True if `child` is a folder strictly nested under `parent`. */
13
- function isDescendantFolder(child, parent) {
14
- const c = folderPrefix(child);
15
- const p = folderPrefix(parent);
11
+ /**
12
+ * The regex matching exactly the sources a module owns: everything under a
13
+ * folder (`^src/core/`), or a single file exactly (`^src/x.ts$`).
14
+ */
15
+ function moduleScopeRegex(m) {
16
+ return m.kind === 'file' ? `^${escapeRegex(m.path)}$` : `^${escapeRegex(m.path)}/`;
17
+ }
18
+ /** True if `source` (a file path) belongs to module `m`, ignoring descendants. */
19
+ function ownsSource(m, source) {
20
+ const p = normalizePath(m.path);
21
+ return m.kind === 'file' ? source === p : source.startsWith(`${p}/`);
22
+ }
23
+ /**
24
+ * True if module `child` is nested strictly under module `parent`. Only a folder
25
+ * can contain anything; a file (or a folder equal to the child) contains nothing.
26
+ * A folder nesting a file leaf counts — that is the whole point of file mapping.
27
+ */
28
+ function isNestedUnder(child, parent) {
29
+ if (parent.kind === 'file')
30
+ return false;
31
+ const c = normalizePath(child.path);
32
+ const p = normalizePath(parent.path);
16
33
  return c !== p && c.startsWith(`${p}/`);
17
34
  }
18
35
  /**
@@ -29,15 +46,17 @@ function buildForbiddenRules(model) {
29
46
  allowedTargets.set(m.id, new Set([m.id]));
30
47
  for (const edge of model.allowed)
31
48
  allowedTargets.get(edge.from)?.add(edge.to);
32
- // Each module's scope: its folder, carving out any mapped descendant folders.
49
+ // Each module's scope: its path, carving out any mapped descendants (a nested
50
+ // folder OR a mapped file leaf living inside it — both belong to their own,
51
+ // more-specific module).
33
52
  const scopeOf = new Map();
34
53
  for (const m of model.modules) {
35
54
  const descendants = model.modules
36
- .filter((other) => isDescendantFolder(other.folder, m.folder))
37
- .map((other) => folderToRegex(other.folder));
55
+ .filter((other) => isNestedUnder(other, m))
56
+ .map((other) => moduleScopeRegex(other));
38
57
  scopeOf.set(m.id, descendants.length
39
- ? { path: folderToRegex(m.folder), pathNot: descendants }
40
- : { path: folderToRegex(m.folder) });
58
+ ? { path: moduleScopeRegex(m), pathNot: descendants }
59
+ : { path: moduleScopeRegex(m) });
41
60
  }
42
61
  // Exemptions apply to the `from` side only: matched files may import freely,
43
62
  // but every module's scope still governs them as a *target*, so importing a
@@ -72,7 +91,8 @@ function buildForbiddenRules(model) {
72
91
  // module claims is forbidden, not free. Additive — the pair rules above still
73
92
  // decide every mapped-to-mapped edge on their own.
74
93
  if (model.governRoot) {
75
- const claimed = model.modules.map((m) => folderToRegex(m.folder));
94
+ const rootAsFolder = { id: '', title: '', path: model.governRoot, kind: 'folder' };
95
+ const claimed = model.modules.map(moduleScopeRegex);
76
96
  for (const from of model.modules) {
77
97
  if (unconstrained.has(from.id))
78
98
  continue;
@@ -81,7 +101,7 @@ function buildForbiddenRules(model) {
81
101
  comment: `${from.title} may not depend on code under '${model.governRoot}' that no module claims`,
82
102
  severity: 'error',
83
103
  from: asImporter(from.id),
84
- to: { path: folderToRegex(model.governRoot), pathNot: claimed },
104
+ to: { path: moduleScopeRegex(rootAsFolder), pathNot: claimed },
85
105
  });
86
106
  }
87
107
  }
@@ -115,7 +135,7 @@ module.exports = {
115
135
  const violations = raw
116
136
  .filter((v) => v.rule?.severity === 'error')
117
137
  .map((v) => ({ from: v.from, to: v.to, rule: v.rule?.name ?? 'unknown' }));
118
- // A module whose folder matched no source files enforces nothing — the
138
+ // A module whose path matched no source files enforces nothing — the
119
139
  // "green but inert" trap. Surface it so a passing check can't hide it.
120
140
  const seen = (output?.modules ?? []).map((m) => String(m.source));
121
141
  // A guardrail that analysed nothing must never report success. Seeing zero
@@ -127,9 +147,8 @@ module.exports = {
127
147
  }
128
148
  const warnings = [];
129
149
  for (const mod of model.modules) {
130
- const prefix = `${mod.folder.replace(/\/+$/, '')}/`;
131
- if (!seen.some((source) => source.startsWith(prefix))) {
132
- warnings.push(`module '${mod.title}' maps to '${mod.folder}', which matched 0 source files`);
150
+ if (!seen.some((source) => ownsSource(mod, source))) {
151
+ warnings.push(`module '${mod.title}' maps to '${mod.path}', which matched 0 source files`);
133
152
  }
134
153
  }
135
154
  // An exemption is a grant, so a dead one is worth knowing about: it means
@@ -152,13 +171,12 @@ module.exports = {
152
171
  // claims. The model not covering the code is as much a gap as the code not
153
172
  // backing the model.
154
173
  if (model.governRoot) {
155
- const rootPrefix = `${folderPrefix(model.governRoot)}/`;
156
- const claimed = model.modules.map((m) => `${folderPrefix(m.folder)}/`);
174
+ const rootPrefix = `${normalizePath(model.governRoot)}/`;
157
175
  const unclaimed = new Set();
158
176
  for (const source of seen) {
159
177
  if (!source.startsWith(rootPrefix))
160
178
  continue;
161
- if (claimed.some((prefix) => source.startsWith(prefix)))
179
+ if (model.modules.some((m) => ownsSource(m, source)))
162
180
  continue;
163
181
  // Exempt code is deliberately outside the architecture; asking a module
164
182
  // to claim it would defeat the point of exempting it.
@@ -1,5 +1,5 @@
1
- import type { VisualizerPort } from '../../core/ports/ports.js';
2
- import type { BoundaryModel } from '../../core/model/boundary-model.js';
1
+ import type { VisualizerPort, DiffView } from '../../core/ports/ports.js';
2
+ import type { BoundaryModel, AllowedEdge } from '../../core/model/boundary-model.js';
3
3
  /**
4
4
  * First visualizer adapter. Reads a LikeC4 workspace and lifts it into the
5
5
  * boundary model: any element carrying a `folder` metadata key becomes a
@@ -11,10 +11,40 @@ export declare class LikeC4Visualizer implements VisualizerPort {
11
11
  constructor(workspaceDir: string);
12
12
  read(): Promise<BoundaryModel>;
13
13
  /**
14
- * Deterministically strip `#proposed` markers from the diagram source,
15
- * promoting intent edges to approved. Source-preserving; never an LLM edit
16
- * it locates each marked relationship in the LikeC4 CST and splices out its
17
- * proposal decoration, leaving all other formatting intact.
14
+ * Deterministically enact a diagram's pending proposals. Source-preserving,
15
+ * never an LLM edit it walks the LikeC4 CST and splices ranges directly:
16
+ * - `#proposed` markers are stripped, promoting their intent edges to
17
+ * approved (the edge/box stays, permanently allowed);
18
+ * - `#proposal-delete` markers take their whole edge or box with them, so
19
+ * approving a proposed removal actually removes it.
20
+ * All other formatting is left intact.
18
21
  */
19
22
  approve(): Promise<void>;
23
+ /**
24
+ * Deterministically mark the given edges and elements `#proposed`. The inverse
25
+ * of `approve` for additions: an undeclared new edge (a self-grant) or box is
26
+ * rewritten into an explicit, colourable proposal. It locates each target's
27
+ * relation/element in the CST by its resolved ids and injects the marker,
28
+ * skipping anything already marked, so it is idempotent.
29
+ */
30
+ propose(edges: AllowedEdge[], moduleIds: string[]): Promise<void>;
31
+ /**
32
+ * Paint intrinsic style on every marked edge and box, so a proposal is
33
+ * highlighted on *every* LikeC4 surface — base views and the "relationships of
34
+ * X" panel, not just the generated diff views (which only carry view-scoped
35
+ * rules). A `#proposed` edge/box gets `style { color amber }`, a
36
+ * `#proposal-delete` `style { color red }`. Source-preserving and idempotent —
37
+ * anything already carrying the canonical style is left alone. `approve` strips
38
+ * this styling back out when it enacts the marker.
39
+ */
40
+ styleMarkers(): Promise<void>;
41
+ /**
42
+ * Generate a focused diff view for every layer that holds a pending change.
43
+ * Walks the diagram's own `#proposed` / `#proposal-delete` markers (never the
44
+ * lock — a `#proposal-delete` has no lock delta), buckets each change into the
45
+ * tightest scope that draws it, and writes one `view … of <scope>` per bucket
46
+ * to a derived `boundry.diff.likec4`. The file is overwritten each run and
47
+ * removed when nothing is proposed, so it always reflects the current diagram.
48
+ */
49
+ emitDiffViews(): Promise<DiffView[]>;
20
50
  }