pumuki 6.3.56 → 6.3.58

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.
Files changed (48) hide show
  1. package/VERSION +1 -1
  2. package/core/facts/HeuristicFact.test.ts +28 -0
  3. package/core/facts/HeuristicFact.ts +11 -0
  4. package/core/facts/detectors/text/android.test.ts +207 -0
  5. package/core/facts/detectors/text/android.ts +765 -0
  6. package/core/facts/detectors/text/ios.test.ts +207 -0
  7. package/core/facts/detectors/text/ios.ts +848 -0
  8. package/core/facts/detectors/typescript/index.test.ts +447 -0
  9. package/core/facts/detectors/typescript/index.ts +1214 -54
  10. package/core/facts/extractHeuristicFacts.ts +285 -1
  11. package/core/gate/Finding.test.ts +28 -0
  12. package/core/gate/Finding.ts +12 -0
  13. package/core/gate/evaluateRules.ts +22 -0
  14. package/core/rules/presets/androidRuleSet.test.ts +52 -2
  15. package/core/rules/presets/androidRuleSet.ts +130 -0
  16. package/core/rules/presets/iosEnterpriseRuleSet.test.ts +276 -1
  17. package/core/rules/presets/iosEnterpriseRuleSet.ts +156 -0
  18. package/core/rules/presets/rulePackVersions.test.ts +2 -2
  19. package/core/rules/presets/rulePackVersions.ts +2 -2
  20. package/docs/codex-skills/{windsurf-rules-android.md → android-enterprise-rules.md} +1 -1
  21. package/docs/codex-skills/{windsurf-rules-backend.md → backend-enterprise-rules.md} +1 -1
  22. package/docs/codex-skills/{windsurf-rules-frontend.md → frontend-enterprise-rules.md} +1 -1
  23. package/docs/codex-skills/{windsurf-rules-ios.md → ios-enterprise-rules.md} +1 -1
  24. package/docs/operations/RELEASE_NOTES.md +37 -7
  25. package/docs/rule-packs/engineering-baseline.md +1 -1
  26. package/docs/rule-packs/ios.md +1 -1
  27. package/integrations/config/skillsCustomRules.ts +5 -0
  28. package/integrations/config/skillsRuleSet.ts +5 -1
  29. package/integrations/evidence/buildEvidence.ts +55 -0
  30. package/integrations/evidence/generateEvidence.test.ts +46 -0
  31. package/integrations/evidence/schema.ts +19 -1
  32. package/integrations/evidence/writeEvidence.ts +52 -0
  33. package/integrations/gate/evaluateAiGate.ts +114 -21
  34. package/integrations/git/findingTraceability.ts +15 -0
  35. package/integrations/git/runPlatformGate.ts +56 -3
  36. package/integrations/git/stageRunners.ts +102 -72
  37. package/integrations/lifecycle/cli.ts +73 -5
  38. package/integrations/lifecycle/doctor.ts +12 -3
  39. package/integrations/lifecycle/preWriteAutomation.ts +1 -1
  40. package/integrations/lifecycle/watch.ts +155 -9
  41. package/integrations/policy/gitAtomicityEnforcement.ts +57 -0
  42. package/integrations/policy/preWriteEnforcement.ts +41 -0
  43. package/integrations/policy/skillsEnforcement.ts +64 -0
  44. package/package.json +3 -4
  45. package/scripts/package-manifest-lib.ts +4 -4
  46. package/scripts/sync-codex-skills.sh +4 -4
  47. package/skills.lock.json +758 -758
  48. package/skills.sources.json +12 -12
package/VERSION CHANGED
@@ -1 +1 @@
1
- v6.3.56
1
+ v6.3.58
@@ -32,3 +32,31 @@ test('HeuristicFact permite omitir filePath', () => {
32
32
  assert.equal(factWithoutPath.filePath, undefined);
33
33
  assert.equal(factWithoutPath.severity, 'INFO');
34
34
  });
