@archrad/deterministic 0.1.0 → 0.1.2

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,10 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.2] - 2026-03-28
11
+
12
+ ### Fixed
13
+
14
+ - **`IR-STRUCT-HTTP_PATH` / `IR-STRUCT-HTTP_METHOD` false positives on `gateway`, `grpc`, `bff`** — structural validation previously used `isHttpLikeType` (correct for lint) for the url/method check. A `gateway` node with no `config.url` produced a spurious error; a `grpc` node with `config.method: "GetUser"` produced two. Introduced `isHttpEndpointType` (narrow: `http`, `https`, `rest`, `api`, `graphql`) for the structural check only. `isHttpLikeType` is unchanged for lint.
15
+ - **`IR-STRUCT-CYCLE` path lost** — extracted `detectCycles(adj: Map<string, string[]>): string[] | null` from the inline `dfs` closure in `validateIrStructural`. Returns the full ordered cycle path; findings now read `Directed cycle detected: a → b → c → a`. Exported from package root.
16
+ - **`IR-STRUCT-NODE_INVALID_CONFIG` (warning)** — `emptyRecord()` previously coerced `"config": ["wrong"]` or `"config": null` silently to `{}`. Structural validation now emits a warning when `config` is present but is not a plain object, including the actual type in the message.
17
+
18
+ ### Added
19
+
20
+ - **`IR-LINT-MISSING-AUTH-010` (warning)** — fires on HTTP-like entry nodes (no incoming edges, including `gateway`, `bff`, `grpc`) with no auth coverage. A node is covered when: (1) an immediate outgoing neighbour is auth-like (`auth`, `middleware`, `oauth`, `jwt`, `saml`, `keycloak`, `okta`, `cognito`, `auth0`, `ldap`, `iam`, `sso`, etc.), (2) an auth-like node has a direct edge to the entry (auth-as-gateway pattern), or (3) `config` carries any of `auth`, `authRequired`, `authentication`, `authorization`, `security`. Explicit opt-out: `config.authRequired: false` for intentionally public endpoints (health, signup, public assets). Maps directly to PCI-DSS and HIPAA requirements.
21
+ - **`IR-LINT-DEAD-NODE-011` (warning)** — fires on nodes with incoming edges but no outgoing edges that are not a recognised sink type (datastore-like, queue-like, or HTTP-like). HTTP nodes are excluded because they return responses to callers. Complements `IR-LINT-ISOLATED-NODE-005` which catches fully disconnected nodes.
22
+ - **OpenAPI ingestion — security definitions** — `archrad ingest openapi` now extracts security scheme names into node config following OpenAPI 3.x precedence: global `doc.security` propagates to all operations as `config.security: ["SchemeName"]` (sorted); operation-level `security` overrides global; explicit `security: []` sets `config.authRequired: false`. Nodes with no security at either level produce no security config and are flagged by `IR-LINT-MISSING-AUTH-010` in CI.
23
+ - **New predicate exports** — `isHttpEndpointType`, `isAuthLikeNodeType` added to `graphPredicates.ts` and exported from the package root alongside the existing predicates, for consumers building custom lint rules.
24
+ - **`detectCycles` exported from package root** — useful for consumers building custom structural validators or tooling on top of the IR adjacency graph.
25
+
26
+ ## [0.1.1] - 2026-03-28
27
+
10
28
  ### Added
11
29
  - **[docs/CUSTOM_RULES.md](docs/CUSTOM_RULES.md)** — custom **`IR-LINT`-style** visitors (`ParsedLintGraph` → **`IrStructuralFinding[]`**): worked **service / `config.timeout`** example, **compose** (`runArchitectureLinting` + org rules) vs **fork** (`LINT_RULE_REGISTRY`); no runtime registry mutation.
12
30
  - **`archrad validate-drift`** — compare an on-disk export directory to a **fresh** deterministic export from the same IR (`DRIFT-MISSING` / `DRIFT-MODIFIED` / optional `DRIFT-EXTRA` with **`--strict-extra`**); **`--json`** for CI. Library: **`runValidateDrift`**, **`diffExpectedExportAgainstFiles`**, **`runDriftCheckAgainstFiles`**, etc. (`src/validate-drift.ts`).
13
- - **VHS tape** **`scripts/record-demo-drift.tape`** → **`demo-drift.gif`**; **`npm run record:demo:drift`**. Storyboard and recording docs updated (**`scripts/DEMO_GIF_STORYBOARD.md`**). **Replay without VHS:** **`scripts/run-demo-drift-sequence.sh`** / **`.ps1`** for ShareX/OBS/asciinema capture; **`README_DEMO_RECORDING.md`** (**When VHS fails**).
31
+ - **VHS tape** **`scripts/record-demo-drift.tape`** → **`demo-drift.gif`**; **`npm run record:demo:drift`**. Storyboard and recording docs updated (**`scripts/DEMO_GIF_STORYBOARD.md`**). **Replay without VHS:** **`scripts/run-demo-drift-sequence.sh`** / **`.ps1`** for ShareX/OBS/asciinema capture; **`README_DEMO_RECORDING.md`** (**When VHS fails**). **`scripts/invoke-drift-check.ps1`** for repeatable drift checks on Windows.
14
32
  - **`graphPredicates.ts`**: shared **`isHttpLikeType`** / **`isDbLikeType`** / **`isQueueLikeNodeType`** (exported from the package root) so structural HTTP checks and IR-LINT stay aligned.
