@planu/cli 0.96.5 → 0.97.1
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/dist/cli/commands/install.d.ts.map +1 -1
- package/dist/cli/commands/install.js +3 -1
- package/dist/cli/commands/install.js.map +1 -1
- package/dist/config/compliance-profiles.json +62 -0
- package/dist/config/license-plans.json +18 -2
- package/dist/config/spec-templates/crud-rest-api/spec.md +35 -0
- package/dist/config/spec-templates/crud-rest-api/template.json +10 -0
- package/dist/config/spec-templates/email-notifications/spec.md +31 -0
- package/dist/config/spec-templates/email-notifications/template.json +10 -0
- package/dist/config/spec-templates/file-upload-s3/spec.md +31 -0
- package/dist/config/spec-templates/file-upload-s3/template.json +11 -0
- package/dist/config/spec-templates/jwt-auth/spec.md +35 -0
- package/dist/config/spec-templates/jwt-auth/template.json +10 -0
- package/dist/config/spec-templates/oauth-social-login/spec.md +31 -0
- package/dist/config/spec-templates/oauth-social-login/template.json +10 -0
- package/dist/config/spec-templates/rate-limiting/spec.md +31 -0
- package/dist/config/spec-templates/rate-limiting/template.json +10 -0
- package/dist/config/spec-templates/stripe-payments/spec.md +32 -0
- package/dist/config/spec-templates/stripe-payments/template.json +10 -0
- package/dist/config/spec-templates/webhook-system/spec.md +36 -0
- package/dist/config/spec-templates/webhook-system/template.json +10 -0
- package/dist/engine/agent-ready-exporter/formatters/devin.d.ts +7 -0
- package/dist/engine/agent-ready-exporter/formatters/devin.d.ts.map +1 -0
- package/dist/engine/agent-ready-exporter/formatters/devin.js +23 -0
- package/dist/engine/agent-ready-exporter/formatters/devin.js.map +1 -0
- package/dist/engine/agent-ready-exporter/formatters/generic.d.ts +7 -0
- package/dist/engine/agent-ready-exporter/formatters/generic.d.ts.map +1 -0
- package/dist/engine/agent-ready-exporter/formatters/generic.js +9 -0
- package/dist/engine/agent-ready-exporter/formatters/generic.js.map +1 -0
- package/dist/engine/agent-ready-exporter/formatters/kiro.d.ts +6 -0
- package/dist/engine/agent-ready-exporter/formatters/kiro.d.ts.map +1 -0
- package/dist/engine/agent-ready-exporter/formatters/kiro.js +34 -0
- package/dist/engine/agent-ready-exporter/formatters/kiro.js.map +1 -0
- package/dist/engine/agent-ready-exporter/formatters/swe-agent.d.ts +6 -0
- package/dist/engine/agent-ready-exporter/formatters/swe-agent.d.ts.map +1 -0
- package/dist/engine/agent-ready-exporter/formatters/swe-agent.js +40 -0
- package/dist/engine/agent-ready-exporter/formatters/swe-agent.js.map +1 -0
- package/dist/engine/agent-ready-exporter.d.ts +11 -0
- package/dist/engine/agent-ready-exporter.d.ts.map +1 -0
- package/dist/engine/agent-ready-exporter.js +86 -0
- package/dist/engine/agent-ready-exporter.js.map +1 -0
- package/dist/engine/compliance-checker.d.ts +19 -0
- package/dist/engine/compliance-checker.d.ts.map +1 -0
- package/dist/engine/compliance-checker.js +145 -0
- package/dist/engine/compliance-checker.js.map +1 -0
- package/dist/engine/generate-tests/generators/property-based-generator.d.ts +12 -0
- package/dist/engine/generate-tests/generators/property-based-generator.d.ts.map +1 -0
- package/dist/engine/generate-tests/generators/property-based-generator.js +159 -0
- package/dist/engine/generate-tests/generators/property-based-generator.js.map +1 -0
- package/dist/engine/jira-exporter.d.ts +39 -0
- package/dist/engine/jira-exporter.d.ts.map +1 -0
- package/dist/engine/jira-exporter.js +190 -0
- package/dist/engine/jira-exporter.js.map +1 -0
- package/dist/engine/linear-exporter.d.ts +32 -0
- package/dist/engine/linear-exporter.d.ts.map +1 -0
- package/dist/engine/linear-exporter.js +184 -0
- package/dist/engine/linear-exporter.js.map +1 -0
- package/dist/engine/property-test-generator.d.ts +14 -0
- package/dist/engine/property-test-generator.d.ts.map +1 -0
- package/dist/engine/property-test-generator.js +223 -0
- package/dist/engine/property-test-generator.js.map +1 -0
- package/dist/engine/skill-evaluator.d.ts +21 -0
- package/dist/engine/skill-evaluator.d.ts.map +1 -0
- package/dist/engine/skill-evaluator.js +126 -0
- package/dist/engine/skill-evaluator.js.map +1 -0
- package/dist/engine/spec-quality-scorer.d.ts +4 -0
- package/dist/engine/spec-quality-scorer.d.ts.map +1 -0
- package/dist/engine/spec-quality-scorer.js +334 -0
- package/dist/engine/spec-quality-scorer.js.map +1 -0
- package/dist/engine/spec-template-engine.d.ts +26 -0
- package/dist/engine/spec-template-engine.d.ts.map +1 -0
- package/dist/engine/spec-template-engine.js +127 -0
- package/dist/engine/spec-template-engine.js.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/storage/compliance-store.d.ts +11 -0
- package/dist/storage/compliance-store.d.ts.map +1 -0
- package/dist/storage/compliance-store.js +30 -0
- package/dist/storage/compliance-store.js.map +1 -0
- package/dist/storage/jira-store.d.ts +30 -0
- package/dist/storage/jira-store.d.ts.map +1 -0
- package/dist/storage/jira-store.js +84 -0
- package/dist/storage/jira-store.js.map +1 -0
- package/dist/storage/linear-store.d.ts +30 -0
- package/dist/storage/linear-store.d.ts.map +1 -0
- package/dist/storage/linear-store.js +84 -0
- package/dist/storage/linear-store.js.map +1 -0
- package/dist/tools/check-readiness.d.ts.map +1 -1
- package/dist/tools/check-readiness.js +8 -2
- package/dist/tools/check-readiness.js.map +1 -1
- package/dist/tools/compliance-handler.d.ts +6 -0
- package/dist/tools/compliance-handler.d.ts.map +1 -0
- package/dist/tools/compliance-handler.js +99 -0
- package/dist/tools/compliance-handler.js.map +1 -0
- package/dist/tools/eval-skill-handler.d.ts +4 -0
- package/dist/tools/eval-skill-handler.d.ts.map +1 -0
- package/dist/tools/eval-skill-handler.js +26 -0
- package/dist/tools/eval-skill-handler.js.map +1 -0
- package/dist/tools/export-spec.d.ts.map +1 -1
- package/dist/tools/export-spec.js +42 -1
- package/dist/tools/export-spec.js.map +1 -1
- package/dist/tools/generate-tests/generators/property-based-generator.d.ts +11 -0
- package/dist/tools/generate-tests/generators/property-based-generator.d.ts.map +1 -0
- package/dist/tools/generate-tests/generators/property-based-generator.js +27 -0
- package/dist/tools/generate-tests/generators/property-based-generator.js.map +1 -0
- package/dist/tools/generate-tests/spec-dispatcher.d.ts +5 -0
- package/dist/tools/generate-tests/spec-dispatcher.d.ts.map +1 -1
- package/dist/tools/generate-tests/spec-dispatcher.js +18 -1
- package/dist/tools/generate-tests/spec-dispatcher.js.map +1 -1
- package/dist/tools/jira-sync-handler.d.ts +6 -0
- package/dist/tools/jira-sync-handler.d.ts.map +1 -0
- package/dist/tools/jira-sync-handler.js +220 -0
- package/dist/tools/jira-sync-handler.js.map +1 -0
- package/dist/tools/linear-sync-handler.d.ts +6 -0
- package/dist/tools/linear-sync-handler.d.ts.map +1 -0
- package/dist/tools/linear-sync-handler.js +212 -0
- package/dist/tools/linear-sync-handler.js.map +1 -0
- package/dist/tools/register-export-tools.d.ts.map +1 -1
- package/dist/tools/register-export-tools.js +10 -3
- package/dist/tools/register-export-tools.js.map +1 -1
- package/dist/tools/register-spec-314.d.ts +3 -0
- package/dist/tools/register-spec-314.d.ts.map +1 -0
- package/dist/tools/register-spec-314.js +38 -0
- package/dist/tools/register-spec-314.js.map +1 -0
- package/dist/tools/register-spec-316.d.ts +3 -0
- package/dist/tools/register-spec-316.d.ts.map +1 -0
- package/dist/tools/register-spec-316.js +71 -0
- package/dist/tools/register-spec-316.js.map +1 -0
- package/dist/tools/register-spec-317.d.ts +3 -0
- package/dist/tools/register-spec-317.d.ts.map +1 -0
- package/dist/tools/register-spec-317.js +158 -0
- package/dist/tools/register-spec-317.js.map +1 -0
- package/dist/tools/register-spec-319.d.ts +3 -0
- package/dist/tools/register-spec-319.d.ts.map +1 -0
- package/dist/tools/register-spec-319.js +59 -0
- package/dist/tools/register-spec-319.js.map +1 -0
- package/dist/tools/register-spec-320.d.ts +3 -0
- package/dist/tools/register-spec-320.d.ts.map +1 -0
- package/dist/tools/register-spec-320.js +60 -0
- package/dist/tools/register-spec-320.js.map +1 -0
- package/dist/tools/spec-marketplace-handler.d.ts +6 -0
- package/dist/tools/spec-marketplace-handler.d.ts.map +1 -0
- package/dist/tools/spec-marketplace-handler.js +113 -0
- package/dist/tools/spec-marketplace-handler.js.map +1 -0
- package/dist/tools/spec-quality-score-handler.d.ts +7 -0
- package/dist/tools/spec-quality-score-handler.d.ts.map +1 -0
- package/dist/tools/spec-quality-score-handler.js +106 -0
- package/dist/tools/spec-quality-score-handler.js.map +1 -0
- package/dist/tools/start-hooks/configure.d.ts +4 -0
- package/dist/tools/start-hooks/configure.d.ts.map +1 -0
- package/dist/tools/start-hooks/configure.js +122 -0
- package/dist/tools/start-hooks/configure.js.map +1 -0
- package/dist/tools/start-hooks/engine.d.ts +13 -0
- package/dist/tools/start-hooks/engine.d.ts.map +1 -0
- package/dist/tools/start-hooks/engine.js +314 -0
- package/dist/tools/start-hooks/engine.js.map +1 -0
- package/dist/tools/start-hooks.d.ts +2 -23
- package/dist/tools/start-hooks.d.ts.map +1 -1
- package/dist/tools/start-hooks.js +4 -516
- package/dist/tools/start-hooks.js.map +1 -1
- package/dist/types/agent-ready.d.ts +58 -0
- package/dist/types/agent-ready.d.ts.map +1 -0
- package/dist/types/agent-ready.js +3 -0
- package/dist/types/agent-ready.js.map +1 -0
- package/dist/types/compliance.d.ts +44 -0
- package/dist/types/compliance.d.ts.map +1 -0
- package/dist/types/compliance.js +3 -0
- package/dist/types/compliance.js.map +1 -0
- package/dist/types/export.d.ts +5 -1
- package/dist/types/export.d.ts.map +1 -1
- package/dist/types/export.js +1 -1
- package/dist/types/export.js.map +1 -1
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +7 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/jira-linear.d.ts +216 -0
- package/dist/types/jira-linear.d.ts.map +1 -0
- package/dist/types/jira-linear.js +3 -0
- package/dist/types/jira-linear.js.map +1 -0
- package/dist/types/property-testing.d.ts +50 -0
- package/dist/types/property-testing.d.ts.map +1 -0
- package/dist/types/property-testing.js +3 -0
- package/dist/types/property-testing.js.map +1 -0
- package/dist/types/skill-eval.d.ts +41 -0
- package/dist/types/skill-eval.d.ts.map +1 -0
- package/dist/types/skill-eval.js +3 -0
- package/dist/types/skill-eval.js.map +1 -0
- package/dist/types/spec-marketplace.d.ts +31 -0
- package/dist/types/spec-marketplace.d.ts.map +1 -0
- package/dist/types/spec-marketplace.js +3 -0
- package/dist/types/spec-marketplace.js.map +1 -0
- package/dist/types/spec-quality.d.ts +36 -0
- package/dist/types/spec-quality.d.ts.map +1 -0
- package/dist/types/spec-quality.js +3 -0
- package/dist/types/spec-quality.js.map +1 -0
- package/package.json +7 -2
- package/src/config/compliance-profiles.json +62 -0
- package/src/config/license-plans.json +18 -2
- package/src/config/spec-templates/crud-rest-api/spec.md +35 -0
- package/src/config/spec-templates/crud-rest-api/template.json +10 -0
- package/src/config/spec-templates/email-notifications/spec.md +31 -0
- package/src/config/spec-templates/email-notifications/template.json +10 -0
- package/src/config/spec-templates/file-upload-s3/spec.md +31 -0
- package/src/config/spec-templates/file-upload-s3/template.json +11 -0
- package/src/config/spec-templates/jwt-auth/spec.md +35 -0
- package/src/config/spec-templates/jwt-auth/template.json +10 -0
- package/src/config/spec-templates/oauth-social-login/spec.md +31 -0
- package/src/config/spec-templates/oauth-social-login/template.json +10 -0
- package/src/config/spec-templates/rate-limiting/spec.md +31 -0
- package/src/config/spec-templates/rate-limiting/template.json +10 -0
- package/src/config/spec-templates/stripe-payments/spec.md +32 -0
- package/src/config/spec-templates/stripe-payments/template.json +10 -0
- package/src/config/spec-templates/webhook-system/spec.md +36 -0
- package/src/config/spec-templates/webhook-system/template.json +10 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Status mapping: Planu spec status → Jira issue status name
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
const SPEC_STATUS_TO_JIRA = {
|
|
5
|
+
draft: 'To Do',
|
|
6
|
+
review: 'In Review',
|
|
7
|
+
approved: 'Approved',
|
|
8
|
+
implementing: 'In Progress',
|
|
9
|
+
done: 'Done',
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Map a Planu spec status to a Jira status name.
|
|
13
|
+
*/
|
|
14
|
+
export function mapSpecStatusToJira(specStatus) {
|
|
15
|
+
return SPEC_STATUS_TO_JIRA[specStatus] ?? 'To Do';
|
|
16
|
+
}
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Jira REST API client
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
export class JiraClient {
|
|
21
|
+
config;
|
|
22
|
+
authHeader;
|
|
23
|
+
baseUrl;
|
|
24
|
+
constructor(config) {
|
|
25
|
+
this.config = config;
|
|
26
|
+
const credentials = `${config.email}:${config.apiToken}`;
|
|
27
|
+
this.authHeader = `Basic ${Buffer.from(credentials).toString('base64')}`;
|
|
28
|
+
this.baseUrl = `https://${config.domain}.atlassian.net/rest/api/3`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Create an Epic in Jira from a spec.
|
|
32
|
+
* Returns a JiraIssueRef with the new issue details.
|
|
33
|
+
*/
|
|
34
|
+
async createEpic(spec) {
|
|
35
|
+
const summary = `${spec.id}: ${spec.title}`;
|
|
36
|
+
const description = buildAtlassianDoc(spec.title, `Planu spec ${spec.id} — status: ${spec.status}`);
|
|
37
|
+
const payload = {
|
|
38
|
+
fields: {
|
|
39
|
+
project: { key: this.config.projectKey },
|
|
40
|
+
issuetype: { name: 'Epic' },
|
|
41
|
+
summary,
|
|
42
|
+
description,
|
|
43
|
+
labels: ['planu-spec'],
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
const data = await this.postJson('/issue', payload);
|
|
47
|
+
const issueUrl = `https://${this.config.domain}.atlassian.net/browse/${data.key}`;
|
|
48
|
+
return {
|
|
49
|
+
specId: spec.id,
|
|
50
|
+
issueKey: data.key,
|
|
51
|
+
issueUrl,
|
|
52
|
+
summary,
|
|
53
|
+
issueType: 'Epic',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Create a Story in Jira linked to a parent Epic.
|
|
58
|
+
* Returns a JiraIssueRef for the created story.
|
|
59
|
+
*/
|
|
60
|
+
async createStory(specId, criterionText, epicKey) {
|
|
61
|
+
const summary = criterionText.slice(0, 255);
|
|
62
|
+
const description = buildAtlassianDoc('Acceptance Criterion', criterionText);
|
|
63
|
+
const payload = {
|
|
64
|
+
fields: {
|
|
65
|
+
project: { key: this.config.projectKey },
|
|
66
|
+
issuetype: { name: 'Story' },
|
|
67
|
+
summary,
|
|
68
|
+
description,
|
|
69
|
+
labels: ['planu-spec'],
|
|
70
|
+
'Epic Link': epicKey,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
const data = await this.postJson('/issue', payload);
|
|
74
|
+
const issueUrl = `https://${this.config.domain}.atlassian.net/browse/${data.key}`;
|
|
75
|
+
return {
|
|
76
|
+
specId,
|
|
77
|
+
issueKey: data.key,
|
|
78
|
+
issueUrl,
|
|
79
|
+
summary,
|
|
80
|
+
issueType: 'Story',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Fetch a Jira issue by key.
|
|
85
|
+
* Returns the issue or null if not found.
|
|
86
|
+
*/
|
|
87
|
+
async getIssue(key) {
|
|
88
|
+
const url = `${this.baseUrl}/issue/${encodeURIComponent(key)}`;
|
|
89
|
+
const response = await globalThis.fetch(url, {
|
|
90
|
+
headers: {
|
|
91
|
+
Authorization: this.authHeader,
|
|
92
|
+
'Content-Type': 'application/json',
|
|
93
|
+
},
|
|
94
|
+
signal: AbortSignal.timeout(10_000),
|
|
95
|
+
});
|
|
96
|
+
if (response.status === 404) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
throw new Error(`Jira getIssue failed: ${response.status} ${response.statusText}`);
|
|
101
|
+
}
|
|
102
|
+
return (await response.json());
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Transition a Jira issue to a target status name.
|
|
106
|
+
* Looks up available transitions and applies the matching one.
|
|
107
|
+
*/
|
|
108
|
+
async updateIssueStatus(issueKey, targetStatus) {
|
|
109
|
+
const transitions = await this.getTransitions(issueKey);
|
|
110
|
+
const match = transitions.find((t) => t.name.toLowerCase() === targetStatus.toLowerCase());
|
|
111
|
+
if (match === undefined) {
|
|
112
|
+
// Status not available — skip silently (workflow may differ per project)
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const url = `${this.baseUrl}/issue/${encodeURIComponent(issueKey)}/transitions`;
|
|
116
|
+
const response = await globalThis.fetch(url, {
|
|
117
|
+
method: 'POST',
|
|
118
|
+
headers: {
|
|
119
|
+
Authorization: this.authHeader,
|
|
120
|
+
'Content-Type': 'application/json',
|
|
121
|
+
},
|
|
122
|
+
body: JSON.stringify({ transition: { id: match.id } }),
|
|
123
|
+
signal: AbortSignal.timeout(10_000),
|
|
124
|
+
});
|
|
125
|
+
if (!response.ok) {
|
|
126
|
+
const errText = await response.text();
|
|
127
|
+
throw new Error(`Jira updateIssueStatus failed: ${response.status} — ${errText}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Private helpers
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
async getTransitions(issueKey) {
|
|
134
|
+
const url = `${this.baseUrl}/issue/${encodeURIComponent(issueKey)}/transitions`;
|
|
135
|
+
const response = await globalThis.fetch(url, {
|
|
136
|
+
headers: {
|
|
137
|
+
Authorization: this.authHeader,
|
|
138
|
+
'Content-Type': 'application/json',
|
|
139
|
+
},
|
|
140
|
+
signal: AbortSignal.timeout(10_000),
|
|
141
|
+
});
|
|
142
|
+
if (!response.ok) {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
const data = (await response.json());
|
|
146
|
+
return data.transitions;
|
|
147
|
+
}
|
|
148
|
+
async postJson(path, body) {
|
|
149
|
+
const url = `${this.baseUrl}${path}`;
|
|
150
|
+
const response = await globalThis.fetch(url, {
|
|
151
|
+
method: 'POST',
|
|
152
|
+
headers: {
|
|
153
|
+
Authorization: this.authHeader,
|
|
154
|
+
'Content-Type': 'application/json',
|
|
155
|
+
},
|
|
156
|
+
body: JSON.stringify(body),
|
|
157
|
+
signal: AbortSignal.timeout(15_000),
|
|
158
|
+
});
|
|
159
|
+
if (!response.ok) {
|
|
160
|
+
const errText = await response.text();
|
|
161
|
+
throw new Error(`Jira POST ${path} failed: ${response.status} — ${errText}`);
|
|
162
|
+
}
|
|
163
|
+
return (await response.json());
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// Atlassian Document Format (ADF) builder
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
/**
|
|
170
|
+
* Build a minimal Atlassian Document Format (ADF) document.
|
|
171
|
+
* Used for Jira issue descriptions in the REST API v3.
|
|
172
|
+
*/
|
|
173
|
+
export function buildAtlassianDoc(heading, bodyText) {
|
|
174
|
+
return {
|
|
175
|
+
version: 1,
|
|
176
|
+
type: 'doc',
|
|
177
|
+
content: [
|
|
178
|
+
{
|
|
179
|
+
type: 'heading',
|
|
180
|
+
attrs: { level: 2 },
|
|
181
|
+
content: [{ type: 'text', text: heading }],
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
type: 'paragraph',
|
|
185
|
+
content: [{ type: 'text', text: bodyText }],
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
//# sourceMappingURL=jira-exporter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jira-exporter.js","sourceRoot":"","sources":["../../src/engine/jira-exporter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { LinearConfig, LinearIssueRef, Spec } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Map a Planu spec status to a Linear workflow state name.
|
|
4
|
+
*/
|
|
5
|
+
export declare function mapSpecStatusToLinear(specStatus: string): string;
|
|
6
|
+
export declare class LinearClient {
|
|
7
|
+
private readonly config;
|
|
8
|
+
constructor(config: LinearConfig);
|
|
9
|
+
/**
|
|
10
|
+
* Create a Linear project (as an issue group / label) from a spec.
|
|
11
|
+
* Since Linear uses issues as primary entities, each spec becomes an issue.
|
|
12
|
+
* Returns a LinearIssueRef with the created issue details.
|
|
13
|
+
*/
|
|
14
|
+
createProject(spec: Spec): Promise<LinearIssueRef>;
|
|
15
|
+
/**
|
|
16
|
+
* Create a sub-issue in Linear for a single acceptance criterion.
|
|
17
|
+
* Returns a LinearIssueRef for the created issue.
|
|
18
|
+
*/
|
|
19
|
+
createIssue(specId: string, criterionText: string, parentId?: string): Promise<LinearIssueRef>;
|
|
20
|
+
/**
|
|
21
|
+
* Update the workflow state (status) of a Linear issue.
|
|
22
|
+
* Looks up the state ID by name within the team, then applies it.
|
|
23
|
+
*/
|
|
24
|
+
updateIssueStatus(issueId: string, stateName: string): Promise<void>;
|
|
25
|
+
private findStateId;
|
|
26
|
+
private graphql;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Build a Markdown description for a Linear issue from a spec.
|
|
30
|
+
*/
|
|
31
|
+
export declare function buildLinearDescription(spec: Spec): string;
|
|
32
|
+
//# sourceMappingURL=linear-exporter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"linear-exporter.d.ts","sourceRoot":"","sources":["../../src/engine/linear-exporter.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,YAAY,EACZ,cAAc,EAId,IAAI,EACL,MAAM,mBAAmB,CAAC;AAgB3B;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEhE;AAMD,qBAAa,YAAY;IACX,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,YAAY;IAEjD;;;;OAIG;IACG,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,cAAc,CAAC;IAqCxD;;;OAGG;IACG,WAAW,CACf,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,EACrB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,cAAc,CAAC;IAsC1B;;;OAGG;IACG,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YA6B5D,WAAW;YAuBX,OAAO;CA6BtB;AAMD;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CASzD"}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
const LINEAR_GRAPHQL_URL = 'https://api.linear.app/graphql';
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// Status mapping: Planu spec status → Linear workflow state name
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
const SPEC_STATUS_TO_LINEAR = {
|
|
6
|
+
draft: 'Todo',
|
|
7
|
+
review: 'In Review',
|
|
8
|
+
approved: 'Todo',
|
|
9
|
+
implementing: 'In Progress',
|
|
10
|
+
done: 'Done',
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Map a Planu spec status to a Linear workflow state name.
|
|
14
|
+
*/
|
|
15
|
+
export function mapSpecStatusToLinear(specStatus) {
|
|
16
|
+
return SPEC_STATUS_TO_LINEAR[specStatus] ?? 'Todo';
|
|
17
|
+
}
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Linear GraphQL client
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
export class LinearClient {
|
|
22
|
+
config;
|
|
23
|
+
constructor(config) {
|
|
24
|
+
this.config = config;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Create a Linear project (as an issue group / label) from a spec.
|
|
28
|
+
* Since Linear uses issues as primary entities, each spec becomes an issue.
|
|
29
|
+
* Returns a LinearIssueRef with the created issue details.
|
|
30
|
+
*/
|
|
31
|
+
async createProject(spec) {
|
|
32
|
+
const title = `${spec.id}: ${spec.title}`;
|
|
33
|
+
const description = buildLinearDescription(spec);
|
|
34
|
+
const mutation = `
|
|
35
|
+
mutation CreateIssue($input: IssueCreateInput!) {
|
|
36
|
+
issueCreate(input: $input) {
|
|
37
|
+
success
|
|
38
|
+
issue {
|
|
39
|
+
id
|
|
40
|
+
url
|
|
41
|
+
title
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
`;
|
|
46
|
+
const variables = {
|
|
47
|
+
input: {
|
|
48
|
+
teamId: this.config.teamId,
|
|
49
|
+
title,
|
|
50
|
+
description,
|
|
51
|
+
labelNames: ['planu-spec'],
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
const data = await this.graphql(mutation, variables);
|
|
55
|
+
const issue = data.issueCreate.issue;
|
|
56
|
+
return {
|
|
57
|
+
specId: spec.id,
|
|
58
|
+
issueId: issue.id,
|
|
59
|
+
issueUrl: issue.url,
|
|
60
|
+
title: issue.title,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Create a sub-issue in Linear for a single acceptance criterion.
|
|
65
|
+
* Returns a LinearIssueRef for the created issue.
|
|
66
|
+
*/
|
|
67
|
+
async createIssue(specId, criterionText, parentId) {
|
|
68
|
+
const title = criterionText.slice(0, 255);
|
|
69
|
+
const mutation = `
|
|
70
|
+
mutation CreateIssue($input: IssueCreateInput!) {
|
|
71
|
+
issueCreate(input: $input) {
|
|
72
|
+
success
|
|
73
|
+
issue {
|
|
74
|
+
id
|
|
75
|
+
url
|
|
76
|
+
title
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
`;
|
|
81
|
+
const input = {
|
|
82
|
+
teamId: this.config.teamId,
|
|
83
|
+
title,
|
|
84
|
+
description: criterionText,
|
|
85
|
+
labelNames: ['planu-spec'],
|
|
86
|
+
};
|
|
87
|
+
if (parentId !== undefined) {
|
|
88
|
+
input.parentId = parentId;
|
|
89
|
+
}
|
|
90
|
+
const data = await this.graphql(mutation, { input });
|
|
91
|
+
const issue = data.issueCreate.issue;
|
|
92
|
+
return {
|
|
93
|
+
specId,
|
|
94
|
+
issueId: issue.id,
|
|
95
|
+
issueUrl: issue.url,
|
|
96
|
+
title: issue.title,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Update the workflow state (status) of a Linear issue.
|
|
101
|
+
* Looks up the state ID by name within the team, then applies it.
|
|
102
|
+
*/
|
|
103
|
+
async updateIssueStatus(issueId, stateName) {
|
|
104
|
+
const stateId = await this.findStateId(stateName);
|
|
105
|
+
if (stateId === null) {
|
|
106
|
+
// State not found — skip silently
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const mutation = `
|
|
110
|
+
mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {
|
|
111
|
+
issueUpdate(id: $id, input: $input) {
|
|
112
|
+
success
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
`;
|
|
116
|
+
const data = await this.graphql(mutation, {
|
|
117
|
+
id: issueId,
|
|
118
|
+
input: { stateId },
|
|
119
|
+
});
|
|
120
|
+
if (!data.issueUpdate.success) {
|
|
121
|
+
throw new Error(`Linear updateIssueStatus failed for issue ${issueId}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// Private helpers
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
async findStateId(stateName) {
|
|
128
|
+
const query = `
|
|
129
|
+
query WorkflowStates($teamId: String!) {
|
|
130
|
+
workflowStates(filter: { team: { id: { eq: $teamId } } }) {
|
|
131
|
+
nodes {
|
|
132
|
+
id
|
|
133
|
+
name
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
`;
|
|
138
|
+
const data = await this.graphql(query, {
|
|
139
|
+
teamId: this.config.teamId,
|
|
140
|
+
});
|
|
141
|
+
const match = data.workflowStates.nodes.find((s) => s.name.toLowerCase() === stateName.toLowerCase());
|
|
142
|
+
return match?.id ?? null;
|
|
143
|
+
}
|
|
144
|
+
async graphql(query, variables) {
|
|
145
|
+
const response = await globalThis.fetch(LINEAR_GRAPHQL_URL, {
|
|
146
|
+
method: 'POST',
|
|
147
|
+
headers: {
|
|
148
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
149
|
+
'Content-Type': 'application/json',
|
|
150
|
+
},
|
|
151
|
+
body: JSON.stringify({ query, variables }),
|
|
152
|
+
signal: AbortSignal.timeout(15_000),
|
|
153
|
+
});
|
|
154
|
+
if (!response.ok) {
|
|
155
|
+
const errText = await response.text();
|
|
156
|
+
throw new Error(`Linear GraphQL request failed: ${response.status} — ${errText}`);
|
|
157
|
+
}
|
|
158
|
+
const json = (await response.json());
|
|
159
|
+
if (json.errors !== undefined && json.errors.length > 0) {
|
|
160
|
+
const messages = json.errors.map((e) => e.message).join('; ');
|
|
161
|
+
throw new Error(`Linear GraphQL errors: ${messages}`);
|
|
162
|
+
}
|
|
163
|
+
if (json.data === undefined) {
|
|
164
|
+
throw new Error('Linear GraphQL returned no data');
|
|
165
|
+
}
|
|
166
|
+
return json.data;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// Description builder
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
/**
|
|
173
|
+
* Build a Markdown description for a Linear issue from a spec.
|
|
174
|
+
*/
|
|
175
|
+
export function buildLinearDescription(spec) {
|
|
176
|
+
const lines = [
|
|
177
|
+
`## ${spec.id}: ${spec.title}`,
|
|
178
|
+
'',
|
|
179
|
+
`**Status:** ${spec.status}`,
|
|
180
|
+
`**Source:** Planu spec`,
|
|
181
|
+
];
|
|
182
|
+
return lines.join('\n');
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=linear-exporter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"linear-exporter.js","sourceRoot":"","sources":["../../src/engine/linear-exporter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PropertyTest, PropertyTestExtractionResult, PropertyTestFrameworkTarget, PropertyBasedGeneratorInput } from '../types/property-testing.js';
|
|
2
|
+
/** Map a language/stack to the best property-based test framework. */
|
|
3
|
+
export declare function detectPropertyFramework(language: string, stack?: string[]): PropertyTestFrameworkTarget;
|
|
4
|
+
/**
|
|
5
|
+
* Extract PropertyTest objects from acceptance criteria strings.
|
|
6
|
+
* Each criterion is analyzed against known invariant patterns.
|
|
7
|
+
* Criteria with no detected pattern are skipped.
|
|
8
|
+
*/
|
|
9
|
+
export declare function extractPropertyTests(input: PropertyBasedGeneratorInput): PropertyTest[];
|
|
10
|
+
/**
|
|
11
|
+
* Full extraction with metadata: detected framework and analyzed criteria list.
|
|
12
|
+
*/
|
|
13
|
+
export declare function extractPropertyTestsWithMeta(input: PropertyBasedGeneratorInput): PropertyTestExtractionResult;
|
|
14
|
+
//# sourceMappingURL=property-test-generator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"property-test-generator.d.ts","sourceRoot":"","sources":["../../src/engine/property-test-generator.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,YAAY,EACZ,4BAA4B,EAC5B,2BAA2B,EAC3B,2BAA2B,EAE5B,MAAM,8BAA8B,CAAC;AAMtC,sEAAsE;AACtE,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,MAAM,EAChB,KAAK,GAAE,MAAM,EAAO,GACnB,2BAA2B,CAyB7B;AA0KD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,2BAA2B,GAAG,YAAY,EAAE,CAuBvF;AAED;;GAEG;AACH,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,2BAA2B,GACjC,4BAA4B,CAS9B"}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// engine/property-test-generator.ts — SPEC-315
|
|
2
|
+
// Extracts invariants from acceptance criteria and generates PropertyTest objects.
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Framework detection
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
/** Map a language/stack to the best property-based test framework. */
|
|
7
|
+
export function detectPropertyFramework(language, stack = []) {
|
|
8
|
+
const lang = language.toLowerCase();
|
|
9
|
+
const stackLower = stack.map((s) => s.toLowerCase());
|
|
10
|
+
if (stackLower.includes('fast-check') || stackLower.includes('fastcheck')) {
|
|
11
|
+
return 'fast-check';
|
|
12
|
+
}
|
|
13
|
+
if (stackLower.includes('hypothesis')) {
|
|
14
|
+
return 'hypothesis';
|
|
15
|
+
}
|
|
16
|
+
if (stackLower.includes('jqwik')) {
|
|
17
|
+
return 'jqwik';
|
|
18
|
+
}
|
|
19
|
+
if (lang === 'typescript' || lang === 'javascript') {
|
|
20
|
+
return 'fast-check';
|
|
21
|
+
}
|
|
22
|
+
if (lang === 'python') {
|
|
23
|
+
return 'hypothesis';
|
|
24
|
+
}
|
|
25
|
+
if (lang === 'java' || lang === 'kotlin') {
|
|
26
|
+
return 'jqwik';
|
|
27
|
+
}
|
|
28
|
+
return 'generic';
|
|
29
|
+
}
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Invariant pattern matchers
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
const INVARIANT_PATTERNS = [
|
|
34
|
+
{
|
|
35
|
+
// "price must be positive" / "amount should be positive" / "value must be >= 0"
|
|
36
|
+
pattern: /(?:price|amount|value|cost|total|balance)\s+(?:must|should)\s+be\s+(?:positive|>?\s*0|greater than 0)/i,
|
|
37
|
+
extract: (_m, raw) => ({
|
|
38
|
+
invariant: `${raw.trim()} — output is always > 0`,
|
|
39
|
+
inputDomain: 'positive floats (min: 0.01)',
|
|
40
|
+
}),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
// "must never be negative" / "should never be negative"
|
|
44
|
+
pattern: /(?:must|should)\s+never\s+be\s+negative/i,
|
|
45
|
+
extract: (_m, raw) => ({
|
|
46
|
+
invariant: `${raw.trim()} — result is never negative`,
|
|
47
|
+
inputDomain: 'non-negative floats (min: 0)',
|
|
48
|
+
}),
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
// "idempotent" / "idempotente"
|
|
52
|
+
pattern: /idempoten(?:t|te)/i,
|
|
53
|
+
extract: (_m, raw) => ({
|
|
54
|
+
invariant: `${raw.trim()} — applying N times equals applying once`,
|
|
55
|
+
inputDomain: 'arbitrary valid inputs',
|
|
56
|
+
}),
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
// "commutative" / "conmutativo"
|
|
60
|
+
pattern: /commutativ(?:e|ity)|conmutativ(?:o|idad)/i,
|
|
61
|
+
extract: (_m, raw) => ({
|
|
62
|
+
invariant: `${raw.trim()} — f(a,b) === f(b,a)`,
|
|
63
|
+
inputDomain: 'pairs of arbitrary values',
|
|
64
|
+
}),
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
// "always returns / always produces"
|
|
68
|
+
pattern: /always\s+(?:returns?|produces?|yields?)\s+(.+)/i,
|
|
69
|
+
extract: (m, raw) => ({
|
|
70
|
+
invariant: `${raw.trim()} — output is always ${m[1] ?? 'defined'}`,
|
|
71
|
+
inputDomain: 'arbitrary valid inputs',
|
|
72
|
+
}),
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
// "must not be empty" / "should not be empty"
|
|
76
|
+
pattern: /(?:must|should)\s+not\s+be\s+empty/i,
|
|
77
|
+
extract: (_m, raw) => ({
|
|
78
|
+
invariant: `${raw.trim()} — result is never empty`,
|
|
79
|
+
inputDomain: 'non-empty strings or arrays',
|
|
80
|
+
}),
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
// "for any input" / "for all inputs" / "para cualquier" / "para todos"
|
|
84
|
+
pattern: /(?:for\s+(?:any|all)\s+(?:valid\s+)?input|para\s+(?:cualquier|todos))/i,
|
|
85
|
+
extract: (_m, raw) => ({
|
|
86
|
+
invariant: `${raw.trim()} — holds for all valid inputs`,
|
|
87
|
+
inputDomain: 'arbitrary valid inputs',
|
|
88
|
+
}),
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
// "length must be between X and Y"
|
|
92
|
+
pattern: /length\s+must\s+be\s+(?:between\s+)?(\d+)\s+and\s+(\d+)/i,
|
|
93
|
+
extract: (m, raw) => ({
|
|
94
|
+
invariant: `${raw.trim()} — length is within [${m[1] ?? '0'}, ${m[2] ?? '∞'}]`,
|
|
95
|
+
inputDomain: `strings of length ${m[1] ?? '0'}..${m[2] ?? '100'}`,
|
|
96
|
+
}),
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
// "must be unique" / "should be unique"
|
|
100
|
+
pattern: /(?:must|should)\s+be\s+unique/i,
|
|
101
|
+
extract: (_m, raw) => ({
|
|
102
|
+
invariant: `${raw.trim()} — no duplicates in output`,
|
|
103
|
+
inputDomain: 'arrays of arbitrary comparable values',
|
|
104
|
+
}),
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
// "must be sorted" / "should be sorted" / "maintains order"
|
|
108
|
+
pattern: /(?:must|should)\s+be\s+sorted|maintains?\s+(?:sort\s+)?order/i,
|
|
109
|
+
extract: (_m, raw) => ({
|
|
110
|
+
invariant: `${raw.trim()} — output is sorted`,
|
|
111
|
+
inputDomain: 'arrays of comparable values',
|
|
112
|
+
}),
|
|
113
|
+
},
|
|
114
|
+
];
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Code generators per framework
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
function buildFastCheckCode(invariant, inputDomain) {
|
|
119
|
+
return `fc.assert(
|
|
120
|
+
fc.property(
|
|
121
|
+
fc.float({ min: 0.01, noNaN: true }), // ${inputDomain}
|
|
122
|
+
(input) => {
|
|
123
|
+
const result = functionUnderTest(input);
|
|
124
|
+
// ${invariant}
|
|
125
|
+
return result !== null && result !== undefined;
|
|
126
|
+
},
|
|
127
|
+
),
|
|
128
|
+
);`;
|
|
129
|
+
}
|
|
130
|
+
function buildHypothesisCode(invariant, inputDomain) {
|
|
131
|
+
const safeName = invariant.toLowerCase().replace(/[^a-z0-9]/g, '_').slice(0, 40);
|
|
132
|
+
return `@given(st.floats(min_value=0.01, allow_nan=False)) # ${inputDomain}
|
|
133
|
+
@settings(max_examples=100, deadline=None)
|
|
134
|
+
def test_${safeName}(input_value: float) -> None:
|
|
135
|
+
"""${invariant}"""
|
|
136
|
+
result = function_under_test(input_value)
|
|
137
|
+
assert result is not None`;
|
|
138
|
+
}
|
|
139
|
+
function buildJqwikCode(invariant, inputDomain) {
|
|
140
|
+
const safeName = invariant.replace(/[^a-zA-Z0-9]/g, '').slice(0, 40);
|
|
141
|
+
return `@Property
|
|
142
|
+
boolean ${safeName}(@ForAll @Positive double input) {
|
|
143
|
+
// ${inputDomain}
|
|
144
|
+
// ${invariant}
|
|
145
|
+
Object result = functionUnderTest(input);
|
|
146
|
+
return result != null;
|
|
147
|
+
}`;
|
|
148
|
+
}
|
|
149
|
+
function buildGenericCode(invariant, inputDomain) {
|
|
150
|
+
return `// Property: ${invariant}
|
|
151
|
+
// Input domain: ${inputDomain}
|
|
152
|
+
// For each arbitrary valid input in the domain:
|
|
153
|
+
// 1. Generate input using your framework's generator
|
|
154
|
+
// 2. Call the function under test
|
|
155
|
+
// 3. Assert: ${invariant}`;
|
|
156
|
+
}
|
|
157
|
+
function buildGeneratorCode(framework, invariant, inputDomain) {
|
|
158
|
+
switch (framework) {
|
|
159
|
+
case 'fast-check':
|
|
160
|
+
return buildFastCheckCode(invariant, inputDomain);
|
|
161
|
+
case 'hypothesis':
|
|
162
|
+
return buildHypothesisCode(invariant, inputDomain);
|
|
163
|
+
case 'jqwik':
|
|
164
|
+
return buildJqwikCode(invariant, inputDomain);
|
|
165
|
+
case 'generic':
|
|
166
|
+
return buildGenericCode(invariant, inputDomain);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function getShrinkStrategy(framework) {
|
|
170
|
+
switch (framework) {
|
|
171
|
+
case 'fast-check':
|
|
172
|
+
return 'automatic (built-in shrinker reduces to minimal failing case)';
|
|
173
|
+
case 'hypothesis':
|
|
174
|
+
return 'automatic (Hypothesis shrinks via @settings(deriving=True))';
|
|
175
|
+
case 'jqwik':
|
|
176
|
+
return 'automatic (jqwik shrinks @ForAll parameters)';
|
|
177
|
+
case 'generic':
|
|
178
|
+
return 'manual (implement shrink by binary search or domain reduction)';
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Public API
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
/**
|
|
185
|
+
* Extract PropertyTest objects from acceptance criteria strings.
|
|
186
|
+
* Each criterion is analyzed against known invariant patterns.
|
|
187
|
+
* Criteria with no detected pattern are skipped.
|
|
188
|
+
*/
|
|
189
|
+
export function extractPropertyTests(input) {
|
|
190
|
+
const framework = detectPropertyFramework(input.language, input.stack);
|
|
191
|
+
const shrinkStrategy = getShrinkStrategy(framework);
|
|
192
|
+
const results = [];
|
|
193
|
+
for (const criterion of input.criteria) {
|
|
194
|
+
for (const { pattern, extract } of INVARIANT_PATTERNS) {
|
|
195
|
+
const match = criterion.match(pattern);
|
|
196
|
+
if (match) {
|
|
197
|
+
const { invariant, inputDomain } = extract(match, criterion);
|
|
198
|
+
results.push({
|
|
199
|
+
invariant,
|
|
200
|
+
inputDomain,
|
|
201
|
+
generatorCode: buildGeneratorCode(framework, invariant, inputDomain),
|
|
202
|
+
framework,
|
|
203
|
+
shrinkStrategy,
|
|
204
|
+
});
|
|
205
|
+
break; // one pattern per criterion
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return results;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Full extraction with metadata: detected framework and analyzed criteria list.
|
|
213
|
+
*/
|
|
214
|
+
export function extractPropertyTestsWithMeta(input) {
|
|
215
|
+
const detectedFramework = detectPropertyFramework(input.language, input.stack);
|
|
216
|
+
const tests = extractPropertyTests(input);
|
|
217
|
+
return {
|
|
218
|
+
tests,
|
|
219
|
+
detectedFramework,
|
|
220
|
+
analyzedCriteria: input.criteria,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=property-test-generator.js.map
|