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 +82 -1
- package/README.md +123 -4
- package/dist/adapters/enforcer/depcruiser.js +42 -24
- package/dist/adapters/visualizer/likec4.d.ts +36 -6
- package/dist/adapters/visualizer/likec4.js +473 -39
- package/dist/cli/index.js +42 -4
- package/dist/core/model/boundary-model.d.ts +29 -3
- package/dist/core/model/boundary-model.js +40 -0
- package/dist/core/pipeline/pipeline.d.ts +31 -4
- package/dist/core/pipeline/pipeline.js +37 -3
- package/dist/core/ports/ports.d.ts +37 -1
- package/package.json +2 -2
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.
|
|
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
|
|
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
|
|
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
|
|
6
|
-
return
|
|
5
|
+
function normalizePath(path) {
|
|
6
|
+
return path.replace(/\/+$/, '');
|
|
7
7
|
}
|
|
8
|
-
function
|
|
9
|
-
|
|
10
|
-
return `^${escaped}/`;
|
|
8
|
+
function escapeRegex(path) {
|
|
9
|
+
return normalizePath(path).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
11
10
|
}
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
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) =>
|
|
37
|
-
.map((other) =>
|
|
55
|
+
.filter((other) => isNestedUnder(other, m))
|
|
56
|
+
.map((other) => moduleScopeRegex(other));
|
|
38
57
|
scopeOf.set(m.id, descendants.length
|
|
39
|
-
? { path:
|
|
40
|
-
: { path:
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
131
|
-
|
|
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 = `${
|
|
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 (
|
|
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
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
}
|