@planu/cli 4.10.12 → 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 +9 -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
@@ -1,8 +1,9 @@
1
1
  // engine/provider-adapters/adapters/claude.ts — SPEC-670: Claude Code renderer.
2
2
  //
3
- // Emits MCP-shaped output: full canonical markdown plus model hint, budget hint and
4
- // `mcp__planu__*` tool callouts. Tag-driven enrichment is a follow-up; for v1 we use
5
- // a static map of spec-tag → recommended Planu MCP tools.
3
+ // Emits MCP-shaped output: full canonical markdown plus evidence-backed execution
4
+ // hints and `mcp__planu__*` tool callouts.
5
+ import { resolveModelId } from '../../model-tier-resolver.js';
6
+ import { resolveExecutionBudget, resolveExecutionTier, } from '../../next-spec-resolver/orchestration-planner.js';
6
7
  import { estimateTokens } from '../shared/token-estimator.js';
7
8
  /**
8
9
  * Static map of spec tag → recommended `mcp__planu__*` tool callouts. A tag-driven
@@ -20,17 +21,31 @@ export const CLAUDE_TOOL_CALLOUT_MAP = Object.freeze({
20
21
  });
21
22
  export class ClaudeRenderer {
22
23
  kind = 'claude';
23
- render(input) {
24
- const { loadedSpec } = input;
24
+ async render(input) {
25
+ const { loadedSpec, projectPath } = input;
25
26
  const { spec, specBody, warnings } = loadedSpec;
26
- const modelHint = spec.model ?? 'sonnet';
27
- const budget = spec.budget ?? 2000;
27
+ const modelTier = resolveExecutionTier(spec);
28
+ const model = modelTier ? await resolveModelId(projectPath, modelTier, 'anthropic') : undefined;
29
+ const budget = resolveExecutionBudget(spec);
30
+ const explicitLegacyTier = resolveExplicitLegacyTier(spec);
28
31
  const toolCallouts = pickToolCallouts(spec.tags);
29
32
  const lines = [];
30
33
  lines.push(`# ${spec.title}`);
31
34
  lines.push('');
32
- lines.push(`> Model hint: \`${modelHint}\` — Token budget: \`${budget}\``);
33
- lines.push('');
35
+ const routingHints = [];
36
+ if (model && modelTier) {
37
+ routingHints.push(`Model hint: \`${model}\` (tier: \`${modelTier}\`)`);
38
+ }
39
+ else if (explicitLegacyTier) {
40
+ routingHints.push(`Legacy model constraint: \`${explicitLegacyTier}\``);
41
+ }
42
+ if (budget !== undefined) {
43
+ routingHints.push(`Token budget constraint: \`${budget}\``);
44
+ }
45
+ if (routingHints.length > 0) {
46
+ lines.push(`> ${routingHints.join(' — ')}`);
47
+ lines.push('');
48
+ }
34
49
  lines.push('## Recommended Planu MCP tools');
35
50
  for (const callout of toolCallouts) {
36
51
  lines.push(`- Run \`${callout}\` to drive this spec forward.`);
@@ -44,19 +59,28 @@ export class ClaudeRenderer {
44
59
  }
45
60
  const content = lines.join('\n');
46
61
  const tokenEstimate = estimateTokens(content);
47
- return Promise.resolve({
62
+ return {
48
63
  provider: 'claude',
49
64
  content,
50
65
  tokenEstimate,
51
66
  extras: {
52
- modelHint,
53
- model: modelHint,
54
- budget,
67
+ ...(model ? { modelHint: model, model } : {}),
68
+ ...(modelTier && (model || explicitLegacyTier) ? { modelTier } : {}),
69
+ ...(budget !== undefined ? { budget } : {}),
55
70
  toolCallouts: [...toolCallouts],
56
71
  ...(warnings.length > 0 ? { warnings } : {}),
57
72
  },
58
- });
73
+ };
74
+ }
75
+ }
76
+ function resolveExplicitLegacyTier(spec) {
77
+ const legacyRecord = spec;
78
+ const hasCurrentContext = legacyRecord.difficulty !== undefined || legacyRecord.scope !== undefined;
79
+ if (hasCurrentContext) {
80
+ return undefined;
59
81
  }
82
+ const value = legacyRecord.model;
83
+ return value === 'haiku' || value === 'sonnet' || value === 'opus' ? value : undefined;
60
84
  }
61
85
  function pickToolCallouts(tags) {
62
86
  const collected = new Set();
@@ -1,7 +1,8 @@
1
1
  // engine/scan-project/index.ts — 4-phase orchestrator for scan_project (SPEC-134)
2
2
  import { join } from 'node:path';
3
- import { mkdir, readdir, writeFile } from 'node:fs/promises';
4
- import { replaceSectionInSpec } from '../spec-format/replace-section.js';
3
+ import { mkdir, readdir } from 'node:fs/promises';
4
+ import { atomicWriteFile } from '../safety/atomic-write-file.js';
5
+ import { serializeValueOnlySpecContent } from '../spec-format/value-only-spec-serializer.js';
5
6
  import { discoverModules } from './module-discoverer.js';
6
7
  import { analyzeCrossModuleDependencies } from './cross-module-analyzer.js';
7
8
  import { calculateProjectHealth } from './health-scorer.js';
@@ -9,7 +10,6 @@ import { generateProjectOverview } from './overview-generator.js';
9
10
  import { generateTeamPlan } from './team-planner.js';
10
11
  import { runDeepAnalysis } from '../reverse-engineer/index.js';
11
12
  import { createSpec, listSpecs } from '../../storage/spec-store.js';
12
- import { generateProgressContent } from '../progress-writer.js';
13
13
  // Re-export submodules for barrel access
14
14
  export { discoverModules } from './module-discoverer.js';
15
15
  export { analyzeCrossModuleDependencies } from './cross-module-analyzer.js';
@@ -69,14 +69,11 @@ async function analyzeModulesInParallel(projectPath, modules, concurrency) {
69
69
  // Phase 4: Spec generation
70
70
  // ---------------------------------------------------------------------------
71
71
  const SPEC_LOCATION = 'planu/specs';
72
- async function writeSpecFiles(specDir, spec, body) {
72
+ async function writeSpecFile(specDir, spec, description, criteria, technical) {
73
73
  await mkdir(specDir, { recursive: true });
74
74
  const specMdPath = join(specDir, 'spec.md');
75
- await writeFile(specMdPath, body, 'utf-8');
76
- // SPEC-1010 PR-C: embed Technical and Progress into unified spec.md instead of
77
- // writing standalone technical.md / progress.md files.
78
- await replaceSectionInSpec(specMdPath, 'Technical', `Auto-generated by scan_project.\n`);
79
- await replaceSectionInSpec(specMdPath, 'Progress', generateProgressContent(spec));
75
+ const content = serializeValueOnlySpecContent({ spec, description, criteria, technical });
76
+ await atomicWriteFile(specMdPath, content);
80
77
  }
81
78
  async function generateSpecs(input, modules, moduleResults, crossAnalysis, healthScore) {
82
79
  const specsGenerated = [];
@@ -102,8 +99,10 @@ async function generateSpecs(input, modules, moduleResults, crossAnalysis, healt
102
99
  const specDir = join(input.path, SPEC_LOCATION, `${specId}-${slug}`);
103
100
  spec.specPath = join(specDir, 'spec.md');
104
101
  spec.technicalPath = spec.specPath;
105
- const body = `# ${spec.title}\n\nAuto-generated by scan_project.\n`;
106
- await writeSpecFiles(specDir, spec, body);
102
+ const description = buildModuleDescription(mod, result);
103
+ const criteria = buildModuleCriteria(mod.name, result, crossAnalysis);
104
+ const technical = buildModuleTechnical(mod, result, deps);
105
+ await writeSpecFile(specDir, spec, description, criteria, technical);
107
106
  await createSpec(input.projectId, spec);
108
107
  specsGenerated.push(specId);
109
108
  currentId++;
@@ -128,10 +127,8 @@ async function generateSpecs(input, modules, moduleResults, crossAnalysis, healt
128
127
  const overviewDir = join(input.path, SPEC_LOCATION, `${overviewSpecId}-${overviewSlug}`);
129
128
  overviewSpec.specPath = join(overviewDir, 'spec.md');
130
129
  overviewSpec.technicalPath = overviewSpec.specPath;
131
- const overviewBody = overviewContent
132
- ? `# Project Overview Automated Scan\n\n${overviewContent}\n`
133
- : `# Project Overview — Automated Scan\n\nAuto-generated by scan_project.\n`;
134
- await writeSpecFiles(overviewDir, overviewSpec, overviewBody);
130
+ const overviewDescription = overviewContent || buildOverviewDescription(modules, healthScore);
131
+ await writeSpecFile(overviewDir, overviewSpec, overviewDescription, buildOverviewCriteria(crossAnalysis), buildOverviewTechnical(modules));
135
132
  await createSpec(input.projectId, overviewSpec);
136
133
  return { specsGenerated, overviewSpecId };
137
134
  }
@@ -177,8 +174,84 @@ function buildScanSpec(params) {
177
174
  gitBranch: params.gitBranch,
178
175
  impactAnalysis: null,
179
176
  reviewNotes: params.reviewNotes,
177
+ generation: {
178
+ method: 'deterministic',
179
+ generatedAt: now,
180
+ },
180
181
  };
181
182
  }
183
+ function buildModuleDescription(module, result) {
184
+ const lines = [
185
+ `# Module: ${module.name}`,
186
+ '',
187
+ '## Description',
188
+ '',
189
+ `\`${module.path}\` contains approximately ${String(module.estimatedFiles)} ${module.language} files.`,
190
+ ];
191
+ if (result?.status === 'failed' && result.error) {
192
+ lines.push('', `Analysis failed: ${result.error}`);
193
+ }
194
+ return lines.join('\n');
195
+ }
196
+ function buildModuleCriteria(moduleName, result, crossAnalysis) {
197
+ const findings = [];
198
+ for (const path of result?.analysis.testCoverage.untestedModules ?? []) {
199
+ findings.push(`Add automated coverage for \`${path}\`.`);
200
+ }
201
+ for (const antipattern of crossAnalysis.antipatterns) {
202
+ if (antipattern.modules.includes(moduleName)) {
203
+ findings.push(antipattern.suggestion);
204
+ }
205
+ }
206
+ for (const cycle of crossAnalysis.circularDeps) {
207
+ if (cycle.includes(moduleName)) {
208
+ findings.push(`Remove the detected circular dependency: ${cycle.join(' -> ')}.`);
209
+ }
210
+ }
211
+ if (result?.status === 'failed' && result.error) {
212
+ findings.push(`Restore static analysis for \`${moduleName}\`: ${result.error}`);
213
+ }
214
+ return [...new Set(findings)].map((text) => ({ text, done: false }));
215
+ }
216
+ function buildModuleTechnical(module, result, dependencies) {
217
+ const lines = [
218
+ '### Scan Evidence',
219
+ '',
220
+ `- Path: \`${module.path}\``,
221
+ `- Language: ${module.language}`,
222
+ `- Files: ${String(module.estimatedFiles)}`,
223
+ `- Analysis status: ${result?.status ?? 'missing'}`,
224
+ ];
225
+ if (result?.status === 'success') {
226
+ lines.push(`- Test/source ratio: ${result.analysis.testCoverage.testToSourceRatio.toFixed(2)}`);
227
+ }
228
+ if (dependencies.length > 0) {
229
+ lines.push('', '### Incoming Dependencies', '', ...dependencies.map((name) => `- ${name}`));
230
+ }
231
+ return lines.join('\n');
232
+ }
233
+ function buildOverviewDescription(modules, healthScore) {
234
+ return [
235
+ '# Project Overview',
236
+ '',
237
+ '## Description',
238
+ '',
239
+ `${String(modules.length)} modules were detected with health score ${String(healthScore.score)}/100 (${healthScore.grade}).`,
240
+ ].join('\n');
241
+ }
242
+ function buildOverviewCriteria(crossAnalysis) {
243
+ return crossAnalysis.antipatterns.map((finding) => ({
244
+ text: finding.suggestion,
245
+ done: false,
246
+ }));
247
+ }
248
+ function buildOverviewTechnical(modules) {
249
+ return [
250
+ '### Scanned Modules',
251
+ '',
252
+ ...modules.map((module) => `- \`${module.path}\` (${module.language})`),
253
+ ].join('\n');
254
+ }
182
255
  /**
183
256
  * Extract the numeric part of a SPEC-NNN string. Returns 0 if no match.
184
257
  */
