@archrad/deterministic 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.3] - 2026-04-04
11
+
12
+ ### Fixed
13
+
14
+ - **CLI** (`validate`, `export`, `validate-drift`): a missing **`--ir`** file now reports **`archrad: --ir file not found: <path>`** instead of **`invalid JSON`**.
15
+
16
+ ### Changed
17
+
18
+ - **`IR-LINT-MISSING-AUTH-010`** — HTTP entry detection uses **`ParsedLintGraph.inDegree`** (same counts as `buildParsedLintGraph`) instead of a separate scan; reverse adjacency for auth-as-gateway only includes edges whose endpoints exist in **`nodeById`**.
19
+ - **Docs:** README documents **OpenAPI security → IR → `IR-LINT-MISSING-AUTH-010`** for the spec-to-compliance workflow.
20
+ - **`graphPredicates.ts`:** clarified comments for **`isHttpEndpointType`** vs **`isHttpLikeType`** (`graphql` vs `gateway` / `bff` / `grpc`).
21
+
22
+ ### Added
23
+
24
+ - **Tests:** structural **`IR-STRUCT-HTTP_*`** coverage for **`graphql`** (validated) vs **`gateway`** (excluded); regression tests locking lint message substrings for **`IR-LINT-DEAD-NODE-011`**, **`IR-LINT-DIRECT-DB-ACCESS-002`**, **`IR-LINT-SYNC-CHAIN-001`** (terminal copy / Show HN).
25
+ - **Fixture** **`fixtures/e2e-no-security-openapi.yaml`** (OpenAPI with **no** `security` / `securitySchemes`) + test asserting **`openApiStringToCanonicalIr` → `validateIrLint` → `IR-LINT-MISSING-AUTH-010`** — same pipeline as **`archrad ingest openapi`** + **`archrad validate`**.
26
+
10
27
  ## [0.1.2] - 2026-03-28
11
28
 
12
29
  ### Fixed
@@ -71,7 +88,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
71
88
  - Documented **codegen vs validation** for retry/timeout IR fields and **InkByte vs OSS** scope in README and structural/semantic doc.
72
89
  - README positioning: **deterministic compiler and linter for system architecture**; validation layers table (OSS vs Cloud); **`validate-drift`**, drift GIF / trust-loop recording docs, library **`runValidateDrift`** example.
73
90
 
74
- [Unreleased]: https://github.com/archradhq/arch-deterministic/compare/v0.1.2...HEAD
91
+ [Unreleased]: https://github.com/archradhq/arch-deterministic/compare/v0.1.3...HEAD
92
+ [0.1.3]: https://github.com/archradhq/arch-deterministic/releases/tag/v0.1.3
75
93
  [0.1.2]: https://github.com/archradhq/arch-deterministic/releases/tag/v0.1.2
76
94
  [0.1.1]: https://github.com/archradhq/arch-deterministic/releases/tag/v0.1.1
77
95
  [0.1.0]: https://github.com/archradhq/arch-deterministic/releases/tag/v0.1.0
package/README.md CHANGED
@@ -105,6 +105,8 @@ archrad validate --ir ./graph.json
105
105
  archrad export --ir ./graph.json --target python --out ./out
106
106
  ```
107
107
 
108
+ **OpenAPI security → IR → lint:** ingestion copies **global** and **per-operation** `security` requirement names onto each HTTP node as `config.security` (sorted, deterministic). An operation with explicit `security: []` becomes `config.authRequired: false` (intentionally public). If the spec declares **no** security at any level, nodes are left without those fields — then **`archrad validate`** can surface **`IR-LINT-MISSING-AUTH-010`** on HTTP-like entry nodes (compliance gap from the spec artifact alone).
109
+
108
110
  **YAML → JSON (lighter authoring):** edit **`fixtures/minimal-graph.yaml`** (or your own file) and compile to IR JSON, then validate or export:
109
111
 
110
112
  ```bash
package/dist/cli.js CHANGED
@@ -21,6 +21,30 @@ async function writeTree(baseDir, files) {
21
21
  await writeFile(dest, content, 'utf8');
22
22
  }
23
23
  }
