@rijalpermana/spec-forge 0.1.0 → 0.1.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
@@ -63,6 +63,7 @@ forge --version
63
63
  cd your-project
64
64
  forge init # scaffolds specs/ + AI bridge (defaults to Claude Code)
65
65
  forge scan # (optional) inventory existing code -> specs/CODEBASE.md
66
+ forge rules # (optional) scaffold specs/RULES.md — project conventions
66
67
  forge smelt btn-fraud-check # interactive Q&A -> brief.md + prd.md stub
67
68
 
68
69
  # draft prd.md by hand, or with your AI tool, using brief.md as grounding
@@ -127,6 +128,7 @@ the writing.
127
128
  | `forge init [--target <t>]` | — | `specs/`, `specs/INDEX.md`, AI bridge files | Yes — bridge files regenerate freely |
128
129
  | `forge bridge <target>` | — | AI bridge files for `<target>` | Yes — always regenerates |
129
130
  | `forge scan [--depth <n>]` | — | `specs/CODEBASE.md` (stack, file stats, existing schema/API files, directory tree) | Yes — refreshes on every run |
131
+ | `forge rules` | — | `specs/RULES.md` (project conventions the AI grounds all drafting in) | Yes — refuses if `RULES.md` exists |
130
132
  | `forge smelt <feature>` | — | `brief.md`, `prd.md` (stub), `INDEX.md` entry (`draft`) | Yes — refuses if `brief.md` exists |
131
133
  | `forge schema <feature>` | `prd.md` | `schema.dbml` (stub) | Yes |
132
134
  | `forge contract <feature>` | `prd.md` | `api-contract.md` (stub) | Yes |
@@ -141,6 +143,7 @@ your-project/
141
143
  ├── specs/
142
144
  │ ├── INDEX.md # creation order, status, dependencies across all features
143
145
  │ ├── CODEBASE.md # (optional) forge scan output: existing project inventory
146
+ │ ├── RULES.md # (optional) forge rules output: project conventions the AI follows
144
147
  │ └── <feature-name>/
145
148
  │ ├── brief.md # smelt Q&A capture
146
149
  │ ├── prd.md # goal, actors, user stories, business rules, out-of-scope
package/dist/cli.js CHANGED
@@ -6821,6 +6821,7 @@ function featureDir(feature, cwd = process.cwd()) {
6821
6821
  }
6822
6822
 
6823
6823
  // src/lib/command-specs.ts
