@archrad/deterministic 0.1.3 → 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.
Files changed (49) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +83 -48
  3. package/biome.json +32 -25
  4. package/dist/cli.js +50 -2
  5. package/dist/exportPipeline.d.ts +3 -2
  6. package/dist/exportPipeline.d.ts.map +1 -1
  7. package/dist/exportPipeline.js +1 -1
  8. package/dist/index.d.ts +2 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +1 -0
  11. package/dist/ir-lint.d.ts +7 -2
  12. package/dist/ir-lint.d.ts.map +1 -1
  13. package/dist/ir-lint.js +5 -3
  14. package/dist/ir-normalize.d.ts +2 -0
  15. package/dist/ir-normalize.d.ts.map +1 -1
  16. package/dist/ir-normalize.js +2 -1
  17. package/dist/lint-graph.d.ts.map +1 -1
  18. package/dist/lint-graph.js +1 -0
  19. package/dist/mcp-server.d.ts +7 -0
  20. package/dist/mcp-server.d.ts.map +1 -0
  21. package/dist/mcp-server.js +236 -0
  22. package/dist/nodeExpress.d.ts.map +1 -1
  23. package/dist/nodeExpress.js +5 -1
  24. package/dist/openapi-to-ir.d.ts.map +1 -1
  25. package/dist/openapi-to-ir.js +6 -4
  26. package/dist/policy-pack.d.ts +62 -0
  27. package/dist/policy-pack.d.ts.map +1 -0
  28. package/dist/policy-pack.js +220 -0
  29. package/dist/pythonFastAPI.d.ts.map +1 -1
  30. package/dist/pythonFastAPI.js +3 -1
  31. package/dist/static-rule-guidance.d.ts +19 -0
  32. package/dist/static-rule-guidance.d.ts.map +1 -0
  33. package/dist/static-rule-guidance.js +165 -0
  34. package/dist/stringEdgeStrip.d.ts +8 -0
  35. package/dist/stringEdgeStrip.d.ts.map +1 -0
  36. package/dist/stringEdgeStrip.js +25 -0
  37. package/dist/validate-drift.d.ts +3 -0
  38. package/dist/validate-drift.d.ts.map +1 -1
  39. package/dist/validate-drift.js +2 -0
  40. package/docs/DRIFT.md +52 -0
  41. package/docs/MCP.md +153 -0
  42. package/docs/RULE_CODES.md +208 -0
  43. package/fixtures/policies/ecommerce-demo.yaml +15 -0
  44. package/fixtures/policy-packs/duplicate-pack/first.yaml +10 -0
  45. package/fixtures/policy-packs/duplicate-pack/second.yaml +10 -0
  46. package/fixtures/policy-packs/sample-only/sample-node-tags.yaml +14 -0
  47. package/package.json +15 -9
  48. package/scripts/npm-postinstall.mjs +22 -0
  49. package/scripts/smoke-mcp.mjs +44 -0
