@planu/cli 4.10.11 → 4.11.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 (47) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/config/project-knowledge-graph.json +42 -5
  3. package/dist/engine/core-bridge-project-graph.d.ts +13 -0
  4. package/dist/engine/core-bridge-project-graph.js +499 -0
  5. package/dist/engine/core-bridge.d.ts +7 -2
  6. package/dist/engine/core-bridge.js +55 -0
  7. package/dist/engine/frontmatter-parser.js +73 -23
  8. package/dist/engine/model-tier-resolver.d.ts +8 -7
  9. package/dist/engine/model-tier-resolver.js +70 -73
  10. package/dist/engine/next-spec-resolver/orchestration-planner.d.ts +5 -0
  11. package/dist/engine/next-spec-resolver/orchestration-planner.js +34 -5
  12. package/dist/engine/project-graph/builder.js +271 -36
  13. package/dist/engine/project-graph/cache.d.ts +22 -4
  14. package/dist/engine/project-graph/cache.js +412 -33
  15. package/dist/engine/project-graph/index.d.ts +1 -0
  16. package/dist/engine/project-graph/index.js +1 -0
  17. package/dist/engine/project-graph/native.d.ts +3 -0
  18. package/dist/engine/project-graph/native.js +36 -0
  19. package/dist/engine/project-graph/query.js +34 -2
  20. package/dist/engine/provider-adapters/adapters/claude.js +38 -14
  21. package/dist/engine/scan-project/index.js +88 -15
  22. package/dist/engine/spec-format/lean-spec-generator.d.ts +2 -2
  23. package/dist/engine/spec-format/lean-spec-generator.js +65 -50
  24. package/dist/engine/spec-format/metadata-value-policy.d.ts +161 -0
  25. package/dist/engine/spec-format/metadata-value-policy.js +87 -0
  26. package/dist/engine/spec-format/value-only-spec-serializer.d.ts +12 -0
  27. package/dist/engine/spec-format/value-only-spec-serializer.js +18 -0
  28. package/dist/engine/spec-generator/fallback-generator.js +4 -2
  29. package/dist/engine/spec-generator/opus-generator.js +5 -2
  30. package/dist/engine/spec-migrator/lean-migration.js +26 -13
  31. package/dist/storage/spec-store.js +6 -6
  32. package/dist/tools/create-spec.js +1027 -739
  33. package/dist/tools/render-spec-for-provider.js +4 -3
  34. package/dist/tools/reverse-engineer/handler.js +76 -43
  35. package/dist/tools/spec-split-handler.js +36 -75
  36. package/dist/types/conventions.d.ts +9 -0
  37. package/dist/types/core-bridge.d.ts +72 -0
  38. package/dist/types/next-spec.d.ts +2 -1
  39. package/dist/types/project-knowledge-graph.d.ts +58 -0
  40. package/dist/types/spec/core.d.ts +7 -4
  41. package/dist/types/spec-format.d.ts +2 -1
  42. package/dist/types/spec-generator.d.ts +8 -2
  43. package/package.json +11 -9
  44. package/planu-native.json +1 -1
  45. package/planu-plugin.json +1 -1
  46. package/dist/engine/spec-format/model-budget-deriver.d.ts +0 -5
  47. package/dist/engine/spec-format/model-budget-deriver.js +0 -7
