
Dev Rfc
- 210 installs
- 191 repo stars
- Updated July 24, 2026
- pproenca/dot-skills
dev-rfc: A skill for development.
About
dev-rfc: A skill for development. This provides functionality for development workflows.
- dev-rfc
Dev Rfc by the numbers
- 210 all-time installs (skills.sh)
- +9 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #1,932 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/pproenca/dot-skills --skill dev-rfcAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 210 |
|---|---|
| repo stars | ★ 191 |
| Last updated | July 24, 2026 |
| Repository | pproenca/dot-skills ↗ |
How do I use dev-rfc for development tasks?
Use dev-rfc for development tasks
Who is it for?
Best when you're working on backend & apis and need structured help with dev rfc.
Skip if: Teams with no backend & apis needs, or anyone wanting a generic chat assistant without this specific workflow.
When should I use this skill?
When you need to use dev-rfc for development tasks, or when dev-rfc: a skill for development.
What you get
Structured output aligned to dev-rfc: dev-rfc.
Files
Dev RFC Skill
Write RFCs and technical proposals that serve two purposes: aligning stakeholders on what to build and why (RFCs), and helping engineers understand how a system works (architecture docs). Most real proposals blend both — the skill helps you pick the right sections for the situation.
Reference Templates
Read references/template.md for the three structural templates:
- RFC — for pre-build alignment. Focuses on abstract, approaches (with fair comparison), service SLAs, observability, and rollout plan.
- Architecture Doc — for documenting built systems. Focuses on diagrams, source tree, data flow, design philosophy.
- One-Pager — for small changes (< 1 week). Problem, Proposed Solution, Rollout. ~20 lines.
Step 0: Pick the Mode
Ask the user (or infer from context) which situation they're in:
| Situation | Mode | Key question the doc answers |
|---|---|---|
| Planning a new system or major change | RFC | "Should we build this, and how?" |
| Documenting an existing system | Architecture Doc | "How does this system work?" |
| Proposing a change to an existing system | RFC (with before/after diagrams in Detailed Design) | "Why are we changing this, and what will it look like?" |
| Small scoped change (< 1 week) | One-Pager | "What and why, briefly?" |
For changes to existing systems, use the RFC structure as the backbone but include before/after architecture diagrams in the Detailed Design section, with [NEW] and [CHANGED] markers on components.
File Placement
- Formal proposal or RFC —
docs/rfcs/RFC-NNN-title.mdas primary location - Architecture doc for the whole project —
ARCHITECTURE.mdat the repo root - Subsystem doc —
docs/design/subsystem-name.mdor alongside the code it describes
If the user hasn't specified where to put the doc, ask.
Step 1: Understand the Project
Before writing anything, build a mental model.
For RFCs:
- Clarify the problem statement and constraints with the user
- Understand who the stakeholders and approvers are
- Ask about existing systems this interacts with or replaces
- Identify hard constraints (timeline, budget, compatibility, compliance)
- Ask what alternatives have already been considered or rejected
For architecture docs:
- Read the project's README, CLAUDE.md, and any existing docs
- Explore the source tree to identify major subsystems and their boundaries
- Trace the main data flow from input to output
- Look at key types/interfaces that flow between modules
- Check git history for context on architectural decisions
Step 2: Draft the Document
Follow the appropriate structure from references/template.md. The sections to include depend on mode and project complexity — the template has scaling guidance for small/medium/large projects.
The most important sections per mode:
RFC — the sections approvers care about most: 1. Abstract — 3-5 sentence executive summary. A reader should be able to decide whether to read the full RFC from this alone. 2. Approaches — all evaluated options with fair comparison. This is what distinguishes an RFC from a spec. 3. Service SLAs & Observability — production readiness. Shows the proposal accounts for how the system will behave and be monitored in production.
Architecture Doc — the sections new contributors care about most: 1. High-Level Architecture (with diagram) — the single-glance understanding. 2. Source Tree — the "where do I find X" index. 3. Key Design Decisions — the "why is it done this way" answers.
Step 3: Write Effective Content
Not all systems are pipelines. Choose the diagram shape that matches the system:
- Pipeline — compilers, data pipelines, ETL. Linear stages with data flowing through.
- Request-flow — services, APIs. Request path through middleware, handlers, dependencies.
- Event-flow — event-driven systems. Producers, queues/topics, consumers.
- Layer diagram — web apps, CRUD. UI → API → service → data layers.
The diagram is the centerpiece. A reader should understand the overall system flow from it alone. Use ASCII box-drawing art. Label stages with module paths and descriptions. Show data formats between stages.
Be concrete, not abstract. Instead of "Module A processes the input and passes it to Module B", write: "Preprocessor::preprocess() emits String (expanded text with line markers), which Lexer::tokenize() consumes to produce Vec<Token>." For proposals, use proposed type/interface names.
Approaches should present genuine alternatives with fair pros/cons. Don't strawman alternatives to make the recommended approach look better. Each approach should have real strengths acknowledged. A reviewer who disagrees with your recommendation should feel their preferred option was represented honestly.
Goals & Non-Goals should be specific and falsifiable. Example (for "migrate auth to OAuth2"):
Goals:
- All user-facing login flows use OAuth2 authorization code flow by end of Q3
- Support Google and GitHub as identity providers at launch
- Session token storage meets SOC 2 requirements (encrypted at rest, 24h max lifetime)
>
Non-Goals:
- Migrating service-to-service auth (stays on mTLS for now)
- Building a custom identity provider — we'll use Auth0
- Supporting SAML (enterprise SSO is a separate Q4 project)
Service SLAs should be concrete. Don't say "high availability" — state "99.95% uptime." Don't say "low latency" — state "P99 under 300ms." Justify each target.
Separate decisions from philosophy. Key Design Decisions are factual choices ("We use SSA form"). Design Philosophy captures principles that guide ongoing decisions ("Separation of concerns through representations").
Step 4: Review Checklist
For RFCs:
- [ ] Is the Abstract clear enough that a reader can decide whether to read the full RFC?
- [ ] Does Approaches honestly represent all options with genuine pros/cons?
- [ ] Is the Recommendation well-justified — does it explain what's being given up?
- [ ] Are Service SLAs concrete with specific numbers, not vague ("high availability")?
- [ ] Are Goals & Non-Goals specific enough to evaluate the proposal against?
- [ ] Is there a concrete rollback plan with decision criteria?
- [ ] Are Open Questions clearly flagged for approver input?
For architecture docs:
- [ ] Can someone unfamiliar with the project understand the flow from the diagram alone?
- [ ] Are concrete types and method names in Data Flow accurate?
- [ ] Do Key Design Decisions cover what a new contributor needs first?
- [ ] Are cross-references to other docs (README, per-module docs) included?
Step 5: Collect Feedback & Iterate (Optional)
After writing the RFC, offer to open a review UI in the user's browser. The review UI runs a local server that auto-saves feedback, supports multiple revision rounds, and lets the user explicitly approve the document.
Path resolution: In the commands below,$SKILL_PATHrefers to the absolute path of this SKILL.md file. Resolve it as the directory containing this file (e.g., if SKILL.md is at/path/to/dev-rfc/SKILL.md, then$(dirname "$SKILL_PATH")is/path/to/dev-rfc). Requires Bun (or Node 22+ with `--experimental-strip-types`).
First Round
1. Save the RFC to its target file path. 2. Start the review server:
bun run "$(dirname "$SKILL_PATH")/scripts/generate_review.ts" <doc-path> --title "<project name>"This starts an HTTP server on localhost:3118 and opens the browser. Feedback auto-saves to <doc-dir>/.rfc-review/feedback.json as the user types (800ms debounce). The server also serves the latest version of the markdown on each refresh. 3. Tell the user: "I've opened the RFC review in your browser at http://localhost:3118. Add feedback to any section, highlight text for inline comments, then click Submit Feedback (for revisions) or Approve (if it looks good). Your feedback auto-saves as you type." 4. When the user says they're done reviewing, read feedback from the workspace:
cat <doc-dir>/.rfc-review/feedback.jsonCheck Status
- `"status": "approved"` — The user approved the RFC. Stop iterating. Announce that the RFC is finalized.
- `"status": "needs_revision"` — The user wants changes. Proceed to revision.
- `"status": "draft"` — The user closed the browser mid-review. Ask if they want to continue or if the current draft feedback is sufficient.
Revision Guidelines
When revising, prioritize feedback in this order (most specific → most general): 1. Inline comments (inline_comments) — targeted at specific text. Address each one. 2. Section feedback (sections[].feedback) — per-section concerns. Revise the relevant section. 3. Overall feedback (overall_feedback) — broad themes. Apply across the document.
Empty feedback for a section means no concerns — skip it. Don't make changes where no feedback was given.
Subsequent Rounds
After revising the RFC, start the next review round with the previous feedback visible as read-only context:
bun run "$(dirname "$SKILL_PATH")/scripts/generate_review.ts" <doc-path> --title "<project name>" \
--previous-feedback <doc-dir>/.rfc-review/feedback-history/feedback-round-N.json \
--iteration N+1The server automatically archives the previous feedback.json to feedback-history/feedback-round-N.json on startup. The reviewer sees their previous feedback (read-only) above each section, so they can verify their concerns were addressed.
Repeat the check-status → revise → re-launch loop until the user approves or opts out.
Termination
Stop iterating when any of these happen:
- The user clicks Approve in the UI (
"status": "approved") - The user says "looks good", "ship it", "done", or similar
- The user explicitly says they don't want more rounds
Static Fallback
If the server can't start (port conflict, environment issue), fall back to static mode:
bun run "$(dirname "$SKILL_PATH")/scripts/generate_review.ts" <doc-path> --title "<project name>" --staticThis opens a standalone HTML file. Feedback downloads as feedback.json to ~/Downloads on submit. Ask the user where the file landed.
Skip this step entirely if the user wants the doc written directly without a review loop, or for one-pagers.
Step 5b: Live Authoring Mode (Optional)
Instead of writing the entire RFC and then reviewing, use live authoring mode where the UI opens first with a skeleton of all planned sections, you write sections one at a time, each section appears in the browser in real-time, and the user gives per-section feedback before you write the next section.
When to Use Live Mode
- The user wants to collaborate on the RFC as it's being written
- The RFC is complex and benefits from iterative alignment on each section
- The user explicitly asks for "live", "interactive", or "step-by-step" mode
Launch the Live Server
1. Plan the section headings for the RFC based on the template and project scope. 2. Start the server in live mode:
bun run "$(dirname "$SKILL_PATH")/scripts/generate_review.ts" --live --title "<project name>" \
--sections '["Abstract","Motivation","Goals and Non-Goals","Detailed Design","Approaches","Service SLAs","Rollout Plan"]'This opens the browser showing a skeleton with all planned sections as pending cards. 3. Tell the user: "I've opened the RFC in live authoring mode at http://localhost:3118. I'll write each section one at a time — you can approve or request changes on each section before I move to the next."
Section-by-Section Protocol
For each section in order:
1. Write the section content as markdown. 2. Push it to the server:
curl -s -X POST http://localhost:3118/api/section/add \
-H 'Content-Type: application/json' \
-d '{"id": "abstract", "heading": "Abstract", "markdown": "## Abstract\n\nYour content here..."}'The section appears in the browser immediately via SSE. 3. Wait for user feedback:
curl -s http://localhost:3118/api/wait-feedback?section=abstractThis blocks until the user clicks "Approve" or "Request Changes" in the browser (5-minute timeout). 4. Handle the response:
{"action": "approve"}— Move to the next section.{"action": "request_changes", "text": "..."}— Read the feedback, revise the section, then push the update:
curl -s -X POST http://localhost:3118/api/section/update \
-H 'Content-Type: application/json' \
-d '{"id": "abstract", "markdown": "## Abstract\n\nRevised content..."}'Then call /api/wait-feedback?section=abstract again.
{"timeout": true}— The user hasn't responded in 5 minutes. Prompt them in the CLI or retry.
Completion
Once all sections are approved, assemble the full markdown from all approved sections and write it to the target file path. The user can also click "Finalize RFC" in the browser once all sections are approved.
Fallback
If live mode encounters issues, fall back to batch mode (Step 5) by writing the full RFC first and then opening the standard review UI.
Tone and Style
- Present tense, declarative voice for architecture docs. Future tense only for proposals.
- Concise — every word should earn its place
- Prefer showing (diagrams, code, type signatures) over telling
- Short paragraphs — this is reference material, not prose
- Markdown formatting: bold for emphasis, code blocks for types/paths, tables for comparisons
Doc Sizing
| Change size | Format | Sections |
|---|---|---|
| Small (< 1 week, single component) | One-pager | Problem, Proposed Solution, Rollout |
| Medium (1-4 weeks, multiple components) | Standard RFC | Full RFC or architecture doc |
| Large (> 1 month, cross-team) | Full RFC + sub-docs | Top-level doc + linked sub-docs for subsystems |
Review Workflow Guidance
Include the status in the metadata header:
1. Draft — Author is still writing. Not ready for formal review. 2. In Review — Shared with approvers. Specify what feedback is needed. 3. Approved — Approvers signed off. Plan of record. 4. Superseded — Newer RFC replaces this one. Link to replacement. 5. Deprecated — System no longer exists or proposal was abandoned.
/**
* marked v15.0.12 - a markdown parser
* Copyright (c) 2011-2025, Christopher Jeffrey. (MIT Licensed)
* https://github.com/markedjs/marked
*/
/**
* DO NOT EDIT THIS FILE
* The code in this file is generated from files in ./src/
*/
(function(g,f){if(typeof exports=="object"&&typeof module<"u"){module.exports=f()}else if("function"==typeof define && define.amd){define("marked",f)}else {g["marked"]=f()}}(typeof globalThis < "u" ? globalThis : typeof self < "u" ? self : this,function(){var exports={};var __exports=exports;var module={exports};
"use strict";var H=Object.defineProperty;var be=Object.getOwnPropertyDescriptor;var Te=Object.getOwnPropertyNames;var we=Object.prototype.hasOwnProperty;var ye=(l,e)=>{for(var t in e)H(l,t,{get:e[t],enumerable:!0})},Re=(l,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of Te(e))!we.call(l,s)&&s!==t&&H(l,s,{get:()=>e[s],enumerable:!(n=be(e,s))||n.enumerable});return l};var Se=l=>Re(H({},"__esModule",{value:!0}),l);var kt={};ye(kt,{Hooks:()=>L,Lexer:()=>x,Marked:()=>E,Parser:()=>b,Renderer:()=>$,TextRenderer:()=>_,Tokenizer:()=>S,defaults:()=>w,getDefaults:()=>z,lexer:()=>ht,marked:()=>k,options:()=>it,parse:()=>pt,parseInline:()=>ct,parser:()=>ut,setOptions:()=>ot,use:()=>lt,walkTokens:()=>at});module.exports=Se(kt);function z(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}var w=z();function N(l){w=l}var I={exec:()=>null};function h(l,e=""){let t=typeof l=="string"?l:l.source,n={replace:(s,i)=>{let r=typeof i=="string"?i:i.source;return r=r.replace(m.caret,"$1"),t=t.replace(s,r),n},getRegex:()=>new RegExp(t,e)};return n}var m={codeRemoveIndent:/^(?: {1,4}| {0,3}\t)/gm,outputLinkReplace:/\\([\[\]])/g,indentCodeCompensation:/^(\s+)(?:```)/,beginningSpace:/^\s+/,endingHash:/#$/,startingSpaceChar:/^ /,endingSpaceChar:/ $/,nonSpaceChar:/[^ ]/,newLineCharGlobal:/\n/g,tabCharGlobal:/\t/g,multipleSpaceGlobal:/\s+/g,blankLine:/^[ \t]*$/,doubleBlankLine:/\n[ \t]*\n[ \t]*$/,blockquoteStart:/^ {0,3}>/,blockquoteSetextReplace:/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,blockquoteSetextReplace2:/^ {0,3}>[ \t]?/gm,listReplaceTabs:/^\t+/,listReplaceNesting:/^ {1,4}(?=( {4})*[^ ])/g,listIsTask:/^\[[ xX]\] /,listReplaceTask:/^\[[ xX]\] +/,anyLine:/\n.*\n/,hrefBrackets:/^<(.*)>$/,tableDelimiter:/[:|]/,tableAlignChars:/^\||\| *$/g,tableRowBlankLine:/\n[ \t]*$/,tableAlignRight:/^ *-+: *$/,tableAlignCenter:/^ *:-+: *$/,tableAlignLeft:/^ *:-+ *$/,startATag:/^<a /i,endATag:/^<\/a>/i,startPreScriptTag:/^<(pre|code|kbd|script)(\s|>)/i,endPreScriptTag:/^<\/(pre|code|kbd|script)(\s|>)/i,startAngleBracket:/^</,endAngleBracket:/>$/,pedanticHrefTitle:/^([^'"]*[^\s])\s+(['"])(.*)\2/,unicodeAlphaNumeric:/[\p{L}\p{N}]/u,escapeTest:/[&<>"']/,escapeReplace:/[&<>"']/g,escapeTestNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,escapeReplaceNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g,unescapeTest:/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig,caret:/(^|[^\[])\^/g,percentDecode:/%25/g,findPipe:/\|/g,splitPipe:/ \|/,slashPipe:/\\\|/g,carriageReturn:/\r\n|\r/g,spaceLine:/^ +$/gm,notSpaceStart:/^\S*/,endingNewline:/\n$/,listItemRegex:l=>new RegExp(`^( {0,3}${l})((?:[ ][^\\n]*)?(?:\\n|$))`),nextBulletRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ ][^\\n]*)?(?:\\n|$))`),hrRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),fencesBeginRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}(?:\`\`\`|~~~)`),headingBeginRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}#`),htmlBeginRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}<(?:[a-z].*>|!--)`,"i")},$e=/^(?:[ \t]*(?:\n|$))+/,_e=/^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/,Le=/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,O=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,ze=/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,F=/(?:[*+-]|\d{1,9}[.)])/,ie=/^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,oe=h(ie).replace(/bull/g,F).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/\|table/g,"").getRegex(),Me=h(ie).replace(/bull/g,F).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/table/g,/ {0,3}\|?(?:[:\- ]*\|)+[\:\- ]*\n/).getRegex(),Q=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,Pe=/^[^\n]+/,U=/(?!\s*\])(?:\\.|[^\[\]\\])+/,Ae=h(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label",U).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),Ee=h(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,F).getRegex(),v="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",K=/<!--(?:-?>|[\s\S]*?(?:-->|$))/,Ce=h("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:</\\1>[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|<![A-Z][\\s\\S]*?(?:>\\n*|$)|<!\\[CDATA\\[[\\s\\S]*?(?:\\]\\]>\\n*|$)|</?(tag)(?: +|\\n|/?>)[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|</(?!script|pre|style|textarea)[a-z][\\w-]*\\s*>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$))","i").replace("comment",K).replace("tag",v).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),le=h(Q).replace("hr",O).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",v).getRegex(),Ie=h(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",le).getRegex(),X={blockquote:Ie,code:_e,def:Ae,fences:Le,heading:ze,hr:O,html:Ce,lheading:oe,list:Ee,newline:$e,paragraph:le,table:I,text:Pe},re=h("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",O).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code","(?: {4}| {0,3} )[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",v).getRegex(),Oe={...X,lheading:Me,table:re,paragraph:h(Q).replace("hr",O).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",re).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",v).getRegex()},Be={...X,html:h(`^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+?</\\1> *(?:\\n{2,}|\\s*$)|<tag(?:"[^"]*"|'[^']*'|\\s[^'"/>\\s]*)*?/?> *(?:\\n{2,}|\\s*$))`).replace("comment",K).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:I,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:h(Q).replace("hr",O).replace("heading",` *#{1,6} *[^
]`).replace("lheading",oe).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},qe=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,ve=/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,ae=/^( {2,}|\\)\n(?!\s*$)/,De=/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\<!\[`*_]|\b_|$)|[^ ](?= {2,}\n)))/,D=/[\p{P}\p{S}]/u,W=/[\s\p{P}\p{S}]/u,ce=/[^\s\p{P}\p{S}]/u,Ze=h(/^((?![*_])punctSpace)/,"u").replace(/punctSpace/g,W).getRegex(),pe=/(?!~)[\p{P}\p{S}]/u,Ge=/(?!~)[\s\p{P}\p{S}]/u,He=/(?:[^\s\p{P}\p{S}]|~)/u,Ne=/\[[^[\]]*?\]\((?:\\.|[^\\\(\)]|\((?:\\.|[^\\\(\)])*\))*\)|`[^`]*?`|<[^<>]*?>/g,ue=/^(?:\*+(?:((?!\*)punct)|[^\s*]))|^_+(?:((?!_)punct)|([^\s_]))/,je=h(ue,"u").replace(/punct/g,D).getRegex(),Fe=h(ue,"u").replace(/punct/g,pe).getRegex(),he="^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)",Qe=h(he,"gu").replace(/notPunctSpace/g,ce).replace(/punctSpace/g,W).replace(/punct/g,D).getRegex(),Ue=h(he,"gu").replace(/notPunctSpace/g,He).replace(/punctSpace/g,Ge).replace(/punct/g,pe).getRegex(),Ke=h("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)","gu").replace(/notPunctSpace/g,ce).replace(/punctSpace/g,W).replace(/punct/g,D).getRegex(),Xe=h(/\\(punct)/,"gu").replace(/punct/g,D).getRegex(),We=h(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),Je=h(K).replace("(?:-->|$)","-->").getRegex(),Ve=h("^comment|^</[a-zA-Z][\\w:-]*\\s*>|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^<![a-zA-Z]+\\s[\\s\\S]*?>|^<!\\[CDATA\\[[\\s\\S]*?\\]\\]>").replace("comment",Je).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),q=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,Ye=h(/^!?\[(label)\]\(\s*(href)(?:(?:[ \t]*(?:\n[ \t]*)?)(title))?\s*\)/).replace("label",q).replace("href",/<(?:\\.|[^\n<>\\])+>|[^ \t\n\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),ke=h(/^!?\[(label)\]\[(ref)\]/).replace("label",q).replace("ref",U).getRegex(),ge=h(/^!?\[(ref)\](?:\[\])?/).replace("ref",U).getRegex(),et=h("reflink|nolink(?!\\()","g").replace("reflink",ke).replace("nolink",ge).getRegex(),J={_backpedal:I,anyPunctuation:Xe,autolink:We,blockSkip:Ne,br:ae,code:ve,del:I,emStrongLDelim:je,emStrongRDelimAst:Qe,emStrongRDelimUnd:Ke,escape:qe,link:Ye,nolink:ge,punctuation:Ze,reflink:ke,reflinkSearch:et,tag:Ve,text:De,url:I},tt={...J,link:h(/^!?\[(label)\]\((.*?)\)/).replace("label",q).getRegex(),reflink:h(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",q).getRegex()},j={...J,emStrongRDelimAst:Ue,emStrongLDelim:Fe,url:h(/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,"i").replace("email",/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/).getRegex(),_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\<!\[`*~_]|\b_|https?:\/\/|ftp:\/\/|www\.|$)|[^ ](?= {2,}\n)|[^a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-](?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)))/},nt={...j,br:h(ae).replace("{2,}","*").getRegex(),text:h(j.text).replace("\\b_","\\b_| {2,}\\n").replace(/\{2,\}/g,"*").getRegex()},B={normal:X,gfm:Oe,pedantic:Be},P={normal:J,gfm:j,breaks:nt,pedantic:tt};var st={"&":"&","<":"<",">":">",'"':""","'":"'"},fe=l=>st[l];function R(l,e){if(e){if(m.escapeTest.test(l))return l.replace(m.escapeReplace,fe)}else if(m.escapeTestNoEncode.test(l))return l.replace(m.escapeReplaceNoEncode,fe);return l}function V(l){try{l=encodeURI(l).replace(m.percentDecode,"%")}catch{return null}return l}function Y(l,e){let t=l.replace(m.findPipe,(i,r,o)=>{let a=!1,c=r;for(;--c>=0&&o[c]==="\\";)a=!a;return a?"|":" |"}),n=t.split(m.splitPipe),s=0;if(n[0].trim()||n.shift(),n.length>0&&!n.at(-1)?.trim()&&n.pop(),e)if(n.length>e)n.splice(e);else for(;n.length<e;)n.push("");for(;s<n.length;s++)n[s]=n[s].trim().replace(m.slashPipe,"|");return n}function A(l,e,t){let n=l.length;if(n===0)return"";let s=0;for(;s<n;){let i=l.charAt(n-s-1);if(i===e&&!t)s++;else if(i!==e&&t)s++;else break}return l.slice(0,n-s)}function de(l,e){if(l.indexOf(e[1])===-1)return-1;let t=0;for(let n=0;n<l.length;n++)if(l[n]==="\\")n++;else if(l[n]===e[0])t++;else if(l[n]===e[1]&&(t--,t<0))return n;return t>0?-2:-1}function me(l,e,t,n,s){let i=e.href,r=e.title||null,o=l[1].replace(s.other.outputLinkReplace,"$1");n.state.inLink=!0;let a={type:l[0].charAt(0)==="!"?"image":"link",raw:t,href:i,title:r,text:o,tokens:n.inlineTokens(o)};return n.state.inLink=!1,a}function rt(l,e,t){let n=l.match(t.other.indentCodeCompensation);if(n===null)return e;let s=n[1];return e.split(`
`).map(i=>{let r=i.match(t.other.beginningSpace);if(r===null)return i;let[o]=r;return o.length>=s.length?i.slice(s.length):i}).join(`
`)}var S=class{options;rules;lexer;constructor(e){this.options=e||w}space(e){let t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:"space",raw:t[0]}}code(e){let t=this.rules.block.code.exec(e);if(t){let n=t[0].replace(this.rules.other.codeRemoveIndent,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?n:A(n,`
`)}}}fences(e){let t=this.rules.block.fences.exec(e);if(t){let n=t[0],s=rt(n,t[3]||"",this.rules);return{type:"code",raw:n,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:s}}}heading(e){let t=this.rules.block.heading.exec(e);if(t){let n=t[2].trim();if(this.rules.other.endingHash.test(n)){let s=A(n,"#");(this.options.pedantic||!s||this.rules.other.endingSpaceChar.test(s))&&(n=s.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:n,tokens:this.lexer.inline(n)}}}hr(e){let t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:A(t[0],`
`)}}blockquote(e){let t=this.rules.block.blockquote.exec(e);if(t){let n=A(t[0],`
`).split(`
`),s="",i="",r=[];for(;n.length>0;){let o=!1,a=[],c;for(c=0;c<n.length;c++)if(this.rules.other.blockquoteStart.test(n[c]))a.push(n[c]),o=!0;else if(!o)a.push(n[c]);else break;n=n.slice(c);let p=a.join(`
`),u=p.replace(this.rules.other.blockquoteSetextReplace,`
$1`).replace(this.rules.other.blockquoteSetextReplace2,"");s=s?`${s}
${p}`:p,i=i?`${i}
${u}`:u;let d=this.lexer.state.top;if(this.lexer.state.top=!0,this.lexer.blockTokens(u,r,!0),this.lexer.state.top=d,n.length===0)break;let g=r.at(-1);if(g?.type==="code")break;if(g?.type==="blockquote"){let T=g,f=T.raw+`
`+n.join(`
`),y=this.blockquote(f);r[r.length-1]=y,s=s.substring(0,s.length-T.raw.length)+y.raw,i=i.substring(0,i.length-T.text.length)+y.text;break}else if(g?.type==="list"){let T=g,f=T.raw+`
`+n.join(`
`),y=this.list(f);r[r.length-1]=y,s=s.substring(0,s.length-g.raw.length)+y.raw,i=i.substring(0,i.length-T.raw.length)+y.raw,n=f.substring(r.at(-1).raw.length).split(`
`);continue}}return{type:"blockquote",raw:s,tokens:r,text:i}}}list(e){let t=this.rules.block.list.exec(e);if(t){let n=t[1].trim(),s=n.length>1,i={type:"list",raw:"",ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]};n=s?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=s?n:"[*+-]");let r=this.rules.other.listItemRegex(n),o=!1;for(;e;){let c=!1,p="",u="";if(!(t=r.exec(e))||this.rules.block.hr.test(e))break;p=t[0],e=e.substring(p.length);let d=t[2].split(`
`,1)[0].replace(this.rules.other.listReplaceTabs,Z=>" ".repeat(3*Z.length)),g=e.split(`
`,1)[0],T=!d.trim(),f=0;if(this.options.pedantic?(f=2,u=d.trimStart()):T?f=t[1].length+1:(f=t[2].search(this.rules.other.nonSpaceChar),f=f>4?1:f,u=d.slice(f),f+=t[1].length),T&&this.rules.other.blankLine.test(g)&&(p+=g+`
`,e=e.substring(g.length+1),c=!0),!c){let Z=this.rules.other.nextBulletRegex(f),te=this.rules.other.hrRegex(f),ne=this.rules.other.fencesBeginRegex(f),se=this.rules.other.headingBeginRegex(f),xe=this.rules.other.htmlBeginRegex(f);for(;e;){let G=e.split(`
`,1)[0],C;if(g=G,this.options.pedantic?(g=g.replace(this.rules.other.listReplaceNesting," "),C=g):C=g.replace(this.rules.other.tabCharGlobal," "),ne.test(g)||se.test(g)||xe.test(g)||Z.test(g)||te.test(g))break;if(C.search(this.rules.other.nonSpaceChar)>=f||!g.trim())u+=`
`+C.slice(f);else{if(T||d.replace(this.rules.other.tabCharGlobal," ").search(this.rules.other.nonSpaceChar)>=4||ne.test(d)||se.test(d)||te.test(d))break;u+=`
`+g}!T&&!g.trim()&&(T=!0),p+=G+`
`,e=e.substring(G.length+1),d=C.slice(f)}}i.loose||(o?i.loose=!0:this.rules.other.doubleBlankLine.test(p)&&(o=!0));let y=null,ee;this.options.gfm&&(y=this.rules.other.listIsTask.exec(u),y&&(ee=y[0]!=="[ ] ",u=u.replace(this.rules.other.listReplaceTask,""))),i.items.push({type:"list_item",raw:p,task:!!y,checked:ee,loose:!1,text:u,tokens:[]}),i.raw+=p}let a=i.items.at(-1);if(a)a.raw=a.raw.trimEnd(),a.text=a.text.trimEnd();else return;i.raw=i.raw.trimEnd();for(let c=0;c<i.items.length;c++)if(this.lexer.state.top=!1,i.items[c].tokens=this.lexer.blockTokens(i.items[c].text,[]),!i.loose){let p=i.items[c].tokens.filter(d=>d.type==="space"),u=p.length>0&&p.some(d=>this.rules.other.anyLine.test(d.raw));i.loose=u}if(i.loose)for(let c=0;c<i.items.length;c++)i.items[c].loose=!0;return i}}html(e){let t=this.rules.block.html.exec(e);if(t)return{type:"html",block:!0,raw:t[0],pre:t[1]==="pre"||t[1]==="script"||t[1]==="style",text:t[0]}}def(e){let t=this.rules.block.def.exec(e);if(t){let n=t[1].toLowerCase().replace(this.rules.other.multipleSpaceGlobal," "),s=t[2]?t[2].replace(this.rules.other.hrefBrackets,"$1").replace(this.rules.inline.anyPunctuation,"$1"):"",i=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline.anyPunctuation,"$1"):t[3];return{type:"def",tag:n,raw:t[0],href:s,title:i}}}table(e){let t=this.rules.block.table.exec(e);if(!t||!this.rules.other.tableDelimiter.test(t[2]))return;let n=Y(t[1]),s=t[2].replace(this.rules.other.tableAlignChars,"").split("|"),i=t[3]?.trim()?t[3].replace(this.rules.other.tableRowBlankLine,"").split(`
`):[],r={type:"table",raw:t[0],header:[],align:[],rows:[]};if(n.length===s.length){for(let o of s)this.rules.other.tableAlignRight.test(o)?r.align.push("right"):this.rules.other.tableAlignCenter.test(o)?r.align.push("center"):this.rules.other.tableAlignLeft.test(o)?r.align.push("left"):r.align.push(null);for(let o=0;o<n.length;o++)r.header.push({text:n[o],tokens:this.lexer.inline(n[o]),header:!0,align:r.align[o]});for(let o of i)r.rows.push(Y(o,r.header.length).map((a,c)=>({text:a,tokens:this.lexer.inline(a),header:!1,align:r.align[c]})));return r}}lheading(e){let t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:t[2].charAt(0)==="="?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){let t=this.rules.block.paragraph.exec(e);if(t){let n=t[1].charAt(t[1].length-1)===`
`?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:n,tokens:this.lexer.inline(n)}}}text(e){let t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){let t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:t[1]}}tag(e){let t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&this.rules.other.startATag.test(t[0])?this.lexer.state.inLink=!0:this.lexer.state.inLink&&this.rules.other.endATag.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&this.rules.other.startPreScriptTag.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&this.rules.other.endPreScriptTag.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){let t=this.rules.inline.link.exec(e);if(t){let n=t[2].trim();if(!this.options.pedantic&&this.rules.other.startAngleBracket.test(n)){if(!this.rules.other.endAngleBracket.test(n))return;let r=A(n.slice(0,-1),"\\");if((n.length-r.length)%2===0)return}else{let r=de(t[2],"()");if(r===-2)return;if(r>-1){let a=(t[0].indexOf("!")===0?5:4)+t[1].length+r;t[2]=t[2].substring(0,r),t[0]=t[0].substring(0,a).trim(),t[3]=""}}let s=t[2],i="";if(this.options.pedantic){let r=this.rules.other.pedanticHrefTitle.exec(s);r&&(s=r[1],i=r[3])}else i=t[3]?t[3].slice(1,-1):"";return s=s.trim(),this.rules.other.startAngleBracket.test(s)&&(this.options.pedantic&&!this.rules.other.endAngleBracket.test(n)?s=s.slice(1):s=s.slice(1,-1)),me(t,{href:s&&s.replace(this.rules.inline.anyPunctuation,"$1"),title:i&&i.replace(this.rules.inline.anyPunctuation,"$1")},t[0],this.lexer,this.rules)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){let s=(n[2]||n[1]).replace(this.rules.other.multipleSpaceGlobal," "),i=t[s.toLowerCase()];if(!i){let r=n[0].charAt(0);return{type:"text",raw:r,text:r}}return me(n,i,n[0],this.lexer,this.rules)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrongLDelim.exec(e);if(!s||s[3]&&n.match(this.rules.other.unicodeAlphaNumeric))return;if(!(s[1]||s[2]||"")||!n||this.rules.inline.punctuation.exec(n)){let r=[...s[0]].length-1,o,a,c=r,p=0,u=s[0][0]==="*"?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(u.lastIndex=0,t=t.slice(-1*e.length+r);(s=u.exec(t))!=null;){if(o=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!o)continue;if(a=[...o].length,s[3]||s[4]){c+=a;continue}else if((s[5]||s[6])&&r%3&&!((r+a)%3)){p+=a;continue}if(c-=a,c>0)continue;a=Math.min(a,a+c+p);let d=[...s[0]][0].length,g=e.slice(0,r+s.index+d+a);if(Math.min(r,a)%2){let f=g.slice(1,-1);return{type:"em",raw:g,text:f,tokens:this.lexer.inlineTokens(f)}}let T=g.slice(2,-2);return{type:"strong",raw:g,text:T,tokens:this.lexer.inlineTokens(T)}}}}codespan(e){let t=this.rules.inline.code.exec(e);if(t){let n=t[2].replace(this.rules.other.newLineCharGlobal," "),s=this.rules.other.nonSpaceChar.test(n),i=this.rules.other.startingSpaceChar.test(n)&&this.rules.other.endingSpaceChar.test(n);return s&&i&&(n=n.substring(1,n.length-1)),{type:"codespan",raw:t[0],text:n}}}br(e){let t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){let t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){let t=this.rules.inline.autolink.exec(e);if(t){let n,s;return t[2]==="@"?(n=t[1],s="mailto:"+n):(n=t[1],s=n),{type:"link",raw:t[0],text:n,href:s,tokens:[{type:"text",raw:n,text:n}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let n,s;if(t[2]==="@")n=t[0],s="mailto:"+n;else{let i;do i=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??"";while(i!==t[0]);n=t[0],t[1]==="www."?s="http://"+t[0]:s=t[0]}return{type:"link",raw:t[0],text:n,href:s,tokens:[{type:"text",raw:n,text:n}]}}}inlineText(e){let t=this.rules.inline.text.exec(e);if(t){let n=this.lexer.state.inRawBlock;return{type:"text",raw:t[0],text:t[0],escaped:n}}}};var x=class l{tokens;options;state;tokenizer;inlineQueue;constructor(e){this.tokens=[],this.tokens.links=Object.create(null),this.options=e||w,this.options.tokenizer=this.options.tokenizer||new S,this.tokenizer=this.options.tokenizer,this.tokenizer.options=this.options,this.tokenizer.lexer=this,this.inlineQueue=[],this.state={inLink:!1,inRawBlock:!1,top:!0};let t={other:m,block:B.normal,inline:P.normal};this.options.pedantic?(t.block=B.pedantic,t.inline=P.pedantic):this.options.gfm&&(t.block=B.gfm,this.options.breaks?t.inline=P.breaks:t.inline=P.gfm),this.tokenizer.rules=t}static get rules(){return{block:B,inline:P}}static lex(e,t){return new l(t).lex(e)}static lexInline(e,t){return new l(t).inlineTokens(e)}lex(e){e=e.replace(m.carriageReturn,`
`),this.blockTokens(e,this.tokens);for(let t=0;t<this.inlineQueue.length;t++){let n=this.inlineQueue[t];this.inlineTokens(n.src,n.tokens)}return this.inlineQueue=[],this.tokens}blockTokens(e,t=[],n=!1){for(this.options.pedantic&&(e=e.replace(m.tabCharGlobal," ").replace(m.spaceLine,""));e;){let s;if(this.options.extensions?.block?.some(r=>(s=r.call({lexer:this},e,t))?(e=e.substring(s.raw.length),t.push(s),!0):!1))continue;if(s=this.tokenizer.space(e)){e=e.substring(s.raw.length);let r=t.at(-1);s.raw.length===1&&r!==void 0?r.raw+=`
`:t.push(s);continue}if(s=this.tokenizer.code(e)){e=e.substring(s.raw.length);let r=t.at(-1);r?.type==="paragraph"||r?.type==="text"?(r.raw+=`
`+s.raw,r.text+=`
`+s.text,this.inlineQueue.at(-1).src=r.text):t.push(s);continue}if(s=this.tokenizer.fences(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.heading(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.hr(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.blockquote(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.list(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.html(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.def(e)){e=e.substring(s.raw.length);let r=t.at(-1);r?.type==="paragraph"||r?.type==="text"?(r.raw+=`
`+s.raw,r.text+=`
`+s.raw,this.inlineQueue.at(-1).src=r.text):this.tokens.links[s.tag]||(this.tokens.links[s.tag]={href:s.href,title:s.title});continue}if(s=this.tokenizer.table(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.lheading(e)){e=e.substring(s.raw.length),t.push(s);continue}let i=e;if(this.options.extensions?.startBlock){let r=1/0,o=e.slice(1),a;this.options.extensions.startBlock.forEach(c=>{a=c.call({lexer:this},o),typeof a=="number"&&a>=0&&(r=Math.min(r,a))}),r<1/0&&r>=0&&(i=e.substring(0,r+1))}if(this.state.top&&(s=this.tokenizer.paragraph(i))){let r=t.at(-1);n&&r?.type==="paragraph"?(r.raw+=`
`+s.raw,r.text+=`
`+s.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=r.text):t.push(s),n=i.length!==e.length,e=e.substring(s.raw.length);continue}if(s=this.tokenizer.text(e)){e=e.substring(s.raw.length);let r=t.at(-1);r?.type==="text"?(r.raw+=`
`+s.raw,r.text+=`
`+s.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=r.text):t.push(s);continue}if(e){let r="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(r);break}else throw new Error(r)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n=e,s=null;if(this.tokens.links){let o=Object.keys(this.tokens.links);if(o.length>0)for(;(s=this.tokenizer.rules.inline.reflinkSearch.exec(n))!=null;)o.includes(s[0].slice(s[0].lastIndexOf("[")+1,-1))&&(n=n.slice(0,s.index)+"["+"a".repeat(s[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;(s=this.tokenizer.rules.inline.anyPunctuation.exec(n))!=null;)n=n.slice(0,s.index)+"++"+n.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;(s=this.tokenizer.rules.inline.blockSkip.exec(n))!=null;)n=n.slice(0,s.index)+"["+"a".repeat(s[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);let i=!1,r="";for(;e;){i||(r=""),i=!1;let o;if(this.options.extensions?.inline?.some(c=>(o=c.call({lexer:this},e,t))?(e=e.substring(o.raw.length),t.push(o),!0):!1))continue;if(o=this.tokenizer.escape(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.tag(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.link(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.reflink(e,this.tokens.links)){e=e.substring(o.raw.length);let c=t.at(-1);o.type==="text"&&c?.type==="text"?(c.raw+=o.raw,c.text+=o.text):t.push(o);continue}if(o=this.tokenizer.emStrong(e,n,r)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.codespan(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.br(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.del(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.autolink(e)){e=e.substring(o.raw.length),t.push(o);continue}if(!this.state.inLink&&(o=this.tokenizer.url(e))){e=e.substring(o.raw.length),t.push(o);continue}let a=e;if(this.options.extensions?.startInline){let c=1/0,p=e.slice(1),u;this.options.extensions.startInline.forEach(d=>{u=d.call({lexer:this},p),typeof u=="number"&&u>=0&&(c=Math.min(c,u))}),c<1/0&&c>=0&&(a=e.substring(0,c+1))}if(o=this.tokenizer.inlineText(a)){e=e.substring(o.raw.length),o.raw.slice(-1)!=="_"&&(r=o.raw.slice(-1)),i=!0;let c=t.at(-1);c?.type==="text"?(c.raw+=o.raw,c.text+=o.text):t.push(o);continue}if(e){let c="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(c);break}else throw new Error(c)}}return t}};var $=class{options;parser;constructor(e){this.options=e||w}space(e){return""}code({text:e,lang:t,escaped:n}){let s=(t||"").match(m.notSpaceStart)?.[0],i=e.replace(m.endingNewline,"")+`
`;return s?'<pre><code class="language-'+R(s)+'">'+(n?i:R(i,!0))+`</code></pre>
`:"<pre><code>"+(n?i:R(i,!0))+`</code></pre>
`}blockquote({tokens:e}){return`<blockquote>
${this.parser.parse(e)}</blockquote>
`}html({text:e}){return e}heading({tokens:e,depth:t}){return`<h${t}>${this.parser.parseInline(e)}</h${t}>
`}hr(e){return`<hr>
`}list(e){let t=e.ordered,n=e.start,s="";for(let o=0;o<e.items.length;o++){let a=e.items[o];s+=this.listitem(a)}let i=t?"ol":"ul",r=t&&n!==1?' start="'+n+'"':"";return"<"+i+r+`>
`+s+"</"+i+`>
`}listitem(e){let t="";if(e.task){let n=this.checkbox({checked:!!e.checked});e.loose?e.tokens[0]?.type==="paragraph"?(e.tokens[0].text=n+" "+e.tokens[0].text,e.tokens[0].tokens&&e.tokens[0].tokens.length>0&&e.tokens[0].tokens[0].type==="text"&&(e.tokens[0].tokens[0].text=n+" "+R(e.tokens[0].tokens[0].text),e.tokens[0].tokens[0].escaped=!0)):e.tokens.unshift({type:"text",raw:n+" ",text:n+" ",escaped:!0}):t+=n+" "}return t+=this.parser.parse(e.tokens,!!e.loose),`<li>${t}</li>
`}checkbox({checked:e}){return"<input "+(e?'checked="" ':"")+'disabled="" type="checkbox">'}paragraph({tokens:e}){return`<p>${this.parser.parseInline(e)}</p>
`}table(e){let t="",n="";for(let i=0;i<e.header.length;i++)n+=this.tablecell(e.header[i]);t+=this.tablerow({text:n});let s="";for(let i=0;i<e.rows.length;i++){let r=e.rows[i];n="";for(let o=0;o<r.length;o++)n+=this.tablecell(r[o]);s+=this.tablerow({text:n})}return s&&(s=`<tbody>${s}</tbody>`),`<table>
<thead>
`+t+`</thead>
`+s+`</table>
`}tablerow({text:e}){return`<tr>
${e}</tr>
`}tablecell(e){let t=this.parser.parseInline(e.tokens),n=e.header?"th":"td";return(e.align?`<${n} align="${e.align}">`:`<${n}>`)+t+`</${n}>
`}strong({tokens:e}){return`<strong>${this.parser.parseInline(e)}</strong>`}em({tokens:e}){return`<em>${this.parser.parseInline(e)}</em>`}codespan({text:e}){return`<code>${R(e,!0)}</code>`}br(e){return"<br>"}del({tokens:e}){return`<del>${this.parser.parseInline(e)}</del>`}link({href:e,title:t,tokens:n}){let s=this.parser.parseInline(n),i=V(e);if(i===null)return s;e=i;let r='<a href="'+e+'"';return t&&(r+=' title="'+R(t)+'"'),r+=">"+s+"</a>",r}image({href:e,title:t,text:n,tokens:s}){s&&(n=this.parser.parseInline(s,this.parser.textRenderer));let i=V(e);if(i===null)return R(n);e=i;let r=`<img src="${e}" alt="${n}"`;return t&&(r+=` title="${R(t)}"`),r+=">",r}text(e){return"tokens"in e&&e.tokens?this.parser.parseInline(e.tokens):"escaped"in e&&e.escaped?e.text:R(e.text)}};var _=class{strong({text:e}){return e}em({text:e}){return e}codespan({text:e}){return e}del({text:e}){return e}html({text:e}){return e}text({text:e}){return e}link({text:e}){return""+e}image({text:e}){return""+e}br(){return""}};var b=class l{options;renderer;textRenderer;constructor(e){this.options=e||w,this.options.renderer=this.options.renderer||new $,this.renderer=this.options.renderer,this.renderer.options=this.options,this.renderer.parser=this,this.textRenderer=new _}static parse(e,t){return new l(t).parse(e)}static parseInline(e,t){return new l(t).parseInline(e)}parse(e,t=!0){let n="";for(let s=0;s<e.length;s++){let i=e[s];if(this.options.extensions?.renderers?.[i.type]){let o=i,a=this.options.extensions.renderers[o.type].call({parser:this},o);if(a!==!1||!["space","hr","heading","code","table","blockquote","list","html","paragraph","text"].includes(o.type)){n+=a||"";continue}}let r=i;switch(r.type){case"space":{n+=this.renderer.space(r);continue}case"hr":{n+=this.renderer.hr(r);continue}case"heading":{n+=this.renderer.heading(r);continue}case"code":{n+=this.renderer.code(r);continue}case"table":{n+=this.renderer.table(r);continue}case"blockquote":{n+=this.renderer.blockquote(r);continue}case"list":{n+=this.renderer.list(r);continue}case"html":{n+=this.renderer.html(r);continue}case"paragraph":{n+=this.renderer.paragraph(r);continue}case"text":{let o=r,a=this.renderer.text(o);for(;s+1<e.length&&e[s+1].type==="text";)o=e[++s],a+=`
`+this.renderer.text(o);t?n+=this.renderer.paragraph({type:"paragraph",raw:a,text:a,tokens:[{type:"text",raw:a,text:a,escaped:!0}]}):n+=a;continue}default:{let o='Token with "'+r.type+'" type was not found.';if(this.options.silent)return console.error(o),"";throw new Error(o)}}}return n}parseInline(e,t=this.renderer){let n="";for(let s=0;s<e.length;s++){let i=e[s];if(this.options.extensions?.renderers?.[i.type]){let o=this.options.extensions.renderers[i.type].call({parser:this},i);if(o!==!1||!["escape","html","link","image","strong","em","codespan","br","del","text"].includes(i.type)){n+=o||"";continue}}let r=i;switch(r.type){case"escape":{n+=t.text(r);break}case"html":{n+=t.html(r);break}case"link":{n+=t.link(r);break}case"image":{n+=t.image(r);break}case"strong":{n+=t.strong(r);break}case"em":{n+=t.em(r);break}case"codespan":{n+=t.codespan(r);break}case"br":{n+=t.br(r);break}case"del":{n+=t.del(r);break}case"text":{n+=t.text(r);break}default:{let o='Token with "'+r.type+'" type was not found.';if(this.options.silent)return console.error(o),"";throw new Error(o)}}}return n}};var L=class{options;block;constructor(e){this.options=e||w}static passThroughHooks=new Set(["preprocess","postprocess","processAllTokens"]);preprocess(e){return e}postprocess(e){return e}processAllTokens(e){return e}provideLexer(){return this.block?x.lex:x.lexInline}provideParser(){return this.block?b.parse:b.parseInline}};var E=class{defaults=z();options=this.setOptions;parse=this.parseMarkdown(!0);parseInline=this.parseMarkdown(!1);Parser=b;Renderer=$;TextRenderer=_;Lexer=x;Tokenizer=S;Hooks=L;constructor(...e){this.use(...e)}walkTokens(e,t){let n=[];for(let s of e)switch(n=n.concat(t.call(this,s)),s.type){case"table":{let i=s;for(let r of i.header)n=n.concat(this.walkTokens(r.tokens,t));for(let r of i.rows)for(let o of r)n=n.concat(this.walkTokens(o.tokens,t));break}case"list":{let i=s;n=n.concat(this.walkTokens(i.items,t));break}default:{let i=s;this.defaults.extensions?.childTokens?.[i.type]?this.defaults.extensions.childTokens[i.type].forEach(r=>{let o=i[r].flat(1/0);n=n.concat(this.walkTokens(o,t))}):i.tokens&&(n=n.concat(this.walkTokens(i.tokens,t)))}}return n}use(...e){let t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach(n=>{let s={...n};if(s.async=this.defaults.async||s.async||!1,n.extensions&&(n.extensions.forEach(i=>{if(!i.name)throw new Error("extension name required");if("renderer"in i){let r=t.renderers[i.name];r?t.renderers[i.name]=function(...o){let a=i.renderer.apply(this,o);return a===!1&&(a=r.apply(this,o)),a}:t.renderers[i.name]=i.renderer}if("tokenizer"in i){if(!i.level||i.level!=="block"&&i.level!=="inline")throw new Error("extension level must be 'block' or 'inline'");let r=t[i.level];r?r.unshift(i.tokenizer):t[i.level]=[i.tokenizer],i.start&&(i.level==="block"?t.startBlock?t.startBlock.push(i.start):t.startBlock=[i.start]:i.level==="inline"&&(t.startInline?t.startInline.push(i.start):t.startInline=[i.start]))}"childTokens"in i&&i.childTokens&&(t.childTokens[i.name]=i.childTokens)}),s.extensions=t),n.renderer){let i=this.defaults.renderer||new $(this.defaults);for(let r in n.renderer){if(!(r in i))throw new Error(`renderer '${r}' does not exist`);if(["options","parser"].includes(r))continue;let o=r,a=n.renderer[o],c=i[o];i[o]=(...p)=>{let u=a.apply(i,p);return u===!1&&(u=c.apply(i,p)),u||""}}s.renderer=i}if(n.tokenizer){let i=this.defaults.tokenizer||new S(this.defaults);for(let r in n.tokenizer){if(!(r in i))throw new Error(`tokenizer '${r}' does not exist`);if(["options","rules","lexer"].includes(r))continue;let o=r,a=n.tokenizer[o],c=i[o];i[o]=(...p)=>{let u=a.apply(i,p);return u===!1&&(u=c.apply(i,p)),u}}s.tokenizer=i}if(n.hooks){let i=this.defaults.hooks||new L;for(let r in n.hooks){if(!(r in i))throw new Error(`hook '${r}' does not exist`);if(["options","block"].includes(r))continue;let o=r,a=n.hooks[o],c=i[o];L.passThroughHooks.has(r)?i[o]=p=>{if(this.defaults.async)return Promise.resolve(a.call(i,p)).then(d=>c.call(i,d));let u=a.call(i,p);return c.call(i,u)}:i[o]=(...p)=>{let u=a.apply(i,p);return u===!1&&(u=c.apply(i,p)),u}}s.hooks=i}if(n.walkTokens){let i=this.defaults.walkTokens,r=n.walkTokens;s.walkTokens=function(o){let a=[];return a.push(r.call(this,o)),i&&(a=a.concat(i.call(this,o))),a}}this.defaults={...this.defaults,...s}}),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return x.lex(e,t??this.defaults)}parser(e,t){return b.parse(e,t??this.defaults)}parseMarkdown(e){return(n,s)=>{let i={...s},r={...this.defaults,...i},o=this.onError(!!r.silent,!!r.async);if(this.defaults.async===!0&&i.async===!1)return o(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));if(typeof n>"u"||n===null)return o(new Error("marked(): input parameter is undefined or null"));if(typeof n!="string")return o(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(n)+", string expected"));r.hooks&&(r.hooks.options=r,r.hooks.block=e);let a=r.hooks?r.hooks.provideLexer():e?x.lex:x.lexInline,c=r.hooks?r.hooks.provideParser():e?b.parse:b.parseInline;if(r.async)return Promise.resolve(r.hooks?r.hooks.preprocess(n):n).then(p=>a(p,r)).then(p=>r.hooks?r.hooks.processAllTokens(p):p).then(p=>r.walkTokens?Promise.all(this.walkTokens(p,r.walkTokens)).then(()=>p):p).then(p=>c(p,r)).then(p=>r.hooks?r.hooks.postprocess(p):p).catch(o);try{r.hooks&&(n=r.hooks.preprocess(n));let p=a(n,r);r.hooks&&(p=r.hooks.processAllTokens(p)),r.walkTokens&&this.walkTokens(p,r.walkTokens);let u=c(p,r);return r.hooks&&(u=r.hooks.postprocess(u)),u}catch(p){return o(p)}}}onError(e,t){return n=>{if(n.message+=`
Please report this to https://github.com/markedjs/marked.`,e){let s="<p>An error occurred:</p><pre>"+R(n.message+"",!0)+"</pre>";return t?Promise.resolve(s):s}if(t)return Promise.reject(n);throw n}}};var M=new E;function k(l,e){return M.parse(l,e)}k.options=k.setOptions=function(l){return M.setOptions(l),k.defaults=M.defaults,N(k.defaults),k};k.getDefaults=z;k.defaults=w;k.use=function(...l){return M.use(...l),k.defaults=M.defaults,N(k.defaults),k};k.walkTokens=function(l,e){return M.walkTokens(l,e)};k.parseInline=M.parseInline;k.Parser=b;k.parser=b.parse;k.Renderer=$;k.TextRenderer=_;k.Lexer=x;k.lexer=x.lex;k.Tokenizer=S;k.Hooks=L;k.parse=k;var it=k.options,ot=k.setOptions,lt=k.use,at=k.walkTokens,ct=k.parseInline,pt=k,ut=b.parse,ht=x.lex;
if(__exports != exports)module.exports = exports;return module.exports}));
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RFC Review: __DOC_TITLE__</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&family=Lora:wght@400;500&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/marked@15/marked.min.js"></script>
<script>if (typeof marked === 'undefined') document.write('<script src="/assets/marked.min.js"><\/script>')</script>
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
<script>if (typeof mermaid === 'undefined') document.write('<script src="/assets/mermaid.min.js"><\/script>')</script>
<script type="module">
import { codeToHtml } from 'https://esm.sh/shiki@3/bundle/web';
window.__shikiHighlight = async function(el) {
const blocks = el.querySelectorAll('pre code[class*="language-"]');
for (const block of blocks) {
const lang = (block.className.match(/language-(\w+)/) || [])[1] || 'text';
try {
const html = await codeToHtml(block.textContent, { lang, theme: 'github-light' });
const wrapper = block.closest('pre');
if (wrapper) {
wrapper.outerHTML = html;
}
} catch { /* unsupported lang, keep original */ }
}
};
</script>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Lora', Georgia, serif;
background: #faf9f5;
color: #141413;
line-height: 1.6;
padding-bottom: 0;
}
.header {
position: sticky;
top: 0;
z-index: 100;
background: #141413;
color: #faf9f5;
padding: 1rem 2rem;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.header-left {
display: flex;
align-items: center;
gap: 0.75rem;
}
.header h1 {
font-family: 'Poppins', sans-serif;
font-size: 1.1rem;
font-weight: 600;
letter-spacing: 0.02em;
}
.iteration-badge {
font-family: 'Poppins', sans-serif;
font-size: 0.7rem;
font-weight: 600;
background: #d97757;
color: #fff;
padding: 2px 10px;
border-radius: 12px;
letter-spacing: 0.03em;
text-transform: uppercase;
}
.header-right {
display: flex;
align-items: center;
gap: 1rem;
}
.progress {
font-family: 'Poppins', sans-serif;
font-size: 0.8rem;
color: #87867f;
}
.progress .count {
color: #d97757;
font-weight: 600;
}
.save-status {
font-family: 'Poppins', sans-serif;
font-size: 0.7rem;
color: #87867f;
min-width: 60px;
text-align: right;
}
@keyframes saveFlash {
0% { color: #d97757; }
100% { color: #87867f; }
}
.save-status.flash {
animation: saveFlash 2s ease forwards;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
/* --- Section cards with staggered entrance --- */
@keyframes fadeUp {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: none; }
}
.section {
margin-bottom: 2rem;
border: 1px solid #e8e6dc;
border-radius: 6px;
background: #fff;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
opacity: 0;
animation: fadeUp 0.4s ease forwards;
}
.section.has-feedback {
border-left: 3px solid #d97757;
}
.section-header {
display: flex;
align-items: center;
gap: 0.5rem;
}
.comment-badge {
display: inline-flex;
align-items: center;
justify-content: center;
background: #d97757;
color: #fff;
font-family: 'Poppins', sans-serif;
font-size: 0.65rem;
font-weight: 600;
min-width: 18px;
height: 18px;
padding: 0 5px;
border-radius: 9px;
line-height: 1;
vertical-align: middle;
}
.section-content {
padding: 1.5rem 2rem;
position: relative;
}
.section-content h2 { font-family: 'Poppins', sans-serif; font-size: 1.25rem; margin-bottom: 0.75rem; }
.section-content h3 { font-family: 'Poppins', sans-serif; font-size: 1.05rem; margin: 1rem 0 0.5rem; }
.section-content p { margin-bottom: 0.75rem; }
.section-content ul, .section-content ol { margin: 0.5rem 0 0.75rem 1.5rem; }
.section-content li { margin-bottom: 0.25rem; }
.section-content code {
background: #f0eee4;
padding: 0.15em 0.4em;
border-radius: 3px;
font-size: 0.9em;
}
.section-content pre {
background: #141413;
color: #faf9f5;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
margin: 0.75rem 0;
}
.section-content pre code {
background: none;
padding: 0;
color: inherit;
}
.section-content table {
width: 100%;
border-collapse: collapse;
margin: 0.75rem 0;
font-size: 0.9rem;
}
.section-content th, .section-content td {
border: 1px solid #e8e6dc;
padding: 0.5rem 0.75rem;
text-align: left;
}
.section-content th {
background: #141413;
color: #faf9f5;
font-family: 'Poppins', sans-serif;
font-weight: 500;
font-size: 0.8rem;
letter-spacing: 0.03em;
}
.section-content blockquote {
border-left: 3px solid #d97757;
padding: 0.5rem 1rem;
margin: 0.75rem 0;
background: #f0eee4;
color: #87867f;
}
mark.commented {
background: rgba(217,119,87,0.25);
border-bottom: 2px solid #d97757;
cursor: pointer;
padding: 0 1px;
border-radius: 2px;
transition: background 0.15s;
}
mark.commented:hover {
background: rgba(217,119,87,0.4);
}
/* --- Floating comment button (fade-in) --- */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.comment-btn {
position: absolute;
z-index: 50;
background: #d97757;
color: #fff;
font-family: 'Poppins', sans-serif;
font-size: 0.75rem;
font-weight: 600;
border: none;
border-radius: 4px;
padding: 4px 10px;
cursor: pointer;
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
transition: background 0.15s;
white-space: nowrap;
animation: fadeIn 0.15s ease;
}
.comment-btn:hover { background: #b8603f; }
/* --- Comment popover (scale+fade entrance) --- */
@keyframes popoverIn {
from { opacity: 0; transform: scale(0.95) translateY(-4px); }
to { opacity: 1; transform: none; }
}
.comment-popover {
position: absolute;
z-index: 60;
background: #fff;
border: 1px solid #e8e6dc;
border-radius: 6px;
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
padding: 0.75rem;
width: 300px;
font-family: 'Lora', Georgia, serif;
animation: popoverIn 0.15s ease;
}
.comment-popover textarea {
width: 100%;
min-height: 60px;
padding: 0.5rem;
border: 1px solid #e8e6dc;
border-radius: 4px;
font-family: 'Lora', Georgia, serif;
font-size: 0.85rem;
line-height: 1.4;
resize: vertical;
background: #faf9f5;
color: #141413;
}
.comment-popover textarea:focus {
outline: none;
border-color: #d97757;
box-shadow: 0 0 0 2px rgba(217,119,87,0.15);
}
.comment-popover .popover-selected-text {
font-size: 0.75rem;
color: #87867f;
margin-bottom: 0.5rem;
padding: 0.3rem 0.5rem;
background: #f0eee4;
border-radius: 3px;
max-height: 3em;
overflow: hidden;
text-overflow: ellipsis;
font-style: italic;
}
.comment-popover .popover-actions {
display: flex;
gap: 0.5rem;
margin-top: 0.5rem;
justify-content: flex-end;
}
.comment-popover .popover-actions button {
font-family: 'Poppins', sans-serif;
font-size: 0.7rem;
font-weight: 600;
padding: 4px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background 0.15s;
}
.comment-popover .popover-save {
background: #d97757;
color: #fff;
}
.comment-popover .popover-save:hover { background: #b8603f; }
.comment-popover .popover-cancel {
background: #e8e6dc;
color: #141413;
}
.comment-popover .popover-cancel:hover { background: #d4d2c8; }
.comment-popover .popover-delete {
background: #B00020;
color: #fff;
margin-right: auto;
}
.comment-popover .popover-delete:hover { background: #800017; }
.mermaid {
text-align: center;
margin: 1rem 0;
overflow-x: auto;
}
/* --- Feedback toggle with chevron --- */
.feedback-toggle {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 2rem;
border-top: 1px solid #e8e6dc;
background: #faf9f5;
cursor: pointer;
font-family: 'Poppins', sans-serif;
font-size: 0.8rem;
color: #87867f;
user-select: none;
transition: color 0.2s;
}
.feedback-toggle:hover { color: #d97757; }
.feedback-toggle .chevron {
display: inline-block;
width: 6px;
height: 6px;
border-right: 2px solid #87867f;
border-bottom: 2px solid #87867f;
transform: rotate(-45deg);
transition: transform 0.2s ease, border-color 0.2s;
flex-shrink: 0;
}
.feedback-toggle.open .chevron {
transform: rotate(45deg);
}
.feedback-toggle:hover .chevron {
border-color: #d97757;
}
.section.has-feedback .feedback-toggle .chevron { border-color: #d97757; }
/* --- Feedback area with animated expand/collapse --- */
.feedback-area {
max-height: 0;
opacity: 0;
overflow: hidden;
padding: 0 2rem;
background: #faf9f5;
transition: max-height 0.3s ease, opacity 0.25s ease, padding-bottom 0.3s ease;
}
.feedback-area.open {
max-height: 300px;
opacity: 1;
padding: 0 2rem 1.5rem;
}
.feedback-area textarea {
width: 100%;
min-height: 80px;
padding: 0.75rem;
border: 1px solid #e8e6dc;
border-radius: 4px;
font-family: 'Lora', Georgia, serif;
font-size: 0.9rem;
line-height: 1.5;
resize: vertical;
background: #fff;
color: #141413;
transition: border-color 0.2s, box-shadow 0.2s;
}
.feedback-area textarea:focus {
outline: none;
border-color: #d97757;
box-shadow: 0 0 0 3px rgba(217,119,87,0.15);
}
/* --- Previous feedback shared label (deduplicated) --- */
.prev-label {
font-family: 'Poppins', sans-serif;
font-size: 0.7rem;
font-weight: 600;
color: #87867f;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.25rem;
}
/* --- Previous feedback (read-only) --- */
.previous-feedback {
margin: 0 2rem 0.75rem;
padding: 0.75rem 1rem;
background: #f5ede7;
border-left: 3px solid #d97757;
border-radius: 0 4px 4px 0;
font-size: 0.85rem;
color: #87867f;
}
.previous-feedback-text {
white-space: pre-wrap;
line-height: 1.5;
}
.previous-inline-comments {
margin: 0 2rem 0.5rem;
padding: 0.5rem 1rem;
background: #f5ede7;
border-left: 3px solid #d97757;
border-radius: 0 4px 4px 0;
font-size: 0.8rem;
color: #87867f;
}
.previous-inline-comments .pic-item {
margin-bottom: 0.4rem;
padding-bottom: 0.4rem;
border-bottom: 1px solid #e8e6dc;
}
.previous-inline-comments .pic-item:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
.previous-inline-comments .pic-selected {
font-style: italic;
color: #87867f;
}
.previous-inline-comments .pic-comment {
margin-top: 0.15rem;
}
.previous-overall {
margin-bottom: 1rem;
padding: 0.75rem 1rem;
background: #f5ede7;
border-left: 3px solid #d97757;
border-radius: 0 4px 4px 0;
font-size: 0.85rem;
color: #87867f;
}
.previous-overall .previous-feedback-text {
white-space: pre-wrap;
line-height: 1.5;
}
/* --- Overall section --- */
.overall-section {
margin: 2rem 0;
padding: 2rem;
border: 1px solid #e8e6dc;
border-radius: 6px;
background: #fff;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
opacity: 0;
animation: fadeUp 0.4s ease forwards;
}
.overall-section h2 {
font-family: 'Poppins', sans-serif;
font-size: 1.1rem;
margin-bottom: 1rem;
letter-spacing: 0.03em;
text-transform: uppercase;
color: #141413;
}
.overall-section textarea {
width: 100%;
min-height: 120px;
padding: 0.75rem;
border: 1px solid #e8e6dc;
border-radius: 4px;
font-family: 'Lora', Georgia, serif;
font-size: 0.9rem;
line-height: 1.5;
resize: vertical;
background: #faf9f5;
color: #141413;
transition: border-color 0.2s, box-shadow 0.2s;
}
.overall-section textarea:focus {
outline: none;
border-color: #d97757;
box-shadow: 0 0 0 3px rgba(217,119,87,0.15);
}
/* --- Sticky submit bar --- */
.submit-bar {
position: sticky;
bottom: 0;
z-index: 90;
background: #141413;
border-top: 1px solid rgba(255,255,255,0.08);
padding: 0.75rem 2rem;
display: flex;
justify-content: center;
gap: 1rem;
box-shadow: 0 -2px 12px rgba(0,0,0,0.2);
}
.submit-btn {
font-family: 'Poppins', sans-serif;
font-size: 0.875rem;
font-weight: 600;
padding: 0.65rem 2.5rem;
border: none;
border-radius: 6px;
cursor: pointer;
background: #d97757;
color: #faf9f5;
transition: background 0.2s;
letter-spacing: 0.02em;
}
.submit-btn:hover { background: #b8603f; }
.approve-btn {
font-family: 'Poppins', sans-serif;
font-size: 0.875rem;
font-weight: 600;
padding: 0.65rem 2.5rem;
border: none;
border-radius: 6px;
cursor: pointer;
background: #788c5d;
color: #faf9f5;
transition: background 0.2s;
letter-spacing: 0.02em;
}
.approve-btn:hover { background: #657a4c; }
/* --- Done overlay with backdrop fade + card entrance --- */
.done-overlay {
display: flex;
position: fixed;
inset: 0;
z-index: 1000;
background: rgba(20,20,19,0.6);
align-items: center;
justify-content: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease;
}
.done-overlay.visible {
opacity: 1;
pointer-events: auto;
}
.done-card {
background: #fff;
border-radius: 10px;
padding: 2.5rem 3rem;
max-width: 420px;
text-align: center;
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
transform: translateY(20px) scale(0.95);
opacity: 0;
transition: transform 0.25s ease 0.05s, opacity 0.25s ease 0.05s;
}
.done-overlay.visible .done-card {
transform: none;
opacity: 1;
}
.done-card h2 {
font-family: 'Poppins', sans-serif;
font-size: 1.3rem;
margin-bottom: 0.75rem;
color: #141413;
}
.done-card p {
font-size: 0.95rem;
color: #87867f;
margin-bottom: 1.5rem;
line-height: 1.5;
}
.done-card button {
font-family: 'Poppins', sans-serif;
font-size: 0.85rem;
font-weight: 600;
padding: 0.6rem 2rem;
border: none;
border-radius: 6px;
cursor: pointer;
background: #d97757;
color: #fff;
transition: background 0.15s;
}
.done-card button:hover { background: #b8603f; }
/* --- Focus-visible for accessibility --- */
button:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(217,119,87,0.3);
}
textarea:focus-visible {
outline: none;
border-color: #d97757;
box-shadow: 0 0 0 3px rgba(217,119,87,0.15);
}
/* --- Responsive header --- */
@media (max-width: 640px) {
.header {
flex-direction: column;
gap: 0.5rem;
padding: 0.75rem 1rem;
}
.header-left, .header-right {
width: 100%;
justify-content: center;
}
.header h1 {
font-size: 0.95rem;
}
.progress {
font-size: 0.7rem;
}
}
/* ============================================
LIVE MODE STYLES
============================================ */
.live-progress {
display: flex;
align-items: center;
gap: 0.75rem;
}
.live-progress-bar {
width: 120px;
height: 6px;
background: #333;
border-radius: 3px;
overflow: hidden;
}
.live-progress-fill {
height: 100%;
background: #788c5d;
border-radius: 3px;
transition: width 0.4s ease;
width: 0%;
}
.live-progress-text {
font-family: 'Poppins', sans-serif;
font-size: 0.75rem;
color: #87867f;
white-space: nowrap;
}
/* Section states */
.section.live-pending {
border: 1px dashed #d4d2c8;
background: #faf9f5;
opacity: 1;
}
.section.live-pending .live-pending-placeholder {
font-family: 'Poppins', sans-serif;
font-size: 0.85rem;
color: #b0afa8;
font-style: italic;
padding: 0.75rem 0;
}
@keyframes livePulse {
0%, 100% { border-color: #d97757; }
50% { border-color: #f0b89a; }
}
.section.live-review {
border: 2px solid #d97757;
animation: livePulse 2s ease-in-out infinite;
opacity: 1;
}
.section.live-approved {
border-left: 3px solid #788c5d;
border-color: #788c5d;
opacity: 1;
}
.section.live-changes-requested,
.section.live-changes_requested {
border-left: 3px solid #d97757;
opacity: 1;
}
.live-approved-indicator {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 2rem;
background: #f0f5e8;
border-bottom: 1px solid #e8e6dc;
font-family: 'Poppins', sans-serif;
font-size: 0.8rem;
color: #788c5d;
font-weight: 600;
cursor: pointer;
user-select: none;
transition: background 0.15s;
}
.live-approved-indicator:hover { background: #e5edda; }
.live-approved-indicator .chevron {
display: inline-block;
width: 6px;
height: 6px;
border-right: 2px solid #788c5d;
border-bottom: 2px solid #788c5d;
transform: rotate(-45deg);
transition: transform 0.2s ease;
flex-shrink: 0;
}
.section.live-approved.expanded .live-approved-indicator .chevron {
transform: rotate(45deg);
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.live-revising-indicator {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 2rem;
background: #fef3ee;
border-top: 1px solid #e8e6dc;
font-family: 'Poppins', sans-serif;
font-size: 0.8rem;
color: #d97757;
}
.live-spinner {
width: 14px;
height: 14px;
border: 2px solid #d97757;
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
flex-shrink: 0;
}
/* Per-section review bar (GitHub PR review model) */
.live-review-bar {
padding: 0.75rem 2rem;
border-top: 1px solid #e8e6dc;
background: #fdf8f5;
}
.live-review-summary {
font-family: 'Poppins', sans-serif;
font-size: 0.8rem;
font-weight: 600;
color: #d97757;
margin-bottom: 0.5rem;
display: none;
}
.live-review-summary.has-comments { display: block; }
.live-review-note {
width: 100%;
padding: 0.6rem 0.75rem;
border: 1px solid #e8e6dc;
border-radius: 4px;
font-family: 'Lora', Georgia, serif;
font-size: 0.85rem;
line-height: 1.5;
resize: vertical;
background: #fff;
color: #141413;
transition: border-color 0.2s, box-shadow 0.2s;
}
.live-review-note:focus {
outline: none;
border-color: #d97757;
box-shadow: 0 0 0 2px rgba(217,119,87,0.15);
}
.live-review-actions {
display: flex;
gap: 0.5rem;
margin-top: 0.5rem;
justify-content: flex-end;
}
.live-approve-btn {
font-family: 'Poppins', sans-serif;
font-size: 0.8rem;
font-weight: 600;
padding: 6px 18px;
border: none;
border-radius: 4px;
cursor: pointer;
background: #788c5d;
color: #fff;
transition: background 0.15s;
}
.live-approve-btn:hover { background: #657a4c; }
.live-changes-btn {
font-family: 'Poppins', sans-serif;
font-size: 0.8rem;
font-weight: 600;
padding: 6px 18px;
border: none;
border-radius: 4px;
cursor: pointer;
background: #d97757;
color: #fff;
transition: background 0.15s;
}
.live-changes-btn:hover { background: #b8603f; }
@keyframes shake {
0%, 100% { transform: translateX(0); }
20% { transform: translateX(-6px); }
40% { transform: translateX(6px); }
60% { transform: translateX(-4px); }
80% { transform: translateX(4px); }
}
.live-review-bar.needs-input {
animation: shake 0.4s ease;
}
.live-review-bar.needs-input .live-review-note {
border-color: #d97757;
box-shadow: 0 0 0 2px rgba(217,119,87,0.25);
}
.live-user-feedback {
padding: 0.75rem 2rem;
background: #fef3ee;
font-size: 0.85rem;
color: #87867f;
border-top: 1px solid #e8e6dc;
}
.live-user-feedback .feedback-text {
white-space: pre-wrap;
line-height: 1.5;
margin-top: 0.25rem;
}
.live-finalize-bar {
position: sticky;
bottom: 0;
z-index: 90;
background: #141413;
border-top: 1px solid rgba(255,255,255,0.08);
padding: 0.75rem 2rem;
display: none;
justify-content: center;
gap: 1rem;
box-shadow: 0 -2px 12px rgba(0,0,0,0.2);
}
.live-finalize-btn {
font-family: 'Poppins', sans-serif;
font-size: 0.875rem;
font-weight: 600;
padding: 0.65rem 2.5rem;
border: none;
border-radius: 6px;
cursor: pointer;
background: #788c5d;
color: #faf9f5;
transition: background 0.2s, opacity 0.2s;
letter-spacing: 0.02em;
}
.live-finalize-btn:hover { background: #657a4c; }
.live-finalize-btn:disabled { opacity: 0.4; cursor: not-allowed; }
</style>
</head>
<body>
<div class="header">
<div class="header-left">
<h1>RFC Review: <span id="doc-title">__DOC_TITLE__</span></h1>
<span class="iteration-badge" id="iteration-badge" style="display:none;"></span>
</div>
<div class="header-right">
<div class="live-progress" id="live-progress" style="display:none;">
<div class="live-progress-bar"><div class="live-progress-fill" id="live-progress-fill"></div></div>
<span class="live-progress-text" id="live-progress-text">0 of 0 sections</span>
</div>
<span class="save-status" id="save-status"></span>
<div class="progress" id="batch-progress"><span class="count" id="feedback-count">0</span> of <span id="section-count">0</span> sections have feedback</div>
</div>
</div>
<div class="container">
<div id="sections"></div>
<div class="overall-section" id="overall-section">
<h2>Overall Feedback</h2>
<div id="previous-overall-container"></div>
<textarea id="overall-feedback" placeholder="General thoughts on the RFC..."></textarea>
</div>
</div>
<div class="submit-bar" id="submit-bar">
<button class="submit-btn" onclick="submitFeedback('needs_revision')">Submit Feedback</button>
<button class="approve-btn" onclick="submitFeedback('approved')">Approve</button>
</div>
<div class="live-finalize-bar" id="live-finalize-bar">
<button class="live-finalize-btn" id="live-finalize-btn" disabled onclick="finalizeRFC()">Finalize RFC</button>
</div>
<!-- Done overlay -->
<div class="done-overlay" id="done-overlay">
<div class="done-card">
<h2 id="done-title">Feedback Submitted</h2>
<p id="done-message">Your feedback has been saved. Return to your Claude Code session — Claude will read your feedback and revise the document.</p>
<button onclick="closeDoneDialog()">OK</button>
</div>
</div>
<script>
const MARKDOWN_CONTENT = __MARKDOWN_CONTENT__;
const PREVIOUS_FEEDBACK = __PREVIOUS_FEEDBACK__;
const ITERATION = __ITERATION__;
const SERVER_MODE = __SERVER_MODE__;
const LIVE_MODE = __LIVE_MODE__;
// Inline comments store: [{ sectionIndex, selectedText, comment, id }]
const inlineComments = [];
let commentIdCounter = 0;
let saveTimeout = null;
let serverAvailable = SERVER_MODE;
// Strip YAML frontmatter (--- ... ---) before rendering
function stripFrontmatter(md) {
return md.replace(/^\s*---[\s\S]*?---\s*\n?/, '');
}
// Strip leading markdown heading (## Title) — used in live mode where heading is already in the card header
function stripLeadingHeading(md) {
return md.replace(/^\s*#{1,6}\s+[^\n]*\n*/, '');
}
document.addEventListener('DOMContentLoaded', () => {
// Show iteration badge if round > 1
if (ITERATION > 1) {
const badge = document.getElementById('iteration-badge');
badge.textContent = `Round ${ITERATION}`;
badge.style.display = '';
}
// --- LIVE MODE ---
if (LIVE_MODE) {
initLiveMode();
return;
}
// --- BATCH MODE (unchanged) ---
const html = marked.parse(stripFrontmatter(MARKDOWN_CONTENT));
const tmp = document.createElement('div');
tmp.innerHTML = html;
// Detect which heading level to split on: prefer H2, fall back to H1
const h2s = tmp.querySelectorAll('h2');
const h1s = tmp.querySelectorAll('h1');
const splitTag = h2s.length > 0 ? 'H2' : (h1s.length > 0 ? 'H1' : 'H2');
const sections = [];
let current = null;
for (const node of Array.from(tmp.childNodes)) {
if (node.nodeType === 1 && node.tagName === splitTag) {
if (current) sections.push(current);
current = { heading: node.textContent.trim(), html: '' };
} else if (current) {
current.html += node.outerHTML || node.textContent;
} else {
if (!current) {
current = { heading: 'Introduction', html: '' };
}
current.html += node.outerHTML || node.textContent;
}
}
if (current) sections.push(current);
const container = document.getElementById('sections');
document.getElementById('section-count').textContent = sections.length;
// Build a lookup from previous feedback by heading
const prevByHeading = {};
const prevInlineBySection = {};
if (PREVIOUS_FEEDBACK && PREVIOUS_FEEDBACK.sections) {
PREVIOUS_FEEDBACK.sections.forEach(s => {
if (s.feedback) prevByHeading[s.heading] = s.feedback;
});
}
if (PREVIOUS_FEEDBACK && PREVIOUS_FEEDBACK.inline_comments) {
PREVIOUS_FEEDBACK.inline_comments.forEach(c => {
if (!prevInlineBySection[c.section]) prevInlineBySection[c.section] = [];
prevInlineBySection[c.section].push(c);
});
}
sections.forEach((sec, i) => {
const div = document.createElement('div');
div.className = 'section';
div.dataset.index = i;
div.style.animationDelay = `${i * 60}ms`;
// Build previous feedback HTML for this section
let prevFeedbackHtml = '';
if (prevByHeading[sec.heading]) {
prevFeedbackHtml = `
<div class="previous-feedback">
<div class="prev-label">Previous Round Feedback</div>
<div class="previous-feedback-text">${escapeHtml(prevByHeading[sec.heading])}</div>
</div>`;
}
// Build previous inline comments for this section
let prevInlineHtml = '';
if (prevInlineBySection[sec.heading] && prevInlineBySection[sec.heading].length > 0) {
const items = prevInlineBySection[sec.heading].map(c =>
`<div class="pic-item">
<div class="pic-selected">"${escapeHtml(c.selected_text.length > 80 ? c.selected_text.slice(0, 80) + '...' : c.selected_text)}"</div>
<div class="pic-comment">${escapeHtml(c.comment)}</div>
</div>`
).join('');
prevInlineHtml = `
<div class="previous-inline-comments">
<div class="prev-label">Previous Inline Comments</div>
${items}
</div>`;
}
const sectionSlug = slugify(sec.heading);
div.dataset.sectionId = sectionSlug;
div.innerHTML = `
<div class="section-content" data-section-index="${i}" data-section-id="${sectionSlug}">
<div class="section-header"><h2>${sec.heading}</h2></div>
${sec.html}
</div>
${prevFeedbackHtml}
${prevInlineHtml}
<div class="feedback-toggle" onclick="toggleFeedback(this)">
<span class="chevron"></span> Add Feedback
</div>
<div class="feedback-area">
<textarea data-section="${i}" placeholder="Your feedback on this section..." oninput="onFeedbackInput()"></textarea>
</div>`;
container.appendChild(div);
});
// Set stagger delay on overall section (appears after all section cards)
const overallSection = document.querySelector('.overall-section');
if (overallSection) {
overallSection.style.animationDelay = `${sections.length * 60}ms`;
}
// Show previous overall feedback if present
if (PREVIOUS_FEEDBACK && PREVIOUS_FEEDBACK.overall_feedback) {
const prevOverallContainer = document.getElementById('previous-overall-container');
prevOverallContainer.innerHTML = `
<div class="previous-overall">
<div class="prev-label">Previous Round Overall Feedback</div>
<div class="previous-feedback-text">${escapeHtml(PREVIOUS_FEEDBACK.overall_feedback)}</div>
</div>`;
}
// Post-process mermaid code blocks into diagram containers
document.querySelectorAll('pre code.language-mermaid').forEach(block => {
const pre = block.parentElement;
const div = document.createElement('div');
div.className = 'mermaid';
div.textContent = block.textContent;
pre.replaceWith(div);
});
mermaid.initialize({ startOnLoad: false, theme: 'neutral' });
mermaid.run();
// Syntax highlighting via Shiki
if (window.__shikiHighlight) {
window.__shikiHighlight(document.getElementById('sections-container'));
}
// Auto-save debounce on overall feedback textarea
document.getElementById('overall-feedback').addEventListener('input', () => {
onFeedbackInput();
});
// Listen for text selection inside section-content areas
document.addEventListener('mouseup', handleTextSelection);
// Close comment button when clicking elsewhere
document.addEventListener('mousedown', (e) => {
if (!e.target.classList.contains('comment-btn')) {
removeCommentBtn();
}
});
});
// --- Auto-save ---
function onFeedbackInput() {
updateProgress();
if (!serverAvailable) return;
clearTimeout(saveTimeout);
saveTimeout = setTimeout(() => autoSave(), 800);
}
function triggerAutoSave() {
if (!serverAvailable) return;
clearTimeout(saveTimeout);
saveTimeout = setTimeout(() => autoSave(), 800);
}
function autoSave() {
if (!serverAvailable) return;
// Live mode persists state via session.json on the server — skip batch feedback.json saves
if (LIVE_MODE) return;
const data = gatherFeedback('draft');
const payload = JSON.stringify(data, null, 2);
const statusEl = document.getElementById('save-status');
statusEl.textContent = 'Saving...';
statusEl.classList.remove('flash');
fetch('/api/feedback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: payload,
})
.then(r => {
if (r.ok) {
statusEl.textContent = '\u2713 Saved';
statusEl.classList.remove('flash');
void statusEl.offsetWidth;
statusEl.classList.add('flash');
setTimeout(() => { if (statusEl.textContent === '\u2713 Saved') statusEl.textContent = ''; }, 2000);
} else {
statusEl.textContent = 'Save failed';
serverAvailable = false;
}
})
.catch(() => {
statusEl.textContent = '';
serverAvailable = false;
});
}
// --- Gather feedback into JSON ---
function gatherFeedback(status) {
const sectionEls = document.querySelectorAll('.section');
const sections = Array.from(sectionEls).map(el => ({
heading: el.querySelector('.section-content h2').textContent.trim(),
feedback: (el.querySelector('.feedback-area textarea')?.value || '').trim()
}));
return {
iteration: ITERATION,
sections,
inline_comments: inlineComments.map(c => {
let sectionName;
if (c.sectionId) {
sectionName = document.querySelector(`[data-section-id="${c.sectionId}"] h2`)?.textContent.trim()
|| document.querySelector(`.section[data-section-id="${c.sectionId}"] .section-content h2`)?.textContent.trim()
|| c.sectionId;
} else if (c.sectionIndex >= 0) {
sectionName = document.querySelectorAll('.section')[c.sectionIndex]
?.querySelector('.section-content h2')?.textContent.trim() || `Section ${c.sectionIndex + 1}`;
} else {
sectionName = 'Unknown Section';
}
return { section: sectionName, selected_text: c.selectedText, comment: c.comment };
}),
overall_feedback: document.getElementById('overall-feedback').value.trim(),
status: status,
timestamp: new Date().toISOString()
};
}
// --- Submit / Approve ---
function submitFeedback(status) {
const result = gatherFeedback(status);
const payload = JSON.stringify(result, null, 2);
if (serverAvailable) {
// Server mode: POST to server, then show overlay
fetch('/api/feedback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: payload,
})
.then(r => {
if (!r.ok) throw new Error('Save failed');
showDoneDialog(status);
})
.catch(() => {
// Server failed — fall back to Blob download
downloadBlob(payload);
showDoneDialog(status);
});
} else {
// Static mode: download as file
downloadBlob(payload);
showDoneDialog(status);
}
}
function downloadBlob(payload) {
const blob = new Blob([payload], { type: 'application/json' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'feedback.json';
a.click();
URL.revokeObjectURL(a.href);
}
function showDoneDialog(status) {
const titleEl = document.getElementById('done-title');
const msgEl = document.getElementById('done-message');
if (status === 'approved') {
titleEl.textContent = 'Document Approved';
msgEl.textContent = 'You approved the RFC. Return to your Claude Code session — Claude will see your approval.';
} else {
titleEl.textContent = 'Feedback Submitted';
msgEl.textContent = 'Your feedback has been saved. Return to your Claude Code session — Claude will read your feedback and revise the document.';
}
document.getElementById('done-overlay').classList.add('visible');
}
function closeDoneDialog() {
document.getElementById('done-overlay').classList.remove('visible');
}
// --- Floating "Comment" button on text selection ---
function handleTextSelection(e) {
// Don't interfere with comment button or popover interactions
if (e.target.closest('.comment-btn') || e.target.closest('.comment-popover')) return;
const sel = window.getSelection();
if (!sel || sel.isCollapsed || !sel.toString().trim()) return;
const range = sel.getRangeAt(0);
const sectionContent = range.commonAncestorContainer.nodeType === 1
? range.commonAncestorContainer.closest('.section-content')
: range.commonAncestorContainer.parentElement?.closest('.section-content');
if (!sectionContent) return;
// Don't show comment button if selection is inside a popover
if (range.commonAncestorContainer.nodeType === 1
? range.commonAncestorContainer.closest('.comment-popover')
: range.commonAncestorContainer.parentElement?.closest('.comment-popover')) {
return;
}
removeCommentBtn();
// Save selection state eagerly — mousedown on the button will collapse the live selection
const savedText = sel.toString().trim();
const savedRange = range.cloneRange();
const rect = range.getBoundingClientRect();
const contentRect = sectionContent.getBoundingClientRect();
const btn = document.createElement('button');
btn.className = 'comment-btn';
btn.textContent = 'Comment';
btn.style.top = (rect.bottom - contentRect.top + 4) + 'px';
btn.style.left = (rect.left - contentRect.left + rect.width / 2 - 30) + 'px';
btn.addEventListener('click', (ev) => {
ev.stopPropagation();
const rawIndex = parseInt(sectionContent.dataset.sectionIndex, 10);
const sectionIndex = isNaN(rawIndex) ? -1 : rawIndex;
const sectionId = sectionContent.dataset.sectionId || null;
openCommentPopover(sectionContent, savedRange, savedText, sectionIndex, sectionId);
removeCommentBtn();
});
sectionContent.appendChild(btn);
}
function removeCommentBtn() {
document.querySelectorAll('.comment-btn').forEach(b => b.remove());
}
// --- Comment Popover (new comment) ---
function openCommentPopover(sectionContent, range, selectedText, sectionIndex, sectionIdOrComment, existingComment) {
// Handle backward-compatible call signature: old callers pass existingComment as 5th arg
let sectionId = null;
if (typeof sectionIdOrComment === 'string' || sectionIdOrComment === null) {
sectionId = sectionIdOrComment;
} else if (typeof sectionIdOrComment === 'object' && sectionIdOrComment !== null) {
existingComment = sectionIdOrComment;
sectionId = sectionContent.dataset.sectionId || null;
}
closeAllPopovers();
const rect = range.getBoundingClientRect();
const contentRect = sectionContent.getBoundingClientRect();
const popover = document.createElement('div');
popover.className = 'comment-popover';
popover.style.top = (rect.bottom - contentRect.top + 8) + 'px';
// Position horizontally, clamping to stay within section
let leftPos = rect.left - contentRect.left;
leftPos = Math.max(0, Math.min(leftPos, contentRect.width - 310));
popover.style.left = leftPos + 'px';
const truncated = selectedText.length > 80 ? selectedText.slice(0, 80) + '...' : selectedText;
popover.innerHTML = `
<div class="popover-selected-text">"${escapeHtml(truncated)}"</div>
<textarea placeholder="Add your comment...">${existingComment ? escapeHtml(existingComment.comment) : ''}</textarea>
<div class="popover-actions">
${existingComment ? '<button class="popover-delete">Delete</button>' : ''}
<button class="popover-cancel">Cancel</button>
<button class="popover-save">Save</button>
</div>
`;
sectionContent.appendChild(popover);
const textarea = popover.querySelector('textarea');
textarea.focus();
// Prevent popover clicks from triggering mousedown handler
popover.addEventListener('mousedown', (e) => e.stopPropagation());
popover.querySelector('.popover-cancel').addEventListener('click', () => {
closeAllPopovers();
});
popover.querySelector('.popover-save').addEventListener('click', () => {
const commentText = textarea.value.trim();
if (!commentText) { closeAllPopovers(); return; }
if (existingComment) {
existingComment.comment = commentText;
} else {
const id = ++commentIdCounter;
// Wrap selected text in a <mark>
try {
const mark = document.createElement('mark');
mark.className = 'commented';
mark.dataset.commentId = id;
range.surroundContents(mark);
mark.addEventListener('click', () => openExistingCommentPopover(mark));
} catch (e) {
// surroundContents fails if selection spans multiple elements;
// fall back to inserting marks around the range contents
const fragment = range.extractContents();
const mark = document.createElement('mark');
mark.className = 'commented';
mark.dataset.commentId = id;
mark.appendChild(fragment);
range.insertNode(mark);
mark.addEventListener('click', () => openExistingCommentPopover(mark));
}
inlineComments.push({
id,
sectionIndex,
sectionId: sectionId || sectionContent.dataset.sectionId || null,
selectedText,
comment: commentText
});
}
closeAllPopovers();
window.getSelection().removeAllRanges();
updateCommentBadges();
triggerAutoSave();
});
if (existingComment) {
popover.querySelector('.popover-delete').addEventListener('click', () => {
// Remove highlight
const mark = sectionContent.querySelector(`mark[data-comment-id="${existingComment.id}"]`);
if (mark) {
const parent = mark.parentNode;
while (mark.firstChild) parent.insertBefore(mark.firstChild, mark);
parent.removeChild(mark);
parent.normalize();
}
// Remove from array
const idx = inlineComments.findIndex(c => c.id === existingComment.id);
if (idx !== -1) inlineComments.splice(idx, 1);
closeAllPopovers();
updateCommentBadges();
triggerAutoSave();
});
}
}
function openExistingCommentPopover(mark) {
const commentId = parseInt(mark.dataset.commentId, 10);
const comment = inlineComments.find(c => c.id === commentId);
if (!comment) return;
const sectionContent = mark.closest('.section-content');
const range = document.createRange();
range.selectNodeContents(mark);
openCommentPopover(sectionContent, range, comment.selectedText, comment.sectionIndex, comment.sectionId || null, comment);
}
function closeAllPopovers() {
document.querySelectorAll('.comment-popover').forEach(p => p.remove());
}
// --- Comment badges on section headers ---
function updateCommentBadges() {
document.querySelectorAll('.section').forEach(section => {
const rawIndex = parseInt(section.dataset.index, 10);
const sectionIndex = isNaN(rawIndex) ? -1 : rawIndex;
const sectionId = section.dataset.sectionId || null;
const count = inlineComments.filter(c =>
(sectionId && c.sectionId === sectionId) || (!sectionId && sectionIndex >= 0 && c.sectionIndex === sectionIndex)
).length;
const header = section.querySelector('.section-header');
if (!header) return;
let badge = header.querySelector('.comment-badge');
if (count > 0) {
if (!badge) {
badge = document.createElement('span');
badge.className = 'comment-badge';
header.appendChild(badge);
}
badge.textContent = count;
} else if (badge) {
badge.remove();
}
// Update review bar summary in live mode
if (LIVE_MODE && sectionId) {
updateLiveReviewSummary(sectionId);
}
});
}
// --- Utility ---
function slugify(heading) {
let s = heading.toLowerCase().trim();
s = s.replace(/[^a-z0-9\s-]/g, '');
s = s.replace(/[\s]+/g, '-');
s = s.replace(/-+/g, '-');
return s.replace(/^-|-$/g, '');
}
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function toggleFeedback(el) {
const area = el.nextElementSibling;
area.classList.toggle('open');
el.classList.toggle('open');
if (area.classList.contains('open')) {
area.querySelector('textarea').focus();
}
}
function updateProgress() {
const textareas = document.querySelectorAll('.feedback-area textarea');
let count = 0;
textareas.forEach(ta => {
const section = ta.closest('.section');
if (ta.value.trim()) {
count++;
section.classList.add('has-feedback');
} else {
section.classList.remove('has-feedback');
}
});
document.getElementById('feedback-count').textContent = count;
}
// ============================================
// LIVE MODE
// ============================================
async function initLiveMode() {
// Hide batch-mode UI
document.getElementById('overall-section').style.display = 'none';
document.getElementById('submit-bar').style.display = 'none';
document.getElementById('batch-progress').style.display = 'none';
// Show live-mode UI
document.getElementById('live-progress').style.display = 'flex';
document.getElementById('live-finalize-bar').style.display = 'flex';
// Fetch current session state
let session;
try {
const resp = await fetch('/api/session');
session = await resp.json();
} catch (e) {
console.error('Failed to fetch session:', e);
return;
}
// Render skeleton cards for all sections
const container = document.getElementById('sections');
container.innerHTML = '';
const entries = Object.entries(session.sections);
entries.sort((a, b) => a[1].order - b[1].order);
for (const [id, sec] of entries) {
const card = createLiveSectionCard(id, sec.heading, sec.order, sec.status, sec.markdown);
container.appendChild(card);
}
updateLiveProgress(session);
// Connect SSE for real-time updates
connectSSE();
// Enable text selection for inline comments in live mode too
document.addEventListener('mouseup', handleTextSelection);
document.addEventListener('mousedown', (e) => {
if (!e.target.classList.contains('comment-btn')) {
removeCommentBtn();
}
});
}
function createLiveSectionCard(id, heading, order, status, markdown) {
const div = document.createElement('div');
div.className = `section live-${status}`;
div.dataset.sectionId = id;
div.dataset.order = order;
div.style.animationDelay = `${(order - 1) * 60}ms`;
if (status === 'pending' || !markdown) {
div.innerHTML = `
<div class="section-content" data-section-id="${id}">
<div class="section-header"><h2>${escapeHtml(heading)}</h2></div>
<div class="live-pending-placeholder">Waiting for agent to write this section...</div>
</div>`;
} else {
const rendered = marked.parse(stripLeadingHeading(stripFrontmatter(markdown)));
div.innerHTML = `
<div class="section-content" data-section-id="${id}">
<div class="section-header"><h2>${escapeHtml(heading)}</h2></div>
<div class="live-section-body">${rendered}</div>
</div>`;
}
if (status === 'review') {
addLiveActionButtons(div, id);
} else if (status === 'approved') {
addApprovedIndicator(div, id, heading);
} else if (status === 'changes_requested') {
addRevisingIndicator(div);
}
return div;
}
function addLiveActionButtons(sectionEl, sectionId) {
// Remove any existing action/feedback elements
sectionEl.querySelectorAll('.live-review-bar, .live-approved-indicator, .live-revising-indicator').forEach(el => el.remove());
const bar = document.createElement('div');
bar.className = 'live-review-bar';
bar.id = `live-review-bar-${sectionId}`;
bar.innerHTML = `
<div class="live-review-summary" id="live-review-summary-${sectionId}"></div>
<textarea class="live-review-note" rows="2" placeholder="Additional notes (optional)..."></textarea>
<div class="live-review-actions">
<button class="live-approve-btn" onclick="submitLiveFeedback('${sectionId}', 'approve')">Approve</button>
<button class="live-changes-btn" onclick="submitLiveFeedback('${sectionId}', 'request_changes')">Request Changes</button>
</div>
`;
sectionEl.appendChild(bar);
// Initialize comment count
updateLiveReviewSummary(sectionId);
}
function addApprovedIndicator(sectionEl, sectionId, heading) {
const content = sectionEl.querySelector('.section-content');
if (content) content.style.display = 'none';
const indicator = document.createElement('div');
indicator.className = 'live-approved-indicator';
const headingText = heading || sectionEl.querySelector('h2')?.textContent || sectionId;
indicator.innerHTML = `<span class="chevron"></span> <span>${escapeHtml(headingText)}</span> — Approved`;
indicator.addEventListener('click', () => {
sectionEl.classList.toggle('expanded');
if (content) content.style.display = sectionEl.classList.contains('expanded') ? '' : 'none';
});
sectionEl.insertBefore(indicator, sectionEl.firstChild);
}
function addRevisingIndicator(sectionEl) {
const indicator = document.createElement('div');
indicator.className = 'live-revising-indicator';
indicator.innerHTML = `<div class="live-spinner"></div> Agent is revising this section...`;
sectionEl.appendChild(indicator);
}
function updateLiveReviewSummary(sectionId) {
const summary = document.getElementById(`live-review-summary-${sectionId}`);
if (!summary) return;
const count = inlineComments.filter(c => c.sectionId === sectionId).length;
if (count > 0) {
summary.textContent = `${count} inline comment${count !== 1 ? 's' : ''}`;
summary.classList.add('has-comments');
} else {
summary.textContent = '';
summary.classList.remove('has-comments');
}
}
async function submitLiveFeedback(sectionId, action) {
// Gather inline comments for this section
const sectionComments = inlineComments
.filter(c => c.sectionId === sectionId)
.map(c => ({ selected_text: c.selectedText, comment: c.comment }));
const bar = document.getElementById(`live-review-bar-${sectionId}`);
const feedbackText = bar?.querySelector('.live-review-note')?.value?.trim() || '';
// For request_changes: need at least inline comments or textarea text
if (action === 'request_changes' && sectionComments.length === 0 && !feedbackText) {
if (bar) {
bar.classList.remove('needs-input');
// Force reflow to restart animation
void bar.offsetWidth;
bar.classList.add('needs-input');
bar.querySelector('.live-review-note')?.focus();
// Remove class after animation completes
setTimeout(() => bar.classList.remove('needs-input'), 400);
}
return;
}
try {
const resp = await fetch('/api/section/feedback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
section: sectionId,
action: action,
text: feedbackText,
inline_comments: sectionComments
})
});
if (resp.ok) {
// Optimistic UI update (SSE will also arrive)
updateSectionStatus(sectionId, action === 'approve' ? 'approved' : 'changes_requested', feedbackText);
}
} catch (e) {
console.error('Failed to submit feedback:', e);
}
}
function updateSectionStatus(sectionId, status, feedbackText) {
const sectionEl = document.querySelector(`.section[data-section-id="${sectionId}"]`);
if (!sectionEl) return;
// Reset classes
sectionEl.className = 'section';
sectionEl.classList.add(`live-${status}`);
// Remove old interactive elements
sectionEl.querySelectorAll('.live-review-bar, .live-approved-indicator, .live-revising-indicator, .live-user-feedback').forEach(el => el.remove());
const heading = sectionEl.querySelector('.section-content h2')?.textContent || sectionId;
if (status === 'approved') {
addApprovedIndicator(sectionEl, sectionId, heading);
} else if (status === 'changes_requested') {
if (feedbackText) {
const fb = document.createElement('div');
fb.className = 'live-user-feedback';
fb.innerHTML = `<div class="prev-label">Your Feedback</div><div class="feedback-text">${escapeHtml(feedbackText)}</div>`;
sectionEl.appendChild(fb);
}
addRevisingIndicator(sectionEl);
}
updateLiveProgressFromDOM();
}
function connectSSE() {
const evtSource = new EventSource('/events');
evtSource.addEventListener('section-added', (e) => {
const data = JSON.parse(e.data);
const existing = document.querySelector(`.section[data-section-id="${data.id}"]`);
if (existing) {
fillSectionContent(existing, data);
}
});
evtSource.addEventListener('section-updated', (e) => {
const data = JSON.parse(e.data);
const existing = document.querySelector(`.section[data-section-id="${data.id}"]`);
if (existing) {
fillSectionContent(existing, data);
}
});
evtSource.addEventListener('section-status', (e) => {
const data = JSON.parse(e.data);
// Only update if not already handled by optimistic update
const sectionEl = document.querySelector(`.section[data-section-id="${data.id}"]`);
if (sectionEl && !sectionEl.classList.contains(`live-${data.status}`)) {
updateSectionStatus(data.id, data.status, data.feedback?.text || '');
}
});
evtSource.addEventListener('ping', () => { /* keep-alive */ });
evtSource.onerror = () => {
// EventSource auto-reconnects; on reconnect the server replays current state
};
}
function fillSectionContent(sectionEl, data) {
// Reset classes
sectionEl.className = 'section';
sectionEl.classList.add(`live-${data.status}`);
// Remove old interactive elements
sectionEl.querySelectorAll('.live-review-bar, .live-approved-indicator, .live-revising-indicator, .live-user-feedback').forEach(el => el.remove());
// Render content (strip leading heading — already shown in card header)
const rendered = marked.parse(stripLeadingHeading(data.markdown || ''));
const content = sectionEl.querySelector('.section-content');
content.style.display = '';
content.innerHTML = `
<div class="section-header"><h2>${escapeHtml(data.heading)}</h2></div>
<div class="live-section-body">${rendered}</div>
`;
// Add action buttons if in review state
if (data.status === 'review') {
addLiveActionButtons(sectionEl, data.id);
}
// Scroll into view
sectionEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Process mermaid diagrams
const mermaidBlocks = content.querySelectorAll('pre code.language-mermaid');
if (mermaidBlocks.length > 0) {
mermaidBlocks.forEach(block => {
const pre = block.parentElement;
const div = document.createElement('div');
div.className = 'mermaid';
div.textContent = block.textContent;
pre.replaceWith(div);
});
mermaid.run();
}
// Syntax highlighting via Shiki for live sections
if (window.__shikiHighlight) {
window.__shikiHighlight(content);
}
updateLiveProgressFromDOM();
}
function updateLiveProgress(session) {
const sections = Object.values(session.sections);
const total = sections.length;
const approved = sections.filter(s => s.status === 'approved').length;
const fill = document.getElementById('live-progress-fill');
const text = document.getElementById('live-progress-text');
if (fill) fill.style.width = total > 0 ? `${(approved / total) * 100}%` : '0%';
if (text) text.textContent = `${approved} of ${total} sections approved`;
const btn = document.getElementById('live-finalize-btn');
if (btn) btn.disabled = approved < total;
}
function updateLiveProgressFromDOM() {
const all = document.querySelectorAll('.section[data-section-id]');
const total = all.length;
const approved = document.querySelectorAll('.section.live-approved').length;
const fill = document.getElementById('live-progress-fill');
const text = document.getElementById('live-progress-text');
if (fill) fill.style.width = total > 0 ? `${(approved / total) * 100}%` : '0%';
if (text) text.textContent = `${approved} of ${total} sections approved`;
const btn = document.getElementById('live-finalize-btn');
if (btn) btn.disabled = approved < total;
}
function finalizeRFC() {
const titleEl = document.getElementById('done-title');
const msgEl = document.getElementById('done-message');
titleEl.textContent = 'RFC Finalized';
msgEl.textContent = 'All sections have been approved. Return to your Claude Code session — Claude will assemble the final document.';
document.getElementById('done-overlay').classList.add('visible');
}
// --- Keyboard Shortcuts ---
document.addEventListener('keydown', (e) => {
// Escape: close any open popover or comment button
if (e.key === 'Escape') {
closeAllPopovers();
removeCommentBtn();
// Close the done overlay if visible
const overlay = document.getElementById('done-overlay');
if (overlay && overlay.classList.contains('visible')) {
overlay.classList.remove('visible');
}
return;
}
// Cmd/Ctrl+Enter: submit feedback (batch mode) or approve current live section
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
// If a comment popover is open and focused, submit the comment
const popover = document.querySelector('.comment-popover');
if (popover) {
const saveBtn = popover.querySelector('.comment-save-btn');
if (saveBtn) {
e.preventDefault();
saveBtn.click();
return;
}
}
// If typing in a textarea, don't trigger global submit
if (e.target.tagName === 'TEXTAREA' && !e.target.closest('.comment-popover')) {
// In live mode, find the parent section and submit
if (LIVE_MODE) {
const reviewBar = e.target.closest('.live-review-bar');
if (reviewBar) {
e.preventDefault();
const approveBtn = reviewBar.querySelector('.live-approve-btn');
if (approveBtn) approveBtn.click();
return;
}
}
}
// In batch mode, submit feedback
if (!LIVE_MODE) {
e.preventDefault();
submitFeedback('needs_revision');
}
}
});
</script>
</body>
</html>
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "dev-rfc",
"dependencies": {
"open": "^10.1.0",
},
"devDependencies": {
"@types/bun": "^1.2.0",
},
},
},
"packages": {
"@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
"@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
"bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
"default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="],
"default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="],
"define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
"is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
"is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
"is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
"open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
"run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="],
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
}
}
{
"version": "1.0.1",
"organization": "dot-skills",
"technology": "Technical Writing",
"date": "May 2026",
"abstract": "Creates well-structured RFCs and technical proposals for software projects, covering both new-system proposals and architecture documentation."
}
{
"name": "dev-rfc",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "bun run scripts/generate_review.ts",
"test": "bun test"
},
"dependencies": {
"open": "^10.1.0"
},
"devDependencies": {
"@types/bun": "^1.2.0"
}
}
RFC Reference Templates
Three templates for three scales. Most real RFCs blend proposal and architecture elements — use the RFC template as the backbone when proposing changes, and pull in Architecture Doc sections for the technical deep dive.
---
Template A: RFC (Technical Proposal)
For pre-build alignment. Answers: "Should we build this, and how?"
# [Project Name] RFC
| Field | Value |
|-------|-------|
| **Author(s)** | [names] |
| **Approver(s)** | [names — who needs to sign off] |
| **Status** | Draft · In Review · Approved · Superseded · Deprecated |
| **Created** | [date] |
| **Last Updated** | [date] |
| **Team** | [owning team] |
---
## Abstract
[3-5 sentence executive summary. State what the RFC proposes, why it matters,
and the key trade-off or insight that makes this proposal non-obvious.
A reader should be able to decide whether to read the full RFC from this alone.]
---
## Motivation
[Why are we doing this? Ground in concrete data or incidents —
"Users experience 3s load times on the dashboard" not "The page is slow."
Include links to metrics, incident reports, or user feedback.]
---
## Goals and Non-Goals
**Goals:**
- [Specific, measurable. "P99 latency under 200ms" not "fast."]
- [Each goal should be verifiable after the project ships.]
**Non-Goals:**
- [What this project will NOT do. Prevents scope creep.]
- [If a non-goal is planned for later, say so.]
---
## Approaches
Present all evaluated options together. Each approach should be given a fair treatment with genuine pros and cons.
### Approach 1: [Name]
**Description:** [How this approach works at a high level.]
**Architecture:**
+------------------+ +------------------+ +------------------+ | Component A |---->| Component B |---->| Component C | | (description) | | (description) | | (description) | +------------------+ +------------------+ +------------------+
**Pros:**
- [Genuine advantage]
- [Genuine advantage]
**Cons:**
- [Honest trade-off]
- [Honest trade-off]
**Estimated Effort:** [T-shirt size or person-weeks]
### Approach 2: [Name]
[Same structure as Approach 1.]
### Approach 3: [Name] (if applicable)
[Same structure.]
### Recommendation
**Chosen approach:** [Name]
**Justification:** [Why this approach wins given the specific goals, constraints,
and trade-offs. Acknowledge what you're giving up relative to the alternatives.]
---
## Detailed Design
Deep dive on the recommended approach only.
### Architecture Changes
[Diagram showing the proposed architecture. Mark new components with `[NEW]`
and changed ones with `[CHANGED]`.]
+---------+ +-------------+ +-----------+ | Client |--->| Gateway |--->| Service A | +---------+ | [NEW] | | [CHANGED] | +------+------+ +-----------+ | +------+------+ | Service B | | [NEW] | +-------------+
### API / Interface Design
[Endpoints, request/response schemas, or function signatures.
Include examples of key request/response payloads.]
### Data Model
[Schema, entity relationships, key fields. Call out denormalization,
indexing strategy, and any migration required.]
---
## Service SLAs
| Metric | Target |
|--------|--------|
| **Availability** | [e.g., 99.95%] |
| **Latency P50** | [e.g., 50ms] |
| **Latency P95** | [e.g., 150ms] |
| **Latency P99** | [e.g., 300ms] |
| **Throughput** | [e.g., 10k RPS] |
| **Error Budget** | [e.g., 0.05% / month] |
[Justify targets based on user expectations and upstream/downstream constraints.]
---
## Service Dependencies
| Dependency | Type | Failure Mode | Mitigation |
|------------|------|-------------|------------|
| [Service/DB] | [sync/async] | [What happens if it's down] | [Circuit breaker, cache, fallback] |
[Call out any new dependencies introduced and their SLA impact.]
---
## Security Considerations
- **Authentication:** [How users/services authenticate]
- **Authorization:** [Access control model, permission boundaries]
- **Data Protection:** [Encryption at rest/transit, sensitive data handling]
- **Threat Model:** [Key attack vectors considered and mitigations]
---
## Testing & Rollout
### Test Plan
- **Unit tests**: [Key behaviors to cover]
- **Integration tests**: [Cross-component interactions]
- **Load tests**: [Performance targets and methodology]
### Rollout Plan
- **Phase 1**: [Feature flags, shadow mode, or canary deployment]
- **Phase 2**: [Gradual rollout — percentage, metrics to watch]
- **Phase 3**: [Full rollout, cleanup of old code paths]
### Rollback Plan
[How to revert if something goes wrong. Include specific steps and decision criteria.]
### Data Migration
[If applicable — how existing data is transformed, backfill strategy, rollback for data changes.]
---
## Observability
- **Key Metrics**: [What to measure — latency, error rates, throughput, business metrics]
- **Dashboards**: [What dashboards to create or update]
- **Alerts**: [What triggers pages vs. warnings, thresholds]
- **Tracing**: [Distributed tracing spans, correlation IDs]
---
## Open Questions
1. [Question — include context and your leaning]
2. [Question]
---
## References
- [Related RFCs, prior art, dashboards, incident reports]---
Domain Addenda
Include the relevant addendum sections based on the system type. These go after the core RFC sections.
Backend / Infrastructure Addendum
## Load & Performance Testing
[Load test methodology, target throughput, stress test scenarios.
Include baseline comparisons.]
## Multi-Region / Data Center
[Replication strategy, consistency model, failover behavior.
How does the system behave during a region outage?]
## Cost Projections
| Resource | Current | Projected | Delta |
|----------|---------|-----------|-------|
| [Compute/Storage/Network] | [$X/mo] | [$Y/mo] | [+$Z/mo] |
[Cost at 1x, 5x, 10x current scale.]
## Customer Support Impact
[New support workflows, runbooks, escalation paths needed.]Frontend / Mobile Addendum
## UI & UX
[Wireframes or mockups. User flows. Interaction patterns.
Link to Figma/design files.]
## Network Interactions
[API calls, WebSocket connections, polling behavior.
Offline handling, retry strategies.]
## Library Dependencies
| Library | Version | Size | License | Justification |
|---------|---------|------|---------|--------------|
| [name] | [ver] | [KB] | [MIT/etc] | [why needed] |
## Performance Budget
| Metric | Budget | Current |
|--------|--------|---------|
| Bundle size (gzipped) | [KB] | [KB] |
| First Contentful Paint | [ms] | [ms] |
| Largest Contentful Paint | [ms] | [ms] |
| Cumulative Layout Shift | [score] | [score] |
| Interaction to Next Paint | [ms] | [ms] |
## Accessibility
[WCAG target level (A/AA/AAA). Keyboard navigation plan.
Screen reader support. Color contrast. Focus management.]
## Analytics
[Events to track, instrumentation plan, A/B test setup.]
## Customer Support Impact
[New support workflows, FAQ updates, known-issue documentation.]Data / ML Addendum
## Data Pipeline Architecture
[Pipeline DAG, scheduling, data sources and sinks.
Processing guarantees (at-least-once, exactly-once).]
## Data Quality
[Validation rules, monitoring, anomaly detection.
Data freshness SLAs, schema evolution strategy.]
## Privacy & Compliance
[PII handling, data retention policies, GDPR/CCPA compliance.
Right to deletion implementation, audit logging.]
## Model Performance
| Metric | Baseline | Target |
|--------|----------|--------|
| [Accuracy/Precision/Recall/F1] | [X%] | [Y%] |
| Inference latency P95 | [ms] | [ms] |
| Model size | [MB] | [MB] |
[Training data requirements, retraining cadence, drift detection.]
## Experimentation Plan
[A/B test design, holdout groups, success criteria,
minimum detectable effect, experiment duration.]---
Template B: Architecture Document
For documenting built systems. Answers: "How does this system work?"
# [Project Name] Architecture Document
| Field | Value |
|-------|-------|
| **Author(s)** | [names] |
| **Last Updated** | [date] |
| **Status** | Current · Outdated · Deprecated |
[1-2 sentence summary. Link to README for build/usage info.]
---
## High-Level Architecture
[Pick the diagram style that matches your system.]
### Pipeline (compilers, data pipelines, ETL)+-------------+ +-------------+ +-------------+ | STAGE 1 |---->| STAGE 2 |---->| STAGE 3 | | src/stage1/ | | src/stage2/ | | src/stage3/ | +-------------+ +-------------+ +-------------+ Input Intermediate Output
### Request Flow (services, APIs)Client --> [Router] --> [Auth] --> [Handler] --> [DB] | +--> [External Service]
### Layer Diagram (web apps, CRUD)+--------------------------------------------+ | UI Layer (src/components/, pages/) | +--------------------------------------------+ | API Layer (src/api/, routes/) | +--------------------------------------------+ | Service Layer (src/services/) | +--------------------------------------------+ | Data Layer (src/models/, repos/) | +--------------------------------------------+
---
## Source Tree
src/ subsystem-a/ What subsystem A does module1/ What module1 does subsystem-b/ What subsystem B does shared/ Shared types, utilities
---
## Data Flow
[Concrete types/interfaces between phases.]
InputType --> module::method() --> IntermediateType --> module::method() --> OutputType
---
## Key Design Decisions
- **Decision 1**: What was chosen and rationale.
- **Decision 2**: What was chosen and rationale.
---
## Design Philosophy
- **Principle.** How it manifests in the codebase.
---
## Sub-System Architecture
### Subsystem A
[Internal architecture, own diagrams if needed. Comparison table for variants.]
---
## Module Documentation
| Module | README |
|--------|--------|
| Subsystem A | [`src/subsystem-a/README.md`](src/subsystem-a/README.md) |---
Template C: One-Pager
For small changes (< 1 week, single component). Keep it under 1 page.
# [Change Name]
| Field | Value |
|-------|-------|
| **Author** | [name] |
| **Status** | Draft · Approved |
| **Date** | [date] |
## Problem
[2-3 sentences. What's broken or missing, and why it matters.]
## Proposed Solution
[What you'll do. 1-2 paragraphs + optional diagram.]
## Rollout
- **How:** [Feature flag / direct deploy / migration script]
- **Rollback:** [How to revert if it breaks]
- **Risks:** [Anything non-obvious, or "None — isolated change"]---
Scaling Guidance
| Change size | Format | RFC sections | Architecture Doc sections |
|---|---|---|---|
| Small (< 1 week) | One-pager (Template C) | Problem, Solution, Rollout | Skip |
| Medium (1-4 weeks) | Standard RFC | All core sections | Architecture in Detailed Design |
| Large (> 1 month, cross-team) | Full RFC + sub-docs | All sections + domain addenda | Full Architecture sections, linked sub-docs |
| Retrospective (documenting what exists) | Architecture doc | Skip — use Template B | All sections |
Related skills
FAQ
What does dev-rfc do?
dev-rfc: A skill for development.
When should I use dev-rfc?
When you need to use dev-rfc for development tasks, or when dev-rfc: a skill for development.
What are the main capabilities?
dev-rfc.