@archrad/deterministic 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.4] - 2026-04-06
11
+
12
+ ### Added
13
+
14
+ - **`loadPolicyPacksFromFiles(sources)`** — compile PolicyPack YAML/JSON from in-memory **`{ name, content }[]`** (same semantics as **`loadPolicyPacksFromDirectory`**; no filesystem).
15
+ - **Declarative policy packs** — YAML/JSON documents (`apiVersion: archrad/v1`, `kind: PolicyPack`) with `rules` that match **nodes** or **edges** on the parsed lint graph. Loader: **`loadPolicyPacksFromDirectory(dir)`** (supports `*.yaml`, `*.yml`, `*.json`; duplicate `rule.id` across the directory is rejected).
16
+ - **CLI** — **`archrad validate`**, **`archrad export`**, and **`archrad validate-drift`** accept **`--policies <dir>`** to merge compiled rules after built-in **`IR-LINT-*`** (skipped when **`--skip-lint`** / **`--skip-ir-lint`** is set).
17
+ - **ArchRad Cloud (InkByte server)** — org **`settings.archPolicyPacks`** (array of **`{ name, content }`**) is merged into deterministic export when **`organizationId`** is on the export request; **`POST /api/drift-check`** accepts **`organizationId`**, **`policyPackFiles`**, and **`skipArchPolicyPacks`** (membership required when **`organizationId`** is set).
18
+ - **Library** — **`validateIrLint(ir, { policyRuleVisitors })`**; **`runDeterministicExport`** / drift helpers accept **`policyRuleVisitors`** the same way.
19
+ - **Normalization** — **`NormalizedNode`** now includes **`metadata`** (from each node’s `metadata` object) so **`buildParsedLintGraph`** / policy **`match.node.tags`** see **`metadata.tags`** on the graph.
20
+ - **`--policies`** applies to all three lint commands consistently —
21
+ `validate-drift` uses the same policy layer when building the
22
+ reference export, so org rules are enforced end-to-end across
23
+ validate → export → drift.
24
+
25
+ ## [0.1.3] - 2026-04-04
26
+
27
+ ### Fixed
28
+
29
+ - **CLI** (`validate`, `export`, `validate-drift`): a missing **`--ir`** file now reports **`archrad: --ir file not found: <path>`** instead of **`invalid JSON`**.
30
+
31
+ ### Changed
32
+
33
+ - **`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`**.
34
+ - **Docs:** README documents **OpenAPI security → IR → `IR-LINT-MISSING-AUTH-010`** for the spec-to-compliance workflow.
35
+ - **`graphPredicates.ts`:** clarified comments for **`isHttpEndpointType`** vs **`isHttpLikeType`** (`graphql` vs `gateway` / `bff` / `grpc`).
36
+
37
+ ### Added
38
+
39
+ - **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).
40
+ - **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`**.
41
+
10
42
  ## [0.1.2] - 2026-03-28
11
43
 
12
44
  ### Fixed
@@ -71,7 +103,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
71
103
  - Documented **codegen vs validation** for retry/timeout IR fields and **InkByte vs OSS** scope in README and structural/semantic doc.
72
104
  - README positioning: **deterministic compiler and linter for system architecture**; validation layers table (OSS vs Cloud); **`validate-drift`**, drift GIF / trust-loop recording docs, library **`runValidateDrift`** example.
73
105
 
74
- [Unreleased]: https://github.com/archradhq/arch-deterministic/compare/v0.1.2...HEAD
106
+ [Unreleased]: https://github.com/archradhq/arch-deterministic/compare/v0.1.3...HEAD
107
+ [0.1.3]: https://github.com/archradhq/arch-deterministic/releases/tag/v0.1.3
75
108
  [0.1.2]: https://github.com/archradhq/arch-deterministic/releases/tag/v0.1.2
76
109
  [0.1.1]: https://github.com/archradhq/arch-deterministic/releases/tag/v0.1.1
77
110
  [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
@@ -131,6 +133,8 @@ archrad validate --ir ./graph.json --fail-on-warning
131
133
  archrad validate --ir ./graph.json --max-warnings 0
132
134
  # Structural only (skip IR-LINT-*):
133
135
  archrad validate --ir ./graph.json --skip-lint
136
+ # Declarative PolicyPack YAML/JSON in a directory (after IR-LINT-*; skipped with --skip-lint):
137
+ archrad validate --ir ./graph.json --policies ./policy-packs
134
138
  ```
135
139
 
136
140
  **Deterministic drift (thin, OSS):** compare an existing export tree on disk to a **fresh** export from the same IR. Detects **missing** / **changed** generated files (line endings normalized). Optional **`--strict-extra`** flags files present on disk but not in the reference export. Not semantic “does code match intent” — **ArchRad Cloud** adds builder/UI drift checks and broader governance.
package/dist/cli.js CHANGED
@@ -10,6 +10,7 @@ import { runDeterministicExport } from './exportPipeline.js';
10
10
  import { isLocalHostPortFree, normalizeGoldenHostPort } from './hostPort.js';
11
11
  import { validateIrStructural, hasIrStructuralErrors } from './ir-structural.js';
12
12
  import { validateIrLint } from './ir-lint.js';
13
+ import { loadPolicyPacksFromDirectory } from './policy-pack.js';
13
14
  import { printFindingsPretty, shouldFailFromFindings, sortFindings, } from './cli-findings.js';
14
15
  import { parseYamlToCanonicalIr, canonicalIrToJsonString, YamlGraphParseError, } from './yamlToIr.js';
15
16
  import { openApiStringToCanonicalIr, OpenApiIngestError } from './openapi-to-ir.js';
@@ -21,12 +22,50 @@ async function writeTree(baseDir, files) {
21
22
  await writeFile(dest, content, 'utf8');
22
23
  }
23
24
  }
25
+ /** Read and parse IR JSON; distinguish missing file from invalid JSON. */
26
+ async function readIrJsonFromPath(irPath) {
27
+ let raw;
28
+ try {
29
+ raw = await readFile(irPath, 'utf8');
30
+ }
31
+ catch (e) {
32
+ const err = e;
33
+ if (err?.code === 'ENOENT') {
34
+ console.error(`archrad: --ir file not found: ${irPath}`);
35
+ }
36
+ else {
37
+ console.error(`archrad: could not read --ir file: ${irPath} (${err?.message ?? String(e)})`);
38
+ }
39
+ return null;
40
+ }
41
+ try {
42
+ return JSON.parse(raw);
43
+ }
44
+ catch {
45
+ console.error('archrad: invalid JSON in --ir file');
46
+ return null;
47
+ }
48
+ }
24
49
  function parseMaxWarnings(v) {
25
50
  if (v == null || v === '')
26
51
  return undefined;
27
52
  const n = parseInt(v, 10);
28
53
  return Number.isFinite(n) ? n : undefined;
29
54
  }
