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 CHANGED
@@ -79,7 +79,8 @@ class LintResult {
79
79
 
80
80
  function parseYamlSimple(content) {
81
81
  const result = {};
82
- const lines = content.split('\n');
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
- const match = content.match(/^---\n([\s\S]*?)\n---/);
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-004: Frontmatter checks
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
- const productConfig = checkProductYml(productDir, productId, result);
846
+ checkProductYml(productDir, productId, result);
847
+ }
824
848
 
825
- if (productConfig) {
826
- // Lint features naming
827
- const featuresDir = path.join(productDir, 'features');
828
- if (fs.existsSync(featuresDir)) {
829
- const features = fs.readdirSync(featuresDir)
830
- .filter(f => f.endsWith('.md') && !f.toLowerCase().startsWith('readme') && f !== 'features-index.md' && f !== 'story-ledger.md');
831
-
832
- for (const feature of features) {
833
- checkArtifactNaming(path.join(featuresDir, feature), 'feature', result, workspaceDir);
834
- // MV-*: Marker vocabulary checks
835
- if (!options.rule || options.rule.startsWith('MV-')) {
836
- checkMarkerVocabulary(path.join(featuresDir, feature), result, options);
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
- // Lint regression tests naming
842
- const rtDir = path.join(productDir, 'qa', 'regression-tests');
843
- if (fs.existsSync(rtDir)) {
844
- const rtFiles = fs.readdirSync(rtDir).filter(f => f.endsWith('.md') && !f.toLowerCase().startsWith('readme'));
845
- for (const rt of rtFiles) {
846
- checkArtifactNaming(path.join(rtDir, rt), 'product-regression-test', result, workspaceDir);
847
- // MV-*: Marker vocabulary checks
848
- if (!options.rule || options.rule.startsWith('MV-')) {
849
- checkMarkerVocabulary(path.join(rtDir, rt), result, options);
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.1",
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 |
@@ -3,6 +3,7 @@
3
3
  artifact_kind: bai
4
4
  spec_version: "4.0"
5
5
  template_version: "4.0.1"
6
+ title: "{Short BAI title (20-40 chars)}"
6
7
 
7
8
  # === Ownership ===
8
9
  role_owner: BA
@@ -3,6 +3,7 @@
3
3
  artifact_kind: bug
4
4
  spec_version: "4.0"
5
5
  template_version: "4.0.1"
6
+ title: "{Short bug title (20-40 chars)}"
6
7
 
7
8
  # === Ownership ===
8
9
  role_owner: QA
@@ -3,6 +3,7 @@
3
3
  artifact_kind: ba
4
4
  spec_version: "4.0"
5
5
  template_version: "4.0.1"
6
+ title: "{Short BA title (20-40 chars)}"
6
7
 
7
8
  # === Ownership ===
8
9
  role_owner: BA
@@ -3,6 +3,7 @@
3
3
  artifact_kind: decision
4
4
  spec_version: "4.0"
5
5
  template_version: "4.0.1"
6
+ title: "{Short decision title (20-40 chars)}"
6
7
 
7
8
  # === Ownership ===
8
9
  role_owner: PO
@@ -3,6 +3,7 @@
3
3
  artifact_kind: devplan
4
4
  spec_version: "4.0"
5
5
  template_version: "4.0.1"
6
+ title: "{Short dev plan title (20-40 chars)}"
6
7
 
7
8
  # === Ownership ===
8
9
  role_owner: DEV
@@ -3,6 +3,7 @@
3
3
  artifact_kind: epic
4
4
  spec_version: "4.0"
5
5
  template_version: "4.0.1"
6
+ title: "{Short epic title (20-40 chars)}"
6
7
 
7
8
  # === Ownership ===
8
9
  role_owner: FA
@@ -3,6 +3,7 @@
3
3
  artifact_kind: fi
4
4
  spec_version: "4.0"
5
5
  template_version: "4.0.1"
6
+ title: "{Short FI title (20-40 chars)}"
6
7
 
7
8
  # === Ownership ===
8
9
  role_owner: FA
@@ -3,6 +3,7 @@
3
3
  artifact_kind: feature
4
4
  spec_version: "4.0"
5
5
  template_version: "4.0.1"
6
+ title: "{Short feature title (20-40 chars)}"
6
7
 
7
8
  # === Ownership ===
8
9
  role_owner: FA
@@ -3,6 +3,7 @@
3
3
  artifact_kind: rt
4
4
  spec_version: "4.0"
5
5
  template_version: "4.0.1"
6
+ title: "{Short regression test title (20-40 chars)}"
6
7
 
7
8
  # === Ownership ===
8
9
  role_owner: QA
@@ -3,6 +3,7 @@
3
3
  artifact_kind: sd
4
4
  spec_version: "4.0"
5
5
  template_version: "4.0.1"
6
+ title: "{Short SD title (20-40 chars)}"
6
7
 
7
8
  # === Ownership ===
8
9
  role_owner: SA
@@ -3,6 +3,7 @@
3
3
  artifact_kind: sdi
4
4
  spec_version: "4.0"
5
5
  template_version: "4.0.1"
6
+ title: "{Short SDI title (20-40 chars)}"
6
7
 
7
8
  # === Ownership ===
8
9
  role_owner: SA
@@ -3,6 +3,7 @@
3
3
  artifact_kind: story
4
4
  spec_version: "4.0"
5
5
  template_version: "4.0.1"
6
+ title: "{Short story title (20-40 chars)}"
6
7
 
7
8
  # === Ownership ===
8
9
  role_owner: FA
@@ -3,6 +3,7 @@
3
3
  artifact_kind: ta
4
4
  spec_version: "4.0"
5
5
  template_version: "4.0.1"
6
+ title: "{Short TA title (20-40 chars)}"
6
7
 
7
8
  # === Ownership ===
8
9
  role_owner: SA
@@ -3,6 +3,7 @@
3
3
  artifact_kind: tai
4
4
  spec_version: "4.0"
5
5
  template_version: "4.0.1"
6
+ title: "{Short TAI title (20-40 chars)}"
6
7
 
7
8
  # === Ownership ===
8
9
  role_owner: SA
@@ -3,6 +3,7 @@
3
3
  artifact_kind: tc
4
4
  spec_version: "4.0"
5
5
  template_version: "4.0.1"
6
+ title: "{Short test case title (20-40 chars)}"
6
7
 
7
8
  # === Ownership ===
8
9
  role_owner: QA