pumuki 6.3.9 → 6.3.11

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
@@ -118,6 +118,8 @@ The `pumuki` binary provides repository lifecycle operations:
118
118
  | `pumuki doctor` | Safety checks (hook drift, tracked `node_modules`, lifecycle state) |
119
119
  | `pumuki status` | Current lifecycle snapshot |
120
120
 
121
+ `pumuki remove` is dependency-safe by design: it never deletes non-Pumuki third-party dependencies and preserves pre-existing third-party empty directories.
122
+
121
123
  ## Gate Commands
122
124
 
123
125
  Dedicated gate binaries are available:
package/VERSION CHANGED
@@ -1 +1 @@
1
- v6.3.9
1
+ v6.3.11
@@ -153,7 +153,16 @@ Estado consolidado del refactor con seguimiento de tareas y evidencia del avance
153
153
  - ✅ Corregir `Quick Start` del `README.md` para consumo real por npm (`pumuki`) y comandos ejecutables de lifecycle/gates.
154
154
  - ✅ Auditar `README.md` con criterios enterprise (profesionalismo, claridad, estructura y completitud) y generar backlog de mejoras priorizado.
155
155
  - ✅ Reescribir `README.md` de forma integral con estándar enterprise (audiencia consumer/framework separada, comandos reales y estructura consistente).
156
- - 🚧 Ejecutar matriz E2E completa en `pumuki-mock-consumer` (`install -> pre-commit/pre-push/ci -> remove`) sobre escenarios `clean`, `violations` y `mixed`.
156
+ - Publicar `pumuki@6.3.9` en npm (tags `latest` y `next`) para reflejar la documentación enterprise reescrita.
157
+ - ✅ Ejecutar matriz E2E completa en `pumuki-mock-consumer` (`install -> pre-commit/pre-push/ci -> remove`) sobre escenarios `clean`, `violations` y `mixed`.
158
+ - ✅ Endurecer `pumuki-mock-consumer` con fixtures multiarchivo por plataforma y runner único `npm run pumuki:matrix`.
159
+ - ✅ Endurecer `pumuki remove` para podar residuos vacíos de `node_modules` sin borrar dependencias reales de terceros.
160
+ - ✅ Restringir poda de vacíos en `node_modules` a repos sin dependencias externas declaradas (seguridad enterprise reforzada).
161
+ - ✅ Publicar `pumuki@6.3.10` con hardening de desinstalación (`latest` y `next`).
162
+ - ✅ Refinar `pumuki remove` para eliminar vacíos nuevos tras uninstall manteniendo vacíos preexistentes de terceros.
163
+ - ✅ Endurecer `pumuki remove` para limpiar trazas del árbol de dependencias de Pumuki sin borrar dependencias ajenas (incluyendo vacíos no relacionados).
164
+ - 🚧 Publicar `pumuki@6.3.11` con la limpieza estricta de trazas y revalidar ciclo install/remove en consumidor mock.
165
+ - 🚧 Integrar MCP en `pumuki-mock-consumer` y validar consumo real de `ai_evidence` desde cliente MCP externo.
157
166
 
158
167
  ## Notas
159
168
  - Estrategia obligatoria: commits atómicos por tarea.
@@ -7,6 +7,8 @@ export type ConsumerDependencySource = 'dependencies' | 'devDependencies' | 'non
7
7
  type ConsumerPackageJson = {
8
8
  dependencies?: Record<string, string>;
9
9
  devDependencies?: Record<string, string>;
10
+ optionalDependencies?: Record<string, string>;
11
+ peerDependencies?: Record<string, string>;
10
12
  };
11
13
 
12
14
  const readPackageJson = (repoRoot: string): ConsumerPackageJson => {
@@ -46,3 +48,23 @@ export const resolveCurrentPumukiDependency = (repoRoot: string): {
46
48
  source: 'none',
47
49
  };
48
50
  };
