@rijalpermana/spec-forge 0.2.0 → 0.2.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/README.md CHANGED
@@ -27,7 +27,8 @@ workflow into whichever AI coding tool you use.
27
27
  `prd.md`.
28
28
  - **Sequencing is enforced by guard rails, not convention.** `schema`,
29
29
  `contract`, `tasks`, and `testcase` all refuse to run until `prd.md`
30
- exists, so nothing gets invented without a spec behind it.
30
+ exists, and `implement` refuses until `tasks.md` exists, so nothing gets
31
+ invented — or coded — without a spec behind it.
31
32
  - **One instruction source, many renderers.** Every AI-tool bridge is
32
33
  generated from `src/lib/command-specs.ts` — the instructions are written
33
34
  once and rendered into each tool's native format, so they can't drift out
@@ -74,6 +75,9 @@ forge mockup btn-fraud-check # mockup.html (requires prd.md)
74
75
  forge tasks btn-fraud-check # tasks.md (requires prd.md)
75
76
  forge testcase btn-fraud-check # testcases.md (requires prd.md)
76
77
  forge verify btn-fraud-check # reports missing files / unresolved [TODO]s
78
+
79
+ forge implement btn-fraud-check # checks tasks.md exists, reports grounding files
80
+ # -> your AI tool then writes the actual code
77
81
  ```
78
82
 
79
83
  ## Spec index
@@ -136,6 +140,7 @@ the writing.
136
140
  | `forge mockup <feature>` | `prd.md` | `mockup.html` (stub) | Yes |
137
141
  | `forge tasks <feature>` | `prd.md` | `tasks.md` (stub) | Yes |
138
142
  | `forge testcase <feature>` | `prd.md` | `testcases.md` (stub) | Yes |
143
+ | `forge implement <feature>` | `tasks.md` | Console report of which grounding files exist; no spec file written | N/A — writes application code, not specs |
139
144
  | `forge verify <feature>` | — | Console report, `INDEX.md` status → `active` if complete | N/A (read-only on spec files) |
140
145
 
141
146
  ## Output structure
@@ -175,5 +180,6 @@ the templates and the AI-drafting instructions will drift out of sync.
175
180
  |---|---|---|
176
181
  | `forge: command not found` | `bun link` wasn't run, or shell hasn't reloaded | Re-run `bun link` in the `forge` package directory, restart your shell |
177
182
  | `No prd.md found for "<feature>"` | Tried `schema`/`contract`/`tasks`/`testcase` before `smelt` | Run `forge smelt <feature>` first |
183
+ | `No tasks.md found for "<feature>"` | Tried `implement` before `tasks` | Run `forge tasks <feature>` first |
178
184
  | `<file> already exists — skipping` | The output spec file is already there | Edit it directly, or delete it to regenerate from the template |
179
185
  | `/forge:*` or `/forge-*` commands don't appear in your AI tool | `forge init`/`forge bridge` wasn't run for that target, or the tool needs a restart | Run `forge bridge <target>`, then restart the AI tool's session |
package/dist/cli.js CHANGED
@@ -6901,6 +6901,15 @@ var COMMAND_SPECS = [
6901
6901
 
6902
6902
  ` + "Test Type must be exactly **Positive** or **Negative** (bolded). Actual Result " + "and Status stay as `-` until QA runs them manually."
6903
6903
  },
6904
+ {
6905
+ name: "implement",
6906
+ argumentHint: "[feature-name]",
6907
+ description: "Work through tasks.md and write the actual code for the feature",
6908
+ requiresPrd: true,
6909
+ readOnly: false,
6910
+ writesCode: true,
6911
+ instructions: GROUND_IN_RULES + "This command doesn't scaffold a spec file — it checks that {{tasks}} exists " + "and reports which grounding files (prd, schema, contract, tasks, testcases, " + "mockup, RULES.md) are present. Read all of them, then implement the feature: " + "follow {{tasks}}'s sequencing literally (data/schema before logic before UI, " + "don't reorder around a missing dependency — flag it instead), match {{schema}} " + "for the data layer and {{contract}} exactly for endpoint shapes, use {{mockup}} " + "only as a layout/flow reference (not final markup), and don't invent scope, " + "fields, or endpoints that aren't traceable to {{prd}}. Once implemented, run " + "through {{testcases}} manually and report which rows pass so the user can fill " + "in Actual Result / Status."
6912
+ },
6904
6913
  {
6905
6914
  name: "verify",
6906
6915
  argumentHint: "[feature-name]",
@@ -6947,7 +6956,7 @@ function writeCommandFiles(dir, specs, fileName, render) {
6947
6956
 
6948
6957
  // src/lib/bridges/claude.ts
6949
6958
  function renderClaude(spec) {
6950
- const allowedTools = spec.readOnly ? `Bash(forge ${spec.name}:*), Read` : `Bash(forge ${spec.name}:*), Read, Edit`;
6959
+ const allowedTools = spec.readOnly ? `Bash(forge ${spec.name}:*), Read` : spec.writesCode ? `Bash(forge ${spec.name}:*), Read, Write, Edit` : `Bash(forge ${spec.name}:*), Read, Edit`;
6951
6960
  const body = substitutePaths(spec.instructions, (file) => `specs/$ARGUMENTS/${file}`);
6952
6961
  return `---
6953
6962
  allowed-tools: ${allowedTools}
@@ -7305,9 +7314,33 @@ function makeScaffoldCommand({ file, label }) {
7305
7314
  };
7306
7315
  }
7307
7316
 
7317
+ // src/commands/implement.ts
7318
+ import { join as join10 } from "node:path";
7319
+ function implement(feature) {
7320
+ const cwd = process.cwd();
7321
+ const dir = featureDir(feature, cwd);
7322
+ const tasksPath = join10(dir, SPEC_FILES.tasks);
7323
+ if (!fileExists(tasksPath)) {
7324
+ console.error(`No tasks.md found for "${feature}". Run "forge tasks ${feature}" first ` + `so implementation follows a sequenced plan instead of ad-hoc scope.`);
7325
+ process.exitCode = 1;
7326
+ return;
7327
+ }
7328
+ console.log(`Implementing: ${feature}
7329
+ `);
7330
+ console.log(`Grounding files:`);
7331
+ for (const file of SPEC_FILE_NAMES) {
7332
+ const path = join10(dir, file);
7333
+ console.log(` ${fileExists(path) ? "[OK] " : "[--] "} specs/${feature}/${file}`);
7334
+ }
7335
+ const rulesPath = join10(specsDir(cwd), "RULES.md");
7336
+ console.log(` ${fileExists(rulesPath) ? "[OK] " : "[--] "} specs/RULES.md`);
7337
+ console.log(`
7338
+ Next: work through specs/${feature}/tasks.md in order — data/schema tasks, ` + `then logic, then UI. Ground every change in schema.dbml, api-contract.md, and ` + `specs/RULES.md (if present); don't invent scope beyond prd.md. Once done, run ` + `through testcases.md manually and fill in Actual Result / Status.`);
7339
+ }
7340
+
7308
7341
  // src/commands/verify.ts
7309
7342
  import { readFileSync as readFileSync3 } from "node:fs";
7310
- import { join as join10 } from "node:path";
7343
+ import { join as join11 } from "node:path";
7311
7344
  function verify(feature) {
7312
7345
  const cwd = process.cwd();
7313
7346
  const dir = featureDir(feature, cwd);
@@ -7316,7 +7349,7 @@ function verify(feature) {
7316
7349
  let missing = 0;
7317
7350
  let withTodo = 0;
7318
7351
  for (const file of SPEC_FILE_NAMES) {
7319
- const path = join10(dir, file);
7352
+ const path = join11(dir, file);
7320
7353
  if (!fileExists(path)) {
7321
7354
  console.log(` [MISSING] ${file}`);
7322
7355
  missing++;
@@ -7347,7 +7380,7 @@ function verify(feature) {
7347
7380
 
7348
7381
  // src/commands/scan.ts
7349
7382
  import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "node:fs";
7350
- import { join as join11, extname as extname2, relative, basename as basename2 } from "node:path";
7383
+ import { join as join12, extname as extname2, relative, basename as basename2 } from "node:path";
7351
7384
  var IGNORE_DIRS = new Set([
7352
7385
  "node_modules",
7353
7386
  ".git",
@@ -7376,7 +7409,7 @@ function scan(options = {}) {
7376
7409
  const files = collectFiles(cwd);
7377
7410
  const schemas = files.filter(isSchemaFile);
7378
7411
  const apis = files.filter(isApiFile);
7379
- const raw = readTemplate(join11(templatesDir, "codebase.md"));
7412
+ const raw = readTemplate(join12(templatesDir, "codebase.md"));
7380
7413
  const rendered = renderTemplate(raw, {
7381
7414
  generatedAt: new Date().toISOString().slice(0, 10),
7382
7415
  depth: String(depth),
@@ -7388,7 +7421,7 @@ function scan(options = {}) {
7388
7421
  tree: renderTree(cwd, depth)
7389
7422
  });
7390
7423
  mkdirSync6(specsDir(cwd), { recursive: true });
7391
- const outPath = join11(specsDir(cwd), "CODEBASE.md");
7424
+ const outPath = join12(specsDir(cwd), "CODEBASE.md");
7392
7425
  writeFileSync5(outPath, rendered, "utf-8");
7393
7426
  console.log(`Wrote ${outPath}`);
7394
7427
  console.log(` ${files.length} files scanned — ${schemas.length} schema-ish, ${apis.length} API-ish`);
@@ -7409,9 +7442,9 @@ function collectFiles(root) {
7409
7442
  for (const e of entries) {
7410
7443
  if (e.isDirectory()) {
7411
7444
  if (!IGNORE_DIRS.has(e.name))
7412
- dirs.push(join11(dir, e.name));
7445
+ dirs.push(join12(dir, e.name));
7413
7446
  } else if (e.isFile()) {
7414
- out.push(relative(root, join11(dir, e.name)));
7447
+ out.push(relative(root, join12(dir, e.name)));
7415
7448
  if (out.length >= MAX_FILES)
7416
7449
  break;
7417
7450
  }
@@ -7421,7 +7454,7 @@ function collectFiles(root) {
7421
7454
  }
7422
7455
  function detectStack(root) {
7423
7456
  const found = [];
7424
- const pkgPath = join11(root, "package.json");
7457
+ const pkgPath = join12(root, "package.json");
7425
7458
  if (existsSync4(pkgPath)) {
7426
7459
  let pkg = {};
7427
7460
  try {
@@ -7429,7 +7462,7 @@ function detectStack(root) {
7429
7462
  } catch {}
7430
7463
  const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
7431
7464
  const has = (n) => Object.prototype.hasOwnProperty.call(deps, n);
7432
- const isTs = has("typescript") || existsSync4(join11(root, "tsconfig.json"));
7465
+ const isTs = has("typescript") || existsSync4(join12(root, "tsconfig.json"));
7433
7466
  found.push(`Node.js${isTs ? " / TypeScript" : ""} (package.json)`);
7434
7467
  const frameworks = [
7435
7468
  ["next", "Next.js"],
@@ -7469,7 +7502,7 @@ function detectStack(root) {
7469
7502
  ["Dockerfile", "Docker (Dockerfile)"]
7470
7503
  ];
7471
7504
  for (const [file, label] of manifests)
7472
- if (existsSync4(join11(root, file)))
7505
+ if (existsSync4(join12(root, file)))
7473
7506
  found.push(label);
7474
7507
  return found;
7475
7508
  }
@@ -7497,7 +7530,7 @@ function isApiFile(f) {
7497
7530
  }
7498
7531
  function renderProject(root, fileCount) {
7499
7532
  let name = basename2(root);
7500
- const pkgPath = join11(root, "package.json");
7533
+ const pkgPath = join12(root, "package.json");
7501
7534
  if (existsSync4(pkgPath)) {
7502
7535
  try {
7503
7536
  const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
@@ -7515,7 +7548,7 @@ function renderProject(root, fileCount) {
7515
7548
  "Gemfile",
7516
7549
  "composer.json",
7517
7550
  "pom.xml"
7518
- ].filter((m) => existsSync4(join11(root, m)));
7551
+ ].filter((m) => existsSync4(join12(root, m)));
7519
7552
  return [
7520
7553
  `- **Name:** ${name}`,
7521
7554
  `- **Root:** ${root}`,
@@ -7560,7 +7593,7 @@ function walkTree(dir, prefix, depthLeft, lines) {
7560
7593
  const isLast = i === shown.length - 1 && filtered.length <= CAP;
7561
7594
  lines.push(`${prefix}${isLast ? "└── " : "├── "}${e.name}${e.isDirectory() ? "/" : ""}`);
7562
7595
  if (e.isDirectory()) {
7563
- walkTree(join11(dir, e.name), prefix + (isLast ? " " : "│ "), depthLeft - 1, lines);
7596
+ walkTree(join12(dir, e.name), prefix + (isLast ? " " : "│ "), depthLeft - 1, lines);
7564
7597
  }
7565
7598
  });
7566
7599
  if (filtered.length > CAP) {
@@ -7582,11 +7615,11 @@ function linesOrNone(items, emptyText) {
7582
7615
  }
7583
7616
 
7584
7617
  // src/commands/rules.ts
7585
- import { join as join12 } from "node:path";
7618
+ import { join as join13 } from "node:path";
7586
7619
  function rules() {
7587
7620
  const cwd = process.cwd();
7588
- const outPath = join12(specsDir(cwd), "RULES.md");
7589
- const raw = readTemplate(join12(templatesDir, "rules.md"));
7621
+ const outPath = join13(specsDir(cwd), "RULES.md");
7622
+ const raw = readTemplate(join13(templatesDir, "rules.md"));
7590
7623
  const written = writeIfAbsent(outPath, raw);
7591
7624
  if (!written) {
7592
7625
  console.log(`${outPath} already exists — edit it directly, or delete it to regenerate the template.`);
@@ -7613,5 +7646,6 @@ program2.command("contract <feature>").description("Scaffold api-contract.md (re
7613
7646
  program2.command("mockup <feature>").description("Scaffold mockup.html (requires prd.md)").action(makeScaffoldCommand({ file: SPEC_FILES.mockup, label: "mockup" }));
7614
7647
  program2.command("tasks <feature>").description("Scaffold tasks.md WBS (requires prd.md)").action(makeScaffoldCommand({ file: SPEC_FILES.tasks, label: "tasks" }));
7615
7648
  program2.command("testcase <feature>").description("Scaffold testcases.md (requires prd.md)").action(makeScaffoldCommand({ file: SPEC_FILES.testcases, label: "testcase" }));
7649
+ program2.command("implement <feature>").description("Check the spec set is ready for coding, and report which grounding files exist (requires tasks.md)").action(implement);
7616
7650
  program2.command("verify <feature>").description("Check that all spec files exist and have no unresolved [TODO] markers").action(verify);
7617
7651
  program2.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rijalpermana/spec-forge",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Spec-driven development CLI: scaffolds specs, schemas, API contracts, task lists, and test cases; bridges into Claude Code as slash commands.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -27,6 +27,31 @@
27
27
  `dto/`, `entities/`, `*.controller.ts`, `*.service.ts`, `*.repository.ts`,
28
28
  `*.module.ts`. The AI should scaffold new work to match this.]
29
29
 
30
+ ## Code design principles
31
+
32
+ - **SOLID:**
33
+ - Single Responsibility — each module, class, or function has one reason
34
+ to change; separate data-fetching, business logic, and presentation.
35
+ - Open/Closed — extend behavior through new code (props, composition,
36
+ new implementations of an interface), not by editing working, tested
37
+ code to bolt on a special case.
38
+ - Liskov Substitution — a derived/extended type must work anywhere its
39
+ base type is accepted, without the caller needing to know the
40
+ difference or handle extra edge cases.
41
+ - Interface Segregation — depend only on the fields/methods actually
42
+ used; don't pass a whole object through when one field is needed, and
43
+ don't force implementers to satisfy methods they don't need.
44
+ - Dependency Inversion — depend on abstractions (interfaces, hooks,
45
+ an injected client), not concrete implementations, so the
46
+ implementation can be swapped or mocked without touching the caller.
47
+ - **KISS:** prefer the simplest design that satisfies the requirement.
48
+ No speculative abstraction, config options, or generalization for
49
+ needs that don't exist yet.
50
+ - **DRY:** don't duplicate logic — extract it once a second real
51
+ duplicate appears, not preemptively on the first occurrence. Two
52
+ similar-looking blocks that change for different reasons are not
53
+ duplication; don't force them into one abstraction.
54
+
30
55
  ## API design
31
56
 
32
57
  - Versioning: [TODO — e.g. URI versioning `/v1/`, `/v2/`]
@@ -40,6 +65,11 @@
40
65
  - Audit/base fields every entity carries: [TODO — e.g. created_by, created_date, ...]
41
66
  - Multi-tenancy / scoping: [TODO — or "n/a"]
42
67
  - Type conventions: [TODO]
68
+ - Enums: don't use DBML's `enum {}` type. Model fixed-value fields as
69
+ `varchar`/`text` with a `note:` documenting the allowed values, or as a
70
+ proper lookup/reference table if the values carry their own metadata
71
+ (label, order, active flag) or are managed at runtime. This avoids a
72
+ schema migration every time a value is added or removed.
43
73
 
44
74
  ## Validation
45
75