@rijalpermana/spec-forge 0.2.1 → 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 +7 -1
- package/dist/cli.js +51 -17
- package/package.json +1 -1
- package/templates/rules.md +5 -0
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,
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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 =
|
|
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(
|
|
7445
|
+
dirs.push(join12(dir, e.name));
|
|
7413
7446
|
} else if (e.isFile()) {
|
|
7414
|
-
out.push(relative(root,
|
|
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 =
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
|
7618
|
+
import { join as join13 } from "node:path";
|
|
7586
7619
|
function rules() {
|
|
7587
7620
|
const cwd = process.cwd();
|
|
7588
|
-
const outPath =
|
|
7589
|
-
const raw = readTemplate(
|
|
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.
|
|
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",
|
package/templates/rules.md
CHANGED
|
@@ -65,6 +65,11 @@
|
|
|
65
65
|
- Audit/base fields every entity carries: [TODO — e.g. created_by, created_date, ...]
|
|
66
66
|
- Multi-tenancy / scoping: [TODO — or "n/a"]
|
|
67
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.
|
|
68
73
|
|
|
69
74
|
## Validation
|
|
70
75
|
|