24
+ /** Read and parse IR JSON; distinguish missing file from invalid JSON. */
25
+ async function readIrJsonFromPath(irPath) {
26
+ let raw;
27
+ try {
28
+ raw = await readFile(irPath, 'utf8');
29
+ }
30
+ catch (e) {
31
+ const err = e;
32
+ if (err?.code === 'ENOENT') {
33
+ console.error(`archrad: --ir file not found: ${irPath}`);
34
+ }
35
+ else {
36
+ console.error(`archrad: could not read --ir file: ${irPath} (${err?.message ?? String(e)})`);
37
+ }
38
+ return null;
39
+ }
40
+ try {
41
+ return JSON.parse(raw);
42
+ }
43
+ catch {
44
+ console.error('archrad: invalid JSON in --ir file');
45
+ return null;
46
+ }
47
+ }
24
48
  function parseMaxWarnings(v) {
25
49
  if (v == null || v === '')
26
50
  return undefined;
@@ -48,12 +72,8 @@ program
48
72
  .option('--max-warnings <n>', 'Exit with error if warning count is greater than n (e.g. 0 allows no warnings)')
49
73
  .action(async (cmdOpts) => {
50
74
  const irPath = resolve(cmdOpts.ir);
51
- let ir;
52
- try {
53
- ir = JSON.parse(await readFile(irPath, 'utf8'));
54
- }
55
- catch {
56
- console.error('archrad: invalid JSON in --ir file');
75
+ const ir = await readIrJsonFromPath(irPath);
76
+ if (ir == null) {
57
77
  process.exitCode = 1;
58
78
  return;
59
79
  }
@@ -182,16 +202,12 @@ program
182
202
  .action(async (cmdOpts) => {
183
203
  const irPath = resolve(cmdOpts.ir);
184
204
  const outDir = resolve(cmdOpts.out);
185
- const raw = await readFile(irPath, 'utf8');
186
- let ir;
187
- try {
188
- ir = JSON.parse(raw);
189
- }
190
- catch {
191
- console.error('archrad: invalid JSON in --ir file');
205
+ const parsed = await readIrJsonFromPath(irPath);
206
+ if (parsed == null) {
192
207
  process.exitCode = 1;
193
208
  return;
194
209
  }
210
+ const ir = parsed;
195
211
  const actualIR = ir.graph ? ir : { graph: ir };
196
212
  const hostPort = normalizeGoldenHostPort(cmdOpts.hostPort ?? process.env.ARCHRAD_HOST_PORT);
197
213
  if (!cmdOpts.skipHostPortCheck) {
@@ -266,15 +282,12 @@ program
266
282
  .action(async (cmdOpts) => {
267
283
  const irPath = resolve(cmdOpts.ir);
268
284
  const outDir = resolve(cmdOpts.out);
269
- let ir;
270
- try {
271
- ir = JSON.parse(await readFile(irPath, 'utf8'));
272
- }
273
- catch {
274
- console.error('archrad: invalid JSON in --ir file');
285
+ const parsed = await readIrJsonFromPath(irPath);
286
+ if (parsed == null) {
275
287
  process.exitCode = 1;
276
288
  return;
277
289
  }
290
+ const ir = parsed;
278
291
  const actualIR = ir.graph ? ir : { graph: ir };
279
292
  const hostPort = normalizeGoldenHostPort(cmdOpts.hostPort ?? process.env.ARCHRAD_HOST_PORT);
280
293
  if (!cmdOpts.skipHostPortCheck) {
@@ -4,9 +4,10 @@
4
4
  /**
5
5
  * Narrow predicate: node types that carry a single HTTP endpoint (`config.url` + HTTP method).
6
6
  * Used by **structural validation** for `IR-STRUCT-HTTP_PATH` / `IR-STRUCT-HTTP_METHOD` checks.
7
- * `gateway`, `grpc`, and `bff` are intentionally excluded they use different config shapes
7
+ * **`http` / `https` / `rest` / `api` / `graphql`** share this contract in the IR (GraphQL is one route + method in the IR model).
8
+ * **`gateway`**, **`grpc`**, and **`bff`** are intentionally **excluded** — they use different config shapes
8
9
  * (upstream routing, proto service/method, multi-route aggregation) and must not be required
9
- * to supply a REST-style url + HTTP method.
10
+ * to supply a REST-style `url` + HTTP method; they remain **`isHttpLikeType`** for lint (entries, health, sync chain, etc.).
10
11
  */
11
12
  export declare function isHttpEndpointType(t: string): boolean;
12
13
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"graphPredicates.d.ts","sourceRoot":"","sources":["../src/graphPredicates.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAMrD;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAQjD;AAED,oEAAoE;AACpE,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAM/C;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CASrD;AAED,iHAAiH;AACjH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAStD"}
1
+ {"version":3,"file":"graphPredicates.d.ts","sourceRoot":"","sources":["../src/graphPredicates.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAMrD;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAQjD;AAED,oEAAoE;AACpE,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAM/C;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CASrD;AAED,iHAAiH;AACjH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAStD"}
@@ -4,9 +4,10 @@
4
4
  /**
5
5
  * Narrow predicate: node types that carry a single HTTP endpoint (`config.url` + HTTP method).
6
6
  * Used by **structural validation** for `IR-STRUCT-HTTP_PATH` / `IR-STRUCT-HTTP_METHOD` checks.
7
- * `gateway`, `grpc`, and `bff` are intentionally excluded they use different config shapes
7
+ * **`http` / `https` / `rest` / `api` / `graphql`** share this contract in the IR (GraphQL is one route + method in the IR model).
8
+ * **`gateway`**, **`grpc`**, and **`bff`** are intentionally **excluded** — they use different config shapes
8
9
  * (upstream routing, proto service/method, multi-route aggregation) and must not be required
9
- * to supply a REST-style url + HTTP method.
10
+ * to supply a REST-style `url` + HTTP method; they remain **`isHttpLikeType`** for lint (entries, health, sync chain, etc.).
10
11
  */
11
12
  export function isHttpEndpointType(t) {
12
13
  const s = String(t ?? '')
@@ -1 +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;AAcvD,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;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CA2D7E;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CAsBtE;AAED;;GAEG;AACH,eAAO,MAAM,kBAAkB,EAAE,aAAa,CAAC,CAAC,CAAC,EAAE,eAAe,KAAK,mBAAmB,EAAE,CAY3F,CAAC;AAEF,gGAAgG;AAChG,wBAAgB,sBAAsB,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CAEhF"}
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;AAcvD,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;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CAoD7E;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CAsBtE;AAED;;GAEG;AACH,eAAO,MAAM,kBAAkB,EAAE,aAAa,CAAC,CAAC,CAAC,EAAE,eAAe,KAAK,mBAAmB,EAAE,CAY3F,CAAC;AAEF,gGAAgG;AAChG,wBAAgB,sBAAsB,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CAEhF"}
@@ -284,23 +284,15 @@ export function ruleMultipleHttpEntries(g) {
284
284
  * for intentionally public endpoints (health, public assets, etc.).
285
285
  */
286
286
  export function ruleHttpMissingAuth(g) {
287
- const { edges, nodeById, adj } = g;
288
- // Build set of nodes that have an incoming sync edge (not entry nodes)
289
- const hasIncomingEdge = new Set();
290
- for (const e of edges) {
291
- if (!e || typeof e !== 'object')
292
- continue;
293
- const { to } = edgeEndpoints(e);
294
- if (to)
295
- hasIncomingEdge.add(to);
296
- }
297
- // Build reverse adjacency: to → [from] for auth-coverage check #2
287
+ const { edges, nodeById, adj, inDegree } = g;
288
+ // Entry = no valid incoming edge (same counts as buildParsedLintGraph.inDegree)
289
+ // Build reverse adjacency: to → [from] for auth-coverage check #2 (valid endpoints only, same as adj)
298
290
  const reverseAdj = new Map();
299
291
  for (const e of edges) {
300
292
  if (!e || typeof e !== 'object')
301
293
  continue;
302
294
  const { from, to } = edgeEndpoints(e);
303
- if (!from || !to)
295
+ if (!from || !to || !nodeById.has(from) || !nodeById.has(to))
304
296
  continue;
305
297
  if (!reverseAdj.has(to))
306
298
  reverseAdj.set(to, []);
@@ -310,7 +302,7 @@ export function ruleHttpMissingAuth(g) {
310
302
  for (const [id, n] of nodeById) {
311
303
  if (!isHttpLikeType(nodeType(n)))
312
304
  continue;
313
- if (hasIncomingEdge.has(id))
305
+ if ((inDegree.get(id) ?? 0) > 0)
314
306
  continue; // not an entry node
315
307
  const cfg = (n.config ?? {});
316
308
  // Explicit opt-out: config.authRequired === false marks an intentionally public endpoint
@@ -0,0 +1,11 @@
1
+ openapi: 3.0.0
2
+ info:
3
+ title: Test API
4
+ version: 1.0.0
5
+ paths:
6
+ /payments:
7
+ post:
8
+ operationId: createPayment
9
+ responses:
10
+ '200':
11
+ description: OK
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archrad/deterministic",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "A deterministic compiler and linter for system architecture. Validate your architecture before you write code. OSS: structural validation + basic architecture lint (rule-based); FastAPI/Express export; OpenAPI document-shape; golden Docker/Makefile — no LLM.",
5
5
  "keywords": [
6
6
  "archrad",