pi-aia-asf 0.17.1 → 0.17.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,13 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ## [0.17.1] - 2026-11-07
10
+ ## [0.17.3] - 2026-11-07
11
11
 
12
12
  ### Added
13
13
 
14
- - (describe changes for 0.17.1)
14
+ - (describe changes for 0.17.3)
15
+
16
+ ## [0.17.2] - 2026-11-07
17
+
18
+ ### Fixed — the tools were installed but never found
15
19
 
16
- ## [0.17.0] - 2026-11-07
20
+ 0.17.1 declared the toolchain as dependencies and then looked for it in
21
+ `<pi-aia-asf>/node_modules`, which does not exist in the layout pi installs: npm
22
+ **hoists** a package's dependencies into the shared `node_modules` one level up.
23
+ Every tool therefore read as missing, the gate fell back to a system `eslint` (v8,
24
+ which rejects `--no-config-lookup`), and eight metrics came back as errors — the
25
+ same blindness as before, with a different message.
26
+
27
+ - Resolution now walks the real `node_modules` chain from ASF's package directory
28
+ upward, exactly as node resolves, so a hoisted install is found. `require.resolve`
29
+ is only a last resort: on Debian it escaped the install entirely and returned
30
+ `/usr/share/nodejs/eslint`.
31
+ - Packages with no `main` field (jscpd) can never be found by `require.resolve`;
32
+ the directory walk finds them.
33
+ - A regression test builds the hoisted layout on disk and asserts all five tools
34
+ resolve inside the install prefix, with ASF's copy winning over PATH.
35
+
36
+ ## [0.17.1] - 2026-11-07
17
37
 
18
38
  ### Added — the health gate ships its own toolchain
19
39
 
@@ -14,9 +14,10 @@
14
14
  * 3. PATH — a global install
15
15
  */
16
16
 
17
- import { existsSync, statSync } from "node:fs";
17
+ import { existsSync, statSync, readFileSync } from "node:fs";
18
18
  import { join, dirname } from "node:path";
19
19
  import { fileURLToPath } from "node:url";
20
+ import { createRequire } from "node:module";
20
21
 
21
22
  /** ASF's package root (this file is <root>/lib/code-health/tools.mjs). */
22
23
  export const ASF_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
@@ -38,14 +39,66 @@ function isExecutable(p) {
38
39
  }
39
40
  }
40
41
 
41
- function findBin(base, name) {
42
- const p = join(base, "node_modules", ".bin", name);
43
- return isExecutable(p) ? p : null;
42
+ /**
43
+ * Locate an installed package's root directory, the way node itself would.
44
+ *
45
+ * A literal `<base>/node_modules/<name>` check is wrong for the layout pi
46
+ * actually installs: npm HOISTS pi-aia-asf's dependencies into a shared
47
+ * node_modules one level up, so the package dir has no node_modules of its own
48
+ * and every tool looked missing — which silently fell through to a system eslint
49
+ * (v8, no flat config) and to eight skips again. require.resolve walks up the
50
+ * real module paths, so it finds the hoisted copy.
51
+ */
52
+ function findPkgDir(base, name) {
53
+ // The ancestor walk comes FIRST: it is exactly node's own node_modules
54
+ // resolution, and require.resolve can escape the install entirely (a Debian
55
+ // NODE_PATH found /usr/share/nodejs/eslint before the copy that shipped with
56
+ // pi-aia-asf — the gate would then measure with eslint 8 and fail to run).
57
+ let dir2 = base;
58
+ for (let i = 0; i < 8; i++) {
59
+ const candidate = join(dir2, "node_modules", name);
60
+ if (existsSync(join(candidate, "package.json"))) {
61
+ try {
62
+ return { dir: candidate, meta: JSON.parse(readFileSync(join(candidate, "package.json"), "utf-8")) };
63
+ } catch { return { dir: candidate, meta: {} }; }
64
+ }
65
+ const up = dirname(dir2);
66
+ if (up === dir2) break;
67
+ dir2 = up;
68
+ }
69
+ try {
70
+ const req = createRequire(join(base, "package.json"));
71
+ let dir = dirname(req.resolve(name));
72
+ for (let i = 0; i < 6; i++) {
73
+ const pj = join(dir, "package.json");
74
+ if (existsSync(pj)) {
75
+ try {
76
+ const meta = JSON.parse(readFileSync(pj, "utf-8"));
77
+ if (meta.name === name) return { dir, meta };
78
+ } catch { /* keep walking */ }
79
+ }
80
+ const up = dirname(dir);
81
+ if (up === dir) break;
82
+ dir = up;
83
+ }
84
+ } catch { /* packages with no `main` (jscpd) cannot be resolved by name */ }
85
+ const literal = join(base, "node_modules", name);
86
+ if (existsSync(literal)) {
87
+ try {
88
+ return { dir: literal, meta: JSON.parse(readFileSync(join(literal, "package.json"), "utf-8")) };
89
+ } catch { return { dir: literal, meta: {} }; }
90
+ }
91
+ return null;
44
92
  }
45
93
 
46
- function findPkg(base, name) {
47
- const p = join(base, "node_modules", name);
48
- return existsSync(p) ? p : null;
94
+ /** The executable a package declares in its own `bin` field. */
95
+ function binFromPkg(found, binName) {
96
+ if (!found) return null;
97
+ const bin = found.meta?.bin;
98
+ const rel = typeof bin === "string" ? bin : bin?.[binName];
99
+ if (!rel) return null;
100
+ const p = join(found.dir, rel);
101
+ return isExecutable(p) ? p : null;
49
102
  }
50
103
 
51
104
  function findOnPath(name) {
@@ -67,9 +120,9 @@ function findOnPath(name) {
67
120
  * bins:Record<string,string|null>, pkgs:Record<string,string|null>,
68
121
  * sources:Record<string,string>}}
69
122
  */
70
- export function resolveTools(projectRoot) {
123
+ export function resolveTools(projectRoot, { asfRoot = ASF_ROOT } = {}) {
71
124
  const bases = [
72
- { dir: ASF_ROOT, source: "asf" },
125
+ { dir: asfRoot, source: "asf" },
73
126
  { dir: projectRoot, source: "project" },
74
127
  ];
75
128
  const bins = {};
@@ -80,7 +133,7 @@ export function resolveTools(projectRoot) {
80
133
  if (spec.bin) {
81
134
  bins[key] = null;
82
135
  for (const b of bases) {
83
- const found = findBin(b.dir, spec.bin);
136
+ const found = binFromPkg(findPkgDir(b.dir, spec.pkg), spec.bin);
84
137
  if (found) {
85
138
  bins[key] = found;
86
139
  sources[key] = b.source;
@@ -98,9 +151,9 @@ export function resolveTools(projectRoot) {
98
151
  if (spec.pkg) {
99
152
  pkgs[key] = null;
100
153
  for (const b of bases) {
101
- const found = findPkg(b.dir, spec.pkg);
154
+ const found = findPkgDir(b.dir, spec.pkg);
102
155
  if (found) {
103
- pkgs[key] = found;
156
+ pkgs[key] = found.dir;
104
157
  if (!sources[key]) sources[key] = b.source;
105
158
  break;
106
159
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-aia-asf",
3
- "version": "0.17.1",
3
+ "version": "0.17.3",
4
4
  "description": "Ai Applied Agentic Software Factory — codifies the full software development flow: intake, research, spec capture, adversarial analysis, planning with approval gates, test-first implementation, and release. Requires pi-vigilant, pi-smart-web-search, pi-smart-fetch, and pi-aia-browser.",
5
5
  "keywords": [
6
6
  "pi-package",