@jaimevalasek/aioson 1.30.2 → 1.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/README.md +19 -6
  3. package/docs/en/1-understand/ecosystem-map.md +45 -29
  4. package/docs/en/1-understand/glossary.md +5 -5
  5. package/docs/en/1-understand/what-is-aioson.md +5 -5
  6. package/docs/en/2-start/existing-project.md +7 -7
  7. package/docs/en/2-start/first-project.md +32 -38
  8. package/docs/en/2-start/initial-decisions.md +18 -17
  9. package/docs/en/3-recipes/README.md +2 -2
  10. package/docs/en/3-recipes/continuity-between-sessions.md +2 -2
  11. package/docs/en/3-recipes/from-idea-to-prd-via-briefing.md +5 -2
  12. package/docs/en/3-recipes/full-feature-with-sheldon.md +327 -338
  13. package/docs/en/4-agents/README.md +28 -14
  14. package/docs/en/4-agents/discovery-design-doc.md +20 -8
  15. package/docs/en/4-agents/forge-run.md +3 -2
  16. package/docs/en/5-reference/cli-reference.md +51 -48
  17. package/docs/en/5-reference/executable-verification.md +10 -7
  18. package/docs/en/5-reference/parallel.md +2 -0
  19. package/docs/en/5-reference/qa-browser.md +2 -2
  20. package/docs/en/README.md +3 -3
  21. package/docs/pt/1-entender/glossario.md +4 -4
  22. package/docs/pt/1-entender/mapa-do-ecossistema.md +32 -21
  23. package/docs/pt/2-comecar/decisoes-iniciais.md +16 -16
  24. package/docs/pt/2-comecar/primeiro-projeto.md +1 -1
  25. package/docs/pt/3-receitas/README.md +1 -1
  26. package/docs/pt/3-receitas/app-saas-do-zero.md +35 -122
  27. package/docs/pt/3-receitas/feature-completa-com-sheldon.md +289 -338
  28. package/docs/pt/4-agentes/analyst.md +9 -5
  29. package/docs/pt/4-agentes/architect.md +9 -5
  30. package/docs/pt/4-agentes/dev.md +19 -7
  31. package/docs/pt/4-agentes/discovery-design-doc.md +25 -23
  32. package/docs/pt/4-agentes/orchestrator.md +164 -118
  33. package/docs/pt/4-agentes/pentester.md +5 -5
  34. package/docs/pt/4-agentes/pm.md +15 -7
  35. package/docs/pt/4-agentes/sheldon.md +166 -99
  36. package/docs/pt/4-agentes/ux-ui.md +7 -4
  37. package/docs/pt/5-referencia/agent-chain-continuity.md +14 -5
  38. package/docs/pt/5-referencia/comandos-cli.md +1 -1
  39. package/docs/pt/5-referencia/harness-retro.md +2 -1
  40. package/docs/pt/5-referencia/sdd-automation-scripts.md +6 -6
  41. package/docs/pt/5-referencia/sdd-framework.md +53 -16
  42. package/docs/pt/5-referencia/sdd-planos-e-estrutura.md +27 -15
  43. package/docs/pt/README.md +4 -4
  44. package/docs/pt/agentes.md +48 -50
  45. package/package.json +2 -2
  46. package/src/artifact-kinds.js +110 -0
  47. package/src/cli.js +82 -42
  48. package/src/commands/agent-epilogue.js +251 -186
  49. package/src/commands/agents.js +104 -48
  50. package/src/commands/audit-code.js +344 -0
  51. package/src/commands/classify.js +75 -13
  52. package/src/commands/feature-close.js +43 -18
  53. package/src/commands/harness-check.js +259 -175
  54. package/src/commands/harness-retro-promote.js +387 -0
  55. package/src/commands/live.js +24 -0
  56. package/src/commands/prototype-check.js +163 -0
  57. package/src/commands/review-feature.js +189 -0
  58. package/src/commands/runtime.js +81 -66
  59. package/src/commands/sync-agents-copy.js +115 -0
  60. package/src/commands/verification-plan.js +116 -0
  61. package/src/commands/verify-artifact.js +530 -0
  62. package/src/commands/verify-implementation.js +428 -0
  63. package/src/commands/workflow-execute.js +309 -309
  64. package/src/commands/workflow-next.js +361 -19
  65. package/src/commands/workflow-plan.js +5 -5
  66. package/src/constants.js +4 -0
  67. package/src/gateway-pointer-merge.js +7 -1
  68. package/src/handoff-contract.js +267 -172
  69. package/src/harness/contract-integrity-gate.js +172 -0
  70. package/src/harness/contract-integrity.js +111 -0
  71. package/src/harness/contract-schema.js +377 -332
  72. package/src/harness/detect-runtime-feature.js +90 -0
  73. package/src/harness/static-criteria.js +192 -0
  74. package/src/i18n/messages/en.js +8 -4
  75. package/src/i18n/messages/es.js +8 -4
  76. package/src/i18n/messages/fr.js +8 -4
  77. package/src/i18n/messages/pt-BR.js +8 -4
  78. package/src/install-wizard.js +8 -8
  79. package/src/installer.js +13 -6
  80. package/src/lib/retro/retro-render.js +10 -1
  81. package/src/lib/retro/retro-sources.js +45 -27
  82. package/src/lib/retro/verification-reports.js +230 -0
  83. package/src/parser.js +6 -0
  84. package/src/preflight-engine.js +12 -12
  85. package/src/runtime-store.js +13 -9
  86. package/src/verification/evidence-bundle.js +251 -0
  87. package/src/verification/ledger-store.js +221 -0
  88. package/src/verification/path-policy.js +74 -0
  89. package/src/verification/policy-engine.js +95 -0
  90. package/src/verification/prompt-package.js +314 -0
  91. package/src/verification/redaction.js +77 -0
  92. package/src/verification/report-parser.js +132 -0
  93. package/src/verification/report-store.js +97 -0
  94. package/src/verification/result.js +16 -0
  95. package/src/verification/runners/index.js +319 -0
  96. package/src/verification/runtime-telemetry.js +144 -0
  97. package/src/verification/schema.js +276 -0
  98. package/src/verification/source-discovery.js +153 -0
  99. package/src/verification-policy.js +398 -0
  100. package/src/version.js +52 -1
  101. package/template/.aioson/agents/analyst.md +9 -6
  102. package/template/.aioson/agents/architect.md +34 -5
  103. package/template/.aioson/agents/briefing-refiner.md +25 -0
  104. package/template/.aioson/agents/briefing.md +69 -12
  105. package/template/.aioson/agents/committer.md +2 -1
  106. package/template/.aioson/agents/copywriter.md +30 -21
  107. package/template/.aioson/agents/design-hybrid-forge.md +28 -15
  108. package/template/.aioson/agents/dev.md +28 -10
  109. package/template/.aioson/agents/deyvin.md +3 -2
  110. package/template/.aioson/agents/discover.md +12 -3
  111. package/template/.aioson/agents/discovery-design-doc.md +7 -2
  112. package/template/.aioson/agents/genome.md +19 -10
  113. package/template/.aioson/agents/neo.md +30 -30
  114. package/template/.aioson/agents/orache.md +20 -11
  115. package/template/.aioson/agents/orchestrator.md +84 -7
  116. package/template/.aioson/agents/pm.md +8 -8
  117. package/template/.aioson/agents/product.md +15 -11
  118. package/template/.aioson/agents/profiler-enricher.md +20 -11
  119. package/template/.aioson/agents/profiler-forge.md +20 -11
  120. package/template/.aioson/agents/profiler-researcher.md +20 -11
  121. package/template/.aioson/agents/qa.md +87 -17
  122. package/template/.aioson/agents/scope-check.md +19 -5
  123. package/template/.aioson/agents/setup.md +12 -1
  124. package/template/.aioson/agents/sheldon.md +92 -9
  125. package/template/.aioson/agents/site-forge.md +11 -2
  126. package/template/.aioson/agents/squad.md +20 -5
  127. package/template/.aioson/agents/ux-ui.md +4 -2
  128. package/template/.aioson/agents/validator.md +33 -1
  129. package/template/.aioson/config/verification.json +61 -0
  130. package/template/.aioson/config.md +13 -9
  131. package/template/.aioson/docs/LAYERS.md +2 -2
  132. package/template/.aioson/docs/autopilot-handoff.md +10 -10
  133. package/template/.aioson/docs/dev/execution-discipline.md +41 -0
  134. package/template/.aioson/docs/dev/phase-loop.md +47 -0
  135. package/template/.aioson/docs/feature-expansion-taxonomy.md +31 -3
  136. package/template/.aioson/docs/presets/workflow.config.full-merged.json +15 -0
  137. package/template/.aioson/docs/presets/workflow.config.lean.json +15 -0
  138. package/template/.aioson/docs/product/prd-contract.md +12 -12
  139. package/template/.aioson/docs/prototype-contract.md +98 -0
  140. package/template/.aioson/docs/reference-identity.md +94 -0
  141. package/template/.aioson/docs/sheldon/harness-contract.md +155 -28
  142. package/template/.aioson/docs/verification-config.md +82 -0
  143. package/template/.aioson/docs/verify-artifact-gates.md +91 -0
  144. package/template/.aioson/docs/workflow-lean-lane.md +129 -0
  145. package/template/.aioson/skills/design/interface-design/references/intent-and-domain.md +2 -0
  146. package/template/.aioson/skills/process/aioson-spec-driven/SKILL.md +16 -14
  147. package/template/.aioson/skills/process/aioson-spec-driven/references/approval-gates.md +2 -2
  148. package/template/.aioson/skills/process/aioson-spec-driven/references/classification-map.md +4 -4
  149. package/template/.aioson/skills/process/aioson-spec-driven/references/dev.md +15 -15
  150. package/template/.aioson/skills/process/briefing-expansion-scout/SKILL.md +25 -3
  151. package/template/.aioson/skills/process/design-hybrid-forge/SKILL.md +5 -3
  152. package/template/.aioson/skills/process/design-hybrid-forge/references/external-source-ingestion.md +89 -0
  153. package/template/.aioson/skills/process/product-scope-expansion/SKILL.md +29 -2
  154. package/template/.aioson/skills/process/prototype-forge/SKILL.md +98 -0
  155. package/template/.aioson/skills/process/reference-identity-extract/SKILL.md +164 -0
  156. package/template/.aioson/skills/process/sheldon-expansion-audit/SKILL.md +23 -2
  157. package/template/.aioson/skills/static/multi-agent-patterns.md +5 -5
  158. package/template/CLAUDE.md +15 -11
