diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f28a70..f653b7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ Each released version is tagged in git (`v0.0.1`, `v0.1.0`, etc.) and includes t ## [Unreleased] +### Added + +- **`.draftwise/constitution.md` — user-editable rules file the host coding agent reads on every drafting command.** `draftwise init` writes a default template into the project on both the brownfield and greenfield paths. Five stable section headings every prompt module references by name: **Voice** (how the agent talks — no filler, push back on weak ideas, ground every claim, turn gaps into questions), **Spec language** (specific over generic, active voice, same term every time), **Edge case discipline** (technical specs only — empty data / errors / loading / permissions / concurrency / large data), **Project conventions** (placeholder filled in by `scan` from observed code or by the greenfield Phase 3 from the chosen stack), and **Domain glossary** (empty by default; project-specific terms). Replaces the role that `src/ai/prompts/principles.js` and `src/ai/prompts/spec-quality.js` used to play before the api-mode drop — those modules injected collaboration + spec-language rules into every system prompt; with synthesis moved entirely into the host coding agent, those rules now live in the user's repo as a markdown file the agent reads on each call. Every drafting prompt (`new`, `tech`, `tasks`, `scan`, `explain`, `greenfield` Phase 3) got a one-line "before drafting, read `.draftwise/constitution.md` if it exists and apply its Voice / Spec language / Edge case discipline sections; skip silently if absent" — back-compat for projects that ran `init` before this feature landed. `scan` and the greenfield Phase 3 additionally instruct the agent to refine the **Project conventions** section from observed code / chosen stack, replacing the placeholder. Reader utility at `src/utils/constitution.js` (`readConstitution(cwd)` returns null on ENOENT, propagates other errors); template at `src/utils/constitution-template.js`. Why: with the api path gone, the project needed somewhere stable for users to tune Draftwise's voice and spec discipline without editing prompts in the npm package — the constitution is that surface, and it's version-controlled alongside the code so changes are reviewable. — Ankur + ### Removed - **`@inquirer/prompts` dropped; CLI is purely flag-driven now.** Every interactive prompt is gone: init's idea input, tech/tasks' spec picker, and scaffold's confirm. Each was a TTY-only convenience layer on top of flags that already drove the canonical input path. With api mode gone (see entry below), the remaining prompts had nothing meaningful to gate — the CLI's job is to load context and hand off to the host coding agent, not run a Q&A loop. Concrete behavior changes: `tech` / `tasks` with multiple specs and no slug arg now error with the available list (was: TTY → inquirer picker, non-TTY → error); `scaffold` requires `--yes` to confirm before writing (was: TTY → inquirer confirm, non-TTY → error); `init` greenfield without `--idea` always prints the structured agent handoff (was: TTY → inquirer input, non-TTY → handoff). The TTY/non-TTY distinction is gone entirely — `src/utils/tty.js`, `test/setup.js`, and `vitest.config.js` are deleted; `deps.isInteractive` is no longer a dependency-injection seam. CLAUDE.md and README.md updated to drop "TTY-only fallback" framing and replace with "flags drive input; CLI never blocks on stdin." — Ankur diff --git a/CLAUDE.md b/CLAUDE.md index a08f50e..ed605cf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,7 +40,7 @@ src/index.js → command router (dynamic imports, help) src/commands/ → one file per CLI command, default export async fn src/core/scanner.js → codebase scanning (frameworks, routes, components, models) src/ai/prompts/ → one prompt module per command. Each exports a `buildAgentInstruction(...)` (or `AGENT_INSTRUCTION` constant for `scan`) that the host coding agent reads — section structure, hard rules, save path. No SDK call from the CLI; the agent does the synthesis. -src/utils/ → config.js (yaml loader; returns projectState/stack/scanMaxFiles), specs.js (list .draftwise/specs/), slug.js, overview.js (read .draftwise/overview.md for greenfield context), scan-cache.js (fingerprinted scan cache, drop-in for scan()), flow-filter.js (narrow scan to flow-relevant items), scan-warnings.js (truncation + missing-framework messages), fs.js (shared pathExists), scan-projection.js (shared compactScan that trims a raw scan into a prompt-sized projection), scan-context.js (shared greenfield/brownfield branch for new/tech/tasks), draftwise-dir.js (`requireDraftwiseDir` guard), agent-handoff.js (shared orienting prefix logged before every agent-mode handoff), project-state.js (filesystem auto-detect for `init` — bail-fast walk for source files using scanner's IGNORE_DIRS + CODE_EXTENSIONS), skill-providers.js (provider dir mapping + Claude-only frontmatter trim + `detectInstalledProviders` filesystem check shared across `skills install` / `uninstall` / `help`) +src/utils/ → config.js (yaml loader; returns projectState/stack/scanMaxFiles), specs.js (list .draftwise/specs/), slug.js, overview.js (read .draftwise/overview.md for greenfield context), scan-cache.js (fingerprinted scan cache, drop-in for scan()), flow-filter.js (narrow scan to flow-relevant items), scan-warnings.js (truncation + missing-framework messages), fs.js (shared pathExists), scan-projection.js (shared compactScan that trims a raw scan into a prompt-sized projection), scan-context.js (shared greenfield/brownfield branch for new/tech/tasks), draftwise-dir.js (`requireDraftwiseDir` guard), agent-handoff.js (shared orienting prefix logged before every agent-mode handoff), project-state.js (filesystem auto-detect for `init` — bail-fast walk for source files using scanner's IGNORE_DIRS + CODE_EXTENSIONS), skill-providers.js (provider dir mapping + Claude-only frontmatter trim + `detectInstalledProviders` filesystem check shared across `skills install` / `uninstall` / `help`), constitution.js (`readConstitution(cwd)` — reads `.draftwise/constitution.md`; returns null on ENOENT for back-compat with pre-constitution projects), constitution-template.js (default template `init` writes — five stable section headings prompts reference by name) test/ → vitest, mirrors src structure .claude-plugin/ → plugin marketplace declaration (see "Claude Code plugin" below) plugin/ → plugin source tree shipped via the marketplace @@ -106,6 +106,8 @@ scan: **Flags drive input; no interactive prompts.** Every command takes its full input set as flags (`--mode`, `--idea`, `--yes`) or positional args, parsed via Node's built-in `util.parseArgs`. Missing required input errors with a specific usage hint — the CLI never blocks waiting for stdin. `draftwise init` is the one special case: greenfield without `--idea` prints a structured **agent handoff** (the question in chat-friendly format + a re-invocation template, all under `AGENT_HANDOFF_PREFIX`) and exits cleanly, so the host coding agent reads stderr, asks the user in chat, and re-invokes with the collected flag. A plain-terminal user reads the same handoff as a usage hint. +**The constitution is the user's voice + spec-quality dial.** `draftwise init` writes `.draftwise/constitution.md` with five stable sections — Voice, Spec language, Edge case discipline, Project conventions, Domain glossary. Every drafting prompt (`new`, `tech`, `tasks`, `scan`, `explain`, greenfield Phase 3) tells the host agent to read this file before drafting and apply the relevant sections. `scan` and the greenfield Phase 3 also refine the **Project conventions** section from observed code / chosen stack, replacing the placeholder. The file is user-editable and version-controlled — changes are reviewable and travel with the repo. Backward compat: prompts say "skip silently if absent" so projects that ran `init` before this feature landed keep working. Reader at `src/utils/constitution.js` returns null on ENOENT. Replaces the role `principles.js` + `spec-quality.js` used to play before the api-mode drop — those modules injected rules into every system prompt; the constitution is the new home for those rules now that synthesis lives in the host agent. + **Single repo, single feature spec at a time.** No cross-spec dependency tracking. No multi-repo. Keep scope tight. --- @@ -138,6 +140,7 @@ Each command is a separate file under `src/commands/` with a single `export defa ├── .gitignore # written by init; excludes .cache/ from version control ├── .cache/ │ └── scan.json # fingerprinted scan cache (gitignored) +├── constitution.md # voice + spec-quality rules — agent reads on every drafting command ├── overview.md # codebase summary (brownfield) or greenfield plan ├── scaffold.json # greenfield only: structured stack data for `draftwise scaffold` ├── flows/ # `draftwise explain` snapshots (brownfield) diff --git a/README.md b/README.md index 9e8acbb..84f5519 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,7 @@ Run `draftwise --help` for the per-command flag list. ``` .draftwise/ ├── .gitignore # written by init; keeps the cache out of version control +├── constitution.md # voice + spec-quality rules — edit to suit your project ├── overview.md # codebase summary (brownfield) or greenfield plan ├── scaffold.json # greenfield only; structured stack data for `draftwise scaffold` ├── specs/ diff --git a/src/ai/prompts/explain.js b/src/ai/prompts/explain.js index 69ef7a5..1312166 100644 --- a/src/ai/prompts/explain.js +++ b/src/ai/prompts/explain.js @@ -1,5 +1,9 @@ export function buildAgentInstruction(flow, slug) { - return `The scanner data above describes a real codebase. The user wants to understand how the "${flow}" flow works. Generate a markdown walkthrough following this section structure, grounded only in what the scanner produced: + return `The scanner data above describes a real codebase. The user wants to understand how the "${flow}" flow works. + +Before writing the walkthrough, read .draftwise/constitution.md if it exists and apply its Voice and Spec language sections. Skip silently if the file is absent. + +Generate a markdown walkthrough following this section structure, grounded only in what the scanner produced: # Flow: ${flow} > One-sentence summary of what this flow does, inferred from the code. diff --git a/src/ai/prompts/greenfield.js b/src/ai/prompts/greenfield.js index 75d5615..4667055 100644 --- a/src/ai/prompts/greenfield.js +++ b/src/ai/prompts/greenfield.js @@ -13,6 +13,7 @@ PHASE 2 — Recommend stacks: PHASE 3 — Write the plan: - Save the chosen stack and the conversation as .draftwise/overview.md, with sections: Idea, Discovery (Q&A), Chosen stack (name, summary, rationale, pros, cons), Directory structure, Initial files, Setup commands, Next steps. + - Also refine .draftwise/constitution.md (which init wrote with default text). Replace the Project conventions section with the chosen stack's directory layout and naming conventions; leave the Voice, Spec language, and Edge case discipline sections intact unless the user asked you to change them. - Also save .draftwise/scaffold.json with the structured stack data so \`draftwise scaffold\` can use it later. Shape: { "stack": "", diff --git a/src/ai/prompts/new.js b/src/ai/prompts/new.js index ede5939..8038500 100644 --- a/src/ai/prompts/new.js +++ b/src/ai/prompts/new.js @@ -2,6 +2,8 @@ export function buildAgentInstruction(idea, projectState = 'brownfield') { if (projectState === 'greenfield') { return `The PM has proposed a feature for a GREENFIELD project: "${idea}". +Before drafting, read .draftwise/constitution.md if it exists and apply its Voice and Spec language sections to the conversation and the final spec. Skip silently if the file is absent. + The project plan (overview.md above) describes the chosen stack and directory structure. There is no existing code yet. Run a conversation with the PM in three phases: PHASE 1 — Plan the conversation: @@ -21,6 +23,8 @@ Hard rule: ASK don't assume; ground every claim in the answers and the project p } return `The PM has proposed: "${idea}". +Before drafting, read .draftwise/constitution.md if it exists and apply its Voice and Spec language sections to the conversation and the final spec. Skip silently if the file is absent. + Use the scanner data above as ground truth for the existing codebase. Run a conversation with the PM in three phases: PHASE 1 — Plan the conversation (in your head, but share the plan with the PM): diff --git a/src/ai/prompts/scan.js b/src/ai/prompts/scan.js index 45ad47d..e736cfe 100644 --- a/src/ai/prompts/scan.js +++ b/src/ai/prompts/scan.js @@ -1,4 +1,8 @@ -export const AGENT_INSTRUCTION = `The scanner data above describes a real codebase. Generate an overview.md grounded only in what the scanner produced. Use these top-level sections, in order: +export const AGENT_INSTRUCTION = `The scanner data above describes a real codebase. + +Before writing the overview, read .draftwise/constitution.md if it exists and apply its Voice and Spec language sections. After writing overview.md, also refine the Project conventions section of constitution.md with the naming patterns, directory layout, and architectural style you observe in the scanner data — replacing the placeholder text. Skip both steps silently if constitution.md is absent. + +Generate an overview.md grounded only in what the scanner produced. Use these top-level sections, in order: # (use the package name as a starting point) > One-sentence description of what this product appears to do, inferred from the codebase. diff --git a/src/ai/prompts/tasks.js b/src/ai/prompts/tasks.js index c0cf010..be17c4b 100644 --- a/src/ai/prompts/tasks.js +++ b/src/ai/prompts/tasks.js @@ -2,6 +2,8 @@ export function buildAgentInstruction(slug, projectState = 'brownfield') { if (projectState === 'greenfield') { return `The technical spec above is approved. The project is GREENFIELD — there's no existing code yet. The chosen stack and directory plan are in overview.md (above). +Before drafting, read .draftwise/constitution.md if it exists and apply its Voice and Spec language sections. Skip silently if the file is absent. + Generate tasks.md and save it to .draftwise/specs/${slug}/tasks.md. Sections in order: @@ -16,7 +18,11 @@ Hard rules: - The first 1-3 tasks must be foundational scaffolding (run setup commands, install deps, configure env). Don't skip them. - Each "Depends on" link must point at a task number you've actually defined.`; } - return `The technical spec above is approved. Use the scanner data as ground truth. Generate tasks.md following the section structure below, ordered so each task's dependencies appear before it. Save it to .draftwise/specs/${slug}/tasks.md. + return `The technical spec above is approved. Use the scanner data as ground truth. + +Before drafting, read .draftwise/constitution.md if it exists and apply its Voice and Spec language sections. Skip silently if the file is absent. + +Generate tasks.md following the section structure below, ordered so each task's dependencies appear before it. Save it to .draftwise/specs/${slug}/tasks.md. Sections in order: - Title + one-sentence framing diff --git a/src/ai/prompts/tech.js b/src/ai/prompts/tech.js index a73833e..cba39ea 100644 --- a/src/ai/prompts/tech.js +++ b/src/ai/prompts/tech.js @@ -2,6 +2,8 @@ export function buildAgentInstruction(slug, projectState = 'brownfield') { if (projectState === 'greenfield') { return `The product spec above is approved. The project is GREENFIELD — there's no existing code yet. The chosen stack and directory plan are in overview.md (above). +Before drafting, read .draftwise/constitution.md if it exists and apply its Voice, Spec language, and Edge case discipline sections. Skip silently if the file is absent. + Generate technical-spec.md and save it to .draftwise/specs/${slug}/technical-spec.md. Sections in order: @@ -16,7 +18,11 @@ Sections in order: Hard rule: every file path must be marked "(new)" and must follow the directory structure from overview.md. Match the chosen stack's conventions exactly — don't impose foreign patterns.`; } - return `The product spec above is approved. Use the scanner data as ground truth for the existing codebase. Generate the technical-spec.md following the section structure below, grounded in real files/routes/models from the scanner. Save it to .draftwise/specs/${slug}/technical-spec.md. + return `The product spec above is approved. Use the scanner data as ground truth for the existing codebase. + +Before drafting, read .draftwise/constitution.md if it exists and apply its Voice, Spec language, and Edge case discipline sections. Skip silently if the file is absent. + +Generate the technical-spec.md following the section structure below, grounded in real files/routes/models from the scanner. Save it to .draftwise/specs/${slug}/technical-spec.md. Sections in order: - Title + one-sentence framing diff --git a/src/commands/init.js b/src/commands/init.js index a275d85..616a6f3 100644 --- a/src/commands/init.js +++ b/src/commands/init.js @@ -8,6 +8,7 @@ import { pathExists } from '../utils/fs.js'; import { AGENT_HANDOFF_PREFIX } from '../utils/agent-handoff.js'; import { detectProjectState as defaultDetectProjectState } from '../utils/project-state.js'; import { buildAgentInstruction as buildGreenfieldAgentInstruction } from '../ai/prompts/greenfield.js'; +import { CONSTITUTION_TEMPLATE } from '../utils/constitution-template.js'; export const HELP = `draftwise init — set up .draftwise/ for the current project @@ -140,12 +141,18 @@ async function runBrownfield({ cwd, log, scan, draftwiseDir }) { 'utf8', ); await writeFile(join(draftwiseDir, '.gitignore'), DRAFTWISE_GITIGNORE, 'utf8'); + await writeFile( + join(draftwiseDir, 'constitution.md'), + CONSTITUTION_TEMPLATE, + 'utf8', + ); log('Created .draftwise/ with:'); - log(' • overview.md (placeholder — `draftwise scan` will fill it in)'); - log(' • specs/ (your specs will live here)'); - log(' • config.yaml (project state)'); - log(' • .gitignore (excludes .cache/ from version control)'); + log(' • overview.md (placeholder — `draftwise scan` will fill it in)'); + log(' • constitution.md (voice + spec-quality rules — edit to suit your project)'); + log(' • specs/ (your specs will live here)'); + log(' • config.yaml (project state)'); + log(' • .gitignore (excludes .cache/ from version control)'); log(''); log( 'Run draftwise commands inside your coding agent (Claude Code, Cursor, etc.).', @@ -178,12 +185,18 @@ async function runGreenfield({ log, draftwiseDir, idea }) { 'utf8', ); await writeFile(join(draftwiseDir, '.gitignore'), DRAFTWISE_GITIGNORE, 'utf8'); + await writeFile( + join(draftwiseDir, 'constitution.md'), + CONSTITUTION_TEMPLATE, + 'utf8', + ); log('Created .draftwise/ with:'); - log(' • overview.md (placeholder — your agent will rewrite from the conversation)'); - log(' • specs/ (your specs will live here)'); - log(' • config.yaml (project state)'); - log(' • .gitignore (excludes .cache/ from version control)'); + log(' • overview.md (placeholder — your agent will rewrite from the conversation)'); + log(' • constitution.md (voice + spec-quality rules — edit to suit your project)'); + log(' • specs/ (your specs will live here)'); + log(' • config.yaml (project state)'); + log(' • .gitignore (excludes .cache/ from version control)'); log(''); log('Run draftwise commands inside your coding agent (Claude Code, Cursor, etc.).'); } diff --git a/src/utils/constitution-template.js b/src/utils/constitution-template.js new file mode 100644 index 0000000..414ee46 --- /dev/null +++ b/src/utils/constitution-template.js @@ -0,0 +1,83 @@ +export const CONSTITUTION_TEMPLATE = `# Draftwise constitution + +This file is the source of truth for how Draftwise drafts and refines specs in +this project. The host coding agent reads it before every \`draftwise new\`, +\`tech\`, \`tasks\`, \`scan\`, \`explain\`, \`clarify\`, and \`refine\` call and +applies the rules below to its output. + +You own this file. Edit it freely — the rules below are sensible defaults, not +constraints. Tune them to match how your team writes, what your codebase looks +like, and what your domain expects. + +## Voice + +Draftwise produces specs through conversation, not form-filling. The agent +should: + +- Skip filler. No "Great question!" or "I'd be happy to help." Get to the + substance. +- Redirect drift. If the user goes off-topic, name it and route back to the + spec. +- Push back on weak ideas instead of repackaging them as agreement. Better + outputs come from real friction. +- Extend existing architecture before adding new pieces. A new component is the + last resort, not the first. +- Flag bad assumptions before drafting. Mark uncertain claims explicitly. Offer + the counter-case on strategic decisions. +- Ground every claim. Cite a real file / route / model from the scanner — or + mark it \`(new)\` for greenfield. Never fabricate paths to fill a section. +- Turn every gap into a question, not an assumption. + +## Spec language + +When writing or refining a spec, the agent should: + +- Be specific over generic. "Returns a 404 when the album doesn't exist" beats + "handles missing resources." +- Use active voice. "The handler validates the token" beats "the token is + validated." +- Use the same term every time. Pick one of {user, member, account, customer} + and stick with it across all sections. +- Cut filler. No "in order to," no "as previously mentioned," no + throat-clearing. +- Show concrete examples for ambiguous claims. If a section says "performance + matters here," name the latency budget. +- Don't blame users for edge cases. "When the request is malformed" beats + "when the user sends a bad request." +- Give every section equal effort. Don't pad weak sections; cut them or merge. + +## Edge case discipline + +This applies to technical specs only. Every endpoint, data model, and component +the spec touches must inline-name how it behaves on: + +- Empty data — what does the UI / response show when the dataset is empty? +- Errors — which errors are caught, which propagate, what does the user see? +- Loading — is there a loading state, a skeleton, a delay budget? +- Permissions — who can call this? What happens to unauthorized callers? +- Concurrency — what happens if two requests / writes race? +- Large data — what's the page size, the limit, the truncation strategy? + +If a section can't answer one of these, list it under "Open technical +questions" instead of skipping it. + +## Project conventions + +_The agent should populate this section from observed code on the next \`scan\` +or \`refine\` call. Until then, it's a placeholder._ + +Document things like: directory layout, naming patterns (PascalCase for +components, snake_case for routes, etc.), preferred libraries / patterns the +team has standardized on, and what to avoid. + +## Domain glossary + +_Empty by default. Fill in as terms come up._ + +Project-specific terms the agent should use consistently. One entry per row, +shortest definition that disambiguates from neighboring terms. + +| Term | Meaning | +|------|---------| +| | | +`; diff --git a/src/utils/constitution.js b/src/utils/constitution.js new file mode 100644 index 0000000..934098e --- /dev/null +++ b/src/utils/constitution.js @@ -0,0 +1,16 @@ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +const CONSTITUTION_PATH_PARTS = ['.draftwise', 'constitution.md']; + +export const CONSTITUTION_RELATIVE_PATH = CONSTITUTION_PATH_PARTS.join('/'); + +export async function readConstitution(cwd = process.cwd()) { + const path = join(cwd, ...CONSTITUTION_PATH_PARTS); + try { + return await readFile(path, 'utf8'); + } catch (err) { + if (err?.code === 'ENOENT') return null; + throw err; + } +} diff --git a/test/commands/init.test.js b/test/commands/init.test.js index 142995b..9dbe083 100644 --- a/test/commands/init.test.js +++ b/test/commands/init.test.js @@ -57,6 +57,16 @@ describe('draftwise init', () => { const gitignore = await readFile(join(drafts, '.gitignore'), 'utf8'); expect(gitignore).toContain('.cache/'); + + const constitution = await readFile( + join(drafts, 'constitution.md'), + 'utf8', + ); + expect(constitution).toContain('# Draftwise constitution'); + expect(constitution).toContain('## Voice'); + expect(constitution).toContain('## Spec language'); + expect(constitution).toContain('## Edge case discipline'); + expect(constitution).toContain('## Project conventions'); }); it('runs end-to-end with no flags (no questions to ask)', async () => { @@ -115,6 +125,14 @@ describe('draftwise init', () => { expect(config).toContain('state: greenfield'); expect(config).not.toContain('ai:'); expect(config).not.toContain('stack:'); + + const constitution = await readFile( + join(dir, '.draftwise', 'constitution.md'), + 'utf8', + ); + expect(constitution).toContain('# Draftwise constitution'); + expect(constitution).toContain('## Voice'); + expect(constitution).toContain('## Project conventions'); }); it('does NOT require source files (empty repo is fine)', async () => { diff --git a/test/utils/constitution.test.js b/test/utils/constitution.test.js new file mode 100644 index 0000000..be91445 --- /dev/null +++ b/test/utils/constitution.test.js @@ -0,0 +1,63 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdtemp, rm, mkdir, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { + readConstitution, + CONSTITUTION_RELATIVE_PATH, +} from '../../src/utils/constitution.js'; +import { CONSTITUTION_TEMPLATE } from '../../src/utils/constitution-template.js'; + +describe('readConstitution', () => { + let dir; + + beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), 'draftwise-constitution-')); + }); + + afterEach(async () => { + await rm(dir, { recursive: true, force: true }); + }); + + it('returns the file contents when .draftwise/constitution.md exists', async () => { + await mkdir(join(dir, '.draftwise'), { recursive: true }); + const body = '# Custom constitution\n\n## Voice\n\nBe terse.\n'; + await writeFile(join(dir, '.draftwise', 'constitution.md'), body, 'utf8'); + + expect(await readConstitution(dir)).toBe(body); + }); + + it('returns null when the file is absent', async () => { + expect(await readConstitution(dir)).toBeNull(); + }); + + it('returns null when .draftwise/ exists but constitution.md does not', async () => { + await mkdir(join(dir, '.draftwise'), { recursive: true }); + expect(await readConstitution(dir)).toBeNull(); + }); + + it('propagates non-ENOENT I/O errors', async () => { + // Pass a path that's a directory where a file is expected — readFile + // surfaces EISDIR (Unix) / EBADF / similar, which is not ENOENT and + // should bubble up so callers see the real failure instead of silently + // skipping the constitution. + await mkdir(join(dir, '.draftwise', 'constitution.md'), { recursive: true }); + await expect(readConstitution(dir)).rejects.toThrow(); + }); +}); + +describe('CONSTITUTION_RELATIVE_PATH', () => { + it('is the canonical .draftwise/constitution.md path', () => { + expect(CONSTITUTION_RELATIVE_PATH).toBe('.draftwise/constitution.md'); + }); +}); + +describe('CONSTITUTION_TEMPLATE', () => { + it('includes the five stable section headings prompts reference by name', () => { + expect(CONSTITUTION_TEMPLATE).toContain('## Voice'); + expect(CONSTITUTION_TEMPLATE).toContain('## Spec language'); + expect(CONSTITUTION_TEMPLATE).toContain('## Edge case discipline'); + expect(CONSTITUTION_TEMPLATE).toContain('## Project conventions'); + expect(CONSTITUTION_TEMPLATE).toContain('## Domain glossary'); + }); +});