pumuki 6.3.56 → 6.3.57
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/VERSION +1 -1
- package/core/facts/HeuristicFact.test.ts +28 -0
- package/core/facts/HeuristicFact.ts +11 -0
- package/core/facts/detectors/text/android.test.ts +207 -0
- package/core/facts/detectors/text/android.ts +765 -0
- package/core/facts/detectors/text/ios.test.ts +207 -0
- package/core/facts/detectors/text/ios.ts +848 -0
- package/core/facts/detectors/typescript/index.test.ts +447 -0
- package/core/facts/detectors/typescript/index.ts +1214 -54
- package/core/facts/extractHeuristicFacts.ts +285 -1
- package/core/gate/Finding.test.ts +28 -0
- package/core/gate/Finding.ts +12 -0
- package/core/gate/evaluateRules.ts +22 -0
- package/core/rules/presets/androidRuleSet.test.ts +52 -2
- package/core/rules/presets/androidRuleSet.ts +130 -0
- package/core/rules/presets/iosEnterpriseRuleSet.test.ts +276 -1
- package/core/rules/presets/iosEnterpriseRuleSet.ts +156 -0
- package/core/rules/presets/rulePackVersions.test.ts +2 -2
- package/core/rules/presets/rulePackVersions.ts +2 -2
- package/docs/codex-skills/{windsurf-rules-android.md → android-enterprise-rules.md} +1 -1
- package/docs/codex-skills/{windsurf-rules-backend.md → backend-enterprise-rules.md} +1 -1
- package/docs/codex-skills/{windsurf-rules-frontend.md → frontend-enterprise-rules.md} +1 -1
- package/docs/codex-skills/{windsurf-rules-ios.md → ios-enterprise-rules.md} +1 -1
- package/docs/operations/RELEASE_NOTES.md +19 -8
- package/docs/rule-packs/engineering-baseline.md +1 -1
- package/docs/rule-packs/ios.md +1 -1
- package/integrations/config/skillsCustomRules.ts +5 -0
- package/integrations/config/skillsRuleSet.ts +5 -1
- package/integrations/evidence/buildEvidence.ts +55 -0
- package/integrations/evidence/generateEvidence.test.ts +46 -0
- package/integrations/evidence/schema.ts +18 -0
- package/integrations/evidence/writeEvidence.ts +52 -0
- package/integrations/gate/evaluateAiGate.ts +30 -3
- package/integrations/git/findingTraceability.ts +15 -0
- package/integrations/git/runPlatformGate.ts +56 -3
- package/integrations/git/stageRunners.ts +4 -41
- package/package.json +1 -1
- package/scripts/package-manifest-lib.ts +4 -4
- package/scripts/sync-codex-skills.sh +4 -4
- package/skills.lock.json +758 -758
- package/skills.sources.json +12 -12
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
v6.3.
|
|
1
|
+
v6.3.57
|
|
@@ -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
|
+
});
|