@anhcodo13/lattice 0.1.0 → 0.2.0
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/README.md +4 -2
- package/dist/cli.js +24 -10
- package/dist/index.d.ts +2 -1
- package/dist/index.js +22 -8
- package/governance/policy.json +13 -13
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ NKLattice is an independent, local CLI for architecture analysis and dependency
|
|
|
9
9
|
Requires Node.js 24 or newer. The npm package is `@anhcodo13/lattice`; the executable is `lattice`.
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
npm install --global @anhcodo13/lattice@0.
|
|
12
|
+
npm install --global @anhcodo13/lattice@0.2.0
|
|
13
13
|
lattice --version
|
|
14
14
|
lattice governance check path/to/app --expect-apps 1
|
|
15
15
|
```
|
|
@@ -44,7 +44,9 @@ The score is an explainable review aid, not a claim about runtime speed or corre
|
|
|
44
44
|
|
|
45
45
|
## Dependency governance
|
|
46
46
|
|
|
47
|
-
The versioned [central policy](governance/policy.json)
|
|
47
|
+
The versioned [central policy](governance/policy.json) sets minimum versions (`>=X.Y.Z`). For each supported app, the CLI checks that its direct `package.json` range cannot install below the minimum, that the npm/pnpm lockfile specifier matches the declaration, and that the resolved version satisfies both. Exact declarations are allowed when they meet the minimum. It returns exit `7` on drift and exit `3` for invalid input. Findings list the required range, declared range, lockfile specifier, locked version, and reason. A minimum has no upper bound: review framework compatibility and security separately before adopting newer major releases.
|
|
48
|
+
|
|
49
|
+
CLI 0.2.0 requires policy schema 2. Existing schema 1 files use exact-version rules and need migration; consumers pinned to an older core SHA or npm package keep their existing behavior until explicitly upgraded.
|
|
48
50
|
|
|
49
51
|
Profiles are `next`, `vite-react`, and `react`. NKLattice can infer them from direct dependencies or read an explicit app profile:
|
|
50
52
|
|
package/dist/cli.js
CHANGED
|
@@ -663,6 +663,7 @@ async function analyzeProject(projectRoot, options = {}) {
|
|
|
663
663
|
|
|
664
664
|
// src/governance/check.ts
|
|
665
665
|
import { resolve as resolve5, relative as relative4, sep as sep3 } from "path";
|
|
666
|
+
import { satisfies, subset, valid, validRange } from "semver";
|
|
666
667
|
|
|
667
668
|
// src/governance/discover.ts
|
|
668
669
|
import { readFile as readFile5 } from "fs/promises";
|
|
@@ -861,15 +862,26 @@ async function checkGovernance(options) {
|
|
|
861
862
|
const declared = app.dependencies[packageName];
|
|
862
863
|
const resolution = declared === void 0 ? { specifier: void 0, version: void 0 } : await readResolution(app, workspaceRoot, packageName);
|
|
863
864
|
const reasons = [];
|
|
864
|
-
|
|
865
|
+
const declaredRange = declared === void 0 ? null : validRange(declared);
|
|
866
|
+
if (declared === void 0) reasons.push("required manifest declaration missing");
|
|
867
|
+
else if (declaredRange === null) reasons.push("unsupported manifest range");
|
|
868
|
+
else if (!subset(declaredRange, expected)) reasons.push("manifest range allows versions below minimum");
|
|
865
869
|
if (resolution.specifier !== declared) reasons.push("lockfile specifier differs");
|
|
866
|
-
if (resolution.version
|
|
870
|
+
if (resolution.version === void 0) reasons.push("lockfile resolution missing");
|
|
871
|
+
else if (!valid(resolution.version)) reasons.push("invalid lockfile resolution");
|
|
872
|
+
else {
|
|
873
|
+
if (!satisfies(resolution.version, expected)) reasons.push("lockfile resolution does not satisfy minimum");
|
|
874
|
+
if (declaredRange !== null && !satisfies(resolution.version, declaredRange)) {
|
|
875
|
+
reasons.push("lockfile resolution outside manifest range");
|
|
876
|
+
}
|
|
877
|
+
}
|
|
867
878
|
if (reasons.length) {
|
|
868
879
|
findings.push({
|
|
869
880
|
app: app.relativePath,
|
|
870
881
|
packageName,
|
|
871
882
|
expected,
|
|
872
883
|
declared,
|
|
884
|
+
lockfileSpecifier: resolution.specifier,
|
|
873
885
|
locked: resolution.version,
|
|
874
886
|
reason: reasons.join("; ")
|
|
875
887
|
});
|
|
@@ -889,7 +901,9 @@ async function checkGovernance(options) {
|
|
|
889
901
|
import { createHash } from "crypto";
|
|
890
902
|
import { readFile as readFile7 } from "fs/promises";
|
|
891
903
|
import { resolve as resolve6 } from "path";
|
|
904
|
+
import { valid as valid2 } from "semver";
|
|
892
905
|
var exactVersion = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
906
|
+
var minimumVersion = /^>=(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
893
907
|
function record3(value) {
|
|
894
908
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
895
909
|
}
|
|
@@ -900,10 +914,10 @@ function parsePolicy(raw, source) {
|
|
|
900
914
|
} catch {
|
|
901
915
|
throw new Error(`Policy at ${source} is not valid JSON`);
|
|
902
916
|
}
|
|
903
|
-
if (!record3(parsed) || parsed.schemaVersion !==
|
|
904
|
-
throw new Error("Policy schemaVersion must be
|
|
917
|
+
if (!record3(parsed) || parsed.schemaVersion !== 2) {
|
|
918
|
+
throw new Error("Policy schemaVersion must be 2");
|
|
905
919
|
}
|
|
906
|
-
if (typeof parsed.policyVersion !== "string" || !exactVersion.test(parsed.policyVersion)) {
|
|
920
|
+
if (typeof parsed.policyVersion !== "string" || !exactVersion.test(parsed.policyVersion) || valid2(parsed.policyVersion) === null) {
|
|
907
921
|
throw new Error("Policy policyVersion must be an exact version");
|
|
908
922
|
}
|
|
909
923
|
if (!record3(parsed.profiles) || Object.keys(parsed.profiles).length === 0) {
|
|
@@ -916,15 +930,15 @@ function parsePolicy(raw, source) {
|
|
|
916
930
|
}
|
|
917
931
|
const versions = {};
|
|
918
932
|
for (const [packageName, version] of Object.entries(rules)) {
|
|
919
|
-
if (typeof version !== "string" || !
|
|
920
|
-
throw new Error(`Policy
|
|
933
|
+
if (typeof version !== "string" || !minimumVersion.test(version) || valid2(version.slice(2)) === null) {
|
|
934
|
+
throw new Error(`Policy minimum version (>=X.Y.Z) required for ${packageName} in ${name}`);
|
|
921
935
|
}
|
|
922
936
|
versions[packageName] = version;
|
|
923
937
|
}
|
|
924
938
|
profiles2[name] = versions;
|
|
925
939
|
}
|
|
926
940
|
return {
|
|
927
|
-
schemaVersion:
|
|
941
|
+
schemaVersion: 2,
|
|
928
942
|
policyVersion: parsed.policyVersion,
|
|
929
943
|
profiles: profiles2,
|
|
930
944
|
source,
|
|
@@ -967,7 +981,7 @@ async function runCli(arguments_, output = defaultOutput) {
|
|
|
967
981
|
}
|
|
968
982
|
let exitCode = 0;
|
|
969
983
|
const program = new Command();
|
|
970
|
-
program.name("lattice").description("Architecture analysis and dependency governance for React teams").version("0.
|
|
984
|
+
program.name("lattice").description("Architecture analysis and dependency governance for React teams").version("0.2.0");
|
|
971
985
|
program.configureOutput({ writeOut: output.out, writeErr: output.err });
|
|
972
986
|
program.exitOverride();
|
|
973
987
|
program.command("analyze [path]").description("Analyze local architecture and write offline reports").option("--output <directory>", "Output directory").action(async (path, flags) => {
|
|
@@ -1018,7 +1032,7 @@ async function runCli(arguments_, output = defaultOutput) {
|
|
|
1018
1032
|
output.out(`${result.appsChecked} application${result.appsChecked === 1 ? "" : "s"} checked; ${result.findings.length} dependency mismatch${result.findings.length === 1 ? "" : "es"}.
|
|
1019
1033
|
`);
|
|
1020
1034
|
for (const finding of result.findings) {
|
|
1021
|
-
output.out(`${finding.app}: ${finding.packageName}
|
|
1035
|
+
output.out(`${finding.app}: ${finding.packageName} required ${finding.expected}; declared ${finding.declared ?? "missing"}; lockfile specifier ${finding.lockfileSpecifier ?? "missing"}; locked ${finding.locked ?? "missing"} (${finding.reason}).
|
|
1022
1036
|
`);
|
|
1023
1037
|
}
|
|
1024
1038
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
type ProfileName = "next" | "vite-react" | "react";
|
|
2
2
|
interface VersionPolicy {
|
|
3
|
-
schemaVersion:
|
|
3
|
+
schemaVersion: 2;
|
|
4
4
|
policyVersion: string;
|
|
5
5
|
profiles: Record<string, Record<string, string>>;
|
|
6
6
|
source: string;
|
|
@@ -11,6 +11,7 @@ interface GovernanceFinding {
|
|
|
11
11
|
packageName: string;
|
|
12
12
|
expected: string;
|
|
13
13
|
declared?: string;
|
|
14
|
+
lockfileSpecifier?: string;
|
|
14
15
|
locked?: string;
|
|
15
16
|
reason: string;
|
|
16
17
|
}
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
import { createHash } from "crypto";
|
|
3
3
|
import { readFile } from "fs/promises";
|
|
4
4
|
import { resolve } from "path";
|
|
5
|
+
import { valid } from "semver";
|
|
5
6
|
var exactVersion = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
7
|
+
var minimumVersion = /^>=(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
6
8
|
function record(value) {
|
|
7
9
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
10
|
}
|
|
@@ -13,10 +15,10 @@ function parsePolicy(raw, source) {
|
|
|
13
15
|
} catch {
|
|
14
16
|
throw new Error(`Policy at ${source} is not valid JSON`);
|
|
15
17
|
}
|
|
16
|
-
if (!record(parsed) || parsed.schemaVersion !==
|
|
17
|
-
throw new Error("Policy schemaVersion must be
|
|
18
|
+
if (!record(parsed) || parsed.schemaVersion !== 2) {
|
|
19
|
+
throw new Error("Policy schemaVersion must be 2");
|
|
18
20
|
}
|
|
19
|
-
if (typeof parsed.policyVersion !== "string" || !exactVersion.test(parsed.policyVersion)) {
|
|
21
|
+
if (typeof parsed.policyVersion !== "string" || !exactVersion.test(parsed.policyVersion) || valid(parsed.policyVersion) === null) {
|
|
20
22
|
throw new Error("Policy policyVersion must be an exact version");
|
|
21
23
|
}
|
|
22
24
|
if (!record(parsed.profiles) || Object.keys(parsed.profiles).length === 0) {
|
|
@@ -29,15 +31,15 @@ function parsePolicy(raw, source) {
|
|
|
29
31
|
}
|
|
30
32
|
const versions = {};
|
|
31
33
|
for (const [packageName, version] of Object.entries(rules)) {
|
|
32
|
-
if (typeof version !== "string" || !
|
|
33
|
-
throw new Error(`Policy
|
|
34
|
+
if (typeof version !== "string" || !minimumVersion.test(version) || valid(version.slice(2)) === null) {
|
|
35
|
+
throw new Error(`Policy minimum version (>=X.Y.Z) required for ${packageName} in ${name}`);
|
|
34
36
|
}
|
|
35
37
|
versions[packageName] = version;
|
|
36
38
|
}
|
|
37
39
|
profiles2[name] = versions;
|
|
38
40
|
}
|
|
39
41
|
return {
|
|
40
|
-
schemaVersion:
|
|
42
|
+
schemaVersion: 2,
|
|
41
43
|
policyVersion: parsed.policyVersion,
|
|
42
44
|
profiles: profiles2,
|
|
43
45
|
source,
|
|
@@ -63,6 +65,7 @@ async function loadPolicy(source) {
|
|
|
63
65
|
|
|
64
66
|
// src/governance/check.ts
|
|
65
67
|
import { resolve as resolve3, relative as relative3, sep as sep2 } from "path";
|
|
68
|
+
import { satisfies, subset, valid as valid2, validRange } from "semver";
|
|
66
69
|
|
|
67
70
|
// src/governance/discover.ts
|
|
68
71
|
import { readFile as readFile2 } from "fs/promises";
|
|
@@ -261,15 +264,26 @@ async function checkGovernance(options) {
|
|
|
261
264
|
const declared = app.dependencies[packageName];
|
|
262
265
|
const resolution = declared === void 0 ? { specifier: void 0, version: void 0 } : await readResolution(app, workspaceRoot, packageName);
|
|
263
266
|
const reasons = [];
|
|
264
|
-
|
|
267
|
+
const declaredRange = declared === void 0 ? null : validRange(declared);
|
|
268
|
+
if (declared === void 0) reasons.push("required manifest declaration missing");
|
|
269
|
+
else if (declaredRange === null) reasons.push("unsupported manifest range");
|
|
270
|
+
else if (!subset(declaredRange, expected)) reasons.push("manifest range allows versions below minimum");
|
|
265
271
|
if (resolution.specifier !== declared) reasons.push("lockfile specifier differs");
|
|
266
|
-
if (resolution.version
|
|
272
|
+
if (resolution.version === void 0) reasons.push("lockfile resolution missing");
|
|
273
|
+
else if (!valid2(resolution.version)) reasons.push("invalid lockfile resolution");
|
|
274
|
+
else {
|
|
275
|
+
if (!satisfies(resolution.version, expected)) reasons.push("lockfile resolution does not satisfy minimum");
|
|
276
|
+
if (declaredRange !== null && !satisfies(resolution.version, declaredRange)) {
|
|
277
|
+
reasons.push("lockfile resolution outside manifest range");
|
|
278
|
+
}
|
|
279
|
+
}
|
|
267
280
|
if (reasons.length) {
|
|
268
281
|
findings.push({
|
|
269
282
|
app: app.relativePath,
|
|
270
283
|
packageName,
|
|
271
284
|
expected,
|
|
272
285
|
declared,
|
|
286
|
+
lockfileSpecifier: resolution.specifier,
|
|
273
287
|
locked: resolution.version,
|
|
274
288
|
reason: reasons.join("; ")
|
|
275
289
|
});
|
package/governance/policy.json
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
{
|
|
2
|
-
"schemaVersion":
|
|
3
|
-
"policyVersion": "
|
|
2
|
+
"schemaVersion": 2,
|
|
3
|
+
"policyVersion": "2.0.0",
|
|
4
4
|
"profiles": {
|
|
5
5
|
"next": {
|
|
6
|
-
"next": "15.5.6",
|
|
7
|
-
"react": "19.2.0",
|
|
8
|
-
"react-dom": "19.2.0",
|
|
9
|
-
"typescript": "5.9.3"
|
|
6
|
+
"next": ">=15.5.6",
|
|
7
|
+
"react": ">=19.2.0",
|
|
8
|
+
"react-dom": ">=19.2.0",
|
|
9
|
+
"typescript": ">=5.9.3"
|
|
10
10
|
},
|
|
11
11
|
"vite-react": {
|
|
12
|
-
"vite": "6.4.3",
|
|
13
|
-
"react": "19.2.0",
|
|
14
|
-
"react-dom": "19.2.0",
|
|
15
|
-
"typescript": "5.9.3"
|
|
12
|
+
"vite": ">=6.4.3",
|
|
13
|
+
"react": ">=19.2.0",
|
|
14
|
+
"react-dom": ">=19.2.0",
|
|
15
|
+
"typescript": ">=5.9.3"
|
|
16
16
|
},
|
|
17
17
|
"react": {
|
|
18
|
-
"react": "19.2.0",
|
|
19
|
-
"react-dom": "19.2.0",
|
|
20
|
-
"typescript": "5.9.3"
|
|
18
|
+
"react": ">=19.2.0",
|
|
19
|
+
"react-dom": ">=19.2.0",
|
|
20
|
+
"typescript": ">=5.9.3"
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anhcodo13/lattice",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"packageManager": "pnpm@10.30.3",
|
|
6
6
|
"type": "module",
|
|
@@ -25,11 +25,13 @@
|
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"commander": "^14.0.0",
|
|
27
27
|
"fast-glob": "^3.3.3",
|
|
28
|
+
"semver": "7.8.5",
|
|
28
29
|
"typescript": "5.9.3",
|
|
29
30
|
"yaml": "^2.9.1"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
33
|
"@types/node": "^24.0.0",
|
|
34
|
+
"@types/semver": "^7.8.0",
|
|
33
35
|
"tsup": "^8.5.0",
|
|
34
36
|
"vitest": "^3.2.0"
|
|
35
37
|
}
|