@jaimevalasek/aioson 1.36.0 → 1.37.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.
- package/CHANGELOG.md +28 -0
- package/docs/en/1-understand/ecosystem-map.md +1 -1
- package/docs/en/1-understand/glossary.md +1 -1
- package/docs/en/2-start/first-project.md +1 -1
- package/docs/en/2-start/initial-decisions.md +4 -2
- package/docs/en/3-recipes/from-idea-to-prd-via-briefing.md +8 -1
- package/docs/en/3-recipes/full-feature-with-sheldon.md +3 -1
- package/docs/en/4-agents/README.md +6 -3
- package/docs/en/4-agents/briefing-refiner.md +146 -0
- package/docs/en/5-reference/README.md +1 -0
- package/docs/en/5-reference/autopilot-handoff.md +286 -0
- package/docs/en/5-reference/cli-reference.md +6 -0
- package/docs/pt/1-entender/glossario.md +1 -1
- package/docs/pt/1-entender/mapa-do-ecossistema.md +1 -1
- package/docs/pt/2-comecar/decisoes-iniciais.md +4 -2
- package/docs/pt/2-comecar/primeiro-projeto.md +1 -1
- package/docs/pt/3-receitas/da-ideia-ao-prd-via-briefing.md +8 -1
- package/docs/pt/3-receitas/feature-completa-com-sheldon.md +3 -1
- package/docs/pt/4-agentes/README.md +13 -11
- package/docs/pt/4-agentes/briefing-refiner.md +64 -40
- package/docs/pt/4-agentes/briefing.md +6 -1
- package/docs/pt/4-agentes/dev.md +19 -1
- package/docs/pt/4-agentes/deyvin.md +4 -0
- package/docs/pt/4-agentes/discover.md +4 -0
- package/docs/pt/4-agentes/neo.md +4 -0
- package/docs/pt/4-agentes/orache.md +6 -0
- package/docs/pt/4-agentes/orchestrator.md +12 -0
- package/docs/pt/4-agentes/pentester.md +6 -0
- package/docs/pt/4-agentes/product.md +19 -1
- package/docs/pt/4-agentes/qa.md +10 -2
- package/docs/pt/4-agentes/setup.md +3 -1
- package/docs/pt/4-agentes/sheldon.md +12 -0
- package/docs/pt/4-agentes/tester.md +6 -0
- package/docs/pt/4-agentes/ux-ui.md +2 -1
- package/docs/pt/5-referencia/README.md +1 -1
- package/docs/pt/5-referencia/agent-chain-continuity.md +1 -1
- package/docs/pt/5-referencia/autopilot-handoff.md +191 -74
- package/docs/pt/5-referencia/comandos-cli.md +16 -7
- package/docs/pt/5-referencia/skills.md +2 -0
- package/docs/pt/agentes.md +3 -1
- package/package.json +1 -1
- package/src/agents.js +1 -1
- package/src/artifact-kinds.js +2 -1
- package/src/autopilot-signal.js +71 -0
- package/src/cli.js +9 -1
- package/src/commands/agents.js +18 -2
- package/src/commands/briefing.js +337 -1
- package/src/commands/feature-close.js +136 -43
- package/src/commands/live.js +47 -11
- package/src/commands/update.js +5 -1
- package/src/commands/verification-plan.js +28 -1
- package/src/commands/verify-artifact.js +64 -1
- package/src/commands/workflow-execute.js +149 -31
- package/src/commands/workflow-next.js +60 -16
- package/src/doctor.js +4 -2
- package/src/i18n/messages/en.js +2 -1
- package/src/i18n/messages/es.js +2 -1
- package/src/i18n/messages/fr.js +2 -1
- package/src/i18n/messages/pt-BR.js +2 -1
- package/src/lib/briefing-refiner/apply-feedback.js +18 -4
- package/src/lib/briefing-refiner/feedback-schema.js +73 -4
- package/src/lib/briefing-refiner/refinement-report.js +11 -0
- package/src/lib/briefing-refiner/review-html.js +388 -68
- package/src/parser.js +6 -0
- package/template/.aioson/agents/briefing-refiner.md +87 -47
- package/template/.aioson/agents/briefing.md +4 -0
- package/template/.aioson/agents/dev.md +9 -2
- package/template/.aioson/agents/deyvin.md +4 -0
- package/template/.aioson/agents/discover.md +4 -0
- package/template/.aioson/agents/neo.md +4 -0
- package/template/.aioson/agents/orache.md +4 -0
- package/template/.aioson/agents/orchestrator.md +16 -0
- package/template/.aioson/agents/pentester.md +4 -0
- package/template/.aioson/agents/product.md +25 -1
- package/template/.aioson/agents/qa.md +5 -1
- package/template/.aioson/agents/sheldon.md +9 -1
- package/template/.aioson/agents/tester.md +4 -0
- package/template/.aioson/agents/ux-ui.md +1 -1
- package/template/.aioson/docs/agent-help.md +126 -0
- package/template/.aioson/docs/autopilot-handoff.md +26 -16
- package/template/.aioson/docs/dev/phase-loop.md +8 -5
- package/template/.aioson/docs/play/llm-data-and-bindings.md +70 -7
- package/template/.aioson/skills/design/interface-design/SKILL.md +17 -0
- package/template/AGENTS.md +36 -36
- package/template/CLAUDE.md +1 -1
package/src/doctor.js
CHANGED
|
@@ -228,8 +228,10 @@ async function runDoctor(targetDir) {
|
|
|
228
228
|
}
|
|
229
229
|
|
|
230
230
|
// Autopilot handoff: protocol doc installed but flag never declared in the
|
|
231
|
-
// context frontmatter
|
|
232
|
-
//
|
|
231
|
+
// context frontmatter. Absent is no longer "silently manual" — @product asks
|
|
232
|
+
// the run mode on screen at each feature kickoff. Declaring an explicit
|
|
233
|
+
// true/false sets a project default (skip the kickoff question) and passes;
|
|
234
|
+
// the advisory just surfaces that a default can be set.
|
|
233
235
|
const autopilotDocExists = await exists(path.join(targetDir, '.aioson/docs/autopilot-handoff.md'));
|
|
234
236
|
if (autopilotDocExists && contextValidation.exists && contextValidation.data) {
|
|
235
237
|
const autoHandoffDeclared = Object.prototype.hasOwnProperty.call(contextValidation.data, 'auto_handoff');
|
package/src/i18n/messages/en.js
CHANGED
|
@@ -412,6 +412,7 @@ module.exports = {
|
|
|
412
412
|
update: {
|
|
413
413
|
not_installed: 'No AIOSON installation found in {targetDir}.',
|
|
414
414
|
done_at: 'Update completed at: {targetDir}',
|
|
415
|
+
template_version: 'Template version applied: {version}',
|
|
415
416
|
files_updated: 'Files updated: {count}',
|
|
416
417
|
backups_created: 'Backups created: {count}',
|
|
417
418
|
profile_renamed: 'i Profile `beginner` renamed to `creator` in project.context.md to better describe the user. Behavior unchanged. Edit the file to switch to `developer` if desired.',
|
|
@@ -491,7 +492,7 @@ module.exports = {
|
|
|
491
492
|
features_dir_present: 'Features directory present (.aioson/context/features/)',
|
|
492
493
|
features_dir_present_hint: 'Create .aioson/context/features/ to host per-feature dossiers (doctor --fix will create it).',
|
|
493
494
|
auto_handoff_declared: 'Autopilot handoff flag declared (auto_handoff in project.context.md)',
|
|
494
|
-
auto_handoff_declared_hint: 'The autopilot-handoff protocol is installed
|
|
495
|
+
auto_handoff_declared_hint: 'The autopilot-handoff protocol is installed and auto_handoff is not set — @product asks the run mode on screen at each feature kickoff (Autopilot / Step by step / Always). Set auto_handoff: true to always autopilot and skip the question, or false to always go step-by-step.',
|
|
495
496
|
claude_commands_present: 'Claude slash commands present ({missing} missing of {required})',
|
|
496
497
|
claude_commands_present_hint: 'Missing: {paths}. Run `aioson doctor . --fix` to restore them from the template.',
|
|
497
498
|
version_drift: 'CLI version matches project.context.md (context: {context}, CLI: {cli})',
|
package/src/i18n/messages/es.js
CHANGED
|
@@ -280,6 +280,7 @@ module.exports = {
|
|
|
280
280
|
update: {
|
|
281
281
|
not_installed: 'No se encontro instalacion de AIOSON en {targetDir}.',
|
|
282
282
|
done_at: 'Actualizacion completada en: {targetDir}',
|
|
283
|
+
template_version: 'Version de template aplicada: {version}',
|
|
283
284
|
files_updated: 'Archivos actualizados: {count}',
|
|
284
285
|
backups_created: 'Backups creados: {count}',
|
|
285
286
|
profile_renamed: 'i Perfil `beginner` renombrado a `creator` en project.context.md para describir mejor al usuario. Comportamiento sin cambios. Edita el archivo para cambiar a `developer` si lo prefieres.',
|
|
@@ -365,7 +366,7 @@ module.exports = {
|
|
|
365
366
|
features_dir_present: 'Directorio de features presente (.aioson/context/features/)',
|
|
366
367
|
features_dir_present_hint: 'Cree .aioson/context/features/ para hospedar dossiers por feature (doctor --fix lo crea).',
|
|
367
368
|
auto_handoff_declared: 'Flag de autopilot handoff declarada (auto_handoff en project.context.md)',
|
|
368
|
-
auto_handoff_declared_hint: 'El protocolo autopilot-handoff esta instalado
|
|
369
|
+
auto_handoff_declared_hint: 'El protocolo autopilot-handoff esta instalado y auto_handoff no esta definido — @product pregunta el modo en pantalla al inicio de cada feature (Autopilot / Paso a paso / Siempre). Defina auto_handoff: true para siempre autopilot y omitir la pregunta, o false para siempre paso a paso.',
|
|
369
370
|
claude_commands_present: 'Slash commands de Claude presentes ({missing} ausentes de {required})',
|
|
370
371
|
claude_commands_present_hint: 'Ausentes: {paths}. Ejecute `aioson doctor . --fix` para restaurarlos.',
|
|
371
372
|
version_drift: 'Version del CLI coincide con project.context.md (contexto: {context}, CLI: {cli})',
|
package/src/i18n/messages/fr.js
CHANGED
|
@@ -279,6 +279,7 @@ module.exports = {
|
|
|
279
279
|
update: {
|
|
280
280
|
not_installed: 'Aucune installation AIOSON trouvee dans {targetDir}.',
|
|
281
281
|
done_at: 'Mise a jour terminee dans : {targetDir}',
|
|
282
|
+
template_version: 'Version du template appliquee : {version}',
|
|
282
283
|
files_updated: 'Fichiers mis a jour : {count}',
|
|
283
284
|
backups_created: 'Sauvegardes creees : {count}',
|
|
284
285
|
profile_renamed: 'i Profil `beginner` renomme en `creator` dans project.context.md pour mieux decrire l utilisateur. Comportement inchange. Modifiez le fichier pour passer a `developer` si vous le souhaitez.',
|
|
@@ -364,7 +365,7 @@ module.exports = {
|
|
|
364
365
|
features_dir_present: 'Repertoire features present (.aioson/context/features/)',
|
|
365
366
|
features_dir_present_hint: 'Creez .aioson/context/features/ pour heberger les dossiers par feature (doctor --fix le cree).',
|
|
366
367
|
auto_handoff_declared: 'Flag autopilot handoff declare (auto_handoff dans project.context.md)',
|
|
367
|
-
auto_handoff_declared_hint: 'Le protocole autopilot-handoff est installe
|
|
368
|
+
auto_handoff_declared_hint: 'Le protocole autopilot-handoff est installe et auto_handoff n\'est pas defini — @product demande le mode a l\'ecran au debut de chaque feature (Autopilot / Pas a pas / Toujours). Definissez auto_handoff: true pour toujours autopilot et sauter la question, ou false pour toujours pas a pas.',
|
|
368
369
|
claude_commands_present: 'Slash commands de Claude presents ({missing} absents sur {required})',
|
|
369
370
|
claude_commands_present_hint: 'Absents : {paths}. Executez `aioson doctor . --fix` pour les restaurer.',
|
|
370
371
|
version_drift: 'Version du CLI conforme a project.context.md (contexte : {context}, CLI : {cli})',
|
|
@@ -379,6 +379,7 @@ module.exports = {
|
|
|
379
379
|
update: {
|
|
380
380
|
not_installed: 'Nenhuma instalacao do AIOSON encontrada em {targetDir}.',
|
|
381
381
|
done_at: 'Atualizacao concluida em: {targetDir}',
|
|
382
|
+
template_version: 'Versao do template aplicada: {version}',
|
|
382
383
|
files_updated: 'Arquivos atualizados: {count}',
|
|
383
384
|
backups_created: 'Backups criados: {count}',
|
|
384
385
|
profile_renamed: 'i Perfil `beginner` renomeado para `creator` em project.context.md para descrever melhor o usuario. Comportamento inalterado. Edite o arquivo para mudar para `developer` se preferir.',
|
|
@@ -465,7 +466,7 @@ module.exports = {
|
|
|
465
466
|
features_dir_present: 'Diretorio de features presente (.aioson/context/features/)',
|
|
466
467
|
features_dir_present_hint: 'Crie .aioson/context/features/ para hospedar dossies por feature (doctor --fix cria automaticamente).',
|
|
467
468
|
auto_handoff_declared: 'Flag de autopilot handoff declarada (auto_handoff no project.context.md)',
|
|
468
|
-
auto_handoff_declared_hint: 'O protocolo autopilot-handoff esta instalado
|
|
469
|
+
auto_handoff_declared_hint: 'O protocolo autopilot-handoff esta instalado e auto_handoff nao esta definido — o @product pergunta o modo na tela no inicio de cada feature (Autopilot / Passo a passo / Sempre). Defina auto_handoff: true para sempre autopilot e pular a pergunta, ou false para sempre passo a passo.',
|
|
469
470
|
claude_commands_present: 'Slash commands do Claude presentes ({missing} ausentes de {required})',
|
|
470
471
|
claude_commands_present_hint: 'Ausentes: {paths}. Rode `aioson doctor . --fix` para restaurar a partir do template.',
|
|
471
472
|
version_drift: 'Versao do CLI bate com project.context.md (contexto: {context}, CLI: {cli})',
|
|
@@ -39,7 +39,10 @@ function summarizeSkippedChanges(feedback) {
|
|
|
39
39
|
return [...sectionChanges, ...decisionChanges];
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
// Declining never writes to briefings.md, so a stale source hash is irrelevant
|
|
43
|
+
// to safety — validating it would only dead-end the user whose briefing moved
|
|
44
|
+
// on. Hence allowStale defaults to TRUE here (and only here).
|
|
45
|
+
async function applyDeclinedFeedback(projectDir, slug, feedback, { allowStale = true } = {}) {
|
|
43
46
|
const briefingPath = resolveBriefingPath(projectDir, slug, 'briefings.md');
|
|
44
47
|
const currentMarkdown = await fs.readFile(briefingPath, 'utf8');
|
|
45
48
|
const currentSourceHash = hashText(currentMarkdown);
|
|
@@ -57,10 +60,12 @@ async function applyDeclinedFeedback(projectDir, slug, feedback, { allowStale =
|
|
|
57
60
|
source_hash: feedback.source_hash,
|
|
58
61
|
applied_hash: currentSourceHash,
|
|
59
62
|
status: 'declined',
|
|
63
|
+
round: feedback.round || 1,
|
|
60
64
|
applied_changes: [],
|
|
61
65
|
skipped_changes: skippedChanges,
|
|
62
66
|
unresolved_comments: unresolvedComments,
|
|
63
67
|
blocking_items: feedback.blocking_items || [],
|
|
68
|
+
findings: feedback.findings || [],
|
|
64
69
|
next_action: 'rerun_review'
|
|
65
70
|
});
|
|
66
71
|
|
|
@@ -104,15 +109,22 @@ async function applyConfirmedFeedback(projectDir, slug, feedback, { confirmed =
|
|
|
104
109
|
if (returnedToDraft) {
|
|
105
110
|
returnApprovedBriefingToDraft(registry, slug);
|
|
106
111
|
}
|
|
112
|
+
// A blocking finding the user left pending blocks the PRD the same way an
|
|
113
|
+
// explicit blocking item does — even if the export step never mirrored it
|
|
114
|
+
// into blocking_items (hand-written feedback).
|
|
115
|
+
const pendingBlockingFindings = (feedback.findings || []).filter(
|
|
116
|
+
(finding) => finding.blocking && finding.status === 'pending'
|
|
117
|
+
);
|
|
118
|
+
const hasBlockers = (feedback.blocking_items || []).length > 0 || pendingBlockingFindings.length > 0;
|
|
107
119
|
markRefinementState(registry, slug, {
|
|
108
|
-
refinement_status:
|
|
120
|
+
refinement_status: hasBlockers ? 'blocked' : 'applied',
|
|
109
121
|
review_html: `.aioson/briefings/${slug}/review.html`,
|
|
110
122
|
refinement_report: `.aioson/briefings/${slug}/refinement-report.md`
|
|
111
123
|
});
|
|
112
124
|
await writeBriefingRegistry(projectDir, registry);
|
|
113
125
|
|
|
114
126
|
const unresolvedComments = (feedback.comments || []).filter((comment) => !comment.resolved);
|
|
115
|
-
const nextAction =
|
|
127
|
+
const nextAction = hasBlockers ? 'resolve_blockers' : 'approve_briefing';
|
|
116
128
|
const report = buildRefinementReport({
|
|
117
129
|
briefing_slug: slug,
|
|
118
130
|
source_briefing_path: `.aioson/briefings/${slug}/briefings.md`,
|
|
@@ -120,15 +132,17 @@ async function applyConfirmedFeedback(projectDir, slug, feedback, { confirmed =
|
|
|
120
132
|
source_hash: feedback.source_hash,
|
|
121
133
|
applied_hash: appliedHash,
|
|
122
134
|
status: 'applied',
|
|
135
|
+
round: feedback.round || 1,
|
|
123
136
|
applied_changes: appliedChanges,
|
|
124
137
|
skipped_changes: [],
|
|
125
138
|
unresolved_comments: unresolvedComments,
|
|
126
139
|
blocking_items: feedback.blocking_items || [],
|
|
140
|
+
findings: feedback.findings || [],
|
|
127
141
|
next_action: nextAction
|
|
128
142
|
});
|
|
129
143
|
await fs.writeFile(resolveBriefingPath(projectDir, slug, 'refinement-report.md'), report, 'utf8');
|
|
130
144
|
|
|
131
|
-
return { ok: true, appliedChanges, nextAction, appliedHash, returnedToDraft };
|
|
145
|
+
return { ok: true, appliedChanges, nextAction, appliedHash, returnedToDraft, pendingBlockingFindings: pendingBlockingFindings.length };
|
|
132
146
|
}
|
|
133
147
|
|
|
134
148
|
module.exports = { applyConfirmedFeedback, applyDeclinedFeedback };
|
|
@@ -7,6 +7,12 @@ const VALID_SECTION_STATUSES = new Set(['unchanged', 'accepted', 'change_request
|
|
|
7
7
|
const VALID_DECISION_STATUSES = new Set(['accepted', 'change_requested', 'remove', 'blocked', 'note']);
|
|
8
8
|
const VALID_SEVERITIES = new Set(['note', 'suggestion', 'important', 'blocking']);
|
|
9
9
|
const VALID_EXPORT_METHODS = new Set(['file-system-access', 'download', 'copy-paste', 'manual-save']);
|
|
10
|
+
const VALID_FINDING_CATEGORIES = new Set(['ambiguity', 'redundancy', 'gap', 'risk', 'pending-decision', 'scope-suggestion']);
|
|
11
|
+
const VALID_FINDING_SEVERITIES = new Set(['low', 'medium', 'high']);
|
|
12
|
+
const VALID_FINDING_STATUSES = new Set(['pending', 'accepted', 'rejected', 'deferred']);
|
|
13
|
+
// 1.0 = sections/comments/decisions/blocking_items only; 1.1 adds findings[] (the
|
|
14
|
+
// agent's audit surfaced as reviewable items) and the refinement round counter.
|
|
15
|
+
const SUPPORTED_SCHEMA_VERSIONS = new Set(['1.0', '1.1']);
|
|
10
16
|
|
|
11
17
|
// Normalization only — NOT a security boundary. This strips well-formed tags
|
|
12
18
|
// and normalizes newlines so the text round-trips cleanly into briefings.md.
|
|
@@ -34,17 +40,64 @@ function normalizeSection(section) {
|
|
|
34
40
|
};
|
|
35
41
|
}
|
|
36
42
|
|
|
37
|
-
function
|
|
43
|
+
function normalizeFinding(finding, index) {
|
|
44
|
+
return {
|
|
45
|
+
id: String(finding.id || `F${index + 1}`),
|
|
46
|
+
section_id: String(finding.section_id || ''),
|
|
47
|
+
category: VALID_FINDING_CATEGORIES.has(finding.category) ? finding.category : 'gap',
|
|
48
|
+
severity: VALID_FINDING_SEVERITIES.has(finding.severity) ? finding.severity : 'medium',
|
|
49
|
+
blocking: Boolean(finding.blocking),
|
|
50
|
+
text: sanitizeText(finding.text),
|
|
51
|
+
recommendation: sanitizeText(finding.recommendation || ''),
|
|
52
|
+
status: VALID_FINDING_STATUSES.has(finding.status) ? finding.status : 'pending',
|
|
53
|
+
note: sanitizeText(finding.note || '')
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Validate agent-supplied audit findings BEFORE they are baked into the review
|
|
59
|
+
* surface. Strict where normalizeFinding is lenient: the agent must name a real
|
|
60
|
+
* section and a valid category, and every finding must carry text.
|
|
61
|
+
*/
|
|
62
|
+
function validateFindingsInput(findings, { sectionIds = [] } = {}) {
|
|
63
|
+
const errors = [];
|
|
64
|
+
if (!Array.isArray(findings)) {
|
|
65
|
+
return { ok: false, errors: ['findings must be an array'] };
|
|
66
|
+
}
|
|
67
|
+
const known = new Set(sectionIds);
|
|
68
|
+
findings.forEach((finding, index) => {
|
|
69
|
+
const label = finding && finding.id ? String(finding.id) : `#${index + 1}`;
|
|
70
|
+
if (!finding || typeof finding !== 'object') {
|
|
71
|
+
errors.push(`finding ${label} must be an object`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (!String(finding.text || '').trim()) errors.push(`finding ${label} has no text`);
|
|
75
|
+
if (!VALID_FINDING_CATEGORIES.has(finding.category)) {
|
|
76
|
+
errors.push(`finding ${label} has invalid category: ${finding.category}`);
|
|
77
|
+
}
|
|
78
|
+
if (finding.severity !== undefined && !VALID_FINDING_SEVERITIES.has(finding.severity)) {
|
|
79
|
+
errors.push(`finding ${label} has invalid severity: ${finding.severity}`);
|
|
80
|
+
}
|
|
81
|
+
if (known.size > 0 && !known.has(String(finding.section_id || ''))) {
|
|
82
|
+
errors.push(`finding ${label} points at unknown section_id: ${finding.section_id}`);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
return { ok: errors.length === 0, errors };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function buildInitialFeedback({ slug, sourcePath, sourceHash, sections, findings = [], round = 1, exportMethod = 'manual-save', now = new Date() }) {
|
|
38
89
|
const timestamp = now.toISOString();
|
|
39
90
|
return {
|
|
40
|
-
schema_version: '1.
|
|
91
|
+
schema_version: '1.1',
|
|
41
92
|
briefing_slug: slug,
|
|
42
93
|
source_briefing_path: sourcePath,
|
|
43
94
|
source_hash: sourceHash,
|
|
44
95
|
review_generated_at: timestamp,
|
|
45
96
|
last_modified_at: timestamp,
|
|
46
97
|
export_method: exportMethod,
|
|
98
|
+
round: Number.isInteger(round) && round > 0 ? round : 1,
|
|
47
99
|
sections: sections.map(normalizeSection),
|
|
100
|
+
findings: findings.map(normalizeFinding),
|
|
48
101
|
comments: [],
|
|
49
102
|
decisions: [],
|
|
50
103
|
blocking_items: []
|
|
@@ -66,7 +119,7 @@ function validateFeedback(feedback, { slug, currentSourceHash, allowStale = fals
|
|
|
66
119
|
if (!feedback || typeof feedback !== 'object') errors.push('feedback must be an object');
|
|
67
120
|
if (errors.length > 0) return { ok: false, stale: false, errors, warnings };
|
|
68
121
|
|
|
69
|
-
if (feedback.schema_version
|
|
122
|
+
if (!SUPPORTED_SCHEMA_VERSIONS.has(feedback.schema_version)) errors.push('schema_version must be 1.0 or 1.1');
|
|
70
123
|
if (slug && feedback.briefing_slug !== slug) errors.push('briefing_slug does not match selected slug');
|
|
71
124
|
if (!isPathInsideBriefing(feedback.source_briefing_path, feedback.briefing_slug || slug)) {
|
|
72
125
|
errors.push('source_briefing_path must stay inside the selected briefing directory');
|
|
@@ -99,6 +152,17 @@ function validateFeedback(feedback, { slug, currentSourceHash, allowStale = fals
|
|
|
99
152
|
}
|
|
100
153
|
}
|
|
101
154
|
|
|
155
|
+
// findings[] is a 1.1 field; a 1.0 payload legitimately has none.
|
|
156
|
+
if (feedback.schema_version === '1.1' && !Array.isArray(feedback.findings)) {
|
|
157
|
+
errors.push('findings must be an array');
|
|
158
|
+
}
|
|
159
|
+
if (Array.isArray(feedback.findings)) {
|
|
160
|
+
for (const finding of feedback.findings) {
|
|
161
|
+
if (!VALID_FINDING_STATUSES.has(finding.status)) errors.push(`invalid finding status: ${finding.status}`);
|
|
162
|
+
if (!VALID_FINDING_CATEGORIES.has(finding.category)) errors.push(`invalid finding category: ${finding.category}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
102
166
|
const stale = Boolean(currentSourceHash && feedback.source_hash !== currentSourceHash);
|
|
103
167
|
if (stale) warnings.push('source_hash differs from current briefings.md');
|
|
104
168
|
if (stale && !allowStale) errors.push('feedback is stale');
|
|
@@ -116,7 +180,12 @@ function assertFeedbackPath(projectDir, slug, candidatePath) {
|
|
|
116
180
|
|
|
117
181
|
module.exports = {
|
|
118
182
|
buildInitialFeedback,
|
|
183
|
+
normalizeFinding,
|
|
119
184
|
sanitizeText,
|
|
120
185
|
validateFeedback,
|
|
121
|
-
|
|
186
|
+
validateFindingsInput,
|
|
187
|
+
assertFeedbackPath,
|
|
188
|
+
VALID_FINDING_CATEGORIES,
|
|
189
|
+
VALID_FINDING_SEVERITIES,
|
|
190
|
+
VALID_FINDING_STATUSES
|
|
122
191
|
};
|
|
@@ -7,6 +7,15 @@ function list(items, formatter) {
|
|
|
7
7
|
|
|
8
8
|
function buildRefinementReport(data) {
|
|
9
9
|
const nextAction = data.next_action || 'rerun_review';
|
|
10
|
+
const findings = Array.isArray(data.findings) ? data.findings : [];
|
|
11
|
+
const findingsBlock = findings.length > 0
|
|
12
|
+
? [
|
|
13
|
+
'## Findings',
|
|
14
|
+
'',
|
|
15
|
+
list(findings, (finding) => `${finding.id} [${finding.category}/${finding.severity}${finding.blocking ? '/blocking' : ''}] (${finding.status}) ${finding.section_id}: ${finding.text}`),
|
|
16
|
+
''
|
|
17
|
+
]
|
|
18
|
+
: [];
|
|
10
19
|
return [
|
|
11
20
|
`# Refinement Report — ${data.briefing_slug}`,
|
|
12
21
|
'',
|
|
@@ -15,8 +24,10 @@ function buildRefinementReport(data) {
|
|
|
15
24
|
`- Source hash: ${data.source_hash || '-'}`,
|
|
16
25
|
`- Applied hash: ${data.applied_hash || '-'}`,
|
|
17
26
|
`- Status: ${data.status || 'review_generated'}`,
|
|
27
|
+
`- Round: ${data.round || 1}`,
|
|
18
28
|
`- Next action: ${nextAction}`,
|
|
19
29
|
'',
|
|
30
|
+
...findingsBlock,
|
|
20
31
|
'## Applied Changes',
|
|
21
32
|
'',
|
|
22
33
|
list(data.applied_changes, (change) => `${change.section_id || change.title}: ${change.summary || 'updated'}`),
|