@archrad/deterministic 0.1.1 → 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 +33 -2
- package/README.md +2 -0
- package/dist/cli.js +32 -19
- package/dist/graphPredicates.d.ts +19 -1
- package/dist/graphPredicates.d.ts.map +1 -1
- package/dist/graphPredicates.js +36 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/ir-structural.d.ts +5 -0
- package/dist/ir-structural.d.ts.map +1 -1
- package/dist/ir-structural.js +58 -38
- package/dist/lint-rules.d.ts +22 -0
- package/dist/lint-rules.d.ts.map +1 -1
- package/dist/lint-rules.js +97 -0
- package/dist/openapi-to-ir.d.ts.map +1 -1
- package/dist/openapi-to-ir.js +49 -5
- package/fixtures/demo-direct-db-layered.json +1 -1
- package/fixtures/e2e-no-security-openapi.yaml +11 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,10 +7,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
-
|
|
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`**.
|
|
11
15
|
|
|
12
16
|
### Changed
|
|
13
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
|
+
|
|
27
|
+
## [0.1.2] - 2026-03-28
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- **`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.
|
|
32
|
+
- **`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.
|
|
33
|
+
- **`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.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- **`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.
|
|
38
|
+
- **`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.
|
|
39
|
+
- **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.
|
|
40
|
+
- **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.
|
|
41
|
+
- **`detectCycles` exported from package root** — useful for consumers building custom structural validators or tooling on top of the IR adjacency graph.
|
|
42
|
+
|
|
14
43
|
## [0.1.1] - 2026-03-28
|
|
15
44
|
|
|
16
45
|
### Added
|
|
@@ -59,6 +88,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
59
88
|
- Documented **codegen vs validation** for retry/timeout IR fields and **InkByte vs OSS** scope in README and structural/semantic doc.
|
|
60
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.
|
|
61
90
|
|
|
62
|
-
[Unreleased]: https://github.com/archradhq/arch-deterministic/compare/v0.1.
|
|
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
|
|
93
|
+
[0.1.2]: https://github.com/archradhq/arch-deterministic/releases/tag/v0.1.2
|
|
63
94
|
[0.1.1]: https://github.com/archradhq/arch-deterministic/releases/tag/v0.1.1
|
|
64
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
|
-
|
|
52
|
-
|
|
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
|
|
186
|
-
|
|
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
|
-
|
|
270
|
-
|
|
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) {
|
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared graph predicates for structural validation and architecture lint (no imports from lint-graph / ir-structural).
|
|
3
3
|
*/
|
|
4
|
-
/**
|
|
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
|
+
* **`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
|
|
9
|
+
* (upstream routing, proto service/method, multi-route aggregation) and must not be required
|
|
10
|
+
* to supply a REST-style `url` + HTTP method; they remain **`isHttpLikeType`** for lint (entries, health, sync chain, etc.).
|
|
11
|
+
*/
|
|
12
|
+
export declare function isHttpEndpointType(t: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Broad predicate: all HTTP-like node types for lint purposes (healthcheck detection,
|
|
15
|
+
* sync-chain analysis, missing-name checks, multiple-entry detection).
|
|
16
|
+
* Superset of `isHttpEndpointType`.
|
|
17
|
+
*/
|
|
5
18
|
export declare function isHttpLikeType(t: string): boolean;
|
|
6
19
|
/** Datastore-like (unchanged semantics; kept adjacent for docs). */
|
|
7
20
|
export declare function isDbLikeType(t: string): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Auth-like node types: dedicated identity/auth/middleware nodes.
|
|
23
|
+
* Used by IR-LINT-MISSING-AUTH-010 to detect HTTP entry nodes with no auth coverage.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isAuthLikeNodeType(t: string): boolean;
|
|
8
26
|
/** Queue / topic / stream node types → treat incoming edges as async boundaries when edge metadata is absent. */
|
|
9
27
|
export declare function isQueueLikeNodeType(t: string): boolean;
|
|
10
28
|
//# sourceMappingURL=graphPredicates.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graphPredicates.d.ts","sourceRoot":"","sources":["../src/graphPredicates.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,
|
|
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"}
|
package/dist/graphPredicates.js
CHANGED
|
@@ -1,16 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared graph predicates for structural validation and architecture lint (no imports from lint-graph / ir-structural).
|
|
3
3
|
*/
|
|
4
|
-
/**
|
|
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
|
+
* **`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
|
|
9
|
+
* (upstream routing, proto service/method, multi-route aggregation) and must not be required
|
|
10
|
+
* to supply a REST-style `url` + HTTP method; they remain **`isHttpLikeType`** for lint (entries, health, sync chain, etc.).
|
|
11
|
+
*/
|
|
12
|
+
export function isHttpEndpointType(t) {
|
|
13
|
+
const s = String(t ?? '')
|
|
14
|
+
.trim()
|
|
15
|
+
.toLowerCase();
|
|
16
|
+
if (!s)
|
|
17
|
+
return false;
|
|
18
|
+
return s === 'http' || s === 'https' || s === 'rest' || s === 'api' || s === 'graphql';
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Broad predicate: all HTTP-like node types for lint purposes (healthcheck detection,
|
|
22
|
+
* sync-chain analysis, missing-name checks, multiple-entry detection).
|
|
23
|
+
* Superset of `isHttpEndpointType`.
|
|
24
|
+
*/
|
|
5
25
|
export function isHttpLikeType(t) {
|
|
6
26
|
const s = String(t ?? '')
|
|
7
27
|
.trim()
|
|
8
28
|
.toLowerCase();
|
|
9
29
|
if (!s)
|
|
10
30
|
return false;
|
|
11
|
-
if (s
|
|
31
|
+
if (isHttpEndpointType(s))
|
|
12
32
|
return true;
|
|
13
|
-
if (s === 'gateway' || s === 'bff' || s === '
|
|
33
|
+
if (s === 'gateway' || s === 'bff' || s === 'grpc')
|
|
14
34
|
return true;
|
|
15
35
|
return /\b(api|gateway|bff|graphql|grpc)\b/.test(s);
|
|
16
36
|
}
|
|
@@ -21,6 +41,19 @@ export function isDbLikeType(t) {
|
|
|
21
41
|
return (/\b(db|database|datastore)\b/.test(t) ||
|
|
22
42
|
/postgres|mongodb|mysql|sqlite|redis|cassandra|dynamo|sql|nosql|warehouse|s3/.test(t));
|
|
23
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Auth-like node types: dedicated identity/auth/middleware nodes.
|
|
46
|
+
* Used by IR-LINT-MISSING-AUTH-010 to detect HTTP entry nodes with no auth coverage.
|
|
47
|
+
*/
|
|
48
|
+
export function isAuthLikeNodeType(t) {
|
|
49
|
+
const s = String(t ?? '')
|
|
50
|
+
.trim()
|
|
51
|
+
.toLowerCase();
|
|
52
|
+
if (!s)
|
|
53
|
+
return false;
|
|
54
|
+
return (/\b(auth|authentication|authorization|middleware|security|iam|idp|identity)\b/.test(s) ||
|
|
55
|
+
/oauth|jwt|saml|keycloak|okta|cognito|auth0|ldap|sso/.test(s));
|
|
56
|
+
}
|
|
24
57
|
/** Queue / topic / stream node types → treat incoming edges as async boundaries when edge metadata is absent. */
|
|
25
58
|
export function isQueueLikeNodeType(t) {
|
|
26
59
|
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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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';
|
package/dist/ir-structural.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/ir-structural.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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:
|
|
259
|
-
nodeId
|
|
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
|
}
|
package/dist/lint-rules.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/lint-rules.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/lint-rules.js
CHANGED
|
@@ -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,100 @@ 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, 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)
|
|
290
|
+
const reverseAdj = new Map();
|
|
291
|
+
for (const e of edges) {
|
|
292
|
+
if (!e || typeof e !== 'object')
|
|
293
|
+
continue;
|
|
294
|
+
const { from, to } = edgeEndpoints(e);
|
|
295
|
+
if (!from || !to || !nodeById.has(from) || !nodeById.has(to))
|
|
296
|
+
continue;
|
|
297
|
+
if (!reverseAdj.has(to))
|
|
298
|
+
reverseAdj.set(to, []);
|
|
299
|
+
reverseAdj.get(to).push(from);
|
|
300
|
+
}
|
|
301
|
+
const findings = [];
|
|
302
|
+
for (const [id, n] of nodeById) {
|
|
303
|
+
if (!isHttpLikeType(nodeType(n)))
|
|
304
|
+
continue;
|
|
305
|
+
if ((inDegree.get(id) ?? 0) > 0)
|
|
306
|
+
continue; // not an entry node
|
|
307
|
+
const cfg = (n.config ?? {});
|
|
308
|
+
// Explicit opt-out: config.authRequired === false marks an intentionally public endpoint
|
|
309
|
+
if (cfg.authRequired === false)
|
|
310
|
+
continue;
|
|
311
|
+
// Coverage check 1: outgoing neighbour is auth-like
|
|
312
|
+
const outNeighbours = adj.get(id) ?? [];
|
|
313
|
+
if (outNeighbours.some((v) => isAuthLikeNodeType(nodeType(nodeById.get(v) ?? {}))))
|
|
314
|
+
continue;
|
|
315
|
+
// Coverage check 2: an auth-like node points directly to this entry node
|
|
316
|
+
const inNeighbours = reverseAdj.get(id) ?? [];
|
|
317
|
+
if (inNeighbours.some((v) => isAuthLikeNodeType(nodeType(nodeById.get(v) ?? {}))))
|
|
318
|
+
continue;
|
|
319
|
+
// Coverage check 3: node config carries an auth signal
|
|
320
|
+
const authConfigKeys = ['auth', 'authrequired', 'authentication', 'authorization', 'security'];
|
|
321
|
+
const cfgKeys = Object.keys(cfg).map((k) => k.toLowerCase());
|
|
322
|
+
if (authConfigKeys.some((k) => cfgKeys.includes(k)))
|
|
323
|
+
continue;
|
|
324
|
+
findings.push({
|
|
325
|
+
code: 'IR-LINT-MISSING-AUTH-010',
|
|
326
|
+
severity: 'warning',
|
|
327
|
+
layer: LAYER,
|
|
328
|
+
message: `HTTP entry node "${id}" has no auth node or auth config in its immediate graph neighbourhood`,
|
|
329
|
+
nodeId: id,
|
|
330
|
+
fixHint: 'Add an auth/middleware node with an edge to or from this entry, or set config.authRequired: false for intentionally public endpoints.',
|
|
331
|
+
suggestion: 'Connect an auth, oauth, jwt, or middleware node. For PCI-DSS / HIPAA systems, every HTTP entry must have a documented auth boundary.',
|
|
332
|
+
impact: 'Unauthenticated HTTP entry points are a compliance gap in regulated environments and a common attack surface.',
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
return findings;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* IR-LINT-DEAD-NODE-011 — non-sink node with incoming edges but no outgoing edges.
|
|
339
|
+
*
|
|
340
|
+
* Datastore-like and queue-like nodes are valid sinks and are excluded.
|
|
341
|
+
* Nodes with no incident edges at all are already caught by IR-LINT-ISOLATED-NODE-005.
|
|
342
|
+
* This rule targets nodes that receive data but forward it nowhere — likely a missing
|
|
343
|
+
* edge, an incomplete integration step, or a stale component.
|
|
344
|
+
*/
|
|
345
|
+
export function ruleDeadNode(g) {
|
|
346
|
+
const findings = [];
|
|
347
|
+
for (const [id, n] of g.nodeById) {
|
|
348
|
+
const out = g.outDegree.get(id) ?? 0;
|
|
349
|
+
const inn = g.inDegree.get(id) ?? 0;
|
|
350
|
+
if (out > 0 || inn === 0)
|
|
351
|
+
continue; // has outgoing, or truly isolated (caught elsewhere)
|
|
352
|
+
const t = nodeType(n);
|
|
353
|
+
if (isDbLikeType(t) || isQueueLikeNodeType(t) || isHttpLikeType(t))
|
|
354
|
+
continue; // valid sinks
|
|
355
|
+
findings.push({
|
|
356
|
+
code: 'IR-LINT-DEAD-NODE-011',
|
|
357
|
+
severity: 'warning',
|
|
358
|
+
layer: LAYER,
|
|
359
|
+
message: `Node "${id}" (type: ${t || 'unknown'}) receives edges but has no outgoing edges — possible missing integration or dead component`,
|
|
360
|
+
nodeId: id,
|
|
361
|
+
fixHint: 'Add an outgoing edge to a downstream node, or remove this node if it is no longer active.',
|
|
362
|
+
suggestion: 'Dead-end non-sink nodes often represent incomplete migrations, dropped integrations, or copy-paste errors in the IR.',
|
|
363
|
+
impact: 'Data entering this node has no documented path forward, which misrepresents runtime behaviour.',
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
return findings;
|
|
367
|
+
}
|
|
273
368
|
/**
|
|
274
369
|
* Ordered registry: add a new rule by implementing `(g) => findings` and appending here.
|
|
275
370
|
*/
|
|
@@ -283,6 +378,8 @@ export const LINT_RULE_REGISTRY = [
|
|
|
283
378
|
ruleHttpMissingName,
|
|
284
379
|
ruleDatastoreNoIncoming,
|
|
285
380
|
ruleMultipleHttpEntries,
|
|
381
|
+
ruleHttpMissingAuth,
|
|
382
|
+
ruleDeadNode,
|
|
286
383
|
];
|
|
287
384
|
/** Run all registered architecture lint visitors (same as legacy `validateIrLint` behavior). */
|
|
288
385
|
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,
|
|
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"}
|
package/dist/openapi-to-ir.js
CHANGED
|
@@ -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
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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 =
|
|
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 : {});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@archrad/deterministic",
|
|
3
|
-
"version": "0.1.
|
|
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",
|