pumuki 6.3.307 → 6.3.308

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.
@@ -69,6 +69,11 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
69
69
  'heuristics.ios.dispatchqueue.ast',
70
70
  'heuristics.ios.dispatchsemaphore.ast',
71
71
  ]),
72
+ 'skills.ios.guideline.ios.sincronizacio-n-nativa-actors-para-estado-compartido-osallocatedunfair':
73
+ heuristicDetector('ios.concurrency.shared-state-actor-boundary', [
74
+ 'heuristics.ios.dispatchqueue.ast',
75
+ 'heuristics.ios.dispatchsemaphore.ast',
76
+ ]),
72
77
  'skills.ios.no-dispatchgroup': heuristicDetector('ios.dispatchgroup', [
73
78
  'heuristics.ios.dispatchgroup.ast',
74
79
  ]),
@@ -81,6 +86,10 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
81
86
  'skills.ios.no-task-detached': heuristicDetector('ios.task-detached', [
82
87
  'heuristics.ios.task-detached.ast',
83
88
  ]),
89
+ 'skills.ios.guideline.ios.cancelar-correctamente-comprobar-task-iscancelled-en-operaciones-larga':
90
+ heuristicDetector('ios.concurrency.long-task-cancellation-check', [
91
+ 'heuristics.ios.concurrency.long-task-without-cancellation-check.ast',
92
+ ]),
84
93
  'skills.ios.no-async-without-await': heuristicDetector('ios.concurrency.async-without-await', [
85
94
  'heuristics.ios.concurrency.async-without-await.ast',
86
95
  ]),
@@ -88,6 +97,10 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
88
97
  heuristicDetector('ios.concurrency.avoid-overuse-async-without-await', [
89
98
  'heuristics.ios.concurrency.async-without-await.ast',
90
99
  ]),
100
+ 'skills.ios.guideline.ios-concurrency.use-references-linting-md-for-rule-intent-and-preferred-fixes-avoid-du':
101
+ heuristicDetector('ios.concurrency.dummy-await', [
102
+ 'heuristics.ios.concurrency.dummy-await.ast',
103
+ ]),
91
104
  'skills.ios.guideline.ios.delegation-pattern-weak-delegates-para-evitar-retain-cycles': heuristicDetector(
92
105
  'ios.memory.strong-delegate',
93
106
  ['heuristics.ios.memory.strong-delegate.ast']
@@ -212,6 +225,10 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
212
225
  'ios.error.typed-error-enum',
213
226
  ['heuristics.ios.error.nserror-throw.ast']
214
227
  ),
228
+ 'skills.ios.guideline.ios.apiendpoint-como-struct-data-driven-ocp-endpoints-en-features-no-enum-':
229
+ heuristicDetector('ios.networking.endpoint-enum-ocp', [
230
+ 'heuristics.ios.networking.endpoint-enum-ocp.ast',
231
+ ]),
215
232
  'skills.ios.guideline.ios.no-loggear-pii-tokens-emails-ids-sensibles': heuristicDetector(
216
233
  'ios.logging.sensitive-data',
217
234
  ['heuristics.ios.logging.sensitive-data.ast']
@@ -255,6 +272,7 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
255
272
  'heuristics.ios.dependencies.carthage.ast',
256
273
  'heuristics.ios.architecture.swinject.ast',
257
274
  'heuristics.ios.architecture.tca-composable-architecture.ast',
275
+ 'heuristics.ios.testing.third-party-ui-test-framework.ast',
258
276
  ]),
259
277
  'skills.ios.guideline.ios.libreri-as-de-terceros-usar-apis-nativas': heuristicDetector(
260
278
  'ios.dependencies.third-party-native-baseline',
@@ -264,6 +282,7 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
264
282
  'heuristics.ios.dependencies.carthage.ast',
265
283
  'heuristics.ios.architecture.swinject.ast',
266
284
  'heuristics.ios.architecture.tca-composable-architecture.ast',
285
+ 'heuristics.ios.testing.third-party-ui-test-framework.ast',
267
286
  ]
268
287
  ),
269
288
  'skills.ios.guideline.ios.keychain-passwords-tokens-no-userdefaults': heuristicDetector(
@@ -330,6 +349,10 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
330
349
  'ios.performance.blocking-sleep',
331
350
  ['heuristics.ios.performance.blocking-sleep.ast']
332
351
  ),
