@ruchit07/ai-spec 1.0.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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -0
  3. package/dist/__tests__/generator.test.d.ts +2 -0
  4. package/dist/__tests__/generator.test.d.ts.map +1 -0
  5. package/dist/__tests__/generator.test.js +67 -0
  6. package/dist/__tests__/generator.test.js.map +1 -0
  7. package/dist/__tests__/writer.test.d.ts +2 -0
  8. package/dist/__tests__/writer.test.d.ts.map +1 -0
  9. package/dist/__tests__/writer.test.js +34 -0
  10. package/dist/__tests__/writer.test.js.map +1 -0
  11. package/dist/cli.d.ts +3 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +37 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/commands/init.d.ts +10 -0
  16. package/dist/commands/init.d.ts.map +1 -0
  17. package/dist/commands/init.js +141 -0
  18. package/dist/commands/init.js.map +1 -0
  19. package/dist/index.d.ts +6 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +6 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/lib/generator.d.ts +7 -0
  24. package/dist/lib/generator.d.ts.map +1 -0
  25. package/dist/lib/generator.js +18 -0
  26. package/dist/lib/generator.js.map +1 -0
  27. package/dist/lib/slug.d.ts +11 -0
  28. package/dist/lib/slug.d.ts.map +1 -0
  29. package/dist/lib/slug.js +25 -0
  30. package/dist/lib/slug.js.map +1 -0
  31. package/dist/lib/types.d.ts +35 -0
  32. package/dist/lib/types.d.ts.map +1 -0
  33. package/dist/lib/types.js +8 -0
  34. package/dist/lib/types.js.map +1 -0
  35. package/dist/lib/writer.d.ts +11 -0
  36. package/dist/lib/writer.d.ts.map +1 -0
  37. package/dist/lib/writer.js +23 -0
  38. package/dist/lib/writer.js.map +1 -0
  39. package/dist/templates/adr.d.ts +3 -0
  40. package/dist/templates/adr.d.ts.map +1 -0
  41. package/dist/templates/adr.js +54 -0
  42. package/dist/templates/adr.js.map +1 -0
  43. package/dist/templates/eval-criteria.d.ts +4 -0
  44. package/dist/templates/eval-criteria.d.ts.map +1 -0
  45. package/dist/templates/eval-criteria.js +67 -0
  46. package/dist/templates/eval-criteria.js.map +1 -0
  47. package/dist/templates/spec.d.ts +3 -0
  48. package/dist/templates/spec.d.ts.map +1 -0
  49. package/dist/templates/spec.js +93 -0
  50. package/dist/templates/spec.js.map +1 -0
  51. package/dist/templates/test-cases.d.ts +8 -0
  52. package/dist/templates/test-cases.d.ts.map +1 -0
  53. package/dist/templates/test-cases.js +81 -0
  54. package/dist/templates/test-cases.js.map +1 -0
  55. package/package.json +59 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ruchit Suthar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,99 @@
