pumuki 6.3.39 → 6.3.41

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 (64) hide show
  1. package/README.md +21 -12
  2. package/VERSION +1 -1
  3. package/core/gate/evaluateRules.test.ts +40 -0
  4. package/core/gate/evaluateRules.ts +7 -1
  5. package/core/rules/Consequence.ts +1 -0
  6. package/docs/CONFIGURATION.md +50 -0
  7. package/docs/INSTALLATION.md +38 -11
  8. package/docs/MCP_SERVERS.md +1 -1
  9. package/docs/README.md +1 -0
  10. package/docs/RELEASE_NOTES.md +58 -0
  11. package/docs/USAGE.md +191 -9
  12. package/docs/registro-maestro-de-seguimiento.md +2 -2
  13. package/docs/seguimiento-activo-pumuki-saas-supermercados.md +1629 -1
  14. package/docs/validation/README.md +2 -1
  15. package/docs/validation/ast-intelligence-roadmap.md +96 -0
  16. package/integrations/config/skillsCustomRules.ts +14 -0
  17. package/integrations/config/skillsDetectorRegistry.ts +11 -1
  18. package/integrations/config/skillsLock.ts +30 -0
  19. package/integrations/config/skillsMarkdownRules.ts +14 -3
  20. package/integrations/config/skillsRuleSet.ts +25 -3
  21. package/integrations/evidence/readEvidence.test.ts +3 -2
  22. package/integrations/evidence/readEvidence.ts +14 -4
  23. package/integrations/evidence/repoState.ts +10 -2
  24. package/integrations/evidence/schema.test.ts +3 -2
  25. package/integrations/evidence/schema.ts +3 -0
  26. package/integrations/evidence/writeEvidence.test.ts +3 -2
  27. package/integrations/gate/evaluateAiGate.ts +511 -2
  28. package/integrations/git/GitService.ts +5 -1
  29. package/integrations/git/astIntelligenceDualValidation.ts +275 -0
  30. package/integrations/git/gitAtomicity.ts +42 -9
  31. package/integrations/git/resolveGitRefs.ts +37 -0
  32. package/integrations/git/runPlatformGate.ts +228 -1
  33. package/integrations/git/runPlatformGateEvaluation.ts +4 -0
  34. package/integrations/git/stageRunners.ts +116 -2
  35. package/integrations/lifecycle/cli.ts +759 -22
  36. package/integrations/lifecycle/doctor.ts +62 -0
  37. package/integrations/lifecycle/index.ts +1 -0
  38. package/integrations/lifecycle/packageInfo.ts +25 -3
  39. package/integrations/lifecycle/policyReconcile.ts +304 -0
  40. package/integrations/lifecycle/preWriteAutomation.ts +42 -2
  41. package/integrations/lifecycle/watch.ts +365 -0
  42. package/integrations/mcp/aiGateCheck.ts +59 -2
  43. package/integrations/mcp/autoExecuteAiStart.ts +25 -1
  44. package/integrations/mcp/preFlightCheck.ts +13 -0
  45. package/integrations/sdd/evidenceScaffold.ts +223 -0
  46. package/integrations/sdd/index.ts +2 -0
  47. package/integrations/sdd/stateSync.ts +400 -0
  48. package/integrations/sdd/syncDocs.ts +97 -2
  49. package/package.json +4 -1
  50. package/scripts/backlog-action-reasons-lib.ts +38 -0
  51. package/scripts/backlog-id-issue-map-lib.ts +69 -0
  52. package/scripts/backlog-json-contract-lib.ts +3 -0
  53. package/scripts/framework-menu-consumer-preflight-lib.ts +6 -0
  54. package/scripts/framework-menu-system-notifications-lib.ts +66 -6
  55. package/scripts/package-install-smoke-command-resolution-lib.ts +64 -0
  56. package/scripts/package-install-smoke-consumer-npm-lib.ts +43 -0
  57. package/scripts/package-install-smoke-consumer-repo-setup-lib.ts +2 -0
  58. package/scripts/package-install-smoke-execution-steps-lib.ts +27 -9
  59. package/scripts/package-install-smoke-lifecycle-lib.ts +15 -4
  60. package/scripts/package-install-smoke-workspace-factory-lib.ts +4 -1
  61. package/scripts/reconcile-consumer-backlog-issues-lib.ts +651 -0
  62. package/scripts/reconcile-consumer-backlog-issues.ts +348 -0
  63. package/scripts/watch-consumer-backlog-lib.ts +465 -0
  64. package/scripts/watch-consumer-backlog.ts +326 -0
