@jaimevalasek/aioson 1.30.1 → 1.33.1

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 (136) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +19 -6
  3. package/docs/design-previews/cognitive-core-ui-auth.html +95 -0
  4. package/docs/design-previews/cognitive-core-ui-kanban.html +231 -0
  5. package/docs/design-previews/cognitive-core-ui-list-detail.html +174 -0
  6. package/docs/design-previews/cognitive-core-ui-preview.css +1315 -0
  7. package/docs/design-previews/cognitive-core-ui-settings.html +142 -0
  8. package/docs/design-previews/cognitive-core-ui-website.html +190 -1009
  9. package/docs/design-previews/cognitive-core-ui.html +281 -463
  10. package/docs/design-previews/index.html +83 -31
  11. package/docs/en/5-reference/cli-reference.md +1 -1
  12. package/docs/pt/5-referencia/comandos-cli.md +1 -1
  13. package/docs/pt/5-referencia/harness-retro.md +2 -1
  14. package/package.json +1 -1
  15. package/src/cli.js +38 -21
  16. package/src/commands/classify.js +389 -327
  17. package/src/commands/harness-retro-promote.js +387 -0
  18. package/src/commands/prototype-check.js +163 -0
  19. package/src/commands/verify-implementation.js +428 -0
  20. package/src/commands/workflow-next.js +222 -9
  21. package/src/constants.js +569 -511
  22. package/src/lib/retro/retro-render.js +10 -1
  23. package/src/lib/retro/retro-sources.js +45 -27
  24. package/src/lib/retro/verification-reports.js +230 -0
  25. package/src/runtime-store.js +13 -9
  26. package/src/verification/evidence-bundle.js +251 -0
  27. package/src/verification/ledger-store.js +221 -0
  28. package/src/verification/path-policy.js +74 -0
  29. package/src/verification/policy-engine.js +95 -0
  30. package/src/verification/prompt-package.js +314 -0
  31. package/src/verification/redaction.js +77 -0
  32. package/src/verification/report-parser.js +132 -0
  33. package/src/verification/report-store.js +97 -0
  34. package/src/verification/result.js +16 -0
  35. package/src/verification/runners/index.js +319 -0
  36. package/src/verification/runtime-telemetry.js +144 -0
  37. package/src/verification/schema.js +276 -0
  38. package/src/verification/source-discovery.js +153 -0
  39. package/template/.aioson/agents/analyst.md +9 -6
  40. package/template/.aioson/agents/briefing-refiner.md +22 -0
  41. package/template/.aioson/agents/briefing.md +69 -12
  42. package/template/.aioson/agents/design-hybrid-forge.md +18 -14
  43. package/template/.aioson/agents/dev.md +23 -10
  44. package/template/.aioson/agents/deyvin.md +3 -2
  45. package/template/.aioson/agents/product.md +15 -11
  46. package/template/.aioson/agents/qa.md +13 -4
  47. package/template/.aioson/agents/scope-check.md +19 -5
  48. package/template/.aioson/agents/sheldon.md +4 -3
  49. package/template/.aioson/agents/ux-ui.md +3 -2
  50. package/template/.aioson/docs/feature-expansion-taxonomy.md +31 -3
  51. package/template/.aioson/docs/play/README.md +2 -0
  52. package/template/.aioson/docs/play/agent-usage-guide.md +7 -1
  53. package/template/.aioson/docs/play/app-compatibility-guide.md +6 -1
  54. package/template/.aioson/docs/play/auth-services-and-testing.md +16 -1
  55. package/template/.aioson/docs/play/manifest-and-runtime.md +23 -0
  56. package/template/.aioson/docs/prototype-contract.md +81 -0
  57. package/template/.aioson/skills/design/aurora-command-ui/SKILL.md +266 -243
  58. package/template/.aioson/skills/design/aurora-command-ui/references/art-direction.md +293 -293
  59. package/template/.aioson/skills/design/aurora-command-ui/references/components.md +827 -827
  60. package/template/.aioson/skills/design/aurora-command-ui/references/dashboards.md +250 -250
  61. package/template/.aioson/skills/design/aurora-command-ui/references/design-tokens.md +585 -585
  62. package/template/.aioson/skills/design/aurora-command-ui/references/motion.md +365 -365
  63. package/template/.aioson/skills/design/aurora-command-ui/references/patterns.md +485 -482
  64. package/template/.aioson/skills/design/aurora-command-ui/references/websites.md +386 -387
  65. package/template/.aioson/skills/design/bold-editorial-ui/SKILL.md +228 -205
  66. package/template/.aioson/skills/design/bold-editorial-ui/references/art-direction.md +338 -338
  67. package/template/.aioson/skills/design/bold-editorial-ui/references/components.md +977 -977
  68. package/template/.aioson/skills/design/bold-editorial-ui/references/dashboards.md +218 -218
  69. package/template/.aioson/skills/design/bold-editorial-ui/references/design-tokens.md +326 -326
  70. package/template/.aioson/skills/design/bold-editorial-ui/references/motion.md +461 -461
  71. package/template/.aioson/skills/design/bold-editorial-ui/references/patterns.md +293 -293
  72. package/template/.aioson/skills/design/bold-editorial-ui/references/websites.md +352 -352
  73. package/template/.aioson/skills/design/clean-saas-ui/SKILL.md +233 -210
  74. package/template/.aioson/skills/design/clean-saas-ui/references/art-direction.md +319 -319
  75. package/template/.aioson/skills/design/clean-saas-ui/references/components.md +365 -365
  76. package/template/.aioson/skills/design/clean-saas-ui/references/dashboards.md +196 -196
  77. package/template/.aioson/skills/design/clean-saas-ui/references/design-tokens.md +244 -244
  78. package/template/.aioson/skills/design/clean-saas-ui/references/motion.md +235 -235
  79. package/template/.aioson/skills/design/clean-saas-ui/references/patterns.md +215 -215
  80. package/template/.aioson/skills/design/clean-saas-ui/references/websites.md +295 -295
  81. package/template/.aioson/skills/design/cognitive-core-ui/SKILL.md +239 -203
  82. package/template/.aioson/skills/design/cognitive-core-ui/references/art-direction.md +339 -339
  83. package/template/.aioson/skills/design/cognitive-core-ui/references/components.md +417 -407
  84. package/template/.aioson/skills/design/cognitive-core-ui/references/dashboards.md +289 -272
  85. package/template/.aioson/skills/design/cognitive-core-ui/references/design-tokens.md +525 -524
  86. package/template/.aioson/skills/design/cognitive-core-ui/references/motion.md +279 -279
  87. package/template/.aioson/skills/design/cognitive-core-ui/references/patterns.md +355 -289
  88. package/template/.aioson/skills/design/cognitive-core-ui/references/websites.md +443 -437
  89. package/template/.aioson/skills/design/glassmorphism-ui/SKILL.md +245 -222
  90. package/template/.aioson/skills/design/glassmorphism-ui/references/art-direction.md +159 -159
  91. package/template/.aioson/skills/design/glassmorphism-ui/references/components.md +498 -498
  92. package/template/.aioson/skills/design/glassmorphism-ui/references/dashboards.md +236 -236
  93. package/template/.aioson/skills/design/glassmorphism-ui/references/design-tokens.md +274 -274
  94. package/template/.aioson/skills/design/glassmorphism-ui/references/motion.md +355 -355
  95. package/template/.aioson/skills/design/glassmorphism-ui/references/patterns.md +198 -198
  96. package/template/.aioson/skills/design/glassmorphism-ui/references/websites.md +307 -307
  97. package/template/.aioson/skills/design/interface-design/SKILL.md +68 -47
  98. package/template/.aioson/skills/design/interface-design/references/components-and-states.md +105 -105
  99. package/template/.aioson/skills/design/interface-design/references/design-directions.md +101 -101
  100. package/template/.aioson/skills/design/interface-design/references/handoff-and-quality.md +92 -71
  101. package/template/.aioson/skills/design/interface-design/references/intent-and-domain.md +74 -74
  102. package/template/.aioson/skills/design/interface-design/references/tokens-and-depth.md +173 -173
  103. package/template/.aioson/skills/design/neo-brutalist-ui/SKILL.md +236 -213
  104. package/template/.aioson/skills/design/neo-brutalist-ui/references/art-direction.md +228 -228
  105. package/template/.aioson/skills/design/neo-brutalist-ui/references/components.md +855 -855
  106. package/template/.aioson/skills/design/neo-brutalist-ui/references/dashboards.md +334 -334
  107. package/template/.aioson/skills/design/neo-brutalist-ui/references/design-tokens.md +342 -342
  108. package/template/.aioson/skills/design/neo-brutalist-ui/references/motion.md +286 -286
  109. package/template/.aioson/skills/design/neo-brutalist-ui/references/patterns.md +458 -458
  110. package/template/.aioson/skills/design/neo-brutalist-ui/references/websites.md +723 -723
  111. package/template/.aioson/skills/design/premium-command-center-ui/SKILL.md +83 -62
  112. package/template/.aioson/skills/design/premium-command-center-ui/references/operations.md +74 -74
  113. package/template/.aioson/skills/design/premium-command-center-ui/references/patterns.md +116 -116
  114. package/template/.aioson/skills/design/premium-command-center-ui/references/validation.md +47 -47
  115. package/template/.aioson/skills/design/premium-command-center-ui/references/visual-system.md +215 -215
  116. package/template/.aioson/skills/design/pt.squarespace.com/.skill-meta.json +31 -31
  117. package/template/.aioson/skills/design/pt.squarespace.com/SKILL.md +94 -66
  118. package/template/.aioson/skills/design/pt.squarespace.com/references/components.md +366 -366
  119. package/template/.aioson/skills/design/pt.squarespace.com/references/design-tokens.md +150 -150
  120. package/template/.aioson/skills/design/pt.squarespace.com/references/motion.md +270 -270
  121. package/template/.aioson/skills/design/pt.squarespace.com/references/patterns.md +189 -189
  122. package/template/.aioson/skills/design/pt.squarespace.com/references/websites.md +161 -161
  123. package/template/.aioson/skills/design/warm-craft-ui/SKILL.md +232 -209
  124. package/template/.aioson/skills/design/warm-craft-ui/references/art-direction.md +324 -324
  125. package/template/.aioson/skills/design/warm-craft-ui/references/components.md +508 -508
  126. package/template/.aioson/skills/design/warm-craft-ui/references/dashboards.md +223 -223
  127. package/template/.aioson/skills/design/warm-craft-ui/references/design-tokens.md +374 -374
  128. package/template/.aioson/skills/design/warm-craft-ui/references/motion.md +356 -356
  129. package/template/.aioson/skills/design/warm-craft-ui/references/patterns.md +288 -288
  130. package/template/.aioson/skills/design/warm-craft-ui/references/websites.md +289 -289
  131. package/template/.aioson/skills/process/briefing-expansion-scout/SKILL.md +25 -3
  132. package/template/.aioson/skills/process/design-hybrid-forge/SKILL.md +5 -3
  133. package/template/.aioson/skills/process/design-hybrid-forge/references/external-source-ingestion.md +89 -0
  134. package/template/.aioson/skills/process/product-scope-expansion/SKILL.md +29 -2
  135. package/template/.aioson/skills/process/prototype-forge/SKILL.md +92 -0
  136. package/template/.aioson/skills/process/sheldon-expansion-audit/SKILL.md +23 -2
