pumuki 6.3.29 → 6.3.31

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.
@@ -7,7 +7,7 @@
7
7
  ## Estado actual
8
8
  - Plan activo: `docs/seguimiento-completo-validacion-ruralgo-03-03-2026.md`
9
9
  - Estado del plan: EN CURSO
10
- - Task activa (`🚧`): `P12.F1.T47` (consolidar monitoreo único de bloqueo remoto `#543/#544` y cerrar administrativamente al desbloquear checks).
10
+ - Task activa (`🚧`): `P12.F2.T51` (publicar release con el hardening `#551` y cerrar trazabilidad final en canónico + master).
11
11
 
12
12
  ## Historial resumido
13
13
  - No se mantienen MDs históricos de seguimiento en este repositorio.
@@ -1243,13 +1243,13 @@ Criterio de salida F5:
1243
1243
  - `gh issue create --title "[P2][OPS] Telemetría estructurada exportable (JSONL/OTel) para auditoría enterprise" ...`
1244
1244
  - edición de `R_GO/docs/technical/08-validation/refactor/pumuki-integration-feedback.md`
1245
1245
  - edición de `R_GO/ruralgo-master-plan.md`
1246
- - `P12.F1.T40` Ejecutar implementación técnica de la mejora estratégica `#543` (policy-as-code versionada/firmada) en Pumuki con ciclo RED -> GREEN -> REFACTOR y trazabilidad E2E.
1247
- - avance en curso:
1246
+ - `P12.F1.T40` Ejecutar implementación técnica de la mejora estratégica `#543` (policy-as-code versionada/firmada) en Pumuki con ciclo RED -> GREEN -> REFACTOR y trazabilidad E2E.
1247
+ - cierre ejecutado:
1248
1248
  - rama de implementación: `feature/543-policy-as-code-signed`.
1249
1249
  - commits técnicos:
1250
1250
  - `a24bf6c` (`feat(gate): add policy-as-code trace metadata and strict validation guard`).
1251
1251
  - `594a83c` (`feat(policy): add contract expiry validation and strict blocking coverage`).
1252
- - PR abierta: `https://github.com/SwiftEnProfundidad/ast-intelligence-hooks/pull/545`.
1252
+ - PR mergeada: `https://github.com/SwiftEnProfundidad/ast-intelligence-hooks/pull/545`.
1253
1253
  - tests/verificación ejecutados en verde:
1254
1254
  - `npx --yes tsx@4.21.0 --test integrations/gate/__tests__/stagePolicies.test.ts integrations/git/__tests__/runPlatformGate.test.ts integrations/git/__tests__/hookGateSummary.test.ts integrations/git/__tests__/EvidenceService.test.ts`
1255
1255
  - `npm run -s typecheck`
@@ -1257,7 +1257,7 @@ Criterio de salida F5:
1257
1257
  - contrato `policy-as-code` con validación runtime `valid/invalid/expired/unknown-source`.
1258
1258
  - bloqueo strict (`PUMUKI_POLICY_STRICT=1`) con códigos explícitos de policy.
1259
1259
  - documentación operativa mínima añadida en `README.md`.
1260
- - bloqueo actual:
1260
+ - contexto histórico de bloqueo (resuelto por bypass administrativo autorizado):
1261
1261
  - checks remotos de GitHub Actions en `#545` fallan de forma sistémica y sin logs recuperables (`log not found` en `gh run view --log`).
1262
1262
  - `security/snyk` reporta límite de cuota privada (`You have used your limit of private tests`).
1263
1263
  - pendiente decisión de merge cuando se desbloquee infraestructura/checks remotos.
@@ -1266,7 +1266,10 @@ Criterio de salida F5:
1266
1266
  - `gh pr checks 545 --json name,state,bucket,link,description`
1267
1267
  - `gh run view 22647944444 --job 65640392393 --log`
1268
1268
 
1269
- - `P12.F1.T41` Desbloquear cierre administrativo de `#543` (merge PR `#545` + cierre issue `#543`) cuando los checks remotos vuelvan a estado operativo.
1269
+ - `P12.F1.T41` Desbloquear cierre administrativo de `#543` (merge PR `#545` + cierre issue `#543`) cuando los checks remotos vuelvan a estado operativo.
1270
+ - cierre ejecutado:
1271
+ - PR `#545` mergeada (commit: `71809fa5d281b2bab62c7e32d89ae4d1b49f0ea3`).
1272
+ - issue `#543` cerrada.
1270
1273
  - issue operativa de soporte creada: `#546` (`ops: unblock remote PR checks (snyk quota + missing job logs)`).
1271
1274
  - avance ejecutado:
1272
1275
  - reintento de runs fallidos lanzado en lote (`gh run rerun <run_id> --failed`) para descartar flake.
@@ -1275,7 +1278,7 @@ Criterio de salida F5:
1275
1278
  - jobs de `CI` con `steps_count=0` en attempt 2.
1276
1279
  - ZIP de logs del run disponible, pero solo contiene `system.txt` (sin logs de pasos de ejecución).
1277
1280
  - issue `#543`, issue `#546` y PR `#545` actualizadas con comentario de progreso y bloqueo remoto.
1278
- - bloqueo reproducido (comando/error):
1281
+ - bloqueo reproducido (comando/error, histórico):
1279
1282
  - comando: `gh run view 22648051050`
