pumuki 6.3.26 → 6.3.28

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 (76) hide show
  1. package/README.md +3 -1
  2. package/bin/pumuki-mcp-enterprise-stdio.js +5 -0
  3. package/bin/pumuki-mcp-evidence-stdio.js +5 -0
  4. package/core/gate/conditionMatches.ts +1 -21
  5. package/core/gate/evaluateGate.js +5 -0
  6. package/core/gate/evaluateRules.js +5 -0
  7. package/core/gate/evaluateRules.ts +1 -24
  8. package/core/gate/scopeMatcher.ts +84 -0
  9. package/docs/EXECUTION_BOARD.md +749 -376
  10. package/docs/MCP_SERVERS.md +41 -2
  11. package/docs/README.md +6 -2
  12. package/docs/REFRACTOR_PROGRESS.md +374 -6
  13. package/docs/validation/README.md +11 -1
  14. package/docs/validation/p9-ruralgo-bug-registry.md +607 -0
  15. package/docs/validation/p9-ruralgo-fork-validation-tracking.md +904 -0
  16. package/docs/validation/real-repo-manual-e2e-ruralgo-fork.md +372 -0
  17. package/integrations/config/skillsCompliance.ts +212 -0
  18. package/integrations/evidence/integrity.ts +352 -0
  19. package/integrations/evidence/rulesCoverage.ts +94 -0
  20. package/integrations/evidence/schema.test.ts +16 -0
  21. package/integrations/evidence/schema.ts +41 -0
  22. package/integrations/evidence/writeEvidence.test.ts +68 -0
  23. package/integrations/evidence/writeEvidence.ts +23 -2
  24. package/integrations/gate/evaluateAiGate.ts +382 -15
  25. package/integrations/gate/stagePolicies.ts +70 -15
  26. package/integrations/gate/waivers.ts +209 -0
  27. package/integrations/git/findingTraceability.ts +3 -23
  28. package/integrations/git/index.js +5 -0
  29. package/integrations/git/runCliCommand.ts +16 -0
  30. package/integrations/git/runPlatformGate.ts +53 -1
  31. package/integrations/git/runPlatformGateEvaluation.ts +13 -0
  32. package/integrations/git/stageRunners.ts +168 -5
  33. package/integrations/lifecycle/adapter.templates.json +72 -5
  34. package/integrations/lifecycle/adapter.ts +78 -4
  35. package/integrations/lifecycle/cli.ts +384 -14
  36. package/integrations/lifecycle/doctor.ts +534 -0
  37. package/integrations/lifecycle/hookBlock.ts +2 -1
  38. package/integrations/lifecycle/index.js +5 -0
  39. package/integrations/lifecycle/install.ts +115 -3
  40. package/integrations/lifecycle/openSpecBootstrap.ts +68 -8
  41. package/integrations/lifecycle/preWriteAutomation.ts +142 -0
  42. package/integrations/mcp/aiGateCheck.ts +6 -0
  43. package/integrations/mcp/aiGateReceipt.ts +188 -0
  44. package/integrations/mcp/enterpriseServer.ts +14 -1
  45. package/integrations/mcp/enterpriseStdioServer.cli.ts +315 -0
  46. package/integrations/mcp/evidenceStdioServer.cli.ts +342 -0
  47. package/integrations/mcp/index.js +5 -0
  48. package/integrations/sdd/index.js +5 -0
  49. package/integrations/sdd/index.ts +2 -0
  50. package/integrations/sdd/policy.ts +191 -2
  51. package/integrations/sdd/sessionStore.ts +139 -19
  52. package/integrations/sdd/syncDocs.ts +180 -0
  53. package/integrations/sdd/types.ts +4 -1
  54. package/integrations/telemetry/structuredTelemetry.ts +197 -0
  55. package/package.json +27 -8
  56. package/scripts/build-p9-validation-manifests.ts +53 -0
  57. package/scripts/check-p9-ruralgo-baseline-clean.ts +200 -0
  58. package/scripts/check-p9-ruralgo-baseline-versioned.ts +198 -0
  59. package/scripts/check-p9-ruralgo-branch-ready.ts +215 -0
  60. package/scripts/check-p9-ruralgo-install-health.ts +288 -0
  61. package/scripts/check-p9-ruralgo-runtime-ready.ts +188 -0
  62. package/scripts/check-package-manifest.ts +49 -0
  63. package/scripts/check-tracking-single-active.sh +40 -0
  64. package/scripts/framework-menu-consumer-preflight-lib.ts +31 -0
  65. package/scripts/framework-menu-consumer-runtime-lib.ts +3 -3
  66. package/scripts/framework-menu-legacy-audit-lib.ts +35 -7
  67. package/scripts/framework-menu-matrix-evidence-lib.ts +6 -2
  68. package/scripts/manage-library.sh +1 -1
  69. package/scripts/p9-ruralgo-baseline-clean-lib.ts +117 -0
  70. package/scripts/p9-ruralgo-baseline-versioned-lib.ts +119 -0
  71. package/scripts/p9-ruralgo-branch-ready-lib.ts +128 -0
  72. package/scripts/p9-ruralgo-install-health-lib.ts +121 -0
  73. package/scripts/p9-ruralgo-runtime-ready-lib.ts +149 -0
  74. package/scripts/p9-validation-manifests-lib.ts +366 -0
  75. package/scripts/package-manifest-lib.ts +9 -0
  76. package/skills.lock.json +1 -1