@@ -0,0 +1,387 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * `aioson harness:retro-promote [path] --feature=<slug> [--to=learnings|rules]`
5
+ *
6
+ * Human-approved promotion path for candidates mined by `harness:retro`.
7
+ * Default is dry-run. `--apply --select=<candidate-key|all>` is required before
8
+ * writing `.aioson/learnings/` or `.aioson/rules/`.
9
+ */
10
+
11
+ const fs = require('node:fs/promises');
12
+ const fsSync = require('node:fs');
13
+ const path = require('node:path');
14
+
15
+ const {
16
+ collectSources,
17
+ resolveFeatureExists
18
+ } = require('../lib/retro/retro-sources');
19
+ const { aggregate } = require('../lib/retro/retro-aggregate');
20
+ const { openRuntimeDb, promoteProjectLearning } = require('../runtime-store');
21
+ const { upsertProjectLearning } = require('./devlog-process');
22
+
23
+ const SLUG_RE = /^[a-z0-9][a-z0-9-]*$/;
24
+ const TARGETS = new Set(['learnings', 'rules']);
25
+ const EXIT_OK = 0;
26
+ const EXIT_IO = 1;
27
+ const EXIT_INPUT = 12;
28
+ const BAR = '-'.repeat(42);
29
+
30
+ function relPosix(value) {
31
+ return String(value || '').replace(/\\/g, '/');
32
+ }
33
+
34
+ function nowIso() {
35
+ return new Date().toISOString();
36
+ }
37
+
38
+ function inputError(logger, message, reason) {
39
+ if (logger && typeof logger.error === 'function') logger.error(message);
40
+ process.exitCode = EXIT_INPUT;
41
+ return { ok: false, exitCode: EXIT_INPUT, reason, message };
42
+ }
43
+
44
+ function ioError(logger, message, err) {
45
+ if (logger && typeof logger.error === 'function') logger.error(message);
46
+ process.exitCode = EXIT_IO;
47
+ return { ok: false, exitCode: EXIT_IO, reason: 'io_error', message: err && err.message ? err.message : String(err) };
48
+ }
49
+
50
+ function sanitizeInline(value) {
51
+ return String(value || '')
52
+ .replace(/[\u0000-\u001F\u007F\u200B-\u200F\u2028\u2029\u202A-\u202E\u2066-\u2069\uFEFF]/g, ' ')
53
+ .replace(/\s+/g, ' ')
54
+ .trim();
55
+ }
56
+
57
+ function yamlString(value) {
58
+ return JSON.stringify(sanitizeInline(value));
59
+ }
60
+
61
+ function slugify(value, fallback = 'retro-finding') {
62
+ const slug = String(value || '')
63
+ .toLowerCase()
64
+ .replace(/[^a-z0-9]+/g, '-')
65
+ .replace(/^-+|-+$/g, '')
66
+ .slice(0, 90);
67
+ return slug || fallback;
68
+ }
69
+
70
+ function candidateTitle(candidate) {
71
+ const first = candidate.occurrences && candidate.occurrences[0] ? candidate.occurrences[0] : null;
72
+ const title = first && first.title ? sanitizeInline(first.title) : null;
73
+ const anchor = candidate.finding_id || (candidate.signature ? `sig:${candidate.signature.slice(0, 12)}` : candidate.key);
74
+ return title
75
+ ? `Retro check: ${title}`
76
+ : `Retro check: ${candidate.feature_slug} ${anchor}`;
77
+ }
78
+
79
+ function candidateSummary(candidate) {
80
+ const reasons = Array.isArray(candidate.reasons) ? candidate.reasons.join(', ') : 'unknown';
81
+ return [
82
+ `key=${candidate.key}`,
83
+ `severity=${candidate.max_severity || 'unknown'}`,
84
+ `reasons=${reasons}`,
85
+ `occurrences=${candidate.cost ? candidate.cost.occurrences : (candidate.occurrences || []).length}`,
86
+ `fail_pass_cycles=${candidate.cost ? candidate.cost.fail_pass_cycles : 0}`
87
+ ].join('; ');
88
+ }
89
+
90
+ function occurrenceLines(candidate) {
91
+ const occurrences = Array.isArray(candidate.occurrences) ? candidate.occurrences : [];
92
+ if (occurrences.length === 0) {
93
+ return ['- No single finding occurrence; candidate came from repeated FAIL/PASS cycles.'];
94
+ }
95
+ return occurrences.map((o) => {
96
+ const id = o.finding_id || (o.signature ? `sig:${String(o.signature).slice(0, 12)}` : 'n/a');
97
+ return `- ${relPosix(o.source_path)} | id=${sanitizeInline(id)} | severity=${sanitizeInline(o.severity || 'unknown')} | status=${sanitizeInline(o.status || 'unknown')} | date=${sanitizeInline(o.date || 'unknown')}`;
98
+ });
99
+ }
100
+
101
+ function buildLearningContent(candidate, { dossierRelPath, generatedAt }) {
102
+ const title = candidateTitle(candidate);
103
+ const lines = [
104
+ '---',
105
+ `title: ${yamlString(title)}`,
106
+ `feature: ${candidate.feature_slug}`,
107
+ 'signal_type: retro-verification',
108
+ 'source: harness-retro-promote',
109
+ `source_dossier: ${dossierRelPath}`,
110
+ 'status: active',
111
+ `promoted_at: ${generatedAt}`,
112
+ '---',
113
+ '',
114
+ `# ${sanitizeInline(title)}`,
115
+ '',
116
+ '## Rule of Thumb',
117
+ '',
118
+ 'Before closing a similar implementation, check that this recurring verification issue is explicitly covered by code evidence and tests.',
119
+ '',
120
+ '## Retro Candidate',
121
+ '',
122
+ `- key: ${candidate.key}`,
123
+ `- severity: ${candidate.max_severity || 'unknown'}`,
124
+ `- reasons: ${(candidate.reasons || []).join(', ') || 'unknown'}`,
125
+ `- source dossier: ${dossierRelPath}`,
126
+ '',
127
+ '## Bounded Evidence',
128
+ '',
129
+ ...occurrenceLines(candidate),
130
+ '',
131
+ 'No raw auditor output, stderr, prompt package, or finding evidence text is stored in this learning.',
132
+ ''
133
+ ];
134
+ return lines.join('\n');
135
+ }
136
+
137
+ function buildRuleContent(candidate, { dossierRelPath, generatedAt, ruleName }) {
138
+ const title = candidateTitle(candidate);
139
+ const lines = [
140
+ '---',
141
+ `name: ${ruleName}`,
142
+ `description: ${yamlString(`Prevent recurring implementation verification issue: ${title}`)}`,
143
+ 'agents: [dev, deyvin, scope-check, qa]',
144
+ 'priority: 6',
145
+ 'version: 1.0.0',
146
+ 'modes: [planning, executing, reviewing]',
147
+ 'task_types: [implementation, verification, testing]',
148
+ 'load_tier: trigger',
149
+ `triggers: [${candidate.feature_slug}, implementation verification, retro candidate, recurring miss]`,
150
+ 'retrieval_intents: [implementation, testing, memory]',
151
+ 'paths: [src/**, tests/**, .aioson/context/features/**]',
152
+ '---',
153
+ '',
154
+ `# ${sanitizeInline(title)}`,
155
+ '',
156
+ 'When implementing or verifying similar behavior, explicitly check this recurring retro issue before claiming the feature is done.',
157
+ '',
158
+ '## Candidate Metadata',
159
+ '',
160
+ `- key: ${candidate.key}`,
161
+ `- feature: ${candidate.feature_slug}`,
162
+ `- severity: ${candidate.max_severity || 'unknown'}`,
163
+ `- reasons: ${(candidate.reasons || []).join(', ') || 'unknown'}`,
164
+ `- source dossier: ${dossierRelPath}`,
165
+ `- promoted at: ${generatedAt}`,
166
+ '',
167
+ '## Bounded Evidence',
168
+ '',
169
+ ...occurrenceLines(candidate),
170
+ '',
171
+ 'This rule intentionally excludes raw auditor output, stderr, prompt packages, and finding evidence text.',
172
+ ''
173
+ ];
174
+ return lines.join('\n');
175
+ }
176
+
177
+ function selectCandidates(candidates, selectValue) {
178
+ if (!selectValue || selectValue === true) return { ok: true, selected: candidates, selection: 'preview_all' };
179
+ const raw = String(selectValue).trim();
180
+ if (raw === 'all') return { ok: true, selected: candidates, selection: 'all' };
181
+ const wanted = new Set(raw.split(',').map((item) => item.trim()).filter(Boolean));
182
+ const selected = candidates.filter((candidate) => wanted.has(candidate.key));
183
+ const missing = [...wanted].filter((key) => !candidates.some((candidate) => candidate.key === key));
184
+ if (missing.length > 0) return { ok: false, reason: 'unknown_selection', missing };
185
+ return { ok: true, selected, selection: raw };
186
+ }
187
+
188
+ function promotionItems(candidates, to) {
189
+ return candidates.map((candidate) => {
190
+ const fileSlug = slugify(`${candidate.feature_slug}-${candidate.key}`);
191
+ const fileName = to === 'rules' ? `retro-${fileSlug}.md` : `${fileSlug}.md`;
192
+ const targetPath = to === 'rules'
193
+ ? path.join('.aioson', 'rules', fileName)
194
+ : path.join('.aioson', 'learnings', 'gotchas', fileName);
195
+ return {
196
+ key: candidate.key,
197
+ feature_slug: candidate.feature_slug,
198
+ title: candidateTitle(candidate),
199
+ severity: candidate.max_severity || 'unknown',
200
+ reasons: candidate.reasons || [],
201
+ occurrences: candidate.cost ? candidate.cost.occurrences : (candidate.occurrences || []).length,
202
+ target: to,
203
+ target_path: relPosix(targetPath)
204
+ };
205
+ });
206
+ }
207
+
208
+ async function ensureLearningIndex(rootDir, item) {
209
+ const indexRel = path.join('.aioson', 'learnings', 'INDEX.md');
210
+ const indexAbs = path.join(rootDir, indexRel);
211
+ let content = '# Project Learnings\n\n';
212
+ try {
213
+ content = await fs.readFile(indexAbs, 'utf8');
214
+ } catch {
215
+ // create below
216
+ }
217
+
218
+ const targetRel = relPosix(path.relative(path.join(rootDir, '.aioson', 'learnings'), path.join(rootDir, item.target_path)));
219
+ if (content.includes(`](${targetRel})`)) return false;
220
+
221
+ const entry = `- [${sanitizeInline(item.title)}](${targetRel}) - retro candidate ${item.key}\n`;
222
+ const next = content
223
+ .replace(/\r\n/g, '\n')
224
+ .replace(/\n?_No project learnings yet\._\n?/i, '\n')
225
+ .replace(/\s*$/, '\n');
226
+ await fs.mkdir(path.dirname(indexAbs), { recursive: true });
227
+ await fs.writeFile(indexAbs, `${next}${entry}`, 'utf8');
228
+ return true;
229
+ }
230
+
231
+ async function writePromotionFile(rootDir, candidate, item, { dossierRelPath, generatedAt }) {
232
+ const abs = path.join(rootDir, item.target_path);
233
+ if (fsSync.existsSync(abs)) return { action: 'skipped', reason: 'already_exists', path: item.target_path };
234
+ await fs.mkdir(path.dirname(abs), { recursive: true });
235
+ const content = item.target === 'rules'
236
+ ? buildRuleContent(candidate, {
237
+ dossierRelPath,
238
+ generatedAt,
239
+ ruleName: path.basename(item.target_path, '.md')
240
+ })
241
+ : buildLearningContent(candidate, { dossierRelPath, generatedAt });
242
+ await fs.writeFile(abs, content, 'utf8');
243
+ return { action: 'written', path: item.target_path };
244
+ }
245
+
246
+ async function recordRuntimeLearning(rootDir, candidate, item) {
247
+ const handle = await openRuntimeDb(rootDir);
248
+ try {
249
+ const result = upsertProjectLearning(handle.db, {
250
+ title: item.title,
251
+ type: 'quality',
252
+ kind: 'gotcha',
253
+ featureSlug: item.feature_slug,
254
+ evidence: candidateSummary(candidate),
255
+ sourceSession: `harness-retro-promote:${candidate.key}`
256
+ });
257
+ if (item.target === 'rules') {
258
+ promoteProjectLearning(handle.db, result.learningId, item.target_path);
259
+ }
260
+ return result;
261
+ } finally {
262
+ handle.db.close();
263
+ }
264
+ }
265
+
266
+ async function applyPromotions(rootDir, selectedCandidates, items, { dossierRelPath, generatedAt, target }) {
267
+ const written = [];
268
+ const skipped = [];
269
+ const runtime = [];
270
+
271
+ for (let index = 0; index < selectedCandidates.length; index += 1) {
272
+ const candidate = selectedCandidates[index];
273
+ const item = items[index];
274
+ const fileResult = await writePromotionFile(rootDir, candidate, item, { dossierRelPath, generatedAt });
275
+ if (fileResult.action === 'written') written.push(fileResult);
276
+ else skipped.push(fileResult);
277
+
278
+ if (target === 'learnings') {
279
+ await ensureLearningIndex(rootDir, item);
280
+ }
281
+
282
+ runtime.push({
283
+ key: item.key,
284
+ ...(await recordRuntimeLearning(rootDir, candidate, item))
285
+ });
286
+ }
287
+
288
+ return { written, skipped, runtime };
289
+ }
290
+
291
+ async function runHarnessRetroPromote({ args, options = {}, logger } = {}) {
292
+ const log = logger || { log() {}, error() {} };
293
+ const rootDir = path.resolve(process.cwd(), (args && args[0]) || '.');
294
+ const slug = options.feature !== undefined && options.feature !== true
295
+ ? String(options.feature || '').trim()
296
+ : '';
297
+ const target = options.to === undefined || options.to === true
298
+ ? 'learnings'
299
+ : String(options.to).trim();
300
+ const apply = Boolean(options.apply);
301
+
302
+ if (!slug) return inputError(log, 'harness:retro-promote requires --feature=<slug>', 'missing_feature');
303
+ if (!SLUG_RE.test(slug)) return inputError(log, `Invalid feature slug: ${slug}`, 'invalid_slug');
304
+ if (!TARGETS.has(target)) return inputError(log, `Invalid --to target: ${target} (use learnings or rules)`, 'invalid_target');
305
+ if (!resolveFeatureExists(rootDir, slug)) return inputError(log, `Feature not found: ${slug}`, 'feature_not_found');
306
+
307
+ const dossierRelPath = relPosix(path.join('.aioson', 'context', 'retro', `${slug}.md`));
308
+ if (!fsSync.existsSync(path.join(rootDir, dossierRelPath))) {
309
+ return inputError(log, `Retro dossier not found: ${dossierRelPath}. Run harness:retro first.`, 'dossier_missing');
310
+ }
311
+
312
+ const sources = collectSources(rootDir, [slug]);
313
+ const { candidates } = aggregate(sources);
314
+ const selection = selectCandidates(candidates, options.select);
315
+ if (!selection.ok) {
316
+ return inputError(log, `Unknown candidate key(s): ${selection.missing.join(', ')}`, selection.reason);
317
+ }
318
+
319
+ if (apply && (options.select === undefined || options.select === true || String(options.select).trim() === '')) {
320
+ return inputError(log, 'Applying retro promotion requires --select=<candidate-key|all>', 'selection_required');
321
+ }
322
+
323
+ const items = promotionItems(selection.selected, target);
324
+ const result = {
325
+ ok: true,
326
+ exitCode: EXIT_OK,
327
+ dry_run: !apply,
328
+ applied: apply,
329
+ target,
330
+ feature: slug,
331
+ dossier: dossierRelPath,
332
+ candidates: candidates.length,
333
+ selected: selection.selected.length,
334
+ selection: selection.selection,
335
+ items,
336
+ written: [],
337
+ skipped: [],
338
+ runtime: []
339
+ };
340
+
341
+ if (apply && selection.selected.length > 0) {
342
+ try {
343
+ const applied = await applyPromotions(rootDir, selection.selected, items, {
344
+ dossierRelPath,
345
+ generatedAt: nowIso(),
346
+ target
347
+ });
348
+ result.written = applied.written;
349
+ result.skipped = applied.skipped;
350
+ result.runtime = applied.runtime;
351
+ } catch (err) {
352
+ return ioError(log, `Failed to apply retro promotion: ${err.message}`, err);
353
+ }
354
+ }
355
+
356
+ if (options.json) return result;
357
+
358
+ log.log('');
359
+ log.log('Retro Promotion');
360
+ log.log(BAR);
361
+ log.log(`Feature: ${slug}`);
362
+ log.log(`Target: ${target}`);
363
+ log.log(`Mode: ${apply ? 'apply' : 'dry-run'}`);
364
+ log.log(`Candidates: ${candidates.length}; selected: ${selection.selected.length}`);
365
+ for (const item of items) {
366
+ log.log(`- ${item.key} -> ${item.target_path}`);
367
+ }
368
+ if (!apply && items.length > 0) {
369
+ log.log('');
370
+ log.log('Apply explicitly with: --apply --select=<candidate-key|all>');
371
+ }
372
+ log.log('');
373
+
374
+ return result;
375
+ }
376
+
377
+ module.exports = {
378
+ runHarnessRetroPromote,
379
+ _internal: {
380
+ buildLearningContent,
381
+ buildRuleContent,
382
+ candidateTitle,
383
+ selectCandidates,
384
+ promotionItems
385
+ }
386
+ };
387
+
@@ -0,0 +1,163 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * aioson prototype:check — deterministic fidelity guard for the prototype contract.
5
+ *
6
+ * When a PRD carries a `## Prototype reference` (the carrier @product writes), this
7
+ * command verifies that the prototype actually reaches the build:
8
+ * 1. the referenced prototype.html + manifest exist (no dangling pointer);
9
+ * 2. a requirements-{slug}.md bridge exists;
10
+ * 3. the Core interactions the manifest lists are echoed as acceptance criteria
11
+ * in requirements (this is the only place infidelity becomes machine-checkable —
12
+ * @validator never reads the prototype, only the AC authored by @analyst).
13
+ *
14
+ * It is a STRUCTURAL check, not a semantic one: coverage is a folded substring match
15
+ * of each manifest interaction phrase against the requirements text. The prototype
16
+ * contract instructs @analyst to echo the interaction names verbatim
17
+ * (e.g. "add card persists and re-renders"), so the match is deterministic, not fuzzy.
18
+ *
19
+ * Features with no `## Prototype reference` are a no-op (status: not_applicable).
20
+ *
21
+ * Usage:
22
+ * aioson prototype:check . --feature=kanban
23
+ * aioson prototype:check . --feature=kanban --json
24
+ */
25
+
26
+ const path = require('node:path');
27
+ const { readFileSafe, contextDir } = require('../preflight-engine');
28
+ const { resolveInsideRoot } = require('../verification/path-policy');
29
+
30
+ const BAR = '━'.repeat(30);
31
+
32
+ // Match the surface detectors: fold diacritics so a localized requirements file
33
+ // (pt-BR ACs) is compared the same way as an English one.
34
+ function fold(s) {
35
+ return String(s || '').normalize('NFD').replace(/\p{Diacritic}/gu, '').toLowerCase();
36
+ }
37
+
38
+ // The `## Prototype reference` section body, if present. Captures from the heading
39
+ // to the next `## ` heading or end of file (no `m` flag, so `$` means end-of-input).
40
+ function prototypeReferenceSection(prd) {
41
+ const m = String(prd || '').match(/##\s+Prototype reference[^\n]*\n([\s\S]*?)(?=\n##\s|$)/i);
42
+ return m ? m[1] : null;
43
+ }
44
+
45
+ function parsePath(section, key) {
46
+ const m = String(section || '').match(new RegExp(`^[-*]\\s*${key}:\\s*(\\S+)`, 'mi'));
47
+ return m ? m[1] : null;
48
+ }
49
+
50
+ // Core interactions are listed in the manifest as backtick-quoted tokens
51
+ // (e.g. - `add card` — ...). Prefer a "Core interactions" section; fall back to the
52
+ // whole manifest so older manifests still yield tokens. Deduped, normalized.
53
+ function extractInteractions(manifest) {
54
+ const text = String(manifest || '');
55
+ const section = text.match(/##\s+Core interactions[\s\S]*?(?=\n##\s|$)/i);
56
+ const scope = section ? section[0] : text;
57
+ const tokens = [...scope.matchAll(/`([^`]+)`/g)].map((t) => t[1].trim()).filter(Boolean);
58
+ return [...new Set(tokens)];
59
+ }
60
+
61
+ async function runPrototypeCheck({ args, options = {}, logger }) {
62
+ const targetDir = path.resolve(process.cwd(), args[0] || '.');
63
+ const slug = options.feature ? String(options.feature) : null;
64
+ const strict = Boolean(options.strict || String(options.policy || '').toLowerCase() === 'strict');
65
+ const dir = contextDir(targetDir);
66
+
67
+ const prdFile = slug ? `prd-${slug}.md` : 'prd.md';
68
+ const reqFile = slug ? `requirements-${slug}.md` : 'requirements.md';
69
+ const prd = await readFileSafe(path.join(dir, prdFile));
70
+
71
+ const emit = (result) => {
72
+ if (options.json) return result;
73
+ logger.log('');
74
+ logger.log(slug ? `Prototype check — ${slug}` : 'Prototype check');
75
+ logger.log(BAR);
76
+ logger.log(`Status: ${result.status}`);
77
+ if (result.message) logger.log(result.message);
78
+ if (result.interactions && result.interactions.total > 0) {
79
+ logger.log(`Interactions covered: ${result.interactions.covered}/${result.interactions.total}`);
80
+ if (result.interactions.uncovered.length) {
81
+ logger.log(`Uncovered: ${result.interactions.uncovered.map((i) => `"${i}"`).join(', ')}`);
82
+ }
83
+ }
84
+ logger.log('');
85
+ return result;
86
+ };
87
+
88
+ if (!prd) {
89
+ return emit({ ok: true, status: 'skipped', reason: 'no_prd', feature_slug: slug,
90
+ message: `No ${prdFile} found — nothing to check.` });
91
+ }
92
+
93
+ const section = prototypeReferenceSection(prd);
94
+ if (!section) {
95
+ return emit({ ok: true, status: 'not_applicable', feature_slug: slug,
96
+ message: 'PRD has no `## Prototype reference` — feature has no prototype contract.' });
97
+ }
98
+
99
+ // Resolve prototype + manifest paths (from the section, else the default location).
100
+ const protoRel = parsePath(section, 'prototype')
101
+ || (slug ? `.aioson/briefings/${slug}/prototype.html` : null);
102
+ const manifestRel = parsePath(section, 'manifest')
103
+ || (slug ? `.aioson/briefings/${slug}/prototype-manifest.md` : null);
104
+
105
+ const checks = { prototype_exists: false, manifest_exists: false, requirements_exists: false };
106
+
107
+ const protoSafe = protoRel ? resolveInsideRoot(targetDir, protoRel) : { ok: false, reason: 'missing_path' };
108
+ if (!protoSafe.ok) {
109
+ return emit({ ok: false, status: 'fail', reason: protoSafe.reason, field: 'prototype', feature_slug: slug, checks,
110
+ message: `\`## Prototype reference\` prototype path is invalid: ${protoRel || '(unspecified)'}.` });
111
+ }
112
+ const protoContent = await readFileSafe(protoSafe.path);
113
+ checks.prototype_exists = protoContent !== null;
114
+ if (!checks.prototype_exists) {
115
+ return emit({ ok: false, status: 'fail', reason: 'dangling_prototype', feature_slug: slug, checks,
116
+ message: `\`## Prototype reference\` points to ${protoRel || '(unspecified)'}, but that file is missing.` });
117
+ }
118
+
119
+ const manifestSafe = manifestRel ? resolveInsideRoot(targetDir, manifestRel) : { ok: false, reason: 'missing_path' };
120
+ if (!manifestSafe.ok) {
121
+ return emit({ ok: false, status: 'fail', reason: manifestSafe.reason, field: 'manifest', feature_slug: slug, checks,
122
+ message: `\`## Prototype reference\` manifest path is invalid: ${manifestRel || '(unspecified)'}.` });
123
+ }
124
+ const manifest = await readFileSafe(manifestSafe.path);
125
+ checks.manifest_exists = manifest !== null;
126
+ if (!checks.manifest_exists) {
127
+ return emit({ ok: false, status: 'fail', reason: 'missing_manifest', feature_slug: slug, checks,
128
+ message: `Prototype exists but its manifest ${manifestRel || '(unspecified)'} is missing.` });
129
+ }
130
+
131
+ const requirements = await readFileSafe(path.join(dir, reqFile));
132
+ checks.requirements_exists = requirements !== null;
133
+ if (!checks.requirements_exists) {
134
+ return emit({ ok: false, status: 'fail', reason: 'missing_requirements', feature_slug: slug, checks,
135
+ message: `PRD references a prototype but ${reqFile} is missing — @analyst has not authored the acceptance-criteria bridge.` });
136
+ }
137
+
138
+ const interactions = extractInteractions(manifest);
139
+ const reqFolded = fold(requirements);
140
+ const uncovered = interactions.filter((i) => !reqFolded.includes(fold(i)));
141
+ const covered = interactions.length - uncovered.length;
142
+ const interactionsResult = { total: interactions.length, covered, uncovered };
143
+
144
+ if (interactions.length === 0) {
145
+ return emit({ ok: true, status: 'ok', feature_slug: slug, checks, interactions: interactionsResult,
146
+ message: 'Prototype, manifest, and requirements all present. Manifest lists no machine-readable Core interactions to cover.' });
147
+ }
148
+
149
+ if (covered === 0) {
150
+ return emit({ ok: false, status: 'fail', reason: 'no_ac_coverage', feature_slug: slug, checks, interactions: interactionsResult,
151
+ message: 'None of the prototype Core interactions appear in the requirements ACs — the prototype is not reaching @validator.' });
152
+ }
153
+
154
+ if (uncovered.length > 0) {
155
+ return emit({ ok: !strict, status: strict ? 'fail' : 'warn', reason: 'partial_ac_coverage', feature_slug: slug, checks, interactions: interactionsResult,
156
+ message: 'Some prototype Core interactions have no matching acceptance criterion. Add an AC per interaction (or defer it explicitly in the PRD).' });
157
+ }
158
+
159
+ return emit({ ok: true, status: 'ok', feature_slug: slug, checks, interactions: interactionsResult,
160
+ message: 'Every prototype Core interaction is echoed in the requirements ACs.' });
161
+ }
162
+
163
+ module.exports = { runPrototypeCheck };