ark-runtime-kernel 1.0.0 โ†’ 1.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
@@ -2,126 +2,147 @@
2
2
 
3
3
  # ๐Ÿ›๏ธ Ark โ€” Architectural Runtime Kernel
4
4
 
5
- **Make your architecture a machine-readable, enforceable contract** โ€”<br/>
6
- respected by AI agents at write time, CI at merge time, and the runtime itself.
5
+ **Stop AI agents (and humans) from quietly breaking your architecture.**<br/>
6
+ One machine-readable contract โ€” enforced at write time, merge time, and (optionally) runtime.
7
7
 
8
- [![CI](https://github.com/pedroknigge/ark/actions/workflows/ci.yml/badge.svg)](https://github.com/pedroknigge/ark/actions/workflows/ci.yml)
8
+ [![CI](https://github.com/pedroknigge/ark-runtime-kernel/actions/workflows/ci.yml/badge.svg)](https://github.com/pedroknigge/ark-runtime-kernel/actions/workflows/ci.yml)
9
9
  [![npm](https://img.shields.io/npm/v/ark-runtime-kernel?color=cb3837&label=npm)](https://www.npmjs.com/package/ark-runtime-kernel)
10
10
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
11
11
  ![Node](https://img.shields.io/badge/node-%3E%3D18-339933?logo=node.js)
12
12
  ![TypeScript](https://img.shields.io/badge/TypeScript-first-3178c6?logo=typescript&logoColor=white)
13
13
  ![Zero deps](https://img.shields.io/badge/dependencies-0-success)
14
14
 
15
- **Zero runtime dependencies** ยท TypeScript-first ยท Hexagonal + Event-Driven + DDD governance kernel
16
-
17
- [Quick Start](#60-second-setup) ยท [The Three Gates](#the-three-gates-visual) ยท [AI Write Gate](#ai-write-path-gate-ark-mcp) ยท [CI Gate](#ark-check--the-ci-gate) ยท [Docs](#documentation)
15
+ [2-Minute Setup](#2-minute-setup) ยท [Why Ark](#why-ark-and-not-just-a-linter) ยท [AI Write Gate](#the-ai-write-gate) ยท [CI Gate](#ark-check--the-ci-gate) ยท [Runtime Kernel](#the-runtime-kernel-opt-in) ยท [Docs](#documentation)
18
16
 
19
17
  </div>
20
18
 
21
19
  ---
22
20
 
23
- ## The Three Gates (Visual)
24
-
25
- ```mermaid
26
- flowchart LR
27
- A["โœ๏ธ Write Time<br/>AI Agents"] -->|ark-mcp + validate_code| B["๐Ÿšซ Blocked"]
28
- A -->|valid| C["๐Ÿ’พ Disk"]
21
+ This is what happens when an agent tries to import a persistence adapter into your domain layer with Ark's write gate active:
29
22
 
30
- D["๐Ÿ”€ Merge Time<br/>CI / PRs"] -->|ark-check| E["โŒ Fail"]
31
- D -->|valid| F["โœ… Merge"]
23
+ ![An AI agent is blocked from importing a persistence adapter into the domain layer, then self-corrects by defining a port](docs/assets/ark-write-gate.svg)
32
24
 
33
- G["โš™๏ธ Runtime<br/>In-process"] -->|createArkKernel<br/>strict defaults| H["๐Ÿ›ก๏ธ Enforce<br/>contracts + layers"]
34
- G --> I["๐Ÿ“Š Observability<br/>+ Manifest"]
35
-
36
- style A fill:#e0f2fe,color:#0c4a6e
37
- style D fill:#fef3c7,color:#92400e
38
- style G fill:#dcfce7,color:#166534
39
- ```
40
-
41
- **One config. Three enforcement moments.**
42
-
43
- | Gate | Tool | When it runs | What it enforces |
44
- |--------------|---------------|-------------------------------|-----------------------------------------------|
45
- | **Write** | `ark-mcp` | Agent PreToolUse (Write/Edit) | Layer rules, unknown intents, forbidden patterns |
46
- | **Merge** | `ark-check` | CI (GitHub Actions etc.) | Cross-layer imports + intent references (real TS resolver) |
47
- | **Runtime** | `createArkKernel()` | Running process | Intent registry, event contracts, observed layer flow, policies |
25
+ The agent doesn't just get blocked โ€” it gets the violation as feedback, reads the architecture contract, and **fixes its own approach**. No review round-trip.
48
26
 
49
- ---
27
+ ## 2-Minute Setup
50
28
 
51
- ## 60-Second Setup
29
+ No code changes. No new runtime. Just a config and a CI line.
52
30
 
53
31
  ```bash
54
32
  npm install -D ark-runtime-kernel typescript
33
+ npx ark-check --init # infers layers from your existing folders โ†’ ark.config.json
34
+ npx ark-check # done: cross-layer imports now fail the check
55
35
  ```
56
36
 
57
- ### 1. Bootstrap your config from reality
37
+ Adopting on a codebase that already has violations? Freeze them and ratchet down:
58
38
 
59
39
  ```bash
60
- npx ark-check --init # detects your folders and writes ark.config.json
40
+ npx ark-check --update-baseline # writes .ark-baseline.json โ€” commit it
41
+ npx ark-check --baseline # only NEW violations fail from now on
61
42
  ```
62
43
 
63
- ### 2. Gate CI
44
+ Then gate your agents (Claude Code shown; [Cursor / Codex / others](docs/ai-gates.md)):
64
45
 
65
- ```bash
66
- npx ark-check --root . --config ark.config.json --strict-config
46
+ ```json
47
+ // .claude/settings.json
48
+ {
49
+ "hooks": {
50
+ "PreToolUse": [{
51
+ "matcher": "Write|Edit|MultiEdit",
52
+ "hooks": [{ "type": "command",
53
+ "command": "npx ark-mcp --hook --root \"$CLAUDE_PROJECT_DIR\" --config ark.config.json" }]
54
+ }]
55
+ }
56
+ }
67
57
  ```
68
58
 
69
- ### 3. Gate AI agents (write path)
59
+ > The same `ark.config.json` powers every gate.
60
+
61
+ ## Why Ark (and not just a linter)?
62
+
63
+ If you only need import-boundary linting in CI, [dependency-cruiser](https://github.com/sverweij/dependency-cruiser), [eslint-plugin-boundaries](https://github.com/javierbrea/eslint-plugin-boundaries), and Nx module boundaries are solid tools. Ark's reason to exist is the **write-time, agent-native half** they don't cover:
64
+
65
+ | | Ark | dependency-cruiser | eslint-plugin-boundaries | Nx boundaries |
66
+ |-----------------------------------------|:---:|:---:|:---:|:---:|
67
+ | Cross-layer import checks in CI | โœ… (TS resolver) | โœ… | โœ… | โœ… |
68
+ | Blocks AI agents **before** code lands (MCP + hook) | โœ… | โŒ | โŒ | โŒ |
69
+ | Machine-readable contract for agents (`ark://manifest`) | โœ… | โŒ | โŒ | โŒ |
70
+ | Event/intent governance (who may publish what) | โœ… | โŒ | โŒ | โŒ |
71
+ | Baseline ratchet for existing codebases | โœ… | โŒ | โž– (via ESLint) | โŒ |
72
+ | Optional runtime enforcement | โœ… | โŒ | โŒ | โŒ |
73
+ | Runtime dependencies | 0 | many | many | Nx |
74
+
75
+ **One config. Three enforcement moments:**
76
+
77
+ | Gate | Tool | When it runs | What it enforces |
78
+ |--------------|---------------|-------------------------------|-----------------------------------------------|
79
+ | **Write** | `ark-mcp` | Agent PreToolUse (Write/Edit) | Layer rules, unknown intents, forbidden patterns |
80
+ | **Merge** | `ark-check` | CI (GitHub Actions etc.) | Cross-layer imports + intent references (real TS resolver) |
81
+ | **Runtime** | `createArkKernel()` | Running process (opt-in) | Intent registry, event contracts, observed layer flow, policies |
82
+
83
+ ## The AI Write Gate
84
+
85
+ `ark-mcp` is a zero-dependency MCP server + one-shot hook:
86
+
87
+ - **`ark-mcp --hook`** โ€” PreToolUse gate: computes the **post-edit** file content, validates it against your layers, exits 2 with the violations when the write must be blocked. The agent self-corrects.
88
+ - **`validate_code` tool** โ€” on-demand validation of a snippet, for runtimes without hooks.
89
+ - **`ark://manifest` resource** โ€” the architecture as JSON, so agents read the rules *before* generating code instead of learning by rejection.
90
+
91
+ Copy-paste setups for **Claude Code, Cursor, and OpenAI Codex**: [docs/ai-gates.md](docs/ai-gates.md).
92
+
93
+ ## `ark-check` โ€” The CI Gate
70
94
 
71
95
  ```bash
72
- npx ark-mcp --root . --config ark.config.json
96
+ npx ark-check --root . --config ark.config.json --strict-config # fail on coverage gaps too
97
+ npx ark-check --json # machine-readable
98
+ npx ark-check --baseline # ratchet mode
73
99
  ```
74
100
 
75
- Bind `--hook` mode to your agent's `PreToolUse` for Write/Edit (see full docs below).
101
+ **What it catches (via real TypeScript module resolution โ€” path aliases included):**
76
102
 
77
- > The same `ark.config.json` powers all three gates.
78
-
79
- ---
103
+ - Import/export violations (relative, aliases, packages, dynamic `import()`, `require`)
104
+ - String intent references across forbidden layers
105
+ - Raw `publish()` calls that bypass registered intent creators
106
+ - Missing / mismatched publish `source` metadata
80
107
 
81
- ## What Ark Actually Does
108
+ Violations come with the layer edge, the resolved target, and a fix hint:
82
109
 
83
- Ark turns architecture from **diagrams + good intentions** into **executable contracts**.
110
+ ```
111
+ โœ– LAYER_IMPORT_VIOLATION src/domain/order.ts:3
112
+ DomainModel โ†’ PersistenceAdapters (src/adapters/persistence/pg-order-repository.ts)
113
+ DomainModel must not import PersistenceAdapters.
114
+ fix: Depend on a port/interface owned by an inner layer instead, or move this code.
115
+ ```
84
116
 
85
- ### Core Capabilities
117
+ ### GitHub Action
86
118
 
87
- - **Intent Registry** โ€” Semantic names (`Domain.Order.OrderPlaced`, `Application.PlaceOrder`) with declared produces/dependsOn relationships.
88
- - **Policy Engine** โ€” Hard policies (throw) + soft policies (observe). Built-in clean-architecture matrix.
89
- - **Strict Event Bus** โ€” Registered intents only, known sources, event contracts, add-only interceptors.
90
- - **Observed Layer Flow** โ€” Runtime enforcement (`'hard' | 'soft' | 'off'`) of *actual* producer โ†’ event flows against your layer rules.
91
- - **Event Contracts** โ€” Payload shape validation (including nested + enums).
92
- - **11-Layer Profile** โ€” First-class support for proper Hexagonal/Event-Driven boundaries.
93
- - **Manifest** โ€” `ark.manifest().toJSON()` โ†’ complete machine-readable contract for agents and tools.
94
- - **Observability & Drift** โ€” Declared vs observed flow reports.
95
- - **Audit / Outbox / Projections / Workflow (Saga)** โ€” Pluggable in-memory defaults + interfaces.
96
- - **Static + AI Gates** โ€” `ark-check` (deep) + `ark-mcp` + ESLint plugin.
119
+ ```yaml
120
+ - uses: pedroknigge/ark-runtime-kernel@main
121
+ with:
122
+ github-token: ${{ secrets.GITHUB_TOKEN }} # comments violations on the PR
123
+ ```
97
124
 
98
- ### Enforcement Scope (Be Honest With Yourself)
125
+ Inputs: `root`, `config`, `strict-config`, `baseline`, `version`.
99
126
 
100
- **Hard at runtime (governed paths only):**
101
- - Unregistered intents / bad names
102
- - Unknown sources
103
- - Contract violations
104
- - Hard policy violations
105
- - Observed layer flow violations (when `hard`)
127
+ ### ESLint plugin (in-editor feedback)
106
128
 
107
- **CI (with ark-check):**
108
- - Cross-layer imports (real module resolution)
109
- - Intent string references across boundaries
110
- - Raw `publish()` calls
111
- - Missing `source` on strict publishes
129
+ ```js
130
+ // eslint.config.js
131
+ import ark from 'ark-runtime-kernel/eslint';
132
+ export default [ark.configs.recommended];
133
+ ```
112
134
 
113
- **Everything else is out of scope** unless you route it through Ark or cover it with config + CI.
135
+ Rules: `ark/no-domain-infra-imports`, `ark/no-raw-event-publish`, `ark/require-publish-source`.
114
136
 
115
- ---
137
+ ## The Runtime Kernel (opt-in)
116
138
 
117
- ## Quick Start โ€” Strict Kernel (Recommended)
139
+ The gates above need **zero changes to your code**. When you also want *runtime* guarantees โ€” registered intents only, payload contracts, observed producerโ†’event layer flows โ€” route your events through the kernel:
118
140
 
119
141
  ```ts
120
142
  import { createArkKernel } from 'ark-runtime-kernel';
121
143
 
122
- const ark = createArkKernel(); // or createStrictArkKernel()
144
+ const ark = createArkKernel(); // strict defaults
123
145
 
124
- // 1. Define intents
125
146
  const OrderPlaced = ark.registry.define<
126
147
  'Domain.Order.OrderPlaced',
127
148
  { orderId: string; amount: number }
@@ -132,7 +153,8 @@ ark.registry.define<'Application.PlaceOrder', { orderId: string }>(
132
153
  { produces: ['Domain.Order.OrderPlaced'] }
133
154
  );
134
155
 
135
- // 2. Register contracts (optional but powerful)
156
+ // Payload contracts: Ark's own schema format, or any Standard Schema
157
+ // validator (zod, valibot, arktype) via `standardSchema`.
136
158
  ark.eventContracts.register({
137
159
  intent: 'Domain.Order.OrderPlaced',
138
160
  version: '1',
@@ -143,179 +165,60 @@ ark.eventContracts.register({
143
165
  },
144
166
  });
145
167
 
146
- // 3. Projections (read models)
147
168
  ark.projections.register({
148
169
  name: 'OrderIds',
149
170
  sourceIntents: ['Domain.Order.OrderPlaced'],
150
171
  initialState: { ids: [] as string[] },
151
- project: (event, state) => ({
152
- ids: [...state.ids, event.payload.orderId as string],
153
- }),
172
+ project: (event, state) => ({ ids: [...state.ids, event.payload.orderId as string] }),
154
173
  });
155
174
 
156
- // 4. Publish through source-bound publisher (recommended)
157
175
  const publisher = ark.publisher('Application.PlaceOrder');
176
+ await publisher.publish(OrderPlaced, { orderId: 'o1', amount: 129 }, { eventVersion: '1' });
158
177
 
159
- await publisher.publish(OrderPlaced, { orderId: 'o1', amount: 129 }, {
160
- eventVersion: '1',
161
- correlationId: 'corr-xyz',
162
- });
163
-
164
- console.log(await ark.projections.getState('OrderIds'));
165
- console.log(ark.observability.report());
166
- console.log(JSON.stringify(ark.manifest().toJSON(), null, 2));
178
+ ark.manifest().toJSON(); // the complete machine-readable contract
167
179
  ```
168
180
 
169
- See `examples/basic/` for a runnable version.
170
-
171
- ---
181
+ What it gives you: intent registry with produces/dependsOn, strict event bus (registered intents only, known sources), event contracts, hard/soft policies, observed layer-flow enforcement (`'hard' | 'soft' | 'off'`), projections, observability/drift reports, and pluggable audit/outbox/workflow interfaces (in-memory defaults โ€” see [production hardening](docs/production-hardening.md)).
172
182
 
173
- ## AI Write-Path Gate (`ark-mcp`)
183
+ **Honest scope:** runtime enforcement covers governed paths only โ€” what you route through Ark. Everything else is covered by the static gates.
174
184
 
175
- **The killer feature for agentic coding.**
185
+ ### NestJS
176
186
 
177
- ### Pre-write hook (blocks bad code before disk)
187
+ ```ts
188
+ import { ArkModule, InjectArk } from 'ark-runtime-kernel/nestjs';
189
+ import type { ArkKernel } from 'ark-runtime-kernel';
178
190
 
179
- In Claude Code (`.claude/settings.json`):
191
+ @Module({ imports: [ArkModule.forRoot()] })
192
+ export class AppModule {}
180
193
 
181
- ```json
182
- {
183
- "hooks": {
184
- "PreToolUse": [{
185
- "matcher": "Write|Edit|MultiEdit",
186
- "hooks": [{
187
- "type": "command",
188
- "command": "npx ark-mcp --hook --root \"$CLAUDE_PROJECT_DIR\""
189
- }]
190
- }]
191
- }
194
+ @Injectable()
195
+ export class PlaceOrderService {
196
+ constructor(@InjectArk() private readonly ark: ArkKernel) {}
192
197
  }
193
198
  ```
194
199
 
195
- When blocked, the agent gets the violations back as feedback and can fix + retry.
196
-
197
- ### Full MCP server
198
-
199
- ```bash
200
- npx ark-mcp --root . --config ark.config.json
201
- ```
202
-
203
- Exposes:
204
- - Resource: `ark://manifest`
205
- - Tool: `validate_code(source, layer?, filePath?)`
206
-
207
- Register in `.mcp.json`.
208
-
209
- ---
210
-
211
- ## `ark-check` โ€” The CI Gate
212
-
213
- ```bash
214
- # Basic
215
- npx ark-check --root . --config ark.config.json
216
-
217
- # Fail on coverage gaps too
218
- npx ark-check --root . --config ark.config.json --strict-config
219
-
220
- # JSON for tools
221
- npx ark-check --json
222
- ```
223
-
224
- **What it catches (via real TypeScript resolution):**
225
- - Import/export violations (relative, aliases, packages, dynamic import, require)
226
- - String intent references across forbidden layers
227
- - Raw publish calls
228
- - Missing source metadata
229
- - Source-layer mismatch
230
-
231
- `--init` generates a real config from the directories that *actually exist* in your project.
232
-
233
- ---
234
-
235
- ## ESLint Plugin (dev guardrails)
236
-
237
- ```js
238
- // eslint.config.js
239
- import ark from 'ark-runtime-kernel/eslint';
240
-
241
- export default [
242
- ark.configs.recommended,
243
- ];
244
- ```
245
-
246
- Rules:
247
- - `ark/no-domain-infra-imports`
248
- - `ark/no-raw-event-publish`
249
- - `ark/require-publish-source`
250
-
251
- ---
252
-
253
- ## What Ark Is / Is Not
254
-
255
- | โœ… Ark is | โŒ Ark is not |
256
- |---------------------------------------|-------------------------------------------|
257
- | Runtime + CI + AI governance kernel | Database or queue |
258
- | Enforceable architectural contract | Full distributed workflow engine |
259
- | Machine-readable manifest for agents | Replacement for your domain logic |
260
- | Zero-dependency TypeScript library | Complete semantic / type analyzer |
261
- | Observable drift + history | OpenTelemetry implementation |
262
- | Focused, explicit, pluggable | Magic that covers code you never route |
263
-
264
- ---
265
-
266
- ## Architecture Profile (11 Layers)
267
-
268
- The built-in profile + `ark.config.json` give you a sane default taxonomy:
269
-
270
- `DomainModel โ†’ ApplicationOrchestration โ†’ PersistenceAdapters โ†’ ...` (and 8 more)
271
-
272
- You can customize freely. Rules are deny-by-default except for a few explicitly allowed flows.
273
-
274
- ---
275
-
276
- ## Production Notes
277
-
278
- All stores (`Audit`, `Outbox`, `Projections`, `Workflow`) default to in-memory.
279
-
280
- See [docs/production-hardening.md](./docs/production-hardening.md) for the interface contracts you must implement for durability.
281
-
282
- ---
200
+ `@nestjs/common` is an optional peer dependency โ€” the core stays zero-dependency.
283
201
 
284
202
  ## Documentation
285
203
 
286
- - [Agent Integration Guide](docs/agent-guide.md) โ€” wiring `ark-mcp` into Claude Code, Cursor, and other agent runtimes
287
- - [Production Hardening](docs/production-hardening.md) โ€” durable store interfaces to implement (`AuditStore`, `OutboxStore`, โ€ฆ)
288
- - [Example Config](docs/ark-check-example.json) โ€” a hand-curated `ark.config.json` starting point
289
- - [Runnable Examples](examples/) โ€” `examples/basic/` (kernel tour) and `examples/publish-smoke/` (consumer smoke test)
290
- - [Changelog](CHANGELOG.md)
291
-
292
- ---
204
+ - [AI Gates](docs/ai-gates.md) โ€” copy-paste setups for Claude Code, Cursor, Codex, and any hook-capable runtime
205
+ - [Agent Integration Guide](docs/agent-guide.md) โ€” manifest discovery and validation flows for agents
206
+ - [Production Hardening](docs/production-hardening.md) โ€” durable store interfaces (`AuditStore`, `OutboxStore`, โ€ฆ)
207
+ - [Example Config](docs/ark-check-example.json) โ€” a hand-curated `ark.config.json`
208
+ - [Runnable Examples](examples/) โ€” including `examples/hexagonal-order-api/`, a full hexagonal API you can break on purpose
209
+ - [Roadmap](ROADMAP.md) ยท [Contributing](CONTRIBUTING.md) ยท [Changelog](CHANGELOG.md)
293
210
 
294
211
  ## Development
295
212
 
296
213
  ```bash
297
- npm install
214
+ npm ci
215
+ npm run build # ark-mcp loads dist/
216
+ npx vitest run
298
217
  npm run typecheck
299
- npm run check:architecture
300
- npm test
301
- npm run build
218
+ npm run check:architecture # Ark gates itself in CI
302
219
  ```
303
220
 
304
- **Release process (already scripted):**
305
-
306
- ```bash
307
- npm run release:npm # full verify + publish
308
- npm run release:npm -- --dry # dry run
309
- ```
310
-
311
- The release script:
312
- 1. Typechecks + runs all tests + self architecture check
313
- 2. Builds
314
- 3. Temporarily swaps in the minimal publish manifest
315
- 4. Publishes
316
- 5. Restores dev manifest
317
-
318
- ---
221
+ Release: `npm run release:npm` (verifies typecheck + tests + architecture gate, then publishes; `-- --dry` for a dry run).
319
222
 
320
223
  ## License
321
224
 
@@ -324,5 +227,3 @@ MIT ยฉ Pedro Knigge
324
227
  ---
325
228
 
326
229
  **Ark doesn't generate architecture. It protects the architecture you already have โ€” at the exact moments it matters most.**
327
-
328
- Built for teams that use AI heavily and refuse to let entropy win.
package/bin/ark-check.mjs CHANGED
@@ -23,6 +23,8 @@ function parseArgs(argv) {
23
23
  strictConfig: false,
24
24
  init: false,
25
25
  force: false,
26
+ baseline: undefined,
27
+ updateBaseline: false,
26
28
  };
27
29
  for (let i = 2; i < argv.length; i += 1) {
28
30
  const arg = argv[i];
@@ -30,6 +32,12 @@ function parseArgs(argv) {
30
32
  else if (arg === '--strict-config') args.strictConfig = true;
31
33
  else if (arg === '--init') args.init = true;
32
34
  else if (arg === '--force') args.force = true;
35
+ else if (arg === '--baseline' || arg === '--update-baseline') {
36
+ if (arg === '--update-baseline') args.updateBaseline = true;
37
+ // optional path value: consume the next arg only when it isn't another flag
38
+ const next = argv[i + 1];
39
+ args.baseline = next && !next.startsWith('-') ? argv[++i] : '.ark-baseline.json';
40
+ }
33
41
  else if (arg === '--root') args.root = path.resolve(argv[++i]);
34
42
  else if (arg === '--config') args.config = argv[++i];
35
43
  else if (arg === '--manifest') args.manifest = argv[++i];
@@ -42,10 +50,15 @@ function parseArgs(argv) {
42
50
 
43
51
  function usage() {
44
52
  return [
45
- 'Usage: ark-check --root <project> --config <ark.config.json> [--manifest <ark.manifest.json>] [--tsconfig <tsconfig.json>] [--strict-config] [--json]',
53
+ 'Usage: ark-check --root <project> --config <ark.config.json> [--manifest <ark.manifest.json>] [--tsconfig <tsconfig.json>] [--strict-config] [--json] [--baseline [file]]',
46
54
  ' ark-check --init [--force]',
55
+ ' ark-check --update-baseline [file] freeze current violations (default .ark-baseline.json)',
47
56
  ' ark-check --print-config eleven-layer',
48
57
  '',
58
+ 'Adopting Ark in an existing codebase? Run --update-baseline once to freeze existing',
59
+ 'violations, commit the baseline file, and gate CI with --baseline: only NEW violations',
60
+ 'fail the check, so the ratchet only moves toward zero.',
61
+ '',
49
62
  '--init scans the project for the built-in layer directory conventions (src/domain,',
50
63
  'src/application, src/adapters/persistence, ...) and writes an ark.config.json covering',
51
64
  'only the layers that actually exist, with the default rules filtered to those layers.',
@@ -564,6 +577,71 @@ function publishHasSource(ts, node) {
564
577
  );
565
578
  }
566
579
 
580
+ // ponytail: baseline keys exclude the line number so unrelated edits that shift lines
581
+ // don't resurrect frozen violations; the trade-off is that N identical violations in one
582
+ // file collapse to one key.
583
+ function baselineKey(violation) {
584
+ return [
585
+ violation.ruleId,
586
+ violation.file,
587
+ violation.fromLayer ?? '',
588
+ violation.toLayer ?? '',
589
+ violation.target ?? '',
590
+ ].join('|');
591
+ }
592
+
593
+ function readBaseline(root, baselinePath) {
594
+ const fullPath = path.isAbsolute(baselinePath) ? baselinePath : path.join(root, baselinePath);
595
+ if (!fs.existsSync(fullPath)) return { keys: new Set(), fullPath, exists: false };
596
+ const raw = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
597
+ return { keys: new Set(raw.violations ?? []), fullPath, exists: true };
598
+ }
599
+
600
+ function writeBaseline(root, baselinePath, violations) {
601
+ const fullPath = path.isAbsolute(baselinePath) ? baselinePath : path.join(root, baselinePath);
602
+ const keys = [...new Set(violations.map(baselineKey))].sort();
603
+ fs.writeFileSync(
604
+ fullPath,
605
+ `${JSON.stringify({ version: 1, note: 'Frozen ark-check violations. Only NEW violations fail --baseline runs. Regenerate with: ark-check --update-baseline', violations: keys }, null, 2)}\n`
606
+ );
607
+ return { fullPath, count: keys.length };
608
+ }
609
+
610
+ const useColor = process.stderr.isTTY && !process.env.NO_COLOR;
611
+ const color = {
612
+ red: (s) => (useColor ? `\x1b[31m${s}\x1b[0m` : s),
613
+ yellow: (s) => (useColor ? `\x1b[33m${s}\x1b[0m` : s),
614
+ green: (s) => (useColor ? `\x1b[32m${s}\x1b[0m` : s),
615
+ dim: (s) => (useColor ? `\x1b[2m${s}\x1b[0m` : s),
616
+ bold: (s) => (useColor ? `\x1b[1m${s}\x1b[0m` : s),
617
+ };
618
+
619
+ const FIX_HINTS = {
620
+ LAYER_IMPORT_VIOLATION:
621
+ 'Depend on a port/interface owned by an inner layer instead, or move this code to a layer allowed to make this import.',
622
+ LAYER_INTENT_REFERENCE_VIOLATION:
623
+ 'Reference intents through a layer that owns them (e.g. subscribe from an adapter, not from the domain).',
624
+ RAW_EVENT_PUBLISH:
625
+ 'Define the intent with ark.registry.define(...) and publish through the returned creator.',
626
+ PUBLISH_MISSING_SOURCE:
627
+ 'Add metadata.source (the publishing intent name) to the publish call.',
628
+ PUBLISH_SOURCE_LAYER_MISMATCH:
629
+ 'Use a source intent that belongs to the same layer as the publishing file, or move the file.',
630
+ };
631
+
632
+ function printViolation(violation) {
633
+ const location = `${violation.file}:${violation.line}`;
634
+ console.error(`${color.red('โœ–')} ${color.bold(violation.ruleId)} ${location}`);
635
+ if (violation.fromLayer && violation.toLayer) {
636
+ const target = violation.target ? ` ${color.dim(`(${violation.target})`)}` : '';
637
+ console.error(` ${violation.fromLayer} โ†’ ${violation.toLayer}${target}`);
638
+ }
639
+ console.error(` ${violation.message}`);
640
+ const hint = FIX_HINTS[violation.ruleId];
641
+ if (hint) console.error(` ${color.dim(`fix: ${hint}`)}`);
642
+ console.error('');
643
+ }
644
+
567
645
  function moduleSpecifierFromCall(ts, node) {
568
646
  if (!ts.isCallExpression(node)) return undefined;
569
647
 
@@ -733,38 +811,82 @@ async function main() {
733
811
  visit(sourceFile);
734
812
  }
735
813
 
814
+ if (args.updateBaseline) {
815
+ const { fullPath, count } = writeBaseline(root, args.baseline, violations);
816
+ console.log(`Wrote ${fullPath} with ${count} frozen violation key(s).`);
817
+ console.log('Commit it and gate CI with: ark-check --baseline (only NEW violations fail).');
818
+ return;
819
+ }
820
+
821
+ let suppressed = [];
822
+ let activeViolations = violations;
823
+ let staleBaselineKeys = 0;
824
+ if (args.baseline) {
825
+ const baseline = readBaseline(root, args.baseline);
826
+ if (baseline.exists) {
827
+ suppressed = violations.filter((violation) => baseline.keys.has(baselineKey(violation)));
828
+ activeViolations = violations.filter(
829
+ (violation) => !baseline.keys.has(baselineKey(violation))
830
+ );
831
+ const currentKeys = new Set(violations.map(baselineKey));
832
+ staleBaselineKeys = [...baseline.keys].filter((key) => !currentKeys.has(key)).length;
833
+ } else {
834
+ warnings.push(
835
+ configWarning(
836
+ 'BASELINE_NOT_FOUND',
837
+ `Baseline file not found: ${baseline.fullPath}. Generate it with: ark-check --update-baseline`
838
+ )
839
+ );
840
+ }
841
+ }
842
+
843
+ const ok = activeViolations.length === 0 && (!args.strictConfig || warnings.length === 0);
844
+
736
845
  if (args.json) {
737
846
  console.log(JSON.stringify({
738
- ok: violations.length === 0 && (!args.strictConfig || warnings.length === 0),
739
- violations,
847
+ ok,
848
+ violations: activeViolations,
849
+ suppressedViolations: suppressed.length,
850
+ staleBaselineKeys,
740
851
  warnings,
741
852
  }, null, 2));
742
- } else if (violations.length === 0) {
853
+ } else {
743
854
  for (const warning of warnings) {
744
- console.error(`warning ${warning.ruleId} ${warning.message}`);
855
+ console.error(`${color.yellow('warning')} ${warning.ruleId} ${warning.message}`);
745
856
  }
746
- if (warnings.length === 0) {
747
- console.log('Ark check passed.');
748
- } else if (args.strictConfig) {
749
- console.error(`Ark check failed with ${warnings.length} config warning(s).`);
750
- } else {
751
- console.log(`Ark check passed with ${warnings.length} config warning(s).`);
857
+ for (const violation of activeViolations) {
858
+ printViolation(violation);
752
859
  }
753
- } else {
754
- for (const warning of warnings) {
755
- console.error(`warning ${warning.ruleId} ${warning.message}`);
860
+
861
+ const baselineNote =
862
+ suppressed.length > 0 ? ` (${suppressed.length} suppressed by baseline)` : '';
863
+ if (staleBaselineKeys > 0) {
864
+ console.error(
865
+ color.dim(
866
+ `${staleBaselineKeys} baseline entr(y/ies) no longer occur โ€” tighten the ratchet with: ark-check --update-baseline`
867
+ )
868
+ );
756
869
  }
757
- for (const violation of violations) {
870
+ if (activeViolations.length === 0) {
871
+ if (warnings.length === 0) {
872
+ console.log(`${color.green('โœ”')} Ark check passed.${baselineNote}`);
873
+ } else if (args.strictConfig) {
874
+ console.error(
875
+ `${color.red('โœ–')} Ark check failed with ${warnings.length} config warning(s).${baselineNote}`
876
+ );
877
+ } else {
878
+ console.log(
879
+ `${color.green('โœ”')} Ark check passed with ${warnings.length} config warning(s).${baselineNote}`
880
+ );
881
+ }
882
+ } else {
758
883
  console.error(
759
- `${violation.file}:${violation.line} ${violation.ruleId} ${violation.message}`
884
+ `${color.red('โœ–')} ${activeViolations.length} violation(s).${baselineNote}`
760
885
  );
761
886
  }
762
887
  }
763
888
 
764
- process.exitCode =
765
- violations.length === 0 && (!args.strictConfig || warnings.length === 0)
766
- ? 0
767
- : 1;
889
+ process.exitCode = ok ? 0 : 1;
768
890
  }
769
891
 
770
892
  main().catch((error) => {
package/bin/ark-mcp.mjs CHANGED
File without changes