create-quiver 0.17.3 → 0.17.5

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 (29) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/docs/CLI_UX_GUIDE.md +11 -4
  3. package/docs/TROUBLESHOOTING.md +17 -9
  4. package/docs/reference/commands.md +21 -9
  5. package/docs/workflows/existing-project.md +5 -5
  6. package/package.json +1 -1
  7. package/specs/quiver-v56-analyze-project-usable-doc-merge/EVIDENCE_REPORT.md +11 -0
  8. package/specs/quiver-v56-analyze-project-usable-doc-merge/EXECUTION_PLAN.md +28 -0
  9. package/specs/quiver-v56-analyze-project-usable-doc-merge/SPEC.md +96 -0
  10. package/specs/quiver-v56-analyze-project-usable-doc-merge/STATUS.md +26 -0
  11. package/specs/quiver-v56-analyze-project-usable-doc-merge/pr.md +56 -0
  12. package/specs/quiver-v56-analyze-project-usable-doc-merge/slices/slice-01-document-classification-merge-engine/CLOSURE_BRIEF.md +61 -0
  13. package/specs/quiver-v56-analyze-project-usable-doc-merge/slices/slice-01-document-classification-merge-engine/EXECUTION_BRIEF.md +59 -0
  14. package/specs/quiver-v56-analyze-project-usable-doc-merge/slices/slice-01-document-classification-merge-engine/pr.md +61 -0
  15. package/specs/quiver-v56-analyze-project-usable-doc-merge/slices/slice-01-document-classification-merge-engine/slice.json +71 -0
  16. package/specs/quiver-v56-analyze-project-usable-doc-merge/slices/slice-02-apply-integration-validation-contract/CLOSURE_BRIEF.md +30 -0
  17. package/specs/quiver-v56-analyze-project-usable-doc-merge/slices/slice-02-apply-integration-validation-contract/EXECUTION_BRIEF.md +39 -0
  18. package/specs/quiver-v56-analyze-project-usable-doc-merge/slices/slice-02-apply-integration-validation-contract/pr.md +62 -0
  19. package/specs/quiver-v56-analyze-project-usable-doc-merge/slices/slice-02-apply-integration-validation-contract/slice.json +80 -0
  20. package/specs/quiver-v56-analyze-project-usable-doc-merge/slices/slice-03-cli-docs-real-fixture-smoke/CLOSURE_BRIEF.md +29 -0
  21. package/specs/quiver-v56-analyze-project-usable-doc-merge/slices/slice-03-cli-docs-real-fixture-smoke/EXECUTION_BRIEF.md +32 -0
  22. package/specs/quiver-v56-analyze-project-usable-doc-merge/slices/slice-03-cli-docs-real-fixture-smoke/pr.md +61 -0
  23. package/specs/quiver-v56-analyze-project-usable-doc-merge/slices/slice-03-cli-docs-real-fixture-smoke/slice.json +79 -0
  24. package/src/create-quiver/commands/ai.js +50 -2
  25. package/src/create-quiver/index.js +1 -1
  26. package/src/create-quiver/lib/ai/analyze-project-docs.js +300 -5
  27. package/src/create-quiver/lib/ai/analyze-project-proposal.js +11 -0
  28. package/src/create-quiver/lib/ai/analyze-project-validation.js +26 -0
  29. package/src/create-quiver/lib/i18n/messages/es.js +1 -1
@@ -15,6 +15,43 @@ const ANALYZE_DOC_PROPOSAL_SCHEMA_VERSION = 1;
15
15
  const ANALYZE_DOC_PROPOSAL_KIND = 'quiver-analyze-project-doc-proposal';
16
16
  const ANALYZE_MANAGED_START = '<!-- quiver:analyze-project:start -->';
17
17
  const ANALYZE_MANAGED_END = '<!-- quiver:analyze-project:end -->';