@@ -5,6 +5,7 @@ Este directorio contiene solo documentación oficial y estable de validación pa
5
5
  ## Documentación oficial vigente
6
6
 
7
7
  - `adapter-hook-runtime-validation.md`
8
+ - `ast-intelligence-roadmap.md`
8
9
  - `c022-phase-acceptance-contract.md`
9
10
  - `detection-audit-baseline.md`
10
11
  - `enterprise-consumer-isolation-policy.md`
@@ -14,7 +15,7 @@ Este directorio contiene solo documentación oficial y estable de validación pa
14
15
  ## Estado operativo actual
15
16
 
16
17
  - Master de seguimiento: `docs/registro-maestro-de-seguimiento.md`.
17
- - Plan activo: `docs/seguimiento-completo-validacion-ruralgo-03-03-2026.md`.
18
+ - Plan activo: `docs/seguimiento-activo-pumuki-saas-supermercados.md`.
18
19
  - Suite contractual enterprise (MVP): `npm run -s validation:contract-suite:enterprise`.
19
20
  - Perfiles activos del reporte JSON:
20
21
  - `minimal`
@@ -0,0 +1,96 @@
1
+ # AST Intelligence por Nodos (RFC + PoC)
2
+
3
+ Estado: implementado en modo incremental (issue `#616`).
4
+
5
+ ## Objetivo
6
+
7
+ Evolucionar de validación heurística clásica a validación AST por nodos, multilenguaje y trazable desde reglas compiladas de skills.
8
+
9
+ ## Alcance implementado (PoC v1)
10
+
11
+ - Modo dual de validación legacy + AST (`off/shadow/strict`).
12
+ - Comparación determinista por regla `skills.*` contra nodos `ast_nodes=[...]` compilados en runtime (`skills-ir:*`).
13
+ - Métricas de divergencia por ejecución:
14
+ - `mapped_rules`
15
+ - `divergences`
16
+ - `false_positives`
17
+ - `false_negatives`
18
+ - `latency_ms`
19
+ - `languages`
20
+ - Integración directa en gate de `PRE_COMMIT`, `PRE_PUSH`, `CI`.
21
+
22
+ PoC cubre lenguajes críticos ya usados en el runtime actual:
23
+
24
+ - TypeScript
25
+ - Swift
26
+ - Kotlin
27
+
28
+ ## Arquitectura objetivo (30/60/90)
29
+
30
+ ### 0-30 días (P0)
31
+
32
+ - Consolidar modo dual como baseline estable:
33
+ - `off`: desactivado.
34
+ - `shadow`: compara y reporta sin bloquear.
35
+ - `strict`: bloquea si hay divergencias.
36
+ - Mantener compatibilidad total con rulepacks existentes.
37
+ - Publicar métricas mínimas para auditoría de divergencias.
38
+
39
+ ### 31-60 días (P1)
40
+
41
+ - Backends AST por lenguaje con extractor dedicado (TS/Swift/Kotlin en prioridad).
42
+ - Trazabilidad 1:1 `skill rule -> ast node -> finding`.
43
+ - Telemetría con series temporales de FP/FN por stage.
44
+
45
+ ### 61-90 días (P2)
46
+
47
+ - Rollout gradual por porcentaje/repositorio.
48
+ - Políticas por stage para strict automático cuando FP/FN estén bajo umbral.
49
+ - Contratos de regresión multi-repo (fixtures enterprise).
50
+
51
+ ## Contrato de compatibilidad (legacy + nuevo)
52
+
53
+ - El modo legacy sigue siendo la referencia de bloqueo por defecto.
54
+ - El modo AST entra en:
55
+ - `shadow` para medir paridad.
56
+ - `strict` para enforcement cuando la paridad sea aceptable.
57
+ - No se rompe compatibilidad con bundles/rules actuales; el modo dual usa la metadata runtime `skills-ir:*`.
58
+
59
+ ## Rollout y rollback
60
+
61
+ ### Rollout
62
+
63
+ 1. Activar en `shadow`:
64
+
65
+ ```bash
66
+ PUMUKI_AST_INTELLIGENCE_DUAL_MODE=shadow
67
+ ```
68
+
69
+ 2. Observar divergencias por stage/repositorio.
70
+ 3. Promover a `strict` cuando divergencias y latencia estén dentro de umbral.
71
+
72
+ ### Rollback inmediato
73
+
74
+ ```bash
75
+ PUMUKI_AST_INTELLIGENCE_DUAL_MODE=off
76
+ ```
77
+
78
+ ## Umbrales recomendados para pasar a strict
79
+
80
+ - `false_positives` y `false_negatives` cerca de 0 de forma sostenida.
81
+ - `latency_ms` aceptable para hooks locales.
82
+ - Sin regresiones en `test:stage-gates`.
83
+
84
+ ## Evidencia técnica de esta entrega
85
+
86
+ - Runtime:
87
+ - `integrations/git/astIntelligenceDualValidation.ts`
88
+ - `integrations/git/runPlatformGate.ts`
89
+ - `integrations/git/runPlatformGateEvaluation.ts`
90
+ - Tests:
91
+ - `integrations/git/__tests__/astIntelligenceDualValidation.test.ts`
92
+ - `integrations/git/__tests__/runPlatformGateAstIntelligenceDualMode.test.ts`
93
+ - `integrations/git/__tests__/runPlatformGateEvaluation.test.ts`
94
+ - Validación:
95
+ - `npm run -s typecheck`
96
+ - `npm run -s test:stage-gates`
@@ -28,6 +28,7 @@ type CustomSkillsRuleV1 = {
28
28
  confidence?: SkillsRuleConfidence;
29
29
  locked?: boolean;
30
30
  evaluationMode?: SkillsRuleEvaluationMode;
31
+ ast_node_ids?: string[];
31
32
  };
