@archrad/deterministic 0.1.4 → 0.1.6
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 +39 -4
- package/README.md +81 -48
- package/biome.json +32 -25
- package/dist/cli.js +1 -1
- package/dist/mcp-server-tools-patch.d.ts +29 -0
- package/dist/mcp-server-tools-patch.d.ts.map +1 -0
- package/dist/mcp-server-tools-patch.js +71 -0
- package/dist/mcp-server.d.ts +7 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +256 -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/CI.md +122 -0
- package/docs/DRIFT.md +52 -0
- package/docs/MCP.md +153 -0
- package/docs/RULE_CODES.md +208 -0
- package/package.json +17 -10
- package/scripts/generate-corpus.mjs +667 -0
- package/scripts/npm-postinstall.mjs +22 -0
- package/scripts/smoke-mcp.mjs +44 -0
|
@@ -0,0 +1,256 @@
|
|
|
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
|
+
import { MCP_TOOL_ARCHRAD_LIST_RULE_CODES, MCP_TOOL_ARCHRAD_LINT_SUMMARY, MCP_TOOL_ARCHRAD_POLICY_PACKS_LOAD, MCP_TOOL_ARCHRAD_SUGGEST_FIX, MCP_TOOL_ARCHRAD_VALIDATE_DRIFT, MCP_TOOL_ARCHRAD_VALIDATE_IR, } from './mcp-server-tools-patch.js';
|
|
14
|
+
const VERSION = '0.1.6';
|
|
15
|
+
/** Hard cap for `irPath` reads (see docs/MCP.md). */
|
|
16
|
+
const MAX_IR_FILE_BYTES = 25 * 1024 * 1024;
|
|
17
|
+
function jsonResult(payload) {
|
|
18
|
+
return {
|
|
19
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
async function loadIrFromArgs(args) {
|
|
23
|
+
const hasInline = args.ir !== undefined;
|
|
24
|
+
const hasPath = args.irPath != null && String(args.irPath).trim() !== '';
|
|
25
|
+
if (hasInline && hasPath) {
|
|
26
|
+
return { ok: false, error: 'Provide only one of `ir` or `irPath`.' };
|
|
27
|
+
}
|
|
28
|
+
if (hasPath) {
|
|
29
|
+
const p = resolve(args.irPath);
|
|
30
|
+
let st;
|
|
31
|
+
try {
|
|
32
|
+
st = await stat(p);
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
return { ok: false, error: `irPath not readable: ${e instanceof Error ? e.message : String(e)}` };
|
|
36
|
+
}
|
|
37
|
+
if (!st.isFile()) {
|
|
38
|
+
return { ok: false, error: `irPath is not a file: ${p}` };
|
|
39
|
+
}
|
|
40
|
+
if (st.size > MAX_IR_FILE_BYTES) {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
error: `IR file is ${st.size} bytes (max ${MAX_IR_FILE_BYTES}). Split the graph, trim fixtures, or validate smaller subgraphs.`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const raw = await readFile(p, 'utf8');
|
|
47
|
+
try {
|
|
48
|
+
return { ok: true, ir: JSON.parse(raw) };
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
return { ok: false, error: `Invalid JSON in irPath: ${e instanceof Error ? e.message : String(e)}` };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (hasInline) {
|
|
55
|
+
return { ok: true, ir: args.ir };
|
|
56
|
+
}
|
|
57
|
+
return { ok: false, error: 'Provide `ir` (inline JSON) or `irPath` (path to IR JSON file).' };
|
|
58
|
+
}
|
|
59
|
+
async function main() {
|
|
60
|
+
const server = new McpServer({
|
|
61
|
+
name: 'archrad-deterministic',
|
|
62
|
+
version: VERSION,
|
|
63
|
+
});
|
|
64
|
+
server.registerTool('archrad_validate_ir', {
|
|
65
|
+
title: MCP_TOOL_ARCHRAD_VALIDATE_IR.title,
|
|
66
|
+
description: MCP_TOOL_ARCHRAD_VALIDATE_IR.description,
|
|
67
|
+
inputSchema: {
|
|
68
|
+
ir: z.unknown().optional().describe('Inline IR graph as a JSON object. Use for small graphs only.'),
|
|
69
|
+
irPath: z.string().optional().describe('Absolute or relative path to an IR JSON file. Preferred for large graphs.'),
|
|
70
|
+
policiesDirectory: z
|
|
71
|
+
.string()
|
|
72
|
+
.optional()
|
|
73
|
+
.describe('Path to a directory of PolicyPack YAML/JSON files. Optional — omit if you have no custom rules.'),
|
|
74
|
+
},
|
|
75
|
+
}, async (args) => {
|
|
76
|
+
const loaded = await loadIrFromArgs(args);
|
|
77
|
+
if (!loaded.ok)
|
|
78
|
+
return jsonResult({ ok: false, phase: 'input', error: loaded.error });
|
|
79
|
+
const irRaw = loaded.ir;
|
|
80
|
+
const norm = normalizeIrGraph(irRaw);
|
|
81
|
+
if ('findings' in norm) {
|
|
82
|
+
return jsonResult({ ok: false, phase: 'normalize', findings: norm.findings });
|
|
83
|
+
}
|
|
84
|
+
const structural = validateIrStructural(irRaw);
|
|
85
|
+
let policyRuleVisitors;
|
|
86
|
+
if (args.policiesDirectory) {
|
|
87
|
+
const dir = resolve(args.policiesDirectory);
|
|
88
|
+
const packLoaded = await loadPolicyPacksFromDirectory(dir);
|
|
89
|
+
if (!packLoaded.ok) {
|
|
90
|
+
return jsonResult({ ok: false, phase: 'policy_packs', errors: packLoaded.errors });
|
|
91
|
+
}
|
|
92
|
+
policyRuleVisitors = packLoaded.visitors;
|
|
93
|
+
}
|
|
94
|
+
const irLintFindings = validateIrLint(irRaw, { policyRuleVisitors });
|
|
95
|
+
const combined = sortFindings([...structural, ...irLintFindings]);
|
|
96
|
+
return jsonResult({
|
|
97
|
+
ok: combined.every((f) => f.severity !== 'error'),
|
|
98
|
+
irStructuralFindings: structural,
|
|
99
|
+
irLintFindings,
|
|
100
|
+
combined,
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
server.registerTool('archrad_lint_summary', {
|
|
104
|
+
title: MCP_TOOL_ARCHRAD_LINT_SUMMARY.title,
|
|
105
|
+
description: MCP_TOOL_ARCHRAD_LINT_SUMMARY.description,
|
|
106
|
+
inputSchema: {
|
|
107
|
+
ir: z.unknown().optional().describe('Inline IR graph as a JSON object.'),
|
|
108
|
+
irPath: z.string().optional().describe('Absolute or relative path to an IR JSON file.'),
|
|
109
|
+
policiesDirectory: z
|
|
110
|
+
.string()
|
|
111
|
+
.optional()
|
|
112
|
+
.describe('Path to a directory of PolicyPack YAML/JSON files. Optional.'),
|
|
113
|
+
},
|
|
114
|
+
}, async (args) => {
|
|
115
|
+
const loaded = await loadIrFromArgs(args);
|
|
116
|
+
if (!loaded.ok)
|
|
117
|
+
return jsonResult({ summary: loaded.error });
|
|
118
|
+
const irRaw = loaded.ir;
|
|
119
|
+
const norm = normalizeIrGraph(irRaw);
|
|
120
|
+
if ('findings' in norm) {
|
|
121
|
+
return jsonResult({ summary: `Normalize failed: ${norm.findings.map((f) => f.message).join('; ')}` });
|
|
122
|
+
}
|
|
123
|
+
const structural = validateIrStructural(irRaw);
|
|
124
|
+
let policyRuleVisitors;
|
|
125
|
+
if (args.policiesDirectory) {
|
|
126
|
+
const packLoaded = await loadPolicyPacksFromDirectory(resolve(args.policiesDirectory));
|
|
127
|
+
if (!packLoaded.ok) {
|
|
128
|
+
return jsonResult({ summary: `Policy packs failed: ${packLoaded.errors.join('; ')}` });
|
|
129
|
+
}
|
|
130
|
+
policyRuleVisitors = packLoaded.visitors;
|
|
131
|
+
}
|
|
132
|
+
const irLintFindings = validateIrLint(irRaw, { policyRuleVisitors });
|
|
133
|
+
const combined = sortFindings([...structural, ...irLintFindings]);
|
|
134
|
+
const errors = combined.filter((f) => f.severity === 'error');
|
|
135
|
+
const warnings = combined.filter((f) => f.severity === 'warning');
|
|
136
|
+
const lines = [
|
|
137
|
+
`Findings: ${combined.length} (${errors.length} errors, ${warnings.length} warnings).`,
|
|
138
|
+
...combined.slice(0, 20).map((f) => `- [${f.code}] ${f.message}`),
|
|
139
|
+
];
|
|
140
|
+
if (combined.length > 20)
|
|
141
|
+
lines.push(`… and ${combined.length - 20} more.`);
|
|
142
|
+
return jsonResult({
|
|
143
|
+
summary: lines.join('\n'),
|
|
144
|
+
counts: { total: combined.length, errors: errors.length, warnings: warnings.length },
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
server.registerTool('archrad_suggest_fix', {
|
|
148
|
+
title: MCP_TOOL_ARCHRAD_SUGGEST_FIX.title,
|
|
149
|
+
description: MCP_TOOL_ARCHRAD_SUGGEST_FIX.description,
|
|
150
|
+
inputSchema: {
|
|
151
|
+
findingCode: z.string().min(1).describe('The finding code to look up, e.g. "IR-LINT-MISSING-AUTH-010".'),
|
|
152
|
+
},
|
|
153
|
+
}, async (args) => {
|
|
154
|
+
const g = getStaticRuleGuidance(args.findingCode);
|
|
155
|
+
if (!g) {
|
|
156
|
+
return jsonResult({
|
|
157
|
+
ok: false,
|
|
158
|
+
findingCode: args.findingCode,
|
|
159
|
+
error: 'Unknown built-in code. PolicyPack and org rules use custom rule ids in YAML — see your pack. Call archrad_list_rule_codes to see all built-in codes with static guidance.',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return jsonResult({ ok: true, ...g });
|
|
163
|
+
});
|
|
164
|
+
server.registerTool('archrad_list_rule_codes', {
|
|
165
|
+
title: MCP_TOOL_ARCHRAD_LIST_RULE_CODES.title,
|
|
166
|
+
description: MCP_TOOL_ARCHRAD_LIST_RULE_CODES.description,
|
|
167
|
+
inputSchema: {},
|
|
168
|
+
}, async () => jsonResult({ codes: listStaticRuleCodes() }));
|
|
169
|
+
server.registerTool('archrad_validate_drift', {
|
|
170
|
+
title: MCP_TOOL_ARCHRAD_VALIDATE_DRIFT.title,
|
|
171
|
+
description: MCP_TOOL_ARCHRAD_VALIDATE_DRIFT.description,
|
|
172
|
+
inputSchema: {
|
|
173
|
+
ir: z.unknown().optional().describe('Inline IR graph as a JSON object.'),
|
|
174
|
+
irPath: z.string().optional().describe('Absolute or relative path to an IR JSON file.'),
|
|
175
|
+
target: z
|
|
176
|
+
.enum(['python', 'nodejs'])
|
|
177
|
+
.describe('Export target language. Use "nodejs" for Node.js/TypeScript, "python" for Python.'),
|
|
178
|
+
exportDir: z
|
|
179
|
+
.string()
|
|
180
|
+
.describe('Absolute path to the on-disk export directory to compare against the IR.'),
|
|
181
|
+
policiesDirectory: z.string().optional().describe('Path to a PolicyPack directory. Optional.'),
|
|
182
|
+
skipIrLint: z.boolean().optional().describe('Set to true to skip IR-LINT checks and only check for drift. Default: false.'),
|
|
183
|
+
},
|
|
184
|
+
}, async (args) => {
|
|
185
|
+
const loaded = await loadIrFromArgs(args);
|
|
186
|
+
if (!loaded.ok)
|
|
187
|
+
return jsonResult({ ok: false, phase: 'input', error: loaded.error });
|
|
188
|
+
const irRaw = loaded.ir;
|
|
189
|
+
const norm = normalizeIrGraph(irRaw);
|
|
190
|
+
if ('findings' in norm) {
|
|
191
|
+
return jsonResult({ ok: false, phase: 'normalize', findings: norm.findings });
|
|
192
|
+
}
|
|
193
|
+
const actualIR = irRaw && typeof irRaw === 'object' && irRaw !== null && 'graph' in irRaw
|
|
194
|
+
? irRaw
|
|
195
|
+
: { graph: norm.graph };
|
|
196
|
+
let policyRuleVisitors;
|
|
197
|
+
if (args.policiesDirectory) {
|
|
198
|
+
const packLoaded = await loadPolicyPacksFromDirectory(resolve(args.policiesDirectory));
|
|
199
|
+
if (!packLoaded.ok) {
|
|
200
|
+
return jsonResult({ ok: false, phase: 'policy_packs', errors: packLoaded.errors });
|
|
201
|
+
}
|
|
202
|
+
policyRuleVisitors = packLoaded.visitors;
|
|
203
|
+
}
|
|
204
|
+
const outDir = resolve(args.exportDir);
|
|
205
|
+
const result = await runValidateDrift(actualIR, args.target, outDir, {
|
|
206
|
+
skipIrLint: args.skipIrLint ?? false,
|
|
207
|
+
policyRuleVisitors,
|
|
208
|
+
});
|
|
209
|
+
return jsonResult({
|
|
210
|
+
ok: result.ok,
|
|
211
|
+
driftFindings: result.driftFindings,
|
|
212
|
+
extraBlocking: result.extraBlocking,
|
|
213
|
+
irStructuralFindings: result.exportResult.irStructuralFindings,
|
|
214
|
+
irLintFindings: result.exportResult.irLintFindings,
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
server.registerTool('archrad_policy_packs_load', {
|
|
218
|
+
title: MCP_TOOL_ARCHRAD_POLICY_PACKS_LOAD.title,
|
|
219
|
+
description: MCP_TOOL_ARCHRAD_POLICY_PACKS_LOAD.description,
|
|
220
|
+
inputSchema: {
|
|
221
|
+
directory: z.string().optional().describe('Path to a directory of PolicyPack YAML/JSON files.'),
|
|
222
|
+
files: z
|
|
223
|
+
.array(z.object({
|
|
224
|
+
name: z.string().describe('Filename, e.g. "auth-rules.yaml".'),
|
|
225
|
+
content: z.string().describe('Raw file content as a string.'),
|
|
226
|
+
}))
|
|
227
|
+
.optional()
|
|
228
|
+
.describe('In-memory file list. Use when you have policy content as strings rather than on-disk files.'),
|
|
229
|
+
},
|
|
230
|
+
}, async (args) => {
|
|
231
|
+
if (args.files && args.files.length > 0) {
|
|
232
|
+
const loaded = loadPolicyPacksFromFiles(args.files.map((f) => ({ name: f.name, content: f.content })));
|
|
233
|
+
if (!loaded.ok) {
|
|
234
|
+
return jsonResult({ ok: false, errors: loaded.errors });
|
|
235
|
+
}
|
|
236
|
+
return jsonResult({ ok: true, ruleCount: loaded.ruleCount });
|
|
237
|
+
}
|
|
238
|
+
if (args.directory) {
|
|
239
|
+
const loaded = await loadPolicyPacksFromDirectory(resolve(args.directory));
|
|
240
|
+
if (!loaded.ok) {
|
|
241
|
+
return jsonResult({ ok: false, errors: loaded.errors });
|
|
242
|
+
}
|
|
243
|
+
return jsonResult({ ok: true, ruleCount: loaded.ruleCount });
|
|
244
|
+
}
|
|
245
|
+
return jsonResult({
|
|
246
|
+
ok: false,
|
|
247
|
+
error: 'Provide either directory (path string) or files (array of {name, content}).',
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
const transport = new StdioServerTransport();
|
|
251
|
+
await server.connect(transport);
|
|
252
|
+
}
|
|
253
|
+
main().catch((err) => {
|
|
254
|
+
console.error('archrad-mcp:', err);
|
|
255
|
+
process.exitCode = 1;
|
|
256
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nodeExpress.d.ts","sourceRoot":"","sources":["../src/nodeExpress.ts"],"names":[],"mappings":"
|
|
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"}
|
package/dist/nodeExpress.js
CHANGED
|
@@ -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) {
|
|
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;
|
|
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"}
|
package/dist/openapi-to-ir.js
CHANGED
|
@@ -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
|
-
.
|
|
22
|
-
.replace(
|
|
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
|
|
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":"
|
|
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"}
|
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
|
+
}
|