@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,290 @@
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 { edgeEndpoints, nodeType, isDbLikeType, isHttpLikeType, looksLikeHealthUrl, buildSyncAdjacencyForLint, edgeRepresentsAsyncBoundary, } from './lint-graph.js';
6
+ const LAYER = 'lint';
7
+ /** IR-LINT-DIRECT-DB-ACCESS-002 — HTTP-like → datastore-like in one hop (broader than strict api/gateway + database enum). */
8
+ export function ruleDirectDbAccess(g) {
9
+ const findings = [];
10
+ const seenPair = new Set();
11
+ for (const e of g.edges) {
12
+ if (!e || typeof e !== 'object')
13
+ continue;
14
+ const { from, to } = edgeEndpoints(e);
15
+ if (!from || !to)
16
+ continue;
17
+ const pairKey = `${from}\0${to}`;
18
+ const a = g.nodeById.get(from);
19
+ const b = g.nodeById.get(to);
20
+ if (!a || !b)
21
+ continue;
22
+ const ta = nodeType(a);
23
+ const tb = nodeType(b);
24
+ if (isHttpLikeType(ta) && isDbLikeType(tb)) {
25
+ if (seenPair.has(pairKey))
26
+ continue;
27
+ seenPair.add(pairKey);
28
+ findings.push({
29
+ code: 'IR-LINT-DIRECT-DB-ACCESS-002',
30
+ severity: 'warning',
31
+ layer: LAYER,
32
+ message: `API node "${from}" connects directly to datastore node "${to}"`,
33
+ nodeId: from,
34
+ fixHint: 'Introduce a service or domain layer between HTTP handlers and persistence.',
35
+ suggestion: 'Route traffic through an application/service node so HTTP is not coupled to a single DB node.',
36
+ impact: 'Harder to test, swap storage, or enforce invariants at a single boundary.',
37
+ });
38
+ }
39
+ }
40
+ return findings;
41
+ }
42
+ /** IR-LINT-HIGH-FANOUT-004 */
43
+ export function ruleHighFanout(g) {
44
+ const FANOUT_THRESHOLD = 5;
45
+ const findings = [];
46
+ for (const [id, deg] of g.outDegree) {
47
+ if (deg >= FANOUT_THRESHOLD) {
48
+ findings.push({
49
+ code: 'IR-LINT-HIGH-FANOUT-004',
50
+ severity: 'warning',
51
+ layer: LAYER,
52
+ message: `Node "${id}" has ${deg} outgoing dependencies (threshold ${FANOUT_THRESHOLD})`,
53
+ nodeId: id,
54
+ fixHint: 'Split responsibilities or group related downstream calls.',
55
+ suggestion: 'Consider a facade, batching, or async handoff to reduce coupling and blast radius.',
56
+ impact: 'Hotspots for change, failure, and latency under load.',
57
+ });
58
+ }
59
+ }
60
+ return findings;
61
+ }
62
+ /**
63
+ * IR-LINT-SYNC-CHAIN-001 — longest **synchronous** path from HTTP entry nodes.
64
+ * Edges marked async (see `edgeRepresentsAsyncBoundary` in `lint-graph.ts`) are excluded from depth.
65
+ *
66
+ * **HTTP entry roots:** Prefer HTTP-like nodes with **no incoming sync** edges. If every HTTP-like node
67
+ * has an incoming sync edge (e.g. internal-only graph shape), we **fall back** to treating **all** HTTP-like
68
+ * nodes as possible starts so the rule can still surface deep sync chains. See `docs/ENGINEERING_NOTES.md`.
69
+ */
70
+ export function ruleSyncChainFromHttpEntry(g) {
71
+ const { edges, nodeById } = g;
72
+ const syncAdj = buildSyncAdjacencyForLint(g);
73
+ const hasIncomingSync = new Set();
74
+ for (const e of edges) {
75
+ if (!e || typeof e !== 'object')
76
+ continue;
77
+ const rec = e;
78
+ if (edgeRepresentsAsyncBoundary(rec, g))
79
+ continue;
80
+ const { to } = edgeEndpoints(rec);
81
+ if (to)
82
+ hasIncomingSync.add(to);
83
+ }
84
+ const httpEntryIds = [];
85
+ for (const [id, n] of nodeById) {
86
+ if (isHttpLikeType(nodeType(n)) && !hasIncomingSync.has(id))
87
+ httpEntryIds.push(id);
88
+ }
89
+ const starts = httpEntryIds.length > 0
90
+ ? httpEntryIds
91
+ : [...nodeById.keys()].filter((id) => isHttpLikeType(nodeType(nodeById.get(id))));
92
+ const memo = new Map();
93
+ function maxDepth(u, stack) {
94
+ if (stack.has(u))
95
+ return 0;
96
+ if (memo.has(u))
97
+ return memo.get(u);
98
+ stack.add(u);
99
+ let d = 0;
100
+ for (const v of syncAdj.get(u) || []) {
101
+ d = Math.max(d, 1 + maxDepth(v, stack));
102
+ }
103
+ stack.delete(u);
104
+ memo.set(u, d);
105
+ return d;
106
+ }
107
+ const SYNC_CHAIN_THRESHOLD = 3;
108
+ let maxChain = 0;
109
+ for (const start of starts) {
110
+ memo.clear();
111
+ maxChain = Math.max(maxChain, maxDepth(start, new Set()));
112
+ }
113
+ if (maxChain >= SYNC_CHAIN_THRESHOLD && starts.length > 0) {
114
+ return [
115
+ {
116
+ code: 'IR-LINT-SYNC-CHAIN-001',
117
+ severity: 'warning',
118
+ layer: LAYER,
119
+ message: `Long synchronous dependency chain from HTTP entry (depth ≈ ${maxChain} hops; async-marked edges excluded)`,
120
+ fixHint: 'Shorten the call graph or mark message/queue edges as async in edge metadata.',
121
+ suggestion: 'Set `metadata.protocol: "async"` or `config.async: true` on non-blocking edges, or use queues between services.',
122
+ impact: 'High tail latency and failure amplification under load when calls are actually synchronous.',
123
+ },
124
+ ];
125
+ }
126
+ return [];
127
+ }
128
+ /** IR-LINT-NO-HEALTHCHECK-003 */
129
+ export function ruleNoHealthcheck(g) {
130
+ const httpNodes = [...g.nodeById.entries()].filter(([, n]) => isHttpLikeType(nodeType(n)));
131
+ if (httpNodes.length === 0)
132
+ return [];
133
+ for (const [, n] of httpNodes) {
134
+ const cfg = n.config || {};
135
+ const url = String(cfg.url ?? '').trim();
136
+ if (looksLikeHealthUrl(url))
137
+ return [];
138
+ }
139
+ return [
140
+ {
141
+ code: 'IR-LINT-NO-HEALTHCHECK-003',
142
+ severity: 'warning',
143
+ layer: LAYER,
144
+ message: 'No HTTP node exposes a typical health/readiness path (/health, /healthz, /healthcheck, /ping, /status, /live, /ready). Heuristic: one route per HTTP node; gateway/BFF with many routes may need a dedicated health node.',
145
+ fixHint: 'Add a GET route such as /health for orchestrators and load balancers.',
146
+ suggestion: 'Expose liveness vs readiness separately if your platform distinguishes them.',
147
+ impact: 'Weaker deploy/rollback safety and harder operations automation.',
148
+ },
149
+ ];
150
+ }
151
+ /**
152
+ * IR-LINT-ISOLATED-NODE-005 — node with no incident edges while the graph has at least one edge elsewhere.
153
+ */
154
+ export function ruleIsolatedNode(g) {
155
+ if (g.edges.length === 0 || g.nodeById.size <= 1)
156
+ return [];
157
+ const findings = [];
158
+ for (const [id] of g.nodeById) {
159
+ const out = g.outDegree.get(id) ?? 0;
160
+ const inn = g.inDegree.get(id) ?? 0;
161
+ if (out === 0 && inn === 0) {
162
+ findings.push({
163
+ code: 'IR-LINT-ISOLATED-NODE-005',
164
+ severity: 'warning',
165
+ layer: LAYER,
166
+ message: `Node "${id}" is not connected to any edge (disconnected subgraph)`,
167
+ nodeId: id,
168
+ fixHint: 'Remove the orphan node or add edges so it participates in the architecture.',
169
+ suggestion: 'Disconnected nodes often indicate stale IR or a missing integration step.',
170
+ impact: 'Export and reviews may not reflect real runtime behavior for this component.',
171
+ });
172
+ }
173
+ }
174
+ return findings;
175
+ }
176
+ /** IR-LINT-DUPLICATE-EDGE-006 — same from→to pair appears more than once. */
177
+ export function ruleDuplicateEdge(g) {
178
+ const seen = new Map();
179
+ const findings = [];
180
+ for (let i = 0; i < g.edges.length; i++) {
181
+ const e = g.edges[i];
182
+ if (!e || typeof e !== 'object')
183
+ continue;
184
+ const { from, to } = edgeEndpoints(e);
185
+ if (!from || !to)
186
+ continue;
187
+ const key = `${from}\0${to}`;
188
+ if (seen.has(key)) {
189
+ findings.push({
190
+ code: 'IR-LINT-DUPLICATE-EDGE-006',
191
+ severity: 'warning',
192
+ layer: LAYER,
193
+ message: `Duplicate edge from "${from}" to "${to}" (also at index ${seen.get(key)})`,
194
+ edgeIndex: i,
195
+ nodeId: from,
196
+ fixHint: 'Collapse duplicate edges or distinguish them with metadata if your IR allows.',
197
+ suggestion: 'Parallel duplicate edges rarely add information and clutter graph views.',
198
+ impact: 'Downstream generators and metrics may double-count dependencies.',
199
+ });
200
+ }
201
+ else {
202
+ seen.set(key, i);
203
+ }
204
+ }
205
+ return findings;
206
+ }
207
+ /** IR-LINT-HTTP-MISSING-NAME-007 */
208
+ export function ruleHttpMissingName(g) {
209
+ const findings = [];
210
+ for (const [id, n] of g.nodeById) {
211
+ if (!isHttpLikeType(nodeType(n)))
212
+ continue;
213
+ const name = String(n.name ?? '').trim();
214
+ if (!name) {
215
+ findings.push({
216
+ code: 'IR-LINT-HTTP-MISSING-NAME-007',
217
+ severity: 'warning',
218
+ layer: LAYER,
219
+ message: `HTTP-like node "${id}" has no display name`,
220
+ nodeId: id,
221
+ fixHint: 'Set a short human-readable `name` for docs, OpenAPI titles, and team communication.',
222
+ suggestion: 'Names appear in generated README snippets and UI graph labels when mirrored from IR.',
223
+ impact: 'Harder to navigate large graphs and generated documentation.',
224
+ });
225
+ }
226
+ }
227
+ return findings;
228
+ }
229
+ /** IR-LINT-DATASTORE-NO-INCOMING-008 */
230
+ export function ruleDatastoreNoIncoming(g) {
231
+ const findings = [];
232
+ for (const [id, n] of g.nodeById) {
233
+ if (!isDbLikeType(nodeType(n)))
234
+ continue;
235
+ if ((g.inDegree.get(id) ?? 0) === 0) {
236
+ findings.push({
237
+ code: 'IR-LINT-DATASTORE-NO-INCOMING-008',
238
+ severity: 'warning',
239
+ layer: LAYER,
240
+ message: `Datastore-like node "${id}" has no incoming edges`,
241
+ nodeId: id,
242
+ fixHint: 'Connect a service or migration path to this datastore, or remove it if unused.',
243
+ suggestion: 'Orphan persistence nodes often mean the IR is incomplete or a dead component.',
244
+ impact: 'Risk of shipping diagrams that do not match how data is actually written.',
245
+ });
246
+ }
247
+ }
248
+ return findings;
249
+ }
250
+ /** IR-LINT-MULTIPLE-HTTP-ENTRIES-009 — more than one HTTP node with no incoming edges (multiple public entry surfaces). */
251
+ export function ruleMultipleHttpEntries(g) {
252
+ const entries = [];
253
+ for (const [id, n] of g.nodeById) {
254
+ if (!isHttpLikeType(nodeType(n)))
255
+ continue;
256
+ if ((g.inDegree.get(id) ?? 0) === 0)
257
+ entries.push(id);
258
+ }
259
+ if (entries.length <= 1)
260
+ return [];
261
+ return [
262
+ {
263
+ code: 'IR-LINT-MULTIPLE-HTTP-ENTRIES-009',
264
+ severity: 'warning',
265
+ layer: LAYER,
266
+ message: `Multiple HTTP entry nodes with no incoming edges (${entries.length}): ${entries.join(', ')}`,
267
+ fixHint: 'Consider a single API gateway or BFF, or document why multiple public surfaces are intentional.',
268
+ suggestion: 'Many teams standardize on one northbound HTTP edge for auth, rate limits, and observability.',
269
+ impact: 'Operational duplication and inconsistent cross-cutting concerns across entrypoints.',
270
+ },
271
+ ];
272
+ }
273
+ /**
274
+ * Ordered registry: add a new rule by implementing `(g) => findings` and appending here.
275
+ */
276
+ export const LINT_RULE_REGISTRY = [
277
+ ruleDirectDbAccess,
278
+ ruleHighFanout,
279
+ ruleSyncChainFromHttpEntry,
280
+ ruleNoHealthcheck,
281
+ ruleIsolatedNode,
282
+ ruleDuplicateEdge,
283
+ ruleHttpMissingName,
284
+ ruleDatastoreNoIncoming,
285
+ ruleMultipleHttpEntries,
286
+ ];
287
+ /** Run all registered architecture lint visitors (same as legacy `validateIrLint` behavior). */
288
+ export function runArchitectureLinting(g) {
289
+ return LINT_RULE_REGISTRY.flatMap((rule) => rule(g));
290
+ }
@@ -0,0 +1,2 @@
1
+ export default function generateNodeExpressFiles(actualIR: any, opts?: any): Promise<Record<string, string>>;
2
+ //# sourceMappingURL=nodeExpress.d.ts.map
@@ -0,0 +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"}