352
+ 'skills.ios.guideline.ios-concurrency.use-references-threading-md-to-avoid-thread-centric-debugging-and-rely':
353
+ heuristicDetector('ios.concurrency.thread-centric-debugging', [
354
+ 'heuristics.ios.concurrency.thread-centric-debugging.ast',
355
+ ]),
333
356
  'skills.ios.guideline.ios.structured-concurrency-task-taskgroup-actor-asyncsequence-asyncstream':
334
357
  heuristicDetector('ios.concurrency.structured-baseline', [
335
358
  'heuristics.ios.callback-style.ast',
@@ -375,6 +398,21 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
375
398
  'ios.accessibility.on-tap-without-button-trait',
376
399
  ['heuristics.ios.accessibility.on-tap-without-button-trait.ast']
377
400
  ),
401
+ 'skills.ios.guideline.ios.voiceover-labels-hints-traits': heuristicDetector(
402
+ 'ios.accessibility.voiceover-labels-traits',
403
+ [
404
+ 'heuristics.ios.accessibility.icon-only-control-label.ast',
405
+ 'heuristics.ios.accessibility.on-tap-without-button-trait.ast',
406
+ ]
407
+ ),
408
+ 'skills.ios.guideline.ios-swiftui-expert.use-interactive-only-for-tappable-focusable-elements':
409
+ heuristicDetector('ios.swiftui.glass-interactive-static-element', [
410
+ 'heuristics.ios.swiftui.glass-interactive-static-element.ast',
411
+ ]),
412
+ 'skills.ios.guideline.ios-swiftui-expert.use-glasseffectid-with-namespace-for-morphing-transitions':
413
+ heuristicDetector('ios.swiftui.glasseffectid-namespace', [
414
+ 'heuristics.ios.swiftui.glasseffectid-without-namespace.ast',
415
+ ]),
378
416
  'skills.ios.guideline.ios.image-optimization-resize-compress-cache': heuristicDetector(
379
417
  'ios.swiftui.image-data-decoding',
380
418
  ['heuristics.ios.swiftui.image-data-decoding.ast']
@@ -415,10 +453,30 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
415
453
  heuristicDetector('ios.dependencies.swift-tools-version', [
416
454
  'heuristics.ios.dependencies.swift-tools-version-below-6-2.ast',
417
455
  ]),
456
+ 'skills.ios.guideline.ios-concurrency.use-read-on-package-swift-for-swiftpm-settings-tools-version-strict-co':
457
+ heuristicDetector('ios.dependencies.swift-tools-version', [
458
+ 'heuristics.ios.dependencies.swift-tools-version-below-6-2.ast',
459
+ 'heuristics.ios.concurrency.swiftpm-default-isolation-not-mainactor.ast',
460
+ 'heuristics.ios.concurrency.swiftpm-strict-concurrency-below-complete.ast',
461
+ ]),
418
462
  'skills.ios.guideline.ios.strict-concurrency-checking-activar-en-complete':
419
463
  heuristicDetector('ios.concurrency.strict-concurrency-below-complete', [
420
464
  'heuristics.ios.concurrency.strict-concurrency-below-complete.ast',
421
465
  ]),
466
+ 'skills.ios.guideline.ios-concurrency.use-grep-for-swiftstrictconcurrency-or-swiftdefaultactorisolation-in-p':
467
+ heuristicDetector('ios.concurrency.project-settings', [
468
+ 'heuristics.ios.concurrency.strict-concurrency-below-complete.ast',
469
+ 'heuristics.ios.concurrency.default-actor-isolation-not-mainactor.ast',
470
+ 'heuristics.ios.concurrency.swiftpm-default-isolation-not-mainactor.ast',
471
+ 'heuristics.ios.concurrency.swiftpm-strict-concurrency-below-complete.ast',
472
+ ]),
473
+ 'skills.ios.guideline.ios.validar-configuracio-n-de-concurrencia-swiftstrictconcurrency-swiftdef':
474
+ heuristicDetector('ios.concurrency.project-settings', [
475
+ 'heuristics.ios.concurrency.strict-concurrency-below-complete.ast',
476
+ 'heuristics.ios.concurrency.default-actor-isolation-not-mainactor.ast',
477
+ 'heuristics.ios.concurrency.swiftpm-default-isolation-not-mainactor.ast',
478
+ 'heuristics.ios.concurrency.swiftpm-strict-concurrency-below-complete.ast',
479
+ ]),
422
480
  'skills.ios.guideline.ios.file-naming-pascalcase-para-tipos': heuristicDetector(
423
481
  'ios.naming.non-pascal-case-type',
424
482
  ['heuristics.ios.naming.non-pascal-case-type.ast']
@@ -646,6 +704,10 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
646
704
  heuristicDetector('ios.swiftui.large-viewbuilder-function', [
647
705
  'heuristics.ios.swiftui.large-viewbuilder-function.ast',
648
706
  ]),
707
+ 'skills.ios.guideline.ios.composicio-n-de-views-views-pequen-os-reutilizables':
708
+ heuristicDetector('ios.swiftui.large-viewbuilder-function', [
709
+ 'heuristics.ios.swiftui.large-viewbuilder-function.ast',
710
+ ]),
649
711
  'skills.ios.guideline.ios-swiftui-expert.pass-only-needed-values-to-views-avoid-large-config-or-context-objects':
650
712
  heuristicDetector('ios.swiftui.large-config-context-prop', [
651
713
  'heuristics.ios.swiftui.large-config-context-prop.ast',
@@ -775,6 +837,10 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
775
837
  'ios.testing.quick-nimble',
776
838
  ['heuristics.ios.testing.quick-nimble.ast']
777
839
  ),
840
+ 'skills.ios.guideline.ios.xcuitest-ui-testing-nativo': heuristicDetector(
841
+ 'ios.testing.third-party-ui-test-framework',
842
+ ['heuristics.ios.testing.third-party-ui-test-framework.ast']
843
+ ),
778
844
  'skills.ios.no-nsmanagedobject-boundary': heuristicDetector(
779
845
  'ios.core-data.nsmanagedobject-boundary',
780
846
  ['heuristics.ios.core-data.nsmanagedobject-boundary.ast']
@@ -1150,6 +1216,34 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
1150
1216
  'heuristics.ios.solid.lsp.narrowed-precondition.ast',
1151
1217
  ]
1152
1218
  ),
1219
+ 'skills.ios.guideline.ios.feature-first-cada-feature-es-un-bounded-context-no-se-acoplan-entre-s':
1220
+ heuristicDetector('ios.architecture.cross-feature-import', [
1221
+ 'heuristics.ios.architecture.cross-feature-import.ast',
1222
+ ]),
1223
+ 'skills.ios.guideline.ios.las-features-no-se-importan-entre-si':
1224
+ heuristicDetector('ios.architecture.cross-feature-import', [
1225
+ 'heuristics.ios.architecture.cross-feature-import.ast',
1226
+ ]),
1227
+ 'skills.ios.guideline.ios.shared-kernel-solo-tipos-mi-nimos-compartidos-entre-features':
1228
+ heuristicDetector('ios.architecture.cross-feature-import', [
1229
+ 'heuristics.ios.architecture.cross-feature-import.ast',
1230
+ ]),
1231
+ 'skills.ios.guideline.ios.clean-por-feature-presentation-application-domain-infrastructure-domai':
1232
+ heuristicDetector('ios.architecture.layer-direction', [
1233
+ 'heuristics.ios.architecture.layer-direction-violation.ast',
1234
+ ]),
1235
+ 'skills.ios.guideline.ios.seguir-clean-architecture-domain-application-infrastructure-presentati':
1236
+ heuristicDetector('ios.architecture.layer-direction', [
1237
+ 'heuristics.ios.architecture.layer-direction-violation.ast',
1238
+ ]),
1239
+ 'skills.ios.guideline.ios.ddd-entidades-value-objects-repositorios-protocolos-eventos-de-dominio':
1240
+ heuristicDetector('ios.architecture.layer-direction', [
1241
+ 'heuristics.ios.architecture.layer-direction-violation.ast',
1242
+ ]),
1243
+ 'skills.ios.guideline.ios.public-api-solo-exponer-lo-necesario-public-internal-private':
1244
+ heuristicDetector('ios.architecture.excessive-public-api', [
1245
+ 'heuristics.ios.architecture.excessive-public-api.ast',
1246
+ ]),
1153
1247
  'skills.ios.guideline.ios.testability-inyectar-protocols-no-tipos-concretos':
1154
1248
  heuristicDetector('ios.testability.protocol-boundaries', [
1155
1249
  'heuristics.ios.solid.dip.concrete-framework-dependency.ast',
@@ -271,7 +271,49 @@ const equivalentRuleFamilies: ReadonlyArray<ReadonlyArray<string>> = [
271
271
  ['skills.ios.no-task-detached', 'heuristics.ios.task-detached.ast'],
272
272
  ['skills.ios.no-unchecked-sendable', 'heuristics.ios.unchecked-sendable.ast'],
273
273
  ['skills.ios.no-preconcurrency', 'heuristics.ios.preconcurrency.ast'],
274
+ [
275
+ 'skills.ios.guideline.ios-concurrency.use-grep-for-swiftstrictconcurrency-or-swiftdefaultactorisolation-in-p',
276
+ 'heuristics.ios.concurrency.strict-concurrency-below-complete.ast',
277
+ 'heuristics.ios.concurrency.default-actor-isolation-not-mainactor.ast',
278
+ 'heuristics.ios.concurrency.swiftpm-default-isolation-not-mainactor.ast',
279
+ 'heuristics.ios.concurrency.swiftpm-strict-concurrency-below-complete.ast',
280
+ ],
281
+ [
282
+ 'skills.ios.guideline.ios.validar-configuracio-n-de-concurrencia-swiftstrictconcurrency-swiftdef',
283
+ 'heuristics.ios.concurrency.strict-concurrency-below-complete.ast',
284
+ 'heuristics.ios.concurrency.default-actor-isolation-not-mainactor.ast',
285
+ 'heuristics.ios.concurrency.swiftpm-default-isolation-not-mainactor.ast',
286
+ 'heuristics.ios.concurrency.swiftpm-strict-concurrency-below-complete.ast',
287
+ ],
274
288
  ['skills.ios.no-nonisolated-unsafe', 'heuristics.ios.nonisolated-unsafe.ast'],
289
+ [
290
+ 'skills.ios.guideline.ios.feature-first-cada-feature-es-un-bounded-context-no-se-acoplan-entre-s',
291
+ 'heuristics.ios.architecture.cross-feature-import.ast',
292
+ ],
293
+ [
294
+ 'skills.ios.guideline.ios.las-features-no-se-importan-entre-si',
295
+ 'heuristics.ios.architecture.cross-feature-import.ast',
296
+ ],
297
+ [
298
+ 'skills.ios.guideline.ios.shared-kernel-solo-tipos-mi-nimos-compartidos-entre-features',
299
+ 'heuristics.ios.architecture.cross-feature-import.ast',
300
+ ],
301
+ [
302
+ 'skills.ios.guideline.ios.clean-por-feature-presentation-application-domain-infrastructure-domai',
303
+ 'heuristics.ios.architecture.layer-direction-violation.ast',
304
+ ],
305
+ [
306
+ 'skills.ios.guideline.ios.seguir-clean-architecture-domain-application-infrastructure-presentati',
307
+ 'heuristics.ios.architecture.layer-direction-violation.ast',
308
+ ],
309
+ [
310
+ 'skills.ios.guideline.ios.ddd-entidades-value-objects-repositorios-protocolos-eventos-de-dominio',
311
+ 'heuristics.ios.architecture.layer-direction-violation.ast',
312
+ ],
313
+ [
314
+ 'skills.ios.guideline.ios.public-api-solo-exponer-lo-necesario-public-internal-private',
315
+ 'heuristics.ios.architecture.excessive-public-api.ast',
316
+ ],
275
317
  ['skills.ios.no-assume-isolated', 'heuristics.ios.assume-isolated.ast'],
276
318
  ['skills.ios.no-observable-object', 'heuristics.ios.observable-object.ast'],
277
319
  [
@@ -312,6 +354,10 @@ const equivalentRuleFamilies: ReadonlyArray<ReadonlyArray<string>> = [
312
354
  'heuristics.ios.testing.legacy-expectation-description.ast',
313
355
  ],
314
356
  ['skills.ios.no-mixed-testing-frameworks', 'heuristics.ios.testing.mixed-frameworks.ast'],
357
+ [
358
+ 'skills.ios.guideline.ios.xcuitest-ui-testing-nativo',
359
+ 'heuristics.ios.testing.third-party-ui-test-framework.ast',
360
+ ],
315
361
  [
316
362
  'skills.ios.no-nsmanagedobject-boundary',
317
363
  'heuristics.ios.core-data.nsmanagedobject-boundary.ast',
@@ -24,6 +24,13 @@
24
24
  "evidence": {
25
25
  "command": "npx --yes --package pumuki@latest pumuki-mcp-evidence-stdio"
26
26
  }
27
+ },
28
+ "enforcement": {
29
+ "pre_write": {
30
+ "required_before_edit": true,
31
+ "command": "npx --yes --package pumuki@latest pumuki-pre-write",
32
+ "agent_instruction": "STOP before editing files unless this command has produced a valid PRE_WRITE lease for the current repo and diff."
33
+ }
27
34
  }
28
35
  }
29
36
  }
@@ -54,6 +61,13 @@
54
61
  "evidence": {
55
62
  "command": "npx --yes --package pumuki@latest pumuki-mcp-evidence-stdio"
56
63
  }
64
+ },
65
+ "enforcement": {
66
+ "pre_write": {
67
+ "required_before_edit": true,
68
+ "command": "npx --yes --package pumuki@latest pumuki-pre-write",
69
+ "agent_instruction": "STOP before editing files unless this command has produced a valid PRE_WRITE lease for the current repo and diff."
70
+ }
57
71
  }
58
72
  }
59
73
  }
@@ -76,6 +90,13 @@
76
90
  "ci": {
77
91
  "command": "npx --yes --package pumuki@latest pumuki-ci"
78
92
  }
93
+ },
94
+ "enforcement": {
95
+ "pre_write": {
96
+ "required_before_edit": true,
97
+ "command": "npx --yes --package pumuki@latest pumuki-pre-write",
98
+ "agent_instruction": "STOP before editing files unless this command has produced a valid PRE_WRITE lease for the current repo and diff."
99
+ }
79
100
  }
80
101
  },
