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
|
@@ -2,14 +2,25 @@
|
|
|
2
2
|
* The single, source-agnostic representation everything in Boundry compiles
|
|
3
3
|
* from. A visualizer adapter produces it; an enforcer adapter renders it.
|
|
4
4
|
*/
|
|
5
|
-
/** A unit of architecture that maps to a folder of source
|
|
5
|
+
/** A unit of architecture that maps to a folder or a single file of source. */
|
|
6
6
|
export interface Module {
|
|
7
7
|
/** Stable id, taken from the diagram element. */
|
|
8
8
|
id: string;
|
|
9
9
|
/** Human-readable name for messages. */
|
|
10
10
|
title: string;
|
|
11
|
-
/**
|
|
12
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Source path the module owns — a folder prefix (e.g. "src/core") or a single
|
|
13
|
+
* file (e.g. "src/ports/contract.ts"), per `kind`.
|
|
14
|
+
*/
|
|
15
|
+
path: string;
|
|
16
|
+
/**
|
|
17
|
+
* Whether `path` is a folder (owns everything under it) or a single file
|
|
18
|
+
* (owns exactly that file). A file leaf lets a guarded contract or port sit
|
|
19
|
+
* beside its sibling sub-folders in a nested diagram, so its edges stay
|
|
20
|
+
* sibling-to-sibling — which is what keeps a deep C4 tree legal in LikeC4
|
|
21
|
+
* (ancestor↔descendant relationships are rejected) *and* enforceable.
|
|
22
|
+
*/
|
|
23
|
+
kind: 'folder' | 'file';
|
|
13
24
|
}
|
|
14
25
|
/** A permitted dependency: modules in `from` may import modules in `to`. */
|
|
15
26
|
export interface AllowedEdge {
|
|
@@ -71,3 +82,18 @@ export declare function newlyExemptedImporters(base: BoundaryModel, head: Bounda
|
|
|
71
82
|
* dependencies granted WITHOUT going through a proposal — i.e. self-approvals.
|
|
72
83
|
*/
|
|
73
84
|
export declare function newlyAllowedEdges(base: BoundaryModel, head: BoundaryModel): AllowedEdge[];
|
|
85
|
+
/**
|
|
86
|
+
* Modules present at `head` but not at `base`, keyed by id. A `#proposed` element
|
|
87
|
+
* stays a module (the marker is visual-only), so this reports both bare-added and
|
|
88
|
+
* already-proposed new boxes — the annotator makes marking idempotent.
|
|
89
|
+
*/
|
|
90
|
+
export declare function newlyAddedModules(base: BoundaryModel, head: BoundaryModel): Module[];
|
|
91
|
+
/**
|
|
92
|
+
* The accepted state, written as a canonical string — the lock. `approve` records
|
|
93
|
+
* it so change detection has a baseline it OWNS, rather than trusting whatever git
|
|
94
|
+
* happens to hold. Deterministic: fields and lists are sorted, so the same model
|
|
95
|
+
* always serializes byte-for-byte the same.
|
|
96
|
+
*/
|
|
97
|
+
export declare function serializeModel(model: BoundaryModel): string;
|
|
98
|
+
/** Read a lock back into a model. Inverse of {@link serializeModel}. */
|
|
99
|
+
export declare function parseModel(text: string): BoundaryModel;
|
|
@@ -34,3 +34,43 @@ export function newlyAllowedEdges(base, head) {
|
|
|
34
34
|
const allowedAtBase = new Set(base.allowed.map(edgeKey));
|
|
35
35
|
return head.allowed.filter((edge) => !allowedAtBase.has(edgeKey(edge)));
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Modules present at `head` but not at `base`, keyed by id. A `#proposed` element
|
|
39
|
+
* stays a module (the marker is visual-only), so this reports both bare-added and
|
|
40
|
+
* already-proposed new boxes — the annotator makes marking idempotent.
|
|
41
|
+
*/
|
|
42
|
+
export function newlyAddedModules(base, head) {
|
|
43
|
+
const atBase = new Set(base.modules.map((m) => m.id));
|
|
44
|
+
return head.modules.filter((m) => !atBase.has(m.id));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The accepted state, written as a canonical string — the lock. `approve` records
|
|
48
|
+
* it so change detection has a baseline it OWNS, rather than trusting whatever git
|
|
49
|
+
* happens to hold. Deterministic: fields and lists are sorted, so the same model
|
|
50
|
+
* always serializes byte-for-byte the same.
|
|
51
|
+
*/
|
|
52
|
+
export function serializeModel(model) {
|
|
53
|
+
const canonical = {
|
|
54
|
+
modules: [...model.modules]
|
|
55
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
56
|
+
.map((m) => ({ id: m.id, title: m.title, path: m.path, kind: m.kind })),
|
|
57
|
+
allowed: [...model.allowed]
|
|
58
|
+
.sort((a, b) => edgeKey(a).localeCompare(edgeKey(b)))
|
|
59
|
+
.map((e) => ({ from: e.from, to: e.to })),
|
|
60
|
+
wildcards: [...model.wildcards].sort(),
|
|
61
|
+
exemptImporters: [...model.exemptImporters].sort(),
|
|
62
|
+
...(model.governRoot ? { governRoot: model.governRoot } : {}),
|
|
63
|
+
};
|
|
64
|
+
return `${JSON.stringify(canonical, null, 2)}\n`;
|
|
65
|
+
}
|
|
66
|
+
/** Read a lock back into a model. Inverse of {@link serializeModel}. */
|
|
67
|
+
export function parseModel(text) {
|
|
68
|
+
const raw = JSON.parse(text);
|
|
69
|
+
return {
|
|
70
|
+
modules: raw.modules ?? [],
|
|
71
|
+
allowed: raw.allowed ?? [],
|
|
72
|
+
wildcards: raw.wildcards ?? [],
|
|
73
|
+
exemptImporters: raw.exemptImporters ?? [],
|
|
74
|
+
governRoot: raw.governRoot,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import type { VisualizerPort, EnforcerPort, EnforcerConfig, CheckResult } from "../ports/ports.js";
|
|
2
|
-
import type { AllowedEdge } from "../model/boundary-model.js";
|
|
1
|
+
import type { VisualizerPort, EnforcerPort, EnforcerConfig, CheckResult, DiffView } from "../ports/ports.js";
|
|
2
|
+
import type { AllowedEdge, Module } from "../model/boundary-model.js";
|
|
3
|
+
/** What the annotator turned into explicit `#proposed` proposals. */
|
|
4
|
+
export interface AnnotateResult {
|
|
5
|
+
edges: AllowedEdge[];
|
|
6
|
+
modules: Module[];
|
|
7
|
+
}
|
|
3
8
|
/**
|
|
4
9
|
* What a diagram grants that the approved base did not. Both kinds of grant are
|
|
5
10
|
* reported, because both let code cross a boundary that was previously closed.
|
|
@@ -32,6 +37,28 @@ export declare class Pipeline {
|
|
|
32
37
|
* protocol.
|
|
33
38
|
*/
|
|
34
39
|
verify(base: VisualizerPort): Promise<VerifyResult>;
|
|
35
|
-
/**
|
|
36
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Enact the diagram's pending proposals (strip `#proposed`, remove
|
|
42
|
+
* `#proposal-delete`), then return the resulting accepted model as a lock
|
|
43
|
+
* string. The caller persists it: that lock — written by approve, not inferred
|
|
44
|
+
* from git — is the baseline change detection compares against.
|
|
45
|
+
*/
|
|
46
|
+
approve(): Promise<string>;
|
|
47
|
+
/**
|
|
48
|
+
* Compare the diagram against a previously accepted lock and rewrite every
|
|
49
|
+
* undeclared addition — a bare new edge (a self-grant) or box — into an
|
|
50
|
+
* explicit `#proposed` proposal in the source, then paints intrinsic styling on
|
|
51
|
+
* every marker so the proposal is highlighted on every LikeC4 surface. Turns
|
|
52
|
+
* silent drift into a reviewable, colourable proposal without trusting git for
|
|
53
|
+
* the baseline.
|
|
54
|
+
*/
|
|
55
|
+
annotate(lock: string): Promise<AnnotateResult>;
|
|
56
|
+
/**
|
|
57
|
+
* (Re)generate a focused diff view per layer that holds a pending change, so a
|
|
58
|
+
* reviewer sees each proposal in the scope where it is actually drawn — a
|
|
59
|
+
* proposal nested in a box is invisible once that box collapses at a wider
|
|
60
|
+
* view. Reads the diagram's own `#proposed` / `#proposal-delete` markers, so it
|
|
61
|
+
* composes with `annotate` (annotate marks drift, this frames it).
|
|
62
|
+
*/
|
|
63
|
+
diffViews(): Promise<DiffView[]>;
|
|
37
64
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { newlyAllowedEdges, newlyExemptedImporters, } from "../model/boundary-model.js";
|
|
1
|
+
import { newlyAddedModules, newlyAllowedEdges, newlyExemptedImporters, parseModel, serializeModel, } from "../model/boundary-model.js";
|
|
2
2
|
/**
|
|
3
3
|
* The SDK's public surface. Orchestrates the two driven ports and stays blind
|
|
4
4
|
* to any concrete diagram source or linter — swap adapters, this is untouched.
|
|
@@ -33,8 +33,42 @@ export class Pipeline {
|
|
|
33
33
|
exemptions: newlyExemptedImporters(baseModel, headModel),
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Enact the diagram's pending proposals (strip `#proposed`, remove
|
|
38
|
+
* `#proposal-delete`), then return the resulting accepted model as a lock
|
|
39
|
+
* string. The caller persists it: that lock — written by approve, not inferred
|
|
40
|
+
* from git — is the baseline change detection compares against.
|
|
41
|
+
*/
|
|
37
42
|
async approve() {
|
|
38
|
-
|
|
43
|
+
await this.visualizer.approve();
|
|
44
|
+
const accepted = await this.visualizer.read();
|
|
45
|
+
return serializeModel(accepted);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Compare the diagram against a previously accepted lock and rewrite every
|
|
49
|
+
* undeclared addition — a bare new edge (a self-grant) or box — into an
|
|
50
|
+
* explicit `#proposed` proposal in the source, then paints intrinsic styling on
|
|
51
|
+
* every marker so the proposal is highlighted on every LikeC4 surface. Turns
|
|
52
|
+
* silent drift into a reviewable, colourable proposal without trusting git for
|
|
53
|
+
* the baseline.
|
|
54
|
+
*/
|
|
55
|
+
async annotate(lock) {
|
|
56
|
+
const accepted = parseModel(lock);
|
|
57
|
+
const head = await this.visualizer.read();
|
|
58
|
+
const edges = newlyAllowedEdges(accepted, head);
|
|
59
|
+
const modules = newlyAddedModules(accepted, head);
|
|
60
|
+
await this.visualizer.propose(edges, modules.map((m) => m.id));
|
|
61
|
+
await this.visualizer.styleMarkers();
|
|
62
|
+
return { edges, modules };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* (Re)generate a focused diff view per layer that holds a pending change, so a
|
|
66
|
+
* reviewer sees each proposal in the scope where it is actually drawn — a
|
|
67
|
+
* proposal nested in a box is invisible once that box collapses at a wider
|
|
68
|
+
* view. Reads the diagram's own `#proposed` / `#proposal-delete` markers, so it
|
|
69
|
+
* composes with `annotate` (annotate marks drift, this frames it).
|
|
70
|
+
*/
|
|
71
|
+
async diffViews() {
|
|
72
|
+
return this.visualizer.emitDiffViews();
|
|
39
73
|
}
|
|
40
74
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BoundaryModel } from '../model/boundary-model.js';
|
|
1
|
+
import type { AllowedEdge, BoundaryModel } from '../model/boundary-model.js';
|
|
2
2
|
/** A driven port: turns some diagram source into the boundary model. */
|
|
3
3
|
export interface VisualizerPort {
|
|
4
4
|
read(): Promise<BoundaryModel>;
|
|
@@ -7,6 +7,42 @@ export interface VisualizerPort {
|
|
|
7
7
|
* promoting intent edges to approved. Source-preserving; never an LLM edit.
|
|
8
8
|
*/
|
|
9
9
|
approve(): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Deterministically mark the given edges and elements `#proposed` in the
|
|
12
|
+
* source. The inverse of `approve` for additions: it rewrites an undeclared
|
|
13
|
+
* change into an explicit, colourable proposal. Idempotent — anything already
|
|
14
|
+
* marked is left alone. Source-preserving; never an LLM edit.
|
|
15
|
+
*/
|
|
16
|
+
propose(edges: AllowedEdge[], moduleIds: string[]): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Paint intrinsic `style { color … }` on every edge/box carrying a marker, so a
|
|
19
|
+
* proposal is highlighted on every LikeC4 surface — base views and the
|
|
20
|
+
* relationships panel, not only the generated diff views. Amber for `#proposed`,
|
|
21
|
+
* red for `#proposal-delete`. Idempotent; `approve` strips it back out.
|
|
22
|
+
* Source-preserving; never an LLM edit.
|
|
23
|
+
*/
|
|
24
|
+
styleMarkers(): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* (Re)generate a focused diff view for every layer that holds a pending
|
|
27
|
+
* change — an edge or box tagged `#proposed` or `#proposal-delete`. Each view
|
|
28
|
+
* is scoped to the tightest element that contains the change (the model root
|
|
29
|
+
* for a top-level one), because a proposal nested inside a box is invisible
|
|
30
|
+
* once that box collapses at a wider scope. A derived artifact: overwritten
|
|
31
|
+
* each run, and removed when nothing is proposed. Returns one entry per view.
|
|
32
|
+
*/
|
|
33
|
+
emitDiffViews(): Promise<DiffView[]>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* One emitted diff view: a single layer (element scope) that holds at least one
|
|
37
|
+
* pending change, and how many changes fall in it.
|
|
38
|
+
*/
|
|
39
|
+
export interface DiffView {
|
|
40
|
+
/** The generated view id, e.g. `boundry_diff_root` or `boundry_diff_billing`. */
|
|
41
|
+
id: string;
|
|
42
|
+
/** The scope element's fqn, or undefined for the model root. */
|
|
43
|
+
scope?: string;
|
|
44
|
+
/** How many pending `#proposed` / `#proposal-delete` changes this layer holds. */
|
|
45
|
+
changes: number;
|
|
10
46
|
}
|
|
11
47
|
/** A generated linter config artifact. */
|
|
12
48
|
export interface EnforcerConfig {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "boundry",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Compile a C4 architecture diagram into a deterministic dependency linter. Deterministic architectural guardrails for AI agents and humans.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"dependency-cruiser": "^16.4.0",
|
|
59
|
-
"likec4": "^1.
|
|
59
|
+
"likec4": "^1.58.0",
|
|
60
60
|
"typescript": "^5.6.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|