boundry 0.0.1 → 0.1.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/README.md
CHANGED
|
@@ -28,6 +28,25 @@ diagram (LikeC4) ──► boundary model ──► dependency-cruiser rules
|
|
|
28
28
|
Elements without a `folder` (actors, external systems, notes) are ignored, so a
|
|
29
29
|
rich communication diagram and an enforcement diagram can be the same file.
|
|
30
30
|
|
|
31
|
+
### Governing a whole root (opt-in)
|
|
32
|
+
|
|
33
|
+
By default a folder no element maps to is *ignored* — any module may import it.
|
|
34
|
+
That is what keeps a communication diagram usable as an enforcement diagram, but
|
|
35
|
+
it means brand-new, unmodelled code is free to import. Declare a root as fully
|
|
36
|
+
governed and the whole tree becomes the universe instead:
|
|
37
|
+
|
|
38
|
+
```likec4
|
|
39
|
+
system app 'App' {
|
|
40
|
+
metadata { governRoot 'src' }
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Now importing anything under `src/` that no module claims is a **violation**, and
|
|
45
|
+
`check` **warns** about code under the root that no module covers. That is the
|
|
46
|
+
mirror of the zero-files warning: the model failing to cover the code is as much
|
|
47
|
+
a gap as the code failing to back the model. Nothing changes unless you declare
|
|
48
|
+
it.
|
|
49
|
+
|
|
31
50
|
## See it
|
|
32
51
|
|
|
33
52
|
The diagram you draw *is* the whole spec. Below is the example architecture that
|
|
@@ -53,6 +53,21 @@ function buildForbiddenRules(model) {
|
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
+
// A declared govern root makes the whole tree the universe: territory no
|
|
57
|
+
// module claims is forbidden, not free. Additive — the pair rules above still
|
|
58
|
+
// decide every mapped-to-mapped edge on their own.
|
|
59
|
+
if (model.governRoot) {
|
|
60
|
+
const claimed = model.modules.map((m) => folderToRegex(m.folder));
|
|
61
|
+
for (const from of model.modules) {
|
|
62
|
+
rules.push({
|
|
63
|
+
name: `boundary:${from.id}->unmapped`,
|
|
64
|
+
comment: `${from.title} may not depend on code under '${model.governRoot}' that no module claims`,
|
|
65
|
+
severity: 'error',
|
|
66
|
+
from: scopeOf.get(from.id),
|
|
67
|
+
to: { path: folderToRegex(model.governRoot), pathNot: claimed },
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
56
71
|
return rules;
|
|
57
72
|
}
|
|
58
73
|
/** First enforcer adapter: targets dependency-cruiser for TypeScript. */
|
|
@@ -93,6 +108,24 @@ module.exports = {
|
|
|
93
108
|
warnings.push(`module '${mod.title}' maps to '${mod.folder}', which matched 0 source files`);
|
|
94
109
|
}
|
|
95
110
|
}
|
|
111
|
+
// The symmetric guard: code under a declared govern root that no module
|
|
112
|
+
// claims. The model not covering the code is as much a gap as the code not
|
|
113
|
+
// backing the model.
|
|
114
|
+
if (model.governRoot) {
|
|
115
|
+
const rootPrefix = `${folderPrefix(model.governRoot)}/`;
|
|
116
|
+
const claimed = model.modules.map((m) => `${folderPrefix(m.folder)}/`);
|
|
117
|
+
const unclaimed = new Set();
|
|
118
|
+
for (const source of seen) {
|
|
119
|
+
if (!source.startsWith(rootPrefix))
|
|
120
|
+
continue;
|
|
121
|
+
if (claimed.some((prefix) => source.startsWith(prefix)))
|
|
122
|
+
continue;
|
|
123
|
+
unclaimed.add(source.slice(0, source.lastIndexOf('/')));
|
|
124
|
+
}
|
|
125
|
+
for (const dir of unclaimed) {
|
|
126
|
+
warnings.push(`'${dir}' is under govern root '${model.governRoot}' but no module claims it`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
96
129
|
return { ok: violations.length === 0, violations, warnings };
|
|
97
130
|
}
|
|
98
131
|
}
|
|
@@ -91,7 +91,14 @@ export class LikeC4Visualizer {
|
|
|
91
91
|
const model = await likec4.computedModel();
|
|
92
92
|
const modules = [];
|
|
93
93
|
const folderIds = new Set();
|
|
94
|
+
let governRoot;
|
|
94
95
|
for (const el of model.elements()) {
|
|
96
|
+
// Opt-in: any element may declare the code root as fully governed. It
|
|
97
|
+
// needs no `folder` of its own — it is a declaration, not a module.
|
|
98
|
+
const rootMeta = el.getMetadata('governRoot');
|
|
99
|
+
const declaredRoot = Array.isArray(rootMeta) ? rootMeta[0] : rootMeta;
|
|
100
|
+
if (declaredRoot && !governRoot)
|
|
101
|
+
governRoot = declaredRoot;
|
|
95
102
|
const meta = el.getMetadata('folder');
|
|
96
103
|
const folder = Array.isArray(meta) ? meta[0] : meta;
|
|
97
104
|
if (folder) {
|
|
@@ -112,7 +119,7 @@ export class LikeC4Visualizer {
|
|
|
112
119
|
allowed.push({ from, to });
|
|
113
120
|
}
|
|
114
121
|
}
|
|
115
|
-
return { modules, allowed };
|
|
122
|
+
return { modules, allowed, governRoot };
|
|
116
123
|
}
|
|
117
124
|
/**
|
|
118
125
|
* Deterministically strip `#proposed` markers from the diagram source,
|
|
@@ -19,6 +19,13 @@ export interface AllowedEdge {
|
|
|
19
19
|
export interface BoundaryModel {
|
|
20
20
|
modules: Module[];
|
|
21
21
|
allowed: AllowedEdge[];
|
|
22
|
+
/**
|
|
23
|
+
* Optional code root declared fully governed. Every folder under it is
|
|
24
|
+
* expected to be modelled, so importing territory no module claims is
|
|
25
|
+
* forbidden rather than free. Omitted (the default) = unmapped folders are
|
|
26
|
+
* ignored, which keeps a communication diagram usable as an enforcement one.
|
|
27
|
+
*/
|
|
28
|
+
governRoot?: string;
|
|
22
29
|
}
|
|
23
30
|
/**
|
|
24
31
|
* Edges allowed at `head` that were not allowed at `base`.
|
package/package.json
CHANGED