pumuki 6.3.204 → 6.3.206

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.
@@ -23,7 +23,14 @@ export type LifecycleAuditResult = {
23
23
  command: 'pumuki audit';
24
24
  repo_root: string;
25
25
  stage: LifecycleAuditStage;
26
- scope: { kind: 'repo' | 'staged'; staged_matching_extensions_count: number };
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 = { kind: 'repo' } | { kind: 'staged' };
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;
@@ -211,6 +372,17 @@ const isScopedPreWriteGlobalEnforcementOnly = (params: {
211
372
  (finding) => finding.code === 'SKILLS_GLOBAL_ENFORCEMENT_INCOMPLETE_CRITICAL'
212
373
  );
213
374
 
375
+ const isRangePrePushWithoutSupportedCodeSddOnly = (params: {
376
+ stage: LifecycleAuditStage;
377
+ scope: LifecycleAuditScope;
378
+ findings: ReadonlyArray<LifecycleAuditFinding>;
379
+ }): boolean =>
380
+ params.stage === 'PRE_PUSH' &&
381
+ params.scope.kind === 'range' &&
382
+ params.scope.matchingExtensions.length === 0 &&
383
+ params.findings.length > 0 &&
384
+ params.findings.every((finding) => finding.code === 'SDD_CHANGE_MISSING');
385
+
214
386
  const toScopedAuditAdvisoryFinding = (
215
387
  finding: LifecycleAuditFinding
216
388
  ): LifecycleAuditFinding => ({
@@ -223,6 +395,18 @@ const toScopedAuditAdvisoryFinding = (
223
395
  blocking: false,
224
396
  });
225
397
 
398
+ const toRangeNoSupportedCodeAuditAdvisoryFinding = (
399
+ finding: LifecycleAuditFinding
400
+ ): LifecycleAuditFinding => ({
401
+ ...finding,
402
+ severity: 'INFO',
403
+ code: 'AUDIT_RANGE_NO_SUPPORTED_CODE_ADVISORY',
404
+ message:
405
+ 'Range PRE_PUSH audit found no supported code files in the branch delta; SDD baseline debt is retained as advisory for this atomic split slice. ' +
406
+ finding.message,
407
+ blocking: false,
408
+ });
409
+
226
410
  export const runLifecycleAudit = async (params: {
227
411
  stage: LifecycleAuditStage;
228
412
  auditMode: 'gate' | 'engine';
@@ -246,15 +430,19 @@ export const runLifecycleAudit = async (params: {
246
430
  const stagedMatchingExtensions = collectStagedMatchingExtensions(git, extensions);
247
431
  const scope = resolveLifecycleAuditScope({
248
432
  stage: params.stage,
433
+ git,
434
+ repoRoot,
435
+ extensions,
249
436
  stagedMatchingExtensions,
250
437
  });
438
+ const gateScope = toGateScope(scope);
251
439
 
252
440
  const gateParams =
253
441
  params.auditMode === 'engine'
254
442
  ? {
255
443
  policy: resolved.policy,
256
444
  policyTrace: resolved.trace,
257
- scope,
445
+ scope: gateScope,
258
446
  silent: true,
259
447
  auditMode: 'engine' as const,
260
448
  dependencies: {
@@ -273,7 +461,7 @@ export const runLifecycleAudit = async (params: {
273
461
  : {
274
462
  policy: resolved.policy,
275
463
  policyTrace: resolved.trace,
276
- scope,
464
+ scope: gateScope,
277
465
  silent: true,
278
466
  auditMode: 'gate' as const,
279
467
  };
@@ -298,13 +486,23 @@ export const runLifecycleAudit = async (params: {
298
486
  scope,
299
487
  findings,
300
488
  });
489
+ const rangePrePushWithoutSupportedCodeSddOnly = isRangePrePushWithoutSupportedCodeSddOnly({
490
+ stage: params.stage,
491
+ scope,
492
+ findings,
493
+ });
301
494
  const gateAllowed = originalGateExitCode === 0;
302
495
  const effectiveFindings = scopedGlobalEnforcementOnly
303
496
  ? findings.map(toScopedAuditAdvisoryFinding)
497
+ : rangePrePushWithoutSupportedCodeSddOnly
498
+ ? findings.map(toRangeNoSupportedCodeAuditAdvisoryFinding)
304
499
  : gateAllowed
305
500
  ? findings.map(toGateAllowedAuditAdvisoryFinding)
306
501
  : findings;
307
- const gateExitCode = scopedGlobalEnforcementOnly ? 0 : originalGateExitCode;
502
+ const gateExitCode =
503
+ scopedGlobalEnforcementOnly || rangePrePushWithoutSupportedCodeSddOnly
504
+ ? 0
505
+ : originalGateExitCode;
308
506
  const effectiveSnapshotOutcome =
309
507
  gateExitCode === 0 && snapshotOutcome === 'BLOCK' ? 'PASS' : snapshotOutcome;
310
508
 
@@ -312,10 +510,7 @@ export const runLifecycleAudit = async (params: {
312
510
  command: 'pumuki audit',
313
511
  repo_root: repoRoot,
314
512
  stage: params.stage,
315
- scope: {
316
- kind: scope.kind,
317
- staged_matching_extensions_count: stagedMatchingExtensions.length,
318
- },
513
+ scope: toResultScope({ scope, stagedMatchingExtensions }),
319
514
  audit_mode: params.auditMode,
320
515
  gate_exit_code: gateExitCode,
321
516
  files_scanned: filesScanned,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki",
3
- "version": "6.3.204",
3
+ "version": "6.3.206",
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": {