@@ -1,332 +1,377 @@
1
- 'use strict';
2
-
3
- /**
4
- * Validação de schema do harness-contract.json (loop-guardrails REQ-1).
5
- *
6
- * Responsabilidade distinta de `harness:validate` (que valida a IMPLEMENTAÇÃO
7
- * contra criteria via @validator). Aqui valida-se o CONTRATO em si, no
8
- * preflight do `self:loop` — um typo em `allowed_files` não pode desligar o
9
- * scope guard silenciosamente. Mensagens usam "contract schema invalid",
10
- * nunca "validation verdict".
11
- *
12
- * Retrocompat (REQ-11 / EC-12): contratos antigos (feature/contract_mode/
13
- * governor/criteria) passam; campos novos são todos opcionais.
14
- */
15
-
16
- const { validateGlobPattern } = require('./glob-match');
17
-
18
- /** REQ-4 — defaults proibidos, sempre aplicados e não-removíveis (SEC-SBD-05). */
19
- const DEFAULT_FORBIDDEN_GLOBS = Object.freeze([
20
- '.env*',
21
- '*.pem',
22
- '*.key',
23
- 'secrets/**',
24
- '.git/**',
25
- 'node_modules/**',
26
- 'package-lock.json',
27
- 'yarn.lock',
28
- 'pnpm-lock.yaml',
29
- 'npm-shrinkwrap.json',
30
- 'bun.lockb'
31
- ]);
32
-
33
- const HUMAN_GATE_THEMES = Object.freeze([
34
- 'payment_logic_change',
35
- 'auth_permission_change',
36
- 'database_destructive_change',
37
- 'publish'
38
- ]);
39
-
40
- /** Mapa default tema→globs (requirements §2.1); override via human_gate.themes[].paths. */
41
- const DEFAULT_THEME_PATHS = Object.freeze({
42
- payment_logic_change: Object.freeze(['**/billing/**', '**/payment/**']),
43
- auth_permission_change: Object.freeze(['**/auth/**']),
44
- database_destructive_change: Object.freeze(['**/migrations/**']),
45
- publish: Object.freeze([]) // gate de comando (REQ-13), nunca diff
46
- });
47
-
48
- /**
49
- * Presets do contract_mode (REQ-19). Preenchem apenas valores do governor
50
- * NÃO definidos explicitamente no contrato; valor explícito sempre vence.
51
- * `BALANCED` mantém o comportamento atual (nenhum preenchimento).
52
- */
53
- const CONTRACT_PRESETS = Object.freeze({
54
- safe: Object.freeze({
55
- max_steps: 10,
56
- error_streak_limit: 3,
57
- cost_ceiling_tokens: 200000,
58
- max_runtime_minutes: 30,
59
- max_changed_files: 20,
60
- max_diff_lines: 1500
61
- }),
62
- builder: Object.freeze({
63
- max_steps: 30,
64
- error_streak_limit: 5,
65
- cost_ceiling_tokens: 1000000,
66
- max_runtime_minutes: 120,
67
- max_changed_files: 60,
68
- max_diff_lines: 6000
69
- }),
70
- autopilot: Object.freeze({
71
- max_steps: 50,
72
- error_streak_limit: 8,
73
- cost_ceiling_tokens: 3000000,
74
- max_runtime_minutes: 360,
75
- max_changed_files: null,
76
- max_diff_lines: null
77
- })
78
- });
79
-
80
- const KNOWN_TOP_FIELDS = new Set([
81
- 'feature', 'contract_mode', 'governor', 'criteria',
82
- 'allowed_files', 'forbidden_files', 'human_gate'
83
- ]);
84
- const KNOWN_GOVERNOR_FIELDS = new Set([
85
- 'max_steps', 'error_streak_limit', 'cost_ceiling_tokens',
86
- 'max_runtime_minutes', 'max_changed_files', 'max_diff_lines'
87
- ]);
88
- const KNOWN_HUMAN_GATE_FIELDS = new Set(['required_for', 'themes']);
89
- const KNOWN_THEME_FIELDS = new Set(['name', 'paths']);
90
- const KNOWN_CRITERIA_FIELDS = new Set(['id', 'description', 'assertion', 'binary', 'verification']);
91
-
92
- const VALID_MODES = new Set(['balanced', 'safe', 'builder', 'autopilot']);
93
-
94
- function isPlainObject(v) {
95
- return v !== null && typeof v === 'object' && !Array.isArray(v);
96
- }
97
-
98
- function isPositiveInt(v) {
99
- return Number.isInteger(v) && v > 0;
100
- }
101
-
102
- function checkOptionalLimit(errors, field, value) {
103
- if (value === undefined || value === null) return;
104
- if (!isPositiveInt(value)) {
105
- errors.push({ field, reason: 'must be a positive integer or null' });
106
- }
107
- }
108
-
109
- function checkGlobArray(errors, field, value) {
110
- if (!Array.isArray(value)) {
111
- errors.push({ field, reason: 'must be an array of glob strings' });
112
- return;
113
- }
114
- value.forEach((pattern, i) => {
115
- const result = validateGlobPattern(pattern);
116
- if (!result.ok) {
117
- errors.push({ field: `${field}[${i}]`, reason: result.reason });
118
- }
119
- });
120
- }
121
-
122
- /**
123
- * Valida o contrato. Retorna { ok, errors: [{field, reason}], warnings: [{field, reason}] }.
124
- * `ok === false` quando há ao menos um erro — o preflight deve encerrar antes
125
- * de qualquer execução (REQ-1).
126
- */
127
- function validateContract(contract) {
128
- const errors = [];
129
- const warnings = [];
130
-
131
- if (!isPlainObject(contract)) {
132
- return { ok: false, errors: [{ field: '(root)', reason: 'contract must be a JSON object' }], warnings };
133
- }
134
-
135
- for (const key of Object.keys(contract)) {
136
- if (!KNOWN_TOP_FIELDS.has(key)) {
137
- errors.push({ field: key, reason: 'unknown field — check for typos (contract schema invalid)' });
138
- }
139
- }
140
-
141
- if (typeof contract.feature !== 'string' || !contract.feature.trim()) {
142
- errors.push({ field: 'feature', reason: 'must be a non-empty string' });
143
- }
144
-
145
- if (contract.contract_mode !== undefined) {
146
- if (typeof contract.contract_mode !== 'string' || !VALID_MODES.has(contract.contract_mode.toLowerCase())) {
147
- errors.push({ field: 'contract_mode', reason: 'must be one of: balanced, safe, builder, autopilot' });
148
- }
149
- }
150
-
151
- if (!isPlainObject(contract.governor)) {
152
- errors.push({ field: 'governor', reason: 'must be an object' });
153
- } else {
154
- for (const key of Object.keys(contract.governor)) {
155
- if (!KNOWN_GOVERNOR_FIELDS.has(key)) {
156
- errors.push({ field: `governor.${key}`, reason: 'unknown field check for typos (contract schema invalid)' });
157
- }
158
- }
159
- const g = contract.governor;
160
- if (g.max_steps !== undefined && g.max_steps !== null && !(Number.isInteger(g.max_steps) && g.max_steps >= 0)) {
161
- errors.push({ field: 'governor.max_steps', reason: 'must be a non-negative integer' });
162
- }
163
- if (g.error_streak_limit !== undefined && g.error_streak_limit !== null && !(Number.isInteger(g.error_streak_limit) && g.error_streak_limit >= 0)) {
164
- errors.push({ field: 'governor.error_streak_limit', reason: 'must be a non-negative integer' });
165
- }
166
- checkOptionalLimit(errors, 'governor.cost_ceiling_tokens', g.cost_ceiling_tokens);
167
- checkOptionalLimit(errors, 'governor.max_runtime_minutes', g.max_runtime_minutes);
168
- checkOptionalLimit(errors, 'governor.max_changed_files', g.max_changed_files);
169
- checkOptionalLimit(errors, 'governor.max_diff_lines', g.max_diff_lines);
170
- }
171
-
172
- if (contract.allowed_files !== undefined) {
173
- checkGlobArray(errors, 'allowed_files', contract.allowed_files);
174
- // EC-5: allowlist vazia bloquearia tudo — warning + tratada como ausente
175
- if (Array.isArray(contract.allowed_files) && contract.allowed_files.length === 0) {
176
- warnings.push({
177
- field: 'allowed_files',
178
- reason: 'empty allowlist would block every write treated as absent'
179
- });
180
- }
181
- }
182
-
183
- if (contract.forbidden_files !== undefined) {
184
- checkGlobArray(errors, 'forbidden_files', contract.forbidden_files);
185
- }
186
-
187
- if (contract.human_gate !== undefined) {
188
- if (!isPlainObject(contract.human_gate)) {
189
- errors.push({ field: 'human_gate', reason: 'must be an object' });
190
- } else {
191
- for (const key of Object.keys(contract.human_gate)) {
192
- if (!KNOWN_HUMAN_GATE_FIELDS.has(key)) {
193
- errors.push({ field: `human_gate.${key}`, reason: 'unknown field check for typos (contract schema invalid)' });
194
- }
195
- }
196
- const hg = contract.human_gate;
197
- if (!Array.isArray(hg.required_for)) {
198
- errors.push({ field: 'human_gate.required_for', reason: 'must be an array of themes (required when human_gate is present)' });
199
- } else {
200
- hg.required_for.forEach((theme, i) => {
201
- if (!HUMAN_GATE_THEMES.includes(theme)) {
202
- errors.push({ field: `human_gate.required_for[${i}]`, reason: `unknown theme "${theme}" valid: ${HUMAN_GATE_THEMES.join(', ')}` });
203
- }
204
- });
205
- }
206
- if (hg.themes !== undefined) {
207
- if (!Array.isArray(hg.themes)) {
208
- errors.push({ field: 'human_gate.themes', reason: 'must be an array' });
209
- } else {
210
- hg.themes.forEach((theme, i) => {
211
- if (!isPlainObject(theme)) {
212
- errors.push({ field: `human_gate.themes[${i}]`, reason: 'must be an object { name, paths }' });
213
- return;
214
- }
215
- for (const key of Object.keys(theme)) {
216
- if (!KNOWN_THEME_FIELDS.has(key)) {
217
- errors.push({ field: `human_gate.themes[${i}].${key}`, reason: 'unknown field — check for typos (contract schema invalid)' });
218
- }
219
- }
220
- if (!HUMAN_GATE_THEMES.includes(theme.name)) {
221
- errors.push({ field: `human_gate.themes[${i}].name`, reason: `unknown theme "${theme.name}" valid: ${HUMAN_GATE_THEMES.join(', ')}` });
222
- }
223
- checkGlobArray(errors, `human_gate.themes[${i}].paths`, theme.paths);
224
- });
225
- }
226
- }
227
- }
228
- }
229
-
230
- if (contract.criteria !== undefined) {
231
- if (!Array.isArray(contract.criteria)) {
232
- errors.push({ field: 'criteria', reason: 'must be an array' });
233
- } else {
234
- contract.criteria.forEach((criterion, i) => {
235
- if (!isPlainObject(criterion)) {
236
- errors.push({ field: `criteria[${i}]`, reason: 'must be an object' });
237
- return;
238
- }
239
- for (const key of Object.keys(criterion)) {
240
- if (!KNOWN_CRITERIA_FIELDS.has(key)) {
241
- errors.push({ field: `criteria[${i}].${key}`, reason: 'unknown field — check for typos (contract schema invalid)' });
242
- }
243
- }
244
- if (typeof criterion.id !== 'string' || !criterion.id.trim()) {
245
- errors.push({ field: `criteria[${i}].id`, reason: 'must be a non-empty string' });
246
- }
247
- if (criterion.verification !== undefined && (typeof criterion.verification !== 'string' || !criterion.verification.trim())) {
248
- errors.push({ field: `criteria[${i}].verification`, reason: 'must be a non-empty shell command string when present' });
249
- }
250
- if (criterion.binary !== undefined && typeof criterion.binary !== 'boolean') {
251
- errors.push({ field: `criteria[${i}].binary`, reason: 'must be a boolean' });
252
- }
253
- // Cobertura executável: critério binário sem verification continua
254
- // válido (julgado pelo @validator), mas é dívida de verificação.
255
- if (criterion.binary === true && criterion.verification === undefined) {
256
- warnings.push({
257
- field: `criteria[${i}].verification`,
258
- reason: `binary criterion "${criterion.id}" has no executable verification command @validator will LLM-judge it`
259
- });
260
- }
261
- });
262
- }
263
- }
264
-
265
- return { ok: errors.length === 0, errors, warnings };
266
- }
267
-
268
- /**
269
- * Resolve o contrato VALIDADO para sua forma efetiva:
270
- * - preset do contract_mode preenche valores do governor não definidos (REQ-19);
271
- * - `forbidden_files` mesclado com os defaults não-removíveis (REQ-4);
272
- * - `allowed_files: []` tratado como ausente (EC-5);
273
- * - mapa tema→paths resolvido (override substitui, não mescla).
274
- *
275
- * Não muta o contrato original; retorna um objeto efetivo para os guards.
276
- */
277
- function resolveContract(contract) {
278
- const mode = typeof contract.contract_mode === 'string'
279
- ? contract.contract_mode.toLowerCase()
280
- : 'balanced';
281
- const preset = CONTRACT_PRESETS[mode] || null;
282
-
283
- const governor = { ...(contract.governor || {}) };
284
- if (preset) {
285
- for (const [key, value] of Object.entries(preset)) {
286
- if (governor[key] === undefined) governor[key] = value;
287
- }
288
- }
289
-
290
- const forbidden = [
291
- ...DEFAULT_FORBIDDEN_GLOBS,
292
- ...(Array.isArray(contract.forbidden_files) ? contract.forbidden_files : [])
293
- ];
294
-
295
- const allowed = Array.isArray(contract.allowed_files) && contract.allowed_files.length > 0
296
- ? contract.allowed_files.slice()
297
- : null;
298
-
299
- const themePaths = { ...DEFAULT_THEME_PATHS };
300
- const requiredFor = contract.human_gate && Array.isArray(contract.human_gate.required_for)
301
- ? contract.human_gate.required_for.slice()
302
- : [];
303
- if (contract.human_gate && Array.isArray(contract.human_gate.themes)) {
304
- for (const theme of contract.human_gate.themes) {
305
- if (theme && HUMAN_GATE_THEMES.includes(theme.name) && Array.isArray(theme.paths)) {
306
- themePaths[theme.name] = theme.paths.slice();
307
- }
308
- }
309
- }
310
-
311
- return {
312
- feature: contract.feature,
313
- contract_mode: mode,
314
- governor,
315
- criteria: Array.isArray(contract.criteria) ? contract.criteria : [],
316
- allowed_files: allowed,
317
- forbidden_files: forbidden,
318
- human_gate: {
319
- required_for: requiredFor,
320
- theme_paths: themePaths
321
- }
322
- };
323
- }
324
-
325
- module.exports = {
326
- DEFAULT_FORBIDDEN_GLOBS,
327
- HUMAN_GATE_THEMES,
328
- DEFAULT_THEME_PATHS,
329
- CONTRACT_PRESETS,
330
- validateContract,
331
- resolveContract
332
- };
1
+ 'use strict';
2
+
3
+ /**
4
+ * Validação de schema do harness-contract.json (loop-guardrails REQ-1).
5
+ *
6
+ * Responsabilidade distinta de `harness:validate` (que valida a IMPLEMENTAÇÃO
7
+ * contra criteria via @validator). Aqui valida-se o CONTRATO em si, no
8
+ * preflight do `self:loop` — um typo em `allowed_files` não pode desligar o
9
+ * scope guard silenciosamente. Mensagens usam "contract schema invalid",
10
+ * nunca "validation verdict".
11
+ *
12
+ * Retrocompat (REQ-11 / EC-12): contratos antigos (feature/contract_mode/
13
+ * governor/criteria) passam; campos novos são todos opcionais.
14
+ */
15
+
16
+ const { validateGlobPattern } = require('./glob-match');
17
+
18
+ /** REQ-4 — defaults proibidos, sempre aplicados e não-removíveis (SEC-SBD-05). */
19
+ const DEFAULT_FORBIDDEN_GLOBS = Object.freeze([
20
+ '.env*',
21
+ '*.pem',
22
+ '*.key',
23
+ 'secrets/**',
24
+ '.git/**',
25
+ 'node_modules/**',
26
+ 'package-lock.json',
27
+ 'yarn.lock',
28
+ 'pnpm-lock.yaml',
29
+ 'npm-shrinkwrap.json',
30
+ 'bun.lockb'
31
+ ]);
32
+
33
+ const HUMAN_GATE_THEMES = Object.freeze([
34
+ 'payment_logic_change',
35
+ 'auth_permission_change',
36
+ 'database_destructive_change',
37
+ 'publish'
38
+ ]);
39
+
40
+ /** Mapa default tema→globs (requirements §2.1); override via human_gate.themes[].paths. */
41
+ const DEFAULT_THEME_PATHS = Object.freeze({
42
+ payment_logic_change: Object.freeze(['**/billing/**', '**/payment/**']),
43
+ auth_permission_change: Object.freeze(['**/auth/**']),
44
+ database_destructive_change: Object.freeze(['**/migrations/**']),
45
+ publish: Object.freeze([]) // gate de comando (REQ-13), nunca diff
46
+ });
47
+
48
+ /**
49
+ * Presets do contract_mode (REQ-19). Preenchem apenas valores do governor
50
+ * NÃO definidos explicitamente no contrato; valor explícito sempre vence.
51
+ * `BALANCED` mantém o comportamento atual (nenhum preenchimento).
52
+ */
53
+ const CONTRACT_PRESETS = Object.freeze({
54
+ safe: Object.freeze({
55
+ max_steps: 10,
56
+ error_streak_limit: 3,
57
+ cost_ceiling_tokens: 200000,
58
+ max_runtime_minutes: 30,
59
+ max_changed_files: 20,
60
+ max_diff_lines: 1500
61
+ }),
62
+ builder: Object.freeze({
63
+ max_steps: 30,
64
+ error_streak_limit: 5,
65
+ cost_ceiling_tokens: 1000000,
66
+ max_runtime_minutes: 120,
67
+ max_changed_files: 60,
68
+ max_diff_lines: 6000
69
+ }),
70
+ autopilot: Object.freeze({
71
+ max_steps: 50,
72
+ error_streak_limit: 8,
73
+ cost_ceiling_tokens: 3000000,
74
+ max_runtime_minutes: 360,
75
+ max_changed_files: null,
76
+ max_diff_lines: null
77
+ })
78
+ });
79
+
80
+ const KNOWN_TOP_FIELDS = new Set([
81
+ 'feature', 'contract_mode', 'governor', 'criteria',
82
+ 'allowed_files', 'forbidden_files', 'human_gate'
83
+ ]);
84
+ const KNOWN_GOVERNOR_FIELDS = new Set([
85
+ 'max_steps', 'error_streak_limit', 'cost_ceiling_tokens',
86
+ 'max_runtime_minutes', 'max_changed_files', 'max_diff_lines'
87
+ ]);
88
+ const KNOWN_HUMAN_GATE_FIELDS = new Set(['required_for', 'themes']);
89
+ const KNOWN_THEME_FIELDS = new Set(['name', 'paths']);
90
+ const KNOWN_CRITERIA_FIELDS = new Set([
91
+ 'id', 'description', 'assertion', 'binary', 'verification',
92
+ // SG-* static criteria (build-independent): proven by reading files, not running a command.
93
+ 'must_match', 'must_not_match', 'files'
94
+ ]);
95
+
96
+ const VALID_MODES = new Set(['balanced', 'safe', 'builder', 'autopilot']);
97
+
98
+ function isPlainObject(v) {
99
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
100
+ }
101
+
102
+ function isPositiveInt(v) {
103
+ return Number.isInteger(v) && v > 0;
104
+ }
105
+
106
+ function checkOptionalLimit(errors, field, value) {
107
+ if (value === undefined || value === null) return;
108
+ if (!isPositiveInt(value)) {
109
+ errors.push({ field, reason: 'must be a positive integer or null' });
110
+ }
111
+ }
112
+
113
+ function checkGlobArray(errors, field, value) {
114
+ if (!Array.isArray(value)) {
115
+ errors.push({ field, reason: 'must be an array of glob strings' });
116
+ return;
117
+ }
118
+ value.forEach((pattern, i) => {
119
+ const result = validateGlobPattern(pattern);
120
+ if (!result.ok) {
121
+ errors.push({ field: `${field}[${i}]`, reason: result.reason });
122
+ }
123
+ });
124
+ }
125
+
126
+ /**
127
+ * Valida o contrato. Retorna { ok, errors: [{field, reason}], warnings: [{field, reason}] }.
128
+ * `ok === false` quando há ao menos um erro — o preflight deve encerrar antes
129
+ * de qualquer execução (REQ-1).
130
+ */
131
+ function validateContract(contract) {
132
+ const errors = [];
133
+ const warnings = [];
134
+
135
+ if (!isPlainObject(contract)) {
136
+ return { ok: false, errors: [{ field: '(root)', reason: 'contract must be a JSON object' }], warnings };
137
+ }
138
+
139
+ for (const key of Object.keys(contract)) {
140
+ if (!KNOWN_TOP_FIELDS.has(key)) {
141
+ errors.push({ field: key, reason: 'unknown field — check for typos (contract schema invalid)' });
142
+ }
143
+ }
144
+
145
+ if (typeof contract.feature !== 'string' || !contract.feature.trim()) {
146
+ errors.push({ field: 'feature', reason: 'must be a non-empty string' });
147
+ }
148
+
149
+ if (contract.contract_mode !== undefined) {
150
+ if (typeof contract.contract_mode !== 'string' || !VALID_MODES.has(contract.contract_mode.toLowerCase())) {
151
+ errors.push({ field: 'contract_mode', reason: 'must be one of: balanced, safe, builder, autopilot' });
152
+ }
153
+ }
154
+
155
+ if (!isPlainObject(contract.governor)) {
156
+ errors.push({ field: 'governor', reason: 'must be an object' });
157
+ } else {
158
+ for (const key of Object.keys(contract.governor)) {
159
+ if (!KNOWN_GOVERNOR_FIELDS.has(key)) {
160
+ errors.push({ field: `governor.${key}`, reason: 'unknown field check for typos (contract schema invalid)' });
161
+ }
162
+ }
163
+ const g = contract.governor;
164
+ if (g.max_steps !== undefined && g.max_steps !== null && !(Number.isInteger(g.max_steps) && g.max_steps >= 0)) {
165
+ errors.push({ field: 'governor.max_steps', reason: 'must be a non-negative integer' });
166
+ }
167
+ if (g.error_streak_limit !== undefined && g.error_streak_limit !== null && !(Number.isInteger(g.error_streak_limit) && g.error_streak_limit >= 0)) {
168
+ errors.push({ field: 'governor.error_streak_limit', reason: 'must be a non-negative integer' });
169
+ }
170
+ checkOptionalLimit(errors, 'governor.cost_ceiling_tokens', g.cost_ceiling_tokens);
171
+ checkOptionalLimit(errors, 'governor.max_runtime_minutes', g.max_runtime_minutes);
172
+ checkOptionalLimit(errors, 'governor.max_changed_files', g.max_changed_files);
173
+ checkOptionalLimit(errors, 'governor.max_diff_lines', g.max_diff_lines);
174
+ }
175
+
176
+ if (contract.allowed_files !== undefined) {
177
+ checkGlobArray(errors, 'allowed_files', contract.allowed_files);
178
+ // EC-5: allowlist vazia bloquearia tudowarning + tratada como ausente
179
+ if (Array.isArray(contract.allowed_files) && contract.allowed_files.length === 0) {
180
+ warnings.push({
181
+ field: 'allowed_files',
182
+ reason: 'empty allowlist would block every write — treated as absent'
183
+ });
184
+ }
185
+ }
186
+
187
+ if (contract.forbidden_files !== undefined) {
188
+ checkGlobArray(errors, 'forbidden_files', contract.forbidden_files);
189
+ }
190
+
191
+ if (contract.human_gate !== undefined) {
192
+ if (!isPlainObject(contract.human_gate)) {
193
+ errors.push({ field: 'human_gate', reason: 'must be an object' });
194
+ } else {
195
+ for (const key of Object.keys(contract.human_gate)) {
196
+ if (!KNOWN_HUMAN_GATE_FIELDS.has(key)) {
197
+ errors.push({ field: `human_gate.${key}`, reason: 'unknown field — check for typos (contract schema invalid)' });
198
+ }
199
+ }
200
+ const hg = contract.human_gate;
201
+ if (!Array.isArray(hg.required_for)) {
202
+ errors.push({ field: 'human_gate.required_for', reason: 'must be an array of themes (required when human_gate is present)' });
203
+ } else {
204
+ hg.required_for.forEach((theme, i) => {
205
+ if (!HUMAN_GATE_THEMES.includes(theme)) {
206
+ errors.push({ field: `human_gate.required_for[${i}]`, reason: `unknown theme "${theme}" — valid: ${HUMAN_GATE_THEMES.join(', ')}` });
207
+ }
208
+ });
209
+ }
210
+ if (hg.themes !== undefined) {
211
+ if (!Array.isArray(hg.themes)) {
212
+ errors.push({ field: 'human_gate.themes', reason: 'must be an array' });
213
+ } else {
214
+ hg.themes.forEach((theme, i) => {
215
+ if (!isPlainObject(theme)) {
216
+ errors.push({ field: `human_gate.themes[${i}]`, reason: 'must be an object { name, paths }' });
217
+ return;
218
+ }
219
+ for (const key of Object.keys(theme)) {
220
+ if (!KNOWN_THEME_FIELDS.has(key)) {
221
+ errors.push({ field: `human_gate.themes[${i}].${key}`, reason: 'unknown fieldcheck for typos (contract schema invalid)' });
222
+ }
223
+ }
224
+ if (!HUMAN_GATE_THEMES.includes(theme.name)) {
225
+ errors.push({ field: `human_gate.themes[${i}].name`, reason: `unknown theme "${theme.name}" — valid: ${HUMAN_GATE_THEMES.join(', ')}` });
226
+ }
227
+ checkGlobArray(errors, `human_gate.themes[${i}].paths`, theme.paths);
228
+ });
229
+ }
230
+ }
231
+ }
232
+ }
233
+
234
+ if (contract.criteria !== undefined) {
235
+ if (!Array.isArray(contract.criteria)) {
236
+ errors.push({ field: 'criteria', reason: 'must be an array' });
237
+ } else {
238
+ contract.criteria.forEach((criterion, i) => {
239
+ if (!isPlainObject(criterion)) {
240
+ errors.push({ field: `criteria[${i}]`, reason: 'must be an object' });
241
+ return;
242
+ }
243
+ for (const key of Object.keys(criterion)) {
244
+ if (!KNOWN_CRITERIA_FIELDS.has(key)) {
245
+ errors.push({ field: `criteria[${i}].${key}`, reason: 'unknown field check for typos (contract schema invalid)' });
246
+ }
247
+ }
248
+ if (typeof criterion.id !== 'string' || !criterion.id.trim()) {
249
+ errors.push({ field: `criteria[${i}].id`, reason: 'must be a non-empty string' });
250
+ }
251
+ if (criterion.verification !== undefined && (typeof criterion.verification !== 'string' || !criterion.verification.trim())) {
252
+ errors.push({ field: `criteria[${i}].verification`, reason: 'must be a non-empty shell command string when present' });
253
+ }
254
+ if (criterion.binary !== undefined && typeof criterion.binary !== 'boolean') {
255
+ errors.push({ field: `criteria[${i}].binary`, reason: 'must be a boolean' });
256
+ }
257
+
258
+ // SG-* static criteria must_match / must_not_match are arrays of
259
+ // non-empty pattern strings; files[] is the (non-empty) set they read.
260
+ for (const patField of ['must_match', 'must_not_match']) {
261
+ if (criterion[patField] === undefined) continue;
262
+ if (!Array.isArray(criterion[patField])) {
263
+ errors.push({ field: `criteria[${i}].${patField}`, reason: 'must be an array of non-empty pattern strings' });
264
+ continue;
265
+ }
266
+ criterion[patField].forEach((pat, j) => {
267
+ if (typeof pat !== 'string' || !pat.trim()) {
268
+ errors.push({ field: `criteria[${i}].${patField}[${j}]`, reason: 'must be a non-empty string' });
269
+ }
270
+ });
271
+ }
272
+ if (criterion.files !== undefined) {
273
+ if (!Array.isArray(criterion.files)) {
274
+ errors.push({ field: `criteria[${i}].files`, reason: 'must be an array of file paths' });
275
+ } else {
276
+ criterion.files.forEach((fp, j) => {
277
+ if (typeof fp !== 'string' || !fp.trim()) {
278
+ errors.push({ field: `criteria[${i}].files[${j}]`, reason: 'must be a non-empty file path' });
279
+ }
280
+ });
281
+ }
282
+ }
283
+ const isStatic =
284
+ (Array.isArray(criterion.must_match) && criterion.must_match.length > 0) ||
285
+ (Array.isArray(criterion.must_not_match) && criterion.must_not_match.length > 0);
286
+ if (isStatic) {
287
+ // A static criterion needs files[] to read.
288
+ if (!Array.isArray(criterion.files) || criterion.files.length === 0) {
289
+ errors.push({ field: `criteria[${i}].files`, reason: 'a static criterion (must_match / must_not_match) requires a non-empty files[] to read' });
290
+ }
291
+ // A criterion is either runtime (verification) or static (patterns), not both.
292
+ if (criterion.verification !== undefined) {
293
+ errors.push({ field: `criteria[${i}].verification`, reason: 'a criterion is either runtime (verification) or static (must_match / must_not_match), not both — split into two criteria' });
294
+ }
295
+ }
296
+
297
+ // Cobertura executável: critério binário sem verification continua
298
+ // válido (julgado pelo @validator), mas é dívida de verificação. Um
299
+ // critério SG-* estático é checável deterministicamente — não é dívida.
300
+ if (criterion.binary === true && criterion.verification === undefined && !isStatic) {
301
+ warnings.push({
302
+ field: `criteria[${i}].verification`,
303
+ reason: `binary criterion "${criterion.id}" has no executable verification command — @validator will LLM-judge it`
304
+ });
305
+ }
306
+ });
307
+ }
308
+ }
309
+
310
+ return { ok: errors.length === 0, errors, warnings };
311
+ }
312
+
313
+ /**
314
+ * Resolve o contrato VALIDADO para sua forma efetiva:
315
+ * - preset do contract_mode preenche valores do governor não definidos (REQ-19);
316
+ * - `forbidden_files` mesclado com os defaults não-removíveis (REQ-4);
317
+ * - `allowed_files: []` tratado como ausente (EC-5);
318
+ * - mapa tema→paths resolvido (override substitui, não mescla).
319
+ *
320
+ * Não muta o contrato original; retorna um objeto efetivo para os guards.
321
+ */
322
+ function resolveContract(contract) {
323
+ const mode = typeof contract.contract_mode === 'string'
324
+ ? contract.contract_mode.toLowerCase()
325
+ : 'balanced';
326
+ const preset = CONTRACT_PRESETS[mode] || null;
327
+
328
+ const governor = { ...(contract.governor || {}) };
329
+ if (preset) {
330
+ for (const [key, value] of Object.entries(preset)) {
331
+ if (governor[key] === undefined) governor[key] = value;
332
+ }
333
+ }
334
+
335
+ const forbidden = [
336
+ ...DEFAULT_FORBIDDEN_GLOBS,
337
+ ...(Array.isArray(contract.forbidden_files) ? contract.forbidden_files : [])
338
+ ];
339
+
340
+ const allowed = Array.isArray(contract.allowed_files) && contract.allowed_files.length > 0
341
+ ? contract.allowed_files.slice()
342
+ : null;
343
+
344
+ const themePaths = { ...DEFAULT_THEME_PATHS };
345
+ const requiredFor = contract.human_gate && Array.isArray(contract.human_gate.required_for)
346
+ ? contract.human_gate.required_for.slice()
347
+ : [];
348
+ if (contract.human_gate && Array.isArray(contract.human_gate.themes)) {
349
+ for (const theme of contract.human_gate.themes) {
350
+ if (theme && HUMAN_GATE_THEMES.includes(theme.name) && Array.isArray(theme.paths)) {
351
+ themePaths[theme.name] = theme.paths.slice();
352
+ }
353
+ }
354
+ }
355
+
356
+ return {
357
+ feature: contract.feature,
358
+ contract_mode: mode,
359
+ governor,
360
+ criteria: Array.isArray(contract.criteria) ? contract.criteria : [],
361
+ allowed_files: allowed,
362
+ forbidden_files: forbidden,
363
+ human_gate: {
364
+ required_for: requiredFor,
365
+ theme_paths: themePaths
366
+ }
367
+ };
368
+ }
369
+
370
+ module.exports = {
371
+ DEFAULT_FORBIDDEN_GLOBS,
372
+ HUMAN_GATE_THEMES,
373
+ DEFAULT_THEME_PATHS,
374
+ CONTRACT_PRESETS,
375
+ validateContract,
376
+ resolveContract
377
+ };