boundry 0.4.0 → 0.6.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 +70 -0
- package/README.md +60 -40
- package/dist/adapters/visualizer/likec4.d.ts +28 -7
- package/dist/adapters/visualizer/likec4.js +172 -30
- package/dist/cli/index.js +27 -46
- package/dist/core/pipeline/pipeline.d.ts +17 -11
- package/dist/core/pipeline/pipeline.js +22 -15
- package/dist/core/ports/ports.d.ts +12 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,76 @@ 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.6.0] — 2026-07-22
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **`boundry diff` now emits a single "proposed changes" view by default** ([#8]),
|
|
12
|
+
instead of one view per changed layer. The per-layer shape (added in 0.4.0,
|
|
13
|
+
extended per-altitude in 0.5.0) had two failure modes on real changes: it
|
|
14
|
+
multiplied views (one multi-slice change produced ~21), and its per-layer
|
|
15
|
+
`include *` collapsed deeply-nested proposals into their grey ancestor box, so a
|
|
16
|
+
nested proposed leaf silently lost its amber. The new `boundry_diff` view uses
|
|
17
|
+
`include * -> * where tag is #proposed` with **no** bare `include *`, so it pulls
|
|
18
|
+
in exactly the proposed edges and the leaf modules they touch — every change is
|
|
19
|
+
drawn at leaf level, uncollapsed and uniformly coloured, on one landing. A
|
|
20
|
+
`#proposed` box with no edge yet is included by fqn so a proposed-but-unwired
|
|
21
|
+
module still shows, and every endpoint's ancestor chain is included so each
|
|
22
|
+
change nests under its layer/system for context. `approve` still deletes the
|
|
23
|
+
derived file (0.5.0 behaviour).
|
|
24
|
+
- The per-altitude per-layer views from 0.5.0 remain available behind
|
|
25
|
+
**`boundry diff --per-layer`** — useful for a small change, opt-in.
|
|
26
|
+
|
|
27
|
+
[#8]: https://github.com/makspiechota/boundry/issues/8
|
|
28
|
+
|
|
29
|
+
## [0.5.0] — 2026-07-21
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **`approve` no longer leaves a stale, corrupted `boundry.diff.likec4`** ([#6]).
|
|
34
|
+
`approve` used to walk the derived diff file along with the model and splice the
|
|
35
|
+
marker tokens out of its *view rules* (`style element.tag = #proposed …`,
|
|
36
|
+
`include … where tag is #proposed`), mangling them into invalid LikeC4 — so
|
|
37
|
+
`likec4 validate` (and any `arch:verify` gate) went red immediately after a
|
|
38
|
+
successful approve, recoverable only by a separate `diff` run. `approve` now
|
|
39
|
+
skips the derived file and deletes it as part of enacting: the moment the last
|
|
40
|
+
proposal is approved those views are stale — they frame changes that are now
|
|
41
|
+
approved — so the post-approve workspace is clean and validates.
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
|
|
45
|
+
- **Per-altitude diff views** ([#7]). `boundry diff` now emits a focused view for
|
|
46
|
+
*every* layer that draws a changed edge, not only the common-ancestor layer. A
|
|
47
|
+
cross-system dependency between two deeply-nested leaves is drawable at each
|
|
48
|
+
endpoint's system and container as well as at the shared root; `diff` emits one
|
|
49
|
+
`view … of <scope>` for each, so a reviewer can open the change at whatever
|
|
50
|
+
altitude matters — the whole system, a specific container — instead of being
|
|
51
|
+
dropped at the noisy root view. The scopes are derived from how LikeC4's
|
|
52
|
+
`include *` actually renders: the common ancestor plus each endpoint's ancestor
|
|
53
|
+
chain down to its parent, never *above* the common ancestor (where both
|
|
54
|
+
endpoints collapse into one child and the edge becomes an undrawn self-loop) and
|
|
55
|
+
never the leaf endpoints themselves. Boxes are unchanged — one view at the box's
|
|
56
|
+
parent layer.
|
|
57
|
+
|
|
58
|
+
### Changed
|
|
59
|
+
|
|
60
|
+
- **`verify` now compares against the accepted `boundry.lock`, not a git ref.**
|
|
61
|
+
The lock was introduced in 0.4.0 to decouple *approved* from *committed*, but
|
|
62
|
+
`verify --base <ref>` kept re-deriving its baseline from a diagram at a git ref
|
|
63
|
+
— quietly re-coupling "accepted" to "committed", the very thing the lock exists
|
|
64
|
+
to break. `verify` now reads the lock, exactly as `annotate` does, so the two
|
|
65
|
+
share one baseline and can never disagree about what "accepted" means. It also
|
|
66
|
+
catches a committed-but-*unapproved* bare edge, which the git-ref baseline
|
|
67
|
+
silently absorbed. The `--base` flag is gone from `verify` and `approve`;
|
|
68
|
+
`approve`'s anti-laundering pre-check now runs the same lock-based gate, and the
|
|
69
|
+
first `approve` (no lock yet) bootstraps the initial accepted state. **Tradeoff,
|
|
70
|
+
by design:** the baseline is the working-tree lock, so the gate leans on
|
|
71
|
+
`approve` being a human act — the lock moves only when someone approves, which
|
|
72
|
+
is why the skill forbids agents from running it.
|
|
73
|
+
|
|
74
|
+
[#6]: https://github.com/makspiechota/boundry/issues/6
|
|
75
|
+
[#7]: https://github.com/makspiechota/boundry/issues/7
|
|
76
|
+
|
|
7
77
|
## [0.4.0] — 2026-07-17
|
|
8
78
|
|
|
9
79
|
Makes the "one file can be both the communication diagram and the enforcement
|
package/README.md
CHANGED
|
@@ -227,15 +227,21 @@ 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
|
|
231
|
-
boundry approve --arch arch
|
|
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
|
|
235
|
-
appeared *without* going through a proposal. Because proposals
|
|
236
|
-
the allow-list, the newly-allowed set is exactly the set of
|
|
237
|
-
diff engine required. `approve
|
|
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.)
|
|
239
245
|
|
|
240
246
|
**To retire an edge or a box, propose its removal** with `#proposal-delete`
|
|
241
247
|
instead of deleting it. The marker colours it red; a pending deletion changes
|
|
@@ -258,12 +264,12 @@ Point your agents at
|
|
|
258
264
|
[`.claude/skills/define-architecture-boundaries`](.claude/skills/define-architecture-boundaries/SKILL.md)
|
|
259
265
|
and they'll follow this protocol.
|
|
260
266
|
|
|
261
|
-
### Catching drift —
|
|
267
|
+
### Catching drift — `annotate` (prototype)
|
|
262
268
|
|
|
263
|
-
`verify
|
|
264
|
-
*
|
|
265
|
-
accepted
|
|
266
|
-
|
|
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.
|
|
267
273
|
|
|
268
274
|
```bash
|
|
269
275
|
boundry approve --arch arch # enact proposals AND write boundry.lock
|
|
@@ -287,31 +293,43 @@ this styling back out with the marker: a `#proposed` edge returns to bare, a
|
|
|
287
293
|
`#proposed` box stays but loses its colour, a `#proposal-delete` is removed
|
|
288
294
|
outright. Requires **LikeC4 ≥ 1.58** to render.
|
|
289
295
|
|
|
290
|
-
### Reviewing a proposal —
|
|
296
|
+
### Reviewing a proposal — the diff view (prototype)
|
|
291
297
|
|
|
292
|
-
|
|
293
|
-
|
|
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`:
|
|
298
|
+
`diff` generates a single **proposed-changes** view — every pending change on one
|
|
299
|
+
landing — into a derived `boundry.diff.likec4`:
|
|
296
300
|
|
|
297
301
|
```bash
|
|
298
302
|
boundry diff --arch arch # (re)write boundry.diff.likec4
|
|
299
|
-
likec4 serve arch #
|
|
303
|
+
likec4 serve arch # open 'Boundry diff — proposed changes'
|
|
300
304
|
```
|
|
301
305
|
|
|
302
306
|
The highlighting is **generated, not hand-styled** — `diff` emits the LikeC4
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
307
|
+
rules into the derived file, so every `#proposed` box fills amber and edge goes
|
|
308
|
+
amber + solid, every `#proposal-delete` red, deterministically. That closes the
|
|
309
|
+
last manual seam: `annotate` marks, `diff` colours, no agent-dependent styling
|
|
310
|
+
step. Unchanged elements keep their defaults, and the rules live only in the
|
|
311
|
+
generated view, so your own views are untouched.
|
|
312
|
+
|
|
313
|
+
The view uses `include * -> * where tag is #proposed` (no bare `include *`), so it
|
|
314
|
+
pulls in exactly the proposed edges and the leaf modules they touch — a
|
|
315
|
+
deeply-nested proposal renders as its **own coloured node** instead of collapsing
|
|
316
|
+
into a grey ancestor. A proposed *module* with no edge yet is included too, so a
|
|
317
|
+
proposed-but-unwired box still shows. Each change nests under its layer/system for
|
|
318
|
+
context.
|
|
319
|
+
|
|
320
|
+
For a small change you can ask for the old per-layer shape instead — one focused
|
|
321
|
+
`view … of <scope>` for every layer that draws a change:
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
boundry diff --arch arch --per-layer
|
|
325
|
+
```
|
|
309
326
|
|
|
310
327
|
The file is a **derived artifact**: overwritten every run, removed when nothing is
|
|
311
|
-
proposed, so it always matches the current diagram
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
328
|
+
proposed, so it always matches the current diagram. `approve` deletes it too, as
|
|
329
|
+
part of enacting — the moment the last proposal is approved the view is stale, so
|
|
330
|
+
the post-approve workspace validates clean. It reads the diagram's own markers
|
|
331
|
+
(not the lock), so it frames whatever `annotate` or a human has marked. Being
|
|
332
|
+
derived, it's a `.gitignore` candidate (`boundry.diff.likec4`).
|
|
315
333
|
|
|
316
334
|
> Rendering the coloured diff views needs **LikeC4 ≥ 1.58** (the style-rule syntax
|
|
317
335
|
> Boundry emits). Boundry itself depends on that floor; the tool you review with
|
|
@@ -322,10 +340,10 @@ candidate (`boundry.diff.likec4`).
|
|
|
322
340
|
```
|
|
323
341
|
boundry check [--arch <dir>] [--cwd <dir>] [sources...]
|
|
324
342
|
boundry generate [--arch <dir>] [--cwd <dir>] [--out <file>]
|
|
325
|
-
boundry verify [--arch <dir>] [--cwd <dir>]
|
|
326
|
-
boundry approve [--arch <dir>] [--cwd <dir>]
|
|
343
|
+
boundry verify [--arch <dir>] [--cwd <dir>]
|
|
344
|
+
boundry approve [--arch <dir>] [--cwd <dir>]
|
|
327
345
|
boundry annotate [--arch <dir>]
|
|
328
|
-
boundry diff [--arch <dir>]
|
|
346
|
+
boundry diff [--arch <dir>] [--per-layer]
|
|
329
347
|
```
|
|
330
348
|
|
|
331
349
|
| Flag | Meaning |
|
|
@@ -333,19 +351,22 @@ boundry diff [--arch <dir>]
|
|
|
333
351
|
| `--arch <dir>` | LikeC4 workspace directory (all `.likec4` files in it are merged). Default `.`. |
|
|
334
352
|
| `--cwd <dir>` | Repo root to check. `folder` paths are relative to it. Lets you run from anywhere. |
|
|
335
353
|
| `--out <file>` | `generate` only: where to write the dependency-cruiser config. Default `.dependency-cruiser.cjs`. |
|
|
336
|
-
| `--
|
|
354
|
+
| `--per-layer` | `diff` only: emit one focused view per layer instead of the single proposed-changes view. |
|
|
337
355
|
| `sources...` | `check` only: paths to lint. Default `src`. |
|
|
338
356
|
|
|
339
357
|
- **`check`** compiles the rules and runs the linter. Exits non-zero on any violation.
|
|
340
358
|
- **`generate`** just emits the dependency-cruiser config so you can commit it or
|
|
341
359
|
run `depcruise` yourself.
|
|
342
|
-
- **`verify`** rejects dependencies granted without a proposal
|
|
343
|
-
|
|
344
|
-
|
|
360
|
+
- **`verify`** rejects dependencies granted without a proposal, comparing the
|
|
361
|
+
diagram against the accepted `boundry.lock`. Needs a lock — run `approve` once
|
|
362
|
+
to record one.
|
|
363
|
+
- **`approve`** runs the same gate, then enacts markers (strips `#proposed`,
|
|
364
|
+
removes `#proposal-delete`) and writes `boundry.lock`. For humans, not agents.
|
|
345
365
|
- **`annotate`** rewrites undeclared additions as `#proposed`, diffing against
|
|
346
366
|
`boundry.lock`.
|
|
347
|
-
- **`diff`** generates a
|
|
348
|
-
|
|
367
|
+
- **`diff`** generates a single colour-coded "proposed changes" review view into a
|
|
368
|
+
derived `boundry.diff.likec4` (or, with `--per-layer`, one focused view per layer
|
|
369
|
+
that draws a change).
|
|
349
370
|
|
|
350
371
|
Boundry warns (but does not fail) when a mapped folder matches **zero** files, and
|
|
351
372
|
**fails outright** when a check analysed no files at all — so a passing check can
|
|
@@ -366,9 +387,8 @@ jobs:
|
|
|
366
387
|
with: { node-version: 20 }
|
|
367
388
|
- run: npm ci
|
|
368
389
|
- run: npx boundry check --arch arch src
|
|
369
|
-
#
|
|
370
|
-
- run: npx boundry verify --arch arch
|
|
371
|
-
if: github.event_name == 'pull_request'
|
|
390
|
+
# Reject any dependency granted without a proposal, vs the committed lock.
|
|
391
|
+
- run: npx boundry verify --arch arch
|
|
372
392
|
```
|
|
373
393
|
|
|
374
394
|
## Programmatic use (SDK)
|
|
@@ -18,6 +18,13 @@ export declare class LikeC4Visualizer implements VisualizerPort {
|
|
|
18
18
|
* - `#proposal-delete` markers take their whole edge or box with them, so
|
|
19
19
|
* approving a proposed removal actually removes it.
|
|
20
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.
|
|
21
28
|
*/
|
|
22
29
|
approve(): Promise<void>;
|
|
23
30
|
/**
|
|
@@ -39,12 +46,26 @@ export declare class LikeC4Visualizer implements VisualizerPort {
|
|
|
39
46
|
*/
|
|
40
47
|
styleMarkers(): Promise<void>;
|
|
41
48
|
/**
|
|
42
|
-
* Generate
|
|
43
|
-
* Walks the diagram's own `#proposed` / `#proposal-delete`
|
|
44
|
-
* lock — a `#proposal-delete` has no lock delta)
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
49
|
+
* Generate the review views for the diagram's pending changes, into a derived
|
|
50
|
+
* `boundry.diff.likec4`. Walks the diagram's own `#proposed` / `#proposal-delete`
|
|
51
|
+
* markers (never the lock — a `#proposal-delete` has no lock delta).
|
|
52
|
+
*
|
|
53
|
+
* By default it emits a **single `boundry_diff` view** that draws every change at
|
|
54
|
+
* once, uncollapsed and uniformly highlighted (see `renderSingleView`) — the diff
|
|
55
|
+
* landing. With `perLayer`, it instead emits one focused `view … of <scope>` for
|
|
56
|
+
* every layer that draws a change (a box in its parent layer; an edge in every
|
|
57
|
+
* layer that draws it, per `edgeScopes`) — useful for a small change, but it
|
|
58
|
+
* multiplies views and collapses deeply-nested proposals into grey ancestors on
|
|
59
|
+
* a broad `include *`, which is why the single view is the default (issue #8).
|
|
60
|
+
*
|
|
61
|
+
* The file is overwritten each run and removed when nothing is proposed, so it
|
|
62
|
+
* always reflects the current diagram.
|
|
48
63
|
*/
|
|
49
|
-
emitDiffViews(): Promise<DiffView[]>;
|
|
64
|
+
emitDiffViews(perLayer?: boolean): Promise<DiffView[]>;
|
|
65
|
+
/**
|
|
66
|
+
* The `--per-layer` shape: buckets each change into every layer that draws it and
|
|
67
|
+
* renders one scoped `view … of <scope>` per layer. Kept as an opt-in to the
|
|
68
|
+
* single-view default.
|
|
69
|
+
*/
|
|
70
|
+
private perLayerViews;
|
|
50
71
|
}
|
|
@@ -28,6 +28,46 @@ function parentScope(fqn) {
|
|
|
28
28
|
parts.pop();
|
|
29
29
|
return parts.length ? parts.join('.') : undefined;
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Every layer that actually *draws* the edge `from -> to`, so a reviewer can open
|
|
33
|
+
* the change at whatever altitude matters — the whole system, a specific
|
|
34
|
+
* container, or the endpoint's immediate parent — not only the auto-picked common
|
|
35
|
+
* ancestor (issue #7). The set is derived from how LikeC4's `include *` renders a
|
|
36
|
+
* scoped view, verified against 1.58:
|
|
37
|
+
*
|
|
38
|
+
* - The tightest common ancestor draws it nested (both endpoints inside).
|
|
39
|
+
* - Every proper ancestor of an endpoint *below* that common ancestor draws it
|
|
40
|
+
* too: that side's subtree is expanded and the far endpoint is pulled in as a
|
|
41
|
+
* boundary node (e.g. `of s1` → `s1.c1 -> s2`).
|
|
42
|
+
* - Ancestors *above* the common ancestor are deliberately excluded: both
|
|
43
|
+
* endpoints collapse into the same child there, so the edge becomes a
|
|
44
|
+
* self-loop and LikeC4 draws nothing. The common ancestor is the widest layer
|
|
45
|
+
* that still draws it.
|
|
46
|
+
*
|
|
47
|
+
* So the scopes are the common ancestor plus each endpoint's ancestor chain from
|
|
48
|
+
* just below it down to the endpoint's parent — never the endpoints themselves
|
|
49
|
+
* (a view scoped *to* a leaf endpoint has an empty subtree).
|
|
50
|
+
*/
|
|
51
|
+
function edgeScopes(from, to) {
|
|
52
|
+
const common = commonScope(from, to);
|
|
53
|
+
const depthOf = (scope) => (scope ? scope.split('.').length : 0);
|
|
54
|
+
const commonDepth = depthOf(common);
|
|
55
|
+
const byId = new Map();
|
|
56
|
+
const add = (scope) => {
|
|
57
|
+
byId.set(scopeViewId(scope), scope);
|
|
58
|
+
};
|
|
59
|
+
add(common);
|
|
60
|
+
for (const endpoint of [from, to]) {
|
|
61
|
+
const parts = endpoint.split('.');
|
|
62
|
+
// Proper ancestors of the endpoint strictly below the common ancestor: prefix
|
|
63
|
+
// lengths (commonDepth + 1) .. (parts.length - 1). The last is the endpoint's
|
|
64
|
+
// parent; the endpoint itself (full length) is never a scope.
|
|
65
|
+
for (let len = commonDepth + 1; len <= parts.length - 1; len++) {
|
|
66
|
+
add(parts.slice(0, len).join('.'));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return [...byId.values()];
|
|
70
|
+
}
|
|
31
71
|
/**
|
|
32
72
|
* Deterministic highlight rules for the marker tags actually in use. Referencing
|
|
33
73
|
* an undeclared tag is a hard LikeC4 error, and a tag can only be *used* if it is
|
|
@@ -64,6 +104,61 @@ function renderDiffView(scope, highlight) {
|
|
|
64
104
|
const lines = [` title 'Boundry diff · ${scope ?? 'root'}'`, ' include *', ...highlight];
|
|
65
105
|
return `${opener}\n${lines.join('\n')}\n }`;
|
|
66
106
|
}
|
|
107
|
+
/** Every proper ancestor of an element, shallowest first — its parent chain up to (not including) the root. */
|
|
108
|
+
function ancestorFqns(fqn) {
|
|
109
|
+
const parts = fqn.split('.');
|
|
110
|
+
const out = [];
|
|
111
|
+
for (let len = 1; len <= parts.length - 1; len++)
|
|
112
|
+
out.push(parts.slice(0, len).join('.'));
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The single `boundry_diff` view: one landing that draws *every* pending change at
|
|
117
|
+
* once, with uniform highlighting and no empty views. It sidesteps the per-layer
|
|
118
|
+
* shape's two failure modes (issue #8):
|
|
119
|
+
*
|
|
120
|
+
* - **No collapse.** `include * -> * where tag is #proposed` (with NO bare
|
|
121
|
+
* `include *`) pulls in exactly the proposed edges and the leaf modules they
|
|
122
|
+
* touch — so a deeply-nested proposed leaf renders as its own amber node
|
|
123
|
+
* instead of collapsing into a grey ancestor, as it does under `include *`.
|
|
124
|
+
* - **Standalone boxes.** A `#proposed` box with no edge is included by fqn, so
|
|
125
|
+
* a proposed-but-unwired module still shows (and is coloured by the style rule).
|
|
126
|
+
*
|
|
127
|
+
* Every endpoint's and box's full ancestor chain is included too, so each change
|
|
128
|
+
* nests under its layer/system for context (verified against LikeC4 1.58). Rules
|
|
129
|
+
* are emitted only for marker tags actually in use — referencing an undeclared tag
|
|
130
|
+
* is a hard LikeC4 error.
|
|
131
|
+
*/
|
|
132
|
+
function renderSingleView(edges, boxes, usedTags) {
|
|
133
|
+
const lines = [` title 'Boundry diff — proposed changes'`];
|
|
134
|
+
// Edge includes: one per marker that appears on an edge. These pull the proposed
|
|
135
|
+
// edges and their endpoint leaves in at leaf level, uncollapsed.
|
|
136
|
+
if (edges.some((e) => e.marker === '#proposed')) {
|
|
137
|
+
lines.push(' include * -> * where tag is #proposed with { color amber; line solid }');
|
|
138
|
+
}
|
|
139
|
+
if (edges.some((e) => e.marker === '#proposal-delete')) {
|
|
140
|
+
lines.push(' include * -> * where tag is #proposal-delete with { color red; line solid }');
|
|
141
|
+
}
|
|
142
|
+
// Element includes: the proposed/proposal-delete boxes themselves (so a standalone
|
|
143
|
+
// one shows), plus the full ancestor chain of every endpoint and box (so each
|
|
144
|
+
// change nests under its containers). Deterministic: sorted, de-duplicated.
|
|
145
|
+
const includeFqns = new Set();
|
|
146
|
+
for (const box of boxes)
|
|
147
|
+
includeFqns.add(box.fqn);
|
|
148
|
+
const anchors = [...edges.flatMap((e) => [e.from, e.to]), ...boxes.map((b) => b.fqn)];
|
|
149
|
+
for (const fqn of anchors)
|
|
150
|
+
for (const ancestor of ancestorFqns(fqn))
|
|
151
|
+
includeFqns.add(ancestor);
|
|
152
|
+
if (includeFqns.size) {
|
|
153
|
+
lines.push(` include ${[...includeFqns].sort().join(', ')}`);
|
|
154
|
+
}
|
|
155
|
+
// Style rules colour the tagged boxes (edges are coloured by their include above).
|
|
156
|
+
if (usedTags.has('proposed'))
|
|
157
|
+
lines.push(' style element.tag = #proposed { color amber }');
|
|
158
|
+
if (usedTags.has('proposal-delete'))
|
|
159
|
+
lines.push(' style element.tag = #proposal-delete { color red }');
|
|
160
|
+
return ` view boundry_diff {\n${lines.join('\n')}\n }`;
|
|
161
|
+
}
|
|
67
162
|
/**
|
|
68
163
|
* An exemption pattern has to be a usable regex before it reaches the linter. A
|
|
69
164
|
* broken or empty one would otherwise be a silent hole: it exempts files from
|
|
@@ -393,11 +488,20 @@ export class LikeC4Visualizer {
|
|
|
393
488
|
* - `#proposal-delete` markers take their whole edge or box with them, so
|
|
394
489
|
* approving a proposed removal actually removes it.
|
|
395
490
|
* All other formatting is left intact.
|
|
491
|
+
*
|
|
492
|
+
* The derived `boundry.diff.likec4` is skipped, not approved: it holds no real
|
|
493
|
+
* markers, only view rules that *reference* the marker tags (`style element.tag
|
|
494
|
+
* = #proposed`, `include … where tag is #proposed`). Splicing those `#proposed`
|
|
495
|
+
* tokens out would corrupt the rules into invalid LikeC4. And it is stale the
|
|
496
|
+
* moment the last proposal is enacted — it describes changes that are now
|
|
497
|
+
* approved — so approve deletes it, leaving a workspace `likec4 validate` accepts.
|
|
396
498
|
*/
|
|
397
499
|
async approve() {
|
|
398
500
|
const likec4 = await LikeC4.fromWorkspace(this.workspaceDir);
|
|
399
501
|
for (const doc of likec4.LangiumDocuments.all) {
|
|
400
502
|
const filePath = doc.uri.fsPath;
|
|
503
|
+
if (filePath.endsWith(DIFF_FILE))
|
|
504
|
+
continue;
|
|
401
505
|
const text = doc.textDocument?.getText?.() ?? readFileSync(filePath, 'utf8');
|
|
402
506
|
const ranges = collectApprovalRanges(doc.parseResult?.value, text);
|
|
403
507
|
if (ranges.length === 0)
|
|
@@ -408,6 +512,12 @@ export class LikeC4Visualizer {
|
|
|
408
512
|
}
|
|
409
513
|
writeFileSync(filePath, next);
|
|
410
514
|
}
|
|
515
|
+
// Enacting the last proposal makes any diff views stale — they frame changes
|
|
516
|
+
// that are now approved. Remove the derived artifact so the post-approve
|
|
517
|
+
// workspace validates and never renders an approved-away change.
|
|
518
|
+
const diffPath = join(this.workspaceDir, DIFF_FILE);
|
|
519
|
+
if (existsSync(diffPath))
|
|
520
|
+
rmSync(diffPath);
|
|
411
521
|
}
|
|
412
522
|
/**
|
|
413
523
|
* Deterministically mark the given edges and elements `#proposed`. The inverse
|
|
@@ -527,26 +637,28 @@ export class LikeC4Visualizer {
|
|
|
527
637
|
}
|
|
528
638
|
}
|
|
529
639
|
/**
|
|
530
|
-
* Generate
|
|
531
|
-
* Walks the diagram's own `#proposed` / `#proposal-delete`
|
|
532
|
-
* lock — a `#proposal-delete` has no lock delta)
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
640
|
+
* Generate the review views for the diagram's pending changes, into a derived
|
|
641
|
+
* `boundry.diff.likec4`. Walks the diagram's own `#proposed` / `#proposal-delete`
|
|
642
|
+
* markers (never the lock — a `#proposal-delete` has no lock delta).
|
|
643
|
+
*
|
|
644
|
+
* By default it emits a **single `boundry_diff` view** that draws every change at
|
|
645
|
+
* once, uncollapsed and uniformly highlighted (see `renderSingleView`) — the diff
|
|
646
|
+
* landing. With `perLayer`, it instead emits one focused `view … of <scope>` for
|
|
647
|
+
* every layer that draws a change (a box in its parent layer; an edge in every
|
|
648
|
+
* layer that draws it, per `edgeScopes`) — useful for a small change, but it
|
|
649
|
+
* multiplies views and collapses deeply-nested proposals into grey ancestors on
|
|
650
|
+
* a broad `include *`, which is why the single view is the default (issue #8).
|
|
651
|
+
*
|
|
652
|
+
* The file is overwritten each run and removed when nothing is proposed, so it
|
|
653
|
+
* always reflects the current diagram.
|
|
536
654
|
*/
|
|
537
|
-
async emitDiffViews() {
|
|
655
|
+
async emitDiffViews(perLayer = false) {
|
|
538
656
|
const likec4 = await LikeC4.fromWorkspace(this.workspaceDir);
|
|
539
|
-
//
|
|
540
|
-
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
const layer = layers.get(key) ?? { scope, changes: 0 };
|
|
545
|
-
layer.changes += 1;
|
|
546
|
-
layers.set(key, layer);
|
|
547
|
-
};
|
|
548
|
-
// Which marker tags actually appear — so the emitted highlight rules only ever
|
|
549
|
-
// reference declared tags (an undeclared tag reference is a hard LikeC4 error).
|
|
657
|
+
// Lift every marked edge and box once; both view shapes are built from this.
|
|
658
|
+
const edges = [];
|
|
659
|
+
const boxes = [];
|
|
660
|
+
// Which marker tags actually appear — so the emitted rules only ever reference
|
|
661
|
+
// declared tags (an undeclared tag reference is a hard LikeC4 error).
|
|
550
662
|
const usedTags = new Set();
|
|
551
663
|
for (const doc of likec4.LangiumDocuments.all) {
|
|
552
664
|
const filePath = doc.uri.fsPath;
|
|
@@ -563,23 +675,53 @@ export class LikeC4Visualizer {
|
|
|
563
675
|
const from = fqnOf(endpointElement(statement.source));
|
|
564
676
|
const to = fqnOf(endpointElement(statement.target));
|
|
565
677
|
if (from && to)
|
|
566
|
-
|
|
678
|
+
edges.push({ from, to, marker });
|
|
567
679
|
}
|
|
568
680
|
else if (statement?.$type === 'Element') {
|
|
569
681
|
const fqn = fqnOf(statement);
|
|
570
682
|
if (fqn)
|
|
571
|
-
|
|
683
|
+
boxes.push({ fqn, marker });
|
|
572
684
|
}
|
|
573
685
|
});
|
|
574
686
|
}
|
|
575
687
|
const diffPath = join(this.workspaceDir, DIFF_FILE);
|
|
576
|
-
if (
|
|
688
|
+
if (edges.length === 0 && boxes.length === 0) {
|
|
577
689
|
// Nothing proposed — clear any stale views so `serve` never renders a diff
|
|
578
690
|
// that no longer exists (and never dangles a reference to an approved-away box).
|
|
579
691
|
if (existsSync(diffPath))
|
|
580
692
|
rmSync(diffPath);
|
|
581
693
|
return [];
|
|
582
694
|
}
|
|
695
|
+
const { header, body, views } = perLayer
|
|
696
|
+
? this.perLayerViews(edges, boxes, usedTags)
|
|
697
|
+
: {
|
|
698
|
+
header: 'a single landing view of every pending change',
|
|
699
|
+
body: renderSingleView(edges, boxes, usedTags),
|
|
700
|
+
views: [{ id: 'boundry_diff', changes: edges.length + boxes.length }],
|
|
701
|
+
};
|
|
702
|
+
writeFileSync(diffPath, `// Generated by \`boundry diff\` — ${header}.\n` +
|
|
703
|
+
`// Derived artifact: regenerate after approve; do not hand-edit.\n` +
|
|
704
|
+
`views {\n${body}\n}\n`);
|
|
705
|
+
return views;
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* The `--per-layer` shape: buckets each change into every layer that draws it and
|
|
709
|
+
* renders one scoped `view … of <scope>` per layer. Kept as an opt-in to the
|
|
710
|
+
* single-view default.
|
|
711
|
+
*/
|
|
712
|
+
perLayerViews(edges, boxes, usedTags) {
|
|
713
|
+
const layers = new Map();
|
|
714
|
+
const record = (scope) => {
|
|
715
|
+
const key = scopeViewId(scope);
|
|
716
|
+
const layer = layers.get(key) ?? { scope, changes: 0 };
|
|
717
|
+
layer.changes += 1;
|
|
718
|
+
layers.set(key, layer);
|
|
719
|
+
};
|
|
720
|
+
for (const edge of edges)
|
|
721
|
+
for (const scope of edgeScopes(edge.from, edge.to))
|
|
722
|
+
record(scope);
|
|
723
|
+
for (const box of boxes)
|
|
724
|
+
record(parentScope(box.fqn));
|
|
583
725
|
// Root first, then by fqn, so the output is stable across runs.
|
|
584
726
|
const ordered = [...layers.values()].sort((a, b) => {
|
|
585
727
|
if (!a.scope)
|
|
@@ -589,14 +731,14 @@ export class LikeC4Visualizer {
|
|
|
589
731
|
return a.scope.localeCompare(b.scope);
|
|
590
732
|
});
|
|
591
733
|
const highlight = highlightLines(usedTags);
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
}
|
|
734
|
+
return {
|
|
735
|
+
header: 'one focused view per layer with a pending change',
|
|
736
|
+
body: ordered.map((layer) => renderDiffView(layer.scope, highlight)).join('\n'),
|
|
737
|
+
views: ordered.map((layer) => ({
|
|
738
|
+
id: scopeViewId(layer.scope),
|
|
739
|
+
scope: layer.scope,
|
|
740
|
+
changes: layer.changes,
|
|
741
|
+
})),
|
|
742
|
+
};
|
|
601
743
|
}
|
|
602
744
|
}
|
package/dist/cli/index.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { existsSync, mkdtempSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
5
3
|
import { join, relative, resolve } from 'node:path';
|
|
6
4
|
import { Pipeline } from '../core/pipeline/pipeline.js';
|
|
7
5
|
import { LikeC4Visualizer } from '../adapters/visualizer/likec4.js';
|
|
8
6
|
import { DepCruiserEnforcer } from '../adapters/enforcer/depcruiser.js';
|
|
9
|
-
const USAGE = 'usage: boundry <generate|check|approve|verify|annotate|diff> [--arch <dir>] [--
|
|
7
|
+
const USAGE = 'usage: boundry <generate|check|approve|verify|annotate|diff> [--arch <dir>] [--cwd <dir>] [--out <file>] [--per-layer] [sources...]';
|
|
10
8
|
function optValue(args, flag) {
|
|
11
9
|
const i = args.indexOf(flag);
|
|
12
10
|
return i >= 0 ? args[i + 1] : undefined;
|
|
@@ -22,30 +20,6 @@ function positionals(args) {
|
|
|
22
20
|
}
|
|
23
21
|
return out;
|
|
24
22
|
}
|
|
25
|
-
/**
|
|
26
|
-
* Materialize the architecture as of a git ref into a temp workspace, so the
|
|
27
|
-
* previously-approved boundary model can be lifted and compared against HEAD.
|
|
28
|
-
*/
|
|
29
|
-
function materializeArchAt(archDir, ref) {
|
|
30
|
-
const gitRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
31
|
-
cwd: archDir,
|
|
32
|
-
encoding: 'utf8',
|
|
33
|
-
}).trim();
|
|
34
|
-
const workDir = mkdtempSync(join(tmpdir(), 'boundry-base-'));
|
|
35
|
-
for (const file of readdirSync(archDir).filter((f) => f.endsWith('.likec4'))) {
|
|
36
|
-
try {
|
|
37
|
-
const content = execFileSync('git', ['show', `${ref}:${relative(gitRoot, join(archDir, file))}`], {
|
|
38
|
-
cwd: gitRoot,
|
|
39
|
-
encoding: 'utf8',
|
|
40
|
-
});
|
|
41
|
-
writeFileSync(join(workDir, file), content);
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
// The file did not exist at `ref` — everything it declares is new.
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return workDir;
|
|
48
|
-
}
|
|
49
23
|
function countGrants(granted) {
|
|
50
24
|
return granted.edges.length + granted.exemptions.length;
|
|
51
25
|
}
|
|
@@ -69,17 +43,17 @@ async function main() {
|
|
|
69
43
|
const archDir = resolve(optValue(rest, '--arch') ?? '.');
|
|
70
44
|
const outArg = optValue(rest, '--out');
|
|
71
45
|
const outFile = outArg ? resolve(outArg) : undefined;
|
|
72
|
-
const baseRef = optValue(rest, '--base');
|
|
73
46
|
// `--cwd` lets you check a repo without cd-ing into it. `folder` metadata is
|
|
74
47
|
// relative to the target repo root, so the enforcer runs from there.
|
|
75
48
|
const cwd = optValue(rest, '--cwd');
|
|
76
49
|
if (cwd)
|
|
77
50
|
process.chdir(resolve(cwd));
|
|
78
51
|
// The accepted-state lock lives beside the diagram it locks. `approve` writes
|
|
79
|
-
// it; `annotate`
|
|
52
|
+
// it; `verify` and `annotate` read it — the baseline Boundry owns, decoupled
|
|
53
|
+
// from git, so "accepted" never means merely "committed".
|
|
80
54
|
const lockFile = join(archDir, 'boundry.lock');
|
|
55
|
+
const readLock = () => existsSync(lockFile) ? readFileSync(lockFile, 'utf8') : undefined;
|
|
81
56
|
const pipeline = new Pipeline(new LikeC4Visualizer(archDir), new DepCruiserEnforcer());
|
|
82
|
-
const grantedSince = (ref) => pipeline.verify(new LikeC4Visualizer(materializeArchAt(archDir, ref)));
|
|
83
57
|
if (command === 'generate') {
|
|
84
58
|
const config = await pipeline.generate();
|
|
85
59
|
const out = outFile ?? resolve(config.filename);
|
|
@@ -105,37 +79,43 @@ async function main() {
|
|
|
105
79
|
return;
|
|
106
80
|
}
|
|
107
81
|
if (command === 'verify') {
|
|
108
|
-
|
|
109
|
-
|
|
82
|
+
const lock = readLock();
|
|
83
|
+
if (!lock) {
|
|
84
|
+
console.error(`Boundry: no ${relative(process.cwd(), lockFile)} — run 'boundry approve' first to record the accepted state`);
|
|
110
85
|
process.exitCode = 2;
|
|
111
86
|
return;
|
|
112
87
|
}
|
|
113
|
-
const granted = await
|
|
88
|
+
const granted = await pipeline.verify(lock);
|
|
114
89
|
if (countGrants(granted) === 0) {
|
|
115
|
-
console.log(
|
|
90
|
+
console.log('Boundry: ✓ nothing granted without a #proposed marker (vs the accepted lock)');
|
|
116
91
|
return;
|
|
117
92
|
}
|
|
118
|
-
console.error(`Boundry: ✗ ${countGrants(granted)} grant(s) made without a #proposed marker (vs
|
|
93
|
+
console.error(`Boundry: ✗ ${countGrants(granted)} grant(s) made without a #proposed marker (vs the accepted lock)`);
|
|
119
94
|
listGrants(granted);
|
|
120
95
|
process.exitCode = 1;
|
|
121
96
|
return;
|
|
122
97
|
}
|
|
123
98
|
if (command === 'approve') {
|
|
124
99
|
// Approving must never launder an edge that skipped the proposal protocol.
|
|
125
|
-
|
|
126
|
-
|
|
100
|
+
// The accepted lock is the baseline: a bare edge added since the last approve
|
|
101
|
+
// is in the allow-list but not the lock, so it surfaces here; a #proposed one
|
|
102
|
+
// is excluded from the allow-list, so it passes and gets enacted below. The
|
|
103
|
+
// first approve has no lock yet — it establishes the initial accepted state.
|
|
104
|
+
const lock = readLock();
|
|
105
|
+
if (lock) {
|
|
106
|
+
const granted = await pipeline.verify(lock);
|
|
127
107
|
if (countGrants(granted) > 0) {
|
|
128
|
-
console.error(`Boundry: ✗ refusing to approve — ${countGrants(granted)} grant(s) were made without a #proposed marker (vs
|
|
108
|
+
console.error(`Boundry: ✗ refusing to approve — ${countGrants(granted)} grant(s) were made without a #proposed marker (vs the accepted lock)`);
|
|
129
109
|
listGrants(granted);
|
|
130
110
|
process.exitCode = 1;
|
|
131
111
|
return;
|
|
132
112
|
}
|
|
133
113
|
}
|
|
134
114
|
else {
|
|
135
|
-
console.error('Boundry: ⚠ no
|
|
115
|
+
console.error('Boundry: ⚠ no boundry.lock yet — approving to record the initial accepted state');
|
|
136
116
|
}
|
|
137
|
-
const
|
|
138
|
-
writeFileSync(lockFile,
|
|
117
|
+
const nextLock = await pipeline.approve();
|
|
118
|
+
writeFileSync(lockFile, nextLock);
|
|
139
119
|
console.log('Boundry: ✓ approved — enacted the diagram and wrote', relative(process.cwd(), lockFile));
|
|
140
120
|
return;
|
|
141
121
|
}
|
|
@@ -159,7 +139,8 @@ async function main() {
|
|
|
159
139
|
return;
|
|
160
140
|
}
|
|
161
141
|
if (command === 'diff') {
|
|
162
|
-
const
|
|
142
|
+
const perLayer = rest.includes('--per-layer');
|
|
143
|
+
const views = await pipeline.diffViews(perLayer);
|
|
163
144
|
const diffFile = join(archDir, 'boundry.diff.likec4');
|
|
164
145
|
if (views.length === 0) {
|
|
165
146
|
console.log('Boundry: ✓ nothing proposed — no diff views to draw');
|
|
@@ -167,10 +148,10 @@ async function main() {
|
|
|
167
148
|
}
|
|
168
149
|
console.log(`Boundry: ✎ wrote ${views.length} diff view(s) to ${relative(process.cwd(), diffFile)}:`);
|
|
169
150
|
for (const view of views) {
|
|
170
|
-
const
|
|
171
|
-
console.log(` ${view.id} (
|
|
151
|
+
const where = perLayer ? `layer ${view.scope ?? 'root'}, ` : '';
|
|
152
|
+
console.log(` ${view.id} (${where}${view.changes} change(s))`);
|
|
172
153
|
}
|
|
173
|
-
console.log(' Open the diagram in `likec4 serve` to review
|
|
154
|
+
console.log(' Open the diagram in `likec4 serve` to review.');
|
|
174
155
|
return;
|
|
175
156
|
}
|
|
176
157
|
console.error(USAGE);
|
|
@@ -31,12 +31,18 @@ export declare class Pipeline {
|
|
|
31
31
|
/** Diagram -> boundary model -> run the linter over `sources`. */
|
|
32
32
|
check(sources: string[]): Promise<CheckResult>;
|
|
33
33
|
/**
|
|
34
|
-
* What this diagram grants that `
|
|
35
|
-
* `#proposed` marker, and importer exemptions added. Proposals
|
|
36
|
-
* from the allow-list, so anything reported here bypassed the
|
|
37
|
-
* protocol.
|
|
34
|
+
* What this diagram grants that the accepted `lock` did not — dependencies
|
|
35
|
+
* added without a `#proposed` marker, and importer exemptions added. Proposals
|
|
36
|
+
* are excluded from the allow-list, so anything reported here bypassed the
|
|
37
|
+
* approval protocol.
|
|
38
|
+
*
|
|
39
|
+
* The baseline is the lock — the accepted state `approve` records — not a git
|
|
40
|
+
* ref. The lock exists precisely to decouple "accepted" from "committed", so
|
|
41
|
+
* verify reads from it, exactly as `annotate` does; the two can never disagree
|
|
42
|
+
* about what "accepted" means. It also catches a committed-but-unapproved bare
|
|
43
|
+
* edge, which re-deriving a baseline from a git ref would silently absorb.
|
|
38
44
|
*/
|
|
39
|
-
verify(
|
|
45
|
+
verify(lock: string): Promise<VerifyResult>;
|
|
40
46
|
/**
|
|
41
47
|
* Enact the diagram's pending proposals (strip `#proposed`, remove
|
|
42
48
|
* `#proposal-delete`), then return the resulting accepted model as a lock
|
|
@@ -54,11 +60,11 @@ export declare class Pipeline {
|
|
|
54
60
|
*/
|
|
55
61
|
annotate(lock: string): Promise<AnnotateResult>;
|
|
56
62
|
/**
|
|
57
|
-
* (Re)generate
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
63
|
+
* (Re)generate the review views for the diagram's pending changes. By default a
|
|
64
|
+
* single `boundry_diff` landing view draws every `#proposed` / `#proposal-delete`
|
|
65
|
+
* change at once; with `perLayer`, one focused view per layer that draws a change.
|
|
66
|
+
* Reads the diagram's own markers, so it composes with `annotate` (annotate marks
|
|
67
|
+
* drift, this frames it).
|
|
62
68
|
*/
|
|
63
|
-
diffViews(): Promise<DiffView[]>;
|
|
69
|
+
diffViews(perLayer?: boolean): Promise<DiffView[]>;
|
|
64
70
|
}
|
|
@@ -21,16 +21,23 @@ export class Pipeline {
|
|
|
21
21
|
return this.enforcer.check(model, sources);
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
|
-
* What this diagram grants that `
|
|
25
|
-
* `#proposed` marker, and importer exemptions added. Proposals
|
|
26
|
-
* from the allow-list, so anything reported here bypassed the
|
|
27
|
-
* protocol.
|
|
24
|
+
* What this diagram grants that the accepted `lock` did not — dependencies
|
|
25
|
+
* added without a `#proposed` marker, and importer exemptions added. Proposals
|
|
26
|
+
* are excluded from the allow-list, so anything reported here bypassed the
|
|
27
|
+
* approval protocol.
|
|
28
|
+
*
|
|
29
|
+
* The baseline is the lock — the accepted state `approve` records — not a git
|
|
30
|
+
* ref. The lock exists precisely to decouple "accepted" from "committed", so
|
|
31
|
+
* verify reads from it, exactly as `annotate` does; the two can never disagree
|
|
32
|
+
* about what "accepted" means. It also catches a committed-but-unapproved bare
|
|
33
|
+
* edge, which re-deriving a baseline from a git ref would silently absorb.
|
|
28
34
|
*/
|
|
29
|
-
async verify(
|
|
30
|
-
const
|
|
35
|
+
async verify(lock) {
|
|
36
|
+
const base = parseModel(lock);
|
|
37
|
+
const head = await this.visualizer.read();
|
|
31
38
|
return {
|
|
32
|
-
edges: newlyAllowedEdges(
|
|
33
|
-
exemptions: newlyExemptedImporters(
|
|
39
|
+
edges: newlyAllowedEdges(base, head),
|
|
40
|
+
exemptions: newlyExemptedImporters(base, head),
|
|
34
41
|
};
|
|
35
42
|
}
|
|
36
43
|
/**
|
|
@@ -62,13 +69,13 @@ export class Pipeline {
|
|
|
62
69
|
return { edges, modules };
|
|
63
70
|
}
|
|
64
71
|
/**
|
|
65
|
-
* (Re)generate
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
72
|
+
* (Re)generate the review views for the diagram's pending changes. By default a
|
|
73
|
+
* single `boundry_diff` landing view draws every `#proposed` / `#proposal-delete`
|
|
74
|
+
* change at once; with `perLayer`, one focused view per layer that draws a change.
|
|
75
|
+
* Reads the diagram's own markers, so it composes with `annotate` (annotate marks
|
|
76
|
+
* drift, this frames it).
|
|
70
77
|
*/
|
|
71
|
-
async diffViews() {
|
|
72
|
-
return this.visualizer.emitDiffViews();
|
|
78
|
+
async diffViews(perLayer = false) {
|
|
79
|
+
return this.visualizer.emitDiffViews(perLayer);
|
|
73
80
|
}
|
|
74
81
|
}
|
|
@@ -23,25 +23,26 @@ export interface VisualizerPort {
|
|
|
23
23
|
*/
|
|
24
24
|
styleMarkers(): Promise<void>;
|
|
25
25
|
/**
|
|
26
|
-
* (Re)generate
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
26
|
+
* (Re)generate the review views for the diagram's pending changes — edges and
|
|
27
|
+
* boxes tagged `#proposed` or `#proposal-delete`. By default a single
|
|
28
|
+
* `boundry_diff` landing view draws every change at once, uncollapsed and
|
|
29
|
+
* uniformly highlighted. With `perLayer`, one focused `view … of <scope>` is
|
|
30
|
+
* emitted per layer that draws a change instead. A derived artifact: overwritten
|
|
31
31
|
* each run, and removed when nothing is proposed. Returns one entry per view.
|
|
32
32
|
*/
|
|
33
|
-
emitDiffViews(): Promise<DiffView[]>;
|
|
33
|
+
emitDiffViews(perLayer?: boolean): Promise<DiffView[]>;
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
|
-
* One emitted diff view
|
|
37
|
-
* pending change
|
|
36
|
+
* One emitted diff view. For the default single view there is one entry
|
|
37
|
+
* (`boundry_diff`) counting every pending change; in `perLayer` mode there is one
|
|
38
|
+
* per layer, each scoped to an element and counting the changes that fall in it.
|
|
38
39
|
*/
|
|
39
40
|
export interface DiffView {
|
|
40
|
-
/** The generated view id
|
|
41
|
+
/** The generated view id — `boundry_diff`, or `boundry_diff_<scope>` per layer. */
|
|
41
42
|
id: string;
|
|
42
|
-
/**
|
|
43
|
+
/** perLayer only: the scope element's fqn, or undefined for the model root. */
|
|
43
44
|
scope?: string;
|
|
44
|
-
/** How many pending `#proposed` / `#proposal-delete` changes this
|
|
45
|
+
/** How many pending `#proposed` / `#proposal-delete` changes this view holds. */
|
|
45
46
|
changes: number;
|
|
46
47
|
}
|
|
47
48
|
/** A generated linter config artifact. */
|
package/package.json
CHANGED