@archrad/deterministic 0.1.3 → 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 +15 -0
- package/README.md +2 -0
- package/dist/cli.js +50 -2
- package/dist/exportPipeline.d.ts +3 -2
- package/dist/exportPipeline.d.ts.map +1 -1
- package/dist/exportPipeline.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/ir-lint.d.ts +7 -2
- package/dist/ir-lint.d.ts.map +1 -1
- package/dist/ir-lint.js +5 -3
- package/dist/ir-normalize.d.ts +2 -0
- package/dist/ir-normalize.d.ts.map +1 -1
- package/dist/ir-normalize.js +2 -1
- package/dist/lint-graph.d.ts.map +1 -1
- package/dist/lint-graph.js +1 -0
- package/dist/policy-pack.d.ts +62 -0
- package/dist/policy-pack.d.ts.map +1 -0
- package/dist/policy-pack.js +220 -0
- package/dist/validate-drift.d.ts +3 -0
- package/dist/validate-drift.d.ts.map +1 -1
- package/dist/validate-drift.js +2 -0
- package/fixtures/policies/ecommerce-demo.yaml +15 -0
- package/fixtures/policy-packs/duplicate-pack/first.yaml +10 -0
- package/fixtures/policy-packs/duplicate-pack/second.yaml +10 -0
- package/fixtures/policy-packs/sample-only/sample-node-tags.yaml +14 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ 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
|
+
|
|
10
25
|
## [0.1.3] - 2026-04-04
|
|
11
26
|
|
|
12
27
|
### Fixed
|
package/README.md
CHANGED
|
@@ -133,6 +133,8 @@ archrad validate --ir ./graph.json --fail-on-warning
|
|
|
133
133
|
archrad validate --ir ./graph.json --max-warnings 0
|
|
134
134
|
# Structural only (skip IR-LINT-*):
|
|
135
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
|
|
136
138
|
```
|
|
137
139
|
|
|
138
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';
|
|
@@ -51,6 +52,20 @@ function parseMaxWarnings(v) {
|
|
|
51
52
|
const n = parseInt(v, 10);
|
|
52
53
|
return Number.isFinite(n) ? n : undefined;
|
|
53
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
|
+
}
|
|
54
69
|
function exitPolicyFromOpts(opts) {
|
|
55
70
|
return {
|
|
56
71
|
failOnWarning: Boolean(opts.failOnWarning),
|
|
@@ -61,13 +76,14 @@ const program = new Command();
|
|
|
61
76
|
program
|
|
62
77
|
.name('archrad')
|
|
63
78
|
.description('Validate your architecture before you write code. Deterministic compiler + linter — FastAPI / Express (no LLM, no server).')
|
|
64
|
-
.version('0.1.
|
|
79
|
+
.version('0.1.4');
|
|
65
80
|
program
|
|
66
81
|
.command('validate')
|
|
67
82
|
.description('Validate your architecture before you write code — IR structural (IR-STRUCT-*) + architecture lint (IR-LINT-*)')
|
|
68
83
|
.requiredOption('-i, --ir <path>', 'Path to IR JSON (graph with nodes/edges or full wrapper)')
|
|
69
84
|
.option('--json', 'Print findings as JSON array to stdout')
|
|
70
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-*')
|
|
71
87
|
.option('--fail-on-warning', 'Exit with error if any warning (CI gate)')
|
|
72
88
|
.option('--max-warnings <n>', 'Exit with error if warning count is greater than n (e.g. 0 allows no warnings)')
|
|
73
89
|
.action(async (cmdOpts) => {
|
|
@@ -78,8 +94,19 @@ program
|
|
|
78
94
|
return;
|
|
79
95
|
}
|
|
80
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
|
+
}
|
|
81
106
|
const structural = validateIrStructural(ir);
|
|
82
|
-
const lint = noLint || hasIrStructuralErrors(structural)
|
|
107
|
+
const lint = noLint || hasIrStructuralErrors(structural)
|
|
108
|
+
? []
|
|
109
|
+
: validateIrLint(ir, lintOpts);
|
|
83
110
|
const combined = sortFindings([...structural, ...lint]);
|
|
84
111
|
if (cmdOpts.json) {
|
|
85
112
|
const forJson = combined.map((f) => ({
|
|
@@ -224,11 +251,21 @@ program
|
|
|
224
251
|
}
|
|
225
252
|
const exportOpts = cmdOpts;
|
|
226
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
|
+
}
|
|
227
263
|
try {
|
|
228
264
|
const { files, openApiStructuralWarnings, irStructuralFindings, irLintFindings } = await runDeterministicExport(actualIR, cmdOpts.target, {
|
|
229
265
|
hostPort,
|
|
230
266
|
skipIrStructuralValidation: skipStruct,
|
|
231
267
|
skipIrLint: cmdOpts.skipIrLint,
|
|
268
|
+
...exportLintOpts,
|
|
232
269
|
});
|
|
233
270
|
const combined = sortFindings([...irStructuralFindings, ...irLintFindings]);
|
|
234
271
|
if (combined.length) {
|
|
@@ -277,6 +314,7 @@ program
|
|
|
277
314
|
.addOption(new Option('--danger-skip-ir-structural-validation', 'UNSAFE: skip validateIrStructural during reference export'))
|
|
278
315
|
.addOption(new Option('--skip-ir-structural-validation', 'Deprecated alias').hideHelp())
|
|
279
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')
|
|
280
318
|
.option('--strict-extra', 'Fail if output directory contains files not in the reference export')
|
|
281
319
|
.option('--json', 'Print drift findings and export metadata as JSON')
|
|
282
320
|
.action(async (cmdOpts) => {
|
|
@@ -297,12 +335,22 @@ program
|
|
|
297
335
|
}
|
|
298
336
|
}
|
|
299
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
|
+
}
|
|
300
347
|
try {
|
|
301
348
|
const result = await runValidateDrift(actualIR, cmdOpts.target, outDir, {
|
|
302
349
|
hostPort,
|
|
303
350
|
skipIrStructuralValidation: skipStruct,
|
|
304
351
|
skipIrLint: cmdOpts.skipIrLint,
|
|
305
352
|
strictExtra: cmdOpts.strictExtra,
|
|
353
|
+
...driftLintOpts,
|
|
306
354
|
});
|
|
307
355
|
const combined = sortFindings([
|
|
308
356
|
...result.exportResult.irStructuralFindings,
|
package/dist/exportPipeline.d.ts
CHANGED
|
@@ -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-*
|
|
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;
|
|
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"}
|
package/dist/exportPipeline.js
CHANGED
|
@@ -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.
|
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';
|
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,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;
|
|
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
|
package/dist/ir-lint.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ir-lint.d.ts","sourceRoot":"","sources":["../src/ir-lint.ts"],"names":[],"mappings":"AAAA;;GAEG;
|
|
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
|
-
|
|
15
|
+
const base = runArchitectureLinting(built);
|
|
16
|
+
const extra = options?.policyRuleVisitors?.flatMap((v) => v(built)) ?? [];
|
|
17
|
+
return [...base, ...extra];
|
|
16
18
|
}
|
package/dist/ir-normalize.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/ir-normalize.js
CHANGED
|
@@ -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
|
/**
|
package/dist/lint-graph.d.ts.map
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/lint-graph.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/validate-drift.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/validate-drift.js
CHANGED
|
@@ -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,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,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.
|
|
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",
|