@@ -1,5 +1,5 @@
1
- import type { LeanCriterion, LeanHistoryEntry, LeanSpecInput } from '../../types/index.js';
2
- export type { LeanCriterion, LeanHistoryEntry, LeanSpecInput };
1
+ import type { LeanCriterion, LeanSpecInput } from '../../types/index.js';
2
+ export type { LeanCriterion, LeanHistoryEntry, LeanSpecInput } from '../../types/index.js';
3
3
  /** Generate lean spec.md content: YAML frontmatter + user description only. */
4
4
  export declare function generateLeanSpecContent(input: LeanSpecInput): string;
5
5
  /** Extract acceptance criteria from description. Looks for checkbox patterns or generates a default.
@@ -1,57 +1,41 @@
1
1
  // engine/spec-format/lean-spec-generator.ts — Generates lean spec.md with YAML frontmatter (SPEC-461)
2
2
  // Output: ~30-50 lines. No generic sections, no OWASP, no STRIDE, no resilience criteria.
3
3
  import { parseBddScenarios, renderBddScenariosYaml, convertCheckboxToBdd } from './bdd-parser.js';
4
- import { deriveModelBudget } from './model-budget-deriver.js';
4
+ import { decideMetadataSerialization, } from './metadata-value-policy.js';
5
5
  import { renderGroundingFrontmatter, renderTechnicalReferenceGroundingFrontmatter, } from '../spec-grounding/contract.js';
6
- /** Strip redundant "SPEC-XXX — " or "SPEC-XXX: " prefix from spec titles.
6
+ /** Strip a redundant spec-ID prefix from spec titles.
7
7
  * Titles must not repeat the ID since it is already stored in the `id` field. */
