@archrad/deterministic 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/CONTRIBUTING.md +15 -0
  3. package/LICENSE +17 -0
  4. package/README.md +284 -0
  5. package/SECURITY.md +26 -0
  6. package/biome.json +25 -0
  7. package/demo-validate.gif +0 -0
  8. package/dist/cli-findings.d.ts +23 -0
  9. package/dist/cli-findings.d.ts.map +1 -0
  10. package/dist/cli-findings.js +88 -0
  11. package/dist/cli.d.ts +7 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +341 -0
  14. package/dist/edgeConfigCodeGenerator.d.ts +55 -0
  15. package/dist/edgeConfigCodeGenerator.d.ts.map +1 -0
  16. package/dist/edgeConfigCodeGenerator.js +249 -0
  17. package/dist/exportPipeline.d.ts +23 -0
  18. package/dist/exportPipeline.d.ts.map +1 -0
  19. package/dist/exportPipeline.js +65 -0
  20. package/dist/golden-bundle.d.ts +21 -0
  21. package/dist/golden-bundle.d.ts.map +1 -0
  22. package/dist/golden-bundle.js +166 -0
  23. package/dist/graphPredicates.d.ts +10 -0
  24. package/dist/graphPredicates.d.ts.map +1 -0
  25. package/dist/graphPredicates.js +33 -0
  26. package/dist/hostPort.d.ts +12 -0
  27. package/dist/hostPort.d.ts.map +1 -0
  28. package/dist/hostPort.js +39 -0
  29. package/dist/index.d.ts +22 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +21 -0
  32. package/dist/ir-lint.d.ts +11 -0
  33. package/dist/ir-lint.d.ts.map +1 -0
  34. package/dist/ir-lint.js +16 -0
  35. package/dist/ir-normalize.d.ts +48 -0
  36. package/dist/ir-normalize.d.ts.map +1 -0
  37. package/dist/ir-normalize.js +81 -0
  38. package/dist/ir-structural.d.ts +40 -0
  39. package/dist/ir-structural.d.ts.map +1 -0
  40. package/dist/ir-structural.js +267 -0
  41. package/dist/lint-graph.d.ts +40 -0
  42. package/dist/lint-graph.d.ts.map +1 -0
  43. package/dist/lint-graph.js +133 -0
  44. package/dist/lint-rules.d.ts +40 -0
  45. package/dist/lint-rules.d.ts.map +1 -0
  46. package/dist/lint-rules.js +290 -0
  47. package/dist/nodeExpress.d.ts +2 -0
  48. package/dist/nodeExpress.d.ts.map +1 -0
  49. package/dist/nodeExpress.js +528 -0
  50. package/dist/openapi-structural.d.ts +26 -0
  51. package/dist/openapi-structural.d.ts.map +1 -0
  52. package/dist/openapi-structural.js +82 -0
  53. package/dist/openapi-to-ir.d.ts +26 -0
  54. package/dist/openapi-to-ir.d.ts.map +1 -0
  55. package/dist/openapi-to-ir.js +131 -0
  56. package/dist/pythonFastAPI.d.ts +2 -0
  57. package/dist/pythonFastAPI.d.ts.map +1 -0
  58. package/dist/pythonFastAPI.js +664 -0
  59. package/dist/validate-drift.d.ts +54 -0
  60. package/dist/validate-drift.d.ts.map +1 -0
  61. package/dist/validate-drift.js +184 -0
  62. package/dist/yamlToIr.d.ts +14 -0
  63. package/dist/yamlToIr.d.ts.map +1 -0
  64. package/dist/yamlToIr.js +39 -0
  65. package/docs/CONCEPT_ADOPTION_AND_LIMITS.md +47 -0
  66. package/docs/CUSTOM_RULES.md +87 -0
  67. package/docs/ENGINEERING_NOTES.md +42 -0
  68. package/docs/IR_CONTRACT.md +54 -0
  69. package/docs/STRUCTURAL_VS_SEMANTIC_VALIDATION.md +86 -0
  70. package/fixtures/demo-direct-db-layered.json +37 -0
  71. package/fixtures/demo-direct-db-violation.json +22 -0
  72. package/fixtures/ecommerce-with-warnings.json +89 -0
  73. package/fixtures/invalid-cycle.json +15 -0
  74. package/fixtures/invalid-edge-unknown-node.json +14 -0
  75. package/fixtures/minimal-graph.json +14 -0
  76. package/fixtures/minimal-graph.yaml +13 -0
  77. package/fixtures/payment-retry-demo.json +43 -0
  78. package/llms.txt +99 -0
  79. package/package.json +84 -0
  80. package/schemas/archrad-ir-graph-v1.schema.json +67 -0
  81. package/scripts/DEMO_GIF_STORYBOARD.md +100 -0
  82. package/scripts/GIF_RECORDING_STEP_BY_STEP.md +125 -0
  83. package/scripts/README_DEMO_RECORDING.md +314 -0
  84. package/scripts/SOCIAL_POST_DRIFT_AND_INGESTION.md +17 -0
  85. package/scripts/golden-path-demo.ps1 +25 -0
  86. package/scripts/golden-path-demo.sh +23 -0
  87. package/scripts/invoke-drift-check.ps1 +16 -0
  88. package/scripts/record-demo-drift.tape +50 -0
  89. package/scripts/record-demo-payment-retry.tape +36 -0
  90. package/scripts/record-demo-validate.tape +34 -0
  91. package/scripts/record-demo.tape +33 -0
  92. package/scripts/run-demo-drift-sequence.ps1 +45 -0
  93. package/scripts/run-demo-drift-sequence.sh +41 -0
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Deterministic structural validation of blueprint IR (graph JSON).
3
+ * OSS boundary: shape, references, cycles — not security/compliance semantics (ArchRad Cloud).
4
+ */
5
+ import { isHttpLikeType } from './graphPredicates.js';
6
+ import { materializeNormalizedGraph } from './ir-normalize.js';
7
+ const HTTP_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']);
8
+ /**
9
+ * Normalize product / CLI shapes to a single graph object.
10
+ * Accepts `{ graph: { nodes, edges } }` or a bare `{ nodes, edges }`.
11
+ */
12
+ export function normalizeIrGraph(ir) {
13
+ if (ir == null || typeof ir !== 'object') {
14
+ return {
15
+ findings: [
16
+ {
17
+ code: 'IR-STRUCT-INVALID_ROOT',
18
+ severity: 'error',
19
+ message: 'IR must be a JSON object',
20
+ fixHint: 'Use an object with a `graph` key or a graph object with a `nodes` array.',
21
+ },
22
+ ],
23
+ };
24
+ }
25
+ const o = ir;
26
+ if (o.graph != null && typeof o.graph === 'object') {
27
+ return { graph: o.graph };
28
+ }
29
+ if (Array.isArray(o.nodes)) {
30
+ return { graph: o };
31
+ }
32
+ return {
33
+ findings: [
34
+ {
35
+ code: 'IR-STRUCT-NO_GRAPH',
36
+ severity: 'error',
37
+ message: 'IR has no graph: expected `.graph` or top-level `.nodes` array',
38
+ fixHint: 'Use { "graph": { "nodes": [], "edges": [] } } or { "nodes": [], "edges": [] }.',
39
+ },
40
+ ],
41
+ };
42
+ }
43
+ /**
44
+ * Structural validation only: well-formed graph, edge references, directed cycles, HTTP node config.
45
+ * Same input → same findings (deterministic).
46
+ */
47
+ export function validateIrStructural(ir) {
48
+ const norm = normalizeIrGraph(ir);
49
+ if ('findings' in norm)
50
+ return norm.findings;
51
+ const graph = norm.graph;
52
+ const findings = [];
53
+ if (!Array.isArray(graph.nodes)) {
54
+ findings.push({
55
+ code: 'IR-STRUCT-NODES_NOT_ARRAY',
56
+ severity: 'error',
57
+ message: '`nodes` must be an array',
58
+ fixHint: 'Set `nodes` to at least one node object.',
59
+ });
60
+ return findings;
61
+ }
62
+ const nodes = graph.nodes;
63
+ if (nodes.length === 0) {
64
+ findings.push({
65
+ code: 'IR-STRUCT-EMPTY_GRAPH',
66
+ severity: 'error',
67
+ message: 'Graph has no nodes',
68
+ fixHint: 'Add at least one node to export API code.',
69
+ });
70
+ return findings;
71
+ }
72
+ const { normalized, edgesInputWasMalformed } = materializeNormalizedGraph(graph);
73
+ if (edgesInputWasMalformed) {
74
+ findings.push({
75
+ code: 'IR-STRUCT-EDGES_NOT_ARRAY',
76
+ severity: 'warning',
77
+ message: '`edges` is present but not an array; treating as []',
78
+ fixHint: 'Set `edges` to an array of edge objects.',
79
+ });
80
+ }
81
+ const edges = Array.isArray(graph.edges) ? graph.edges : [];
82
+ /** Ids that appear on at least one valid node object with a non-empty id */
83
+ const seenIds = new Set();
84
+ /** Ids that appear on more than one node — edges must not treat these as unambiguous */
85
+ const duplicateIds = new Set();
86
+ for (let i = 0; i < nodes.length; i++) {
87
+ const n = nodes[i];
88
+ const nn = normalized.nodes[i];
89
+ if (n == null || typeof n !== 'object') {
90
+ findings.push({
91
+ code: 'IR-STRUCT-NODE_INVALID',
92
+ severity: 'error',
93
+ message: `Node at index ${i} is not an object`,
94
+ fixHint: 'Each node must be a JSON object with an `id`.',
95
+ });
96
+ continue;
97
+ }
98
+ const id = nn.id;
99
+ if (!id) {
100
+ findings.push({
101
+ code: 'IR-STRUCT-NODE_NO_ID',
102
+ severity: 'error',
103
+ message: `Node at index ${i} is missing a non-empty \`id\``,
104
+ fixHint: 'Assign a stable string id to every node.',
105
+ });
106
+ continue;
107
+ }
108
+ if (seenIds.has(id)) {
109
+ duplicateIds.add(id);
110
+ findings.push({
111
+ code: 'IR-STRUCT-DUP_NODE_ID',
112
+ severity: 'error',
113
+ message: `Duplicate node id "${id}"`,
114
+ nodeId: id,
115
+ fixHint: 'Ids must be unique across nodes.',
116
+ });
117
+ }
118
+ else {
119
+ seenIds.add(id);
120
+ }
121
+ if (isHttpLikeType(nn.type)) {
122
+ const cfg = nn.config;
123
+ /** Generators accept `route` or `url`; structural checks align so OpenAPI merge + ingest both validate. */
124
+ const url = String(cfg.url ?? cfg.route ?? '').trim();
125
+ // Align default with generators (pythonFastAPI / nodeExpress use post when omitted)
126
+ const method = String(cfg.method ?? 'post').trim();
127
+ if (!url.startsWith('/')) {
128
+ findings.push({
129
+ code: 'IR-STRUCT-HTTP_PATH',
130
+ severity: 'error',
131
+ message: `HTTP-like node "${id}" has invalid path: config.url must be a non-empty string starting with /`,
132
+ nodeId: id,
133
+ fixHint: 'Set config.url (or config.route) to e.g. "/signup".',
134
+ });
135
+ }
136
+ const m = method.toUpperCase();
137
+ if (!HTTP_METHODS.has(m)) {
138
+ findings.push({
139
+ code: 'IR-STRUCT-HTTP_METHOD',
140
+ severity: 'error',
141
+ message: `HTTP-like node "${id}" has unsupported method "${method}"`,
142
+ nodeId: id,
143
+ fixHint: 'Use GET, POST, PUT, PATCH, DELETE, HEAD, or OPTIONS.',
144
+ });
145
+ }
146
+ }
147
+ }
148
+ const isUniqueNodeRef = (id) => seenIds.has(id) && !duplicateIds.has(id);
149
+ for (let i = 0; i < edges.length; i++) {
150
+ const e = edges[i];
151
+ const ne = normalized.edges[i];
152
+ if (e == null || typeof e !== 'object') {
153
+ findings.push({
154
+ code: 'IR-STRUCT-EDGE_INVALID',
155
+ severity: 'error',
156
+ message: `Edge at index ${i} is not an object`,
157
+ edgeIndex: i,
158
+ fixHint: 'Each edge must be an object with from/to (or source/target).',
159
+ });
160
+ continue;
161
+ }
162
+ const { from, to } = ne;
163
+ if (!from || !to) {
164
+ findings.push({
165
+ code: 'IR-STRUCT-EDGE_NO_ENDPOINTS',
166
+ severity: 'error',
167
+ message: `Edge at index ${i} is missing from/source or to/target`,
168
+ edgeIndex: i,
169
+ fixHint: 'Set from→to or source→target to existing node ids.',
170
+ });
171
+ continue;
172
+ }
173
+ if (duplicateIds.has(from)) {
174
+ findings.push({
175
+ code: 'IR-STRUCT-EDGE_AMBIGUOUS_FROM',
176
+ severity: 'error',
177
+ message: `Edge at index ${i} references duplicate node id "${from}" (ambiguous source)`,
178
+ edgeIndex: i,
179
+ nodeId: from,
180
+ fixHint: 'Resolve duplicate node ids before edges can reference them unambiguously.',
181
+ });
182
+ }
183
+ else if (!seenIds.has(from)) {
184
+ findings.push({
185
+ code: 'IR-STRUCT-EDGE_UNKNOWN_FROM',
186
+ severity: 'error',
187
+ message: `Edge at index ${i} references unknown source node "${from}"`,
188
+ edgeIndex: i,
189
+ nodeId: from,
190
+ fixHint: 'Create a node with this id or fix the edge endpoint.',
191
+ });
192
+ }
193
+ if (duplicateIds.has(to)) {
194
+ findings.push({
195
+ code: 'IR-STRUCT-EDGE_AMBIGUOUS_TO',
196
+ severity: 'error',
197
+ message: `Edge at index ${i} references duplicate node id "${to}" (ambiguous target)`,
198
+ edgeIndex: i,
199
+ nodeId: to,
200
+ fixHint: 'Resolve duplicate node ids before edges can reference them unambiguously.',
201
+ });
202
+ }
203
+ else if (!seenIds.has(to)) {
204
+ findings.push({
205
+ code: 'IR-STRUCT-EDGE_UNKNOWN_TO',
206
+ severity: 'error',
207
+ message: `Edge at index ${i} references unknown target node "${to}"`,
208
+ edgeIndex: i,
209
+ nodeId: to,
210
+ fixHint: 'Create a node with this id or fix the edge endpoint.',
211
+ });
212
+ }
213
+ }
214
+ const adj = new Map();
215
+ for (const ne of normalized.edges) {
216
+ const { from, to } = ne;
217
+ if (!from || !to || !isUniqueNodeRef(from) || !isUniqueNodeRef(to))
218
+ continue;
219
+ if (!adj.has(from))
220
+ adj.set(from, []);
221
+ adj.get(from).push(to);
222
+ }
223
+ const visiting = new Set();
224
+ const done = new Set();
225
+ let cycleExampleNode;
226
+ function dfs(u) {
227
+ if (visiting.has(u)) {
228
+ cycleExampleNode = u;
229
+ return true;
230
+ }
231
+ if (done.has(u))
232
+ return false;
233
+ visiting.add(u);
234
+ for (const v of adj.get(u) || []) {
235
+ if (dfs(v))
236
+ return true;
237
+ }
238
+ visiting.delete(u);
239
+ done.add(u);
240
+ return false;
241
+ }
242
+ let hasCycle = false;
243
+ for (const id of seenIds) {
244
+ if (duplicateIds.has(id))
245
+ continue;
246
+ if (!done.has(id) && dfs(id)) {
247
+ hasCycle = true;
248
+ break;
249
+ }
250
+ }
251
+ if (hasCycle) {
252
+ const hint = cycleExampleNode != null
253
+ ? `Directed cycle detected (node "${cycleExampleNode}" was reached again while traversing dependencies).`
254
+ : 'Dependency graph contains a directed cycle';
255
+ findings.push({
256
+ code: 'IR-STRUCT-CYCLE',
257
+ severity: 'error',
258
+ message: hint,
259
+ nodeId: cycleExampleNode,
260
+ fixHint: 'Remove or break cyclic edges unless your tooling explicitly allows execution loops.',
261
+ });
262
+ }
263
+ return findings;
264
+ }
265
+ export function hasIrStructuralErrors(findings) {
266
+ return findings.some((f) => f.severity === 'error');
267
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Normalized graph context for architecture lint rules (visitor pattern).
3
+ */
4
+ import type { IrStructuralFinding } from './ir-structural.js';
5
+ /** Re-export shared predicates (structural + lint use the same HTTP/datastore semantics). */
6
+ export { isHttpLikeType, isDbLikeType, isQueueLikeNodeType } from './graphPredicates.js';
7
+ export type ParsedLintGraph = {
8
+ nodeById: Map<string, Record<string, unknown>>;
9
+ edges: unknown[];
10
+ adj: Map<string, string[]>;
11
+ outDegree: Map<string, number>;
12
+ /** In-degree per node id (built once; use instead of recomputing per rule). */
13
+ inDegree: Map<string, number>;
14
+ };
15
+ export type BuildParsedLintGraphResult = ParsedLintGraph | {
16
+ findings: IrStructuralFinding[];
17
+ };
18
+ export declare function edgeEndpoints(e: Record<string, unknown>): {
19
+ from: string;
20
+ to: string;
21
+ };
22
+ export declare function nodeType(n: Record<string, unknown>): string;
23
+ export declare function looksLikeHealthUrl(url: string): boolean;
24
+ /**
25
+ * When true, the edge is treated as **async** for IR-LINT-SYNC-CHAIN-001 (excluded from sync depth).
26
+ * Convention: `edge.config` / `edge.metadata` may set `async: true`, `protocol: async|message|queue|event`, or channel-like `kind`.
27
+ * Top-level `edge.kind` is merged into `metadata` during normalization; raw edges still pass `rec.kind` here.
28
+ * If `graph` is provided, edges **to** queue/topic-like nodes are treated as async even without edge metadata.
29
+ */
30
+ export declare function edgeRepresentsAsyncBoundary(e: Record<string, unknown>, graph?: ParsedLintGraph): boolean;
31
+ /** Adjacency for sync-only dependency depth (omits edges marked async). */
32
+ export declare function buildSyncAdjacencyForLint(g: ParsedLintGraph): Map<string, string[]>;
33
+ /**
34
+ * Build a parsed graph for lint visitors.
35
+ * On failure (invalid IR root, empty graph, etc.) returns `{ findings }` with IR-STRUCT-* errors instead of `null`.
36
+ */
37
+ export declare function buildParsedLintGraph(ir: unknown): BuildParsedLintGraphResult;
38
+ /** Type guard: successful parse vs structural blockers. */
39
+ export declare function isParsedLintGraph(r: BuildParsedLintGraphResult): r is ParsedLintGraph;
40
+ //# sourceMappingURL=lint-graph.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lint-graph.d.ts","sourceRoot":"","sources":["../src/lint-graph.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAK9D,6FAA6F;AAC7F,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEzF,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC/C,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAC3B,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,+EAA+E;IAC/E,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG,eAAe,GAAG;IAAE,QAAQ,EAAE,mBAAmB,EAAE,CAAA;CAAE,CAAC;AAE/F,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAItF;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAE3D;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAWvD;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,KAAK,CAAC,EAAE,eAAe,GAAG,OAAO,CAqBxG;AAED,2EAA2E;AAC3E,wBAAgB,yBAAyB,CAAC,CAAC,EAAE,eAAe,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAYnF;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,OAAO,GAAG,0BAA0B,CAqD5E;AAED,2DAA2D;AAC3D,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,0BAA0B,GAAG,CAAC,IAAI,eAAe,CAErF"}
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Normalized graph context for architecture lint rules (visitor pattern).
3
+ */
4
+ import { normalizeIrGraph } from './ir-structural.js';
5
+ import { materializeNormalizedGraph } from './ir-normalize.js';
6
+ import { isQueueLikeNodeType } from './graphPredicates.js';
7
+ /** Re-export shared predicates (structural + lint use the same HTTP/datastore semantics). */
8
+ export { isHttpLikeType, isDbLikeType, isQueueLikeNodeType } from './graphPredicates.js';
9
+ export function edgeEndpoints(e) {
10
+ const from = String(e.from ?? e.source ?? '').trim();
11
+ const to = String(e.to ?? e.target ?? '').trim();
12
+ return { from, to };
13
+ }
14
+ export function nodeType(n) {
15
+ return String(n.type ?? n.kind ?? '').toLowerCase();
16
+ }
17
+ export function looksLikeHealthUrl(url) {
18
+ const u = url.toLowerCase();
19
+ return (/(^|\/)health(z)?(\/|$|\?)/.test(u) ||
20
+ /(^|\/)healthcheck(\/|$|\?)/.test(u) ||
21
+ /(^|\/)ping(\/|$|\?)/.test(u) ||
22
+ /(^|\/)status(\/|$|\?)/.test(u) ||
23
+ /(^|\/)alive(\/|$|\?)/.test(u) ||
24
+ /(^|\/)live(\/|$|\?)/.test(u) ||
25
+ /(^|\/)ready(\/|$|\?)/.test(u));
26
+ }
27
+ /**
28
+ * When true, the edge is treated as **async** for IR-LINT-SYNC-CHAIN-001 (excluded from sync depth).
29
+ * Convention: `edge.config` / `edge.metadata` may set `async: true`, `protocol: async|message|queue|event`, or channel-like `kind`.
30
+ * Top-level `edge.kind` is merged into `metadata` during normalization; raw edges still pass `rec.kind` here.
31
+ * If `graph` is provided, edges **to** queue/topic-like nodes are treated as async even without edge metadata.
32
+ */
33
+ export function edgeRepresentsAsyncBoundary(e, graph) {
34
+ const cfg = e.config;
35
+ const meta = e.metadata;
36
+ const c = cfg && typeof cfg === 'object' && !Array.isArray(cfg) ? cfg : undefined;
37
+ const m = meta && typeof meta === 'object' && !Array.isArray(meta) ? meta : undefined;
38
+ if (m?.async === true || c?.async === true)
39
+ return true;
40
+ const proto = String(m?.protocol ?? c?.protocol ?? '').toLowerCase();
41
+ if (proto === 'async' || proto === 'message' || proto === 'queue' || proto === 'event' || proto === 'pubsub') {
42
+ return true;
43
+ }
44
+ const topKind = String(e.kind ?? '').toLowerCase();
45
+ const kind = String(m?.kind ?? c?.kind ?? topKind ?? '').toLowerCase();
46
+ if (/queue|topic|stream|kafka|sns|sqs|amqp|mqtt|nats/.test(kind))
47
+ return true;
48
+ if (graph) {
49
+ const { to } = edgeEndpoints(e);
50
+ if (to) {
51
+ const tn = graph.nodeById.get(to);
52
+ if (tn && isQueueLikeNodeType(nodeType(tn)))
53
+ return true;
54
+ }
55
+ }
56
+ return false;
57
+ }
58
+ /** Adjacency for sync-only dependency depth (omits edges marked async). */
59
+ export function buildSyncAdjacencyForLint(g) {
60
+ const m = new Map();
61
+ for (const id of g.nodeById.keys())
62
+ m.set(id, []);
63
+ for (const e of g.edges) {
64
+ if (!e || typeof e !== 'object')
65
+ continue;
66
+ const rec = e;
67
+ if (edgeRepresentsAsyncBoundary(rec, g))
68
+ continue;
69
+ const { from, to } = edgeEndpoints(rec);
70
+ if (!from || !to || !g.nodeById.has(from) || !g.nodeById.has(to))
71
+ continue;
72
+ m.get(from).push(to);
73
+ }
74
+ return m;
75
+ }
76
+ /**
77
+ * Build a parsed graph for lint visitors.
78
+ * On failure (invalid IR root, empty graph, etc.) returns `{ findings }` with IR-STRUCT-* errors instead of `null`.
79
+ */
80
+ export function buildParsedLintGraph(ir) {
81
+ const norm = normalizeIrGraph(ir);
82
+ if ('findings' in norm)
83
+ return { findings: norm.findings };
84
+ const graph = norm.graph;
85
+ const nodesRaw = graph.nodes;
86
+ if (!Array.isArray(nodesRaw) || nodesRaw.length === 0) {
87
+ return {
88
+ findings: [
89
+ {
90
+ code: 'IR-STRUCT-EMPTY_GRAPH',
91
+ severity: 'error',
92
+ message: 'Graph has no nodes',
93
+ fixHint: 'Add at least one node before running architecture lint.',
94
+ },
95
+ ],
96
+ };
97
+ }
98
+ const { normalized } = materializeNormalizedGraph(graph);
99
+ const nodeById = new Map();
100
+ for (const n of normalized.nodes) {
101
+ if (!n.id)
102
+ continue;
103
+ nodeById.set(n.id, {
104
+ id: n.id,
105
+ type: n.type,
106
+ name: n.name,
107
+ config: n.config,
108
+ schema: n.schema,
109
+ });
110
+ }
111
+ const edges = normalized.edges;
112
+ const adj = new Map();
113
+ const outDegree = new Map();
114
+ const inDegree = new Map();
115
+ for (const id of nodeById.keys()) {
116
+ adj.set(id, []);
117
+ outDegree.set(id, 0);
118
+ inDegree.set(id, 0);
119
+ }
120
+ for (const ne of normalized.edges) {
121
+ const { from, to } = ne;
122
+ if (!from || !to || !nodeById.has(from) || !nodeById.has(to))
123
+ continue;
124
+ adj.get(from).push(to);
125
+ outDegree.set(from, (outDegree.get(from) || 0) + 1);
126
+ inDegree.set(to, (inDegree.get(to) || 0) + 1);
127
+ }
128
+ return { nodeById, edges, adj, outDegree, inDegree };
129
+ }
130
+ /** Type guard: successful parse vs structural blockers. */
131
+ export function isParsedLintGraph(r) {
132
+ return 'nodeById' in r && r.nodeById instanceof Map;
133
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Architecture lint as a registry of visitor functions: each rule inspects ParsedLintGraph
3
+ * and returns IrStructuralFinding[] (layer: lint). Deterministic, no AI, no cloud.
4
+ */
5
+ import type { IrStructuralFinding } from './ir-structural.js';
6
+ import type { ParsedLintGraph } from './lint-graph.js';
7
+ /** IR-LINT-DIRECT-DB-ACCESS-002 — HTTP-like → datastore-like in one hop (broader than strict api/gateway + database enum). */
8
+ export declare function ruleDirectDbAccess(g: ParsedLintGraph): IrStructuralFinding[];
9
+ /** IR-LINT-HIGH-FANOUT-004 */
10
+ export declare function ruleHighFanout(g: ParsedLintGraph): IrStructuralFinding[];
11
+ /**
12
+ * IR-LINT-SYNC-CHAIN-001 — longest **synchronous** path from HTTP entry nodes.
13
+ * Edges marked async (see `edgeRepresentsAsyncBoundary` in `lint-graph.ts`) are excluded from depth.
14
+ *
15
+ * **HTTP entry roots:** Prefer HTTP-like nodes with **no incoming sync** edges. If every HTTP-like node
16
+ * has an incoming sync edge (e.g. internal-only graph shape), we **fall back** to treating **all** HTTP-like
17
+ * nodes as possible starts so the rule can still surface deep sync chains. See `docs/ENGINEERING_NOTES.md`.
18
+ */
19
+ export declare function ruleSyncChainFromHttpEntry(g: ParsedLintGraph): IrStructuralFinding[];
20
+ /** IR-LINT-NO-HEALTHCHECK-003 */
21
+ export declare function ruleNoHealthcheck(g: ParsedLintGraph): IrStructuralFinding[];
22
+ /**
23
+ * IR-LINT-ISOLATED-NODE-005 — node with no incident edges while the graph has at least one edge elsewhere.
24
+ */
25
+ export declare function ruleIsolatedNode(g: ParsedLintGraph): IrStructuralFinding[];
26
+ /** IR-LINT-DUPLICATE-EDGE-006 — same from→to pair appears more than once. */
27
+ export declare function ruleDuplicateEdge(g: ParsedLintGraph): IrStructuralFinding[];
28
+ /** IR-LINT-HTTP-MISSING-NAME-007 */
29
+ export declare function ruleHttpMissingName(g: ParsedLintGraph): IrStructuralFinding[];
30
+ /** IR-LINT-DATASTORE-NO-INCOMING-008 */
31
+ export declare function ruleDatastoreNoIncoming(g: ParsedLintGraph): IrStructuralFinding[];
32
+ /** IR-LINT-MULTIPLE-HTTP-ENTRIES-009 — more than one HTTP node with no incoming edges (multiple public entry surfaces). */
33
+ export declare function ruleMultipleHttpEntries(g: ParsedLintGraph): IrStructuralFinding[];
34
+ /**
35
+ * Ordered registry: add a new rule by implementing `(g) => findings` and appending here.
36
+ */
37
+ export declare const LINT_RULE_REGISTRY: ReadonlyArray<(g: ParsedLintGraph) => IrStructuralFinding[]>;
38
+ /** Run all registered architecture lint visitors (same as legacy `validateIrLint` behavior). */
39
+ export declare function runArchitectureLinting(g: ParsedLintGraph): IrStructuralFinding[];
40
+ //# sourceMappingURL=lint-rules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lint-rules.d.ts","sourceRoot":"","sources":["../src/lint-rules.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAavD,8HAA8H;AAC9H,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CA6B5E;AAED,8BAA8B;AAC9B,wBAAgB,cAAc,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CAkBxE;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CAwDpF;AAED,iCAAiC;AACjC,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CAsB3E;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CAoB1E;AAED,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CA0B3E;AAED,oCAAoC;AACpC,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CAmB7E;AAED,wCAAwC;AACxC,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CAkBjF;AAED,2HAA2H;AAC3H,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CAkBjF;AAED;;GAEG;AACH,eAAO,MAAM,kBAAkB,EAAE,aAAa,CAAC,CAAC,CAAC,EAAE,eAAe,KAAK,mBAAmB,EAAE,CAU3F,CAAC;AAEF,gGAAgG;AAChG,wBAAgB,sBAAsB,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CAEhF"}