@@ -0,0 +1,372 @@
1
+ # Runbook Manual E2E en Repo Real (ruralgo-fork)
2
+
3
+ Runbook manual para validar Pumuki end-to-end en un repositorio real (no mock), usando un fork/copia controlada de RuralGO dentro de `~/Developer/Projects`.
4
+
5
+ ## Objetivo
6
+
7
+ Verificar instalación, gates, lifecycle, comandos loop, servidores MCP y desinstalación limpia en un clon real que ejecutas tú manualmente.
8
+
9
+ ## Rutas
10
+
11
+ - Repo real de origen (RuralGO): `/Users/juancarlosmerlosalbarracin/Developer/Projects/R_GO`
12
+ - Fork/copia de validación: `/Users/juancarlosmerlosalbarracin/Developer/Projects/ruralgo-fork`
13
+
14
+ ## Espejo mínimo de tracking en el fork (manual)
15
+
16
+ Antes de empezar las pruebas reales, crea/actualiza este archivo en el fork:
17
+
18
+ - `ruralgo-fork/docs/strategy/ruralgo-tracking-hub.md`
19
+
20
+ Plantilla mínima recomendada:
21
+
22
+ ```md
23
+ # RuralGO Tracking Hub (P9)
24
+
25
+ - Fuente maestra: `ast-intelligence-hooks/docs/validation/p9-ruralgo-fork-validation-tracking.md`
26
+ - Tarea activa: `P9.F0.T3`
27
+ - Última sincronización: <YYYY-MM-DD HH:mm TZ>
28
+ - Estado:
29
+ - F0: en curso
30
+ - F1..F6: pendientes
31
+ ```
32
+
33
+ ## 0) Preparar fork/copia real en Projects
34
+
35
+ ```bash
36
+ rm -rf /Users/juancarlosmerlosalbarracin/Developer/Projects/ruralgo-fork
37
+ git clone /Users/juancarlosmerlosalbarracin/Developer/Projects/R_GO /Users/juancarlosmerlosalbarracin/Developer/Projects/ruralgo-fork
38
+ cd /Users/juancarlosmerlosalbarracin/Developer/Projects/ruralgo-fork
39
+ git switch develop
40
+ git pull --ff-only origin develop
41
+ ```
42
+
43
+ ## 0.1) P9.F0.T3.ST2 — Preparar rama real #1 (API contacto)
44
+
45
+ ```bash
46
+ cd /Users/juancarlosmerlosalbarracin/Developer/Projects/ruralgo-fork
47
+ git switch develop
48
+ git pull --ff-only origin develop
49
+ git switch -c feature/p9-api-contact-contract
50
+ git status --short --branch
51
+ git rev-parse --abbrev-ref HEAD
52
+ ```
53
+
54
+ Esperado:
55
+ - rama activa: `feature/p9-api-contact-contract`
56
+ - base: `develop`
57
+ - upstream todavía no configurado (se configura al abrir PR #1)
58
+
59
+ Validación automática de precondiciones (desde este repo Pumuki):
60
+
61
+ ```bash
62
+ cd /Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks
63
+ npm run -s validation:p9:ruralgo-branch-ready -- \
64
+ --repo=/Users/juancarlosmerlosalbarracin/Developer/Projects/ruralgo-fork \
65
+ --expected-branch=feature/p9-api-contact-contract \
66
+ --base-ref=origin/develop \
67
+ --json
68
+ ```
69
+
70
+ Esperado:
71
+ - `"ready": true`
72
+ - `"issues": []`
73
+
74
+ ## 0.2) P9.F0.T3.ST3 — Preparación pendiente rama real #2 (UX contacto)
75
+
76
+ No crearla todavía para evitar mezclar trabajo de PRs. Se preparará al terminar PR #1:
77
+
78
+ ```bash
79
+ cd /Users/juancarlosmerlosalbarracin/Developer/Projects/ruralgo-fork
80
+ git switch develop
81
+ git pull --ff-only origin develop
82
+ git switch -c feature/p9-ux-contact-states
83
+ ```
84
+
85
+ Esperado:
86
+ - rama activa: `feature/p9-ux-contact-states`
87
+ - base: `develop`
88
+ - solo después de cerrar/encaminar PR #1
89
+
90
+ Validación automática de precondiciones (cuando toque abrir PR #2):
91
+
92
+ ```bash
93
+ cd /Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks
94
+ npm run -s validation:p9:ruralgo-branch-ready:ux -- \
95
+ --repo=/Users/juancarlosmerlosalbarracin/Developer/Projects/ruralgo-fork \
96
+ --json
97
+ ```
98
+
99
+ Esperado:
100
+ - `"ready": true`
101
+ - `"issues": []`
102
+
103
+ ## 0.3) P9.F1.T1 — Verificar runtime exigido por RuralGO
104
+
105
+ ```bash
106
+ cd /Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks
107
+ npm run -s validation:p9:ruralgo-runtime-ready -- \
108
+ --repo=/Users/juancarlosmerlosalbarracin/Developer/Projects/ruralgo-fork \
109
+ --json
110
+ ```
111
+
112
+ Si tu shell no respeta `nvm use` (sigue mostrando `node v25`), ejecuta los comandos con:
113
+
114
+ ```bash
115
+ PATH="$HOME/.nvm/versions/node/v20.20.0/bin:$PATH" <comando>
116
+ ```
117
+
118
+ Esperado:
119
+ - `"required.node": "20.20.0"`
120
+ - `"required.npm": "10.8.2"`
121
+ - `"ready": true`
122
+ - `"issues": []`
123
+
124
+ ## 1) Línea base limpia (sin restos de Pumuki)
125
+
126
+ ```bash
127
+ npx --yes --package pumuki@latest pumuki uninstall --purge-artifacts || true
128
+ npm uninstall pumuki --save-exact || true
129
+ rm -rf node_modules package-lock.json .ai_evidence.json artifacts
130
+ git status --short --branch
131
+ ```
132
+
133
+ Esperado:
134
+ - sin hooks gestionados por Pumuki
135
+ - sin `.ai_evidence.json`
136
+ - sin `artifacts/`
137
+
138
+ Validación automática de baseline limpio:
139
+
140
+ ```bash
141
+ cd /Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks
142
+ npm run -s validation:p9:ruralgo-baseline-clean -- \
143
+ --repo=/Users/juancarlosmerlosalbarracin/Developer/Projects/ruralgo-fork \
144
+ --json
145
+ ```
146
+
147
+ Esperado:
148
+ - `"ready": true`
149
+ - `"issues": []`
150
+
151
+ ## 2) Instalar latest + checks de estado
152
+
153
+ ```bash
154
+ npx --yes --package pumuki@latest pumuki install
155
+ npx --yes --package pumuki@latest pumuki status --json
156
+ npx --yes --package pumuki@latest pumuki doctor --json
157
+ ls -la .git/hooks | rg 'pre-commit|pre-push' || true
158
+ ```
159
+
160
+ Esperado:
161
+ - instalación operativa sin depender de `npm install` local
162
+ - hooks `pre-commit` y `pre-push` presentes
163
+ - `status` y `doctor` devuelven JSON válido
164
+
165
+ Validación automática de salud de instalación (status + hooks + doctor):
166
+
167
+ ```bash
168
+ cd /Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks
169
+ npm run -s validation:p9:ruralgo-install-health -- \
170
+ --repo=/Users/juancarlosmerlosalbarracin/Developer/Projects/ruralgo-fork \
171
+ --json
172
+ ```
173
+
174
+ Esperado:
175
+ - `"ready": true`
176
+ - `"snapshot.lifecycleInstalled": true`
177
+ - `"snapshot.preCommitManaged": true`
178
+ - `"snapshot.prePushManaged": true`
179
+ - `"snapshot.doctorVerdictPass": true`
180
+
181
+ ## 2.1) P9.F1.T4 — Verificar baseline versionado (openspec + ai_evidence)
182
+
183
+ ```bash
184
+ cd /Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks
185
+ npm run -s validation:p9:ruralgo-baseline-versioned -- \
186
+ --repo=/Users/juancarlosmerlosalbarracin/Developer/Projects/ruralgo-fork \
187
+ --json
188
+ ```
189
+
190
+ Esperado:
191
+ - `"ready": true`
192
+ - `"snapshot.openspecProjectTracked": true`
193
+ - `"snapshot.openspecArchiveGitkeepTracked": true`
194
+ - `"snapshot.openspecSpecsGitkeepTracked": true`
195
+ - `"snapshot.aiEvidenceTracked": true`
196
+
197
+ ## 2.2) P9.F2.T1 — RED del contrato API de contacto (sin implementar endpoint)
198
+
199
+ Arranca backend en modo test con entorno dummy para aislar errores de infraestructura:
200
+
201
+ ```bash
202
+ cd /Users/juancarlosmerlosalbarracin/Developer/Projects/ruralgo-fork
203
+ PATH="$HOME/.nvm/versions/node/v20.20.0/bin:$PATH" \
204
+ NODE_ENV=test \
205
+ E2E_MODE=true \
206
+ SUPABASE_URL=http://127.0.0.1 \
207
+ SUPABASE_SERVICE_ROLE_KEY=dummy \
208
+ WEBHOOK_URL=http://127.0.0.1 \
209
+ TEST_ADMIN_EMAIL=admin@local.test \
210
+ TEST_ADMIN_PASSWORD=Admin123! \
211
+ TEST_SEEDED_EMAIL=seeded@local.test \
212
+ TEST_SEEDED_PASSWORD=Seeded123! \
213
+ TEST_REGULAR_EMAIL=user@local.test \
214
+ TEST_REGULAR_PASSWORD=User1234! \
215
+ E2E_ORDERS_TEST_EMAIL=e2e@local.test \
216
+ E2E_ORDERS_TEST_PASSWORD=Orders123! \
217
+ npm run -w apps/backend dev
218
+ ```
219
+
220
+ En otra terminal, ejecutar probes RED:
221
+
222
+ ```bash
223
+ curl -s -i -X POST http://127.0.0.1:3000/api/contact-form \
224
+ -H 'Content-Type: application/json' \
225
+ -d '{"name":"Ana","email":"ana@example.com","message":"Hola"}'
226
+
227
+ curl -s -i -X POST http://127.0.0.1:3000/api/contact-form \
228
+ -H 'Content-Type: application/json' \
229
+ -d '{"name":"","email":"bad","message":""}'
230
+
231
+ curl -s -i -X POST http://127.0.0.1:3000/api/contact-form \
232
+ -H 'Content-Type: application/json' \
233
+ -H 'X-Force-External-Fail: 1' \
234
+ -d '{"name":"Ana","email":"ana@example.com","message":"Hola"}'
235
+ ```
236
+
237
+ Esperado en RED actual:
238
+ - respuestas `404 Cannot POST /api/contact-form` en los 3 casos (contrato aún no implementado).
239
+
240
+ Esperado tras completar GREEN (`P9.F2.T2`):
241
+ - caso válido => `200` con body `{"status":"sent"}`
242
+ - caso inválido => `400` con errores de validación
243
+ - caso `X-Force-External-Fail: 1` => `502` con mensaje saneado
244
+
245
+ Opcional (solo si el repo consumidor no bloquea por engines):
246
+
247
+ ```bash
248
+ npm install --save-exact pumuki@latest
249
+ ```
250
+
251
+ ## 3) Comportamiento del gate: bloquea y luego pasa
252
+
253
+ Crear una violación intencional:
254
+
255
+ ```bash
256
+ cat > pumuki_e2e_violation.ts <<'TS'
257
+ console.log('violacion intencional');
258
+ TS
259
+ git add pumuki_e2e_violation.ts
260
+ npx --yes --package pumuki@latest pumuki-pre-commit; echo "pre-commit exit=$?"
261
+ ```
262
+
263
+ Corregir y volver a comprobar:
264
+
265
+ ```bash
266
+ cat > pumuki_e2e_violation.ts <<'TS'
267
+ export const pumukiE2e = (): string => 'ok';
268
+ TS
269
+ git add pumuki_e2e_violation.ts
270
+ npx --yes --package pumuki@latest pumuki-pre-commit; echo "pre-commit-fixed exit=$?"
271
+ ```
272
+
273
+ Esperado:
274
+ - la primera ejecución bloquea
275
+ - la segunda ejecución pasa
276
+
277
+ ## 4) Pre-push y gate CI local
278
+
279
+ ```bash
280
+ git branch -f upstream-e2e HEAD
281
+ git branch --set-upstream-to=upstream-e2e feature/p9-api-contact-contract
282
+ npx --yes --package pumuki@latest pumuki-pre-push; echo "pre-push exit=$?"
283
+ GITHUB_BASE_REF=upstream-e2e npx --yes --package pumuki@latest pumuki-ci; echo "ci exit=$?"
284
+ ```
285
+
286
+ Esperado:
287
+ - pre-push y ci validan el scope actual (`staged/diff`) según política
288
+
289
+ ## 5) Smoke de comandos loop
290
+
291
+ ```bash
292
+ SID=$(npx --yes --package pumuki@latest pumuki loop run --objective="manual-e2e" --max-attempts=1 --json | jq -r '.session.id // .session_id')
293
+ echo "SID=$SID"
294
+ npx --yes --package pumuki@latest pumuki loop status --session="$SID" --json
295
+ npx --yes --package pumuki@latest pumuki loop list --json
296
+ npx --yes --package pumuki@latest pumuki loop export --session="$SID" --json
297
+ npx --yes --package pumuki@latest pumuki loop stop --session="$SID" --json
298
+ npx --yes --package pumuki@latest pumuki loop resume --session="$SID" --json || true
299
+ ```
300
+
301
+ Esperado:
302
+ - el ciclo de vida de la sesión loop responde con JSON válido
303
+
304
+ ## 6) Smoke de servidores MCP (manual y visible)
305
+
306
+ Terminal A:
307
+
308
+ ```bash
309
+ cd /Users/juancarlosmerlosalbarracin/Developer/Projects/ruralgo-fork
310
+ PUMUKI_EVIDENCE_PORT=7441 npx --yes --package pumuki@latest pumuki-mcp-evidence
311
+ ```
312
+
313
+ Terminal B:
314
+
315
+ ```bash
316
+ curl -s http://127.0.0.1:7441/health
317
+ ```
318
+
319
+ Terminal C:
320
+
321
+ ```bash
322
+ cd /Users/juancarlosmerlosalbarracin/Developer/Projects/ruralgo-fork
323
+ PUMUKI_ENTERPRISE_MCP_PORT=7491 npx --yes --package pumuki@latest pumuki-mcp-enterprise
324
+ ```
325
+
326
+ Terminal D:
327
+
328
+ ```bash
329
+ curl -s http://127.0.0.1:7491/health
330
+ ```
331
+
332
+ Esperado:
333
+ - ambos endpoints de health devuelven `status=ok`
334
+
335
+ ## 7) Desinstalación limpia (estado final)
336
+
337
+ ```bash
338
+ npx --yes --package pumuki@latest pumuki uninstall --purge-artifacts
339
+ npm uninstall pumuki --save-exact || true
340
+ rm -rf node_modules package-lock.json .ai_evidence.json artifacts
341
+ ls -la .git/hooks | rg 'pre-commit|pre-push' || true
342
+ git status --short --branch
343
+ ```
344
+
345
+ Esperado:
346
+ - sin hooks gestionados por Pumuki
347
+ - sin artefactos de Pumuki
348
+ - el repo queda controlado y limpio en `ruralgo-fork`
349
+
350
+ ## 8) Cierre espejo P9 (F6.T4.ST2)
351
+
352
+ Aplica el cierre final en el espejo del consumidor para desbloquear `P9.F6.T4`:
353
+
354
+ ```bash
355
+ cd /Users/juancarlosmerlosalbarracin/Developer/Projects/ruralgo-fork
356
+ mkdir -p docs/strategy
357
+ cat > docs/strategy/ruralgo-tracking-hub.md <<'MD'
358
+ # RuralGO Tracking Hub (P9)
359
+
360
+ - Fuente maestra: `/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/docs/validation/p9-ruralgo-fork-validation-tracking.md`
361
+ - Estado maestro: `P9.F6.T4.ST1=✅`, `P9.F6.T4.ST2=✅`
362
+ - Veredicto final P9: `GO`
363
+ - Cierre: `P9.T2 => ✅`
364
+ - Última sincronización: $(date '+%Y-%m-%d %H:%M:%S %Z')
365
+ MD
366
+
367
+ git status --short --branch docs/strategy/ruralgo-tracking-hub.md
368
+ ```
369
+
370
+ Esperado:
371
+ - `docs/strategy/ruralgo-tracking-hub.md` presente y actualizado.
372
+ - cierre explícito del espejo con `P9.T2 => ✅` y veredicto `GO`.
@@ -0,0 +1,212 @@
1
+ import type { RuleDefinition } from '../../core/rules/RuleDefinition';
2
+ import type { SkillsRuleSetLoadResult } from './skillsRuleSet';
3
+
4
+ export type SkillsCompliancePlatform = Extract<
5
+ NonNullable<RuleDefinition['platform']>,
6
+ 'ios' | 'android' | 'backend' | 'frontend'
7
+ >;
8
+
9
+ export type SkillsComplianceFileEntry = {
10
+ file_path: string;
11
+ platform: SkillsCompliancePlatform;
12
+ required_rule_ids: string[];
13
+ applied_rule_ids: string[];
14
+ evidence_rule_ids: string[];
15
+ missing_rule_ids: string[];
16
+ status: 'OK' | 'INCOMPLETE';
17
+ };
18
+
19
+ export type SkillsComplianceSnapshot = {
20
+ required_rule_ids: string[];
21
+ applied_rule_ids: string[];
22
+ evidence_rule_ids: string[];
23
+ missing_rule_ids: string[];
24
+ counts: {
25
+ files_in_scope: number;
26
+ files_with_missing: number;
27
+ required: number;
28
+ applied: number;
29
+ evidence: number;
30
+ missing: number;
31
+ };
32
+ by_file: SkillsComplianceFileEntry[];
33
+ };
34
+
35
+ const isTypeScriptOrJavaScript = (path: string): boolean => {
36
+ const normalized = path.toLowerCase();
37
+ return (
38
+ normalized.endsWith('.ts') ||
39
+ normalized.endsWith('.js') ||
40
+ normalized.endsWith('.mts') ||
41
+ normalized.endsWith('.cts') ||
42
+ normalized.endsWith('.mjs') ||
43
+ normalized.endsWith('.cjs')
44
+ );
45
+ };
46
+
47
+ const isReactPath = (path: string): boolean => {
48
+ const normalized = path.toLowerCase();
49
+ return normalized.endsWith('.tsx') || normalized.endsWith('.jsx');
50
+ };
51
+
52
+ const normalizePath = (path: string): string => {
53
+ return path.replace(/\\/g, '/').replace(/^\.\//, '').trim();
54
+ };
55
+
56
+ const inferPlatformFromPath = (
57
+ path: string
58
+ ): SkillsCompliancePlatform | undefined => {
59
+ const normalized = normalizePath(path).toLowerCase();
60
+
61
+ if (normalized.endsWith('.swift')) {
62
+ return 'ios';
63
+ }
64
+
65
+ if (normalized.endsWith('.kt') || normalized.endsWith('.kts')) {
66
+ return 'android';
67
+ }
68
+
69
+ if (isReactPath(normalized)) {
70
+ return 'frontend';
71
+ }
72
+
73
+ if (!isTypeScriptOrJavaScript(normalized)) {
74
+ return undefined;
75
+ }
76
+
77
+ if (
78
+ normalized.startsWith('apps/frontend/') ||
79
+ normalized.startsWith('apps/web/') ||
80
+ /(^|\/)(frontend|web|client)(\/|$)/.test(normalized)
81
+ ) {
82
+ return 'frontend';
83
+ }
84
+
85
+ if (
86
+ normalized.startsWith('apps/backend/') ||
87
+ /(^|\/)(backend|server|api)(\/|$)/.test(normalized)
88
+ ) {
89
+ return 'backend';
90
+ }
91
+
92
+ return undefined;
93
+ };
94
+
95
+ const toUniqueSorted = (values: ReadonlyArray<string>): string[] => {
96
+ return Array.from(
97
+ new Set(values.map((value) => value.trim()).filter((value) => value.length > 0))
98
+ ).sort((left, right) => left.localeCompare(right));
99
+ };
100
+
101
+ const intersectRuleIds = (
102
+ ruleIds: ReadonlyArray<string>,
103
+ selectedRuleIds: ReadonlySet<string>
104
+ ): string[] => {
105
+ return toUniqueSorted(ruleIds.filter((ruleId) => selectedRuleIds.has(ruleId)));
106
+ };
107
+
108
+ const subtractRuleIds = (
109
+ allRuleIds: ReadonlyArray<string>,
110
+ excludedRuleIds: ReadonlySet<string>
111
+ ): string[] => {
112
+ return toUniqueSorted(allRuleIds.filter((ruleId) => !excludedRuleIds.has(ruleId)));
113
+ };
114
+
115
+ const platformOrder: ReadonlyArray<SkillsCompliancePlatform> = [
116
+ 'ios',
117
+ 'android',
118
+ 'backend',
119
+ 'frontend',
120
+ ];
121
+
122
+ const isSupportedPlatform = (
123
+ platform: RuleDefinition['platform']
124
+ ): platform is SkillsCompliancePlatform => {
125
+ return platform !== undefined && platformOrder.includes(platform as SkillsCompliancePlatform);
126
+ };
127
+
128
+ export const evaluateSkillsCompliance = (params: {
129
+ skillsRuleSet: SkillsRuleSetLoadResult;
130
+ observedFilePaths: ReadonlyArray<string>;
131
+ activeRuleIds: ReadonlyArray<string>;
132
+ evaluatedRuleIds: ReadonlyArray<string>;
133
+ matchedRuleIds: ReadonlyArray<string>;
134
+ }): SkillsComplianceSnapshot | undefined => {
135
+ const activeRuleIdsSet = new Set(params.activeRuleIds);
136
+ const evaluatedRuleIdsSet = new Set(params.evaluatedRuleIds);
137
+ const matchedRuleIdsSet = new Set(params.matchedRuleIds);
138
+
139
+ const requiredByPlatform = new Map<SkillsCompliancePlatform, string[]>();
140
+ for (const rule of params.skillsRuleSet.rules) {
141
+ if (!isSupportedPlatform(rule.platform)) {
142
+ continue;
143
+ }
144
+ if (!activeRuleIdsSet.has(rule.id)) {
145
+ continue;
146
+ }
147
+ const current = requiredByPlatform.get(rule.platform) ?? [];
148
+ current.push(rule.id);
149
+ requiredByPlatform.set(rule.platform, current);
150
+ }
151
+
152
+ if (requiredByPlatform.size === 0) {
153
+ return undefined;
154
+ }
155
+
156
+ const normalizedObservedFilePaths = toUniqueSorted(
157
+ params.observedFilePaths.map((path) => normalizePath(path))
158
+ );
159
+ const byFile: SkillsComplianceFileEntry[] = [];
160
+
161
+ for (const filePath of normalizedObservedFilePaths) {
162
+ const platform = inferPlatformFromPath(filePath);
163
+ if (!platform) {
164
+ continue;
165
+ }
166
+
167
+ const requiredRuleIds = toUniqueSorted(requiredByPlatform.get(platform) ?? []);
168
+ if (requiredRuleIds.length === 0) {
169
+ continue;
170
+ }
171
+
172
+ const appliedRuleIds = intersectRuleIds(requiredRuleIds, evaluatedRuleIdsSet);
173
+ const evidenceRuleIds = intersectRuleIds(requiredRuleIds, matchedRuleIdsSet);
174
+ const appliedRuleIdsSet = new Set(appliedRuleIds);
175
+ const missingRuleIds = subtractRuleIds(requiredRuleIds, appliedRuleIdsSet);
176
+
177
+ byFile.push({
178
+ file_path: filePath,
179
+ platform,
180
+ required_rule_ids: requiredRuleIds,
181
+ applied_rule_ids: appliedRuleIds,
182
+ evidence_rule_ids: evidenceRuleIds,
183
+ missing_rule_ids: missingRuleIds,
184
+ status: missingRuleIds.length > 0 ? 'INCOMPLETE' : 'OK',
185
+ });
186
+ }
187
+
188
+ if (byFile.length === 0) {
189
+ return undefined;
190
+ }
191
+
192
+ const requiredRuleIds = toUniqueSorted(byFile.flatMap((item) => item.required_rule_ids));
193
+ const appliedRuleIds = toUniqueSorted(byFile.flatMap((item) => item.applied_rule_ids));
194
+ const evidenceRuleIds = toUniqueSorted(byFile.flatMap((item) => item.evidence_rule_ids));
195
+ const missingRuleIds = toUniqueSorted(byFile.flatMap((item) => item.missing_rule_ids));
196
+
197
+ return {
198
+ required_rule_ids: requiredRuleIds,
199
+ applied_rule_ids: appliedRuleIds,
200
+ evidence_rule_ids: evidenceRuleIds,
201
+ missing_rule_ids: missingRuleIds,
202
+ counts: {
203
+ files_in_scope: byFile.length,
204
+ files_with_missing: byFile.filter((item) => item.missing_rule_ids.length > 0).length,
205
+ required: requiredRuleIds.length,
206
+ applied: appliedRuleIds.length,
207
+ evidence: evidenceRuleIds.length,
208
+ missing: missingRuleIds.length,
209
+ },
210
+ by_file: byFile,
211
+ };
212
+ };