15
33
  - **`IR-STRUCT-EDGE_AMBIGUOUS_FROM` / `IR-STRUCT-EDGE_AMBIGUOUS_TO`** when an edge references a **duplicate** node id.
16
34
  - **`ParsedLintGraph.inDegree`**, **`BuildParsedLintGraphResult`**, **`isParsedLintGraph()`** — `buildParsedLintGraph` returns **`{ findings }`** on parse failure instead of **`null`**.
@@ -28,6 +46,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
28
46
  - CLI **`export`**: **`--skip-ir-structural-validation`**, **`--skip-ir-lint`**, **`--fail-on-warning`**, **`--max-warnings <n>`** (blocks writes when IR policy fails).
29
47
  - Library: **`validateIrLint`**, **`sortFindings`**, **`shouldFailFromFindings`**, **`IrFindingLayer`**.
30
48
  - Fixtures **`invalid-edge-unknown-node.json`**, **`invalid-cycle.json`** for negative tests; **`ecommerce-with-warnings.json`** triggers all four **`IR-LINT-*`** rules for demos and tests.
49
+ - **`package.json` `keywords`**: **`architecture-as-code`**, **`blueprint`**, **`ir`**, **`validate-drift`** for npm discoverability.
31
50
 
32
51
  ### Changed
33
52
  - **`validateIrLint`** returns **structural findings** when the IR cannot be built (same codes as **`normalizeIrGraph`** / empty graph), instead of **`[]`**.
@@ -50,17 +69,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
50
69
  - Canonical **OSS positioning** line in README, `llms.txt`, monorepo/OSS docs: *Includes structural validation + basic architecture linting (rule-based, deterministic).*
51
70
  - Clarified OpenAPI pass as **document shape** (parse + required top-level fields), explicitly **not** Spectral-style lint; README + `docs/STRUCTURAL_VS_SEMANTIC_VALIDATION.md` + code comments.
52
71
  - Documented **codegen vs validation** for retry/timeout IR fields and **InkByte vs OSS** scope in README and structural/semantic doc.
53
- - README positioning: **deterministic compiler and linter for system architecture**; validation layers table (OSS vs Cloud).
54
-
55
- ## [0.1.0] - 2026-02-26
56
-
57
- ### Added
58
- - Deterministic **FastAPI** and **Express** generators from blueprint **IR** (JSON graph).
59
- - **`archrad export`** CLI (`--ir`, `--target`, `--out`).
60
- - **Structural OpenAPI** validation pass on generated bundles (warnings, no LLM repair).
61
- - **Golden path**: `docker-compose.yml`, `Dockerfile`, `Makefile`, README section; container port **8080**; configurable **host** publish port (`--host-port` / `ARCHRAD_HOST_PORT`).
62
- - Optional **localhost preflight** for host port (warn or `--strict-host-port`).
63
- - Library API: `runDeterministicExport`, OpenAPI helpers, golden-layer helpers.
72
+ - 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.
64
73
 
65
- [Unreleased]: https://github.com/archradhq/arch-deterministic/compare/v0.1.0...HEAD
74
+ [Unreleased]: https://github.com/archradhq/arch-deterministic/compare/v0.1.2...HEAD
75
+ [0.1.2]: https://github.com/archradhq/arch-deterministic/releases/tag/v0.1.2
76
+ [0.1.1]: https://github.com/archradhq/arch-deterministic/releases/tag/v0.1.1
66
77
  [0.1.0]: https://github.com/archradhq/arch-deterministic/releases/tag/v0.1.0
@@ -1,10 +1,27 @@
1
1
  /**
2
2
  * Shared graph predicates for structural validation and architecture lint (no imports from lint-graph / ir-structural).
3
3
  */
4
- /** Node types treated as HTTP-like for path/method checks and lint (aligned structural + IR-LINT). */
4
+ /**
5
+ * Narrow predicate: node types that carry a single HTTP endpoint (`config.url` + HTTP method).
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
8
+ * (upstream routing, proto service/method, multi-route aggregation) and must not be required
9
+ * to supply a REST-style url + HTTP method.
10
+ */
11
+ export declare function isHttpEndpointType(t: string): boolean;
12
+ /**
13
+ * Broad predicate: all HTTP-like node types for lint purposes (healthcheck detection,
14
+ * sync-chain analysis, missing-name checks, multiple-entry detection).
15
+ * Superset of `isHttpEndpointType`.
16
+ */
5
17
  export declare function isHttpLikeType(t: string): boolean;
6
18
  /** Datastore-like (unchanged semantics; kept adjacent for docs). */
7
19
  export declare function isDbLikeType(t: string): boolean;
20
+ /**
21
+ * Auth-like node types: dedicated identity/auth/middleware nodes.
22
+ * Used by IR-LINT-MISSING-AUTH-010 to detect HTTP entry nodes with no auth coverage.
23
+ */
24
+ export declare function isAuthLikeNodeType(t: string): boolean;
8
25
  /** Queue / topic / stream node types → treat incoming edges as async boundaries when edge metadata is absent. */
9
26
  export declare function isQueueLikeNodeType(t: string): boolean;
10
27
  //# sourceMappingURL=graphPredicates.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"graphPredicates.d.ts","sourceRoot":"","sources":["../src/graphPredicates.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,sGAAsG;AACtG,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAQjD;AAED,oEAAoE;AACpE,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAM/C;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;;;;;;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,16 +1,35 @@
