@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,54 @@
1
+ /**
2
+ * Thin deterministic drift check: compare a fresh export from IR to an on-disk tree
3
+ * or to an in-memory file map (Cloud API). No semantic IR↔code analysis — regen vs reality.
4
+ */
5
+ import { type DeterministicExportResult } from './exportPipeline.js';
6
+ export type DriftCode = 'DRIFT-MISSING' | 'DRIFT-MODIFIED' | 'DRIFT-EXTRA' | 'DRIFT-NO-EXPORT';
7
+ export type DriftFinding = {
8
+ code: DriftCode;
9
+ path: string;
10
+ message: string;
11
+ };
12
+ export declare function normalizeExportFileContent(content: string): string;
13
+ /**
14
+ * Compare expected export map to an actual map (e.g. from client or built from disk reads).
15
+ */
16
+ export declare function diffExpectedExportAgainstFiles(expected: Record<string, string>, actual: Record<string, string>, options?: {
17
+ strictExtra?: boolean;
18
+ }): DriftFinding[];
19
+ /**
20
+ * Read all files under rootDir into a flat map (relative POSIX paths).
21
+ */
22
+ export declare function readDirectoryAsExportMap(rootDir: string): Promise<Record<string, string>>;
23
+ export declare function diffExpectedExportAgainstDirectory(expected: Record<string, string>, rootDir: string, options?: {
24
+ strictExtra?: boolean;
25
+ }): Promise<DriftFinding[]>;
26
+ export type ValidateDriftResult = {
27
+ ok: boolean;
28
+ driftFindings: DriftFinding[];
29
+ /** True when --strict-extra and any DRIFT-EXTRA was found */
30
+ extraBlocking: boolean;
31
+ exportResult: DeterministicExportResult;
32
+ };
33
+ export declare function runValidateDrift(actualIR: any, target: string, outDir: string, opts?: {
34
+ hostPort?: string | number;
35
+ skipIrStructuralValidation?: boolean;
36
+ skipIrLint?: boolean;
37
+ strictExtra?: boolean;
38
+ }): Promise<ValidateDriftResult>;
39
+ export type DriftCheckFilesResult = {
40
+ ok: boolean;
41
+ driftFindings: DriftFinding[];
42
+ extraBlocking: boolean;
43
+ exportResult: DeterministicExportResult;
44
+ };
45
+ /**
46
+ * Cloud / API: compare a fresh deterministic export to a client-supplied file map (e.g. last export or repo snapshot).
47
+ */
48
+ export declare function runDriftCheckAgainstFiles(actualIR: any, target: string, actualFiles: Record<string, string>, opts?: {
49
+ hostPort?: string | number;
50
+ skipIrStructuralValidation?: boolean;
51
+ skipIrLint?: boolean;
52
+ strictExtra?: boolean;
53
+ }): Promise<DriftCheckFilesResult>;
54
+ //# sourceMappingURL=validate-drift.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-drift.d.ts","sourceRoot":"","sources":["../src/validate-drift.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,EAA0B,KAAK,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AAG7F,MAAM,MAAM,SAAS,GAAG,eAAe,GAAG,gBAAgB,GAAG,aAAa,GAAG,iBAAiB,CAAC;AAE/F,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAElE;AAED;;GAEG;AACH,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAChC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,OAAO,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,GAClC,YAAY,EAAE,CAkChB;AAiBD;;GAEG;AACH,wBAAsB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAQ/F;AAED,wBAAsB,kCAAkC,CACtD,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAChC,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,GAClC,OAAO,CAAC,YAAY,EAAE,CAAC,CAoCzB;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,EAAE,EAAE,OAAO,CAAC;IACZ,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B,6DAA6D;IAC7D,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,EAAE,yBAAyB,CAAC;CACzC,CAAC;AAEF,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,GAAG,EACb,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,IAAI,GAAE;IACJ,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;CAClB,GACL,OAAO,CAAC,mBAAmB,CAAC,CAyC9B;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,EAAE,EAAE,OAAO,CAAC;IACZ,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,EAAE,yBAAyB,CAAC;CACzC,CAAC;AAEF;;GAEG;AACH,wBAAsB,yBAAyB,CAC7C,QAAQ,EAAE,GAAG,EACb,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACnC,IAAI,GAAE;IACJ,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;CAClB,GACL,OAAO,CAAC,qBAAqB,CAAC,CAgChC"}
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Thin deterministic drift check: compare a fresh export from IR to an on-disk tree
3
+ * or to an in-memory file map (Cloud API). No semantic IR↔code analysis — regen vs reality.
4
+ */
5
+ import { readFile, readdir } from 'node:fs/promises';
6
+ import { join } from 'node:path';
7
+ import { runDeterministicExport } from './exportPipeline.js';
8
+ import { normalizeGoldenHostPort } from './hostPort.js';
9
+ export function normalizeExportFileContent(content) {
10
+ return content.replace(/\r\n/g, '\n');
11
+ }
12
+ /**
13
+ * Compare expected export map to an actual map (e.g. from client or built from disk reads).
14
+ */
15
+ export function diffExpectedExportAgainstFiles(expected, actual, options) {
16
+ const findings = [];
17
+ const expectedKeys = Object.keys(expected).sort();
18
+ for (const rel of expectedKeys) {
19
+ if (!(rel in actual)) {
20
+ findings.push({
21
+ code: 'DRIFT-MISSING',
22
+ path: rel,
23
+ message: `Expected generated file is missing from the comparison set`,
24
+ });
25
+ continue;
26
+ }
27
+ const a = normalizeExportFileContent(actual[rel]);
28
+ const e = normalizeExportFileContent(expected[rel]);
29
+ if (a !== e) {
30
+ findings.push({
31
+ code: 'DRIFT-MODIFIED',
32
+ path: rel,
33
+ message: `File content differs from deterministic export for this IR`,
34
+ });
35
+ }
36
+ }
37
+ if (options?.strictExtra) {
38
+ for (const rel of Object.keys(actual).sort()) {
39
+ if (!(rel in expected)) {
40
+ findings.push({
41
+ code: 'DRIFT-EXTRA',
42
+ path: rel,
43
+ message: `File exists in comparison set but is not part of the deterministic export`,
44
+ });
45
+ }
46
+ }
47
+ }
48
+ return findings;
49
+ }
50
+ async function listRelativeFilesRecursive(rootDir, sub = '') {
51
+ const dirPath = sub ? join(rootDir, sub) : rootDir;
52
+ const entries = await readdir(dirPath, { withFileTypes: true });
53
+ const out = [];
54
+ for (const ent of entries) {
55
+ const piece = sub ? `${sub}/${ent.name}` : ent.name;
56
+ if (ent.isDirectory()) {
57
+ out.push(...(await listRelativeFilesRecursive(rootDir, piece)));
58
+ }
59
+ else {
60
+ out.push(piece.replace(/\\/g, '/'));
61
+ }
62
+ }
63
+ return out;
64
+ }
65
+ /**
66
+ * Read all files under rootDir into a flat map (relative POSIX paths).
67
+ */
68
+ export async function readDirectoryAsExportMap(rootDir) {
69
+ const rels = await listRelativeFilesRecursive(rootDir);
70
+ const map = {};
71
+ for (const rel of rels) {
72
+ const full = join(rootDir, ...rel.split('/'));
73
+ map[rel] = await readFile(full, 'utf8');
74
+ }
75
+ return map;
76
+ }
77
+ export async function diffExpectedExportAgainstDirectory(expected, rootDir, options) {
78
+ const findings = [];
79
+ for (const rel of Object.keys(expected).sort()) {
80
+ const fullPath = join(rootDir, ...rel.split('/'));
81
+ let disk;
82
+ try {
83
+ disk = await readFile(fullPath, 'utf8');
84
+ }
85
+ catch {
86
+ findings.push({
87
+ code: 'DRIFT-MISSING',
88
+ path: rel,
89
+ message: `File missing under output directory`,
90
+ });
91
+ continue;
92
+ }
93
+ if (normalizeExportFileContent(disk) !== normalizeExportFileContent(expected[rel])) {
94
+ findings.push({
95
+ code: 'DRIFT-MODIFIED',
96
+ path: rel,
97
+ message: `File differs from deterministic export for this IR`,
98
+ });
99
+ }
100
+ }
101
+ if (options?.strictExtra) {
102
+ const onDisk = await listRelativeFilesRecursive(rootDir);
103
+ for (const rel of onDisk.sort()) {
104
+ if (!(rel in expected)) {
105
+ findings.push({
106
+ code: 'DRIFT-EXTRA',
107
+ path: rel,
108
+ message: `Unexpected file not produced by current deterministic export`,
109
+ });
110
+ }
111
+ }
112
+ }
113
+ return findings;
114
+ }
115
+ export async function runValidateDrift(actualIR, target, outDir, opts = {}) {
116
+ const hostPort = normalizeGoldenHostPort(opts.hostPort ?? process.env.ARCHRAD_HOST_PORT);
117
+ const exportResult = await runDeterministicExport(actualIR, target, {
118
+ hostPort,
119
+ skipIrStructuralValidation: Boolean(opts.skipIrStructuralValidation),
120
+ skipIrLint: Boolean(opts.skipIrLint),
121
+ });
122
+ const { files } = exportResult;
123
+ if (Object.keys(files).length === 0) {
124
+ return {
125
+ ok: false,
126
+ driftFindings: [
127
+ {
128
+ code: 'DRIFT-NO-EXPORT',
129
+ path: '.',
130
+ message: 'No files generated from IR (structural errors or empty graph); cannot compare drift',
131
+ },
132
+ ],
133
+ extraBlocking: false,
134
+ exportResult,
135
+ };
136
+ }
137
+ const driftFindings = await diffExpectedExportAgainstDirectory(files, outDir, {
138
+ strictExtra: Boolean(opts.strictExtra),
139
+ });
140
+ const blocking = driftFindings.filter((f) => f.code === 'DRIFT-MISSING' || f.code === 'DRIFT-MODIFIED' || f.code === 'DRIFT-NO-EXPORT');
141
+ const extra = driftFindings.filter((f) => f.code === 'DRIFT-EXTRA');
142
+ const extraBlocking = Boolean(opts.strictExtra) && extra.length > 0;
143
+ const ok = blocking.length === 0 && !extraBlocking;
144
+ return {
145
+ ok,
146
+ driftFindings,
147
+ extraBlocking,
148
+ exportResult,
149
+ };
150
+ }
151
+ /**
152
+ * Cloud / API: compare a fresh deterministic export to a client-supplied file map (e.g. last export or repo snapshot).
153
+ */
154
+ export async function runDriftCheckAgainstFiles(actualIR, target, actualFiles, opts = {}) {
155
+ const hostPort = normalizeGoldenHostPort(opts.hostPort ?? process.env.ARCHRAD_HOST_PORT);
156
+ const exportResult = await runDeterministicExport(actualIR, target, {
157
+ hostPort,
158
+ skipIrStructuralValidation: Boolean(opts.skipIrStructuralValidation),
159
+ skipIrLint: Boolean(opts.skipIrLint),
160
+ });
161
+ const { files } = exportResult;
162
+ if (Object.keys(files).length === 0) {
163
+ return {
164
+ ok: false,
165
+ driftFindings: [
166
+ {
167
+ code: 'DRIFT-NO-EXPORT',
168
+ path: '.',
169
+ message: 'No files generated from IR (structural errors or empty graph); cannot compare drift',
170
+ },
171
+ ],
172
+ extraBlocking: false,
173
+ exportResult,
174
+ };
175
+ }
176
+ const driftFindings = diffExpectedExportAgainstFiles(files, actualFiles, {
177
+ strictExtra: Boolean(opts.strictExtra),
178
+ });
179
+ const blocking = driftFindings.filter((f) => f.code === 'DRIFT-MISSING' || f.code === 'DRIFT-MODIFIED' || f.code === 'DRIFT-NO-EXPORT');
180
+ const extra = driftFindings.filter((f) => f.code === 'DRIFT-EXTRA');
181
+ const extraBlocking = Boolean(opts.strictExtra) && extra.length > 0;
182
+ const ok = blocking.length === 0 && !extraBlocking;
183
+ return { ok, driftFindings, extraBlocking, exportResult };
184
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Convert a YAML blueprint file into canonical IR JSON shape for validate/export.
3
+ * YAML mirrors JSON IR: either `{ graph: { metadata?, nodes, edges? } }` or a bare `{ nodes, edges?, metadata? }`.
4
+ */
5
+ export declare class YamlGraphParseError extends Error {
6
+ constructor(message: string);
7
+ }
8
+ /**
9
+ * Parse YAML text and return `{ graph: { ... } }` suitable for `validateIrStructural` / `runDeterministicExport`.
10
+ */
11
+ export declare function parseYamlToCanonicalIr(yamlText: string): Record<string, unknown>;
12
+ /** Pretty-printed JSON for stable CLI output (2-space indent + trailing newline). */
13
+ export declare function canonicalIrToJsonString(ir: Record<string, unknown>): string;
14
+ //# sourceMappingURL=yamlToIr.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"yamlToIr.d.ts","sourceRoot":"","sources":["../src/yamlToIr.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA4BhF;AAED,qFAAqF;AACrF,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAE3E"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Convert a YAML blueprint file into canonical IR JSON shape for validate/export.
3
+ * YAML mirrors JSON IR: either `{ graph: { metadata?, nodes, edges? } }` or a bare `{ nodes, edges?, metadata? }`.
4
+ */
5
+ import yaml from 'js-yaml';
6
+ export class YamlGraphParseError extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = 'YamlGraphParseError';
10
+ }
11
+ }
12
+ /**
13
+ * Parse YAML text and return `{ graph: { ... } }` suitable for `validateIrStructural` / `runDeterministicExport`.
14
+ */
15
+ export function parseYamlToCanonicalIr(yamlText) {
16
+ let doc;
17
+ try {
18
+ doc = yaml.load(yamlText, { schema: yaml.JSON_SCHEMA });
19
+ }
20
+ catch (e) {
21
+ const msg = e instanceof Error ? e.message : String(e);
22
+ throw new YamlGraphParseError(`Invalid YAML: ${msg}`);
23
+ }
24
+ if (doc == null || typeof doc !== 'object' || Array.isArray(doc)) {
25
+ throw new YamlGraphParseError('YAML root must be a mapping (object), not null, a scalar, or a sequence at the top level.');
26
+ }
27
+ const o = doc;
28
+ if (o.graph != null && typeof o.graph === 'object' && !Array.isArray(o.graph)) {
29
+ return { graph: o.graph };
30
+ }
31
+ if (Array.isArray(o.nodes)) {
32
+ return { graph: { ...o } };
33
+ }
34
+ throw new YamlGraphParseError('YAML must define either top-level `graph:` (object) or top-level `nodes:` (array). See fixtures/minimal-graph.yaml.');
35
+ }
36
+ /** Pretty-printed JSON for stable CLI output (2-space indent + trailing newline). */
37
+ export function canonicalIrToJsonString(ir) {
38
+ return `${JSON.stringify(ir, null, 2)}\n`;
39
+ }
@@ -0,0 +1,47 @@
1
+ # Concept, adoption, and limits (honest framing)
2
+
3
+ This doc captures how the **deterministic OSS package** fits in the market—not as marketing copy, but as a **product/engineering** read for contributors and partners.
4
+
5
+ ## What’s compelling
6
+
7
+ - **IR as source of truth** — Separating *what should be built* (the graph) from *how it is generated* (templates/codegen) is a clean abstraction. Most tools skew either to opaque AI (hard to reproduce) or to dumb scaffolders (no architectural rigor).
8
+ - **Compiler mental model** — **`archrad validate`** treats the graph like source code: structural checks, lint rules, and CI gates. That discipline is rare in “architecture” tooling.
9
+ - **Tiered validation** — Structural → architecture lint → export-time OpenAPI **document-shape** is intentional: not “block on everything” or “warn on nothing.”
10
+ - **Determinism** — Same IR → same output matters for teams that want **reproducible CI** and defensible pipelines.
11
+
12
+ ## Where the friction is (real adoption hurdles)
13
+
14
+ ### Who owns the IR?
15
+
16
+ This package does **not** include a natural-language or visual **authoring** front-end. Input is **structured JSON** (or your own producer). The README states that **plain English → IR** is out of scope here: use **ArchRad Cloud**, an internal tool, or **your own LLM step**.
17
+
18
+ **Cold start:** Developers must hand-write graph JSON, generate IR from another system, or adopt upstream tooling. **Fixtures** help demos; they don’t remove the onboarding gap.
19
+
20
+ ### One-way export (no round-trip)
21
+
22
+ **`archrad export`** produces a **FastAPI** or **Express** project you can run and edit. There is **no** supported path today to **edit generated code and merge changes back into the IR**. That’s honest: the value proposition is **greenfield / contract-first scaffolding and validation**, not ongoing bidirectional architecture management—unless you rebuild that workflow yourself (e.g. regenerate into a fresh tree, or treat IR as the only editable source).
23
+
24
+ ## Strategic read (OSS vs Cloud)
25
+
26
+ A coherent story is:
27
+
28
+ 1. **OSS** — Auditable, deterministic **compiler + linter** for a defined IR: trust, CI, and “read the same code that emitted your zip.”
29
+ 2. **Cloud / product** — **IR production** (UX, AI-assisted graph), deeper **semantic/policy** validation, and org workflows.
30
+
31
+ If **IR** becomes a **shared format** beyond a single vendor (schemas, community examples, third-party generators), the OSS layer behaves like a **platform hook**. If IR stays **proprietary-in-practice**, the OSS repo is still valuable as a **spec and trust artifact**, but community leverage is smaller.
32
+
33
+ ## What would make OSS adoption stronger (directional ideas)
34
+
35
+ None of these are commitments; they are **plausible** ways to narrow the cold-start gap **without** moving the whole product into OSS:
36
+
37
+ - **Lightweight IR authoring in OSS** — **`archrad yaml-to-ir`** converts **`graph:`** or bare **`nodes:`** YAML to canonical IR JSON (see **`fixtures/minimal-graph.yaml`**). Further ideas: richer **starter templates**, **VS Code** snippets.
38
+ - **Editor integration** — e.g. JSON Schema validation + snippets in **VS Code** / Cursor rules for graph files.
39
+ - **Clear “IR from OpenAPI / Postman”** one-way adapters (if they match your graph model).
40
+
41
+ For product strategy, treat this list as **roadmap candidates**, not shipped features.
42
+
43
+ ## See also
44
+
45
+ - [IR_CONTRACT.md](./IR_CONTRACT.md) — parser boundary and normalized shapes
46
+ - [STRUCTURAL_VS_SEMANTIC_VALIDATION.md](./STRUCTURAL_VS_SEMANTIC_VALIDATION.md) — OSS vs Cloud validation split
47
+ - [README.md](../README.md) — usage and CLI
@@ -0,0 +1,87 @@
1
+ # Custom architecture rules (org / enterprise)
2
+
3
+ OSS **`@archrad/deterministic`** ships a fixed set of **`IR-LINT-*`** visitors in **`src/lint-rules.ts`**. They are **deterministic** graph checks — not Spectral, not LLM, not org policy packs (those sit in **ArchRad Cloud** or your own stack).
4
+
5
+ This doc shows a **minimal, real extension point**: the same **`ParsedLintGraph`** built-ins use, returning **`IrStructuralFinding[]`** with **`layer: 'lint'`**.
6
+
7
+ ## Two supported paths
8
+
9
+ | Path | When to use | `archrad validate` CLI |
10
+ |------|----------------|-------------------------|
11
+ | **Compose in your pipeline** | CI / IDP runs Node; you call the library | Unchanged — your script runs custom rules after **`validateIrLint`** or replaces it with the composed function below. |
12
+ | **Fork `arch-deterministic`** | You want **`archrad validate`** itself to emit **`ORG-*`** codes | Append your visitor to **`LINT_RULE_REGISTRY`** in **`lint-rules.ts`**, rebuild, publish / vendor the fork. |
13
+
14
+ **Do not mutate** **`LINT_RULE_REGISTRY`** from application code at runtime: it is typed as **`ReadonlyArray`** and the project may freeze or replace that array in the future. Treat **`runArchitectureLinting`** as the built-in runner only.
15
+
16
+ ## Worked example: timeout on `service` nodes
17
+
18
+ Assume IR nodes use **`type: "service"`** and you require **`config.timeout`** (milliseconds) on each.
19
+
20
+ **`my-org/require-service-timeout.ts`**
21
+
22
+ ```typescript
23
+ import type { IrStructuralFinding, ParsedLintGraph } from '@archrad/deterministic';
24
+
25
+ function nodeType(n: Record<string, unknown>): string {
26
+ return String(n.type ?? n.kind ?? '').toLowerCase();
27
+ }
28
+
29
+ export function ruleRequireServiceTimeout(g: ParsedLintGraph): IrStructuralFinding[] {
30
+ const findings: IrStructuralFinding[] = [];
31
+ for (const [id, n] of g.nodeById) {
32
+ if (nodeType(n) !== 'service') continue;
33
+ const cfg = (n.config as Record<string, unknown> | undefined) ?? {};
34
+ if (cfg.timeout == null || cfg.timeout === '') {
35
+ findings.push({
36
+ code: 'ORG-001',
37
+ severity: 'warning',
38
+ layer: 'lint',
39
+ message: `Service node "${id}" has no config.timeout`,
40
+ nodeId: id,
41
+ fixHint: 'Set config.timeout (e.g. milliseconds) on this node.',
42
+ });
43
+ }
44
+ }
45
+ return findings;
46
+ }
47
+ ```
48
+
49
+ **`my-org/validate-with-org-lint.ts`** — same entry shape as **`validateIrLint`**:
50
+
51
+ ```typescript
52
+ import {
53
+ buildParsedLintGraph,
54
+ isParsedLintGraph,
55
+ runArchitectureLinting,
56
+ } from '@archrad/deterministic';
57
+ import { ruleRequireServiceTimeout } from './require-service-timeout.js';
58
+
59
+ /** Built-in IR-LINT-* plus org rules (drop-in mental model for validateIrLint). */
60
+ export function validateIrLintWithOrg(ir: unknown) {
61
+ const built = buildParsedLintGraph(ir);
62
+ if (!isParsedLintGraph(built)) return built.findings;
63
+ return [...runArchitectureLinting(built), ...ruleRequireServiceTimeout(built)];
64
+ }
65
+ ```
66
+
67
+ Wire **`validateIrLintWithOrg`** (or your own name) into CI instead of plain **`validateIrLint`** when you need **`ORG-*`** findings. Combine with **`validateIrStructural`**, **`sortFindings`**, and **`shouldFailFromFindings`** the same way as the README library example.
68
+
69
+ ## CLI without a fork
70
+
71
+ There is **no** `archrad validate --rules ./extra.js` flag today. Options:
72
+
73
+ - **Wrapper script** that loads IR JSON, runs **`validateIrStructural`** + **`validateIrLintWithOrg`**, prints findings, exits **`1`** on your policy (mirror **`cli-findings`** formatting if you want).
74
+ - **Fork** and register **`ruleRequireServiceTimeout`** inside **`LINT_RULE_REGISTRY`** so the stock **`archrad validate`** binary includes it.
75
+
76
+ ## Conventions (match built-in rules)
77
+
78
+ - Return **`layer: 'lint'`** for architecture heuristics; use your own **`code`** prefix (**`ORG-*`**, **`ACME-*`**) to avoid colliding with **`IR-LINT-*`** / **`IR-STRUCT-*`**.
79
+ - Use **`ParsedLintGraph`**: **`g.nodeById`**, **`g.edges`**, **`g.adj`**, **`g.inDegree`** / **`g.outDegree`** — see **`src/lint-graph.ts`**.
80
+ - Reuse predicates when it helps: **`isHttpLikeType`**, **`isDbLikeType`**, **`isQueueLikeNodeType`** (exported from the package).
81
+ - Heavy or semantic policy (SOC2 mapping, “is this PII?”) belongs in **product** or a separate engine; keep OSS visitors **fast and deterministic**.
82
+
83
+ ## See also
84
+
85
+ - **[STRUCTURAL_VS_SEMANTIC_VALIDATION.md](./STRUCTURAL_VS_SEMANTIC_VALIDATION.md)** — OSS vs Cloud boundary.
86
+ - **[IR_CONTRACT.md](./IR_CONTRACT.md)** — normalized node/edge shapes before lint runs.
87
+ - **`src/lint-rules.ts`** — reference implementations for **`IR-LINT-*`**.
@@ -0,0 +1,42 @@
1
+ # Engineering notes (audit responses & tradeoffs)
2
+
3
+ Internal reference for **quality posture** and known limits of `@archrad/deterministic`.
4
+
5
+ ## TypeScript
6
+
7
+ - **`strict`: true** — enabled in `tsconfig.json` (with `noUnusedLocals` / `noUnusedParameters`). `tsconfig.build.json` extends it; **`npm test`** runs **`tsc -p tsconfig.build.json --noEmit`** before Vitest.
8
+ - **`skipLibCheck`: true** — keeps builds fast; dependency `.d.ts` issues are not typechecked. Acceptable while `strict` is on for first-party code.
9
+
10
+ ## Linting (Biome)
11
+
12
+ - **Biome** is installed with a **minimal** ruleset (`biome.json`: `noDebugger` only) so `npm run lint` is green without a large style refactor. Expanding to `recommended` rules (and/or formatter) is a follow-up chore.
13
+
14
+ ## IR-LINT-SYNC-CHAIN-001
15
+
16
+ - Depth uses **synchronous edges only**. Edges are excluded when `edgeRepresentsAsyncBoundary` is true — e.g. `metadata.protocol: async|message|queue|event`, `metadata.async: true`, `config.async: true`, top-level `edge.kind` merged into metadata, channel-like `kind`, edges **to** queue/topic/stream-like node types, or target nodes classified as queue-like (see `lint-graph.ts` / `graphPredicates.ts`). Document async boundaries in IR to avoid false positives.
17
+ - **HTTP entry selection:** The rule prefers HTTP-like nodes with **no incoming synchronous** edges as chain **starts**. If that set is empty (every HTTP-like node has an incoming sync edge — e.g. unusual modeling or an internal-only slice), the implementation **falls back** to using **all** HTTP-like nodes as possible starts so deep synchronous chains are still detectable. Interpret warnings in that case with your graph’s northbound entry model.
18
+
19
+ ## CLI safety
20
+
21
+ - **`--danger-skip-ir-structural-validation`** is the **documented** escape hatch; **`--skip-ir-structural-validation`** remains as a **hidden** backward-compatible alias. Do not use either in CI for real bundles.
22
+
23
+ ## `npm install` / `prepare`
24
+
25
+ - **`prepare`** was removed so installing the package does not always compile TS. **`prepublishOnly`** runs **`npm run build`** before **npm publish** (tarball includes `dist/`).
26
+ - **Monorepo / `file:..` consumers** (e.g. InkByte `server`) must **build** `packages/deterministic` **before** `npm ci` in the consumer — already covered in **`docs/DETERMINISTIC_OSS_SYNC.md`** and CI.
27
+
28
+ ## OpenAPI pass
29
+
30
+ - Export only validates **document shape** (parse + required top-level fields), not Spectral-level spec lint. See README and `openapi-structural.ts`.
31
+
32
+ ## Host port probe
33
+
34
+ - Preflight checks **127.0.0.1** only; bindings on `0.0.0.0` / IPv6 or other hosts may not be detected.
35
+
36
+ ## Dependencies
37
+
38
+ - **Major bumps** (e.g. Commander) should be reviewed manually; do not auto-merge without checking CLI behavior.
39
+
40
+ ## Versioning
41
+
42
+ - **0.1.0** / pre-1.0: public API and CLI flags may still evolve; follow **CHANGELOG.md**.
@@ -0,0 +1,54 @@
1
+ # IR contract: parser boundary and validation levels
2
+
3
+ ## External input (parser boundary)
4
+
5
+ The toolchain accepts **either**:
6
+
7
+ - `{ "graph": { ... } }` — product / wrapped shape, or
8
+ - A **bare graph** object with top-level `nodes` (and optional `edges`, `metadata`).
9
+
10
+ `normalizeIrGraph(ir)` returns a single internal **`graph`** object in both cases.
11
+
12
+ ## Internal normalized graph (after unwrap)
13
+
14
+ Always:
15
+
16
+ - `graph.metadata` — object (default `{}` if missing or invalid)
17
+ - `graph.nodes` — array (required for export; validated separately)
18
+ - `graph.edges` — array or absent; non-array `edges` is treated as `[]` with a structural **warning**
19
+
20
+ `materializeNormalizedGraph(graph)` builds the **coerced** view used by structural validation and architecture lint (generators still receive the **original** IR).
21
+
22
+ ## Internal normalized node
23
+
24
+ | Field | Meaning |
25
+ |----------|---------|
26
+ | `id` | string (trimmed from `id`) |
27
+ | `type` | string, lowercased from `type` **or** `kind` |
28
+ | `name` | string |
29
+ | `config` | object (empty if missing / non-object) |
30
+ | `schema` | object (empty if missing / non-object) |
31
+
32
+ ## Internal normalized edge
33
+
34
+ | Field | Meaning |
35
+ |----------|---------|
36
+ | `id` | string |
37
+ | `from` | string, from `from` **or** `source` |
38
+ | `to` | string, from `to` **or** `target` |
39
+ | `config` | object (empty if missing / non-object) |
40
+ | `metadata` | object (preserved for lint, e.g. `protocol: async`, `kind` from top-level **`edge.kind`** when `metadata.kind` is unset; empty if missing) |
41
+
42
+ **Index contract:** `materializeNormalizedGraph` maps **`graph.edges[i]` → `normalized.edges[i]`** 1:1 when `edges` is an array; structural findings reference `edgeIndex` in that array.
43
+
44
+ API: `materializeNormalizedGraph`, `normalizeNodeSlot`, `normalizeEdgeSlot` in `src/ir-normalize.ts` (re-exported from the package entry).
45
+
46
+ ## Validation levels (contract for developers)
47
+
48
+ 1. **JSON Schema validation** — Document contract: `schemas/archrad-ir-graph-v1.schema.json`. Use in editors, CI, or with a schema validator; this package does not require Ajv at runtime for export.
49
+ 2. **IR structural validation** — Runtime checks in `validateIrStructural`: nodes array, unique ids (edges referencing **duplicate** ids get **`IR-STRUCT-EDGE_AMBIGUOUS_*`**), **HTTP-like** node types (shared predicate with lint: `http`, `rest`, `gateway`, …), `config` path/method, edge endpoints, directed cycles (cycle message includes an example node id). Codes: **`IR-STRUCT-*`**.
50
+ 3. **Export-time OpenAPI structural validation** — After codegen, **`validateOpenApiInBundleStructural`** (parse + required OpenAPI document fields). Not Spectral-level API lint.
51
+
52
+ Between (2) and (3), **architecture lint** (`IR-LINT-*`) runs on a parsed graph from the normalized shape; it is heuristic, not JSON Schema. **`validateIrLint`** returns **`IR-STRUCT-*`** when the IR cannot be normalized (same as `normalizeIrGraph`); **`runDeterministicExport`** folds those into **`irStructuralFindings`** if structural validation was skipped so exports still fail closed on invalid roots / empty graphs.
53
+
54
+ See also [STRUCTURAL_VS_SEMANTIC_VALIDATION.md](./STRUCTURAL_VS_SEMANTIC_VALIDATION.md).
@@ -0,0 +1,86 @@
1
+ # Structural vs semantic validation (open core)
2
+
3
+ This document defines how **@archrad/deterministic** (OSS) and **ArchRad Cloud** (product) split validation.
4
+
5
+ **OSS positioning:** *Includes structural validation + basic architecture linting (rule-based, deterministic).*
6
+
7
+ ## OSS layers (names)
8
+
9
+ | Name in docs | Role |
10
+ |--------------|------|
11
+ | **IR structural validation** | Graph shape, references, cycles, HTTP path/method |
12
+ | **Architecture lint (basic)** | Deterministic heuristics (`IR-LINT-*`), no AI |
13
+ | **OpenAPI structural validation** | Document **shape** on generated spec (parse + required fields) |
14
+
15
+ ## Structural (OSS)
16
+
17
+ **Question:** Is the blueprint IR **well-formed** and **safe for the deterministic compiler**?
18
+
19
+ **Where:** `validateIrStructural()`, first step of `archrad validate` / `archrad export`.
20
+
21
+ **Examples:**
22
+
23
+ | Code | Meaning |
24
+ |------|---------|
25
+ | `IR-STRUCT-EDGE_UNKNOWN_FROM` | Edge references a node id that does not exist |
26
+ | `IR-STRUCT-EDGE_AMBIGUOUS_FROM` / `…_TO` | Edge uses an id that appears on **more than one** node |
27
+ | `IR-STRUCT-CYCLE` | Directed cycle (message names an example node on the cycle) |
28
+ | `IR-STRUCT-HTTP_PATH` | HTTP-**like** node (`http`, `rest`, `gateway`, …) `config.url` must start with `/` |
29
+ | `IR-STRUCT-DUP_NODE_ID` | Two nodes share the same `id` |
30
+
31
+ Findings use **`severity`**: `error` (blocks export) or `warning` / `info` when applicable.
32
+
33
+ **Contract:** See **`schemas/archrad-ir-graph-v1.schema.json`** for JSON shape; code rules may be stricter. **Parser boundary and normalized node/edge shapes:** [IR_CONTRACT.md](./IR_CONTRACT.md).
34
+
35
+ ## Architecture lint (OSS)
36
+
37
+ **Question:** Does the graph trip **light, rule-based** architecture smells (still deterministic)?
38
+
39
+ **Where:** `validateIrLint()` (returns **`IR-LINT-*`** when the graph parses; **`IR-STRUCT-*`** only if normalize fails — e.g. invalid root, empty nodes), `archrad validate` (after structural pass; structural errors skip the lint pass), `archrad export` (unless `--skip-ir-lint`). **`buildParsedLintGraph`** returns **`{ findings }`** with structural codes on failure instead of `null`; use **`isParsedLintGraph()`** to narrow.
40
+
41
+ | Code | Meaning |
42
+ |------|---------|
43
+ | `IR-LINT-DIRECT-DB-ACCESS-002` | HTTP node has a direct edge to a datastore-like node |
44
+ | `IR-LINT-SYNC-CHAIN-001` | Long **synchronous** dependency chain from an HTTP entry (async-marked edges excluded; see `edgeRepresentsAsyncBoundary` in `lint-graph.ts`) |
45
+ | `IR-LINT-NO-HEALTHCHECK-003` | HTTP routes exist but no typical health-like path (heuristic incl. `/health`, `/ping`, `/healthcheck`, …; one route per HTTP node) |
46
+ | `IR-LINT-HIGH-FANOUT-004` | Node with ≥5 outgoing edges |
47
+ | `IR-LINT-ISOLATED-NODE-005` | Node with no edges while the graph has other edges (disconnected subgraph) |
48
+ | `IR-LINT-DUPLICATE-EDGE-006` | Same `from`→`to` edge appears more than once |
49
+ | `IR-LINT-HTTP-MISSING-NAME-007` | HTTP-like node has empty `name` |
50
+ | `IR-LINT-DATASTORE-NO-INCOMING-008` | Datastore-like node has no incoming edges |
51
+ | `IR-LINT-MULTIPLE-HTTP-ENTRIES-009` | More than one HTTP node with no incoming edges |
52
+
53
+ **CI:** `archrad validate --fail-on-warning` or `--max-warnings N`. Not org-specific policy — that stays in Cloud. **Custom deterministic visitors** on the same graph (e.g. **`ORG-*`** codes): see **[CUSTOM_RULES.md](./CUSTOM_RULES.md)**.
54
+
55
+ ## Semantic (ArchRad Cloud)
56
+
57
+ **Question:** Is this architecture **appropriate** for security, compliance, scale, and org policy?
58
+
59
+ **Marketing / product names:** **Policy engine**, **architecture intelligence**, **AI remediation** (not shipped in `@archrad/deterministic`).
60
+
61
+ **Examples:** missing auth on sensitive routes, PII handling, SOC2 mapping, deeper bottleneck analysis, idempotency guidance, **AI-assisted repair loops**.
62
+
63
+ This layer is **not** required to use the OSS CLI or library offline.
64
+
65
+ ## OpenAPI “document shape” (OSS, post-generation)
66
+
67
+ After codegen, **`validateOpenApiInBundleStructural`** checks that the generated spec is **parseable OpenAPI 3.x** with **`paths`**, **`info.title`**, and **`info.version`**.
68
+
69
+ That is **document shape**, not full API linting: it does **not** enforce Spectral-style rules (e.g. mandatory `security`, `operationId` conventions, or style). Use Spectral or Cloud semantic checks if you need that depth.
70
+
71
+ ## Codegen vs validation (retry / timeouts)
72
+
73
+ The FastAPI/Express generators **can emit** retry, backoff, or circuit-breaker **code** when the IR includes the right edge/node config. That is **generation**, not proof that every external call is resilient.
74
+
75
+ Requiring “timeouts and retries on all third-party calls” is a **policy / semantic** concern — a good fit for **ArchRad Cloud** or org-specific tooling, not the baseline OSS structural layer.
76
+
77
+ ## OSS package vs InkByte product
78
+
79
+ **`@archrad/deterministic`** (and the public **`arch-deterministic`** repo) ship only what is in this package: IR structural rules, deterministic export, OpenAPI document-shape check, golden Docker/Makefile.
80
+
81
+ The **InkByte** monorepo may contain additional validation, analyzers, and LLM workflows under **`server/`** and elsewhere. Those are **not** implied by the OSS README unless the same code is published in this package.
82
+
83
+ ## One-line summary
84
+
85
+ - **Structural:** “Your graph **compiles**.”
86
+ - **Semantic:** “Your graph **should** run in production, per policy and best practice.”
@@ -0,0 +1,37 @@
1
+ {
2
+ "graph": {
3
+ "metadata": {
4
+ "name": "demo-direct-db-layered",
5
+ "description": "GIF/demo: same intent as violation fixture — API → service → Postgres; health route chained from API entry. Passes architecture lint (no IR-LINT-*)."
6
+ },
7
+ "nodes": [
8
+ {
9
+ "id": "orders-api",
10
+ "type": "http",
11
+ "name": "Orders API",
12
+ "config": { "url": "/orders", "method": "GET" }
13
+ },
14
+ {
15
+ "id": "health",
16
+ "type": "http",
17
+ "name": "Health",
18
+ "config": { "url": "/health", "method": "GET" }
19
+ },
20
+ {
21
+ "id": "orders-svc",
22
+ "type": "service",
23
+ "name": "Orders service"
24
+ },
25
+ {
26
+ "id": "orders-db",
27
+ "type": "postgres",
28
+ "name": "Orders DB"
29
+ }
30
+ ],
31
+ "edges": [
32
+ { "from": "orders-api", "to": "health", "id": "e-api-health" },
33
+ { "from": "orders-api", "to": "orders-svc", "id": "e-api-svc" },
34
+ { "from": "orders-svc", "to": "orders-db", "id": "e-svc-db" }
35
+ ]
36
+ }
37
+ }