@planu/cli 4.3.24 → 4.4.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 (69) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/engine/constitution/sdd-rules-registry.d.ts +12 -0
  3. package/dist/engine/constitution/sdd-rules-registry.js +105 -0
  4. package/dist/engine/evidence-index/done-drift.d.ts +6 -0
  5. package/dist/engine/evidence-index/done-drift.js +44 -0
  6. package/dist/engine/evidence-index/index-builder.d.ts +10 -0
  7. package/dist/engine/evidence-index/index-builder.js +138 -0
  8. package/dist/engine/handoff-artifacts/index.d.ts +1 -16
  9. package/dist/engine/handoff-artifacts/io.d.ts +1 -1
  10. package/dist/engine/handoff-artifacts/schemas.d.ts +1 -1
  11. package/dist/engine/handoff-artifacts/validation-result.d.ts +18 -0
  12. package/dist/engine/handoff-artifacts/validation-result.js +3 -0
  13. package/dist/engine/hooks/handlers/on-impl-change.js +3 -2
  14. package/dist/engine/host-rules-templates/templates.js +3 -0
  15. package/dist/engine/spec-format/lean-spec-generator.js +17 -12
  16. package/dist/engine/spec-grounding/contract.d.ts +19 -0
  17. package/dist/engine/spec-grounding/contract.js +186 -0
  18. package/dist/engine/spec-metrics/actionable-metrics.d.ts +10 -0
  19. package/dist/engine/spec-metrics/actionable-metrics.js +69 -0
  20. package/dist/engine/spec-quality/generic-output-gate.d.ts +3 -0
  21. package/dist/engine/spec-quality/generic-output-gate.js +105 -0
  22. package/dist/engine/test-contract-generator.js +41 -39
  23. package/dist/engine/test-scaffold-generator/unit-scaffold.js +10 -10
  24. package/dist/engine/test-spec-generator/criterion-parser.js +5 -24
  25. package/dist/storage/base-store.d.ts +0 -1
  26. package/dist/storage/base-store.js +0 -4
  27. package/dist/tools/create-spec.js +160 -30
  28. package/dist/tools/generate-tests/generators/concurrency-test-generator/java-templates.js +8 -13
  29. package/dist/tools/generate-tests/generators/concurrency-test-generator/js-templates.js +21 -47
  30. package/dist/tools/generate-tests/generators/concurrency-test-generator/python-rust-templates.js +3 -14
  31. package/dist/tools/git/auto-complete-ops.d.ts +18 -0
  32. package/dist/tools/git/auto-complete-ops.js +63 -0
  33. package/dist/tools/git/branch-ops.d.ts +0 -17
  34. package/dist/tools/git/branch-ops.js +0 -54
  35. package/dist/tools/git/cleanup-ops.js +0 -16
  36. package/dist/tools/git/release-ops.js +1 -1
  37. package/dist/tools/init-project/handler.js +1 -1
  38. package/dist/tools/manage-hooks.js +3 -1
  39. package/dist/tools/status-handler.js +1 -1
  40. package/dist/tools/update-status/evidence-gate.js +23 -6
  41. package/dist/tools/update-status/transition-guard.js +49 -0
  42. package/dist/tools/update-status-actions.js +8 -5
  43. package/dist/types/actionable-spec-metrics.d.ts +8 -0
  44. package/dist/types/actionable-spec-metrics.js +2 -0
  45. package/dist/types/clarification-token.d.ts +1 -1
  46. package/dist/types/clarification.d.ts +2 -18
  47. package/dist/types/evidence-gates.d.ts +1 -1
  48. package/dist/types/evidence-index.d.ts +24 -0
  49. package/dist/types/evidence-index.js +2 -0
  50. package/dist/types/hook-status-update.d.ts +10 -0
  51. package/dist/types/hook-status-update.js +3 -0
  52. package/dist/types/index.d.ts +6 -0
  53. package/dist/types/index.js +6 -0
  54. package/dist/types/interactive-question.d.ts +19 -0
  55. package/dist/types/interactive-question.js +3 -0
  56. package/dist/types/sdd-constitution.d.ts +27 -0
  57. package/dist/types/sdd-constitution.js +2 -0
  58. package/dist/types/spec-format.d.ts +7 -0
  59. package/dist/types/spec-grounding.d.ts +22 -0
  60. package/dist/types/spec-grounding.js +2 -0
  61. package/dist/types/spec-quality.d.ts +10 -0
  62. package/dist/types/storage.d.ts +1 -1
  63. package/package.json +9 -9
  64. package/planu-native.json +1 -1
  65. package/planu-plugin.json +1 -1
  66. package/dist/engine/hooks/index.d.ts +0 -20
  67. package/dist/engine/hooks/index.js +0 -25
  68. package/dist/storage/crud-store-factory.d.ts +0 -22
  69. package/dist/storage/crud-store-factory.js +0 -72
