mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-09-10 19:26:00 +00:00
Add Tech Stack and Monetization sections to OpenPRD (0.3) (#144)
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:
parent
ca0283caa9
commit
91834179b7
36 changed files with 679 additions and 80 deletions
|
|
@ -7,7 +7,7 @@ import { card, mono, pre } from "../openontology/ui";
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "OpenPRD · LogicSRC",
|
title: "OpenPRD · LogicSRC",
|
||||||
description:
|
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" }
|
alternates: { canonical: "/openprd" }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -18,6 +18,8 @@ const SECTIONS: Array<[string, string]> = [
|
||||||
["Users", "Who this is for; personas or segments."],
|
["Users", "Who this is for; personas or segments."],
|
||||||
["Requirements", "Numbered R1, R2, … each tagged [P0], [P1], or [P2]."],
|
["Requirements", "Numbered R1, R2, … each tagged [P0], [P1], or [P2]."],
|
||||||
["UX Notes", "Flows, states, and constraints that shape the experience."],
|
["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."],
|
["Success Metrics", "How the goals will be measured."],
|
||||||
["Risks & Open Questions", "Known risks and the decisions still owed."]
|
["Risks & Open Questions", "Known risks and the decisions still owed."]
|
||||||
];
|
];
|
||||||
|
|
@ -41,18 +43,19 @@ export default function OpenPrdPage(): ReactNode {
|
||||||
read to recover the <em>why</em>.
|
read to recover the <em>why</em>.
|
||||||
</p>
|
</p>
|
||||||
<p style={{ color: "#5b6b7a", fontSize: "0.95rem" }}>
|
<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
|
Status: <strong>0.3</strong>, which adds <code style={mono}>Tech Stack</code> and{" "}
|
||||||
be valid.
|
<code style={mono}>Monetization</code>. A PRD is just a file — it needs no service, and no
|
||||||
|
tooling, to be valid.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="band">
|
<div className="band">
|
||||||
<div className="section-head">
|
<div className="section-head">
|
||||||
<h2>The shape</h2>
|
<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>
|
</div>
|
||||||
<pre style={pre}>{`---
|
<pre style={pre}>{`---
|
||||||
openprd: "0.2"
|
openprd: "0.3"
|
||||||
id: "0001" # four digits, matches the filename
|
id: "0001" # four digits, matches the filename
|
||||||
title: Expand the parked-domain service
|
title: Expand the parked-domain service
|
||||||
status: Draft # Draft|Review|Accepted|Final|Rejected|Withdrawn|Superseded
|
status: Draft # Draft|Review|Accepted|Final|Rejected|Withdrawn|Superseded
|
||||||
|
|
@ -73,6 +76,8 @@ tags: [growth]
|
||||||
- R2 [P1] Next capability.
|
- R2 [P1] Next capability.
|
||||||
|
|
||||||
## UX Notes
|
## UX Notes
|
||||||
|
## Tech Stack
|
||||||
|
## Monetization
|
||||||
## Success Metrics
|
## Success Metrics
|
||||||
## Risks & Open Questions`}</pre>
|
## Risks & Open Questions`}</pre>
|
||||||
<div style={{ display: "grid", gap: "0.6rem", marginTop: "1rem" }}>
|
<div style={{ display: "grid", gap: "0.6rem", marginTop: "1rem" }}>
|
||||||
|
|
@ -130,8 +135,13 @@ tags: [growth]
|
||||||
Its front-matter validates against <code style={mono}>openprd-prd.schema.json</code>.
|
Its front-matter validates against <code style={mono}>openprd-prd.schema.json</code>.
|
||||||
</li>
|
</li>
|
||||||
<li>The id equals the filename's numeric prefix.</li>
|
<li>The id equals the filename'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>
|
</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" }}>
|
<p style={{ color: "#41505d" }}>
|
||||||
Conformance failures are errors. An empty section, a requirement missing its priority tag,
|
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{" "}
|
numbering that skips, a stale index, a one-sided supersession link — those are warnings, and{" "}
|
||||||
|
|
@ -148,7 +158,7 @@ tags: [growth]
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<pre style={pre}>{`logicsrc prd init # template + generated index
|
<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 list # id, title, status, tags, requirements
|
||||||
logicsrc prd validate --strict # conformance + lint, exit 1 on error
|
logicsrc prd validate --strict # conformance + lint, exit 1 on error
|
||||||
logicsrc prd index --write # regenerate prd/README.md
|
logicsrc prd index --write # regenerate prd/README.md
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ Every PRD opens with a YAML front-matter block validated by
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
---
|
---
|
||||||
openprd: "0.2" # standard version (required)
|
openprd: "0.3" # standard version (required)
|
||||||
id: "0001" # 4-digit number == filename prefix (required)
|
id: "0001" # 4-digit number == filename prefix (required)
|
||||||
title: Expand the parked-domain service # imperative title (required)
|
title: Expand the parked-domain service # imperative title (required)
|
||||||
status: Draft # Draft|Review|Accepted|Final|Rejected|Withdrawn|Superseded (required)
|
status: Draft # Draft|Review|Accepted|Final|Rejected|Withdrawn|Superseded (required)
|
||||||
|
|
@ -70,7 +70,7 @@ superseded-by: # optional 4-digit id that replaces this PRD
|
||||||
|
|
||||||
## Body sections
|
## 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.
|
1. `## Problem` — the user/business problem, and why it matters now.
|
||||||
2. `## Goals` — what success looks like, as outcomes (not features).
|
2. `## Goals` — what success looks like, as outcomes (not features).
|
||||||
|
|
@ -78,8 +78,10 @@ The body is Markdown with a fixed, ordered set of `##` sections. All are require
|
||||||
4. `## Users` — who this is for; personas or segments.
|
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.
|
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.
|
6. `## UX Notes` — flows, states, and constraints that shape the experience.
|
||||||
7. `## Success Metrics` — how the goals will be measured.
|
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. `## Risks & Open Questions` — known risks and decisions still owed.
|
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.
|
See [`0000-template.md`](./openprd/0000-template.md) for the copy-paste template.
|
||||||
|
|
||||||
|
|
@ -94,10 +96,11 @@ still just a file: nothing below is required for a document to conform.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
logicsrc prd init # create prd/ with the template and an index
|
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 list # id, title, status, tags, requirement count
|
||||||
logicsrc prd show 0001 # front-matter, sections, and parsed requirements
|
logicsrc prd show 0001 # front-matter, sections, and parsed requirements
|
||||||
logicsrc prd validate --strict # conformance + lint, exit 1 on error
|
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 index --write # regenerate prd/README.md from what is on disk
|
||||||
logicsrc prd status 0001 Review # lifecycle move, refusing illegal transitions
|
logicsrc prd status 0001 Review # lifecycle move, refusing illegal transitions
|
||||||
logicsrc prd tasks 0001 # the optional LogicSRC task bridge
|
logicsrc prd tasks 0001 # the optional LogicSRC task bridge
|
||||||
|
|
@ -137,9 +140,29 @@ name it must be validated as.
|
||||||
|
|
||||||
## Conformance
|
## 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>`,
|
- it lives at `prd/<id>-<slug>.md` with a four-digit `<id>`,
|
||||||
- its front-matter validates against `openprd-prd.schema.json`,
|
- its front-matter validates against `openprd-prd.schema.json`,
|
||||||
- `id` equals the filename's numeric prefix, and
|
- `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.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
openprd: "0.2"
|
openprd: "0.3"
|
||||||
id: "0000"
|
id: "0000"
|
||||||
title: "Short imperative title — start with a verb if possible"
|
title: "Short imperative title — start with a verb if possible"
|
||||||
status: Draft
|
status: Draft
|
||||||
|
|
@ -41,6 +41,16 @@ Who this is for; personas or segments.
|
||||||
|
|
||||||
Flows, states, and constraints that shape the experience.
|
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
|
## Success Metrics
|
||||||
|
|
||||||
How the goals will be measured.
|
How the goals will be measured.
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,7 @@ export function registerPrdCommands(program: Command): void {
|
||||||
writeIndex(resolve(options.dir));
|
writeIndex(resolve(options.dir));
|
||||||
console.log(`Created ${result.path}`);
|
console.log(`Created ${result.path}`);
|
||||||
console.log(`Assigned id ${result.id}. Index updated.`);
|
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) {
|
} catch (error) {
|
||||||
fail((error as Error).message, PRD_EXIT.usage);
|
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("--strict", "treat lint warnings as errors")
|
||||||
.option("--format <format>", "text, json, yaml, or markdown", "text")
|
.option("--format <format>", "text, json, yaml, or markdown", "text")
|
||||||
.option("--id <ref>", "validate a single PRD instead of the collection")
|
.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) => {
|
.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) {
|
if (options.id) {
|
||||||
const { doc } = mustFind(dir, 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));
|
console.log(renderReport(report, options.format as ReportFormat));
|
||||||
if (!report.ok) process.exit(PRD_EXIT.invalid);
|
if (!report.ok) process.exit(PRD_EXIT.invalid);
|
||||||
return;
|
return;
|
||||||
|
|
@ -201,6 +209,7 @@ export function registerPrdCommands(program: Command): void {
|
||||||
const collection = open(dir);
|
const collection = open(dir);
|
||||||
const report = validatePrdCollection(collection, {
|
const report = validatePrdCollection(collection, {
|
||||||
strict: options.strict === true,
|
strict: options.strict === true,
|
||||||
|
expectedVersion,
|
||||||
expectedIndex: renderIndex(collection)
|
expectedIndex: renderIndex(collection)
|
||||||
});
|
});
|
||||||
console.log(renderReport(report, options.format as ReportFormat));
|
console.log(renderReport(report, options.format as ReportFormat));
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,10 @@ numbered, committed collection under prd/, one Markdown file each.
|
||||||
for the template.
|
for the template.
|
||||||
- Front-matter carries openprd, id, title, status, authors, and optional repo,
|
- Front-matter carries openprd, id, title, status, authors, and optional repo,
|
||||||
dates, discussion, implementation, tags, supersedes, superseded-by.
|
dates, discussion, implementation, tags, supersedes, superseded-by.
|
||||||
- The body has eight required sections in order: Problem, Goals, Non-Goals,
|
- The body has ten required sections in order: Problem, Goals, Non-Goals,
|
||||||
Users, Requirements, UX Notes, Success Metrics, Risks & Open Questions.
|
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].
|
- Requirements are numbered R1, R2, … each tagged [P0], [P1], or [P2].
|
||||||
- Lifecycle: Draft → Review → Accepted → Final, or Rejected / Withdrawn /
|
- Lifecycle: Draft → Review → Accepted → Final, or Rejected / Withdrawn /
|
||||||
Superseded. Status lives in front-matter and is the source of truth.
|
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",
|
"logicsrc://openprd/spec",
|
||||||
{
|
{
|
||||||
title: "OpenPRD specification",
|
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"
|
mimeType: "text/markdown"
|
||||||
},
|
},
|
||||||
async () => ({
|
async () => ({
|
||||||
|
|
@ -136,7 +138,7 @@ export function registerOpenPrd(server: McpServer): void {
|
||||||
{
|
{
|
||||||
title: "Validate the PRD collection",
|
title: "Validate the PRD collection",
|
||||||
description:
|
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() },
|
inputSchema: { strict: z.boolean().optional() },
|
||||||
annotations: { readOnlyHint: true, openWorldHint: false }
|
annotations: { readOnlyHint: true, openWorldHint: false }
|
||||||
},
|
},
|
||||||
|
|
@ -235,12 +237,18 @@ export function registerOpenPrd(server: McpServer): void {
|
||||||
type: "text" as const,
|
type: "text" as const,
|
||||||
text: `Draft an OpenPRD document for the change the user describes.
|
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.
|
title starting with a verb, status Draft, and at least one author.
|
||||||
|
|
||||||
Then all eight sections, in this order, none omitted:
|
Then all ten sections, in this order, none omitted:
|
||||||
Problem, Goals, Non-Goals, Users, Requirements, UX Notes, Success Metrics,
|
Problem, Goals, Non-Goals, Users, Requirements, UX Notes, Tech Stack,
|
||||||
Risks & Open Questions. A section may be a single line such as _None._
|
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],
|
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.
|
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,
|
type: "text" as const,
|
||||||
text: `Review this PRD.
|
text: `Review this PRD.
|
||||||
|
|
||||||
Check the shape first: all eight sections present and in order, requirements numbered
|
Check the shape first: every section the declared openprd version requires, present
|
||||||
contiguously with priority tags, front-matter complete.
|
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
|
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
|
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
|
measure the Goals? Is the Tech Stack specific enough to cost the Requirements, or
|
||||||
decoration? Say what you would change and why.`
|
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.`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -203,7 +203,7 @@ describe("MCP: OpenPRD", () => {
|
||||||
it("reports the next free id and the allowed lifecycle moves", async () => {
|
it("reports the next free id and the allowed lifecycle moves", async () => {
|
||||||
const client = await connect();
|
const client = await connect();
|
||||||
// Asserted against the live prd/ directory, so this advances with every PRD added.
|
// 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 moves = await client.callTool({ name: "prd_next_statuses", arguments: { ref: "0001" } });
|
||||||
const payload = JSON.parse(toolText(moves)) as { status: string; allowedNext: string[] };
|
const payload = JSON.parse(toolText(moves)) as { status: string; allowedNext: string[] };
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@
|
||||||
* @logicsrc/openprd — reference implementation of the OpenPRD standard.
|
* @logicsrc/openprd — reference implementation of the OpenPRD standard.
|
||||||
*
|
*
|
||||||
* The standard is docs/openprd.md plus `openprd-prd.schema.json`; this package
|
* 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.
|
* 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 type * from "./types.js";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { formatId, parsePrd, PrdParseError, rewriteFrontMatter, slugify } from "
|
||||||
import { SECTIONS } from "./types.js";
|
import { SECTIONS } from "./types.js";
|
||||||
|
|
||||||
const MINIMAL = `---
|
const MINIMAL = `---
|
||||||
openprd: "0.2"
|
openprd: "0.3"
|
||||||
id: "0007"
|
id: "0007"
|
||||||
title: Do the thing
|
title: Do the thing
|
||||||
status: Draft
|
status: Draft
|
||||||
|
|
@ -38,6 +38,14 @@ Everyone.
|
||||||
|
|
||||||
_None._
|
_None._
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
Node and Postgres.
|
||||||
|
|
||||||
|
## Monetization
|
||||||
|
|
||||||
|
_None._
|
||||||
|
|
||||||
## Success Metrics
|
## Success Metrics
|
||||||
|
|
||||||
It stops hurting.
|
It stops hurting.
|
||||||
|
|
@ -63,7 +71,7 @@ describe("parsePrd", () => {
|
||||||
expect(doc.file).toBe("0007-do-the-thing.md");
|
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]);
|
expect(doc.sections.map((s) => s.name)).toEqual([...SECTIONS]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,16 @@ Who this is for; personas or segments.
|
||||||
|
|
||||||
Flows, states, and constraints that shape the experience.
|
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
|
## Success Metrics
|
||||||
|
|
||||||
How the goals will be measured.
|
How the goals will be measured.
|
||||||
|
|
@ -183,6 +193,10 @@ function placeholder(section: string): string {
|
||||||
return "- R1 [P0] _TODO: first required capability._";
|
return "- R1 [P0] _TODO: first required capability._";
|
||||||
case "UX Notes":
|
case "UX Notes":
|
||||||
return "_TODO: flows, states, and constraints._";
|
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":
|
case "Success Metrics":
|
||||||
return "_TODO: how the goals will be measured._";
|
return "_TODO: how the goals will be measured._";
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,9 @@
|
||||||
* speak this shape.
|
* 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 = [
|
export const SECTIONS = [
|
||||||
"Problem",
|
"Problem",
|
||||||
"Goals",
|
"Goals",
|
||||||
|
|
@ -17,12 +17,33 @@ export const SECTIONS = [
|
||||||
"Users",
|
"Users",
|
||||||
"Requirements",
|
"Requirements",
|
||||||
"UX Notes",
|
"UX Notes",
|
||||||
|
"Tech Stack",
|
||||||
|
"Monetization",
|
||||||
"Success Metrics",
|
"Success Metrics",
|
||||||
"Risks & Open Questions"
|
"Risks & Open Questions"
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type SectionName = (typeof SECTIONS)[number];
|
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 = [
|
export const STATUSES = [
|
||||||
"Draft",
|
"Draft",
|
||||||
"Review",
|
"Review",
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ function scratch(): string {
|
||||||
function conforming(overrides: { frontMatter?: string; body?: string } = {}): string {
|
function conforming(overrides: { frontMatter?: string; body?: string } = {}): string {
|
||||||
const frontMatter =
|
const frontMatter =
|
||||||
overrides.frontMatter ??
|
overrides.frontMatter ??
|
||||||
`openprd: "0.2"
|
`openprd: "0.3"
|
||||||
id: "0001"
|
id: "0001"
|
||||||
title: Do the thing
|
title: Do the thing
|
||||||
status: Draft
|
status: Draft
|
||||||
|
|
@ -59,6 +59,14 @@ Everyone.
|
||||||
|
|
||||||
_None._
|
_None._
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
Node and Postgres.
|
||||||
|
|
||||||
|
## Monetization
|
||||||
|
|
||||||
|
_None._
|
||||||
|
|
||||||
## Success Metrics
|
## Success Metrics
|
||||||
|
|
||||||
It stops hurting.
|
It stops hurting.
|
||||||
|
|
@ -90,21 +98,21 @@ describe("document conformance", () => {
|
||||||
|
|
||||||
it("rejects front-matter that fails the schema", () => {
|
it("rejects front-matter that fails the schema", () => {
|
||||||
const missingStatus = conforming({
|
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");
|
expect(codes(missingStatus)).toContain("OP-C-FRONTMATTER");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects an unknown status value", () => {
|
it("rejects an unknown status value", () => {
|
||||||
const bad = conforming({
|
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");
|
expect(codes(bad)).toContain("OP-C-FRONTMATTER");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects an id that does not match the filename prefix", () => {
|
it("rejects an id that does not match the filename prefix", () => {
|
||||||
const mismatch = conforming({
|
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");
|
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", () => {
|
describe("document lint", () => {
|
||||||
it("warns about an empty section and escalates it under --strict", () => {
|
it("warns about an empty section and escalates it under --strict", () => {
|
||||||
const empty = conforming().replace("## UX Notes\n\n_None._", "## UX Notes\n");
|
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", () => {
|
it("warns when a PRD lists no authors", () => {
|
||||||
const noAuthors = conforming({
|
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");
|
expect(codes(noAuthors)).toContain("OP-L-NO-AUTHOR");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("errors when updated is before created", () => {
|
it("errors when updated is before created", () => {
|
||||||
const backwards = conforming({
|
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"));
|
const findings = validatePrdDocument(parsePrd(backwards, "0001-do-the-thing.md"));
|
||||||
expect(findings.find((f) => f.code === "OP-L-DATE-ORDER")?.severity).toBe("error");
|
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", () => {
|
it("errors when status is Superseded with no replacement named", () => {
|
||||||
const superseded = conforming({
|
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");
|
expect(codes(superseded)).toContain("OP-L-SUPERSEDED-BY");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("errors when a PRD supersedes itself", () => {
|
it("errors when a PRD supersedes itself", () => {
|
||||||
const selfRef = conforming({
|
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");
|
expect(codes(selfRef)).toContain("OP-L-SELF-REFERENCE");
|
||||||
});
|
});
|
||||||
|
|
@ -225,7 +309,7 @@ describe("collection rules", () => {
|
||||||
const collection = collectionWith({
|
const collection = collectionWith({
|
||||||
"0001-one.md": conforming(),
|
"0001-one.md": conforming(),
|
||||||
"0003-three.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);
|
const report = validatePrdCollection(collection);
|
||||||
|
|
@ -237,7 +321,7 @@ describe("collection rules", () => {
|
||||||
const collection = collectionWith({
|
const collection = collectionWith({
|
||||||
"0001-one.md": conforming(),
|
"0001-one.md": conforming(),
|
||||||
"0002-two.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");
|
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", () => {
|
it("errors when a cross-reference points outside the collection", () => {
|
||||||
const collection = collectionWith({
|
const collection = collectionWith({
|
||||||
"0001-one.md": conforming({
|
"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");
|
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", () => {
|
it("warns when supersession is recorded on only one side", () => {
|
||||||
const collection = collectionWith({
|
const collection = collectionWith({
|
||||||
"0001-one.md": conforming({
|
"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({
|
"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");
|
expect(validatePrdCollection(collection).findings.map((f) => f.code)).toContain("OP-L-ONE-SIDED-REFERENCE");
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,15 @@
|
||||||
import { validate as validateSchema } from "@logicsrc/validators";
|
import { validate as validateSchema } from "@logicsrc/validators";
|
||||||
import { slugify } from "./parse.js";
|
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 {
|
export interface ValidateOptions {
|
||||||
/** Promote lint warnings to errors, for CI that wants a clean collection. */
|
/** 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>
|
* - lives at prd/<id>-<slug>.md with a four-digit <id>
|
||||||
* - front-matter validates against openprd-prd.schema.json
|
* - front-matter validates against openprd-prd.schema.json
|
||||||
* - id equals the filename's numeric prefix
|
* - 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
|
* Everything beyond those four is lint: useful, but never the difference
|
||||||
* between conforming and not.
|
* 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 present = doc.sections.map((section) => section.name);
|
||||||
const expected = [...SECTIONS];
|
|
||||||
|
|
||||||
for (const name of expected) {
|
for (const name of expected) {
|
||||||
if (!present.includes(name)) {
|
if (!present.includes(name)) {
|
||||||
|
|
@ -84,12 +96,12 @@ export function validatePrdDocument(doc: PrdDocument, options: ValidateOptions =
|
||||||
code: "OP-C-SECTION-MISSING",
|
code: "OP-C-SECTION-MISSING",
|
||||||
severity: "error",
|
severity: "error",
|
||||||
message: `missing required section "## ${name}"`,
|
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));
|
const ordered = expected.filter((name) => required.includes(name));
|
||||||
if (required.length === ordered.length && required.join("|") !== ordered.join("|")) {
|
if (required.length === ordered.length && required.join("|") !== ordered.join("|")) {
|
||||||
add({
|
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) {
|
for (const name of extra) {
|
||||||
add({
|
add({
|
||||||
code: "OP-L-EXTRA-SECTION",
|
code: "OP-L-EXTRA-SECTION",
|
||||||
severity: "info",
|
severity: "info",
|
||||||
line: doc.sections.find((s) => s.name === name)?.line,
|
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"
|
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) {
|
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;
|
if (!section.empty) continue;
|
||||||
add({
|
add({
|
||||||
code: "OP-L-EMPTY-SECTION",
|
code: "OP-L-EMPTY-SECTION",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"openprdConformance": "0.2",
|
"openprdConformance": "0.3",
|
||||||
"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.",
|
"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": [
|
"valid": [
|
||||||
{
|
{
|
||||||
"fixture": "valid/minimal.md",
|
"fixture": "valid/minimal.md",
|
||||||
|
|
@ -25,6 +25,11 @@
|
||||||
{
|
{
|
||||||
"fixture": "valid/none-sections.md",
|
"fixture": "valid/none-sections.md",
|
||||||
"file": "0005-keep-every-section-even-when-empty.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": [
|
"invalid": [
|
||||||
|
|
@ -32,7 +37,13 @@
|
||||||
"fixture": "invalid/missing-section.md",
|
"fixture": "invalid/missing-section.md",
|
||||||
"file": "0001-missing-a-section.md",
|
"file": "0001-missing-a-section.md",
|
||||||
"code": "OP-C-SECTION-MISSING",
|
"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",
|
"fixture": "invalid/out-of-order.md",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
openprd: "0.2"
|
openprd: "0.3"
|
||||||
id: "0001"
|
id: "0001"
|
||||||
title: Bad filename
|
title: Bad filename
|
||||||
status: Draft
|
status: Draft
|
||||||
|
|
@ -31,6 +31,15 @@ _None._
|
||||||
|
|
||||||
_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
|
## Success Metrics
|
||||||
|
|
||||||
_None._
|
_None._
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
openprd: "0.2"
|
openprd: "0.3"
|
||||||
id: "1"
|
id: "1"
|
||||||
title: Two digit id
|
title: Two digit id
|
||||||
status: Draft
|
status: Draft
|
||||||
|
|
@ -31,6 +31,15 @@ _None._
|
||||||
|
|
||||||
_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
|
## Success Metrics
|
||||||
|
|
||||||
_None._
|
_None._
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
openprd: "0.2"
|
openprd: "0.3"
|
||||||
id: "0001"
|
id: "0001"
|
||||||
title: Unknown status
|
title: Unknown status
|
||||||
status: Shipped
|
status: Shipped
|
||||||
|
|
@ -31,6 +31,15 @@ _None._
|
||||||
|
|
||||||
_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
|
## Success Metrics
|
||||||
|
|
||||||
_None._
|
_None._
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
openprd: "0.2"
|
openprd: "0.3"
|
||||||
id: "0001"
|
id: "0001"
|
||||||
title: Updated before created
|
title: Updated before created
|
||||||
status: Draft
|
status: Draft
|
||||||
|
|
@ -33,6 +33,15 @@ _None._
|
||||||
|
|
||||||
_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
|
## Success Metrics
|
||||||
|
|
||||||
_None._
|
_None._
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
openprd: "0.2"
|
openprd: "0.3"
|
||||||
id: "0001"
|
id: "0001"
|
||||||
title: Duplicate requirement id
|
title: Duplicate requirement id
|
||||||
status: Draft
|
status: Draft
|
||||||
|
|
@ -32,6 +32,15 @@ _None._
|
||||||
|
|
||||||
_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
|
## Success Metrics
|
||||||
|
|
||||||
_None._
|
_None._
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
openprd: "0.2"
|
openprd: "0.3"
|
||||||
id: "0042"
|
id: "0042"
|
||||||
title: Id does not match
|
title: Id does not match
|
||||||
status: Draft
|
status: Draft
|
||||||
|
|
@ -31,6 +31,15 @@ _None._
|
||||||
|
|
||||||
_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
|
## Success Metrics
|
||||||
|
|
||||||
_None._
|
_None._
|
||||||
|
|
|
||||||
|
|
@ -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._
|
||||||
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
openprd: "0.2"
|
openprd: "0.3"
|
||||||
id: "0001"
|
id: "0001"
|
||||||
title: Missing a section
|
title: Missing a section
|
||||||
status: Draft
|
status: Draft
|
||||||
|
|
@ -27,6 +27,15 @@ _None._
|
||||||
|
|
||||||
_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
|
## Success Metrics
|
||||||
|
|
||||||
_None._
|
_None._
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
openprd: "0.2"
|
openprd: "0.3"
|
||||||
id: "0001"
|
id: "0001"
|
||||||
status: Draft
|
status: Draft
|
||||||
authors:
|
authors:
|
||||||
|
|
@ -30,6 +30,15 @@ _None._
|
||||||
|
|
||||||
_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
|
## Success Metrics
|
||||||
|
|
||||||
_None._
|
_None._
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,15 @@ _None._
|
||||||
|
|
||||||
_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
|
## Success Metrics
|
||||||
|
|
||||||
_None._
|
_None._
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
openprd: "0.2"
|
openprd: "0.3"
|
||||||
id: "0001"
|
id: "0001"
|
||||||
title: Sections out of order
|
title: Sections out of order
|
||||||
status: Draft
|
status: Draft
|
||||||
|
|
@ -31,6 +31,15 @@ _None._
|
||||||
|
|
||||||
_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
|
## Success Metrics
|
||||||
|
|
||||||
_None._
|
_None._
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
openprd: "0.2"
|
openprd: "0.3"
|
||||||
id: "0001"
|
id: "0001"
|
||||||
title: Superseded with no replacement
|
title: Superseded with no replacement
|
||||||
status: Superseded
|
status: Superseded
|
||||||
|
|
@ -31,6 +31,15 @@ _None._
|
||||||
|
|
||||||
_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
|
## Success Metrics
|
||||||
|
|
||||||
_None._
|
_None._
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
openprd: "0.2"
|
openprd: "0.3"
|
||||||
id: "0001"
|
id: "0001"
|
||||||
title: Unknown front matter key
|
title: Unknown front matter key
|
||||||
status: Draft
|
status: Draft
|
||||||
|
|
@ -32,6 +32,15 @@ _None._
|
||||||
|
|
||||||
_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
|
## Success Metrics
|
||||||
|
|
||||||
_None._
|
_None._
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
openprd: "0.2"
|
openprd: "0.3"
|
||||||
id: "0002"
|
id: "0002"
|
||||||
title: Use bold requirement markers
|
title: Use bold requirement markers
|
||||||
status: Review
|
status: Review
|
||||||
|
|
@ -31,6 +31,15 @@ _None._
|
||||||
|
|
||||||
_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
|
## Success Metrics
|
||||||
|
|
||||||
_None._
|
_None._
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
openprd: "0.2"
|
openprd: "0.3"
|
||||||
id: "0001"
|
id: "0001"
|
||||||
title: Expand the parked-domain service
|
title: Expand the parked-domain service
|
||||||
status: Draft
|
status: Draft
|
||||||
|
|
@ -38,6 +38,15 @@ _None._
|
||||||
|
|
||||||
_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
|
## Success Metrics
|
||||||
|
|
||||||
_None._
|
_None._
|
||||||
|
|
|
||||||
41
packages/schemas/fixtures/openprd/valid/legacy-0-2.md
Normal file
41
packages/schemas/fixtures/openprd/valid/legacy-0-2.md
Normal 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._
|
||||||
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
openprd: "0.2"
|
openprd: "0.3"
|
||||||
id: "0001"
|
id: "0001"
|
||||||
title: Expand the parked-domain service
|
title: Expand the parked-domain service
|
||||||
status: Draft
|
status: Draft
|
||||||
|
|
@ -31,6 +31,14 @@ _None._
|
||||||
|
|
||||||
_None._
|
_None._
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
_None._
|
||||||
|
|
||||||
|
## Monetization
|
||||||
|
|
||||||
|
_None._
|
||||||
|
|
||||||
## Success Metrics
|
## Success Metrics
|
||||||
|
|
||||||
_None._
|
_None._
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
openprd: "0.2"
|
openprd: "0.3"
|
||||||
id: "0005"
|
id: "0005"
|
||||||
title: Keep every section even when empty
|
title: Keep every section even when empty
|
||||||
status: Accepted
|
status: Accepted
|
||||||
|
|
@ -31,6 +31,14 @@ _None._
|
||||||
|
|
||||||
_None._
|
_None._
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
_None._
|
||||||
|
|
||||||
|
## Monetization
|
||||||
|
|
||||||
|
_None._
|
||||||
|
|
||||||
## Success Metrics
|
## Success Metrics
|
||||||
|
|
||||||
_None._
|
_None._
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
openprd: "0.2"
|
openprd: "0.3"
|
||||||
id: "0003"
|
id: "0003"
|
||||||
title: Organize a long requirements section
|
title: Organize a long requirements section
|
||||||
status: Draft
|
status: Draft
|
||||||
|
|
@ -37,6 +37,15 @@ _None._
|
||||||
|
|
||||||
_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
|
## Success Metrics
|
||||||
|
|
||||||
_None._
|
_None._
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
openprd: "0.2"
|
openprd: "0.3"
|
||||||
id: "0004"
|
id: "0004"
|
||||||
title: Retire the old flow
|
title: Retire the old flow
|
||||||
status: Superseded
|
status: Superseded
|
||||||
|
|
@ -32,6 +32,15 @@ _None._
|
||||||
|
|
||||||
_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
|
## Success Metrics
|
||||||
|
|
||||||
_None._
|
_None._
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
openprd: "0.2"
|
openprd: "0.3"
|
||||||
id: "0000"
|
id: "0000"
|
||||||
title: "Short imperative title — start with a verb if possible"
|
title: "Short imperative title — start with a verb if possible"
|
||||||
status: Draft
|
status: Draft
|
||||||
|
|
@ -41,6 +41,16 @@ Who this is for; personas or segments.
|
||||||
|
|
||||||
Flows, states, and constraints that shape the experience.
|
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
|
## Success Metrics
|
||||||
|
|
||||||
How the goals will be measured.
|
How the goals will be measured.
|
||||||
|
|
|
||||||
129
prd/0007-add-tech-stack-and-monetization-to-openprd.md
Normal file
129
prd/0007-add-tech-stack-and-monetization-to-openprd.md
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
---
|
||||||
|
openprd: "0.3"
|
||||||
|
id: "0007"
|
||||||
|
title: Add Tech Stack and Monetization sections to OpenPRD
|
||||||
|
status: Draft
|
||||||
|
authors:
|
||||||
|
- anthony@profullstack.com
|
||||||
|
repo: profullstack/logicsrc
|
||||||
|
created: 2026-09-06
|
||||||
|
updated: 2026-09-06
|
||||||
|
discussion:
|
||||||
|
implementation:
|
||||||
|
tags:
|
||||||
|
- openprd
|
||||||
|
- standards
|
||||||
|
- monetization
|
||||||
|
supersedes:
|
||||||
|
superseded-by:
|
||||||
|
---
|
||||||
|
|
||||||
|
# Add Tech Stack and Monetization sections to OpenPRD
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
OpenPRD 0.2 fixes eight sections, and two questions that decide whether a
|
||||||
|
product decision is a good one are missing from all of them.
|
||||||
|
|
||||||
|
The first is what the thing is built on. Requirements cannot be costed without
|
||||||
|
it, so the stack gets picked in the first implementation PR instead, by
|
||||||
|
whoever opens it, and the PRD that was supposed to record the *why* is silent
|
||||||
|
on the most expensive choice in the change.
|
||||||
|
|
||||||
|
The second is how it earns. Every section in 0.2 is about the user and none is
|
||||||
|
about the business. A PRD can be filled out completely, reviewed, accepted, and
|
||||||
|
shipped without anyone writing down who pays. Across the fleet that is exactly
|
||||||
|
the question that goes unanswered until after launch.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
- A reader of any OpenPRD 0.3 document can tell what it will be built on and
|
||||||
|
how it earns without leaving the file.
|
||||||
|
- The stack and the revenue model are settled at review time, when changing
|
||||||
|
them is still cheap, rather than in the implementing PR.
|
||||||
|
- "This does not earn on its own" stays a legitimate answer, said out loud
|
||||||
|
rather than by omission.
|
||||||
|
- Every document already written against 0.2 keeps conforming, untouched.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- No financial modelling: the section states a model, not a forecast, and
|
||||||
|
nothing validates the numbers.
|
||||||
|
- No architecture review: `Tech Stack` names what will be used, it is not an
|
||||||
|
ADR and does not replace one.
|
||||||
|
- No forced migration. Collections adopt 0.3 per document, or never.
|
||||||
|
- No front-matter change. Both additions are body sections; the schema is
|
||||||
|
untouched.
|
||||||
|
|
||||||
|
## Users
|
||||||
|
|
||||||
|
- **Authors** — human or agent — who now have somewhere to put two decisions
|
||||||
|
that were previously made in silence.
|
||||||
|
- **Reviewers**, who get a costable stack and a stated revenue model in the
|
||||||
|
document they are already reading.
|
||||||
|
- **Third-party implementers** of the standard, who need to know that a
|
||||||
|
document is judged against the version it declares.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- R1 [P0] `## Tech Stack` and `## Monetization` are required body sections in
|
||||||
|
OpenPRD 0.3, in that order, between `## UX Notes` and `## Success Metrics`.
|
||||||
|
- R2 [P0] Both accept `_None._`, like every other section.
|
||||||
|
- R3 [P0] A document is validated against the section list its own `openprd:`
|
||||||
|
key fixes, so a `0.2` document is still held to eight sections and still
|
||||||
|
conforms.
|
||||||
|
- R4 [P0] `logicsrc prd new` scaffolds ten stub sections, and the shipped
|
||||||
|
template carries both.
|
||||||
|
- R5 [P1] The conformance bundle proves both directions: a 0.3 document missing
|
||||||
|
`Monetization` fails with `OP-C-SECTION-MISSING`, and a 0.2 document with
|
||||||
|
eight sections passes.
|
||||||
|
- R6 [P1] The specification states the compatibility rule and how to adopt 0.3
|
||||||
|
in an existing collection.
|
||||||
|
- R7 [P2] `draft_prd` and `review_prd` on the MCP surface ask for both
|
||||||
|
sections, and `review_prd` challenges a stack too vague to cost and a
|
||||||
|
monetization answer that dodges who pays.
|
||||||
|
|
||||||
|
## UX Notes
|
||||||
|
|
||||||
|
Nothing about authoring changes: the sections appear in the template and in
|
||||||
|
`logicsrc prd new` output, with the same `_TODO:` placeholders as the rest.
|
||||||
|
|
||||||
|
The failure a validator produces for a 0.3 document that predates the change is
|
||||||
|
the ordinary `OP-C-SECTION-MISSING`, naming the section and listing what the
|
||||||
|
declared version requires — so the fix is visible in the message, and the
|
||||||
|
alternative fix (leave it at 0.2) is a one-line edit.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
No new dependency. TypeScript in `packages/openprd` (the section list moves
|
||||||
|
from one constant to a version-keyed lookup), the existing JSON Schema in
|
||||||
|
`packages/schemas` — unchanged, since the front-matter is unchanged — the
|
||||||
|
Markdown fixtures beside it, the Next.js landing page in `apps/logicsrc-web`,
|
||||||
|
and the MCP surface in `packages/logicsrc-mcp`. Vitest covers it.
|
||||||
|
|
||||||
|
## Monetization
|
||||||
|
|
||||||
|
_None._ OpenPRD is an open standard published to be copied and cited; it is not
|
||||||
|
sold and carries no plan, meter, or SKU. It earns indirectly, by making the
|
||||||
|
LogicSRC standards surface worth adopting — and, from this change onward, by
|
||||||
|
making sure every product decision downstream of it has answered the revenue
|
||||||
|
question in writing.
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
|
||||||
|
- Every PRD written in this repo from 0007 onward declares `0.3` and fills both
|
||||||
|
sections.
|
||||||
|
- The conformance bundle's 0.2 fixture keeps passing, unedited, across future
|
||||||
|
releases: the proof that the version rule holds.
|
||||||
|
- Reviews stop discovering the stack in the implementation PR.
|
||||||
|
|
||||||
|
## Risks & Open Questions
|
||||||
|
|
||||||
|
- Ten required sections is more ceremony, and ceremony gets skipped. If
|
||||||
|
`Monetization` becomes reflexive `_None._` on every PRD, the section has
|
||||||
|
failed and should be reconsidered rather than left as decoration.
|
||||||
|
- Version-aware validation means two live section lists forever. A third
|
||||||
|
addition would make three; at that point the rule needs a real deprecation
|
||||||
|
policy rather than a growing lookup.
|
||||||
|
- Open: whether this repo's own 0001-0006 should be migrated to 0.3 or left as
|
||||||
|
the standing evidence that 0.2 documents still conform. Left at 0.2 for now.
|
||||||
|
|
@ -17,3 +17,4 @@ Status lives in each file's front-matter and is the source of truth:
|
||||||
| [0004](./0004-add-logicsrc-opencreds-spec.md) | Add the LogicSRC OpenCreds specification | Draft | opencreds, credentials, vault, encryption, portability, schemas |
|
| [0004](./0004-add-logicsrc-opencreds-spec.md) | Add the LogicSRC OpenCreds specification | Draft | opencreds, credentials, vault, encryption, portability, schemas |
|
||||||
| [0005](./0005-add-logicsrc-openswarm-spec.md) | Add the LogicSRC OpenSwarm specification family | Draft | openswarm, ipfile, ipdb, ipaudio, ipvideo, iplive, bittorrent, x402, c0mpute |
|
| [0005](./0005-add-logicsrc-openswarm-spec.md) | Add the LogicSRC OpenSwarm specification family | Draft | openswarm, ipfile, ipdb, ipaudio, ipvideo, iplive, bittorrent, x402, c0mpute |
|
||||||
| [0006](./0006-add-pay2seed-spec.md) | Add pay2seed, paid2seed, pay2stream and paid2stream to the OpenSwarm family | Draft | openswarm, pay2seed, paid2seed, pay2stream, paid2stream, iplive, hls, ipfile, ippay, ipdb, bittorrent, torlink, bittorrented, c0mpute |
|
| [0006](./0006-add-pay2seed-spec.md) | Add pay2seed, paid2seed, pay2stream and paid2stream to the OpenSwarm family | Draft | openswarm, pay2seed, paid2seed, pay2stream, paid2stream, iplive, hls, ipfile, ippay, ipdb, bittorrent, torlink, bittorrented, c0mpute |
|
||||||
|
| [0007](./0007-add-tech-stack-and-monetization-to-openprd.md) | Add Tech Stack and Monetization sections to OpenPRD | Draft | openprd, standards, monetization |
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue