gspec 1.14.0 → 1.16.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 +20 -9
- package/bin/gspec.js +218 -59
- package/commands/gspec.analyze.md +13 -8
- package/commands/gspec.audit.md +202 -0
- package/commands/gspec.implement.md +10 -7
- package/commands/gspec.migrate.md +29 -15
- package/commands/gspec.profile.md +55 -35
- package/commands/gspec.style.md +64 -12
- package/dist/antigravity/gspec-analyze/SKILL.md +14 -9
- package/dist/antigravity/gspec-architect/SKILL.md +1 -1
- package/dist/antigravity/gspec-audit/SKILL.md +206 -0
- package/dist/antigravity/gspec-feature/SKILL.md +1 -1
- package/dist/antigravity/gspec-implement/SKILL.md +11 -8
- package/dist/antigravity/gspec-migrate/SKILL.md +30 -16
- package/dist/antigravity/gspec-practices/SKILL.md +1 -1
- package/dist/antigravity/gspec-profile/SKILL.md +56 -36
- package/dist/antigravity/gspec-research/SKILL.md +1 -1
- package/dist/antigravity/gspec-stack/SKILL.md +1 -1
- package/dist/antigravity/gspec-style/SKILL.md +65 -13
- package/dist/claude/gspec-analyze/SKILL.md +14 -9
- package/dist/claude/gspec-architect/SKILL.md +1 -1
- package/dist/claude/gspec-audit/SKILL.md +207 -0
- package/dist/claude/gspec-feature/SKILL.md +1 -1
- package/dist/claude/gspec-implement/SKILL.md +11 -8
- package/dist/claude/gspec-migrate/SKILL.md +30 -16
- package/dist/claude/gspec-practices/SKILL.md +1 -1
- package/dist/claude/gspec-profile/SKILL.md +56 -36
- package/dist/claude/gspec-research/SKILL.md +1 -1
- package/dist/claude/gspec-stack/SKILL.md +1 -1
- package/dist/claude/gspec-style/SKILL.md +65 -13
- package/dist/codex/gspec-analyze/SKILL.md +14 -9
- package/dist/codex/gspec-architect/SKILL.md +1 -1
- package/dist/codex/gspec-audit/SKILL.md +206 -0
- package/dist/codex/gspec-feature/SKILL.md +1 -1
- package/dist/codex/gspec-implement/SKILL.md +11 -8
- package/dist/codex/gspec-migrate/SKILL.md +30 -16
- package/dist/codex/gspec-practices/SKILL.md +1 -1
- package/dist/codex/gspec-profile/SKILL.md +56 -36
- package/dist/codex/gspec-research/SKILL.md +1 -1
- package/dist/codex/gspec-stack/SKILL.md +1 -1
- package/dist/codex/gspec-style/SKILL.md +65 -13
- package/dist/cursor/gspec-analyze.mdc +14 -9
- package/dist/cursor/gspec-architect.mdc +1 -1
- package/dist/cursor/gspec-audit.mdc +205 -0
- package/dist/cursor/gspec-feature.mdc +1 -1
- package/dist/cursor/gspec-implement.mdc +11 -8
- package/dist/cursor/gspec-migrate.mdc +30 -16
- package/dist/cursor/gspec-practices.mdc +1 -1
- package/dist/cursor/gspec-profile.mdc +56 -36
- package/dist/cursor/gspec-research.mdc +1 -1
- package/dist/cursor/gspec-stack.mdc +1 -1
- package/dist/cursor/gspec-style.mdc +65 -13
- package/dist/opencode/gspec-analyze/SKILL.md +14 -9
- package/dist/opencode/gspec-architect/SKILL.md +1 -1
- package/dist/opencode/gspec-audit/SKILL.md +206 -0
- package/dist/opencode/gspec-feature/SKILL.md +1 -1
- package/dist/opencode/gspec-implement/SKILL.md +11 -8
- package/dist/opencode/gspec-migrate/SKILL.md +30 -16
- package/dist/opencode/gspec-practices/SKILL.md +1 -1
- package/dist/opencode/gspec-profile/SKILL.md +56 -36
- package/dist/opencode/gspec-research/SKILL.md +1 -1
- package/dist/opencode/gspec-stack/SKILL.md +1 -1
- package/dist/opencode/gspec-style/SKILL.md +65 -13
- package/package.json +1 -1
- package/templates/spec-sync.md +24 -2
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ These documents become the shared context for all subsequent AI interactions. Wh
|
|
|
25
25
|
|
|
26
26
|
The only commands you *need* are the four fundamentals and `/gspec-implement`. Everything else exists to help when your project calls for it.
|
|
27
27
|
|
|
28
|
-
The fundamentals give your AI tool enough context to build well — it knows what the product is, how it should look, what technologies to use, and what engineering standards to follow. From there, `/gspec-implement` can take a plain-language description and start building. The remaining commands — `/gspec-research`, `/gspec-feature`, `/gspec-architect`, and `/gspec-
|
|
28
|
+
The fundamentals give your AI tool enough context to build well — it knows what the product is, how it should look, what technologies to use, and what engineering standards to follow. From there, `/gspec-implement` can take a plain-language description and start building. The remaining commands — `/gspec-research`, `/gspec-feature`, `/gspec-architect`, `/gspec-analyze`, and `/gspec-audit` — add structure and rigor when the scope or complexity warrants it.
|
|
29
29
|
|
|
30
30
|
```mermaid
|
|
31
31
|
flowchart LR
|
|
@@ -42,8 +42,9 @@ flowchart LR
|
|
|
42
42
|
Architect["4. Architect
|
|
43
43
|
technical blueprint"]
|
|
44
44
|
|
|
45
|
-
Analyze["5. Analyze
|
|
46
|
-
reconcile specs
|
|
45
|
+
Analyze["5. Analyze & Audit
|
|
46
|
+
reconcile specs
|
|
47
|
+
check specs vs code"]
|
|
47
48
|
|
|
48
49
|
Build["6. Build
|
|
49
50
|
implement"]
|
|
@@ -76,7 +77,7 @@ flowchart LR
|
|
|
76
77
|
| Command | Role | What it produces |
|
|
77
78
|
|---|---|---|
|
|
78
79
|
| `/gspec-profile` | Business Strategist | Product identity, audience, value proposition, positioning |
|
|
79
|
-
| `/gspec-style` | UI/UX Designer | Visual design language, design tokens, component patterns |
|
|
80
|
+
| `/gspec-style` | UI/UX Designer | Visual design language, design tokens, component patterns. Produces either a renderable `style.html` design system or a `style.md` Markdown guide |
|
|
80
81
|
| `/gspec-stack` | Software Architect | Technology stack, frameworks, infrastructure, architecture |
|
|
81
82
|
| `/gspec-practices` | Engineering Lead | Development standards, code quality, testing, workflows |
|
|
82
83
|
|
|
@@ -104,13 +105,16 @@ Use `/gspec-feature` when you want detailed PRDs with prioritized capabilities a
|
|
|
104
105
|
|
|
105
106
|
Use `/gspec-architect` when your feature involves significant technical complexity — new data models, service boundaries, auth flows, or integration points that benefit from upfront design. It also **identifies technical gaps and ambiguities** in your specs and proposes solutions, so that `/gspec-implement` can focus on building rather than making architectural decisions. For straightforward features, `/gspec-implement` can make sound architectural decisions on its own using your `stack` and `practices` specs.
|
|
106
107
|
|
|
107
|
-
**5. Analyze** *(optional)* — Reconcile discrepancies
|
|
108
|
+
**5. Analyze & Audit** *(optional)* — Reconcile discrepancies before building, and keep specs honest as the codebase evolves.
|
|
108
109
|
|
|
109
110
|
| Command | Role | What it does |
|
|
110
111
|
|---|---|---|
|
|
111
|
-
| `/gspec-analyze` | Specification Analyst | Cross-references all specs
|
|
112
|
+
| `/gspec-analyze` | Specification Analyst | Cross-references all specs against **each other**, identifies contradictions, and walks you through reconciling each one |
|
|
113
|
+
| `/gspec-audit` | Specification Auditor | Cross-references specs against the **actual codebase**, finds drift (stack mismatches, stale data models, design tokens that don't match the stylesheet, capability checkboxes that lie), and walks you through updating specs to match reality |
|
|
112
114
|
|
|
113
|
-
Use `/gspec-analyze` after `/gspec-architect` (or any time multiple specs exist) to catch conflicts before `/gspec-implement` sees them. For example, if the stack says PostgreSQL but the architecture references MongoDB
|
|
115
|
+
Use `/gspec-analyze` after `/gspec-architect` (or any time multiple specs exist) to catch spec-to-spec conflicts before `/gspec-implement` sees them. For example, if the stack says PostgreSQL but the architecture references MongoDB.
|
|
116
|
+
|
|
117
|
+
Use `/gspec-audit` periodically — before a major release, after a long sprint, or any time you suspect docs have drifted from code. Audit reads package manifests, configs, source files, and test output, then asks you per-finding whether to update the spec to match the code, keep the spec and fix the code separately, or defer. Each finding is presented one at a time with the spec quote and the code evidence side by side. Audit never modifies code.
|
|
114
118
|
|
|
115
119
|
**6. Build** — Implement with full context.
|
|
116
120
|
|
|
@@ -120,6 +124,8 @@ Use `/gspec-analyze` after `/gspec-architect` (or any time multiple specs exist)
|
|
|
120
124
|
|
|
121
125
|
**Spec Sync** — gspec includes always-on spec sync that automatically keeps your specification documents in sync as the code evolves. This is installed alongside the skills and requires no manual intervention — when code changes affect spec-documented behavior, the sync rules prompt your AI tool to update the relevant gspec files.
|
|
122
126
|
|
|
127
|
+
**Design-tool integration** — The style guide supports both Markdown (`style.md`) and a renderable HTML design system (`style.html`) that design-aware AI tools can open, render, and reason about directly. Drop mockups from external design tools (Figma, v0, Framer AI, etc.) into `gspec/design/` and `/gspec-implement` will use them as authoritative visual guidance when building UI.
|
|
128
|
+
|
|
123
129
|
**Maintenance** — Keep specs up to date with the latest gspec format.
|
|
124
130
|
|
|
125
131
|
| Command | Role | What it does |
|
|
@@ -184,18 +190,23 @@ All specifications live in a `gspec/` directory at your project root:
|
|
|
184
190
|
project-root/
|
|
185
191
|
└── gspec/
|
|
186
192
|
├── profile.md # Product identity and positioning
|
|
187
|
-
├── style.
|
|
193
|
+
├── style.html # Visual design language (HTML — renderable design system)
|
|
194
|
+
│ # or style.md if you prefer a Markdown style guide
|
|
188
195
|
├── stack.md # Technology stack and architecture
|
|
189
196
|
├── practices.md # Development standards
|
|
190
197
|
├── architecture.md # Technical architecture blueprint
|
|
191
198
|
├── research.md # Competitive analysis and feature gaps
|
|
199
|
+
├── design/ # Optional — external mockups read during implementation
|
|
200
|
+
│ ├── dashboard.html
|
|
201
|
+
│ ├── checkout-flow.png
|
|
202
|
+
│ └── ...
|
|
192
203
|
└── features/
|
|
193
204
|
├── user-authentication.md
|
|
194
205
|
├── dashboard-analytics.md
|
|
195
206
|
└── ...
|
|
196
207
|
```
|
|
197
208
|
|
|
198
|
-
|
|
209
|
+
Most specs are Markdown. The style guide can also be a self-contained HTML file (`style.html`) that renders the design system as live swatches, typography specimens, and styled component previews — ideal for design-aware AI tools. The optional `gspec/design/` folder holds mockups (HTML, SVG, PNG, JPG) exported from external design tools like Figma, v0, or Framer AI; `/gspec-implement` reads them to reason about layout and visual intent. All files live in your repo, are version-controlled with your code, and are readable by both humans and AI tools.
|
|
199
210
|
|
|
200
211
|
## Key Design Decisions
|
|
201
212
|
|
package/bin/gspec.js
CHANGED
|
@@ -118,6 +118,17 @@ function promptConfirmNo(message) {
|
|
|
118
118
|
});
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
function promptConfirmYes(message) {
|
|
122
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
123
|
+
return new Promise((resolve) => {
|
|
124
|
+
rl.question(message, (answer) => {
|
|
125
|
+
rl.close();
|
|
126
|
+
const trimmed = answer.trim().toLowerCase();
|
|
127
|
+
resolve(trimmed === '' || trimmed.startsWith('y'));
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
121
132
|
function formatStarterName(slug) {
|
|
122
133
|
if (slug === '_none') return 'None';
|
|
123
134
|
return slug
|
|
@@ -244,11 +255,13 @@ async function seedFromSavedSpecs(cwd) {
|
|
|
244
255
|
const gspecDir = join(cwd, 'gspec');
|
|
245
256
|
const filesToWrite = [];
|
|
246
257
|
|
|
258
|
+
// `dest` is null for types whose destination filename depends on the saved file's extension
|
|
259
|
+
// (currently `styles`, which may be .md or .html).
|
|
247
260
|
const CATEGORY_ORDER = [
|
|
248
261
|
{ type: 'profiles', label: 'Select a profile', dest: 'profile.md', mode: 'single' },
|
|
249
262
|
{ type: 'practices', label: 'Select practices', dest: 'practices.md', mode: 'single' },
|
|
250
263
|
{ type: 'stacks', label: 'Select a stack', dest: 'stack.md', mode: 'single' },
|
|
251
|
-
{ type: 'styles', label: 'Select a style', dest:
|
|
264
|
+
{ type: 'styles', label: 'Select a style', dest: null, mode: 'single' },
|
|
252
265
|
{ type: 'features', label: 'Select features (optional)', dest: null, mode: 'multi' },
|
|
253
266
|
];
|
|
254
267
|
|
|
@@ -264,19 +277,24 @@ async function seedFromSavedSpecs(cwd) {
|
|
|
264
277
|
: await promptSelect(cat.label, [...specs, NONE_OPTION]);
|
|
265
278
|
|
|
266
279
|
if (selected !== '_none') {
|
|
280
|
+
const savedFilename = await resolveSavedSpecFilename(cat.type, selected);
|
|
281
|
+
if (!savedFilename) continue;
|
|
282
|
+
const destFilename = cat.dest || destFilenameForRestoredSpec(cat.type, savedFilename);
|
|
267
283
|
filesToWrite.push({
|
|
268
|
-
src: join(gspecHome, cat.type,
|
|
269
|
-
dest: join(gspecDir,
|
|
270
|
-
label: `gspec/${
|
|
284
|
+
src: join(gspecHome, cat.type, savedFilename),
|
|
285
|
+
dest: join(gspecDir, destFilename),
|
|
286
|
+
label: `gspec/${destFilename}`,
|
|
271
287
|
});
|
|
272
288
|
}
|
|
273
289
|
} else {
|
|
274
290
|
let selectedSlugs = await promptMultiSelect(cat.label, specs);
|
|
275
291
|
for (const slug of selectedSlugs) {
|
|
292
|
+
const savedFilename = await resolveSavedSpecFilename(cat.type, slug);
|
|
293
|
+
if (!savedFilename) continue;
|
|
276
294
|
filesToWrite.push({
|
|
277
|
-
src: join(gspecHome, cat.type,
|
|
278
|
-
dest: join(gspecDir, 'features',
|
|
279
|
-
label: `gspec/features/${
|
|
295
|
+
src: join(gspecHome, cat.type, savedFilename),
|
|
296
|
+
dest: join(gspecDir, 'features', savedFilename),
|
|
297
|
+
label: `gspec/features/${savedFilename}`,
|
|
280
298
|
});
|
|
281
299
|
}
|
|
282
300
|
}
|
|
@@ -550,6 +568,11 @@ const MIGRATE_COMMANDS = {
|
|
|
550
568
|
};
|
|
551
569
|
|
|
552
570
|
function parseSpecVersion(content) {
|
|
571
|
+
// HTML spec files store the version as a first-line comment:
|
|
572
|
+
// <!-- spec-version: v1 -->
|
|
573
|
+
const htmlMatch = content.match(/^\s*<!--\s*spec-version:\s*([^\s-][^-]*?)\s*-->/);
|
|
574
|
+
if (htmlMatch) return htmlMatch[1].trim();
|
|
575
|
+
|
|
553
576
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
554
577
|
if (!match) return null;
|
|
555
578
|
const newMatch = match[1].match(/^spec-version:\s*(.+)$/m);
|
|
@@ -567,6 +590,11 @@ async function collectGspecFiles(gspecDir) {
|
|
|
567
590
|
if (entry.endsWith('.md') && entry.toLowerCase() !== 'readme.md') {
|
|
568
591
|
files.push({ path: join(gspecDir, entry), label: `gspec/${entry}` });
|
|
569
592
|
}
|
|
593
|
+
// Pick up style.html (the HTML-format style guide) alongside Markdown specs.
|
|
594
|
+
// Other .html files under gspec/ are not gspec-owned and are skipped.
|
|
595
|
+
if (entry === 'style.html') {
|
|
596
|
+
files.push({ path: join(gspecDir, entry), label: `gspec/${entry}` });
|
|
597
|
+
}
|
|
570
598
|
}
|
|
571
599
|
|
|
572
600
|
for (const subdir of ['features', 'epics']) {
|
|
@@ -637,19 +665,82 @@ const GSPEC_TYPE_MAP = {
|
|
|
637
665
|
'profile.md': 'profiles',
|
|
638
666
|
'stack.md': 'stacks',
|
|
639
667
|
'style.md': 'styles',
|
|
668
|
+
'style.html': 'styles',
|
|
640
669
|
'practices.md': 'practices',
|
|
641
670
|
};
|
|
642
671
|
|
|
643
672
|
// Reverse: restore type folder → gspec/ destination filename
|
|
673
|
+
// The `styles` entry is a function because the destination depends on the saved file's extension.
|
|
644
674
|
const RESTORE_DEST_MAP = {
|
|
645
675
|
profiles: 'profile.md',
|
|
646
676
|
stacks: 'stack.md',
|
|
647
|
-
styles: 'style.md',
|
|
677
|
+
styles: 'style.md', // default when the saved extension is .md
|
|
648
678
|
practices: 'practices.md',
|
|
649
679
|
features: null, // features keep their own filename
|
|
650
680
|
};
|
|
651
681
|
|
|
682
|
+
// Given a save-type folder and a saved slug, resolve the actual filename in ~/.gspec/<type>/.
|
|
683
|
+
// Styles can be stored as .md or .html; all others are .md.
|
|
684
|
+
async function resolveSavedSpecFilename(type, slug) {
|
|
685
|
+
const dir = join(GSPEC_HOME, type);
|
|
686
|
+
const candidates = type === 'styles'
|
|
687
|
+
? [`${slug}.md`, `${slug}.html`]
|
|
688
|
+
: [`${slug}.md`];
|
|
689
|
+
for (const candidate of candidates) {
|
|
690
|
+
try {
|
|
691
|
+
await stat(join(dir, candidate));
|
|
692
|
+
return candidate;
|
|
693
|
+
} catch (e) {
|
|
694
|
+
if (e.code !== 'ENOENT') throw e;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Destination filename in a project's gspec/ directory for a restored saved spec.
|
|
701
|
+
// For styles, preserve the saved file's extension so a .html style guide restores as style.html.
|
|
702
|
+
function destFilenameForRestoredSpec(type, savedFilename) {
|
|
703
|
+
if (type === 'features') return savedFilename;
|
|
704
|
+
if (type === 'styles') {
|
|
705
|
+
return savedFilename.endsWith('.html') ? 'style.html' : 'style.md';
|
|
706
|
+
}
|
|
707
|
+
return RESTORE_DEST_MAP[type];
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function isHtmlSpec(content) {
|
|
711
|
+
const head = content.slice(0, 500).trimStart().toLowerCase();
|
|
712
|
+
if (head.startsWith('<!doctype') || head.startsWith('<html')) return true;
|
|
713
|
+
// Leading HTML comments before <!DOCTYPE> (where we store HTML spec metadata)
|
|
714
|
+
if (head.startsWith('<!--')) {
|
|
715
|
+
// Peek further to see if a <!DOCTYPE> / <html> follows the comments
|
|
716
|
+
const scan = content.slice(0, 2000).toLowerCase();
|
|
717
|
+
return /<!doctype|<html/.test(scan);
|
|
718
|
+
}
|
|
719
|
+
return false;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function parseHtmlMetadata(content) {
|
|
723
|
+
// Consume consecutive `<!-- key: value -->` comments at the top of the file,
|
|
724
|
+
// stopping at the first non-comment, non-blank line.
|
|
725
|
+
const fields = {};
|
|
726
|
+
const lines = content.split('\n');
|
|
727
|
+
let bodyStart = 0;
|
|
728
|
+
for (let i = 0; i < lines.length; i++) {
|
|
729
|
+
const trimmed = lines[i].trim();
|
|
730
|
+
if (trimmed === '') {
|
|
731
|
+
bodyStart = i + 1;
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
const match = trimmed.match(/^<!--\s*([\w-]+):\s*(.+?)\s*-->$/);
|
|
735
|
+
if (!match) break;
|
|
736
|
+
fields[match[1]] = match[2];
|
|
737
|
+
bodyStart = i + 1;
|
|
738
|
+
}
|
|
739
|
+
return { fields, body: lines.slice(bodyStart).join('\n') };
|
|
740
|
+
}
|
|
741
|
+
|
|
652
742
|
function parseFrontmatter(content) {
|
|
743
|
+
if (isHtmlSpec(content)) return parseHtmlMetadata(content);
|
|
653
744
|
const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
654
745
|
if (!match) return { fields: {}, body: content };
|
|
655
746
|
const fields = {};
|
|
@@ -662,7 +753,34 @@ function parseFrontmatter(content) {
|
|
|
662
753
|
return { fields, body: content.slice(match[0].length) };
|
|
663
754
|
}
|
|
664
755
|
|
|
756
|
+
function setHtmlMetadataField(content, key, value) {
|
|
757
|
+
const lines = content.split('\n');
|
|
758
|
+
let lastCommentIndex = -1;
|
|
759
|
+
for (let i = 0; i < lines.length; i++) {
|
|
760
|
+
const trimmed = lines[i].trim();
|
|
761
|
+
if (trimmed === '') continue;
|
|
762
|
+
if (trimmed.startsWith('<!--') && trimmed.endsWith('-->')) {
|
|
763
|
+
lastCommentIndex = i;
|
|
764
|
+
const m = trimmed.match(/^<!--\s*([\w-]+):\s*(.+?)\s*-->$/);
|
|
765
|
+
if (m && m[1] === key) {
|
|
766
|
+
lines[i] = `<!-- ${key}: ${value} -->`;
|
|
767
|
+
return lines.join('\n');
|
|
768
|
+
}
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
break;
|
|
772
|
+
}
|
|
773
|
+
const newComment = `<!-- ${key}: ${value} -->`;
|
|
774
|
+
if (lastCommentIndex >= 0) {
|
|
775
|
+
lines.splice(lastCommentIndex + 1, 0, newComment);
|
|
776
|
+
} else {
|
|
777
|
+
lines.unshift(newComment);
|
|
778
|
+
}
|
|
779
|
+
return lines.join('\n');
|
|
780
|
+
}
|
|
781
|
+
|
|
665
782
|
function setFrontmatterField(content, key, value) {
|
|
783
|
+
if (isHtmlSpec(content)) return setHtmlMetadataField(content, key, value);
|
|
666
784
|
const match = content.match(/^(---\s*\n)([\s\S]*?)(\n---)/);
|
|
667
785
|
if (!match) {
|
|
668
786
|
// No frontmatter — create one
|
|
@@ -700,10 +818,11 @@ async function collectSavableFiles(cwd) {
|
|
|
700
818
|
throw e;
|
|
701
819
|
}
|
|
702
820
|
|
|
703
|
-
// Top-level spec files
|
|
821
|
+
// Top-level spec files. Accept the Markdown specs plus the HTML style guide.
|
|
704
822
|
const topEntries = await readdir(gspecDir);
|
|
705
823
|
for (const entry of topEntries) {
|
|
706
|
-
if (
|
|
824
|
+
if (entry.toLowerCase() === 'readme.md') continue;
|
|
825
|
+
if (!entry.endsWith('.md') && entry !== 'style.html') continue;
|
|
707
826
|
const type = GSPEC_TYPE_MAP[entry];
|
|
708
827
|
if (!type) continue;
|
|
709
828
|
files.push({
|
|
@@ -758,20 +877,53 @@ async function saveSpec(cwd) {
|
|
|
758
877
|
}
|
|
759
878
|
|
|
760
879
|
const selected = files[num - 1];
|
|
880
|
+
// Preserve the source file's extension when saving (.md for most specs, .html for style.html).
|
|
881
|
+
const ext = selected.path.endsWith('.html') ? '.html' : '.md';
|
|
761
882
|
|
|
762
|
-
//
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
883
|
+
// Read source content and look for an existing name in frontmatter
|
|
884
|
+
let content = await readFile(selected.path, 'utf-8');
|
|
885
|
+
const { fields: sourceFields } = parseFrontmatter(content);
|
|
886
|
+
const existingName = sourceFields.name;
|
|
887
|
+
|
|
888
|
+
let name;
|
|
889
|
+
let overwriteConfirmed = false;
|
|
890
|
+
|
|
891
|
+
if (existingName) {
|
|
892
|
+
const existingPath = join(GSPEC_HOME, selected.type, `${existingName}${ext}`);
|
|
893
|
+
let savedExists = false;
|
|
894
|
+
try {
|
|
895
|
+
await stat(existingPath);
|
|
896
|
+
savedExists = true;
|
|
897
|
+
} catch (e) {
|
|
898
|
+
if (e.code !== 'ENOENT') throw e;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
if (savedExists) {
|
|
902
|
+
const overwrite = await promptConfirmYes(
|
|
903
|
+
chalk.bold(`\n Overwrite existing ~/.gspec/${selected.type}/${existingName}${ext}? [Y/n]: `)
|
|
904
|
+
);
|
|
905
|
+
if (overwrite) {
|
|
906
|
+
name = existingName;
|
|
907
|
+
overwriteConfirmed = true;
|
|
908
|
+
}
|
|
909
|
+
} else {
|
|
910
|
+
name = existingName;
|
|
911
|
+
}
|
|
767
912
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
913
|
+
|
|
914
|
+
if (!name) {
|
|
915
|
+
const answered = await promptInput(chalk.bold('\n Save name (no spaces, e.g. my-saas-stack): '));
|
|
916
|
+
if (!answered) {
|
|
917
|
+
console.error(chalk.red('\n Name is required.'));
|
|
918
|
+
process.exit(1);
|
|
919
|
+
}
|
|
920
|
+
if (/\s/.test(answered)) {
|
|
921
|
+
console.error(chalk.red('\n Name cannot contain spaces. Use hyphens instead (e.g. my-saas-stack).'));
|
|
922
|
+
process.exit(1);
|
|
923
|
+
}
|
|
924
|
+
name = answered;
|
|
771
925
|
}
|
|
772
926
|
|
|
773
|
-
// Read content and update frontmatter with name
|
|
774
|
-
let content = await readFile(selected.path, 'utf-8');
|
|
775
927
|
content = setFrontmatterField(content, 'name', name);
|
|
776
928
|
|
|
777
929
|
// Ensure description exists
|
|
@@ -783,28 +935,36 @@ async function saveSpec(cwd) {
|
|
|
783
935
|
}
|
|
784
936
|
}
|
|
785
937
|
|
|
786
|
-
// Write to ~/.gspec/{type}/{name}
|
|
938
|
+
// Write to ~/.gspec/{type}/{name}{ext}
|
|
787
939
|
const destDir = join(GSPEC_HOME, selected.type);
|
|
788
|
-
const destPath = join(destDir, `${name}
|
|
940
|
+
const destPath = join(destDir, `${name}${ext}`);
|
|
789
941
|
await mkdir(destDir, { recursive: true });
|
|
790
942
|
|
|
791
|
-
// Check
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
943
|
+
// Check for conflict unless overwrite was already confirmed above
|
|
944
|
+
if (!overwriteConfirmed) {
|
|
945
|
+
try {
|
|
946
|
+
await stat(destPath);
|
|
947
|
+
const overwrite = await promptConfirm(chalk.yellow(`\n ${selected.type}/${name}${ext} already exists. Overwrite? [y/N]: `));
|
|
948
|
+
if (!overwrite) {
|
|
949
|
+
console.log(chalk.dim('\n Save cancelled.\n'));
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
} catch (e) {
|
|
953
|
+
if (e.code !== 'ENOENT') throw e;
|
|
798
954
|
}
|
|
799
|
-
} catch (e) {
|
|
800
|
-
if (e.code !== 'ENOENT') throw e;
|
|
801
955
|
}
|
|
802
956
|
|
|
803
957
|
// Uncheck all implementation checkboxes so saved specs start fresh
|
|
804
958
|
content = content.replace(/- \[x\]/g, '- [ ]');
|
|
805
959
|
|
|
806
960
|
await writeFile(destPath, content, 'utf-8');
|
|
807
|
-
console.log(chalk.green(`\n ✓ Saved to ~/.gspec/${selected.type}/${name}
|
|
961
|
+
console.log(chalk.green(`\n ✓ Saved to ~/.gspec/${selected.type}/${name}${ext}\n`));
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function isSavedSpecFile(type, filename) {
|
|
965
|
+
if (filename.endsWith('.md')) return true;
|
|
966
|
+
if (type === 'styles' && filename.endsWith('.html')) return true;
|
|
967
|
+
return false;
|
|
808
968
|
}
|
|
809
969
|
|
|
810
970
|
async function listSavedTypes() {
|
|
@@ -816,8 +976,7 @@ async function listSavedTypes() {
|
|
|
816
976
|
const info = await stat(join(GSPEC_HOME, entry));
|
|
817
977
|
if (info.isDirectory()) {
|
|
818
978
|
const files = await readdir(join(GSPEC_HOME, entry));
|
|
819
|
-
|
|
820
|
-
if (mdFiles.length > 0) types.push(entry);
|
|
979
|
+
if (files.some((f) => isSavedSpecFile(entry, f))) types.push(entry);
|
|
821
980
|
}
|
|
822
981
|
} catch { /* skip */ }
|
|
823
982
|
}
|
|
@@ -833,11 +992,11 @@ async function listSavedSpecs(type) {
|
|
|
833
992
|
const entries = await readdir(dir);
|
|
834
993
|
const specs = [];
|
|
835
994
|
for (const entry of entries) {
|
|
836
|
-
if (!entry
|
|
995
|
+
if (!isSavedSpecFile(type, entry)) continue;
|
|
837
996
|
const content = await readFile(join(dir, entry), 'utf-8');
|
|
838
997
|
const { fields } = parseFrontmatter(content);
|
|
839
998
|
specs.push({
|
|
840
|
-
slug: entry.replace(/\.md$/, ''),
|
|
999
|
+
slug: entry.replace(/\.(md|html)$/, ''),
|
|
841
1000
|
description: fields.description || '',
|
|
842
1001
|
});
|
|
843
1002
|
}
|
|
@@ -910,25 +1069,20 @@ async function restoreSpec(specPath, cwd) {
|
|
|
910
1069
|
}
|
|
911
1070
|
|
|
912
1071
|
async function restoreFile(type, name, cwd) {
|
|
913
|
-
const
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
} catch (e) {
|
|
918
|
-
if (e.code === 'ENOENT') {
|
|
919
|
-
console.error(chalk.red(`\n Not found: ~/.gspec/${type}/${name}.md\n`));
|
|
920
|
-
process.exit(1);
|
|
921
|
-
}
|
|
922
|
-
throw e;
|
|
1072
|
+
const savedFilename = await resolveSavedSpecFilename(type, name);
|
|
1073
|
+
if (!savedFilename) {
|
|
1074
|
+
console.error(chalk.red(`\n Not found: ~/.gspec/${type}/${name}.md\n`));
|
|
1075
|
+
process.exit(1);
|
|
923
1076
|
}
|
|
1077
|
+
const srcPath = join(GSPEC_HOME, type, savedFilename);
|
|
924
1078
|
|
|
925
1079
|
const gspecDir = join(cwd, 'gspec');
|
|
926
1080
|
let destPath;
|
|
927
1081
|
|
|
928
1082
|
if (type === 'features') {
|
|
929
|
-
destPath = join(gspecDir, 'features',
|
|
1083
|
+
destPath = join(gspecDir, 'features', savedFilename);
|
|
930
1084
|
} else {
|
|
931
|
-
const destFile =
|
|
1085
|
+
const destFile = destFilenameForRestoredSpec(type, savedFilename);
|
|
932
1086
|
if (!destFile) {
|
|
933
1087
|
console.error(chalk.red(`\n Unknown spec type: ${type}\n`));
|
|
934
1088
|
process.exit(1);
|
|
@@ -1157,10 +1311,18 @@ async function restorePlaybook(name, cwd) {
|
|
|
1157
1311
|
restorations.push({ type: 'features', slug: f });
|
|
1158
1312
|
}
|
|
1159
1313
|
|
|
1314
|
+
// Resolve each restoration to its actual saved filename (styles may be .md or .html)
|
|
1315
|
+
for (const r of restorations) {
|
|
1316
|
+
r.savedFilename = await resolveSavedSpecFilename(r.type, r.slug);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1160
1319
|
// Check for existing files
|
|
1161
1320
|
const existing = [];
|
|
1162
1321
|
for (const r of restorations) {
|
|
1163
|
-
|
|
1322
|
+
if (!r.savedFilename) continue;
|
|
1323
|
+
const destFile = r.type === 'features'
|
|
1324
|
+
? join('features', r.savedFilename)
|
|
1325
|
+
: destFilenameForRestoredSpec(r.type, r.savedFilename);
|
|
1164
1326
|
const destPath = join(gspecDir, destFile);
|
|
1165
1327
|
try {
|
|
1166
1328
|
await stat(destPath);
|
|
@@ -1186,18 +1348,15 @@ async function restorePlaybook(name, cwd) {
|
|
|
1186
1348
|
// Restore all specs
|
|
1187
1349
|
const outdated = [];
|
|
1188
1350
|
for (const r of restorations) {
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
} catch (e) {
|
|
1193
|
-
if (e.code === 'ENOENT') {
|
|
1194
|
-
console.log(` ${chalk.yellow('!')} Skipped ${r.type}/${r.slug} — not found in ~/.gspec/`);
|
|
1195
|
-
continue;
|
|
1196
|
-
}
|
|
1197
|
-
throw e;
|
|
1351
|
+
if (!r.savedFilename) {
|
|
1352
|
+
console.log(` ${chalk.yellow('!')} Skipped ${r.type}/${r.slug} — not found in ~/.gspec/`);
|
|
1353
|
+
continue;
|
|
1198
1354
|
}
|
|
1355
|
+
const srcFile = join(GSPEC_HOME, r.type, r.savedFilename);
|
|
1199
1356
|
|
|
1200
|
-
const destFile = r.type === 'features'
|
|
1357
|
+
const destFile = r.type === 'features'
|
|
1358
|
+
? join('features', r.savedFilename)
|
|
1359
|
+
: destFilenameForRestoredSpec(r.type, r.savedFilename);
|
|
1201
1360
|
const destPath = join(gspecDir, destFile);
|
|
1202
1361
|
await mkdir(dirname(destPath), { recursive: true });
|
|
1203
1362
|
const specContent = await readFile(srcFile, 'utf-8');
|
|
@@ -4,6 +4,8 @@ Your task is to read all existing gspec specification documents, identify discre
|
|
|
4
4
|
|
|
5
5
|
This command is designed to be run **after** `gspec-architect` (or at any point when multiple specs exist) and **before** `gspec-implement`, to ensure the implementing agent receives a coherent, conflict-free set of instructions.
|
|
6
6
|
|
|
7
|
+
> **Analyze vs. audit.** `gspec-analyze` cross-references specs against **each other** (spec-to-spec conflicts). `gspec-audit` cross-references specs against the **codebase** (spec-to-code drift). If the user's intent is "do my docs still reflect what the code does?", route to `gspec-audit` instead.
|
|
8
|
+
|
|
7
9
|
You should:
|
|
8
10
|
- Read and deeply cross-reference all available gspec documents
|
|
9
11
|
- Identify concrete discrepancies — not style differences or minor wording variations, but substantive contradictions where two specs disagree on a fact, technology, behavior, or requirement
|
|
@@ -23,11 +25,12 @@ Read **every** available gspec document in this order:
|
|
|
23
25
|
|
|
24
26
|
1. `gspec/profile.md` — Product identity, scope, audience, and positioning
|
|
25
27
|
2. `gspec/stack.md` — Technology choices, frameworks, infrastructure
|
|
26
|
-
3. `gspec/style.md` — Visual design language, tokens, component styling
|
|
27
|
-
4. `gspec/
|
|
28
|
-
5. `gspec/
|
|
29
|
-
6. `gspec/
|
|
30
|
-
7. `gspec/
|
|
28
|
+
3. `gspec/style.md` **or** `gspec/style.html` — Visual design language, tokens, component styling. Read whichever exists; read both if both are present. For an HTML style guide, the canonical token values are the CSS custom properties defined in the `<style>` block — inspect those when cross-referencing token-related claims
|
|
29
|
+
4. `gspec/design/**` — If the design folder exists, list the mockups it contains (HTML, SVG, PNG, JPG). You do not need to deeply parse images, but note which screens or flows have mockups so you can flag features that reference a screen lacking a mockup, or mockups that depict behavior contradicted by a feature PRD
|
|
30
|
+
5. `gspec/practices.md` — Development standards, testing, conventions
|
|
31
|
+
6. `gspec/architecture.md` — Technical blueprint: project structure, data model, API design, environment
|
|
32
|
+
7. `gspec/research.md` — Competitive analysis and feature proposals
|
|
33
|
+
8. `gspec/features/*.md` — Individual feature requirements and dependencies
|
|
31
34
|
|
|
32
35
|
If fewer than two spec files exist, inform the user that there is nothing to cross-reference and stop.
|
|
33
36
|
|
|
@@ -53,8 +56,10 @@ Systematically compare specs against each other. Look for these categories of di
|
|
|
53
56
|
- Authentication or authorization requirements differ between specs
|
|
54
57
|
|
|
55
58
|
#### Design & Style Conflicts
|
|
56
|
-
- A feature PRD references visual patterns or components that contradict `style.md`
|
|
57
|
-
- Architecture's component structure doesn't align with the design system in
|
|
59
|
+
- A feature PRD references visual patterns or components that contradict the style guide (`style.md` or `style.html`)
|
|
60
|
+
- Architecture's component structure doesn't align with the design system in the style guide
|
|
61
|
+
- A mockup in `gspec/design/` depicts a layout, color, or component treatment that contradicts the style guide's tokens or patterns
|
|
62
|
+
- A feature PRD describes a screen that has a mockup in `gspec/design/`, but the PRD and mockup disagree on behavior or composition
|
|
58
63
|
|
|
59
64
|
#### Practice & Convention Conflicts
|
|
60
65
|
- Architecture's file naming, testing approach, or code organization contradicts `practices.md`
|
|
@@ -123,7 +128,7 @@ When updating specs to resolve a discrepancy:
|
|
|
123
128
|
|
|
124
129
|
- **Surgical updates only** — change the minimum text needed to resolve the conflict
|
|
125
130
|
- **Preserve format and tone** — match the existing document's style, heading structure, and voice
|
|
126
|
-
- **Preserve `spec-version`
|
|
131
|
+
- **Preserve `spec-version` metadata** — do not alter or remove it. For Markdown files this is YAML frontmatter (`---\nspec-version: ...\n---`); for HTML style guides it is the first-line comment (`<!-- spec-version: ... -->`). Both must be left intact.
|
|
127
132
|
- **Do not rewrite sections** — if a one-line change resolves the conflict, make a one-line change
|
|
128
133
|
- **Do not add changelog annotations** — the git history captures what changed
|
|
129
134
|
|