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.
@@ -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 code. */
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
- /** Source path prefix the module owns, e.g. "src/core". */
12
- folder: string;
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
- /** Approve proposed edges: strip their `#proposed` markers from the diagram. */
36
- approve(): Promise<void>;
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
- /** Approve proposed edges: strip their `#proposed` markers from the diagram. */
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
- return this.visualizer.approve();
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.2.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.46.0",
59
+ "likec4": "^1.58.0",
60
60
  "typescript": "^5.6.0"
61
61
  },
62
62
  "devDependencies": {