prd-writing
GitHub指导如何编写严谨的产品需求文档(PRD)和技能。强调在动笔前必须阅读并验证现有代码,追踪所有下游消费者及性能影响,记录被否决的方案和开放问题。适用于涉及跨子系统或架构变更的功能设计文档撰写与评审。
Trigger Scenarios
Install
npx skills add netalertx/NetAlertX --skill prd-writing -g -y
SKILL.md
Frontmatter
{
"name": "prd-writing",
"description": "Rigorous PRD-writing methodology — challenge the idea, verify every claim against actual code, trace every downstream consumer of a mechanism, evaluate performance impact against the schema\/indexes that actually exist, record rejected alternatives and open-issue decisions explicitly, and do a dedicated final-check pass before calling it done. Use this when asked to write, draft, or review a PRD, design doc, or feature proposal for this codebase."
}
PRD Writing
When to use
Triggered by: "write a PRD", "draft a design doc", "spec out this feature", "create a PRD for X". Reserve this for changes where getting the design wrong is expensive to unwind — new cross-cutting mechanisms, schema changes, anything touching multiple subsystems. A one-file bug fix doesn't need this process.
A UI feature that needs to survive a page reload, coordinate across tabs, or react to a backend push/poll is a cross-cutting mechanism even when it looks like "just a small UI feature." It's easy to start implementing straight away because the visible surface (a badge, an icon) looks trivial - but the hard part is always the state-propagation mechanism underneath, and that mechanism usually has hidden dependencies on other code that already touches the same data. A real case: a "settings still applying" indicator went through three full implementation-and-break cycles (a cookie with a guessed timeout, a new PHP endpoint duplicating existing logic, a localStorage/polling redesign with its own bugs) before anyone traced every existing consumer of app_state.json and found that front/js/sse_manager.js's handleStateUpdate() already pushed the exact signal needed to every open page. Step 4 ("trace every downstream consumer") below would have caught this on attempt #1 if it had been applied before writing any code, not after three failures.
Core principle: a PRD is a claim-verification exercise, not a writing exercise
Every sentence that asserts something about how the code currently works must be checked against the actual code before it goes in — not written from memory, not inferred from a plugin's name or reputation, not assumed because it sounds plausible. Two failure patterns to watch for:
- Calling a plugin "the cleanest case" for some behavior based on its category alone. Read the actual script — it may already report a live per-row signal and compute an equivalent value internally that was never wired up.
- Claiming "no changes needed" for a query based on one sub-case (e.g. a device that starts absent) without checking the transition sub-case (a device going from online to newly-suppressed) — the two sub-cases can use different existence checks, so a fix covering one can silently miss the other.
Both are plausible, well-written, and wrong. Reading the code first catches both.
Process
- Understand the current mechanism by reading the actual code before writing anything. Cite
file:linefor every claim about current behavior. Delegate to an Explore/general-purpose agent for breadth if the surface area is large, but treat its findings as a starting point to spot-check, not a finished citation — verify anything load-bearing yourself before it goes in the PRD. The same applies to a prior audit or PRD this one continues from: re-read its full detail section for the specific finding, not just a one-line summary-table row, before citing or extending it — a summary row can omit a caveat ("already indexed," "already fixed elsewhere") that only the detail text states, and citing the row alone can reintroduce a claim the detail text already corrected. - Challenge the idea before designing it. If the user proposes a solution, ask: is this solving the right problem? Does it conflate unrelated concerns (see axis-separation, next)? Does a similar or previously-rejected mechanism already exist that this would collide with semantically? A naming near-collision with an existing field/concept that has different, incompatible semantics is a signal to stop and check precedence rules, not a coincidence to wave off.
- Identify the independent axes. A feature request that arrives as "option A and option B" is often two or three orthogonal concerns bundled together — e.g. "should this exist at all," "should it notify," and "should it assert presence" are three separate questions, not one. Cramming them into a single enum/flag produces combinations you can't express later (what if a plugin wants A+C but not B?). Give each axis its own mechanism. A recurring instance of this: if the design detects or handles data that indicates a likely security-relevant event (a plugin sanitizing suspicious/attacker-influenced input, a rejected/malformed value, an auth failure, etc.), "log it" and "alert the user about it" are two separate axes, not one. Server-log visibility (
mylog("none", ...)in this codebase — the always-shown floor, matching how real errors are already logged) is close to a hard requirement whenever such an event is detected at all; a user-facing alert (write_notification()) is a materially bigger decision — it's persistent, unprompted, and (per existing precedent, e.g.api_server_start.py's unauthorized-access alert) not rate-limited anywhere in this codebase, so a repeat offender can spam it. Don't let "should we detect/handle this" and "should we alert the user about it" collapse into one decision — record the logging as close to a given, and the user-facing alert as its own, explicit, separately-decidable (and separately deferrable) open issue. - For every mechanism, trace every downstream consumer — not just the first one you find. The single highest-value question before calling a design complete: "where else does this exact same check or logic get independently re-derived?" In a codebase without one source of truth for a concept (e.g. "is this record currently active" computed by three different queries in three different files), patching the first occurrence and stopping is the most common way a design ships with a hidden, silent gap. Grep for the pattern, not just the function you already know about.
- Record rejected alternatives with the reasoning, not just the chosen design. Give it its own subsection (
### Rejected: X). Without this, a future reader — or your own future self — re-proposes the rejected idea because the "why not" only ever existed in a conversation, not in the document. - Force every open question to an explicit decision, even if the decision is "accept as-is for v1, revisit if feedback says otherwise." An open question left unresolved in a PRD gets silently decided by whoever implements it — usually differently than anyone intended.
- Write the test plan as part of the PRD, not after. Concrete test cases — naming real functions/queries, not "add tests for X" — force you to notice design gaps you'd otherwise miss; the moment you try to write "assert Y happens" and realize the current design can't produce Y is often the first time the gap becomes visible. Check the repo for an existing test pattern for this shape of change before inventing a new one (e.g. a prior presence-logic bug fixed via
test/db_test_helpers.pyfixtures is the template for the next one, not a reason to build new test infrastructure). When execution actually starts, write that test code before the implementation, run it against the pre-fix code, and confirm it fails for the right reason before writing a line of the fix. What "the right reason" means depends on what's being tested: a regression test against existing behavior must fail with a real behavioral assertion mismatch, not a collection/import error - a test that never failed red that way can't be trusted to have caught anything, and is usually the first sign the fixture doesn't actually distinguish broken from fixed behavior (a real case: a single-device DB fixture passed against both the buggy and the fixed code, because nothing in it could tell "this row matches" from "any row matches" - seescan-pipelineGotcha 8). A test for a brand-new contract (a function/interface that doesn't exist yet) legitimately fails with a missing-interface error instead (AttributeError,ImportError) before it's written - that's the expected red for that case, not a sign the test is wrong. Writing the test first also tends to surface implementation code that's awkward to exercise in isolation - treat that as a refactor signal, not friction to route around. - Ask explicitly whether validating this needs real end-to-end infrastructure (a new or modified plugin, a UI click-through) or whether synthetic unit-level fixtures suffice — don't assume either way. Check whether the functions under test take a DB connection/dict/list as a parameter (testable in isolation, no real plugin needed) or require a real file on disk (harder to fake, may need one).
- Ask explicitly whether this feature/mechanism should be exposed via the API, and give a recommendation, not just flag it as an open question. This codebase already has three real API surfaces to consider extending rather than inventing a fourth: REST (
server/api_server/*_endpoint.py), GraphQL (graphql_endpoint.py), and a Prometheus/metricsendpoint (prometheus_endpoint.py). Skipping this question doesn't mean "no API access needed"; it means the answer gets silently decided later by whoever first wants to query the new data externally, usually as its own separate feature request re-litigating a design this PRD already had full context to settle. Not everything needs exposure: purely internal/diagnostic state with no plausible external consumer doesn't, but say so explicitly, with the reason, rather than leaving it unaddressed. - Check performance against the real schema and real scale, not assumptions. For every new or changed query: does it use an existing index, or add an unindexed lookup, a new join, or a correlated subquery? Grep
CREATE INDEXfor every index on the tables involved, not just the first one you find — a column can have both a plain index and a separate expression index (e.g.idx_eve_mac_date_type ON Events(eveMac, ...)alongsideidx_eve_lower_mac_date_type ON Events(LOWER(eveMac), ...)), and missing the second one produces a wrong verdict. Then weigh cost by how often the query runs (once is nothing; every few minutes forever is a standing cost) and by real scale — production users run 10,000+ devices, not a homelab handful. ACurrentScanwith 2-5 rows per device (one per contributing plugin) is routinely 20,000-50,000+ rows in one cycle; reason about that number, not a smaller hopeful one. A correlatedEXISTS/subquery re-evaluated per outer row is fine if the correlated column is indexed — e.g.current_scan_presence_condition()(server/scan/presence.py) is exactly this shape againstCurrentScan.scanMac, covered byidx_currentscan_scanmac. The real risk is an unindexed correlated lookup: an accidental self-join scanning the full inner table per outer row, which looks fine and passes tests at small scale but isn't at production scale. Check withEXPLAIN QUERY PLANat a realistic row count, built against the complete real index set (copy everyCREATE INDEXfor the table, or run it against an actualapp.db) rather than a hand-picked subset — a partial index set produces a misleading plan in either direction, not just "looks worse than it is." If it comes back unindexed for real, aGROUP BYaggregate is the usual fix. - Do a dedicated final-check pass, out loud, before calling it done. Re-read the whole document end to end and specifically check:
- Did a correction made mid-document propagate everywhere it needed to (the Design subsection and Affected Files and Tests and any execution-plan summary)? A correction landing in one place and not its siblings is worse than never catching it, because now the document silently contradicts itself.
- Does every "this is the cleanest/simplest real case" claim still hold up if you actually re-read that specific piece of code right now, or was it asserted by pattern-matching a name/category? Re-verify, don't re-assert.
- Does anything render incorrectly as markdown — an unfenced ASCII diagram or code block will collapse into one line under lazy-paragraph-continuation, the same class of bug as a list missing its preceding blank line.
- Do any internal anchor links' slugs match their headings?
- Does the design still cleanly separate its axes, or did a later addition quietly re-conflate two concerns inside what's supposed to be a single-purpose mechanism (the same mistake step 3 exists to catch at the top level can reappear one level down inside an individual mechanism's own value set — e.g. a 3-value enum where two of the values are secretly independent booleans in a trenchcoat).
- Leave a visible trail of corrections instead of silently rewriting. When a review pass — yours or someone else's — finds something wrong, write "Correction (caught in review): ..." inline rather than quietly fixing the earlier text and moving on. This is what makes a PRD trustworthy to a second reader: they can see what was checked and what changed, not just receive a polished final answer with no visible seams.
- When marking a PRD Implemented, ask once whether anything found along the way generalizes beyond this one document — a tooling trick, a wrong assumption that took real digging to disprove, a review catch that would recur on the next PRD. The PRD's own "Implementation notes" section is the right place for what happened in this PRD; a skill or memory update is the right place for anything that would otherwise have to be rediscovered next time. One question, not a mandatory new section — skip it when nothing generalizes, don't force it on every PRD regardless of size.
Structure to follow
- Problem — grounded in specific, cited current behavior, not a general complaint.
- Goals / Non-goals — non-goals should name specific things that sound in-scope but aren't, each with a one-line reason.
- Design — one subsection per independent axis/mechanism (step 3). Include a
### Rejected: Xsubsection for any alternative seriously considered (step 5). - Open issues — each with an explicit recorded decision (step 6), not left dangling.
- Affected files — concrete
file:function:linereferences, not bare filenames. - Backward compatibility — explicit default values and why they preserve current behavior for existing consumers.
- Performance impact — the baseline (what's unindexed/slow today, independent of this change), what the change adds that's negligible, what's new and worth mitigating, and concrete mitigations rather than a vague "should be fine" (step 10).
- Docs/skills to update — anywhere this needs to be reflected outside the code itself (external docs, paired skill files, template files new authors copy from).
- Tests — organized by mechanism, each case naming the real function/query it exercises and the concrete assertion (step 7), plus a manual verification checklist for anything that can't be unit-tested (including an
EXPLAIN QUERY PLANcheck at realistic scale if the Performance impact section found a genuine risk). - (Optional) Execution plan — phased, referencing the same file/function names used above rather than restating the design in vaguer terms.
- Implementation checklist — added to the PRD file itself when implementation actually starts (not at draft time), three boxes matching the "Implementing an approved PRD" section below: plan written before any code; test written and confirmed red against the pre-fix code before the fix was applied; no PRD filename/path/word left in committed code. Check each box only as it's actually done, not preemptively - the point is a forcing function visible in the file you're editing, not a formality.
Before starting: check for an existing architecture-reference skill
If a skill already documents the subsystem the feature touches, load it before researching from scratch — don't re-derive call graphs or mechanism details that are already written down. If the feature touches a subsystem with no such skill, and understanding it required significant re-derivation from raw code, that's a signal to write one afterward so the next PRD in that area doesn't start from zero.
Implementing an approved PRD
Add the Implementation checklist (see Structure above) to the PRD file as the first edit, before touching any other code - an unchecked, visible checklist in the file you're actively working in is the forcing function; a rule read once at skill-load time is not.
Before writing a single line of implementation code, write an explicit plan (via EnterPlanMode or an equivalent written plan) covering which files change, in what order, and which test(s) get written first. Understanding the PRD's design is not the same as planning its implementation - in practice these have been conflated, going straight from "I've read and understood this PRD" into code edits without ever writing either the plan or the test required below.
Then follow step 7 above literally, not just in spirit, at implementation time too: write the test, run it against the pre-fix code, confirm it fails for the right reason (a real behavioral mismatch, or a legitimate missing-interface error for a brand-new contract - not a setup bug), and only then write the fix. Writing the test after the implementation and asserting it "would have failed" is not equivalent - it's an unverified claim. If you catch yourself having implemented first, the correct recovery is to actually revert the implementation, confirm the test fails red against the reverted code, and only then reapply the fix - not to just add tests afterward and call it done.
Never reference a PRD's filename, path, or the word "PRD" in committed code, comments, docstrings, or commit messages - PRDs/ is gitignored and local-only, so a reader of the committed code later has no way to find what it's pointing at, and the reference rots the moment the PRD file is deleted or moved. Explain the why self-containedly in the code itself (a design PRD's rationale restated in the comment, not a pointer to go read the PRD) instead.
Where to save
.gemini/internal-docs/PRDs/<kebab-case-name>.md, unless the user specifies otherwise. Mark the status line (**Status:** Draft — pending review) so it's clear this hasn't been approved yet, and keep the author line accurate about who made the calls (a design discussion with an assistant is not sole assistant authorship).
Version History
-
fe29f5d
Current 2026-10-19 05:51
新增关于UI特性作为跨切面机制的详细案例警示;强调对代理发现的广度信息需进行抽样验证而非直接采信;细化了引用先前审计结果的规范。
- f6010e0 2026-09-27 22:53
- cd1d0ed 2026-09-22 11:07