55
+ /** Load `--policies` directory; on failure prints to stderr and returns null (caller should exit 1). */
56
+ async function loadPoliciesOption(policiesDir) {
57
+ if (policiesDir == null || policiesDir === '')
58
+ return {};
59
+ const dir = resolve(policiesDir);
60
+ const loaded = await loadPolicyPacksFromDirectory(dir);
61
+ if (!loaded.ok) {
62
+ for (const e of loaded.errors) {
63
+ console.error(`archrad: ${e}`);
64
+ }
65
+ return null;
66
+ }
67
+ return { policyRuleVisitors: loaded.visitors };
68
+ }
30
69
  function exitPolicyFromOpts(opts) {
31
70
  return {
32
71
  failOnWarning: Boolean(opts.failOnWarning),
@@ -37,29 +76,37 @@ const program = new Command();
37
76
  program
38
77
  .name('archrad')
39
78
  .description('Validate your architecture before you write code. Deterministic compiler + linter — FastAPI / Express (no LLM, no server).')
40
- .version('0.1.0');
79
+ .version('0.1.4');
41
80
  program
42
81
  .command('validate')
43
82
  .description('Validate your architecture before you write code — IR structural (IR-STRUCT-*) + architecture lint (IR-LINT-*)')
44
83
  .requiredOption('-i, --ir <path>', 'Path to IR JSON (graph with nodes/edges or full wrapper)')
45
84
  .option('--json', 'Print findings as JSON array to stdout')
46
85
  .option('--skip-lint', 'Skip architecture lint (IR-LINT-*); structural only')
86
+ .option('--policies <dir>', 'Directory of PolicyPack YAML/JSON (*.yaml, *.yml, *.json); merged after IR-LINT-*')
47
87
  .option('--fail-on-warning', 'Exit with error if any warning (CI gate)')
48
88
  .option('--max-warnings <n>', 'Exit with error if warning count is greater than n (e.g. 0 allows no warnings)')
49
89
  .action(async (cmdOpts) => {
50
90
  const irPath = resolve(cmdOpts.ir);
51
- let ir;
52
- try {
53
- ir = JSON.parse(await readFile(irPath, 'utf8'));
54
- }
55
- catch {
56
- console.error('archrad: invalid JSON in --ir file');
91
+ const ir = await readIrJsonFromPath(irPath);
92
+ if (ir == null) {
57
93
  process.exitCode = 1;
58
94
  return;
59
95
  }
60
96
  const noLint = Boolean(cmdOpts.skipLint);
97
+ let lintOpts = {};
98
+ if (!noLint && cmdOpts.policies) {
99
+ const loaded = await loadPoliciesOption(cmdOpts.policies);
100
+ if (loaded == null) {
101
+ process.exitCode = 1;
102
+ return;
103
+ }
104
+ lintOpts = loaded;
105
+ }
61
106
  const structural = validateIrStructural(ir);
62
- const lint = noLint || hasIrStructuralErrors(structural) ? [] : validateIrLint(ir);
107
+ const lint = noLint || hasIrStructuralErrors(structural)
108
+ ? []
109
+ : validateIrLint(ir, lintOpts);
63
110
  const combined = sortFindings([...structural, ...lint]);
64
111
  if (cmdOpts.json) {
65
112
  const forJson = combined.map((f) => ({
@@ -182,16 +229,12 @@ program
182
229
  .action(async (cmdOpts) => {
183
230
  const irPath = resolve(cmdOpts.ir);
184
231
  const outDir = resolve(cmdOpts.out);
185
- const raw = await readFile(irPath, 'utf8');
186
- let ir;
187
- try {
188
- ir = JSON.parse(raw);
189
- }
190
- catch {
191
- console.error('archrad: invalid JSON in --ir file');
232
+ const parsed = await readIrJsonFromPath(irPath);
233
+ if (parsed == null) {
192
234
  process.exitCode = 1;
193
235
  return;
194
236
  }
237
+ const ir = parsed;
195
238
  const actualIR = ir.graph ? ir : { graph: ir };
196
239
  const hostPort = normalizeGoldenHostPort(cmdOpts.hostPort ?? process.env.ARCHRAD_HOST_PORT);
197
240
  if (!cmdOpts.skipHostPortCheck) {
@@ -208,11 +251,21 @@ program
208
251
  }
209
252
  const exportOpts = cmdOpts;
210
253
  const skipStruct = Boolean(exportOpts.dangerSkipIrStructuralValidation || exportOpts.skipIrStructuralValidation);
254
+ let exportLintOpts = {};
255
+ if (!cmdOpts.skipIrLint && cmdOpts.policies) {
256
+ const loaded = await loadPoliciesOption(cmdOpts.policies);
257
+ if (loaded == null) {
258
+ process.exitCode = 1;
259
+ return;
260
+ }
261
+ exportLintOpts = loaded;
262
+ }
211
263
  try {
212
264
  const { files, openApiStructuralWarnings, irStructuralFindings, irLintFindings } = await runDeterministicExport(actualIR, cmdOpts.target, {
213
265
  hostPort,
214
266
  skipIrStructuralValidation: skipStruct,
215
267
  skipIrLint: cmdOpts.skipIrLint,
268
+ ...exportLintOpts,
216
269
  });
217
270
  const combined = sortFindings([...irStructuralFindings, ...irLintFindings]);
218
271
  if (combined.length) {
@@ -261,20 +314,18 @@ program
261
314
  .addOption(new Option('--danger-skip-ir-structural-validation', 'UNSAFE: skip validateIrStructural during reference export'))
262
315
  .addOption(new Option('--skip-ir-structural-validation', 'Deprecated alias').hideHelp())
263
316
  .option('--skip-ir-lint', 'Skip architecture lint when building reference export')
317
+ .option('--policies <dir>', 'Directory of PolicyPack YAML/JSON; merged after IR-LINT-* for the reference export')
264
318
  .option('--strict-extra', 'Fail if output directory contains files not in the reference export')
265
319
  .option('--json', 'Print drift findings and export metadata as JSON')
266
320
  .action(async (cmdOpts) => {
267
321
  const irPath = resolve(cmdOpts.ir);
268
322
  const outDir = resolve(cmdOpts.out);
269
- let ir;
270
- try {
271
- ir = JSON.parse(await readFile(irPath, 'utf8'));
272
- }
273
- catch {
274
- console.error('archrad: invalid JSON in --ir file');
323
+ const parsed = await readIrJsonFromPath(irPath);
324
+ if (parsed == null) {
275
325
  process.exitCode = 1;
276
326
  return;
277
327
  }
328
+ const ir = parsed;
278
329
  const actualIR = ir.graph ? ir : { graph: ir };
279
330
  const hostPort = normalizeGoldenHostPort(cmdOpts.hostPort ?? process.env.ARCHRAD_HOST_PORT);
280
331
  if (!cmdOpts.skipHostPortCheck) {
@@ -284,12 +335,22 @@ program
284
335
  }
285
336
  }
286
337
  const skipStruct = Boolean(cmdOpts.dangerSkipIrStructuralValidation || cmdOpts.skipIrStructuralValidation);
338
+ let driftLintOpts = {};
339
+ if (!cmdOpts.skipIrLint && cmdOpts.policies) {
340
+ const loaded = await loadPoliciesOption(cmdOpts.policies);
341
+ if (loaded == null) {
342
+ process.exitCode = 1;
343
+ return;
344
+ }
345
+ driftLintOpts = loaded;
346
+ }
287
347
  try {
288
348
  const result = await runValidateDrift(actualIR, cmdOpts.target, outDir, {
289
349
  hostPort,
290
350
  skipIrStructuralValidation: skipStruct,
291
351
  skipIrLint: cmdOpts.skipIrLint,
292
352
  strictExtra: cmdOpts.strictExtra,
353
+ ...driftLintOpts,
293
354
  });
294
355
  const combined = sortFindings([
295
356
  ...result.exportResult.irStructuralFindings,
@@ -3,6 +3,7 @@
3
3
  * Used by the ArchRad server and the `archrad` CLI.
4
4
  */
5
5
  import { type IrStructuralFinding } from './ir-structural.js';
6
+ import { type ValidateIrLintOptions } from './ir-lint.js';
6
7
  export type DeterministicExportResult = {
7
8
  files: Record<string, string>;
8
9
  /** Human-readable lines when generated OpenAPI fails **document-shape** checks (not full spec lint) */
@@ -13,11 +14,11 @@ export type DeterministicExportResult = {
13
14
  * Errors block codegen; this field stays the single source for “graph does not compile.”
14
15
  */
15
16
  irStructuralFindings: IrStructuralFinding[];
16
- /** IR-LINT-* heuristics only; does not include IR-STRUCT-* (those live in `irStructuralFindings`). */
17
+ /** IR-LINT-* plus optional declarative policy-pack findings; does not include IR-STRUCT-* (those live in `irStructuralFindings`). */
17
18
  irLintFindings: IrStructuralFinding[];
18
19
  };
19
20
  /**
20
21
  * Generate FastAPI or Express project files + golden Docker/Makefile + OpenAPI **document-shape** check.
21
22
  */
22
- export declare function runDeterministicExport(actualIR: any, target: string, opts?: Record<string, any>): Promise<DeterministicExportResult>;
23
+ export declare function runDeterministicExport(actualIR: any, target: string, opts?: Record<string, any> & ValidateIrLintOptions): Promise<DeterministicExportResult>;
23
24
  //# sourceMappingURL=exportPipeline.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"exportPipeline.d.ts","sourceRoot":"","sources":["../src/exportPipeline.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH,OAAO,EAGL,KAAK,mBAAmB,EACzB,MAAM,oBAAoB,CAAC;AAG5B,MAAM,MAAM,yBAAyB,GAAG;IACtC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,uGAAuG;IACvG,yBAAyB,EAAE,MAAM,EAAE,CAAC;IACpC;;;;OAIG;IACH,oBAAoB,EAAE,mBAAmB,EAAE,CAAC;IAC5C,sGAAsG;IACtG,cAAc,EAAE,mBAAmB,EAAE,CAAC;CACvC,CAAC;AAEF;;GAEG;AACH,wBAAsB,sBAAsB,CAC1C,QAAQ,EAAE,GAAG,EACb,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,GAC7B,OAAO,CAAC,yBAAyB,CAAC,CAwDpC"}
1
+ {"version":3,"file":"exportPipeline.d.ts","sourceRoot":"","sources":["../src/exportPipeline.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH,OAAO,EAGL,KAAK,mBAAmB,EACzB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAkB,KAAK,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAE1E,MAAM,MAAM,yBAAyB,GAAG;IACtC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,uGAAuG;IACvG,yBAAyB,EAAE,MAAM,EAAE,CAAC;IACpC;;;;OAIG;IACH,oBAAoB,EAAE,mBAAmB,EAAE,CAAC;IAC5C,qIAAqI;IACrI,cAAc,EAAE,mBAAmB,EAAE,CAAC;CACvC,CAAC;AAEF;;GAEG;AACH,wBAAsB,sBAAsB,CAC1C,QAAQ,EAAE,GAAG,EACb,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,qBAA0B,GACrD,OAAO,CAAC,yBAAyB,CAAC,CAwDpC"}
@@ -21,7 +21,7 @@ export async function runDeterministicExport(actualIR, target, opts = {}) {
21
21
  }
22
22
  let irLintFindings = [];
23
23
  if (!skipLint) {
24
- const lintPass = validateIrLint(actualIR);
24
+ const lintPass = validateIrLint(actualIR, { policyRuleVisitors: opts.policyRuleVisitors });
25
25
  if (skipIr) {
26
26
  // Dangerous mode: full structural pass is off, but parse/normalize failures still return IR-STRUCT-* from
27
27
  // validateIrLint — fold those into irStructuralFindings so InkByte / CLI consumers block and log like normal.
@@ -4,9 +4,10 @@
4
4
  /**
5
5
  * Narrow predicate: node types that carry a single HTTP endpoint (`config.url` + HTTP method).
6
6
  * Used by **structural validation** for `IR-STRUCT-HTTP_PATH` / `IR-STRUCT-HTTP_METHOD` checks.
7
- * `gateway`, `grpc`, and `bff` are intentionally excluded they use different config shapes
7
+ * **`http` / `https` / `rest` / `api` / `graphql`** share this contract in the IR (GraphQL is one route + method in the IR model).
8
+ * **`gateway`**, **`grpc`**, and **`bff`** are intentionally **excluded** — they use different config shapes
8
9
  * (upstream routing, proto service/method, multi-route aggregation) and must not be required
9
- * to supply a REST-style url + HTTP method.
10
+ * to supply a REST-style `url` + HTTP method; they remain **`isHttpLikeType`** for lint (entries, health, sync chain, etc.).
10
11
  */
11
12
  export declare function isHttpEndpointType(t: string): boolean;
12
13
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"graphPredicates.d.ts","sourceRoot":"","sources":["../src/graphPredicates.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAMrD;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAQjD;AAED,oEAAoE;AACpE,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAM/C;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CASrD;AAED,iHAAiH;AACjH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAStD"}
1
+ {"version":3,"file":"graphPredicates.d.ts","sourceRoot":"","sources":["../src/graphPredicates.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAMrD;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAQjD;AAED,oEAAoE;AACpE,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAM/C;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CASrD;AAED,iHAAiH;AACjH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAStD"}
@@ -4,9 +4,10 @@
4
4
  /**
5
5
  * Narrow predicate: node types that carry a single HTTP endpoint (`config.url` + HTTP method).
6
6
  * Used by **structural validation** for `IR-STRUCT-HTTP_PATH` / `IR-STRUCT-HTTP_METHOD` checks.
7
- * `gateway`, `grpc`, and `bff` are intentionally excluded they use different config shapes
7
+ * **`http` / `https` / `rest` / `api` / `graphql`** share this contract in the IR (GraphQL is one route + method in the IR model).
8
+ * **`gateway`**, **`grpc`**, and **`bff`** are intentionally **excluded** — they use different config shapes
8
9
  * (upstream routing, proto service/method, multi-route aggregation) and must not be required
9
- * to supply a REST-style url + HTTP method.
10
+ * to supply a REST-style `url` + HTTP method; they remain **`isHttpLikeType`** for lint (entries, health, sync chain, etc.).
10
11
  */
11
12
  export function isHttpEndpointType(t) {
12
13
  const s = String(t ?? '')
package/dist/index.d.ts CHANGED
@@ -12,7 +12,8 @@ export { runDeterministicExport, type DeterministicExportResult } from './export
12
12
  export { diffExpectedExportAgainstFiles, diffExpectedExportAgainstDirectory, readDirectoryAsExportMap, runValidateDrift, runDriftCheckAgainstFiles, normalizeExportFileContent, type DriftFinding, type DriftCode, type ValidateDriftResult, type DriftCheckFilesResult, } from './validate-drift.js';
13
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
- export { validateIrLint } from './ir-lint.js';
15
+ export { validateIrLint, type ValidateIrLintOptions } from './ir-lint.js';
16
+ export { loadPolicyPacksFromDirectory, loadPolicyPacksFromFiles, type LoadPolicyPacksResult, type PolicyPackFileSource, type PolicyPackDocumentV1, type PolicyRuleV1, type PolicyPackMetadataV1, type PolicyNodeSelectorV1, type PolicyEdgeMatchV1, type PolicySeverity, } from './policy-pack.js';
16
17
  export { runArchitectureLinting, LINT_RULE_REGISTRY } from './lint-rules.js';
17
18
  export { buildParsedLintGraph, isParsedLintGraph, type ParsedLintGraph, type BuildParsedLintGraphResult, } from './lint-graph.js';
18
19
  export { isHttpLikeType, isHttpEndpointType, isDbLikeType, isQueueLikeNodeType, isAuthLikeNodeType } from './graphPredicates.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,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"}
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,KAAK,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAE1E,OAAO,EACL,4BAA4B,EAC5B,wBAAwB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EACzB,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EACtB,KAAK,cAAc,GACpB,MAAM,kBAAkB,CAAC;AAC1B,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
@@ -13,6 +13,7 @@ export { diffExpectedExportAgainstFiles, diffExpectedExportAgainstDirectory, rea
13
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
+ export { loadPolicyPacksFromDirectory, loadPolicyPacksFromFiles, } from './policy-pack.js';
16
17
  export { runArchitectureLinting, LINT_RULE_REGISTRY } from './lint-rules.js';
17
18
  export { buildParsedLintGraph, isParsedLintGraph, } from './lint-graph.js';
18
19
  export { isHttpLikeType, isHttpEndpointType, isDbLikeType, isQueueLikeNodeType, isAuthLikeNodeType } from './graphPredicates.js';
package/dist/ir-lint.d.ts CHANGED
@@ -1,11 +1,16 @@
1
1
  /**
2
2
  * Architecture lint (IR-LINT-*): thin entry — parses graph then runs visitor registry in `lint-rules.ts`.
3
3
  */
4
+ import type { ParsedLintGraph } from './lint-graph.js';
4
5
  import type { IrStructuralFinding } from './ir-structural.js';
6
+ export type ValidateIrLintOptions = {
7
+ /** Extra visitors after built-in IR-LINT-* (declarative policy packs, org rules). */
8
+ policyRuleVisitors?: ReadonlyArray<(g: ParsedLintGraph) => IrStructuralFinding[]>;
9
+ };
5
10
  /**
6
- * Run architecture lint (IR-LINT-*). If the IR cannot be parsed (invalid root, empty graph, etc.),
11
+ * Run architecture lint (IR-LINT-*) plus optional policy visitors. If the IR cannot be parsed (invalid root, empty graph, etc.),
7
12
  * returns the same **structural** findings as `normalizeIrGraph` / `validateIrStructural` would surface
8
13
  * for that shape — callers that only invoke `validateIrLint` still see blockers instead of a silent `[]`.
9
14
  */
10
- export declare function validateIrLint(ir: unknown): IrStructuralFinding[];
15
+ export declare function validateIrLint(ir: unknown, options?: ValidateIrLintOptions): IrStructuralFinding[];
11
16
  //# sourceMappingURL=ir-lint.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ir-lint.d.ts","sourceRoot":"","sources":["../src/ir-lint.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAE9D;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,OAAO,GAAG,mBAAmB,EAAE,CAIjE"}
1
+ {"version":3,"file":"ir-lint.d.ts","sourceRoot":"","sources":["../src/ir-lint.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAGvD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAE9D,MAAM,MAAM,qBAAqB,GAAG;IAClC,qFAAqF;IACrF,kBAAkB,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC,EAAE,eAAe,KAAK,mBAAmB,EAAE,CAAC,CAAC;CACnF,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,mBAAmB,EAAE,CAMlG"}
package/dist/ir-lint.js CHANGED
@@ -4,13 +4,15 @@
4
4
  import { buildParsedLintGraph, isParsedLintGraph } from './lint-graph.js';
5
5
  import { runArchitectureLinting } from './lint-rules.js';
6
6
  /**
7
- * Run architecture lint (IR-LINT-*). If the IR cannot be parsed (invalid root, empty graph, etc.),
7
+ * Run architecture lint (IR-LINT-*) plus optional policy visitors. If the IR cannot be parsed (invalid root, empty graph, etc.),
8
8
  * returns the same **structural** findings as `normalizeIrGraph` / `validateIrStructural` would surface
9
9
  * for that shape — callers that only invoke `validateIrLint` still see blockers instead of a silent `[]`.
10
10
  */
11
- export function validateIrLint(ir) {
11
+ export function validateIrLint(ir, options) {
12
12
  const built = buildParsedLintGraph(ir);
13
13
  if (!isParsedLintGraph(built))
14
14
  return built.findings;
15
- return runArchitectureLinting(built);
15
+ const base = runArchitectureLinting(built);
16
+ const extra = options?.policyRuleVisitors?.flatMap((v) => v(built)) ?? [];
17
+ return [...base, ...extra];
16
18
  }
@@ -9,6 +9,8 @@ export type NormalizedNode = {
9
9
  name: string;
10
10
  config: Record<string, unknown>;
11
11
  schema: Record<string, unknown>;
12
+ /** Node-level metadata (e.g. `tags` for policy packs / lint). */
13
+ metadata: Record<string, unknown>;
12
14
  };
13
15
  export type NormalizedEdge = {
14
16
  id: string;
@@ -1 +1 @@
1
- {"version":3,"file":"ir-normalize.d.ts","sourceRoot":"","sources":["../src/ir-normalize.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,cAAc,GAAG;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,+EAA+E;IAC/E,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,KAAK,EAAE,cAAc,EAAE,CAAC;CACzB,CAAC;AAOF;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,cAAc,CAiB9D;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,cAAc,CAuB9D;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,UAAU,EAAE,eAAe,CAAC;IAC5B,sEAAsE;IACtE,sBAAsB,EAAE,OAAO,CAAC;CACjC,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,iBAAiB,CAkB5F"}
1
+ {"version":3,"file":"ir-normalize.d.ts","sourceRoot":"","sources":["../src/ir-normalize.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,cAAc,GAAG;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,iEAAiE;IACjE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,+EAA+E;IAC/E,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,KAAK,EAAE,cAAc,EAAE,CAAC;CACzB,CAAC;AAOF;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,cAAc,CAkB9D;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,cAAc,CAuB9D;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,UAAU,EAAE,eAAe,CAAC;IAC5B,sEAAsE;IACtE,sBAAsB,EAAE,OAAO,CAAC;CACjC,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,iBAAiB,CAkB5F"}
@@ -12,7 +12,7 @@ function emptyRecord(obj) {
12
12
  */
13
13
  export function normalizeNodeSlot(raw) {
14
14
  if (raw == null || typeof raw !== 'object') {
15
- return { id: '', type: '', name: '', config: {}, schema: {} };
15
+ return { id: '', type: '', name: '', config: {}, schema: {}, metadata: {} };
16
16
  }
17
17
  const r = raw;
18
18
  const id = String(r.id ?? '').trim();
@@ -26,6 +26,7 @@ export function normalizeNodeSlot(raw) {
26
26
  name,
27
27
  config: emptyRecord(r.config),
28
28
  schema: emptyRecord(r.schema),
29
+ metadata: emptyRecord(r.metadata),
29
30
  };
30
31
  }
31
32
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"lint-graph.d.ts","sourceRoot":"","sources":["../src/lint-graph.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAK9D,6FAA6F;AAC7F,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEzF,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC/C,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAC3B,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,+EAA+E;IAC/E,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG,eAAe,GAAG;IAAE,QAAQ,EAAE,mBAAmB,EAAE,CAAA;CAAE,CAAC;AAE/F,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAItF;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAE3D;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAWvD;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,KAAK,CAAC,EAAE,eAAe,GAAG,OAAO,CAqBxG;AAED,2EAA2E;AAC3E,wBAAgB,yBAAyB,CAAC,CAAC,EAAE,eAAe,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAYnF;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,OAAO,GAAG,0BAA0B,CAqD5E;AAED,2DAA2D;AAC3D,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,0BAA0B,GAAG,CAAC,IAAI,eAAe,CAErF"}
1
+ {"version":3,"file":"lint-graph.d.ts","sourceRoot":"","sources":["../src/lint-graph.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAK9D,6FAA6F;AAC7F,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEzF,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC/C,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAC3B,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,+EAA+E;IAC/E,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG,eAAe,GAAG;IAAE,QAAQ,EAAE,mBAAmB,EAAE,CAAA;CAAE,CAAC;AAE/F,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAItF;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAE3D;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAWvD;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,KAAK,CAAC,EAAE,eAAe,GAAG,OAAO,CAqBxG;AAED,2EAA2E;AAC3E,wBAAgB,yBAAyB,CAAC,CAAC,EAAE,eAAe,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAYnF;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,OAAO,GAAG,0BAA0B,CAsD5E;AAED,2DAA2D;AAC3D,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,0BAA0B,GAAG,CAAC,IAAI,eAAe,CAErF"}
@@ -106,6 +106,7 @@ export function buildParsedLintGraph(ir) {
106
106
  name: n.name,
107
107
  config: n.config,
108
108
  schema: n.schema,
109
+ metadata: n.metadata,
109
110
  });
110
111
  }
111
112
  const edges = normalized.edges;
@@ -1 +1 @@
1
- {"version":3,"file":"lint-rules.d.ts","sourceRoot":"","sources":["../src/lint-rules.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAcvD,8HAA8H;AAC9H,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CA6B5E;AAED,8BAA8B;AAC9B,wBAAgB,cAAc,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CAkBxE;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CAwDpF;AAED,iCAAiC;AACjC,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CAsB3E;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CAoB1E;AAED,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CA0B3E;AAED,oCAAoC;AACpC,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CAmB7E;AAED,wCAAwC;AACxC,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CAkBjF;AAED,2HAA2H;AAC3H,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CAkBjF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CA2D7E;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CAsBtE;AAED;;GAEG;AACH,eAAO,MAAM,kBAAkB,EAAE,aAAa,CAAC,CAAC,CAAC,EAAE,eAAe,KAAK,mBAAmB,EAAE,CAY3F,CAAC;AAEF,gGAAgG;AAChG,wBAAgB,sBAAsB,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CAEhF"}
1
+ {"version":3,"file":"lint-rules.d.ts","sourceRoot":"","sources":["../src/lint-rules.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAcvD,8HAA8H;AAC9H,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CA6B5E;AAED,8BAA8B;AAC9B,wBAAgB,cAAc,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CAkBxE;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CAwDpF;AAED,iCAAiC;AACjC,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CAsB3E;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CAoB1E;AAED,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CA0B3E;AAED,oCAAoC;AACpC,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CAmB7E;AAED,wCAAwC;AACxC,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CAkBjF;AAED,2HAA2H;AAC3H,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CAkBjF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CAoD7E;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CAsBtE;AAED;;GAEG;AACH,eAAO,MAAM,kBAAkB,EAAE,aAAa,CAAC,CAAC,CAAC,EAAE,eAAe,KAAK,mBAAmB,EAAE,CAY3F,CAAC;AAEF,gGAAgG;AAChG,wBAAgB,sBAAsB,CAAC,CAAC,EAAE,eAAe,GAAG,mBAAmB,EAAE,CAEhF"}
@@ -284,23 +284,15 @@ export function ruleMultipleHttpEntries(g) {
284
284
  * for intentionally public endpoints (health, public assets, etc.).
285
285
  */
286
286
  export function ruleHttpMissingAuth(g) {
287
- const { edges, nodeById, adj } = g;
288
- // Build set of nodes that have an incoming sync edge (not entry nodes)
289
- const hasIncomingEdge = new Set();
290
- for (const e of edges) {
291
- if (!e || typeof e !== 'object')
292
- continue;
293
- const { to } = edgeEndpoints(e);
294
- if (to)
295
- hasIncomingEdge.add(to);
296
- }
297
- // Build reverse adjacency: to → [from] for auth-coverage check #2
287
+ const { edges, nodeById, adj, inDegree } = g;
288
+ // Entry = no valid incoming edge (same counts as buildParsedLintGraph.inDegree)
289
+ // Build reverse adjacency: to → [from] for auth-coverage check #2 (valid endpoints only, same as adj)
298
290
  const reverseAdj = new Map();
299
291
  for (const e of edges) {
300
292
  if (!e || typeof e !== 'object')
301
293
  continue;
302
294
  const { from, to } = edgeEndpoints(e);
303
- if (!from || !to)
295
+ if (!from || !to || !nodeById.has(from) || !nodeById.has(to))
304
296
  continue;
305
297
  if (!reverseAdj.has(to))
306
298
  reverseAdj.set(to, []);
@@ -310,7 +302,7 @@ export function ruleHttpMissingAuth(g) {
310
302
  for (const [id, n] of nodeById) {
311
303
  if (!isHttpLikeType(nodeType(n)))
312
304
  continue;
313
- if (hasIncomingEdge.has(id))
305
+ if ((inDegree.get(id) ?? 0) > 0)
314
306
  continue; // not an entry node
315
307
  const cfg = (n.config ?? {});
316
308
  // Explicit opt-out: config.authRequired === false marks an intentionally public endpoint
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Declarative org policy packs (YAML/JSON) — deterministic graph matchers on ParsedLintGraph.
3
+ * Codes should use a stable prefix (e.g. ORG-*, ACME-*) to avoid colliding with IR-LINT-* / IR-STRUCT-*.
4
+ */
5
+ import type { ParsedLintGraph } from './lint-graph.js';
6
+ import type { IrStructuralFinding } from './ir-structural.js';
7
+ export type PolicySeverity = 'error' | 'warning' | 'info';
8
+ /** Single-node selector: all provided predicates must match (AND). */
9
+ export type PolicyNodeSelectorV1 = {
10
+ id?: string;
11
+ type?: string | string[];
12
+ /** All listed tags must appear on `node.metadata.tags` (array of strings). */
13
+ tags?: string[];
14
+ };
15
+ export type PolicyEdgeMatchV1 = {
16
+ from: PolicyNodeSelectorV1;
17
+ to: PolicyNodeSelectorV1;
18
+ };
19
+ export type PolicyRuleV1 = {
20
+ id: string;
21
+ severity: PolicySeverity;
22
+ message: string;
23
+ fixHint?: string;
24
+ match: {
25
+ node?: PolicyNodeSelectorV1;
26
+ edge?: PolicyEdgeMatchV1;
27
+ };
28
+ };
29
+ export type PolicyPackMetadataV1 = {
30
+ name?: string;
31
+ org?: string;
32
+ };
33
+ export type PolicyPackDocumentV1 = {
34
+ apiVersion: 'archrad/v1';
35
+ kind: 'PolicyPack';
36
+ metadata?: PolicyPackMetadataV1;
37
+ rules: PolicyRuleV1[];
38
+ };
39
+ export type LoadPolicyPacksResult = {
40
+ ok: true;
41
+ visitors: ReadonlyArray<(g: ParsedLintGraph) => IrStructuralFinding[]>;
42
+ ruleCount: number;
43
+ } | {
44
+ ok: false;
45
+ errors: string[];
46
+ };
47
+ export type PolicyPackFileSource = {
48
+ /** Virtual filename (must end with .yaml, .yml, or .json for parse rules). */
49
+ name: string;
50
+ content: string;
51
+ };
52
+ /**
53
+ * Load policy packs from in-memory file sources (same semantics as {@link loadPolicyPacksFromDirectory}).
54
+ * Use for ArchRad Cloud, tests, and API bodies — no filesystem required.
55
+ */
56
+ export declare function loadPolicyPacksFromFiles(sources: ReadonlyArray<PolicyPackFileSource>): LoadPolicyPacksResult;
57
+ /**
58
+ * Load and compile all policy YAML/JSON files in a directory into lint visitors.
59
+ * Filenames: `*.yaml`, `*.yml`, `*.json` (other files ignored).
60
+ */
61
+ export declare function loadPolicyPacksFromDirectory(dir: string): Promise<LoadPolicyPacksResult>;
62
+ //# sourceMappingURL=policy-pack.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy-pack.d.ts","sourceRoot":"","sources":["../src/policy-pack.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAE9D,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAE1D,sEAAsE;AACtE,MAAM,MAAM,oBAAoB,GAAG;IACjC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACzB,8EAA8E;IAC9E,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,oBAAoB,CAAC;IAC3B,EAAE,EAAE,oBAAoB,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,cAAc,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE;QACL,IAAI,CAAC,EAAE,oBAAoB,CAAC;QAC5B,IAAI,CAAC,EAAE,iBAAiB,CAAC;KAC1B,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,UAAU,EAAE,YAAY,CAAC;IACzB,IAAI,EAAE,YAAY,CAAC;IACnB,QAAQ,CAAC,EAAE,oBAAoB,CAAC;IAChC,KAAK,EAAE,YAAY,EAAE,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAC7B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,aAAa,CAAC,CAAC,CAAC,EAAE,eAAe,KAAK,mBAAmB,EAAE,CAAC,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACvG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAgHpC,MAAM,MAAM,oBAAoB,GAAG;IACjC,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,aAAa,CAAC,oBAAoB,CAAC,GAAG,qBAAqB,CAkC5G;AA4BD;;;GAGG;AACH,wBAAsB,4BAA4B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAmC9F"}
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Declarative org policy packs (YAML/JSON) — deterministic graph matchers on ParsedLintGraph.
3
+ * Codes should use a stable prefix (e.g. ORG-*, ACME-*) to avoid colliding with IR-LINT-* / IR-STRUCT-*.
4
+ */
5
+ import { readFile, readdir } from 'node:fs/promises';
6
+ import { join, resolve } from 'node:path';
7
+ import yaml from 'js-yaml';
8
+ import { edgeEndpoints, nodeType } from './lint-graph.js';
9
+ function isNonEmptyRecord(x) {
10
+ return x != null && typeof x === 'object' && !Array.isArray(x);
11
+ }
12
+ function normalizeTypes(t) {
13
+ if (t == null)
14
+ return null;
15
+ const arr = Array.isArray(t) ? t : [t];
16
+ return arr.map((s) => String(s).trim().toLowerCase()).filter(Boolean);
17
+ }
18
+ /** True if selector has at least one predicate (empty match-all forbidden for v1). */
19
+ function selectorHasPredicate(sel) {
20
+ if (sel.id != null && String(sel.id).trim() !== '')
21
+ return true;
22
+ if (sel.type != null) {
23
+ const n = normalizeTypes(sel.type);
24
+ if (n && n.length > 0)
25
+ return true;
26
+ }
27
+ if (sel.tags != null && Array.isArray(sel.tags) && sel.tags.length > 0)
28
+ return true;
29
+ return false;
30
+ }
31
+ function nodeMatchesSelector(n, sel) {
32
+ if (sel.id != null && String(n.id ?? '') !== String(sel.id))
33
+ return false;
34
+ const types = normalizeTypes(sel.type);
35
+ if (types && types.length > 0) {
36
+ const nt = nodeType(n);
37
+ if (!types.includes(nt))
38
+ return false;
39
+ }
40
+ if (sel.tags != null && sel.tags.length > 0) {
41
+ const meta = n.metadata ?? {};
42
+ const raw = meta.tags;
43
+ if (!Array.isArray(raw))
44
+ return false;
45
+ const have = new Set(raw.map((x) => String(x).toLowerCase()));
46
+ for (const t of sel.tags) {
47
+ if (!have.has(String(t).toLowerCase()))
48
+ return false;
49
+ }
50
+ }
51
+ return true;
52
+ }
53
+ function compileRule(rule, source) {
54
+ if (!rule.id || typeof rule.id !== 'string' || !/^[A-Za-z0-9_.-]+$/.test(rule.id)) {
55
+ throw new Error(`[${source}] invalid rule.id`);
56
+ }
57
+ if (!['error', 'warning', 'info'].includes(rule.severity)) {
58
+ throw new Error(`[${source}] rule "${rule.id}": severity must be error | warning | info`);
59
+ }
60
+ if (!rule.message || typeof rule.message !== 'string') {
61
+ throw new Error(`[${source}] rule "${rule.id}": message is required`);
62
+ }
63
+ const hasNode = rule.match?.node != null;
64
+ const hasEdge = rule.match?.edge != null;
65
+ if (hasNode === hasEdge) {
66
+ throw new Error(`[${source}] rule "${rule.id}": specify exactly one of match.node or match.edge`);
67
+ }
68
+ if (hasNode) {
69
+ const sel = rule.match.node;
70
+ if (!selectorHasPredicate(sel)) {
71
+ throw new Error(`[${source}] rule "${rule.id}": match.node must include id, type, and/or tags`);
72
+ }
73
+ return (g) => {
74
+ const findings = [];
75
+ for (const [id, n] of g.nodeById) {
76
+ if (nodeMatchesSelector(n, sel)) {
77
+ findings.push({
78
+ code: rule.id,
79
+ severity: rule.severity,
80
+ message: rule.message,
81
+ nodeId: id,
82
+ layer: 'lint',
83
+ fixHint: rule.fixHint,
84
+ });
85
+ }
86
+ }
87
+ return findings;
88
+ };
89
+ }
90
+ const edge = rule.match.edge;
91
+ if (!selectorHasPredicate(edge.from) || !selectorHasPredicate(edge.to)) {
92
+ throw new Error(`[${source}] rule "${rule.id}": match.edge.from and match.edge.to must each include id, type, and/or tags`);
93
+ }
94
+ return (g) => {
95
+ const findings = [];
96
+ for (let edgeIndex = 0; edgeIndex < g.edges.length; edgeIndex++) {
97
+ const e = g.edges[edgeIndex];
98
+ if (!e || typeof e !== 'object')
99
+ continue;
100
+ const { from, to } = edgeEndpoints(e);
101
+ if (!from || !to)
102
+ continue;
103
+ const a = g.nodeById.get(from);
104
+ const b = g.nodeById.get(to);
105
+ if (!a || !b)
106
+ continue;
107
+ if (nodeMatchesSelector(a, edge.from) && nodeMatchesSelector(b, edge.to)) {
108
+ findings.push({
109
+ code: rule.id,
110
+ severity: rule.severity,
111
+ message: rule.message,
112
+ nodeId: to,
113
+ edgeIndex,
114
+ layer: 'lint',
115
+ fixHint: rule.fixHint,
116
+ });
117
+ }
118
+ }
119
+ return findings;
120
+ };
121
+ }
122
+ /**
123
+ * Load policy packs from in-memory file sources (same semantics as {@link loadPolicyPacksFromDirectory}).
124
+ * Use for ArchRad Cloud, tests, and API bodies — no filesystem required.
125
+ */
126
+ export function loadPolicyPacksFromFiles(sources) {
127
+ const errors = [];
128
+ const visitors = [];
129
+ const seenIds = new Set();
130
+ let ruleCount = 0;
131
+ const sorted = [...sources].sort((a, b) => a.name.localeCompare(b.name));
132
+ if (sorted.length === 0) {
133
+ return { ok: false, errors: ['no policy sources provided'] };
134
+ }
135
+ for (const src of sorted) {
136
+ const name = src.name?.trim() || 'unnamed';
137
+ try {
138
+ const doc = parseDocument(src.content, name);
139
+ for (const rule of doc.rules) {
140
+ if (seenIds.has(rule.id)) {
141
+ errors.push(`duplicate rule id "${rule.id}" (file ${name})`);
142
+ continue;
143
+ }
144
+ seenIds.add(rule.id);
145
+ visitors.push(compileRule(rule, name));
146
+ ruleCount += 1;
147
+ }
148
+ }
149
+ catch (e) {
150
+ errors.push(`${name}: ${e instanceof Error ? e.message : String(e)}`);
151
+ }
152
+ }
153
+ if (errors.length > 0) {
154
+ return { ok: false, errors };
155
+ }
156
+ return { ok: true, visitors, ruleCount };
157
+ }
158
+ function parseDocument(text, filename) {
159
+ const ext = filename.toLowerCase();
160
+ let data;
161
+ if (ext.endsWith('.json')) {
162
+ data = JSON.parse(text);
163
+ }
164
+ else if (ext.endsWith('.yaml') || ext.endsWith('.yml')) {
165
+ data = yaml.load(text);
166
+ }
167
+ else {
168
+ throw new Error(`unsupported policy file extension: ${filename}`);
169
+ }
170
+ if (!isNonEmptyRecord(data)) {
171
+ throw new Error('policy document must be a JSON object');
172
+ }
173
+ const doc = data;
174
+ if (doc.apiVersion !== 'archrad/v1') {
175
+ throw new Error(`apiVersion must be "archrad/v1" (got ${String(doc.apiVersion)})`);
176
+ }
177
+ if (doc.kind !== 'PolicyPack') {
178
+ throw new Error(`kind must be PolicyPack (got ${String(doc.kind)})`);
179
+ }
180
+ if (!Array.isArray(doc.rules) || doc.rules.length === 0) {
181
+ throw new Error('rules must be a non-empty array');
182
+ }
183
+ return doc;
184
+ }
185
+ /**
186
+ * Load and compile all policy YAML/JSON files in a directory into lint visitors.
187
+ * Filenames: `*.yaml`, `*.yml`, `*.json` (other files ignored).
188
+ */
189
+ export async function loadPolicyPacksFromDirectory(dir) {
190
+ const root = resolve(dir);
191
+ const errors = [];
192
+ let names;
193
+ try {
194
+ names = await readdir(root);
195
+ }
196
+ catch (e) {
197
+ const err = e;
198
+ return { ok: false, errors: [`cannot read policies directory ${root}: ${err.message}`] };
199
+ }
200
+ const policyFiles = names.filter((n) => /\.(yaml|yml|json)$/i.test(n)).sort();
201
+ if (policyFiles.length === 0) {
202
+ return { ok: false, errors: [`no policy files (*.yaml, *.yml, *.json) in ${root}`] };
203
+ }
204
+ const sources = [];
205
+ for (const name of policyFiles) {
206
+ const full = join(root, name);
207
+ try {
208
+ const text = await readFile(full, 'utf8');
209
+ sources.push({ name, content: text });
210
+ }
211
+ catch (e) {
212
+ const err = e;
213
+ errors.push(`${full}: ${err.message}`);
214
+ }
215
+ }
216
+ if (errors.length > 0) {
217
+ return { ok: false, errors };
218
+ }
219
+ return loadPolicyPacksFromFiles(sources);
220
+ }
@@ -3,6 +3,7 @@
3
3
  * or to an in-memory file map (Cloud API). No semantic IR↔code analysis — regen vs reality.
4
4
  */
5
5
  import { type DeterministicExportResult } from './exportPipeline.js';
6
+ import type { ValidateIrLintOptions } from './ir-lint.js';
6
7
  export type DriftCode = 'DRIFT-MISSING' | 'DRIFT-MODIFIED' | 'DRIFT-EXTRA' | 'DRIFT-NO-EXPORT';
7
8
  export type DriftFinding = {
8
9
  code: DriftCode;
@@ -35,6 +36,7 @@ export declare function runValidateDrift(actualIR: any, target: string, outDir:
35
36
  skipIrStructuralValidation?: boolean;
36
37
  skipIrLint?: boolean;
37
38
  strictExtra?: boolean;
39
+ policyRuleVisitors?: ValidateIrLintOptions['policyRuleVisitors'];
38
40
  }): Promise<ValidateDriftResult>;
39
41
  export type DriftCheckFilesResult = {
40
42
  ok: boolean;
@@ -50,5 +52,6 @@ export declare function runDriftCheckAgainstFiles(actualIR: any, target: string,
50
52
  skipIrStructuralValidation?: boolean;
51
53
  skipIrLint?: boolean;
52
54
  strictExtra?: boolean;
55
+ policyRuleVisitors?: ValidateIrLintOptions['policyRuleVisitors'];
53
56
  }): Promise<DriftCheckFilesResult>;
54
57
  //# sourceMappingURL=validate-drift.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validate-drift.d.ts","sourceRoot":"","sources":["../src/validate-drift.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,EAA0B,KAAK,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AAG7F,MAAM,MAAM,SAAS,GAAG,eAAe,GAAG,gBAAgB,GAAG,aAAa,GAAG,iBAAiB,CAAC;AAE/F,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAElE;AAED;;GAEG;AACH,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAChC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,OAAO,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,GAClC,YAAY,EAAE,CAkChB;AAiBD;;GAEG;AACH,wBAAsB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAQ/F;AAED,wBAAsB,kCAAkC,CACtD,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAChC,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,GAClC,OAAO,CAAC,YAAY,EAAE,CAAC,CAoCzB;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,EAAE,EAAE,OAAO,CAAC;IACZ,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B,6DAA6D;IAC7D,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,EAAE,yBAAyB,CAAC;CACzC,CAAC;AAEF,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,GAAG,EACb,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,IAAI,GAAE;IACJ,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;CAClB,GACL,OAAO,CAAC,mBAAmB,CAAC,CAyC9B;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,EAAE,EAAE,OAAO,CAAC;IACZ,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,EAAE,yBAAyB,CAAC;CACzC,CAAC;AAEF;;GAEG;AACH,wBAAsB,yBAAyB,CAC7C,QAAQ,EAAE,GAAG,EACb,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACnC,IAAI,GAAE;IACJ,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;CAClB,GACL,OAAO,CAAC,qBAAqB,CAAC,CAgChC"}
1
+ {"version":3,"file":"validate-drift.d.ts","sourceRoot":"","sources":["../src/validate-drift.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,EAA0B,KAAK,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AAC7F,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAG1D,MAAM,MAAM,SAAS,GAAG,eAAe,GAAG,gBAAgB,GAAG,aAAa,GAAG,iBAAiB,CAAC;AAE/F,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAElE;AAED;;GAEG;AACH,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAChC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,OAAO,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,GAClC,YAAY,EAAE,CAkChB;AAiBD;;GAEG;AACH,wBAAsB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAQ/F;AAED,wBAAsB,kCAAkC,CACtD,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAChC,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,GAClC,OAAO,CAAC,YAAY,EAAE,CAAC,CAoCzB;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,EAAE,EAAE,OAAO,CAAC;IACZ,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B,6DAA6D;IAC7D,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,EAAE,yBAAyB,CAAC;CACzC,CAAC;AAEF,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,GAAG,EACb,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,IAAI,GAAE;IACJ,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,kBAAkB,CAAC,EAAE,qBAAqB,CAAC,oBAAoB,CAAC,CAAC;CAC7D,GACL,OAAO,CAAC,mBAAmB,CAAC,CA0C9B;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,EAAE,EAAE,OAAO,CAAC;IACZ,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,EAAE,yBAAyB,CAAC;CACzC,CAAC;AAEF;;GAEG;AACH,wBAAsB,yBAAyB,CAC7C,QAAQ,EAAE,GAAG,EACb,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACnC,IAAI,GAAE;IACJ,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,kBAAkB,CAAC,EAAE,qBAAqB,CAAC,oBAAoB,CAAC,CAAC;CAC7D,GACL,OAAO,CAAC,qBAAqB,CAAC,CAiChC"}
@@ -118,6 +118,7 @@ export async function runValidateDrift(actualIR, target, outDir, opts = {}) {
118
118
  hostPort,
119
119
  skipIrStructuralValidation: Boolean(opts.skipIrStructuralValidation),
120
120
  skipIrLint: Boolean(opts.skipIrLint),
121
+ policyRuleVisitors: opts.policyRuleVisitors,
121
122
  });
122
123
  const { files } = exportResult;
123
124
  if (Object.keys(files).length === 0) {
@@ -157,6 +158,7 @@ export async function runDriftCheckAgainstFiles(actualIR, target, actualFiles, o
157
158
  hostPort,
158
159
  skipIrStructuralValidation: Boolean(opts.skipIrStructuralValidation),
159
160
  skipIrLint: Boolean(opts.skipIrLint),
161
+ policyRuleVisitors: opts.policyRuleVisitors,
160
162
  });
161
163
  const { files } = exportResult;
162
164
  if (Object.keys(files).length === 0) {
@@ -0,0 +1,11 @@
1
+ openapi: 3.0.0
2
+ info:
3
+ title: Test API
4
+ version: 1.0.0
5
+ paths:
6
+ /payments:
7
+ post:
8
+ operationId: createPayment
9
+ responses:
10
+ '200':
11
+ description: OK
@@ -0,0 +1,15 @@
1
+ # Demo pack for validate + --policies: matches checkout-api → orders-db in ecommerce-with-warnings.json
2
+ apiVersion: archrad/v1
3
+ kind: PolicyPack
4
+ metadata:
5
+ name: ecommerce-demo
6
+ rules:
7
+ - id: ORG-DIRECT-HTTP-DATASTORE-001
8
+ severity: warning
9
+ message: Policy pack demo — direct HTTP to datastore edge (for Show HN / CI smoke)
10
+ match:
11
+ edge:
12
+ from:
13
+ type: http
14
+ to:
15
+ type: postgres
@@ -0,0 +1,10 @@
1
+ apiVersion: archrad/v1
2
+ kind: PolicyPack
3
+ rules:
4
+ - id: DUP-RULE-001
5
+ severity: warning
6
+ message: first
7
+ match:
8
+ node:
9
+ type: http
10
+ tags: [a]
@@ -0,0 +1,10 @@
1
+ apiVersion: archrad/v1
2
+ kind: PolicyPack
3
+ rules:
4
+ - id: DUP-RULE-001
5
+ severity: warning
6
+ message: second duplicate
7
+ match:
8
+ node:
9
+ type: database
10
+ tags: [b]
@@ -0,0 +1,14 @@
1
+ # Flags HTTP nodes tagged deprecated (forbidden pattern — selector matches the violation)
2
+ apiVersion: archrad/v1
3
+ kind: PolicyPack
4
+ metadata:
5
+ name: sample-node-tags
6
+ rules:
7
+ - id: ORG-HTTP-DEPRECATED-001
8
+ severity: warning
9
+ message: Deprecated HTTP routes must not remain in the graph
10
+ fixHint: Remove the deprecated tag or replace the node
11
+ match:
12
+ node:
13
+ type: http
14
+ tags: [deprecated]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archrad/deterministic",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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",