1280
1283
  - error observado: `The job was not started because your account is locked due to a billing issue.` (anotación repetida en todos los jobs de `CI`).
1281
1284
  - comando: `gh pr checks 545 --json name,state,bucket,link,description`
@@ -1310,8 +1313,11 @@ Criterio de salida F5:
1310
1313
  - `npm run -s typecheck`
1311
1314
  - `gh pr view 547 --json state,mergeStateStatus,statusCheckRollup,headRefOid,url`
1312
1315
 
1313
- - `P12.F1.T44` Desbloquear cierre administrativo de `#544` (merge PR `#547` + cierre issue `#544`) cuando los checks remotos vuelvan a estado operativo.
1314
- - bloqueo reproducido (comando/error):
1316
+ - `P12.F1.T44` Desbloquear cierre administrativo de `#544` (merge PR `#547` + cierre issue `#544`) cuando los checks remotos vuelvan a estado operativo.
1317
+ - cierre ejecutado:
1318
+ - PR `#547` mergeada (commit: `76e385fb43778b273ee347dbe9254bbf472f8063`).
1319
+ - issue `#544` cerrada.
1320
+ - bloqueo reproducido (comando/error, histórico):
1315
1321
  - comando: `gh run view 22648407847`
1316
1322
  - error observado: `The job was not started because your account is locked due to a billing issue.` (anotación repetida en jobs de `CI`).
1317
1323
  - comando: `gh pr checks 547 --json name,state,bucket,link,description`
@@ -1320,10 +1326,21 @@ Criterio de salida F5:
1320
1326
  - desbloquear billing de la cuenta/organización de GitHub Actions.
1321
1327
  - restaurar cuota/permisos de Snyk o ajustar temporalmente ese check requerido.
1322
1328
 
1323
- - `P12.F1.T42` Sincronizar canónico RuralGO tras cierre de `#543` (`REPORTED -> FIXED` en feedback + master plan con refs reales).
1324
- - salida esperada:
1325
- - `R_GO/docs/technical/08-validation/refactor/pumuki-integration-feedback.md` actualizado a `FIXED` para `PUMUKI-INC-055`.
1326
- - `R_GO/ruralgo-master-plan.md` con leyenda actualizada (solo 1 `🚧` activa) y referencia de issue/PR/commit.
1329
+ - `P12.F1.T42` Sincronizar canónico RuralGO tras cierre de `#543` (`REPORTED -> FIXED` en feedback + master plan con refs reales).
1330
+ - cierre ejecutado:
1331
+ - se resolvió el bloqueo histórico de rama creando una rama docs limpia desde `origin/develop` en `R_GO`: `docs/rgo-sync-issue-543-fixed-20260304`.
1332
+ - commit documental atómico aplicado: `66d88dacb` (`docs(validation): sync canonical issue 543 to fixed`).
1333
+ - PR abierta en `R_GO`: `https://github.com/SwiftEnProfundidad/R_GO/pull/1503`.
1334
+ - canónico actualizado en la PR:
1335
+ - `docs/technical/08-validation/refactor/pumuki-integration-feedback.md` (`PUMUKI-INC-055 => ✅ FIXED`).
1336
+ - `ruralgo-master-plan.md` (`#543 => ✅`, leyenda recalculada).
1337
+ - evidencia:
1338
+ - `git -C /tmp/rgo-sync-issue-543-*/ commit -m "docs(validation): sync canonical issue 543 to fixed"`
1339
+ - `git -C /tmp/rgo-sync-issue-543-*/ push -u origin docs/rgo-sync-issue-543-fixed-20260304`
1340
+ - `gh pr create --base develop --head docs/rgo-sync-issue-543-fixed-20260304`
1341
+ - hook gates en verde:
1342
+ - `pre-commit => ALLOW/PASS`
1343
+ - `pre-push => ALLOW/PASS`
1327
1344
 
1328
1345
  - ✅ `P12.F1.T46` Sincronizar canónico RuralGO en estado intermedio de `#544` (`REPORTED` con refs reales de issue/branch/PR/commit/evidencia) y alinear master plan con una única mejora `🚧`.
1329
1346
  - cierre ejecutado:
@@ -1338,8 +1355,10 @@ Criterio de salida F5:
1338
1355
  - `npx --yes --package pumuki@latest pumuki-pre-commit` (gate `ALLOW/PASS`)
1339
1356
  - `npx --yes --package pumuki@latest pumuki-pre-push` (gate `ALLOW/PASS`)
1340
1357
 
1341
- - 🚧 `P12.F1.T47` Consolidar monitoreo único del bloqueo remoto para `#543/#544` en issue operativa `#546` y ejecutar cierre administrativo automático en cuanto los checks remotos queden operativos.
1342
- - avance en curso:
1358
+ - `P12.F1.T47` Consolidar monitoreo único del bloqueo remoto para `#543/#544` en issue operativa `#546` y ejecutar cierre administrativo automático en cuanto los checks remotos queden operativos.
1359
+ - cierre ejecutado:
1360
+ - issue operativa `#546` cerrada tras merge y publicación.
1361
+ - publicación npm completada: `pumuki@6.3.29` (tag `v6.3.29`).
1343
1362
  - issue operativa actualizada con seguimiento de `#547`:
1344
1363
  - `https://github.com/SwiftEnProfundidad/ast-intelligence-hooks/issues/546#issuecomment-3994327893`
1345
1364
  - `https://github.com/SwiftEnProfundidad/ast-intelligence-hooks/issues/546#issuecomment-3994336761`
@@ -1670,10 +1689,58 @@ Criterio de salida F5:
1670
1689
  - `gh run rerun 22648216106 --failed` (rerun solicitado en iteración actual).
1671
1690
  - `gh run rerun 22670371464 --failed` (rerun solicitado en iteración actual).
1672
1691
 
1673
- - `P12.F1.T45` Sincronizar canónico RuralGO tras cierre de `#544` (`REPORTED -> FIXED` en feedback + master plan con refs reales).
1692
+ - `P12.F1.T48` Corregir regresión crítica de packaging en `pumuki@6.3.29` (módulo telemetry ausente en npm) y publicar patch release.
1693
+ - cierre ejecutado:
1694
+ - issue creada: `#548` (`bug: npm package misses integrations/telemetry and breaks hooks at runtime`).
1695
+ - fix implementado:
1696
+ - `package.json` incluye `integrations/telemetry/*.ts` en `files`.
1697
+ - `scripts/package-manifest-lib.ts` exige `integrations/telemetry/gateTelemetry.ts` como ruta requerida.
1698
+ - PR mergeada: `#549` (commit de merge `1075752d338f38354772781b5e32e0face35189c`).
1699
+ - issue `#548` cerrada.
1700
+ - release publicada: `pumuki@6.3.30` + tag `v6.3.30`.
1701
+ - evidencia:
1702
+ - `npx --yes tsx@4.21.0 --test scripts/__tests__/package-manifest-lib.test.ts scripts/__tests__/check-package-manifest.test.ts`
1703
+ - `npm run -s validation:package-manifest`
1704
+ - `npm pack --json --dry-run | rg "integrations/telemetry/gateTelemetry.ts"`
1705
+ - `npm run -s typecheck`
1706
+ - `npm publish --access public`
1707
+
1708
+ - ✅ `P12.F1.T45` Sincronizar canónico RuralGO tras cierre de `#544` (`REPORTED -> FIXED` en feedback + master plan con refs reales).
1709
+ - cierre ejecutado:
1710
+ - rama docs creada y publicada: `docs/rgo-sync-issue-544-fixed-20260304`.
1711
+ - PR abierta y mergeada: `R_GO#1504` (`https://github.com/SwiftEnProfundidad/R_GO/pull/1504`).
1712
+ - commit de merge en `develop`: `5c0306ba14ab59511a10d12d01a35911481154e3`.
1713
+ - evidencia:
1714
+ - `R_GO/docs/technical/08-validation/refactor/pumuki-integration-feedback.md` actualizado a `✅ FIXED` para `PUMUKI-INC-056`.
1715
+ - `R_GO/ruralgo-master-plan.md` actualizado con leyenda y siguiente mejora estratégica activa.
1716
+ - hooks en verde durante commit/push de la rama docs (`PRE_COMMIT=ALLOW`, `PRE_PUSH=ALLOW`).
1717
+
1718
+ - ✅ `P12.F2.T49` Ejecutar la siguiente mejora estratégica pendiente: suite contractual multi-repo de regresión enterprise (issue -> rama -> PR -> evidencia).
1719
+ - cierre ejecutado:
1720
+ - issue creada y priorizada: `#551` (`feature: suite contractual multi-repo de regresión enterprise`).
1721
+ - sincronización canónica RuralGO aplicada y mergeada: `R_GO#1505` (`https://github.com/SwiftEnProfundidad/R_GO/pull/1505`).
1722
+ - commit de merge en `R_GO/develop`: `64e40d2365f3edf1137cf70a038a638398503db1`.
1723
+ - evidencia:
1724
+ - `R_GO/docs/technical/08-validation/refactor/pumuki-integration-feedback.md` actualizado con `PUMUKI-INC-057` en estado `🚧 REPORTED (#551)`.
1725
+ - `R_GO/ruralgo-master-plan.md` actualizado con mejora estratégica activa referenciada a `#551`.
1726
+
1727
+ - ✅ `P12.F2.T50` Ejecutar implementación técnica de la suite contractual multi-repo (`#551`) con ciclo RED -> GREEN -> REFACTOR y trazabilidad E2E.
1728
+ - cierre ejecutado:
1729
+ - rama técnica creada: `feature/551-enterprise-contract-suite`.
1730
+ - PR abierta y mergeada: `#553` (`https://github.com/SwiftEnProfundidad/ast-intelligence-hooks/pull/553`).
1731
+ - issue cerrada: `#551`.
1732
+ - commit de merge en `develop`: `0a3a005f004f6a65bb33946450cdb382af1a59f6`.
1733
+ - evidencia:
1734
+ - `npx --yes tsx@4.21.0 --test scripts/__tests__/enterprise-contract-suite-args-lib.test.ts scripts/__tests__/enterprise-contract-suite-report-lib.test.ts`
1735
+ - `npm run -s typecheck`
1736
+ - `npm run -s validation:contract-suite:enterprise -- --json`
1737
+ - `npm run -s validation:package-manifest`
1738
+
1739
+ - 🚧 `P12.F2.T51` Publicar release que incluya el hardening `#551` (suite contractual) y dejar trazabilidad final en canónico + master.
1674
1740
  - salida esperada:
