@structured-vibe-coding/core 0.12.1 → 0.13.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/README.md +1 -0
- package/defaults/memory/index.md +3 -0
- package/defaults/templates/features.md +42 -0
- package/defaults/templates/greenfield-architecture.md +39 -0
- package/defaults/templates/requirements.md +29 -0
- package/defaults/templates/vision.md +34 -0
- package/defaults/workflows/greenfield.yaml +92 -0
- package/dist/{agents-R4RMNXOS.js → agents-WL3PNTJL.js} +5 -2
- package/dist/change-context-W4HDO2IK.js +23 -0
- package/dist/{chunk-KZHNYGD6.js → chunk-67AUAGNV.js} +1 -38
- package/dist/chunk-A752UVPF.js +317 -0
- package/dist/chunk-C725DWSB.js +122 -0
- package/dist/chunk-OJFMJHNL.js +42 -0
- package/dist/{chunk-24S2YWWL.js → chunk-S7SHVKG4.js} +22 -435
- package/dist/chunk-SM734TFW.js +784 -0
- package/dist/{config-4QZMRSL3.js → config-ZYEEIRXJ.js} +2 -1
- package/dist/greenfield-NTLW6XBL.js +23 -0
- package/dist/index.d.ts +66 -1
- package/dist/index.js +103 -537
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ npm install @structured-vibe-coding/core
|
|
|
13
13
|
## Docs
|
|
14
14
|
|
|
15
15
|
- [First change tutorial](https://github.com/harsha09/spec-driven-development/blob/main/docs/tutorials/first-change.md)
|
|
16
|
+
- [Greenfield](https://github.com/harsha09/spec-driven-development/blob/main/docs/guides/greenfield.md)
|
|
16
17
|
- [Docs site](https://harsha09.github.io/spec-driven-development/)
|
|
17
18
|
- [Repository](https://github.com/harsha09/spec-driven-development)
|
|
18
19
|
|
package/defaults/memory/index.md
CHANGED
|
@@ -14,10 +14,13 @@ Parent page for **stable** project docs. Humans and coding agents: start here, t
|
|
|
14
14
|
| Topic | File |
|
|
15
15
|
|-------|------|
|
|
16
16
|
| Product / vision | [product.md](product.md) |
|
|
17
|
+
| Requirements | [requirements.md](requirements.md) (after greenfield) |
|
|
18
|
+
| Feature backlog | [features.md](features.md) (after greenfield; `sdd feature start F-001`) |
|
|
17
19
|
| Constitution (non-negotiables) | [constitution.md](constitution.md) |
|
|
18
20
|
| Architecture | [architecture.md](architecture.md) |
|
|
19
21
|
| Coding & process conventions | [conventions.md](conventions.md) |
|
|
20
22
|
|
|
23
|
+
**Greenfield:** `sdd greenfield "one-line idea"` then promote into memory on complete.
|
|
21
24
|
Add rows here when you introduce new durable docs (e.g. `security.md`, `ops.md`).
|
|
22
25
|
|
|
23
26
|
## Active work
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Feature backlog
|
|
2
|
+
|
|
3
|
+
> **{{title}}** · `{{id}}`
|
|
4
|
+
> Each feature should fit **one PR**. Start implementation with:
|
|
5
|
+
> `sdd feature start F-001`
|
|
6
|
+
|
|
7
|
+
## How to write a feature
|
|
8
|
+
|
|
9
|
+
Copy the block below for each item. Keep **Status** as `planned` until started.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## F-001: <short name>
|
|
14
|
+
|
|
15
|
+
- **Status:** planned
|
|
16
|
+
- **Priority:** must
|
|
17
|
+
- **Workflow:** feature
|
|
18
|
+
- **Summary:** One or two sentences of user-visible outcome.
|
|
19
|
+
- **Requirements:** R-001
|
|
20
|
+
- **Notes:**
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## F-002: <short name>
|
|
25
|
+
|
|
26
|
+
- **Status:** planned
|
|
27
|
+
- **Priority:** must
|
|
28
|
+
- **Workflow:** feature
|
|
29
|
+
- **Summary:**
|
|
30
|
+
- **Requirements:**
|
|
31
|
+
- **Notes:**
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## F-003: <short name>
|
|
36
|
+
|
|
37
|
+
- **Status:** planned
|
|
38
|
+
- **Priority:** should
|
|
39
|
+
- **Workflow:** feature
|
|
40
|
+
- **Summary:**
|
|
41
|
+
- **Requirements:**
|
|
42
|
+
- **Notes:**
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# High-level architecture
|
|
2
|
+
|
|
3
|
+
> **{{title}}** · `{{id}}`
|
|
4
|
+
> MVP only — postpone scale fantasies.
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
What system are we building and what does it touch?
|
|
9
|
+
|
|
10
|
+
-
|
|
11
|
+
|
|
12
|
+
## Major components
|
|
13
|
+
|
|
14
|
+
| Component | Responsibility |
|
|
15
|
+
|-----------|----------------|
|
|
16
|
+
| | |
|
|
17
|
+
|
|
18
|
+
## Data
|
|
19
|
+
|
|
20
|
+
-
|
|
21
|
+
|
|
22
|
+
## Key flows
|
|
23
|
+
|
|
24
|
+
1.
|
|
25
|
+
2.
|
|
26
|
+
|
|
27
|
+
## Tech choices (provisional)
|
|
28
|
+
|
|
29
|
+
-
|
|
30
|
+
|
|
31
|
+
## Risks & mitigations
|
|
32
|
+
|
|
33
|
+
| Risk | Mitigation |
|
|
34
|
+
|------|------------|
|
|
35
|
+
| | |
|
|
36
|
+
|
|
37
|
+
## Open questions
|
|
38
|
+
|
|
39
|
+
1.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Requirements
|
|
2
|
+
|
|
3
|
+
> **{{title}}** · `{{id}}`
|
|
4
|
+
> Prefer testable statements. Avoid UI pixel detail unless critical.
|
|
5
|
+
|
|
6
|
+
## Must have
|
|
7
|
+
|
|
8
|
+
| ID | Requirement | Acceptance signal |
|
|
9
|
+
|----|-------------|-------------------|
|
|
10
|
+
| R-001 | | |
|
|
11
|
+
| R-002 | | |
|
|
12
|
+
|
|
13
|
+
## Should have
|
|
14
|
+
|
|
15
|
+
| ID | Requirement | Acceptance signal |
|
|
16
|
+
|----|-------------|-------------------|
|
|
17
|
+
| R-010 | | |
|
|
18
|
+
|
|
19
|
+
## Nice to have (later)
|
|
20
|
+
|
|
21
|
+
-
|
|
22
|
+
|
|
23
|
+
## Assumptions
|
|
24
|
+
|
|
25
|
+
-
|
|
26
|
+
|
|
27
|
+
## Risks
|
|
28
|
+
|
|
29
|
+
-
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Product vision
|
|
2
|
+
|
|
3
|
+
> **{{title}}** · `{{id}}` · {{date}}
|
|
4
|
+
> Greenfield — keep the **smallest useful product**.
|
|
5
|
+
|
|
6
|
+
## One-liner
|
|
7
|
+
|
|
8
|
+
{{idea}}
|
|
9
|
+
|
|
10
|
+
## Who is this for?
|
|
11
|
+
|
|
12
|
+
-
|
|
13
|
+
|
|
14
|
+
## Problem
|
|
15
|
+
|
|
16
|
+
-
|
|
17
|
+
|
|
18
|
+
## MVP success (how we know it worked)
|
|
19
|
+
|
|
20
|
+
-
|
|
21
|
+
|
|
22
|
+
## Non-goals (explicitly out of v1)
|
|
23
|
+
|
|
24
|
+
-
|
|
25
|
+
|
|
26
|
+
## Constraints
|
|
27
|
+
|
|
28
|
+
- Time box:
|
|
29
|
+
- Platforms:
|
|
30
|
+
- Compliance / privacy:
|
|
31
|
+
|
|
32
|
+
## Open questions
|
|
33
|
+
|
|
34
|
+
1.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
name: greenfield
|
|
2
|
+
description: >
|
|
3
|
+
New product from a one-line idea — vision, requirements, feature backlog,
|
|
4
|
+
high-level architecture (then implement features as separate changes)
|
|
5
|
+
version: 1
|
|
6
|
+
recommendation:
|
|
7
|
+
priority: 40
|
|
8
|
+
when:
|
|
9
|
+
keywords: [greenfield, product, mvp, startup, new app, from scratch, build an app]
|
|
10
|
+
min_complexity: medium
|
|
11
|
+
stages:
|
|
12
|
+
- id: vision
|
|
13
|
+
title: Product vision
|
|
14
|
+
summary: Who, problem, MVP success, non-goals — keep the smallest useful product
|
|
15
|
+
skippable: false
|
|
16
|
+
artifacts:
|
|
17
|
+
- id: vision
|
|
18
|
+
path: vision.md
|
|
19
|
+
template: vision.md
|
|
20
|
+
required: true
|
|
21
|
+
gate:
|
|
22
|
+
type: soft
|
|
23
|
+
checklist:
|
|
24
|
+
- "Users and problem are explicit"
|
|
25
|
+
- "MVP success is testable"
|
|
26
|
+
- "Non-goals limit scope"
|
|
27
|
+
agent_context:
|
|
28
|
+
instructions: |
|
|
29
|
+
GREENFIELD vision stage. Expand the one-line idea into a concrete MVP vision.
|
|
30
|
+
Prefer the smallest useful product. Do not invent enterprise scope.
|
|
31
|
+
Write only vision.md. Do not implement code yet.
|
|
32
|
+
|
|
33
|
+
- id: requirements
|
|
34
|
+
title: Requirements
|
|
35
|
+
summary: Testable product requirements derived from vision
|
|
36
|
+
skippable: false
|
|
37
|
+
artifacts:
|
|
38
|
+
- id: requirements
|
|
39
|
+
path: requirements.md
|
|
40
|
+
template: requirements.md
|
|
41
|
+
required: true
|
|
42
|
+
gate:
|
|
43
|
+
type: soft
|
|
44
|
+
checklist:
|
|
45
|
+
- "Requirements are testable (not vague wishes)"
|
|
46
|
+
agent_context:
|
|
47
|
+
instructions: |
|
|
48
|
+
Write testable requirements (shall/should). Link each to user value.
|
|
49
|
+
Avoid solution detail that belongs in architecture. Update requirements.md only.
|
|
50
|
+
|
|
51
|
+
- id: features
|
|
52
|
+
title: Feature backlog
|
|
53
|
+
summary: PR-sized features with ids F-001… for later sdd feature start
|
|
54
|
+
skippable: false
|
|
55
|
+
artifacts:
|
|
56
|
+
- id: features
|
|
57
|
+
path: features.md
|
|
58
|
+
template: features.md
|
|
59
|
+
required: true
|
|
60
|
+
gate:
|
|
61
|
+
type: soft
|
|
62
|
+
checklist:
|
|
63
|
+
- "Features are small enough for one PR each"
|
|
64
|
+
- "Each feature has id F-NNN and status planned"
|
|
65
|
+
agent_context:
|
|
66
|
+
instructions: |
|
|
67
|
+
Slice the MVP into PR-sized features. Use ids F-001, F-002, … exactly as in the template.
|
|
68
|
+
Status must be `planned` until implemented. Prefer must-have first. No code yet.
|
|
69
|
+
|
|
70
|
+
- id: architecture
|
|
71
|
+
title: High-level architecture
|
|
72
|
+
summary: System shape, major components, data, risks for the MVP
|
|
73
|
+
skippable: false
|
|
74
|
+
artifacts:
|
|
75
|
+
- id: architecture
|
|
76
|
+
path: architecture.md
|
|
77
|
+
template: greenfield-architecture.md
|
|
78
|
+
required: true
|
|
79
|
+
gate:
|
|
80
|
+
type: soft
|
|
81
|
+
checklist:
|
|
82
|
+
- "Major components and boundaries are clear"
|
|
83
|
+
- "Risks and open questions listed"
|
|
84
|
+
agent_context:
|
|
85
|
+
instructions: |
|
|
86
|
+
Propose a simple MVP architecture. Avoid over-engineering.
|
|
87
|
+
Align with requirements and features. Write architecture.md only.
|
|
88
|
+
When the human runs sdd complete, files promote into memory/ for the product.
|
|
89
|
+
|
|
90
|
+
on_complete:
|
|
91
|
+
archive: false
|
|
92
|
+
domain_sync: never
|
|
@@ -19,8 +19,11 @@ import {
|
|
|
19
19
|
removeOtherAgentHosts,
|
|
20
20
|
renderThinAgent,
|
|
21
21
|
writeAgentHandoff
|
|
22
|
-
} from "./chunk-
|
|
23
|
-
import "./chunk-
|
|
22
|
+
} from "./chunk-S7SHVKG4.js";
|
|
23
|
+
import "./chunk-OJFMJHNL.js";
|
|
24
|
+
import "./chunk-C725DWSB.js";
|
|
25
|
+
import "./chunk-A752UVPF.js";
|
|
26
|
+
import "./chunk-67AUAGNV.js";
|
|
24
27
|
import "./chunk-7MJYPJMR.js";
|
|
25
28
|
import "./chunk-EPFURATC.js";
|
|
26
29
|
export {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildContext,
|
|
3
|
+
clearActiveIf,
|
|
4
|
+
getActiveChangeId,
|
|
5
|
+
listChanges,
|
|
6
|
+
loadChangeMeta,
|
|
7
|
+
resolveChangeId,
|
|
8
|
+
saveChangeMeta,
|
|
9
|
+
setActiveChange
|
|
10
|
+
} from "./chunk-A752UVPF.js";
|
|
11
|
+
import "./chunk-67AUAGNV.js";
|
|
12
|
+
import "./chunk-7MJYPJMR.js";
|
|
13
|
+
import "./chunk-EPFURATC.js";
|
|
14
|
+
export {
|
|
15
|
+
buildContext,
|
|
16
|
+
clearActiveIf,
|
|
17
|
+
getActiveChangeId,
|
|
18
|
+
listChanges,
|
|
19
|
+
loadChangeMeta,
|
|
20
|
+
resolveChangeId,
|
|
21
|
+
saveChangeMeta,
|
|
22
|
+
setActiveChange
|
|
23
|
+
};
|
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
configPath,
|
|
3
|
-
sddRoot
|
|
4
|
-
} from "./chunk-7MJYPJMR.js";
|
|
5
|
-
import {
|
|
6
|
-
ensureDir,
|
|
7
|
-
pathExists,
|
|
8
|
-
readYaml,
|
|
9
|
-
writeYaml
|
|
10
|
-
} from "./chunk-EPFURATC.js";
|
|
11
|
-
|
|
12
1
|
// src/schemas.ts
|
|
13
2
|
import { z } from "zod";
|
|
14
3
|
var GateSchema = z.object({
|
|
@@ -150,28 +139,6 @@ var ChangeMetaSchema = z.object({
|
|
|
150
139
|
completed_at: z.string().optional()
|
|
151
140
|
});
|
|
152
141
|
|
|
153
|
-
// src/config.ts
|
|
154
|
-
async function isInitialized(projectRoot) {
|
|
155
|
-
return pathExists(configPath(projectRoot));
|
|
156
|
-
}
|
|
157
|
-
async function loadConfig(projectRoot) {
|
|
158
|
-
const path = configPath(projectRoot);
|
|
159
|
-
if (!await pathExists(path)) {
|
|
160
|
-
throw new Error(
|
|
161
|
-
`SDD is not initialized in ${projectRoot}. Run \`sdd init\` first.`
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
const raw = await readYaml(path);
|
|
165
|
-
return ConfigSchema.parse(raw);
|
|
166
|
-
}
|
|
167
|
-
async function saveConfig(projectRoot, config) {
|
|
168
|
-
await ensureDir(sddRoot(projectRoot));
|
|
169
|
-
await writeYaml(configPath(projectRoot), ConfigSchema.parse(config));
|
|
170
|
-
}
|
|
171
|
-
function defaultConfig() {
|
|
172
|
-
return ConfigSchema.parse({});
|
|
173
|
-
}
|
|
174
|
-
|
|
175
142
|
export {
|
|
176
143
|
GateSchema,
|
|
177
144
|
ArtifactDefSchema,
|
|
@@ -179,9 +146,5 @@ export {
|
|
|
179
146
|
StageSchema,
|
|
180
147
|
WorkflowSchema,
|
|
181
148
|
ConfigSchema,
|
|
182
|
-
ChangeMetaSchema
|
|
183
|
-
isInitialized,
|
|
184
|
-
loadConfig,
|
|
185
|
-
saveConfig,
|
|
186
|
-
defaultConfig
|
|
149
|
+
ChangeMetaSchema
|
|
187
150
|
};
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChangeMetaSchema,
|
|
3
|
+
WorkflowSchema
|
|
4
|
+
} from "./chunk-67AUAGNV.js";
|
|
5
|
+
import {
|
|
6
|
+
activePointerPath,
|
|
7
|
+
changeMetaPath,
|
|
8
|
+
changePath,
|
|
9
|
+
changesDir,
|
|
10
|
+
workflowsDir
|
|
11
|
+
} from "./chunk-7MJYPJMR.js";
|
|
12
|
+
import {
|
|
13
|
+
ensureDir,
|
|
14
|
+
listDirs,
|
|
15
|
+
listFiles,
|
|
16
|
+
pathExists,
|
|
17
|
+
readText,
|
|
18
|
+
readYaml,
|
|
19
|
+
writeText,
|
|
20
|
+
writeYaml
|
|
21
|
+
} from "./chunk-EPFURATC.js";
|
|
22
|
+
|
|
23
|
+
// src/slug.ts
|
|
24
|
+
function slugify(input) {
|
|
25
|
+
return input.toLowerCase().trim().replace(/['"]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60).replace(/-+$/g, "");
|
|
26
|
+
}
|
|
27
|
+
function todayIsoDate(d = /* @__PURE__ */ new Date()) {
|
|
28
|
+
return d.toISOString().slice(0, 10);
|
|
29
|
+
}
|
|
30
|
+
function nowIso(d = /* @__PURE__ */ new Date()) {
|
|
31
|
+
return d.toISOString();
|
|
32
|
+
}
|
|
33
|
+
function changeDirName(title, d = /* @__PURE__ */ new Date()) {
|
|
34
|
+
const slug = slugify(title) || "change";
|
|
35
|
+
return `${todayIsoDate(d)}-${slug}`;
|
|
36
|
+
}
|
|
37
|
+
function uniqueChangeDirName(base, taken) {
|
|
38
|
+
if (!taken.has(base)) return base;
|
|
39
|
+
let n = 2;
|
|
40
|
+
while (taken.has(`${base}-${n}`)) n += 1;
|
|
41
|
+
return `${base}-${n}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/workflow.ts
|
|
45
|
+
import { join } from "pathe";
|
|
46
|
+
async function listWorkflowNames(projectRoot) {
|
|
47
|
+
const dir = workflowsDir(projectRoot);
|
|
48
|
+
const files = await listFiles(dir);
|
|
49
|
+
return files.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml")).map((f) => f.replace(/\.ya?ml$/, "")).sort();
|
|
50
|
+
}
|
|
51
|
+
async function loadWorkflow(projectRoot, name) {
|
|
52
|
+
const dir = workflowsDir(projectRoot);
|
|
53
|
+
const candidates = [`${name}.yaml`, `${name}.yml`];
|
|
54
|
+
for (const file of candidates) {
|
|
55
|
+
const path = join(dir, file);
|
|
56
|
+
if (await pathExists(path)) {
|
|
57
|
+
const raw = await readYaml(path);
|
|
58
|
+
const workflow = WorkflowSchema.parse(raw);
|
|
59
|
+
if (workflow.name !== name) {
|
|
60
|
+
}
|
|
61
|
+
return workflow;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Workflow "${name}" not found in ${dir}. Available: ${(await listWorkflowNames(projectRoot)).join(", ") || "(none)"}`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
function resolveStages(workflow, meta) {
|
|
69
|
+
const base = [...workflow.stages];
|
|
70
|
+
const extra = meta.overrides?.extra_stages ?? [];
|
|
71
|
+
const stages = [...base, ...extra];
|
|
72
|
+
return stages.map((stage) => {
|
|
73
|
+
const gateOverride = meta.overrides?.gates?.[stage.id];
|
|
74
|
+
if (!gateOverride) return stage;
|
|
75
|
+
return {
|
|
76
|
+
...stage,
|
|
77
|
+
gate: {
|
|
78
|
+
...stage.gate,
|
|
79
|
+
type: gateOverride
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function isStageSkipped(meta, stageId) {
|
|
85
|
+
if (meta.overrides?.skip_stages?.includes(stageId)) return true;
|
|
86
|
+
if (meta.skipped?.some((s) => s.stage === stageId)) return true;
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
function shouldAutoSkip(stage, meta) {
|
|
90
|
+
const flags = stage.skip_when?.flags ?? [];
|
|
91
|
+
for (const flag of flags) {
|
|
92
|
+
if (meta.flags?.[flag] === true || meta.flags?.[flag] === "true") {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
function activeStages(workflow, meta) {
|
|
99
|
+
return resolveStages(workflow, meta).filter((s) => {
|
|
100
|
+
if (isStageSkipped(meta, s.id)) return false;
|
|
101
|
+
if (shouldAutoSkip(s, meta)) return false;
|
|
102
|
+
return true;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function getStage(workflow, meta, stageId) {
|
|
106
|
+
return resolveStages(workflow, meta).find((s) => s.id === stageId);
|
|
107
|
+
}
|
|
108
|
+
function nextStageId(workflow, meta) {
|
|
109
|
+
const all = resolveStages(workflow, meta);
|
|
110
|
+
const activeIds = new Set(activeStages(workflow, meta).map((s) => s.id));
|
|
111
|
+
let start = all.findIndex((s) => s.id === meta.stage);
|
|
112
|
+
if (start === -1) start = -1;
|
|
113
|
+
for (let i = start + 1; i < all.length; i++) {
|
|
114
|
+
const id = all[i].id;
|
|
115
|
+
if (activeIds.has(id)) return id;
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
function firstActiveStageId(workflow, meta) {
|
|
120
|
+
return activeStages(workflow, meta)[0]?.id ?? null;
|
|
121
|
+
}
|
|
122
|
+
function inferComplexity(title, hints) {
|
|
123
|
+
const text = `${title} ${(hints?.keywords ?? []).join(" ")}`.toLowerCase();
|
|
124
|
+
const hotfixHints = ["typo", "hotfix", "nit", "rename", "bump", "chore", "readme", "docs"];
|
|
125
|
+
const highHints = [
|
|
126
|
+
"architecture",
|
|
127
|
+
"migration",
|
|
128
|
+
"auth",
|
|
129
|
+
"payment",
|
|
130
|
+
"billing",
|
|
131
|
+
"redesign",
|
|
132
|
+
"platform",
|
|
133
|
+
"arb",
|
|
134
|
+
"epic"
|
|
135
|
+
];
|
|
136
|
+
if (hotfixHints.some((h) => text.includes(h))) return "low";
|
|
137
|
+
if (highHints.some((h) => text.includes(h))) return "high";
|
|
138
|
+
if (text.length > 80) return "medium";
|
|
139
|
+
return "medium";
|
|
140
|
+
}
|
|
141
|
+
async function recommendWorkflow(projectRoot, title, config, opts) {
|
|
142
|
+
const available = await listWorkflowNames(projectRoot);
|
|
143
|
+
const allowed = config.allowed_workflows?.length ? available.filter((n) => config.allowed_workflows.includes(n)) : available;
|
|
144
|
+
if (opts?.preferred) {
|
|
145
|
+
if (!available.includes(opts.preferred)) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Workflow "${opts.preferred}" is not available. Available: ${available.join(", ") || "(none)"}`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
if (config.allowed_workflows?.length && !config.allowed_workflows.includes(opts.preferred)) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`Workflow "${opts.preferred}" is not in allowed_workflows: ${config.allowed_workflows.join(", ")}`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
name: opts.preferred,
|
|
157
|
+
reason: "Explicitly selected",
|
|
158
|
+
alternatives: allowed.filter((n) => n !== opts.preferred)
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (config.default_workflow !== "recommend" && allowed.includes(config.default_workflow)) {
|
|
162
|
+
return {
|
|
163
|
+
name: config.default_workflow,
|
|
164
|
+
reason: `Repo default_workflow=${config.default_workflow}`,
|
|
165
|
+
alternatives: allowed.filter((n) => n !== config.default_workflow)
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const complexity = opts?.complexity ?? inferComplexity(title);
|
|
169
|
+
const scored = [];
|
|
170
|
+
for (const name of allowed) {
|
|
171
|
+
try {
|
|
172
|
+
const wf = await loadWorkflow(projectRoot, name);
|
|
173
|
+
let score = wf.recommendation?.priority ?? 0;
|
|
174
|
+
let reason = `workflow priority ${score}`;
|
|
175
|
+
const when = wf.recommendation?.when;
|
|
176
|
+
if (complexity === "low" && name === "hotfix") {
|
|
177
|
+
score += 50;
|
|
178
|
+
reason = "Low complexity \u2192 hotfix";
|
|
179
|
+
} else if (complexity === "low" && name === "patch") {
|
|
180
|
+
score += 40;
|
|
181
|
+
reason = "Low complexity \u2192 patch";
|
|
182
|
+
} else if (complexity === "medium" && name === "feature") {
|
|
183
|
+
score += 45;
|
|
184
|
+
reason = "Medium complexity \u2192 feature";
|
|
185
|
+
} else if (complexity === "medium" && name === "patch") {
|
|
186
|
+
score += 30;
|
|
187
|
+
reason = "Medium complexity \u2192 patch";
|
|
188
|
+
} else if (complexity === "high" && name === "enterprise-feature") {
|
|
189
|
+
score += 50;
|
|
190
|
+
reason = "High complexity \u2192 enterprise-feature";
|
|
191
|
+
} else if (complexity === "high" && name === "feature") {
|
|
192
|
+
score += 35;
|
|
193
|
+
reason = "High complexity \u2192 feature";
|
|
194
|
+
}
|
|
195
|
+
const keywords = when?.keywords ?? [];
|
|
196
|
+
const titleLower = title.toLowerCase();
|
|
197
|
+
if (keywords.some((k) => titleLower.includes(k.toLowerCase()))) {
|
|
198
|
+
score += 20;
|
|
199
|
+
reason = `Keyword match for ${name}`;
|
|
200
|
+
}
|
|
201
|
+
scored.push({ name, score, reason });
|
|
202
|
+
} catch {
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
scored.sort((a, b) => b.score - a.score);
|
|
206
|
+
const best = scored[0];
|
|
207
|
+
if (!best) {
|
|
208
|
+
throw new Error("No workflows available. Run `sdd init` or add files under .sdd/workflows/");
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
name: best.name,
|
|
212
|
+
reason: best.reason,
|
|
213
|
+
alternatives: scored.slice(1).map((s) => s.name)
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// src/change-context.ts
|
|
218
|
+
async function loadChangeMeta(projectRoot, config, changeId) {
|
|
219
|
+
const path = changeMetaPath(projectRoot, config, changeId);
|
|
220
|
+
if (!await pathExists(path)) {
|
|
221
|
+
throw new Error(`Change not found: ${changeId}`);
|
|
222
|
+
}
|
|
223
|
+
return ChangeMetaSchema.parse(await readYaml(path));
|
|
224
|
+
}
|
|
225
|
+
async function saveChangeMeta(projectRoot, config, meta) {
|
|
226
|
+
meta.updated = nowIso();
|
|
227
|
+
await writeYaml(changeMetaPath(projectRoot, config, meta.id), ChangeMetaSchema.parse(meta));
|
|
228
|
+
}
|
|
229
|
+
async function buildContext(projectRoot, config, changeId) {
|
|
230
|
+
const meta = await loadChangeMeta(projectRoot, config, changeId);
|
|
231
|
+
const workflow = await loadWorkflow(projectRoot, meta.workflow);
|
|
232
|
+
return {
|
|
233
|
+
id: changeId,
|
|
234
|
+
path: changePath(projectRoot, config, changeId),
|
|
235
|
+
meta,
|
|
236
|
+
workflow,
|
|
237
|
+
stages: resolveStages(workflow, meta),
|
|
238
|
+
active: activeStages(workflow, meta)
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
async function listChanges(projectRoot, config) {
|
|
242
|
+
return listDirs(changesDir(projectRoot, config));
|
|
243
|
+
}
|
|
244
|
+
async function getActiveChangeId(projectRoot, config) {
|
|
245
|
+
const pointer = activePointerPath(projectRoot, config);
|
|
246
|
+
if (await pathExists(pointer)) {
|
|
247
|
+
const id = (await readText(pointer)).trim();
|
|
248
|
+
if (id && await pathExists(changePath(projectRoot, config, id))) {
|
|
249
|
+
return id;
|
|
250
|
+
}
|
|
251
|
+
if (id) {
|
|
252
|
+
await writeText(pointer, "");
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const all = await listChanges(projectRoot, config);
|
|
256
|
+
const inProgress = [];
|
|
257
|
+
for (const id of all) {
|
|
258
|
+
try {
|
|
259
|
+
const meta = await loadChangeMeta(projectRoot, config, id);
|
|
260
|
+
if (meta.status === "in_progress" || meta.status === "blocked") {
|
|
261
|
+
inProgress.push({ id, updated: meta.updated ?? meta.created });
|
|
262
|
+
}
|
|
263
|
+
} catch {
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (!inProgress.length) return null;
|
|
267
|
+
inProgress.sort((a, b) => b.updated.localeCompare(a.updated));
|
|
268
|
+
return inProgress[0].id;
|
|
269
|
+
}
|
|
270
|
+
async function clearActiveIf(projectRoot, config, changeId) {
|
|
271
|
+
const pointer = activePointerPath(projectRoot, config);
|
|
272
|
+
if (!await pathExists(pointer)) return;
|
|
273
|
+
const id = (await readText(pointer)).trim();
|
|
274
|
+
if (id === changeId) {
|
|
275
|
+
await writeText(pointer, "");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
async function setActiveChange(projectRoot, config, changeId) {
|
|
279
|
+
await ensureDir(changesDir(projectRoot, config));
|
|
280
|
+
await writeText(activePointerPath(projectRoot, config), `${changeId}
|
|
281
|
+
`);
|
|
282
|
+
}
|
|
283
|
+
async function resolveChangeId(projectRoot, config, changeId) {
|
|
284
|
+
if (changeId) return changeId;
|
|
285
|
+
const active = await getActiveChangeId(projectRoot, config);
|
|
286
|
+
if (!active) {
|
|
287
|
+
throw new Error("No active change. Pass --change <id> or run `sdd new`.");
|
|
288
|
+
}
|
|
289
|
+
return active;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export {
|
|
293
|
+
listWorkflowNames,
|
|
294
|
+
loadWorkflow,
|
|
295
|
+
resolveStages,
|
|
296
|
+
isStageSkipped,
|
|
297
|
+
shouldAutoSkip,
|
|
298
|
+
activeStages,
|
|
299
|
+
getStage,
|
|
300
|
+
nextStageId,
|
|
301
|
+
firstActiveStageId,
|
|
302
|
+
inferComplexity,
|
|
303
|
+
recommendWorkflow,
|
|
304
|
+
slugify,
|
|
305
|
+
todayIsoDate,
|
|
306
|
+
nowIso,
|
|
307
|
+
changeDirName,
|
|
308
|
+
uniqueChangeDirName,
|
|
309
|
+
loadChangeMeta,
|
|
310
|
+
saveChangeMeta,
|
|
311
|
+
buildContext,
|
|
312
|
+
listChanges,
|
|
313
|
+
getActiveChangeId,
|
|
314
|
+
clearActiveIf,
|
|
315
|
+
setActiveChange,
|
|
316
|
+
resolveChangeId
|
|
317
|
+
};
|