@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 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.1.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) requires exact versions. For each supported app, the CLI checks direct `package.json` declarations **and** matching npm/pnpm lockfile resolution. It returns exit `7` on drift and exit `3` for invalid input. The result lists the approved, declared, and locked versions.
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
- if (declared !== expected) reasons.push("manifest version differs");
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 !== expected) reasons.push("lockfile resolution differs");
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 !== 1) {
904
- throw new Error("Policy schemaVersion must be 1");
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" || !exactVersion.test(version)) {
920
- throw new Error(`Policy exact version required for ${packageName} in ${name}`);
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: 1,
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.1.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} expected ${finding.expected}; declared ${finding.declared ?? "missing"}; locked ${finding.locked ?? "missing"} (${finding.reason}).
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: 1;
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 !== 1) {
17
- throw new Error("Policy schemaVersion must be 1");
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" || !exactVersion.test(version)) {
33
- throw new Error(`Policy exact version required for ${packageName} in ${name}`);
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: 1,
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
- if (declared !== expected) reasons.push("manifest version differs");
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 !== expected) reasons.push("lockfile resolution differs");
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
  });
@@ -1,23 +1,23 @@
1
1
  {
2
- "schemaVersion": 1,
3
- "policyVersion": "1.0.0",
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.1.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
  }