51
+
52
+ export const hasDeclaredDependenciesBeyondPumuki = (repoRoot: string): boolean => {
53
+ const pkg = readPackageJson(repoRoot);
54
+ const pumukiPackage = getCurrentPumukiPackageName();
55
+ const ignoredPackages = new Set([pumukiPackage, 'pumuki-ast-hooks']);
56
+
57
+ const hasExternalDependency = (section?: Record<string, string>): boolean => {
58
+ if (!section) {
59
+ return false;
60
+ }
61
+ return Object.keys(section).some((dependencyName) => !ignoredPackages.has(dependencyName));
62
+ };
63
+
64
+ return (
65
+ hasExternalDependency(pkg.dependencies) ||
66
+ hasExternalDependency(pkg.devDependencies) ||
67
+ hasExternalDependency(pkg.optionalDependencies) ||
68
+ hasExternalDependency(pkg.peerDependencies)
69
+ );
70
+ };
@@ -1,5 +1,5 @@
1
- import { existsSync, readdirSync, rmSync, unlinkSync } from 'node:fs';
2
- import { join } from 'node:path';
1
+ import { existsSync, readFileSync, readdirSync, rmSync, unlinkSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
3
  import { resolveCurrentPumukiDependency } from './consumerPackage';
4
4
  import { LifecycleGitService, type ILifecycleGitService } from './gitService';
5
5
  import { LifecycleNpmService, type ILifecycleNpmService } from './npmService';
@@ -13,6 +13,138 @@ export type LifecycleRemoveResult = {
13
13
  removedArtifacts: ReadonlyArray<string>;
14
14
  };
15
15
 
16
+ type NodePackageManifest = {
17
+ dependencies?: Record<string, string>;
18
+ optionalDependencies?: Record<string, string>;
19
+ peerDependencies?: Record<string, string>;
20
+ };
21
+
22
+ const readManifestDependencyNames = (packageDirectory: string): ReadonlyArray<string> => {
23
+ const packageJsonPath = join(packageDirectory, 'package.json');
24
+ if (!existsSync(packageJsonPath)) {
25
+ return [];
26
+ }
27
+
28
+ try {
29
+ const manifest = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as NodePackageManifest;
30
+ const dependencyNames = new Set<string>();
31
+ const sections = [manifest.dependencies, manifest.optionalDependencies, manifest.peerDependencies];
32
+ for (const section of sections) {
33
+ if (!section) {
34
+ continue;
35
+ }
36
+ for (const dependencyName of Object.keys(section)) {
37
+ dependencyNames.add(dependencyName);
38
+ }
39
+ }
40
+ return Array.from(dependencyNames);
41
+ } catch {
42
+ return [];
43
+ }
44
+ };
45
+
46
+ const resolveInstalledDependencyDirectory = (params: {
47
+ dependencyName: string;
48
+ fromPackageDirectory: string;
49
+ nodeModulesPath: string;
50
+ }): string | undefined => {
51
+ const repoRoot = dirname(params.nodeModulesPath);
52
+ let currentDirectory = params.fromPackageDirectory;
53
+
54
+ while (currentDirectory.startsWith(repoRoot)) {
55
+ const candidate = join(currentDirectory, 'node_modules', params.dependencyName);
56
+ if (existsSync(join(candidate, 'package.json'))) {
57
+ return candidate;
58
+ }
59
+
60
+ if (currentDirectory === repoRoot) {
61
+ break;
62
+ }
63
+
64
+ const parentDirectory = dirname(currentDirectory);
65
+ if (parentDirectory === currentDirectory) {
66
+ break;
67
+ }
68
+ currentDirectory = parentDirectory;
69
+ }
70
+
71
+ const topLevelCandidate = join(params.nodeModulesPath, params.dependencyName);
72
+ if (existsSync(join(topLevelCandidate, 'package.json'))) {
73
+ return topLevelCandidate;
74
+ }
75
+
76
+ return undefined;
77
+ };
78
+
79
+ const collectPumukiTraceDirectories = (params: {
80
+ repoRoot: string;
81
+ packageName: string;
82
+ }): ReadonlySet<string> => {
83
+ const nodeModulesPath = join(params.repoRoot, 'node_modules');
84
+ const rootPackageDirectory = join(nodeModulesPath, params.packageName);
85
+ if (!existsSync(join(rootPackageDirectory, 'package.json'))) {
86
+ return new Set<string>();
87
+ }
88
+
89
+ const traceDirectories = new Set<string>();
90
+ const pending = [rootPackageDirectory];
91
+
92
+ while (pending.length > 0) {
93
+ const packageDirectory = pending.pop();
94
+ if (!packageDirectory || traceDirectories.has(packageDirectory)) {
95
+ continue;
96
+ }
97
+ traceDirectories.add(packageDirectory);
98
+
99
+ const dependencyNames = readManifestDependencyNames(packageDirectory);
100
+ for (const dependencyName of dependencyNames) {
101
+ const resolvedDependencyDirectory = resolveInstalledDependencyDirectory({
102
+ dependencyName,
103
+ fromPackageDirectory: packageDirectory,
104
+ nodeModulesPath,
105
+ });
106
+ if (!resolvedDependencyDirectory) {
107
+ continue;
108
+ }
109
+ pending.push(resolvedDependencyDirectory);
110
+ }
111
+ }
112
+
113
+ return traceDirectories;
114
+ };
115
+
116
+ type EmptyDirectoryCleanupResult = 'removed' | 'missing' | 'not-empty';
117
+
118
+ const removeDirectoryIfEmpty = (directoryPath: string): EmptyDirectoryCleanupResult => {
119
+ if (!existsSync(directoryPath)) {
120
+ return 'missing';
121
+ }
122
+
123
+ if (readdirSync(directoryPath).length > 0) {
124
+ return 'not-empty';
125
+ }
126
+
127
+ rmSync(directoryPath, { recursive: true, force: true });
128
+ return 'removed';
129
+ };
130
+
131
+ const cleanupTraceAncestors = (params: {
132
+ tracePath: string;
133
+ nodeModulesPath: string;
134
+ }): void => {
135
+ let currentDirectory = dirname(params.tracePath);
136
+ while (
137
+ currentDirectory.startsWith(params.nodeModulesPath) &&
138
+ currentDirectory !== params.nodeModulesPath
139
+ ) {
140
+ const cleanupResult = removeDirectoryIfEmpty(currentDirectory);
141
+ if (cleanupResult === 'not-empty') {
142
+ break;
143
+ }
144
+ currentDirectory = dirname(currentDirectory);
145
+ }
146
+ };
147
+
16
148
  const cleanupNodeModulesIfOnlyLockfile = (repoRoot: string): void => {
17
149
  const nodeModulesPath = join(repoRoot, 'node_modules');
18
150
  if (!existsSync(nodeModulesPath)) {
@@ -39,8 +171,7 @@ const cleanupNodeModulesIfOnlyLockfile = (repoRoot: string): void => {
39
171
  const binEntry = entries.find((entry) => entry.name === '.bin' && entry.isDirectory());
40
172
  if (binEntry) {
41
173
  const binPath = join(nodeModulesPath, '.bin');
42
- const binEntries = readdirSync(binPath);
43
- if (binEntries.length > 0) {
174
+ if (readdirSync(binPath).length > 0) {
44
175
  return;
45
176
  }
46
177
  rmSync(binPath, { recursive: true, force: true });
@@ -55,6 +186,30 @@ const cleanupNodeModulesIfOnlyLockfile = (repoRoot: string): void => {
55
186
  }
56
187
  };
57
188
 
189
+ const cleanupPumukiTraceDirectories = (params: {
190
+ repoRoot: string;
191
+ traceDirectories: ReadonlySet<string>;
192
+ }): void => {
193
+ const nodeModulesPath = join(params.repoRoot, 'node_modules');
194
+ if (!existsSync(nodeModulesPath)) {
195
+ return;
196
+ }
197
+
198
+ const orderedTraceDirectories = Array.from(params.traceDirectories).sort(
199
+ (left, right) => right.length - left.length
200
+ );
201
+
202
+ for (const traceDirectory of orderedTraceDirectories) {
203
+ removeDirectoryIfEmpty(traceDirectory);
204
+ cleanupTraceAncestors({
205
+ tracePath: traceDirectory,
206
+ nodeModulesPath,
207
+ });
208
+ }
209
+
210
+ cleanupNodeModulesIfOnlyLockfile(params.repoRoot);
211
+ };
212
+
58
213
  export const runLifecycleRemove = (params?: {
59
214
  cwd?: string;
60
215
  git?: ILifecycleGitService;
@@ -72,9 +227,16 @@ export const runLifecycleRemove = (params?: {
72
227
 
73
228
  const currentDependency = resolveCurrentPumukiDependency(repoRoot);
74
229
  const packageName = getCurrentPumukiPackageName();
230
+ const pumukiTraceDirectories = collectPumukiTraceDirectories({
231
+ repoRoot,
232
+ packageName,
233
+ });
75
234
 
76
235
  if (currentDependency.source === 'none') {
77
- cleanupNodeModulesIfOnlyLockfile(repoRoot);
236
+ cleanupPumukiTraceDirectories({
237
+ repoRoot,
238
+ traceDirectories: pumukiTraceDirectories,
239
+ });
78
240
  return {
79
241
  repoRoot,
80
242
  packageRemoved: false,
@@ -84,7 +246,10 @@ export const runLifecycleRemove = (params?: {
84
246
  }
85
247
 
86
248
  npm.runNpm(['uninstall', packageName], repoRoot);
87
- cleanupNodeModulesIfOnlyLockfile(repoRoot);
249
+ cleanupPumukiTraceDirectories({
250
+ repoRoot,
251
+ traceDirectories: pumukiTraceDirectories,
252
+ });
88
253
 
89
254
  return {
90
255
  repoRoot,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki",
3
- "version": "6.3.9",
3
+ "version": "6.3.11",
4
4
  "description": "Enterprise-grade AST Intelligence System with multi-platform support (iOS, Android, Backend, Frontend) and Feature-First + DDD + Clean Architecture enforcement. Includes dynamic violations API for intelligent querying.",
5
5
  "main": "index.js",
6
6
  "bin": {