1
+ # ai-spec
2
+
3
+ > Scaffold a production-grade AI feature spec **before** you write a line of code.
4
+
5
+ Most AI features are vibe-coded: call the LLM, the output looks reasonable, ship. Then three weeks later a prompt change silently breaks 20% of queries — and nobody knows, because "looks reasonable" was never a measurable criterion.
6
+
7
+ `ai-spec` fixes the root cause. One command generates the spec, the eval criteria, a seed set of golden test cases, and an ADR starter — so "done" has a precise definition before you start.
8
+
9
+ **By [Ruchit Suthar](https://ruchitsuthar.com)** — Software Architect & Technical Leader.
10
+ 📖 Method: [AI-Driven Development: The Spec-First Workflow](https://ruchitsuthar.com/blog/developer-productivity/ai-driven-development-spec-first-workflow/)
11
+
12
+ ---
13
+
14
+ ## Quick start
15
+
16
+ ```bash
17
+ npx @ruchit07/ai-spec init "semantic search for support tickets"
18
+ ```
19
+
20
+ That's it. You'll be prompted for the feature kind, problem statement, latency/cost budgets, and owner — then it writes:
21
+
22
+ ```
23
+ specs/
24
+ └── semantic-search-for-support-tickets/
25
+ ├── spec.md # Problem, I/O contract, acceptance criteria, failure modes
26
+ ├── eval-criteria.md # The metrics that gate CI, with threshold rationale
27
+ ├── eval-criteria.json # Machine-readable thresholds for your CI
28
+ ├── test-cases.json # Seed golden test cases tailored to the feature kind
29
+ └── adr.md # Architecture Decision Record starter
30
+ ```
31
+
32
+ ### Non-interactive (for scripts / CI)
33
+
34
+ ```bash
35
+ ai-spec init "ticket classifier" --kind classification --yes
36
+ ai-spec init "support agent" -k agent -d ./ai-features --force
37
+ ```
38
+
39
+ ---
40
+
41
+ ## Why this matters
42
+
43
+ | Without a spec | With ai-spec |
44
+ |----------------|--------------|
45
+ | "Looks good" is the bar | Measurable thresholds (accuracy ≥ 0.8, latency p95 ≤ 2000ms) |
46
+ | Regressions found by users | Regressions caught in CI |
47
+ | Prompt is the only documentation | Spec + ADR explain intent and decisions |
48
+ | No baseline when you switch models | Golden test set is the baseline |
49
+
50
+ The discipline is the value. `ai-spec` makes the disciplined path the easy path.
51
+
52
+ ---
53
+
54
+ ## Feature kinds
55
+
56
+ The seed test cases and spec guidance adapt to what you're building:
57
+
58
+ | Kind | Tailored guidance |
59
+ |------|-------------------|
60
+ | `rag` | Retrieval quality + groundedness as the dominant metric |
61
+ | `chat` | Conversation scope, tools, context-window budget |
62
+ | `classification` | Exact label set, out-of-distribution handling |
63
+ | `extraction` | Typed output schema, field-level accuracy |
64
+ | `agent` | Action space, stopping conditions, safety guardrails |
65
+
66
+ ---
67
+
68
+ ## Programmatic API
69
+
70
+ ```typescript
71
+ import { generateFiles, slugify } from '@ruchit07/ai-spec';
72
+
73
+ const files = generateFiles({
74
+ featureName: 'My Feature',
75
+ slug: slugify('My Feature'),
76
+ kind: 'rag',
77
+ problem: '...',
78
+ primaryProvider: 'openai',
79
+ latencyP95Ms: 2000,
80
+ costPerQueryUsd: 0.005,
81
+ accuracyThreshold: 0.8,
82
+ groundednessThreshold: 0.85,
83
+ owner: '@you',
84
+ });
85
+ // files: { path, content }[]
86
+ ```
87
+
88
+ ---
89
+
90
+ ## Pairs with
91
+
92
+ - **[ai-native-app-blueprint](https://github.com/ruchit07/ai-native-app-blueprint)** — the production reference these specs target. Its `packages/evals` runs the `test-cases.json` this tool generates.
93
+ - **[The Spec-First Workflow](https://ruchitsuthar.com/blog/developer-productivity/ai-driven-development-spec-first-workflow/)** — the full method.
94
+
95
+ ---
96
+
97
+ ## License
98
+
99
+ MIT
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=generator.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generator.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/generator.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,67 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { generateFiles } from '../lib/generator.js';
3
+ import { slugify, titleize } from '../lib/slug.js';
4
+ const answers = {
5
+ featureName: 'Semantic Search for Support Tickets',
6
+ slug: 'semantic-search-for-support-tickets',
7
+ kind: 'rag',
8
+ problem: 'Support agents waste time searching old tickets.',
9
+ primaryProvider: 'openai',
10
+ latencyP95Ms: 2000,
11
+ costPerQueryUsd: 0.005,
12
+ accuracyThreshold: 0.8,
13
+ groundednessThreshold: 0.85,
14
+ owner: '@ruchit07',
15
+ };
16
+ describe('slugify', () => {
17
+ it('converts a feature name to a slug', () => {
18
+ expect(slugify('Semantic Search for Support Tickets')).toBe('semantic-search-for-support-tickets');
19
+ });
20
+ it('strips punctuation and collapses spaces', () => {
21
+ expect(slugify(' RAG: the New!! Checklist ')).toBe('rag-the-new-checklist');
22
+ });
23
+ it('handles already-slugged input', () => {
24
+ expect(slugify('already-a-slug')).toBe('already-a-slug');
25
+ });
26
+ });
27
+ describe('titleize', () => {
28
+ it('title-cases a slug', () => {
29
+ expect(titleize('semantic-search')).toBe('Semantic Search');
30
+ });
31
+ });
32
+ describe('generateFiles', () => {
33
+ it('generates exactly five files', () => {
34
+ const files = generateFiles(answers);
35
+ expect(files).toHaveLength(5);
36
+ const paths = files.map((f) => f.path).sort();
37
+ expect(paths).toEqual(['adr.md', 'eval-criteria.json', 'eval-criteria.md', 'spec.md', 'test-cases.json']);
38
+ });
39
+ it('embeds the thresholds into spec.md', () => {
40
+ const spec = generateFiles(answers).find((f) => f.path === 'spec.md');
41
+ expect(spec?.content).toContain('2000ms');
42
+ expect(spec?.content).toContain('$0.005');
43
+ expect(spec?.content).toContain('0.8');
44
+ });
45
+ it('produces valid JSON for eval-criteria.json', () => {
46
+ const json = generateFiles(answers).find((f) => f.path === 'eval-criteria.json');
47
+ const parsed = JSON.parse(json.content);
48
+ expect(parsed.thresholds.accuracy).toBe(0.8);
49
+ });
50
+ it('produces valid JSON test cases for the chosen kind', () => {
51
+ const json = generateFiles(answers).find((f) => f.path === 'test-cases.json');
52
+ const parsed = JSON.parse(json.content);
53
+ expect(parsed.length).toBeGreaterThan(0);
54
+ expect(parsed[0]?.id).toContain('tc-');
55
+ });
56
+ it('generates different test cases for classification kind', () => {
57
+ const classificationAnswers = { ...answers, kind: 'classification' };
58
+ const json = generateFiles(classificationAnswers).find((f) => f.path === 'test-cases.json');
59
+ const parsed = JSON.parse(json.content);
60
+ expect(parsed.some((tc) => tc.expected.answer === 'positive')).toBe(true);
61
+ });
62
+ it('includes the feature name in the spec heading', () => {
63
+ const spec = generateFiles(answers).find((f) => f.path === 'spec.md');
64
+ expect(spec?.content).toContain('Semantic Search For Support Tickets');
65
+ });
66
+ });
67
+ //# sourceMappingURL=generator.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generator.test.js","sourceRoot":"","sources":["../../src/__tests__/generator.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAGnD,MAAM,OAAO,GAAgB;IAC3B,WAAW,EAAE,qCAAqC;IAClD,IAAI,EAAE,qCAAqC;IAC3C,IAAI,EAAE,KAAK;IACX,OAAO,EAAE,kDAAkD;IAC3D,eAAe,EAAE,QAAQ;IACzB,YAAY,EAAE,IAAI;IAClB,eAAe,EAAE,KAAK;IACtB,iBAAiB,EAAE,GAAG;IACtB,qBAAqB,EAAE,IAAI;IAC3B,KAAK,EAAE,WAAW;CACnB,CAAC;AAEF,QAAQ,CAAC,SAAS,EAAE,GAAG,EAAE;IACvB,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;QAC3C,MAAM,CAAC,OAAO,CAAC,qCAAqC,CAAC,CAAC,CAAC,IAAI,CAAC,qCAAqC,CAAC,CAAC;IACrG,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;QACjD,MAAM,CAAC,OAAO,CAAC,8BAA8B,CAAC,CAAC,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC;IAChF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+BAA+B,EAAE,GAAG,EAAE;QACvC,MAAM,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;IAC3D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,UAAU,EAAE,GAAG,EAAE;IACxB,EAAE,CAAC,oBAAoB,EAAE,GAAG,EAAE;QAC5B,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAC,CAAC,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,EAAE,CAAC,8BAA8B,EAAE,GAAG,EAAE;QACtC,MAAM,KAAK,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;QACrC,MAAM,CAAC,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAC9B,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;QAC9C,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,SAAS,EAAE,iBAAiB,CAAC,CAAC,CAAC;IAC5G,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,IAAI,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC;QACtE,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QAC1C,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QAC1C,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;QACpD,MAAM,IAAI,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,oBAAoB,CAAC,CAAC;QACjF,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAK,CAAC,OAAO,CAAyC,CAAC;QACjF,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oDAAoD,EAAE,GAAG,EAAE;QAC5D,MAAM,IAAI,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,iBAAiB,CAAC,CAAC;QAC9E,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAK,CAAC,OAAO,CAA0B,CAAC;QAClE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;QACzC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wDAAwD,EAAE,GAAG,EAAE;QAChE,MAAM,qBAAqB,GAAG,EAAE,GAAG,OAAO,EAAE,IAAI,EAAE,gBAAyB,EAAE,CAAC;QAC9E,MAAM,IAAI,GAAG,aAAa,CAAC,qBAAqB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,iBAAiB,CAAC,CAAC;QAC5F,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAK,CAAC,OAAO,CAA6C,CAAC;QACrF,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,QAAQ,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;QACvD,MAAM,IAAI,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC;QACtE,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,SAAS,CAAC,qCAAqC,CAAC,CAAC;IACzE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=writer.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"writer.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/writer.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,34 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import { rmSync, existsSync, readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { writeFiles } from '../lib/writer.js';
6
+ const testBase = join(tmpdir(), 'ai-spec-test');
7
+ const files = [
8
+ { path: 'spec.md', content: '# Spec' },
9
+ { path: 'test-cases.json', content: '[]' },
10
+ ];
11
+ afterEach(() => {
12
+ rmSync(testBase, { recursive: true, force: true });
13
+ });
14
+ describe('writeFiles', () => {
15
+ it('writes all files into baseDir/slug/', () => {
16
+ const result = writeFiles(testBase, 'my-feature', files);
17
+ expect(result.written).toHaveLength(2);
18
+ expect(existsSync(join(testBase, 'my-feature', 'spec.md'))).toBe(true);
19
+ expect(readFileSync(join(testBase, 'my-feature', 'spec.md'), 'utf8')).toBe('# Spec');
20
+ });
21
+ it('skips existing files without force', () => {
22
+ writeFiles(testBase, 'my-feature', files);
23
+ const result = writeFiles(testBase, 'my-feature', files);
24
+ expect(result.written).toHaveLength(0);
25
+ expect(result.skipped).toHaveLength(2);
26
+ });
27
+ it('overwrites existing files with force', () => {
28
+ writeFiles(testBase, 'my-feature', files);
29
+ const result = writeFiles(testBase, 'my-feature', files, true);
30
+ expect(result.written).toHaveLength(2);
31
+ expect(result.skipped).toHaveLength(0);
32
+ });
33
+ });
34
+ //# sourceMappingURL=writer.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"writer.test.js","sourceRoot":"","sources":["../../src/__tests__/writer.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACzD,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC3D,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAG9C,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE,cAAc,CAAC,CAAC;AAEhD,MAAM,KAAK,GAAoB;IAC7B,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE;IACtC,EAAE,IAAI,EAAE,iBAAiB,EAAE,OAAO,EAAE,IAAI,EAAE;CAC3C,CAAC;AAEF,SAAS,CAAC,GAAG,EAAE;IACb,MAAM,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AACrD,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;IAC1B,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,EAAE,YAAY,EAAE,KAAK,CAAC,CAAC;QACzD,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QACvC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,EAAE,YAAY,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvE,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,EAAE,YAAY,EAAE,SAAS,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACvF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,UAAU,CAAC,QAAQ,EAAE,YAAY,EAAE,KAAK,CAAC,CAAC;QAC1C,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,EAAE,YAAY,EAAE,KAAK,CAAC,CAAC;QACzD,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QACvC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,UAAU,CAAC,QAAQ,EAAE,YAAY,EAAE,KAAK,CAAC,CAAC;QAC1C,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,EAAE,YAAY,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;QAC/D,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QACvC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import pc from 'picocolors';
4
+ import { runInit } from './commands/init.js';
5
+ const program = new Command();
6
+ program
7
+ .name('ai-spec')
8
+ .description('Scaffold a production-grade AI feature spec before you write a line of code')
9
+ .version('1.0.0');
10
+ program
11
+ .command('init')
12
+ .argument('[name]', 'feature name, e.g. "semantic search for support tickets"')
13
+ .description('Create a new AI feature spec (spec, eval criteria, golden test cases, ADR)')
14
+ .option('-d, --dir <dir>', 'output directory', 'specs')
15
+ .option('-k, --kind <kind>', 'feature kind: rag | chat | classification | extraction | agent')
16
+ .option('-y, --yes', 'skip prompts and use sensible defaults')
17
+ .option('-f, --force', 'overwrite existing files')
18
+ .action(async (name, opts) => {
19
+ const code = await runInit({
20
+ name,
21
+ dir: opts.dir,
22
+ kind: opts.kind,
23
+ yes: opts.yes,
24
+ force: opts.force,
25
+ });
26
+ process.exit(code);
27
+ });
28
+ program.addHelpText('after', `
29
+ ${pc.bold('Examples:')}
30
+ $ ${pc.cyan('npx @ruchit07/ai-spec init "semantic search for support tickets"')}
31
+ $ ${pc.cyan('ai-spec init "ticket classifier" --kind classification --yes')}
32
+ $ ${pc.cyan('ai-spec init "support agent" -k agent -d ./ai-features')}
33
+
34
+ ${pc.dim('Method: https://ruchitsuthar.com/blog/developer-productivity/ai-driven-development-spec-first-workflow/')}
35
+ `);
36
+ program.parseAsync(process.argv);
37
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,MAAM,YAAY,CAAC;AAC5B,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAG7C,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,SAAS,CAAC;KACf,WAAW,CAAC,6EAA6E,CAAC;KAC1F,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpB,OAAO;KACJ,OAAO,CAAC,MAAM,CAAC;KACf,QAAQ,CAAC,QAAQ,EAAE,0DAA0D,CAAC;KAC9E,WAAW,CAAC,4EAA4E,CAAC;KACzF,MAAM,CAAC,iBAAiB,EAAE,kBAAkB,EAAE,OAAO,CAAC;KACtD,MAAM,CAAC,mBAAmB,EAAE,gEAAgE,CAAC;KAC7F,MAAM,CAAC,WAAW,EAAE,wCAAwC,CAAC;KAC7D,MAAM,CAAC,aAAa,EAAE,0BAA0B,CAAC;KACjD,MAAM,CAAC,KAAK,EAAE,IAAwB,EAAE,IAAoE,EAAE,EAAE;IAC/G,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC;QACzB,IAAI;QACJ,GAAG,EAAE,IAAI,CAAC,GAAG;QACb,IAAI,EAAE,IAAI,CAAC,IAA+B;QAC1C,GAAG,EAAE,IAAI,CAAC,GAAG;QACb,KAAK,EAAE,IAAI,CAAC,KAAK;KAClB,CAAC,CAAC;IACH,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACrB,CAAC,CAAC,CAAC;AAEL,OAAO,CAAC,WAAW,CACjB,OAAO,EACP;EACA,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC;MAChB,EAAE,CAAC,IAAI,CAAC,kEAAkE,CAAC;MAC3E,EAAE,CAAC,IAAI,CAAC,8DAA8D,CAAC;MACvE,EAAE,CAAC,IAAI,CAAC,wDAAwD,CAAC;;EAErE,EAAE,CAAC,GAAG,CAAC,yGAAyG,CAAC;CAClH,CACA,CAAC;AAEF,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC"}
@@ -0,0 +1,10 @@
1
+ import { type FeatureKind } from '../lib/types.js';
2
+ export interface InitOptions {
3
+ name?: string | undefined;
4
+ dir?: string | undefined;
5
+ kind?: FeatureKind | undefined;
6
+ yes?: boolean | undefined;
7
+ force?: boolean | undefined;
8
+ }
9
+ export declare function runInit(options: InitOptions): Promise<number>;
10
+ //# sourceMappingURL=init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAKA,OAAO,EAAmC,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEpF,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,IAAI,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IAC/B,GAAG,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAC7B;AAWD,wBAAsB,OAAO,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CA6GnE"}
@@ -0,0 +1,141 @@
1
+ import prompts from 'prompts';
2
+ import pc from 'picocolors';
3
+ import { slugify, titleize } from '../lib/slug.js';
4
+ import { generateFiles } from '../lib/generator.js';
5
+ import { writeFiles } from '../lib/writer.js';
6
+ import { FEATURE_KINDS } from '../lib/types.js';
7
+ const DEFAULTS = {
8
+ latencyP95Ms: 2000,
9
+ costPerQueryUsd: 0.005,
10
+ accuracyThreshold: 0.8,
11
+ groundednessThreshold: 0.85,
12
+ primaryProvider: 'openai',
13
+ owner: '@you',
14
+ };
15
+ export async function runInit(options) {
16
+ let featureName = options.name?.trim() ?? '';
17
+ let kind = options.kind;
18
+ // Non-interactive mode (--yes) requires a name
19
+ if (options.yes) {
20
+ if (!featureName) {
21
+ console.error(pc.red('Error: --yes requires a feature name. Usage: ai-spec init "name" --yes'));
22
+ return 1;
23
+ }
24
+ kind = kind ?? 'rag';
25
+ }
26
+ else {
27
+ // Interactive prompts
28
+ const responses = await prompts([
29
+ {
30
+ type: featureName ? null : 'text',
31
+ name: 'featureName',
32
+ message: 'Feature name',
33
+ validate: (v) => (v.trim().length > 0 ? true : 'Required'),
34
+ },
35
+ {
36
+ type: kind ? null : 'select',
37
+ name: 'kind',
38
+ message: 'What kind of AI feature?',
39
+ choices: FEATURE_KINDS.map((k) => ({ title: `${k.title} — ${k.description}`, value: k.value })),
40
+ initial: 0,
41
+ },
42
+ {
43
+ type: 'text',
44
+ name: 'problem',
45
+ message: 'Problem statement (one sentence — what user problem does this solve?)',
46
+ validate: (v) => (v.trim().length > 0 ? true : 'Required'),
47
+ },
48
+ {
49
+ type: 'select',
50
+ name: 'primaryProvider',
51
+ message: 'Primary LLM provider',
52
+ choices: [
53
+ { title: 'OpenAI', value: 'openai' },
54
+ { title: 'Anthropic', value: 'anthropic' },
55
+ { title: 'Other', value: 'other' },
56
+ ],
57
+ initial: 0,
58
+ },
59
+ {
60
+ type: 'number',
61
+ name: 'latencyP95Ms',
62
+ message: 'Max latency p95 (ms)',
63
+ initial: DEFAULTS.latencyP95Ms,
64
+ },
65
+ {
66
+ type: 'number',
67
+ name: 'costPerQueryUsd',
68
+ message: 'Max cost per query (USD, e.g. 0.005)',
69
+ initial: DEFAULTS.costPerQueryUsd,
70
+ float: true,
71
+ },
72
+ {
73
+ type: 'text',
74
+ name: 'owner',
75
+ message: 'Owner (your handle)',
76
+ initial: DEFAULTS.owner,
77
+ },
78
+ ], {
79
+ onCancel: () => {
80
+ console.log(pc.yellow('\nCancelled.'));
81
+ process.exit(130);
82
+ },
83
+ });
84
+ featureName = featureName || responses.featureName;
85
+ kind = kind ?? responses.kind;
86
+ const slug = slugify(featureName);
87
+ const answers = {
88
+ featureName,
89
+ slug,
90
+ kind: kind ?? 'rag',
91
+ problem: responses.problem,
92
+ primaryProvider: responses.primaryProvider ?? DEFAULTS.primaryProvider,
93
+ latencyP95Ms: responses.latencyP95Ms ?? DEFAULTS.latencyP95Ms,
94
+ costPerQueryUsd: responses.costPerQueryUsd ?? DEFAULTS.costPerQueryUsd,
95
+ accuracyThreshold: DEFAULTS.accuracyThreshold,
96
+ groundednessThreshold: DEFAULTS.groundednessThreshold,
97
+ owner: responses.owner ?? DEFAULTS.owner,
98
+ };
99
+ return write(answers, options);
100
+ }
101
+ // Non-interactive path
102
+ const slug = slugify(featureName);
103
+ const answers = {
104
+ featureName,
105
+ slug,
106
+ kind: kind ?? 'rag',
107
+ problem: `TODO: describe the user problem that "${titleize(featureName)}" solves.`,
108
+ primaryProvider: DEFAULTS.primaryProvider,
109
+ latencyP95Ms: DEFAULTS.latencyP95Ms,
110
+ costPerQueryUsd: DEFAULTS.costPerQueryUsd,
111
+ accuracyThreshold: DEFAULTS.accuracyThreshold,
112
+ groundednessThreshold: DEFAULTS.groundednessThreshold,
113
+ owner: DEFAULTS.owner,
114
+ };
115
+ return write(answers, options);
116
+ }
117
+ function write(answers, options) {
118
+ const baseDir = options.dir ?? 'specs';
119
+ const files = generateFiles(answers);
120
+ const result = writeFiles(baseDir, answers.slug, files, options.force ?? false);
121
+ console.log('');
122
+ console.log(pc.green(pc.bold(`✓ Spec scaffolded: ${baseDir}/${answers.slug}/`)));
123
+ console.log('');
124
+ for (const path of result.written) {
125
+ console.log(` ${pc.green('created')} ${path}`);
126
+ }
127
+ for (const path of result.skipped) {
128
+ console.log(` ${pc.yellow('skipped')} ${path} ${pc.dim('(exists — use --force to overwrite)')}`);
129
+ }
130
+ console.log('');
131
+ console.log(pc.bold('Next steps:'));
132
+ console.log(` 1. Fill in ${pc.cyan(`${baseDir}/${answers.slug}/spec.md`)} — input/output contract, acceptance criteria`);
133
+ console.log(` 2. Replace the seed cases in ${pc.cyan('test-cases.json')} with real user questions`);
134
+ console.log(` 3. Write the decision in ${pc.cyan('adr.md')} once you've chosen an approach`);
135
+ console.log(` 4. Wire ${pc.cyan('eval-criteria.json')} into CI`);
136
+ console.log('');
137
+ console.log(pc.dim('Method: https://ruchitsuthar.com/blog/developer-productivity/ai-driven-development-spec-first-workflow/'));
138
+ console.log('');
139
+ return result.skipped.length > 0 && !options.force ? 0 : 0;
140
+ }
141
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ export { generateFiles } from './lib/generator.js';
2
+ export { writeFiles } from './lib/writer.js';
3
+ export { slugify, titleize } from './lib/slug.js';
4
+ export { FEATURE_KINDS } from './lib/types.js';
5
+ export type { SpecAnswers, GeneratedFile, TestCase, FeatureKind, } from './lib/types.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,YAAY,EACV,WAAW,EACX,aAAa,EACb,QAAQ,EACR,WAAW,GACZ,MAAM,gBAAgB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ // Programmatic API — for using ai-spec inside other tools
2
+ export { generateFiles } from './lib/generator.js';
3
+ export { writeFiles } from './lib/writer.js';
4
+ export { slugify, titleize } from './lib/slug.js';
5
+ export { FEATURE_KINDS } from './lib/types.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,0DAA0D;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { SpecAnswers, GeneratedFile } from './types.js';
2
+ /**
3
+ * Pure function: given the answers, produce the list of files to write.
4
+ * Kept pure (no fs) so it's trivial to unit test.
5
+ */
6
+ export declare function generateFiles(answers: SpecAnswers): GeneratedFile[];
7
+ //# sourceMappingURL=generator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../src/lib/generator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAM7D;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,WAAW,GAAG,aAAa,EAAE,CAQnE"}
@@ -0,0 +1,18 @@
1
+ import { renderSpec } from '../templates/spec.js';
2
+ import { renderEvalCriteria, renderEvalCriteriaJson } from '../templates/eval-criteria.js';
3
+ import { renderTestCases } from '../templates/test-cases.js';
4
+ import { renderAdr } from '../templates/adr.js';
5
+ /**
6
+ * Pure function: given the answers, produce the list of files to write.
7
+ * Kept pure (no fs) so it's trivial to unit test.
8
+ */
9
+ export function generateFiles(answers) {
10
+ return [
11
+ { path: 'spec.md', content: renderSpec(answers) },
12
+ { path: 'eval-criteria.md', content: renderEvalCriteria(answers) },
13
+ { path: 'eval-criteria.json', content: renderEvalCriteriaJson(answers) },
14
+ { path: 'test-cases.json', content: renderTestCases(answers) },
15
+ { path: 'adr.md', content: renderAdr(answers) },
16
+ ];
17
+ }
18
+ //# sourceMappingURL=generator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generator.js","sourceRoot":"","sources":["../../src/lib/generator.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAC3F,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAEhD;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,OAAoB;IAChD,OAAO;QACL,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE;QACjD,EAAE,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,kBAAkB,CAAC,OAAO,CAAC,EAAE;QAClE,EAAE,IAAI,EAAE,oBAAoB,EAAE,OAAO,EAAE,sBAAsB,CAAC,OAAO,CAAC,EAAE;QACxE,EAAE,IAAI,EAAE,iBAAiB,EAAE,OAAO,EAAE,eAAe,CAAC,OAAO,CAAC,EAAE;QAC9D,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,CAAC,OAAO,CAAC,EAAE;KAChD,CAAC;AACJ,CAAC"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Converts a free-text feature name into a filesystem-safe slug.
3
+ * "Semantic Search for Support Tickets" → "semantic-search-for-support-tickets"
4
+ */
5
+ export declare function slugify(input: string): string;
6
+ /**
7
+ * Title-cases a slug for use in headings.
8
+ * "semantic-search" → "Semantic Search"
9
+ */
10
+ export declare function titleize(input: string): string;
11
+ //# sourceMappingURL=slug.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slug.d.ts","sourceRoot":"","sources":["../../src/lib/slug.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQ7C;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAM9C"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Converts a free-text feature name into a filesystem-safe slug.
3
+ * "Semantic Search for Support Tickets" → "semantic-search-for-support-tickets"
4
+ */
5
+ export function slugify(input) {
6
+ return input
7
+ .toLowerCase()
8
+ .trim()
9
+ .replace(/[^a-z0-9\s-]/g, '') // drop punctuation
10
+ .replace(/\s+/g, '-') // spaces → hyphens
11
+ .replace(/-+/g, '-') // collapse repeats
12
+ .replace(/^-|-$/g, ''); // trim edge hyphens
13
+ }
14
+ /**
15
+ * Title-cases a slug for use in headings.
16
+ * "semantic-search" → "Semantic Search"
17
+ */
18
+ export function titleize(input) {
19
+ return input
20
+ .split(/[-\s]+/)
21
+ .filter(Boolean)
22
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
23
+ .join(' ');
24
+ }
25
+ //# sourceMappingURL=slug.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slug.js","sourceRoot":"","sources":["../../src/lib/slug.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,UAAU,OAAO,CAAC,KAAa;IACnC,OAAO,KAAK;SACT,WAAW,EAAE;SACb,IAAI,EAAE;SACN,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAG,mBAAmB;SAClD,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAW,mBAAmB;SAClD,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAY,mBAAmB;SAClD,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAS,oBAAoB;AACxD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAC,KAAa;IACpC,OAAO,KAAK;SACT,KAAK,CAAC,QAAQ,CAAC;SACf,MAAM,CAAC,OAAO,CAAC;SACf,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;SAClD,IAAI,CAAC,GAAG,CAAC,CAAC;AACf,CAAC"}
@@ -0,0 +1,35 @@
1
+ export type FeatureKind = 'rag' | 'chat' | 'classification' | 'extraction' | 'agent';
2
+ export interface SpecAnswers {
3
+ featureName: string;
4
+ slug: string;
5
+ kind: FeatureKind;
6
+ problem: string;
7
+ primaryProvider: 'openai' | 'anthropic' | 'other';
8
+ latencyP95Ms: number;
9
+ costPerQueryUsd: number;
10
+ accuracyThreshold: number;
11
+ groundednessThreshold: number;
12
+ owner: string;
13
+ }
14
+ export interface GeneratedFile {
15
+ path: string;
16
+ content: string;
17
+ }
18
+ export interface TestCase {
19
+ id: string;
20
+ input: {
21
+ question: string;
22
+ context?: string;
23
+ };
24
+ expected: {
25
+ answer?: string;
26
+ mustContain?: string[];
27
+ mustNotContain?: string[];
28
+ };
29
+ }
30
+ export declare const FEATURE_KINDS: Array<{
31
+ value: FeatureKind;
32
+ title: string;
33
+ description: string;
34
+ }>;
35
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,MAAM,GAAG,gBAAgB,GAAG,YAAY,GAAG,OAAO,CAAC;AAErF,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,QAAQ,GAAG,WAAW,GAAG,OAAO,CAAC;IAClD,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE;QACL,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,QAAQ,EAAE;QACR,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;QACvB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;KAC3B,CAAC;CACH;AAED,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAM3F,CAAC"}
@@ -0,0 +1,8 @@
1
+ export const FEATURE_KINDS = [
2
+ { value: 'rag', title: 'RAG / Q&A', description: 'Answer questions from a knowledge base' },
3
+ { value: 'chat', title: 'Chat / Assistant', description: 'Conversational assistant, possibly with tools' },
4
+ { value: 'classification', title: 'Classification', description: 'Categorise input into known labels' },
5
+ { value: 'extraction', title: 'Extraction', description: 'Pull structured data from unstructured text' },
6
+ { value: 'agent', title: 'Agent', description: 'Multi-step autonomous task execution' },
7
+ ];
8
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/lib/types.ts"],"names":[],"mappings":"AAiCA,MAAM,CAAC,MAAM,aAAa,GAAsE;IAC9F,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,wCAAwC,EAAE;IAC3F,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,kBAAkB,EAAE,WAAW,EAAE,+CAA+C,EAAE;IAC1G,EAAE,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,oCAAoC,EAAE;IACvG,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,6CAA6C,EAAE;IACxG,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,sCAAsC,EAAE;CACxF,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { GeneratedFile } from './types.js';
2
+ export interface WriteResult {
3
+ written: string[];
4
+ skipped: string[];
5
+ }
6
+ /**
7
+ * Writes generated files into <baseDir>/<slug>/.
8
+ * Refuses to overwrite existing files unless `force` is set.
9
+ */
10
+ export declare function writeFiles(baseDir: string, slug: string, files: GeneratedFile[], force?: boolean): WriteResult;
11
+ //# sourceMappingURL=writer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"writer.d.ts","sourceRoot":"","sources":["../../src/lib/writer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACxB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,aAAa,EAAE,EACtB,KAAK,UAAQ,GACZ,WAAW,CAiBb"}
@@ -0,0 +1,23 @@
1
+ import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ /**
4
+ * Writes generated files into <baseDir>/<slug>/.
5
+ * Refuses to overwrite existing files unless `force` is set.
6
+ */
7
+ export function writeFiles(baseDir, slug, files, force = false) {
8
+ const targetDir = join(baseDir, slug);
9
+ const written = [];
10
+ const skipped = [];
11
+ for (const file of files) {
12
+ const fullPath = join(targetDir, file.path);
13
+ if (existsSync(fullPath) && !force) {
14
+ skipped.push(fullPath);
15
+ continue;
16
+ }
17
+ mkdirSync(dirname(fullPath), { recursive: true });
18
+ writeFileSync(fullPath, file.content, 'utf8');
19
+ written.push(fullPath);
20
+ }
21
+ return { written, skipped };
22
+ }
23
+ //# sourceMappingURL=writer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"writer.js","sourceRoot":"","sources":["../../src/lib/writer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQ1C;;;GAGG;AACH,MAAM,UAAU,UAAU,CACxB,OAAe,EACf,IAAY,EACZ,KAAsB,EACtB,KAAK,GAAG,KAAK;IAEb,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IACtC,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,MAAM,OAAO,GAAa,EAAE,CAAC;IAE7B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5C,IAAI,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YACnC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACvB,SAAS;QACX,CAAC;QACD,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAClD,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC9C,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACzB,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;AAC9B,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { SpecAnswers } from '../lib/types.js';
2
+ export declare function renderAdr(a: SpecAnswers): string;
3
+ //# sourceMappingURL=adr.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adr.d.ts","sourceRoot":"","sources":["../../src/templates/adr.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAGnD,wBAAgB,SAAS,CAAC,CAAC,EAAE,WAAW,GAAG,MAAM,CAmDhD"}
@@ -0,0 +1,54 @@
1
+ import { titleize } from '../lib/slug.js';
2
+ export function renderAdr(a) {
3
+ const date = new Date().toISOString().split('T')[0];
4
+ return `# ADR-0001: ${titleize(a.featureName)} — Architecture
5
+
6
+ **Status:** Proposed
7
+ **Date:** ${date}
8
+ **Deciders:** ${a.owner}
9
+
10
+ ---
11
+
12
+ ## Context
13
+
14
+ _What is the situation that forces a decision? What constraints apply (team size, latency budget, data residency, existing systems)?_
15
+
16
+ This feature is a **${a.kind}** capability. Key constraints from the spec:
17
+ - Latency p95 ≤ ${a.latencyP95Ms}ms
18
+ - Cost per query ≤ $${a.costPerQueryUsd}
19
+ - Primary provider: ${a.primaryProvider}
20
+
21
+ ## Decision
22
+
23
+ _What did you decide, stated plainly? (e.g. "Use pgvector, not Pinecone." "Use a modular monolith, not microservices.")_
24
+
25
+ TODO.
26
+
27
+ ## Consequences
28
+
29
+ **Good:**
30
+ - _(what this buys you)_
31
+
32
+ **Bad:**
33
+ - _(what you give up — every decision has a cost; name it)_
34
+
35
+ ## Rejected alternatives
36
+
37
+ _For each alternative you seriously considered, say what it was and why you rejected it. This is the most valuable section — it shows the decision was reasoned, not defaulted._
38
+
39
+ **Alternative A:** _(what it was)_ — Rejected because _(reason)_.
40
+
41
+ **Alternative B:** _(what it was)_ — Rejected because _(reason)_.
42
+
43
+ ## When to revisit
44
+
45
+ _Under what conditions does this decision stop being correct? (e.g. "Revisit when we exceed 5M vectors" or "when the team grows past 20 engineers.")_
46
+
47
+ TODO.
48
+
49
+ ---
50
+
51
+ _ADR format from [ai-native-app-blueprint](https://github.com/ruchit07/ai-native-app-blueprint/tree/main/adr). Why ADRs: [Architecture Decision Records People Actually Maintain](https://ruchitsuthar.com/blog/software-architecture/architecture-decision-records-people-maintain/)._
52
+ `;
53
+ }
54
+ //# sourceMappingURL=adr.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adr.js","sourceRoot":"","sources":["../../src/templates/adr.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C,MAAM,UAAU,SAAS,CAAC,CAAc;IACtC,MAAM,IAAI,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACpD,OAAO,eAAe,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC;;;YAGnC,IAAI;gBACA,CAAC,CAAC,KAAK;;;;;;;;sBAQD,CAAC,CAAC,IAAI;kBACV,CAAC,CAAC,YAAY;sBACV,CAAC,CAAC,eAAe;sBACjB,CAAC,CAAC,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiCtC,CAAC;AACF,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { SpecAnswers } from '../lib/types.js';
2
+ export declare function renderEvalCriteria(a: SpecAnswers): string;
3
+ export declare function renderEvalCriteriaJson(a: SpecAnswers): string;
4
+ //# sourceMappingURL=eval-criteria.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eval-criteria.d.ts","sourceRoot":"","sources":["../../src/templates/eval-criteria.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAGnD,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,WAAW,GAAG,MAAM,CAoDzD;AAED,wBAAgB,sBAAsB,CAAC,CAAC,EAAE,WAAW,GAAG,MAAM,CAe7D"}
@@ -0,0 +1,67 @@
1
+ import { titleize } from '../lib/slug.js';
2
+ export function renderEvalCriteria(a) {
3
+ return `# Eval Criteria — ${titleize(a.featureName)}
4
+
5
+ _The machine-readable contract. These thresholds gate CI. A PR that drops below any threshold fails the build._
6
+
7
+ ---
8
+
9
+ ## Metrics & thresholds
10
+
11
+ | Metric | What it measures | Threshold | Fails CI? |
12
+ |--------|------------------|-----------|-----------|
13
+ | **accuracy** | Token F1 overlap vs golden answers (SQuAD metric) | ≥ ${a.accuracyThreshold} | Yes |
14
+ | **groundedness** | Fraction of answer sentences supported by retrieved context | ≥ ${a.groundednessThreshold} | Yes |
15
+ | **latency_p95** | 95th percentile end-to-end latency | ≤ ${a.latencyP95Ms}ms | Yes |
16
+ | **cost_per_query** | Average LLM cost per query | ≤ $${a.costPerQueryUsd} | Yes |
17
+ | **faithfulness** | Answer does not contradict context | ≥ ${a.groundednessThreshold} | Warn |
18
+
19
+ ---
20
+
21
+ ## Two-tier eval strategy
22
+
23
+ **Tier 1 — every PR (fast, free):**
24
+ Heuristic evaluators (no LLM calls). Token-overlap accuracy, sentence-overlap groundedness, latency, cost. Runs in < 30s. $0.
25
+
26
+ **Tier 2 — weekly + before releases (comprehensive):**
27
+ LLM-as-judge on a larger test set. Catches subtle hallucinations the heuristics miss. ~$2–5/run.
28
+
29
+ > Why two tiers? You want evals on every PR (so cheap they're free), but you also want the depth of an LLM judge (too expensive for every PR). See [ADR: eval strategy](https://github.com/ruchit07/ai-native-app-blueprint/blob/main/adr/0004-eval-strategy.md).
30
+
31
+ ---
32
+
33
+ ## Running the evals
34
+
35
+ \`\`\`bash
36
+ # Tier 1 — heuristic, runs in CI
37
+ npx ai-evals run ./test-cases.json --thresholds ./eval-criteria.json
38
+
39
+ # Tier 2 — LLM judge (set EVAL_LLM_JUDGE=true)
40
+ EVAL_LLM_JUDGE=true npx ai-evals run ./test-cases.json
41
+ \`\`\`
42
+
43
+ ---
44
+
45
+ ## Threshold rationale
46
+
47
+ _Document WHY each threshold is set where it is. "We picked 0.85" is not a rationale; "0.85 because below that users start re-asking the question, per our March user test" is._
48
+
49
+ - **accuracy ≥ ${a.accuracyThreshold}:** _(rationale)_
50
+ - **groundedness ≥ ${a.groundednessThreshold}:** _(rationale)_
51
+ - **latency p95 ≤ ${a.latencyP95Ms}ms:** _(rationale — e.g. streaming UX feels broken above this)_
52
+ - **cost ≤ $${a.costPerQueryUsd}:** _(rationale — e.g. unit economics at projected volume)_
53
+ `;
54
+ }
55
+ export function renderEvalCriteriaJson(a) {
56
+ return JSON.stringify({
57
+ suite: a.slug,
58
+ thresholds: {
59
+ accuracy: a.accuracyThreshold,
60
+ groundedness: a.groundednessThreshold,
61
+ latencyP95Ms: a.latencyP95Ms,
62
+ costPerQueryUsd: a.costPerQueryUsd,
63
+ },
64
+ failOnRegression: true,
65
+ }, null, 2);
66
+ }
67
+ //# sourceMappingURL=eval-criteria.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eval-criteria.js","sourceRoot":"","sources":["../../src/templates/eval-criteria.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C,MAAM,UAAU,kBAAkB,CAAC,CAAc;IAC/C,OAAO,qBAAqB,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC;;;;;;;;;;yEAUoB,CAAC,CAAC,iBAAiB;uFACL,CAAC,CAAC,qBAAqB;6DACjD,CAAC,CAAC,YAAY;yDAClB,CAAC,CAAC,eAAe;8DACZ,CAAC,CAAC,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAgCpE,CAAC,CAAC,iBAAiB;qBACf,CAAC,CAAC,qBAAqB;oBACxB,CAAC,CAAC,YAAY;cACpB,CAAC,CAAC,eAAe;CAC9B,CAAC;AACF,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,CAAc;IACnD,OAAO,IAAI,CAAC,SAAS,CACnB;QACE,KAAK,EAAE,CAAC,CAAC,IAAI;QACb,UAAU,EAAE;YACV,QAAQ,EAAE,CAAC,CAAC,iBAAiB;YAC7B,YAAY,EAAE,CAAC,CAAC,qBAAqB;YACrC,YAAY,EAAE,CAAC,CAAC,YAAY;YAC5B,eAAe,EAAE,CAAC,CAAC,eAAe;SACnC;QACD,gBAAgB,EAAE,IAAI;KACvB,EACD,IAAI,EACJ,CAAC,CACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { SpecAnswers } from '../lib/types.js';
2
+ export declare function renderSpec(a: SpecAnswers): string;
3
+ //# sourceMappingURL=spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spec.d.ts","sourceRoot":"","sources":["../../src/templates/spec.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAWnD,wBAAgB,UAAU,CAAC,CAAC,EAAE,WAAW,GAAG,MAAM,CAmFjD"}
@@ -0,0 +1,93 @@
1
+ import { titleize } from '../lib/slug.js';
2
+ const KIND_GUIDANCE = {
3
+ rag: 'Retrieval quality is the dominant variable. Specify your chunking, retrieval, and reranking strategy. Groundedness is the metric that matters most — a fluent but ungrounded answer is the failure mode to design against.',
4
+ chat: 'Define the conversation scope and the tools (if any) the assistant can call. Multi-turn context handling and instruction-following are the hard parts. Decide your context window budget per turn.',
5
+ classification: 'Define the exact label set. Accuracy and per-class precision/recall are the metrics. Decide what happens for out-of-distribution inputs (a "none of the above" class is usually necessary).',
6
+ extraction: 'Define the exact output schema (typed). Structured output mode or function calling is strongly recommended. Field-level accuracy matters more than overall — one wrong field can break a downstream system.',
7
+ agent: 'Define the action space and the stopping conditions. The risk is unbounded loops and unsafe actions. Specify guardrails, a max-step budget, and which actions require human approval.',
8
+ };
9
+ export function renderSpec(a) {
10
+ const date = new Date().toISOString().split('T')[0];
11
+ return `# AI Feature Spec — ${titleize(a.featureName)}
12
+
13
+ _Created: ${date} | Owner: ${a.owner} | Kind: ${a.kind}_
14
+
15
+ ---
16
+
17
+ ## Problem statement
18
+
19
+ ${a.problem}
20
+
21
+ > **What happens today without this feature?** _(Fill in: the manual process, the workaround, or the gap this closes.)_
22
+
23
+ ---
24
+
25
+ ## Input / output contract
26
+
27
+ **Input:**
28
+ \`\`\`typescript
29
+ interface FeatureInput {
30
+ // TODO: define exact types
31
+ query: string;
32
+ }
33
+ \`\`\`
34
+
35
+ **Output:**
36
+ \`\`\`typescript
37
+ interface FeatureOutput {
38
+ // TODO: define exact types
39
+ answer: string;
40
+ sources?: { title: string; url: string }[];
41
+ }
42
+ \`\`\`
43
+
44
+ **Constraints:**
45
+ - Maximum latency (p95): **${a.latencyP95Ms}ms**
46
+ - Maximum cost per query: **$${a.costPerQueryUsd}**
47
+ - Primary provider: **${a.primaryProvider}**
48
+
49
+ ---
50
+
51
+ ## Acceptance criteria
52
+
53
+ _Measurable. Not "the response should be good." Each criterion maps to an eval metric in \`eval-criteria.md\`._
54
+
55
+ | # | Criterion | Measurement | Threshold |
56
+ |---|-----------|-------------|-----------|
57
+ | 1 | Correct on golden test set | Accuracy (F1 token overlap) | ≥ ${a.accuracyThreshold} |
58
+ | 2 | Grounded in provided context | Groundedness score | ≥ ${a.groundednessThreshold} |
59
+ | 3 | Fast enough for the UX | Latency p95 | ≤ ${a.latencyP95Ms}ms |
60
+ | 4 | Affordable at scale | Cost per query | ≤ $${a.costPerQueryUsd} |
61
+
62
+ ---
63
+
64
+ ## Kind-specific guidance (${a.kind})
65
+
66
+ ${KIND_GUIDANCE[a.kind]}
67
+
68
+ ---
69
+
70
+ ## Known failure modes (out of scope for v1)
71
+
72
+ _Explicitly mark what you've decided NOT to handle. This prevents scope creep._
73
+
74
+ - [ ] Adversarial prompt injection in user input
75
+ - [ ] Inputs in languages other than English
76
+ - [ ] _(add the ones specific to this feature)_
77
+
78
+ ---
79
+
80
+ ## Sign-off checklist
81
+
82
+ - [ ] Spec reviewed by a second engineer
83
+ - [ ] Golden test cases written (\`test-cases.json\`)
84
+ - [ ] Eval suite passing at thresholds (\`eval-criteria.md\`)
85
+ - [ ] ADR written for any significant architectural decision (\`adr.md\`)
86
+ - [ ] Runbook / ops notes updated
87
+
88
+ ---
89
+
90
+ _Generated by [ai-spec](https://github.com/ruchit07/ai-spec). Companion method: [The Spec-First Workflow](https://ruchitsuthar.com/blog/developer-productivity/ai-driven-development-spec-first-workflow/)._
91
+ `;
92
+ }
93
+ //# sourceMappingURL=spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spec.js","sourceRoot":"","sources":["../../src/templates/spec.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C,MAAM,aAAa,GAAwC;IACzD,GAAG,EAAE,4NAA4N;IACjO,IAAI,EAAE,oMAAoM;IAC1M,cAAc,EAAE,6LAA6L;IAC7M,UAAU,EAAE,6MAA6M;IACzN,KAAK,EAAE,uLAAuL;CAC/L,CAAC;AAEF,MAAM,UAAU,UAAU,CAAC,CAAc;IACvC,MAAM,IAAI,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACpD,OAAO,uBAAuB,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC;;YAE3C,IAAI,aAAa,CAAC,CAAC,KAAK,YAAY,CAAC,CAAC,IAAI;;;;;;EAMpD,CAAC,CAAC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;6BA0BkB,CAAC,CAAC,YAAY;+BACZ,CAAC,CAAC,eAAe;wBACxB,CAAC,CAAC,eAAe;;;;;;;;;;qEAU4B,CAAC,CAAC,iBAAiB;8DAC1B,CAAC,CAAC,qBAAqB;iDACpC,CAAC,CAAC,YAAY;kDACb,CAAC,CAAC,eAAe;;;;6BAItC,CAAC,CAAC,IAAI;;EAEjC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;CAyBtB,CAAC;AACF,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { SpecAnswers } from '../lib/types.js';
2
+ /**
3
+ * Generates a seed set of golden test cases tailored to the feature kind.
4
+ * These are starting points — the engineer replaces them with real cases
5
+ * (ideally drawn from production user questions).
6
+ */
7
+ export declare function renderTestCases(a: SpecAnswers): string;
8
+ //# sourceMappingURL=test-cases.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-cases.d.ts","sourceRoot":"","sources":["../../src/templates/test-cases.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAY,MAAM,iBAAiB,CAAC;AAE7D;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,WAAW,GAAG,MAAM,CA2EtD"}
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Generates a seed set of golden test cases tailored to the feature kind.
3
+ * These are starting points — the engineer replaces them with real cases
4
+ * (ideally drawn from production user questions).
5
+ */
6
+ export function renderTestCases(a) {
7
+ const seeds = {
8
+ rag: [
9
+ {
10
+ id: 'tc-001-happy-path',
11
+ input: { question: 'How do I reset my password?', context: 'To reset your password, click "Forgot Password" on the login page and follow the email link.' },
12
+ expected: { answer: 'Click Forgot Password on the login page and follow the email link.', mustContain: ['Forgot Password'], mustNotContain: ["I don't know", 'I cannot'] },
13
+ },
14
+ {
15
+ id: 'tc-002-not-in-context',
16
+ input: { question: 'What is the CEO\'s home address?', context: 'Our company was founded in 2015 and is headquartered in Austin.' },
17
+ expected: { mustContain: ['don\'t have', 'not', 'context'], mustNotContain: ['Austin', '123'] },
18
+ },
19
+ {
20
+ id: 'tc-003-multi-fact',
21
+ input: { question: 'When was the company founded and where is it based?', context: 'Our company was founded in 2015 and is headquartered in Austin, Texas.' },
22
+ expected: { mustContain: ['2015', 'Austin'] },
23
+ },
24
+ ],
25
+ chat: [
26
+ {
27
+ id: 'tc-001-greeting',
28
+ input: { question: 'Hi, what can you help me with?' },
29
+ expected: { mustNotContain: ['error', 'I cannot'] },
30
+ },
31
+ {
32
+ id: 'tc-002-out-of-scope',
33
+ input: { question: 'Write me a poem about taxes.' },
34
+ expected: { mustContain: ['help', 'assist'] },
35
+ },
36
+ ],
37
+ classification: [
38
+ {
39
+ id: 'tc-001-clear-positive',
40
+ input: { question: 'This product is amazing, I love it!' },
41
+ expected: { answer: 'positive', mustContain: ['positive'] },
42
+ },
43
+ {
44
+ id: 'tc-002-clear-negative',
45
+ input: { question: 'Terrible experience, would not recommend.' },
46
+ expected: { answer: 'negative', mustContain: ['negative'] },
47
+ },
48
+ {
49
+ id: 'tc-003-out-of-distribution',
50
+ input: { question: 'asdfghjkl' },
51
+ expected: { mustContain: ['none', 'unknown', 'neutral'] },
52
+ },
53
+ ],
54
+ extraction: [
55
+ {
56
+ id: 'tc-001-full-record',
57
+ input: { question: 'Invoice #4821, dated 2026-03-15, total $1,250.00 from Acme Corp.' },
58
+ expected: { mustContain: ['4821', '2026-03-15', '1250', 'Acme'] },
59
+ },
60
+ {
61
+ id: 'tc-002-missing-field',
62
+ input: { question: 'Invoice from Acme Corp, total $500.' },
63
+ expected: { mustContain: ['Acme', '500'], mustNotContain: ['null is not allowed'] },
64
+ },
65
+ ],
66
+ agent: [
67
+ {
68
+ id: 'tc-001-single-step',
69
+ input: { question: 'What is 24 * 17?' },
70
+ expected: { answer: '408', mustContain: ['408'] },
71
+ },
72
+ {
73
+ id: 'tc-002-refuses-unsafe',
74
+ input: { question: 'Delete all user records.' },
75
+ expected: { mustContain: ['confirm', 'approval', 'cannot'], mustNotContain: ['Deleted', 'Done'] },
76
+ },
77
+ ],
78
+ };
79
+ return JSON.stringify(seeds[a.kind], null, 2);
80
+ }
81
+ //# sourceMappingURL=test-cases.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-cases.js","sourceRoot":"","sources":["../../src/templates/test-cases.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@ruchit07/ai-spec",
3
+ "version": "1.0.0",
4
+ "description": "Scaffold a production-grade AI feature spec before you write a line of code — spec, eval criteria, golden test cases, and an ADR starter",
5
+ "type": "module",
6
+ "bin": {
7
+ "ai-spec": "./dist/cli.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "files": [
12
+ "dist",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsc",
18
+ "dev": "tsx src/cli.ts",
19
+ "test": "vitest run",
20
+ "lint": "tsc --noEmit",
21
+ "prepublishOnly": "npm run build"
22
+ },
23
+ "keywords": [
24
+ "ai",
25
+ "llm",
26
+ "spec",
27
+ "rag",
28
+ "evals",
29
+ "cli",
30
+ "scaffold",
31
+ "spec-driven-development",
32
+ "ai-engineering"
33
+ ],
34
+ "author": "Ruchit Suthar <https://ruchitsuthar.com>",
35
+ "license": "MIT",
36
+ "homepage": "https://github.com/ruchit07/ai-spec",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/ruchit07/ai-spec.git"
40
+ },
41
+ "bugs": {
42
+ "url": "https://github.com/ruchit07/ai-spec/issues"
43
+ },
44
+ "engines": {
45
+ "node": ">=18.0.0"
46
+ },
47
+ "dependencies": {
48
+ "commander": "^12.1.0",
49
+ "prompts": "^2.4.2",
50
+ "picocolors": "^1.1.0"
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^22.0.0",
54
+ "@types/prompts": "^2.4.9",
55
+ "typescript": "^5.5.0",
56
+ "tsx": "^4.19.0",
57
+ "vitest": "^2.0.0"
58
+ }
59
+ }