pumuki 6.3.203 → 6.3.205
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/core/facts/detectors/text/ios.test.ts +33 -0
- package/core/facts/detectors/text/ios.ts +14 -0
- package/core/facts/extractHeuristicFacts.ts +1 -0
- package/core/rules/presets/heuristics/ios.test.ts +6 -1
- package/core/rules/presets/heuristics/ios.ts +18 -0
- package/docs/codex-skills/ios-enterprise-rules.md +2 -0
- package/integrations/config/skillsDetectorRegistry.ts +4 -0
- package/integrations/config/skillsMarkdownRules.ts +7 -0
- package/integrations/lifecycle/audit.ts +170 -8
- package/package.json +1 -1
- package/skills.lock.json +3 -3
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
hasSwiftForceUnwrap,
|
|
24
24
|
hasSwiftGeometryReaderUsage,
|
|
25
25
|
hasSwiftHardcodedUiStringUsage,
|
|
26
|
+
hasSwiftIconOnlyControlWithoutAccessibilityLabelUsage,
|
|
26
27
|
hasSwiftLooseAssetResourceUsage,
|
|
27
28
|
hasSwiftLegacyOnChangeUsage,
|
|
28
29
|
hasSwiftLegacyExpectationDescriptionUsage,
|
|
@@ -377,6 +378,38 @@ func wait() async throws {
|
|
|
377
378
|
assert.equal(hasSwiftMainThreadBlockingSleepUsage(ignored), false);
|
|
378
379
|
});
|
|
379
380
|
|
|
381
|
+
test('detector iOS de accesibilidad detecta botones icon-only sin label explicita', () => {
|
|
382
|
+
const source = `
|
|
383
|
+
struct ToolbarView: View {
|
|
384
|
+
var body: some View {
|
|
385
|
+
Button {
|
|
386
|
+
delete()
|
|
387
|
+
} label: {
|
|
388
|
+
Image(systemName: "trash")
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
`;
|
|
393
|
+
const ignored = `
|
|
394
|
+
struct ToolbarView: View {
|
|
395
|
+
var body: some View {
|
|
396
|
+
Button {
|
|
397
|
+
delete()
|
|
398
|
+
} label: {
|
|
399
|
+
Image(systemName: "trash")
|
|
400
|
+
}
|
|
401
|
+
.accessibilityLabel(Text("Delete item"))
|
|
402
|
+
Button("Delete") { delete() }
|
|
403
|
+
let text = "Button { Image(systemName: \\"trash\\") }"
|
|
404
|
+
// Button { Image(systemName: "trash") }
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
`;
|
|
408
|
+
|
|
409
|
+
assert.equal(hasSwiftIconOnlyControlWithoutAccessibilityLabelUsage(source), true);
|
|
410
|
+
assert.equal(hasSwiftIconOnlyControlWithoutAccessibilityLabelUsage(ignored), false);
|
|
411
|
+
});
|
|
412
|
+
|
|
380
413
|
test('hasSwiftUncheckedSendableUsage detecta @unchecked Sendable', () => {
|
|
381
414
|
const source = `
|
|
382
415
|
final class LegacyBox: @unchecked Sendable {}
|
|
@@ -608,6 +608,20 @@ export const hasSwiftMainThreadBlockingSleepUsage = (source: string): boolean =>
|
|
|
608
608
|
});
|
|
609
609
|
};
|
|
610
610
|
|
|
611
|
+
export const hasSwiftIconOnlyControlWithoutAccessibilityLabelUsage = (source: string): boolean => {
|
|
612
|
+
const sanitized = sanitizeSwiftSourceForMultilineRegex(source);
|
|
613
|
+
const iconOnlyButtonPattern =
|
|
614
|
+
/\bButton\s*(?:\([^)]*\))?\s*\{[\s\S]{0,240}?\bImage\s*\(\s*systemName\s*:\s*""\s*\)[\s\S]{0,240}?\}/g;
|
|
615
|
+
for (const match of sanitized.matchAll(iconOnlyButtonPattern)) {
|
|
616
|
+
const segment = match[0] ?? '';
|
|
617
|
+
const following = sanitized.slice(match.index ?? 0, (match.index ?? 0) + segment.length + 160);
|
|
618
|
+
if (!/\.\s*accessibilityLabel\s*\(/.test(following)) {
|
|
619
|
+
return true;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return false;
|
|
623
|
+
};
|
|
624
|
+
|
|
611
625
|
export const hasSwiftUncheckedSendableUsage = (source: string): boolean => {
|
|
612
626
|
return scanCodeLikeSource(source, ({ source: swiftSource, index, current }) => {
|
|
613
627
|
if (current !== '@' || !swiftSource.startsWith('@unchecked', index)) {
|
|
@@ -659,6 +659,7 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
|
|
|
659
659
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftFixedFontSizeUsage, ruleId: 'heuristics.ios.accessibility.fixed-font-size.ast', code: 'HEURISTICS_IOS_ACCESSIBILITY_FIXED_FONT_SIZE_AST', message: 'AST heuristic detected fixed font sizing in iOS production code; Dynamic Type semantic text styles remain the preferred baseline.' },
|
|
660
660
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftPhysicalTextAlignmentUsage, ruleId: 'heuristics.ios.localization.physical-text-alignment.ast', code: 'HEURISTICS_IOS_LOCALIZATION_PHYSICAL_TEXT_ALIGNMENT_AST', message: 'AST heuristic detected physical left/right text alignment in iOS production code; leading/trailing remain the preferred RTL-safe baseline.' },
|
|
661
661
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftMainThreadBlockingSleepUsage, ruleId: 'heuristics.ios.performance.blocking-sleep.ast', code: 'HEURISTICS_IOS_PERFORMANCE_BLOCKING_SLEEP_AST', message: 'AST heuristic detected blocking sleep usage in iOS production code; async clocks, suspension or cancellable scheduling remain the preferred baseline.' },
|
|
662
|
+
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftIconOnlyControlWithoutAccessibilityLabelUsage, ruleId: 'heuristics.ios.accessibility.icon-only-control-label.ast', code: 'HEURISTICS_IOS_ACCESSIBILITY_ICON_ONLY_CONTROL_LABEL_AST', message: 'AST heuristic detected an icon-only SwiftUI control without accessibilityLabel; explicit accessible labels remain the preferred baseline.' },
|
|
662
663
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftUncheckedSendableUsage, ruleId: 'heuristics.ios.unchecked-sendable.ast', code: 'HEURISTICS_IOS_UNCHECKED_SENDABLE_AST', message: 'AST heuristic detected @unchecked Sendable usage.' },
|
|
663
664
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftPreconcurrencyUsage, ruleId: 'heuristics.ios.preconcurrency.ast', code: 'HEURISTICS_IOS_PRECONCURRENCY_AST', message: 'AST heuristic detected @preconcurrency usage.' },
|
|
664
665
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftNonisolatedUnsafeUsage, ruleId: 'heuristics.ios.nonisolated-unsafe.ast', code: 'HEURISTICS_IOS_NONISOLATED_UNSAFE_AST', message: 'AST heuristic detected nonisolated(unsafe) usage.' },
|
|
@@ -3,7 +3,7 @@ import test from 'node:test';
|
|
|
3
3
|
import { iosRules } from './ios';
|
|
4
4
|
|
|
5
5
|
test('iosRules define reglas heurísticas locked para plataforma ios', () => {
|
|
6
|
-
assert.equal(iosRules.length,
|
|
6
|
+
assert.equal(iosRules.length, 58);
|
|
7
7
|
|
|
8
8
|
const ids = iosRules.map((rule) => rule.id);
|
|
9
9
|
assert.deepEqual(ids, [
|
|
@@ -31,6 +31,7 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
|
|
|
31
31
|
'heuristics.ios.accessibility.fixed-font-size.ast',
|
|
32
32
|
'heuristics.ios.localization.physical-text-alignment.ast',
|
|
33
33
|
'heuristics.ios.performance.blocking-sleep.ast',
|
|
34
|
+
'heuristics.ios.accessibility.icon-only-control-label.ast',
|
|
34
35
|
'heuristics.ios.unchecked-sendable.ast',
|
|
35
36
|
'heuristics.ios.preconcurrency.ast',
|
|
36
37
|
'heuristics.ios.nonisolated-unsafe.ast',
|
|
@@ -131,6 +132,10 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
|
|
|
131
132
|
byId.get('heuristics.ios.performance.blocking-sleep.ast')?.then.code,
|
|
132
133
|
'HEURISTICS_IOS_PERFORMANCE_BLOCKING_SLEEP_AST'
|
|
133
134
|
);
|
|
135
|
+
assert.equal(
|
|
136
|
+
byId.get('heuristics.ios.accessibility.icon-only-control-label.ast')?.then.code,
|
|
137
|
+
'HEURISTICS_IOS_ACCESSIBILITY_ICON_ONLY_CONTROL_LABEL_AST'
|
|
138
|
+
);
|
|
134
139
|
assert.equal(
|
|
135
140
|
byId.get('heuristics.ios.preconcurrency.ast')?.then.code,
|
|
136
141
|
'HEURISTICS_IOS_PRECONCURRENCY_AST'
|
|
@@ -433,6 +433,24 @@ export const iosRules: RuleSet = [
|
|
|
433
433
|
code: 'HEURISTICS_IOS_PERFORMANCE_BLOCKING_SLEEP_AST',
|
|
434
434
|
},
|
|
435
435
|
},
|
|
436
|
+
{
|
|
437
|
+
id: 'heuristics.ios.accessibility.icon-only-control-label.ast',
|
|
438
|
+
description: 'Detects icon-only SwiftUI controls without explicit accessibility labels.',
|
|
439
|
+
severity: 'WARN',
|
|
440
|
+
platform: 'ios',
|
|
441
|
+
locked: true,
|
|
442
|
+
when: {
|
|
443
|
+
kind: 'Heuristic',
|
|
444
|
+
where: {
|
|
445
|
+
ruleId: 'heuristics.ios.accessibility.icon-only-control-label.ast',
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
then: {
|
|
449
|
+
kind: 'Finding',
|
|
450
|
+
message: 'AST heuristic detected an icon-only SwiftUI control without accessibilityLabel; explicit accessible labels remain the preferred baseline.',
|
|
451
|
+
code: 'HEURISTICS_IOS_ACCESSIBILITY_ICON_ONLY_CONTROL_LABEL_AST',
|
|
452
|
+
},
|
|
453
|
+
},
|
|
436
454
|
{
|
|
437
455
|
id: 'heuristics.ios.unchecked-sendable.ast',
|
|
438
456
|
description: 'Detects @unchecked Sendable usage in iOS production code.',
|
|
@@ -626,7 +626,9 @@ struct APIEndpoint: Sendable {
|
|
|
626
626
|
|
|
627
627
|
- `skills.ios.guideline.ios.dynamic-type-font-scaling-automa-tico` se mapea a `heuristics.ios.accessibility.fixed-font-size.ast`.
|
|
628
628
|
- `skills.ios.guideline.ios.dynamic-type-fuentes-escalables-y-layouts-adaptativos` se mapea a `heuristics.ios.accessibility.fixed-font-size.ast`.
|
|
629
|
+
- `skills.ios.guideline.ios.accessibility-labels-accessibilitylabel` se mapea a `heuristics.ios.accessibility.icon-only-control-label.ast`.
|
|
629
630
|
- En `PROJECT MODE: brownfield`, este hallazgo detecta tamaños de fuente fijos (`.font(.system(size:))`, `Font.system(size:)`, `UIFont.systemFont(ofSize:)`) como señal de adopción hacia Dynamic Type y estilos semánticos. No marca `.font(.headline)`, `.font(.body)` ni otros estilos semánticos.
|
|
631
|
+
- En `PROJECT MODE: brownfield`, este hallazgo detecta controles SwiftUI icon-only con `Button` + `Image(systemName:)` sin `.accessibilityLabel` cercano como señal de adopción hacia labels accesibles explícitas. No intenta inferir todos los casos de accesibilidad ni marca botones con texto visible.
|
|
630
632
|
|
|
631
633
|
### Enforcement AST inicial de RTL iOS
|
|
632
634
|
|
|
@@ -131,6 +131,10 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
|
|
|
131
131
|
'ios.performance.blocking-sleep',
|
|
132
132
|
['heuristics.ios.performance.blocking-sleep.ast']
|
|
133
133
|
),
|
|
134
|
+
'skills.ios.guideline.ios.accessibility-labels-accessibilitylabel': heuristicDetector(
|
|
135
|
+
'ios.accessibility.icon-only-control-label',
|
|
136
|
+
['heuristics.ios.accessibility.icon-only-control-label.ast']
|
|
137
|
+
),
|
|
134
138
|
'skills.ios.no-unchecked-sendable': heuristicDetector('ios.unchecked-sendable', [
|
|
135
139
|
'heuristics.ios.unchecked-sendable.ast',
|
|
136
140
|
]),
|
|
@@ -416,6 +416,13 @@ const normalizeKnownRuleTarget = (
|
|
|
416
416
|
) {
|
|
417
417
|
return 'skills.ios.guideline.ios.background-threads-no-bloquear-main-thread';
|
|
418
418
|
}
|
|
419
|
+
if (
|
|
420
|
+
includes('accessibility labels') ||
|
|
421
|
+
includes('accessibilitylabel') ||
|
|
422
|
+
includes('accessibility label')
|
|
423
|
+
) {
|
|
424
|
+
return 'skills.ios.guideline.ios.accessibility-labels-accessibilitylabel';
|
|
425
|
+
}
|
|
419
426
|
if (
|
|
420
427
|
includes('mixing legacy xctest style') ||
|
|
421
428
|
includes('mixed xctest and swift testing') ||
|
|
@@ -23,7 +23,14 @@ export type LifecycleAuditResult = {
|
|
|
23
23
|
command: 'pumuki audit';
|
|
24
24
|
repo_root: string;
|
|
25
25
|
stage: LifecycleAuditStage;
|
|
26
|
-
scope: {
|
|
26
|
+
scope: {
|
|
27
|
+
kind: 'repo' | 'staged' | 'range';
|
|
28
|
+
staged_matching_extensions_count: number;
|
|
29
|
+
range_matching_extensions_count?: number;
|
|
30
|
+
base_ref?: string;
|
|
31
|
+
from_ref?: string;
|
|
32
|
+
to_ref?: string;
|
|
33
|
+
};
|
|
27
34
|
audit_mode: 'gate' | 'engine';
|
|
28
35
|
gate_exit_code: number;
|
|
29
36
|
files_scanned: number | null;
|
|
@@ -54,7 +61,16 @@ type LifecycleAuditDependencies = {
|
|
|
54
61
|
runPlatformGate: typeof runPlatformGate;
|
|
55
62
|
};
|
|
56
63
|
|
|
57
|
-
type LifecycleAuditScope =
|
|
64
|
+
type LifecycleAuditScope =
|
|
65
|
+
| { kind: 'repo' }
|
|
66
|
+
| { kind: 'staged' }
|
|
67
|
+
| {
|
|
68
|
+
kind: 'range';
|
|
69
|
+
baseRef: string;
|
|
70
|
+
fromRef: string;
|
|
71
|
+
toRef: string;
|
|
72
|
+
matchingExtensions: ReadonlyArray<string>;
|
|
73
|
+
};
|
|
58
74
|
|
|
59
75
|
const POLICY_RECONCILE_HINT =
|
|
60
76
|
'If .pumuki/policy-as-code.json signatures drift after a pumuki upgrade, run: pumuki policy reconcile --apply';
|
|
@@ -84,16 +100,161 @@ const collectStagedMatchingExtensions = (
|
|
|
84
100
|
.filter((path) => hasAllowedExtension(path, extensions));
|
|
85
101
|
};
|
|
86
102
|
|
|
103
|
+
const runGitOrNull = (
|
|
104
|
+
git: Pick<IGitService, 'runGit'>,
|
|
105
|
+
args: ReadonlyArray<string>,
|
|
106
|
+
cwd: string
|
|
107
|
+
): string | null => {
|
|
108
|
+
try {
|
|
109
|
+
const output = git.runGit([...args], cwd).trim();
|
|
110
|
+
return output.length > 0 ? output : null;
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const isResolvableRef = (
|
|
117
|
+
git: Pick<IGitService, 'runGit'>,
|
|
118
|
+
repoRoot: string,
|
|
119
|
+
ref: string
|
|
120
|
+
): boolean => runGitOrNull(git, ['rev-parse', '--verify', ref], repoRoot) !== null;
|
|
121
|
+
|
|
122
|
+
const branchPrefersDevelopBase = (branch: string): boolean =>
|
|
123
|
+
/^(feature|bugfix|chore|refactor|docs)\//.test(branch);
|
|
124
|
+
|
|
125
|
+
const branchPrefersMainBase = (branch: string): boolean => /^hotfix\//.test(branch);
|
|
126
|
+
|
|
127
|
+
const collectRangeMatchingExtensions = (params: {
|
|
128
|
+
git: Pick<IGitService, 'runGit'>;
|
|
129
|
+
repoRoot: string;
|
|
130
|
+
fromRef: string;
|
|
131
|
+
toRef: string;
|
|
132
|
+
extensions: ReadonlyArray<string>;
|
|
133
|
+
}): string[] => {
|
|
134
|
+
const raw = runGitOrNull(
|
|
135
|
+
params.git,
|
|
136
|
+
['diff', '--name-only', `${params.fromRef}..${params.toRef}`],
|
|
137
|
+
params.repoRoot
|
|
138
|
+
);
|
|
139
|
+
return (raw ?? '')
|
|
140
|
+
.split('\n')
|
|
141
|
+
.map((line) => line.trim())
|
|
142
|
+
.filter((line) => line.length > 0)
|
|
143
|
+
.filter((path) => hasAllowedExtension(path, params.extensions));
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const resolvePrePushRangeScope = (params: {
|
|
147
|
+
git: Pick<IGitService, 'runGit'>;
|
|
148
|
+
repoRoot: string;
|
|
149
|
+
extensions: ReadonlyArray<string>;
|
|
150
|
+
}): LifecycleAuditScope | null => {
|
|
151
|
+
const branch =
|
|
152
|
+
runGitOrNull(params.git, ['rev-parse', '--abbrev-ref', 'HEAD'], params.repoRoot) ?? '';
|
|
153
|
+
if (branch === 'HEAD' || branch.length === 0) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const explicitBase =
|
|
158
|
+
process.env.PUMUKI_AUDIT_PRE_PUSH_BASE_REF?.trim() ??
|
|
159
|
+
process.env.PUMUKI_PRE_PUSH_BASE_REF?.trim() ??
|
|
160
|
+
'';
|
|
161
|
+
const upstreamTracking = runGitOrNull(
|
|
162
|
+
params.git,
|
|
163
|
+
['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}'],
|
|
164
|
+
params.repoRoot
|
|
165
|
+
);
|
|
166
|
+
const preferredBaseRefs = [
|
|
167
|
+
explicitBase,
|
|
168
|
+
...(branchPrefersDevelopBase(branch) ? ['origin/develop'] : []),
|
|
169
|
+
...(branchPrefersMainBase(branch) ? ['origin/main', 'origin/master'] : []),
|
|
170
|
+
upstreamTracking ?? '',
|
|
171
|
+
'origin/develop',
|
|
172
|
+
'origin/main',
|
|
173
|
+
'origin/master',
|
|
174
|
+
].filter((ref, index, refs) => ref.length > 0 && refs.indexOf(ref) === index);
|
|
175
|
+
|
|
176
|
+
const baseRef = preferredBaseRefs.find((ref) =>
|
|
177
|
+
isResolvableRef(params.git, params.repoRoot, ref)
|
|
178
|
+
);
|
|
179
|
+
if (typeof baseRef === 'undefined') {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const mergeBase = runGitOrNull(params.git, ['merge-base', baseRef, 'HEAD'], params.repoRoot);
|
|
184
|
+
if (mergeBase === null) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
kind: 'range',
|
|
190
|
+
baseRef,
|
|
191
|
+
fromRef: mergeBase,
|
|
192
|
+
toRef: 'HEAD',
|
|
193
|
+
matchingExtensions: collectRangeMatchingExtensions({
|
|
194
|
+
git: params.git,
|
|
195
|
+
repoRoot: params.repoRoot,
|
|
196
|
+
fromRef: mergeBase,
|
|
197
|
+
toRef: 'HEAD',
|
|
198
|
+
extensions: params.extensions,
|
|
199
|
+
}),
|
|
200
|
+
};
|
|
201
|
+
};
|
|
202
|
+
|
|
87
203
|
const resolveLifecycleAuditScope = (params: {
|
|
88
204
|
stage: LifecycleAuditStage;
|
|
205
|
+
git: Pick<IGitService, 'runGit'>;
|
|
206
|
+
repoRoot: string;
|
|
207
|
+
extensions: ReadonlyArray<string>;
|
|
89
208
|
stagedMatchingExtensions: ReadonlyArray<string>;
|
|
90
209
|
}): LifecycleAuditScope => {
|
|
91
210
|
if (params.stage === 'PRE_WRITE' && params.stagedMatchingExtensions.length > 0) {
|
|
92
211
|
return { kind: 'staged' };
|
|
93
212
|
}
|
|
213
|
+
if (params.stage === 'PRE_PUSH') {
|
|
214
|
+
const rangeScope = resolvePrePushRangeScope({
|
|
215
|
+
git: params.git,
|
|
216
|
+
repoRoot: params.repoRoot,
|
|
217
|
+
extensions: params.extensions,
|
|
218
|
+
});
|
|
219
|
+
if (rangeScope !== null) {
|
|
220
|
+
return rangeScope;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
94
223
|
return { kind: 'repo' };
|
|
95
224
|
};
|
|
96
225
|
|
|
226
|
+
const toGateScope = (scope: LifecycleAuditScope) => {
|
|
227
|
+
if (scope.kind === 'range') {
|
|
228
|
+
return {
|
|
229
|
+
kind: 'range' as const,
|
|
230
|
+
fromRef: scope.fromRef,
|
|
231
|
+
toRef: scope.toRef,
|
|
232
|
+
extensions: [...DEFAULT_FACT_FILE_EXTENSIONS],
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
return { kind: scope.kind };
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const toResultScope = (params: {
|
|
239
|
+
scope: LifecycleAuditScope;
|
|
240
|
+
stagedMatchingExtensions: ReadonlyArray<string>;
|
|
241
|
+
}): LifecycleAuditResult['scope'] => {
|
|
242
|
+
const base = {
|
|
243
|
+
kind: params.scope.kind,
|
|
244
|
+
staged_matching_extensions_count: params.stagedMatchingExtensions.length,
|
|
245
|
+
};
|
|
246
|
+
if (params.scope.kind !== 'range') {
|
|
247
|
+
return base;
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
...base,
|
|
251
|
+
range_matching_extensions_count: params.scope.matchingExtensions.length,
|
|
252
|
+
base_ref: params.scope.baseRef,
|
|
253
|
+
from_ref: params.scope.fromRef,
|
|
254
|
+
to_ref: params.scope.toRef,
|
|
255
|
+
};
|
|
256
|
+
};
|
|
257
|
+
|
|
97
258
|
const isFindingBlocking = (finding: SnapshotFinding): boolean => {
|
|
98
259
|
if (typeof finding.blocking === 'boolean') {
|
|
99
260
|
return finding.blocking;
|
|
@@ -246,15 +407,19 @@ export const runLifecycleAudit = async (params: {
|
|
|
246
407
|
const stagedMatchingExtensions = collectStagedMatchingExtensions(git, extensions);
|
|
247
408
|
const scope = resolveLifecycleAuditScope({
|
|
248
409
|
stage: params.stage,
|
|
410
|
+
git,
|
|
411
|
+
repoRoot,
|
|
412
|
+
extensions,
|
|
249
413
|
stagedMatchingExtensions,
|
|
250
414
|
});
|
|
415
|
+
const gateScope = toGateScope(scope);
|
|
251
416
|
|
|
252
417
|
const gateParams =
|
|
253
418
|
params.auditMode === 'engine'
|
|
254
419
|
? {
|
|
255
420
|
policy: resolved.policy,
|
|
256
421
|
policyTrace: resolved.trace,
|
|
257
|
-
scope,
|
|
422
|
+
scope: gateScope,
|
|
258
423
|
silent: true,
|
|
259
424
|
auditMode: 'engine' as const,
|
|
260
425
|
dependencies: {
|
|
@@ -273,7 +438,7 @@ export const runLifecycleAudit = async (params: {
|
|
|
273
438
|
: {
|
|
274
439
|
policy: resolved.policy,
|
|
275
440
|
policyTrace: resolved.trace,
|
|
276
|
-
scope,
|
|
441
|
+
scope: gateScope,
|
|
277
442
|
silent: true,
|
|
278
443
|
auditMode: 'gate' as const,
|
|
279
444
|
};
|
|
@@ -312,10 +477,7 @@ export const runLifecycleAudit = async (params: {
|
|
|
312
477
|
command: 'pumuki audit',
|
|
313
478
|
repo_root: repoRoot,
|
|
314
479
|
stage: params.stage,
|
|
315
|
-
scope: {
|
|
316
|
-
kind: scope.kind,
|
|
317
|
-
staged_matching_extensions_count: stagedMatchingExtensions.length,
|
|
318
|
-
},
|
|
480
|
+
scope: toResultScope({ scope, stagedMatchingExtensions }),
|
|
319
481
|
audit_mode: params.auditMode,
|
|
320
482
|
gate_exit_code: gateExitCode,
|
|
321
483
|
files_scanned: filesScanned,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.205",
|
|
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": {
|
package/skills.lock.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": "1.0",
|
|
3
3
|
"compilerVersion": "1.0.0",
|
|
4
|
-
"generatedAt": "2026-05-
|
|
4
|
+
"generatedAt": "2026-05-13T12:03:23.736Z",
|
|
5
5
|
"bundles": [
|
|
6
6
|
{
|
|
7
7
|
"name": "android-guidelines",
|
|
@@ -5764,7 +5764,7 @@
|
|
|
5764
5764
|
"name": "ios-guidelines",
|
|
5765
5765
|
"version": "1.0.0",
|
|
5766
5766
|
"source": "file:vendor/skills/ios-enterprise-rules/SKILL.md",
|
|
5767
|
-
"hash": "
|
|
5767
|
+
"hash": "41ef368c744a0b772fa408e83fe7edf4621fe2766ba3ea47d6753339690dbbd2",
|
|
5768
5768
|
"rules": [
|
|
5769
5769
|
{
|
|
5770
5770
|
"id": "skills.ios.guideline.ios.accessibility-identifiers-para-localizar-elementos",
|
|
@@ -5787,7 +5787,7 @@
|
|
|
5787
5787
|
"sourcePath": "vendor/skills/ios-enterprise-rules/SKILL.md",
|
|
5788
5788
|
"confidence": "MEDIUM",
|
|
5789
5789
|
"locked": true,
|
|
5790
|
-
"evaluationMode": "
|
|
5790
|
+
"evaluationMode": "AUTO",
|
|
5791
5791
|
"origin": "core"
|
|
5792
5792
|
},
|
|
5793
5793
|
{
|