81
102
  "mcpServers": {
@@ -139,6 +160,13 @@
139
160
  "ci": {
140
161
  "command": "npx --yes --package pumuki@latest pumuki-ci"
141
162
  }
163
+ },
164
+ "enforcement": {
165
+ "pre_write": {
166
+ "required_before_edit": true,
167
+ "command": "npx --yes --package pumuki@latest pumuki-pre-write",
168
+ "agent_instruction": "STOP before editing files unless this command has produced a valid PRE_WRITE lease for the current repo and diff."
169
+ }
142
170
  }
143
171
  }
144
172
  },
@@ -166,6 +194,13 @@
166
194
  "evidence": {
167
195
  "command": "npx --yes --package pumuki@latest pumuki-mcp-evidence-stdio"
168
196
  }
197
+ },
198
+ "enforcement": {
199
+ "pre_write": {
200
+ "required_before_edit": true,
201
+ "command": "npx --yes --package pumuki@latest pumuki-pre-write",
202
+ "agent_instruction": "STOP before editing files unless this command has produced a valid PRE_WRITE lease for the current repo and diff."
203
+ }
169
204
  }
170
205
  }
171
206
  }
@@ -178,7 +213,14 @@
178
213
  "postCommand": "npx --yes --package pumuki@latest pumuki-pre-commit",
