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 +121 -220
- package/bin/ark-check.mjs +142 -20
- package/bin/ark-mcp.mjs +0 -0
- package/dist/index.cjs +25 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -957
- package/dist/index.d.ts +4 -957
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -1
- package/dist/nestjs/index.cjs +2301 -0
- package/dist/nestjs/index.cjs.map +1 -0
- package/dist/nestjs/index.d.cts +22 -0
- package/dist/nestjs/index.d.ts +22 -0
- package/dist/nestjs/index.js +2298 -0
- package/dist/nestjs/index.js.map +1 -0
- package/dist/types-7K_KQCgS.d.cts +991 -0
- package/dist/types-7K_KQCgS.d.ts +991 -0
- package/package.json +43 -8
- package/server.json +31 -0
package/README.md
CHANGED
|
@@ -2,126 +2,147 @@
|
|
|
2
2
|
|
|
3
3
|
# ๐๏ธ Ark โ Architectural Runtime Kernel
|
|
4
4
|
|
|
5
|
-
**
|
|
6
|
-
|
|
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
|
-
[](https://github.com/pedroknigge/ark/actions/workflows/ci.yml)
|
|
8
|
+
[](https://github.com/pedroknigge/ark-runtime-kernel/actions/workflows/ci.yml)
|
|
9
9
|
[](https://www.npmjs.com/package/ark-runtime-kernel)
|
|
10
10
|
[](LICENSE)
|
|
11
11
|

|
|
12
12
|

|
|
13
13
|

|
|
14
14
|
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
D -->|valid| F["โ
Merge"]
|
|
23
|
+

|
|
32
24
|
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
+
Adopting on a codebase that already has violations? Freeze them and ratchet down:
|
|
58
38
|
|
|
59
39
|
```bash
|
|
60
|
-
npx ark-check --
|
|
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
|
-
|
|
44
|
+
Then gate your agents (Claude Code shown; [Cursor / Codex / others](docs/ai-gates.md)):
|
|
64
45
|
|
|
65
|
-
```
|
|
66
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
101
|
+
**What it catches (via real TypeScript module resolution โ path aliases included):**
|
|
76
102
|
|
|
77
|
-
|
|
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
|
-
|
|
108
|
+
Violations come with the layer edge, the resolved target, and a fix hint:
|
|
82
109
|
|
|
83
|
-
|
|
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
|
-
###
|
|
117
|
+
### GitHub Action
|
|
86
118
|
|
|
87
|
-
|
|
88
|
-
-
|
|
89
|
-
|
|
90
|
-
-
|
|
91
|
-
|
|
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
|
-
|
|
125
|
+
Inputs: `root`, `config`, `strict-config`, `baseline`, `version`.
|
|
99
126
|
|
|
100
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
129
|
+
```js
|
|
130
|
+
// eslint.config.js
|
|
131
|
+
import ark from 'ark-runtime-kernel/eslint';
|
|
132
|
+
export default [ark.configs.recommended];
|
|
133
|
+
```
|
|
112
134
|
|
|
113
|
-
|
|
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
|
-
|
|
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(); //
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
185
|
+
### NestJS
|
|
176
186
|
|
|
177
|
-
|
|
187
|
+
```ts
|
|
188
|
+
import { ArkModule, InjectArk } from 'ark-runtime-kernel/nestjs';
|
|
189
|
+
import type { ArkKernel } from 'ark-runtime-kernel';
|
|
178
190
|
|
|
179
|
-
|
|
191
|
+
@Module({ imports: [ArkModule.forRoot()] })
|
|
192
|
+
export class AppModule {}
|
|
180
193
|
|
|
181
|
-
|
|
182
|
-
{
|
|
183
|
-
|
|
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
|
-
|
|
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
|
-
- [
|
|
287
|
-
- [
|
|
288
|
-
- [
|
|
289
|
-
- [
|
|
290
|
-
- [
|
|
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
|
|
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
|
-
|
|
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
|
|
739
|
-
violations,
|
|
847
|
+
ok,
|
|
848
|
+
violations: activeViolations,
|
|
849
|
+
suppressedViolations: suppressed.length,
|
|
850
|
+
staleBaselineKeys,
|
|
740
851
|
warnings,
|
|
741
852
|
}, null, 2));
|
|
742
|
-
} else
|
|
853
|
+
} else {
|
|
743
854
|
for (const warning of warnings) {
|
|
744
|
-
console.error(
|
|
855
|
+
console.error(`${color.yellow('warning')} ${warning.ruleId} ${warning.message}`);
|
|
745
856
|
}
|
|
746
|
-
|
|
747
|
-
|
|
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
|
-
|
|
754
|
-
|
|
755
|
-
|
|
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
|
-
|
|
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
|
-
`${
|
|
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
|