teamspec 4.3.1 → 4.3.2
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/lib/linter.js +54 -29
- package/package.json +2 -2
- package/teamspec-core/agents/AGENT_BOOTSTRAP.md +32 -1
- package/teamspec-core/templates/bai-template.md +1 -0
- package/teamspec-core/templates/bug-report-template.md +1 -0
- package/teamspec-core/templates/business-analysis-template.md +1 -0
- package/teamspec-core/templates/decision-log-template.md +1 -0
- package/teamspec-core/templates/dev-plan-template.md +1 -0
- package/teamspec-core/templates/epic-template.md +1 -0
- package/teamspec-core/templates/feature-increment-template.md +1 -0
- package/teamspec-core/templates/feature-template.md +1 -0
- package/teamspec-core/templates/rt-template.md +1 -0
- package/teamspec-core/templates/sd-template.md +1 -0
- package/teamspec-core/templates/sdi-template.md +1 -0
- package/teamspec-core/templates/story-template.md +1 -0
- package/teamspec-core/templates/ta-template.md +1 -0
- package/teamspec-core/templates/tai-template.md +1 -0
- package/teamspec-core/templates/tc-template.md +1 -0
package/lib/linter.js
CHANGED
|
@@ -79,7 +79,8 @@ class LintResult {
|
|
|
79
79
|
|
|
80
80
|
function parseYamlSimple(content) {
|
|
81
81
|
const result = {};
|
|
82
|
-
|
|
82
|
+
// Handle both Unix (\n) and Windows (\r\n) line endings
|
|
83
|
+
const lines = content.split(/\r?\n/);
|
|
83
84
|
|
|
84
85
|
for (const line of lines) {
|
|
85
86
|
if (!line.trim() || line.trim().startsWith('#')) continue;
|
|
@@ -111,7 +112,8 @@ function parseYamlSimple(content) {
|
|
|
111
112
|
* Parse YAML frontmatter from markdown files
|
|
112
113
|
*/
|
|
113
114
|
function parseFrontmatter(content) {
|
|
114
|
-
|
|
115
|
+
// Handle both Unix (\n) and Windows (\r\n) line endings
|
|
116
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
115
117
|
if (!match) return {};
|
|
116
118
|
return parseYamlSimple(match[1]);
|
|
117
119
|
}
|
|
@@ -301,6 +303,24 @@ function checkSpecVersion(filePath, frontmatter, result) {
|
|
|
301
303
|
}
|
|
302
304
|
}
|
|
303
305
|
|
|
306
|
+
/**
|
|
307
|
+
* MV-005: title present and valid length (20-40 characters)
|
|
308
|
+
*/
|
|
309
|
+
function checkTitle(filePath, frontmatter, result) {
|
|
310
|
+
if (!frontmatter.title) {
|
|
311
|
+
result.add('MV-005', `Missing required frontmatter field: title`, filePath, SEVERITY.ERROR);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const title = frontmatter.title.toString().trim();
|
|
316
|
+
const length = title.length;
|
|
317
|
+
if (length < 20) {
|
|
318
|
+
result.add('MV-005', `Title "${title}" is too short (${length} chars). Must be 20-40 characters.`, filePath, SEVERITY.ERROR);
|
|
319
|
+
} else if (length > 40) {
|
|
320
|
+
result.add('MV-005', `Title "${title.substring(0, 37)}..." is too long (${length} chars). Must be 20-40 characters.`, filePath, SEVERITY.ERROR);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
304
324
|
/**
|
|
305
325
|
* MV-003: role_owner present and valid
|
|
306
326
|
*/
|
|
@@ -367,7 +387,7 @@ function checkMarkerVocabulary(filePath, result, options = {}) {
|
|
|
367
387
|
// Skip files without frontmatter (not TeamSpec artifacts)
|
|
368
388
|
if (Object.keys(frontmatter).length === 0) return;
|
|
369
389
|
|
|
370
|
-
// MV-001 to MV-
|
|
390
|
+
// MV-001 to MV-005: Frontmatter checks
|
|
371
391
|
if (!options.rule || options.rule === 'MV-001') {
|
|
372
392
|
checkArtifactKind(filePath, frontmatter, result);
|
|
373
393
|
}
|
|
@@ -380,6 +400,9 @@ function checkMarkerVocabulary(filePath, result, options = {}) {
|
|
|
380
400
|
if (!options.rule || options.rule === 'MV-004') {
|
|
381
401
|
checkKeywords(filePath, frontmatter, result);
|
|
382
402
|
}
|
|
403
|
+
if (!options.rule || options.rule === 'MV-005') {
|
|
404
|
+
checkTitle(filePath, frontmatter, result);
|
|
405
|
+
}
|
|
383
406
|
|
|
384
407
|
// MV-010, MV-011: Section checks
|
|
385
408
|
if (!options.rule || options.rule === 'MV-010') {
|
|
@@ -820,35 +843,37 @@ function lintProduct(workspaceDir, productId, result, options) {
|
|
|
820
843
|
|
|
821
844
|
// TS-PROD-002: product.yml validation
|
|
822
845
|
if (!options.rule || options.rule === 'TS-PROD-002') {
|
|
823
|
-
|
|
846
|
+
checkProductYml(productDir, productId, result);
|
|
847
|
+
}
|
|
824
848
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
}
|
|
849
|
+
// Lint features (naming + marker vocabulary)
|
|
850
|
+
const featuresDir = path.join(productDir, 'features');
|
|
851
|
+
if (fs.existsSync(featuresDir)) {
|
|
852
|
+
const features = fs.readdirSync(featuresDir)
|
|
853
|
+
.filter(f => f.endsWith('.md') && !f.toLowerCase().startsWith('readme') && f !== 'features-index.md' && f !== 'story-ledger.md');
|
|
854
|
+
|
|
855
|
+
for (const feature of features) {
|
|
856
|
+
if (!options.rule || options.rule === 'TS-NAMING-001') {
|
|
857
|
+
checkArtifactNaming(path.join(featuresDir, feature), 'feature', result, workspaceDir);
|
|
858
|
+
}
|
|
859
|
+
// MV-*: Marker vocabulary checks
|
|
860
|
+
if (!options.rule || options.rule.startsWith('MV-')) {
|
|
861
|
+
checkMarkerVocabulary(path.join(featuresDir, feature), result, options);
|
|
839
862
|
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
840
865
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
866
|
+
// Lint regression tests (naming + marker vocabulary)
|
|
867
|
+
const rtDir = path.join(productDir, 'qa', 'regression-tests');
|
|
868
|
+
if (fs.existsSync(rtDir)) {
|
|
869
|
+
const rtFiles = fs.readdirSync(rtDir).filter(f => f.endsWith('.md') && !f.toLowerCase().startsWith('readme'));
|
|
870
|
+
for (const rt of rtFiles) {
|
|
871
|
+
if (!options.rule || options.rule === 'TS-NAMING-001') {
|
|
872
|
+
checkArtifactNaming(path.join(rtDir, rt), 'product-regression-test', result, workspaceDir);
|
|
873
|
+
}
|
|
874
|
+
// MV-*: Marker vocabulary checks
|
|
875
|
+
if (!options.rule || options.rule.startsWith('MV-')) {
|
|
876
|
+
checkMarkerVocabulary(path.join(rtDir, rt), result, options);
|
|
852
877
|
}
|
|
853
878
|
}
|
|
854
879
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "teamspec",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.2",
|
|
4
4
|
"description": "CLI tool to bootstrap TeamSpec 4.0 Product-Canon operating model in any repository",
|
|
5
5
|
"main": "lib/cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -56,4 +56,4 @@
|
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"yaml": "^2.8.2"
|
|
58
58
|
}
|
|
59
|
-
}
|
|
59
|
+
}
|
|
@@ -56,6 +56,36 @@ When creating or editing TeamSpec artifacts:
|
|
|
56
56
|
7. **Delta-only for stories** — Stories describe changes, NEVER full behavior
|
|
57
57
|
8. **PRX is immutable** — Never change a product's prefix after creation
|
|
58
58
|
9. **Source-lock all claims** — Every statement needs file path + section OR `{TBD}`
|
|
59
|
+
10. **Always include title** — Every artifact MUST have a `title` field in frontmatter (20-40 characters)
|
|
60
|
+
|
|
61
|
+
#### Title Generation Guidelines
|
|
62
|
+
|
|
63
|
+
All artifacts require a human-readable `title` field (20-40 characters) for display in teamspec_viewer:
|
|
64
|
+
|
|
65
|
+
**Requirements:**
|
|
66
|
+
- Length: Exactly 20-40 characters
|
|
67
|
+
- Style: Clear, descriptive noun phrases
|
|
68
|
+
- Format: Title case or sentence case
|
|
69
|
+
- Content: Captures the artifact's essence without technical jargon
|
|
70
|
+
|
|
71
|
+
**Good Examples:**
|
|
72
|
+
- "Role-Specific Dashboards" (27 chars)
|
|
73
|
+
- "OAuth Login Implementation" (27 chars)
|
|
74
|
+
- "User Authentication Flow" (25 chars)
|
|
75
|
+
- "Payment Gateway Integration" (28 chars)
|
|
76
|
+
- "Quarterly Performance Review" (29 chars)
|
|
77
|
+
|
|
78
|
+
**Bad Examples:**
|
|
79
|
+
- "Login" (5 chars — too short)
|
|
80
|
+
- "Implementation of a comprehensive user authentication and authorization system" (78 chars — too long)
|
|
81
|
+
- "f-ACME-001-user-auth" (20 chars — uses technical ID)
|
|
82
|
+
- "TODO: Add title here" (20 chars — placeholder)
|
|
83
|
+
|
|
84
|
+
**Pattern Tips:**
|
|
85
|
+
- Features: "{Capability} {Area}" (e.g., "Advanced Search Filters")
|
|
86
|
+
- Stories: "{Action} {Object}" (e.g., "Add Google OAuth Button")
|
|
87
|
+
- Epics: "{Initiative} {Scope}" (e.g., "Checkout Flow Redesign")
|
|
88
|
+
- Technical: "{Component} {Purpose}" (e.g., "API Rate Limiting Logic")
|
|
59
89
|
|
|
60
90
|
### 0.3 Artifact Quick-Lookup
|
|
61
91
|
|
|
@@ -77,6 +107,7 @@ Templates and artifacts contain YAML frontmatter with LLM-relevant metadata:
|
|
|
77
107
|
```yaml
|
|
78
108
|
---
|
|
79
109
|
artifact_kind: feature | story | epic | fi | ...
|
|
110
|
+
title: "Short human-readable title (20-40 chars)"
|
|
80
111
|
keywords: [searchable terms]
|
|
81
112
|
anti_keywords: [terms that indicate wrong artifact]
|
|
82
113
|
links_required: [mandatory relationships]
|
|
@@ -108,7 +139,7 @@ When reading or generating artifacts, recognize these standard markers:
|
|
|
108
139
|
|
|
109
140
|
| Marker Type | Examples | Purpose |
|
|
110
141
|
|-------------|----------|---------|
|
|
111
|
-
| **Frontmatter** | `artifact_kind`, `role_owner`, `keywords` | Machine-readable metadata |
|
|
142
|
+
| **Frontmatter** | `artifact_kind`, `title`, `role_owner`, `keywords` | Machine-readable metadata |
|
|
112
143
|
| **Section** | `## Purpose`, `## Scope`, `## Current Behavior` | Content boundaries |
|
|
113
144
|
| **Contract** | `> **Contract:**`, `> **Not this:**` | Section rules |
|
|
114
145
|
| **Inline** | `{TBD}`, `BR-XXX-NNN:`, `→ artifact-id` | Specific callouts |
|