pumuki 6.3.94 → 6.3.96
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.
- package/docs/operations/RELEASE_NOTES.md +8 -0
- package/integrations/config/skillsCustomRules.ts +99 -18
- package/integrations/gate/evaluateAiGate.ts +66 -12
- package/integrations/lifecycle/cli.ts +16 -2
- package/integrations/lifecycle/cliSdd.ts +1 -0
- package/integrations/lifecycle/governanceObservationSnapshot.ts +52 -6
- package/integrations/sdd/evidenceScaffold.ts +109 -2
- package/package.json +1 -1
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## 2026-04-20 (v6.3.96)
|
|
2
|
+
- **Diagnóstico explícito de skills faltantes**: `status`, `doctor` y el AI gate ya no degradan a señales genéricas cuando falta una skill requerida o su fuente es ilegible; ahora emiten `SKILLS_REQUIRED_SOURCE_MISSING` o `SKILLS_REQUIRED_SOURCE_UNREADABLE` con nombre de skill, ruta concreta y resolución accionable.
|
|
3
|
+
- **Rollout recomendado**: publicar `pumuki@6.3.96`, repin inmediato en `Flux_training` y repetir la repro del repo sin `docs/codex-skills`, confirmando que `status --json` y `doctor --json` incluyen la ruta `docs/codex-skills/windsurf-rules-backend.md` en `skills_contract.source_diagnostics`.
|
|
4
|
+
|
|
5
|
+
## 2026-04-20 (v6.3.95)
|
|
6
|
+
- **Enforcement operativo de trazabilidad contractual**: `pumuki sdd evidence` exige ahora `--traceability-markdown=<path>` cuando el repositorio declara en `AGENTS.md` la matriz mínima `ARCHIVO | SKILL | REGLA | EVIDENCIA | ESTADO`; el comando falla si falta la cabecera exacta o si no existe al menos una fila real.
|
|
7
|
+
- **Rollout recomendado**: publicar `pumuki@6.3.95`, repin inmediato en `Flux_training` y repetir la doble repro de `pumuki sdd evidence`: sin `--traceability-markdown` debe bloquear, y con un markdown repo-local que contenga la matriz contractual debe pasar.
|
|
8
|
+
|
|
1
9
|
## 2026-04-20 (v6.3.94)
|
|
2
10
|
- **Subtitle de bloqueo 100% en español**: la causa primaria visible de notificaciones ya traduce explícitamente los mensajes exactos de `console.log usage is not allowed in frontend code.` y `...backend code.` antes de construir el `subtitle`, evitando el último escape de copy inglés en bloqueos reales de frontend/backend.
|
|
3
11
|
- **Rollout recomendado**: publicar `pumuki@6.3.94`, repin inmediato en `Flux_training` y repetir un rojo real de frontend con `PUMUKI_SKIP_CHAINED_PRE_WRITE=1 pnpm exec pumuki-pre-commit`, confirmando que el `subtitle` contiene `Se detectó uso de "console.log" en frontend.` y no la frase inglesa raw.
|
|
@@ -49,6 +49,15 @@ export type CompiledImportedSkillsRulesResult = Omit<
|
|
|
49
49
|
'outputPath'
|
|
50
50
|
>;
|
|
51
51
|
|
|
52
|
+
export type SkillImportSourceDiagnostic = {
|
|
53
|
+
skillName: string;
|
|
54
|
+
canonicalSkillName: string;
|
|
55
|
+
sourcePath: string;
|
|
56
|
+
sourceType: 'declared_path' | 'required_skill';
|
|
57
|
+
issue: 'missing' | 'unreadable';
|
|
58
|
+
resolution: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
52
61
|
type VendorSkillManifestEntry = {
|
|
53
62
|
name: string;
|
|
54
63
|
file: string;
|
|
@@ -423,22 +432,57 @@ const loadVendorSkillsManifest = (
|
|
|
423
432
|
return bySkillName;
|
|
424
433
|
};
|
|
425
434
|
|
|
426
|
-
|
|
435
|
+
const buildSkillImportResolution = (sourcePath: string): string => {
|
|
436
|
+
const normalized = sourcePath.replace(/\\/g, '/');
|
|
437
|
+
if (normalized.includes('/docs/codex-skills/') || normalized.startsWith('docs/codex-skills/')) {
|
|
438
|
+
return `Restaura ${sourcePath} o ejecuta ./scripts/sync-codex-skills.sh para resincronizar las skills vendorizadas.`;
|
|
439
|
+
}
|
|
440
|
+
if (normalized.includes('/vendor/skills/') || normalized.startsWith('vendor/skills/')) {
|
|
441
|
+
return `Restaura ${sourcePath} desde vendor/skills o resincroniza las dependencias del repositorio.`;
|
|
442
|
+
}
|
|
443
|
+
return `Corrige la ruta declarada o restaura ${sourcePath} para que la skill requerida vuelva a estar disponible.`;
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
const canReadSkillSourceFile = (sourcePath: string): boolean => {
|
|
447
|
+
try {
|
|
448
|
+
readFileSync(sourcePath, 'utf8');
|
|
449
|
+
return true;
|
|
450
|
+
} catch {
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
export const resolveSkillImportSourcesWithDiagnostics = (params: {
|
|
427
456
|
repoRoot?: string;
|
|
428
457
|
explicitSources?: ReadonlyArray<string>;
|
|
429
|
-
}): string[] => {
|
|
458
|
+
}): { sourceFiles: string[]; diagnostics: SkillImportSourceDiagnostic[] } => {
|
|
430
459
|
const repoRoot = params.repoRoot ?? process.cwd();
|
|
431
460
|
const resolved = new Map<string, { path: string; priority: number }>();
|
|
432
461
|
const vendoredManifest = loadVendorSkillsManifest(repoRoot);
|
|
433
|
-
|
|
434
|
-
|
|
462
|
+
const diagnostics: SkillImportSourceDiagnostic[] = [];
|
|
463
|
+
|
|
464
|
+
const registerDiagnostic = (
|
|
465
|
+
rawPath: string,
|
|
466
|
+
skillName: string,
|
|
467
|
+
canonicalSkillName: string,
|
|
468
|
+
sourceType: SkillImportSourceDiagnostic['sourceType'],
|
|
469
|
+
issue: SkillImportSourceDiagnostic['issue'],
|
|
470
|
+
): void => {
|
|
435
471
|
const normalized = normalizePath({
|
|
436
472
|
repoRoot,
|
|
437
473
|
path: rawPath,
|
|
438
474
|
});
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
475
|
+
diagnostics.push({
|
|
476
|
+
skillName,
|
|
477
|
+
canonicalSkillName,
|
|
478
|
+
sourcePath: normalized,
|
|
479
|
+
sourceType,
|
|
480
|
+
issue,
|
|
481
|
+
resolution: buildSkillImportResolution(normalized),
|
|
482
|
+
});
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
const pushResolved = (normalized: string): void => {
|
|
442
486
|
const skillKey = toCanonicalImportedSkillName(sourceSkillNameFromPath(normalized));
|
|
443
487
|
const priority = isVendoredSkillMarkdownPath(normalized) ? 2 : 1;
|
|
444
488
|
const current = resolved.get(skillKey);
|
|
@@ -451,13 +495,36 @@ export const resolveSkillImportSources = (params: {
|
|
|
451
495
|
}
|
|
452
496
|
};
|
|
453
497
|
|
|
498
|
+
const registerSource = (
|
|
499
|
+
rawPath: string,
|
|
500
|
+
skillName: string,
|
|
501
|
+
canonicalSkillName: string,
|
|
502
|
+
sourceType: SkillImportSourceDiagnostic['sourceType'],
|
|
503
|
+
): void => {
|
|
504
|
+
const normalized = normalizePath({
|
|
505
|
+
repoRoot,
|
|
506
|
+
path: rawPath,
|
|
507
|
+
});
|
|
508
|
+
if (!existsSync(normalized)) {
|
|
509
|
+
registerDiagnostic(rawPath, skillName, canonicalSkillName, sourceType, 'missing');
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (!canReadSkillSourceFile(normalized)) {
|
|
513
|
+
registerDiagnostic(rawPath, skillName, canonicalSkillName, sourceType, 'unreadable');
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
pushResolved(normalized);
|
|
517
|
+
};
|
|
518
|
+
|
|
454
519
|
if (params.explicitSources && params.explicitSources.length > 0) {
|
|
455
520
|
for (const source of params.explicitSources) {
|
|
456
|
-
|
|
521
|
+
const normalized = normalizePath({ repoRoot, path: source });
|
|
522
|
+
registerSource(source, sourceSkillNameFromPath(normalized), toCanonicalImportedSkillName(sourceSkillNameFromPath(normalized)), 'declared_path');
|
|
457
523
|
}
|
|
458
|
-
return
|
|
459
|
-
.map((item) => item.path)
|
|
460
|
-
|
|
524
|
+
return {
|
|
525
|
+
sourceFiles: [...resolved.values()].map((item) => item.path).sort(),
|
|
526
|
+
diagnostics,
|
|
527
|
+
};
|
|
461
528
|
}
|
|
462
529
|
|
|
463
530
|
for (const profileFile of ['AGENTS.md', 'SKILLS.md']) {
|
|
@@ -467,23 +534,37 @@ export const resolveSkillImportSources = (params: {
|
|
|
467
534
|
}
|
|
468
535
|
const content = readFileSync(profilePath, 'utf8');
|
|
469
536
|
for (const candidate of extractSkillPathsFromText(content)) {
|
|
470
|
-
|
|
537
|
+
const normalized = normalizePath({ repoRoot, path: candidate });
|
|
538
|
+
const skillName = sourceSkillNameFromPath(normalized);
|
|
539
|
+
registerSource(candidate, skillName, toCanonicalImportedSkillName(skillName), 'declared_path');
|
|
471
540
|
}
|
|
472
541
|
for (const requiredSkillName of extractRequiredSkillNamesFromText(content)) {
|
|
473
542
|
const canonicalName = toCanonicalImportedSkillName(requiredSkillName);
|
|
474
543
|
const manifestPath = vendoredManifest.get(canonicalName);
|
|
475
544
|
if (manifestPath) {
|
|
476
|
-
|
|
545
|
+
registerSource(manifestPath, requiredSkillName, canonicalName, 'required_skill');
|
|
477
546
|
continue;
|
|
478
547
|
}
|
|
479
|
-
|
|
480
|
-
|
|
548
|
+
const canonicalVendoredPath = `vendor/skills/${canonicalName}/SKILL.md`;
|
|
549
|
+
const requiredVendoredPath = `vendor/skills/${requiredSkillName}/SKILL.md`;
|
|
550
|
+
const selectedPath = existsSync(normalizePath({ repoRoot, path: requiredVendoredPath }))
|
|
551
|
+
? requiredVendoredPath
|
|
552
|
+
: canonicalVendoredPath;
|
|
553
|
+
registerSource(selectedPath, requiredSkillName, canonicalName, 'required_skill');
|
|
481
554
|
}
|
|
482
555
|
}
|
|
483
556
|
|
|
484
|
-
return
|
|
485
|
-
.map((item) => item.path)
|
|
486
|
-
|
|
557
|
+
return {
|
|
558
|
+
sourceFiles: [...resolved.values()].map((item) => item.path).sort(),
|
|
559
|
+
diagnostics,
|
|
560
|
+
};
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
export const resolveSkillImportSources = (params: {
|
|
564
|
+
repoRoot?: string;
|
|
565
|
+
explicitSources?: ReadonlyArray<string>;
|
|
566
|
+
}): string[] => {
|
|
567
|
+
return resolveSkillImportSourcesWithDiagnostics(params).sourceFiles;
|
|
487
568
|
};
|
|
488
569
|
|
|
489
570
|
export const compileImportedSkillsRules = (params: {
|
|
@@ -12,6 +12,10 @@ import {
|
|
|
12
12
|
loadEffectiveSkillsLock,
|
|
13
13
|
loadRequiredSkillsLock,
|
|
14
14
|
} from '../config/skillsEffectiveLock';
|
|
15
|
+
import {
|
|
16
|
+
resolveSkillImportSourcesWithDiagnostics,
|
|
17
|
+
type SkillImportSourceDiagnostic,
|
|
18
|
+
} from '../config/skillsCustomRules';
|
|
15
19
|
import {
|
|
16
20
|
resolveSkillsEnforcement,
|
|
17
21
|
type SkillsEnforcementResolution,
|
|
@@ -56,6 +60,7 @@ export type AiGateSkillsContractAssessment = {
|
|
|
56
60
|
status: 'PASS' | 'FAIL' | 'NOT_APPLICABLE';
|
|
57
61
|
detected_platforms: ReadonlyArray<PreWriteSkillsPlatform>;
|
|
58
62
|
requirements: ReadonlyArray<AiGateSkillsContractPlatformRequirement>;
|
|
63
|
+
source_diagnostics: ReadonlyArray<SkillImportSourceDiagnostic>;
|
|
59
64
|
violations: ReadonlyArray<AiGateViolation>;
|
|
60
65
|
};
|
|
61
66
|
|
|
@@ -725,6 +730,20 @@ const collectPreWriteCrossPlatformCriticalViolations = (params: {
|
|
|
725
730
|
];
|
|
726
731
|
};
|
|
727
732
|
|
|
733
|
+
const toRequiredSkillSourceViolations = (
|
|
734
|
+
diagnostics: ReadonlyArray<SkillImportSourceDiagnostic>,
|
|
735
|
+
skillsEnforcement: SkillsEnforcementResolution
|
|
736
|
+
): AiGateViolation[] =>
|
|
737
|
+
diagnostics.map((diagnostic) =>
|
|
738
|
+
toSkillsViolation(
|
|
739
|
+
skillsEnforcement,
|
|
740
|
+
diagnostic.issue === 'missing'
|
|
741
|
+
? 'SKILLS_REQUIRED_SOURCE_MISSING'
|
|
742
|
+
: 'SKILLS_REQUIRED_SOURCE_UNREADABLE',
|
|
743
|
+
`La skill requerida "${diagnostic.skillName}" no está disponible en ${diagnostic.sourcePath}. ${diagnostic.resolution}`
|
|
744
|
+
)
|
|
745
|
+
);
|
|
746
|
+
|
|
728
747
|
const toSkillsContractAssessment = (params: {
|
|
729
748
|
stage: AiGateStage;
|
|
730
749
|
repoRoot: string;
|
|
@@ -734,12 +753,22 @@ const toSkillsContractAssessment = (params: {
|
|
|
734
753
|
skillsEnforcement: SkillsEnforcementResolution;
|
|
735
754
|
}): AiGateSkillsContractAssessment => {
|
|
736
755
|
const requiredPlatforms = toLockRequiredPlatforms(params.requiredLock);
|
|
756
|
+
const sourceDiagnostics = resolveSkillImportSourcesWithDiagnostics({
|
|
757
|
+
repoRoot: params.repoRoot,
|
|
758
|
+
}).diagnostics;
|
|
737
759
|
|
|
738
760
|
if (params.evidenceResult.kind !== 'valid') {
|
|
761
|
+
const sourceViolations = toRequiredSkillSourceViolations(
|
|
762
|
+
sourceDiagnostics,
|
|
763
|
+
params.skillsEnforcement
|
|
764
|
+
);
|
|
739
765
|
return {
|
|
740
766
|
stage: params.stage,
|
|
741
|
-
enforced: requiredPlatforms.length > 0,
|
|
742
|
-
status:
|
|
767
|
+
enforced: requiredPlatforms.length > 0 || sourceDiagnostics.length > 0,
|
|
768
|
+
status:
|
|
769
|
+
requiredPlatforms.length > 0 || sourceDiagnostics.length > 0
|
|
770
|
+
? 'FAIL'
|
|
771
|
+
: 'NOT_APPLICABLE',
|
|
743
772
|
detected_platforms: [],
|
|
744
773
|
requirements: requiredPlatforms.map((platform) => ({
|
|
745
774
|
platform,
|
|
@@ -758,8 +787,10 @@ const toSkillsContractAssessment = (params: {
|
|
|
758
787
|
...PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform],
|
|
759
788
|
],
|
|
760
789
|
})),
|
|
761
|
-
|
|
762
|
-
|
|
790
|
+
source_diagnostics: sourceDiagnostics,
|
|
791
|
+
violations: [
|
|
792
|
+
...sourceViolations,
|
|
793
|
+
...(requiredPlatforms.length > 0
|
|
763
794
|
? [
|
|
764
795
|
toSkillsViolation(
|
|
765
796
|
params.skillsEnforcement,
|
|
@@ -767,7 +798,8 @@ const toSkillsContractAssessment = (params: {
|
|
|
767
798
|
`Required repo skills exist, but active platforms could not be detected for ${params.stage}.`
|
|
768
799
|
),
|
|
769
800
|
]
|
|
770
|
-
: [],
|
|
801
|
+
: []),
|
|
802
|
+
],
|
|
771
803
|
};
|
|
772
804
|
}
|
|
773
805
|
|
|
@@ -825,11 +857,15 @@ const toSkillsContractAssessment = (params: {
|
|
|
825
857
|
) {
|
|
826
858
|
return {
|
|
827
859
|
stage: params.stage,
|
|
828
|
-
enforced:
|
|
829
|
-
status: 'NOT_APPLICABLE',
|
|
860
|
+
enforced: sourceDiagnostics.length > 0,
|
|
861
|
+
status: sourceDiagnostics.length > 0 ? 'FAIL' : 'NOT_APPLICABLE',
|
|
830
862
|
detected_platforms: [],
|
|
831
863
|
requirements: [],
|
|
832
|
-
|
|
864
|
+
source_diagnostics: sourceDiagnostics,
|
|
865
|
+
violations: toRequiredSkillSourceViolations(
|
|
866
|
+
sourceDiagnostics,
|
|
867
|
+
params.skillsEnforcement
|
|
868
|
+
),
|
|
833
869
|
};
|
|
834
870
|
}
|
|
835
871
|
const requirements: AiGateSkillsContractPlatformRequirement[] = requiredPlatforms.map((platform) => ({
|
|
@@ -856,7 +892,12 @@ const toSkillsContractAssessment = (params: {
|
|
|
856
892
|
status: 'FAIL',
|
|
857
893
|
detected_platforms: [],
|
|
858
894
|
requirements,
|
|
895
|
+
source_diagnostics: sourceDiagnostics,
|
|
859
896
|
violations: [
|
|
897
|
+
...toRequiredSkillSourceViolations(
|
|
898
|
+
sourceDiagnostics,
|
|
899
|
+
params.skillsEnforcement
|
|
900
|
+
),
|
|
860
901
|
toSkillsViolation(
|
|
861
902
|
params.skillsEnforcement,
|
|
862
903
|
'EVIDENCE_SKILLS_PLATFORMS_UNDETECTED',
|
|
@@ -868,11 +909,15 @@ const toSkillsContractAssessment = (params: {
|
|
|
868
909
|
if (assessmentPlatforms.length === 0) {
|
|
869
910
|
return {
|
|
870
911
|
stage: params.stage,
|
|
871
|
-
enforced:
|
|
872
|
-
status: 'NOT_APPLICABLE',
|
|
912
|
+
enforced: sourceDiagnostics.length > 0,
|
|
913
|
+
status: sourceDiagnostics.length > 0 ? 'FAIL' : 'NOT_APPLICABLE',
|
|
873
914
|
detected_platforms: [],
|
|
874
915
|
requirements: [],
|
|
875
|
-
|
|
916
|
+
source_diagnostics: sourceDiagnostics,
|
|
917
|
+
violations: toRequiredSkillSourceViolations(
|
|
918
|
+
sourceDiagnostics,
|
|
919
|
+
params.skillsEnforcement
|
|
920
|
+
),
|
|
876
921
|
};
|
|
877
922
|
}
|
|
878
923
|
|
|
@@ -884,7 +929,10 @@ const toSkillsContractAssessment = (params: {
|
|
|
884
929
|
);
|
|
885
930
|
|
|
886
931
|
const requirements: AiGateSkillsContractPlatformRequirement[] = [];
|
|
887
|
-
const violations: AiGateViolation[] =
|
|
932
|
+
const violations: AiGateViolation[] = toRequiredSkillSourceViolations(
|
|
933
|
+
sourceDiagnostics,
|
|
934
|
+
params.skillsEnforcement
|
|
935
|
+
);
|
|
888
936
|
if (requiredPlatforms.length > 0 && detectedPlatforms.length === 0) {
|
|
889
937
|
violations.push(
|
|
890
938
|
toSkillsViolation(
|
|
@@ -995,6 +1043,7 @@ const toSkillsContractAssessment = (params: {
|
|
|
995
1043
|
status: violations.length === 0 ? 'PASS' : 'FAIL',
|
|
996
1044
|
detected_platforms: detectedPlatforms,
|
|
997
1045
|
requirements,
|
|
1046
|
+
source_diagnostics: sourceDiagnostics,
|
|
998
1047
|
violations,
|
|
999
1048
|
};
|
|
1000
1049
|
};
|
|
@@ -1596,6 +1645,11 @@ export const evaluateAiGate = (
|
|
|
1596
1645
|
|| (params.stage === 'PRE_WRITE' && requiredSkillsPlatforms.length === 0)
|
|
1597
1646
|
? []
|
|
1598
1647
|
: [
|
|
1648
|
+
...skillsContract.violations.filter(
|
|
1649
|
+
(violation) =>
|
|
1650
|
+
violation.code === 'SKILLS_REQUIRED_SOURCE_MISSING'
|
|
1651
|
+
|| violation.code === 'SKILLS_REQUIRED_SOURCE_UNREADABLE'
|
|
1652
|
+
),
|
|
1599
1653
|
toSkillsViolation(
|
|
1600
1654
|
skillsEnforcement,
|
|
1601
1655
|
'EVIDENCE_SKILLS_CONTRACT_INCOMPLETE',
|
|
@@ -152,6 +152,7 @@ export type ParsedArgs = {
|
|
|
152
152
|
sddEvidenceTestStatus?: SddEvidenceScaffoldTestStatus;
|
|
153
153
|
sddEvidenceTestOutput?: string;
|
|
154
154
|
sddEvidenceFromEvidence?: string;
|
|
155
|
+
sddEvidenceTraceabilityMarkdown?: string;
|
|
155
156
|
sddStateSyncDryRun?: boolean;
|
|
156
157
|
sddStateSyncScenarioId?: string;
|
|
157
158
|
sddStateSyncStatus?: SddStateSyncStatus;
|
|
@@ -208,7 +209,7 @@ Pumuki lifecycle commands:
|
|
|
208
209
|
pumuki sdd sync [--change=<change-id>] [--stage=PRE_WRITE|PRE_COMMIT|PRE_PUSH|CI] [--task=<task-id>] [--from-evidence=<path>] [--dry-run] [--json]
|
|
209
210
|
pumuki sdd learn --change=<change-id> [--stage=PRE_WRITE|PRE_COMMIT|PRE_PUSH|CI] [--task=<task-id>] [--from-evidence=<path>] [--dry-run] [--json]
|
|
210
211
|
pumuki sdd auto-sync --change=<change-id> [--stage=PRE_WRITE|PRE_COMMIT|PRE_PUSH|CI] [--task=<task-id>] [--from-evidence=<path>] [--dry-run] [--json]
|
|
211
|
-
pumuki sdd evidence --scenario-id=<id> --test-command=<command> --test-status=passed|failed [--test-output=<path>] [--from-evidence=<path>] [--dry-run] [--json]
|
|
212
|
+
pumuki sdd evidence --scenario-id=<id> --test-command=<command> --test-status=passed|failed [--test-output=<path>] [--from-evidence=<path>] [--traceability-markdown=<path>] [--dry-run] [--json]
|
|
212
213
|
pumuki sdd state-sync [--scenario-id=<id>] [--status=todo|in_progress|blocked|done] [--from-evidence=<path>] [--board-path=<path>] [--force] [--dry-run] [--json]
|
|
213
214
|
aliases de --stage: RED=PRE_WRITE, GREEN=PRE_COMMIT, REFACTOR=PRE_PUSH, CLOSE=CI
|
|
214
215
|
`.trim();
|
|
@@ -612,6 +613,7 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
|
|
|
612
613
|
let sddEvidenceTestStatus: ParsedArgs['sddEvidenceTestStatus'];
|
|
613
614
|
let sddEvidenceTestOutput: ParsedArgs['sddEvidenceTestOutput'];
|
|
614
615
|
let sddEvidenceFromEvidence: ParsedArgs['sddEvidenceFromEvidence'];
|
|
616
|
+
let sddEvidenceTraceabilityMarkdown: ParsedArgs['sddEvidenceTraceabilityMarkdown'];
|
|
615
617
|
let sddStateSyncDryRun = false;
|
|
616
618
|
let sddStateSyncScenarioId: ParsedArgs['sddStateSyncScenarioId'];
|
|
617
619
|
let sddStateSyncStatus: ParsedArgs['sddStateSyncStatus'];
|
|
@@ -1095,6 +1097,17 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
|
|
|
1095
1097
|
sddEvidenceTestOutput = testOutputPath;
|
|
1096
1098
|
continue;
|
|
1097
1099
|
}
|
|
1100
|
+
if (arg.startsWith('--traceability-markdown=')) {
|
|
1101
|
+
if (sddCommand !== 'evidence') {
|
|
1102
|
+
throw new Error(`--traceability-markdown is only supported with "pumuki sdd evidence".\n\n${HELP_TEXT}`);
|
|
1103
|
+
}
|
|
1104
|
+
const traceabilityMarkdownPath = arg.slice('--traceability-markdown='.length).trim();
|
|
1105
|
+
if (traceabilityMarkdownPath.length === 0) {
|
|
1106
|
+
throw new Error(`Invalid --traceability-markdown value "${arg}".`);
|
|
1107
|
+
}
|
|
1108
|
+
sddEvidenceTraceabilityMarkdown = traceabilityMarkdownPath;
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1098
1111
|
if (arg.startsWith('--from-evidence=')) {
|
|
1099
1112
|
if (sddCommand === 'sync-docs') {
|
|
1100
1113
|
sddSyncDocsFromEvidence = parseSddEvidencePath(
|
|
@@ -1255,7 +1268,7 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
|
|
|
1255
1268
|
sddAutoSyncChange
|
|
1256
1269
|
) {
|
|
1257
1270
|
throw new Error(
|
|
1258
|
-
`"pumuki sdd evidence" only supports --scenario-id=<id> --test-command=<command> --test-status=passed|failed [--test-output=<path>] [--from-evidence=<path>] [--dry-run] [--json].\n\n${HELP_TEXT}`
|
|
1271
|
+
`"pumuki sdd evidence" only supports --scenario-id=<id> --test-command=<command> --test-status=passed|failed [--test-output=<path>] [--from-evidence=<path>] [--traceability-markdown=<path>] [--dry-run] [--json].\n\n${HELP_TEXT}`
|
|
1259
1272
|
);
|
|
1260
1273
|
}
|
|
1261
1274
|
if (!sddEvidenceScenarioId) {
|
|
@@ -1278,6 +1291,7 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
|
|
|
1278
1291
|
sddEvidenceTestStatus,
|
|
1279
1292
|
...(sddEvidenceTestOutput ? { sddEvidenceTestOutput } : {}),
|
|
1280
1293
|
...(sddEvidenceFromEvidence ? { sddEvidenceFromEvidence } : {}),
|
|
1294
|
+
...(sddEvidenceTraceabilityMarkdown ? { sddEvidenceTraceabilityMarkdown } : {}),
|
|
1281
1295
|
};
|
|
1282
1296
|
}
|
|
1283
1297
|
if (sddCommand === 'state-sync') {
|
|
@@ -451,6 +451,7 @@ export const runSddCommand = async (parsed: ParsedArgs, activeDependencies: Life
|
|
|
451
451
|
testStatus: parsed.sddEvidenceTestStatus,
|
|
452
452
|
testOutputPath: parsed.sddEvidenceTestOutput,
|
|
453
453
|
fromEvidencePath: parsed.sddEvidenceFromEvidence,
|
|
454
|
+
traceabilityMarkdownPath: parsed.sddEvidenceTraceabilityMarkdown,
|
|
454
455
|
});
|
|
455
456
|
if (parsed.json) {
|
|
456
457
|
writeInfo(JSON.stringify(evidenceResult, null, 2));
|
|
@@ -4,6 +4,10 @@ import { readEvidenceResult } from '../evidence/readEvidence';
|
|
|
4
4
|
import { readRepoTrackingState } from '../evidence/trackingContract';
|
|
5
5
|
import type { RepoTrackingState } from '../evidence/schema';
|
|
6
6
|
import { loadRequiredSkillsLock } from '../config/skillsEffectiveLock';
|
|
7
|
+
import {
|
|
8
|
+
resolveSkillImportSourcesWithDiagnostics,
|
|
9
|
+
type SkillImportSourceDiagnostic,
|
|
10
|
+
} from '../config/skillsCustomRules';
|
|
7
11
|
import { readSddStatus } from '../sdd';
|
|
8
12
|
import type { SddStatusPayload } from '../sdd/types';
|
|
9
13
|
import type { LifecycleExperimentalFeaturesSnapshot } from './experimentalFeaturesSnapshot';
|
|
@@ -54,6 +58,7 @@ export type GovernanceSkillsContractSummary = {
|
|
|
54
58
|
status: 'PASS' | 'FAIL' | 'NOT_APPLICABLE';
|
|
55
59
|
detected_platforms: ReadonlyArray<GovernanceSkillsContractPlatform>;
|
|
56
60
|
requirements: ReadonlyArray<GovernanceSkillsContractPlatformRequirement>;
|
|
61
|
+
source_diagnostics: ReadonlyArray<SkillImportSourceDiagnostic>;
|
|
57
62
|
violations: ReadonlyArray<GovernanceSkillsContractViolation>;
|
|
58
63
|
};
|
|
59
64
|
|
|
@@ -228,15 +233,29 @@ const toCoverageDetectedPlatforms = (
|
|
|
228
233
|
|
|
229
234
|
const summarizeSkillsContract = (repoRoot: string): GovernanceSkillsContractSummary => {
|
|
230
235
|
const requiredPlatforms = toRequiredSkillsPlatforms(repoRoot);
|
|
236
|
+
const sourceDiagnostics = resolveSkillImportSourcesWithDiagnostics({ repoRoot }).diagnostics;
|
|
231
237
|
const evidenceResult = readEvidenceResult(repoRoot);
|
|
232
238
|
if (evidenceResult.kind !== 'valid') {
|
|
233
239
|
return {
|
|
234
240
|
stage: 'PRE_WRITE',
|
|
235
|
-
enforced:
|
|
236
|
-
status:
|
|
241
|
+
enforced: requiredPlatforms.length > 0 || sourceDiagnostics.length > 0,
|
|
242
|
+
status:
|
|
243
|
+
requiredPlatforms.length > 0 || sourceDiagnostics.length > 0
|
|
244
|
+
? 'FAIL'
|
|
245
|
+
: 'NOT_APPLICABLE',
|
|
237
246
|
detected_platforms: [],
|
|
238
247
|
requirements: [],
|
|
239
|
-
|
|
248
|
+
source_diagnostics: sourceDiagnostics,
|
|
249
|
+
violations: sourceDiagnostics.map((diagnostic) => ({
|
|
250
|
+
severity: 'ERROR',
|
|
251
|
+
code:
|
|
252
|
+
diagnostic.issue === 'missing'
|
|
253
|
+
? 'SKILLS_REQUIRED_SOURCE_MISSING'
|
|
254
|
+
: 'SKILLS_REQUIRED_SOURCE_UNREADABLE',
|
|
255
|
+
message:
|
|
256
|
+
`La skill requerida "${diagnostic.skillName}" no está disponible en ` +
|
|
257
|
+
`${diagnostic.sourcePath}. ${diagnostic.resolution}`,
|
|
258
|
+
})),
|
|
240
259
|
};
|
|
241
260
|
}
|
|
242
261
|
|
|
@@ -246,11 +265,21 @@ const summarizeSkillsContract = (repoRoot: string): GovernanceSkillsContractSumm
|
|
|
246
265
|
if (assessmentPlatforms.length === 0) {
|
|
247
266
|
return {
|
|
248
267
|
stage: 'PRE_WRITE',
|
|
249
|
-
enforced:
|
|
250
|
-
status: 'NOT_APPLICABLE',
|
|
268
|
+
enforced: sourceDiagnostics.length > 0,
|
|
269
|
+
status: sourceDiagnostics.length > 0 ? 'FAIL' : 'NOT_APPLICABLE',
|
|
251
270
|
detected_platforms: [],
|
|
252
271
|
requirements: [],
|
|
253
|
-
|
|
272
|
+
source_diagnostics: sourceDiagnostics,
|
|
273
|
+
violations: sourceDiagnostics.map((diagnostic) => ({
|
|
274
|
+
severity: 'ERROR',
|
|
275
|
+
code:
|
|
276
|
+
diagnostic.issue === 'missing'
|
|
277
|
+
? 'SKILLS_REQUIRED_SOURCE_MISSING'
|
|
278
|
+
: 'SKILLS_REQUIRED_SOURCE_UNREADABLE',
|
|
279
|
+
message:
|
|
280
|
+
`La skill requerida "${diagnostic.skillName}" no está disponible en ` +
|
|
281
|
+
`${diagnostic.sourcePath}. ${diagnostic.resolution}`,
|
|
282
|
+
})),
|
|
254
283
|
};
|
|
255
284
|
}
|
|
256
285
|
|
|
@@ -266,6 +295,18 @@ const summarizeSkillsContract = (repoRoot: string): GovernanceSkillsContractSumm
|
|
|
266
295
|
|
|
267
296
|
const requirements: GovernanceSkillsContractPlatformRequirement[] = [];
|
|
268
297
|
const violations: GovernanceSkillsContractViolation[] = [];
|
|
298
|
+
for (const diagnostic of sourceDiagnostics) {
|
|
299
|
+
violations.push({
|
|
300
|
+
severity: 'ERROR',
|
|
301
|
+
code:
|
|
302
|
+
diagnostic.issue === 'missing'
|
|
303
|
+
? 'SKILLS_REQUIRED_SOURCE_MISSING'
|
|
304
|
+
: 'SKILLS_REQUIRED_SOURCE_UNREADABLE',
|
|
305
|
+
message:
|
|
306
|
+
`La skill requerida "${diagnostic.skillName}" no está disponible en ` +
|
|
307
|
+
`${diagnostic.sourcePath}. ${diagnostic.resolution}`,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
269
310
|
for (const platform of assessmentPlatforms) {
|
|
270
311
|
const requiredRulePrefix = GOVERNANCE_SKILLS_RULE_PREFIXES[platform];
|
|
271
312
|
const requiredBundles = [...GOVERNANCE_REQUIRED_SKILLS_BUNDLES[platform]];
|
|
@@ -355,6 +396,7 @@ const summarizeSkillsContract = (repoRoot: string): GovernanceSkillsContractSumm
|
|
|
355
396
|
status: violations.length === 0 ? 'PASS' : 'FAIL',
|
|
356
397
|
detected_platforms: detectedPlatforms,
|
|
357
398
|
requirements,
|
|
399
|
+
source_diagnostics: sourceDiagnostics,
|
|
358
400
|
violations,
|
|
359
401
|
};
|
|
360
402
|
};
|
|
@@ -485,12 +527,16 @@ export const readGovernanceObservationSnapshot = (params: {
|
|
|
485
527
|
if (skillsContract.status === 'FAIL') {
|
|
486
528
|
attention.push('SKILLS_CONTRACT_INCOMPLETE');
|
|
487
529
|
}
|
|
530
|
+
if (skillsContract.source_diagnostics.length > 0) {
|
|
531
|
+
attention.push('SKILLS_REQUIRED_SOURCE_INVALID');
|
|
532
|
+
}
|
|
488
533
|
|
|
489
534
|
let governanceEffective: GovernanceObservationSnapshot['governance_effective'] = 'green';
|
|
490
535
|
if (
|
|
491
536
|
evidence.readable === 'invalid'
|
|
492
537
|
|| (evidence.readable === 'valid' && evidence.ai_gate_status === 'BLOCKED')
|
|
493
538
|
|| (evidence.readable === 'valid' && evidence.snapshot_outcome === 'BLOCK')
|
|
539
|
+
|| skillsContract.source_diagnostics.length > 0
|
|
494
540
|
) {
|
|
495
541
|
governanceEffective = 'blocked';
|
|
496
542
|
} else if (attention.length > 0) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { basename, dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
4
4
|
import { readEvidenceResult, type EvidenceReadResult } from '../evidence/readEvidence';
|
|
5
5
|
|
|
@@ -15,6 +15,7 @@ export type SddEvidenceScaffoldResult = {
|
|
|
15
15
|
testStatus: SddEvidenceScaffoldTestStatus;
|
|
16
16
|
testOutputPath: string | null;
|
|
17
17
|
fromEvidencePath: string | null;
|
|
18
|
+
traceabilityMarkdownPath: string | null;
|
|
18
19
|
};
|
|
19
20
|
output: {
|
|
20
21
|
path: string;
|
|
@@ -44,6 +45,13 @@ export type SddEvidenceScaffoldResult = {
|
|
|
44
45
|
source: 'pumuki-sdd-evidence';
|
|
45
46
|
stack: 'sdd-evidence-scaffold';
|
|
46
47
|
};
|
|
48
|
+
traceability: {
|
|
49
|
+
required: boolean;
|
|
50
|
+
path: string | null;
|
|
51
|
+
header_present: boolean;
|
|
52
|
+
rows: number;
|
|
53
|
+
status: 'valid';
|
|
54
|
+
};
|
|
47
55
|
// Legacy fields kept for state-sync compatibility in existing consumers.
|
|
48
56
|
scenario_id: string;
|
|
49
57
|
test_run: {
|
|
@@ -65,10 +73,13 @@ export type SddEvidenceScaffoldResult = {
|
|
|
65
73
|
const computeDigest = (value: string): string =>
|
|
66
74
|
`sha256:${createHash('sha256').update(value, 'utf8').digest('hex')}`;
|
|
67
75
|
|
|
76
|
+
const TRACEABILITY_HEADER = '| ARCHIVO | SKILL | REGLA | EVIDENCIA | ESTADO |';
|
|
77
|
+
const TRACEABILITY_CONTRACT_MARKER = 'ARCHIVO | SKILL | REGLA | EVIDENCIA | ESTADO';
|
|
78
|
+
|
|
68
79
|
const resolveRepoBoundPath = (params: {
|
|
69
80
|
repoRoot: string;
|
|
70
81
|
candidatePath: string;
|
|
71
|
-
flagName: '--from-evidence' | '--test-output';
|
|
82
|
+
flagName: '--from-evidence' | '--test-output' | '--traceability-markdown';
|
|
72
83
|
}): string => {
|
|
73
84
|
const repoRootAbsolute = resolve(params.repoRoot);
|
|
74
85
|
const resolved = isAbsolute(params.candidatePath)
|
|
@@ -141,6 +152,87 @@ const resolveScenarioReference = (scenarioId: string): string => {
|
|
|
141
152
|
return `${normalized}.feature`;
|
|
142
153
|
};
|
|
143
154
|
|
|
155
|
+
const repoRequiresTraceabilityMatrix = (repoRoot: string): boolean => {
|
|
156
|
+
const agentsPath = resolve(repoRoot, 'AGENTS.md');
|
|
157
|
+
if (!existsSync(agentsPath)) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
const contents = readFileSync(agentsPath, 'utf8');
|
|
161
|
+
return (
|
|
162
|
+
contents.includes('Plantilla obligatoria de trazabilidad por turno') ||
|
|
163
|
+
contents.includes(TRACEABILITY_CONTRACT_MARKER)
|
|
164
|
+
);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const validateTraceabilityMarkdown = (params: {
|
|
168
|
+
repoRoot: string;
|
|
169
|
+
required: boolean;
|
|
170
|
+
traceabilityMarkdownPath?: string;
|
|
171
|
+
}): {
|
|
172
|
+
path: string | null;
|
|
173
|
+
header_present: boolean;
|
|
174
|
+
rows: number;
|
|
175
|
+
} => {
|
|
176
|
+
if (!params.required) {
|
|
177
|
+
return {
|
|
178
|
+
path: params.traceabilityMarkdownPath?.trim() ? params.traceabilityMarkdownPath.trim() : null,
|
|
179
|
+
header_present: false,
|
|
180
|
+
rows: 0,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const candidate = params.traceabilityMarkdownPath?.trim() ?? '';
|
|
184
|
+
if (candidate.length === 0) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
`[pumuki][sdd] evidence requires --traceability-markdown=<path> because this repository declares the contractual traceability matrix (${TRACEABILITY_HEADER}).`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
const absolutePath = resolveRepoBoundPath({
|
|
190
|
+
repoRoot: params.repoRoot,
|
|
191
|
+
candidatePath: candidate,
|
|
192
|
+
flagName: '--traceability-markdown',
|
|
193
|
+
});
|
|
194
|
+
if (!existsSync(absolutePath)) {
|
|
195
|
+
throw new Error(
|
|
196
|
+
`[pumuki][sdd] traceability markdown file does not exist: ${candidate}. Add the contractual matrix and retry.`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
const markdown = readFileSync(absolutePath, 'utf8');
|
|
200
|
+
const lines = markdown.split(/\r?\n/);
|
|
201
|
+
const headerIndex = lines.findIndex((line) => line.trim() === TRACEABILITY_HEADER);
|
|
202
|
+
if (headerIndex < 0) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`[pumuki][sdd] traceability markdown must include the contractual header ${TRACEABILITY_HEADER}.`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
let rows = 0;
|
|
208
|
+
for (const line of lines.slice(headerIndex + 1)) {
|
|
209
|
+
const trimmed = line.trim();
|
|
210
|
+
if (!trimmed.startsWith('|')) {
|
|
211
|
+
if (rows > 0) {
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (/^\|\s*-+\s*\|/.test(trimmed)) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (trimmed === TRACEABILITY_HEADER) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
rows += 1;
|
|
223
|
+
}
|
|
224
|
+
if (rows === 0) {
|
|
225
|
+
throw new Error(
|
|
226
|
+
'[pumuki][sdd] traceability markdown must include at least one data row under the contractual matrix header.'
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
path: relative(params.repoRoot, absolutePath).split('\\').join('/'),
|
|
231
|
+
header_present: true,
|
|
232
|
+
rows,
|
|
233
|
+
};
|
|
234
|
+
};
|
|
235
|
+
|
|
144
236
|
export const runSddEvidenceScaffold = (params?: {
|
|
145
237
|
repoRoot?: string;
|
|
146
238
|
dryRun?: boolean;
|
|
@@ -149,6 +241,7 @@ export const runSddEvidenceScaffold = (params?: {
|
|
|
149
241
|
testStatus?: SddEvidenceScaffoldTestStatus;
|
|
150
242
|
testOutputPath?: string;
|
|
151
243
|
fromEvidencePath?: string;
|
|
244
|
+
traceabilityMarkdownPath?: string;
|
|
152
245
|
outputPath?: string;
|
|
153
246
|
now?: () => Date;
|
|
154
247
|
evidenceReader?: (repoRoot: string) => EvidenceReadResult;
|
|
@@ -195,6 +288,12 @@ export const runSddEvidenceScaffold = (params?: {
|
|
|
195
288
|
evidenceResult: evidenceReader(repoRoot),
|
|
196
289
|
fromEvidencePath,
|
|
197
290
|
});
|
|
291
|
+
const traceabilityRequired = repoRequiresTraceabilityMatrix(repoRoot);
|
|
292
|
+
const traceability = validateTraceabilityMarkdown({
|
|
293
|
+
repoRoot,
|
|
294
|
+
required: traceabilityRequired,
|
|
295
|
+
traceabilityMarkdownPath: params?.traceabilityMarkdownPath,
|
|
296
|
+
});
|
|
198
297
|
|
|
199
298
|
const now = params?.now ?? (() => new Date());
|
|
200
299
|
const generatedAt = now().toISOString();
|
|
@@ -232,6 +331,13 @@ export const runSddEvidenceScaffold = (params?: {
|
|
|
232
331
|
source: 'pumuki-sdd-evidence',
|
|
233
332
|
stack: 'sdd-evidence-scaffold',
|
|
234
333
|
},
|
|
334
|
+
traceability: {
|
|
335
|
+
required: traceabilityRequired,
|
|
336
|
+
path: traceability.path,
|
|
337
|
+
header_present: traceability.header_present,
|
|
338
|
+
rows: traceability.rows,
|
|
339
|
+
status: 'valid',
|
|
340
|
+
},
|
|
235
341
|
scenario_id: scenarioId,
|
|
236
342
|
test_run: {
|
|
237
343
|
command: testCommand,
|
|
@@ -267,6 +373,7 @@ export const runSddEvidenceScaffold = (params?: {
|
|
|
267
373
|
testStatus,
|
|
268
374
|
testOutputPath,
|
|
269
375
|
fromEvidencePath,
|
|
376
|
+
traceabilityMarkdownPath: traceability.path,
|
|
270
377
|
},
|
|
271
378
|
output: {
|
|
272
379
|
path: outputRelativePath,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.96",
|
|
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": {
|