35
+
36
+ test('HeuristicFact conserva metadata semantica opcional del canario iOS', () => {
37
+ const fact: HeuristicFact = {
38
+ kind: 'Heuristic',
39
+ ruleId: 'heuristics.ios.canary-001.presentation-mixed-responsibilities.ast',
40
+ severity: 'CRITICAL',
41
+ code: 'HEURISTICS_IOS_CANARY_001_PRESENTATION_MIXED_RESPONSIBILITIES_AST',
42
+ message: 'Semantic iOS canary triggered.',
43
+ filePath: 'apps/ios/Sources/AppShell/Application/AppShellViewModel.swift',
44
+ lines: [1, 2, 3, 4],
45
+ primary_node: {
46
+ kind: 'class',
47
+ name: 'AppShellViewModel',
48
+ lines: [1],
49
+ },
50
+ related_nodes: [
51
+ { kind: 'property', name: 'shared singleton', lines: [2] },
52
+ { kind: 'call', name: 'URLSession.shared', lines: [3] },
53
+ ],
54
+ why: 'Mezcla responsabilidades incompatibles.',
55
+ impact: 'Complica tests y cambios.',
56
+ expected_fix: 'Extraer collaborators.',
57
+ };
58
+
59
+ assert.equal(fact.primary_node?.name, 'AppShellViewModel');
60
+ assert.equal(fact.related_nodes?.length, 2);
61
+ assert.equal(fact.expected_fix, 'Extraer collaborators.');
62
+ });
@@ -1,5 +1,11 @@
1
1
  import type { Severity } from '../rules/Severity';
2
2
 