18
+ const CONTEXT_PREP_MANAGED_START = '<!-- quiver:context-prep:start -->';
19
+ const CONTEXT_PREP_MANAGED_END = '<!-- quiver:context-prep:end -->';
20
+
21
+ const CRITICAL_SCAFFOLD_PLACEHOLDER_PATTERNS = [
22
+ /\[Uno o dos parrafos que expliquen el proyecto\.\]/i,
23
+ /\[Frase principal del proyecto\]/i,
24
+ /\[Describe el usuario principal\.\]/i,
25
+ /\[Objetivo actual \d+\]/i,
26
+ /\[Datos sensibles que nunca se guardan\]/i,
27
+ /\[Datos que si se guardan\]/i,
28
+ /\[Medida de seguridad importante\]/i,
29
+ /\[One or two paragraphs? that explain the project\.\]/i,
30
+ /\[Project tagline\]/i,
31
+ /\[Describe the primary user\.\]/i,
32
+ /\[Current goal \d+\]/i,
33
+ /\[Sensitive data that is never stored\]/i,
34
+ /\[Data that is stored\]/i,
35
+ /\[Important security measure\]/i,
36
+ /\[Short project summary\.\]/i,
37
+ /\[Description\]/i,
38
+ /\[TODO: confirm [^\]\n]+\]/i,
39
+ /\[TODO: confirmar [^\]\n]+\]/i,
40
+ ];
41
+
42
+ const SCAFFOLD_BOILERPLATE_PATTERNS = [
43
+ /^purpose:\s*"Human-readable project overview"$/i,
44
+ /^applies_when:\s*"onboarding, review"$/i,
45
+ /^supersedes:\s*null$/i,
46
+ /^El stack, package manager y comandos se generan en `docs\/PROJECT_MAP\.md` despues de ejecutar `analyze`\.$/i,
47
+ /^The stack, package manager, and command surface are generated in `docs\/PROJECT_MAP\.md` after analysis\.$/i,
48
+ /^If sos agente IA, lee `AI_CONTEXT\.md`\./i,
49
+ /^Read `AI_CONTEXT\.md` first if you are an AI agent\./i,
50
+ /^Use `npx create-quiver ai prepare-context --dry-run`/i,
51
+ /^TODO: confirm any repo fact/i,
52
+ /^Assumption: missing README_FOR_AI\.md/i,
53
+ /^Pending confirmation:/i,
54
+ ];
18
55
 