8
8
  function sanitizeTitle(title, specId) {
9
- // Match "SPEC-123 ", "SPEC-123: ", "SPEC-123 - " at the start
9
+ // Match common spec ID separators at the start.
10
10
  const prefixRegex = new RegExp(`^${specId}\\s*[—:\\-]\\s*`, 'i');
11
11
  return title.replace(prefixRegex, '').trim();
12
12
  }
13
13
  /** Generate lean spec.md content: YAML frontmatter + user description only. */
14
14
  export function generateLeanSpecContent(input) {
15
- const { spec, description, estimation, extraCriteria = [], acFormat = 'checkbox' } = input;
15
+ const { spec, description, extraCriteria = [], acFormat = 'checkbox' } = input;
16
16
  const now = spec.createdAt.slice(0, 10); // YYYY-MM-DD
17
17
  const cleanTitle = sanitizeTitle(spec.title, spec.id);
18
- const history = [{ date: now, event: 'created' }];
19
- const { model, budget } = deriveModelBudget(spec.difficulty, estimation.devHours);
20
- const frontmatterBase = [
21
- '---',
22
- `id: ${spec.id}`,
23
- ...(spec.uuid ? [`uuid: ${spec.uuid}`] : []),
24
- ...(spec.idempotencyKey ? [`idempotencyKey: ${spec.idempotencyKey}`] : []),
25
- `title: "${escapeYaml(cleanTitle)}"`,
26
- `status: ${spec.status}`,
27
- `type: ${spec.type}`,
28
- `target: ${spec.target}`,
29
- `scope: ${spec.scope}`,
18
+ const frontmatterBase = ['---'];
19
+ appendMetadata(frontmatterBase, 'id', spec.id, [`id: ${spec.id}`]);
20
+ appendMetadata(frontmatterBase, 'title', cleanTitle, [`title: "${escapeYaml(cleanTitle)}"`]);
21
+ appendMetadata(frontmatterBase, 'status', spec.status, [`status: ${spec.status}`]);
22
+ appendMetadata(frontmatterBase, 'type', spec.type, [`type: ${spec.type}`]);
23
+ appendMetadata(frontmatterBase, 'target', spec.target, [`target: ${spec.target}`]);
24
+ appendMetadata(frontmatterBase, 'scope', spec.scope, [`scope: ${spec.scope}`]);
25
+ appendMetadata(frontmatterBase, 'difficulty', spec.difficulty, [
30
26
  `difficulty: ${String(spec.difficulty)}`,
31
- `risk: ${spec.risk}`,
32
- `tags: [${spec.tags.join(', ')}]`,
33
- `branch: ${spec.gitBranch}`,
34
- `created: ${now}`,
35
- // SPEC-717: spec format versioning fields
36
- `spec_format_version: "1.0"`,
37
- `spec_version: "1.0.0"`,
38
- `estimation:`,
39
- ` devHours: ${String(estimation.devHours)}`,
40
- ` reviewHours: ${String(estimation.reviewHours)}`,
41
- ` cost: ${String(estimation.totalCostUsd)}`,
42
- ` model: ${estimation.recommendedModel}`,
43
- `model: ${model}`,
44
- `budget: ${String(budget)}`,
45
- ...(spec.generatedWithModel ? [`generatedWithModel: ${spec.generatedWithModel}`] : []),
46
- ...(spec.generatedAt ? [`generatedAt: ${spec.generatedAt}`] : []),
47
- ...(spec.qualityWarnings && spec.qualityWarnings.length > 0
48
- ? [`qualityWarnings: ${JSON.stringify(spec.qualityWarnings)}`]
49
- : []),
50
- ...(input.groundingCriteria ? renderGroundingFrontmatter(input.groundingCriteria) : []),
51
- ...(input.groundingTechnicalReferences
52
- ? renderTechnicalReferenceGroundingFrontmatter(input.groundingTechnicalReferences)
53
- : []),
54
- ];
27
+ ]);
28
+ appendMetadata(frontmatterBase, 'risk', spec.risk, [`risk: ${spec.risk}`]);
29
+ appendMetadata(frontmatterBase, 'tags', spec.tags, [`tags: [${spec.tags.join(', ')}]`]);
30
+ appendMetadata(frontmatterBase, 'branch', spec.gitBranch, [`branch: ${spec.gitBranch}`]);
31
+ appendMetadata(frontmatterBase, 'created', now, [`created: ${now}`]);
32
+ appendMetadata(frontmatterBase, 'spec_format_version', '1.0', [`spec_format_version: "1.0"`]);
33
+ appendMetadata(frontmatterBase, 'spec_version', '1.0.0', [`spec_version: "1.0.0"`]);
34
+ appendMetadata(frontmatterBase, 'generation', spec.generation, renderGeneration(spec.generation));
35
+ appendMetadata(frontmatterBase, 'qualityWarnings', spec.qualityWarnings, [
36
+ `qualityWarnings: ${JSON.stringify(spec.qualityWarnings)}`,
37
+ ]);
38
+ appendGroundingMetadata(frontmatterBase, input);
55
39
  let acLines;
56
40
  if (acFormat === 'bdd') {
57
41
  acLines = buildBddLines(description, extraCriteria, input.criteriaOverride, input.scenarioTestPaths ?? []);
@@ -59,18 +43,49 @@ export function generateLeanSpecContent(input) {
59
43
  else {
60
44
  acLines = buildCheckboxLines(description, extraCriteria, input.criteriaOverride);
61
45
  }
62
- const lines = [
63
- ...frontmatterBase,
64
- ...acLines,
65
- 'history:',
66
- ...history.map((h) => ` - date: ${h.date}\n event: ${h.event}`),
67
- '---',
68
- '',
69
- description.trim(),
70
- '',
71
- ];
46
+ appendMetadata(frontmatterBase, acFormat === 'bdd' ? 'scenarios' : 'criteria', acLines.slice(1), acLines);
47
+ const lines = [...frontmatterBase, '---', '', description.trim(), ''];
72
48
  return lines.join('\n');
73
49
  }
50
+ function appendMetadata(lines, field, value, rendered) {
51
+ if (decideMetadataSerialization(field, value).persist) {
52
+ lines.push(...rendered);
53
+ }
54
+ }
55
+ function renderGeneration(generation) {
56
+ if (!generation) {
57
+ return [];
58
+ }
59
+ return [
60
+ 'generation:',
61
+ ` method: ${generation.method}`,
62
+ ...(generation.host ? [` host: "${escapeYaml(generation.host)}"`] : []),
63
+ ...(generation.modelId && generation.method !== 'deterministic'
64
+ ? [` modelId: "${escapeYaml(generation.modelId)}"`]
65
+ : []),
66
+ ` generatedAt: "${escapeYaml(generation.generatedAt)}"`,
67
+ ];
68
+ }
69
+ function appendGroundingMetadata(lines, input) {
70
+ const criteria = input.groundingCriteria ?? [];
71
+ const technicalReferences = input.groundingTechnicalReferences ?? [];
72
+ const hasGrounding = criteria.length > 0 || technicalReferences.length > 0;
73
+ appendMetadata(lines, 'grounding_required', hasGrounding, ['grounding_required: true']);
74
+ if (!decideMetadataSerialization('grounding', hasGrounding ? { evidence: true } : undefined).persist) {
75
+ return;
76
+ }
77
+ if (criteria.length > 0) {
78
+ const [requiredLine, ...groundingLines] = renderGroundingFrontmatter(criteria);
79
+ void requiredLine;
80
+ lines.push(...groundingLines);
81
+ }
82
+ else {
83
+ lines.push('grounding:');
84
+ }
85
+ if (technicalReferences.length > 0) {
86
+ lines.push(...renderTechnicalReferenceGroundingFrontmatter(technicalReferences));
87
+ }
88
+ }
74
89
  /** Build checkbox-format criteria lines. */
75
90
  function buildCheckboxLines(description, extraCriteria, criteriaOverride) {
76
91
  const criteria = criteriaOverride ?? [
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Single source of truth for metadata written by the new-spec serializer.
3
+ * Keys match the root YAML fields so adding a writer requires an explicit policy decision.
4
+ */
5
+ export declare const SPEC_METADATA_VALUE_POLICY: {
6
+ readonly id: {
7
+ readonly classification: "required";
8
+ readonly consumers: readonly [string, ...string[]];
9
+ readonly purpose: string;
10
+ };
11
+ readonly title: {
12
+ readonly classification: "required";
13
+ readonly consumers: readonly [string, ...string[]];
14
+ readonly purpose: string;
15
+ };
16
+ readonly status: {
17
+ readonly classification: "required";
18
+ readonly consumers: readonly [string, ...string[]];
19
+ readonly purpose: string;
20
+ };
21
+ readonly type: {
22
+ readonly classification: "required";
23
+ readonly consumers: readonly [string, ...string[]];
24
+ readonly purpose: string;
25
+ };
26
+ readonly target: {
27
+ readonly classification: "required";
28
+ readonly consumers: readonly [string, ...string[]];
29
+ readonly purpose: string;
30
+ };
31
+ readonly scope: {
32
+ readonly classification: "required";
33
+ readonly consumers: readonly [string, ...string[]];
34
+ readonly purpose: string;
35
+ };
36
+ readonly difficulty: {
37
+ readonly classification: "required";
38
+ readonly consumers: readonly [string, ...string[]];
39
+ readonly purpose: string;
40
+ };
41
+ readonly risk: {
42
+ readonly classification: "required";
43
+ readonly consumers: readonly [string, ...string[]];
44
+ readonly purpose: string;
45
+ };
46
+ readonly branch: {
47
+ readonly classification: "required";
48
+ readonly consumers: readonly [string, ...string[]];
49
+ readonly purpose: string;
50
+ };
51
+ readonly created: {
52
+ readonly classification: "required";
53
+ readonly consumers: readonly [string, ...string[]];
54
+ readonly purpose: string;
55
+ };
56
+ readonly spec_format_version: {
57
+ readonly classification: "required";
58
+ readonly consumers: readonly [string, ...string[]];
59
+ readonly purpose: string;
60
+ };
61
+ readonly spec_version: {
62
+ readonly classification: "required";
63
+ readonly consumers: readonly [string, ...string[]];
64
+ readonly purpose: string;
65
+ };
66
+ readonly tags: {
67
+ readonly classification: "conditional";
68
+ readonly consumers: readonly [string, ...string[]];
69
+ readonly purpose: string;
70
+ };
71
+ readonly generation: {
72
+ readonly classification: "conditional";
73
+ readonly consumers: readonly [string, ...string[]];
74
+ readonly purpose: string;
75
+ };
76
+ readonly qualityWarnings: {
77
+ readonly classification: "conditional";
78
+ readonly consumers: readonly [string, ...string[]];
79
+ readonly purpose: string;
80
+ };
81
+ readonly grounding_required: {
82
+ readonly classification: "conditional";
83
+ readonly consumers: readonly [string, ...string[]];
84
+ readonly purpose: string;
85
+ };
86
+ readonly grounding: {
87
+ readonly classification: "conditional";
88
+ readonly consumers: readonly [string, ...string[]];
89
+ readonly purpose: string;
90
+ };
91
+ readonly criteria: {
92
+ readonly classification: "conditional";
93
+ readonly consumers: readonly [string, ...string[]];
94
+ readonly purpose: string;
95
+ };
96
+ readonly scenarios: {
97
+ readonly classification: "conditional";
98
+ readonly consumers: readonly [string, ...string[]];
99
+ readonly purpose: string;
100
+ };
101
+ readonly history: {
102
+ readonly classification: "conditional";
103
+ readonly consumers: readonly [string, ...string[]];
104
+ readonly purpose: string;
105
+ };
106
+ readonly plannerToken: {
107
+ readonly classification: "external-only";
108
+ readonly consumers: readonly [];
109
+ readonly purpose: string;
110
+ };
111
+ readonly uuid: {
112
+ readonly classification: "external-only";
113
+ readonly consumers: readonly [];
114
+ readonly purpose: string;
115
+ };
116
+ readonly idempotencyKey: {
117
+ readonly classification: "external-only";
118
+ readonly consumers: readonly [];
119
+ readonly purpose: string;
120
+ };
121
+ readonly estimation: {
122
+ readonly classification: "external-only";
123
+ readonly consumers: readonly [];
124
+ readonly purpose: string;
125
+ };
126
+ readonly model: {
127
+ readonly classification: "derived-at-execution";
128
+ readonly consumers: readonly [];
129
+ readonly purpose: string;
130
+ };
131
+ readonly budget: {
132
+ readonly classification: "derived-at-execution";
133
+ readonly consumers: readonly [];
134
+ readonly purpose: string;
135
+ };
136
+ readonly generatedWithModel: {
137
+ readonly classification: "legacy-read-only";
138
+ readonly consumers: readonly [];
139
+ readonly purpose: string;
140
+ };
141
+ readonly generatedAt: {
142
+ readonly classification: "legacy-read-only";
143
+ readonly consumers: readonly [];
144
+ readonly purpose: string;
145
+ };
146
+ };
147
+ export declare function getMetadataFieldPolicy(field: keyof typeof SPEC_METADATA_VALUE_POLICY): (typeof SPEC_METADATA_VALUE_POLICY)[keyof typeof SPEC_METADATA_VALUE_POLICY];
148
+ export declare function decideMetadataSerialization(field: keyof typeof SPEC_METADATA_VALUE_POLICY, value: unknown): {
149
+ readonly field: keyof typeof SPEC_METADATA_VALUE_POLICY;
150
+ readonly classification: (typeof SPEC_METADATA_VALUE_POLICY)[keyof typeof SPEC_METADATA_VALUE_POLICY]['classification'];
151
+ readonly persist: boolean;
152
+ readonly consumers: readonly string[];
153
+ readonly purpose: string;
154
+ };
155
+ export declare function listMetadataFieldPolicies(): readonly {
156
+ readonly field: keyof typeof SPEC_METADATA_VALUE_POLICY;
157
+ readonly classification: (typeof SPEC_METADATA_VALUE_POLICY)[keyof typeof SPEC_METADATA_VALUE_POLICY]['classification'];
158
+ readonly consumers: readonly string[];
159
+ readonly purpose: string;
160
+ }[];
161
+ //# sourceMappingURL=metadata-value-policy.d.ts.map
@@ -0,0 +1,87 @@
1
+ const required = (consumers, purpose) => ({ classification: 'required', consumers, purpose });
2
+ const conditional = (consumers, purpose) => ({ classification: 'conditional', consumers, purpose });
3
+ const omitted = (classification, purpose) => ({ classification, consumers: [], purpose });
4
+ /**
5
+ * Single source of truth for metadata written by the new-spec serializer.
6
+ * Keys match the root YAML fields so adding a writer requires an explicit policy decision.
7
+ */
8
+ export const SPEC_METADATA_VALUE_POLICY = {
9
+ id: required(['spec-store', 'lifecycle-tools'], 'Canonical spec identity.'),
10
+ title: required(['spec-store', 'list-specs'], 'Human-readable spec identity.'),
11
+ status: required(['transition-guard', 'spec-store'], 'Lifecycle gate state.'),
12
+ type: required(['spec-store', 'spec-filters'], 'Work classification.'),
13
+ target: required(['agent-planner', 'spec-store'], 'Implementation target.'),
14
+ scope: required(['readiness-gate', 'agent-planner'], 'Implementation breadth.'),
15
+ difficulty: required(['readiness-gate', 'runtime-routing'], 'Current complexity signal.'),
16
+ risk: required(['readiness-gate', 'challenge-spec'], 'Delivery risk signal.'),
17
+ branch: required(['git-manager', 'spec-store'], 'Lifecycle branch association.'),
18
+ created: required(['spec-store', 'audit-trail'], 'Creation evidence.'),
19
+ spec_format_version: required(['spec-migrator', 'spec-parser'], 'Schema compatibility.'),
20
+ spec_version: required(['version-gate', 'bump-spec-version'], 'Content version lifecycle.'),
21
+ tags: conditional(['spec-search', 'spec-filters'], 'Non-empty discovery labels.'),
22
+ generation: conditional(['spec-parser', 'provenance-audit'], 'Truthful provenance for the component that produced the current body.'),
23
+ qualityWarnings: conditional(['readiness-gate', 'spec-health-scorer'], 'Non-empty generator diagnostics that affect review.'),
24
+ grounding_required: conditional(['grounding-gate'], 'Signals that persisted grounding evidence must pass approval gates.'),
25
+ grounding: conditional(['grounding-gate'], 'Non-empty criterion or technical evidence.'),
26
+ criteria: conditional(['readiness-gate', 'validate'], 'Checkbox acceptance contract.'),
27
+ scenarios: conditional(['readiness-gate', 'validate'], 'BDD acceptance contract.'),
28
+ history: conditional(['version-gate', 'bump-spec-version'], 'Non-creation content version events.'),
29
+ plannerToken: omitted('external-only', 'Signed planner identity belongs to lifecycle state.'),
30
+ uuid: omitted('external-only', 'Operational identity belongs to the project store.'),
31
+ idempotencyKey: omitted('external-only', 'Retry deduplication belongs to the project store.'),
32
+ estimation: omitted('external-only', 'Advisory estimation telemetry belongs to tool state.'),
33
+ model: omitted('derived-at-execution', 'Provider routing uses current execution context.'),
34
+ budget: omitted('derived-at-execution', 'Token budgets use current execution context.'),
35
+ generatedWithModel: omitted('legacy-read-only', 'Ambiguous legacy generator provenance.'),
36
+ generatedAt: omitted('legacy-read-only', 'Legacy timestamp superseded by generation.'),
37
+ };
38
+ export function getMetadataFieldPolicy(field) {
39
+ return lookupMetadataFieldPolicy(field);
40
+ }
41
+ function lookupMetadataFieldPolicy(field) {
42
+ if (!Object.hasOwn(SPEC_METADATA_VALUE_POLICY, field)) {
43
+ throw new Error(`Unknown spec metadata field: ${field}`);
44
+ }
45
+ return SPEC_METADATA_VALUE_POLICY[field];
46
+ }
47
+ export function decideMetadataSerialization(field, value) {
48
+ const policy = lookupMetadataFieldPolicy(field);
49
+ const hasValue = hasMeaningfulValue(value);
50
+ if (policy.classification === 'required' && !hasValue) {
51
+ throw new Error(`Missing required spec metadata value: ${field}`);
52
+ }
53
+ return {
54
+ field,
55
+ classification: policy.classification,
56
+ persist: policy.classification === 'required' || (policy.classification === 'conditional' && hasValue),
57
+ consumers: policy.consumers,
58
+ purpose: policy.purpose,
59
+ };
60
+ }
61
+ export function listMetadataFieldPolicies() {
62
+ return Object.entries(SPEC_METADATA_VALUE_POLICY).map(([field, policy]) => ({
63
+ field: field,
64
+ classification: policy.classification,
65
+ consumers: policy.consumers,
66
+ purpose: policy.purpose,
67
+ }));
68
+ }
69
+ function hasMeaningfulValue(value) {
70
+ if (value === undefined || value === null || value === false) {
71
+ return false;
72
+ }
73
+ if (typeof value === 'string') {
74
+ return value.trim().length > 0;
75
+ }
76
+ if (typeof value === 'number') {
77
+ return Number.isFinite(value) && value !== 0;
78
+ }
79
+ if (Array.isArray(value)) {
80
+ return value.length > 0;
81
+ }
82
+ if (typeof value === 'object') {
83
+ return Object.keys(value).length > 0;
84
+ }
85
+ return true;
86
+ }
87
+ //# sourceMappingURL=metadata-value-policy.js.map
@@ -0,0 +1,12 @@
1
+ import type { LeanCriterion, Spec } from '../../types/index.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 declare function serializeValueOnlySpecContent(input: {
7
+ spec: Spec;
8
+ description: string;
9
+ criteria?: readonly LeanCriterion[];
10
+ technical?: string;
11
+ }): string;
12
+ //# sourceMappingURL=value-only-spec-serializer.d.ts.map