179
214
  "pushCommand": "npx --yes --package pumuki@latest pumuki-pre-push",
180
215
  "ciCommand": "npx --yes --package pumuki@latest pumuki-ci",
181
- "mcpCommand": "npx --yes --package pumuki@latest pumuki-mcp-enterprise-stdio"
216
+ "mcpCommand": "npx --yes --package pumuki@latest pumuki-mcp-enterprise-stdio",
217
+ "enforcement": {
218
+ "pre_write": {
219
+ "required_before_edit": true,
220
+ "command": "npx --yes --package pumuki@latest pumuki-pre-write",
221
+ "agent_instruction": "STOP before editing files unless this command has produced a valid PRE_WRITE lease for the current repo and diff."
222
+ }
223
+ }
182
224
  }
183
225
  },
184
226
  {
@@ -246,6 +288,13 @@
246
288
  "evidence": {
247
289
  "command": "npx --yes --package pumuki@latest pumuki-mcp-evidence-stdio"
248
290
  }
291
+ },
292
+ "enforcement": {
293
+ "pre_write": {
294
+ "required_before_edit": true,
295
+ "command": "npx --yes --package pumuki@latest pumuki-pre-write",
296
+ "agent_instruction": "STOP before editing files unless this command has produced a valid PRE_WRITE lease for the current repo and diff."
297
+ }
249
298
  }