32
33
 
33
34
  export type CustomSkillsRulesFileV1 = {
@@ -113,6 +114,14 @@ const isCustomRule = (value: unknown): value is CustomSkillsRuleV1 => {
113
114
  ) {
114
115
  return false;
115
116
  }
117
+ if (
118
+ typeof value.ast_node_ids !== 'undefined' &&
119
+ (!Array.isArray(value.ast_node_ids) ||
120
+ value.ast_node_ids.length === 0 ||
121
+ !value.ast_node_ids.every(isNonEmptyString))
122
+ ) {
123
+ return false;
124
+ }
116
125
  if (typeof value.locked !== 'undefined' && typeof value.locked !== 'boolean') {
117
126
  return false;
118
127
  }
@@ -196,6 +205,10 @@ const toCustomRulesFile = (
196
205
  confidence: rule.confidence,
197
206
  locked: rule.locked ?? false,
198
207
  evaluationMode: rule.evaluationMode ?? 'AUTO',
208
+ ast_node_ids:
209
+ rule.astNodeIds && rule.astNodeIds.length > 0
210
+ ? [...new Set(rule.astNodeIds)].sort()
211
+ : undefined,
199
212
  }))
200
213
  .sort((left, right) => left.id.localeCompare(right.id));
201
214
 
@@ -239,6 +252,7 @@ export const loadCustomSkillsLock = (
239
252
 
240
253
  const rules: SkillsCompiledRule[] = payload.rules.map((rule) => ({
241
254
  ...rule,
255
+ astNodeIds: rule.ast_node_ids,
242
256
  sourceSkill: 'custom-guidelines',
243
257
  sourcePath: '.pumuki/custom-rules.json',
244
258
  evaluationMode: rule.evaluationMode ?? 'AUTO',
@@ -1,4 +1,4 @@
1
- import type { SkillsCompiledRule } from './skillsLock';
1
+ import { normalizeSkillsAstNodeIds, type SkillsCompiledRule } from './skillsLock';
2
2
 
3
3
  type SkillsDetectorKind = 'heuristic';
4
4
 
@@ -153,3 +153,13 @@ export const resolveMappedHeuristicRuleIds = (
153
153
  ): ReadonlyArray<string> => {
154
154
  return registryByRuleId[ruleId]?.mappedHeuristicRuleIds ?? [];
155
155
  };
156
+
157
+ export const resolveMappedHeuristicRuleIdsForCompiledRule = (
158
+ rule: SkillsCompiledRule
159
+ ): ReadonlyArray<string> => {
160
+ const dynamicAstNodeIds = normalizeSkillsAstNodeIds(rule.astNodeIds);
161
+ if (dynamicAstNodeIds.length > 0) {
162
+ return dynamicAstNodeIds;
163
+ }
164
+ return resolveMappedHeuristicRuleIds(rule.id);
165
+ };
@@ -10,6 +10,7 @@ export type SkillsStage = Exclude<GateStage, 'STAGED'>;
10
10
  export type SkillsRuleConfidence = 'HIGH' | 'MEDIUM' | 'LOW';
11
11
  export type SkillsRuleEvaluationMode = 'AUTO' | 'DECLARATIVE';
12
12
  export type SkillsRuleOrigin = 'core' | 'custom';
13
+ export type SkillsAstNodeId = string;
13
14
 
14
15
  export type SkillsCompiledRule = {
15
16
  id: string;
@@ -23,6 +24,7 @@ export type SkillsCompiledRule = {
23
24
  locked?: boolean;
24
25
  evaluationMode?: SkillsRuleEvaluationMode;
25
26
  origin?: SkillsRuleOrigin;
27
+ astNodeIds?: ReadonlyArray<SkillsAstNodeId>;
26
28
  };
27
29
 
28
30
  export type SkillsLockBundle = {
@@ -43,6 +45,7 @@ export type SkillsLockV1 = {
43
45
  const SKILLS_LOCK_FILE = 'skills.lock.json';
44
46
  const SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
45
47
  const SHA256_PATTERN = /^[A-Fa-f0-9]{64}$/;
48
+ const AST_NODE_ID_PATTERN = /^heuristics\.[A-Za-z0-9._-]+\.ast$/;
46
49
 
47
50
  const severityValues = new Set<Severity>(['INFO', 'WARN', 'ERROR', 'CRITICAL']);
48
51
  const stageValues = new Set<SkillsStage>(['PRE_COMMIT', 'PRE_PUSH', 'CI']);
@@ -90,6 +93,23 @@ const isRuleEvaluationMode = (value: unknown): value is SkillsRuleEvaluationMode
90
93
  return typeof value === 'string' && evaluationModeValues.has(value as SkillsRuleEvaluationMode);
91
94
  };
92
95
 
96
+ const isAstNodeId = (value: unknown): value is SkillsAstNodeId => {
97
+ return typeof value === 'string' && AST_NODE_ID_PATTERN.test(value.trim());
98
+ };
99
+
100
+ export const normalizeSkillsAstNodeIds = (
101
+ astNodeIds?: ReadonlyArray<SkillsAstNodeId>
102
+ ): SkillsAstNodeId[] => {
103
+ if (!astNodeIds || astNodeIds.length === 0) {
104
+ return [];
105
+ }
106
+
107
+ const normalized = astNodeIds
108
+ .map((value) => value.trim().toLowerCase())
109
+ .filter((value) => AST_NODE_ID_PATTERN.test(value));
110
+ return [...new Set(normalized)].sort();
111
+ };
112
+
93
113
  const isRuleOrigin = (value: unknown): value is SkillsRuleOrigin => {
94
114
  return typeof value === 'string' && originValues.has(value as SkillsRuleOrigin);
95
115
  };
@@ -146,6 +166,15 @@ const isSkillsCompiledRule = (value: unknown): value is SkillsCompiledRule => {
146
166
  return false;
147
167
  }
148
168
 
169
+ if (
170
+ typeof value.astNodeIds !== 'undefined' &&
171
+ (!Array.isArray(value.astNodeIds) ||
172
+ value.astNodeIds.length === 0 ||
173
+ value.astNodeIds.some((item) => !isAstNodeId(item)))
174
+ ) {
175
+ return false;
176
+ }
177
+
149
178
  if (typeof value.origin !== 'undefined' && !isRuleOrigin(value.origin)) {
150
179
  return false;
151
180
  }
@@ -221,6 +250,7 @@ const normalizedRuleForHash = (rule: SkillsCompiledRule): Record<string, string
221
250
  locked: rule.locked ?? false,
222
251
  evaluationMode: rule.evaluationMode ?? null,
223
252
  origin: rule.origin ?? null,
253
+ astNodeIds: normalizeSkillsAstNodeIds(rule.astNodeIds),
224
254
  };
225
255
  };
226
256
 
@@ -13,6 +13,7 @@ const MARKDOWN_LINK_PATTERN = /\[([^\]]+)\]\(([^)]+)\)/g;
13
13
  const INLINE_CODE_PATTERN = /`([^`]+)`/g;
14
14
  const MARKDOWN_BOLD_PATTERN = /[*_]{1,3}/g;
15
15
  const MULTISPACE_PATTERN = /\s+/g;
16
+ const AST_NODE_ID_PATTERN = /\bheuristics\.[a-z0-9._-]+\.ast\b/gi;
16
17
  const RULE_KEYWORDS =
17
18
  /\b(always|siempre|prefer|use|usar|avoid|evitar|never|nunca|must|obligatorio|required|disallow|do not|no)\b/i;
18
19
 
@@ -132,6 +133,15 @@ const extractRuleCandidateLines = (markdown: string): string[] => {
132
133
  return candidates;
133
134
  };
134
135
 
136
+ const extractAstNodeIdsFromLine = (line: string): string[] => {
137
+ const matches = line.match(AST_NODE_ID_PATTERN);
138
+ if (!matches) {
139
+ return [];
140
+ }
141
+ const normalized = matches.map((token) => token.trim().toLowerCase());
142
+ return [...new Set(normalized)].sort();
143
+ };
144
+
135
145
  const resolvePlatformFromBundle = (
136
146
  bundleName: string
137
147
  ): SkillsCompiledRule['platform'] => {
@@ -301,6 +311,7 @@ export const extractCompiledRulesFromSkillMarkdown = (params: {
301
311
  if (description.length < 6) {
302
312
  continue;
303
313
  }
314
+ const astNodeIds = extractAstNodeIdsFromLine(rawLine);
304
315
 
305
316
  const knownRuleId = normalizeKnownRuleTarget(platform, normalizeForLookup(description));
306
317
  let nextId: string;
@@ -325,9 +336,8 @@ export const extractCompiledRulesFromSkillMarkdown = (params: {
325
336
  }
326
337
  usedIds.add(nextId);
327
338
 
328
- const evaluationMode: SkillsRuleEvaluationMode = knownRuleId
329
- ? 'AUTO'
330
- : 'DECLARATIVE';
339
+ const evaluationMode: SkillsRuleEvaluationMode =
340
+ knownRuleId || astNodeIds.length > 0 ? 'AUTO' : 'DECLARATIVE';
331
341
 
332
342
  rules.push({
333
343
  id: nextId,
@@ -341,6 +351,7 @@ export const extractCompiledRulesFromSkillMarkdown = (params: {
341
351
  locked: true,
342
352
  evaluationMode,
343
353
  origin: params.origin ?? 'core',
354
+ ...(astNodeIds.length > 0 ? { astNodeIds } : {}),
344
355
  });
345
356
  }
346
357
 
@@ -13,7 +13,7 @@ import {
13
13
  import { loadSkillsPolicy, type SkillsBundlePolicy } from './skillsPolicy';
14
14
  import type { DetectedPlatforms } from '../platform/detectPlatforms';
15
15
  import { loadEffectiveSkillsLock } from './skillsEffectiveLock';
16
- import { resolveMappedHeuristicRuleIds } from './skillsDetectorRegistry';
16
+ import { resolveMappedHeuristicRuleIdsForCompiledRule } from './skillsDetectorRegistry';
17
17
 
18
18
  export type SkillsRuleSetLoadResult = {
19
19
  rules: RuleSet;
@@ -240,6 +240,22 @@ const toCode = (ruleId: string): string => {
240
240
  return `SKILLS_${ruleId.replace(/[^A-Za-z0-9]+/g, '_').toUpperCase()}`;
241
241
  };
242
242
 
243
+ const toSkillsRuntimeIrSource = (params: {
244
+ rule: SkillsCompiledRule;
245
+ mappedHeuristicRuleIds: ReadonlyArray<string>;
246
+ }): string => {
247
+ const astNodeIds = [...params.mappedHeuristicRuleIds].sort();
248
+ const astNodeToken = astNodeIds.length > 0 ? astNodeIds.join(',') : 'none';
249
+ const evaluationMode = resolveRuleEvaluationMode(params.rule);
250
+ return (
251
+ `skills-ir:rule=${params.rule.id};` +
252
+ `source_skill=${params.rule.sourceSkill};` +
253
+ `source_path=${params.rule.sourcePath};` +
254
+ `evaluation_mode=${evaluationMode};` +
255
+ `ast_nodes=[${astNodeToken}]`
256
+ );
257
+ };
258
+
243
259
  const stageApplies = (
244
260
  currentStage: Exclude<GateStage, 'STAGED'>,
245
261
  ruleStage?: Exclude<GateStage, 'STAGED'>
@@ -333,7 +349,7 @@ const toRuleDefinition = (params: {
333
349
  repoRoot: string;
334
350
  observedFilePaths?: ReadonlyArray<string>;
335
351
  }): RuleDefinition | undefined => {
336
- const mappedHeuristicRuleIds = resolveMappedHeuristicRuleIds(params.rule.id);
352
+ const mappedHeuristicRuleIds = resolveMappedHeuristicRuleIdsForCompiledRule(params.rule);
337
353
 
338
354
  if (!stageApplies(params.stage, params.rule.stage)) {
339
355
  return undefined;
@@ -345,6 +361,10 @@ const toRuleDefinition = (params: {
345
361
  bundlePolicy: params.bundlePolicy,
346
362
  stage: params.stage,
347
363
  });
364
+ const runtimeIrSource = toSkillsRuntimeIrSource({
365
+ rule: params.rule,
366
+ mappedHeuristicRuleIds,
367
+ });
348
368
 
349
369
  if (evaluationMode === 'AUTO') {
350
370
  if (mappedHeuristicRuleIds.length === 0) {
@@ -377,6 +397,7 @@ const toRuleDefinition = (params: {
377
397
  kind: 'Finding',
378
398
  message: params.rule.description,
379
399
  code: toCode(params.rule.id),
400
+ source: runtimeIrSource,
380
401
  },
381
402
  scope: resolveScopeForPlatform(
382
403
  params.rule.platform,
@@ -403,6 +424,7 @@ const toRuleDefinition = (params: {
403
424
  kind: 'Finding',
404
425
  message: `[Declarative] ${params.rule.description}`,
405
426
  code: `${toCode(params.rule.id)}_DECLARATIVE`,
427
+ source: runtimeIrSource,
406
428
  },
407
429
  scope: resolveScopeForPlatform(
408
430
  params.rule.platform,
@@ -500,7 +522,7 @@ export const loadSkillsRuleSetForStage = (
500
522
  continue;
501
523
  }
502
524
 
503
- const mappedRuleIds = resolveMappedHeuristicRuleIds(compiledRule.id);
525
+ const mappedRuleIds = resolveMappedHeuristicRuleIdsForCompiledRule(compiledRule);
504
526
  const evaluationMode = resolveRuleEvaluationMode(compiledRule);
505
527
  if (evaluationMode === 'AUTO' && mappedRuleIds.length === 0) {
506
528
  unsupportedAutoRuleIds.add(compiledRule.id);
@@ -3,6 +3,7 @@ import { writeFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import test from 'node:test';
5
5
  import { withTempDir } from '../__tests__/helpers/tempDir';
6
+ import { getCurrentPumukiVersion } from '../lifecycle/packageInfo';
6
7
  import type { AiEvidenceV2_1 } from './schema';
7
8
  import { readEvidence, readEvidenceResult } from './readEvidence';
8
9
  import { buildEvidenceChain } from './evidenceChain';
@@ -49,8 +50,8 @@ const sampleEvidence = (): AiEvidenceV2_1 => {
49
50
  },
50
51
  lifecycle: {
51
52
  installed: true,
52
- package_version: '6.3.16',
53
- lifecycle_version: '6.3.16',
53
+ package_version: getCurrentPumukiVersion(),
54
+ lifecycle_version: getCurrentPumukiVersion(),
54
55
  hooks: {
55
56
  pre_commit: 'managed',
56
57
  pre_push: 'managed',
@@ -25,8 +25,15 @@ export type EvidenceReadResult =
25
25
  const toDigest = (value: string): string =>
26
26
  `sha256:${createHash('sha256').update(value, 'utf8').digest('hex')}`;
27
27
 
28
- export const readEvidenceResult = (repoRoot: string): EvidenceReadResult => {
29
- const evidencePath = resolve(repoRoot, '.ai_evidence.json');
28
+ export const readEvidenceResult = (
29
+ repoRoot: string,
30
+ options?: { evidencePath?: string }
31
+ ): EvidenceReadResult => {
32
+ const evidencePathInput = options?.evidencePath?.trim();
33
+ const evidencePath =
34
+ typeof evidencePathInput === 'string' && evidencePathInput.length > 0
35
+ ? resolve(repoRoot, evidencePathInput)
36
+ : resolve(repoRoot, '.ai_evidence.json');
30
37
  if (!existsSync(evidencePath)) {
31
38
  return {
32
39
  kind: 'missing',
@@ -126,7 +133,10 @@ export const readEvidenceResult = (repoRoot: string): EvidenceReadResult => {
126
133
  }
127
134
  };
128
135
 
129
- export const readEvidence = (repoRoot: string): AiEvidenceV2_1 | undefined => {
130
- const result = readEvidenceResult(repoRoot);
136
+ export const readEvidence = (
137
+ repoRoot: string,
138
+ options?: { evidencePath?: string }
139
+ ): AiEvidenceV2_1 | undefined => {
140
+ const result = readEvidenceResult(repoRoot, options);
131
141
  return result.kind === 'valid' ? result.evidence : undefined;
132
142
  };
@@ -2,6 +2,7 @@ import { execFileSync as runBinarySync } from 'node:child_process';
2
2
  import { existsSync, readFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { readLifecycleStatus } from '../lifecycle/status';
5
+ import { resolvePumukiVersionMetadata } from '../lifecycle/packageInfo';
5
6
  import type { RepoHardModeState, RepoHookState, RepoState } from './schema';
6
7
 
7
8
  type HookStateShape = { exists: boolean; managedBlockPresent: boolean };
@@ -129,6 +130,9 @@ export const captureRepoState = (repoRoot: string): RepoState => {
129
130
  const unstaged = statusLines.filter((line) => line[1] && line[1] !== ' ').length;
130
131
  const { ahead, behind } = toAheadBehind(repoRoot, upstream);
131
132
  const lifecycle = readLifecycleStatusSafe(repoRoot);
133
+ const versionMetadata = resolvePumukiVersionMetadata({ repoRoot });
134
+ const consumerFacingVersion = versionMetadata.resolvedVersion;
135
+ const installedVersion = versionMetadata.consumerInstalledVersion;
132
136
  const hardModeState = readHardModeState(repoRoot);
133
137
 
134
138
  return {
@@ -145,8 +149,12 @@ export const captureRepoState = (repoRoot: string): RepoState => {
145
149
  },
146
150
  lifecycle: {
147
151
  installed: lifecycle.lifecycleState.installed === 'true',
148
- package_version: lifecycle.packageVersion ?? null,
149
- lifecycle_version: lifecycle.lifecycleState.version ?? null,
152
+ // package/lifecycle version should be stable from consumer perspective.
153
+ package_version: consumerFacingVersion,
154
+ lifecycle_version: consumerFacingVersion,
155
+ package_version_source: versionMetadata.source,
156
+ package_version_runtime: versionMetadata.runtimeVersion,
157
+ package_version_installed: installedVersion,
150
158
  hooks: {
151
159
  pre_commit: toHookState(lifecycle.hookStatus['pre-commit']),
152
160
  pre_push: toHookState(lifecycle.hookStatus['pre-push']),
@@ -1,5 +1,6 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import test from 'node:test';
3
+ import { getCurrentPumukiVersion } from '../lifecycle/packageInfo';
3
4
  import type { AiEvidenceV2_1, CompatibilityViolation, HumanIntentState, SnapshotFinding } from './schema';
4
5
 
5
6
  const sampleIntent = (): HumanIntentState => ({
@@ -122,8 +123,8 @@ test('AiEvidenceV2_1 soporta snapshot/ledger/platforms/rulesets con contrato 2.1
122
123
  },
123
124
  lifecycle: {
124
125
  installed: true,
125
- package_version: '6.3.16',
126
- lifecycle_version: '6.3.16',
126
+ package_version: getCurrentPumukiVersion(),
127
+ lifecycle_version: getCurrentPumukiVersion(),
127
128
  hooks: {
128
129
  pre_commit: 'managed',
129
130
  pre_push: 'managed',
@@ -169,6 +169,9 @@ export type RepoState = {
169
169
  installed: boolean;
170
170
  package_version: string | null;
171
171
  lifecycle_version: string | null;
172
+ package_version_source?: 'consumer-node-modules' | 'runtime-package';
173
+ package_version_runtime?: string | null;
174
+ package_version_installed?: string | null;
172
175
  hooks: {
173
176
  pre_commit: RepoHookState;
174
177
  pre_push: RepoHookState;
@@ -4,6 +4,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
5
  import test from 'node:test';
6
6
  import { withTempDir } from '../__tests__/helpers/tempDir';
7
+ import { getCurrentPumukiVersion } from '../lifecycle/packageInfo';
7
8
  import type { AiEvidenceV2_1 } from './schema';
8
9
  import { writeEvidence } from './writeEvidence';
9
10
 
@@ -135,8 +136,8 @@ const sampleEvidence = (repoRoot: string): AiEvidenceV2_1 => ({
135
136
  },
136
137
  lifecycle: {
137
138
  installed: true,
138
- package_version: '6.3.16',
139
- lifecycle_version: '6.3.16',
139
+ package_version: getCurrentPumukiVersion(),
140
+ lifecycle_version: getCurrentPumukiVersion(),
140
141
  hooks: {
141
142
  pre_commit: 'managed',
142
143
  pre_push: 'managed',