1675
- - `R_GO/docs/technical/08-validation/refactor/pumuki-integration-feedback.md` actualizado a `FIXED` para `PUMUKI-INC-056`.
1676
- - `R_GO/ruralgo-master-plan.md` con leyenda final actualizada y referencia de merge real.
1741
+ - versión npm publicada con el MVP de suite contractual multi-repo.
1742
+ - canónico RuralGO y master plan alineados con refs de release.
1743
+ - estado operativo preparado para siguiente bloque de mejoras.
1677
1744
 
1678
1745
  Criterio de salida F6:
1679
1746
  - veredicto final trazable y cierre administrativo completo.
@@ -15,6 +15,7 @@ Este directorio contiene solo documentación oficial y estable de validación pa
15
15
 
16
16
  - Master de seguimiento: `docs/registro-maestro-de-seguimiento.md`.
17
17
  - Plan activo: `docs/seguimiento-completo-validacion-ruralgo-03-03-2026.md`.
18
+ - Suite contractual enterprise (MVP): `npm run -s validation:contract-suite:enterprise`.
18
19
 
19
20
  ## Política de higiene
20
21
 
@@ -0,0 +1,372 @@
1
+ import { appendFileSync, mkdirSync } from 'node:fs';
2
+ import { dirname, isAbsolute, join } from 'node:path';
3
+ import type { Finding } from '../../core/gate/Finding';
4
+ import type { GateOutcome } from '../../core/gate/GateOutcome';
5
+ import type { GateStage } from '../../core/gate/GateStage';
6
+ import type { Severity } from '../../core/rules/Severity';
7
+ import type { RepoState } from '../evidence/schema';
8
+ import type { ResolvedStagePolicy } from '../gate/stagePolicies';
9
+ import type { SddDecision } from '../sdd';
10
+
11
+ const TELEMETRY_EVENT_SCHEMA = 'telemetry_event_v1';
12
+ const TELEMETRY_EVENT_SCHEMA_VERSION = '1.0';
13
+ const DEFAULT_OTEL_SERVICE_NAME = 'pumuki';
14
+ const DEFAULT_OTEL_TIMEOUT_MS = 1500;
15
+
16
+ type PolicyTrace = ResolvedStagePolicy['trace'] & {
17
+ version?: string;
18
+ signature?: string;
19
+ policySource?: string;
20
+ validation?: {
21
+ status: 'valid' | 'invalid' | 'expired' | 'unknown-source';
22
+ code: string;
23
+ };
24
+ };
25
+
26
+ const toSeverityCounts = (
27
+ findings: ReadonlyArray<Finding>
28
+ ): Record<Severity, number> => {
29
+ const counts: Record<Severity, number> = {
30
+ CRITICAL: 0,
31
+ ERROR: 0,
32
+ WARN: 0,
33
+ INFO: 0,
34
+ };
35
+ for (const finding of findings) {
36
+ counts[finding.severity] += 1;
37
+ }
38
+ return counts;
39
+ };
40
+
41
+ const toBlockingFindingsCount = (findings: ReadonlyArray<Finding>): number => {
42
+ let count = 0;
43
+ for (const finding of findings) {
44
+ if (finding.severity === 'CRITICAL' || finding.severity === 'ERROR') {
45
+ count += 1;
46
+ }
47
+ }
48
+ return count;
49
+ };
50
+
51
+ const toGateSeverityText = (outcome: GateOutcome): string => {
52
+ if (outcome === 'BLOCK') {
53
+ return 'ERROR';
54
+ }
55
+ if (outcome === 'WARN') {
56
+ return 'WARN';
57
+ }
58
+ return 'INFO';
59
+ };
60
+
61
+ const toGateSeverityNumber = (outcome: GateOutcome): number => {
62
+ if (outcome === 'BLOCK') {
63
+ return 17;
64
+ }
65
+ if (outcome === 'WARN') {
66
+ return 13;
67
+ }
68
+ return 9;
69
+ };
70
+
71
+ const toOtelTimeoutMs = (value: string | undefined): number => {
72
+ const parsed = Number.parseInt(value ?? '', 10);
73
+ if (!Number.isFinite(parsed) || parsed <= 0) {
74
+ return DEFAULT_OTEL_TIMEOUT_MS;
75
+ }
76
+ return parsed;
77
+ };
78
+
79
+ const resolveTargetPath = (repoRoot: string, targetPath: string): string => {
80
+ if (isAbsolute(targetPath)) {
81
+ return targetPath;
82
+ }
83
+ return join(repoRoot, targetPath);
84
+ };
85
+
86
+ const defaultAppendJsonlLine = (targetPath: string, line: string): void => {
87
+ mkdirSync(dirname(targetPath), { recursive: true });
88
+ appendFileSync(targetPath, line, 'utf8');
89
+ };
90
+
91
+ const defaultPostOtelPayload = async (params: {
92
+ endpoint: string;
93
+ payload: unknown;
94
+ timeoutMs: number;
95
+ }): Promise<void> => {
96
+ if (typeof fetch !== 'function') {
97
+ return;
98
+ }
99
+ const controller = new AbortController();
100
+ const timeoutId = setTimeout(() => {
101
+ controller.abort();
102
+ }, params.timeoutMs);
103
+ try {
104
+ await fetch(params.endpoint, {
105
+ method: 'POST',
106
+ headers: {
107
+ 'content-type': 'application/json',
108
+ },
109
+ body: JSON.stringify(params.payload),
110
+ signal: controller.signal,
111
+ });
112
+ } finally {
113
+ clearTimeout(timeoutId);
114
+ }
115
+ };
116
+
117
+ export type GateTelemetryEventV1 = {
118
+ schema: typeof TELEMETRY_EVENT_SCHEMA;
119
+ schema_version: typeof TELEMETRY_EVENT_SCHEMA_VERSION;
120
+ emitted_at: string;
121
+ stage: GateStage;
122
+ audit_mode: 'gate' | 'engine';
123
+ gate_outcome: GateOutcome;
124
+ files_scanned: number;
125
+ findings_total: number;
126
+ blocking_findings: number;
127
+ severity_counts: Record<Severity, number>;
128
+ repo: {
129
+ root: string;
130
+ branch: string | null;
131
+ upstream: string | null;
132
+ dirty: boolean;
133
+ staged: number;
134
+ unstaged: number;
135
+ ahead: number;
136
+ behind: number;
137
+ };
138
+ policy?: {
139
+ source: PolicyTrace['source'];
140
+ bundle: string;
141
+ hash: string;
142
+ version?: string;
143
+ signature?: string;
144
+ policy_source?: string;
145
+ validation_status?: 'valid' | 'invalid' | 'expired' | 'unknown-source';
146
+ validation_code?: string;
147
+ };
148
+ sdd?: {
149
+ allowed: boolean;
150
+ code: string;
151
+ message: string;
152
+ };
153
+ };
154
+
155
+ export type EmitGateTelemetryResult = {
156
+ skipped: boolean;
157
+ jsonl_path?: string;
158
+ otel_dispatched: boolean;
159
+ event: GateTelemetryEventV1;
160
+ };
161
+
162
+ export type EmitGateTelemetryDependencies = {
163
+ env: NodeJS.ProcessEnv;
164
+ now: () => Date;
165
+ appendJsonlLine: (targetPath: string, line: string) => void;
166
+ postOtelPayload: (params: {
167
+ endpoint: string;
168
+ payload: unknown;
169
+ timeoutMs: number;
170
+ }) => Promise<void>;
171
+ };
172
+
173
+ const defaultDependencies: EmitGateTelemetryDependencies = {
174
+ env: process.env,
175
+ now: () => new Date(),
176
+ appendJsonlLine: defaultAppendJsonlLine,
177
+ postOtelPayload: defaultPostOtelPayload,
178
+ };
179
+
180
+ const toTelemetryEvent = (params: {
181
+ stage: GateStage;
182
+ auditMode: 'gate' | 'engine';
183
+ gateOutcome: GateOutcome;
184
+ filesScanned: number;
185
+ findings: ReadonlyArray<Finding>;
186
+ repoRoot: string;
187
+ repoState: RepoState;
188
+ policyTrace?: PolicyTrace;
189
+ sddDecision?: Pick<SddDecision, 'allowed' | 'code' | 'message'>;
190
+ emittedAt: Date;
191
+ }): GateTelemetryEventV1 => {
192
+ return {
193
+ schema: TELEMETRY_EVENT_SCHEMA,
194
+ schema_version: TELEMETRY_EVENT_SCHEMA_VERSION,
195
+ emitted_at: params.emittedAt.toISOString(),
196
+ stage: params.stage,
197
+ audit_mode: params.auditMode,
198
+ gate_outcome: params.gateOutcome,
199
+ files_scanned: params.filesScanned,
200
+ findings_total: params.findings.length,
201
+ blocking_findings: toBlockingFindingsCount(params.findings),
202
+ severity_counts: toSeverityCounts(params.findings),
203
+ repo: {
204
+ root: params.repoRoot,
205
+ branch: params.repoState.git.branch,
206
+ upstream: params.repoState.git.upstream,
207
+ dirty: params.repoState.git.dirty,
208
+ staged: params.repoState.git.staged,
209
+ unstaged: params.repoState.git.unstaged,
210
+ ahead: params.repoState.git.ahead,
211
+ behind: params.repoState.git.behind,
212
+ },
213
+ ...(params.policyTrace
214
+ ? {
215
+ policy: {
216
+ source: params.policyTrace.source,
217
+ bundle: params.policyTrace.bundle,
218
+ hash: params.policyTrace.hash,
219
+ ...(params.policyTrace.version ? { version: params.policyTrace.version } : {}),
220
+ ...(params.policyTrace.signature ? { signature: params.policyTrace.signature } : {}),
221
+ ...(params.policyTrace.policySource
222
+ ? { policy_source: params.policyTrace.policySource }
223
+ : {}),
224
+ ...(params.policyTrace.validation
225
+ ? {
226
+ validation_status: params.policyTrace.validation.status,
227
+ validation_code: params.policyTrace.validation.code,
228
+ }
229
+ : {}),
230
+ },
231
+ }
232
+ : {}),
233
+ ...(params.sddDecision
234
+ ? {
235
+ sdd: {
236
+ allowed: params.sddDecision.allowed,
237
+ code: params.sddDecision.code,
238
+ message: params.sddDecision.message,
239
+ },
240
+ }
241
+ : {}),
242
+ };
243
+ };
244
+
245
+ const toOtelPayload = (params: {
246
+ event: GateTelemetryEventV1;
247
+ serviceName: string;
248
+ }): unknown => {
249
+ const eventBody = JSON.stringify(params.event);
250
+ return {
251
+ resourceLogs: [
252
+ {
253
+ resource: {
254
+ attributes: [
255
+ {
256
+ key: 'service.name',
257
+ value: { stringValue: params.serviceName },
258
+ },
259
+ {
260
+ key: 'pumuki.telemetry.schema',
261
+ value: { stringValue: params.event.schema },
262
+ },
263
+ ],
264
+ },
265
+ scopeLogs: [
266
+ {
267
+ scope: {
268
+ name: 'pumuki.telemetry',
269
+ version: params.event.schema_version,
270
+ },
271
+ logRecords: [
272
+ {
273
+ timeUnixNano: `${Date.parse(params.event.emitted_at) * 1000000}`,
274
+ severityNumber: toGateSeverityNumber(params.event.gate_outcome),
275
+ severityText: toGateSeverityText(params.event.gate_outcome),
276
+ body: { stringValue: eventBody },
277
+ attributes: [
278
+ {
279
+ key: 'pumuki.stage',
280
+ value: { stringValue: params.event.stage },
281
+ },
282
+ {
283
+ key: 'pumuki.gate_outcome',
284
+ value: { stringValue: params.event.gate_outcome },
285
+ },
286
+ {
287
+ key: 'pumuki.policy_hash',
288
+ value: {
289
+ stringValue: params.event.policy?.hash ?? '',
290
+ },
291
+ },
292
+ ],
293
+ },
294
+ ],
295
+ },
296
+ ],
297
+ },
298
+ ],
299
+ };
300
+ };
301
+
302
+ export const emitGateTelemetryEvent = async (
303
+ params: {
304
+ stage: GateStage;
305
+ auditMode: 'gate' | 'engine';
306
+ gateOutcome: GateOutcome;
307
+ filesScanned: number;
308
+ findings: ReadonlyArray<Finding>;
309
+ repoRoot: string;
310
+ repoState: RepoState;
311
+ policyTrace?: PolicyTrace;
312
+ sddDecision?: Pick<SddDecision, 'allowed' | 'code' | 'message'>;
313
+ },
314
+ dependencies: Partial<EmitGateTelemetryDependencies> = {}
315
+ ): Promise<EmitGateTelemetryResult> => {
316
+ const activeDependencies: EmitGateTelemetryDependencies = {
317
+ ...defaultDependencies,
318
+ ...dependencies,
319
+ };
320
+ const jsonlPathRaw = activeDependencies.env.PUMUKI_TELEMETRY_JSONL_PATH?.trim() ?? '';
321
+ const otelEndpoint = activeDependencies.env.PUMUKI_TELEMETRY_OTEL_ENDPOINT?.trim() ?? '';
322
+ const otelServiceName =
323
+ activeDependencies.env.PUMUKI_TELEMETRY_OTEL_SERVICE_NAME?.trim() ||
324
+ DEFAULT_OTEL_SERVICE_NAME;
325
+ const otelTimeoutMs = toOtelTimeoutMs(
326
+ activeDependencies.env.PUMUKI_TELEMETRY_OTEL_TIMEOUT_MS
327
+ );
328
+
329
+ const event = toTelemetryEvent({
330
+ ...params,
331
+ emittedAt: activeDependencies.now(),
332
+ });
333
+
334
+ if (jsonlPathRaw.length === 0 && otelEndpoint.length === 0) {
335
+ return {
336
+ skipped: true,
337
+ otel_dispatched: false,
338
+ event,
339
+ };
340
+ }
341
+
342
+ let jsonlPath: string | undefined;
343
+ if (jsonlPathRaw.length > 0) {
344
+ jsonlPath = resolveTargetPath(params.repoRoot, jsonlPathRaw);
345
+ activeDependencies.appendJsonlLine(jsonlPath, `${JSON.stringify(event)}\n`);
346
+ }
347
+
348
+ let otelDispatched = false;
349
+ if (otelEndpoint.length > 0) {
350
+ const payload = toOtelPayload({
351
+ event,
352
+ serviceName: otelServiceName,
353
+ });
354
+ try {
355
+ await activeDependencies.postOtelPayload({
356
+ endpoint: otelEndpoint,
357
+ payload,
358
+ timeoutMs: otelTimeoutMs,
359
+ });
360
+ otelDispatched = true;
361
+ } catch {
362
+ otelDispatched = false;
363
+ }
364
+ }
365
+
366
+ return {
367
+ skipped: false,
368
+ ...(jsonlPath ? { jsonl_path: jsonlPath } : {}),
369
+ otel_dispatched: otelDispatched,
370
+ event,
371
+ };
372
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki",
3
- "version": "6.3.29",
3
+ "version": "6.3.31",
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": {
@@ -108,6 +108,7 @@
108
108
  "validation:package-smoke": "node --import tsx scripts/package-install-smoke.ts --mode=block",
109
109
  "validation:package-smoke:minimal": "node --import tsx scripts/package-install-smoke.ts --mode=minimal",
110
110
  "validation:lifecycle-smoke": "node --import tsx scripts/package-install-smoke.ts --mode=minimal",
111
+ "validation:contract-suite:enterprise": "node --import tsx scripts/run-enterprise-contract-suite.ts",
111
112
  "validation:c020-benchmark": "node --import tsx scripts/run-c020-benchmark.ts",
112
113
  "validation:clean-artifacts": "npx --yes tsx@4.21.0 scripts/clean-validation-artifacts.ts",
113
114
  "skills:compile": "npx --yes tsx@4.21.0 scripts/compile-skills-lock.ts",
@@ -202,6 +203,7 @@
202
203
  "integrations/mcp/*.ts",
203
204
  "integrations/notifications/*.ts",
204
205
  "integrations/platform/*.ts",
206
+ "integrations/telemetry/*.ts",
205
207
  "scripts/*.ts",
206
208
  "scripts/adapters/*.ts",
207
209
  "scripts/adapters/*.md",
@@ -0,0 +1,60 @@
1
+ import { resolve } from 'node:path';
2
+ import type { EnterpriseContractSuiteOptions } from './enterprise-contract-suite-contract';
3
+
4
+ export const DEFAULT_ENTERPRISE_CONTRACT_REPORT_PATH =
5
+ '.audit-reports/enterprise-contract-suite/report.json';
6
+
7
+ export const DEFAULT_ENTERPRISE_CONTRACT_SUMMARY_PATH =
8
+ '.audit-reports/enterprise-contract-suite/summary.md';
9
+
10
+ export const parseEnterpriseContractSuiteArgs = (
11
+ argv: ReadonlyArray<string>,
12
+ cwd = process.cwd()
13
+ ): EnterpriseContractSuiteOptions => {
14
+ const options: EnterpriseContractSuiteOptions = {
15
+ repoRoot: resolve(cwd),
16
+ reportPath: DEFAULT_ENTERPRISE_CONTRACT_REPORT_PATH,
17
+ summaryPath: DEFAULT_ENTERPRISE_CONTRACT_SUMMARY_PATH,
18
+ printJson: false,
19
+ };
20
+
21
+ for (const arg of argv) {
22
+ if (arg === '--json') {
23
+ options.printJson = true;
24
+ continue;
25
+ }
26
+
27
+ if (arg.startsWith('--repo=')) {
28
+ const value = arg.slice('--repo='.length).trim();
29
+ if (!value) {
30
+ throw new Error('Flag --repo requires a non-empty path.');
31
+ }
32
+ options.repoRoot = resolve(value);
33
+ continue;
34
+ }
35
+
36
+ if (arg.startsWith('--out=')) {
37
+ const value = arg.slice('--out='.length).trim();
38
+ if (!value) {
39
+ throw new Error('Flag --out requires a non-empty path.');
40
+ }
41
+ options.reportPath = value;
42
+ continue;
43
+ }
44
+
45
+ if (arg.startsWith('--summary=')) {
46
+ const value = arg.slice('--summary='.length).trim();
47
+ if (!value) {
48
+ throw new Error('Flag --summary requires a non-empty path.');
49
+ }
50
+ options.summaryPath = value;
51
+ continue;
52
+ }
53
+
54
+ throw new Error(
55
+ `Unsupported argument "${arg}". Allowed: --repo=<path> --out=<path> --summary=<path> --json`
56
+ );
57
+ }
58
+
59
+ return options;
60
+ };
@@ -0,0 +1,31 @@
1
+ export type EnterpriseContractProfileId = 'minimal' | 'block';
2
+
3
+ export type EnterpriseContractProfileSpec = {
4
+ id: EnterpriseContractProfileId;
5
+ mode: 'minimal' | 'block';
6
+ expectedExitCode: number;
7
+ };
8
+
9
+ export type EnterpriseContractProfileResult = {
10
+ id: EnterpriseContractProfileId;
11
+ mode: 'minimal' | 'block';
12
+ command: string;
13
+ expectedExitCode: number;
14
+ exitCode: number;
15
+ status: 'PASS' | 'FAIL';
16
+ };
17
+
18
+ export type EnterpriseContractSuiteReport = {
19
+ suiteVersion: '1';
20
+ generatedAt: string;
21
+ repoRoot: string;
22
+ profiles: ReadonlyArray<EnterpriseContractProfileResult>;
23
+ overall: 'PASS' | 'FAIL';
24
+ };
25
+
26
+ export type EnterpriseContractSuiteOptions = {
27
+ repoRoot: string;
28
+ reportPath: string;
29
+ summaryPath: string;
30
+ printJson: boolean;
31
+ };
@@ -0,0 +1,45 @@
1
+ import type {
2
+ EnterpriseContractProfileResult,
3
+ EnterpriseContractSuiteReport,
4
+ } from './enterprise-contract-suite-contract';
5
+
6
+ export const resolveEnterpriseContractOverall = (
7
+ profiles: ReadonlyArray<EnterpriseContractProfileResult>
8
+ ): 'PASS' | 'FAIL' =>
9
+ profiles.every((profile) => profile.status === 'PASS') ? 'PASS' : 'FAIL';
10
+
11
+ export const buildEnterpriseContractReport = (params: {
12
+ repoRoot: string;
13
+ generatedAt?: string;
14
+ profiles: ReadonlyArray<EnterpriseContractProfileResult>;
15
+ }): EnterpriseContractSuiteReport => ({
16
+ suiteVersion: '1',
17
+ generatedAt: params.generatedAt ?? new Date().toISOString(),
18
+ repoRoot: params.repoRoot,
19
+ profiles: params.profiles,
20
+ overall: resolveEnterpriseContractOverall(params.profiles),
21
+ });
22
+
23
+ export const renderEnterpriseContractSummary = (
24
+ report: EnterpriseContractSuiteReport
25
+ ): string => {
26
+ const lines: string[] = [
27
+ '# Enterprise Contract Suite Summary',
28
+ '',
29
+ `- Overall: ${report.overall}`,
30
+ `- Generated At: ${report.generatedAt}`,
31
+ `- Repo Root: ${report.repoRoot}`,
32
+ '',
33
+ '## Profiles',
34
+ '',
35
+ ];
36
+
37
+ for (const profile of report.profiles) {
38
+ lines.push(
39
+ `- ${profile.id}: status=${profile.status} expected_exit=${profile.expectedExitCode} actual_exit=${profile.exitCode}`
40
+ );
41
+ }
42
+
43
+ lines.push('');
44
+ return lines.join('\n');
45
+ };
@@ -10,6 +10,7 @@ export const REQUIRED_PACKAGE_PATHS = [
10
10
  'core/rules/presets/heuristics/typescript.ts',
11
11
  'scripts/package-install-smoke.ts',
12
12
  'integrations/git/runPlatformGate.ts',
13
+ 'integrations/telemetry/gateTelemetry.ts',
13
14
  'integrations/lifecycle/cli.ts',
14
15
  'integrations/notifications/emitAuditSummaryNotification.ts',
15
16
  'integrations/evidence/buildEvidence.ts',
@@ -0,0 +1,78 @@
1
+ import { dirname, join } from 'node:path';
2
+ import { writeFileSync } from 'node:fs';
3
+ import {
4
+ parseEnterpriseContractSuiteArgs,
5
+ } from './enterprise-contract-suite-args-lib';
6
+ import {
7
+ buildEnterpriseContractReport,
8
+ renderEnterpriseContractSummary,
9
+ } from './enterprise-contract-suite-report-lib';
10
+ import type {
11
+ EnterpriseContractProfileResult,
12
+ EnterpriseContractProfileSpec,
13
+ } from './enterprise-contract-suite-contract';
14
+ import { ensureDirectory } from './package-install-smoke-file-lib';
15
+ import { runCommand } from './package-install-smoke-command-lib';
16
+
17
+ const PROFILE_SPECS: ReadonlyArray<EnterpriseContractProfileSpec> = [
18
+ {
19
+ id: 'minimal',
20
+ mode: 'minimal',
21
+ // The minimal fixture intentionally omits platform skill contracts to assert strict governance blocking.
22
+ expectedExitCode: 1,
23
+ },
24
+ {
25
+ id: 'block',
26
+ mode: 'block',
27
+ // The block fixture is expected to complete successfully as a deterministic BLOCK smoke scenario.
28
+ expectedExitCode: 0,
29
+ },
30
+ ];
31
+
32
+ const runProfile = (
33
+ repoRoot: string,
34
+ profile: EnterpriseContractProfileSpec
35
+ ): EnterpriseContractProfileResult => {
36
+ const args = ['--import', 'tsx', 'scripts/package-install-smoke.ts', `--mode=${profile.mode}`];
37
+ const result = runCommand({
38
+ cwd: repoRoot,
39
+ executable: 'node',
40
+ args,
41
+ });
42
+
43
+ return {
44
+ id: profile.id,
45
+ mode: profile.mode,
46
+ command: `node ${args.join(' ')}`,
47
+ expectedExitCode: profile.expectedExitCode,
48
+ exitCode: result.exitCode,
49
+ status: result.exitCode === profile.expectedExitCode ? 'PASS' : 'FAIL',
50
+ };
51
+ };
52
+
53
+ const writeTextFile = (repoRoot: string, relativePath: string, content: string): void => {
54
+ const filePath = join(repoRoot, relativePath);
55
+ ensureDirectory(dirname(filePath));
56
+ writeFileSync(filePath, content, 'utf8');
57
+ };
58
+
59
+ const main = (): number => {
60
+ const options = parseEnterpriseContractSuiteArgs(process.argv.slice(2));
61
+ const results = PROFILE_SPECS.map((profile) => runProfile(options.repoRoot, profile));
62
+
63
+ const report = buildEnterpriseContractReport({
64
+ repoRoot: options.repoRoot,
65
+ profiles: results,
66
+ });
67
+
68
+ writeTextFile(options.repoRoot, options.reportPath, `${JSON.stringify(report, null, 2)}\n`);
69
+ writeTextFile(options.repoRoot, options.summaryPath, renderEnterpriseContractSummary(report));
70
+
71
+ if (options.printJson) {
72
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
73
+ }
74
+
75
+ return report.overall === 'PASS' ? 0 : 1;
76
+ };
77
+
78
+ process.exit(main());