250
299
  }
251
300
  }
@@ -269,6 +269,20 @@ const readNestedString = (
269
269
  return typeof cursor === 'string' && cursor.trim().length > 0 ? cursor : undefined;
270
270
  };
271
271
 
272
+ const readNestedBoolean = (
273
+ source: Record<string, unknown>,
274
+ path: ReadonlyArray<string>
275
+ ): boolean | undefined => {
276
+ let cursor: unknown = source;
277
+ for (const segment of path) {
278
+ if (!isRecord(cursor)) {
279
+ return undefined;
280
+ }
281
+ cursor = cursor[segment];
282
+ }
283
+ return typeof cursor === 'boolean' ? cursor : undefined;
284
+ };
285
+
272
286
  const hasRobustPumukiCommandResolution = (command: string): boolean => {
273
287
  const normalized = command.trim();
274
288
  if (normalized.length === 0) {
@@ -396,17 +410,26 @@ const evaluateAdapterWiringCheck = (repoRoot: string): DoctorDeepCheck => {
396
410
  const missingPaths = requiredCommandPaths
397
411
  .filter((path) => !readNestedString(parsed, path))
398
412
  .map((path) => path.join('.'));
399
-
400
- if (missingPaths.length > 0) {
413
+ const missingContractPaths = [
414
+ !readNestedBoolean(parsed, ['enforcement', 'pre_write', 'required_before_edit'])
415
+ ? 'enforcement.pre_write.required_before_edit'
416
+ : undefined,
417
+ !readNestedString(parsed, ['enforcement', 'pre_write', 'agent_instruction'])
418
+ ? 'enforcement.pre_write.agent_instruction'
419
+ : undefined,
420
+ ].filter((path): path is string => typeof path === 'string');
421
+
422
+ if (missingPaths.length > 0 || missingContractPaths.length > 0) {
423
+ const missing = [...missingPaths, ...missingContractPaths];
401
424
  return buildDeepCheck({
402
425
  id: 'adapter-wiring',
403
426
  status: 'fail',
404
427
  severity: 'error',
405
- message: `Adapter wiring is incomplete (missing: ${missingPaths.join(', ')}).`,
428
+ message: `Adapter wiring is incomplete (missing: ${missing.join(', ')}).`,
406
429
  remediation: 'Re-run "pumuki adapter install --agent=codex" and keep generated commands unchanged.',
407
430
  metadata: {
408
431
  path: adapterPath,
409
- missing_count: missingPaths.length,
432
+ missing_count: missing.length,
410
433
  },
411
434
  });
412
435
  }
@@ -445,13 +468,26 @@ const evaluateAdapterWiringCheck = (repoRoot: string): DoctorDeepCheck => {
445
468
  });
446
469
  }
447
470
 
471
+ const preWriteEnforcementCommand =
472
+ readNestedString(parsed, ['enforcement', 'pre_write', 'command']) ??
473
+ readNestedString(parsed, ['hooks', 'pre_write', 'command']);
474
+ const preWriteAgentInstruction = readNestedString(parsed, [
475
+ 'enforcement',
476
+ 'pre_write',
477
+ 'agent_instruction',
478
+ ]);
479
+
448
480
  return buildDeepCheck({
449
481
  id: 'adapter-wiring',
450
482
  status: 'pass',
451
483
  severity: 'info',
452
- message: 'Adapter wiring contract is valid.',
484
+ message:
485
+ 'Adapter wiring contract is valid; PRE_WRITE hard-stop is required before editing.',
453
486
  metadata: {
454
487
  path: adapterPath,
488
+ pre_write_required_before_edit: true,
489
+ pre_write_command: preWriteEnforcementCommand ?? null,
490
+ pre_write_agent_instruction: preWriteAgentInstruction ?? null,
455
491
  },
456
492
  });
457
493
  };
@@ -300,7 +300,11 @@ export const toPreWriteEnforcementGapFinding = (params: {
300
300
  code: 'ENFORCEMENT_GAP_PRE_WRITE_LEASE_MISSING',
301
301
  message:
302
302
  `PRE_WRITE hard-stop lease is not valid at ${params.stage}: ${params.status.code}. ` +
303
- 'Run PRE_WRITE before editing code; hooks/audit cannot accept code diffs without a prior clean PRE_WRITE lease.',
303
+ 'User action: run `npx pumuki-pre-write` before editing code, then rerun the gate. ' +
304
+ 'AI agent instruction: STOP; do not edit files until a valid PRE_WRITE lease exists for this repo and diff.',
305
+ expected_fix:
306
+ 'User: run `npx pumuki-pre-write` or `npx pumuki sdd validate --stage=PRE_WRITE --json` before editing. ' +
307
+ 'AI agent: stop immediately, do not apply patches or write files, and continue only after the command produces a valid clean or validated-diff PRE_WRITE lease.',
304
308
  filePath: '.pumuki/prewrite-lease.json',
305
309
  matchedBy: 'PreWriteLeaseGuard',
306
310
  source: 'prewrite-lease',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki",
3
- "version": "6.3.307",
3
+ "version": "6.3.308",
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": {
@@ -1,9 +1,30 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
 
4
- export const extractRuleIdsFromEvidence = (repoRoot: string): string[] => {
4
+ export type CanaryEvidenceRuleMatch = {
5
+ ruleId: string;
6
+ filePath: string;
7
+ };
8
+
9
+ const normalizeEvidencePath = (value: string): string => value.replaceAll('\\', '/');
10
+
11
+ const readEvidenceSnapshotFindings = (repoRoot: string): Array<Record<string, unknown>> => {
5
12
  const evidencePath = join(repoRoot, '.ai_evidence.json');
6
13
  try {
14
+ const parsed = JSON.parse(readFileSync(evidencePath, 'utf8')) as {
15
+ snapshot?: {
16
+ findings?: Array<Record<string, unknown>>;
17
+ };
18
+ };
19
+ return Array.isArray(parsed?.snapshot?.findings) ? parsed.snapshot.findings : [];
20
+ } catch {
21
+ return [];
22
+ }
23
+ };
24
+
25
+ export const extractRuleIdsFromEvidence = (repoRoot: string): string[] => {
26
+ try {
27
+ const evidencePath = join(repoRoot, '.ai_evidence.json');
7
28
  const parsed = JSON.parse(readFileSync(evidencePath, 'utf8')) as {
8
29
  snapshot?: {
9
30
  findings?: Array<{ ruleId?: unknown }>;
@@ -32,3 +53,23 @@ export const extractRuleIdsFromEvidence = (repoRoot: string): string[] => {
32
53
  return [];
33
54
  }
34
55
  };
56
+
57
+ export const extractRuleMatchesFromEvidence = (repoRoot: string): CanaryEvidenceRuleMatch[] => {
58
+ return readEvidenceSnapshotFindings(repoRoot)
59
+ .map((finding) => {
60
+ const ruleId = typeof finding.ruleId === 'string' ? finding.ruleId : '';
61
+ const filePath =
62
+ typeof finding.filePath === 'string'
63
+ ? finding.filePath
64
+ : typeof finding.file === 'string'
65
+ ? finding.file
66
+ : typeof finding.path === 'string'
67
+ ? finding.path
68
+ : '';
69
+ return {
70
+ ruleId,
71
+ filePath: normalizeEvidencePath(filePath),
72
+ };
73
+ })
74
+ .filter((match) => match.ruleId.length > 0 && match.filePath.length > 0);
75
+ };
@@ -11,7 +11,11 @@ import {
11
11
  runWorkingTreePrePushGateSilent,
12
12
  } from './framework-menu-gate-lib';
13
13
  import { readMatrixOptionReport, type MatrixOptionId } from './framework-menu-matrix-evidence-lib';
14
- import { extractRuleIdsFromEvidence } from './framework-menu-matrix-canary-evidence';
14
+ import {
15
+ extractRuleIdsFromEvidence,
16
+ extractRuleMatchesFromEvidence,
17
+ type CanaryEvidenceRuleMatch,
18
+ } from './framework-menu-matrix-canary-evidence';
15
19
  import { resolveConsumerMenuCanaryScenario } from './framework-menu-matrix-canary-scenario';
16
20
  import type {
17
21
  ConsumerMenuCanaryPlatform,
@@ -102,6 +106,10 @@ export const runConsumerMenuCanary = async (params?: {
102
106
  runGate?: (option: MatrixOptionId) => Promise<void>;
103
107
  readOptionReport?: typeof readMatrixOptionReport;
104
108
  extractRuleIds?: (repoRoot: string) => string[];
109
+ extractRuleMatches?: (
110
+ repoRoot: string,
111
+ scenario: ReturnType<typeof resolveConsumerMenuCanaryScenario>
112
+ ) => CanaryEvidenceRuleMatch[];
105
113
  };
106
114
  }): Promise<ConsumerMenuCanaryResult> => {
107
115
  const previousCwd = cwd();
@@ -116,6 +124,7 @@ export const runConsumerMenuCanary = async (params?: {
116
124
  const runGate = dependencies?.runGate ?? runGateByOption;
117
125
  const readOptionReport = dependencies?.readOptionReport ?? readMatrixOptionReport;
118
126
  const extractRuleIds = dependencies?.extractRuleIds ?? extractRuleIdsFromEvidence;
127
+ const extractRuleMatches = dependencies?.extractRuleMatches ?? extractRuleMatchesFromEvidence;
119
128
 
120
129
  chdir(repoRoot);
121
130
  try {
@@ -132,11 +141,25 @@ export const runConsumerMenuCanary = async (params?: {
132
141
  await runGate(scenario.option);
133
142
  const optionReport = readOptionReport(repoRoot, scenario.option);
134
143
  const ruleIds = extractRuleIds(repoRoot);
144
+ const expectedRuleIds = [scenario.expectedRuleId, ...(scenario.equivalentRuleIds ?? [])];
145
+ const ruleMatches = extractRuleMatches(repoRoot, scenario);
146
+ const canaryRuleMatched = ruleMatches.some((match) =>
147
+ expectedRuleIds.includes(match.ruleId)
148
+ && match.filePath.replaceAll('\\', '/').endsWith(canaryRelativePath)
149
+ );
150
+ const blocked = optionReport.outcome === 'BLOCK' && optionReport.diagnosis === 'violations-detected';
135
151
 
136
152
  return {
137
153
  option: scenario.option,
138
154
  detected:
139
- optionReport.totalViolations > 0 && ruleIds.includes(scenario.expectedRuleId),
155
+ blocked
156
+ && optionReport.totalViolations > 0
157
+ && expectedRuleIds.some((ruleId) => ruleIds.includes(ruleId))
158
+ && canaryRuleMatched,
159
+ outcome: optionReport.outcome,
160
+ diagnosis: optionReport.diagnosis,
161
+ canaryRelativePath,
162
+ canaryRuleMatched,
140
163
  totalViolations: optionReport.totalViolations,
141
164
  filesScanned: optionReport.filesScanned,
142
165
  ruleIds,
@@ -39,6 +39,7 @@ export const resolveConsumerMenuCanaryScenario = (params?: {
39
39
  platform,
40
40
  option: stage === 'PRE_COMMIT' ? '1' : '2',
41
41
  expectedRuleId: 'skills.ios.no-force-unwrap',
42
+ equivalentRuleIds: ['ios.no-force-unwrap'],
42
43
  canaryRelativePath: `apps/ios/App/__pumuki_matrix_canary_ios_${suffix}.swift`,
43
44
  canarySource: [
44
45
  'import Foundation',
@@ -78,11 +79,12 @@ export const resolveConsumerMenuCanaryScenario = (params?: {
78
79
  platform: 'backend',
79
80
  option: stage === 'PRE_COMMIT' ? '1' : '2',
80
81
  expectedRuleId: 'skills.backend.no-empty-catch',
82
+ equivalentRuleIds: ['common.error.empty_catch', 'heuristics.ts.empty-catch.ast'],
81
83
  canaryRelativePath: `apps/backend/src/__pumuki_matrix_canary_backend_${suffix}.ts`,
82
84
  canarySource: [
83
85
  'export const __pumukiMatrixCanaryBackend = (): void => {',
84
86
  ' try {',
85
- " throw new Error('pumuki-matrix-canary')",
87
+ ' void 0;',
86
88
  ' } catch {}',
87
89
  '};',
88
90
  '',
@@ -1,4 +1,4 @@
1
- import type { MatrixOptionId } from './framework-menu-matrix-evidence-lib';
1
+ import type { MatrixOptionDiagnosis, MatrixOptionId } from './framework-menu-matrix-evidence-lib';
2
2
 
3
3
  export type ConsumerMenuCanaryStage = 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
4
4
  export type ConsumerMenuCanaryPlatform = 'ios' | 'android' | 'backend' | 'frontend';
@@ -6,6 +6,10 @@ export type ConsumerMenuCanaryPlatform = 'ios' | 'android' | 'backend' | 'fronte
6
6
  export type ConsumerMenuCanaryResult = {
7
7
  option: MatrixOptionId;
8
8
  detected: boolean;
9
+ outcome: string;
10
+ diagnosis: MatrixOptionDiagnosis;
11
+ canaryRelativePath: string;
12
+ canaryRuleMatched: boolean;
9
13
  totalViolations: number;
10
14
  filesScanned: number;
11
15
  ruleIds: string[];
@@ -16,6 +20,7 @@ export type ConsumerMenuCanaryScenario = {
16
20
  platform: ConsumerMenuCanaryPlatform;
17
21
  option: MatrixOptionId;
18
22
  expectedRuleId: string;
23
+ equivalentRuleIds?: string[];
19
24
  canaryRelativePath: string;
20
25
  canarySource: string;
21
26
  };
@@ -1,5 +1,6 @@
1
1
  import type { PumukiCriticalNotificationEvent } from './framework-menu-system-notifications-types';
2
2
  import {
3
+ resolvePrioritizedBlockingCauses,
3
4
  resolveBlockedCauseSummary,
4
5
  resolveBlockedRemediation,
5
6
  resolveProjectLabel,
@@ -25,7 +26,7 @@ const buildBlockingCausesDetails = (
25
26
  const header = total === 1
26
27
  ? 'Causas bloqueantes: 1'
27
28
  : `Causas bloqueantes: ${total}`;
28
- return causes
29
+ return resolvePrioritizedBlockingCauses(causes)
29
30
  .flatMap((cause, index) => formatBlockingCauseForDialog(cause, index))
30
31
  .reduce((lines, line) => [...lines, line], [header])
31
32
  .join('\n');