1
1
  /**
2
2
  * Shared graph predicates for structural validation and architecture lint (no imports from lint-graph / ir-structural).
3
3
  */
4
- /** Node types treated as HTTP-like for path/method checks and lint (aligned structural + IR-LINT). */
4
+ /**
5
+ * Narrow predicate: node types that carry a single HTTP endpoint (`config.url` + HTTP method).
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
8
+ * (upstream routing, proto service/method, multi-route aggregation) and must not be required
9
+ * to supply a REST-style url + HTTP method.
10
+ */
11
+ export function isHttpEndpointType(t) {
12
+ const s = String(t ?? '')
13
+ .trim()
14
+ .toLowerCase();
15
+ if (!s)
16
+ return false;
17
+ return s === 'http' || s === 'https' || s === 'rest' || s === 'api' || s === 'graphql';
18
+ }
19
+ /**
20
+ * Broad predicate: all HTTP-like node types for lint purposes (healthcheck detection,
21
+ * sync-chain analysis, missing-name checks, multiple-entry detection).
22
+ * Superset of `isHttpEndpointType`.
23
+ */
5
24
  export function isHttpLikeType(t) {
6
25
  const s = String(t ?? '')
7
26
  .trim()
8
27
  .toLowerCase();
9
28
  if (!s)
10
29
  return false;
11
- if (s === 'http' || s === 'https' || s === 'rest' || s === 'api')
30
+ if (isHttpEndpointType(s))
12
31
  return true;
13
- if (s === 'gateway' || s === 'bff' || s === 'graphql' || s === 'grpc')
32
+ if (s === 'gateway' || s === 'bff' || s === 'grpc')
14
33
  return true;
15
34
  return /\b(api|gateway|bff|graphql|grpc)\b/.test(s);
16
35
  }
@@ -21,6 +40,19 @@ export function isDbLikeType(t) {
21
40
  return (/\b(db|database|datastore)\b/.test(t) ||
22
41
  /postgres|mongodb|mysql|sqlite|redis|cassandra|dynamo|sql|nosql|warehouse|s3/.test(t));
23
42
  }
43
+ /**
44
+ * Auth-like node types: dedicated identity/auth/middleware nodes.
45
+ * Used by IR-LINT-MISSING-AUTH-010 to detect HTTP entry nodes with no auth coverage.
46
+ */
47
+ export function isAuthLikeNodeType(t) {
48
+ const s = String(t ?? '')
49
+ .trim()
50
+ .toLowerCase();
51
+ if (!s)
52
+ return false;
53
+ return (/\b(auth|authentication|authorization|middleware|security|iam|idp|identity)\b/.test(s) ||
54
+ /oauth|jwt|saml|keycloak|okta|cognito|auth0|ldap|sso/.test(s));
55
+ }
24
56
  /** Queue / topic / stream node types → treat incoming edges as async boundaries when edge metadata is absent. */