@@ -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
  /**
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Declarative org policy packs (YAML/JSON) — deterministic graph matchers on ParsedLintGraph.
3
+ * Codes should use a stable prefix (e.g. ORG-*, ACME-*) to avoid colliding with IR-LINT-* / IR-STRUCT-*.
4
+ */
5
+ import type { ParsedLintGraph } from './lint-graph.js';
6
+ import type { IrStructuralFinding } from './ir-structural.js';
7
+ export type PolicySeverity = 'error' | 'warning' | 'info';
8
+ /** Single-node selector: all provided predicates must match (AND). */
9
+ export type PolicyNodeSelectorV1 = {
10
+ id?: string;
11
+ type?: string | string[];
12
+ /** All listed tags must appear on `node.metadata.tags` (array of strings). */
13
+ tags?: string[];
14
+ };
15
+ export type PolicyEdgeMatchV1 = {
16
+ from: PolicyNodeSelectorV1;
17
+ to: PolicyNodeSelectorV1;
18
+ };
19
+ export type PolicyRuleV1 = {
20
+ id: string;
21
+ severity: PolicySeverity;
22
+ message: string;
23
+ fixHint?: string;
24
+ match: {
25
+ node?: PolicyNodeSelectorV1;
26
+ edge?: PolicyEdgeMatchV1;
27
+ };
28
+ };
29
+ export type PolicyPackMetadataV1 = {
30
+ name?: string;
31
+ org?: string;
32
+ };
33
+ export type PolicyPackDocumentV1 = {
34
+ apiVersion: 'archrad/v1';
35
+ kind: 'PolicyPack';
36
+ metadata?: PolicyPackMetadataV1;
37
+ rules: PolicyRuleV1[];
38
+ };
39
+ export type LoadPolicyPacksResult = {
40
+ ok: true;
41
+ visitors: ReadonlyArray<(g: ParsedLintGraph) => IrStructuralFinding[]>;
42
+ ruleCount: number;
43
+ } | {
44
+ ok: false;
45
+ errors: string[];
46
+ };
47
+ export type PolicyPackFileSource = {
48
+ /** Virtual filename (must end with .yaml, .yml, or .json for parse rules). */
49
+ name: string;
50
+ content: string;
51
+ };
52
+ /**
53
+ * Load policy packs from in-memory file sources (same semantics as {@link loadPolicyPacksFromDirectory}).
54
+ * Use for ArchRad Cloud, tests, and API bodies — no filesystem required.
55
+ */
56
+ export declare function loadPolicyPacksFromFiles(sources: ReadonlyArray<PolicyPackFileSource>): LoadPolicyPacksResult;
57
+ /**
58
+ * Load and compile all policy YAML/JSON files in a directory into lint visitors.
59
+ * Filenames: `*.yaml`, `*.yml`, `*.json` (other files ignored).
60
+ */
61
+ export declare function loadPolicyPacksFromDirectory(dir: string): Promise<LoadPolicyPacksResult>;
62
+ //# sourceMappingURL=policy-pack.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy-pack.d.ts","sourceRoot":"","sources":["../src/policy-pack.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAE9D,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAE1D,sEAAsE;AACtE,MAAM,MAAM,oBAAoB,GAAG;IACjC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACzB,8EAA8E;IAC9E,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,oBAAoB,CAAC;IAC3B,EAAE,EAAE,oBAAoB,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,cAAc,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE;QACL,IAAI,CAAC,EAAE,oBAAoB,CAAC;QAC5B,IAAI,CAAC,EAAE,iBAAiB,CAAC;KAC1B,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,UAAU,EAAE,YAAY,CAAC;IACzB,IAAI,EAAE,YAAY,CAAC;IACnB,QAAQ,CAAC,EAAE,oBAAoB,CAAC;IAChC,KAAK,EAAE,YAAY,EAAE,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAC7B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,aAAa,CAAC,CAAC,CAAC,EAAE,eAAe,KAAK,mBAAmB,EAAE,CAAC,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACvG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAgHpC,MAAM,MAAM,oBAAoB,GAAG;IACjC,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,aAAa,CAAC,oBAAoB,CAAC,GAAG,qBAAqB,CAkC5G;AA4BD;;;GAGG;AACH,wBAAsB,4BAA4B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAmC9F"}
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Declarative org policy packs (YAML/JSON) — deterministic graph matchers on ParsedLintGraph.
3
+ * Codes should use a stable prefix (e.g. ORG-*, ACME-*) to avoid colliding with IR-LINT-* / IR-STRUCT-*.
4
+ */
5
+ import { readFile, readdir } from 'node:fs/promises';
6
+ import { join, resolve } from 'node:path';
7
+ import yaml from 'js-yaml';
8
+ import { edgeEndpoints, nodeType } from './lint-graph.js';
9
+ function isNonEmptyRecord(x) {
10
+ return x != null && typeof x === 'object' && !Array.isArray(x);
11
+ }
12
+ function normalizeTypes(t) {
13
+ if (t == null)
14
+ return null;
15
+ const arr = Array.isArray(t) ? t : [t];
16
+ return arr.map((s) => String(s).trim().toLowerCase()).filter(Boolean);
17
+ }
18
+ /** True if selector has at least one predicate (empty match-all forbidden for v1). */
19
+ function selectorHasPredicate(sel) {
20
+ if (sel.id != null && String(sel.id).trim() !== '')
21
+ return true;
22
+ if (sel.type != null) {
23
+ const n = normalizeTypes(sel.type);
24
+ if (n && n.length > 0)
25
+ return true;
26
+ }
27
+ if (sel.tags != null && Array.isArray(sel.tags) && sel.tags.length > 0)
28
+ return true;
29
+ return false;
30
+ }
31
+ function nodeMatchesSelector(n, sel) {
32
+ if (sel.id != null && String(n.id ?? '') !== String(sel.id))
33
+ return false;
34
+ const types = normalizeTypes(sel.type);
35
+ if (types && types.length > 0) {
36
+ const nt = nodeType(n);
37
+ if (!types.includes(nt))
38
+ return false;
39
+ }
40
+ if (sel.tags != null && sel.tags.length > 0) {
41
+ const meta = n.metadata ?? {};
42
+ const raw = meta.tags;
43
+ if (!Array.isArray(raw))
44
+ return false;
45
+ const have = new Set(raw.map((x) => String(x).toLowerCase()));
46
+ for (const t of sel.tags) {
47
+ if (!have.has(String(t).toLowerCase()))
48
+ return false;
49
+ }
50
+ }
51
+ return true;
52
+ }
53
+ function compileRule(rule, source) {
54
+ if (!rule.id || typeof rule.id !== 'string' || !/^[A-Za-z0-9_.-]+$/.test(rule.id)) {
55
+ throw new Error(`[${source}] invalid rule.id`);
56
+ }
57
+ if (!['error', 'warning', 'info'].includes(rule.severity)) {
58
+ throw new Error(`[${source}] rule "${rule.id}": severity must be error | warning | info`);
59
+ }
60
+ if (!rule.message || typeof rule.message !== 'string') {
61
+ throw new Error(`[${source}] rule "${rule.id}": message is required`);
62
+ }
63
+ const hasNode = rule.match?.node != null;
64
+ const hasEdge = rule.match?.edge != null;
65
+ if (hasNode === hasEdge) {
66
+ throw new Error(`[${source}] rule "${rule.id}": specify exactly one of match.node or match.edge`);
67
+ }
68
+ if (hasNode) {
69
+ const sel = rule.match.node;
70
+ if (!selectorHasPredicate(sel)) {
71
+ throw new Error(`[${source}] rule "${rule.id}": match.node must include id, type, and/or tags`);
72
+ }
73
+ return (g) => {
74
+ const findings = [];
75
+ for (const [id, n] of g.nodeById) {
76
+ if (nodeMatchesSelector(n, sel)) {
77
+ findings.push({
78
+ code: rule.id,
79
+ severity: rule.severity,
80
+ message: rule.message,
81
+ nodeId: id,
82
+ layer: 'lint',
83
+ fixHint: rule.fixHint,
84
+ });
85
+ }
86
+ }
87
+ return findings;
88
+ };
89
+ }
90
+ const edge = rule.match.edge;
91
+ if (!selectorHasPredicate(edge.from) || !selectorHasPredicate(edge.to)) {
92
+ throw new Error(`[${source}] rule "${rule.id}": match.edge.from and match.edge.to must each include id, type, and/or tags`);
93
+ }
94
+ return (g) => {
95
+ const findings = [];
96
+ for (let edgeIndex = 0; edgeIndex < g.edges.length; edgeIndex++) {
97
+ const e = g.edges[edgeIndex];
98
+ if (!e || typeof e !== 'object')
99
+ continue;
100
+ const { from, to } = edgeEndpoints(e);
101
+ if (!from || !to)
102
+ continue;
103
+ const a = g.nodeById.get(from);
104
+ const b = g.nodeById.get(to);
105
+ if (!a || !b)
106
+ continue;
107
+ if (nodeMatchesSelector(a, edge.from) && nodeMatchesSelector(b, edge.to)) {
108
+ findings.push({
109
+ code: rule.id,
110
+ severity: rule.severity,
111
+ message: rule.message,
112
+ nodeId: to,
113
+ edgeIndex,
114
+ layer: 'lint',
115
+ fixHint: rule.fixHint,
116
+ });
117
+ }
118
+ }
119
+ return findings;
120
+ };
121
+ }
122
+ /**
123
+ * Load policy packs from in-memory file sources (same semantics as {@link loadPolicyPacksFromDirectory}).
124
+ * Use for ArchRad Cloud, tests, and API bodies — no filesystem required.
125
+ */
126
+ export function loadPolicyPacksFromFiles(sources) {
127
+ const errors = [];
128
+ const visitors = [];
129
+ const seenIds = new Set();
130
+ let ruleCount = 0;
131
+ const sorted = [...sources].sort((a, b) => a.name.localeCompare(b.name));
132
+ if (sorted.length === 0) {
133
+ return { ok: false, errors: ['no policy sources provided'] };
134
+ }
135
+ for (const src of sorted) {
136
+ const name = src.name?.trim() || 'unnamed';
137
+ try {
138
+ const doc = parseDocument(src.content, name);
139
+ for (const rule of doc.rules) {
140
+ if (seenIds.has(rule.id)) {
141
+ errors.push(`duplicate rule id "${rule.id}" (file ${name})`);
142
+ continue;
143
+ }
144
+ seenIds.add(rule.id);
145
+ visitors.push(compileRule(rule, name));
146
+ ruleCount += 1;
147
+ }
148
+ }
149
+ catch (e) {
150
+ errors.push(`${name}: ${e instanceof Error ? e.message : String(e)}`);
151
+ }
152
+ }
153
+ if (errors.length > 0) {
154
+ return { ok: false, errors };
155
+ }
156
+ return { ok: true, visitors, ruleCount };
157
+ }
158
+ function parseDocument(text, filename) {
159
+ const ext = filename.toLowerCase();
160
+ let data;
161
+ if (ext.endsWith('.json')) {
162
+ data = JSON.parse(text);
163
+ }
164
+ else if (ext.endsWith('.yaml') || ext.endsWith('.yml')) {
165
+ data = yaml.load(text);
166
+ }
167
+ else {
168
+ throw new Error(`unsupported policy file extension: ${filename}`);
169
+ }
170
+ if (!isNonEmptyRecord(data)) {
171
+ throw new Error('policy document must be a JSON object');
172
+ }
173
+ const doc = data;
174
+ if (doc.apiVersion !== 'archrad/v1') {
175
+ throw new Error(`apiVersion must be "archrad/v1" (got ${String(doc.apiVersion)})`);
176
+ }
177
+ if (doc.kind !== 'PolicyPack') {
178
+ throw new Error(`kind must be PolicyPack (got ${String(doc.kind)})`);
179
+ }
180
+ if (!Array.isArray(doc.rules) || doc.rules.length === 0) {
181
+ throw new Error('rules must be a non-empty array');
182
+ }
183
+ return doc;
184
+ }
185
+ /**
186
+ * Load and compile all policy YAML/JSON files in a directory into lint visitors.
187
+ * Filenames: `*.yaml`, `*.yml`, `*.json` (other files ignored).
188
+ */
189
+ export async function loadPolicyPacksFromDirectory(dir) {
190
+ const root = resolve(dir);
191
+ const errors = [];
192
+ let names;
193
+ try {
194
+ names = await readdir(root);
195
+ }
196
+ catch (e) {
197
+ const err = e;
198
+ return { ok: false, errors: [`cannot read policies directory ${root}: ${err.message}`] };
199
+ }
200
+ const policyFiles = names.filter((n) => /\.(yaml|yml|json)$/i.test(n)).sort();
201
+ if (policyFiles.length === 0) {
202
+ return { ok: false, errors: [`no policy files (*.yaml, *.yml, *.json) in ${root}`] };
203
+ }
204
+ const sources = [];
205
+ for (const name of policyFiles) {
206
+ const full = join(root, name);
207
+ try {
208
+ const text = await readFile(full, 'utf8');
209
+ sources.push({ name, content: text });
210
+ }
211
+ catch (e) {
212
+ const err = e;
213
+ errors.push(`${full}: ${err.message}`);
214
+ }
215
+ }
216
+ if (errors.length > 0) {
217
+ return { ok: false, errors };
218
+ }
219
+ return loadPolicyPacksFromFiles(sources);
220
+ }
@@ -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"}
@@ -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
- return String(id || '').replace(/[^A-Za-z0-9_\-]/g, '-').replace(/^-+|-+$/g, '').toLowerCase() || 'node';
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"}