lean-spec 0.2.9 โ 0.2.15-dev.21022397862
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 +101 -9
- package/bin/lean-spec-rust.js +116 -0
- package/bin/lean-spec.js +9 -1
- package/binaries/darwin-arm64/lean-spec +0 -0
- package/binaries/darwin-arm64/package.json +20 -0
- package/binaries/darwin-x64/lean-spec +0 -0
- package/binaries/darwin-x64/package.json +20 -0
- package/binaries/linux-arm64/lean-spec +0 -0
- package/binaries/linux-arm64/package.json +20 -0
- package/binaries/linux-x64/lean-spec +0 -0
- package/binaries/linux-x64/package.json +20 -0
- package/binaries/windows-x64/lean-spec.exe +0 -0
- package/binaries/windows-x64/package.json +20 -0
- package/package.json +9 -43
- package/templates/detailed/AGENTS.md +72 -101
- package/templates/standard/AGENTS.md +72 -101
- package/dist/backfill-WYW47B42.js +0 -5
- package/dist/backfill-WYW47B42.js.map +0 -1
- package/dist/chunk-ENX5NYTE.js +0 -297
- package/dist/chunk-ENX5NYTE.js.map +0 -1
- package/dist/chunk-FILG4YF3.js +0 -2990
- package/dist/chunk-FILG4YF3.js.map +0 -1
- package/dist/chunk-JX4PXHNC.js +0 -7945
- package/dist/chunk-JX4PXHNC.js.map +0 -1
- package/dist/chunk-K4VTB6BF.js +0 -298
- package/dist/chunk-K4VTB6BF.js.map +0 -1
- package/dist/chunk-SZNMHOHN.js +0 -444
- package/dist/chunk-SZNMHOHN.js.map +0 -1
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -129
- package/dist/cli.js.map +0 -1
- package/dist/frontmatter-Q5UJOZLR.js +0 -3
- package/dist/frontmatter-Q5UJOZLR.js.map +0 -1
- package/dist/mcp-server.d.ts +0 -16
- package/dist/mcp-server.js +0 -8
- package/dist/mcp-server.js.map +0 -1
- package/dist/validate-LMCR5JTN.js +0 -5
- package/dist/validate-LMCR5JTN.js.map +0 -1
|
@@ -12,132 +12,103 @@
|
|
|
12
12
|
|
|
13
13
|
> **Why?** Skipping discovery creates duplicate work. Manual file creation breaks LeanSpec tooling.
|
|
14
14
|
|
|
15
|
-
## ๐ง
|
|
16
|
-
|
|
17
|
-
###
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
|
22
|
-
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
|
28
|
-
|
|
|
29
|
-
|
|
|
30
|
-
|
|
|
31
|
-
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
lean-spec board # Project overview
|
|
45
|
-
lean-spec list # See all specs
|
|
46
|
-
lean-spec search "query" # Find relevant specs
|
|
47
|
-
lean-spec create <name> # Create new spec
|
|
48
|
-
lean-spec update <spec> --status <status> # Update status
|
|
49
|
-
lean-spec link <spec> --depends-on <other> # Add dependencies
|
|
50
|
-
lean-spec unlink <spec> --depends-on <other> # Remove dependencies
|
|
51
|
-
lean-spec deps <spec> # Show dependencies
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
**Tip:** Check if you have LeanSpec MCP tools available before using CLI.
|
|
55
|
-
|
|
56
|
-
## โ ๏ธ SDD Workflow Checkpoints
|
|
57
|
-
|
|
58
|
-
### Before Starting ANY Task
|
|
59
|
-
|
|
60
|
-
1. ๐ **Run `board`** - What's the current project state?
|
|
61
|
-
2. ๐ **Run `search`** - Are there related specs already?
|
|
62
|
-
3. ๐ **Check existing specs** - Is there one for this work?
|
|
63
|
-
|
|
64
|
-
### During Implementation
|
|
65
|
-
|
|
66
|
-
4. ๐ **Update status to `in-progress`** BEFORE coding
|
|
67
|
-
5. ๐ **Document decisions** in the spec as you work
|
|
68
|
-
6. ๐ **Link dependencies** if you discover blocking relationships
|
|
69
|
-
|
|
70
|
-
### After Completing Work
|
|
71
|
-
|
|
72
|
-
7. โ
**Update status to `complete`** when done
|
|
73
|
-
8. ๐ **Document what you learned** in the spec
|
|
74
|
-
9. ๐ค **Create follow-up specs** if needed
|
|
75
|
-
|
|
76
|
-
### ๐ซ Common Mistakes to Avoid
|
|
15
|
+
## ๐ง Managing Specs
|
|
16
|
+
|
|
17
|
+
### MCP Tools (Preferred) with CLI Fallback
|
|
18
|
+
|
|
19
|
+
| Action | MCP Tool | CLI Fallback |
|
|
20
|
+
|--------|----------|--------------|
|
|
21
|
+
| Project status | `board` | `lean-spec board` |
|
|
22
|
+
| List specs | `list` | `lean-spec list` |
|
|
23
|
+
| Search specs | `search` | `lean-spec search "query"` |
|
|
24
|
+
| View spec | `view` | `lean-spec view <spec>` |
|
|
25
|
+
| Create spec | `create` | `lean-spec create <name>` |
|
|
26
|
+
| Update spec | `update` | `lean-spec update <spec> --status <status>` |
|
|
27
|
+
| Link specs | `link` | `lean-spec link <spec> --depends-on <other>` |
|
|
28
|
+
| Unlink specs | `unlink` | `lean-spec unlink <spec> --depends-on <other>` |
|
|
29
|
+
| Dependencies | `deps` | `lean-spec deps <spec>` |
|
|
30
|
+
| Token count | `tokens` | `lean-spec tokens <spec>` |
|
|
31
|
+
| Validate specs | `validate` | `lean-spec validate` |
|
|
32
|
+
|
|
33
|
+
## โ ๏ธ Core Rules
|
|
34
|
+
|
|
35
|
+
| Rule | Details |
|
|
36
|
+
|------|---------|
|
|
37
|
+
| **NEVER edit frontmatter manually** | Use `update`, `link`, `unlink` for: `status`, `priority`, `tags`, `assignee`, `transitions`, timestamps, `depends_on` |
|
|
38
|
+
| **ALWAYS link spec references** | Content mentions another spec โ `lean-spec link <spec> --depends-on <other>` |
|
|
39
|
+
| **Track status transitions** | `planned` โ `in-progress` (before coding) โ `complete` (after done) |
|
|
40
|
+
| **Keep specs current** | Document progress, decisions, and learnings as work happens. Obsolete specs mislead both humans and AI |
|
|
41
|
+
| **No nested code blocks** | Use indentation instead |
|
|
42
|
+
|
|
43
|
+
### ๐ซ Common Mistakes
|
|
77
44
|
|
|
78
45
|
| โ Don't | โ
Do Instead |
|
|
79
46
|
|----------|---------------|
|
|
80
47
|
| Create spec files manually | Use `create` tool |
|
|
81
|
-
| Skip discovery
|
|
82
|
-
| Leave status as "planned"
|
|
83
|
-
| Finish work without updating spec | Document decisions, update status |
|
|
48
|
+
| Skip discovery | Run `board` and `search` first |
|
|
49
|
+
| Leave status as "planned" | Update to `in-progress` before coding |
|
|
84
50
|
| Edit frontmatter manually | Use `update` tool |
|
|
85
|
-
|
|
|
86
|
-
|
|
87
|
-
## Core Rules
|
|
88
|
-
|
|
89
|
-
1. **Read README.md first** - Understand project context
|
|
90
|
-
2. **Check specs/** - Review existing specs before starting
|
|
91
|
-
3. **Use MCP tools** - Prefer MCP over CLI when available
|
|
92
|
-
4. **Follow LeanSpec principles** - Clarity over documentation
|
|
93
|
-
5. **Keep it minimal** - If it doesn't add clarity, cut it
|
|
94
|
-
6. **NEVER manually edit frontmatter** - Use `update`, `link`, `unlink` tools
|
|
95
|
-
7. **Track progress in specs** - Update status and document decisions
|
|
51
|
+
| Complete spec without documentation | Document progress, prompts, learnings first |
|
|
96
52
|
|
|
97
|
-
##
|
|
53
|
+
## ๐ SDD Workflow
|
|
98
54
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
-
|
|
102
|
-
|
|
55
|
+
```
|
|
56
|
+
BEFORE: board โ search โ check existing specs
|
|
57
|
+
DURING: update status to in-progress โ code โ document decisions โ link dependencies
|
|
58
|
+
AFTER: document completion โ update status to complete
|
|
59
|
+
```
|
|
103
60
|
|
|
104
|
-
**
|
|
105
|
-
- Bug fixes
|
|
106
|
-
- Trivial changes
|
|
107
|
-
- Self-explanatory refactors
|
|
61
|
+
**Status tracks implementation, NOT spec writing.**
|
|
108
62
|
|
|
109
63
|
## Spec Dependencies
|
|
110
64
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
**Use when:** True blocking dependency, work order matters, one spec builds on another.
|
|
65
|
+
Use `depends_on` to express blocking relationships between specs:
|
|
66
|
+
- **`depends_on`** = True blocker, work order matters, directional (A depends on B)
|
|
114
67
|
|
|
68
|
+
Link dependencies when one spec builds on another:
|
|
115
69
|
```bash
|
|
116
70
|
lean-spec link <spec> --depends-on <other-spec>
|
|
117
71
|
```
|
|
118
72
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
## Quality Standards
|
|
73
|
+
## When to Use Specs
|
|
122
74
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
- Never leave specs with stale status
|
|
75
|
+
| โ
Write spec | โ Skip spec |
|
|
76
|
+
|---------------|--------------|
|
|
77
|
+
| Multi-part features | Bug fixes |
|
|
78
|
+
| Breaking changes | Trivial changes |
|
|
79
|
+
| Design decisions | Self-explanatory refactors |
|
|
129
80
|
|
|
130
|
-
##
|
|
81
|
+
## Token Thresholds
|
|
131
82
|
|
|
132
83
|
| Tokens | Status |
|
|
133
84
|
|--------|--------|
|
|
134
85
|
| <2,000 | โ
Optimal |
|
|
135
86
|
| 2,000-3,500 | โ
Good |
|
|
136
87
|
| 3,500-5,000 | โ ๏ธ Consider splitting |
|
|
137
|
-
| >5,000 | ๐ด
|
|
88
|
+
| >5,000 | ๐ด Must split |
|
|
89
|
+
|
|
90
|
+
## Quality Validation
|
|
91
|
+
|
|
92
|
+
Before completing work, validate spec quality:
|
|
93
|
+
```bash
|
|
94
|
+
lean-spec validate # Check structure and quality
|
|
95
|
+
lean-spec validate --check-deps # Verify dependency alignment
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Validation checks:
|
|
99
|
+
- Missing required sections
|
|
100
|
+
- Excessive length (>400 lines)
|
|
101
|
+
- Content/frontmatter dependency misalignment
|
|
102
|
+
- Invalid frontmatter fields
|
|
103
|
+
|
|
104
|
+
## First Principles (Priority Order)
|
|
138
105
|
|
|
139
|
-
|
|
106
|
+
1. **Context Economy** - <2,000 tokens optimal, >3,500 needs splitting
|
|
107
|
+
2. **Signal-to-Noise** - Every word must inform a decision
|
|
108
|
+
3. **Intent Over Implementation** - Capture why, let how emerge
|
|
109
|
+
4. **Bridge the Gap** - Both human and AI must understand
|
|
110
|
+
5. **Progressive Disclosure** - Add complexity only when pain is felt
|
|
140
111
|
|
|
141
112
|
---
|
|
142
113
|
|
|
143
|
-
**Remember:** LeanSpec tracks what you're building. Keep specs in sync with your work!
|
|
114
|
+
**Remember:** LeanSpec tracks what you're building. Keep specs in sync with your work!
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":[],"names":[],"mappings":"","file":"backfill-WYW47B42.js"}
|
package/dist/chunk-ENX5NYTE.js
DELETED
|
@@ -1,297 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs/promises';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import matter from 'gray-matter';
|
|
4
|
-
import yaml from 'js-yaml';
|
|
5
|
-
import dayjs from 'dayjs';
|
|
6
|
-
|
|
7
|
-
// src/frontmatter.ts
|
|
8
|
-
function normalizeDateFields(data) {
|
|
9
|
-
const dateFields = ["created", "completed", "updated", "due"];
|
|
10
|
-
for (const field of dateFields) {
|
|
11
|
-
if (data[field] instanceof Date) {
|
|
12
|
-
data[field] = data[field].toISOString().split("T")[0];
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
function enrichWithTimestamps(data, previousData) {
|
|
17
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
18
|
-
if (!data.created_at) {
|
|
19
|
-
data.created_at = now;
|
|
20
|
-
}
|
|
21
|
-
if (previousData) {
|
|
22
|
-
data.updated_at = now;
|
|
23
|
-
}
|
|
24
|
-
if (data.status === "complete" && previousData?.status !== "complete" && !data.completed_at) {
|
|
25
|
-
data.completed_at = now;
|
|
26
|
-
if (!data.completed) {
|
|
27
|
-
data.completed = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
if (previousData && data.status !== previousData.status) {
|
|
31
|
-
if (!Array.isArray(data.transitions)) {
|
|
32
|
-
data.transitions = [];
|
|
33
|
-
}
|
|
34
|
-
data.transitions.push({
|
|
35
|
-
status: data.status,
|
|
36
|
-
at: now
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
function normalizeTagsField(data) {
|
|
41
|
-
if (data.tags && typeof data.tags === "string") {
|
|
42
|
-
try {
|
|
43
|
-
const parsed = JSON.parse(data.tags);
|
|
44
|
-
if (Array.isArray(parsed)) {
|
|
45
|
-
data.tags = parsed;
|
|
46
|
-
}
|
|
47
|
-
} catch {
|
|
48
|
-
data.tags = data.tags.split(",").map((t) => t.trim());
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
function validateCustomField(value, expectedType) {
|
|
53
|
-
switch (expectedType) {
|
|
54
|
-
case "string":
|
|
55
|
-
if (typeof value === "string") {
|
|
56
|
-
return { valid: true, coerced: value };
|
|
57
|
-
}
|
|
58
|
-
return { valid: true, coerced: String(value) };
|
|
59
|
-
case "number":
|
|
60
|
-
if (typeof value === "number") {
|
|
61
|
-
return { valid: true, coerced: value };
|
|
62
|
-
}
|
|
63
|
-
const num = Number(value);
|
|
64
|
-
if (!isNaN(num)) {
|
|
65
|
-
return { valid: true, coerced: num };
|
|
66
|
-
}
|
|
67
|
-
return { valid: false, error: `Cannot convert '${value}' to number` };
|
|
68
|
-
case "boolean":
|
|
69
|
-
if (typeof value === "boolean") {
|
|
70
|
-
return { valid: true, coerced: value };
|
|
71
|
-
}
|
|
72
|
-
if (value === "true" || value === "yes" || value === "1") {
|
|
73
|
-
return { valid: true, coerced: true };
|
|
74
|
-
}
|
|
75
|
-
if (value === "false" || value === "no" || value === "0") {
|
|
76
|
-
return { valid: true, coerced: false };
|
|
77
|
-
}
|
|
78
|
-
return { valid: false, error: `Cannot convert '${value}' to boolean` };
|
|
79
|
-
case "array":
|
|
80
|
-
if (Array.isArray(value)) {
|
|
81
|
-
return { valid: true, coerced: value };
|
|
82
|
-
}
|
|
83
|
-
return { valid: false, error: `Expected array but got ${typeof value}` };
|
|
84
|
-
default:
|
|
85
|
-
return { valid: false, error: `Unknown type: ${expectedType}` };
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
function validateCustomFields(frontmatter, config) {
|
|
89
|
-
if (!config?.frontmatter?.custom) {
|
|
90
|
-
return frontmatter;
|
|
91
|
-
}
|
|
92
|
-
const result = { ...frontmatter };
|
|
93
|
-
for (const [fieldName, expectedType] of Object.entries(config.frontmatter.custom)) {
|
|
94
|
-
if (fieldName in result) {
|
|
95
|
-
const validation = validateCustomField(result[fieldName], expectedType);
|
|
96
|
-
if (validation.valid) {
|
|
97
|
-
result[fieldName] = validation.coerced;
|
|
98
|
-
} else {
|
|
99
|
-
console.warn(`Warning: Invalid custom field '${fieldName}': ${validation.error}`);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return result;
|
|
104
|
-
}
|
|
105
|
-
async function parseFrontmatter(filePath, config) {
|
|
106
|
-
try {
|
|
107
|
-
const content = await fs.readFile(filePath, "utf-8");
|
|
108
|
-
const parsed = matter(content, {
|
|
109
|
-
engines: {
|
|
110
|
-
yaml: (str) => yaml.load(str, { schema: yaml.FAILSAFE_SCHEMA })
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
if (!parsed.data || Object.keys(parsed.data).length === 0) {
|
|
114
|
-
return parseFallbackFields(content);
|
|
115
|
-
}
|
|
116
|
-
if (!parsed.data.status) {
|
|
117
|
-
console.warn(`Warning: Missing required field 'status' in ${filePath}`);
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
if (!parsed.data.created) {
|
|
121
|
-
console.warn(`Warning: Missing required field 'created' in ${filePath}`);
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
const validStatuses = ["planned", "in-progress", "complete", "archived"];
|
|
125
|
-
if (!validStatuses.includes(parsed.data.status)) {
|
|
126
|
-
console.warn(`Warning: Invalid status '${parsed.data.status}' in ${filePath}. Valid values: ${validStatuses.join(", ")}`);
|
|
127
|
-
}
|
|
128
|
-
if (parsed.data.priority) {
|
|
129
|
-
const validPriorities = ["low", "medium", "high", "critical"];
|
|
130
|
-
if (!validPriorities.includes(parsed.data.priority)) {
|
|
131
|
-
console.warn(`Warning: Invalid priority '${parsed.data.priority}' in ${filePath}. Valid values: ${validPriorities.join(", ")}`);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
normalizeTagsField(parsed.data);
|
|
135
|
-
const knownFields = [
|
|
136
|
-
"status",
|
|
137
|
-
"created",
|
|
138
|
-
"tags",
|
|
139
|
-
"priority",
|
|
140
|
-
"depends_on",
|
|
141
|
-
"updated",
|
|
142
|
-
"completed",
|
|
143
|
-
"assignee",
|
|
144
|
-
"reviewer",
|
|
145
|
-
"issue",
|
|
146
|
-
"pr",
|
|
147
|
-
"epic",
|
|
148
|
-
"breaking",
|
|
149
|
-
"due",
|
|
150
|
-
"created_at",
|
|
151
|
-
"updated_at",
|
|
152
|
-
"completed_at",
|
|
153
|
-
"transitions"
|
|
154
|
-
];
|
|
155
|
-
const customFields = config?.frontmatter?.custom ? Object.keys(config.frontmatter.custom) : [];
|
|
156
|
-
const allKnownFields = [...knownFields, ...customFields];
|
|
157
|
-
const unknownFields = Object.keys(parsed.data).filter((k) => !allKnownFields.includes(k));
|
|
158
|
-
if (unknownFields.length > 0) {
|
|
159
|
-
console.warn(`Info: Unknown fields in ${filePath}: ${unknownFields.join(", ")}`);
|
|
160
|
-
}
|
|
161
|
-
const validatedData = validateCustomFields(parsed.data, config);
|
|
162
|
-
return validatedData;
|
|
163
|
-
} catch (error) {
|
|
164
|
-
console.error(`Error parsing frontmatter from ${filePath}:`, error);
|
|
165
|
-
return null;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
function parseFallbackFields(content) {
|
|
169
|
-
const statusMatch = content.match(/\*\*Status\*\*:\s*(?:๐
\s*)?(\w+(?:-\w+)?)/i);
|
|
170
|
-
const createdMatch = content.match(/\*\*Created\*\*:\s*(\d{4}-\d{2}-\d{2})/);
|
|
171
|
-
if (statusMatch && createdMatch) {
|
|
172
|
-
const status = statusMatch[1].toLowerCase().replace(/\s+/g, "-");
|
|
173
|
-
const created = createdMatch[1];
|
|
174
|
-
return {
|
|
175
|
-
status,
|
|
176
|
-
created
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
async function updateFrontmatter(filePath, updates) {
|
|
182
|
-
const content = await fs.readFile(filePath, "utf-8");
|
|
183
|
-
const parsed = matter(content, {
|
|
184
|
-
engines: {
|
|
185
|
-
yaml: (str) => yaml.load(str, { schema: yaml.FAILSAFE_SCHEMA })
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
const previousData = { ...parsed.data };
|
|
189
|
-
const newData = { ...parsed.data, ...updates };
|
|
190
|
-
normalizeDateFields(newData);
|
|
191
|
-
enrichWithTimestamps(newData, previousData);
|
|
192
|
-
if (updates.status === "complete" && !newData.completed) {
|
|
193
|
-
newData.completed = dayjs().format("YYYY-MM-DD");
|
|
194
|
-
}
|
|
195
|
-
if ("updated" in parsed.data) {
|
|
196
|
-
newData.updated = dayjs().format("YYYY-MM-DD");
|
|
197
|
-
}
|
|
198
|
-
let updatedContent = parsed.content;
|
|
199
|
-
updatedContent = updateVisualMetadata(updatedContent, newData);
|
|
200
|
-
const newContent = matter.stringify(updatedContent, newData);
|
|
201
|
-
await fs.writeFile(filePath, newContent, "utf-8");
|
|
202
|
-
}
|
|
203
|
-
function updateVisualMetadata(content, frontmatter) {
|
|
204
|
-
const statusEmoji = getStatusEmojiPlain(frontmatter.status);
|
|
205
|
-
const statusLabel = frontmatter.status.charAt(0).toUpperCase() + frontmatter.status.slice(1).replace("-", " ");
|
|
206
|
-
const created = dayjs(frontmatter.created).format("YYYY-MM-DD");
|
|
207
|
-
let metadataLine = `> **Status**: ${statusEmoji} ${statusLabel}`;
|
|
208
|
-
if (frontmatter.priority) {
|
|
209
|
-
const priorityLabel = frontmatter.priority.charAt(0).toUpperCase() + frontmatter.priority.slice(1);
|
|
210
|
-
metadataLine += ` \xB7 **Priority**: ${priorityLabel}`;
|
|
211
|
-
}
|
|
212
|
-
metadataLine += ` \xB7 **Created**: ${created}`;
|
|
213
|
-
if (frontmatter.tags && frontmatter.tags.length > 0) {
|
|
214
|
-
metadataLine += ` \xB7 **Tags**: ${frontmatter.tags.join(", ")}`;
|
|
215
|
-
}
|
|
216
|
-
let secondLine = "";
|
|
217
|
-
if (frontmatter.assignee || frontmatter.reviewer) {
|
|
218
|
-
const assignee = frontmatter.assignee || "TBD";
|
|
219
|
-
const reviewer = frontmatter.reviewer || "TBD";
|
|
220
|
-
secondLine = `
|
|
221
|
-
> **Assignee**: ${assignee} \xB7 **Reviewer**: ${reviewer}`;
|
|
222
|
-
}
|
|
223
|
-
const metadataPattern = /^>\s+\*\*Status\*\*:.*(?:\n>\s+\*\*Assignee\*\*:.*)?/m;
|
|
224
|
-
if (metadataPattern.test(content)) {
|
|
225
|
-
return content.replace(metadataPattern, metadataLine + secondLine);
|
|
226
|
-
} else {
|
|
227
|
-
const titleMatch = content.match(/^#\s+.+$/m);
|
|
228
|
-
if (titleMatch) {
|
|
229
|
-
const insertPos = titleMatch.index + titleMatch[0].length;
|
|
230
|
-
return content.slice(0, insertPos) + "\n\n" + metadataLine + secondLine + "\n" + content.slice(insertPos);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
return content;
|
|
234
|
-
}
|
|
235
|
-
function getStatusEmojiPlain(status) {
|
|
236
|
-
switch (status) {
|
|
237
|
-
case "planned":
|
|
238
|
-
return "\u{1F5D3}\uFE0F";
|
|
239
|
-
case "in-progress":
|
|
240
|
-
return "\u23F3";
|
|
241
|
-
case "complete":
|
|
242
|
-
return "\u2705";
|
|
243
|
-
case "archived":
|
|
244
|
-
return "\u{1F4E6}";
|
|
245
|
-
default:
|
|
246
|
-
return "\u{1F4C4}";
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
async function getSpecFile(specDir, defaultFile = "README.md") {
|
|
250
|
-
const specFile = path.join(specDir, defaultFile);
|
|
251
|
-
try {
|
|
252
|
-
await fs.access(specFile);
|
|
253
|
-
return specFile;
|
|
254
|
-
} catch {
|
|
255
|
-
return null;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
function matchesFilter(frontmatter, filter) {
|
|
259
|
-
if (filter.status) {
|
|
260
|
-
const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];
|
|
261
|
-
if (!statuses.includes(frontmatter.status)) {
|
|
262
|
-
return false;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
if (filter.tags && filter.tags.length > 0) {
|
|
266
|
-
if (!frontmatter.tags || frontmatter.tags.length === 0) {
|
|
267
|
-
return false;
|
|
268
|
-
}
|
|
269
|
-
const hasAllTags = filter.tags.every((tag) => frontmatter.tags.includes(tag));
|
|
270
|
-
if (!hasAllTags) {
|
|
271
|
-
return false;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
if (filter.priority) {
|
|
275
|
-
const priorities = Array.isArray(filter.priority) ? filter.priority : [filter.priority];
|
|
276
|
-
if (!frontmatter.priority || !priorities.includes(frontmatter.priority)) {
|
|
277
|
-
return false;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
if (filter.assignee) {
|
|
281
|
-
if (frontmatter.assignee !== filter.assignee) {
|
|
282
|
-
return false;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
if (filter.customFields) {
|
|
286
|
-
for (const [key, value] of Object.entries(filter.customFields)) {
|
|
287
|
-
if (frontmatter[key] !== value) {
|
|
288
|
-
return false;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
return true;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
export { enrichWithTimestamps, getSpecFile, matchesFilter, normalizeDateFields, normalizeTagsField, parseFrontmatter, updateFrontmatter, validateCustomField, validateCustomFields };
|
|
296
|
-
//# sourceMappingURL=chunk-ENX5NYTE.js.map
|
|
297
|
-
//# sourceMappingURL=chunk-ENX5NYTE.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/frontmatter.ts"],"names":[],"mappings":"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","file":"chunk-ENX5NYTE.js","sourcesContent":["import * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport matter from 'gray-matter';\nimport yaml from 'js-yaml';\nimport dayjs from 'dayjs';\nimport type { LeanSpecConfig } from './config.js';\n\n// Valid status values\nexport type SpecStatus = 'planned' | 'in-progress' | 'complete' | 'archived';\n\n// Valid priority values\nexport type SpecPriority = 'low' | 'medium' | 'high' | 'critical';\n\n// Status transition record\nexport interface StatusTransition {\n status: SpecStatus;\n at: string; // ISO 8601 timestamp\n}\n\n// Core frontmatter fields\nexport interface SpecFrontmatter {\n // Required fields\n status: SpecStatus;\n created: string; // YYYY-MM-DD format\n\n // Recommended fields\n tags?: string[];\n priority?: SpecPriority;\n\n // Power user fields\n depends_on?: string[];\n updated?: string;\n completed?: string;\n assignee?: string;\n reviewer?: string;\n issue?: string;\n pr?: string;\n epic?: string;\n breaking?: boolean;\n due?: string; // YYYY-MM-DD format\n\n // Timestamp fields (for velocity tracking)\n created_at?: string; // ISO 8601 timestamp\n updated_at?: string; // ISO 8601 timestamp\n completed_at?: string; // ISO 8601 timestamp\n transitions?: StatusTransition[]; // Status change history\n\n // Allow any additional fields (for extensibility)\n [key: string]: unknown;\n}\n\n/**\n * Convert Date objects to YYYY-MM-DD string format\n * (gray-matter auto-parses YYYY-MM-DD strings as Date objects)\n */\nexport function normalizeDateFields(data: Record<string, unknown>): void {\n const dateFields = ['created', 'completed', 'updated', 'due'];\n \n for (const field of dateFields) {\n if (data[field] instanceof Date) {\n data[field] = (data[field] as Date).toISOString().split('T')[0];\n }\n }\n}\n\n/**\n * Enrich frontmatter with timestamps for velocity tracking\n * Auto-generates timestamps when missing and tracks status transitions\n */\nexport function enrichWithTimestamps(\n data: Record<string, unknown>,\n previousData?: Record<string, unknown>\n): void {\n const now = new Date().toISOString();\n\n // Set created_at if missing - always use current timestamp\n // Do NOT infer from created date field since that's just YYYY-MM-DD without time\n if (!data.created_at) {\n data.created_at = now;\n }\n\n // Update updated_at on any change (if previousData exists)\n if (previousData) {\n data.updated_at = now;\n }\n\n // Set completed_at when status changes to complete\n if (\n data.status === 'complete' &&\n previousData?.status !== 'complete' &&\n !data.completed_at\n ) {\n data.completed_at = now;\n // Also set the completed date field\n if (!data.completed) {\n data.completed = new Date().toISOString().split('T')[0];\n }\n }\n\n // Track status transition (optional)\n if (previousData && data.status !== previousData.status) {\n if (!Array.isArray(data.transitions)) {\n data.transitions = [];\n }\n (data.transitions as StatusTransition[]).push({\n status: data.status as SpecStatus,\n at: now,\n });\n }\n}\n\n/**\n * Normalize tags field - parse JSON strings into arrays\n * Handles cases where AI accidentally creates tags as '[\"..\",\"..\"]' strings\n */\nexport function normalizeTagsField(data: Record<string, unknown>): void {\n if (data.tags && typeof data.tags === 'string') {\n try {\n // Try to parse as JSON array\n const parsed = JSON.parse(data.tags as string);\n if (Array.isArray(parsed)) {\n data.tags = parsed;\n }\n } catch {\n // If not valid JSON, treat as comma-separated string\n data.tags = (data.tags as string).split(',').map(t => t.trim());\n }\n }\n}\n\n/**\n * Validate and coerce custom field types\n */\nexport function validateCustomField(\n value: unknown,\n expectedType: 'string' | 'number' | 'boolean' | 'array'\n): { valid: boolean; coerced?: unknown; error?: string } {\n switch (expectedType) {\n case 'string':\n if (typeof value === 'string') {\n return { valid: true, coerced: value };\n }\n // Coerce to string\n return { valid: true, coerced: String(value) };\n \n case 'number':\n if (typeof value === 'number') {\n return { valid: true, coerced: value };\n }\n // Try to coerce to number\n const num = Number(value);\n if (!isNaN(num)) {\n return { valid: true, coerced: num };\n }\n return { valid: false, error: `Cannot convert '${value}' to number` };\n \n case 'boolean':\n if (typeof value === 'boolean') {\n return { valid: true, coerced: value };\n }\n // Coerce string to boolean\n if (value === 'true' || value === 'yes' || value === '1') {\n return { valid: true, coerced: true };\n }\n if (value === 'false' || value === 'no' || value === '0') {\n return { valid: true, coerced: false };\n }\n return { valid: false, error: `Cannot convert '${value}' to boolean` };\n \n case 'array':\n if (Array.isArray(value)) {\n return { valid: true, coerced: value };\n }\n return { valid: false, error: `Expected array but got ${typeof value}` };\n \n default:\n return { valid: false, error: `Unknown type: ${expectedType}` };\n }\n}\n\n/**\n * Validate custom fields according to config\n */\nexport function validateCustomFields(\n frontmatter: Record<string, unknown>,\n config?: LeanSpecConfig\n): Record<string, unknown> {\n if (!config?.frontmatter?.custom) {\n return frontmatter;\n }\n \n const result = { ...frontmatter };\n \n for (const [fieldName, expectedType] of Object.entries(config.frontmatter.custom)) {\n if (fieldName in result) {\n const validation = validateCustomField(result[fieldName], expectedType);\n if (validation.valid) {\n result[fieldName] = validation.coerced;\n } else {\n console.warn(`Warning: Invalid custom field '${fieldName}': ${validation.error}`);\n }\n }\n }\n \n return result;\n}\n\n// Parse frontmatter from a spec file\nexport async function parseFrontmatter(\n filePath: string,\n config?: LeanSpecConfig\n): Promise<SpecFrontmatter | null> {\n try {\n const content = await fs.readFile(filePath, 'utf-8');\n const parsed = matter(content, {\n engines: {\n yaml: (str) => yaml.load(str, { schema: yaml.FAILSAFE_SCHEMA }) as Record<string, unknown>\n }\n });\n\n if (!parsed.data || Object.keys(parsed.data).length === 0) {\n // No frontmatter found, try fallback to inline fields\n return parseFallbackFields(content);\n }\n\n // Validate required fields\n if (!parsed.data.status) {\n console.warn(`Warning: Missing required field 'status' in ${filePath}`);\n return null;\n }\n\n if (!parsed.data.created) {\n console.warn(`Warning: Missing required field 'created' in ${filePath}`);\n return null;\n }\n\n // Validate status enum\n const validStatuses: SpecStatus[] = ['planned', 'in-progress', 'complete', 'archived'];\n if (!validStatuses.includes(parsed.data.status)) {\n console.warn(`Warning: Invalid status '${parsed.data.status}' in ${filePath}. Valid values: ${validStatuses.join(', ')}`);\n }\n\n // Validate priority enum if present\n if (parsed.data.priority) {\n const validPriorities: SpecPriority[] = ['low', 'medium', 'high', 'critical'];\n if (!validPriorities.includes(parsed.data.priority)) {\n console.warn(`Warning: Invalid priority '${parsed.data.priority}' in ${filePath}. Valid values: ${validPriorities.join(', ')}`);\n }\n }\n\n // Normalize tags field (parse JSON strings to arrays)\n normalizeTagsField(parsed.data);\n \n // Warn about unknown fields (informational only)\n const knownFields = [\n 'status', 'created', 'tags', 'priority', 'depends_on',\n 'updated', 'completed', 'assignee', 'reviewer', 'issue', 'pr', 'epic', 'breaking', 'due',\n 'created_at', 'updated_at', 'completed_at', 'transitions'\n ];\n \n // Add custom fields from config to known fields\n const customFields = config?.frontmatter?.custom ? Object.keys(config.frontmatter.custom) : [];\n const allKnownFields = [...knownFields, ...customFields];\n \n const unknownFields = Object.keys(parsed.data).filter(k => !allKnownFields.includes(k));\n if (unknownFields.length > 0) {\n console.warn(`Info: Unknown fields in ${filePath}: ${unknownFields.join(', ')}`);\n }\n \n // Validate and coerce custom fields\n const validatedData = validateCustomFields(parsed.data, config);\n\n return validatedData as SpecFrontmatter;\n } catch (error) {\n console.error(`Error parsing frontmatter from ${filePath}:`, error);\n return null;\n }\n}\n\n// Fallback: Parse inline fields from older specs\nfunction parseFallbackFields(content: string): SpecFrontmatter | null {\n const statusMatch = content.match(/\\*\\*Status\\*\\*:\\s*(?:๐
\\s*)?(\\w+(?:-\\w+)?)/i);\n const createdMatch = content.match(/\\*\\*Created\\*\\*:\\s*(\\d{4}-\\d{2}-\\d{2})/);\n\n if (statusMatch && createdMatch) {\n const status = statusMatch[1].toLowerCase().replace(/\\s+/g, '-') as SpecStatus;\n const created = createdMatch[1];\n\n return {\n status,\n created,\n };\n }\n\n return null;\n}\n\n// Update frontmatter in a spec file\nexport async function updateFrontmatter(\n filePath: string,\n updates: Partial<SpecFrontmatter>\n): Promise<void> {\n const content = await fs.readFile(filePath, 'utf-8');\n const parsed = matter(content, {\n engines: {\n yaml: (str) => yaml.load(str, { schema: yaml.FAILSAFE_SCHEMA }) as Record<string, unknown>\n }\n });\n\n // Store previous data for timestamp enrichment\n const previousData = { ...parsed.data };\n\n // Merge updates with existing data\n const newData = { ...parsed.data, ...updates };\n\n // Ensure date fields remain as strings (gray-matter auto-parses YYYY-MM-DD as Date objects)\n normalizeDateFields(newData);\n\n // Enrich with timestamps\n enrichWithTimestamps(newData, previousData);\n\n // Auto-update timestamps if fields exist (legacy behavior)\n if (updates.status === 'complete' && !newData.completed) {\n newData.completed = dayjs().format('YYYY-MM-DD');\n }\n\n if ('updated' in parsed.data) {\n newData.updated = dayjs().format('YYYY-MM-DD');\n }\n\n // Update visual metadata badges in content\n let updatedContent = parsed.content;\n updatedContent = updateVisualMetadata(updatedContent, newData as SpecFrontmatter);\n\n // Stringify back to file\n const newContent = matter.stringify(updatedContent, newData);\n await fs.writeFile(filePath, newContent, 'utf-8');\n}\n\n// Update visual metadata badges in content\nfunction updateVisualMetadata(content: string, frontmatter: SpecFrontmatter): string {\n const statusEmoji = getStatusEmojiPlain(frontmatter.status);\n const statusLabel = frontmatter.status.charAt(0).toUpperCase() + frontmatter.status.slice(1).replace('-', ' ');\n \n // Parse created date with dayjs - handles all formats consistently\n const created = dayjs(frontmatter.created).format('YYYY-MM-DD');\n \n // Build metadata line\n let metadataLine = `> **Status**: ${statusEmoji} ${statusLabel}`;\n \n if (frontmatter.priority) {\n const priorityLabel = frontmatter.priority.charAt(0).toUpperCase() + frontmatter.priority.slice(1);\n metadataLine += ` ยท **Priority**: ${priorityLabel}`;\n }\n \n metadataLine += ` ยท **Created**: ${created}`;\n \n if (frontmatter.tags && frontmatter.tags.length > 0) {\n metadataLine += ` ยท **Tags**: ${frontmatter.tags.join(', ')}`;\n }\n \n // For enterprise template with assignee/reviewer\n let secondLine = '';\n if (frontmatter.assignee || frontmatter.reviewer) {\n const assignee = frontmatter.assignee || 'TBD';\n const reviewer = frontmatter.reviewer || 'TBD';\n secondLine = `\\n> **Assignee**: ${assignee} ยท **Reviewer**: ${reviewer}`;\n }\n \n // Replace existing metadata block or add after title\n const metadataPattern = /^>\\s+\\*\\*Status\\*\\*:.*(?:\\n>\\s+\\*\\*Assignee\\*\\*:.*)?/m;\n \n if (metadataPattern.test(content)) {\n // Replace existing metadata\n return content.replace(metadataPattern, metadataLine + secondLine);\n } else {\n // Add after title (# title)\n const titleMatch = content.match(/^#\\s+.+$/m);\n if (titleMatch) {\n const insertPos = titleMatch.index! + titleMatch[0].length;\n return content.slice(0, insertPos) + '\\n\\n' + metadataLine + secondLine + '\\n' + content.slice(insertPos);\n }\n }\n \n return content;\n}\n\nfunction getStatusEmojiPlain(status: string): string {\n switch (status) {\n case 'planned': return '๐๏ธ';\n case 'in-progress': return 'โณ';\n case 'complete': return 'โ
';\n case 'archived': return '๐ฆ';\n default: return '๐';\n }\n}\n\n// Get spec file path from spec directory\nexport async function getSpecFile(specDir: string, defaultFile: string = 'README.md'): Promise<string | null> {\n const specFile = path.join(specDir, defaultFile);\n \n try {\n await fs.access(specFile);\n return specFile;\n } catch {\n return null;\n }\n}\n\n// Filter specs by criteria\nexport interface SpecFilterOptions {\n status?: SpecStatus | SpecStatus[];\n tags?: string[];\n priority?: SpecPriority | SpecPriority[];\n assignee?: string;\n customFields?: Record<string, unknown>;\n}\n\nexport function matchesFilter(frontmatter: SpecFrontmatter, filter: SpecFilterOptions): boolean {\n // Status filter\n if (filter.status) {\n const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];\n if (!statuses.includes(frontmatter.status)) {\n return false;\n }\n }\n\n // Tags filter (spec must have ALL specified tags)\n if (filter.tags && filter.tags.length > 0) {\n if (!frontmatter.tags || frontmatter.tags.length === 0) {\n return false;\n }\n const hasAllTags = filter.tags.every(tag => frontmatter.tags!.includes(tag));\n if (!hasAllTags) {\n return false;\n }\n }\n\n // Priority filter\n if (filter.priority) {\n const priorities = Array.isArray(filter.priority) ? filter.priority : [filter.priority];\n if (!frontmatter.priority || !priorities.includes(frontmatter.priority)) {\n return false;\n }\n }\n\n // Assignee filter\n if (filter.assignee) {\n if (frontmatter.assignee !== filter.assignee) {\n return false;\n }\n }\n \n // Custom fields filter\n if (filter.customFields) {\n for (const [key, value] of Object.entries(filter.customFields)) {\n if (frontmatter[key] !== value) {\n return false;\n }\n }\n }\n\n return true;\n}\n"]}
|