pumuki 6.3.34 → 6.3.35
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/CONFIGURATION.md +95 -0
- package/docs/RELEASE_NOTES.md +31 -0
- package/docs/registro-maestro-de-seguimiento.md +2 -1
- package/docs/seguimiento-completo-validacion-ruralgo-03-03-2026.md +108 -4
- package/integrations/evidence/schema.ts +5 -0
- package/integrations/gate/degradedMode.ts +131 -0
- package/integrations/gate/stagePolicies.ts +12 -0
- package/integrations/git/EvidenceService.ts +9 -0
- package/integrations/git/runPlatformGate.ts +48 -0
- package/integrations/git/stageRunners.ts +6 -0
- package/integrations/lifecycle/cli.ts +127 -17
- package/integrations/sdd/index.ts +1 -1
- package/integrations/sdd/policy.ts +37 -0
- package/integrations/sdd/syncDocs.ts +259 -22
- package/integrations/sdd/types.ts +1 -0
- package/integrations/telemetry/gateTelemetry.ts +21 -0
- package/package.json +1 -1
package/docs/CONFIGURATION.md
CHANGED
|
@@ -97,6 +97,101 @@ Defined in `integrations/gate/stagePolicies.ts`:
|
|
|
97
97
|
- `PRE_PUSH`: block `ERROR`, warn from `WARN`
|
|
98
98
|
- `CI`: block `ERROR`, warn from `WARN`
|
|
99
99
|
|
|
100
|
+
## Degraded mode (offline / air-gapped)
|
|
101
|
+
|
|
102
|
+
Pumuki supports a deterministic degraded contract by stage:
|
|
103
|
+
|
|
104
|
+
- `PRE_WRITE`
|
|
105
|
+
- `PRE_COMMIT`
|
|
106
|
+
- `PRE_PUSH`
|
|
107
|
+
- `CI`
|
|
108
|
+
|
|
109
|
+
Resolution precedence:
|
|
110
|
+
|
|
111
|
+
1. Environment variables (`PUMUKI_DEGRADED_MODE=1`)
|
|
112
|
+
2. File contract (`.pumuki/degraded-mode.json`)
|
|
113
|
+
|
|
114
|
+
Environment variables:
|
|
115
|
+
|
|
116
|
+
- `PUMUKI_DEGRADED_MODE`: enable/disable (`1|0`, `true|false`, `yes|no`)
|
|
117
|
+
- `PUMUKI_DEGRADED_REASON`: operator-visible reason
|
|
118
|
+
- `PUMUKI_DEGRADED_ACTION`: global default action (`allow|block`)
|
|
119
|
+
- `PUMUKI_DEGRADED_ACTION_PRE_WRITE`: per-stage override
|
|
120
|
+
- `PUMUKI_DEGRADED_ACTION_PRE_COMMIT`: per-stage override
|
|
121
|
+
- `PUMUKI_DEGRADED_ACTION_PRE_PUSH`: per-stage override
|
|
122
|
+
- `PUMUKI_DEGRADED_ACTION_CI`: per-stage override
|
|
123
|
+
|
|
124
|
+
File contract (`.pumuki/degraded-mode.json`):
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{
|
|
128
|
+
"version": "1.0",
|
|
129
|
+
"enabled": true,
|
|
130
|
+
"reason": "offline-airgapped",
|
|
131
|
+
"stages": {
|
|
132
|
+
"PRE_WRITE": "block",
|
|
133
|
+
"PRE_COMMIT": "allow",
|
|
134
|
+
"PRE_PUSH": "block",
|
|
135
|
+
"CI": "block"
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Runtime behavior:
|
|
141
|
+
|
|
142
|
+
- `action=block`:
|
|
143
|
+
- gate adds finding `governance.degraded-mode.blocked`
|
|
144
|
+
- SDD returns `SDD_DEGRADED_MODE_BLOCKED`
|
|
145
|
+
- `action=allow`:
|
|
146
|
+
- gate adds informational finding `governance.degraded-mode.active`
|
|
147
|
+
- SDD returns `ALLOWED` with degraded metadata in `decision.details`
|
|
148
|
+
|
|
149
|
+
Traceability:
|
|
150
|
+
|
|
151
|
+
- `policyTrace.degraded` is emitted for gate stages.
|
|
152
|
+
- hook summaries include `degraded_mode`, `degraded_action`, and `degraded_reason` when active.
|
|
153
|
+
- evidence/telemetry include degraded metadata in policy trace when available.
|
|
154
|
+
|
|
155
|
+
## SDD sync-docs learning artifact
|
|
156
|
+
|
|
157
|
+
When `pumuki sdd sync-docs` runs with `--change=<change-id>`, the command emits a machine-readable learning payload.
|
|
158
|
+
|
|
159
|
+
Write path:
|
|
160
|
+
|
|
161
|
+
- `openspec/changes/<change-id>/learning.json`
|
|
162
|
+
|
|
163
|
+
Payload schema (`v1.0`):
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"version": "1.0",
|
|
168
|
+
"change_id": "rgo-1700-01",
|
|
169
|
+
"stage": "PRE_COMMIT",
|
|
170
|
+
"task": "P12.F2.T67",
|
|
171
|
+
"generated_at": "2026-03-04T10:05:00.000Z",
|
|
172
|
+
"failed_patterns": ["ai-gate.blocked"],
|
|
173
|
+
"successful_patterns": ["sync-docs.completed", "sync-docs.updated"],
|
|
174
|
+
"rule_updates": [
|
|
175
|
+
"ai-gate.unblock.required",
|
|
176
|
+
"ai-gate.violation.EVIDENCE_STALE.review"
|
|
177
|
+
],
|
|
178
|
+
"gate_anomalies": ["ai-gate.violation.EVIDENCE_STALE"],
|
|
179
|
+
"sync_docs": {
|
|
180
|
+
"updated": true,
|
|
181
|
+
"file_paths": [
|
|
182
|
+
"docs/technical/08-validation/refactor/pumuki-integration-feedback.md"
|
|
183
|
+
]
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Behavior:
|
|
189
|
+
|
|
190
|
+
- `--dry-run`: includes learning payload in JSON output with `learning.written=false` and does not write files.
|
|
191
|
+
- non dry-run: persists `learning.json` deterministically and reports digest/path in output.
|
|
192
|
+
- `rule_updates`: deterministic recommendations derived from evidence/gate signals (`missing`, `invalid`, `blocked`, `allowed`).
|
|
193
|
+
- dedicated command: `pumuki sdd learn --change=<id> [--stage=<stage>] [--task=<task>] [--dry-run] [--json]` generates/persists the same artifact without requiring `sync-docs`.
|
|
194
|
+
|
|
100
195
|
## Gate telemetry export (optional)
|
|
101
196
|
|
|
102
197
|
Structured telemetry output is disabled by default and can be enabled with environment variables:
|
package/docs/RELEASE_NOTES.md
CHANGED
|
@@ -5,6 +5,37 @@ Detailed commit history remains available through Git history (`git log` / `git
|
|
|
5
5
|
|
|
6
6
|
## 2026-03 (enterprise hardening updates)
|
|
7
7
|
|
|
8
|
+
### 2026-03-04 (v6.3.35)
|
|
9
|
+
|
|
10
|
+
- SDD enterprise incremental hardening shipped:
|
|
11
|
+
- New dedicated command `pumuki sdd learn` with `--change`, optional `--stage/--task`, `--dry-run`, and `--json`.
|
|
12
|
+
- `sync-docs` learning artifact now emits deterministic signal-derived `rule_updates`.
|
|
13
|
+
- Learning payload remains deterministic across missing/invalid/blocked/allowed evidence states.
|
|
14
|
+
- Traceability:
|
|
15
|
+
- implementation PRs: `#593`, `#596`, `#599`
|
|
16
|
+
- Consumer quick verification:
|
|
17
|
+
- `npx --yes --package pumuki@latest pumuki sdd learn --change=rgo-quickstart-01 --dry-run --json`
|
|
18
|
+
- `npx --yes --package pumuki@latest pumuki sdd sync-docs --change=rgo-quickstart-01 --stage=PRE_WRITE --task=P12.F2.T68 --dry-run --json`
|
|
19
|
+
- expected signal:
|
|
20
|
+
- `command=pumuki sdd learn` available in CLI help.
|
|
21
|
+
- `learning.artifact.rule_updates` populated deterministically when evidence is blocked/invalid.
|
|
22
|
+
|
|
23
|
+
### 2026-03-04 (v6.3.34)
|
|
24
|
+
|
|
25
|
+
- Telemetry hardening shipped for long-running enterprise repos:
|
|
26
|
+
- Gate telemetry JSONL supports deterministic size-guard rotation with `PUMUKI_TELEMETRY_JSONL_MAX_BYTES`.
|
|
27
|
+
- Enterprise contract suite now includes profile `telemetry-rotation` to validate JSONL rollover behavior.
|
|
28
|
+
- Traceability:
|
|
29
|
+
- implementation PRs: `#574`, `#577`
|
|
30
|
+
- release PR: `#580`
|
|
31
|
+
- Consumer quick verification:
|
|
32
|
+
- `npx --yes --package pumuki@latest pumuki doctor --json`
|
|
33
|
+
- `npm run -s validation:contract-suite:enterprise -- --json`
|
|
34
|
+
- `PUMUKI_TELEMETRY_JSONL_PATH=.pumuki/artifacts/gate-telemetry.jsonl PUMUKI_TELEMETRY_JSONL_MAX_BYTES=512 npx --yes --package pumuki@latest pumuki sdd validate --stage=PRE_WRITE --json`
|
|
35
|
+
- expected signal:
|
|
36
|
+
- contract suite list includes profile `telemetry-rotation`
|
|
37
|
+
- after repeated validations, files `gate-telemetry.jsonl` and `gate-telemetry.jsonl.1` are present
|
|
38
|
+
|
|
8
39
|
### 2026-03-04 (v6.3.33)
|
|
9
40
|
|
|
10
41
|
- Runtime hardening shipped for enterprise diagnosis:
|
|
@@ -7,7 +7,8 @@
|
|
|
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
|
-
-
|
|
10
|
+
- Última task cerrada (`✅`): `P12.F2.T68` (nuevo comando `pumuki sdd learn`, issue `#597`, PR `#599`).
|
|
11
|
+
- Task activa (`🚧`): `P12.F2.T69` (publicar release `6.3.35` con cierre SDD incremental en npm).
|
|
11
12
|
|
|
12
13
|
## Historial resumido
|
|
13
14
|
- No se mantienen MDs históricos de seguimiento en este repositorio.
|
|
@@ -1838,11 +1838,115 @@ Criterio de salida F5:
|
|
|
1838
1838
|
- `npm run -s validation:tracking-single-active`
|
|
1839
1839
|
- `docs/validation/README.md` actualizado con perfiles activos de la suite contractual.
|
|
1840
1840
|
|
|
1841
|
-
-
|
|
1841
|
+
- ✅ `P12.F2.T60` Publicar patch release con mejoras de telemetría (`#574`, `#577`) para disponibilidad inmediata en npm.
|
|
1842
|
+
- cierre ejecutado:
|
|
1843
|
+
- issue de release creada y cerrada: `#578`.
|
|
1844
|
+
- rama release creada y mergeada: `release/6.3.34` -> `#580` (`https://github.com/SwiftEnProfundidad/ast-intelligence-hooks/pull/580`).
|
|
1845
|
+
- publicación npm completada: `pumuki@6.3.34`.
|
|
1846
|
+
- evidencia:
|
|
1847
|
+
- `npm run -s validation:tracking-single-active`
|
|
1848
|
+
- `npm view pumuki version` => `6.3.34`
|
|
1849
|
+
- `gh issue close 578 --comment "Closed via PR #580 and npm publish 6.3.34 (telemetry rotation + contract profile telemetry-rotation)."`
|
|
1850
|
+
|
|
1851
|
+
- ✅ `P12.F2.T61` Publicar documentación de adopción de `6.3.34` (release notes + verificación operativa rápida).
|
|
1852
|
+
- cierre ejecutado:
|
|
1853
|
+
- issue documental creada y cerrada: `#581`.
|
|
1854
|
+
- `docs/RELEASE_NOTES.md` actualizado con entrada `2026-03-04 (v6.3.34)` y comandos de verificación operativa.
|
|
1855
|
+
- trazabilidad sincronizada en plan activo + registro maestro.
|
|
1856
|
+
- evidencia:
|
|
1857
|
+
- `npm run -s validation:tracking-single-active`
|
|
1858
|
+
- `gh issue close 581 --comment "Closed via release notes update for 6.3.34 and tracking sync."`
|
|
1859
|
+
|
|
1860
|
+
- ✅ `P12.F2.T62` Ejecutar la siguiente mejora estratégica pendiente: modo degradado offline/air-gapped para gates enterprise (`#583`).
|
|
1861
|
+
- cierre ejecutado:
|
|
1862
|
+
- contrato reutilizable implementado en `integrations/gate/degradedMode.ts` con precedencia `env -> .pumuki/degraded-mode.json`.
|
|
1863
|
+
- `resolvePolicyForStage` expone `trace.degraded` por stage.
|
|
1864
|
+
- `runPlatformGate` aplica enforcement:
|
|
1865
|
+
- `action=block` -> finding `governance.degraded-mode.blocked` + gate `BLOCK`.
|
|
1866
|
+
- `action=allow` -> finding `governance.degraded-mode.active` + gate permite continuar.
|
|
1867
|
+
- hooks exitosos publican resumen degradado (`degraded_mode`, `degraded_action`, `degraded_reason`).
|
|
1868
|
+
- SDD integra el contrato:
|
|
1869
|
+
- `action=block` -> `SDD_DEGRADED_MODE_BLOCKED`.
|
|
1870
|
+
- `action=allow` -> `ALLOWED` con metadata degradada en `decision.details`.
|
|
1871
|
+
- documentación operativa actualizada en `docs/CONFIGURATION.md`.
|
|
1872
|
+
- evidencia:
|
|
1873
|
+
- `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/sdd/__tests__/policy.test.ts` => `58 passed, 0 failed`.
|
|
1874
|
+
- `npx --yes tsx@4.21.0 --test integrations/telemetry/__tests__/gateTelemetry.test.ts` => `4 passed`.
|
|
1875
|
+
- `npx --yes tsx@4.21.0 --test integrations/git/__tests__/EvidenceService.test.ts` => `15 passed`.
|
|
1876
|
+
|
|
1877
|
+
- ✅ `P12.F2.T63` Iniciar SDD pendiente enterprise: extender `pumuki sdd sync-docs` con contexto explícito (`--change`, `--stage`, `--task`) y trazabilidad canónica (`#585`).
|
|
1878
|
+
- cierre ejecutado:
|
|
1879
|
+
- `runSddSyncDocs` amplía contrato con contexto explícito (`change`, `stage`, `task`) en salida canónica.
|
|
1880
|
+
- `parseLifecycleCliArgs` soporta:
|
|
1881
|
+
- `pumuki sdd sync-docs --change=<id> --stage=<stage> --task=<task-id> [--dry-run] [--json]`.
|
|
1882
|
+
- `runLifecycleCli` propaga contexto al runtime y lo imprime en salida texto/json.
|
|
1883
|
+
- cobertura de tests ampliada para parsing + ejecución `dry-run` + contrato JSON de contexto.
|
|
1884
|
+
- evidencia:
|
|
1885
|
+
- `npx --yes tsx@4.21.0 --test integrations/sdd/__tests__/syncDocs.test.ts integrations/lifecycle/__tests__/cli.test.ts` => `31 passed, 0 failed`.
|
|
1886
|
+
|
|
1887
|
+
- ✅ `P12.F2.T64` Continuar SDD pendiente enterprise: ampliar `sync-docs` a múltiples documentos canónicos y secciones gestionadas (`#587`).
|
|
1888
|
+
- cierre ejecutado:
|
|
1889
|
+
- `runSddSyncDocs` soporta objetivos múltiples (`targets`) con secciones gestionadas por archivo.
|
|
1890
|
+
- actualización determinista por archivo/sección y salida con diffs por cada target.
|
|
1891
|
+
- fail-safe preservado: conflicto en cualquier archivo aborta el sync antes de cualquier escritura parcial.
|
|
1892
|
+
- cobertura de tests ampliada para:
|
|
1893
|
+
- actualización multi-documento.
|
|
1894
|
+
- garantía fail-safe sin escrituras parciales.
|
|
1895
|
+
- evidencia:
|
|
1896
|
+
- `npx --yes tsx@4.21.0 --test integrations/sdd/__tests__/syncDocs.test.ts integrations/lifecycle/__tests__/cli.test.ts` => `33 passed, 0 failed`.
|
|
1897
|
+
|
|
1898
|
+
- ✅ `P12.F2.T65` Continuar SDD pendiente enterprise: artefacto de aprendizaje machine-readable por change durante `sync-docs` (`#589`).
|
|
1899
|
+
- cierre ejecutado:
|
|
1900
|
+
- `runSddSyncDocs` incorpora contrato `learning` en salida con:
|
|
1901
|
+
- `path`, `written`, `digest`, `artifact`.
|
|
1902
|
+
- cuando se provee `--change`:
|
|
1903
|
+
- `--dry-run`: no escribe archivo y retorna payload de aprendizaje.
|
|
1904
|
+
- ejecución normal: persiste `openspec/changes/<change>/learning.json`.
|
|
1905
|
+
- cobertura de tests ampliada para dry-run y persistencia real del artefacto.
|
|
1906
|
+
- documentación mínima del esquema añadida en `docs/CONFIGURATION.md`.
|
|
1907
|
+
- evidencia:
|
|
1908
|
+
- `npx --yes tsx@4.21.0 --test integrations/sdd/__tests__/syncDocs.test.ts integrations/lifecycle/__tests__/cli.test.ts` => `34 passed, 0 failed`.
|
|
1909
|
+
|
|
1910
|
+
- ✅ `P12.F2.T66` Continuar SDD pendiente enterprise: enriquecer `learning.json` con señales reales de gate/evidence (`#591`).
|
|
1911
|
+
- cierre ejecutado:
|
|
1912
|
+
- `runSddSyncDocs` incorpora señales deterministas de runtime para poblar:
|
|
1913
|
+
- `failed_patterns`
|
|
1914
|
+
- `successful_patterns`
|
|
1915
|
+
- `gate_anomalies`
|
|
1916
|
+
- el cálculo cubre casos `evidence missing/invalid/valid` y decisión SDD permitida/bloqueada.
|
|
1917
|
+
- soporte explícito para inyectar lector de evidencia en tests (`evidenceReader`) sin romper contrato CLI.
|
|
1918
|
+
- PR mergeada: `#593` (`commit 0da619a3804ec939bc33385cfc57032c195b4ee1`).
|
|
1919
|
+
- evidencia:
|
|
1920
|
+
- `npx --yes tsx@4.21.0 --test integrations/sdd/__tests__/syncDocs.test.ts integrations/lifecycle/__tests__/cli.test.ts` => `35 passed, 0 failed`.
|
|
1921
|
+
- `npm run -s typecheck` => `exit 0`.
|
|
1922
|
+
|
|
1923
|
+
- ✅ `P12.F2.T67` Continuar SDD pendiente enterprise: hacer `rule_updates` accionable en `learning.json` (`#594`).
|
|
1924
|
+
- cierre ejecutado:
|
|
1925
|
+
- `runSddSyncDocs` ahora deriva `rule_updates` de señales de evidencia/gate (`missing`, `invalid`, `blocked`, `allowed`) de forma determinista.
|
|
1926
|
+
- añadidas recomendaciones específicas por familia de señal (`evidence.*`, `ai-gate.*`, `sdd.*`, `snapshot.*`).
|
|
1927
|
+
- ampliada cobertura en `syncDocs` para escenarios `invalid` y `allow` además de `blocked`/persistencia.
|
|
1928
|
+
- documentación de contrato actualizada en `docs/CONFIGURATION.md`.
|
|
1929
|
+
- PR mergeada: `#596` (`commit 51d894c8835c9b04cb57dd810197ac7fc3d0cd3e`).
|
|
1930
|
+
- evidencia:
|
|
1931
|
+
- `npx --yes tsx@4.21.0 --test integrations/sdd/__tests__/syncDocs.test.ts integrations/lifecycle/__tests__/cli.test.ts` => `37 passed, 0 failed`.
|
|
1932
|
+
- `npm run -s typecheck` => `exit 0`.
|
|
1933
|
+
|
|
1934
|
+
- ✅ `P12.F2.T68` Continuar SDD pendiente enterprise: añadir comando dedicado `pumuki sdd learn` (`#597`).
|
|
1935
|
+
- cierre ejecutado:
|
|
1936
|
+
- nuevo runtime `runSddLearn` en capa SDD con salida/persistencia de `learning.json` sin depender de `sync-docs`.
|
|
1937
|
+
- CLI ampliada con:
|
|
1938
|
+
- `pumuki sdd learn --change=<id> --stage=<stage> --task=<task-id> [--dry-run] [--json]`.
|
|
1939
|
+
- cobertura de regresión añadida en parser y ejecución (`cli.test.ts`) y documentación actualizada en `docs/CONFIGURATION.md`.
|
|
1940
|
+
- PR mergeada: `#599` (`commit c971c643883ccce679c0c5cdb3363bdd6e6cace6`).
|
|
1941
|
+
- evidencia:
|
|
1942
|
+
- `npx --yes tsx@4.21.0 --test integrations/sdd/__tests__/syncDocs.test.ts integrations/lifecycle/__tests__/cli.test.ts` => `38 passed, 0 failed`.
|
|
1943
|
+
- `npm run -s typecheck` => `exit 0`.
|
|
1944
|
+
|
|
1945
|
+
- 🚧 `P12.F2.T69` Publicar release `6.3.35` con cierre SDD incremental.
|
|
1842
1946
|
- salida esperada:
|
|
1843
|
-
-
|
|
1844
|
-
-
|
|
1845
|
-
-
|
|
1947
|
+
- bump de versión (`package.json`, `package-lock.json`) + nota de release.
|
|
1948
|
+
- publicación npm exitosa y verificación `npm view pumuki version`.
|
|
1949
|
+
- smoke mínimo con `npx --yes --package pumuki@latest pumuki --help`.
|
|
1846
1950
|
|
|
1847
1951
|
Criterio de salida F6:
|
|
1848
1952
|
- veredicto final trazable y cierre administrativo completo.
|
|
@@ -91,6 +91,11 @@ export type RulesetState = {
|
|
|
91
91
|
source?: string;
|
|
92
92
|
validation_status?: 'valid' | 'invalid' | 'expired' | 'unknown-source';
|
|
93
93
|
validation_code?: string;
|
|
94
|
+
degraded_mode_enabled?: boolean;
|
|
95
|
+
degraded_mode_action?: 'allow' | 'block';
|
|
96
|
+
degraded_mode_reason?: string;
|
|
97
|
+
degraded_mode_source?: 'env' | 'file:.pumuki/degraded-mode.json';
|
|
98
|
+
degraded_mode_code?: 'DEGRADED_MODE_ALLOWED' | 'DEGRADED_MODE_BLOCKED';
|
|
94
99
|
};
|
|
95
100
|
|
|
96
101
|
export type HumanIntentConfidence = 'high' | 'medium' | 'low' | 'unset';
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export type DegradedStage = 'PRE_WRITE' | 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
|
|
5
|
+
export type DegradedAction = 'allow' | 'block';
|
|
6
|
+
export type DegradedCode = 'DEGRADED_MODE_ALLOWED' | 'DEGRADED_MODE_BLOCKED';
|
|
7
|
+
|
|
8
|
+
export type DegradedResolution = {
|
|
9
|
+
enabled: true;
|
|
10
|
+
action: DegradedAction;
|
|
11
|
+
reason: string;
|
|
12
|
+
source: 'env' | 'file:.pumuki/degraded-mode.json';
|
|
13
|
+
code: DegradedCode;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const DEGRADED_MODE_FILE_PATH = '.pumuki/degraded-mode.json';
|
|
17
|
+
|
|
18
|
+
const toBooleanFlag = (value: string | undefined): boolean | null => {
|
|
19
|
+
const normalized = value?.trim().toLowerCase();
|
|
20
|
+
if (!normalized) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
if (normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on') {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
if (normalized === '0' || normalized === 'false' || normalized === 'no' || normalized === 'off') {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const toAction = (value: unknown): DegradedAction | null => {
|
|
33
|
+
if (typeof value !== 'string') {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const normalized = value.trim().toLowerCase();
|
|
37
|
+
if (normalized === 'allow') {
|
|
38
|
+
return 'allow';
|
|
39
|
+
}
|
|
40
|
+
if (normalized === 'block') {
|
|
41
|
+
return 'block';
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const toCode = (action: DegradedAction): DegradedCode => {
|
|
47
|
+
return action === 'block' ? 'DEGRADED_MODE_BLOCKED' : 'DEGRADED_MODE_ALLOWED';
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const resolveActionFromEnv = (stage: DegradedStage): DegradedAction => {
|
|
51
|
+
const stageKey = `PUMUKI_DEGRADED_ACTION_${stage}`;
|
|
52
|
+
const byStage = toAction(process.env[stageKey]);
|
|
53
|
+
if (byStage) {
|
|
54
|
+
return byStage;
|
|
55
|
+
}
|
|
56
|
+
const globalAction = toAction(process.env.PUMUKI_DEGRADED_ACTION);
|
|
57
|
+
if (globalAction) {
|
|
58
|
+
return globalAction;
|
|
59
|
+
}
|
|
60
|
+
return 'allow';
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type DegradedModeFile = {
|
|
64
|
+
version?: unknown;
|
|
65
|
+
enabled?: unknown;
|
|
66
|
+
reason?: unknown;
|
|
67
|
+
action?: unknown;
|
|
68
|
+
stages?: unknown;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const isRecord = (value: unknown): value is Record<string, unknown> => {
|
|
72
|
+
return typeof value === 'object' && value !== null;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const resolveFromFile = (
|
|
76
|
+
repoRoot: string,
|
|
77
|
+
stage: DegradedStage
|
|
78
|
+
): DegradedResolution | undefined => {
|
|
79
|
+
const configPath = join(repoRoot, DEGRADED_MODE_FILE_PATH);
|
|
80
|
+
if (!existsSync(configPath)) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const parsed = JSON.parse(readFileSync(configPath, 'utf8')) as DegradedModeFile;
|
|
85
|
+
if (parsed.enabled !== true) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
const stageActions = isRecord(parsed.stages) ? parsed.stages : undefined;
|
|
89
|
+
const stageAction = stageActions ? toAction(stageActions[stage]) : null;
|
|
90
|
+
const globalAction = toAction(parsed.action);
|
|
91
|
+
const action = stageAction ?? globalAction ?? 'allow';
|
|
92
|
+
const reason =
|
|
93
|
+
typeof parsed.reason === 'string' && parsed.reason.trim().length > 0
|
|
94
|
+
? parsed.reason.trim()
|
|
95
|
+
: 'degraded-mode';
|
|
96
|
+
return {
|
|
97
|
+
enabled: true,
|
|
98
|
+
action,
|
|
99
|
+
reason,
|
|
100
|
+
source: 'file:.pumuki/degraded-mode.json',
|
|
101
|
+
code: toCode(action),
|
|
102
|
+
};
|
|
103
|
+
} catch {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export const resolveDegradedMode = (
|
|
109
|
+
stage: DegradedStage,
|
|
110
|
+
repoRoot: string = process.cwd()
|
|
111
|
+
): DegradedResolution | undefined => {
|
|
112
|
+
const enabledFromEnv = toBooleanFlag(process.env.PUMUKI_DEGRADED_MODE);
|
|
113
|
+
if (enabledFromEnv === true) {
|
|
114
|
+
const action = resolveActionFromEnv(stage);
|
|
115
|
+
const reason =
|
|
116
|
+
process.env.PUMUKI_DEGRADED_REASON?.trim().length
|
|
117
|
+
? process.env.PUMUKI_DEGRADED_REASON.trim()
|
|
118
|
+
: 'degraded-mode';
|
|
119
|
+
return {
|
|
120
|
+
enabled: true,
|
|
121
|
+
action,
|
|
122
|
+
reason,
|
|
123
|
+
source: 'env',
|
|
124
|
+
code: toCode(action),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (enabledFromEnv === false) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
return resolveFromFile(repoRoot, stage);
|
|
131
|
+
};
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
loadSkillsPolicy,
|
|
11
11
|
} from '../config/skillsPolicy';
|
|
12
12
|
import type { SkillsStage } from '../config/skillsLock';
|
|
13
|
+
import { resolveDegradedMode } from './degradedMode';
|
|
13
14
|
|
|
14
15
|
const heuristicsPromotionStageAllowList = new Set<GateStage>([
|
|
15
16
|
'PRE_COMMIT',
|
|
@@ -58,6 +59,13 @@ export type ResolvedStagePolicy = {
|
|
|
58
59
|
message: string;
|
|
59
60
|
strict: boolean;
|
|
60
61
|
};
|
|
62
|
+
degraded?: {
|
|
63
|
+
enabled: true;
|
|
64
|
+
action: 'allow' | 'block';
|
|
65
|
+
reason: string;
|
|
66
|
+
source: 'env' | 'file:.pumuki/degraded-mode.json';
|
|
67
|
+
code: 'DEGRADED_MODE_ALLOWED' | 'DEGRADED_MODE_BLOCKED';
|
|
68
|
+
};
|
|
61
69
|
};
|
|
62
70
|
};
|
|
63
71
|
|
|
@@ -519,6 +527,7 @@ export const resolvePolicyForStage = (
|
|
|
519
527
|
stage: SkillsStage,
|
|
520
528
|
repoRoot: string = process.cwd()
|
|
521
529
|
): ResolvedStagePolicy => {
|
|
530
|
+
const degraded = resolveDegradedMode(stage, repoRoot);
|
|
522
531
|
const hardModeState = resolveHardModeState(repoRoot);
|
|
523
532
|
if (hardModeState.enabled) {
|
|
524
533
|
const profileName = hardModeState.profileName;
|
|
@@ -553,6 +562,7 @@ export const resolvePolicyForStage = (
|
|
|
553
562
|
signature: policyAsCode.signature,
|
|
554
563
|
policySource: policyAsCode.policySource,
|
|
555
564
|
validation: policyAsCode.validation,
|
|
565
|
+
...(degraded ? { degraded } : {}),
|
|
556
566
|
},
|
|
557
567
|
};
|
|
558
568
|
}
|
|
@@ -586,6 +596,7 @@ export const resolvePolicyForStage = (
|
|
|
586
596
|
signature: policyAsCode.signature,
|
|
587
597
|
policySource: policyAsCode.policySource,
|
|
588
598
|
validation: policyAsCode.validation,
|
|
599
|
+
...(degraded ? { degraded } : {}),
|
|
589
600
|
},
|
|
590
601
|
};
|
|
591
602
|
}
|
|
@@ -621,6 +632,7 @@ export const resolvePolicyForStage = (
|
|
|
621
632
|
signature: policyAsCode.signature,
|
|
622
633
|
policySource: policyAsCode.policySource,
|
|
623
634
|
validation: policyAsCode.validation,
|
|
635
|
+
...(degraded ? { degraded } : {}),
|
|
624
636
|
},
|
|
625
637
|
};
|
|
626
638
|
};
|
|
@@ -112,6 +112,15 @@ export class EvidenceService implements IEvidenceService {
|
|
|
112
112
|
validation_code: params.policyTrace.validation.code,
|
|
113
113
|
}
|
|
114
114
|
: {}),
|
|
115
|
+
...(params.policyTrace.degraded
|
|
116
|
+
? {
|
|
117
|
+
degraded_mode_enabled: params.policyTrace.degraded.enabled,
|
|
118
|
+
degraded_mode_action: params.policyTrace.degraded.action,
|
|
119
|
+
degraded_mode_reason: params.policyTrace.degraded.reason,
|
|
120
|
+
degraded_mode_source: params.policyTrace.degraded.source,
|
|
121
|
+
degraded_mode_code: params.policyTrace.degraded.code,
|
|
122
|
+
}
|
|
123
|
+
: {}),
|
|
115
124
|
});
|
|
116
125
|
}
|
|
117
126
|
|
|
@@ -366,6 +366,40 @@ const toPolicyAsCodeBlockingFinding = (params: {
|
|
|
366
366
|
};
|
|
367
367
|
};
|
|
368
368
|
|
|
369
|
+
const toDegradedModeFinding = (params: {
|
|
370
|
+
stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
|
|
371
|
+
policyTrace?: ResolvedStagePolicy['trace'];
|
|
372
|
+
}): Finding | undefined => {
|
|
373
|
+
const degraded = params.policyTrace?.degraded;
|
|
374
|
+
if (!degraded?.enabled) {
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
if (degraded.action === 'block') {
|
|
378
|
+
return {
|
|
379
|
+
ruleId: 'governance.degraded-mode.blocked',
|
|
380
|
+
severity: 'ERROR',
|
|
381
|
+
code: degraded.code,
|
|
382
|
+
message:
|
|
383
|
+
`Degraded mode is active at ${params.stage} with fail-closed action=block. ` +
|
|
384
|
+
`reason=${degraded.reason} source=${degraded.source}.`,
|
|
385
|
+
filePath: '.pumuki/degraded-mode.json',
|
|
386
|
+
matchedBy: 'DegradedModeGuard',
|
|
387
|
+
source: 'degraded-mode',
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
ruleId: 'governance.degraded-mode.active',
|
|
392
|
+
severity: 'INFO',
|
|
393
|
+
code: degraded.code,
|
|
394
|
+
message:
|
|
395
|
+
`Degraded mode is active at ${params.stage} with fail-open action=allow. ` +
|
|
396
|
+
`reason=${degraded.reason} source=${degraded.source}.`,
|
|
397
|
+
filePath: '.pumuki/degraded-mode.json',
|
|
398
|
+
matchedBy: 'DegradedModeGuard',
|
|
399
|
+
source: 'degraded-mode',
|
|
400
|
+
};
|
|
401
|
+
};
|
|
402
|
+
|
|
369
403
|
const toGateWaiverAppliedFinding = (params: {
|
|
370
404
|
stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
|
|
371
405
|
waiver: Extract<GateWaiverResult, { kind: 'applied' }>['waiver'];
|
|
@@ -621,6 +655,16 @@ export async function runPlatformGate(params: {
|
|
|
621
655
|
policyTrace: params.policyTrace,
|
|
622
656
|
})
|
|
623
657
|
: undefined;
|
|
658
|
+
const degradedModeFinding =
|
|
659
|
+
params.policy.stage === 'PRE_COMMIT' ||
|
|
660
|
+
params.policy.stage === 'PRE_PUSH' ||
|
|
661
|
+
params.policy.stage === 'CI'
|
|
662
|
+
? toDegradedModeFinding({
|
|
663
|
+
stage: params.policy.stage,
|
|
664
|
+
policyTrace: params.policyTrace,
|
|
665
|
+
})
|
|
666
|
+
: undefined;
|
|
667
|
+
const degradedModeBlocks = params.policyTrace?.degraded?.action === 'block';
|
|
624
668
|
const rulesCoverage = coverage
|
|
625
669
|
? {
|
|
626
670
|
stage: params.policy.stage,
|
|
@@ -665,6 +709,7 @@ export async function runPlatformGate(params: {
|
|
|
665
709
|
const effectiveFindings = sddBlockingFinding
|
|
666
710
|
? [
|
|
667
711
|
sddBlockingFinding,
|
|
712
|
+
...(degradedModeFinding ? [degradedModeFinding] : []),
|
|
668
713
|
...(policyAsCodeBlockingFinding ? [policyAsCodeBlockingFinding] : []),
|
|
669
714
|
...(unsupportedSkillsMappingFinding ? [unsupportedSkillsMappingFinding] : []),
|
|
670
715
|
...(platformSkillsCoverageFinding ? [platformSkillsCoverageFinding] : []),
|
|
@@ -678,8 +723,10 @@ export async function runPlatformGate(params: {
|
|
|
678
723
|
|| skillsScopeComplianceFinding
|
|
679
724
|
|| coverageBlockingFinding
|
|
680
725
|
|| policyAsCodeBlockingFinding
|
|
726
|
+
|| degradedModeFinding
|
|
681
727
|
|| tddBddEvaluation.findings.length > 0
|
|
682
728
|
? [
|
|
729
|
+
...(degradedModeFinding ? [degradedModeFinding] : []),
|
|
683
730
|
...(policyAsCodeBlockingFinding ? [policyAsCodeBlockingFinding] : []),
|
|
684
731
|
...(unsupportedSkillsMappingFinding ? [unsupportedSkillsMappingFinding] : []),
|
|
685
732
|
...(platformSkillsCoverageFinding ? [platformSkillsCoverageFinding] : []),
|
|
@@ -692,6 +739,7 @@ export async function runPlatformGate(params: {
|
|
|
692
739
|
const decision = dependencies.evaluateGate([...effectiveFindings], params.policy);
|
|
693
740
|
const baseGateOutcome =
|
|
694
741
|
sddBlockingFinding ||
|
|
742
|
+
degradedModeBlocks ||
|
|
695
743
|
policyAsCodeBlockingFinding ||
|
|
696
744
|
unsupportedSkillsMappingFinding ||
|
|
697
745
|
platformSkillsCoverageFinding ||
|
|
@@ -120,11 +120,17 @@ const emitSuccessfulHookGateSummary = (params: {
|
|
|
120
120
|
params.stage === 'PRE_COMMIT'
|
|
121
121
|
? PRE_COMMIT_EVIDENCE_MAX_AGE_SECONDS
|
|
122
122
|
: PRE_PUSH_EVIDENCE_MAX_AGE_SECONDS;
|
|
123
|
+
const degradedSummary =
|
|
124
|
+
params.policyTrace.degraded?.enabled
|
|
125
|
+
? ` degraded_mode=enabled degraded_action=${params.policyTrace.degraded.action}` +
|
|
126
|
+
` degraded_reason=${params.policyTrace.degraded.reason}`
|
|
127
|
+
: '';
|
|
123
128
|
params.dependencies.writeHookGateSummary(
|
|
124
129
|
`[pumuki][hook-gate] stage=${params.stage} policy_bundle=${params.policyTrace.bundle} policy_hash=${params.policyTrace.hash}` +
|
|
125
130
|
` policy_version=${params.policyTrace.version ?? 'n/a'}` +
|
|
126
131
|
` policy_signature=${params.policyTrace.signature ?? 'n/a'}` +
|
|
127
132
|
` policy_source=${params.policyTrace.policySource ?? 'n/a'}` +
|
|
133
|
+
`${degradedSummary}` +
|
|
128
134
|
` decision=ALLOW evidence_kind=${evidence.kind} evidence_age_seconds=${evidenceAgeSeconds ?? 'n/a'} max_age_seconds=${maxAgeSeconds}`
|
|
129
135
|
);
|
|
130
136
|
};
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
openSddSession,
|
|
28
28
|
readSddStatus,
|
|
29
29
|
refreshSddSession,
|
|
30
|
+
runSddLearn,
|
|
30
31
|
runSddSyncDocs,
|
|
31
32
|
type SddStage,
|
|
32
33
|
} from '../sdd';
|
|
@@ -62,7 +63,7 @@ type LifecycleCommand =
|
|
|
62
63
|
| 'adapter'
|
|
63
64
|
| 'analytics';
|
|
64
65
|
|
|
65
|
-
type SddCommand = 'status' | 'validate' | 'session' | 'sync-docs';
|
|
66
|
+
type SddCommand = 'status' | 'validate' | 'session' | 'sync-docs' | 'learn';
|
|
66
67
|
type LoopCommand = 'run' | 'status' | 'stop' | 'resume' | 'list' | 'export';
|
|
67
68
|
type AnalyticsCommand = 'hotspots';
|
|
68
69
|
type AnalyticsHotspotsCommand = 'report' | 'diagnose';
|
|
@@ -87,6 +88,13 @@ type ParsedArgs = {
|
|
|
87
88
|
sddChangeId?: string;
|
|
88
89
|
sddTtlMinutes?: number;
|
|
89
90
|
sddSyncDocsDryRun?: boolean;
|
|
91
|
+
sddSyncDocsChange?: string;
|
|
92
|
+
sddSyncDocsStage?: SddStage;
|
|
93
|
+
sddSyncDocsTask?: string;
|
|
94
|
+
sddLearnDryRun?: boolean;
|
|
95
|
+
sddLearnChange?: string;
|
|
96
|
+
sddLearnStage?: SddStage;
|
|
97
|
+
sddLearnTask?: string;
|
|
90
98
|
adapterCommand?: 'install';
|
|
91
99
|
adapterAgent?: AdapterAgent;
|
|
92
100
|
adapterDryRun?: boolean;
|
|
@@ -120,7 +128,8 @@ Pumuki lifecycle commands:
|
|
|
120
128
|
pumuki sdd session --open --change=<change-id> [--ttl-minutes=<n>] [--json]
|
|
121
129
|
pumuki sdd session --refresh [--ttl-minutes=<n>] [--json]
|
|
122
130
|
pumuki sdd session --close [--json]
|
|
123
|
-
pumuki sdd sync-docs [--dry-run] [--json]
|
|
131
|
+
pumuki sdd sync-docs [--change=<change-id>] [--stage=PRE_WRITE|PRE_COMMIT|PRE_PUSH|CI] [--task=<task-id>] [--dry-run] [--json]
|
|
132
|
+
pumuki sdd learn --change=<change-id> [--stage=PRE_WRITE|PRE_COMMIT|PRE_PUSH|CI] [--task=<task-id>] [--dry-run] [--json]
|
|
124
133
|
`.trim();
|
|
125
134
|
|
|
126
135
|
const LOOP_RUN_POLICY: GatePolicy = {
|
|
@@ -420,6 +429,13 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
|
|
|
420
429
|
let sddChangeId: ParsedArgs['sddChangeId'];
|
|
421
430
|
let sddTtlMinutes: ParsedArgs['sddTtlMinutes'];
|
|
422
431
|
let sddSyncDocsDryRun = false;
|
|
432
|
+
let sddSyncDocsChange: ParsedArgs['sddSyncDocsChange'];
|
|
433
|
+
let sddSyncDocsStage: ParsedArgs['sddSyncDocsStage'];
|
|
434
|
+
let sddSyncDocsTask: ParsedArgs['sddSyncDocsTask'];
|
|
435
|
+
let sddLearnDryRun = false;
|
|
436
|
+
let sddLearnChange: ParsedArgs['sddLearnChange'];
|
|
437
|
+
let sddLearnStage: ParsedArgs['sddLearnStage'];
|
|
438
|
+
let sddLearnTask: ParsedArgs['sddLearnTask'];
|
|
423
439
|
let adapterCommand: ParsedArgs['adapterCommand'];
|
|
424
440
|
let adapterAgent: ParsedArgs['adapterAgent'];
|
|
425
441
|
let adapterDryRun = false;
|
|
@@ -598,7 +614,8 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
|
|
|
598
614
|
subcommandRaw !== 'status' &&
|
|
599
615
|
subcommandRaw !== 'validate' &&
|
|
600
616
|
subcommandRaw !== 'session' &&
|
|
601
|
-
subcommandRaw !== 'sync-docs'
|
|
617
|
+
subcommandRaw !== 'sync-docs' &&
|
|
618
|
+
subcommandRaw !== 'learn'
|
|
602
619
|
) {
|
|
603
620
|
throw new Error(`Unsupported SDD subcommand "${subcommandRaw}".\n\n${HELP_TEXT}`);
|
|
604
621
|
}
|
|
@@ -610,18 +627,30 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
|
|
|
610
627
|
continue;
|
|
611
628
|
}
|
|
612
629
|
if (arg === '--dry-run') {
|
|
613
|
-
if (sddCommand
|
|
614
|
-
|
|
630
|
+
if (sddCommand === 'sync-docs') {
|
|
631
|
+
sddSyncDocsDryRun = true;
|
|
632
|
+
continue;
|
|
615
633
|
}
|
|
616
|
-
|
|
617
|
-
|
|
634
|
+
if (sddCommand === 'learn') {
|
|
635
|
+
sddLearnDryRun = true;
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
throw new Error(`--dry-run is only supported with "pumuki sdd sync-docs" or "pumuki sdd learn".\n\n${HELP_TEXT}`);
|
|
618
639
|
}
|
|
619
640
|
if (arg.startsWith('--stage=')) {
|
|
620
|
-
if (sddCommand
|
|
621
|
-
|
|
641
|
+
if (sddCommand === 'validate') {
|
|
642
|
+
sddStage = parseSddStage(arg.slice('--stage='.length));
|
|
643
|
+
continue;
|
|
622
644
|
}
|
|
623
|
-
|
|
624
|
-
|
|
645
|
+
if (sddCommand === 'sync-docs') {
|
|
646
|
+
sddSyncDocsStage = parseSddStage(arg.slice('--stage='.length));
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
if (sddCommand === 'learn') {
|
|
650
|
+
sddLearnStage = parseSddStage(arg.slice('--stage='.length));
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
throw new Error(`--stage is only supported with "pumuki sdd validate", "pumuki sdd sync-docs" or "pumuki sdd learn".\n\n${HELP_TEXT}`);
|
|
625
654
|
}
|
|
626
655
|
if (arg === '--open') {
|
|
627
656
|
if (sddCommand !== 'session') {
|
|
@@ -645,11 +674,46 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
|
|
|
645
674
|
continue;
|
|
646
675
|
}
|
|
647
676
|
if (arg.startsWith('--change=')) {
|
|
648
|
-
if (sddCommand
|
|
649
|
-
|
|
677
|
+
if (sddCommand === 'session') {
|
|
678
|
+
sddChangeId = arg.slice('--change='.length).trim();
|
|
679
|
+
continue;
|
|
650
680
|
}
|
|
651
|
-
|
|
652
|
-
|
|
681
|
+
if (sddCommand === 'sync-docs') {
|
|
682
|
+
const changeValue = arg.slice('--change='.length).trim();
|
|
683
|
+
if (changeValue.length === 0) {
|
|
684
|
+
throw new Error(`Invalid --change value "${arg}".`);
|
|
685
|
+
}
|
|
686
|
+
sddSyncDocsChange = changeValue;
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
if (sddCommand === 'learn') {
|
|
690
|
+
const changeValue = arg.slice('--change='.length).trim();
|
|
691
|
+
if (changeValue.length === 0) {
|
|
692
|
+
throw new Error(`Invalid --change value "${arg}".`);
|
|
693
|
+
}
|
|
694
|
+
sddLearnChange = changeValue;
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
throw new Error(`--change is only supported with "pumuki sdd session", "pumuki sdd sync-docs" or "pumuki sdd learn".\n\n${HELP_TEXT}`);
|
|
698
|
+
}
|
|
699
|
+
if (arg.startsWith('--task=')) {
|
|
700
|
+
if (sddCommand === 'sync-docs') {
|
|
701
|
+
const taskValue = arg.slice('--task='.length).trim();
|
|
702
|
+
if (taskValue.length === 0) {
|
|
703
|
+
throw new Error(`Invalid --task value "${arg}".`);
|
|
704
|
+
}
|
|
705
|
+
sddSyncDocsTask = taskValue;
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
if (sddCommand === 'learn') {
|
|
709
|
+
const taskValue = arg.slice('--task='.length).trim();
|
|
710
|
+
if (taskValue.length === 0) {
|
|
711
|
+
throw new Error(`Invalid --task value "${arg}".`);
|
|
712
|
+
}
|
|
713
|
+
sddLearnTask = taskValue;
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
throw new Error(`--task is only supported with "pumuki sdd sync-docs" or "pumuki sdd learn".\n\n${HELP_TEXT}`);
|
|
653
717
|
}
|
|
654
718
|
if (arg.startsWith('--ttl-minutes=')) {
|
|
655
719
|
if (sddCommand !== 'session') {
|
|
@@ -685,7 +749,7 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
|
|
|
685
749
|
if (sddCommand === 'sync-docs') {
|
|
686
750
|
if (sddSessionAction || sddChangeId || typeof sddTtlMinutes === 'number') {
|
|
687
751
|
throw new Error(
|
|
688
|
-
`"pumuki sdd sync-docs" only supports [--dry-run] [--json].\n\n${HELP_TEXT}`
|
|
752
|
+
`"pumuki sdd sync-docs" only supports [--change=<change-id>] [--stage=PRE_WRITE|PRE_COMMIT|PRE_PUSH|CI] [--task=<task-id>] [--dry-run] [--json].\n\n${HELP_TEXT}`
|
|
689
753
|
);
|
|
690
754
|
}
|
|
691
755
|
return {
|
|
@@ -694,6 +758,29 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
|
|
|
694
758
|
json,
|
|
695
759
|
sddCommand,
|
|
696
760
|
sddSyncDocsDryRun,
|
|
761
|
+
...(sddSyncDocsChange ? { sddSyncDocsChange } : {}),
|
|
762
|
+
...(sddSyncDocsStage ? { sddSyncDocsStage } : {}),
|
|
763
|
+
...(sddSyncDocsTask ? { sddSyncDocsTask } : {}),
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
if (sddCommand === 'learn') {
|
|
767
|
+
if (sddSessionAction || sddChangeId || typeof sddTtlMinutes === 'number') {
|
|
768
|
+
throw new Error(
|
|
769
|
+
`"pumuki sdd learn" only supports --change=<change-id> [--stage=PRE_WRITE|PRE_COMMIT|PRE_PUSH|CI] [--task=<task-id>] [--dry-run] [--json].\n\n${HELP_TEXT}`
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
if (!sddLearnChange || sddLearnChange.length === 0) {
|
|
773
|
+
throw new Error(`Missing --change=<change-id> for "pumuki sdd learn".\n\n${HELP_TEXT}`);
|
|
774
|
+
}
|
|
775
|
+
return {
|
|
776
|
+
command: commandRaw,
|
|
777
|
+
purgeArtifacts: false,
|
|
778
|
+
json,
|
|
779
|
+
sddCommand,
|
|
780
|
+
sddLearnDryRun,
|
|
781
|
+
sddLearnChange,
|
|
782
|
+
...(sddLearnStage ? { sddLearnStage } : {}),
|
|
783
|
+
...(sddLearnTask ? { sddLearnTask } : {}),
|
|
697
784
|
};
|
|
698
785
|
}
|
|
699
786
|
|
|
@@ -1696,12 +1783,15 @@ export const runLifecycleCli = async (
|
|
|
1696
1783
|
const syncResult = runSddSyncDocs({
|
|
1697
1784
|
repoRoot: process.cwd(),
|
|
1698
1785
|
dryRun: parsed.sddSyncDocsDryRun === true,
|
|
1786
|
+
change: parsed.sddSyncDocsChange,
|
|
1787
|
+
stage: parsed.sddSyncDocsStage,
|
|
1788
|
+
task: parsed.sddSyncDocsTask,
|
|
1699
1789
|
});
|
|
1700
1790
|
if (parsed.json) {
|
|
1701
1791
|
writeInfo(JSON.stringify(syncResult, null, 2));
|
|
1702
1792
|
} else {
|
|
1703
1793
|
writeInfo(
|
|
1704
|
-
`[pumuki][sdd] sync-docs dry_run=${syncResult.dryRun ? 'yes' : 'no'} updated=${syncResult.updated ? 'yes' : 'no'} files=${syncResult.files.length}`
|
|
1794
|
+
`[pumuki][sdd] sync-docs dry_run=${syncResult.dryRun ? 'yes' : 'no'} updated=${syncResult.updated ? 'yes' : 'no'} files=${syncResult.files.length} change=${syncResult.context.change ?? 'none'} stage=${syncResult.context.stage ?? 'none'} task=${syncResult.context.task ?? 'none'}`
|
|
1705
1795
|
);
|
|
1706
1796
|
for (const file of syncResult.files) {
|
|
1707
1797
|
writeInfo(
|
|
@@ -1714,6 +1804,26 @@ export const runLifecycleCli = async (
|
|
|
1714
1804
|
}
|
|
1715
1805
|
return 0;
|
|
1716
1806
|
}
|
|
1807
|
+
if (parsed.sddCommand === 'learn') {
|
|
1808
|
+
const learnResult = runSddLearn({
|
|
1809
|
+
repoRoot: process.cwd(),
|
|
1810
|
+
dryRun: parsed.sddLearnDryRun === true,
|
|
1811
|
+
change: parsed.sddLearnChange,
|
|
1812
|
+
stage: parsed.sddLearnStage,
|
|
1813
|
+
task: parsed.sddLearnTask,
|
|
1814
|
+
});
|
|
1815
|
+
if (parsed.json) {
|
|
1816
|
+
writeInfo(JSON.stringify(learnResult, null, 2));
|
|
1817
|
+
} else {
|
|
1818
|
+
writeInfo(
|
|
1819
|
+
`[pumuki][sdd] learn dry_run=${learnResult.dryRun ? 'yes' : 'no'} change=${learnResult.context.change} stage=${learnResult.context.stage ?? 'none'} task=${learnResult.context.task ?? 'none'} written=${learnResult.learning.written ? 'yes' : 'no'} digest=${learnResult.learning.digest}`
|
|
1820
|
+
);
|
|
1821
|
+
writeInfo(
|
|
1822
|
+
`[pumuki][sdd] learning_path=${learnResult.learning.path} failed=${learnResult.learning.artifact.failed_patterns.length} successful=${learnResult.learning.artifact.successful_patterns.length} anomalies=${learnResult.learning.artifact.gate_anomalies.length} updates=${learnResult.learning.artifact.rule_updates.length}`
|
|
1823
|
+
);
|
|
1824
|
+
}
|
|
1825
|
+
return 0;
|
|
1826
|
+
}
|
|
1717
1827
|
return 0;
|
|
1718
1828
|
}
|
|
1719
1829
|
case 'adapter': {
|
|
@@ -9,4 +9,4 @@ export type {
|
|
|
9
9
|
} from './types';
|
|
10
10
|
export { evaluateSddPolicy, readSddStatus } from './policy';
|
|
11
11
|
export { closeSddSession, openSddSession, readSddSession, refreshSddSession } from './sessionStore';
|
|
12
|
-
export { runSddSyncDocs } from './syncDocs';
|
|
12
|
+
export { runSddLearn, runSddSyncDocs } from './syncDocs';
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
validateOpenSpecChanges,
|
|
9
9
|
} from './openSpecCli';
|
|
10
10
|
import { readSddSession, refreshSddSession } from './sessionStore';
|
|
11
|
+
import { resolveDegradedMode } from '../gate/degradedMode';
|
|
11
12
|
import type {
|
|
12
13
|
SddDecision,
|
|
13
14
|
SddEvaluateResult,
|
|
@@ -208,6 +209,42 @@ export const evaluateSddPolicy = (params: {
|
|
|
208
209
|
};
|
|
209
210
|
}
|
|
210
211
|
|
|
212
|
+
const degradedMode = resolveDegradedMode(params.stage, repoRoot);
|
|
213
|
+
if (degradedMode?.action === 'block') {
|
|
214
|
+
return {
|
|
215
|
+
stage: params.stage,
|
|
216
|
+
status,
|
|
217
|
+
decision: blocked(
|
|
218
|
+
'SDD_DEGRADED_MODE_BLOCKED',
|
|
219
|
+
`SDD blocked by degraded mode (action=block). reason=${degradedMode.reason} source=${degradedMode.source}.`,
|
|
220
|
+
{
|
|
221
|
+
degraded_mode: true,
|
|
222
|
+
degraded_action: degradedMode.action,
|
|
223
|
+
degraded_reason: degradedMode.reason,
|
|
224
|
+
degraded_source: degradedMode.source,
|
|
225
|
+
degraded_code: degradedMode.code,
|
|
226
|
+
}
|
|
227
|
+
),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (degradedMode?.action === 'allow') {
|
|
232
|
+
return {
|
|
233
|
+
stage: params.stage,
|
|
234
|
+
status,
|
|
235
|
+
decision: allowed(
|
|
236
|
+
`SDD allowed in degraded mode (action=allow). reason=${degradedMode.reason} source=${degradedMode.source}.`,
|
|
237
|
+
{
|
|
238
|
+
degraded_mode: true,
|
|
239
|
+
degraded_action: degradedMode.action,
|
|
240
|
+
degraded_reason: degradedMode.reason,
|
|
241
|
+
degraded_source: degradedMode.source,
|
|
242
|
+
degraded_code: degradedMode.code,
|
|
243
|
+
}
|
|
244
|
+
),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
211
248
|
if (!status.openspec.installed) {
|
|
212
249
|
return {
|
|
213
250
|
stage: params.stage,
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { resolve } from 'node:path';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { readEvidenceResult, type EvidenceReadResult } from '../evidence/readEvidence';
|
|
4
5
|
import { readSddStatus } from './policy';
|
|
5
|
-
|
|
6
|
-
export const SDD_SYNC_DOCS_CANONICAL_FILES = [
|
|
7
|
-
'docs/technical/08-validation/refactor/pumuki-integration-feedback.md',
|
|
8
|
-
] as const;
|
|
6
|
+
import type { SddStage } from './types';
|
|
9
7
|
|
|
10
8
|
const SDD_STATUS_SECTION = {
|
|
11
9
|
id: 'sdd-status',
|
|
@@ -21,6 +19,18 @@ type ManagedSectionSyncResult = {
|
|
|
21
19
|
diffMarkdown: string;
|
|
22
20
|
};
|
|
23
21
|
|
|
22
|
+
export type SddSyncDocsManagedSection = {
|
|
23
|
+
id: string;
|
|
24
|
+
beginMarker: string;
|
|
25
|
+
endMarker: string;
|
|
26
|
+
renderBody: (repoRoot: string) => string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type SddSyncDocsTarget = {
|
|
30
|
+
path: string;
|
|
31
|
+
sections: ReadonlyArray<SddSyncDocsManagedSection>;
|
|
32
|
+
};
|
|
33
|
+
|
|
24
34
|
export type SddSyncDocsFileResult = {
|
|
25
35
|
path: string;
|
|
26
36
|
updated: boolean;
|
|
@@ -34,8 +44,45 @@ export type SddSyncDocsResult = {
|
|
|
34
44
|
command: 'pumuki sdd sync-docs';
|
|
35
45
|
dryRun: boolean;
|
|
36
46
|
repoRoot: string;
|
|
47
|
+
context: {
|
|
48
|
+
change: string | null;
|
|
49
|
+
stage: SddStage | null;
|
|
50
|
+
task: string | null;
|
|
51
|
+
};
|
|
37
52
|
updated: boolean;
|
|
38
53
|
files: ReadonlyArray<SddSyncDocsFileResult>;
|
|
54
|
+
learning?: {
|
|
55
|
+
path: string;
|
|
56
|
+
written: boolean;
|
|
57
|
+
digest: string;
|
|
58
|
+
artifact: {
|
|
59
|
+
version: '1.0';
|
|
60
|
+
change_id: string;
|
|
61
|
+
stage: SddStage | null;
|
|
62
|
+
task: string | null;
|
|
63
|
+
generated_at: string;
|
|
64
|
+
failed_patterns: string[];
|
|
65
|
+
successful_patterns: string[];
|
|
66
|
+
rule_updates: string[];
|
|
67
|
+
gate_anomalies: string[];
|
|
68
|
+
sync_docs: {
|
|
69
|
+
updated: boolean;
|
|
70
|
+
file_paths: string[];
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type SddLearnResult = {
|
|
77
|
+
command: 'pumuki sdd learn';
|
|
78
|
+
dryRun: boolean;
|
|
79
|
+
repoRoot: string;
|
|
80
|
+
context: {
|
|
81
|
+
change: string;
|
|
82
|
+
stage: SddStage | null;
|
|
83
|
+
task: string | null;
|
|
84
|
+
};
|
|
85
|
+
learning: NonNullable<SddSyncDocsResult['learning']>;
|
|
39
86
|
};
|
|
40
87
|
|
|
41
88
|
const normalizeSectionBody = (value: string): string => value.trim().replace(/\r\n/g, '\n');
|
|
@@ -86,6 +133,24 @@ const formatSddStatusManagedBody = (repoRoot: string): string => {
|
|
|
86
133
|
].join('\n');
|
|
87
134
|
};
|
|
88
135
|
|
|
136
|
+
const DEFAULT_SYNC_DOCS_TARGETS: ReadonlyArray<SddSyncDocsTarget> = [
|
|
137
|
+
{
|
|
138
|
+
path: 'docs/technical/08-validation/refactor/pumuki-integration-feedback.md',
|
|
139
|
+
sections: [
|
|
140
|
+
{
|
|
141
|
+
id: SDD_STATUS_SECTION.id,
|
|
142
|
+
beginMarker: SDD_STATUS_SECTION.begin,
|
|
143
|
+
endMarker: SDD_STATUS_SECTION.end,
|
|
144
|
+
renderBody: formatSddStatusManagedBody,
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
export const SDD_SYNC_DOCS_CANONICAL_FILES = DEFAULT_SYNC_DOCS_TARGETS.map(
|
|
151
|
+
(target) => target.path
|
|
152
|
+
);
|
|
153
|
+
|
|
89
154
|
const applyManagedSection = (params: {
|
|
90
155
|
filePath: string;
|
|
91
156
|
source: string;
|
|
@@ -132,38 +197,124 @@ const applyManagedSection = (params: {
|
|
|
132
197
|
return { nextSource, result };
|
|
133
198
|
};
|
|
134
199
|
|
|
200
|
+
const toSortedArray = (set: Set<string>): string[] => {
|
|
201
|
+
return [...set].sort((left, right) => left.localeCompare(right));
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const collectLearningSignals = (params: {
|
|
205
|
+
updated: boolean;
|
|
206
|
+
evidenceResult: EvidenceReadResult;
|
|
207
|
+
}): {
|
|
208
|
+
failedPatterns: string[];
|
|
209
|
+
successfulPatterns: string[];
|
|
210
|
+
gateAnomalies: string[];
|
|
211
|
+
ruleUpdates: string[];
|
|
212
|
+
} => {
|
|
213
|
+
const failedPatterns = new Set<string>();
|
|
214
|
+
const successfulPatterns = new Set<string>();
|
|
215
|
+
const gateAnomalies = new Set<string>();
|
|
216
|
+
const ruleUpdates = new Set<string>();
|
|
217
|
+
|
|
218
|
+
successfulPatterns.add('sync-docs.completed');
|
|
219
|
+
successfulPatterns.add(params.updated ? 'sync-docs.updated' : 'sync-docs.no_changes');
|
|
220
|
+
|
|
221
|
+
if (params.evidenceResult.kind === 'missing') {
|
|
222
|
+
gateAnomalies.add('evidence.missing');
|
|
223
|
+
ruleUpdates.add('evidence.bootstrap.required');
|
|
224
|
+
} else if (params.evidenceResult.kind === 'invalid') {
|
|
225
|
+
gateAnomalies.add(`evidence.invalid.${params.evidenceResult.reason}`);
|
|
226
|
+
ruleUpdates.add('evidence.rebuild.required');
|
|
227
|
+
if (params.evidenceResult.reason === 'schema') {
|
|
228
|
+
ruleUpdates.add('evidence.schema.repair');
|
|
229
|
+
}
|
|
230
|
+
if (params.evidenceResult.reason === 'evidence-chain-invalid') {
|
|
231
|
+
ruleUpdates.add('evidence.chain.repair');
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
const evidence = params.evidenceResult.evidence;
|
|
235
|
+
if (evidence.ai_gate.status === 'BLOCKED') {
|
|
236
|
+
failedPatterns.add('ai-gate.blocked');
|
|
237
|
+
ruleUpdates.add('ai-gate.unblock.required');
|
|
238
|
+
for (const violation of evidence.ai_gate.violations) {
|
|
239
|
+
gateAnomalies.add(`ai-gate.violation.${violation.code}`);
|
|
240
|
+
ruleUpdates.add(`ai-gate.violation.${violation.code}.review`);
|
|
241
|
+
}
|
|
242
|
+
} else {
|
|
243
|
+
successfulPatterns.add('ai-gate.allowed');
|
|
244
|
+
}
|
|
245
|
+
if (evidence.snapshot.outcome === 'BLOCK') {
|
|
246
|
+
gateAnomalies.add('snapshot.outcome.block');
|
|
247
|
+
ruleUpdates.add('snapshot.outcome.review');
|
|
248
|
+
}
|
|
249
|
+
const sddDecision = evidence.sdd_metrics?.decision;
|
|
250
|
+
if (sddDecision) {
|
|
251
|
+
if (sddDecision.allowed) {
|
|
252
|
+
successfulPatterns.add('sdd.allowed');
|
|
253
|
+
} else {
|
|
254
|
+
failedPatterns.add(`sdd.blocked.${sddDecision.code}`);
|
|
255
|
+
ruleUpdates.add(`sdd.${sddDecision.code}.remediate`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
failedPatterns: toSortedArray(failedPatterns),
|
|
262
|
+
successfulPatterns: toSortedArray(successfulPatterns),
|
|
263
|
+
gateAnomalies: toSortedArray(gateAnomalies),
|
|
264
|
+
ruleUpdates: toSortedArray(ruleUpdates),
|
|
265
|
+
};
|
|
266
|
+
};
|
|
267
|
+
|
|
135
268
|
export const runSddSyncDocs = (params?: {
|
|
136
269
|
repoRoot?: string;
|
|
137
270
|
dryRun?: boolean;
|
|
271
|
+
change?: string;
|
|
272
|
+
stage?: SddStage;
|
|
273
|
+
task?: string;
|
|
274
|
+
targets?: ReadonlyArray<SddSyncDocsTarget>;
|
|
275
|
+
now?: () => Date;
|
|
276
|
+
evidenceReader?: (repoRoot: string) => EvidenceReadResult;
|
|
138
277
|
}): SddSyncDocsResult => {
|
|
139
278
|
const repoRoot = resolve(params?.repoRoot ?? process.cwd());
|
|
140
279
|
const dryRun = params?.dryRun === true;
|
|
280
|
+
const change = params?.change?.trim() ? params.change.trim() : null;
|
|
281
|
+
const stage = params?.stage ?? null;
|
|
282
|
+
const task = params?.task?.trim() ? params.task.trim() : null;
|
|
283
|
+
const targets = params?.targets ?? DEFAULT_SYNC_DOCS_TARGETS;
|
|
284
|
+
const now = params?.now ?? (() => new Date());
|
|
285
|
+
const evidenceReader = params?.evidenceReader ?? readEvidenceResult;
|
|
141
286
|
|
|
142
|
-
const updates =
|
|
143
|
-
const absolutePath = resolve(repoRoot,
|
|
287
|
+
const updates = targets.map((target) => {
|
|
288
|
+
const absolutePath = resolve(repoRoot, target.path);
|
|
144
289
|
if (!existsSync(absolutePath)) {
|
|
145
290
|
throw new Error(
|
|
146
|
-
`[pumuki][sdd] sync-docs missing canonical file: ${
|
|
291
|
+
`[pumuki][sdd] sync-docs missing canonical file: ${target.path}`
|
|
147
292
|
);
|
|
148
293
|
}
|
|
149
294
|
|
|
150
295
|
const currentSource = readFileSync(absolutePath, 'utf8');
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
296
|
+
let nextSource = currentSource;
|
|
297
|
+
const sectionUpdates: ManagedSectionSyncResult[] = [];
|
|
298
|
+
|
|
299
|
+
for (const section of target.sections) {
|
|
300
|
+
const update = applyManagedSection({
|
|
301
|
+
filePath: target.path,
|
|
302
|
+
source: nextSource,
|
|
303
|
+
beginMarker: section.beginMarker,
|
|
304
|
+
endMarker: section.endMarker,
|
|
305
|
+
renderedBody: section.renderBody(repoRoot),
|
|
306
|
+
sectionId: section.id,
|
|
307
|
+
});
|
|
308
|
+
nextSource = update.nextSource;
|
|
309
|
+
sectionUpdates.push(update.result);
|
|
310
|
+
}
|
|
160
311
|
|
|
161
312
|
return {
|
|
162
|
-
relativePath,
|
|
313
|
+
relativePath: target.path,
|
|
163
314
|
absolutePath,
|
|
164
315
|
currentSource,
|
|
165
|
-
nextSource
|
|
166
|
-
sections:
|
|
316
|
+
nextSource,
|
|
317
|
+
sections: sectionUpdates,
|
|
167
318
|
};
|
|
168
319
|
});
|
|
169
320
|
|
|
@@ -188,11 +339,97 @@ export const runSddSyncDocs = (params?: {
|
|
|
188
339
|
}),
|
|
189
340
|
}));
|
|
190
341
|
|
|
342
|
+
const updated = files.some((file) => file.updated);
|
|
343
|
+
let learning: SddSyncDocsResult['learning'];
|
|
344
|
+
if (change) {
|
|
345
|
+
const signals = collectLearningSignals({
|
|
346
|
+
updated,
|
|
347
|
+
evidenceResult: evidenceReader(repoRoot),
|
|
348
|
+
});
|
|
349
|
+
const artifact = {
|
|
350
|
+
version: '1.0' as const,
|
|
351
|
+
change_id: change,
|
|
352
|
+
stage,
|
|
353
|
+
task,
|
|
354
|
+
generated_at: now().toISOString(),
|
|
355
|
+
failed_patterns: signals.failedPatterns,
|
|
356
|
+
successful_patterns: signals.successfulPatterns,
|
|
357
|
+
rule_updates: signals.ruleUpdates,
|
|
358
|
+
gate_anomalies: signals.gateAnomalies,
|
|
359
|
+
sync_docs: {
|
|
360
|
+
updated,
|
|
361
|
+
file_paths: files.map((file) => file.path),
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
const relativePath = `openspec/changes/${change}/learning.json`;
|
|
365
|
+
const absolutePath = resolve(repoRoot, relativePath);
|
|
366
|
+
const serialized = `${JSON.stringify(artifact, null, 2)}\n`;
|
|
367
|
+
const digest = computeDigest(serialized);
|
|
368
|
+
if (!dryRun) {
|
|
369
|
+
mkdirSync(dirname(absolutePath), { recursive: true });
|
|
370
|
+
writeFileSync(absolutePath, serialized, 'utf8');
|
|
371
|
+
}
|
|
372
|
+
learning = {
|
|
373
|
+
path: relativePath,
|
|
374
|
+
written: !dryRun,
|
|
375
|
+
digest,
|
|
376
|
+
artifact,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
191
380
|
return {
|
|
192
381
|
command: 'pumuki sdd sync-docs',
|
|
193
382
|
dryRun,
|
|
194
383
|
repoRoot,
|
|
195
|
-
|
|
384
|
+
context: {
|
|
385
|
+
change,
|
|
386
|
+
stage,
|
|
387
|
+
task,
|
|
388
|
+
},
|
|
389
|
+
updated,
|
|
196
390
|
files,
|
|
391
|
+
...(learning ? { learning } : {}),
|
|
392
|
+
};
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
export const runSddLearn = (params?: {
|
|
396
|
+
repoRoot?: string;
|
|
397
|
+
dryRun?: boolean;
|
|
398
|
+
change?: string;
|
|
399
|
+
stage?: SddStage;
|
|
400
|
+
task?: string;
|
|
401
|
+
now?: () => Date;
|
|
402
|
+
evidenceReader?: (repoRoot: string) => EvidenceReadResult;
|
|
403
|
+
}): SddLearnResult => {
|
|
404
|
+
const change = params?.change?.trim();
|
|
405
|
+
if (!change) {
|
|
406
|
+
throw new Error('[pumuki][sdd] learn requires --change=<change-id>.');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const result = runSddSyncDocs({
|
|
410
|
+
repoRoot: params?.repoRoot,
|
|
411
|
+
dryRun: params?.dryRun,
|
|
412
|
+
change,
|
|
413
|
+
stage: params?.stage,
|
|
414
|
+
task: params?.task,
|
|
415
|
+
now: params?.now,
|
|
416
|
+
evidenceReader: params?.evidenceReader,
|
|
417
|
+
targets: [],
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
if (!result.learning) {
|
|
421
|
+
throw new Error('[pumuki][sdd] learn could not generate learning artifact.');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
command: 'pumuki sdd learn',
|
|
426
|
+
dryRun: result.dryRun,
|
|
427
|
+
repoRoot: result.repoRoot,
|
|
428
|
+
context: {
|
|
429
|
+
change,
|
|
430
|
+
stage: result.context.stage,
|
|
431
|
+
task: result.context.task,
|
|
432
|
+
},
|
|
433
|
+
learning: result.learning,
|
|
197
434
|
};
|
|
198
435
|
};
|
|
@@ -28,6 +28,13 @@ type PolicyTrace = ResolvedStagePolicy['trace'] & {
|
|
|
28
28
|
status: 'valid' | 'invalid' | 'expired' | 'unknown-source';
|
|
29
29
|
code: string;
|
|
30
30
|
};
|
|
31
|
+
degraded?: {
|
|
32
|
+
enabled: true;
|
|
33
|
+
action: 'allow' | 'block';
|
|
34
|
+
reason: string;
|
|
35
|
+
source: 'env' | 'file:.pumuki/degraded-mode.json';
|
|
36
|
+
code: 'DEGRADED_MODE_ALLOWED' | 'DEGRADED_MODE_BLOCKED';
|
|
37
|
+
};
|
|
31
38
|
};
|
|
32
39
|
|
|
33
40
|
const toSeverityCounts = (
|
|
@@ -184,6 +191,11 @@ export type GateTelemetryEventV1 = {
|
|
|
184
191
|
policy_source?: string;
|
|
185
192
|
validation_status?: 'valid' | 'invalid' | 'expired' | 'unknown-source';
|
|
186
193
|
validation_code?: string;
|
|
194
|
+
degraded_mode_enabled?: boolean;
|
|
195
|
+
degraded_mode_action?: 'allow' | 'block';
|
|
196
|
+
degraded_mode_reason?: string;
|
|
197
|
+
degraded_mode_source?: 'env' | 'file:.pumuki/degraded-mode.json';
|
|
198
|
+
degraded_mode_code?: 'DEGRADED_MODE_ALLOWED' | 'DEGRADED_MODE_BLOCKED';
|
|
187
199
|
};
|
|
188
200
|
sdd?: {
|
|
189
201
|
allowed: boolean;
|
|
@@ -267,6 +279,15 @@ const toTelemetryEvent = (params: {
|
|
|
267
279
|
validation_code: params.policyTrace.validation.code,
|
|
268
280
|
}
|
|
269
281
|
: {}),
|
|
282
|
+
...(params.policyTrace.degraded
|
|
283
|
+
? {
|
|
284
|
+
degraded_mode_enabled: params.policyTrace.degraded.enabled,
|
|
285
|
+
degraded_mode_action: params.policyTrace.degraded.action,
|
|
286
|
+
degraded_mode_reason: params.policyTrace.degraded.reason,
|
|
287
|
+
degraded_mode_source: params.policyTrace.degraded.source,
|
|
288
|
+
degraded_mode_code: params.policyTrace.degraded.code,
|
|
289
|
+
}
|
|
290
|
+
: {}),
|
|
270
291
|
},
|
|
271
292
|
}
|
|
272
293
|
: {}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.35",
|
|
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": {
|