@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.
- package/CHANGELOG.md +6 -0
- package/dist/engine/constitution/sdd-rules-registry.d.ts +12 -0
- package/dist/engine/constitution/sdd-rules-registry.js +105 -0
- package/dist/engine/evidence-index/done-drift.d.ts +6 -0
- package/dist/engine/evidence-index/done-drift.js +44 -0
- package/dist/engine/evidence-index/index-builder.d.ts +10 -0
- package/dist/engine/evidence-index/index-builder.js +138 -0
- package/dist/engine/handoff-artifacts/index.d.ts +1 -16
- package/dist/engine/handoff-artifacts/io.d.ts +1 -1
- package/dist/engine/handoff-artifacts/schemas.d.ts +1 -1
- package/dist/engine/handoff-artifacts/validation-result.d.ts +18 -0
- package/dist/engine/handoff-artifacts/validation-result.js +3 -0
- package/dist/engine/hooks/handlers/on-impl-change.js +3 -2
- package/dist/engine/host-rules-templates/templates.js +3 -0
- package/dist/engine/spec-format/lean-spec-generator.js +17 -12
- package/dist/engine/spec-grounding/contract.d.ts +19 -0
- package/dist/engine/spec-grounding/contract.js +186 -0
- package/dist/engine/spec-metrics/actionable-metrics.d.ts +10 -0
- package/dist/engine/spec-metrics/actionable-metrics.js +69 -0
- package/dist/engine/spec-quality/generic-output-gate.d.ts +3 -0
- package/dist/engine/spec-quality/generic-output-gate.js +105 -0
- package/dist/engine/test-contract-generator.js +41 -39
- package/dist/engine/test-scaffold-generator/unit-scaffold.js +10 -10
- package/dist/engine/test-spec-generator/criterion-parser.js +5 -24
- package/dist/storage/base-store.d.ts +0 -1
- package/dist/storage/base-store.js +0 -4
- package/dist/tools/create-spec.js +160 -30
- package/dist/tools/generate-tests/generators/concurrency-test-generator/java-templates.js +8 -13
- package/dist/tools/generate-tests/generators/concurrency-test-generator/js-templates.js +21 -47
- package/dist/tools/generate-tests/generators/concurrency-test-generator/python-rust-templates.js +3 -14
- package/dist/tools/git/auto-complete-ops.d.ts +18 -0
- package/dist/tools/git/auto-complete-ops.js +63 -0
- package/dist/tools/git/branch-ops.d.ts +0 -17
- package/dist/tools/git/branch-ops.js +0 -54
- package/dist/tools/git/cleanup-ops.js +0 -16
- package/dist/tools/git/release-ops.js +1 -1
- package/dist/tools/init-project/handler.js +1 -1
- package/dist/tools/manage-hooks.js +3 -1
- package/dist/tools/status-handler.js +1 -1
- package/dist/tools/update-status/evidence-gate.js +23 -6
- package/dist/tools/update-status/transition-guard.js +49 -0
- package/dist/tools/update-status-actions.js +8 -5
- package/dist/types/actionable-spec-metrics.d.ts +8 -0
- package/dist/types/actionable-spec-metrics.js +2 -0
- package/dist/types/clarification-token.d.ts +1 -1
- package/dist/types/clarification.d.ts +2 -18
- package/dist/types/evidence-gates.d.ts +1 -1
- package/dist/types/evidence-index.d.ts +24 -0
- package/dist/types/evidence-index.js +2 -0
- package/dist/types/hook-status-update.d.ts +10 -0
- package/dist/types/hook-status-update.js +3 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.js +6 -0
- package/dist/types/interactive-question.d.ts +19 -0
- package/dist/types/interactive-question.js +3 -0
- package/dist/types/sdd-constitution.d.ts +27 -0
- package/dist/types/sdd-constitution.js +2 -0
- package/dist/types/spec-format.d.ts +7 -0
- package/dist/types/spec-grounding.d.ts +22 -0
- package/dist/types/spec-grounding.js +2 -0
- package/dist/types/spec-quality.d.ts +10 -0
- package/dist/types/storage.d.ts +1 -1
- package/package.json +9 -9
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
- package/dist/engine/hooks/index.d.ts +0 -20
- package/dist/engine/hooks/index.js +0 -25
- package/dist/storage/crud-store-factory.d.ts +0 -22
- package/dist/storage/crud-store-factory.js +0 -72
|
@@ -4,8 +4,9 @@ import { ti } from '../i18n/index.js';
|
|
|
4
4
|
import { knowledgeStore, specStore } from '../storage/index.js';
|
|
5
5
|
import { readTechnologySelectionContract } from '../storage/technology-selection-store.js';
|
|
6
6
|
import { formatSuccess, addNextSteps, toolResult, interactiveResult } from './response-helpers.js';
|
|
7
|
-
import { writeFile, mkdir, rm, readFile, stat } from 'node:fs/promises';
|
|
7
|
+
import { writeFile, mkdir, rm, readFile, stat as fsStat } from 'node:fs/promises';
|
|
8
8
|
import { createHash } from 'node:crypto';
|
|
9
|
+
import { join as pathJoin } from 'node:path';
|
|
9
10
|
import { estimateSpec } from '../engine/estimator.js';
|
|
10
11
|
import { checkSpecReadiness } from '../engine/readiness-checker.js';
|
|
11
12
|
import { buildSpecContext, buildSplitResult } from './create-spec/spec-builder.js';
|
|
@@ -15,7 +16,7 @@ import { notifyStoreChange } from '../engine/doc-generator/portal/regen-hook.js'
|
|
|
15
16
|
import { compactObj } from '../engine/compact-obj.js';
|
|
16
17
|
import { buildCreateSpecSummary } from '../engine/human-summary.js';
|
|
17
18
|
import { runAutoPostCreatePipeline } from './create-spec/auto-pipeline.js';
|
|
18
|
-
import { generateLeanSpecContent } from '../engine/spec-format/lean-spec-generator.js';
|
|
19
|
+
import { extractCriteria, generateLeanSpecContent, } from '../engine/spec-format/lean-spec-generator.js';
|
|
19
20
|
import { generateLeanTechnicalContent, } from '../engine/spec-format/lean-technical-generator.js';
|
|
20
21
|
import { extractFilesFromSpecBody } from '../engine/spec-format/technical-md-populator.js';
|
|
21
22
|
import { buildUnifiedSpecContent } from '../engine/spec-format/unified-spec-builder.js';
|
|
@@ -38,6 +39,9 @@ import { suggestOutOfScope } from '../engine/scope-boundaries/index.js';
|
|
|
38
39
|
import { adviseSimilarSpecs } from '../engine/complexity-budget/index.js';
|
|
39
40
|
import { issuePlannerToken } from '../engine/reviewer-tokens/issuer.js';
|
|
40
41
|
import { generateInteractiveQuestions } from './create-spec/question-generator.js';
|
|
42
|
+
import { buildCriterionGroundingRecords, filterGroundedCriteria, getAdvisoryCriteria, getContractCriteria, } from '../engine/spec-grounding/contract.js';
|
|
43
|
+
import { checkGenericSpecOutput } from '../engine/spec-quality/generic-output-gate.js';
|
|
44
|
+
import { calculateActionableSpecMetrics, shouldExposeMetric, } from '../engine/spec-metrics/actionable-metrics.js';
|
|
41
45
|
/** SPEC-584: Persist a clarification token when interactive questions are emitted. Best-effort. */
|
|
42
46
|
async function persistClarificationToken(earlyReturn, projectId, toolName) {
|
|
43
47
|
try {
|
|
@@ -121,6 +125,69 @@ async function resolveTechnicalFiles(input) {
|
|
|
121
125
|
}
|
|
122
126
|
return { create: [], modify: [], test: [] };
|
|
123
127
|
}
|
|
128
|
+
async function groundTechnicalFiles(input) {
|
|
129
|
+
const userInput = input.userInput.toLowerCase();
|
|
130
|
+
const autopilotPaths = new Set([
|
|
131
|
+
...input.autopilot.suggestedFiles.modify.map((file) => file.path),
|
|
132
|
+
...input.autopilot.suggestedFiles.create.map((file) => file.path),
|
|
133
|
+
...input.autopilot.suggestedFiles.test.map((file) => file.path),
|
|
134
|
+
]);
|
|
135
|
+
const grounded = {
|
|
136
|
+
create: [],
|
|
137
|
+
modify: [],
|
|
138
|
+
test: [],
|
|
139
|
+
};
|
|
140
|
+
const records = [];
|
|
141
|
+
const advisoryFiles = [];
|
|
142
|
+
for (const section of ['create', 'modify', 'test']) {
|
|
143
|
+
for (const file of input.files[section]) {
|
|
144
|
+
const pathInUserInput = userInput.includes(file.path.toLowerCase());
|
|
145
|
+
const exists = await pathExists(pathJoin(input.projectPath, file.path));
|
|
146
|
+
const fromAutopilot = autopilotPaths.has(file.path);
|
|
147
|
+
const derivedTest = section === 'test' && isDerivedFromGroundedSource(file.path, input.files.modify);
|
|
148
|
+
if (pathInUserInput || exists || fromAutopilot || derivedTest) {
|
|
149
|
+
grounded[section].push(file);
|
|
150
|
+
records.push({
|
|
151
|
+
path: file.path,
|
|
152
|
+
section,
|
|
153
|
+
source: pathInUserInput ? 'user_input' : 'project_evidence',
|
|
154
|
+
evidence: [
|
|
155
|
+
...(pathInUserInput ? ['create_spec.description'] : []),
|
|
156
|
+
...(exists ? [`file:${file.path}`] : []),
|
|
157
|
+
...(fromAutopilot ? ['autopilot.suggestedFiles'] : []),
|
|
158
|
+
...(derivedTest ? ['derived-test-path'] : []),
|
|
159
|
+
],
|
|
160
|
+
confidence: pathInUserInput || exists ? 'high' : 'medium',
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
advisoryFiles.push(file);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { files: grounded, records, advisoryFiles };
|
|
169
|
+
}
|
|
170
|
+
function isDerivedFromGroundedSource(testPath, modifyFiles) {
|
|
171
|
+
return modifyFiles.some((file) => {
|
|
172
|
+
const withoutSrc = file.path.startsWith('src/') ? file.path.slice(4) : file.path;
|
|
173
|
+
const ext = withoutSrc.replace(/^.*(\.[^.]+)$/, '$1');
|
|
174
|
+
const base = withoutSrc.slice(0, -ext.length);
|
|
175
|
+
return testPath === `tests/${base}.test${ext}` || testPath === `tests/${withoutSrc}`;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
async function pathExists(path) {
|
|
179
|
+
try {
|
|
180
|
+
const fs = await import('node:fs/promises');
|
|
181
|
+
if (typeof fs.stat !== 'function') {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
await fs.stat(path);
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
124
191
|
const HIGH_RISK_WARNING = 'High-risk spec — consider: ' +
|
|
125
192
|
'1) Is the approach technically feasible within the project constraints? ' +
|
|
126
193
|
'2) What edge cases (network failure, concurrent writes, empty state) could break this? ' +
|
|
@@ -304,10 +371,10 @@ async function findByIdempotencyKey(projectPath, key, projectId, windowMs = 10 *
|
|
|
304
371
|
// Fallback: scan frontmatter in spec.md files (catches specs written before store update)
|
|
305
372
|
try {
|
|
306
373
|
const { glob } = await import('glob');
|
|
307
|
-
const { join:
|
|
308
|
-
const specFiles = await glob(
|
|
374
|
+
const { join: joinPath } = await import('node:path');
|
|
375
|
+
const specFiles = await glob(joinPath(projectPath, 'planu/specs/*/spec.md'));
|
|
309
376
|
for (const specFile of specFiles) {
|
|
310
|
-
const fileStat = await
|
|
377
|
+
const fileStat = await fsStat(specFile).catch(() => null);
|
|
311
378
|
if (fileStat === null || fileStat.mtimeMs < cutoff) {
|
|
312
379
|
continue;
|
|
313
380
|
}
|
|
@@ -338,12 +405,12 @@ async function findByIdempotencyKey(projectPath, key, projectId, windowMs = 10 *
|
|
|
338
405
|
async function findRecentlyWrittenSpecs(projectPath, windowMs) {
|
|
339
406
|
try {
|
|
340
407
|
const { glob } = await import('glob');
|
|
341
|
-
const { join:
|
|
408
|
+
const { join: joinPath } = await import('node:path');
|
|
342
409
|
const cutoff = Date.now() - windowMs;
|
|
343
|
-
const specFiles = await glob(
|
|
410
|
+
const specFiles = await glob(joinPath(projectPath, 'planu/specs/*/spec.md'));
|
|
344
411
|
const results = [];
|
|
345
412
|
for (const specFile of specFiles) {
|
|
346
|
-
const fileStat = await
|
|
413
|
+
const fileStat = await fsStat(specFile).catch(() => null);
|
|
347
414
|
if (fileStat === null || fileStat.mtimeMs < cutoff) {
|
|
348
415
|
continue;
|
|
349
416
|
}
|
|
@@ -528,7 +595,7 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
528
595
|
}
|
|
529
596
|
// Create spec directory and write lean files (SPEC-461)
|
|
530
597
|
await measureStep('mkdir-specDir', () => mkdir(specDir, { recursive: true }));
|
|
531
|
-
const filteredCriteria = autopilot.suggestedCriteria;
|
|
598
|
+
const filteredCriteria = filterGroundedCriteria(autopilot.suggestedCriteria);
|
|
532
599
|
const technologyContract = await readTechnologySelectionContract(params.projectPath ?? '');
|
|
533
600
|
const contractNote = technologyContract
|
|
534
601
|
? [
|
|
@@ -568,13 +635,24 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
568
635
|
spec.generatedWithModel = generatedSpec.generatedWithModel;
|
|
569
636
|
spec.generatedAt = generatedSpec.generatedAt;
|
|
570
637
|
spec.qualityWarnings = generatedSpec.qualityWarnings;
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
638
|
+
const baseCriteria = extractCriteria(generatedSpec.specBody).map((criterion) => criterion.text);
|
|
639
|
+
const groundingCriteria = buildCriterionGroundingRecords({
|
|
640
|
+
criteria: [...baseCriteria, ...filteredCriteria],
|
|
641
|
+
userInput: description,
|
|
642
|
+
projectEvidence: [
|
|
643
|
+
...autopilot.detectedPatterns.map((pattern) => `detected-pattern:${pattern}`),
|
|
644
|
+
...autopilot.suggestedFiles.modify.map((file) => `file:${file.path}`),
|
|
645
|
+
...autopilot.suggestedFiles.create.map((file) => `file:${file.path}`),
|
|
646
|
+
...autopilot.suggestedFiles.test.map((file) => `file:${file.path}`),
|
|
647
|
+
],
|
|
648
|
+
generatedEvidence: [generatedSpec.generatedWithModel],
|
|
577
649
|
});
|
|
650
|
+
const contractCriteria = getContractCriteria(groundingCriteria);
|
|
651
|
+
const advisoryCriteria = getAdvisoryCriteria(groundingCriteria).map((record) => record.text);
|
|
652
|
+
const actionableMetrics = calculateActionableSpecMetrics({
|
|
653
|
+
criteria: [...baseCriteria, ...filteredCriteria],
|
|
654
|
+
groundingRecords: groundingCriteria,
|
|
655
|
+
}).filter(shouldExposeMetric);
|
|
578
656
|
const technicalFiles = await measureStep('resolveTechnicalFiles', () => resolveTechnicalFiles({
|
|
579
657
|
generatedSpecBody: generatedSpec.specBody,
|
|
580
658
|
generatedTechnicalSection: generatedSpec.technicalSection,
|
|
@@ -582,16 +660,59 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
582
660
|
autopilot,
|
|
583
661
|
fallbackReason: generatedSpec.fallbackReason,
|
|
584
662
|
}));
|
|
663
|
+
const groundedTechnical = await measureStep('groundTechnicalFiles', () => groundTechnicalFiles({
|
|
664
|
+
files: technicalFiles,
|
|
665
|
+
projectPath: params.projectPath ?? '',
|
|
666
|
+
userInput: description,
|
|
667
|
+
autopilot,
|
|
668
|
+
}));
|
|
669
|
+
const leanSpec = generateLeanSpecContent({
|
|
670
|
+
spec,
|
|
671
|
+
description: generatedSpec.specBody,
|
|
672
|
+
estimation,
|
|
673
|
+
criteriaOverride: contractCriteria.map((record) => ({
|
|
674
|
+
text: record.text,
|
|
675
|
+
done: false,
|
|
676
|
+
})),
|
|
677
|
+
groundingCriteria,
|
|
678
|
+
groundingTechnicalReferences: groundedTechnical.records,
|
|
679
|
+
acFormat: params.acFormat,
|
|
680
|
+
});
|
|
585
681
|
const leanTechnical = generateLeanTechnicalContent({
|
|
586
682
|
specId: spec.id,
|
|
587
|
-
filesToCreate:
|
|
588
|
-
filesToModify:
|
|
589
|
-
filesToTest:
|
|
683
|
+
filesToCreate: groundedTechnical.files.create,
|
|
684
|
+
filesToModify: groundedTechnical.files.modify,
|
|
685
|
+
filesToTest: groundedTechnical.files.test,
|
|
590
686
|
});
|
|
591
687
|
// SPEC-709: write unified spec.md from origin — no separate technical.md.
|
|
592
688
|
// The legacy two-file output is preserved by appending the technical body
|
|
593
689
|
// as a `## Technical` section inside spec.md.
|
|
594
690
|
const unifiedSpec = buildUnifiedSpecContent(leanSpec, leanTechnical);
|
|
691
|
+
const genericOutputGate = checkGenericSpecOutput(unifiedSpec);
|
|
692
|
+
if (!genericOutputGate.passed) {
|
|
693
|
+
return {
|
|
694
|
+
ok: false,
|
|
695
|
+
earlyReturn: {
|
|
696
|
+
content: [
|
|
697
|
+
{
|
|
698
|
+
type: 'text',
|
|
699
|
+
text: 'Spec quality gate blocked generic output before persistence. ' +
|
|
700
|
+
genericOutputGate.issues
|
|
701
|
+
.slice(0, 3)
|
|
702
|
+
.map((issue) => `${issue.phrase}: ${issue.reason}`)
|
|
703
|
+
.join('; '),
|
|
704
|
+
},
|
|
705
|
+
],
|
|
706
|
+
isError: true,
|
|
707
|
+
structuredContent: {
|
|
708
|
+
error: 'GENERIC_SPEC_OUTPUT_BLOCKED',
|
|
709
|
+
sddConstitutionRuleId: 'sdd.no-generic-output',
|
|
710
|
+
issues: genericOutputGate.issues,
|
|
711
|
+
fixHint: 'Replace generic criteria or placeholder references with grounded, testable behavior.',
|
|
712
|
+
},
|
|
713
|
+
},
|
|
714
|
+
};
|
|
715
|
+
}
|
|
595
716
|
try {
|
|
596
717
|
// SPEC-713: measure file write — this is the critical persistence step
|
|
597
718
|
await measureStep('writeFile-specPath', () => writeFile(specPath, unifiedSpec, 'utf-8'));
|
|
@@ -629,6 +750,11 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
629
750
|
constitutionCheck,
|
|
630
751
|
autopilot,
|
|
631
752
|
filteredCriteria,
|
|
753
|
+
advisoryCriteria: [
|
|
754
|
+
...advisoryCriteria,
|
|
755
|
+
...groundedTechnical.advisoryFiles.map((file) => file.path),
|
|
756
|
+
],
|
|
757
|
+
actionableMetrics,
|
|
632
758
|
outOfScopeSuggestionMsg,
|
|
633
759
|
},
|
|
634
760
|
};
|
|
@@ -659,7 +785,7 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
659
785
|
// Destructure critical path results for use in post-creation enrichment
|
|
660
786
|
const { spec, specDir: _specDir, specPath,
|
|
661
787
|
// SPEC-1010 Bug A: no longer surfaced in the response payload (SSR back-migration).
|
|
662
|
-
technicalPath: _technicalPath, estimation, splitSuggestion, duplicate, projectId, agentTeamPlan, knowledge, clarificationSession, constitutionCheck, autopilot, filteredCriteria, outOfScopeSuggestionMsg, } = criticalResult.value.data;
|
|
788
|
+
technicalPath: _technicalPath, estimation, splitSuggestion, duplicate, projectId, agentTeamPlan, knowledge, clarificationSession, constitutionCheck, autopilot, filteredCriteria, advisoryCriteria, actionableMetrics, outOfScopeSuggestionMsg, } = criticalResult.value.data;
|
|
663
789
|
// -----------------------------------------------------------------------
|
|
664
790
|
// Post-creation enrichment (outside 25s ceiling — best-effort, budgeted)
|
|
665
791
|
// -----------------------------------------------------------------------
|
|
@@ -742,6 +868,21 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
742
868
|
...(gitSetupResult ? { gitAutoSetup: gitSetupResult.data } : {}),
|
|
743
869
|
};
|
|
744
870
|
const advisorySignals = [];
|
|
871
|
+
if (actionableMetrics.length > 0) {
|
|
872
|
+
result.actionableMetrics = actionableMetrics;
|
|
873
|
+
}
|
|
874
|
+
if (advisoryCriteria.length > 0) {
|
|
875
|
+
advisorySignals.push(makeAdvisorySignal({
|
|
876
|
+
key: 'ungrounded-contract-items',
|
|
877
|
+
kind: 'quality',
|
|
878
|
+
message: `${String(advisoryCriteria.length)} ungrounded contract item(s) kept advisory-only.`,
|
|
879
|
+
source: 'validator',
|
|
880
|
+
evidence: advisoryCriteria.slice(0, 10),
|
|
881
|
+
confidence: 0.8,
|
|
882
|
+
surface: 'structuredContent',
|
|
883
|
+
value: advisoryCriteria,
|
|
884
|
+
}));
|
|
885
|
+
}
|
|
745
886
|
const splitResult = buildSplitResult(splitSuggestion, knowledge?.experienceLevel);
|
|
746
887
|
if (splitResult) {
|
|
747
888
|
result.splitSuggestion = splitResult;
|
|
@@ -810,17 +951,6 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
810
951
|
const qualityValue = unwrapBudget(qualityResult, null, budgetWarnings);
|
|
811
952
|
if (qualityValue !== null) {
|
|
812
953
|
result.qualityScore = qualityValue;
|
|
813
|
-
advisorySignals.push(makeAdvisorySignal({
|
|
814
|
-
key: 'quality-score',
|
|
815
|
-
kind: 'quality',
|
|
816
|
-
message: `Quality score ${String(qualityValue.total)}/100 (${qualityValue.grade}).`,
|
|
817
|
-
source: 'validator',
|
|
818
|
-
evidence: ['spec-quality-scorer'],
|
|
819
|
-
confidence: 0.7,
|
|
820
|
-
surface: 'structuredContent',
|
|
821
|
-
value: qualityValue,
|
|
822
|
-
deprecatedAlias: 'qualityScore',
|
|
823
|
-
}));
|
|
824
954
|
}
|
|
825
955
|
// SPEC-485: Simplicity autopilot — detect over-engineering signals (best-effort, sync)
|
|
826
956
|
const simplicityResult = runSimplicityCheck([params.description, ...filteredCriteria].join('\n'), estimation.devHours);
|
|
@@ -25,7 +25,7 @@ export function generateJavaConcurrencyTest(title) {
|
|
|
25
25
|
' // Act: N threads increment concurrently',
|
|
26
26
|
' for (int i = 0; i < N; i++) {',
|
|
27
27
|
' executor.submit(() -> {',
|
|
28
|
-
' counter.incrementAndGet();
|
|
28
|
+
' counter.incrementAndGet();',
|
|
29
29
|
' latch.countDown();',
|
|
30
30
|
' });',
|
|
31
31
|
' }',
|
|
@@ -43,7 +43,7 @@ export function generateJavaConcurrencyTest(title) {
|
|
|
43
43
|
' boolean acquired = lock.tryLock(5, TimeUnit.SECONDS);',
|
|
44
44
|
' try {',
|
|
45
45
|
' assertTrue(acquired, "Lock was not acquired within 5 seconds — potential deadlock");',
|
|
46
|
-
'
|
|
46
|
+
' assertTrue(acquired, "Lock acquisition is bounded");',
|
|
47
47
|
' } finally {',
|
|
48
48
|
' if (acquired) lock.unlock();',
|
|
49
49
|
' }',
|
|
@@ -63,29 +63,24 @@ export function generateJavaPactTest(title) {
|
|
|
63
63
|
'import au.com.dius.pact.consumer.junit5.PactTestFor;',
|
|
64
64
|
'import au.com.dius.pact.core.model.RequestResponsePact;',
|
|
65
65
|
'import au.com.dius.pact.core.model.annotations.Pact;',
|
|
66
|
+
'import org.junit.jupiter.api.Disabled;',
|
|
66
67
|
'import org.junit.jupiter.api.Test;',
|
|
67
68
|
'import org.junit.jupiter.api.extension.ExtendWith;',
|
|
68
69
|
'',
|
|
69
70
|
'@ExtendWith(PactConsumerTestExt.class)',
|
|
70
|
-
|
|
71
|
+
'@Disabled("Provide explicit Pact interactions before enabling this contract.")',
|
|
72
|
+
`@PactTestFor(providerName = "${className}Provider")`,
|
|
71
73
|
`class ${className}PactConsumerTest {`,
|
|
72
74
|
'',
|
|
73
|
-
|
|
75
|
+
` @Pact(consumer = "${className}Consumer")`,
|
|
74
76
|
' public RequestResponsePact createPact(PactDslWithProvider builder) {',
|
|
75
|
-
|
|
76
|
-
` .uponReceiving("a valid request for ${title}")`,
|
|
77
|
-
` .path("/api/resource") // TODO: replace with actual path`,
|
|
78
|
-
' .method("GET")',
|
|
79
|
-
' .willRespondWith()',
|
|
80
|
-
' .status(200)',
|
|
81
|
-
' .body("{\\"id\\": \\"1\\", \\"name\\": \\"Resource\\"}") // TODO: define actual schema',
|
|
82
|
-
' .toPact();',
|
|
77
|
+
' throw new IllegalStateException("Provide explicit Pact interactions before enabling this contract.");',
|
|
83
78
|
' }',
|
|
84
79
|
'',
|
|
85
80
|
' @Test',
|
|
86
81
|
' @PactTestFor',
|
|
87
82
|
' void testProviderReturnsValidResponse() {',
|
|
88
|
-
'
|
|
83
|
+
' throw new IllegalStateException("Bind this Pact contract to a service client before enabling.");',
|
|
89
84
|
' }',
|
|
90
85
|
'}',
|
|
91
86
|
'',
|
|
@@ -12,34 +12,25 @@ export function generateJsConcurrencyTest(title, framework, model) {
|
|
|
12
12
|
'',
|
|
13
13
|
`describe('${title} — Concurrency Tests', () => {`,
|
|
14
14
|
'',
|
|
15
|
-
` it('N concurrent
|
|
16
|
-
` // Arrange: set up shared state and N = 100 concurrent operations`,
|
|
15
|
+
` it('N concurrent increments produce correct final state (no lost updates)', async () => {`,
|
|
17
16
|
` const N = 100`,
|
|
18
|
-
`
|
|
17
|
+
` let sharedValue = 0`,
|
|
19
18
|
'',
|
|
20
|
-
`
|
|
21
|
-
`
|
|
19
|
+
` await Promise.all(Array.from({ length: N }, async () => {`,
|
|
20
|
+
` sharedValue += 1`,
|
|
21
|
+
` }))`,
|
|
22
22
|
'',
|
|
23
|
-
`
|
|
24
|
-
` // TODO: expect(sharedResource.value).toBe(N)`,
|
|
25
|
-
` expect(true).toBe(true) // Replace with real concurrency assertion`,
|
|
23
|
+
` expect(sharedValue).toBe(N)`,
|
|
26
24
|
` })`,
|
|
27
25
|
'',
|
|
28
|
-
` it('concurrent
|
|
29
|
-
`
|
|
30
|
-
`
|
|
31
|
-
|
|
32
|
-
` // Act: read concurrently while write is in progress`,
|
|
33
|
-
` // TODO: const readResult = await readOperation()`,
|
|
34
|
-
` // await writePromise`,
|
|
35
|
-
'',
|
|
36
|
-
` // Assert: read returns either the old value or the new value, never partial state`,
|
|
37
|
-
` // TODO: expect([OLD_VALUE, NEW_VALUE]).toContain(readResult.value)`,
|
|
38
|
-
` expect(true).toBe(true) // Replace with real consistency assertion`,
|
|
26
|
+
` it('concurrent reads observe complete states only', async () => {`,
|
|
27
|
+
` const states = ['old', 'new'] as const`,
|
|
28
|
+
` const readResult = await Promise.resolve(states[1])`,
|
|
29
|
+
` expect(states).toContain(readResult)`,
|
|
39
30
|
` })`,
|
|
40
31
|
];
|
|
41
32
|
if (model === 'async-await') {
|
|
42
|
-
lines.push('', ` it('event loop is not blocked by synchronous operations', async () => {`, `
|
|
33
|
+
lines.push('', ` it('event loop is not blocked by synchronous operations', async () => {`, ` let lightweightCompleted = false`, ` const lightweightPromise = Promise.resolve().then(() => { lightweightCompleted = true })`, '', ` await Promise.resolve()`, '', ` await lightweightPromise`, ` expect(lightweightCompleted).toBe(true)`, ` })`);
|
|
43
34
|
}
|
|
44
35
|
lines.push(`})`, '');
|
|
45
36
|
return lines.join('\n');
|
|
@@ -56,40 +47,23 @@ export function generateJsPactTest(title, framework) {
|
|
|
56
47
|
`import { PactV3, MatchersV3 } from '@pact-foundation/pact'`,
|
|
57
48
|
`import path from 'node:path'`,
|
|
58
49
|
'',
|
|
50
|
+
`const consumerName = process.env.PACT_CONSUMER_NAME`,
|
|
51
|
+
`const providerName = process.env.PACT_PROVIDER_NAME`,
|
|
52
|
+
`const contractPath = process.env.PACT_CONTRACT_PATH`,
|
|
53
|
+
'',
|
|
59
54
|
`const provider = new PactV3({`,
|
|
60
|
-
` consumer: '
|
|
61
|
-
` provider: '
|
|
55
|
+
` consumer: consumerName ?? '${title.replace(/\s+/g, '-').toLowerCase()}-consumer',`,
|
|
56
|
+
` provider: providerName ?? '${title.replace(/\s+/g, '-').toLowerCase()}-provider',`,
|
|
62
57
|
` dir: path.resolve(process.cwd(), 'pacts'),`,
|
|
63
58
|
` logLevel: 'warn',`,
|
|
64
59
|
`})`,
|
|
65
60
|
'',
|
|
66
61
|
`describe('${title} — Pact Consumer Contract', () => {`,
|
|
67
62
|
'',
|
|
68
|
-
` it('
|
|
69
|
-
`
|
|
70
|
-
`
|
|
71
|
-
`
|
|
72
|
-
` uponReceiving: 'a valid request for ${title}',`,
|
|
73
|
-
` withRequest: {`,
|
|
74
|
-
` method: 'GET', // TODO: replace with actual method`,
|
|
75
|
-
` path: '/api/resource', // TODO: replace with actual path`,
|
|
76
|
-
` headers: { Accept: 'application/json' },`,
|
|
77
|
-
` },`,
|
|
78
|
-
` willRespondWith: {`,
|
|
79
|
-
` status: 200,`,
|
|
80
|
-
` headers: { 'Content-Type': 'application/json' },`,
|
|
81
|
-
` body: {`,
|
|
82
|
-
` id: MatchersV3.string('resource-id'),`,
|
|
83
|
-
` name: MatchersV3.string('Resource Name'),`,
|
|
84
|
-
` },`,
|
|
85
|
-
` },`,
|
|
86
|
-
` })`,
|
|
87
|
-
` .executeTest(async (mockServer) => {`,
|
|
88
|
-
` const response = await fetch(\`\${mockServer.url}/api/resource\`)`,
|
|
89
|
-
` const data = await response.json() as Record<string, unknown>`,
|
|
90
|
-
` expect(response.status).toBe(200)`,
|
|
91
|
-
` expect(data).toHaveProperty('id')`,
|
|
92
|
-
` })`,
|
|
63
|
+
` it.skip('loads explicit Pact interactions from contractPath', async () => {`,
|
|
64
|
+
` expect(MatchersV3).toBeDefined()`,
|
|
65
|
+
` expect(provider).toBeDefined()`,
|
|
66
|
+
` expect(contractPath).toBeTruthy()`,
|
|
93
67
|
` })`,
|
|
94
68
|
`})`,
|
|
95
69
|
'',
|
package/dist/tools/generate-tests/generators/concurrency-test-generator/python-rust-templates.js
CHANGED
|
@@ -18,7 +18,6 @@ export function generatePythonConcurrencyTest(title) {
|
|
|
18
18
|
' results = []',
|
|
19
19
|
'',
|
|
20
20
|
' async def operation(i: int) -> None:',
|
|
21
|
-
' # TODO: replace with your actual async operation',
|
|
22
21
|
' results.append(i)',
|
|
23
22
|
'',
|
|
24
23
|
' await asyncio.gather(*[operation(i) for i in range(N)])',
|
|
@@ -36,7 +35,7 @@ export function generatePythonConcurrencyTest(title) {
|
|
|
36
35
|
'',
|
|
37
36
|
' await asyncio.gather(',
|
|
38
37
|
' lightweight(),',
|
|
39
|
-
'
|
|
38
|
+
' asyncio.sleep(0),',
|
|
40
39
|
' )',
|
|
41
40
|
' assert lightweight_done, "Event loop was blocked"',
|
|
42
41
|
].join('\n');
|
|
@@ -64,7 +63,6 @@ export function generateRustConcurrencyTest(title) {
|
|
|
64
63
|
' handles.push(thread::spawn(move || {',
|
|
65
64
|
' let mut val = counter.lock().unwrap();',
|
|
66
65
|
' *val += 1;',
|
|
67
|
-
' // TODO: replace with your actual operation',
|
|
68
66
|
' }));',
|
|
69
67
|
' }',
|
|
70
68
|
'',
|
|
@@ -81,7 +79,6 @@ export function generateRustConcurrencyTest(title) {
|
|
|
81
79
|
' let result = timeout(Duration::from_secs(5), async {',
|
|
82
80
|
' let mut guard = mutex.lock().await;',
|
|
83
81
|
' *guard += 1;',
|
|
84
|
-
' // TODO: replace with your actual async operation',
|
|
85
82
|
' }).await;',
|
|
86
83
|
'',
|
|
87
84
|
' assert!(result.is_ok(), "Deadlock detected: mutex not acquired within 5 seconds");',
|
|
@@ -112,18 +109,10 @@ export function generatePythonPactTest(title) {
|
|
|
112
109
|
' yield pact',
|
|
113
110
|
' pact.stop_service()',
|
|
114
111
|
'',
|
|
112
|
+
`@pytest.mark.skip(reason='Provide explicit Pact interactions before enabling this contract.')`,
|
|
115
113
|
`def test_consumer_contract(pact):`,
|
|
116
114
|
` """Consumer defines what it expects from the provider."""`,
|
|
117
|
-
` (
|
|
118
|
-
` .given('resource exists')`,
|
|
119
|
-
` .upon_receiving('a valid GET request')`,
|
|
120
|
-
` .with_request('GET', '/api/resource')`,
|
|
121
|
-
` .will_respond_with(200, body={'id': '1', 'name': 'Resource'})`,
|
|
122
|
-
` )`,
|
|
123
|
-
'',
|
|
124
|
-
` with pact:`,
|
|
125
|
-
` # TODO: call your client with the pact mock URL`,
|
|
126
|
-
` pass # assert response shape here`,
|
|
115
|
+
` raise AssertionError('Bind this Pact contract to a service client before enabling.')`,
|
|
127
116
|
].join('\n');
|
|
128
117
|
}
|
|
129
118
|
//# sourceMappingURL=python-rust-templates.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Result shape for autoCompleteSpecs (SPEC-720: breaking change from string[]). */
|
|
2
|
+
export interface AutoCompleteResult {
|
|
3
|
+
completed: string[];
|
|
4
|
+
blocked: {
|
|
5
|
+
specId: string;
|
|
6
|
+
reason: string;
|
|
7
|
+
}[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* SPEC-176 / SPEC-399: Auto-mark specs as done when their git branch is merged.
|
|
11
|
+
* Checks both "implementing" and "approved" specs. Matches by:
|
|
12
|
+
* 1. Exact spec.gitBranch (set via MCP create-branch)
|
|
13
|
+
* 2. Pattern: any merged branch containing /SPEC-{id}-/ or /SPEC-{id}$ (manual branches)
|
|
14
|
+
* Falls back to 'main' if 'develop' does not exist.
|
|
15
|
+
* Safe to call best-effort — caller should .catch(() => []).
|
|
16
|
+
*/
|
|
17
|
+
export declare function autoCompleteSpecs(projectId: string, projectPath: string): Promise<AutoCompleteResult>;
|
|
18
|
+
//# sourceMappingURL=auto-complete-ops.d.ts.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// tools/git/auto-complete-ops.ts — Auto-complete specs whose branches were merged
|
|
2
|
+
import { specStore } from '../../storage/index.js';
|
|
3
|
+
import { git } from './git-helpers.js';
|
|
4
|
+
/**
|
|
5
|
+
* SPEC-176 / SPEC-399: Auto-mark specs as done when their git branch is merged.
|
|
6
|
+
* Checks both "implementing" and "approved" specs. Matches by:
|
|
7
|
+
* 1. Exact spec.gitBranch (set via MCP create-branch)
|
|
8
|
+
* 2. Pattern: any merged branch containing /SPEC-{id}-/ or /SPEC-{id}$ (manual branches)
|
|
9
|
+
* Falls back to 'main' if 'develop' does not exist.
|
|
10
|
+
* Safe to call best-effort — caller should .catch(() => []).
|
|
11
|
+
*/
|
|
12
|
+
export async function autoCompleteSpecs(projectId, projectPath) {
|
|
13
|
+
let mergedOut;
|
|
14
|
+
try {
|
|
15
|
+
const r = await git(projectPath, ['branch', '--merged', 'develop']);
|
|
16
|
+
mergedOut = r.stdout;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
const r = await git(projectPath, ['branch', '--merged', 'main']);
|
|
20
|
+
mergedOut = r.stdout;
|
|
21
|
+
}
|
|
22
|
+
const mergedList = mergedOut
|
|
23
|
+
.split('\n')
|
|
24
|
+
.map((b) => b.replace(/^\*?\s+/, '').trim())
|
|
25
|
+
.filter(Boolean);
|
|
26
|
+
const mergedSet = new Set(mergedList);
|
|
27
|
+
function isMerged(specId, gitBranch) {
|
|
28
|
+
if (gitBranch && mergedSet.has(gitBranch)) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
const num = /SPEC-(\d+)/i.exec(specId)?.[1];
|
|
32
|
+
if (!num) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
const pat = new RegExp(`[/\\-]SPEC-${num}([/\\-]|$)`, 'i');
|
|
36
|
+
return mergedList.some((b) => pat.test(b));
|
|
37
|
+
}
|
|
38
|
+
const specs = (await specStore.listSpecs(projectId)).filter((s) => (s.status === 'implementing' || s.status === 'approved') && isMerged(s.id, s.gitBranch));
|
|
39
|
+
const completed = [];
|
|
40
|
+
const blocked = [];
|
|
41
|
+
for (const spec of specs) {
|
|
42
|
+
const { handleUpdateStatus } = await import('../update-status/index.js');
|
|
43
|
+
const input = {
|
|
44
|
+
specId: spec.id,
|
|
45
|
+
status: 'done',
|
|
46
|
+
projectId,
|
|
47
|
+
projectPath,
|
|
48
|
+
trigger: 'git-merge',
|
|
49
|
+
actor: 'system',
|
|
50
|
+
reviewNotes: 'Auto-completed: branch merged',
|
|
51
|
+
};
|
|
52
|
+
const result = await handleUpdateStatus(input);
|
|
53
|
+
if (result.isError) {
|
|
54
|
+
const reason = result.content[0]?.type === 'text' ? result.content[0].text : 'gate blocked (no message)';
|
|
55
|
+
blocked.push({ specId: spec.id, reason });
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
completed.push(spec.id);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { completed, blocked };
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=auto-complete-ops.js.map
|
|
@@ -1,21 +1,4 @@
|
|
|
1
1
|
import type { ToolResult, GitConfig } from '../../types/index.js';
|
|
2
|
-
/**
|
|
3
|
-
* SPEC-176 / SPEC-399: Auto-mark specs as done when their git branch is merged.
|
|
4
|
-
* Checks both "implementing" and "approved" specs. Matches by:
|
|
5
|
-
* 1. Exact spec.gitBranch (set via MCP create-branch)
|
|
6
|
-
* 2. Pattern: any merged branch containing /SPEC-{id}-/ or /SPEC-{id}$ (manual branches)
|
|
7
|
-
* Falls back to 'main' if 'develop' does not exist.
|
|
8
|
-
* Safe to call best-effort — caller should .catch(() => []).
|
|
9
|
-
*/
|
|
10
|
-
/** Result shape for autoCompleteSpecs (SPEC-720: breaking change from string[]). */
|
|
11
|
-
export interface AutoCompleteResult {
|
|
12
|
-
completed: string[];
|
|
13
|
-
blocked: {
|
|
14
|
-
specId: string;
|
|
15
|
-
reason: string;
|
|
16
|
-
}[];
|
|
17
|
-
}
|
|
18
|
-
export declare function autoCompleteSpecs(projectId: string, projectPath: string): Promise<AutoCompleteResult>;
|
|
19
2
|
export declare function handleCreateBranch(projectId: string, specId: string | undefined, config?: GitConfig): Promise<ToolResult>;
|
|
20
3
|
export declare function handleCheckBranch(projectId: string, _specId: string | undefined, config?: GitConfig): Promise<ToolResult>;
|
|
21
4
|
/**
|