3
+ export type HeuristicNode = {
4
+ kind: 'class' | 'property' | 'call' | 'member';
5
+ name: string;
6
+ lines?: readonly number[];
7
+ };
8
+
3
9
  export interface HeuristicFact {
4
10
  kind: 'Heuristic';
5
11
  ruleId: string;
@@ -8,4 +14,9 @@ export interface HeuristicFact {
8
14
  message: string;
9
15
  filePath?: string;
10
16
  lines?: readonly number[];
17
+ primary_node?: HeuristicNode;
18
+ related_nodes?: readonly HeuristicNode[];
19
+ why?: string;
20
+ impact?: string;
21
+ expected_fix?: string;
11
22
  }
@@ -1,6 +1,11 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import test from 'node:test';
3
3
  import {
4
+ findKotlinConcreteDependencyDipMatch,
5
+ findKotlinInterfaceSegregationMatch,
6
+ findKotlinLiskovSubstitutionMatch,
7
+ findKotlinOpenClosedWhenMatch,
8
+ findKotlinPresentationSrpMatch,
4
9
  hasKotlinGlobalScopeUsage,
5
10
  hasKotlinRunBlockingUsage,
6
11
  hasKotlinThreadSleepCall,
@@ -85,3 +90,205 @@ fun main() {
85
90
  assert.equal(hasKotlinRunBlockingUsage(commentedSource), false);
86
91
  assert.equal(hasKotlinRunBlockingUsage(partialSource), false);
87
92
  });
93
+
94
+ test('findKotlinPresentationSrpMatch devuelve payload semantico para SRP-Android en presentation', () => {
95
+ const source = `
96
+ import android.content.SharedPreferences
97
+ import androidx.lifecycle.ViewModel
98
+ import androidx.navigation.NavController
99
+ import okhttp3.OkHttpClient
100
+ import okhttp3.Request
101
+
102
+ class PumukiSrpAndroidCanaryViewModel(
103
+ private val navController: NavController,
104
+ ) : ViewModel() {
105
+ fun restoreSessionSnapshot() {}
106
+
107
+ suspend fun fetchRemoteCatalog() {
108
+ val client = OkHttpClient()
109
+ client.newCall(
110
+ Request.Builder()
111
+ .url("https://example.com/catalog.json")
112
+ .build()
113
+ )
114
+ }
115
+
116
+ fun cacheLastStore(preferences: SharedPreferences, storeId: String) {
117
+ preferences.edit().putString("last-store-id", storeId).apply()
118
+ }
119
+
120
+ fun openStoreMap() {
121
+ navController.navigate("store-map")
122
+ }
123
+ }
124
+ `;
125
+
126
+ const match = findKotlinPresentationSrpMatch(source);
127
+
128
+ assert.ok(match);
129
+ assert.deepEqual(match.primary_node, {
130
+ kind: 'class',
131
+ name: 'PumukiSrpAndroidCanaryViewModel',
132
+ lines: [8],
133
+ });
134
+ assert.deepEqual(match.related_nodes, [
135
+ { kind: 'member', name: 'session/auth flow', lines: [11] },
136
+ { kind: 'call', name: 'remote networking', lines: [14] },
137
+ { kind: 'call', name: 'local persistence', lines: [22] },
138
+ { kind: 'member', name: 'navigation flow', lines: [27] },
139
+ ]);
140
+ assert.match(match.why, /SRP/i);
141
+ assert.match(match.impact, /múltiples razones de cambio/i);
142
+ assert.match(match.expected_fix, /casos de uso|coordinadores/i);
143
+ });
144
+
145
+ test('findKotlinConcreteDependencyDipMatch devuelve payload semantico para DIP-Android en application', () => {
146
+ const source = `
147
+ import android.content.SharedPreferences
148
+ import okhttp3.OkHttpClient
149
+ import okhttp3.Request
150
+
151
+ class PumukiDipAndroidCanaryUseCase(
152
+ private val preferences: SharedPreferences,
153
+ ) {
154
+ private val client: OkHttpClient = OkHttpClient()
155
+
156
+ suspend fun execute() {
157
+ val request = Request.Builder()
158
+ .url("https://example.com/catalog.json")
159
+ .build()
160
+
161
+ client.newCall(request)
162
+ preferences.edit().putLong("last-sync", 1L).apply()
163
+ }
164
+ }
165
+ `;
166
+
167
+ const match = findKotlinConcreteDependencyDipMatch(source);
168
+
169
+ assert.ok(match);
170
+ assert.deepEqual(match.primary_node, {
171
+ kind: 'class',
172
+ name: 'PumukiDipAndroidCanaryUseCase',
173
+ lines: [6],
174
+ });
175
+ assert.deepEqual(match.related_nodes, [
176
+ { kind: 'property', name: 'concrete dependency: SharedPreferences', lines: [7] },
177
+ { kind: 'property', name: 'concrete dependency: OkHttpClient', lines: [9] },
178
+ { kind: 'call', name: 'OkHttpClient()', lines: [9] },
179
+ ]);
180
+ assert.match(match.why, /DIP/i);
181
+ assert.match(match.impact, /infraestructura|alto nivel|coste de sustituir/i);
182
+ assert.match(match.expected_fix, /puertos|abstracciones|gateways/i);
183
+ });
184
+
185
+ test('findKotlinOpenClosedWhenMatch devuelve payload semantico para OCP-Android en application', () => {
186
+ const source = `
187
+ enum class PumukiOcpAndroidCanaryChannel {
188
+ GroceryPickup,
189
+ HomeDelivery,
190
+ }
191
+
192
+ class PumukiOcpAndroidCanaryUseCase {
193
+ fun resolve(channel: PumukiOcpAndroidCanaryChannel): String {
194
+ return when (channel) {
195
+ PumukiOcpAndroidCanaryChannel.GroceryPickup -> "pickup"
196
+ PumukiOcpAndroidCanaryChannel.HomeDelivery -> "delivery"
197
+ }
198
+ }
199
+ }
200
+ `;
201
+
202
+ const match = findKotlinOpenClosedWhenMatch(source);
203
+
204
+ assert.ok(match);
205
+ assert.deepEqual(match.primary_node, {
206
+ kind: 'class',
207
+ name: 'PumukiOcpAndroidCanaryUseCase',
208
+ lines: [7],
209
+ });
210
+ assert.deepEqual(match.related_nodes, [
211
+ { kind: 'member', name: 'discriminator switch: channel', lines: [9] },
212
+ { kind: 'member', name: 'branch GroceryPickup', lines: [10] },
213
+ { kind: 'member', name: 'branch HomeDelivery', lines: [11] },
214
+ ]);
215
+ assert.match(match.why, /OCP/i);
216
+ assert.match(match.impact, /nuevo caso|modificar/i);
217
+ assert.match(match.expected_fix, /estrategia|interfaz|registry/i);
218
+ });
219
+
220
+ test('findKotlinInterfaceSegregationMatch devuelve payload semantico para ISP-Android en application', () => {
221
+ const source = `
222
+ interface PumukiIspAndroidCanarySessionPort {
223
+ suspend fun restoreSession()
224
+ suspend fun persistSessionID(id: String)
225
+ suspend fun clearSession()
226
+ suspend fun refreshToken(): String
227
+ }
228
+
229
+ class PumukiIspAndroidCanaryUseCase(
230
+ private val sessionPort: PumukiIspAndroidCanarySessionPort,
231
+ ) {
232
+ suspend fun execute() {
233
+ sessionPort.restoreSession()
234
+ }
235
+ }
236
+ `;
237
+
238
+ const match = findKotlinInterfaceSegregationMatch(source);
239
+
240
+ assert.ok(match);
241
+ assert.deepEqual(match.primary_node, {
242
+ kind: 'class',
243
+ name: 'PumukiIspAndroidCanaryUseCase',
244
+ lines: [9],
245
+ });
246
+ assert.deepEqual(match.related_nodes, [
247
+ { kind: 'member', name: 'fat interface: PumukiIspAndroidCanarySessionPort', lines: [2] },
248
+ { kind: 'call', name: 'used member: restoreSession', lines: [13] },
249
+ { kind: 'member', name: 'unused contract member: persistSessionID', lines: [4] },
250
+ { kind: 'member', name: 'unused contract member: clearSession', lines: [5] },
251
+ ]);
252
+ assert.match(match.why, /ISP/i);
253
+ assert.match(match.impact, /contrato demasiado ancho|cambios ajenos/i);
254
+ assert.match(match.expected_fix, /interfaces pequeñas|puerto mínimo/i);
255
+ });
256
+
257
+ test('findKotlinLiskovSubstitutionMatch devuelve payload semantico para LSP-Android en application', () => {
258
+ const source = `
259
+ interface PumukiLspAndroidCanaryDiscountPolicy {
260
+ fun apply(amount: Double): Double
261
+ }
262
+
263
+ class PumukiLspAndroidCanaryStandardDiscountPolicy : PumukiLspAndroidCanaryDiscountPolicy {
264
+ override fun apply(amount: Double): Double {
265
+ return amount * 0.9
266
+ }
267
+ }
268
+
269
+ class PumukiLspAndroidCanaryPremiumDiscountPolicy : PumukiLspAndroidCanaryDiscountPolicy {
270
+ override fun apply(amount: Double): Double {
271
+ require(amount >= 100.0)
272
+ error("premium-only")
273
+ }
274
+ }
275
+ `;
276
+
277
+ const match = findKotlinLiskovSubstitutionMatch(source);
278
+
279
+ assert.ok(match);
280
+ assert.deepEqual(match.primary_node, {
281
+ kind: 'class',
282
+ name: 'PumukiLspAndroidCanaryPremiumDiscountPolicy',
283
+ lines: [12],
284
+ });
285
+ assert.deepEqual(match.related_nodes, [
286
+ { kind: 'member', name: 'base contract: PumukiLspAndroidCanaryDiscountPolicy', lines: [2] },
287
+ { kind: 'member', name: 'safe substitute: PumukiLspAndroidCanaryStandardDiscountPolicy', lines: [6] },
288
+ { kind: 'member', name: 'narrowed precondition: apply', lines: [14] },
289
+ { kind: 'call', name: 'error', lines: [15] },
290
+ ]);
291
+ assert.match(match.why, /LSP/i);
292
+ assert.match(match.impact, /sustituci|regresion|crash/i);
293
+ assert.match(match.expected_fix, /contrato base|estrategia|subtipo/i);
294
+ });