Add Tech Stack and Monetization sections to OpenPRD (0.3) (#144)
Some checks are pending
CI / build (push) Waiting to run
test / test (push) Waiting to run

OpenPRD 0.2 fixed eight body sections, none of which asked what the thing is
built on or how it earns. The stack got chosen in the first implementation PR
instead of at review, and a PRD could be filled out completely without anyone
writing down who pays. PRD 0006 had already grown a hand-rolled
`## Business model` section, which is the gap showing.

0.3 adds two required sections between `UX Notes` and `Success Metrics`:

  - Tech Stack — languages, frameworks, datastores, third-party services, and
    anything the work must not depend on. It makes the requirements costable.
  - Monetization — the revenue model: who pays, for what, how much, and when.
    `_None._` stays a valid answer, but it now has to be said out loud.

Adding required sections would normally invalidate every document already
written, so a document is now held to the section list its own `openprd:` key
fixes. A 0.2 document keeps conforming with eight sections, forever; a 0.3
document needs ten. Adoption is per document, and `logicsrc prd validate
--expect-version 0.3` (new flag, wiring up the validator option that already
existed) reports the stragglers as OP-L-VERSION.

The front-matter schema is untouched — both additions are body sections.

Conformance bundle proves both directions: invalid/missing-monetization.md
fails with OP-C-SECTION-MISSING, and valid/legacy-0-2.md passes unedited.

This repo's own PRDs 0001-0006 stay at 0.2 as standing evidence that the
compatibility rule holds. PRD 0007 records the decision at 0.3.


Claude-Session: https://claude.ai/code/session_017XRNNm6pK6nPi7rJ6bJNHu

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Anthony Ettinger 2026-09-06 16:50:11 -07:00 committed by GitHub
parent ca0283caa9
commit 91834179b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 679 additions and 80 deletions

View file

@ -148,7 +148,7 @@ export function registerPrdCommands(program: Command): void {
writeIndex(resolve(options.dir));
console.log(`Created ${result.path}`);
console.log(`Assigned id ${result.id}. Index updated.`);
console.log(`\nNext: fill in the eight sections, then logicsrc prd validate ${options.dir}`);
console.log(`\nNext: fill in the ten sections, then logicsrc prd validate ${options.dir}`);
} catch (error) {
fail((error as Error).message, PRD_EXIT.usage);
}
@ -188,11 +188,19 @@ export function registerPrdCommands(program: Command): void {
.option("--strict", "treat lint warnings as errors")
.option("--format <format>", "text, json, yaml, or markdown", "text")
.option("--id <ref>", "validate a single PRD instead of the collection")
.description("Check conformance: filename, front-matter, id match, and the eight sections.")
.option(
"--expect-version <version>",
"flag PRDs that do not declare this openprd version (lint; --strict makes it an error)"
)
.description("Check conformance: filename, front-matter, id match, and the standard sections.")
.action((dir: string, options) => {
// Each document is still validated against the section list its own
// openprd version fixes; this only asks whether the collection is uniform.
const expectedVersion = options.expectVersion as string | undefined;
if (options.id) {
const { doc } = mustFind(dir, options.id);
const report = reportFor(doc, { strict: options.strict === true });
const report = reportFor(doc, { strict: options.strict === true, expectedVersion });
console.log(renderReport(report, options.format as ReportFormat));
if (!report.ok) process.exit(PRD_EXIT.invalid);
return;
@ -201,6 +209,7 @@ export function registerPrdCommands(program: Command): void {
const collection = open(dir);
const report = validatePrdCollection(collection, {
strict: options.strict === true,
expectedVersion,
expectedIndex: renderIndex(collection)
});
console.log(renderReport(report, options.format as ReportFormat));

View file

@ -34,8 +34,10 @@ numbered, committed collection under prd/, one Markdown file each.
for the template.
- Front-matter carries openprd, id, title, status, authors, and optional repo,
dates, discussion, implementation, tags, supersedes, superseded-by.
- The body has eight required sections in order: Problem, Goals, Non-Goals,
Users, Requirements, UX Notes, Success Metrics, Risks & Open Questions.
- The body has ten required sections in order: Problem, Goals, Non-Goals,
Users, Requirements, UX Notes, Tech Stack, Monetization, Success Metrics,
Risks & Open Questions. (0.2 documents have eight: no Tech Stack, no
Monetization. They are validated against the version they declare.)
- Requirements are numbered R1, R2, each tagged [P0], [P1], or [P2].
- Lifecycle: Draft Review Accepted Final, or Rejected / Withdrawn /
Superseded. Status lives in front-matter and is the source of truth.
@ -69,7 +71,7 @@ export function registerOpenPrd(server: McpServer): void {
"logicsrc://openprd/spec",
{
title: "OpenPRD specification",
description: "Numbered PRDs: layout, front-matter, the eight sections, and the lifecycle.",
description: "Numbered PRDs: layout, front-matter, the ten sections, and the lifecycle.",
mimeType: "text/markdown"
},
async () => ({
@ -136,7 +138,7 @@ export function registerOpenPrd(server: McpServer): void {
{
title: "Validate the PRD collection",
description:
"Checks conformance — filename, front-matter, id match, the eight sections in order — plus collection rules.",
"Checks conformance — filename, front-matter, id match, the sections in order — plus collection rules.",
inputSchema: { strict: z.boolean().optional() },
annotations: { readOnlyHint: true, openWorldHint: false }
},
@ -235,12 +237,18 @@ export function registerOpenPrd(server: McpServer): void {
type: "text" as const,
text: `Draft an OpenPRD document for the change the user describes.
Front-matter: openprd "0.2", a four-digit id matching the filename, an imperative
Front-matter: openprd "0.3", a four-digit id matching the filename, an imperative
title starting with a verb, status Draft, and at least one author.
Then all eight sections, in this order, none omitted:
Problem, Goals, Non-Goals, Users, Requirements, UX Notes, Success Metrics,
Risks & Open Questions. A section may be a single line such as _None._
Then all ten sections, in this order, none omitted:
Problem, Goals, Non-Goals, Users, Requirements, UX Notes, Tech Stack,
Monetization, Success Metrics, Risks & Open Questions. A section may be a single
line such as _None._
Tech Stack names the languages, frameworks, datastores, and third-party services
the work will be built on, and anything it must not depend on. Monetization
states the revenue model: who pays, for what, how much, and when or _None._
when the change does not earn on its own.
Requirements are numbered R1, R2, contiguously, each tagged [P0], [P1], or [P2],
one capability per line. Goals are outcomes, not features. Non-Goals bound the work.
@ -263,13 +271,16 @@ they are settled.`
type: "text" as const,
text: `Review this PRD.
Check the shape first: all eight sections present and in order, requirements numbered
contiguously with priority tags, front-matter complete.
Check the shape first: every section the declared openprd version requires, present
and in order, requirements numbered contiguously with priority tags, front-matter
complete.
Then the substance: are the Goals outcomes rather than features? Do the Non-Goals
actually bound the work? Is every P0 requirement testable? Do the Success Metrics
measure the Goals? Do the Risks name real decisions still owed, or is that section
decoration? Say what you would change and why.`
measure the Goals? Is the Tech Stack specific enough to cost the Requirements, or
is it a wish list? Does Monetization say who pays and how much, or does it dodge?
Do the Risks name real decisions still owed, or is that section decoration? Say
what you would change and why.`
}
}
]

View file

@ -203,7 +203,7 @@ describe("MCP: OpenPRD", () => {
it("reports the next free id and the allowed lifecycle moves", async () => {
const client = await connect();
// Asserted against the live prd/ directory, so this advances with every PRD added.
expect(toolText(await client.callTool({ name: "prd_next_id", arguments: {} }))).toBe("0007");
expect(toolText(await client.callTool({ name: "prd_next_id", arguments: {} }))).toBe("0008");
const moves = await client.callTool({ name: "prd_next_statuses", arguments: { ref: "0001" } });
const payload = JSON.parse(toolText(moves)) as { status: string; allowedNext: string[] };

View file

@ -2,11 +2,11 @@
* @logicsrc/openprd reference implementation of the OpenPRD standard.
*
* The standard is docs/openprd.md plus `openprd-prd.schema.json`; this package
* implements it. A PRD is just a Markdown file with front-matter and eight
* implements it. A PRD is just a Markdown file with front-matter and ten
* sections it needs no service to exist, and none of this code to be valid.
*/
export { OPENPRD_VERSION, SECTIONS, STATUSES } from "./types.js";
export { OPENPRD_VERSION, SECTIONS, SECTIONS_0_2, STATUSES, sectionsForVersion } from "./types.js";
export type * from "./types.js";
export {

View file

@ -3,7 +3,7 @@ import { formatId, parsePrd, PrdParseError, rewriteFrontMatter, slugify } from "
import { SECTIONS } from "./types.js";
const MINIMAL = `---
openprd: "0.2"
openprd: "0.3"
id: "0007"
title: Do the thing
status: Draft
@ -38,6 +38,14 @@ Everyone.
_None._
## Tech Stack
Node and Postgres.
## Monetization
_None._
## Success Metrics
It stops hurting.
@ -63,7 +71,7 @@ describe("parsePrd", () => {
expect(doc.file).toBe("0007-do-the-thing.md");
});
it("finds all eight sections in order", () => {
it("finds all ten sections in order", () => {
expect(doc.sections.map((s) => s.name)).toEqual([...SECTIONS]);
});

View file

@ -52,6 +52,16 @@ Who this is for; personas or segments.
Flows, states, and constraints that shape the experience.
## Tech Stack
Languages, frameworks, datastores, and third-party services this will be built
on, and anything it must not depend on.
## Monetization
The revenue model: who pays, for what, how much, and when. None, when the
change does not earn on its own.
## Success Metrics
How the goals will be measured.
@ -183,6 +193,10 @@ function placeholder(section: string): string {
return "- R1 [P0] _TODO: first required capability._";
case "UX Notes":
return "_TODO: flows, states, and constraints._";
case "Tech Stack":
return "_TODO: languages, frameworks, datastores, and services._";
case "Monetization":
return "_TODO: the revenue model — who pays, for what, how much._";
case "Success Metrics":
return "_TODO: how the goals will be measured._";
default:

View file

@ -7,9 +7,9 @@
* speak this shape.
*/
export const OPENPRD_VERSION = "0.2";
export const OPENPRD_VERSION = "0.3";
/** The eight `##` sections, in the order the standard requires. */
/** The ten `##` sections of OpenPRD 0.3, in the order the standard requires. */
export const SECTIONS = [
"Problem",
"Goals",
@ -17,12 +17,33 @@ export const SECTIONS = [
"Users",
"Requirements",
"UX Notes",
"Tech Stack",
"Monetization",
"Success Metrics",
"Risks & Open Questions"
] as const;
export type SectionName = (typeof SECTIONS)[number];
/**
* OpenPRD 0.2 had eight sections. 0.3 adds `Tech Stack` and `Monetization`
* after `UX Notes`, which would retroactively break every published 0.2
* document, so the section list a document is held to is the one its own
* `openprd:` version fixes.
*/
export const SECTIONS_0_2 = SECTIONS.filter(
(section) => section !== "Tech Stack" && section !== "Monetization"
) as readonly SectionName[];
/** The sections required by a declared standard version. Unknown → current. */
export function sectionsForVersion(version: string | undefined | null): readonly SectionName[] {
const [major, minor] = String(version ?? "")
.split(".")
.map((part) => Number.parseInt(part, 10));
if (major === 0 && Number.isInteger(minor) && (minor as number) < 3) return SECTIONS_0_2;
return SECTIONS;
}
export const STATUSES = [
"Draft",
"Review",

View file

@ -24,7 +24,7 @@ function scratch(): string {
function conforming(overrides: { frontMatter?: string; body?: string } = {}): string {
const frontMatter =
overrides.frontMatter ??
`openprd: "0.2"
`openprd: "0.3"
id: "0001"
title: Do the thing
status: Draft
@ -59,6 +59,14 @@ Everyone.
_None._
## Tech Stack
Node and Postgres.
## Monetization
_None._
## Success Metrics
It stops hurting.
@ -90,21 +98,21 @@ describe("document conformance", () => {
it("rejects front-matter that fails the schema", () => {
const missingStatus = conforming({
frontMatter: `openprd: "0.2"\nid: "0001"\ntitle: Do the thing`
frontMatter: `openprd: "0.3"\nid: "0001"\ntitle: Do the thing`
});
expect(codes(missingStatus)).toContain("OP-C-FRONTMATTER");
});
it("rejects an unknown status value", () => {
const bad = conforming({
frontMatter: `openprd: "0.2"\nid: "0001"\ntitle: Do the thing\nstatus: Shipped`
frontMatter: `openprd: "0.3"\nid: "0001"\ntitle: Do the thing\nstatus: Shipped`
});
expect(codes(bad)).toContain("OP-C-FRONTMATTER");
});
it("rejects an id that does not match the filename prefix", () => {
const mismatch = conforming({
frontMatter: `openprd: "0.2"\nid: "0009"\ntitle: Do the thing\nstatus: Draft`
frontMatter: `openprd: "0.3"\nid: "0009"\ntitle: Do the thing\nstatus: Draft`
});
expect(codes(mismatch)).toContain("OP-C-ID-MISMATCH");
});
@ -135,6 +143,82 @@ describe("document conformance", () => {
});
});
/**
* 0.3 added Tech Stack and Monetization. A document is held to the section list
* its own `openprd:` version fixed, so publishing 0.3 could not retroactively
* invalidate anything already written against 0.2.
*/
describe("section list by declared version", () => {
const eightSections = `## Problem
Something hurts.
## Goals
Make it stop.
## Non-Goals
_None._
## Users
Everyone.
## Requirements
- R1 [P0] First capability.
## UX Notes
_None._
## Success Metrics
It stops hurting.
## Risks & Open Questions
- Might not stop.`;
const legacy = (version: string) =>
conforming({
frontMatter: `openprd: "${version}"\nid: "0001"\ntitle: Do the thing\nstatus: Draft\nauthors:\n - a@example.com`,
body: eightSections
});
it("still accepts a 0.2 document with only the original eight sections", () => {
const report = reportFor(parsePrd(legacy("0.2"), "0001-do-the-thing.md"));
expect(report.findings.filter((f) => f.severity === "error")).toEqual([]);
expect(report.ok).toBe(true);
});
it("rejects the same eight sections when the document declares 0.3", () => {
const findings = validatePrdDocument(parsePrd(legacy("0.3"), "0001-do-the-thing.md"));
const missing = findings.filter((f) => f.code === "OP-C-SECTION-MISSING");
expect(missing.map((f) => f.message)).toEqual([
'missing required section "## Tech Stack"',
'missing required section "## Monetization"'
]);
});
it("treats the 0.3 sections as extra, not required, inside a 0.2 document", () => {
const findings = validatePrdDocument(
parsePrd(
conforming({
frontMatter: `openprd: "0.2"\nid: "0001"\ntitle: Do the thing\nstatus: Draft\nauthors:\n - a@example.com`
}),
"0001-do-the-thing.md"
)
);
expect(findings.filter((f) => f.severity === "error")).toEqual([]);
expect(findings.filter((f) => f.code === "OP-L-EXTRA-SECTION").map((f) => f.message)).toEqual([
'"## Tech Stack" is not one of the 8 standard sections',
'"## Monetization" is not one of the 8 standard sections'
]);
});
});
describe("document lint", () => {
it("warns about an empty section and escalates it under --strict", () => {
const empty = conforming().replace("## UX Notes\n\n_None._", "## UX Notes\n");
@ -168,14 +252,14 @@ describe("document lint", () => {
it("warns when a PRD lists no authors", () => {
const noAuthors = conforming({
frontMatter: `openprd: "0.2"\nid: "0001"\ntitle: Do the thing\nstatus: Draft`
frontMatter: `openprd: "0.3"\nid: "0001"\ntitle: Do the thing\nstatus: Draft`
});
expect(codes(noAuthors)).toContain("OP-L-NO-AUTHOR");
});
it("errors when updated is before created", () => {
const backwards = conforming({
frontMatter: `openprd: "0.2"\nid: "0001"\ntitle: Do the thing\nstatus: Draft\nauthors:\n - a@example.com\ncreated: 2026-07-10\nupdated: 2026-07-01`
frontMatter: `openprd: "0.3"\nid: "0001"\ntitle: Do the thing\nstatus: Draft\nauthors:\n - a@example.com\ncreated: 2026-07-10\nupdated: 2026-07-01`
});
const findings = validatePrdDocument(parsePrd(backwards, "0001-do-the-thing.md"));
expect(findings.find((f) => f.code === "OP-L-DATE-ORDER")?.severity).toBe("error");
@ -183,14 +267,14 @@ describe("document lint", () => {
it("errors when status is Superseded with no replacement named", () => {
const superseded = conforming({
frontMatter: `openprd: "0.2"\nid: "0001"\ntitle: Do the thing\nstatus: Superseded\nauthors:\n - a@example.com`
frontMatter: `openprd: "0.3"\nid: "0001"\ntitle: Do the thing\nstatus: Superseded\nauthors:\n - a@example.com`
});
expect(codes(superseded)).toContain("OP-L-SUPERSEDED-BY");
});
it("errors when a PRD supersedes itself", () => {
const selfRef = conforming({
frontMatter: `openprd: "0.2"\nid: "0001"\ntitle: Do the thing\nstatus: Draft\nauthors:\n - a@example.com\nsupersedes: "0001"`
frontMatter: `openprd: "0.3"\nid: "0001"\ntitle: Do the thing\nstatus: Draft\nauthors:\n - a@example.com\nsupersedes: "0001"`
});
expect(codes(selfRef)).toContain("OP-L-SELF-REFERENCE");
});
@ -225,7 +309,7 @@ describe("collection rules", () => {
const collection = collectionWith({
"0001-one.md": conforming(),
"0003-three.md": conforming({
frontMatter: `openprd: "0.2"\nid: "0003"\ntitle: Three\nstatus: Draft\nauthors:\n - a@example.com`
frontMatter: `openprd: "0.3"\nid: "0003"\ntitle: Three\nstatus: Draft\nauthors:\n - a@example.com`
})
});
const report = validatePrdCollection(collection);
@ -237,7 +321,7 @@ describe("collection rules", () => {
const collection = collectionWith({
"0001-one.md": conforming(),
"0002-two.md": conforming({
frontMatter: `openprd: "0.2"\nid: "0001"\ntitle: Two\nstatus: Draft\nauthors:\n - a@example.com`
frontMatter: `openprd: "0.3"\nid: "0001"\ntitle: Two\nstatus: Draft\nauthors:\n - a@example.com`
})
});
expect(validatePrdCollection(collection).findings.map((f) => f.code)).toContain("OP-C-DUPLICATE-ID");
@ -246,7 +330,7 @@ describe("collection rules", () => {
it("errors when a cross-reference points outside the collection", () => {
const collection = collectionWith({
"0001-one.md": conforming({
frontMatter: `openprd: "0.2"\nid: "0001"\ntitle: One\nstatus: Draft\nauthors:\n - a@example.com\nsupersedes: "0099"`
frontMatter: `openprd: "0.3"\nid: "0001"\ntitle: One\nstatus: Draft\nauthors:\n - a@example.com\nsupersedes: "0099"`
})
});
expect(validatePrdCollection(collection).findings.map((f) => f.code)).toContain("OP-C-UNKNOWN-REFERENCE");
@ -255,10 +339,10 @@ describe("collection rules", () => {
it("warns when supersession is recorded on only one side", () => {
const collection = collectionWith({
"0001-one.md": conforming({
frontMatter: `openprd: "0.2"\nid: "0001"\ntitle: One\nstatus: Superseded\nauthors:\n - a@example.com\nsuperseded-by: "0002"`
frontMatter: `openprd: "0.3"\nid: "0001"\ntitle: One\nstatus: Superseded\nauthors:\n - a@example.com\nsuperseded-by: "0002"`
}),
"0002-two.md": conforming({
frontMatter: `openprd: "0.2"\nid: "0002"\ntitle: Two\nstatus: Draft\nauthors:\n - a@example.com`
frontMatter: `openprd: "0.3"\nid: "0002"\ntitle: Two\nstatus: Draft\nauthors:\n - a@example.com`
})
});
expect(validatePrdCollection(collection).findings.map((f) => f.code)).toContain("OP-L-ONE-SIDED-REFERENCE");

View file

@ -1,6 +1,15 @@
import { validate as validateSchema } from "@logicsrc/validators";
import { slugify } from "./parse.js";
import { SECTIONS, type Finding, type PrdCollection, type PrdDocument, type Severity, type ValidationReport } from "./types.js";
import {
OPENPRD_VERSION,
sectionsForVersion,
type Finding,
type PrdCollection,
type PrdDocument,
type SectionName,
type Severity,
type ValidationReport
} from "./types.js";
export interface ValidateOptions {
/** Promote lint warnings to errors, for CI that wants a clean collection. */
@ -17,7 +26,7 @@ const TEMPLATE_ID = "0000";
* - lives at prd/<id>-<slug>.md with a four-digit <id>
* - front-matter validates against openprd-prd.schema.json
* - id equals the filename's numeric prefix
* - all eight body sections are present in order
* - all body sections for the declared version are present in order
*
* Everything beyond those four is lint: useful, but never the difference
* between conforming and not.
@ -73,10 +82,13 @@ export function validatePrdDocument(doc: PrdDocument, options: ValidateOptions =
});
}
/* ── 4. The eight sections, present and in order ─────────────────────── */
/* ── 4. The standard sections, present and in order ──────────────────── */
// A document is held to the section list its own `openprd:` version fixes, so
// 0.2 documents keep conforming after 0.3 added Tech Stack and Monetization.
const expected = [...sectionsForVersion(doc.frontMatter.openprd)];
const isStandard = (name: string) => expected.includes(name as SectionName);
const present = doc.sections.map((section) => section.name);
const expected = [...SECTIONS];
for (const name of expected) {
if (!present.includes(name)) {
@ -84,12 +96,12 @@ export function validatePrdDocument(doc: PrdDocument, options: ValidateOptions =
code: "OP-C-SECTION-MISSING",
severity: "error",
message: `missing required section "## ${name}"`,
hint: `The eight sections are: ${expected.join(", ")}`
hint: `OpenPRD ${doc.frontMatter.openprd ?? OPENPRD_VERSION} requires: ${expected.join(", ")}`
});
}
}
const required = present.filter((name) => expected.includes(name as (typeof SECTIONS)[number]));
const required = present.filter(isStandard);
const ordered = expected.filter((name) => required.includes(name));
if (required.length === ordered.length && required.join("|") !== ordered.join("|")) {
add({
@ -99,13 +111,13 @@ export function validatePrdDocument(doc: PrdDocument, options: ValidateOptions =
});
}
const extra = present.filter((name) => !expected.includes(name as (typeof SECTIONS)[number]));
const extra = present.filter((name) => !isStandard(name));
for (const name of extra) {
add({
code: "OP-L-EXTRA-SECTION",
severity: "info",
line: doc.sections.find((s) => s.name === name)?.line,
message: `"## ${name}" is not one of the eight standard sections`,
message: `"## ${name}" is not one of the ${expected.length} standard sections`,
hint: "Use a ### subsection inside a standard section instead"
});
}
@ -122,7 +134,7 @@ export function validatePrdDocument(doc: PrdDocument, options: ValidateOptions =
}
for (const section of doc.sections) {
if (!expected.includes(section.name as (typeof SECTIONS)[number])) continue;
if (!isStandard(section.name)) continue;
if (!section.empty) continue;
add({
code: "OP-L-EMPTY-SECTION",

View file

@ -1,6 +1,6 @@
{
"openprdConformance": "0.2",
"description": "Conformance fixtures for OpenPRD 0.2. Every valid fixture must parse and validate with no errors; every invalid fixture must fail with the stated code. `file` is the filename the fixture must be validated as, since the standard's rules depend on it.",
"openprdConformance": "0.3",
"description": "Conformance fixtures for OpenPRD 0.3. Every valid fixture must parse and validate with no errors; every invalid fixture must fail with the stated code. `file` is the filename the fixture must be validated as, since the standard's rules depend on it. A document is held to the section list its own `openprd:` version fixes, so valid/legacy-0-2.md still conforms with the eight sections 0.2 defined.",
"valid": [
{
"fixture": "valid/minimal.md",
@ -25,6 +25,11 @@
{
"fixture": "valid/none-sections.md",
"file": "0005-keep-every-section-even-when-empty.md"
},
{
"fixture": "valid/legacy-0-2.md",
"file": "0006-keep-a-0-2-document-valid.md",
"reason": "A 0.2 document has eight sections and must keep conforming under 0.3."
}
],
"invalid": [
@ -32,7 +37,13 @@
"fixture": "invalid/missing-section.md",
"file": "0001-missing-a-section.md",
"code": "OP-C-SECTION-MISSING",
"reason": "The eight body sections are all required; Users is absent."
"reason": "The body sections are all required; Users is absent."
},
{
"fixture": "invalid/missing-monetization.md",
"file": "0001-omit-the-monetization-section.md",
"code": "OP-C-SECTION-MISSING",
"reason": "0.3 requires Tech Stack and Monetization; Monetization is absent."
},
{
"fixture": "invalid/out-of-order.md",

View file

@ -1,5 +1,5 @@
---
openprd: "0.2"
openprd: "0.3"
id: "0001"
title: Bad filename
status: Draft
@ -31,6 +31,15 @@ _None._
_None._
## Tech Stack
TypeScript on Node 22, Postgres, and the existing Cloudflare worker. No new
runtime.
## Monetization
Bundled into the existing Pro plan at $19/mo; no separate SKU.
## Success Metrics
_None._

View file

@ -1,5 +1,5 @@
---
openprd: "0.2"
openprd: "0.3"
id: "1"
title: Two digit id
status: Draft
@ -31,6 +31,15 @@ _None._
_None._
## Tech Stack
TypeScript on Node 22, Postgres, and the existing Cloudflare worker. No new
runtime.
## Monetization
Bundled into the existing Pro plan at $19/mo; no separate SKU.
## Success Metrics
_None._

View file

@ -1,5 +1,5 @@
---
openprd: "0.2"
openprd: "0.3"
id: "0001"
title: Unknown status
status: Shipped
@ -31,6 +31,15 @@ _None._
_None._
## Tech Stack
TypeScript on Node 22, Postgres, and the existing Cloudflare worker. No new
runtime.
## Monetization
Bundled into the existing Pro plan at $19/mo; no separate SKU.
## Success Metrics
_None._

View file

@ -1,5 +1,5 @@
---
openprd: "0.2"
openprd: "0.3"
id: "0001"
title: Updated before created
status: Draft
@ -33,6 +33,15 @@ _None._
_None._
## Tech Stack
TypeScript on Node 22, Postgres, and the existing Cloudflare worker. No new
runtime.
## Monetization
Bundled into the existing Pro plan at $19/mo; no separate SKU.
## Success Metrics
_None._

View file

@ -1,5 +1,5 @@
---
openprd: "0.2"
openprd: "0.3"
id: "0001"
title: Duplicate requirement id
status: Draft
@ -32,6 +32,15 @@ _None._
_None._
## Tech Stack
TypeScript on Node 22, Postgres, and the existing Cloudflare worker. No new
runtime.
## Monetization
Bundled into the existing Pro plan at $19/mo; no separate SKU.
## Success Metrics
_None._

View file

@ -1,5 +1,5 @@
---
openprd: "0.2"
openprd: "0.3"
id: "0042"
title: Id does not match
status: Draft
@ -31,6 +31,15 @@ _None._
_None._
## Tech Stack
TypeScript on Node 22, Postgres, and the existing Cloudflare worker. No new
runtime.
## Monetization
Bundled into the existing Pro plan at $19/mo; no separate SKU.
## Success Metrics
_None._

View file

@ -0,0 +1,45 @@
---
openprd: "0.3"
id: "0001"
title: Omit the monetization section
status: Draft
authors:
- anthony@profullstack.com
---
## Problem
The thing is broken and it costs us money every week.
## Goals
_None._
## Non-Goals
_None._
## Users
_None._
## Requirements
- R1 [P0] First required capability.
## UX Notes
_None._
## Tech Stack
_None._
## Success Metrics
_None._
## Risks & Open Questions
_None._

View file

@ -1,5 +1,5 @@
---
openprd: "0.2"
openprd: "0.3"
id: "0001"
title: Missing a section
status: Draft
@ -27,6 +27,15 @@ _None._
_None._
## Tech Stack
TypeScript on Node 22, Postgres, and the existing Cloudflare worker. No new
runtime.
## Monetization
Bundled into the existing Pro plan at $19/mo; no separate SKU.
## Success Metrics
_None._

View file

@ -1,5 +1,5 @@
---
openprd: "0.2"
openprd: "0.3"
id: "0001"
status: Draft
authors:
@ -30,6 +30,15 @@ _None._
_None._
## Tech Stack
TypeScript on Node 22, Postgres, and the existing Cloudflare worker. No new
runtime.
## Monetization
Bundled into the existing Pro plan at $19/mo; no separate SKU.
## Success Metrics
_None._

View file

@ -24,6 +24,15 @@ _None._
_None._
## Tech Stack
TypeScript on Node 22, Postgres, and the existing Cloudflare worker. No new
runtime.
## Monetization
Bundled into the existing Pro plan at $19/mo; no separate SKU.
## Success Metrics
_None._

View file

@ -1,5 +1,5 @@
---
openprd: "0.2"
openprd: "0.3"
id: "0001"
title: Sections out of order
status: Draft
@ -31,6 +31,15 @@ _None._
_None._
## Tech Stack
TypeScript on Node 22, Postgres, and the existing Cloudflare worker. No new
runtime.
## Monetization
Bundled into the existing Pro plan at $19/mo; no separate SKU.
## Success Metrics
_None._

View file

@ -1,5 +1,5 @@
---
openprd: "0.2"
openprd: "0.3"
id: "0001"
title: Superseded with no replacement
status: Superseded
@ -31,6 +31,15 @@ _None._
_None._
## Tech Stack
TypeScript on Node 22, Postgres, and the existing Cloudflare worker. No new
runtime.
## Monetization
Bundled into the existing Pro plan at $19/mo; no separate SKU.
## Success Metrics
_None._

View file

@ -1,5 +1,5 @@
---
openprd: "0.2"
openprd: "0.3"
id: "0001"
title: Unknown front matter key
status: Draft
@ -32,6 +32,15 @@ _None._
_None._
## Tech Stack
TypeScript on Node 22, Postgres, and the existing Cloudflare worker. No new
runtime.
## Monetization
Bundled into the existing Pro plan at $19/mo; no separate SKU.
## Success Metrics
_None._

View file

@ -1,5 +1,5 @@
---
openprd: "0.2"
openprd: "0.3"
id: "0002"
title: Use bold requirement markers
status: Review
@ -31,6 +31,15 @@ _None._
_None._
## Tech Stack
TypeScript on Node 22, Postgres, and the existing Cloudflare worker. No new
runtime.
## Monetization
Bundled into the existing Pro plan at $19/mo; no separate SKU.
## Success Metrics
_None._

View file

@ -1,5 +1,5 @@
---
openprd: "0.2"
openprd: "0.3"
id: "0001"
title: Expand the parked-domain service
status: Draft
@ -38,6 +38,15 @@ _None._
_None._
## Tech Stack
TypeScript on Node 22, Postgres, and the existing Cloudflare worker. No new
runtime.
## Monetization
Bundled into the existing Pro plan at $19/mo; no separate SKU.
## Success Metrics
_None._

View file

@ -0,0 +1,41 @@
---
openprd: "0.2"
id: "0006"
title: Keep a 0.2 document valid
status: Draft
authors:
- anthony@profullstack.com
---
## Problem
The thing is broken and it costs us money every week.
## Goals
_None._
## Non-Goals
_None._
## Users
_None._
## Requirements
- R1 [P0] First required capability.
## UX Notes
_None._
## Success Metrics
_None._
## Risks & Open Questions
_None._

View file

@ -1,5 +1,5 @@
---
openprd: "0.2"
openprd: "0.3"
id: "0001"
title: Expand the parked-domain service
status: Draft
@ -31,6 +31,14 @@ _None._
_None._
## Tech Stack
_None._
## Monetization
_None._
## Success Metrics
_None._

View file

@ -1,5 +1,5 @@
---
openprd: "0.2"
openprd: "0.3"
id: "0005"
title: Keep every section even when empty
status: Accepted
@ -31,6 +31,14 @@ _None._
_None._
## Tech Stack
_None._
## Monetization
_None._
## Success Metrics
_None._

View file

@ -1,5 +1,5 @@
---
openprd: "0.2"
openprd: "0.3"
id: "0003"
title: Organize a long requirements section
status: Draft
@ -37,6 +37,15 @@ _None._
_None._
## Tech Stack
TypeScript on Node 22, Postgres, and the existing Cloudflare worker. No new
runtime.
## Monetization
Bundled into the existing Pro plan at $19/mo; no separate SKU.
## Success Metrics
_None._

View file

@ -1,5 +1,5 @@
---
openprd: "0.2"
openprd: "0.3"
id: "0004"
title: Retire the old flow
status: Superseded
@ -32,6 +32,15 @@ _None._
_None._
## Tech Stack
TypeScript on Node 22, Postgres, and the existing Cloudflare worker. No new
runtime.
## Monetization
Bundled into the existing Pro plan at $19/mo; no separate SKU.
## Success Metrics
_None._