6824
+ var GROUND_IN_RULES = "If specs/RULES.md exists, read it first and follow those project conventions — " + "they override any defaults described below. ";
6824
6825
  var COMMAND_SPECS = [
6825
6826
  {
6826
6827
  name: "scan",
@@ -6830,13 +6831,21 @@ var COMMAND_SPECS = [
6830
6831
  readOnly: true,
6831
6832
  instructions: "This command scans the existing project deterministically (no code is " + "interpreted) and writes specs/CODEBASE.md — a factual inventory of the detected " + "stack, file statistics, existing schema and API files, and the directory tree. " + "After it runs, read specs/CODEBASE.md and use it as grounding: when drafting " + "schemas, contracts, or tasks later, reuse the conventions, entities, and files " + "that already exist here instead of inventing new ones. Don't echo the whole file " + "back — just confirm the stack and the spec-relevant files you found."
6832
6833
  },
6834
+ {
6835
+ name: "rules",
6836
+ argumentHint: "(no arguments — scaffolds specs/RULES.md)",
6837
+ description: "Scaffold specs/RULES.md — project conventions the AI follows when drafting",
6838
+ requiresPrd: false,
6839
+ readOnly: false,
6840
+ instructions: "This command scaffolds specs/RULES.md — a project-level conventions file " + "(tech stack, naming, module structure, API/response format, data, validation, " + "security, testing, docs, tooling). After it runs, help the user fill the [TODO] " + "sections: infer what you can from specs/CODEBASE.md and the existing code, and " + "ask about anything you can't. Keep each rule factual and enforceable. Once " + "filled, every other forge drafting command must obey these rules."
6841
+ },
6833
6842
  {
6834
6843
  name: "smelt",
6835
6844
  argumentHint: "[feature-name]",
6836
6845
  description: "Extract raw requirements into a grounding brief, then draft the PRD",
6837
6846
  requiresPrd: false,
6838
6847
  readOnly: false,
6839
- instructions: "This command is interactive it prompts for goal, actors, constraints, and " + "out-of-scope items in the terminal. Once it completes, read {{brief}} and " + "draft {{prd}} following the structure already stubbed there. Do not invent " + "scope, actors, or rules that aren't grounded in the brief — if something's " + "missing, ask what to clarify instead of guessing."
6848
+ instructions: GROUND_IN_RULES + "This command has two input modes. Default: it prompts for goal, actors, " + "constraints, and out-of-scope items in the terminal. With --from <file>: it " + "stages an existing BRD/requirements document verbatim in the feature folder as " + "source-brd.<ext> (extension preserved — .md, .pdf, .docx, …) and leaves " + "'[TODO: extract from source-brd.<ext>]' markers in {{brief}}. If a source-brd.* " + "file exists, read it (if your tool can't open that format — e.g. .docx — say so " + "and ask the user to export it to Markdown or PDF) and replace each marker in " + "{{brief}} with the value extracted from the document, citing the BRD section it " + "came from so every requirement stays traceable. Then read {{brief}} and draft " + "{{prd}} following the structure already stubbed there. Do not invent scope, " + "actors, or rules that aren't grounded in the brief or the BRD — if something's " + "missing, ask what to clarify instead of guessing."
6840
6849
  },
6841
6850
  {
6842
6851
  name: "schema",
@@ -6844,7 +6853,7 @@ var COMMAND_SPECS = [
6844
6853
  description: "Scaffold schema.dbml, then draft it from prd.md",
6845
6854
  requiresPrd: true,
6846
6855
  readOnly: false,
6847
- instructions: "Read {{prd}} and fill {{schema}} with tables, columns, and relationships that " + "are directly traceable to entities, actors, or business rules in the PRD. " + "Follow existing DBML conventions in this repo (multi-tenant RLS pattern, " + "tenant_id scoping, Drizzle-compatible types) if a reference schema exists " + "elsewhere in the project."
6856
+ instructions: GROUND_IN_RULES + "Read {{prd}} and fill {{schema}} with tables, columns, and relationships that " + "are directly traceable to entities, actors, or business rules in the PRD. " + "Follow existing DBML conventions in this repo (multi-tenant RLS pattern, " + "tenant_id scoping, Drizzle-compatible types) if a reference schema exists " + "elsewhere in the project."
6848
6857
  },
6849
6858
  {
6850
6859
  name: "contract",
@@ -6852,7 +6861,7 @@ var COMMAND_SPECS = [
6852
6861
  description: "Scaffold api-contract.md, then draft it from prd.md",
6853
6862
  requiresPrd: true,
6854
6863
  readOnly: false,
6855
- instructions: "Read {{prd}} and fill {{contract}}. Use exactly this format per endpoint, " + `one block per endpoint, no deviation:
6864
+ instructions: GROUND_IN_RULES + "Read {{prd}} and fill {{contract}}. Use exactly this format per endpoint, " + `one block per endpoint, no deviation:
6856
6865
 
6857
6866
  ` + `Title: [short name]
6858
6867
  ` + `endpoint: [METHOD] [path]
@@ -6866,7 +6875,7 @@ var COMMAND_SPECS = [
6866
6875
  description: "Scaffold tasks.md, then draft it from prd.md",
6867
6876
  requiresPrd: true,
6868
6877
  readOnly: false,
6869
- instructions: "Read {{prd}} and {{schema}} (if present), then fill {{tasks}}. Sequence " + "strictly: data/schema tasks before logic tasks before UI tasks. Call out any " + "dependency inversion explicitly rather than silently reordering — if the PRD " + "implies model or logic work before its data source exists, flag it instead of " + "hiding it."
6878
+ instructions: GROUND_IN_RULES + "Read {{prd}} and {{schema}} (if present), then fill {{tasks}}. Sequence " + "strictly: data/schema tasks before logic tasks before UI tasks. Call out any " + "dependency inversion explicitly rather than silently reordering — if the PRD " + "implies model or logic work before its data source exists, flag it instead of " + "hiding it."
6870
6879
  },
6871
6880
  {
6872
6881
  name: "testcase",
@@ -6874,7 +6883,7 @@ var COMMAND_SPECS = [
6874
6883
  description: "Scaffold testcases.md, then draft it from prd.md",
6875
6884
  requiresPrd: true,
6876
6885
  readOnly: false,
6877
- instructions: "Read {{prd}} and fill {{testcases}}. Use exactly this table format, one row " + `per scenario, do not change columns:
6886
+ instructions: GROUND_IN_RULES + "Read {{prd}} and fill {{testcases}}. Use exactly this table format, one row " + `per scenario, do not change columns:
6878
6887
 
6879
6888
  ` + `| No | Test Scenario | Test Case | Test Type | Expected Result | Actual Result | Status | Remark |
6880
6889
  ` + `| ------ | ------ | ------ | ------ | ------ | ------ | ------ | ------ |
@@ -7146,7 +7155,8 @@ Next: forge smelt <feature-name>`);
7146
7155
 
7147
7156
  // src/commands/smelt.ts
7148
7157
  var import_prompts = __toESM(require_prompts3(), 1);
7149
- import { join as join8 } from "node:path";
7158
+ import { copyFileSync, mkdirSync as mkdirSync5 } from "node:fs";
7159
+ import { basename, extname, join as join8, resolve } from "node:path";
7150
7160
 
7151
7161
  // src/lib/template.ts
7152
7162
  import { readFileSync as readFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "node:fs";
@@ -7169,7 +7179,8 @@ function fileExists(path) {
7169
7179
  }
7170
7180
 
7171
7181
  // src/commands/smelt.ts
7172
- async function smelt(feature) {
7182
+ var SOURCE_DOC_BASE = "source-brd";
7183
+ async function smelt(feature, opts = {}) {
7173
7184
  const cwd = process.cwd();
7174
7185
  const dir = featureDir(feature, cwd);
7175
7186
  const briefPath = join8(dir, SPEC_FILES.brief);
@@ -7186,36 +7197,20 @@ async function smelt(feature) {
7186
7197
  console.log(`Existing features: ${existingFeatures.join(", ")}
7187
7198
  `);
7188
7199
  }
7189
- const answers = await import_prompts.default([
7190
- { type: "text", name: "goal", message: "One-sentence goal — what must this feature deliver?" },
7191
- { type: "text", name: "actors", message: "Who are the actors/roles involved?" },
7192
- { type: "text", name: "constraints", message: "Key constraints (regulatory, technical, deadline)?" },
7193
- { type: "text", name: "outOfScope", message: "What's explicitly OUT of scope?" },
7194
- {
7195
- type: "text",
7196
- name: "dependsOn",
7197
- message: "Does this depend on another existing feature? (feature name, or leave blank)"
7198
- }
7199
- ]);
7200
- if (!answers.goal) {
7201
- console.log("Smelting cancelled.");
7200
+ const briefVars = opts.from ? stageSourceDoc(opts.from, dir) : await runInteractive(feature);
7201
+ if (!briefVars)
7202
7202
  return;
7203
- }
7204
7203
  const briefRaw = readTemplate(join8(templatesDir, SPEC_FILES.brief));
7205
7204
  const brief = renderTemplate(briefRaw, {
7206
7205
  feature,
7207
7206
  date: new Date().toISOString().slice(0, 10),
7208
- goal: answers.goal ?? "",
7209
- actors: answers.actors ?? "",
7210
- constraints: answers.constraints ?? "",
7211
- outOfScope: answers.outOfScope ?? ""
7207
+ ...briefVars
7212
7208
  });
7213
7209
  writeIfAbsent(briefPath, brief);
7214
7210
  const prdRaw = readTemplate(join8(templatesDir, SPEC_FILES.prd));
7215
7211
  const prd = renderTemplate(prdRaw, { feature });
7216
7212
  const prdWritten = writeIfAbsent(prdPath, prd);
7217
- const dependsOn = answers.dependsOn?.trim() || "-";
7218
- upsertIndexEntry(cwd, feature, { status: "draft", dependsOn });
7213
+ upsertIndexEntry(cwd, feature, { status: "draft", dependsOn: briefVars.dependsOn });
7219
7214
  console.log(`
7220
7215
  Wrote ${briefPath}`);
7221
7216
  if (prdWritten)
@@ -7224,6 +7219,55 @@ Wrote ${briefPath}`);
7224
7219
  console.log(`
7225
7220
  Next: open this project in your AI tool and draft prd.md from brief.md ` + `— use the /forge:smelt, /forge-smelt, or AGENTS.md instructions, depending ` + `on which bridge you initialized.`);
7226
7221
  }
7222
+ function stageSourceDoc(from, dir) {
7223
+ const srcPath = resolve(process.cwd(), from);
7224
+ if (!fileExists(srcPath)) {
7225
+ console.log(`Source document not found: ${srcPath}`);
7226
+ process.exitCode = 1;
7227
+ return;
7228
+ }
7229
+ const docName = `${SOURCE_DOC_BASE}${extname(srcPath).toLowerCase()}`;
7230
+ const destPath = join8(dir, docName);
7231
+ if (fileExists(destPath)) {
7232
+ console.log(`${destPath} already exists — leaving it in place.`);
7233
+ } else {
7234
+ mkdirSync5(dir, { recursive: true });
7235
+ copyFileSync(srcPath, destPath);
7236
+ console.log(`Staged ${basename(srcPath)} → ${destPath}`);
7237
+ }
7238
+ const marker = `[TODO: extract from ${docName}]`;
7239
+ return {
7240
+ goal: marker,
7241
+ actors: marker,
7242
+ constraints: marker,
7243
+ outOfScope: marker,
7244
+ dependsOn: "-"
7245
+ };
7246
+ }
7247
+ async function runInteractive(feature) {
7248
+ const answers = await import_prompts.default([
7249
+ { type: "text", name: "goal", message: "One-sentence goal — what must this feature deliver?" },
7250
+ { type: "text", name: "actors", message: "Who are the actors/roles involved?" },
7251
+ { type: "text", name: "constraints", message: "Key constraints (regulatory, technical, deadline)?" },
7252
+ { type: "text", name: "outOfScope", message: "What's explicitly OUT of scope?" },
7253
+ {
7254
+ type: "text",
7255
+ name: "dependsOn",
7256
+ message: "Does this depend on another existing feature? (feature name, or leave blank)"
7257
+ }
7258
+ ]);
7259
+ if (!answers.goal) {
7260
+ console.log("Smelting cancelled.");
7261
+ return null;
7262
+ }
7263
+ return {
7264
+ goal: answers.goal ?? "",
7265
+ actors: answers.actors ?? "",
7266
+ constraints: answers.constraints ?? "",
7267
+ outOfScope: answers.outOfScope ?? "",
7268
+ dependsOn: answers.dependsOn?.trim() || "-"
7269
+ };
7270
+ }
7227
7271
 
7228
7272
  // src/commands/scaffold.ts
7229
7273
  import { join as join9 } from "node:path";
@@ -7290,8 +7334,8 @@ function verify(feature) {
7290
7334
  }
7291
7335
 
7292
7336
  // src/commands/scan.ts
7293
- import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
7294
- import { join as join11, extname, relative, basename } from "node:path";
7337
+ import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "node:fs";
7338
+ import { join as join11, extname as extname2, relative, basename as basename2 } from "node:path";
7295
7339
  var IGNORE_DIRS = new Set([
7296
7340
  "node_modules",
7297
7341
  ".git",
@@ -7331,7 +7375,7 @@ function scan(options = {}) {
7331
7375
  apis: pathListOrNone(apis),
7332
7376
  tree: renderTree(cwd, depth)
7333
7377
  });
7334
- mkdirSync5(specsDir(cwd), { recursive: true });
7378
+ mkdirSync6(specsDir(cwd), { recursive: true });
7335
7379
  const outPath = join11(specsDir(cwd), "CODEBASE.md");
7336
7380
  writeFileSync5(outPath, rendered, "utf-8");
7337
7381
  console.log(`Wrote ${outPath}`);
@@ -7418,8 +7462,8 @@ function detectStack(root) {
7418
7462
  return found;
7419
7463
  }
7420
7464
  function isSchemaFile(f) {
7421
- const ext = extname(f).toLowerCase();
7422
- const base = basename(f).toLowerCase();
7465
+ const ext = extname2(f).toLowerCase();
7466
+ const base = basename2(f).toLowerCase();
7423
7467
  if (ext === ".dbml" || ext === ".prisma" || ext === ".sql")
7424
7468
  return true;
7425
7469
  if (/schema/.test(base) && [".ts", ".js", ".py", ".rb", ".go", ".json"].includes(ext))
@@ -7430,7 +7474,7 @@ function isSchemaFile(f) {
7430
7474
  }
7431
7475
  function isApiFile(f) {
7432
7476
  const p = f.toLowerCase();
7433
- const base = basename(p);
7477
+ const base = basename2(p);
7434
7478
  if (/(openapi|swagger)\.(ya?ml|json)$/.test(base))
7435
7479
  return true;
7436
7480
  if (/\.(controller|route|router|resolver)\.(ts|js|py)$/.test(base))
@@ -7440,7 +7484,7 @@ function isApiFile(f) {
7440
7484
  return false;
7441
7485
  }
7442
7486
  function renderProject(root, fileCount) {
7443
- let name = basename(root);
7487
+ let name = basename2(root);
7444
7488
  const pkgPath = join11(root, "package.json");
7445
7489
  if (existsSync4(pkgPath)) {
7446
7490
  try {
@@ -7471,7 +7515,7 @@ function renderProject(root, fileCount) {
7471
7515
  function fileStats(files) {
7472
7516
  const counts = new Map;
7473
7517
  for (const f of files) {
7474
- const ext = extname(f).toLowerCase() || "(no ext)";
7518
+ const ext = extname2(f).toLowerCase() || "(no ext)";
7475
7519
  counts.set(ext, (counts.get(ext) ?? 0) + 1);
7476
7520
  }
7477
7521
  const top = [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 12);
@@ -7479,7 +7523,7 @@ function fileStats(files) {
7479
7523
  `);
7480
7524
  }
7481
7525
  function renderTree(root, maxDepth) {
7482
- const lines = [`${basename(root) || root}/`];
7526
+ const lines = [`${basename2(root) || root}/`];
7483
7527
  walkTree(root, "", maxDepth, lines);
7484
7528
  return lines.join(`
7485
7529
  `);
@@ -7525,13 +7569,31 @@ function linesOrNone(items, emptyText) {
7525
7569
  `) : emptyText;
7526
7570
  }
7527
7571
 
7572
+ // src/commands/rules.ts
7573
+ import { join as join12 } from "node:path";
7574
+ function rules() {
7575
+ const cwd = process.cwd();
7576
+ const outPath = join12(specsDir(cwd), "RULES.md");
7577
+ const raw = readTemplate(join12(templatesDir, "rules.md"));
7578
+ const written = writeIfAbsent(outPath, raw);
7579
+ if (!written) {
7580
+ console.log(`${outPath} already exists — edit it directly, or delete it to regenerate the template.`);
7581
+ return;
7582
+ }
7583
+ console.log(`Wrote ${outPath}`);
7584
+ console.log(` Fill in the [TODO] sections with your project's conventions.`);
7585
+ console.log(`
7586
+ Every forge drafting command (smelt, schema, contract, tasks, testcase) ` + `will ground the AI in these rules.`);
7587
+ }
7588
+
7528
7589
  // src/cli.ts
7529
7590
  var program2 = new Command;
7530
- program2.name("forge").description("Spec-driven development CLI — scaffolds specs, schemas, API contracts, tasks, and test cases.").version("0.1.0");
7591
+ program2.name("forge").description("Spec-driven development CLI — scaffolds specs, schemas, API contracts, tasks, and test cases.").version("0.1.2");
7531
7592
  program2.command("init").description("Scaffold specs/ folder and an AI-tool bridge in the current project").option("-t, --target <target>", "bridge target: claude, cursor, windsurf, or generic", "claude").action((options) => init({ target: options.target }));
7532
7593
  program2.command("bridge <target>").description("(Re)generate AI-tool bridge commands: claude, cursor, windsurf, or generic").action((target) => bridge(target));
7533
7594
  program2.command("scan").description("Inventory the existing project structure into specs/CODEBASE.md").option("-d, --depth <n>", "directory-tree depth to include in the report", "2").action((options) => scan({ depth: Number(options.depth) }));
7534
- program2.command("smelt <feature>").description("Extract raw requirements into a grounding brief and stub the PRD").action(smelt);
7595
+ program2.command("rules").description("Scaffold specs/RULES.md project conventions the AI follows when drafting").action(() => rules());
7596
+ program2.command("smelt <feature>").description("Extract raw requirements into a grounding brief and stub the PRD").option("-f, --from <file>", "extract from an existing BRD/requirements document instead of interactive Q&A").action((feature, options) => smelt(feature, { from: options.from }));
7535
7597
  program2.command("schema <feature>").description("Scaffold schema.dbml (requires prd.md)").action(makeScaffoldCommand({ file: SPEC_FILES.schema, label: "schema" }));
7536
7598
  program2.command("contract <feature>").description("Scaffold api-contract.md (requires prd.md)").action(makeScaffoldCommand({ file: SPEC_FILES.contract, label: "contract" }));
7537
7599
  program2.command("tasks <feature>").description("Scaffold tasks.md WBS (requires prd.md)").action(makeScaffoldCommand({ file: SPEC_FILES.tasks, label: "tasks" }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rijalpermana/spec-forge",
3
- "version": "0.1.0",
3
+ "version": "0.1.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",
@@ -0,0 +1,71 @@
1
+ # Project Rules & Conventions
2
+
3
+ > Read by the AI before drafting any spec (schema, contract, tasks, testcases)
4
+ > through `forge`. Rules here **override** forge's built-in defaults. Fill every
5
+ > `[TODO]`, delete sections that don't apply, and keep entries factual and
6
+ > enforceable — the AI follows this literally.
7
+
8
+ ## Tech stack
9
+
10
+ - Language / runtime: [TODO]
11
+ - Framework: [TODO]
12
+ - Database / ORM: [TODO]
13
+ - Other key libraries: [TODO]
14
+
15
+ ## Naming conventions
16
+
17
+ - Folders & files: [TODO — e.g. kebab-case]
18
+ - Classes / types: [TODO — e.g. PascalCase]
19
+ - Variables & functions: [TODO — e.g. camelCase]
20
+ - Database columns: [TODO — e.g. snake_case]
21
+ - API endpoints / paths: [TODO — e.g. kebab-case, `/v1/...`]
22
+ - Forbidden: [TODO — e.g. no underscores in file/folder names]
23
+
24
+ ## Project & module structure
25
+
26
+ [TODO — describe the folder layout a new feature/module must follow, e.g.
27
+ `dto/`, `entities/`, `*.controller.ts`, `*.service.ts`, `*.repository.ts`,
28
+ `*.module.ts`. The AI should scaffold new work to match this.]
29
+
30
+ ## API design
31
+
32
+ - Versioning: [TODO — e.g. URI versioning `/v1/`, `/v2/`]
33
+ - Auth: [TODO — e.g. global guard, how to opt an endpoint out]
34
+ - Response envelope: [TODO — paste the exact success shape the API returns]
35
+ - Error shape & status: [TODO — how errors are formatted and what HTTP status is used]
36
+ - Pagination: [TODO — shape of paginated responses]
37
+
38
+ ## Data & schema
39
+
40
+ - Audit/base fields every entity carries: [TODO — e.g. created_by, created_date, ...]
41
+ - Multi-tenancy / scoping: [TODO — or "n/a"]
42
+ - Type conventions: [TODO]
43
+
44
+ ## Validation
45
+
46
+ [TODO — e.g. all inputs validated with a schema/validator; every field documented with an example]
47
+
48
+ ## Security
49
+
50
+ [TODO — e.g. secrets from env only; encryption standard; required headers; rate limiting]
51
+
52
+ ## Testing
53
+
54
+ - Framework: [TODO]
55
+ - Coverage threshold: [TODO — e.g. 80% branches/functions/lines/statements, build fails below]
56
+ - Test file convention: [TODO — e.g. `*.spec.ts` beside the file under test]
57
+ - Minimum per unit: [TODO — e.g. 1 success case + 1 error case]
58
+
59
+ ## Documentation
60
+
61
+ [TODO — e.g. OpenAPI/Swagger required; every endpoint and DTO documented with examples]
62
+
63
+ ## Tooling
64
+
65
+ - Lint: [TODO]
66
+ - Format: [TODO]
67
+ - Pre-commit checks: [TODO]
68
+
69
+ ## Out of bounds
70
+
71
+ [TODO — anything the AI must never do in this codebase]