@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 +11 -0
- package/README.md +81 -48
- package/biome.json +32 -25
- package/dist/cli.js +1 -1
- package/dist/mcp-server.d.ts +7 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +236 -0
- package/dist/nodeExpress.d.ts.map +1 -1
- package/dist/nodeExpress.js +5 -1
- package/dist/openapi-to-ir.d.ts.map +1 -1
- package/dist/openapi-to-ir.js +6 -4
- package/dist/pythonFastAPI.d.ts.map +1 -1
- package/dist/pythonFastAPI.js +3 -1
- package/dist/static-rule-guidance.d.ts +19 -0
- package/dist/static-rule-guidance.d.ts.map +1 -0
- package/dist/static-rule-guidance.js +165 -0
- package/dist/stringEdgeStrip.d.ts +8 -0
- package/dist/stringEdgeStrip.d.ts.map +1 -0
- package/dist/stringEdgeStrip.js +25 -0
- package/docs/DRIFT.md +52 -0
- package/docs/MCP.md +153 -0
- package/docs/RULE_CODES.md +208 -0
- package/package.json +15 -9
- package/scripts/npm-postinstall.mjs +22 -0
- package/scripts/smoke-mcp.mjs +44 -0
package/dist/pythonFastAPI.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// Deterministic Python FastAPI exporter
|
|
2
2
|
// Produces a map of filename -> content given an IR (plan graph) and options.
|
|
3
3
|
import { getEdgeConfig, generateRetryCode, generateCircuitBreakerCode } from './edgeConfigCodeGenerator.js';
|
|
4
|
+
import { MAX_UNTRUSTED_STRING_LEN, stripLeadingTrailingHyphens } from './stringEdgeStrip.js';
|
|
4
5
|
function safeId(id) {
|
|
5
|
-
|
|
6
|
+
const raw = String(id || '').slice(0, MAX_UNTRUSTED_STRING_LEN);
|
|
7
|
+
return stripLeadingTrailingHyphens(raw.replace(/[^A-Za-z0-9_\-]/g, '-')).toLowerCase() || 'node';
|
|
6
8
|
}
|
|
7
9
|
function handlerNameFor(n) {
|
|
8
10
|
if (n && n.config && n.config.name)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static, deterministic remediation text for built-in finding codes (IR-STRUCT-*, IR-LINT-*, DRIFT-*).
|
|
3
|
+
* Used by MCP `archrad_suggest_fix` — not generated architecture; same hints the engine documents in findings.
|
|
4
|
+
*/
|
|
5
|
+
export type StaticRuleGuidance = {
|
|
6
|
+
findingCode: string;
|
|
7
|
+
title: string;
|
|
8
|
+
remediation: string;
|
|
9
|
+
/** Canonical docs path (no analytics/query params). */
|
|
10
|
+
docsUrl: string;
|
|
11
|
+
};
|
|
12
|
+
/** Public OSS repo (subtree); `docs/` is at repo root in arch-deterministic. */
|
|
13
|
+
export declare const RULE_CODES_DOC_BASE = "https://github.com/archradhq/arch-deterministic/blob/main/docs/RULE_CODES.md";
|
|
14
|
+
/** GitHub heading anchor (must match markdown `## CODE` in docs/RULE_CODES.md). */
|
|
15
|
+
export declare function githubRuleCodeAnchor(code: string): string;
|
|
16
|
+
export declare function docsUrlForFindingCode(code: string): string;
|
|
17
|
+
export declare function listStaticRuleCodes(): string[];
|
|
18
|
+
export declare function getStaticRuleGuidance(findingCode: string): StaticRuleGuidance | null;
|
|
19
|
+
//# sourceMappingURL=static-rule-guidance.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"static-rule-guidance.d.ts","sourceRoot":"","sources":["../src/static-rule-guidance.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,kBAAkB,GAAG;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,uDAAuD;IACvD,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,gFAAgF;AAChF,eAAO,MAAM,mBAAmB,iFACgD,CAAC;AAEjF,mFAAmF;AACnF,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKzD;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1D;AA2KD,wBAAgB,mBAAmB,IAAI,MAAM,EAAE,CAE9C;AAED,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CASpF"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static, deterministic remediation text for built-in finding codes (IR-STRUCT-*, IR-LINT-*, DRIFT-*).
|
|
3
|
+
* Used by MCP `archrad_suggest_fix` — not generated architecture; same hints the engine documents in findings.
|
|
4
|
+
*/
|
|
5
|
+
/** Public OSS repo (subtree); `docs/` is at repo root in arch-deterministic. */
|
|
6
|
+
export const RULE_CODES_DOC_BASE = 'https://github.com/archradhq/arch-deterministic/blob/main/docs/RULE_CODES.md';
|
|
7
|
+
/** GitHub heading anchor (must match markdown `## CODE` in docs/RULE_CODES.md). */
|
|
8
|
+
export function githubRuleCodeAnchor(code) {
|
|
9
|
+
return code
|
|
10
|
+
.toLowerCase()
|
|
11
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
12
|
+
.replace(/^-|-$/g, '');
|
|
13
|
+
}
|
|
14
|
+
export function docsUrlForFindingCode(code) {
|
|
15
|
+
return `${RULE_CODES_DOC_BASE}#${githubRuleCodeAnchor(code)}`;
|
|
16
|
+
}
|
|
17
|
+
/** Built-in codes with curated guidance. Org PolicyPack codes (e.g. ORG-*) are not listed here. */
|
|
18
|
+
const GUIDANCE = {
|
|
19
|
+
'IR-LINT-DIRECT-DB-ACCESS-002': {
|
|
20
|
+
title: 'HTTP-like node connects directly to a datastore',
|
|
21
|
+
remediation: 'Introduce a service or domain layer between HTTP handlers and persistence: add intermediate nodes and edges so the API does not couple directly to a single DB node. This preserves testability, storage swaps, and invariant enforcement at a clear boundary.',
|
|
22
|
+
},
|
|
23
|
+
'IR-LINT-HIGH-FANOUT-004': {
|
|
24
|
+
title: 'High outgoing dependency count',
|
|
25
|
+
remediation: 'Reduce fan-out: split responsibilities, add a facade, batch calls, or use async handoff (queues) so one node does not synchronously depend on many downstreams. High fan-out increases blast radius and latency under load.',
|
|
26
|
+
},
|
|
27
|
+
'IR-LINT-SYNC-CHAIN-001': {
|
|
28
|
+
title: 'Long synchronous chain from HTTP entry',
|
|
29
|
+
remediation: 'Shorten the synchronous call graph or mark non-blocking hops as async: set `metadata.protocol` / edge metadata for async boundaries, or `config.async` where applicable, so depth reflects real execution. Deep sync chains amplify latency and failures.',
|
|
30
|
+
},
|
|
31
|
+
'IR-LINT-NO-HEALTHCHECK-003': {
|
|
32
|
+
title: 'No typical health/readiness route on HTTP nodes',
|
|
33
|
+
remediation: 'Add at least one GET route such as `/health` or `/ready` on an HTTP node (or document a dedicated health node). Orchestrators and load balancers rely on these for safe deploys and rollbacks.',
|
|
34
|
+
},
|
|
35
|
+
'IR-LINT-ISOLATED-NODE-005': {
|
|
36
|
+
title: 'Node has no incident edges',
|
|
37
|
+
remediation: 'Remove the orphan or connect it with edges so it participates in the architecture. Isolated nodes usually mean stale IR or a missing integration.',
|
|
38
|
+
},
|
|
39
|
+
'IR-LINT-DUPLICATE-EDGE-006': {
|
|
40
|
+
title: 'Duplicate from→to edge',
|
|
41
|
+
remediation: 'Collapse duplicate edges or distinguish them with metadata if your model allows. Parallel duplicates clutter views and can double-count in generators.',
|
|
42
|
+
},
|
|
43
|
+
'IR-LINT-HTTP-MISSING-NAME-007': {
|
|
44
|
+
title: 'HTTP-like node missing display name',
|
|
45
|
+
remediation: 'Set a short human-readable `name` on the node for docs, OpenAPI titles, and graph labels.',
|
|
46
|
+
},
|
|
47
|
+
'IR-LINT-DATASTORE-NO-INCOMING-008': {
|
|
48
|
+
title: 'Datastore has no incoming edges',
|
|
49
|
+
remediation: 'Connect a service or data path to this datastore, or remove it if unused. Orphan persistence nodes misrepresent how data is written.',
|
|
50
|
+
},
|
|
51
|
+
'IR-LINT-MULTIPLE-HTTP-ENTRIES-009': {
|
|
52
|
+
title: 'Multiple HTTP entry nodes without incoming edges',
|
|
53
|
+
remediation: 'Prefer a single API gateway or BFF unless multiple public surfaces are intentional and documented. Multiple entries duplicate auth, rate limits, and observability concerns.',
|
|
54
|
+
},
|
|
55
|
+
'IR-LINT-MISSING-AUTH-010': {
|
|
56
|
+
title: 'HTTP entry missing auth coverage',
|
|
57
|
+
remediation: 'Add an auth boundary: connect an auth, oauth, jwt, or middleware node with an edge to or from this entry, or set `config.authRequired: false` for intentionally public endpoints (health, assets). Regulated environments expect a documented auth path for every public HTTP entry.',
|
|
58
|
+
},
|
|
59
|
+
'IR-LINT-DEAD-NODE-011': {
|
|
60
|
+
title: 'Non-sink node with incoming edges but no outgoing edges',
|
|
61
|
+
remediation: 'Add an outgoing edge to a downstream consumer, or remove the node if it is obsolete. Dead-end non-sinks often indicate incomplete migrations or IR mistakes.',
|
|
62
|
+
},
|
|
63
|
+
'IR-STRUCT-INVALID_ROOT': {
|
|
64
|
+
title: 'IR root is not a JSON object',
|
|
65
|
+
remediation: 'Pass a single JSON object: either `{ "graph": { "nodes": [], "edges": [] } }` or a graph object with a top-level `nodes` array.',
|
|
66
|
+
},
|
|
67
|
+
'IR-STRUCT-NO_GRAPH': {
|
|
68
|
+
title: 'Missing graph shape',
|
|
69
|
+
remediation: 'Include `.graph` with `nodes` (and optional `edges`) or a top-level `nodes` array so the document describes a graph.',
|
|
70
|
+
},
|
|
71
|
+
'IR-STRUCT-NODES_NOT_ARRAY': {
|
|
72
|
+
title: '`nodes` is not an array',
|
|
73
|
+
remediation: 'Set `nodes` to an array of node objects, each with a string `id` and a type/kind.',
|
|
74
|
+
},
|
|
75
|
+
'IR-STRUCT-EDGES_NOT_ARRAY': {
|
|
76
|
+
title: '`edges` is present but not an array',
|
|
77
|
+
remediation: 'Set `edges` to an array of edge objects (or omit `edges` if there are no edges). Malformed `edges` is treated as empty with a warning.',
|
|
78
|
+
},
|
|
79
|
+
'IR-STRUCT-EMPTY_GRAPH': {
|
|
80
|
+
title: 'Graph has no nodes',
|
|
81
|
+
remediation: 'Add at least one node before validation or export. An empty graph cannot generate a service.',
|
|
82
|
+
},
|
|
83
|
+
'IR-STRUCT-NODE_INVALID': {
|
|
84
|
+
title: 'Node entry is not an object',
|
|
85
|
+
remediation: 'Each element of `nodes` must be a JSON object with at least `id` and type information.',
|
|
86
|
+
},
|
|
87
|
+
'IR-STRUCT-NODE_NO_ID': {
|
|
88
|
+
title: 'Node missing non-empty id',
|
|
89
|
+
remediation: 'Assign a stable string `id` to every node. Ids are used for edges and code generation.',
|
|
90
|
+
},
|
|
91
|
+
'IR-STRUCT-DUP_NODE_ID': {
|
|
92
|
+
title: 'Duplicate node id',
|
|
93
|
+
remediation: 'Ensure node ids are unique. Edges cannot reference duplicate ids unambiguously.',
|
|
94
|
+
},
|
|
95
|
+
'IR-STRUCT-NODE_INVALID_CONFIG': {
|
|
96
|
+
title: 'Node `config` is not a plain object',
|
|
97
|
+
remediation: 'Use a plain object for `config` (e.g. `{ "url": "/api", "method": "GET" }`). Arrays and null are not valid.',
|
|
98
|
+
},
|
|
99
|
+
'IR-STRUCT-HTTP_PATH': {
|
|
100
|
+
title: 'HTTP endpoint path invalid',
|
|
101
|
+
remediation: 'Set `config.url` or `config.route` to a non-empty path starting with `/`, e.g. `/users`.',
|
|
102
|
+
},
|
|
103
|
+
'IR-STRUCT-HTTP_METHOD': {
|
|
104
|
+
title: 'HTTP method not supported',
|
|
105
|
+
remediation: 'Use GET, POST, PUT, PATCH, DELETE, HEAD, or OPTIONS in `config.method` (default may be applied as POST).',
|
|
106
|
+
},
|
|
107
|
+
'IR-STRUCT-EDGE_INVALID': {
|
|
108
|
+
title: 'Edge is not an object',
|
|
109
|
+
remediation: 'Each edge must be an object with `from`/`to` (or `source`/`target`) referencing node ids.',
|
|
110
|
+
},
|
|
111
|
+
'IR-STRUCT-EDGE_NO_ENDPOINTS': {
|
|
112
|
+
title: 'Edge missing endpoints',
|
|
113
|
+
remediation: 'Set both ends of the edge to existing node ids using `from` and `to` (or legacy `source`/`target`).',
|
|
114
|
+
},
|
|
115
|
+
'IR-STRUCT-EDGE_AMBIGUOUS_FROM': {
|
|
116
|
+
title: 'Edge references duplicate source id',
|
|
117
|
+
remediation: 'Resolve duplicate node ids first; edges cannot point to an ambiguous source.',
|
|
118
|
+
},
|
|
119
|
+
'IR-STRUCT-EDGE_UNKNOWN_FROM': {
|
|
120
|
+
title: 'Edge references unknown source node',
|
|
121
|
+
remediation: 'Add a node with the referenced id or correct the `from` endpoint.',
|
|
122
|
+
},
|
|
123
|
+
'IR-STRUCT-EDGE_AMBIGUOUS_TO': {
|
|
124
|
+
title: 'Edge references duplicate target id',
|
|
125
|
+
remediation: 'Resolve duplicate node ids first; edges cannot point to an ambiguous target.',
|
|
126
|
+
},
|
|
127
|
+
'IR-STRUCT-EDGE_UNKNOWN_TO': {
|
|
128
|
+
title: 'Edge references unknown target node',
|
|
129
|
+
remediation: 'Add a node with the referenced id or correct the `to` endpoint.',
|
|
130
|
+
},
|
|
131
|
+
'IR-STRUCT-CYCLE': {
|
|
132
|
+
title: 'Directed cycle in the graph',
|
|
133
|
+
remediation: 'Remove or break cyclic edges unless your deployment explicitly allows synchronous loops. Cycles block layering and complicate codegen assumptions.',
|
|
134
|
+
},
|
|
135
|
+
'DRIFT-MISSING': {
|
|
136
|
+
title: 'Exported file missing on disk',
|
|
137
|
+
remediation: 'Regenerate the export (`archrad export`) or restore the missing file so the tree matches the deterministic output for this IR.',
|
|
138
|
+
},
|
|
139
|
+
'DRIFT-MODIFIED': {
|
|
140
|
+
title: 'File differs from deterministic export',
|
|
141
|
+
remediation: 'Revert manual edits to generated files or update the IR and re-export so the on-disk tree matches the compiler output.',
|
|
142
|
+
},
|
|
143
|
+
'DRIFT-EXTRA': {
|
|
144
|
+
title: 'Extra file not in deterministic export',
|
|
145
|
+
remediation: 'Remove stray files from the export directory or add them to the model if they should be generated. Use `--strict-extra` semantics as documented for your CI gate.',
|
|
146
|
+
},
|
|
147
|
+
'DRIFT-NO-EXPORT': {
|
|
148
|
+
title: 'No export produced for drift comparison',
|
|
149
|
+
remediation: 'Fix IR structural/lint errors blocking export, or verify `--target` and IR content so the exporter emits files.',
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
export function listStaticRuleCodes() {
|
|
153
|
+
return Object.keys(GUIDANCE).sort();
|
|
154
|
+
}
|
|
155
|
+
export function getStaticRuleGuidance(findingCode) {
|
|
156
|
+
const g = GUIDANCE[findingCode];
|
|
157
|
+
if (!g)
|
|
158
|
+
return null;
|
|
159
|
+
return {
|
|
160
|
+
findingCode,
|
|
161
|
+
title: g.title,
|
|
162
|
+
remediation: g.remediation,
|
|
163
|
+
docsUrl: docsUrlForFindingCode(findingCode),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linear-time trimming of repeated edge characters (avoids polynomial ReDoS on
|
|
3
|
+
* patterns like `/^-+|-+$/` when applied to uncontrolled strings).
|
|
4
|
+
*/
|
|
5
|
+
export declare const MAX_UNTRUSTED_STRING_LEN = 8192;
|
|
6
|
+
export declare function stripLeadingTrailingHyphens(s: string, maxLen?: number): string;
|
|
7
|
+
export declare function stripLeadingTrailingUnderscores(s: string, maxLen?: number): string;
|
|
8
|
+
//# sourceMappingURL=stringEdgeStrip.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stringEdgeStrip.d.ts","sourceRoot":"","sources":["../src/stringEdgeStrip.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,wBAAwB,OAAO,CAAC;AAE7C,wBAAgB,2BAA2B,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,SAA2B,GAAG,MAAM,CAOhG;AAED,wBAAgB,+BAA+B,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,SAA2B,GAAG,MAAM,CAOpG"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linear-time trimming of repeated edge characters (avoids polynomial ReDoS on
|
|
3
|
+
* patterns like `/^-+|-+$/` when applied to uncontrolled strings).
|
|
4
|
+
*/
|
|
5
|
+
export const MAX_UNTRUSTED_STRING_LEN = 8192;
|
|
6
|
+
export function stripLeadingTrailingHyphens(s, maxLen = MAX_UNTRUSTED_STRING_LEN) {
|
|
7
|
+
const t = s.length <= maxLen ? s : s.slice(0, maxLen);
|
|
8
|
+
let i = 0;
|
|
9
|
+
let j = t.length;
|
|
10
|
+
while (i < j && t[i] === '-')
|
|
11
|
+
i++;
|
|
12
|
+
while (j > i && t[j - 1] === '-')
|
|
13
|
+
j--;
|
|
14
|
+
return t.slice(i, j);
|
|
15
|
+
}
|
|
16
|
+
export function stripLeadingTrailingUnderscores(s, maxLen = MAX_UNTRUSTED_STRING_LEN) {
|
|
17
|
+
const t = s.length <= maxLen ? s : s.slice(0, maxLen);
|
|
18
|
+
let i = 0;
|
|
19
|
+
let j = t.length;
|
|
20
|
+
while (i < j && t[i] === '_')
|
|
21
|
+
i++;
|
|
22
|
+
while (j > i && t[j - 1] === '_')
|
|
23
|
+
j--;
|
|
24
|
+
return t.slice(i, j);
|
|
25
|
+
}
|
package/docs/DRIFT.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Deterministic drift (`validate-drift`)
|
|
2
|
+
|
|
3
|
+
**Canonical OSS description** — versioned with **`@archrad/deterministic`**.
|
|
4
|
+
**Not** semantic “does this code match business intent?” — that class of analysis is out of scope for this layer (see **`STRUCTURAL_VS_SEMANTIC_VALIDATION.md`**).
|
|
5
|
+
|
|
6
|
+
## What it is
|
|
7
|
+
|
|
8
|
+
**Drift** here means: you already have an **on-disk tree** (usually from `archrad export`), and you want to know whether it still matches what the **deterministic exporter would produce today** from the **same IR**.
|
|
9
|
+
|
|
10
|
+
The engine:
|
|
11
|
+
|
|
12
|
+
1. Runs a **fresh export** from your IR in memory (same pipeline as `archrad export`).
|
|
13
|
+
2. Compares the **expected file set + contents** to what is on disk (paths normalized, line endings normalized to `\n`).
|
|
14
|
+
3. Emits **DRIFT-*** findings when something is missing, changed, or (optionally) extra.
|
|
15
|
+
|
|
16
|
+
So: **regen vs reality** — a thin gate for CI and pre-merge checks.
|
|
17
|
+
|
|
18
|
+
## What it is not
|
|
19
|
+
|
|
20
|
+
- **Not** diffing IR to arbitrary hand-written code semantics.
|
|
21
|
+
- **Not** proving runtime behavior or security — use tests, review, and (where applicable) **ArchRad Cloud** governance.
|
|
22
|
+
|
|
23
|
+
## CLI
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
archrad validate-drift -i ./graph.json -t python -o ./out
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Use **`--json`** in CI. **`--strict-extra`** treats unexpected files in the output directory as findings. **`--skip-ir-lint`** / **`--policies`** follow the same semantics as **`export`** (see **`README.md`**).
|
|
30
|
+
|
|
31
|
+
## Library
|
|
32
|
+
|
|
33
|
+
**`runValidateDrift`**, **`diffExpectedExportAgainstFiles`**, **`readDirectoryAsExportMap`** — see **`src/validate-drift.ts`**.
|
|
34
|
+
|
|
35
|
+
## MCP
|
|
36
|
+
|
|
37
|
+
**`archrad_validate_drift`** — same semantics: IR (inline or **`irPath`**) + **`target`** + **`exportDir`**. See **`MCP.md`**.
|
|
38
|
+
|
|
39
|
+
## DRIFT-* codes
|
|
40
|
+
|
|
41
|
+
| Code | Meaning |
|
|
42
|
+
|------|--------|
|
|
43
|
+
| **DRIFT-MISSING** | A file the exporter would emit is missing on disk. |
|
|
44
|
+
| **DRIFT-MODIFIED** | File exists but content differs from the deterministic export. |
|
|
45
|
+
| **DRIFT-EXTRA** | File exists on disk but is not in the reference export (with **`--strict-extra`**). |
|
|
46
|
+
| **DRIFT-NO-EXPORT** | Exporter produced no file map (often blocked by structural/lint errors or empty target output). |
|
|
47
|
+
|
|
48
|
+
Remediation text for each code (aligned with MCP **`archrad_suggest_fix`**) lives in **`RULE_CODES.md`** and **`src/static-rule-guidance.ts`**.
|
|
49
|
+
|
|
50
|
+
## Product site
|
|
51
|
+
|
|
52
|
+
**archrad.com** may host narrative pages (e.g. `/docs/drift`) for onboarding; **definitions and CLI/MCP behavior** are maintained **here** in the OSS repo so they ship with the engine.
|
package/docs/MCP.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# ArchRad MCP server — specification
|
|
2
|
+
|
|
3
|
+
**Status:** **0.1.x** — `archrad-mcp` ships in **`@archrad/deterministic`** (stdio MCP, same engine as CLI).
|
|
4
|
+
**Audience:** Engineering + launch narrative (Show HN / registry listing).
|
|
5
|
+
|
|
6
|
+
## 1. Problem statement
|
|
7
|
+
|
|
8
|
+
| Mode | Behavior |
|
|
9
|
+
|------|------------|
|
|
10
|
+
| **Reactive** | CI runs `archrad` after code exists → catches drift late. |
|
|
11
|
+
| **Proactive** | An MCP server answers **before** edits land: “Is this edge allowed?”, “What does IR-LINT say about this sketch?” |
|
|
12
|
+
|
|
13
|
+
Agents optimize for *working code*; ArchRad supplies **deterministic constraints** so the loop can ask the engine *before* violating architecture.
|
|
14
|
+
|
|
15
|
+
## 2. OSS vs product boundary
|
|
16
|
+
|
|
17
|
+
Aligned with **`STRUCTURAL_VS_SEMANTIC_VALIDATION.md`** (this folder) and your org’s OSS/product split docs.
|
|
18
|
+
|
|
19
|
+
| Surface | Ships in **OSS** (`@archrad/deterministic`, binary **`archrad-mcp`**) | Stays in **product / Cloud** |
|
|
20
|
+
|---------|------------------------------------------------------------------------|------------------------------|
|
|
21
|
+
| IR structural validation (`IR-STRUCT-*`) | Yes | — |
|
|
22
|
+
| Architecture lint (`IR-LINT-*`) + PolicyPack YAML from **local dir** | Yes | — |
|
|
23
|
+
| `validate_drift` vs on-disk export (local paths) | Yes | — |
|
|
24
|
+
| Static remediation text per **built-in** code (`archrad_suggest_fix`) | Yes | — |
|
|
25
|
+
| Org **`settings.archPolicyPacks`**, Firestore, membership | — | Yes |
|
|
26
|
+
| Semantic “is this business-correct?” reasoning | — | Yes (future assisted tools) |
|
|
27
|
+
|
|
28
|
+
**Rule of thumb:** If the tool only needs **IR JSON + local files**, it can live in the public MCP server. If it needs **tenant identity, billing, or org policy from Cloud**, expose a **separate** “ArchRad Cloud” MCP connector (private or authenticated) — do not move Cloud policy enforcement into OSS without an explicit product decision.
|
|
29
|
+
|
|
30
|
+
## 3. Transport and packaging
|
|
31
|
+
|
|
32
|
+
- **Protocol:** [Model Context Protocol](https://modelcontextprotocol.io/) over **stdio** (default for Cursor, Claude Desktop, Copilot agent adapters).
|
|
33
|
+
- **Package:** **`@archrad/deterministic`** publishes two binaries: **`archrad`** (CLI) and **`archrad-mcp`** (MCP). One implementation backs CLI + MCP.
|
|
34
|
+
- **Registry:** Optional `server.json` / manifest for MCP Registry; document install for Cursor “Add MCP server”.
|
|
35
|
+
|
|
36
|
+
## 4. IR payload size and `ir` vs `irPath`
|
|
37
|
+
|
|
38
|
+
| Concern | Guidance |
|
|
39
|
+
|---------|----------|
|
|
40
|
+
| **Inline `ir`** | Fine for small/medium graphs. Large JSON in a single tool call still stresses **host message limits** and **model context** — prefer **`irPath`** when the IR is big. |
|
|
41
|
+
| **`irPath`** | Absolute or relative path to a **single JSON file** (same shape as CLI `--ir`). Enforced **max file size 25 MiB** in the server (hard cap); if you exceed it, split validation or trim fixtures. |
|
|
42
|
+
| **Soft ceiling** | Below ~**5k–10k nodes**, inline JSON is usually workable if the host allows; above that, **`irPath` is recommended**. This is not a graph semantics limit — only practical transport/memory. |
|
|
43
|
+
| **Exactly one** | Provide **`ir`** **or** **`irPath`**, not both, not neither (for tools that need IR). |
|
|
44
|
+
|
|
45
|
+
**Privacy:** The OSS server does **not** add analytics or tracking parameters to tool responses. Optional product docs URLs use a stable path only (see **`archrad_suggest_fix`**).
|
|
46
|
+
|
|
47
|
+
## 5. Local testing
|
|
48
|
+
|
|
49
|
+
### 5.1 Smoke script (npm)
|
|
50
|
+
|
|
51
|
+
From the **`@archrad/deterministic`** package root (where **`package.json`** lives):
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm run build
|
|
55
|
+
npm run smoke:mcp
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Exit code **0** means the MCP server spawned **`dist/mcp-server.js`**, listed tools, and successfully called **`archrad_suggest_fix`**.
|
|
59
|
+
|
|
60
|
+
### 5.2 MCP Inspector (browser UI)
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npx @modelcontextprotocol/inspector node dist/mcp-server.js
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Open the URL Inspector prints (often **http://localhost:6274**). Under **Tools**, run **`archrad_list_rule_codes`** (no args), then **`archrad_validate_ir`** with **`irPath`** set to **`fixtures/minimal-graph.json`** (relative paths work if the process was started with **cwd** set to the package root).
|
|
67
|
+
|
|
68
|
+
### 5.3 Cursor (MCP config + chat)
|
|
69
|
+
|
|
70
|
+
1. **Build** so **`dist/mcp-server.js`** exists (`npm run build`).
|
|
71
|
+
2. In Cursor **Settings → MCP**, add a server (exact JSON shape depends on Cursor version):
|
|
72
|
+
- **Command:** `node` (or full path to `node.exe` on Windows).
|
|
73
|
+
- **Args:** full path to **`dist/mcp-server.js`**.
|
|
74
|
+
- **Cwd (recommended):** the deterministic package root (directory containing **`package.json`**). Relative **`irPath`** values like **`fixtures/minimal-graph.json`** resolve from this directory.
|
|
75
|
+
3. **Chat:** Cursor does not always show a “run tool” button for every server. Use **Agent** mode (or another mode that supports **tool use**) and ask explicitly, for example:
|
|
76
|
+
|
|
77
|
+
**List codes:**
|
|
78
|
+
|
|
79
|
+
> Use the MCP tool **`archrad_list_rule_codes`** (no arguments) and show me the raw JSON result.
|
|
80
|
+
|
|
81
|
+
**Validate via file:**
|
|
82
|
+
|
|
83
|
+
> Use the MCP tool **`archrad_validate_ir`** with **`irPath`** set to **`fixtures/minimal-graph.json`** (relative to the deterministic package). Show the full tool result.
|
|
84
|
+
|
|
85
|
+
If the model says it cannot find the tool, the MCP server failed to start or the configured server name does not match — check Cursor’s MCP panel for errors.
|
|
86
|
+
|
|
87
|
+
4. **If `irPath` fails** with “file not found”, use the **absolute path** to **`fixtures/minimal-graph.json`**.
|
|
88
|
+
|
|
89
|
+
### 5.4 Success criteria
|
|
90
|
+
|
|
91
|
+
- **`archrad_list_rule_codes`:** JSON with a **`codes`** array.
|
|
92
|
+
- **`archrad_validate_ir`:** JSON with **`irStructuralFindings`**, **`irLintFindings`**, **`combined`**, **`ok`** — not a connection or file error.
|
|
93
|
+
|
|
94
|
+
## 6. Tools (0.1.5)
|
|
95
|
+
|
|
96
|
+
Tools are **idempotent** and **deterministic** where stated.
|
|
97
|
+
|
|
98
|
+
### 6.1 Core
|
|
99
|
+
|
|
100
|
+
| Tool | Input | Output | Notes |
|
|
101
|
+
|------|--------|--------|-------|
|
|
102
|
+
| **`archrad_validate_ir`** | `ir` **or** `irPath`; optional `policiesDirectory` | `{ ok, irStructuralFindings, irLintFindings, combined }` | Same as CLI validate. |
|
|
103
|
+
| **`archrad_lint_summary`** | `ir` **or** `irPath`; optional `policiesDirectory` | Short summary + counts | Agent-friendly. |
|
|
104
|
+
| **`archrad_validate_drift`** | `ir` **or** `irPath`; `target`; `exportDir`; optional policies, `skipIrLint` | Drift + export findings | Same as CLI `validate-drift`. |
|
|
105
|
+
| **`archrad_policy_packs_load`** | `directory` or `files[]` | `{ ok, ruleCount }` or errors | Compiles packs; does not return visitor functions over MCP. |
|
|
106
|
+
|
|
107
|
+
### 6.2 Static guidance (no generated architecture)
|
|
108
|
+
|
|
109
|
+
| Tool | Input | Output | Notes |
|
|
110
|
+
|------|--------|--------|-------|
|
|
111
|
+
| **`archrad_suggest_fix`** | `findingCode` (e.g. `IR-LINT-MISSING-AUTH-010`) | `{ ok, findingCode, title, remediation, docsUrl }` or `{ ok: false, error }` | **Curated text only** — not JSON Patch, not IR edits, not LLM output. **`docsUrl`** points to the **[`RULE_CODES.md`](https://github.com/archradhq/arch-deterministic/blob/main/docs/RULE_CODES.md)** section for that code on **GitHub** (canonical OSS; no query strings). Unknown built-in codes and **PolicyPack/org** ids return `ok: false` with a short explanation. |
|
|
112
|
+
| **`archrad_list_rule_codes`** | _(none)_ | `{ codes: string[] }` | Sorted list of built-in codes with static guidance. |
|
|
113
|
+
|
|
114
|
+
**Explicit non-goal:** **`archrad_suggest_fix` must not** return machine-generated graph edits or “patches” that invent services — that would be **generative** and would break the deterministic OSS contract.
|
|
115
|
+
|
|
116
|
+
### 6.3 Explicit non-goals (MVP)
|
|
117
|
+
|
|
118
|
+
- No automatic **code** generation inside MCP (keep **`export`** as CLI/CI).
|
|
119
|
+
- No remote calls to ArchRad Cloud unless a **separate authenticated** server is defined.
|
|
120
|
+
- No **tracking** query parameters in MCP tool payloads.
|
|
121
|
+
|
|
122
|
+
## 7. Resources (optional)
|
|
123
|
+
|
|
124
|
+
| Resource URI | Content |
|
|
125
|
+
|--------------|---------|
|
|
126
|
+
| `archrad://docs/ir-contract` | Pointer to bundled `IR_CONTRACT.md` snippet or link. |
|
|
127
|
+
| `archrad://schemas/ir-graph-v1` | JSON Schema for IR graph validation. |
|
|
128
|
+
|
|
129
|
+
## 8. Security
|
|
130
|
+
|
|
131
|
+
- **Local-only by default:** IR and paths stay on the user machine; **no telemetry** in the OSS server unless explicitly documented elsewhere.
|
|
132
|
+
- **Path sandbox:** Validate `exportDir` / `irPath` against workspace roots if the host passes a `workspaceRoot` (host-dependent).
|
|
133
|
+
- **Cloud MCP (future):** OAuth or API keys; never embed product secrets in OSS.
|
|
134
|
+
|
|
135
|
+
## 9. Launch narrative (copy bank)
|
|
136
|
+
|
|
137
|
+
- **One-liner:** *AI agents write code fast but drift from your architecture; ArchRad MCP gives them the same deterministic IR checks as CI, inside the agent loop.*
|
|
138
|
+
- **Show HN angle:** *Architectural conscience for Copilot / Cursor — IR-LINT before you merge.*
|
|
139
|
+
|
|
140
|
+
## 10. Related repo tasks
|
|
141
|
+
|
|
142
|
+
- **Quality:** Keep graph/lint work responsive so MCP tools stay usable on mid-size IRs.
|
|
143
|
+
|
|
144
|
+
## 11. Implementation checklist
|
|
145
|
+
|
|
146
|
+
1. ~~Node MCP server (`@modelcontextprotocol/sdk`), stdio transport.~~
|
|
147
|
+
2. ~~`archrad_validate_ir` + `archrad_validate_drift` + policy load + static `archrad_suggest_fix`.~~
|
|
148
|
+
3. README + **`docs/MCP.md`**: Cursor config, `ir` / `irPath`, local testing (smoke, Inspector, chat prompts).
|
|
149
|
+
4. Publish **`@archrad/deterministic`** to npm; subtree to public repo per release process.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
*This document aligns OSS MCP scope with product strategy; update when adding Cloud-only tools.*
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Built-in finding codes (IR-STRUCT, IR-LINT, DRIFT)
|
|
2
|
+
|
|
3
|
+
**Canonical OSS reference** — ships in **`@archrad/deterministic`**.
|
|
4
|
+
**MCP** **`archrad_suggest_fix`** returns **`title`**, **`remediation`**, and a **`docsUrl`** pointing at the matching section below. **PolicyPack / org** rules use custom ids (e.g. `ORG-*`) — not listed here.
|
|
5
|
+
|
|
6
|
+
**Product / marketing** copy may live on **archrad.com**; **deterministic semantics** are defined in this repo.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## DRIFT-EXTRA
|
|
11
|
+
|
|
12
|
+
Extra file not in deterministic export. **Remediation:** Remove stray files from the export directory or add them to the model if they should be generated. Use **`--strict-extra`** semantics as documented for your CI gate.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## DRIFT-MISSING
|
|
17
|
+
|
|
18
|
+
Exported file missing on disk. **Remediation:** Regenerate the export (**`archrad export`**) or restore the missing file so the tree matches the deterministic output for this IR.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## DRIFT-MODIFIED
|
|
23
|
+
|
|
24
|
+
File differs from deterministic export. **Remediation:** Revert manual edits to generated files or update the IR and re-export so the on-disk tree matches the compiler output.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## DRIFT-NO-EXPORT
|
|
29
|
+
|
|
30
|
+
No export produced for drift comparison. **Remediation:** Fix IR structural/lint errors blocking export, or verify **`--target`** and IR content so the exporter emits files.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## IR-LINT-DATASTORE-NO-INCOMING-008
|
|
35
|
+
|
|
36
|
+
Datastore has no incoming edges. **Remediation:** Connect a service or data path to this datastore, or remove it if unused.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## IR-LINT-DEAD-NODE-011
|
|
41
|
+
|
|
42
|
+
Non-sink node with incoming edges but no outgoing edges. **Remediation:** Add an outgoing edge to a downstream consumer, or remove the node if it is obsolete.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## IR-LINT-DIRECT-DB-ACCESS-002
|
|
47
|
+
|
|
48
|
+
HTTP-like node connects directly to a datastore. **Remediation:** Introduce a service or domain layer between HTTP handlers and persistence.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## IR-LINT-DUPLICATE-EDGE-006
|
|
53
|
+
|
|
54
|
+
Duplicate from→to edge. **Remediation:** Collapse duplicate edges or distinguish them with metadata if your model allows.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## IR-LINT-HIGH-FANOUT-004
|
|
59
|
+
|
|
60
|
+
High outgoing dependency count. **Remediation:** Reduce fan-out: split responsibilities, add a facade, batch calls, or use async handoff.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## IR-LINT-HTTP-MISSING-NAME-007
|
|
65
|
+
|
|
66
|
+
HTTP-like node missing display name. **Remediation:** Set a short human-readable **`name`** on the node.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## IR-LINT-ISOLATED-NODE-005
|
|
71
|
+
|
|
72
|
+
Node has no incident edges. **Remediation:** Remove the orphan or connect it with edges.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## IR-LINT-MISSING-AUTH-010
|
|
77
|
+
|
|
78
|
+
HTTP entry missing auth coverage. **Remediation:** Add an auth boundary (auth/middleware/oauth/jwt node or **`config.authRequired: false`** for public endpoints).
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## IR-LINT-MULTIPLE-HTTP-ENTRIES-009
|
|
83
|
+
|
|
84
|
+
Multiple HTTP entry nodes without incoming edges. **Remediation:** Prefer a single API gateway or BFF unless multiple public surfaces are intentional.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## IR-LINT-NO-HEALTHCHECK-003
|
|
89
|
+
|
|
90
|
+
No typical health/readiness route on HTTP nodes. **Remediation:** Add a GET route such as **`/health`** or **`/ready`**.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## IR-LINT-SYNC-CHAIN-001
|
|
95
|
+
|
|
96
|
+
Long synchronous chain from HTTP entry. **Remediation:** Shorten the graph or mark non-blocking hops as async in edge metadata.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## IR-STRUCT-CYCLE
|
|
101
|
+
|
|
102
|
+
Directed cycle in the graph. **Remediation:** Remove or break cyclic edges unless your tooling explicitly allows execution loops.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## IR-STRUCT-DUP_NODE_ID
|
|
107
|
+
|
|
108
|
+
Duplicate node id. **Remediation:** Ensure node ids are unique.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## IR-STRUCT-EDGE_AMBIGUOUS_FROM
|
|
113
|
+
|
|
114
|
+
Edge references duplicate source id. **Remediation:** Resolve duplicate node ids first.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## IR-STRUCT-EDGE_AMBIGUOUS_TO
|
|
119
|
+
|
|
120
|
+
Edge references duplicate target id. **Remediation:** Resolve duplicate node ids first.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## IR-STRUCT-EDGE_INVALID
|
|
125
|
+
|
|
126
|
+
Edge is not an object. **Remediation:** Each edge must be an object with **`from`**/**`to`** (or **`source`**/**`target`**).
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## IR-STRUCT-EDGE_NO_ENDPOINTS
|
|
131
|
+
|
|
132
|
+
Edge missing endpoints. **Remediation:** Set **`from`** and **`to`** to existing node ids.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## IR-STRUCT-EDGE_UNKNOWN_FROM
|
|
137
|
+
|
|
138
|
+
Edge references unknown source node. **Remediation:** Add a node with the referenced id or correct **`from`**.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## IR-STRUCT-EDGE_UNKNOWN_TO
|
|
143
|
+
|
|
144
|
+
Edge references unknown target node. **Remediation:** Add a node with the referenced id or correct **`to`**.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## IR-STRUCT-EDGES_NOT_ARRAY
|
|
149
|
+
|
|
150
|
+
**`edges`** is present but not an array. **Remediation:** Set **`edges`** to an array of edge objects (or omit **`edges`**).
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## IR-STRUCT-EMPTY_GRAPH
|
|
155
|
+
|
|
156
|
+
Graph has no nodes. **Remediation:** Add at least one node before validation or export.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## IR-STRUCT-HTTP_METHOD
|
|
161
|
+
|
|
162
|
+
HTTP method not supported. **Remediation:** Use GET, POST, PUT, PATCH, DELETE, HEAD, or OPTIONS.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## IR-STRUCT-HTTP_PATH
|
|
167
|
+
|
|
168
|
+
HTTP endpoint path invalid. **Remediation:** Set **`config.url`** or **`config.route`** to a path starting with **`/`**.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## IR-STRUCT-INVALID_ROOT
|
|
173
|
+
|
|
174
|
+
IR root is not a JSON object. **Remediation:** Pass a single JSON object with **`graph`** or top-level **`nodes`**.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## IR-STRUCT-NO_GRAPH
|
|
179
|
+
|
|
180
|
+
Missing graph shape. **Remediation:** Include **`.graph`** with **`nodes`** or a top-level **`nodes`** array.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## IR-STRUCT-NODE_INVALID
|
|
185
|
+
|
|
186
|
+
Node entry is not an object. **Remediation:** Each **`nodes`** entry must be a JSON object with **`id`** and type information.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## IR-STRUCT-NODE_INVALID_CONFIG
|
|
191
|
+
|
|
192
|
+
Node **`config`** is not a plain object. **Remediation:** Use a plain object for **`config`**.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## IR-STRUCT-NODE_NO_ID
|
|
197
|
+
|
|
198
|
+
Node missing non-empty id. **Remediation:** Assign a stable string **`id`** to every node.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## IR-STRUCT-NODES_NOT_ARRAY
|
|
203
|
+
|
|
204
|
+
**`nodes`** is not an array. **Remediation:** Set **`nodes`** to an array of node objects.
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
*Full strings in MCP/CLI mirror **`src/static-rule-guidance.ts`** (single implementation).*
|