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,188 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import process from 'node:process';
5
+ import {
6
+ evaluateP9RuralgoRuntimeReadiness,
7
+ normalizeSemverToken,
8
+ type P9RuralgoRuntimePolicy,
9
+ } from './p9-ruralgo-runtime-ready-lib';
10
+
11
+ type CliOptions = {
12
+ repoPath: string;
13
+ json: boolean;
14
+ policy: P9RuralgoRuntimePolicy;
15
+ };
16
+
17
+ type PackageJsonEngines = {
18
+ engines?: {
19
+ node?: string;
20
+ npm?: string;
21
+ };
22
+ };
23
+
24
+ const DEFAULT_REPO_PATH =
25
+ '/Users/juancarlosmerlosalbarracin/Developer/Projects/ruralgo-fork';
26
+
27
+ const printHelp = (): void => {
28
+ process.stdout.write(
29
+ [
30
+ 'Uso:',
31
+ ' node --import tsx scripts/check-p9-ruralgo-runtime-ready.ts [opciones]',
32
+ '',
33
+ 'Opciones:',
34
+ ' --repo=<path> Ruta del repo ruralgo-fork (default: ~/Developer/Projects/ruralgo-fork)',
35
+ ' --allow-missing-node-engine No falla si falta engines.node',
36
+ ' --allow-missing-npm-engine No falla si falta engines.npm',
37
+ ' --json Salida JSON',
38
+ ' --help Muestra ayuda',
39
+ '',
40
+ 'Exit code:',
41
+ ' 0 => runtime listo para F1.T1',
42
+ ' 1 => runtime no listo',
43
+ '',
44
+ ].join('\n'),
45
+ );
46
+ };
47
+
48
+ const parseArgs = (argv: ReadonlyArray<string>): CliOptions => {
49
+ const options: CliOptions = {
50
+ repoPath: DEFAULT_REPO_PATH,
51
+ json: false,
52
+ policy: {
53
+ requireNodeEngine: true,
54
+ requireNpmEngine: true,
55
+ },
56
+ };
57
+
58
+ for (const arg of argv) {
59
+ if (arg === '--json') {
60
+ options.json = true;
61
+ continue;
62
+ }
63
+ if (arg === '--allow-missing-node-engine') {
64
+ options.policy.requireNodeEngine = false;
65
+ continue;
66
+ }
67
+ if (arg === '--allow-missing-npm-engine') {
68
+ options.policy.requireNpmEngine = false;
69
+ continue;
70
+ }
71
+ if (arg.startsWith('--repo=')) {
72
+ options.repoPath = arg.slice('--repo='.length);
73
+ continue;
74
+ }
75
+ throw new Error(`Argumento no soportado: ${arg}`);
76
+ }
77
+
78
+ return options;
79
+ };
80
+
81
+ const resolveCurrentNpmVersion = (repoPath: string): string | null => {
82
+ try {
83
+ const version = execFileSync('npm', ['-v'], {
84
+ cwd: repoPath,
85
+ encoding: 'utf8',
86
+ stdio: ['ignore', 'pipe', 'pipe'],
87
+ }).trim();
88
+ return version.length > 0 ? version : null;
89
+ } catch {
90
+ const userAgent = process.env.npm_config_user_agent ?? '';
91
+ const match = userAgent.match(/npm\/(\d+\.\d+\.\d+)/);
92
+ return match ? match[1] : null;
93
+ }
94
+ };
95
+
96
+ const main = (): number => {
97
+ const argv = process.argv.slice(2);
98
+ if (argv.includes('--help')) {
99
+ printHelp();
100
+ return 0;
101
+ }
102
+
103
+ let options: CliOptions;
104
+ try {
105
+ options = parseArgs(argv);
106
+ } catch (error) {
107
+ process.stderr.write(`[p9][runtime-ready] ${(error as Error).message}\n`);
108
+ process.stderr.write('Usa --help para ver opciones.\n');
109
+ return 1;
110
+ }
111
+
112
+ const packageJsonPath = join(options.repoPath, 'package.json');
113
+ const packageJsonAvailable = existsSync(packageJsonPath);
114
+ let packageJsonValid = false;
115
+ let requiredNode: string | null = null;
116
+ let requiredNpm: string | null = null;
117
+
118
+ if (packageJsonAvailable) {
119
+ try {
120
+ const packageJson = JSON.parse(
121
+ readFileSync(packageJsonPath, 'utf8'),
122
+ ) as PackageJsonEngines;
123
+ packageJsonValid = true;
124
+ requiredNode = packageJson.engines?.node ?? null;
125
+ requiredNpm = packageJson.engines?.npm ?? null;
126
+ } catch {
127
+ packageJsonValid = false;
128
+ }
129
+ }
130
+
131
+ const result = evaluateP9RuralgoRuntimeReadiness(
132
+ {
133
+ packageJsonAvailable,
134
+ packageJsonValid,
135
+ requiredNode,
136
+ requiredNpm,
137
+ currentNode: normalizeSemverToken(process.versions.node),
138
+ currentNpm: resolveCurrentNpmVersion(options.repoPath),
139
+ },
140
+ options.policy,
141
+ );
142
+
143
+ const payload = {
144
+ ready: result.ready,
145
+ repoPath: options.repoPath,
146
+ required: {
147
+ node: requiredNode,
148
+ npm: requiredNpm,
149
+ },
150
+ current: {
151
+ node: normalizeSemverToken(process.versions.node),
152
+ npm: resolveCurrentNpmVersion(options.repoPath),
153
+ },
154
+ policy: options.policy,
155
+ issues: result.issues,
156
+ };
157
+
158
+ if (options.json) {
159
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
160
+ } else {
161
+ process.stdout.write('[p9][runtime-ready] PRECHECK\n');
162
+ process.stdout.write(`[p9][runtime-ready] repo=${payload.repoPath}\n`);
163
+ process.stdout.write(
164
+ `[p9][runtime-ready] required.node=${payload.required.node ?? 'n/a'} required.npm=${
165
+ payload.required.npm ?? 'n/a'
166
+ }\n`,
167
+ );
168
+ process.stdout.write(
169
+ `[p9][runtime-ready] current.node=${payload.current.node ?? 'n/a'} current.npm=${
170
+ payload.current.npm ?? 'n/a'
171
+ }\n`,
172
+ );
173
+ process.stdout.write(
174
+ `[p9][runtime-ready] status=${payload.ready ? 'READY' : 'NOT_READY'} issues=${
175
+ payload.issues.length
176
+ }\n`,
177
+ );
178
+ for (const issue of payload.issues) {
179
+ process.stdout.write(
180
+ `[p9][runtime-ready][${issue.severity}] ${issue.code}: ${issue.message}\n`,
181
+ );
182
+ }
183
+ }
184
+
185
+ return payload.ready ? 0 : 1;
186
+ };
187
+
188
+ process.exitCode = main();
@@ -15,6 +15,7 @@ export type PackPayload = {
15
15
  type PackageJson = {
16
16
  name?: string;
17
17
  version?: string;
18
+ exports?: unknown;
18
19
  };
19
20
 
20
21
  type ReadTextFile = (path: string, encoding: BufferEncoding) => string;
@@ -35,6 +36,43 @@ type NpmPackDryRunPayload = {
35
36
  files?: NpmPackDryRunFile[];
36
37
  };
37
38
 
39
+ export type InvalidRuntimeExportTarget = {
40
+ subpath: string;
41
+ target: string;
42
+ };
43
+
44
+ const RUNTIME_EXPORT_TARGET_PATTERN = /\.(?:[cm]?js|json|node)$/;
45
+
46
+ const collectExportTargets = (
47
+ value: unknown,
48
+ subpath: string,
49
+ acc: InvalidRuntimeExportTarget[]
50
+ ): void => {
51
+ if (typeof value === 'string') {
52
+ if (!RUNTIME_EXPORT_TARGET_PATTERN.test(value)) {
53
+ acc.push({ subpath, target: value });
54
+ }
55
+ return;
56
+ }
57
+
58
+ if (value && typeof value === 'object') {
59
+ for (const [key, nestedValue] of Object.entries(value)) {
60
+ const nestedSubpath = key.startsWith('.') || subpath === '(root)'
61
+ ? key
62
+ : `${subpath}.${key}`;
63
+ collectExportTargets(nestedValue, nestedSubpath, acc);
64
+ }
65
+ }
66
+ };
67
+
68
+ export const findInvalidRuntimeExportTargets = (
69
+ exportsField: unknown
70
+ ): ReadonlyArray<InvalidRuntimeExportTarget> => {
71
+ const invalid: InvalidRuntimeExportTarget[] = [];
72
+ collectExportTargets(exportsField, '(root)', invalid);
73
+ return invalid.sort((a, b) => a.subpath.localeCompare(b.subpath) || a.target.localeCompare(b.target));
74
+ };
75
+
38
76
  const parseNpmPackDryRunFiles = (stdout: string): ReadonlyArray<string> => {
39
77
  let parsed: unknown;
40
78
  try {
@@ -138,6 +176,17 @@ export const runPackageManifestCheck = async (
138
176
  );
139
177
  }
140
178
 
179
+ const packageJsonPath = resolve(deps.cwd, 'package.json');
180
+ const packageJsonRaw = deps.readTextFile(packageJsonPath, 'utf8');
181
+ const packageJson = JSON.parse(packageJsonRaw) as PackageJson;
182
+ const invalidExportTargets = findInvalidRuntimeExportTargets(packageJson.exports);
183
+ if (invalidExportTargets.length > 0) {
184
+ const details = invalidExportTargets.map((entry) => `${entry.subpath} -> ${entry.target}`).join('\n- ');
185
+ throw new Error(
186
+ `Package exports must target runtime-loadable files (.js/.cjs/.mjs/.json/.node):\n- ${details}`
187
+ );
188
+ }
189
+
141
190
  deps.writeStdout(`package manifest check passed for ${payload.id}\n`);
142
191
  deps.writeStdout(`files scanned: ${filePaths.length}\n`);
143
192
  };
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ if [[ "$#" -gt 0 ]]; then
5
+ FILES=("$@")
6
+ else
7
+ FILES=(
8
+ "docs/REFRACTOR_PROGRESS.md"
9
+ "docs/EXECUTION_BOARD.md"
10
+ "docs/validation/p9-ruralgo-fork-validation-tracking.md"
11
+ )
12
+ fi
13
+
14
+ ACTIVE_PATTERN="^- 🚧 \`[A-Za-z0-9.-]+\` "
15
+ HAS_ERROR=0
16
+
17
+ for FILE in "${FILES[@]}"; do
18
+ if [[ ! -f "${FILE}" ]]; then
19
+ echo "[tracking-single-active] missing file: ${FILE}" >&2
20
+ HAS_ERROR=1
21
+ continue
22
+ fi
23
+
24
+ ACTIVE_COUNT="$(rg -n "${ACTIVE_PATTERN}" "${FILE}" | wc -l | tr -d ' ')"
25
+ echo "[tracking-single-active] file=${FILE}"
26
+ echo "[tracking-single-active] in_progress_count=${ACTIVE_COUNT}"
27
+
28
+ if [[ "${ACTIVE_COUNT}" -ne 1 ]]; then
29
+ echo "[tracking-single-active] ERROR: expected exactly 1 in-progress task (🚧), found ${ACTIVE_COUNT}." >&2
30
+ echo "[tracking-single-active] current in-progress entries:" >&2
31
+ rg -n "${ACTIVE_PATTERN}" "${FILE}" >&2 || true
32
+ HAS_ERROR=1
33
+ fi
34
+ done
35
+
36
+ if [[ "${HAS_ERROR}" -ne 0 ]]; then
37
+ exit 1
38
+ fi
39
+
40
+ echo "[tracking-single-active] OK: exactly one in-progress task in each tracking file."
@@ -49,6 +49,29 @@ const hasViolationCode = (
49
49
  const ACTIONABLE_HINTS_BY_CODE: Readonly<Record<string, string>> = {
50
50
  EVIDENCE_MISSING: 'ejecuta una auditoría (1/2/3/4) para regenerar .ai_evidence.json.',
51
51
  EVIDENCE_INVALID: 'regenera .ai_evidence.json desde una opción de auditoría.',
52
+ EVIDENCE_INTEGRITY_MISSING: 'regenera evidencia para crear metadata criptográfica de integridad.',
53
+ EVIDENCE_INTEGRITY_UNAVAILABLE: 'regenera evidencia válida para restaurar integridad.',
54
+ EVIDENCE_INTEGRITY_SCHEMA_INVALID: 'regenera evidencia; el bloque de integridad es inválido.',
55
+ EVIDENCE_INTEGRITY_HASH_FORMAT_INVALID:
56
+ 'regenera evidencia; hash de integridad con formato inválido.',
57
+ EVIDENCE_INTEGRITY_PREVIOUS_CHAIN_HASH_INVALID:
58
+ 'regenera evidencia para reparar previous_chain_hash.',
59
+ EVIDENCE_INTEGRITY_TIMESTAMP_MISMATCH:
60
+ 'regenera evidencia para alinear generated_at y timestamp.',
61
+ EVIDENCE_INTEGRITY_PAYLOAD_HASH_MISMATCH:
62
+ 'regenera evidencia; payload_hash no coincide con el payload.',
63
+ EVIDENCE_INTEGRITY_CHAIN_HASH_MISMATCH:
64
+ 'regenera evidencia; chain_hash no coincide con la cadena esperada.',
65
+ EVIDENCE_INTEGRITY_SIGNATURE_REQUIRED:
66
+ 'configura firma de evidencia o ajusta la política de firma.',
67
+ EVIDENCE_INTEGRITY_SIGNATURE_FORMAT_INVALID:
68
+ 'regenera evidencia firmada; metadatos de firma inválidos.',
69
+ EVIDENCE_INTEGRITY_SIGNATURE_KEY_MISSING:
70
+ 'configura PUMUKI_EVIDENCE_SIGNING_KEY para verificar firma.',
71
+ EVIDENCE_INTEGRITY_SIGNATURE_KEY_MISMATCH:
72
+ 'alinea PUMUKI_EVIDENCE_SIGNING_KEY_ID con key_id de evidencia.',
73
+ EVIDENCE_INTEGRITY_SIGNATURE_INVALID:
74
+ 'regenera evidencia firmada; la firma actual no verifica.',
52
75
  EVIDENCE_STALE: 'refresca evidencia antes de continuar con commit/push.',
53
76
  EVIDENCE_TIMESTAMP_INVALID: 'regenera evidencia para obtener un timestamp válido.',
54
77
  EVIDENCE_GATE_BLOCKED: 'corrige primero las violaciones bloqueantes y vuelve a auditar.',
@@ -65,6 +88,7 @@ const ACTIONABLE_HINTS_BY_CODE: Readonly<Record<string, string>> = {
65
88
  EVIDENCE_TIMESTAMP_FUTURE: 'corrige la hora del sistema y regenera evidencia.',
66
89
  GITFLOW_PROTECTED_BRANCH:
67
90
  'evita trabajo directo en ramas protegidas (usa feature/*).',
91
+ WAIVER_POLICY_INVALID: 'corrige el archivo de waivers antes de continuar.',
68
92
  };
69
93
 
70
94
  const buildHints = (
@@ -206,12 +230,19 @@ export const formatConsumerPreflight = (
206
230
  ): string => {
207
231
  const git = preflight.result.repo_state.git;
208
232
  const evidence = preflight.result.evidence;
233
+ const policyTrace = preflight.result.policy.trace;
209
234
  const lines = [
210
235
  'PRE-FLIGHT CHECK',
211
236
  `Stage: ${preflight.stage}`,
212
237
  `Branch: ${git.branch ?? 'unknown'} · Upstream: ${git.upstream ?? 'none'}`,
213
238
  `Worktree: dirty=${git.dirty ? 'yes' : 'no'} staged=${git.staged} unstaged=${git.unstaged} ahead=${git.ahead} behind=${git.behind}`,
239
+ `Policy: source=${policyTrace.source} bundle=${policyTrace.bundle} hash=${policyTrace.hash}`,
240
+ `Policy signature: version=${policyTrace.version ?? 'n/a'} signature=${policyTrace.signature ?? 'n/a'}`,
214
241
  `Evidence: kind=${evidence.kind} age=${evidence.age_seconds ?? 'n/a'}s max=${evidence.max_age_seconds}s`,
242
+ `Evidence source: ${evidence.source} path=${evidence.path}`,
243
+ `Evidence digest: ${evidence.digest ?? 'n/a'} generated_at=${evidence.generated_at ?? 'n/a'}`,
244
+ `Evidence integrity: status=${evidence.integrity.status} chain_hash=${evidence.integrity.chain_hash ?? 'n/a'} prev=${evidence.integrity.previous_chain_hash ?? 'n/a'} signature=${evidence.integrity.signature_present ? 'present' : 'absent'} verified=${evidence.integrity.signature_verified === null ? 'n/a' : evidence.integrity.signature_verified ? 'yes' : 'no'}`,
245
+ `Waivers: status=${preflight.result.waivers.status} applied=${preflight.result.waivers.applied.length} path=${preflight.result.waivers.path}`,
215
246
  `Gate: ${preflight.status} (${preflight.result.violations.length} violations)`,
216
247
  ];
217
248
  lines.push(...buildBlockingCauseLines(preflight));
@@ -73,17 +73,17 @@ export const createConsumerMenuRuntime = (params: {
73
73
  };
74
74
 
75
75
  const printEmptyScopeHint = (summary: LegacyAuditSummary, scope: 'staged' | 'workingTree'): void => {
76
- if (summary.status !== 'ok' || summary.filesScanned > 0) {
76
+ if (summary.status !== 'ok' || summary.filesScanned > 0 || summary.totalViolations > 0) {
77
77
  return;
78
78
  }
79
79
  if (scope === 'staged') {
80
80
  params.write(
81
- '\nℹ Scope vacío (staged): no hay archivos staged para auditar. Resultado PASS por alcance vacío; usa 1 o 2 para validar repo completo.\n'
81
+ '\nℹ Scope vacío (staged): no hay archivos staged para auditar. Resultado PASS parcial por alcance vacío; usa 1 o 2 para validar repo completo.\n'
82
82
  );
83
83
  return;
84
84
  }
85
85
  params.write(
86
- '\nℹ Scope vacío (working tree): no hay cambios sin commitear para auditar. Resultado PASS por alcance vacío; usa 1 o 2 para validar repo completo.\n'
86
+ '\nℹ Scope vacío (working tree): no hay cambios sin commitear para auditar. Resultado PASS parcial por alcance vacío; usa 1 o 2 para validar repo completo.\n'
87
87
  );
88
88
  };
89
89
 
@@ -417,6 +417,11 @@ const inferRulesetBundle = (
417
417
  };
418
418
 
419
419
  const normalized = ruleId.toLowerCase();
420
+ const inferPolicyBundle = (): string =>
421
+ findBundle((bundle) => bundle.startsWith('gate-policy.default.'))
422
+ ?? findBundle((bundle) => bundle.startsWith('policyRuleSet@'))
423
+ ?? findBundle((bundle) => bundle === 'project-rules')
424
+ ?? 'policyRuleSet (inferred)';
420
425
  if (normalized.startsWith('heuristics.')) {
421
426
  return findBundle((bundle) => bundle.startsWith('astHeuristicsRuleSet@'))
422
427
  ?? 'astHeuristicsRuleSet (inferred)';
@@ -437,6 +442,10 @@ const inferRulesetBundle = (
437
442
  return findBundle((bundle) => bundle.startsWith('frontendRuleSet@'))
438
443
  ?? 'frontendRuleSet (inferred)';
439
444
  }
445
+ if (normalized.startsWith('common.')) {
446
+ return findBundle((bundle) => bundle.startsWith('commonRuleSet@'))
447
+ ?? 'commonRuleSet (inferred)';
448
+ }
440
449
  if (normalized.startsWith('skills.ios.')) {
441
450
  return findBundle((bundle) => bundle.startsWith('ios-') && bundle.includes('guidelines@'))
442
451
  ?? 'ios-guidelines (inferred)';
@@ -454,7 +463,14 @@ const inferRulesetBundle = (
454
463
  ?? 'frontend-guidelines (inferred)';
455
464
  }
456
465
  if (normalized.startsWith('methodology.') || normalized.startsWith('project.')) {
457
- return findBundle((bundle) => bundle === 'project-rules') ?? 'project-rules (inferred)';
466
+ return inferPolicyBundle();
467
+ }
468
+ if (
469
+ normalized.startsWith('workflow.')
470
+ || normalized.startsWith('sdd.')
471
+ || normalized.startsWith('generic_')
472
+ ) {
473
+ return inferPolicyBundle();
458
474
  }
459
475
  return 'unknown-ruleset';
460
476
  };
@@ -576,6 +592,9 @@ const computeLegacySeverity = (
576
592
  };
577
593
 
578
594
  const formatStatusLine = (summary: LegacyAuditSummary): string => {
595
+ if (summary.filesScanned <= 0 && summary.totalViolations === 0) {
596
+ return 'ℹ STATUS: SCOPE EMPTY\nNo auditable files detected in current scope';
597
+ }
579
598
  if (summary.bySeverity.CRITICAL > 0 || summary.bySeverity.HIGH > 0) {
580
599
  return '⚠ STATUS: ACTION REQUIRED\nCritical or high-severity issues detected';
581
600
  }
@@ -1037,6 +1056,7 @@ export const formatLegacyAuditReport = (
1037
1056
  return '.ai_evidence.json is invalid. Regenerate evidence and retry.';
1038
1057
  }
1039
1058
 
1059
+ const scopeEmpty = summary.filesScanned <= 0 && summary.totalViolations === 0;
1040
1060
  const topViolations = summary.topViolations.length === 0
1041
1061
  ? ['• none']
1042
1062
  : summary.topViolations.flatMap((violation) => [
@@ -1052,7 +1072,9 @@ export const formatLegacyAuditReport = (
1052
1072
 
1053
1073
  const commitStatus = summary.bySeverity.CRITICAL > 0 || summary.bySeverity.HIGH > 0
1054
1074
  ? 'COMMIT BLOCKED — STRICT REPO+STAGING'
1055
- : 'COMMIT ALLOWED';
1075
+ : scopeEmpty
1076
+ ? 'COMMIT ALLOWED — SCOPE EMPTY (PARTIAL PASS)'
1077
+ : 'COMMIT ALLOWED';
1056
1078
 
1057
1079
  const overviewPanel = renderPanel([
1058
1080
  OVERVIEW_TITLE,
@@ -1117,18 +1139,24 @@ export const formatLegacyAuditReport = (
1117
1139
 
1118
1140
  const blockedMessage = summary.bySeverity.CRITICAL > 0 || summary.bySeverity.HIGH > 0
1119
1141
  ? 'ACTION REQUIRED: Critical or high-severity issues detected. Please review and fix before proceeding.'
1120
- : 'No blocking violations detected.';
1142
+ : scopeEmpty
1143
+ ? 'PASS parcial por alcance vacío: no hay archivos auditables en el scope actual.'
1144
+ : 'No blocking violations detected.';
1121
1145
  const actionLine = commitStatus.includes('BLOCKED')
1122
1146
  ? 'Action: clean entire repository before committing.'
1123
- : 'Action: proceed with commit flow.';
1147
+ : scopeEmpty
1148
+ ? 'Action: run option 1 or 2 to validate full-scope before relying on this pass.'
1149
+ : 'Action: proceed with commit flow.';
1124
1150
  const affectedRatio = summary.filesScanned > 0
1125
1151
  ? Math.round((summary.filesAffected / Math.max(1, summary.filesScanned)) * 100)
1126
1152
  : 0;
1127
1153
  const nextAction = commitStatus.includes('BLOCKED')
1128
1154
  ? 'Next action: fix CRITICAL/HIGH findings and rerun full audit.'
1129
- : summary.bySeverity.MEDIUM > 0 || summary.bySeverity.LOW > 0
1130
- ? 'Next action: schedule MEDIUM/LOW cleanup without blocking delivery.'
1131
- : 'Next action: maintain baseline and continue with regular checks.';
1155
+ : scopeEmpty
1156
+ ? 'Next action: run option 1 or 2 to obtain full-scope audit signal.'
1157
+ : summary.bySeverity.MEDIUM > 0 || summary.bySeverity.LOW > 0
1158
+ ? 'Next action: schedule MEDIUM/LOW cleanup without blocking delivery.'
1159
+ : 'Next action: maintain baseline and continue with regular checks.';
1132
1160
 
1133
1161
  const metricsPanel = renderPanel([
1134
1162
  'METRICS',
@@ -50,12 +50,16 @@ export const toMatrixOptionReport = (
50
50
  if (summary.status !== 'ok') {
51
51
  return EMPTY_OPTION_REPORT;
52
52
  }
53
+ const diagnosis = resolveMatrixOptionDiagnosis(summary);
54
+ const normalizedOutcome = diagnosis === 'scope-empty' && summary.outcome.toUpperCase() === 'PASS'
55
+ ? 'PASS_SCOPE_EMPTY'
56
+ : summary.outcome;
53
57
  return {
54
58
  stage: summary.stage,
55
- outcome: summary.outcome,
59
+ outcome: normalizedOutcome,
56
60
  filesScanned: summary.filesScanned,
57
61
  totalViolations: summary.totalViolations,
58
- diagnosis: resolveMatrixOptionDiagnosis(summary),
62
+ diagnosis,
59
63
  };
60
64
  };
61
65
 
@@ -10,7 +10,7 @@
10
10
 
11
11
  set -e
12
12
 
13
- PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
+ PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
14
14
  cd "$PROJECT_ROOT"
15
15
 
16
16
  GREEN='\033[0;32m'
@@ -0,0 +1,117 @@
1
+ export type P9RuralgoBaselineIssueCode =
2
+ | 'NOT_GIT_REPO'
3
+ | 'PRE_COMMIT_MANAGED_HOOK_PRESENT'
4
+ | 'PRE_PUSH_MANAGED_HOOK_PRESENT'
5
+ | 'AI_EVIDENCE_PRESENT'
6
+ | 'ARTIFACTS_PRESENT'
7
+ | 'PUMUKI_DIR_PRESENT'
8
+ | 'NODE_MODULES_PRESENT'
9
+ | 'PACKAGE_LOCK_PRESENT';
10
+
11
+ export type P9RuralgoBaselineIssue = {
12
+ code: P9RuralgoBaselineIssueCode;
13
+ severity: 'error' | 'warning';
14
+ message: string;
15
+ };
16
+
17
+ export type P9RuralgoBaselineSnapshot = {
18
+ isGitRepo: boolean;
19
+ hasManagedPreCommitHook: boolean;
20
+ hasManagedPrePushHook: boolean;
21
+ hasAiEvidenceFile: boolean;
22
+ hasArtifactsDir: boolean;
23
+ hasPumukiDir: boolean;
24
+ hasNodeModulesDir: boolean;
25
+ hasPackageLockFile: boolean;
26
+ };
27
+
28
+ export type P9RuralgoBaselinePolicy = {
29
+ requireNoManagedHooks: boolean;
30
+ requireNoAiEvidence: boolean;
31
+ requireNoArtifacts: boolean;
32
+ requireNoPumukiDir: boolean;
33
+ requireNoNodeModules: boolean;
34
+ requireNoPackageLock: boolean;
35
+ };
36
+
37
+ export type P9RuralgoBaselineResult = {
38
+ ready: boolean;
39
+ issues: ReadonlyArray<P9RuralgoBaselineIssue>;
40
+ };
41
+
42
+ export const DEFAULT_P9_RURALGO_BASELINE_POLICY: P9RuralgoBaselinePolicy = {
43
+ requireNoManagedHooks: true,
44
+ requireNoAiEvidence: true,
45
+ requireNoArtifacts: true,
46
+ requireNoPumukiDir: true,
47
+ requireNoNodeModules: true,
48
+ requireNoPackageLock: true,
49
+ };
50
+
51
+ const pushIssue = (
52
+ issues: P9RuralgoBaselineIssue[],
53
+ code: P9RuralgoBaselineIssueCode,
54
+ message: string,
55
+ severity: 'error' | 'warning' = 'error',
56
+ ): void => {
57
+ issues.push({ code, severity, message });
58
+ };
59
+
60
+ export const evaluateP9RuralgoBaselineClean = (
61
+ snapshot: P9RuralgoBaselineSnapshot,
62
+ policyOverrides?: Partial<P9RuralgoBaselinePolicy>,
63
+ ): P9RuralgoBaselineResult => {
64
+ const policy: P9RuralgoBaselinePolicy = {
65
+ ...DEFAULT_P9_RURALGO_BASELINE_POLICY,
66
+ ...(policyOverrides ?? {}),
67
+ };
68
+ const issues: P9RuralgoBaselineIssue[] = [];
69
+
70
+ if (!snapshot.isGitRepo) {
71
+ pushIssue(issues, 'NOT_GIT_REPO', 'La ruta objetivo no es un repositorio git válido.');
72
+ return { ready: false, issues };
73
+ }
74
+
75
+ if (policy.requireNoManagedHooks && snapshot.hasManagedPreCommitHook) {
76
+ pushIssue(
77
+ issues,
78
+ 'PRE_COMMIT_MANAGED_HOOK_PRESENT',
79
+ 'Existe bloque gestionado de Pumuki en .git/hooks/pre-commit.',
80
+ );
81
+ }
82
+ if (policy.requireNoManagedHooks && snapshot.hasManagedPrePushHook) {
83
+ pushIssue(
84
+ issues,
85
+ 'PRE_PUSH_MANAGED_HOOK_PRESENT',
86
+ 'Existe bloque gestionado de Pumuki en .git/hooks/pre-push.',
87
+ );
88
+ }
89
+ if (policy.requireNoAiEvidence && snapshot.hasAiEvidenceFile) {
90
+ pushIssue(issues, 'AI_EVIDENCE_PRESENT', 'Existe .ai_evidence.json en baseline limpio.');
91
+ }
92
+ if (policy.requireNoArtifacts && snapshot.hasArtifactsDir) {
93
+ pushIssue(issues, 'ARTIFACTS_PRESENT', 'Existe directorio artifacts/ en baseline limpio.');
94
+ }
95
+ if (policy.requireNoPumukiDir && snapshot.hasPumukiDir) {
96
+ pushIssue(issues, 'PUMUKI_DIR_PRESENT', 'Existe directorio .pumuki/ en baseline limpio.');
97
+ }
98
+ if (policy.requireNoNodeModules && snapshot.hasNodeModulesDir) {
99
+ pushIssue(
100
+ issues,
101
+ 'NODE_MODULES_PRESENT',
102
+ 'Existe directorio node_modules/ en baseline limpio.',
103
+ );
104
+ }
105
+ if (policy.requireNoPackageLock && snapshot.hasPackageLockFile) {
106
+ pushIssue(
107
+ issues,
108
+ 'PACKAGE_LOCK_PRESENT',
109
+ 'Existe package-lock.json en baseline limpio.',
110
+ );
111
+ }
112
+
113
+ return {
114
+ ready: !issues.some((issue) => issue.severity === 'error'),
115
+ issues,
116
+ };
117
+ };