Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions apps/logicsrc-web/src/app/openprd/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { card, mono, pre } from "../openontology/ui";
export const metadata: Metadata = {
title: "OpenPRD · LogicSRC",
description:
"OpenPRD is a lightweight open standard for product requirements documents: a numbered, committed collection under prd/, one Markdown file each, with front-matter, eight fixed sections, and an enforced lifecycle.",
"OpenPRD is a lightweight open standard for product requirements documents: a numbered, committed collection under prd/, one Markdown file each, with front-matter, ten fixed sections, and an enforced lifecycle.",
alternates: { canonical: "/openprd" }
};

Expand All @@ -18,6 +18,8 @@ const SECTIONS: Array<[string, string]> = [
["Users", "Who this is for; personas or segments."],
["Requirements", "Numbered R1, R2, … each tagged [P0], [P1], or [P2]."],
["UX Notes", "Flows, states, and constraints that shape the experience."],
["Tech Stack", "Languages, frameworks, datastores, and services it is built on — and what it must not depend on."],
["Monetization", "The revenue model: who pays, for what, how much, and when."],
["Success Metrics", "How the goals will be measured."],
["Risks & Open Questions", "Known risks and the decisions still owed."]
];
Expand All @@ -41,18 +43,19 @@ export default function OpenPrdPage(): ReactNode {
read to recover the <em>why</em>.
</p>
<p style={{ color: "#5b6b7a", fontSize: "0.95rem" }}>
Status: <strong>0.2</strong>. A PRD is just a file — it needs no service, and no tooling, to
be valid.
Status: <strong>0.3</strong>, which adds <code style={mono}>Tech Stack</code> and{" "}
<code style={mono}>Monetization</code>. A PRD is just a file — it needs no service, and no
tooling, to be valid.
</p>
</div>

<div className="band">
<div className="section-head">
<h2>The shape</h2>
<p>Front-matter, then eight sections in a fixed order. All of them required.</p>
<p>Front-matter, then ten sections in a fixed order. All of them required.</p>
</div>
<pre style={pre}>{`---
openprd: "0.2"
openprd: "0.3"
id: "0001" # four digits, matches the filename
title: Expand the parked-domain service
status: Draft # Draft|Review|Accepted|Final|Rejected|Withdrawn|Superseded
Expand All @@ -73,6 +76,8 @@ tags: [growth]
- R2 [P1] Next capability.

## UX Notes
## Tech Stack
## Monetization
## Success Metrics
## Risks & Open Questions`}</pre>
<div style={{ display: "grid", gap: "0.6rem", marginTop: "1rem" }}>
Expand Down Expand Up @@ -130,8 +135,13 @@ tags: [growth]
Its front-matter validates against <code style={mono}>openprd-prd.schema.json</code>.
</li>
<li>The id equals the filename&apos;s numeric prefix.</li>
<li>All eight body sections are present, in order.</li>
<li>All ten body sections are present, in order.</li>
</ol>
<p style={{ color: "#41505d" }}>
A document is judged against the version it declares, not the newest one — a{" "}
<code style={mono}>0.2</code> PRD is still held to the eight sections{" "}
<code style={mono}>0.2</code> fixed, so adding two sections broke nothing already written.
</p>
<p style={{ color: "#41505d" }}>
Conformance failures are errors. An empty section, a requirement missing its priority tag,
numbering that skips, a stale index, a one-sided supersession link — those are warnings, and{" "}
Expand All @@ -148,7 +158,7 @@ tags: [growth]
</p>
</div>
<pre style={pre}>{`logicsrc prd init # template + generated index
logicsrc prd new "Expand the service" # next free number, eight stub sections
logicsrc prd new "Expand the service" # next free number, ten stub sections
logicsrc prd list # id, title, status, tags, requirements
logicsrc prd validate --strict # conformance + lint, exit 1 on error
logicsrc prd index --write # regenerate prd/README.md
Expand Down
37 changes: 30 additions & 7 deletions docs/openprd.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Every PRD opens with a YAML front-matter block validated by

```yaml
---
openprd: "0.2" # standard version (required)
openprd: "0.3" # standard version (required)
id: "0001" # 4-digit number == filename prefix (required)
title: Expand the parked-domain service # imperative title (required)
status: Draft # Draft|Review|Accepted|Final|Rejected|Withdrawn|Superseded (required)
Expand All @@ -70,16 +70,18 @@ superseded-by: # optional 4-digit id that replaces this PRD

## Body sections

The body is Markdown with a fixed, ordered set of `##` sections. All are required (a section MAY be a single line such as `_None._`), which keeps every PRD skimmable and diffable:
The body is Markdown with a fixed, ordered set of `##` sections. All are required (a section MAY be a single line such as `_None._`), which keeps every PRD skimmable and diffable. OpenPRD `0.3` fixes ten:

1. `## Problem` — the user/business problem, and why it matters now.
2. `## Goals` — what success looks like, as outcomes (not features).
3. `## Non-Goals` — explicitly out of scope, to bound the work.
4. `## Users` — who this is for; personas or segments.
5. `## Requirements` — numbered `R1`, `R2`, … each prefixed with a priority tag `[P0]`/`[P1]`/`[P2]`. One capability per line.
6. `## UX Notes` — flows, states, and constraints that shape the experience.
7. `## Success Metrics` — how the goals will be measured.
8. `## Risks & Open Questions` — known risks and decisions still owed.
7. `## Tech Stack` — languages, frameworks, datastores, and third-party services the work will be built on, plus anything it must not depend on. Naming the stack in the PRD is what makes the requirements costable, and what stops the choice from being made silently in the first PR.
8. `## Monetization` — the revenue model: who pays, for what, how much, and when. Free, internal, or loss-leading work says so here (`_None._` is a valid answer, and a deliberate one); the section exists so that "how does this earn?" is answered before the code, not after the launch.
9. `## Success Metrics` — how the goals will be measured.
10. `## Risks & Open Questions` — known risks and decisions still owed.

See [`0000-template.md`](./openprd/0000-template.md) for the copy-paste template.

Expand All @@ -94,10 +96,11 @@ still just a file: nothing below is required for a document to conform.

```bash
logicsrc prd init # create prd/ with the template and an index
logicsrc prd new "Expand the service" # next free number, filled front-matter, eight stub sections
logicsrc prd new "Expand the service" # next free number, filled front-matter, ten stub sections
logicsrc prd list # id, title, status, tags, requirement count
logicsrc prd show 0001 # front-matter, sections, and parsed requirements
logicsrc prd validate --strict # conformance + lint, exit 1 on error
logicsrc prd validate --expect-version 0.3 # flag PRDs still declaring an older version
logicsrc prd index --write # regenerate prd/README.md from what is on disk
logicsrc prd status 0001 Review # lifecycle move, refusing illegal transitions
logicsrc prd tasks 0001 # the optional LogicSRC task bridge
Expand Down Expand Up @@ -137,9 +140,29 @@ name it must be validated as.

## Conformance

A document conforms to OpenPRD `0.2` when:
A document conforms to OpenPRD `0.3` when:

- it lives at `prd/<id>-<slug>.md` with a four-digit `<id>`,
- its front-matter validates against `openprd-prd.schema.json`,
- `id` equals the filename's numeric prefix, and
- all eight body sections are present in order.
- all ten body sections are present in order.

## Versioning

A document is judged against the version it declares in its own `openprd:`
key, not against the newest one. That is what makes it safe to add a section:

| Version | Sections | Change |
| --- | --- | --- |
| `0.2` | eight | Problem … Risks & Open Questions |
| `0.3` | ten | adds `Tech Stack` and `Monetization` after `UX Notes` |

So a `0.2` document keeps conforming forever, and validators MUST hold it to
the eight sections `0.2` fixed. Adopting `0.3` in an existing collection is a
per-document edit: bump `openprd` to `"0.3"` and add the two sections, using
`_None._` where they do not apply. Nothing forces a whole collection to move at
once; a collection that wants uniformity asks for it with
`logicsrc prd validate --expect-version 0.3`, which reports every document
declaring something else as `OP-L-VERSION` (a warning, or an error under
`--strict`). Each document is still validated against the sections its own
version fixes.
12 changes: 11 additions & 1 deletion docs/openprd/0000-template.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
openprd: "0.2"
openprd: "0.3"
id: "0000"
title: "Short imperative title — start with a verb if possible"
status: Draft
Expand Down Expand Up @@ -41,6 +41,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.
Expand Down
15 changes: 12 additions & 3 deletions packages/cli/src/prd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
Expand All @@ -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));
Expand Down
35 changes: 23 additions & 12 deletions packages/logicsrc-mcp/src/openprd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 () => ({
Expand Down Expand Up @@ -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 }
},
Expand Down Expand Up @@ -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.
Expand All @@ -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.`
}
}
]
Expand Down
2 changes: 1 addition & 1 deletion packages/logicsrc-mcp/src/standards.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] };
Expand Down
4 changes: 2 additions & 2 deletions packages/openprd/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
12 changes: 10 additions & 2 deletions packages/openprd/src/parse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -38,6 +38,14 @@ Everyone.

_None._

## Tech Stack

Node and Postgres.

## Monetization

_None._

## Success Metrics

It stops hurting.
Expand All @@ -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]);
});

Expand Down
14 changes: 14 additions & 0 deletions packages/openprd/src/scaffold.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down
Loading
Loading