@archrad/deterministic 0.1.4 → 0.1.5

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/CHANGELOG.md CHANGED
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.5] - 2026-04-07
11
+
12
+ ### Added
13
+
14
+ - **`archrad-mcp`** — [Model Context Protocol](https://modelcontextprotocol.io/) server over **stdio** (same deterministic engine as the CLI). Tools: **`archrad_validate_ir`**, **`archrad_lint_summary`**, **`archrad_validate_drift`**, **`archrad_policy_packs_load`**, **`archrad_suggest_fix`** (static remediation per built-in code — **not** generated patches), **`archrad_list_rule_codes`**. IR input: inline **`ir`** or **`irPath`** (JSON file; max **25 MiB**). **`archrad_suggest_fix`** **`docsUrl`** → GitHub **`docs/RULE_CODES.md`**. Scope: **`docs/MCP.md`** (OSS: IR + local paths; no tracking params in tool responses).
15
+ - **Docs:** **`docs/DRIFT.md`** (deterministic drift semantics), **`docs/RULE_CODES.md`** (built-in codes + anchors for MCP **`docsUrl`**), **`docs/MCP.md`** (local testing).
16
+
17
+ ### Fixed
18
+
19
+ - **CodeQL** `js/polynomial-redos` — linear-time hyphen/underscore edge stripping for **`safeId`** / OpenAPI-derived ids (replaces ambiguous alternation regexes).
20
+
10
21
  ## [0.1.4] - 2026-04-06
11
22
 
12
23
  ### Added
package/README.md CHANGED
@@ -2,28 +2,80 @@
2
2
 
3
3
  ![archrad validate — IR-LINT-DIRECT-DB-ACCESS-002 first, fix on the graph, clean gate](demo-validate.gif)
4
4
 
5
- <!-- GIFs: validate hero — `npm run build && vhs scripts/record-demo-validate.tape` → demo-validate.gif. Drift — `npm run record:demo:drift` → demo-drift.gif (terminal tape) or replay scripts + capture. For “edit → save → validate-drift” (IDE + terminal, skeptic-grade), see scripts/DEMO_GIF_STORYBOARD.md “Trust loop”. scripts/README_DEMO_RECORDING.md. package.json `files`. -->
5
+ ![Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue) ![no LLM](https://img.shields.io/badge/no%20LLM-deterministic-green) ![no account](https://img.shields.io/badge/no%20account-offline-lightgrey)
6
6
 
7
- **A deterministic compiler and linter for system architecture.**
7
+ Your architecture drifts before you write a single line of code. `archrad validate` catches it — deterministically, in CI, before the PR merges.
8
8
 
9
- **Validate your architecture before you write code.**
9
+ Define your system as a graph. ArchRAD compiles it, lints it against architecture rules, and tells you exactly what's wrong — with rule codes, not opinions.
10
10
 
11
- **For AI agents / IDE assistants:** see **`llms.txt`** in this package (llms.txt-style project summary for tools like Claude Code, Devin, etc.).
11
+ ---
12
+
13
+ ## Quick start (60 seconds)
14
+
15
+ ```bash
16
+ npm install -g @archrad/deterministic
17
+ archrad validate --ir fixtures/demo-direct-db-violation.json
18
+ ```
19
+
20
+ You should see something like (exact wording may vary slightly by version):
21
+
22
+ ```
23
+ ⚠️ IR-LINT-DIRECT-DB-ACCESS-002: API node "orders-api" connects directly to datastore node "orders-db"
24
+ Fix: Introduce a service or domain layer between HTTP handlers and persistence.
25
+ ```
26
+
27
+ For a smaller graph (single endpoint, no DB edge), try **`fixtures/minimal-graph.json`** — you will get different warnings (e.g. health/auth heuristics), not **`IR-LINT-DIRECT-DB-ACCESS-002`**.
28
+
29
+ No IR file yet? Cold-start from an existing OpenAPI spec:
30
+
31
+ ```bash
32
+ archrad ingest openapi --spec ./openapi.yaml --out ./graph.json
33
+ archrad validate --ir ./graph.json
34
+ ```
35
+
36
+ ---
37
+
38
+ ## What it does
39
+
40
+ ArchRAD is a blueprint compiler and governance layer. You define your architecture as an IR — nodes, edges, allowed connections — and ArchRAD validates it against a deterministic rule engine. The same IR, the same rules, the same inputs always produce the same findings.
12
41
 
13
- **Apache-2.0** blueprint **IR** (JSON graph) **FastAPI** or **Express** + **OpenAPI**, **Docker**, **Makefile** — **no LLM**, **no account**, **offline**.
42
+ | Command | What it checks | Codes |
43
+ |---------|---------------|-------|
44
+ | `archrad validate` | Graph structure + architecture lint | `IR-STRUCT-*` `IR-LINT-*` |
45
+ | `archrad validate-drift` | IR vs generated code on disk | `DRIFT-*` |
46
+ | `archrad ingest openapi` | Derive IR from existing OpenAPI spec | — |
47
+ | `archrad export` | Compile IR → FastAPI or Express + Docker | — |
48
+
49
+ ## CI integration
50
+
51
+ ```bash
52
+ # Fail on any structural error (default):
53
+ archrad validate --ir ./graph.json
54
+
55
+ # Also fail on lint warnings:
56
+ archrad validate --ir ./graph.json --fail-on-warning
14
57
 
15
- **OSS positioning:** *Includes structural validation + basic architecture linting (rule-based, deterministic).*
58
+ # Machine-readable output for GitHub Actions:
59
+ archrad validate --ir ./graph.json --json
60
+ ```
16
61
 
17
- > This is not only a generator: **`archrad validate`** treats your graph like source code — **IR-STRUCT-*** (shape/refs/cycles) and **IR-LINT-*** (light architecture heuristics: health routes, fan-out, sync chains, HTTP→DB coupling). Then **`archrad export`** compiles to runnable projects. After you have a tree on disk, **`archrad validate-drift`** compares it to a **fresh** export from the same IR (missing/changed files — **not** semantic code review). Generated **OpenAPI** gets a **document-shape** pass (parse + required fields — **not** Spectral-style spec lint).
62
+ ## MCP server (Cursor / Claude Desktop)
18
63
 
19
- **Open core (OSS):** **IR structural validation**, **basic architecture lint** (rule-based, deterministic), **OpenAPI document-shape** checks. **ArchRad Cloud** adds **policy / compliance**, deeper **architecture intelligence**, and **AI remediation** — see **`docs/STRUCTURAL_VS_SEMANTIC_VALIDATION.md`**.
64
+ After install, `archrad-mcp` is on your PATH. Add it to your IDE:
20
65
 
21
- ### Concept, cold start, and strategy (honest scope)
66
+ ```json
67
+ {
68
+ "mcpServers": {
69
+ "archrad": { "command": "archrad-mcp" }
70
+ }
71
+ }
72
+ ```
22
73
 
23
- The **core idea** is sound: **IR = what to build**, **codegen = how** — with compiler-style validation and deterministic output. **Adoption friction** is real: this repo is a **compiler with no bundled authoring UI** (JSON/graph in; you bring **ArchRad Cloud**, another tool, or hand-written IR). **Where the IR comes from** is intentionally **plural**: **manual** graph JSON/YAML today, plus **`archrad ingest openapi`** (and more ingestion surfaces over time — e.g. **IaC**). Treat **OpenAPI IR** as a **starting lane**, not the whole roadmap: regenerate IR in CI, then **`archrad validate`** / **`archrad export`** — structural HTTP surface only, not full system semantics. Draft language for “drift / where does IR come from?” threads: **`scripts/SOCIAL_POST_DRIFT_AND_INGESTION.md`**. **Export is one-way** (no built-in round-trip from edited code back to IR)—best thought of as **scaffold + contract validation**, not full lifecycle architecture sync unless you own that workflow. Many teams will still treat the OSS layer as a **trust and CI artifact** that makes the Cloud/AI path auditable. Read **`docs/CONCEPT_ADOPTION_AND_LIMITS.md`** for the full framing and future directions (e.g. lightweight YAML/IDE ergonomics).
74
+ Your agent can call the same engine as the CLI via **six** MCP tools (e.g. `archrad_validate_ir`, `archrad_lint_summary`, `archrad_validate_drift`, `archrad_policy_packs_load`, `archrad_list_rule_codes`, `archrad_suggest_fix`). See [docs/MCP.md](docs/MCP.md) for parameters and local testing.
24
75
 
25
76
  ---
26
77
 
78
+
27
79
  ## How it works (architecture)
28
80
 
29
81
  ```
@@ -67,7 +119,7 @@ IR (nodes/edges) → validateIrStructural (IR-STRUCT-*) → errors block exp
67
119
  | **AI remediation** | Repair loops, suggested edits |
68
120
 
69
121
  1. **IR structural validation:** duplicate/missing node ids, bad HTTP `config.url` / `config.method`, unknown edge endpoints, directed cycles.
70
- 2. **Architecture lint:** Implemented as a **registry of visitor functions** on a parsed graph (`buildParsedLintGraph` → **`LINT_RULE_REGISTRY`** in **`src/lint-rules.ts`**). If the IR cannot be parsed, **`buildParsedLintGraph`** returns **`{ findings }`** (IR-STRUCT-*) instead of **`null`**; use **`isParsedLintGraph()`** or call **`validateIrLint`**, which forwards those findings. Each rule returns **`IrStructuralFinding[]`**; **`runArchitectureLinting`** / **`validateIrLint`** flatten them. **Custom org rules:** compose **`runArchitectureLinting`** with your own **`(g) => findings`** in CI (worked example: **`docs/CUSTOM_RULES.md`**), or **fork** and append to **`LINT_RULE_REGISTRY`** if the stock **`archrad validate`** CLI must emit your codes. CLI **`archrad validate`** / **`archrad export`** print lint under **Architecture lint (IR-LINT-*)** (grouped separately from structural). Codes include **IR-LINT-DIRECT-DB-ACCESS-002**, **IR-LINT-SYNC-CHAIN-001**, **IR-LINT-NO-HEALTHCHECK-003**, **IR-LINT-HIGH-FANOUT-004**, **IR-LINT-ISOLATED-NODE-005**, **IR-LINT-DUPLICATE-EDGE-006**, **IR-LINT-HTTP-MISSING-NAME-007**, **IR-LINT-DATASTORE-NO-INCOMING-008**, **IR-LINT-MULTIPLE-HTTP-ENTRIES-009**. **Sync-chain** depth counts **synchronous** edges only; mark message/queue/async hops via **`edge.metadata.protocol`** / **`config.async`** (see **`edgeRepresentsAsyncBoundary`** in **`lint-graph.ts`** and **`docs/ENGINEERING_NOTES.md`**).
122
+ 2. **Architecture lint:** Implemented as a **registry of visitor functions** on a parsed graph (`buildParsedLintGraph` → **`LINT_RULE_REGISTRY`** in **`src/lint-rules.ts`**). If the IR cannot be parsed, **`buildParsedLintGraph`** returns **`{ findings }`** (IR-STRUCT-*) instead of **`null`**; use **`isParsedLintGraph()`** or call **`validateIrLint`**, which forwards those findings. Each rule returns **`IrStructuralFinding[]`**; **`runArchitectureLinting`** / **`validateIrLint`** flatten them. **Custom org rules:** compose **`runArchitectureLinting`** with your own **`(g) => findings`** in CI (worked example: **`docs/CUSTOM_RULES.md`**), or **fork** and append to **`LINT_RULE_REGISTRY`** if the stock **`archrad validate`** CLI must emit your codes. CLI **`archrad validate`** / **`archrad export`** print lint under **Architecture lint (IR-LINT-*)** (grouped separately from structural). Codes include **IR-LINT-DIRECT-DB-ACCESS-002**, **IR-LINT-SYNC-CHAIN-001**, **IR-LINT-NO-HEALTHCHECK-003**, **IR-LINT-HIGH-FANOUT-004**, **IR-LINT-ISOLATED-NODE-005**, **IR-LINT-DUPLICATE-EDGE-006**, **IR-LINT-HTTP-MISSING-NAME-007**, **IR-LINT-DATASTORE-NO-INCOMING-008**, **IR-LINT-MULTIPLE-HTTP-ENTRIES-009**, **IR-LINT-MISSING-AUTH-010**, **IR-LINT-DEAD-NODE-011**. **Sync-chain** depth counts **synchronous** edges only; mark message/queue/async hops via **`edge.metadata.protocol`** / **`config.async`** (see **`edgeRepresentsAsyncBoundary`** in **`lint-graph.ts`** and **`docs/ENGINEERING_NOTES.md`**).
71
123
  3. **Generators** → `openapi.yaml`, handlers, deps.
72
124
  4. **Golden path** → `make run` / `docker compose up --build`.
73
125
  5. **OpenAPI document shape** on the bundle — **not** [Spectral](https://github.com/stoplightio/spectral)-level lint. Issues → **`openApiStructuralWarnings`**.
@@ -76,6 +128,8 @@ IR (nodes/edges) → validateIrStructural (IR-STRUCT-*) → errors block exp
76
128
 
77
129
  **Trust builder:** **IR-STRUCT-*** errors block export; **IR-LINT-*** warnings are visible and can **gate CI** via **`--fail-on-warning`** / **`--max-warnings`**; OpenAPI shape issues surface as export warnings.
78
130
 
131
+ **Reference (OSS):** **[`docs/DRIFT.md`](docs/DRIFT.md)** (deterministic **`validate-drift`**), **[`docs/RULE_CODES.md`](docs/RULE_CODES.md)** (finding codes; MCP **`docsUrl`** targets GitHub anchors), **[`docs/MCP.md`](docs/MCP.md)** (MCP tools + local testing).
132
+
79
133
  ### Codegen vs validation (retry, timeouts, policy)
80
134
 
81
135
  Generators **may emit** retry/timeout/circuit-breaker **code** when the IR carries matching edge or node config (e.g. `retryPolicy`). That is **code generation**, not a guarantee. OSS does **not** currently **require** or **lint** “every external call must have timeout/retry” — that class of rule is **semantic / policy** and fits **ArchRad Cloud** or custom linters on top of the IR.
@@ -92,10 +146,17 @@ Generators **may emit** retry/timeout/circuit-breaker **code** when the IR carri
92
146
  | **CLI validate** | CI / pre-commit: IR structural + architecture lint, no codegen | `archrad validate --ir graph.json` |
93
147
  | **CLI validate-drift** | After export or merges: on-disk tree vs fresh deterministic export from same IR | `archrad validate-drift -i graph.json -t python -o ./out` |
94
148
  | **Library** (`@archrad/deterministic`) | IDPs / pipelines | `runDeterministicExport` → files + findings; **`runValidateDrift`** / **`runDriftCheckAgainstFiles`** for drift |
149
+ | **MCP** (`archrad-mcp`) | Cursor / Claude Desktop / other MCP hosts | stdio server: validate IR, lint summary, drift, policy packs, static **`archrad_suggest_fix`** — see **`docs/MCP.md`** |
150
+
151
+ **MCP (Cursor example):** after `npm i -g @archrad/deterministic` (or `npx`), add a server with command **`archrad-mcp`** and no args (stdio). Pass **`ir`** inline or **`irPath`** to a JSON file for large graphs. **`archrad_suggest_fix`** returns curated text for a finding code (e.g. `IR-LINT-MISSING-AUTH-010`) — not machine-generated IR patches. **Step-by-step testing** (smoke script, MCP Inspector, Cursor chat prompts): **`docs/MCP.md`**, section **Local testing**.
95
152
 
96
153
  ### CLI
97
154
 
98
- **Input is structured IR (JSON), not natural language.** There is no `archrad export --prompt "..."`. You pass a **graph file** (nodes/edges) like **`fixtures/minimal-graph.json`**. The **npm README GIF** uses **`fixtures/demo-direct-db-violation.json`** → **`fixtures/demo-direct-db-layered.json`**: **failure-first** **`IR-LINT-DIRECT-DB-ACCESS-002`**, fix on the graph, then a **clean** validate (no codegen in the clip). For a graph that hits **many** lint rules at once (stress test), use **`fixtures/ecommerce-with-warnings.json`**. For a **golden payment + retry** graph (edge `config.retry.maxAttempts` + payment node `config.retryPolicy`, both `3`), use **`fixtures/payment-retry-demo.json`** — export shows **`retry_policy`** / retry helpers in **`app/main.py`**. Record that path with **`scripts/record-demo-payment-retry.tape`** (→ **`demo-payment-retry.gif`**). **Drift clip:** **`scripts/record-demo-drift.tape`** / **`npm run record:demo:drift`** (→ **`demo-drift.gif`**) — export, **`tail`** before/after a one-line tamper on **`out/app/main.py`**, then **`validate-drift`**; if **VHS** fails on your machine, run **`scripts/run-demo-drift-sequence.ps1`** or **`.sh`** while screen-capturing (see **`scripts/README_DEMO_RECORDING.md`**). See **`scripts/DEMO_GIF_STORYBOARD.md`** for all tapes. **CLI:** `--target python` is the FastAPI bundle; there is no separate `fastapi` target name. To go from **plain English → IR**, use **ArchRad Cloud** or your own LLM step; this package only does **IR → files**.
155
+ **Input is structured IR (JSON), not natural language.** There is no `archrad export --prompt "..."`. Pass a **graph file** (nodes/edges).
156
+
157
+ **Fixtures** (in this repo): **`fixtures/minimal-graph.json`** (small); **`fixtures/demo-direct-db-violation.json`** / **`fixtures/demo-direct-db-layered.json`** (before/after **`IR-LINT-DIRECT-DB-ACCESS-002`**); **`fixtures/ecommerce-with-warnings.json`** (many lint rules); **`fixtures/payment-retry-demo.json`** (retry-related codegen in export). **`--target python`** is the FastAPI bundle; there is no separate `fastapi` target. To go from **plain English → IR**, use **ArchRad Cloud** or your own LLM step; this package only does **IR → files**.
158
+
159
+ **Recording demos and GIFs** (VHS, storyboards, drift replay): **`scripts/README_DEMO_RECORDING.md`** only — not required to use the CLI.
99
160
 
100
161
  **OpenAPI → JSON (spec as source of truth):** each operation under `paths` becomes an `http` node (`config.url` + `config.method`). Then validate and export like any other IR:
101
162
 
@@ -117,16 +178,13 @@ archrad validate --ir ./graph.json
117
178
 
118
179
  YAML must have either top-level **`graph:`** (object) or top-level **`nodes:`** (array); bare graphs are wrapped as `{ "graph": { ... } }` automatically.
119
180
 
120
- After `npm run build` (required after `npm ci`; there is no `prepare` hook — see **`docs/ENGINEERING_NOTES.md`**):
181
+ **After `npm install -g` or `npx`** (typical):
121
182
 
122
183
  ```bash
123
- node dist/cli.js export --ir fixtures/minimal-graph.json --target python --out ./my-api
124
- node dist/cli.js yaml-to-ir --yaml fixtures/minimal-graph.yaml --out /tmp/ir.json
125
- # After global install / npx:
126
184
  archrad export --ir ./graph.json --target node --out ./my-express-api
127
185
 
128
186
  # Validate IR (structural + architecture lint). Pretty output; exit 1 on structural errors by default:
129
- node dist/cli.js validate --ir fixtures/minimal-graph.json
187
+ archrad validate --ir ./graph.json
130
188
  # Machine-readable + CI gates:
131
189
  archrad validate --ir ./graph.json --json
132
190
  archrad validate --ir ./graph.json --fail-on-warning
@@ -137,6 +195,8 @@ archrad validate --ir ./graph.json --skip-lint
137
195
  archrad validate --ir ./graph.json --policies ./policy-packs
138
196
  ```
139
197
 
198
+ **From a git clone** (contributors): run **`npm ci && npm run build`** in the package root (there is no `prepare` hook — see **`docs/ENGINEERING_NOTES.md`**), then use **`node dist/cli.js`** the same way you would use **`archrad`** (e.g. **`node dist/cli.js validate --ir fixtures/minimal-graph.json`**).
199
+
140
200
  **Deterministic drift (thin, OSS):** compare an existing export tree on disk to a **fresh** export from the same IR. Detects **missing** / **changed** generated files (line endings normalized). Optional **`--strict-extra`** flags files present on disk but not in the reference export. Not semantic “does code match intent” — **ArchRad Cloud** adds builder/UI drift checks and broader governance.
141
201
 
142
202
  ```bash
@@ -149,8 +209,6 @@ archrad validate-drift -i ./graph.json -t python -o ./out --skip-host-port-check
149
209
  archrad validate-drift -i ./graph.json -t python -o ./out --strict-extra
150
210
  ```
151
211
 
152
- Regenerate the matching clip: **`npm run record:demo:drift`** (VHS) → **`demo-drift.gif`**, or **`scripts/run-demo-drift-sequence.ps1`** / **`.sh`** + ShareX/OBS if VHS is unavailable (see **`scripts/DEMO_GIF_STORYBOARD.md`**).
153
-
154
212
  #### Example: validate architecture
155
213
 
156
214
  ```bash
@@ -183,13 +241,6 @@ By default, if **8080** (or your `--host-port`) looks **busy** on localhost, the
183
241
 
184
242
  **Export** runs **IR structural validation**, then **architecture lint**, then codegen. **Structural errors** abort with **no files written**. **`irLintFindings`** contains only **`IR-LINT-*`**; **`IR-STRUCT-*`** from a failed parse always appear under **`irStructuralFindings`** (including when structural validation was skipped). **Lint warnings** print by default; use **`--fail-on-warning`** / **`--max-warnings`** to block writes for CI.
185
243
 
186
- ### Validate the package as a developer
187
-
188
- 1. `cd packages/deterministic && npm ci && npm run build && npm test`
189
- 2. `node dist/cli.js export --ir fixtures/minimal-graph.json --target python --out ./tmp-out`
190
- 3. `cd tmp-out && make run` then `curl` the URL shown in the generated **README** (port matches `--host-port` if you set it).
191
- 4. Optional: `node dist/cli.js export ... --host-port 18080` if **8080** is already taken.
192
-
193
244
  ### Library
194
245
 
195
246
  ```typescript
@@ -224,9 +275,9 @@ Optional: `isLocalHostPortFree` / `normalizeGoldenHostPort` from the same packag
224
275
 
225
276
  ---
226
277
 
227
- ## Golden path (~60 seconds)
278
+ ## Golden path (contributors — local clone)
228
279
 
229
- From the package root (after build):
280
+ This path assumes you **cloned the repo** and ran **`npm ci && npm run build`** in the package root. If you only installed with **`npm install -g @archrad/deterministic`**, use **`archrad`** instead of **`node dist/cli.js`** (same flags).
230
281
 
231
282
  ```bash
232
283
  node dist/cli.js export --ir fixtures/minimal-graph.json --target python --out ./out
@@ -238,13 +289,9 @@ curl -sS -X POST http://localhost:8080/signup -H "Content-Type: application/json
238
289
 
239
290
  You should see **422 Unprocessable Entity** (FastAPI/Pydantic) or **400** with a clear body — proof the stack is live and validation matches the spec, not a silent 500.
240
291
 
241
- **Helper script** (prints the same flow; use when recording a terminal GIF):
242
-
243
- ```bash
244
- bash scripts/golden-path-demo.sh
245
- ```
292
+ Quick check from a clone: **`cd packages/deterministic && npm ci && npm run build && npm test`**, then export to **`./tmp-out`**, **`cd tmp-out && make run`**, **`curl`** as above. Use **`--host-port 18080`** (or **`node dist/cli.js export ... --host-port 18080`**) if **8080** is busy.
246
293
 
247
- See **`scripts/README_DEMO_RECORDING.md`** for **VHS / asciinema / ttyrec** tips, **When VHS fails**, **drift** replay scripts, and the **trust loop** (IDE edit + terminal **`validate-drift`**). The hero GIF at the top is **`demo-validate.gif`**; **`demo-drift.gif`** and the **trust-loop** storyboard live in **`scripts/DEMO_GIF_STORYBOARD.md`**.
294
+ Optional: **`bash scripts/golden-path-demo.sh`** runs the same flow. **Demo recording** (GIFs, tapes, drift replays): **`scripts/README_DEMO_RECORDING.md`**.
248
295
 
249
296
  ---
250
297
 
@@ -261,20 +308,6 @@ See **`scripts/README_DEMO_RECORDING.md`** for **VHS / asciinema / ttyrec** tips
261
308
 
262
309
  You can depend on this CLI and library **without** ArchRad Cloud. The cloud product stacks collaboration and AI on top of the same deterministic contract.
263
310
 
264
- **InkByte vs this package:** Deeper workflow analysis, enterprise validation routes, and LLM-assisted flows may exist in the **private InkByte monorepo** (`server/`, etc.); they are **not** part of the **`@archrad/deterministic`** npm surface unless shipped here. This README describes **only** what the OSS package proves.
265
-
266
- ---
267
-
268
- ## Monorepo vs public OSS repo
269
-
270
- The **canonical source** for this engine may live in a **private monorepo** next to the full product; `server` can depend on `file:../packages/deterministic`. The **public** GitHub repo should contain **only** this package — canonical clone: **`https://github.com/archradhq/arch-deterministic`**. Subtree publish: **`docs/OSS_VS_PRODUCT_REPOS.md`** and **`docs/PUBLISH_DETERMINISTIC_OSS.md`** (in the product monorepo).
271
-
272
- ---
273
-
274
- ## Publishing the public OSS repo
275
-
276
- From the private monorepo root: **`docs/PUBLISH_DETERMINISTIC_OSS.md`**. This tree includes **`.github/workflows/ci.yml`** and **Dependabot**; they run when this folder is the **git root** of the public repo.
277
-
278
311
  ---
279
312
 
280
313
  ## Contributing
package/biome.json CHANGED
@@ -1,25 +1,32 @@
1
- {
2
- "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
3
- "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
4
- "files": {
5
- "ignore": ["dist", "node_modules", "*.json", "fixtures", "schemas"]
6
- },
7
- "formatter": {
8
- "enabled": false
9
- },
10
- "organizeImports": { "enabled": false },
11
- "linter": {
12
- "enabled": true,
13
- "rules": {
14
- "recommended": false,
15
- "suspicious": {
16
- "noDebugger": "error"
17
- }
18
- }
19
- },
20
- "javascript": {
21
- "formatter": {
22
- "quoteStyle": "single"
23
- }
24
- }
25
- }
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.4.10/schema.json",
3
+ "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
4
+ "files": {
5
+ "includes": [
6
+ "**",
7
+ "!**/dist",
8
+ "!**/node_modules",
9
+ "!**/*.json",
10
+ "!**/fixtures",
11
+ "!**/schemas"
12
+ ]
13
+ },
14
+ "formatter": {
15
+ "enabled": false
16
+ },
17
+ "assist": { "actions": { "source": { "organizeImports": "off" } } },
18
+ "linter": {
19
+ "enabled": true,
20
+ "rules": {
21
+ "recommended": false,
22
+ "suspicious": {
23
+ "noDebugger": "error"
24
+ }
25
+ }
26
+ },
27
+ "javascript": {
28
+ "formatter": {
29
+ "quoteStyle": "single"
30
+ }
31
+ }
32
+ }
package/dist/cli.js CHANGED
@@ -76,7 +76,7 @@ const program = new Command();
76
76
  program
77
77
  .name('archrad')
78
78
  .description('Validate your architecture before you write code. Deterministic compiler + linter — FastAPI / Express (no LLM, no server).')
79
- .version('0.1.4');
79
+ .version('0.1.5');
80
80
  program
81
81
  .command('validate')
82
82
  .description('Validate your architecture before you write code — IR structural (IR-STRUCT-*) + architecture lint (IR-LINT-*)')
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * archrad-mcp — Model Context Protocol server (stdio) for deterministic IR validation,
4
+ * architecture lint, policy packs, and drift checks. Uses the same engine as `archrad` CLI.
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=mcp-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":";AACA;;;GAGG"}
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * archrad-mcp — Model Context Protocol server (stdio) for deterministic IR validation,
4
+ * architecture lint, policy packs, and drift checks. Uses the same engine as `archrad` CLI.
5
+ */
6
+ import { readFile, stat } from 'node:fs/promises';
7
+ import { resolve } from 'node:path';
8
+ import { z } from 'zod';
9
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
11
+ import { normalizeIrGraph, validateIrStructural, validateIrLint, runValidateDrift, sortFindings, loadPolicyPacksFromDirectory, loadPolicyPacksFromFiles, } from './index.js';
12
+ import { getStaticRuleGuidance, listStaticRuleCodes } from './static-rule-guidance.js';
13
+ const VERSION = '0.1.5';
14
+ /** Hard cap for `irPath` reads (see docs/MCP.md). */
15
+ const MAX_IR_FILE_BYTES = 25 * 1024 * 1024;
16
+ const irSourceSchema = {
17
+ ir: z.unknown().optional(),
18
+ irPath: z.string().optional(),
19
+ };
20
+ function jsonResult(payload) {
21
+ return {
22
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
23
+ };
24
+ }
25
+ async function loadIrFromArgs(args) {
26
+ const hasInline = args.ir !== undefined;
27
+ const hasPath = args.irPath != null && String(args.irPath).trim() !== '';
28
+ if (hasInline && hasPath) {
29
+ return { ok: false, error: 'Provide only one of `ir` or `irPath`.' };
30
+ }
31
+ if (hasPath) {
32
+ const p = resolve(args.irPath);
33
+ let st;
34
+ try {
35
+ st = await stat(p);
36
+ }
37
+ catch (e) {
38
+ return { ok: false, error: `irPath not readable: ${e instanceof Error ? e.message : String(e)}` };
39
+ }
40
+ if (!st.isFile()) {
41
+ return { ok: false, error: `irPath is not a file: ${p}` };
42
+ }
43
+ if (st.size > MAX_IR_FILE_BYTES) {
44
+ return {
45
+ ok: false,
46
+ error: `IR file is ${st.size} bytes (max ${MAX_IR_FILE_BYTES}). Split the graph, trim fixtures, or validate smaller subgraphs.`,
47
+ };
48
+ }
49
+ const raw = await readFile(p, 'utf8');
50
+ try {
51
+ return { ok: true, ir: JSON.parse(raw) };
52
+ }
53
+ catch (e) {
54
+ return { ok: false, error: `Invalid JSON in irPath: ${e instanceof Error ? e.message : String(e)}` };
55
+ }
56
+ }
57
+ if (hasInline) {
58
+ return { ok: true, ir: args.ir };
59
+ }
60
+ return { ok: false, error: 'Provide `ir` (inline JSON) or `irPath` (path to IR JSON file).' };
61
+ }
62
+ async function main() {
63
+ const server = new McpServer({
64
+ name: 'archrad-deterministic',
65
+ version: VERSION,
66
+ });
67
+ server.registerTool('archrad_suggest_fix', {
68
+ title: 'Static remediation for a finding code',
69
+ description: 'Deterministic title, remediation text, and canonical docs URL for a built-in IR-STRUCT / IR-LINT / DRIFT code. Does not generate patches or IR edits.',
70
+ inputSchema: {
71
+ findingCode: z.string().min(1),
72
+ },
73
+ }, async (args) => {
74
+ const g = getStaticRuleGuidance(args.findingCode);
75
+ if (!g) {
76
+ return jsonResult({
77
+ ok: false,
78
+ findingCode: args.findingCode,
79
+ error: 'Unknown built-in code. PolicyPack and org rules use custom rule ids in YAML — see your pack. Use archrad_list_rule_codes for built-in codes.',
80
+ });
81
+ }
82
+ return jsonResult({ ok: true, ...g });
83
+ });
84
+ server.registerTool('archrad_list_rule_codes', {
85
+ title: 'List built-in rule codes',
86
+ description: 'Sorted list of IR-STRUCT-*, IR-LINT-*, and DRIFT-* codes that have static guidance via archrad_suggest_fix.',
87
+ inputSchema: {},
88
+ }, async () => jsonResult({ codes: listStaticRuleCodes() }));
89
+ server.registerTool('archrad_validate_ir', {
90
+ title: 'Validate IR (structural + IR-LINT)',
91
+ description: 'Run deterministic structural validation (IR-STRUCT-*) and architecture lint (IR-LINT-*). Pass `ir` inline or `irPath` to a JSON file (recommended for large graphs). Optional local PolicyPack directory.',
92
+ inputSchema: {
93
+ ...irSourceSchema,
94
+ policiesDirectory: z.string().optional(),
95
+ },
96
+ }, async (args) => {
97
+ const loaded = await loadIrFromArgs(args);
98
+ if (!loaded.ok)
99
+ return jsonResult({ ok: false, phase: 'input', error: loaded.error });
100
+ const irRaw = loaded.ir;
101
+ const norm = normalizeIrGraph(irRaw);
102
+ if ('findings' in norm) {
103
+ return jsonResult({ ok: false, phase: 'normalize', findings: norm.findings });
104
+ }
105
+ const structural = validateIrStructural(irRaw);
106
+ let policyRuleVisitors;
107
+ if (args.policiesDirectory) {
108
+ const dir = resolve(args.policiesDirectory);
109
+ const packLoaded = await loadPolicyPacksFromDirectory(dir);
110
+ if (!packLoaded.ok) {
111
+ return jsonResult({ ok: false, phase: 'policy_packs', errors: packLoaded.errors });
112
+ }
113
+ policyRuleVisitors = packLoaded.visitors;
114
+ }
115
+ const irLintFindings = validateIrLint(irRaw, { policyRuleVisitors });
116
+ const combined = sortFindings([...structural, ...irLintFindings]);
117
+ return jsonResult({
118
+ ok: combined.every((f) => f.severity !== 'error'),
119
+ irStructuralFindings: structural,
120
+ irLintFindings,
121
+ combined,
122
+ });
123
+ });
124
+ server.registerTool('archrad_lint_summary', {
125
+ title: 'Lint summary',
126
+ description: 'Short text summary of IR structural + lint findings. Use `ir` or `irPath` (see archrad_validate_ir).',
127
+ inputSchema: {
128
+ ...irSourceSchema,
129
+ policiesDirectory: z.string().optional(),
130
+ },
131
+ }, async (args) => {
132
+ const loaded = await loadIrFromArgs(args);
133
+ if (!loaded.ok)
134
+ return jsonResult({ summary: loaded.error });
135
+ const irRaw = loaded.ir;
136
+ const norm = normalizeIrGraph(irRaw);
137
+ if ('findings' in norm) {
138
+ return jsonResult({ summary: `Normalize failed: ${norm.findings.map((f) => f.message).join('; ')}` });
139
+ }
140
+ const structural = validateIrStructural(irRaw);
141
+ let policyRuleVisitors;
142
+ if (args.policiesDirectory) {
143
+ const packLoaded = await loadPolicyPacksFromDirectory(resolve(args.policiesDirectory));
144
+ if (!packLoaded.ok) {
145
+ return jsonResult({ summary: `Policy packs failed: ${packLoaded.errors.join('; ')}` });
146
+ }
147
+ policyRuleVisitors = packLoaded.visitors;
148
+ }
149
+ const irLintFindings = validateIrLint(irRaw, { policyRuleVisitors });
150
+ const combined = sortFindings([...structural, ...irLintFindings]);
151
+ const errors = combined.filter((f) => f.severity === 'error');
152
+ const warnings = combined.filter((f) => f.severity === 'warning');
153
+ const lines = [
154
+ `Findings: ${combined.length} (${errors.length} errors, ${warnings.length} warnings).`,
155
+ ...combined.slice(0, 20).map((f) => `- [${f.code}] ${f.message}`),
156
+ ];
157
+ if (combined.length > 20)
158
+ lines.push(`… and ${combined.length - 20} more.`);
159
+ return jsonResult({ summary: lines.join('\n'), counts: { total: combined.length, errors: errors.length, warnings: warnings.length } });
160
+ });
161
+ server.registerTool('archrad_validate_drift', {
162
+ title: 'Validate drift',
163
+ description: 'Compare on-disk export to a fresh deterministic export. Pass `ir` or `irPath` (JSON file).',
164
+ inputSchema: {
165
+ ...irSourceSchema,
166
+ target: z.enum(['python', 'node', 'nodejs']),
167
+ exportDir: z.string(),
168
+ policiesDirectory: z.string().optional(),
169
+ skipIrLint: z.boolean().optional(),
170
+ },
171
+ }, async (args) => {
172
+ const loaded = await loadIrFromArgs(args);
173
+ if (!loaded.ok)
174
+ return jsonResult({ ok: false, phase: 'input', error: loaded.error });
175
+ const irRaw = loaded.ir;
176
+ const norm = normalizeIrGraph(irRaw);
177
+ if ('findings' in norm) {
178
+ return jsonResult({ ok: false, phase: 'normalize', findings: norm.findings });
179
+ }
180
+ const actualIR = irRaw && typeof irRaw === 'object' && irRaw !== null && 'graph' in irRaw
181
+ ? irRaw
182
+ : { graph: norm.graph };
183
+ let policyRuleVisitors;
184
+ if (args.policiesDirectory) {
185
+ const packLoaded = await loadPolicyPacksFromDirectory(resolve(args.policiesDirectory));
186
+ if (!packLoaded.ok) {
187
+ return jsonResult({ ok: false, phase: 'policy_packs', errors: packLoaded.errors });
188
+ }
189
+ policyRuleVisitors = packLoaded.visitors;
190
+ }
191
+ const outDir = resolve(args.exportDir);
192
+ const result = await runValidateDrift(actualIR, args.target, outDir, {
193
+ skipIrLint: args.skipIrLint ?? false,
194
+ policyRuleVisitors,
195
+ });
196
+ return jsonResult({
197
+ ok: result.ok,
198
+ driftFindings: result.driftFindings,
199
+ extraBlocking: result.extraBlocking,
200
+ irStructuralFindings: result.exportResult.irStructuralFindings,
201
+ irLintFindings: result.exportResult.irLintFindings,
202
+ });
203
+ });
204
+ server.registerTool('archrad_policy_packs_load', {
205
+ title: 'Load policy packs',
206
+ description: 'Compile PolicyPack YAML/JSON from a directory or from in-memory file list.',
207
+ inputSchema: {
208
+ directory: z.string().optional(),
209
+ files: z
210
+ .array(z.object({ name: z.string(), content: z.string() }))
211
+ .optional(),
212
+ },
213
+ }, async (args) => {
214
+ if (args.files && args.files.length > 0) {
215
+ const loaded = loadPolicyPacksFromFiles(args.files.map((f) => ({ name: f.name, content: f.content })));
216
+ if (!loaded.ok) {
217
+ return jsonResult({ ok: false, errors: loaded.errors });
218
+ }
219
+ return jsonResult({ ok: true, ruleCount: loaded.ruleCount });
220
+ }
221
+ if (args.directory) {
222
+ const loaded = await loadPolicyPacksFromDirectory(resolve(args.directory));
223
+ if (!loaded.ok) {
224
+ return jsonResult({ ok: false, errors: loaded.errors });
225
+ }
226
+ return jsonResult({ ok: true, ruleCount: loaded.ruleCount });
227
+ }
228
+ return jsonResult({ ok: false, error: 'Provide `directory` or `files`.' });
229
+ });
230
+ const transport = new StdioServerTransport();
231
+ await server.connect(transport);
232
+ }
233
+ main().catch((err) => {
234
+ console.error('archrad-mcp:', err);
235
+ process.exitCode = 1;
236
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"nodeExpress.d.ts","sourceRoot":"","sources":["../src/nodeExpress.ts"],"names":[],"mappings":"AAIA,wBAA8B,wBAAwB,CAAC,QAAQ,EAAE,GAAG,EAAE,IAAI,GAAE,GAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAC,MAAM,CAAC,CAAC,CAuapH"}
1
+ {"version":3,"file":"nodeExpress.d.ts","sourceRoot":"","sources":["../src/nodeExpress.ts"],"names":[],"mappings":"AAKA,wBAA8B,wBAAwB,CAAC,QAAQ,EAAE,GAAG,EAAE,IAAI,GAAE,GAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAC,MAAM,CAAC,CAAC,CA0apH"}
@@ -1,6 +1,7 @@
1
1
  // Deterministic Node Express exporter (skeleton)
2
2
  // Exports a map of filename -> content for a generated Express app.
3
3
  import { getEdgeConfig, generateRetryCode, generateCircuitBreakerCode } from './edgeConfigCodeGenerator.js';
4
+ import { MAX_UNTRUSTED_STRING_LEN, stripLeadingTrailingHyphens } from './stringEdgeStrip.js';
4
5
  export default async function generateNodeExpressFiles(actualIR, opts = {}) {
5
6
  const files = {};
6
7
  const graph = (actualIR && actualIR.graph) ? actualIR.graph : (actualIR || {});
@@ -12,7 +13,10 @@ export default async function generateNodeExpressFiles(actualIR, opts = {}) {
12
13
  const nonHttpNodes = [];
13
14
  // Track edge config utilities (retry, circuit breaker) to include once
14
15
  const edgeUtilityCode = new Set();
15
- function safeId(id) { return String(id || '').replace(/[^A-Za-z0-9_\-]/g, '-').replace(/^-+|-+$/g, '').toLowerCase() || 'node'; }
16
+ function safeId(id) {
17
+ const raw = String(id || '').slice(0, MAX_UNTRUSTED_STRING_LEN);
18
+ return stripLeadingTrailingHyphens(raw.replace(/[^A-Za-z0-9_\-]/g, '-')).toLowerCase() || 'node';
19
+ }
16
20
  function handlerName(n) { return `handler_${safeId(n && (n.id || n.name))}`.replace(/-/g, '_'); }
17
21
  /**
18
22
  * Generate code for inner nodes (support nodes) that are embedded within a key node
@@ -1 +1 @@
1
- {"version":3,"file":"openapi-to-ir.d.ts","sourceRoot":"","sources":["../src/openapi-to-ir.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,OAAO,EAAE,MAAM;CAI5B;AAuBD,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC,CAAC;AAEF;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,eAAe,EAAE,CAqD1F;AAyDD;;GAEG;AACH,wBAAgB,4BAA4B,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAgClG;AAED,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAMnF;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAQnF"}
1
+ {"version":3,"file":"openapi-to-ir.d.ts","sourceRoot":"","sources":["../src/openapi-to-ir.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,OAAO,EAAE,MAAM;CAI5B;AA0BD,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC,CAAC;AAEF;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,eAAe,EAAE,CAqD1F;AAyDD;;GAEG;AACH,wBAAgB,4BAA4B,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAgClG;AAED,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAMnF;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAQnF"}
@@ -4,6 +4,7 @@
4
4
  * This is not semantic architecture truth — only operations under `paths` become `http` nodes.
5
5
  */
6
6
  import { parseOpenApiString, validateOpenApiStructural } from './openapi-structural.js';
7
+ import { MAX_UNTRUSTED_STRING_LEN, stripLeadingTrailingHyphens, stripLeadingTrailingUnderscores } from './stringEdgeStrip.js';
7
8
  export class OpenApiIngestError extends Error {
8
9
  constructor(message) {
9
10
  super(message);
@@ -16,15 +17,16 @@ function normalizeOpenApiPath(pathKey) {
16
17
  return s.startsWith('/') ? s : `/${s}`;
17
18
  }
18
19
  function safeServiceName(title) {
19
- const t = String(title || 'openapi-service')
20
+ const t = stripLeadingTrailingHyphens(String(title || 'openapi-service')
20
21
  .trim()
21
- .replace(/[^a-zA-Z0-9_-]+/g, '-')
22
- .replace(/^-+|-+$/g, '')
22
+ .slice(0, 256)
23
+ .replace(/[^a-zA-Z0-9_-]+/g, '-'))
23
24
  .toLowerCase();
24
25
  return t.slice(0, 63) || 'openapi-service';
25
26
  }
26
27
  function safeNodeId(path, method) {
27
- const slug = `${method}_${path}`.replace(/[^a-zA-Z0-9]+/g, '_').replace(/^_|_$/g, '').toLowerCase();
28
+ const combined = `${method}_${path}`.slice(0, MAX_UNTRUSTED_STRING_LEN);
29
+ const slug = stripLeadingTrailingUnderscores(combined.replace(/[^a-zA-Z0-9]+/g, '_')).toLowerCase();
28
30
  return `openapi_${slug || 'route'}`.slice(0, 80);
29
31
  }
30
32
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"pythonFastAPI.d.ts","sourceRoot":"","sources":["../src/pythonFastAPI.ts"],"names":[],"mappings":"AA4OA,wBAA8B,0BAA0B,CAAC,QAAQ,EAAE,GAAG,EAAE,IAAI,GAAE,GAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAC,MAAM,CAAC,CAAC,CA2ctH"}
1
+ {"version":3,"file":"pythonFastAPI.d.ts","sourceRoot":"","sources":["../src/pythonFastAPI.ts"],"names":[],"mappings":"AA8OA,wBAA8B,0BAA0B,CAAC,QAAQ,EAAE,GAAG,EAAE,IAAI,GAAE,GAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAC,MAAM,CAAC,CAAC,CA2ctH"}