@ecoma-io/archkeep 0.13.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/LICENSE +202 -0
- package/README.md +262 -0
- package/cli.mjs +2792 -0
- package/index.mjs +85 -0
- package/lsp.mjs +81 -0
- package/nx.mjs +24 -0
- package/package.json +81 -0
- package/presets/clean-architecture.json +78 -0
- package/presets/ddd-bounded-contexts.json +88 -0
- package/presets/hexagonal.json +68 -0
- package/presets/layered.json +92 -0
- package/presets/modular-monolith.json +85 -0
- package/presets/vertical-slice.json +68 -0
- package/src/analysis/analyze.mjs +218 -0
- package/src/analysis/contract.md +259 -0
- package/src/analysis/go.mjs +414 -0
- package/src/analysis/manifest-util.mjs +68 -0
- package/src/analysis/python.mjs +1266 -0
- package/src/analysis/registry.mjs +74 -0
- package/src/analysis/rust.mjs +674 -0
- package/src/analysis/source-util.mjs +230 -0
- package/src/analysis/typescript.mjs +1034 -0
- package/src/analysis/vue.mjs +156 -0
- package/src/architecture-intent/intent-fingerprint.mjs +29 -0
- package/src/architecture-intent/judge.mjs +539 -0
- package/src/architecture-intent/model.mjs +703 -0
- package/src/architecture-intent/selectors.mjs +170 -0
- package/src/canonical.mjs +48 -0
- package/src/commands/README.md +266 -0
- package/src/commands/adr.mjs +248 -0
- package/src/commands/check.mjs +989 -0
- package/src/commands/context-command.mjs +212 -0
- package/src/commands/context.mjs +790 -0
- package/src/commands/custom-rules.mjs +428 -0
- package/src/commands/debt.mjs +218 -0
- package/src/commands/diff.mjs +523 -0
- package/src/commands/discover.mjs +159 -0
- package/src/commands/drift.mjs +473 -0
- package/src/commands/edge-constraints.mjs +355 -0
- package/src/commands/explain.mjs +359 -0
- package/src/commands/fitness.mjs +226 -0
- package/src/commands/graph.mjs +297 -0
- package/src/commands/health.mjs +213 -0
- package/src/commands/history.mjs +614 -0
- package/src/commands/impact.mjs +226 -0
- package/src/commands/plan-context-command.mjs +496 -0
- package/src/commands/policy.mjs +138 -0
- package/src/commands/provenance-command.mjs +352 -0
- package/src/commands/provenance.mjs +159 -0
- package/src/commands/reconcile.mjs +219 -0
- package/src/commands/report.mjs +553 -0
- package/src/commands/snapshot-meta.mjs +107 -0
- package/src/commands/waivers.mjs +240 -0
- package/src/config.mjs +1308 -0
- package/src/containment.mjs +234 -0
- package/src/custom-rules/evidence.mjs +340 -0
- package/src/custom-rules/host.mjs +1023 -0
- package/src/custom-rules/values.mjs +43 -0
- package/src/entry-point.mjs +55 -0
- package/src/errors.mjs +36 -0
- package/src/eslint-config.mjs +542 -0
- package/src/go-work.mjs +394 -0
- package/src/governance/adr-registry.mjs +539 -0
- package/src/governance/clock.mjs +69 -0
- package/src/governance/debt-ledger.mjs +274 -0
- package/src/governance/discovery-proposal.mjs +423 -0
- package/src/governance/fitness-registry.mjs +504 -0
- package/src/governance/fitness-rules.mjs +668 -0
- package/src/governance/metrics.mjs +392 -0
- package/src/governance/preset-fingerprints.json +16 -0
- package/src/governance/profile-registry.mjs +366 -0
- package/src/governance/provenance-record.mjs +177 -0
- package/src/governance/reconcile-candidates.mjs +301 -0
- package/src/governance/reconcile-score.mjs +503 -0
- package/src/governance/row-schema.mjs +208 -0
- package/src/governance/verdict.mjs +127 -0
- package/src/governance/waiver.mjs +105 -0
- package/src/graph/create-dependencies.mjs +96 -0
- package/src/intent/intent-manifest.json +347 -0
- package/src/intent/mask-non-code.mjs +640 -0
- package/src/lsp/boundary-config.mjs +225 -0
- package/src/lsp/diagnose.mjs +202 -0
- package/src/lsp/diagnostics.mjs +241 -0
- package/src/lsp/protocol.mjs +215 -0
- package/src/lsp/server.mjs +922 -0
- package/src/lsp/workspace-index.mjs +891 -0
- package/src/nx-json.mjs +95 -0
- package/src/options.mjs +611 -0
- package/src/process.mjs +91 -0
- package/src/providers/moon.mjs +733 -0
- package/src/providers/native/README.md +204 -0
- package/src/providers/native/coverage.mjs +74 -0
- package/src/providers/native/differential.fixtures.mjs +1277 -0
- package/src/providers/native/discover.mjs +431 -0
- package/src/providers/native/graph.mjs +234 -0
- package/src/providers/native/index.mjs +152 -0
- package/src/providers/native/model.mjs +755 -0
- package/src/providers/nx.mjs +178 -0
- package/src/report/README.md +89 -0
- package/src/report/adr-text.mjs +129 -0
- package/src/report/context-text.mjs +109 -0
- package/src/report/debt-text.mjs +105 -0
- package/src/report/diff-text.mjs +219 -0
- package/src/report/discover-text.mjs +186 -0
- package/src/report/drift-text.mjs +194 -0
- package/src/report/envelope-shape.mjs +161 -0
- package/src/report/evidence.mjs +157 -0
- package/src/report/explain-text.mjs +159 -0
- package/src/report/graph-text.mjs +116 -0
- package/src/report/health-text.mjs +123 -0
- package/src/report/history-text.mjs +204 -0
- package/src/report/impact-text.mjs +128 -0
- package/src/report/json.mjs +173 -0
- package/src/report/plan-context-text.mjs +159 -0
- package/src/report/provenance-text.mjs +78 -0
- package/src/report/reconcile-text.mjs +159 -0
- package/src/report/report-text.mjs +264 -0
- package/src/report/sarif.mjs +953 -0
- package/src/report/text.mjs +823 -0
- package/src/report/waivers-text.mjs +100 -0
- package/src/rules/README.md +123 -0
- package/src/rules/index.mjs +962 -0
- package/src/rules/match.mjs +1708 -0
- package/src/rules/messages.mjs +73 -0
- package/src/rules/reachability.mjs +224 -0
- package/src/rules/specifiers.mjs +300 -0
- package/src/rules/tags.mjs +238 -0
- package/src/rules/topology.mjs +333 -0
- package/src/tsconfig-paths.mjs +237 -0
- package/src/verdict.mjs +145 -0
- package/src/workspace.mjs +580 -0
package/index.mjs
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The engine entry — what a consumer gets from `import "@ecoma-io/archkeep"`.
|
|
3
|
+
* Nx is one caller of this graph, not the only one: the CLI (`cli.mjs`) and
|
|
4
|
+
* the language server (`lsp.mjs`) both work from these same primitives, and
|
|
5
|
+
* this file is the surface a future integration (Bazel, Turborepo, a CI
|
|
6
|
+
* action) would import instead of reaching into `src/` directly. The Nx
|
|
7
|
+
* plugin itself lives at the `./nx` subpath (`nx.mjs`) — the one module
|
|
8
|
+
* `nx.json → plugins` is allowed to name — precisely so that loading the
|
|
9
|
+
* engine never pulls Nx's own startup path in for free.
|
|
10
|
+
*
|
|
11
|
+
* What's re-exported here is discovery (read a real workspace: the project
|
|
12
|
+
* graph, tracked files, per-file analysis) and judgment (evaluate that
|
|
13
|
+
* workspace against a boundary config) — the two halves `cli.mjs` composes.
|
|
14
|
+
* Nothing under `src/rules/`, `src/analysis/`, or `src/report/` is duplicated
|
|
15
|
+
* here; this module only re-exports their public functions. The one
|
|
16
|
+
* exception is `createDependencies` below, which is not a re-export and
|
|
17
|
+
* holds one line of logic: a guard against exactly this file being
|
|
18
|
+
* misregistered as the Nx plugin face that `./nx` (`nx.mjs`) alone is meant
|
|
19
|
+
* to be.
|
|
20
|
+
*/
|
|
21
|
+
export {
|
|
22
|
+
annotateMFERemotes,
|
|
23
|
+
annotatePackageFacts,
|
|
24
|
+
analyzeWorkspace,
|
|
25
|
+
createWorkspace,
|
|
26
|
+
declaredPackages,
|
|
27
|
+
environmentForTree,
|
|
28
|
+
findWorkspaceRoot,
|
|
29
|
+
listTrackedFiles,
|
|
30
|
+
packageEntryPoints,
|
|
31
|
+
projectIsMFERemote,
|
|
32
|
+
runProcess,
|
|
33
|
+
selectFiles,
|
|
34
|
+
} from "./src/workspace.mjs";
|
|
35
|
+
|
|
36
|
+
export { nxProvider, readProjectGraph } from "./src/providers/nx.mjs";
|
|
37
|
+
|
|
38
|
+
export {
|
|
39
|
+
analyzeFile,
|
|
40
|
+
analyzerFor,
|
|
41
|
+
LANGUAGE_BY_EXTENSION,
|
|
42
|
+
languageOf,
|
|
43
|
+
} from "./src/analysis/analyze.mjs";
|
|
44
|
+
|
|
45
|
+
export {
|
|
46
|
+
findBoundaryConfigViolations,
|
|
47
|
+
loadBoundaryConfig,
|
|
48
|
+
loadBoundaryConfigFile,
|
|
49
|
+
suppressionCovers,
|
|
50
|
+
} from "./src/config.mjs";
|
|
51
|
+
|
|
52
|
+
export { evaluate, MESSAGE_IDS, MESSAGES, renderMessage } from "./src/rules/index.mjs";
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The one export this file adds beyond a re-export, and it exists to fail
|
|
56
|
+
* loudly rather than to do anything.
|
|
57
|
+
*
|
|
58
|
+
* Measured against nx 23.1.1 in this tree: a consumer writing
|
|
59
|
+
* `"plugins": ["@ecoma-io/archkeep"]` in `nx.json` — the bare package name,
|
|
60
|
+
* missing the `/nx` — resolves to this file. Before this export existed the
|
|
61
|
+
* file had neither `name` nor `createDependencies`, and Nx accepted that
|
|
62
|
+
* silently: `nx graph` exited 0, printed no warning, and computed zero
|
|
63
|
+
* polyglot edges, indistinguishable from a workspace with nothing to find.
|
|
64
|
+
* That is the exact failure this project exists to refuse, one layer above
|
|
65
|
+
* every check it ships: a misregistration that reads as a clean graph.
|
|
66
|
+
*
|
|
67
|
+
* Exporting `name` alongside this was tried and measured too, and dropped:
|
|
68
|
+
* with only `name` added (still no `createDependencies`) Nx's behaviour did
|
|
69
|
+
* not change at all — same exit 0, same silence, same empty graph — so it
|
|
70
|
+
* buys nothing. `createDependencies` throwing is what Nx actually surfaces:
|
|
71
|
+
* with this export in place, `nx graph` prints the message below to the
|
|
72
|
+
* terminal, naming the fix. `name` is left for `./nx` alone to export, which
|
|
73
|
+
* keeps this file from looking like a working plugin face — it is the
|
|
74
|
+
* engine, not the face; `AGENTS.md` ("Layout, and what each layer may know")
|
|
75
|
+
* says which module carries which.
|
|
76
|
+
*
|
|
77
|
+
* @throws {Error} named `archkeep: registered as the engine entry`, always.
|
|
78
|
+
*/
|
|
79
|
+
export function createDependencies() {
|
|
80
|
+
throw new Error(
|
|
81
|
+
"archkeep: registered as the engine entry, not the Nx plugin face — nx.json named " +
|
|
82
|
+
'"@ecoma-io/archkeep" instead of "@ecoma-io/archkeep/nx". Change the `plugin` value in ' +
|
|
83
|
+
'nx.json to "@ecoma-io/archkeep/nx" (README.md, "Installing it").',
|
|
84
|
+
);
|
|
85
|
+
}
|
package/lsp.mjs
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Language-server entry — the stdio wiring, and nothing else.
|
|
4
|
+
*
|
|
5
|
+
* Everything the server decides lives in `src/lsp/`; this file only turns a
|
|
6
|
+
* byte stream into messages and back. The split is what lets the protocol
|
|
7
|
+
* conversation be driven in-process by a test (`src/lsp/server.test.mjs`) while
|
|
8
|
+
* `src/lsp.integration.test.mjs` still drives this executable over real pipes,
|
|
9
|
+
* because framing, stream wiring and the exit contract only hold together in a
|
|
10
|
+
* real process.
|
|
11
|
+
*
|
|
12
|
+
* Framing is the LSP base protocol: `Content-Length: <n>\r\n\r\n<utf8 json>`.
|
|
13
|
+
* Implemented in `src/lsp/protocol.mjs` rather than pulled from a dependency
|
|
14
|
+
* because it is a fixed external contract of about forty lines, and because
|
|
15
|
+
* this tool stays on Node built-ins (AGENTS.md).
|
|
16
|
+
*
|
|
17
|
+
* **stdout carries the protocol and nothing else.** Every log line goes to
|
|
18
|
+
* stderr: one stray `console.log` here would be read by the client as a
|
|
19
|
+
* malformed frame and desynchronise the stream for the rest of the session.
|
|
20
|
+
*/
|
|
21
|
+
import { isProgramEntry } from "./src/entry-point.mjs";
|
|
22
|
+
import { encodeMessage, frameMessages } from "./src/lsp/protocol.mjs";
|
|
23
|
+
import { createServer } from "./src/lsp/server.mjs";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Ends the process, but not before the client has the bytes.
|
|
27
|
+
*
|
|
28
|
+
* `process.exit()` discards whatever is still queued on a pipe, and the thing
|
|
29
|
+
* most often still queued is the reply to `shutdown` — the message that tells
|
|
30
|
+
* the client the session ended cleanly. An empty write is ordered behind every
|
|
31
|
+
* earlier one, so its callback runs only once they have all reached the OS.
|
|
32
|
+
* `exitCode` is set first so that a stream which never drains still ends the
|
|
33
|
+
* process with the right code instead of a silent 0.
|
|
34
|
+
*/
|
|
35
|
+
function exitAfterFlush(output, code) {
|
|
36
|
+
process.exitCode = code;
|
|
37
|
+
output.write("", () => process.exit(code));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Wires a server to a pair of streams and returns it. */
|
|
41
|
+
export function serve(input = process.stdin, output = process.stdout, onExit = null) {
|
|
42
|
+
const send = (message) => output.write(encodeMessage(message));
|
|
43
|
+
const exit = onExit ?? ((code) => exitAfterFlush(output, code));
|
|
44
|
+
const log = (text) => process.stderr.write(`${text}\n`);
|
|
45
|
+
const server = createServer({ send, exit, log });
|
|
46
|
+
|
|
47
|
+
/** @type {Buffer} */
|
|
48
|
+
let pending = Buffer.alloc(0);
|
|
49
|
+
// No encoding is ever set on `input`, so a data chunk is always a Buffer.
|
|
50
|
+
input.on("data", (/** @type {Buffer} */ chunk) => {
|
|
51
|
+
const framed = frameMessages(Buffer.concat([pending, chunk]));
|
|
52
|
+
// An implausible `Content-Length` is a poisoned stream, not a client that
|
|
53
|
+
// will send more bytes — see `MAX_CONTENT_LENGTH` in `./src/lsp/protocol.mjs`.
|
|
54
|
+
// The session is closed loudly (a log line naming the length, and a
|
|
55
|
+
// non-zero exit) rather than held open forever on a body that will never
|
|
56
|
+
// arrive: a frame that cannot be framed must never look like a clean,
|
|
57
|
+
// empty conversation.
|
|
58
|
+
if (framed.protocolError !== undefined) {
|
|
59
|
+
pending = Buffer.alloc(0);
|
|
60
|
+
log(framed.protocolError);
|
|
61
|
+
// Exit 2 is deliberate, and not the CLI's usage-error 2 — that is a
|
|
62
|
+
// different process (`docs/reference/exit-codes.md`). The server's own
|
|
63
|
+
// contract (`./src/lsp/server.mjs`) ends a session 0 after a clean
|
|
64
|
+
// `shutdown` and 1 when the pipe closed without one; a poisoned protocol
|
|
65
|
+
// stream is neither, and sharing either code would read as a clean or
|
|
66
|
+
// merely-dirty shutdown to whatever supervised the process.
|
|
67
|
+
exit(2);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
pending = framed.rest;
|
|
71
|
+
for (const message of framed.messages) server.handle(message);
|
|
72
|
+
});
|
|
73
|
+
// A client that closed the pipe without saying `exit` did not shut the server
|
|
74
|
+
// down; reporting that as a clean stop would hide a crashed editor.
|
|
75
|
+
input.on("end", () => server.handle({ jsonrpc: "2.0", method: "exit" }));
|
|
76
|
+
return server;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (isProgramEntry(import.meta.url)) {
|
|
80
|
+
serve();
|
|
81
|
+
}
|
package/nx.mjs
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nx plugin entry — the module `nx.json → plugins` names, reached through this
|
|
3
|
+
* package's `./nx` subpath export, and the only surface Nx itself is allowed
|
|
4
|
+
* to know about.
|
|
5
|
+
*
|
|
6
|
+
* It holds no logic on purpose. Nx loads this file on every graph computation,
|
|
7
|
+
* including on machines running a single unrelated target, so what it imports
|
|
8
|
+
* is what every `nx` invocation pays for. Keeping the entry a re-export means
|
|
9
|
+
* the rule engine, the CLI, and the language server can grow under `src/`
|
|
10
|
+
* without any of it being dragged into Nx's own startup path — and it keeps
|
|
11
|
+
* `CreateDependenciesContext`, the shape most likely to move under an Nx
|
|
12
|
+
* upgrade, touching exactly one module (`src/graph/create-dependencies.mjs`).
|
|
13
|
+
*
|
|
14
|
+
* The plugin's own contract with Nx is the two names below: `name`, and a
|
|
15
|
+
* `createDependencies` returning raw dependency records. The root entry
|
|
16
|
+
* (`index.mjs`) is the engine those same primitives serve to the CLI and the
|
|
17
|
+
* language server — Nx is one caller of the graph, not the only one.
|
|
18
|
+
*/
|
|
19
|
+
export {
|
|
20
|
+
createDependencies,
|
|
21
|
+
resolvePolyglotDependencies,
|
|
22
|
+
} from "./src/graph/create-dependencies.mjs";
|
|
23
|
+
|
|
24
|
+
export const name = "archkeep";
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ecoma-io/archkeep",
|
|
3
|
+
"version": "0.13.0",
|
|
4
|
+
"description": "Architecture enforcement for polyglot repositories — dependency graphs and module boundaries for Go, Rust, Python, TypeScript, JavaScript and Vue",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"architecture",
|
|
7
|
+
"enforcement",
|
|
8
|
+
"dependency-graph",
|
|
9
|
+
"monorepo",
|
|
10
|
+
"module-boundaries",
|
|
11
|
+
"polyglot",
|
|
12
|
+
"static-analysis",
|
|
13
|
+
"language-server",
|
|
14
|
+
"nx",
|
|
15
|
+
"nx-plugin",
|
|
16
|
+
"golang",
|
|
17
|
+
"rust",
|
|
18
|
+
"python",
|
|
19
|
+
"typescript"
|
|
20
|
+
],
|
|
21
|
+
"type": "module",
|
|
22
|
+
"license": "Apache-2.0",
|
|
23
|
+
"bin": {
|
|
24
|
+
"archkeep": "cli.mjs",
|
|
25
|
+
"archkeep-lsp": "lsp.mjs"
|
|
26
|
+
},
|
|
27
|
+
"main": "index.mjs",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": "./index.mjs",
|
|
30
|
+
"./nx": "./nx.mjs",
|
|
31
|
+
"./presets/*.json": "./presets/*.json",
|
|
32
|
+
"./package.json": "./package.json"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"index.mjs",
|
|
36
|
+
"nx.mjs",
|
|
37
|
+
"cli.mjs",
|
|
38
|
+
"lsp.mjs",
|
|
39
|
+
"src/",
|
|
40
|
+
"!src/**/*.test.mjs",
|
|
41
|
+
"!src/conformance/",
|
|
42
|
+
"!src/custom-rules/wasm-fixture.mjs",
|
|
43
|
+
"!src/report/envelope-shape.json",
|
|
44
|
+
"!src/custom-rules/evidence-golden.json",
|
|
45
|
+
"presets/",
|
|
46
|
+
"LICENSE",
|
|
47
|
+
"README.md"
|
|
48
|
+
],
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"smol-toml": "1.8.0"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"@nx/eslint-plugin": ">=21",
|
|
54
|
+
"nx": ">=21",
|
|
55
|
+
"typescript": ">=5 <7",
|
|
56
|
+
"vue": ">=3"
|
|
57
|
+
},
|
|
58
|
+
"peerDependenciesMeta": {
|
|
59
|
+
"@nx/eslint-plugin": {
|
|
60
|
+
"optional": true
|
|
61
|
+
},
|
|
62
|
+
"nx": {
|
|
63
|
+
"optional": true
|
|
64
|
+
},
|
|
65
|
+
"vue": {
|
|
66
|
+
"optional": true
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"engines": {
|
|
70
|
+
"node": ">=22"
|
|
71
|
+
},
|
|
72
|
+
"homepage": "https://github.com/ecoma-io/archkeep/tree/main/packages/archkeep#readme",
|
|
73
|
+
"bugs": {
|
|
74
|
+
"url": "https://github.com/ecoma-io/archkeep/issues"
|
|
75
|
+
},
|
|
76
|
+
"repository": {
|
|
77
|
+
"type": "git",
|
|
78
|
+
"url": "git+https://github.com/ecoma-io/archkeep.git",
|
|
79
|
+
"directory": "packages/archkeep"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"profiles": [
|
|
4
|
+
{
|
|
5
|
+
"name": "clean-architecture",
|
|
6
|
+
"block": {
|
|
7
|
+
"depConstraints": [
|
|
8
|
+
{
|
|
9
|
+
"sourceTag": "layer:entities",
|
|
10
|
+
"onlyDependOnLibsWithTags": ["layer:entities"],
|
|
11
|
+
"description": "Enterprise business rules sit at the centre: entities depend on nothing outside their own layer.",
|
|
12
|
+
"remediation": "Move the collaborator into layer:entities, or invert the dependency so the outer layer depends on the entity."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"sourceTag": "layer:use-cases",
|
|
16
|
+
"onlyDependOnLibsWithTags": ["layer:use-cases", "layer:entities"],
|
|
17
|
+
"description": "Application business rules may reach inward to entities and sideways within their own layer, never outward.",
|
|
18
|
+
"remediation": "Declare a port in layer:use-cases and let layer:interface-adapters implement it."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"sourceTag": "layer:interface-adapters",
|
|
22
|
+
"onlyDependOnLibsWithTags": [
|
|
23
|
+
"layer:interface-adapters",
|
|
24
|
+
"layer:use-cases",
|
|
25
|
+
"layer:entities"
|
|
26
|
+
],
|
|
27
|
+
"description": "Controllers, presenters and gateways convert between the outside world and the use cases; they never depend on a framework project.",
|
|
28
|
+
"remediation": "Keep the framework detail in layer:frameworks and pass it in through the adapter's own interface."
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"sourceTag": "layer:frameworks",
|
|
32
|
+
"onlyDependOnLibsWithTags": [
|
|
33
|
+
"layer:frameworks",
|
|
34
|
+
"layer:interface-adapters",
|
|
35
|
+
"layer:use-cases",
|
|
36
|
+
"layer:entities"
|
|
37
|
+
],
|
|
38
|
+
"description": "Drivers, delivery mechanisms and the composition root — the only layer allowed to know every other one.",
|
|
39
|
+
"remediation": "Nothing inside this layer may be imported from a project tagged with an inner layer; move the shared piece inward instead."
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"moduleBoundaryOptions": {
|
|
43
|
+
"allow": [],
|
|
44
|
+
"buildTargets": ["build"],
|
|
45
|
+
"enforceBuildableLibDependency": false,
|
|
46
|
+
"allowCircularSelfDependency": false,
|
|
47
|
+
"checkDynamicDependenciesExceptions": [],
|
|
48
|
+
"ignoredCircularDependencies": [],
|
|
49
|
+
"banTransitiveDependencies": false,
|
|
50
|
+
"checkNestedExternalImports": false
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "clean-architecture-pure-core",
|
|
56
|
+
"base": "clean-architecture",
|
|
57
|
+
"block": {
|
|
58
|
+
"depConstraints": [
|
|
59
|
+
{
|
|
60
|
+
"sourceTag": "layer:entities",
|
|
61
|
+
"bannedExternalImports": ["*"],
|
|
62
|
+
"description": "The Dependency Rule applied to third-party code: entities import no package outside the workspace, Node built-ins included.",
|
|
63
|
+
"remediation": "Wrap the package behind an interface owned by layer:entities and implement it in an outer layer."
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"sourceTag": "layer:use-cases",
|
|
67
|
+
"bannedExternalImports": ["*"],
|
|
68
|
+
"description": "Application business rules stay framework-free for the same reason entities do.",
|
|
69
|
+
"remediation": "Declare a port for the capability and let layer:interface-adapters bring the package in."
|
|
70
|
+
}
|
|
71
|
+
],
|
|
72
|
+
"moduleBoundaryOptions": {
|
|
73
|
+
"checkNestedExternalImports": true
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"profiles": [
|
|
4
|
+
{
|
|
5
|
+
"name": "ddd-bounded-contexts",
|
|
6
|
+
"block": {
|
|
7
|
+
"depConstraints": [
|
|
8
|
+
{
|
|
9
|
+
"sourceTag": "layer:domain",
|
|
10
|
+
"onlyDependOnLibsWithTags": ["layer:domain"],
|
|
11
|
+
"description": "The model of one bounded context. It names no contract, no service and no store — only model.",
|
|
12
|
+
"remediation": "Move the collaborator into layer:domain, or express the need as an interface the application layer satisfies."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"sourceTag": "layer:application",
|
|
16
|
+
"onlyDependOnLibsWithTags": [
|
|
17
|
+
"layer:application",
|
|
18
|
+
"layer:domain",
|
|
19
|
+
"layer:published-language"
|
|
20
|
+
],
|
|
21
|
+
"description": "Use cases orchestrate the model and speak to other contexts through their published language, never through their model.",
|
|
22
|
+
"remediation": "Translate at the boundary: consume the other context's layer:published-language project instead of its layer:domain one."
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"sourceTag": "layer:infrastructure",
|
|
26
|
+
"onlyDependOnLibsWithTags": [
|
|
27
|
+
"layer:infrastructure",
|
|
28
|
+
"layer:application",
|
|
29
|
+
"layer:domain",
|
|
30
|
+
"layer:published-language"
|
|
31
|
+
],
|
|
32
|
+
"description": "Persistence, messaging and the anti-corruption layer — where another context's shape is allowed to arrive and be translated.",
|
|
33
|
+
"remediation": "Nothing inside the model or the use cases may import this layer; invert the dependency with an interface they own."
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"sourceTag": "layer:published-language",
|
|
37
|
+
"onlyDependOnLibsWithTags": ["layer:published-language"],
|
|
38
|
+
"description": "Integration events and contracts are what other contexts are allowed to couple to, so they may couple to nothing else.",
|
|
39
|
+
"remediation": "A contract that needs a model type is leaking the model; restate the type in the contract project."
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"moduleBoundaryOptions": {
|
|
43
|
+
"allow": [],
|
|
44
|
+
"buildTargets": ["build"],
|
|
45
|
+
"enforceBuildableLibDependency": false,
|
|
46
|
+
"allowCircularSelfDependency": false,
|
|
47
|
+
"checkDynamicDependenciesExceptions": [],
|
|
48
|
+
"ignoredCircularDependencies": [],
|
|
49
|
+
"banTransitiveDependencies": false,
|
|
50
|
+
"checkNestedExternalImports": false
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "ddd-bounded-contexts-isolated",
|
|
56
|
+
"base": "ddd-bounded-contexts",
|
|
57
|
+
"block": {
|
|
58
|
+
"depConstraints": [
|
|
59
|
+
{
|
|
60
|
+
"sourceTag": "share:private",
|
|
61
|
+
"notDependOnLibsWithTags": ["share:private"],
|
|
62
|
+
"description": "Whatever a context keeps to itself is reachable from nothing but itself. Valid only where one project is one bounded context — see the presets page before selecting this profile.",
|
|
63
|
+
"remediation": "Publish what the other context needs as a share:published contract, and depend on that."
|
|
64
|
+
}
|
|
65
|
+
],
|
|
66
|
+
"moduleBoundaryOptions": {}
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "ddd-bounded-contexts-partitioned",
|
|
71
|
+
"base": "ddd-bounded-contexts",
|
|
72
|
+
"block": {
|
|
73
|
+
"fitness": [
|
|
74
|
+
{
|
|
75
|
+
"name": "context-isolation",
|
|
76
|
+
"match": ["tag:layer:domain", "tag:layer:application", "tag:layer:infrastructure"],
|
|
77
|
+
"condition": {
|
|
78
|
+
"type": "tag-axis-isolation",
|
|
79
|
+
"axis": "context",
|
|
80
|
+
"exempt": ["tag:layer:published-language"]
|
|
81
|
+
},
|
|
82
|
+
"reason": "What ddd-bounded-contexts-isolated means, without its one-project-per-context assumption. That profile forbids share:private depending on share:private, which reports two private projects of the SAME context depending on each other. This one asks the question the style actually asks — does this edge leave its context? — by reading the context: axis relative to the source, so a context may span as many projects as it needs while still reaching another context only through a layer:published-language contract. It matches on the three layer tags the base profile's own rows key on, never on a second axis a workspace might not carry: a project matching none of them is already reported by those rows, so this function can never quietly select nothing. Tag every project with a context: value; a matched project carrying none is reported as unjudgeable rather than waved through."
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"profiles": [
|
|
4
|
+
{
|
|
5
|
+
"name": "hexagonal",
|
|
6
|
+
"block": {
|
|
7
|
+
"depConstraints": [
|
|
8
|
+
{
|
|
9
|
+
"sourceTag": "layer:domain",
|
|
10
|
+
"onlyDependOnLibsWithTags": ["layer:domain"],
|
|
11
|
+
"description": "The inside of the hexagon. The domain knows neither its ports nor anything that plugs into them.",
|
|
12
|
+
"remediation": "Move the collaborator into layer:domain, or express the need as a port and let an adapter satisfy it."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"sourceTag": "layer:ports",
|
|
16
|
+
"onlyDependOnLibsWithTags": ["layer:ports", "layer:domain"],
|
|
17
|
+
"description": "A port is the domain's own statement of what it needs or offers, so it may name domain types and other ports.",
|
|
18
|
+
"remediation": "A port that needs an adapter's type is describing an implementation; keep the type in layer:domain instead."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"sourceTag": "layer:adapters",
|
|
22
|
+
"onlyDependOnLibsWithTags": ["layer:ports", "layer:domain"],
|
|
23
|
+
"description": "An adapter implements a port. It may not depend on another adapter — two adapters that talk to each other have made a driven side into a driving one.",
|
|
24
|
+
"remediation": "Route the collaboration through a port, or promote the shared piece into layer:domain."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"sourceTag": "layer:app",
|
|
28
|
+
"onlyDependOnLibsWithTags": [
|
|
29
|
+
"layer:app",
|
|
30
|
+
"layer:adapters",
|
|
31
|
+
"layer:ports",
|
|
32
|
+
"layer:domain"
|
|
33
|
+
],
|
|
34
|
+
"description": "The composition root: the one place allowed to name a concrete adapter and wire it to a port.",
|
|
35
|
+
"remediation": "Nothing inside the hexagon may import this layer; move the wiring outward rather than the dependency inward."
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"moduleBoundaryOptions": {
|
|
39
|
+
"allow": [],
|
|
40
|
+
"buildTargets": ["build"],
|
|
41
|
+
"enforceBuildableLibDependency": false,
|
|
42
|
+
"allowCircularSelfDependency": false,
|
|
43
|
+
"checkDynamicDependenciesExceptions": [],
|
|
44
|
+
"ignoredCircularDependencies": [],
|
|
45
|
+
"banTransitiveDependencies": false,
|
|
46
|
+
"checkNestedExternalImports": false
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"name": "hexagonal-pure-domain",
|
|
52
|
+
"base": "hexagonal",
|
|
53
|
+
"block": {
|
|
54
|
+
"depConstraints": [
|
|
55
|
+
{
|
|
56
|
+
"sourceTag": "layer:domain",
|
|
57
|
+
"bannedExternalImports": ["*"],
|
|
58
|
+
"description": "The hexagon's inside takes no third-party package at all, Node built-ins included — every capability arrives through a port.",
|
|
59
|
+
"remediation": "Declare a port for the capability and implement it in layer:adapters, where the package belongs."
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
"moduleBoundaryOptions": {
|
|
63
|
+
"checkNestedExternalImports": true
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"profiles": [
|
|
4
|
+
{
|
|
5
|
+
"name": "layered-strict",
|
|
6
|
+
"block": {
|
|
7
|
+
"depConstraints": [
|
|
8
|
+
{
|
|
9
|
+
"sourceTag": "tier:presentation",
|
|
10
|
+
"onlyDependOnLibsWithTags": ["tier:presentation", "tier:application"],
|
|
11
|
+
"description": "Strict layering: presentation may reach its own tier and application, the tier immediately below it, and nothing else. A downward edge that skips a tier is a finding here.",
|
|
12
|
+
"remediation": "Route the call through application rather than reaching past it, or move the collaborator up to the tier that is allowed to see it."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"sourceTag": "tier:application",
|
|
16
|
+
"onlyDependOnLibsWithTags": ["tier:application", "tier:domain"],
|
|
17
|
+
"description": "Strict layering: application may reach its own tier and domain, the tier immediately below it, and nothing else. A downward edge that skips a tier is a finding here.",
|
|
18
|
+
"remediation": "Route the call through domain rather than reaching past it, or move the collaborator up to the tier that is allowed to see it."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"sourceTag": "tier:domain",
|
|
22
|
+
"onlyDependOnLibsWithTags": ["tier:domain", "tier:infrastructure"],
|
|
23
|
+
"description": "Strict layering: domain may reach its own tier and infrastructure, the tier immediately below it, and nothing else. There is nothing beneath infrastructure, so every finding this row can produce is an upward edge.",
|
|
24
|
+
"remediation": "Nothing above may be imported from here; invert the dependency, or move the shared piece down into infrastructure where both sides may see it."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"sourceTag": "tier:infrastructure",
|
|
28
|
+
"onlyDependOnLibsWithTags": ["tier:infrastructure"],
|
|
29
|
+
"description": "The bottom tier. It may reach its own tier and nothing else, because there is nothing beneath it — so every finding this row can produce is an upward edge.",
|
|
30
|
+
"remediation": "Nothing above may be imported from here; invert the dependency, or move the shared piece down into this tier where both sides may see it."
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"moduleBoundaryOptions": {
|
|
34
|
+
"allow": [],
|
|
35
|
+
"buildTargets": ["build"],
|
|
36
|
+
"enforceBuildableLibDependency": false,
|
|
37
|
+
"allowCircularSelfDependency": false,
|
|
38
|
+
"checkDynamicDependenciesExceptions": [],
|
|
39
|
+
"ignoredCircularDependencies": [],
|
|
40
|
+
"banTransitiveDependencies": false,
|
|
41
|
+
"checkNestedExternalImports": false
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"name": "layered-relaxed",
|
|
47
|
+
"block": {
|
|
48
|
+
"depConstraints": [
|
|
49
|
+
{
|
|
50
|
+
"sourceTag": "tier:presentation",
|
|
51
|
+
"onlyDependOnLibsWithTags": [
|
|
52
|
+
"tier:presentation",
|
|
53
|
+
"tier:application",
|
|
54
|
+
"tier:domain",
|
|
55
|
+
"tier:infrastructure"
|
|
56
|
+
],
|
|
57
|
+
"description": "Relaxed layering: presentation may reach its own tier and every tier beneath it, at any depth.",
|
|
58
|
+
"remediation": "An upward edge is the violation here; invert the dependency, or move the shared piece down to a tier both sides may see."
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"sourceTag": "tier:application",
|
|
62
|
+
"onlyDependOnLibsWithTags": ["tier:application", "tier:domain", "tier:infrastructure"],
|
|
63
|
+
"description": "Relaxed layering: application may reach its own tier and every tier beneath it, at any depth.",
|
|
64
|
+
"remediation": "An upward edge is the violation here; invert the dependency, or move the shared piece down to a tier both sides may see."
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"sourceTag": "tier:domain",
|
|
68
|
+
"onlyDependOnLibsWithTags": ["tier:domain", "tier:infrastructure"],
|
|
69
|
+
"description": "Relaxed layering: domain may reach its own tier and every tier beneath it, at any depth.",
|
|
70
|
+
"remediation": "An upward edge is the violation here; invert the dependency, or move the shared piece down to a tier both sides may see."
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"sourceTag": "tier:infrastructure",
|
|
74
|
+
"onlyDependOnLibsWithTags": ["tier:infrastructure"],
|
|
75
|
+
"description": "The bottom tier. It may reach its own tier and nothing else, because there is nothing beneath it — so every finding this row can produce is an upward edge.",
|
|
76
|
+
"remediation": "Nothing above may be imported from here; invert the dependency, or move the shared piece down into this tier where both sides may see it."
|
|
77
|
+
}
|
|
78
|
+
],
|
|
79
|
+
"moduleBoundaryOptions": {
|
|
80
|
+
"allow": [],
|
|
81
|
+
"buildTargets": ["build"],
|
|
82
|
+
"enforceBuildableLibDependency": false,
|
|
83
|
+
"allowCircularSelfDependency": false,
|
|
84
|
+
"checkDynamicDependenciesExceptions": [],
|
|
85
|
+
"ignoredCircularDependencies": [],
|
|
86
|
+
"banTransitiveDependencies": false,
|
|
87
|
+
"checkNestedExternalImports": false
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
}
|