25
57
  export function isQueueLikeNodeType(t) {
26
58
  const s = String(t ?? '')
package/dist/index.d.ts CHANGED
@@ -10,12 +10,12 @@ export { default as generatePythonFastAPIFiles } from './pythonFastAPI.js';
10
10
  export { default as generateNodeExpressFiles } from './nodeExpress.js';
11
11
  export { runDeterministicExport, type DeterministicExportResult } from './exportPipeline.js';
12
12
  export { diffExpectedExportAgainstFiles, diffExpectedExportAgainstDirectory, readDirectoryAsExportMap, runValidateDrift, runDriftCheckAgainstFiles, normalizeExportFileContent, type DriftFinding, type DriftCode, type ValidateDriftResult, type DriftCheckFilesResult, } from './validate-drift.js';
13
- export { normalizeIrGraph, validateIrStructural, hasIrStructuralErrors, type IrStructuralFinding, type IrStructuralSeverity, type IrFindingLayer, } from './ir-structural.js';
13
+ export { normalizeIrGraph, validateIrStructural, hasIrStructuralErrors, detectCycles, type IrStructuralFinding, type IrStructuralSeverity, type IrFindingLayer, } from './ir-structural.js';
14
14
  export { materializeNormalizedGraph, normalizeNodeSlot, normalizeEdgeSlot, type NormalizedGraph, type NormalizedNode, type NormalizedEdge, type MaterializeResult, } from './ir-normalize.js';
15
15
  export { validateIrLint } from './ir-lint.js';
16
16
  export { runArchitectureLinting, LINT_RULE_REGISTRY } from './lint-rules.js';
17
17
  export { buildParsedLintGraph, isParsedLintGraph, type ParsedLintGraph, type BuildParsedLintGraphResult, } from './lint-graph.js';
18
- export { isHttpLikeType, isDbLikeType, isQueueLikeNodeType } from './graphPredicates.js';
18
+ export { isHttpLikeType, isHttpEndpointType, isDbLikeType, isQueueLikeNodeType, isAuthLikeNodeType } from './graphPredicates.js';
19
19
  export { sortFindings, shouldFailFromFindings, type ValidationExitPolicy } from './cli-findings.js';
20
20
  export { parseYamlToCanonicalIr, canonicalIrToJsonString, YamlGraphParseError, } from './yamlToIr.js';
21
21
  export { openApiDocumentToHttpNodes, openApiDocumentToCanonicalIr, openApiStringToCanonicalIr, openApiUnknownToCanonicalIr, OpenApiIngestError, type OpenApiHttpNode, } from './openapi-to-ir.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,yBAAyB,EACzB,mBAAmB,EACnB,iCAAiC,GAClC,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EACL,uBAAuB,EACvB,2BAA2B,EAC3B,mBAAmB,EACnB,yBAAyB,EACzB,uBAAuB,EACvB,KAAK,WAAW,EAChB,KAAK,kBAAkB,GACxB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,wBAAwB,EACxB,uBAAuB,EACvB,mBAAmB,GACpB,MAAM,eAAe,CAAC;AAEvB,cAAc,8BAA8B,CAAC;AAE7C,OAAO,EAAE,OAAO,IAAI,0BAA0B,EAAE,MAAM,oBAAoB,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,wBAAwB,EAAE,MAAM,kBAAkB,CAAC;AAEvE,OAAO,EAAE,sBAAsB,EAAE,KAAK,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AAE7F,OAAO,EACL,8BAA8B,EAC9B,kCAAkC,EAClC,wBAAwB,EACxB,gBAAgB,EAChB,yBAAyB,EACzB,0BAA0B,EAC1B,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,GAC3B,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,qBAAqB,EACrB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,cAAc,GACpB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,0BAA0B,EAC1B,iBAAiB,EACjB,iBAAiB,EACjB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,iBAAiB,GACvB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC7E,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,KAAK,eAAe,EACpB,KAAK,0BAA0B,GAChC,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEzF,OAAO,EAAE,YAAY,EAAE,sBAAsB,EAAE,KAAK,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAEpG,OAAO,EACL,sBAAsB,EACtB,uBAAuB,EACvB,mBAAmB,GACpB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,0BAA0B,EAC1B,4BAA4B,EAC5B,0BAA0B,EAC1B,2BAA2B,EAC3B,kBAAkB,EAClB,KAAK,eAAe,GACrB,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,yBAAyB,EACzB,mBAAmB,EACnB,iCAAiC,GAClC,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EACL,uBAAuB,EACvB,2BAA2B,EAC3B,mBAAmB,EACnB,yBAAyB,EACzB,uBAAuB,EACvB,KAAK,WAAW,EAChB,KAAK,kBAAkB,GACxB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,wBAAwB,EACxB,uBAAuB,EACvB,mBAAmB,GACpB,MAAM,eAAe,CAAC;AAEvB,cAAc,8BAA8B,CAAC;AAE7C,OAAO,EAAE,OAAO,IAAI,0BAA0B,EAAE,MAAM,oBAAoB,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,wBAAwB,EAAE,MAAM,kBAAkB,CAAC;AAEvE,OAAO,EAAE,sBAAsB,EAAE,KAAK,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AAE7F,OAAO,EACL,8BAA8B,EAC9B,kCAAkC,EAClC,wBAAwB,EACxB,gBAAgB,EAChB,yBAAyB,EACzB,0BAA0B,EAC1B,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,GAC3B,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,qBAAqB,EACrB,YAAY,EACZ,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,cAAc,GACpB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,0BAA0B,EAC1B,iBAAiB,EACjB,iBAAiB,EACjB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,iBAAiB,GACvB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC7E,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,KAAK,eAAe,EACpB,KAAK,0BAA0B,GAChC,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,YAAY,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAEjI,OAAO,EAAE,YAAY,EAAE,sBAAsB,EAAE,KAAK,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAEpG,OAAO,EACL,sBAAsB,EACtB,uBAAuB,EACvB,mBAAmB,GACpB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,0BAA0B,EAC1B,4BAA4B,EAC5B,0BAA0B,EAC1B,2BAA2B,EAC3B,kBAAkB,EAClB,KAAK,eAAe,GACrB,MAAM,oBAAoB,CAAC"}
package/dist/index.js CHANGED
@@ -10,12 +10,12 @@ export { default as generatePythonFastAPIFiles } from './pythonFastAPI.js';
10
10
  export { default as generateNodeExpressFiles } from './nodeExpress.js';
11
11
  export { runDeterministicExport } from './exportPipeline.js';
12
12
  export { diffExpectedExportAgainstFiles, diffExpectedExportAgainstDirectory, readDirectoryAsExportMap, runValidateDrift, runDriftCheckAgainstFiles, normalizeExportFileContent, } from './validate-drift.js';
13
- export { normalizeIrGraph, validateIrStructural, hasIrStructuralErrors, } from './ir-structural.js';
13
+ export { normalizeIrGraph, validateIrStructural, hasIrStructuralErrors, detectCycles, } from './ir-structural.js';
14
14
  export { materializeNormalizedGraph, normalizeNodeSlot, normalizeEdgeSlot, } from './ir-normalize.js';
15
15
  export { validateIrLint } from './ir-lint.js';
16
16
  export { runArchitectureLinting, LINT_RULE_REGISTRY } from './lint-rules.js';
17
17
  export { buildParsedLintGraph, isParsedLintGraph, } from './lint-graph.js';
18
- export { isHttpLikeType, isDbLikeType, isQueueLikeNodeType } from './graphPredicates.js';
18
+ export { isHttpLikeType, isHttpEndpointType, isDbLikeType, isQueueLikeNodeType, isAuthLikeNodeType } from './graphPredicates.js';
19
19
  export { sortFindings, shouldFailFromFindings } from './cli-findings.js';
20
20
  export { parseYamlToCanonicalIr, canonicalIrToJsonString, YamlGraphParseError, } from './yamlToIr.js';
21
21
  export { openApiDocumentToHttpNodes, openApiDocumentToCanonicalIr, openApiStringToCanonicalIr, openApiUnknownToCanonicalIr, OpenApiIngestError, } from './openapi-to-ir.js';
@@ -31,6 +31,11 @@ export declare function normalizeIrGraph(ir: unknown): {
31
31
  } | {
32
32
  findings: IrStructuralFinding[];
33
33
  };
34
+ /**
35
+ * DFS cycle detector. Returns the cycle as an ordered node-id array (the repeated node is first)
36
+ * or `null` if the graph is acyclic. Extracted for testability and to avoid closure over mutable state.
37
+ */
38
+ export declare function detectCycles(adj: Map<string, string[]>): string[] | null;
34
39
  /**
35
40
  * Structural validation only: well-formed graph, edge references, directed cycles, HTTP node config.
36
41
  * Same input → same findings (deterministic).
@@ -1 +1 @@
1
- {"version":3,"file":"ir-structural.d.ts","sourceRoot":"","sources":["../src/ir-structural.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,MAAM,MAAM,oBAAoB,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAEhE,uFAAuF;AACvF,MAAM,MAAM,cAAc,GAAG,YAAY,GAAG,MAAM,CAAC;AAEnD,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,oCAAoC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6BAA6B;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qGAAqG;IACrG,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iCAAiC;IACjC,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,+CAA+C;IAC/C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wCAAwC;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAIF;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,OAAO,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GAAG;IAAE,QAAQ,EAAE,mBAAmB,EAAE,CAAA;CAAE,CA8BtH;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,OAAO,GAAG,mBAAmB,EAAE,CAgOvE;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,mBAAmB,EAAE,GAAG,OAAO,CAE9E"}
1
+ {"version":3,"file":"ir-structural.d.ts","sourceRoot":"","sources":["../src/ir-structural.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,MAAM,MAAM,oBAAoB,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAEhE,uFAAuF;AACvF,MAAM,MAAM,cAAc,GAAG,YAAY,GAAG,MAAM,CAAC;AAEnD,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,oCAAoC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6BAA6B;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qGAAqG;IACrG,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iCAAiC;IACjC,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,+CAA+C;IAC/C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wCAAwC;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAIF;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,OAAO,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GAAG;IAAE,QAAQ,EAAE,mBAAmB,EAAE,CAAA;CAAE,CA8BtH;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,MAAM,EAAE,GAAG,IAAI,CA2BxE;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,OAAO,GAAG,mBAAmB,EAAE,CAkNvE;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,mBAAmB,EAAE,GAAG,OAAO,CAE9E"}
@@ -2,7 +2,7 @@
2
2
  * Deterministic structural validation of blueprint IR (graph JSON).
3
3
  * OSS boundary: shape, references, cycles — not security/compliance semantics (ArchRad Cloud).
4
4
  */
5
- import { isHttpLikeType } from './graphPredicates.js';
5
+ import { isHttpEndpointType } from './graphPredicates.js';
6
6
  import { materializeNormalizedGraph } from './ir-normalize.js';
7
7
  const HTTP_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']);
8
8
  /**
@@ -40,6 +40,40 @@ export function normalizeIrGraph(ir) {
40
40
  ],
41
41
  };
42
42
  }
43
+ /**
44
+ * DFS cycle detector. Returns the cycle as an ordered node-id array (the repeated node is first)
45
+ * or `null` if the graph is acyclic. Extracted for testability and to avoid closure over mutable state.
46
+ */
47
+ export function detectCycles(adj) {
48
+ const visiting = new Map(); // node → index in path when first entered
49
+ const path = [];
50
+ const done = new Set();
51
+ function dfs(u) {
52
+ if (visiting.has(u))
53
+ return path.slice(visiting.get(u));
54
+ if (done.has(u))
55
+ return null;
56
+ visiting.set(u, path.length);
57
+ path.push(u);
58
+ for (const v of adj.get(u) ?? []) {
59
+ const cycle = dfs(v);
60
+ if (cycle)
61
+ return cycle;
62
+ }
63
+ path.pop();
64
+ visiting.delete(u);
65
+ done.add(u);
66
+ return null;
67
+ }
68
+ for (const id of adj.keys()) {
69
+ if (!done.has(id)) {
70
+ const cycle = dfs(id);
71
+ if (cycle)
72
+ return cycle;
73
+ }
74
+ }
75
+ return null;
76
+ }
43
77
  /**
44
78
  * Structural validation only: well-formed graph, edge references, directed cycles, HTTP node config.
45
79
  * Same input → same findings (deterministic).
@@ -118,7 +152,21 @@ export function validateIrStructural(ir) {
118
152
  else {
119
153
  seenIds.add(id);
120
154
  }
121
- if (isHttpLikeType(nn.type)) {
155
+ const rawCfg = n.config;
156
+ if (rawCfg !== undefined) {
157
+ const cfgInvalid = rawCfg === null || Array.isArray(rawCfg) || typeof rawCfg !== 'object';
158
+ if (cfgInvalid) {
159
+ const got = rawCfg === null ? 'null' : Array.isArray(rawCfg) ? 'array' : typeof rawCfg;
160
+ findings.push({
161
+ code: 'IR-STRUCT-NODE_INVALID_CONFIG',
162
+ severity: 'warning',
163
+ message: `Node "${id}" has a non-object \`config\` (got ${got}); treated as {}`,
164
+ nodeId: id,
165
+ fixHint: 'Set `config` to a plain object, e.g. { "url": "/foo", "method": "GET" }.',
166
+ });
167
+ }
168
+ }
169
+ if (isHttpEndpointType(nn.type)) {
122
170
  const cfg = nn.config;
123
171
  /** Generators accept `route` or `url`; structural checks align so OpenAPI merge + ingest both validate. */
124
172
  const url = String(cfg.url ?? cfg.route ?? '').trim();
@@ -128,7 +176,7 @@ export function validateIrStructural(ir) {
128
176
  findings.push({
129
177
  code: 'IR-STRUCT-HTTP_PATH',
130
178
  severity: 'error',
131
- message: `HTTP-like node "${id}" has invalid path: config.url must be a non-empty string starting with /`,
179
+ message: `HTTP endpoint node "${id}" has invalid path: config.url must be a non-empty string starting with /`,
132
180
  nodeId: id,
133
181
  fixHint: 'Set config.url (or config.route) to e.g. "/signup".',
134
182
  });
@@ -138,7 +186,7 @@ export function validateIrStructural(ir) {
138
186
  findings.push({
139
187
  code: 'IR-STRUCT-HTTP_METHOD',
140
188
  severity: 'error',
141
- message: `HTTP-like node "${id}" has unsupported method "${method}"`,
189
+ message: `HTTP endpoint node "${id}" has unsupported method "${method}"`,
142
190
  nodeId: id,
143
191
  fixHint: 'Use GET, POST, PUT, PATCH, DELETE, HEAD, or OPTIONS.',
144
192
  });
@@ -220,43 +268,15 @@ export function validateIrStructural(ir) {
220
268
  adj.set(from, []);
221
269
  adj.get(from).push(to);
222
270
  }
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';
271
+ const cyclePath = detectCycles(adj);
272
+ if (cyclePath !== null) {
273
+ const nodeId = cyclePath[0];
274
+ const pathStr = [...cyclePath, cyclePath[0]].join(' → ');
255
275
  findings.push({
256
276
  code: 'IR-STRUCT-CYCLE',
257
277
  severity: 'error',
258
- message: hint,
259
- nodeId: cycleExampleNode,
278
+ message: `Directed cycle detected: ${pathStr}`,
279
+ nodeId,
260
280
  fixHint: 'Remove or break cyclic edges unless your tooling explicitly allows execution loops.',
261
281
  });
262
282
  }
@@ -31,6 +31,28 @@ export declare function ruleHttpMissingName(g: ParsedLintGraph): IrStructuralFin
31
31
  export declare function ruleDatastoreNoIncoming(g: ParsedLintGraph): IrStructuralFinding[];
32
32
  /** IR-LINT-MULTIPLE-HTTP-ENTRIES-009 — more than one HTTP node with no incoming edges (multiple public entry surfaces). */
33
33
  export declare function ruleMultipleHttpEntries(g: ParsedLintGraph): IrStructuralFinding[];
34
+ /**
35
+ * IR-LINT-MISSING-AUTH-010 — HTTP entry node (no incoming sync edges) with no auth coverage.
36
+ *
37
+ * A node is considered auth-covered when ANY of:
38
+ * 1. One of its immediate outgoing neighbours is an auth-like node type.
39
+ * 2. An auth-like node has a direct edge TO it (auth-as-gateway pattern).
40
+ * 3. Its own `config` carries an auth-signal key: `auth`, `authRequired`,
41
+ * `authentication`, `authorization`, or `security`.
42
+ *
43
+ * Escape hatch: set `config.authRequired: false` (explicit opt-out) to silence the rule
44
+ * for intentionally public endpoints (health, public assets, etc.).
45
+ */
46
+ export declare function ruleHttpMissingAuth(g: ParsedLintGraph): IrStructuralFinding[];
47
+ /**
48
+ * IR-LINT-DEAD-NODE-011 — non-sink node with incoming edges but no outgoing edges.
49
+ *
50
+ * Datastore-like and queue-like nodes are valid sinks and are excluded.
51
+ * Nodes with no incident edges at all are already caught by IR-LINT-ISOLATED-NODE-005.
52
+ * This rule targets nodes that receive data but forward it nowhere — likely a missing
53
+ * edge, an incomplete integration step, or a stale component.
54
+ */
55
+ export declare function ruleDeadNode(g: ParsedLintGraph): IrStructuralFinding[];
34
56
  /**
35
57
  * Ordered registry: add a new rule by implementing `(g) => findings` and appending here.
36
58
  */
@@ -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;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"}
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"}
@@ -3,6 +3,7 @@
3
3
  * and returns IrStructuralFinding[] (layer: lint). Deterministic, no AI, no cloud.
4
4
  */
5
5
  import { edgeEndpoints, nodeType, isDbLikeType, isHttpLikeType, looksLikeHealthUrl, buildSyncAdjacencyForLint, edgeRepresentsAsyncBoundary, } from './lint-graph.js';
6
+ import { isAuthLikeNodeType, isQueueLikeNodeType } from './graphPredicates.js';
6
7
  const LAYER = 'lint';
7
8
  /** IR-LINT-DIRECT-DB-ACCESS-002 — HTTP-like → datastore-like in one hop (broader than strict api/gateway + database enum). */
8
9
  export function ruleDirectDbAccess(g) {
@@ -270,6 +271,108 @@ export function ruleMultipleHttpEntries(g) {
270
271
  },
271
272
  ];
272
273
  }
274
+ /**
275
+ * IR-LINT-MISSING-AUTH-010 — HTTP entry node (no incoming sync edges) with no auth coverage.
276
+ *
277
+ * A node is considered auth-covered when ANY of:
278
+ * 1. One of its immediate outgoing neighbours is an auth-like node type.
279
+ * 2. An auth-like node has a direct edge TO it (auth-as-gateway pattern).
280
+ * 3. Its own `config` carries an auth-signal key: `auth`, `authRequired`,
281
+ * `authentication`, `authorization`, or `security`.
282
+ *
283
+ * Escape hatch: set `config.authRequired: false` (explicit opt-out) to silence the rule
284
+ * for intentionally public endpoints (health, public assets, etc.).
285
+ */
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
298
+ const reverseAdj = new Map();
299
+ for (const e of edges) {
300
+ if (!e || typeof e !== 'object')
301
+ continue;
302
+ const { from, to } = edgeEndpoints(e);
303
+ if (!from || !to)
304
+ continue;
305
+ if (!reverseAdj.has(to))
306
+ reverseAdj.set(to, []);
307
+ reverseAdj.get(to).push(from);
308
+ }
309
+ const findings = [];
310
+ for (const [id, n] of nodeById) {
311
+ if (!isHttpLikeType(nodeType(n)))
312
+ continue;
313
+ if (hasIncomingEdge.has(id))
314
+ continue; // not an entry node
315
+ const cfg = (n.config ?? {});
316
+ // Explicit opt-out: config.authRequired === false marks an intentionally public endpoint
317
+ if (cfg.authRequired === false)
318
+ continue;
319
+ // Coverage check 1: outgoing neighbour is auth-like
320
+ const outNeighbours = adj.get(id) ?? [];
321
+ if (outNeighbours.some((v) => isAuthLikeNodeType(nodeType(nodeById.get(v) ?? {}))))
322
+ continue;
323
+ // Coverage check 2: an auth-like node points directly to this entry node
324
+ const inNeighbours = reverseAdj.get(id) ?? [];
325
+ if (inNeighbours.some((v) => isAuthLikeNodeType(nodeType(nodeById.get(v) ?? {}))))
326
+ continue;
327
+ // Coverage check 3: node config carries an auth signal
328
+ const authConfigKeys = ['auth', 'authrequired', 'authentication', 'authorization', 'security'];
329
+ const cfgKeys = Object.keys(cfg).map((k) => k.toLowerCase());
330
+ if (authConfigKeys.some((k) => cfgKeys.includes(k)))
331
+ continue;
332
+ findings.push({
333
+ code: 'IR-LINT-MISSING-AUTH-010',
334
+ severity: 'warning',
335
+ layer: LAYER,
336
+ message: `HTTP entry node "${id}" has no auth node or auth config in its immediate graph neighbourhood`,
337
+ nodeId: id,
338
+ fixHint: 'Add an auth/middleware node with an edge to or from this entry, or set config.authRequired: false for intentionally public endpoints.',
339
+ suggestion: 'Connect an auth, oauth, jwt, or middleware node. For PCI-DSS / HIPAA systems, every HTTP entry must have a documented auth boundary.',
340
+ impact: 'Unauthenticated HTTP entry points are a compliance gap in regulated environments and a common attack surface.',
341
+ });
342
+ }
343
+ return findings;
344
+ }
345
+ /**
346
+ * IR-LINT-DEAD-NODE-011 — non-sink node with incoming edges but no outgoing edges.
347
+ *
348
+ * Datastore-like and queue-like nodes are valid sinks and are excluded.
349
+ * Nodes with no incident edges at all are already caught by IR-LINT-ISOLATED-NODE-005.
350
+ * This rule targets nodes that receive data but forward it nowhere — likely a missing
351
+ * edge, an incomplete integration step, or a stale component.
352
+ */
353
+ export function ruleDeadNode(g) {
354
+ const findings = [];
355
+ for (const [id, n] of g.nodeById) {
356
+ const out = g.outDegree.get(id) ?? 0;
357
+ const inn = g.inDegree.get(id) ?? 0;
358
+ if (out > 0 || inn === 0)
359
+ continue; // has outgoing, or truly isolated (caught elsewhere)
360
+ const t = nodeType(n);
361
+ if (isDbLikeType(t) || isQueueLikeNodeType(t) || isHttpLikeType(t))
362
+ continue; // valid sinks
363
+ findings.push({
364
+ code: 'IR-LINT-DEAD-NODE-011',
365
+ severity: 'warning',
366
+ layer: LAYER,
367
+ message: `Node "${id}" (type: ${t || 'unknown'}) receives edges but has no outgoing edges — possible missing integration or dead component`,
368
+ nodeId: id,
369
+ fixHint: 'Add an outgoing edge to a downstream node, or remove this node if it is no longer active.',
370
+ suggestion: 'Dead-end non-sink nodes often represent incomplete migrations, dropped integrations, or copy-paste errors in the IR.',
371
+ impact: 'Data entering this node has no documented path forward, which misrepresents runtime behaviour.',
372
+ });
373
+ }
374
+ return findings;
375
+ }
273
376
  /**
274
377
  * Ordered registry: add a new rule by implementing `(g) => findings` and appending here.
275
378
  */
@@ -283,6 +386,8 @@ export const LINT_RULE_REGISTRY = [
283
386
  ruleHttpMissingName,
284
387
  ruleDatastoreNoIncoming,
285
388
  ruleMultipleHttpEntries,
389
+ ruleHttpMissingAuth,
390
+ ruleDeadNode,
286
391
  ];
287
392
  /** Run all registered architecture lint visitors (same as legacy `validateIrLint` behavior). */
288
393
  export function runArchitectureLinting(g) {
@@ -1 +1 @@
1
- {"version":3,"file":"openapi-to-ir.d.ts","sourceRoot":"","sources":["../src/openapi-to-ir.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,OAAO,EAAE,MAAM;CAI5B;AAuBD,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC,CAAC;AAEF;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,eAAe,EAAE,CAgD1F;AAaD;;GAEG;AACH,wBAAgB,4BAA4B,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAgClG;AAED,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAMnF;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAQnF"}
1
+ {"version":3,"file":"openapi-to-ir.d.ts","sourceRoot":"","sources":["../src/openapi-to-ir.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,OAAO,EAAE,MAAM;CAI5B;AAuBD,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC,CAAC;AAEF;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,eAAe,EAAE,CAqD1F;AAyDD;;GAEG;AACH,wBAAgB,4BAA4B,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAgClG;AAED,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAMnF;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAQnF"}
@@ -35,6 +35,7 @@ export function openApiDocumentToHttpNodes(doc) {
35
35
  if (!paths || typeof paths !== 'object' || Array.isArray(paths)) {
36
36
  return [];
37
37
  }
38
+ const globalSecurity = doc.security;
38
39
  const nodes = [];
39
40
  const usedIds = new Set();
40
41
  for (const [pathKey, pathItem] of Object.entries(paths)) {
@@ -45,17 +46,19 @@ export function openApiDocumentToHttpNodes(doc) {
45
46
  const op = pathItem[m];
46
47
  if (!op || typeof op !== 'object' || Array.isArray(op))
47
48
  continue;
48
- const summary = typeof op.summary === 'string'
49
- ? String(op.summary)
50
- : typeof op.operationId === 'string'
51
- ? String(op.operationId)
49
+ const opRec = op;
50
+ const summary = typeof opRec.summary === 'string'
51
+ ? String(opRec.summary)
52
+ : typeof opRec.operationId === 'string'
53
+ ? String(opRec.operationId)
52
54
  : '';
53
55
  let id = safeNodeId(url, m);
54
56
  while (usedIds.has(id)) {
55
57
  id = `${id}_${Math.random().toString(36).slice(2, 7)}`;
56
58
  }
57
59
  usedIds.add(id);
58
- const operationId = op.operationId;
60
+ const operationId = opRec.operationId;
61
+ const securityConfig = resolveOperationSecurity(opRec, globalSecurity);
59
62
  nodes.push({
60
63
  id,
61
64
  type: 'http',
@@ -67,12 +70,53 @@ export function openApiDocumentToHttpNodes(doc) {
67
70
  method: m.toUpperCase(),
68
71
  openApiIngest: true,
69
72
  ...(typeof operationId === 'string' && operationId.trim() ? { operationId } : {}),
73
+ ...securityConfig,
70
74
  },
71
75
  });
72
76
  }
73
77
  }
74
78
  return nodes;
75
79
  }
80
+ /**
81
+ * Extract unique scheme names from a security requirement array.
82
+ * Each entry is an object whose keys are scheme names, e.g. `[{ "BearerAuth": [] }]`.
83
+ * Returns sorted names for determinism.
84
+ */
85
+ function extractSecuritySchemeNames(securityArray) {
86
+ if (!Array.isArray(securityArray))
87
+ return [];
88
+ const names = new Set();
89
+ for (const req of securityArray) {
90
+ if (req && typeof req === 'object' && !Array.isArray(req)) {
91
+ for (const name of Object.keys(req)) {
92
+ if (name.trim())
93
+ names.add(name.trim());
94
+ }
95
+ }
96
+ }
97
+ return [...names].sort();
98
+ }
99
+ /**
100
+ * Resolve effective security for a single operation, respecting OpenAPI 3.x precedence:
101
+ * operation-level `security` overrides the global spec-level `security`.
102
+ * An explicit empty array `[]` means intentionally no auth (public endpoint).
103
+ *
104
+ * Returns:
105
+ * - `{ authRequired: false }` when the effective security is explicitly empty `[]`
106
+ * - `{ security: string[] }` when scheme names are present
107
+ * - `{}` when no security is declared at either level
108
+ */
109
+ function resolveOperationSecurity(op, globalSecurity) {
110
+ const hasOperationSecurity = 'security' in op;
111
+ const effective = hasOperationSecurity ? op.security : globalSecurity;
112
+ if (!Array.isArray(effective))
113
+ return {};
114
+ // Explicit empty array → intentionally public
115
+ if (effective.length === 0)
116
+ return { authRequired: false };
117
+ const names = extractSecuritySchemeNames(effective);
118
+ return names.length > 0 ? { security: names } : {};
119
+ }
76
120
  function provenanceBlock(doc) {
77
121
  const ver = doc.openapi != null ? String(doc.openapi) : doc.swagger != null ? String(doc.swagger) : '';
78
122
  const info = (doc.info && typeof doc.info === 'object' ? doc.info : {});
@@ -9,7 +9,7 @@
9
9
  "id": "orders-api",
10
10
  "type": "http",
11
11
  "name": "Orders API",
12
- "config": { "url": "/orders", "method": "GET" }
12
+ "config": { "url": "/orders", "method": "GET", "auth": "bearer" }
13
13
  },
14
14
  {
15
15
  "id": "health",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archrad/deterministic",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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",