boundry 0.3.0 → 0.5.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,13 +4,106 @@ 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.3.0] — unreleased
7
+ ## [0.5.0] — 2026-07-21
8
+
9
+ ### Fixed
10
+
11
+ - **`approve` no longer leaves a stale, corrupted `boundry.diff.likec4`** ([#6]).
12
+ `approve` used to walk the derived diff file along with the model and splice the
13
+ marker tokens out of its *view rules* (`style element.tag = #proposed …`,
14
+ `include … where tag is #proposed`), mangling them into invalid LikeC4 — so
15
+ `likec4 validate` (and any `arch:verify` gate) went red immediately after a
16
+ successful approve, recoverable only by a separate `diff` run. `approve` now
17
+ skips the derived file and deletes it as part of enacting: the moment the last
18
+ proposal is approved those views are stale — they frame changes that are now
19
+ approved — so the post-approve workspace is clean and validates.
20
+
21
+ ### Added
22
+
23
+ - **Per-altitude diff views** ([#7]). `boundry diff` now emits a focused view for
24
+ *every* layer that draws a changed edge, not only the common-ancestor layer. A
25
+ cross-system dependency between two deeply-nested leaves is drawable at each
26
+ endpoint's system and container as well as at the shared root; `diff` emits one
27
+ `view … of <scope>` for each, so a reviewer can open the change at whatever
28
+ altitude matters — the whole system, a specific container — instead of being
29
+ dropped at the noisy root view. The scopes are derived from how LikeC4's
30
+ `include *` actually renders: the common ancestor plus each endpoint's ancestor
31
+ chain down to its parent, never *above* the common ancestor (where both
32
+ endpoints collapse into one child and the edge becomes an undrawn self-loop) and
33
+ never the leaf endpoints themselves. Boxes are unchanged — one view at the box's
34
+ parent layer.
35
+
36
+ ### Changed
37
+
38
+ - **`verify` now compares against the accepted `boundry.lock`, not a git ref.**
39
+ The lock was introduced in 0.4.0 to decouple *approved* from *committed*, but
40
+ `verify --base <ref>` kept re-deriving its baseline from a diagram at a git ref
41
+ — quietly re-coupling "accepted" to "committed", the very thing the lock exists
42
+ to break. `verify` now reads the lock, exactly as `annotate` does, so the two
43
+ share one baseline and can never disagree about what "accepted" means. It also
44
+ catches a committed-but-*unapproved* bare edge, which the git-ref baseline
45
+ silently absorbed. The `--base` flag is gone from `verify` and `approve`;
46
+ `approve`'s anti-laundering pre-check now runs the same lock-based gate, and the
47
+ first `approve` (no lock yet) bootstraps the initial accepted state. **Tradeoff,
48
+ by design:** the baseline is the working-tree lock, so the gate leans on
49
+ `approve` being a human act — the lock moves only when someone approves, which
50
+ is why the skill forbids agents from running it.
51
+
52
+ [#6]: https://github.com/makspiechota/boundry/issues/6
53
+ [#7]: https://github.com/makspiechota/boundry/issues/7
54
+
55
+ ## [0.4.0] — 2026-07-17
8
56
 
9
57
  Makes the "one file can be both the communication diagram and the enforcement
10
- model" promise hold for deep, nested C4 trees.
58
+ model" promise hold for deep, nested C4 trees, and makes a proposed change
59
+ highlight itself — deterministically, on every LikeC4 surface.
11
60
 
12
61
  ### Added
13
62
 
63
+ - **Accepted-state lock + `annotate` (prototype).** `approve` now records the
64
+ accepted model to `boundry.lock` beside the diagram — a baseline Boundry
65
+ **owns**, rather than inferring it from whatever git happens to hold, so
66
+ "approved" is decoupled from "committed". `boundry annotate` diffs the diagram
67
+ against that lock and rewrites every undeclared addition — a bare new edge (a
68
+ self-grant) or box — into an explicit `#proposed` proposal in the source,
69
+ deterministically and idempotently. Turns silent drift into a reviewable,
70
+ colourable proposal. (Additive only; deletions are reported, not round-tripped
71
+ back into the DSL — re-materialising a removed box would resurrect it as an
72
+ enforced module.)
73
+ - **Cross-surface highlighting** ([#5]). `annotate` also paints each marker with
74
+ an intrinsic `style { color amber }` (`#proposed`) / `style { color red }`
75
+ (`#proposal-delete`) on the element itself. Intrinsic style is the only styling
76
+ LikeC4 renders on **every** surface — base views *and* the "relationships of X"
77
+ panel — so a proposal is highlighted wherever a reviewer looks, not just inside
78
+ the diff views (whose view-scoped rules stop at the view boundary). Idempotent,
79
+ and `approve` strips the styling back out with the marker: a `#proposed` edge
80
+ returns to bare, a `#proposed` box loses its colour, a `#proposal-delete` is
81
+ removed outright. Needs LikeC4 ≥ 1.58 to render.
82
+ - **Per-layer diff views — `boundry diff` (prototype).** Generates a focused
83
+ LikeC4 view for every layer that holds a pending change — an edge or box tagged
84
+ `#proposed` or `#proposal-delete` — scoped to the tightest element that draws
85
+ it, into a derived `boundry.diff.likec4`. This matters because a proposal nested
86
+ inside a box is invisible once that box collapses at a wider zoom, so a single
87
+ all-up view hides it; one view per layer surfaces every change in the scope
88
+ where it renders. It reads the diagram's own markers, not the lock, so it frames
89
+ whatever `annotate` or a human marked. The file is derived — overwritten each
90
+ run, removed when nothing is proposed — so it always reflects the current
91
+ diagram; regenerate after `approve`.
92
+ - **Deterministic highlighting** ([#4]). `diff` emits the LikeC4 style rules
93
+ into the derived file, so a `#proposed` box fills amber and its edge goes
94
+ amber + solid, a `#proposal-delete` red — with no hand-styling. Boxes are
95
+ styled in place (`style element.tag`, never force-included), so a nested
96
+ proposal stays in its layer; the rules live only in the generated views, so
97
+ user-authored views are untouched. Rules are emitted only for marker tags
98
+ actually in use (referencing an undeclared tag is a hard LikeC4 error).
99
+ - **`#proposal-delete` — the deletion half of the approval lifecycle.** Tag an
100
+ edge or a box `#proposal-delete` to propose retiring it. The marker colours it
101
+ red; a pending deletion changes nothing (the edge stays allowed, the box stays
102
+ enforced), so proposing a removal never breaks the build. On `approve`, the
103
+ marked edge or box is removed from the diagram outright — deterministically, by
104
+ splicing the LikeC4 CST — where `#proposed` only strips its own marker and
105
+ leaves the edge behind. `#proposed` (amber, additive) and `#proposal-delete`
106
+ (red, subtractive) are now the two halves of every change.
14
107
  - **File-level mapping — `metadata { file 'src/x.ts' }`** ([#3]). An element maps
15
108
  to a single file instead of a folder. A file module owns exactly its file; a
16
109
  folder module owns its subtree minus any mapped descendants — nested folders
@@ -26,8 +119,19 @@ model" promise hold for deep, nested C4 trees.
26
119
 
27
120
  - **`Module.folder: string` → `Module.path: string` + `Module.kind: 'folder' | 'file'`.**
28
121
  SDK-breaking; the diagram surface and CLI are unaffected.
122
+ - **`likec4` floor raised to `^1.58.0`.** The diff-view style-rule syntax Boundry
123
+ emits is verified against LikeC4 1.58; rendering the coloured views needs a
124
+ renderer at least that new.
29
125
 
30
126
  [#3]: https://github.com/makspiechota/boundry/issues/3
127
+ [#4]: https://github.com/makspiechota/boundry/issues/4
128
+ [#5]: https://github.com/makspiechota/boundry/issues/5
129
+
130
+ ## [0.3.0] — 2026-07-16 — superseded by 0.4.0
131
+
132
+ Published early from a pre-feature snapshot (≈ 0.2.0 plus the `#anything`
133
+ wildcard) and never carried the work now listed under 0.4.0. npm versions are
134
+ immutable, so it stays on the registry; use **0.4.0**.
31
135
 
32
136
  ## [0.2.0] — 2026-07-16
33
137
 
package/README.md CHANGED
@@ -227,27 +227,120 @@ deterministically, by splicing the LikeC4 CST. No model call, no reformatting:
227
227
  source-preserving, idempotent, byte-exact.
228
228
 
229
229
  ```bash
230
- boundry verify --arch arch --base origin/main # any edge granted without a marker?
231
- boundry approve --arch arch --base origin/main # HUMAN ONLY: strip markers = approve
230
+ boundry verify --arch arch # any edge granted without a marker, vs the lock?
231
+ boundry approve --arch arch # HUMAN ONLY: strip markers = approve, update the lock
232
232
  ```
233
233
 
234
- `verify` compares against the approved diagram at a git ref and rejects edges that
235
- appeared *without* going through a proposal. Because proposals are excluded from
236
- the allow-list, the newly-allowed set is exactly the set of self-approvals — no
237
- diff engine required. `approve --base` runs the same gate first, so it won't
238
- launder a self-granted edge into an approved one.
234
+ `verify` compares the working diagram against the accepted **`boundry.lock`** and
235
+ rejects edges that appeared *without* going through a proposal. Because proposals
236
+ are excluded from the allow-list, the newly-allowed set is exactly the set of
237
+ self-approvals — no diff engine required. `approve` runs the same gate first, so
238
+ it won't launder a self-granted edge into an approved one; then it enacts the
239
+ proposals and records the new accepted state to the lock.
240
+
241
+ The baseline is the lock, not a git ref: it's the state Boundry *owns*, so
242
+ "accepted" never collapses into merely "committed". (One consequence, by design:
243
+ the gate leans on `approve` being a human act — the lock moves only when someone
244
+ approves. That's why the skill forbids agents from running it.)
245
+
246
+ **To retire an edge or a box, propose its removal** with `#proposal-delete`
247
+ instead of deleting it. The marker colours it red; a pending deletion changes
248
+ nothing (the edge stays allowed, the box stays enforced) so it never breaks the
249
+ build. Then `approve` removes the marked edge or box outright:
250
+
251
+ ```likec4
252
+ module legacy 'Legacy' {
253
+ #proposal-delete
254
+ metadata { folder 'src/legacy' }
255
+ }
256
+ api -> legacy #proposal-delete
257
+ ```
258
+
259
+ So `#proposed` and `#proposal-delete` are the two halves of a change — an amber
260
+ addition that `approve` makes permanent, and a red removal that `approve` takes
261
+ away — each a visible mark on the diagram until a human acts.
239
262
 
240
263
  Point your agents at
241
264
  [`.claude/skills/define-architecture-boundaries`](.claude/skills/define-architecture-boundaries/SKILL.md)
242
265
  and they'll follow this protocol.
243
266
 
267
+ ### Catching drift — `annotate` (prototype)
268
+
269
+ `verify` catches a self-grant and fails; `annotate` is the other half — it
270
+ *rewrites* the same drift into a reviewable proposal. Both read the same baseline,
271
+ the accepted **`boundry.lock`** that `approve` records beside the diagram, so they
272
+ can never disagree about what "accepted" means.
273
+
274
+ ```bash
275
+ boundry approve --arch arch # enact proposals AND write boundry.lock
276
+ boundry annotate --arch arch # rewrite undeclared additions as #proposed
277
+ ```
278
+
279
+ `annotate` finds every edge or box that drifted past the lock without a marker —
280
+ a self-grant — and rewrites it in place as a `#proposed` proposal. That's not
281
+ cosmetic: a `#proposed` edge leaves the allow-list, so the silent grant becomes a
282
+ red-again check and a highlighted box on the diagram, awaiting a real approval. It
283
+ handles additions only; a removal is reported, not re-drawn (re-adding a deleted
284
+ box would resurrect it as an enforced module).
285
+
286
+ It also **paints the markers** — every `#proposed` edge/box gets an intrinsic
287
+ `style { color amber }`, every `#proposal-delete` a `style { color red }`, written
288
+ onto the element itself. Intrinsic style is the one styling LikeC4 renders on
289
+ *every* surface — base views **and the "relationships of X" panel** — so a
290
+ proposal shows up highlighted wherever a reviewer looks, not only in the generated
291
+ diff views (whose view-scoped rules stop at the view boundary). `approve` strips
292
+ this styling back out with the marker: a `#proposed` edge returns to bare, a
293
+ `#proposed` box stays but loses its colour, a `#proposal-delete` is removed
294
+ outright. Requires **LikeC4 ≥ 1.58** to render.
295
+
296
+ ### Reviewing a proposal — per-layer diff views (prototype)
297
+
298
+ A proposal nested inside a box is invisible at a wider zoom: at the top level the
299
+ box collapses and its inner `#proposed` edge disappears. So `diff` generates a
300
+ **focused view for every layer that draws a pending change** — into a derived
301
+ `boundry.diff.likec4`:
302
+
303
+ ```bash
304
+ boundry diff --arch arch # (re)write boundry.diff.likec4
305
+ likec4 serve arch # review each layer, changes coloured
306
+ ```
307
+
308
+ The highlighting is **generated, not hand-styled** — `diff` emits the LikeC4
309
+ style rules into the derived file, so every `#proposed` box fills amber and edge
310
+ goes amber + solid, every `#proposal-delete` red, deterministically. That closes
311
+ the last manual seam: `annotate` marks, `diff` colours, no agent-dependent
312
+ styling step. Boxes are styled *in place* (never force-included), so a nested
313
+ proposal stays in its own layer; unchanged elements keep their defaults; and the
314
+ rules live only in the generated views, so your own views are untouched.
315
+
316
+ A changed edge is drawn at **every altitude it appears**, not only the
317
+ common-ancestor layer — so a cross-system dependency between two deeply-nested
318
+ leaves gets a view at each endpoint's system and container as well as at the
319
+ shared root. Open the change at whatever altitude matters (the whole system, a
320
+ specific container) instead of being dropped at the noisy root view. `diff` emits
321
+ only the layers that genuinely draw the edge: never above the common ancestor
322
+ (where it collapses to an undrawn self-loop), never the leaf endpoints themselves.
323
+
324
+ The file is a **derived artifact**: overwritten every run, removed when nothing is
325
+ proposed, so it always matches the current diagram. `approve` deletes it too, as
326
+ part of enacting — the moment the last proposal is approved the views are stale,
327
+ so the post-approve workspace validates clean. It reads the diagram's own markers
328
+ (not the lock), so it frames whatever `annotate` or a human has marked. Being
329
+ derived, it's a `.gitignore` candidate (`boundry.diff.likec4`).
330
+
331
+ > Rendering the coloured diff views needs **LikeC4 ≥ 1.58** (the style-rule syntax
332
+ > Boundry emits). Boundry itself depends on that floor; the tool you review with
333
+ > (`likec4 serve`, the CLI, or the IDE extension) has to meet it too.
334
+
244
335
  ## CLI
245
336
 
246
337
  ```
247
338
  boundry check [--arch <dir>] [--cwd <dir>] [sources...]
248
339
  boundry generate [--arch <dir>] [--cwd <dir>] [--out <file>]
249
- boundry verify [--arch <dir>] [--cwd <dir>] --base <git-ref>
250
- boundry approve [--arch <dir>] [--cwd <dir>] [--base <git-ref>]
340
+ boundry verify [--arch <dir>] [--cwd <dir>]
341
+ boundry approve [--arch <dir>] [--cwd <dir>]
342
+ boundry annotate [--arch <dir>]
343
+ boundry diff [--arch <dir>]
251
344
  ```
252
345
 
253
346
  | Flag | Meaning |
@@ -255,14 +348,20 @@ boundry approve [--arch <dir>] [--cwd <dir>] [--base <git-ref>]
255
348
  | `--arch <dir>` | LikeC4 workspace directory (all `.likec4` files in it are merged). Default `.`. |
256
349
  | `--cwd <dir>` | Repo root to check. `folder` paths are relative to it. Lets you run from anywhere. |
257
350
  | `--out <file>` | `generate` only: where to write the dependency-cruiser config. Default `.dependency-cruiser.cjs`. |
258
- | `--base <ref>` | `verify`/`approve`: the git ref holding the approved diagram to compare against. |
259
351
  | `sources...` | `check` only: paths to lint. Default `src`. |
260
352
 
261
353
  - **`check`** compiles the rules and runs the linter. Exits non-zero on any violation.
262
354
  - **`generate`** just emits the dependency-cruiser config so you can commit it or
263
355
  run `depcruise` yourself.
264
- - **`verify`** rejects dependencies granted without a proposal.
265
- - **`approve`** strips `#proposed` markers. For humans, not agents.
356
+ - **`verify`** rejects dependencies granted without a proposal, comparing the
357
+ diagram against the accepted `boundry.lock`. Needs a lock — run `approve` once
358
+ to record one.
359
+ - **`approve`** runs the same gate, then enacts markers (strips `#proposed`,
360
+ removes `#proposal-delete`) and writes `boundry.lock`. For humans, not agents.
361
+ - **`annotate`** rewrites undeclared additions as `#proposed`, diffing against
362
+ `boundry.lock`.
363
+ - **`diff`** generates a focused, colour-coded review view per layer that holds a
364
+ pending change, into a derived `boundry.diff.likec4`.
266
365
 
267
366
  Boundry warns (but does not fail) when a mapped folder matches **zero** files, and
268
367
  **fails outright** when a check analysed no files at all — so a passing check can
@@ -283,9 +382,8 @@ jobs:
283
382
  with: { node-version: 20 }
284
383
  - run: npm ci
285
384
  - run: npx boundry check --arch arch src
286
- # On PRs, also reject dependencies granted without a proposal.
287
- - run: npx boundry verify --arch arch --base origin/${{ github.base_ref }}
288
- if: github.event_name == 'pull_request'
385
+ # Reject any dependency granted without a proposal, vs the committed lock.
386
+ - run: npx boundry verify --arch arch
289
387
  ```
290
388
 
291
389
  ## Programmatic use (SDK)
@@ -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,49 @@ 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.
21
+ *
22
+ * The derived `boundry.diff.likec4` is skipped, not approved: it holds no real
23
+ * markers, only view rules that *reference* the marker tags (`style element.tag
24
+ * = #proposed`, `include … where tag is #proposed`). Splicing those `#proposed`
25
+ * tokens out would corrupt the rules into invalid LikeC4. And it is stale the
26
+ * moment the last proposal is enacted — it describes changes that are now
27
+ * approved — so approve deletes it, leaving a workspace `likec4 validate` accepts.
18
28
  */
19
29
  approve(): Promise<void>;
30
+ /**
31
+ * Deterministically mark the given edges and elements `#proposed`. The inverse
32
+ * of `approve` for additions: an undeclared new edge (a self-grant) or box is
33
+ * rewritten into an explicit, colourable proposal. It locates each target's
34
+ * relation/element in the CST by its resolved ids and injects the marker,
35
+ * skipping anything already marked, so it is idempotent.
36
+ */
37
+ propose(edges: AllowedEdge[], moduleIds: string[]): Promise<void>;
38
+ /**
39
+ * Paint intrinsic style on every marked edge and box, so a proposal is
40
+ * highlighted on *every* LikeC4 surface — base views and the "relationships of
41
+ * X" panel, not just the generated diff views (which only carry view-scoped
42
+ * rules). A `#proposed` edge/box gets `style { color amber }`, a
43
+ * `#proposal-delete` `style { color red }`. Source-preserving and idempotent —
44
+ * anything already carrying the canonical style is left alone. `approve` strips
45
+ * this styling back out when it enacts the marker.
46
+ */
47
+ styleMarkers(): Promise<void>;
48
+ /**
49
+ * Generate a focused diff view for every layer that holds a pending change.
50
+ * Walks the diagram's own `#proposed` / `#proposal-delete` markers (never the
51
+ * lock — a `#proposal-delete` has no lock delta) and writes one `view … of
52
+ * <scope>` per layer to a derived `boundry.diff.likec4`. A box is bucketed into
53
+ * its parent layer; an edge into *every* layer that draws it (see `edgeScopes`),
54
+ * so a cross-system dependency can be reviewed at each altitude, not only the
55
+ * common-ancestor layer. The file is overwritten each run and removed when
56
+ * nothing is proposed, so it always reflects the current diagram.
57
+ */
58
+ emitDiffViews(): Promise<DiffView[]>;
20
59
  }