19
56
  const docProposalEntrySchema = z.object({
20
57
  path: z.string().trim().min(1),
@@ -67,6 +104,10 @@ function normalizeDocContent(content) {
67
104
  return `${String(content || '').replace(/\s+$/g, '')}\n`;
68
105
  }
69
106
 
107
+ function normalizeNewlines(content) {
108
+ return String(content || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
109
+ }
110
+
70
111
  function managedBlock(content) {
71
112
  return [
72
113
  ANALYZE_MANAGED_START,
@@ -76,8 +117,240 @@ function managedBlock(content) {
76
117
  ].join('\n');
77
118
  }
78
119
 
120
+ function findManagedBlockRange(content, startMarker, endMarker) {
121
+ const text = normalizeNewlines(content);
122
+ const start = text.indexOf(startMarker);
123
+ const end = text.indexOf(endMarker);
124
+ if (start < 0 || end <= start) {
125
+ return null;
126
+ }
127
+ return {
128
+ start,
129
+ end: end + endMarker.length,
130
+ content: text.slice(start + startMarker.length, end),
131
+ };
132
+ }
133
+
134
+ function removeManagedBlock(content, startMarker, endMarker) {
135
+ const text = normalizeNewlines(content);
136
+ const range = findManagedBlockRange(text, startMarker, endMarker);
137
+ if (!range) {
138
+ return {
139
+ content: text,
140
+ removed: false,
141
+ block: '',
142
+ };
143
+ }
144
+ return {
145
+ content: `${text.slice(0, range.start)}${text.slice(range.end)}`.replace(/\n{3,}/g, '\n\n').trimEnd(),
146
+ removed: true,
147
+ block: range.content,
148
+ };
149
+ }
150
+
151
+ function splitFrontmatter(content) {
152
+ const text = normalizeNewlines(content);
153
+ if (!text.startsWith('---\n')) {
154
+ return {
155
+ frontmatter: '',
156
+ body: text,
157
+ valid: false,
158
+ present: false,
159
+ };
160
+ }
161
+ const match = text.slice(4).match(/\n---\n/);
162
+ if (!match || match.index === undefined) {
163
+ return {
164
+ frontmatter: '',
165
+ body: text,
166
+ valid: false,
167
+ present: true,
168
+ };
169
+ }
170
+ const endIndex = 4 + match.index + '\n---\n'.length;
171
+ return {
172
+ frontmatter: text.slice(0, endIndex),
173
+ body: text.slice(endIndex),
174
+ valid: true,
175
+ present: true,
176
+ };
177
+ }
178
+
179
+ function collectCriticalPlaceholders(content) {
180
+ const findings = [];
181
+ const text = normalizeNewlines(content);
182
+ for (const pattern of CRITICAL_SCAFFOLD_PLACEHOLDER_PATTERNS) {
183
+ const match = text.match(pattern);
184
+ if (match) {
185
+ findings.push(match[0]);
186
+ }
187
+ }
188
+ return [...new Set(findings)].sort();
189
+ }
190
+
191
+ function isCriticalPlaceholderLine(line) {
192
+ const trimmed = String(line || '').trim();
193
+ return CRITICAL_SCAFFOLD_PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(trimmed));
194
+ }
195
+
196
+ function isScaffoldBoilerplateLine(line) {
197
+ const trimmed = String(line || '')
198
+ .replace(/^[-*]\s+/, '')
199
+ .replace(/^>\s*/, '')
200
+ .trim();
201
+ if (!trimmed) {
202
+ return true;
203
+ }
204
+ if (/^#+\s+/.test(trimmed)) {
205
+ return true;
206
+ }
207
+ if (/^last_updated:\s*"/i.test(trimmed) || /^token_cost:\s*\d+/i.test(trimmed)) {
208
+ return true;
209
+ }
210
+ if (/^Files Considered$|^Assumptions$|^Risks$|^Contradictions$|^Omitted Paths$/i.test(trimmed.replace(/^#+\s*/, ''))) {
211
+ return true;
212
+ }
213
+ return SCAFFOLD_BOILERPLATE_PATTERNS.some((pattern) => pattern.test(trimmed));
214
+ }
215
+
216
+ function significantHumanLines(content) {
217
+ return normalizeNewlines(content)
218
+ .split('\n')
219
+ .map((line) => line.trim())
220
+ .filter((line) => line.length >= 12)
221
+ .filter((line) => !isCriticalPlaceholderLine(line))
222
+ .filter((line) => !isScaffoldBoilerplateLine(line));
223
+ }
224
+
225
+ function classifyAnalyzeProjectDoc(currentContent) {
226
+ const current = normalizeNewlines(currentContent);
227
+ const frontmatter = splitFrontmatter(current);
228
+ const withoutAnalyze = removeManagedBlock(frontmatter.body, ANALYZE_MANAGED_START, ANALYZE_MANAGED_END);
229
+ const withoutContextPrep = removeManagedBlock(withoutAnalyze.content, CONTEXT_PREP_MANAGED_START, CONTEXT_PREP_MANAGED_END);
230
+ const visibleBody = withoutContextPrep.content.trim();
231
+ const visiblePlaceholders = collectCriticalPlaceholders(visibleBody);
232
+ const contextPrepPlaceholders = collectCriticalPlaceholders(withoutContextPrep.block);
233
+ const humanLines = significantHumanLines(visibleBody);
234
+ const contextPrepHumanLines = significantHumanLines(withoutContextPrep.block);
235
+ const hasManagedOnly = !visibleBody && (withoutAnalyze.removed || withoutContextPrep.removed);
236
+
237
+ let classification = 'unknown';
238
+ if (!current.trim()) {
239
+ classification = 'managed_only';
240
+ } else if (hasManagedOnly) {
241
+ classification = 'managed_only';
242
+ } else if (visiblePlaceholders.length > 0 && humanLines.length === 0) {
243
+ classification = 'scaffold';
244
+ } else if (visiblePlaceholders.length > 0 && humanLines.length > 0) {
245
+ classification = 'partial_scaffold';
246
+ } else if (withoutAnalyze.removed || withoutContextPrep.removed) {
247
+ classification = humanLines.length > 0 ? 'mixed' : 'managed_only';
248
+ } else if (humanLines.length > 0) {
249
+ classification = 'human_content';
250
+ }
251
+
252
+ return {
253
+ classification,
254
+ frontmatter,
255
+ visible_body: visibleBody,
256
+ has_analyze_project_block: withoutAnalyze.removed,
257
+ has_context_prep_block: withoutContextPrep.removed,
258
+ context_prep_is_scaffold: withoutContextPrep.removed && contextPrepHumanLines.length === 0 && contextPrepPlaceholders.length > 0,
259
+ critical_placeholders: visiblePlaceholders,
260
+ context_prep_placeholders: contextPrepPlaceholders,
261
+ human_line_count: humanLines.length,
262
+ };
263
+ }
264
+
265
+ function composeWithFrontmatter(frontmatter, body) {
266
+ const prefix = frontmatter?.valid ? `${frontmatter.frontmatter.trimEnd()}\n\n` : '';
267
+ return `${prefix}${normalizeDocContent(body)}`;
268
+ }
269
+
270
+ function visibleAnalyzeProjectContent(proposedContent) {
271
+ return normalizeDocContent(proposedContent).trimEnd();
272
+ }
273
+
274
+ function buildPreservedContentSection(content) {
275
+ const body = normalizeNewlines(content).trim();
276
+ if (!body) {
277
+ return '';
278
+ }
279
+ return `## Existing Content Preserved\n\n${body}`;
280
+ }
281
+
282
+ function normalizedMarkdownEquivalent(left, right) {
283
+ return normalizeNewlines(left).trim() === normalizeNewlines(right).trim();
284
+ }
285
+
286
+ function mergeAnalyzeProjectDoc(currentContent, proposedContent) {
287
+ const current = normalizeNewlines(currentContent);
288
+ const proposedVisible = visibleAnalyzeProjectContent(proposedContent);
289
+ const classification = classifyAnalyzeProjectDoc(current);
290
+ const currentWithoutAnalyze = removeManagedBlock(classification.frontmatter.body, ANALYZE_MANAGED_START, ANALYZE_MANAGED_END);
291
+ const currentWithoutContextPrep = removeManagedBlock(currentWithoutAnalyze.content, CONTEXT_PREP_MANAGED_START, CONTEXT_PREP_MANAGED_END);
292
+ const bodyWithoutManaged = currentWithoutContextPrep.content.trim();
293
+ const report = {
294
+ classification: classification.classification,
295
+ strategy: 'preserve-and-update-managed-block',
296
+ scaffold_replaced: false,
297
+ human_content_preserved: false,
298
+ analyze_project_block_replaced: classification.has_analyze_project_block,
299
+ context_prep_removed: false,
300
+ critical_placeholders: classification.critical_placeholders,
301
+ warnings: [],
302
+ };
303
+
304
+ let visibleBody;
305
+ if (['scaffold', 'managed_only'].includes(classification.classification)) {
306
+ visibleBody = proposedVisible;
307
+ report.strategy = classification.classification === 'scaffold'
308
+ ? 'replace-scaffold-primary-content'
309
+ : 'replace-managed-only-content';
310
+ report.scaffold_replaced = classification.classification === 'scaffold';
311
+ } else if (['partial_scaffold', 'mixed'].includes(classification.classification)) {
312
+ const cleaned = bodyWithoutManaged
313
+ .split('\n')
314
+ .filter((line) => !isCriticalPlaceholderLine(line))
315
+ .join('\n')
316
+ .replace(/\n{3,}/g, '\n\n')
317
+ .trim();
318
+ const preserved = normalizedMarkdownEquivalent(cleaned, proposedVisible)
319
+ ? ''
320
+ : buildPreservedContentSection(cleaned);
321
+ visibleBody = preserved ? `${proposedVisible}\n\n${preserved}` : proposedVisible;
322
+ report.strategy = 'replace-placeholders-preserve-human-content';
323
+ report.scaffold_replaced = classification.critical_placeholders.length > 0;
324
+ report.human_content_preserved = Boolean(preserved);
325
+ } else {
326
+ visibleBody = bodyWithoutManaged
327
+ ? `${bodyWithoutManaged}\n\n${managedBlock(proposedContent).trimEnd()}`
328
+ : managedBlock(proposedContent).trimEnd();
329
+ report.human_content_preserved = bodyWithoutManaged.trim().length > 0;
330
+ if (classification.classification === 'unknown') {
331
+ report.warnings.push('document classification is unknown; preserved existing content and updated managed block only');
332
+ }
333
+ return {
334
+ content: composeWithFrontmatter(classification.frontmatter, visibleBody),
335
+ report,
336
+ };
337
+ }
338
+
339
+ if (classification.has_context_prep_block && (classification.context_prep_is_scaffold || classification.context_prep_placeholders.length > 0)) {
340
+ report.context_prep_removed = true;
341
+ }
342
+
343
+ return {
344
+ content: composeWithFrontmatter(
345
+ classification.frontmatter,
346
+ `${visibleBody}\n\n${managedBlock(proposedContent).trimEnd()}`,
347
+ ),
348
+ report,
349
+ };
350
+ }
351
+
79
352
  function mergeManagedBlock(currentContent, proposedContent) {
80
- const current = String(currentContent || '');
353
+ const current = normalizeNewlines(currentContent);
81
354
  const block = managedBlock(proposedContent);
82
355
  const startIndex = current.indexOf(ANALYZE_MANAGED_START);
83
356
  const endIndex = current.indexOf(ANALYZE_MANAGED_END);
@@ -93,6 +366,21 @@ function mergeManagedBlock(currentContent, proposedContent) {
93
366
  return `${current.replace(/\s+$/g, '')}\n\n${block}`;
94
367
  }
95
368
 
369
+ function redactSnapshotValue(value, repoRoot) {
370
+ if (typeof value === 'string') {
371
+ return redactSensitiveLocalValues(value, { projectRoot: repoRoot });
372
+ }
373
+ if (Array.isArray(value)) {
374
+ return value.map((item) => redactSnapshotValue(item, repoRoot));
375
+ }
376
+ if (value && typeof value === 'object') {
377
+ return Object.fromEntries(
378
+ Object.entries(value).map(([key, item]) => [key, redactSnapshotValue(item, repoRoot)]),
379
+ );
380
+ }
381
+ return value;
382
+ }
383
+
96
384
  function validateDocPath(docPath) {
97
385
  let normalized;
98
386
  try {
@@ -238,9 +526,10 @@ function buildAnalyzeProjectWritePlan(repoRoot, proposal) {
238
526
  const destinationPath = path.join(repoRoot, doc.path);
239
527
  const exists = fs.existsSync(destinationPath);
240
528
  const currentContent = exists ? fs.readFileSync(destinationPath, 'utf8') : '';
241
- const proposedContent = doc.action === 'skip'
242
- ? currentContent
243
- : mergeManagedBlock(currentContent, doc.content);
529
+ const merged = doc.action === 'skip'
530
+ ? { content: currentContent, report: { classification: 'skipped', strategy: 'skip' } }
531
+ : mergeAnalyzeProjectDoc(currentContent, doc.content);
532
+ const proposedContent = merged.content;
244
533
  const changed = currentContent.replace(/\r\n/g, '\n') !== proposedContent.replace(/\r\n/g, '\n');
245
534
  const action = doc.action === 'skip' || !changed ? 'skip' : exists ? 'update' : 'create';
246
535
 
@@ -256,6 +545,7 @@ function buildAnalyzeProjectWritePlan(repoRoot, proposal) {
256
545
  before_sha256: exists ? sha256(currentContent) : null,
257
546
  after_sha256: action === 'skip' ? (exists ? sha256(currentContent) : null) : sha256(proposedContent),
258
547
  diff: buildDiffSnippet(doc.path, currentContent, proposedContent),
548
+ merge_report: merged.report,
259
549
  };
260
550
  });
261
551
  }
@@ -303,7 +593,7 @@ function createAnalyzeProjectSnapshot(repoRoot, run, writePlan, options = {}) {
303
593
 
304
594
  const providerArtifactPath = path.join(rawRoot, 'analyze-project-provider-artifact.json');
305
595
  const providerArtifact = options.providerArtifact
306
- ? JSON.parse(redactSensitiveLocalValues(JSON.stringify(options.providerArtifact), { projectRoot: repoRoot }))
596
+ ? redactSnapshotValue(options.providerArtifact, repoRoot)
307
597
  : null;
308
598
  fs.writeFileSync(providerArtifactPath, `${JSON.stringify(providerArtifact, null, 2)}\n`);
309
599
 
@@ -353,12 +643,17 @@ module.exports = {
353
643
  ANALYZE_DOC_PROPOSAL_SCHEMA_VERSION,
354
644
  ANALYZE_MANAGED_END,
355
645
  ANALYZE_MANAGED_START,
646
+ CONTEXT_PREP_MANAGED_END,
647
+ CONTEXT_PREP_MANAGED_START,
356
648
  AnalyzeProjectDocsError,
357
649
  analyzeProjectDocProposalSchema,
358
650
  buildAnalyzeProjectDocProposal,
359
651
  buildAnalyzeProjectWritePlan,
652
+ classifyAnalyzeProjectDoc,
653
+ collectCriticalPlaceholders,
360
654
  createAnalyzeProjectSnapshot,
361
655
  formatAnalyzeProjectDiffPreview,
656
+ mergeAnalyzeProjectDoc,
362
657
  mergeManagedBlock,
363
658
  normalizeAnalyzeProjectDocProposal,
364
659
  parseAnalyzeProjectDocProposal,
@@ -127,6 +127,7 @@ const proposalManifestSchema = z.object({
127
127
  repair_manifest: relativePathSchema('repair_manifest').nullable(),
128
128
  doc_paths: z.array(relativePathSchema('doc_paths')).default([]),
129
129
  doc_before_hashes: z.record(z.string().trim().min(1), z.string().trim().min(1).nullable()).default({}),
130
+ merge_plan: z.array(z.record(z.string(), z.unknown())).default([]),
130
131
  proposal_sha256: z.string().trim().min(1),
131
132
  events: z.array(z.record(z.string(), z.unknown())).default([]),
132
133
  }).strict();
@@ -139,6 +140,7 @@ const writeManifestActionSchema = z.object({
139
140
  snapshot_path: relativePathSchema('actions.snapshot_path').nullable(),
140
141
  dirty: z.boolean().default(false),
141
142
  status: z.enum(['planned', 'written', 'skipped', 'failed']).default('planned'),
143
+ merge_report: z.record(z.string(), z.unknown()).optional(),
142
144
  }).strict();
143
145
 
144
146
  const writeManifestSchema = z.object({
@@ -419,9 +421,16 @@ function writeAnalyzeProjectProposalArtifacts(repoRoot, options = {}) {
419
421
  const proposalJson = `${JSON.stringify(proposal, null, 2)}\n`;
420
422
  const proposalHash = sha256(proposalJson);
421
423
  const docBeforeHashes = {};
424
+ const mergePlan = [];
422
425
 
423
426
  for (const item of writePlan) {
424
427
  docBeforeHashes[item.path] = item.before_sha256 || null;
428
+ mergePlan.push({
429
+ path: item.path,
430
+ action: item.action,
431
+ dirty: item.dirty === true,
432
+ merge_report: item.merge_report || null,
433
+ });
425
434
  }
426
435
 
427
436
  const selectedContextPath = artifactPathFromRef(options.selectedContextManifest)
@@ -441,6 +450,7 @@ function writeAnalyzeProjectProposalArtifacts(repoRoot, options = {}) {
441
450
  repair_manifest: repairPath,
442
451
  doc_paths: writePlan.map((item) => item.path),
443
452
  doc_before_hashes: docBeforeHashes,
453
+ merge_plan: mergePlan,
444
454
  proposal_sha256: proposalHash,
445
455
  events: [
446
456
  {
@@ -524,6 +534,7 @@ function writeAnalyzeProjectWriteManifest(repoRoot, options = {}) {
524
534
  snapshot_path: snapshotEntry?.snapshot_path || null,
525
535
  dirty: item.dirty === true,
526
536
  status,
537
+ merge_report: item.merge_report || undefined,
527
538
  };
528
539
  });
529
540
  const validation = options.validation || {
@@ -4,6 +4,8 @@ const path = require('node:path');
4
4
  const {
5
5
  ANALYZE_MANAGED_END,
6
6
  ANALYZE_MANAGED_START,
7
+ classifyAnalyzeProjectDoc,
8
+ collectCriticalPlaceholders,
7
9
  normalizeAnalyzeProjectDocProposal,
8
10
  } = require('./analyze-project-docs');
9
11
  const { normalizeAnalyzeProjectAnalysis } = require('./analyze-project-parser');
@@ -326,6 +328,29 @@ function addPlaceholderValidation(docPath, managedContent, errors) {
326
328
  }
327
329
  }
328
330
 
331
+ function addVisiblePlaceholderValidation(docPath, content, warnings, errors, options = {}) {
332
+ const classification = classifyAnalyzeProjectDoc(content);
333
+ const placeholders = collectCriticalPlaceholders(classification.visible_body || '');
334
+ if (placeholders.length === 0) {
335
+ return;
336
+ }
337
+
338
+ const issue = makeIssue(
339
+ docPath,
340
+ 'visible-critical-placeholder',
341
+ `critical scaffold placeholder remains in primary visible content: ${placeholders.slice(0, 3).join(', ')}`,
342
+ {
343
+ classification: classification.classification,
344
+ placeholders,
345
+ },
346
+ );
347
+ if (options.strict === true) {
348
+ errors.push(issue);
349
+ } else {
350
+ warnings.push(issue);
351
+ }
352
+ }
353
+
329
354
  function normalizeFactValue(value) {
330
355
  return String(value || '')
331
356
  .replace(/[`*_]/g, '')
@@ -446,6 +471,7 @@ function validateAnalyzeProjectPostWrite(repoRoot, report, options = {}) {
446
471
  content: file.content,
447
472
  managedBlock,
448
473
  });
474
+ addVisiblePlaceholderValidation(docPath, file.content, warnings, errors, { strict: options.strict === true });
449
475
  if (!managedBlock.ok) {
450
476
  errors.push(makeIssue(docPath, managedBlock.issue, `Quiver managed block is missing or unbalanced in ${docPath}.`));
451
477
  continue;
@@ -37,7 +37,7 @@ module.exports = {
37
37
  'ai active-slice status|reconcile': 'Inspecciona o simula reconciliar el estado local de active-slice desde todas las fuentes soportadas.',
38
38
  'ai status': 'Muestra fase actual del ciclo de vida de IA, versiones aprobadas, bloqueos y proximo comando.',
39
39
  'ai resume': 'Reanuda la guia desde la ultima fase valida del ciclo de vida sin memoria de chat.',
40
- 'ai analyze-project': 'Analiza una muestra acotada del proyecto y genera propuestas documentales auditadas.',
40
+ 'ai analyze-project': 'Analiza una muestra acotada del proyecto y aplica actualizaciones documentales validadas.',
41
41
  'ai onboard': 'Ejecuta o imprime el prompt de onboarding del planner con contexto optimizado por tokens.',
42
42
  'ai prepare-context': 'Previsualiza o escribe actualizaciones docs-only del contexto de IA con supuestos y riesgos.',
43
43
  'ai agent set|list|show|doctor|repair': 'Gestiona, diagnostica y simula reparaciones de perfiles planner, executor, reviewer y doctor sin secretos.',