@@ -0,0 +1,186 @@
1
+ const GENERIC_CRITERION_PATTERNS = [
2
+ /\bimplementation complete\b/i,
3
+ /\bcomplete and tested\b/i,
4
+ /\bensure (adequate )?coverage\b/i,
5
+ /\ball edge cases\b/i,
6
+ /\bgeneric validation\b/i,
7
+ /\bautopilot could not infer testable behavior\b/i,
8
+ /\bdefine acceptance criteria\b/i,
9
+ ];
10
+ const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---/;
11
+ export function normalizeCriterionText(text) {
12
+ return text
13
+ .replace(/^[-*]\s*\[[ xX]\]\s*/, '')
14
+ .replace(/\s+/g, ' ')
15
+ .trim()
16
+ .toLowerCase();
17
+ }
18
+ export function isGenericCriterion(text) {
19
+ return GENERIC_CRITERION_PATTERNS.some((pattern) => pattern.test(text));
20
+ }
21
+ export function filterGroundedCriteria(criteria) {
22
+ return criteria.filter((criterion) => criterion.trim().length > 0 && !isGenericCriterion(criterion));
23
+ }
24
+ export function buildCriterionGroundingRecords(args) {
25
+ const userInput = normalizeCriterionText(args.userInput);
26
+ const projectEvidence = args.projectEvidence ?? [];
27
+ const generatedEvidence = args.generatedEvidence ?? [];
28
+ return args.criteria.map((criterion) => {
29
+ const normalized = normalizeCriterionText(criterion);
30
+ if (isGenericCriterion(criterion)) {
31
+ return {
32
+ text: criterion,
33
+ source: 'ungrounded_advisory',
34
+ evidence: [],
35
+ confidence: 'low',
36
+ };
37
+ }
38
+ if (normalized.length > 0 && userInput.includes(normalized)) {
39
+ return {
40
+ text: criterion,
41
+ source: 'user_input',
42
+ evidence: ['create_spec.description'],
43
+ confidence: 'high',
44
+ };
45
+ }
46
+ if (projectEvidence.length > 0) {
47
+ return {
48
+ text: criterion,
49
+ source: 'project_evidence',
50
+ evidence: projectEvidence.slice(0, 8),
51
+ confidence: 'medium',
52
+ };
53
+ }
54
+ return {
55
+ text: criterion,
56
+ source: 'documented_assumption',
57
+ evidence: generatedEvidence.length > 0 ? generatedEvidence.slice(0, 4) : ['generated-spec-body'],
58
+ confidence: 'medium',
59
+ };
60
+ });
61
+ }
62
+ export function getContractCriteria(records) {
63
+ return records.filter((record) => record.source === 'user_input' || record.source === 'project_evidence');
64
+ }
65
+ export function getAdvisoryCriteria(records) {
66
+ return records.filter((record) => record.source === 'documented_assumption' || record.source === 'ungrounded_advisory');
67
+ }
68
+ export function renderGroundingFrontmatter(records) {
69
+ if (records.length === 0) {
70
+ return ['grounding_required: true', 'grounding:', ' criteria: []'];
71
+ }
72
+ return [
73
+ 'grounding_required: true',
74
+ 'grounding:',
75
+ ' criteria:',
76
+ ...records.flatMap((record) => [
77
+ ` - text: "${escapeYaml(record.text)}"`,
78
+ ` source: ${record.source}`,
79
+ ` evidence: [${record.evidence.map((item) => `"${escapeYaml(item)}"`).join(', ')}]`,
80
+ ` confidence: ${record.confidence}`,
81
+ ]),
82
+ ];
83
+ }
84
+ export function renderTechnicalReferenceGroundingFrontmatter(records) {
85
+ if (records.length === 0) {
86
+ return [' technicalReferences: []'];
87
+ }
88
+ return [
89
+ ' technicalReferences:',
90
+ ...records.flatMap((record) => [
91
+ ` - path: "${escapeYaml(record.path)}"`,
92
+ ` section: ${record.section}`,
93
+ ` source: ${record.source}`,
94
+ ` evidence: [${record.evidence.map((item) => `"${escapeYaml(item)}"`).join(', ')}]`,
95
+ ` confidence: ${record.confidence}`,
96
+ ]),
97
+ ];
98
+ }
99
+ export function checkGroundedSpecContract(spec, content) {
100
+ const frontmatter = FRONTMATTER_RE.exec(content)?.[1] ?? '';
101
+ const required = /^grounding_required:\s*true\s*$/m.test(frontmatter);
102
+ const records = parseCriterionGroundingRecords(frontmatter);
103
+ if (!required) {
104
+ return { passed: true, required: false, issues: [], records };
105
+ }
106
+ const issues = [];
107
+ if (records.length === 0 && spec.scope !== 'trivial') {
108
+ issues.push(`Spec ${spec.id} requires grounding metadata but has no grounded criteria records.`);
109
+ }
110
+ for (const record of records) {
111
+ if (record.source === 'ungrounded_advisory') {
112
+ issues.push(`Criterion is advisory-only and cannot be approved as contract: ${record.text}`);
113
+ }
114
+ if (record.evidence.length === 0 && record.source !== 'ungrounded_advisory') {
115
+ issues.push(`Criterion has no grounding evidence: ${record.text}`);
116
+ }
117
+ }
118
+ const technicalRecords = parseTechnicalReferenceGroundingRecords(frontmatter);
119
+ for (const record of technicalRecords) {
120
+ if (record.source === 'ungrounded_advisory') {
121
+ issues.push(`Technical reference is advisory-only and cannot be contract: ${record.path}`);
122
+ }
123
+ if (record.evidence.length === 0 && record.source !== 'ungrounded_advisory') {
124
+ issues.push(`Technical reference has no grounding evidence: ${record.path}`);
125
+ }
126
+ }
127
+ return {
128
+ passed: issues.length === 0,
129
+ required: true,
130
+ issues,
131
+ records,
132
+ };
133
+ }
134
+ export function parseCriterionGroundingRecords(frontmatter) {
135
+ const records = [];
136
+ const chunks = frontmatter.split(/\n\s{4}- text:\s*/).slice(1);
137
+ for (const chunk of chunks) {
138
+ const text = /^"((?:\\"|[^"])*)"/.exec(chunk)?.[1]?.replace(/\\"/g, '"') ?? '';
139
+ const source = /^\s{6}source:\s*(\S+)/m.exec(chunk)?.[1];
140
+ const confidence = /^\s{6}confidence:\s*(\S+)/m.exec(chunk)?.[1];
141
+ const evidenceRaw = /^\s{6}evidence:\s*\[(.*)\]/m.exec(chunk)?.[1] ?? '';
142
+ if (!text || !source || !confidence) {
143
+ continue;
144
+ }
145
+ records.push({
146
+ text,
147
+ source,
148
+ evidence: parseYamlInlineStringArray(evidenceRaw),
149
+ confidence,
150
+ });
151
+ }
152
+ return records;
153
+ }
154
+ export function parseTechnicalReferenceGroundingRecords(frontmatter) {
155
+ const records = [];
156
+ const chunks = frontmatter.split(/\n\s{4}- path:\s*/).slice(1);
157
+ for (const chunk of chunks) {
158
+ const path = /^"((?:\\"|[^"])*)"/.exec(chunk)?.[1]?.replace(/\\"/g, '"') ?? '';
159
+ const section = /^\s{6}section:\s*(\S+)/m.exec(chunk)?.[1];
160
+ const source = /^\s{6}source:\s*(\S+)/m.exec(chunk)?.[1];
161
+ const confidence = /^\s{6}confidence:\s*(\S+)/m.exec(chunk)?.[1];
162
+ const evidenceRaw = /^\s{6}evidence:\s*\[(.*)\]/m.exec(chunk)?.[1] ?? '';
163
+ if (!path || !section || !source || !confidence) {
164
+ continue;
165
+ }
166
+ records.push({
167
+ path,
168
+ section,
169
+ source,
170
+ evidence: parseYamlInlineStringArray(evidenceRaw),
171
+ confidence,
172
+ });
173
+ }
174
+ return records;
175
+ }
176
+ function parseYamlInlineStringArray(raw) {
177
+ if (raw.trim().length === 0) {
178
+ return [];
179
+ }
180
+ const matches = raw.matchAll(/"((?:\\"|[^"])*)"/g);
181
+ return [...matches].map((match) => match[1]?.replace(/\\"/g, '"') ?? '').filter(Boolean);
182
+ }
183
+ function escapeYaml(value) {
184
+ return value.replace(/"/g, '\\"');
185
+ }
186
+ //# sourceMappingURL=contract.js.map
@@ -0,0 +1,10 @@
1
+ import type { ActionableSpecMetric, CriterionGroundingRecord, EvidenceGateIssue, SpecEvidenceIndex } from '../../types/index.js';
2
+ export declare function calculateActionableSpecMetrics(args: {
3
+ criteria: readonly string[];
4
+ groundingRecords?: readonly CriterionGroundingRecord[];
5
+ evidenceIndex?: SpecEvidenceIndex;
6
+ ambiguityFindings?: readonly string[];
7
+ driftIssues?: readonly EvidenceGateIssue[];
8
+ }): ActionableSpecMetric[];
9
+ export declare function shouldExposeMetric(metric: ActionableSpecMetric): boolean;
10
+ //# sourceMappingURL=actionable-metrics.d.ts.map
@@ -0,0 +1,69 @@
1
+ export function calculateActionableSpecMetrics(args) {
2
+ return [
3
+ groundingCoverage(args.criteria, args.groundingRecords ?? []),
4
+ verificationCoverage(args.evidenceIndex),
5
+ ambiguityCount(args.ambiguityFindings ?? []),
6
+ driftRisk(args.driftIssues ?? []),
7
+ ].filter((metric) => metric !== null);
8
+ }
9
+ export function shouldExposeMetric(metric) {
10
+ return metric.inputs.length > 0 && metric.nextAction.trim().length > 0;
11
+ }
12
+ function groundingCoverage(criteria, records) {
13
+ const missing = criteria.filter((criterion) => !records.some((record) => normalize(record.text) === normalize(criterion) &&
14
+ record.source !== 'ungrounded_advisory' &&
15
+ record.evidence.length > 0));
16
+ if (criteria.length === 0 || missing.length === 0) {
17
+ return null;
18
+ }
19
+ return {
20
+ name: 'groundingCoverage',
21
+ value: Math.round(((criteria.length - missing.length) / criteria.length) * 100),
22
+ inputs: missing,
23
+ nextAction: 'Add grounding evidence or move ungrounded criteria to discovery/advisory output.',
24
+ };
25
+ }
26
+ function verificationCoverage(index) {
27
+ if (!index) {
28
+ return null;
29
+ }
30
+ const missing = index.criteria.filter((entry) => !entry.evidence.some((record) => record.kind === 'validation' && record.status === 'valid'));
31
+ if (index.criteria.length === 0 || missing.length === 0) {
32
+ return null;
33
+ }
34
+ return {
35
+ name: 'verificationCoverage',
36
+ value: Math.round(((index.criteria.length - missing.length) / index.criteria.length) * 100),
37
+ inputs: missing.map((entry) => entry.criterion),
38
+ nextAction: 'Attach validation evidence mapped to each uncovered criterion.',
39
+ };
40
+ }
41
+ function ambiguityCount(findings) {
42
+ if (findings.length === 0) {
43
+ return null;
44
+ }
45
+ return {
46
+ name: 'ambiguityCount',
47
+ value: findings.length,
48
+ inputs: [...findings],
49
+ nextAction: 'Resolve each ambiguity or mark it explicitly out of scope before implementation.',
50
+ };
51
+ }
52
+ function driftRisk(issues) {
53
+ const driftIssues = issues.filter((issue) => issue.code.startsWith('done_drift_'));
54
+ if (driftIssues.length === 0) {
55
+ return null;
56
+ }
57
+ return {
58
+ name: 'driftRisk',
59
+ value: driftIssues.some((issue) => issue.code === 'done_drift_unapproved_scope')
60
+ ? 'high'
61
+ : 'medium',
62
+ inputs: driftIssues.map((issue) => issue.message),
63
+ nextAction: 'Reconcile scope, replace stale evidence, or map validation to the affected criteria.',
64
+ };
65
+ }
66
+ function normalize(value) {
67
+ return value.replace(/\s+/g, ' ').trim().toLowerCase();
68
+ }
69
+ //# sourceMappingURL=actionable-metrics.js.map
@@ -0,0 +1,3 @@
1
+ import type { GenericSpecOutputGateResult } from '../../types/index.js';
2
+ export declare function checkGenericSpecOutput(content: string): GenericSpecOutputGateResult;
3
+ //# sourceMappingURL=generic-output-gate.d.ts.map
@@ -0,0 +1,105 @@
1
+ const CRITERIA_SECTION_RE = /^criteria:\n([\s\S]*?)(?=^[a-zA-Z_][\w-]*:|\n---|\s*$)/m;
2
+ const CRITERION_TEXT_RE = /^\s+- text:\s*"?(.+?)"?\s*$/gm;
3
+ const FILE_LINE_RE = /^-\s+(.+?)\s+\((pending|done)\)\s*$/gm;
4
+ const GENERIC_CRITERION_RULES = [
5
+ {
6
+ pattern: /\bimplementation (is )?complete\b/i,
7
+ reason: 'self-certifying completion is not a testable behavior',
8
+ },
9
+ {
10
+ pattern: /\bcomplete and tested\b/i,
11
+ reason: 'completion and testing must be proven by evidence, not criteria text',
12
+ },
13
+ {
14
+ pattern: /\ball edge cases\b/i,
15
+ reason: 'edge cases must be named explicitly',
16
+ },
17
+ {
18
+ pattern: /\bgeneric validation\b/i,
19
+ reason: 'validation must identify the behavior and expected result',
20
+ },
21
+ {
22
+ pattern: /\bensure (adequate )?coverage\b/i,
23
+ reason: 'coverage claims must map to concrete validation evidence',
24
+ },
25
+ {
26
+ pattern: /\bautopilot could not infer testable behavior\b/i,
27
+ reason: 'fallback uncertainty cannot become an acceptance criterion',
28
+ },
29
+ {
30
+ pattern: /\bdefine acceptance criteria\b/i,
31
+ reason: 'a request to define criteria is not itself a criterion',
32
+ },
33
+ {
34
+ pattern: /\b(example only|for example|sample)\b/i,
35
+ reason: 'example-only references cannot become contract criteria',
36
+ },
37
+ {
38
+ pattern: /\b(add|create|implement)\s+(api\s+)?(endpoint|route|migration)s?\b/i,
39
+ reason: 'broad implementation references must name grounded behavior and evidence',
40
+ },
41
+ ];
42
+ const PLACEHOLDER_REFERENCE_RULES = [
43
+ {
44
+ pattern: /\b(to be determined|tbd|placeholder|not specified)\b/i,
45
+ reason: 'placeholder references are not grounded implementation ownership',
46
+ },
47
+ {
48
+ pattern: /(^|\/)(example|sample|mock|dummy)[\w.-]*\.[a-z]+$/i,
49
+ reason: 'example file names must not be persisted as technical ownership',
50
+ },
51
+ ];
52
+ export function checkGenericSpecOutput(content) {
53
+ const issues = [
54
+ ...checkCriteria(frontmatterCriteria(content)),
55
+ ...checkTechnicalReferences(content),
56
+ ];
57
+ return { passed: issues.length === 0, issues };
58
+ }
59
+ function frontmatterCriteria(content) {
60
+ const criteriaSection = CRITERIA_SECTION_RE.exec(content)?.[1] ?? '';
61
+ const criteria = [];
62
+ let match = CRITERION_TEXT_RE.exec(criteriaSection);
63
+ while (match) {
64
+ const phrase = match[1]?.replace(/\\"/g, '"').trim() ?? '';
65
+ if (phrase.length > 0) {
66
+ criteria.push(phrase);
67
+ }
68
+ match = CRITERION_TEXT_RE.exec(criteriaSection);
69
+ }
70
+ return criteria;
71
+ }
72
+ function checkCriteria(criteria) {
73
+ const issues = [];
74
+ for (const criterion of criteria) {
75
+ for (const rule of GENERIC_CRITERION_RULES) {
76
+ if (rule.pattern.test(criterion)) {
77
+ issues.push({
78
+ kind: rule.reason.includes('coverage') ? 'unsupported-verification' : 'generic-criterion',
79
+ phrase: criterion,
80
+ reason: rule.reason,
81
+ });
82
+ }
83
+ }
84
+ }
85
+ return issues;
86
+ }
87
+ function checkTechnicalReferences(content) {
88
+ const issues = [];
89
+ let match = FILE_LINE_RE.exec(content);
90
+ while (match) {
91
+ const phrase = match[1]?.replace(/`/g, '').trim() ?? '';
92
+ for (const rule of PLACEHOLDER_REFERENCE_RULES) {
93
+ if (rule.pattern.test(phrase)) {
94
+ issues.push({
95
+ kind: 'placeholder-reference',
96
+ phrase,
97
+ reason: rule.reason,
98
+ });
99
+ }
100
+ }
101
+ match = FILE_LINE_RE.exec(content);
102
+ }
103
+ return issues;
104
+ }
105
+ //# sourceMappingURL=generic-output-gate.js.map
@@ -25,12 +25,12 @@ function buildContractContent(title, endpoints, format) {
25
25
  return buildOpenApiContent(title, endpoints);
26
26
  }
27
27
  if (format === 'graphql-schema') {
28
- return buildGraphqlContent(title);
28
+ return buildGraphqlContent(title, endpoints);
29
29
  }
30
30
  if (format === 'pact') {
31
31
  return buildPactContent(title, endpoints);
32
32
  }
33
- return buildTrpcContent(title);
33
+ return buildTrpcContent(title, endpoints);
34
34
  }
35
35
  function buildOpenApiContent(title, endpoints) {
36
36
  const paths = endpoints
@@ -45,34 +45,45 @@ function buildOpenApiContent(title, endpoints) {
45
45
  const openapiVersion = getStandardVersion('openapi') || '3.0.3';
46
46
  return `openapi: '${openapiVersion}'\ninfo:\n title: "${title} API Contract"\n version: '1.0.0'\npaths:\n${paths}\n`;
47
47
  }
48
- function buildGraphqlContent(title) {
48
+ function toGraphqlFieldName(ep) {
49
+ const pathName = ep.path
50
+ .split('/')
51
+ .filter(Boolean)
52
+ .map((segment) => segment.replace(/[{}:]/g, ''))
53
+ .join(' ');
54
+ const base = toSnake(pathName || 'root')
55
+ .replace(/_+/g, '_')
56
+ .replace(/^_|_$/g, '');
57
+ if (ep.method === 'GET') {
58
+ return base || 'root';
59
+ }
60
+ return `${ep.method.toLowerCase()}_${base || 'root'}`;
61
+ }
62
+ function buildGraphqlContent(title, endpoints) {
49
63
  const typeName = toPascal(title);
64
+ const queryFields = endpoints
65
+ .filter((ep) => ep.method === 'GET')
66
+ .map((ep) => ` ${toGraphqlFieldName(ep)}: ${typeName}`)
67
+ .join('\n');
68
+ const mutationFields = endpoints
69
+ .filter((ep) => ep.method !== 'GET')
70
+ .map((ep) => ` ${toGraphqlFieldName(ep)}: ${typeName}`)
71
+ .join('\n');
50
72
  return [
51
73
  `# GraphQL schema for: ${title}`,
52
74
  '# Generated by Planu SDD MCP Server',
75
+ '# Only endpoint-derived fields are emitted; add domain fields from an explicit schema.',
53
76
  '',
54
77
  `type ${typeName} {`,
55
78
  ' id: ID!',
56
- ' # TODO: add fields',
57
79
  '}',
58
80
  '',
59
81
  'type Query {',
60
- ` get${typeName}(id: ID!): ${typeName}`,
61
- ` list${typeName}s: [${typeName}!]!`,
82
+ queryFields || ' _empty: Boolean',
62
83
  '}',
63
84
  '',
64
85
  'type Mutation {',
65
- ` create${typeName}(input: Create${typeName}Input!): ${typeName}!`,
66
- ` update${typeName}(id: ID!, input: Update${typeName}Input!): ${typeName}!`,
67
- ` delete${typeName}(id: ID!): Boolean!`,
68
- '}',
69
- '',
70
- `input Create${typeName}Input {`,
71
- ' # TODO: add fields',
72
- '}',
73
- '',
74
- `input Update${typeName}Input {`,
75
- ' # TODO: add fields',
86
+ mutationFields || ' _empty: Boolean',
76
87
  '}',
77
88
  '',
78
89
  ].join('\n');
@@ -90,29 +101,25 @@ function buildPactContent(title, endpoints) {
90
101
  metadata: { pactSpecification: { version: '2.0.0' } },
91
102
  }, null, 2);
92
103
  }
93
- function buildTrpcContent(title) {
104
+ function buildTrpcProcedure(ep) {
105
+ const procedureName = toGraphqlFieldName(ep);
106
+ const procedureKind = ep.method === 'GET' ? 'query' : 'mutation';
107
+ return [
108
+ ` ${procedureName}: publicProcedure.${procedureKind}(async () => {`,
109
+ ` throw new Error('Bind ${ep.method} ${ep.path} to its implementation before running this router.');`,
110
+ ' }),',
111
+ ].join('\n');
112
+ }
113
+ function buildTrpcContent(title, endpoints) {
94
114
  const routerName = toSnake(title);
115
+ const procedures = endpoints.map((ep) => buildTrpcProcedure(ep)).join('\n');
95
116
  return [
96
117
  `// tRPC router contract for: ${title}`,
97
118
  '// Generated by Planu SDD MCP Server',
98
119
  "import { router, publicProcedure } from '../trpc';",
99
- "import { z } from 'zod';",
100
120
  '',
101
121
  `export const ${routerName}Router = router({`,
102
- ' getAll: publicProcedure.query(async () => {',
103
- ' // TODO: implement',
104
- ' return [];',
105
- ' }),',
106
- ' getById: publicProcedure.input(z.object({ id: z.string() })).query(async ({ input }) => {',
107
- ' // TODO: implement',
108
- ' return { id: input.id };',
109
- ' }),',
110
- ' create: publicProcedure',
111
- ' .input(z.object({ /* TODO: add fields */ }))',
112
- ' .mutation(async () => {',
113
- ' // TODO: implement',
114
- " return { id: 'new-id' };",
115
- ' }),',
122
+ procedures,
116
123
  '});',
117
124
  '',
118
125
  ].join('\n');
@@ -142,12 +149,7 @@ function buildSampleData(ep) {
142
149
  if (ep.responseSchema && Object.keys(ep.responseSchema).length > 0) {
143
150
  return ep.responseSchema;
144
151
  }
145
- return {
146
- id: 'sample-id-001',
147
- createdAt: new Date().toISOString(),
148
- updatedAt: new Date().toISOString(),
149
- data: {},
150
- };
152
+ return {};
151
153
  }
152
154
  /**
153
155
  * Generate an MSW (Mock Service Worker) handler file from mock definitions.
@@ -37,26 +37,26 @@ function buildBaseUnitContent(title, criteria, framework) {
37
37
  }
38
38
  function buildJsUnit(title, criteria, framework) {
39
39
  const importLine = framework === 'vitest'
40
- ? "import { describe, it, expect } from 'vitest';"
41
- : "import { describe, it, expect } from '@jest/globals';";
40
+ ? "import { describe, it } from 'vitest';"
41
+ : "import { describe, it } from '@jest/globals';";
42
42
  const tests = criteria
43
- .map((c) => ` it('${c}', () => {\n // TODO: implement\n expect(true).toBe(true);\n });`)
43
+ .map((c) => ` it.skip('${c}', () => {\n throw new Error('Bind this criterion to implementation evidence before enabling.');\n });`)
44
44
  .join('\n\n');
45
45
  return `${importLine}\n\ndescribe('${title}', () => {\n${tests}\n});\n`;
46
46
  }
47
47
  function buildPytestUnit(title, criteria) {
48
48
  const cls = toPascal(title);
49
49
  const methods = criteria
50
- .map((c) => ` def test_${toSnake(c)}(self):\n """${c}"""\n # TODO: implement\n assert True`)
50
+ .map((c) => ` @pytest.mark.skip(reason="Bind this criterion to implementation evidence before enabling.")\n def test_${toSnake(c)}(self):\n """${c}"""\n raise AssertionError("Bind this criterion to implementation evidence before enabling.")`)
51
51
  .join('\n\n');
52
52
  return `import pytest\n\n\nclass Test${cls}:\n """Unit tests for ${title}."""\n\n${methods}\n`;
53
53
  }
54
54
  function buildJunitUnit(title, criteria) {
55
55
  const cls = toPascal(title);
56
56
  const methods = criteria
57
- .map((c) => ` @Test\n void ${toSnake(c).replace(/_([a-z])/g, (_, l) => l.toUpperCase())}() {\n // TODO: implement ${c}\n assertTrue(true);\n }`)
57
+ .map((c) => ` @Test\n @Disabled("Bind this criterion to implementation evidence before enabling.")\n void ${toSnake(c).replace(/_([a-z])/g, (_, l) => l.toUpperCase())}() {\n fail("Bind this criterion to implementation evidence before enabling: ${c}");\n }`)
58
58
  .join('\n\n');
59
- return `import org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n\nclass ${cls}Test {\n\n${methods}\n}\n`;
59
+ return `import org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n\nclass ${cls}Test {\n\n${methods}\n}\n`;
60
60
  }
61
61
  // === Integration test scaffold ===
62
62
  /**
@@ -77,11 +77,11 @@ export function generateIntegrationTestScaffold(specTitle, framework) {
77
77
  function buildIntegrationContent(title, framework) {
78
78
  if (framework === 'pytest') {
79
79
  const cls = toPascal(title);
80
- return `import pytest\n\n\nclass Test${cls}Integration:\n """Integration tests for ${title}."""\n\n def test_api_endpoint(self):\n """Test API endpoint integration."""\n # TODO: implement\n assert True\n\n def test_database_round_trip(self):\n """Test database persistence."""\n # TODO: implement\n assert True\n`;
80
+ return `import pytest\n\n\nclass Test${cls}Integration:\n """Integration tests for ${title}."""\n\n @pytest.mark.skip(reason="Bind integration boundaries before enabling.")\n def test_declared_integration_boundary(self):\n raise AssertionError("Bind integration boundaries before enabling.")\n`;
81
81
  }
82
82
  const importLine = framework === 'vitest'
83
- ? "import { describe, it, expect } from 'vitest';"
84
- : "import { describe, it, expect } from '@jest/globals';";
85
- return `${importLine}\n\ndescribe('${title} — Integration', () => {\n it('should handle API endpoint correctly', async () => {\n // TODO: implement\n expect(true).toBe(true);\n });\n\n it('should persist data to storage', async () => {\n // TODO: implement\n expect(true).toBe(true);\n });\n});\n`;
83
+ ? "import { describe, it } from 'vitest';"
84
+ : "import { describe, it } from '@jest/globals';";
85
+ return `${importLine}\n\ndescribe('${title} — Integration', () => {\n it.skip('declared integration boundary is verified', async () => {\n throw new Error('Bind integration boundaries before enabling.');\n });\n});\n`;
86
86
  }
87
87
  //# sourceMappingURL=unit-scaffold.js.map
@@ -1,29 +1,10 @@
1
1
  // engine/test-spec-generator/criterion-parser.ts — Extract Given/When/Then from criteria text
2
2
  export function suggestTestData(criterion) {
3
- const lower = criterion.toLowerCase();
4
- if (lower.includes('api') || lower.includes('endpoint') || lower.includes('url')) {
5
- return 'endpoint: "https://api.example.com/v1/resource", method: "POST"';
6
- }
7
- if (lower.includes('user') || lower.includes('usuario') || lower.includes('usuário')) {
8
- return 'user: { id: "usr-123", email: "test@example.com", role: "admin" }';
9
- }
10
- if (lower.includes('email')) {
11
- return 'email: "user@example.com", subject: "Test subject", body: "Test body"';
12
- }
13
- if (lower.includes('file') || lower.includes('arquivo') || lower.includes('fichero')) {
14
- return 'file: { path: "/tmp/test.json", size: 1024, mimeType: "application/json" }';
15
- }
16
- if (lower.includes('database') || lower.includes('db') || lower.includes('banco')) {
17
- return 'db: { host: "localhost", port: 5432, name: "test_db" }';
18
- }
19
- if (lower.includes('token') || lower.includes('auth') || lower.includes('jwt')) {
20
- return 'token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.test.signature"';
21
- }
22
- if (lower.includes('config')) {
23
- return 'config: { enabled: true, timeout: 5000, retries: 3 }';
24
- }
25
- if (lower.includes('spec') || lower.includes('criteria')) {
26
- return 'specId: "SPEC-001", title: "Test feature", criteria: ["criterion 1", "criterion 2"]';
3
+ const quotedValues = Array.from(criterion.matchAll(/["'`]([^"'`]+)["'`]/g))
4
+ .map((match) => match[1]?.trim())
5
+ .filter((value) => Boolean(value));
6
+ if (quotedValues.length > 0) {
7
+ return `explicitValues: ${JSON.stringify(quotedValues)}`;
27
8
  }
28
9
  return undefined;
29
10
  }
@@ -50,5 +50,4 @@ export declare function projectDataDir(projectId: string): string;
50
50
  * and CI environments where HOME is not writable).
51
51
  */
52
52
  export declare function globalDataDir(): string;
53
- export { createCrudStore } from './crud-store-factory.js';
54
53
  //# sourceMappingURL=base-store.d.ts.map
@@ -116,8 +116,4 @@ export function globalDataDir() {
116
116
  function isNodeError(err) {
117
117
  return err instanceof Error && 'code' in err;
118
118
  }
119
- // ---------------------------------------------------------------------------
120
- // Generic CRUD store factory (re-exported from crud-store-factory.ts)
121
- // ---------------------------------------------------------------------------
122
- export { createCrudStore } from './crud-store-factory.js';
123
119
  //# sourceMappingURL=base-store.js.map