@@ -0,0 +1,18 @@
1
+ import { generateLeanSpecContent } from './lean-spec-generator.js';
2
+ /**
3
+ * Authority for secondary new-spec writers. Frontmatter always flows through
4
+ * the value-only serializer; callers may add only source-backed body content.
5
+ */
6
+ export function serializeValueOnlySpecContent(input) {
7
+ const leanSpec = generateLeanSpecContent({
8
+ spec: input.spec,
9
+ description: input.description,
10
+ criteriaOverride: input.criteria ? [...input.criteria] : [],
11
+ });
12
+ const technical = input.technical?.trim();
13
+ if (!technical || /^##\s+Technical\b/m.test(input.description)) {
14
+ return leanSpec;
15
+ }
16
+ return `${leanSpec.trimEnd()}\n\n## Technical\n\n${technical}\n`;
17
+ }
18
+ //# sourceMappingURL=value-only-spec-serializer.js.map
@@ -14,8 +14,10 @@ export class FallbackGenerator {
14
14
  return Promise.resolve({
15
15
  specBody,
16
16
  technicalSection,
17
- generatedWithModel: 'deterministic-fallback',
18
- generatedAt: new Date().toISOString(),
17
+ generation: {
18
+ method: 'deterministic',
19
+ generatedAt: new Date().toISOString(),
20
+ },
19
21
  qualityWarnings: [
20
22
  'Fallback generator used. Source description was preserved; technical ownership must come from explicit source paths or local project analysis.',
21
23
  ],
@@ -89,8 +89,11 @@ function parseGeneratedText(text, model) {
89
89
  return {
90
90
  specBody: text,
91
91
  technicalSection: technicalMatch?.[1]?.trim() ?? '',
92
- generatedWithModel: model,
93
- generatedAt: new Date().toISOString(),
92
+ generation: {
93
+ method: 'internal-model',
94
+ modelId: model,
95
+ generatedAt: new Date().toISOString(),
96
+ },
94
97
  qualityWarnings: [],
95
98
  };
96
99
  }
@@ -35,19 +35,31 @@ export async function isOldFormat(specDir) {
35
35
  return false;
36
36
  }
37
37
  }
38
- /** Check if a spec is already in lean format (has estimation in YAML frontmatter). */
38
+ /** Check if a spec already uses the lean frontmatter contract. */
39
39
  function isLeanFormat(content) {
40
40
  const fmMatch = /^---\n([\s\S]*?)\n---/m.exec(content);
41
41
  if (!fmMatch?.[1]) {
42
42
  return false;
43
43
  }
44
- return fmMatch[1].includes('estimation:') && fmMatch[1].includes('criteria:');
44
+ return (/^spec_format_version:\s*["']?1\.0["']?\s*$/m.test(fmMatch[1]) ||
45
+ /^(?:criteria|scenarios):\s*$/m.test(fmMatch[1]));
46
+ }
47
+ function isTerminalStatus(status) {
48
+ return status === 'done' || status === 'discarded';
49
+ }
50
+ function readTopLevelStatus(content) {
51
+ const frontmatter = /^---\n([\s\S]*?)\n---/m.exec(content)?.[1];
52
+ if (!frontmatter) {
53
+ return undefined;
54
+ }
55
+ const value = /^status:\s*["']?([^\s"']+)["']?\s*$/m.exec(frontmatter)?.[1];
56
+ return value;
45
57
  }
46
58
  /** Extract the user description from old spec.md (skip generic sections, keep real content). */
47
59
  function extractDescription(content) {
48
60
  // Remove frontmatter
49
61
  const withoutFm = content.replace(/^---\n[\s\S]*?\n---\n/, '').trim();
50
- // Remove the heading line (# SPEC-XXX: Title)
62
+ // Remove the heading line containing a spec ID and title.
51
63
  const lines = withoutFm.split('\n');
52
64
  const startIdx = lines.findIndex((l) => /^#\s+SPEC-\d+/.test(l));
53
65
  const afterHeading = startIdx >= 0 ? lines.slice(startIdx + 1) : lines;
@@ -320,7 +332,9 @@ function buildSpecFromMetadata(metadata, id, title, specStatus, estimation, spec
320
332
  tags: Array.isArray(metadata.tags) ? metadata.tags : [],
321
333
  dependencies: [],
322
334
  blockedBy: [],
323
- gitBranch: typeof metadata.branch === 'string' ? metadata.branch : '',
335
+ gitBranch: typeof metadata.branch === 'string' && metadata.branch.trim().length > 0
336
+ ? metadata.branch
337
+ : `feat/${id.toLowerCase()}`,
324
338
  impactAnalysis: null,
325
339
  };
326
340
  }
@@ -369,18 +383,23 @@ export async function migrateSpecToLean(specDir, projectPath) {
369
383
  catch {
370
384
  return 'skipped'; // No spec.md — not a valid spec dir
371
385
  }
386
+ // Parse frontmatter
387
+ const { metadata } = parseFrontmatterLocal(specContent);
388
+ const status = readTopLevelStatus(specContent) ?? 'draft';
389
+ // Terminal specs are immutable during automatic migration. This guard runs before
390
+ // backups, writes, or cleanup so init_project cannot alter completed decisions.
391
+ if (isTerminalStatus(status)) {
392
+ return 'skipped';
393
+ }
372
394
  // Skip if already lean
373
395
  if (isLeanFormat(specContent)) {
374
396
  return 'skipped';
375
397
  }
376
- // Parse frontmatter
377
- const { metadata } = parseFrontmatterLocal(specContent);
378
398
  const id = resolveSpecId(metadata, specDir, specContent);
379
399
  if (!id) {
380
400
  return 'missing valid spec id';
381
401
  }
382
402
  const title = typeof metadata.title === 'string' ? metadata.title : 'Untitled';
383
- const status = typeof metadata.status === 'string' ? metadata.status : 'draft';
384
403
  // Read old technical.md first (needed for estimation extraction)
385
404
  let oldTechContent = '';
386
405
  let files = { create: [], modify: [], test: [] };
@@ -396,12 +415,6 @@ export async function migrateSpecToLean(specDir, projectPath) {
396
415
  const criteria = extractOldCriteria(specContent);
397
416
  const estimation = buildEstimationFromOld(metadata, oldTechContent);
398
417
  const spec = buildSpecFromMetadata(metadata, id, title, status, estimation, specPath, techPath);
399
- // If status is done, mark all criteria as done
400
- if (status === 'done') {
401
- for (const c of criteria) {
402
- c.done = true;
403
- }
404
- }
405
418
  // Generate lean spec.md with extracted criteria
406
419
  const leanSpecLines = generateLeanSpecContent({ spec, description, estimation });
407
420
  // Inject real criteria (replace the default one)
@@ -118,8 +118,8 @@ export async function createSpec(projectId, spec) {
118
118
  ...spec,
119
119
  healthScore: computeHealthScore(spec, criteria.length > 0 ? criteria : undefined).score,
120
120
  };
121
- specs.push(specWithScore);
122
- await saveAll(projectId, specs);
121
+ const nextSpecs = [...specs, specWithScore];
122
+ await saveAll(projectId, nextSpecs);
123
123
  return specWithScore;
124
124
  });
125
125
  }
@@ -158,8 +158,8 @@ async function __internalUpdateSpec(projectId, specId, updates) {
158
158
  ...merged,
159
159
  healthScore: computeHealthScore(merged, criteria.length > 0 ? criteria : undefined).score,
160
160
  };
161
- specs[idx] = updated;
162
- await saveAll(projectId, specs);
161
+ const nextSpecs = specs.map((current, currentIdx) => (currentIdx === idx ? updated : current));
162
+ await saveAll(projectId, nextSpecs);
163
163
  return updated;
164
164
  });
165
165
  }
@@ -204,8 +204,8 @@ export async function deleteSpec(projectId, specId) {
204
204
  if (idx === -1) {
205
205
  return false;
206
206
  }
207
- specs.splice(idx, 1);
208
- await saveAll(projectId, specs);
207
+ const nextSpecs = specs.filter((_, currentIdx) => currentIdx !== idx);
208
+ await saveAll(projectId, nextSpecs);
209
209
  return true;
210
210
  });
211
211
  }