[{"data":1,"prerenderedAt":3553},["ShallowReactive",2],{"post-python-virtual-environments":3,"rel-python-virtual-environments":322,"sib-python-virtual-environments":3445},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"layout":11,"subtitle":12,"image":13,"optimized_image":13,"category":14,"tags":15,"author":17,"paginate":6,"body":18,"_type":316,"_id":317,"_source":318,"_file":319,"_stem":320,"_extension":321},"/posts/python-virtual-environments","posts",false,"","Python Virtual Environments (venv)","A Comprehensive Guide to Python Virtual Environments (venv)","2023-09-17 22:00:00","post","A Comprehensive Guide","/assets/img/uploads/python-image.jpg","code",[16,14],"python","jaimedearcos",{"type":19,"children":20,"toc":310},"root",[21,29,36,41,61,67,72,77,230,236,241,270,275,280,299,304],{"type":22,"tag":23,"props":24,"children":25},"element","p",{},[26],{"type":27,"value":28},"text","In the world of Python development, managing dependencies can be a challenging task. You might encounter situations where different projects require different versions of the same library, or you simply want to isolate your project's environment to ensure consistency. That's where Python's venv comes into play. In this tutorial, we'll explore the advantages of using venv in your Python projects, guide you through its installation using pip, and provide an example of how to install dependencies and execute a Python script within your virtual environment.",{"type":22,"tag":30,"props":31,"children":33},"h2",{"id":32},"advantages-of-using-venv",[34],{"type":27,"value":35},"Advantages of Using venv",{"type":22,"tag":23,"props":37,"children":38},{},[39],{"type":27,"value":40},"Python's venv module, introduced in Python 3.3, allows you to create isolated Python environments for your projects. Here are some of the key advantages:",{"type":22,"tag":42,"props":43,"children":44},"ul",{},[45,51,56],{"type":22,"tag":46,"props":47,"children":48},"li",{},[49],{"type":27,"value":50},"Dependency Isolation: venv allows you to create a self-contained environment for each project. This means you can have different versions of Python and project-specific libraries without conflicts.",{"type":22,"tag":46,"props":52,"children":53},{},[54],{"type":27,"value":55},"Version Control Integration: Including your virtual environment in version control (e.g., Git) ensures that others can recreate your development environment precisely, making collaboration smoother.",{"type":22,"tag":46,"props":57,"children":58},{},[59],{"type":27,"value":60},"Easy Cleanup: Virtual environments are easy to create and delete, so you can keep your system clean by removing unused environments when you're done with a project.",{"type":22,"tag":30,"props":62,"children":64},{"id":63},"installing-venv-with-pip",[65],{"type":27,"value":66},"Installing venv with pip",{"type":22,"tag":23,"props":68,"children":69},{},[70],{"type":27,"value":71},"Before you can create virtual environments with venv, make sure you have Python 3.3 or higher installed on your system. Most modern Python installations come with venv included, so you don't need to install it separately.",{"type":22,"tag":23,"props":73,"children":74},{},[75],{"type":27,"value":76},"To create a new virtual environment, follow these steps:",{"type":22,"tag":78,"props":79,"children":80},"ol",{},[81,86,91,140],{"type":22,"tag":46,"props":82,"children":83},{},[84],{"type":27,"value":85},"Open your terminal or command prompt.",{"type":22,"tag":46,"props":87,"children":88},{},[89],{"type":27,"value":90},"Navigate to the directory where you want to create the virtual environment.",{"type":22,"tag":46,"props":92,"children":93},{},[94,96],{"type":27,"value":95},"Run the following command to create a new virtual environment named \"myenv\" (you can replace \"myenv\" with your preferred name):",{"type":22,"tag":97,"props":98,"children":102},"pre",{"className":99,"code":100,"language":101,"meta":7,"style":7},"language-bash shiki shiki-themes github-dark github-light","$ python -m venv myenv\n","bash",[103],{"type":22,"tag":14,"props":104,"children":105},{"__ignoreMap":7},[106],{"type":22,"tag":107,"props":108,"children":111},"span",{"class":109,"line":110},"line",1,[112,118,124,130,135],{"type":22,"tag":107,"props":113,"children":115},{"style":114},"--shiki-default:#B392F0;--shiki-light:#6F42C1",[116],{"type":27,"value":117},"$",{"type":22,"tag":107,"props":119,"children":121},{"style":120},"--shiki-default:#9ECBFF;--shiki-light:#032F62",[122],{"type":27,"value":123}," python",{"type":22,"tag":107,"props":125,"children":127},{"style":126},"--shiki-default:#79B8FF;--shiki-light:#005CC5",[128],{"type":27,"value":129}," -m",{"type":22,"tag":107,"props":131,"children":132},{"style":120},[133],{"type":27,"value":134}," venv",{"type":22,"tag":107,"props":136,"children":137},{"style":120},[138],{"type":27,"value":139}," myenv\n",{"type":22,"tag":46,"props":141,"children":142},{},[143,145,153,192,200,224,228],{"type":27,"value":144},"To activate the virtual environment, use the appropriate command for your operating system:",{"type":22,"tag":42,"props":146,"children":147},{},[148],{"type":22,"tag":46,"props":149,"children":150},{},[151],{"type":27,"value":152},"On Windows:",{"type":22,"tag":97,"props":154,"children":156},{"className":99,"code":155,"language":101,"meta":7,"style":7},"$ myenv\\Scripts\\activate\n",[157],{"type":22,"tag":14,"props":158,"children":159},{"__ignoreMap":7},[160],{"type":22,"tag":107,"props":161,"children":162},{"class":109,"line":110},[163,167,172,177,182,187],{"type":22,"tag":107,"props":164,"children":165},{"style":114},[166],{"type":27,"value":117},{"type":22,"tag":107,"props":168,"children":169},{"style":120},[170],{"type":27,"value":171}," myenv",{"type":22,"tag":107,"props":173,"children":174},{"style":126},[175],{"type":27,"value":176},"\\S",{"type":22,"tag":107,"props":178,"children":179},{"style":120},[180],{"type":27,"value":181},"cripts",{"type":22,"tag":107,"props":183,"children":184},{"style":126},[185],{"type":27,"value":186},"\\a",{"type":22,"tag":107,"props":188,"children":189},{"style":120},[190],{"type":27,"value":191},"ctivate\n",{"type":22,"tag":42,"props":193,"children":194},{},[195],{"type":22,"tag":46,"props":196,"children":197},{},[198],{"type":27,"value":199},"On macOS and Linux:",{"type":22,"tag":97,"props":201,"children":203},{"className":99,"code":202,"language":101,"meta":7,"style":7},"$ source myenv/bin/activate\n",[204],{"type":22,"tag":14,"props":205,"children":206},{"__ignoreMap":7},[207],{"type":22,"tag":107,"props":208,"children":209},{"class":109,"line":110},[210,214,219],{"type":22,"tag":107,"props":211,"children":212},{"style":114},[213],{"type":27,"value":117},{"type":22,"tag":107,"props":215,"children":216},{"style":120},[217],{"type":27,"value":218}," source",{"type":22,"tag":107,"props":220,"children":221},{"style":120},[222],{"type":27,"value":223}," myenv/bin/activate\n",{"type":22,"tag":225,"props":226,"children":227},"br",{},[],{"type":27,"value":229},"Once activated, your terminal prompt will change, indicating that you are now working within the virtual environment.",{"type":22,"tag":30,"props":231,"children":233},{"id":232},"installing-dependencies-and-running-a-python-script",[234],{"type":27,"value":235},"Installing Dependencies and Running a Python Script",{"type":22,"tag":23,"props":237,"children":238},{},[239],{"type":27,"value":240},"Now that you're inside your virtual environment, you can install project-specific dependencies using pip. For example, let's install the popular library requests:",{"type":22,"tag":97,"props":242,"children":244},{"className":99,"code":243,"language":101,"meta":7,"style":7},"$ pip install requests\n",[245],{"type":22,"tag":14,"props":246,"children":247},{"__ignoreMap":7},[248],{"type":22,"tag":107,"props":249,"children":250},{"class":109,"line":110},[251,255,260,265],{"type":22,"tag":107,"props":252,"children":253},{"style":114},[254],{"type":27,"value":117},{"type":22,"tag":107,"props":256,"children":257},{"style":120},[258],{"type":27,"value":259}," pip",{"type":22,"tag":107,"props":261,"children":262},{"style":120},[263],{"type":27,"value":264}," install",{"type":22,"tag":107,"props":266,"children":267},{"style":120},[268],{"type":27,"value":269}," requests\n",{"type":22,"tag":23,"props":271,"children":272},{},[273],{"type":27,"value":274},"With your dependencies installed, you can create a Python script or use an existing one within the virtual environment. To run your script, simply execute it as you normally would. All the installed packages will be available for your script to use without affecting the global Python environment.",{"type":22,"tag":23,"props":276,"children":277},{},[278],{"type":27,"value":279},"When you're done working on your project, you can deactivate the virtual environment with the following command:",{"type":22,"tag":97,"props":281,"children":283},{"className":99,"code":282,"language":101,"meta":7,"style":7},"$ deactivate\n",[284],{"type":22,"tag":14,"props":285,"children":286},{"__ignoreMap":7},[287],{"type":22,"tag":107,"props":288,"children":289},{"class":109,"line":110},[290,294],{"type":22,"tag":107,"props":291,"children":292},{"style":114},[293],{"type":27,"value":117},{"type":22,"tag":107,"props":295,"children":296},{"style":120},[297],{"type":27,"value":298}," deactivate\n",{"type":22,"tag":23,"props":300,"children":301},{},[302],{"type":27,"value":303},"And that's it! You've successfully set up and used a virtual environment with venv in Python. This tool is invaluable for managing dependencies and keeping your projects organized. Incorporate venv into your Python development workflow to take full advantage of its benefits.",{"type":22,"tag":305,"props":306,"children":307},"style",{},[308],{"type":27,"value":309},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}",{"title":7,"searchDepth":311,"depth":311,"links":312},2,[313,314,315],{"id":32,"depth":311,"text":35},{"id":63,"depth":311,"text":66},{"id":232,"depth":311,"text":235},"markdown","content:posts:python-virtual-environments.md","content","posts/python-virtual-environments.md","posts/python-virtual-environments","md",[323,882,2043],{"_path":324,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":325,"description":326,"layout":11,"date":327,"subtitle":328,"image":329,"optimized_image":329,"category":14,"tags":330,"author":17,"paginate":6,"body":333,"_type":316,"_id":879,"_source":318,"_file":880,"_stem":881,"_extension":321},"/posts/chat-with-pdfs-locally-part-1","Part 1/3: Chat with Your PDFs Locally","Build a local PDF chat app that extracts text, indexes it in a vector database, and answers questions with document, section, and page references. Includes runnable code and a Makefile.","2025-12-15T11:00:00.000Z","Build a small RAG app with Ollama, Chroma, and source-aware answers","/assets/img/uploads/local-pdf-chat-part-one.jpg",[14,331,16,332],"ai","architecture",{"type":19,"children":334,"toc":871},[335,346,368,374,431,465,470,483,489,523,625,677,712,718,723,771,784,789,794,800,805,810,815,821,833,838,843,849,854,867],{"type":22,"tag":23,"props":336,"children":337},{},[338,340],{"type":27,"value":339},"I have plenty of PDFs that are easy to store and surprisingly hard to use. The answer to a question might be one paragraph inside a long guide, but finding it means remembering the right filename, opening it, and searching for the words the author happened to use. A chat box feels like a natural interface. The difficult part is making its answer traceable: ",{"type":22,"tag":341,"props":342,"children":343},"em",{},[344],{"type":27,"value":345},"which document, which section, and which page support this claim?",{"type":22,"tag":23,"props":347,"children":348},{},[349,351,366],{"type":27,"value":350},"This is the first post in a three-part series. We will build a small, fully local app that accepts PDFs with selectable text, answers questions about them, and shows the source excerpts. ",{"type":22,"tag":352,"props":353,"children":357},"a",{"href":354,"rel":355},"https://github.com/JaimeDeArcos/document-manager",[356],"nofollow",[358,360],{"type":27,"value":359},"The complete Python project, README, and Makefile live in ",{"type":22,"tag":14,"props":361,"children":363},{"className":362},[],[364],{"type":27,"value":365},"document-manager",{"type":27,"value":367},". Part 2 will examine retrieval mistakes and how to evaluate answers; Part 3 will add OCR for scanned PDFs. The numbered titles are deliberate: each post extends the same working example.",{"type":22,"tag":30,"props":369,"children":371},{"id":370},"what-we-are-building",[372],{"type":27,"value":373},"What we are building",{"type":22,"tag":23,"props":375,"children":376},{},[377,379,386,388,395,397,404,406,413,415,421,423,429],{"type":27,"value":378},"The interface is a ",{"type":22,"tag":352,"props":380,"children":383},{"href":381,"rel":382},"https://docs.streamlit.io/develop/tutorials/chat-and-llm-apps/build-conversational-apps",[356],[384],{"type":27,"value":385},"Streamlit",{"type":27,"value":387}," chat. ",{"type":22,"tag":352,"props":389,"children":392},{"href":390,"rel":391},"https://pymupdf.readthedocs.io/en/latest/pymupdf4llm/",[356],[393],{"type":27,"value":394},"PyMuPDF4LLM",{"type":27,"value":396}," extracts text by page. ",{"type":22,"tag":352,"props":398,"children":401},{"href":399,"rel":400},"https://docs.trychroma.com/reference/python",[356],[402],{"type":27,"value":403},"Chroma",{"type":27,"value":405}," stores text chunks, their metadata, and vectors on disk. ",{"type":22,"tag":352,"props":407,"children":410},{"href":408,"rel":409},"https://ollama.com/download",[356],[411],{"type":27,"value":412},"Ollama",{"type":27,"value":414}," runs two local models: ",{"type":22,"tag":14,"props":416,"children":418},{"className":417},[],[419],{"type":27,"value":420},"embeddinggemma",{"type":27,"value":422}," to turn text into vectors and ",{"type":22,"tag":14,"props":424,"children":426},{"className":425},[],[427],{"type":27,"value":428},"gemma3:4b",{"type":27,"value":430}," to compose an answer from the retrieved passages.",{"type":22,"tag":97,"props":432,"children":435},{"className":433,"code":434,"language":27,"meta":7,"style":7},"language-text shiki shiki-themes github-dark github-light","PDF → extract text → split into chunks → embed → Chroma\n\nquestion → embed → retrieve nearby chunks → answer → show sources\n",[436],{"type":22,"tag":14,"props":437,"children":438},{"__ignoreMap":7},[439,447,456],{"type":22,"tag":107,"props":440,"children":441},{"class":109,"line":110},[442],{"type":22,"tag":107,"props":443,"children":444},{},[445],{"type":27,"value":446},"PDF → extract text → split into chunks → embed → Chroma\n",{"type":22,"tag":107,"props":448,"children":449},{"class":109,"line":311},[450],{"type":22,"tag":107,"props":451,"children":453},{"emptyLinePlaceholder":452},true,[454],{"type":27,"value":455},"\n",{"type":22,"tag":107,"props":457,"children":459},{"class":109,"line":458},3,[460],{"type":22,"tag":107,"props":461,"children":462},{},[463],{"type":27,"value":464},"question → embed → retrieve nearby chunks → answer → show sources\n",{"type":22,"tag":23,"props":466,"children":467},{},[468],{"type":27,"value":469},"This is retrieval-augmented generation, or RAG. The language model does not read every PDF on each question. We first select likely passages and supply only those passages as context. That makes the system more useful, but a vector search result is merely a candidate. It may be the closest passage while still being irrelevant. That distinction matters whenever the answer is uncertain.",{"type":22,"tag":23,"props":471,"children":472},{},[473,475,481],{"type":27,"value":474},"The app runs for one user on one machine. Uploaded PDFs and the vector index stay in its ",{"type":22,"tag":14,"props":476,"children":478},{"className":477},[],[479],{"type":27,"value":480},"data/",{"type":27,"value":482}," directory; the browser app binds to localhost. Installation and model downloads need internet access, but ordinary questions use the local Ollama service. There is no cloud API key or separate vector database server.",{"type":22,"tag":30,"props":484,"children":486},{"id":485},"run-the-example",[487],{"type":27,"value":488},"Run the example",{"type":22,"tag":23,"props":490,"children":491},{},[492,494,505,507,513,515,521],{"type":27,"value":493},"Install Python 3.11 or newer, ",{"type":22,"tag":352,"props":495,"children":498},{"href":496,"rel":497},"https://docs.astral.sh/uv/getting-started/installation/",[356],[499],{"type":22,"tag":14,"props":500,"children":502},{"className":501},[],[503],{"type":27,"value":504},"uv",{"type":27,"value":506},", Ollama, and ",{"type":22,"tag":14,"props":508,"children":510},{"className":509},[],[511],{"type":27,"value":512},"make",{"type":27,"value":514},". Start the Ollama app, or run ",{"type":22,"tag":14,"props":516,"children":518},{"className":517},[],[519],{"type":27,"value":520},"ollama serve",{"type":27,"value":522}," in a separate terminal. Then:",{"type":22,"tag":97,"props":524,"children":526},{"className":99,"code":525,"language":101,"meta":7,"style":7},"git clone https://github.com/JaimeDeArcos/document-manager.git\ncd document-manager\nmake setup\nmake models\nmake doctor\nmake demo-pdf\nmake run\n",[527],{"type":22,"tag":14,"props":528,"children":529},{"__ignoreMap":7},[530,548,561,573,586,599,612],{"type":22,"tag":107,"props":531,"children":532},{"class":109,"line":110},[533,538,543],{"type":22,"tag":107,"props":534,"children":535},{"style":114},[536],{"type":27,"value":537},"git",{"type":22,"tag":107,"props":539,"children":540},{"style":120},[541],{"type":27,"value":542}," clone",{"type":22,"tag":107,"props":544,"children":545},{"style":120},[546],{"type":27,"value":547}," https://github.com/JaimeDeArcos/document-manager.git\n",{"type":22,"tag":107,"props":549,"children":550},{"class":109,"line":311},[551,556],{"type":22,"tag":107,"props":552,"children":553},{"style":126},[554],{"type":27,"value":555},"cd",{"type":22,"tag":107,"props":557,"children":558},{"style":120},[559],{"type":27,"value":560}," document-manager\n",{"type":22,"tag":107,"props":562,"children":563},{"class":109,"line":458},[564,568],{"type":22,"tag":107,"props":565,"children":566},{"style":114},[567],{"type":27,"value":512},{"type":22,"tag":107,"props":569,"children":570},{"style":120},[571],{"type":27,"value":572}," setup\n",{"type":22,"tag":107,"props":574,"children":576},{"class":109,"line":575},4,[577,581],{"type":22,"tag":107,"props":578,"children":579},{"style":114},[580],{"type":27,"value":512},{"type":22,"tag":107,"props":582,"children":583},{"style":120},[584],{"type":27,"value":585}," models\n",{"type":22,"tag":107,"props":587,"children":589},{"class":109,"line":588},5,[590,594],{"type":22,"tag":107,"props":591,"children":592},{"style":114},[593],{"type":27,"value":512},{"type":22,"tag":107,"props":595,"children":596},{"style":120},[597],{"type":27,"value":598}," doctor\n",{"type":22,"tag":107,"props":600,"children":602},{"class":109,"line":601},6,[603,607],{"type":22,"tag":107,"props":604,"children":605},{"style":114},[606],{"type":27,"value":512},{"type":22,"tag":107,"props":608,"children":609},{"style":120},[610],{"type":27,"value":611}," demo-pdf\n",{"type":22,"tag":107,"props":613,"children":615},{"class":109,"line":614},7,[616,620],{"type":22,"tag":107,"props":617,"children":618},{"style":114},[619],{"type":27,"value":512},{"type":22,"tag":107,"props":621,"children":622},{"style":120},[623],{"type":27,"value":624}," run\n",{"type":22,"tag":23,"props":626,"children":627},{},[628,634,636,642,644,650,652,658,660,666,668,675],{"type":22,"tag":14,"props":629,"children":631},{"className":630},[],[632],{"type":27,"value":633},"make setup",{"type":27,"value":635}," installs the locked Python dependencies. ",{"type":22,"tag":14,"props":637,"children":639},{"className":638},[],[640],{"type":27,"value":641},"make models",{"type":27,"value":643}," pulls both models into Ollama; this is the largest download. ",{"type":22,"tag":14,"props":645,"children":647},{"className":646},[],[648],{"type":27,"value":649},"make doctor",{"type":27,"value":651}," checks that the service and models are available. ",{"type":22,"tag":14,"props":653,"children":655},{"className":654},[],[656],{"type":27,"value":657},"make run",{"type":27,"value":659}," opens the app at ",{"type":22,"tag":14,"props":661,"children":663},{"className":662},[],[664],{"type":27,"value":665},"http://127.0.0.1:8501",{"type":27,"value":667},". The ",{"type":22,"tag":352,"props":669,"children":672},{"href":670,"rel":671},"https://github.com/JaimeDeArcos/document-manager#readme",[356],[673],{"type":27,"value":674},"README",{"type":27,"value":676}," documents ports, environment variables, and troubleshooting.",{"type":22,"tag":23,"props":678,"children":679},{},[680,682,688,690,696,698,703,705,710],{"type":27,"value":681},"Upload ",{"type":22,"tag":14,"props":683,"children":685},{"className":684},[],[686],{"type":27,"value":687},"data/demo-handbook.pdf",{"type":27,"value":689}," in the sidebar and ask: ",{"type":22,"tag":691,"props":692,"children":693},"strong",{},[694],{"type":27,"value":695},"How long are project notes kept?",{"type":27,"value":697}," The sample PDF says 90 days in its ",{"type":22,"tag":341,"props":699,"children":700},{},[701],{"type":27,"value":702},"Retention",{"type":27,"value":704}," section on page 1. The response should show that reference and an excerpt. Then ask ",{"type":22,"tag":691,"props":706,"children":707},{},[708],{"type":27,"value":709},"What is the holiday policy?",{"type":27,"value":711}," There is no supported answer in that document, so the intended behavior is to say so. A local model can still make a mistake; check any cited excerpt rather than assuming this guardrail is infallible. You can also upload your own text-based PDFs, up to 25 MB each.",{"type":22,"tag":30,"props":713,"children":715},{"id":714},"preserve-a-path-back-to-the-original-page",[716],{"type":27,"value":717},"Preserve a path back to the original page",{"type":22,"tag":23,"props":719,"children":720},{},[721],{"type":27,"value":722},"To cite a PDF, we need to retain its structure while indexing it. The extraction step requests page chunks from PyMuPDF4LLM:",{"type":22,"tag":97,"props":724,"children":727},{"className":725,"code":726,"language":16,"meta":7,"style":7},"language-python shiki shiki-themes github-dark github-light","pages = pymupdf4llm.to_markdown(document, page_chunks=True)\n",[728],{"type":22,"tag":14,"props":729,"children":730},{"__ignoreMap":7},[731],{"type":22,"tag":107,"props":732,"children":733},{"class":109,"line":110},[734,740,746,751,757,761,766],{"type":22,"tag":107,"props":735,"children":737},{"style":736},"--shiki-default:#E1E4E8;--shiki-light:#24292E",[738],{"type":27,"value":739},"pages ",{"type":22,"tag":107,"props":741,"children":743},{"style":742},"--shiki-default:#F97583;--shiki-light:#D73A49",[744],{"type":27,"value":745},"=",{"type":22,"tag":107,"props":747,"children":748},{"style":736},[749],{"type":27,"value":750}," pymupdf4llm.to_markdown(document, ",{"type":22,"tag":107,"props":752,"children":754},{"style":753},"--shiki-default:#FFAB70;--shiki-light:#E36209",[755],{"type":27,"value":756},"page_chunks",{"type":22,"tag":107,"props":758,"children":759},{"style":742},[760],{"type":27,"value":745},{"type":22,"tag":107,"props":762,"children":763},{"style":126},[764],{"type":27,"value":765},"True",{"type":22,"tag":107,"props":767,"children":768},{"style":736},[769],{"type":27,"value":770},")\n",{"type":22,"tag":23,"props":772,"children":773},{},[774,776,782],{"type":27,"value":775},"For each page, the app splits text into manageable chunks and stores the filename, one-based page number, and section label alongside the vector. A section label comes from a PDF table of contents entry or a Markdown heading detected in the extracted text. If neither exists, the honest label is ",{"type":22,"tag":14,"props":777,"children":779},{"className":778},[],[780],{"type":27,"value":781},"Page N",{"type":27,"value":783},". We do not ask the answer model to guess where a passage came from.",{"type":22,"tag":23,"props":785,"children":786},{},[787],{"type":27,"value":788},"Chunking has a trade-off. An entire long PDF is too broad for useful retrieval and too expensive to pass to the answer model. Tiny fragments lose context. This example caps chunks around 1,200 characters with a small overlap and keeps each chunk within its original page. It is a starting point, not a universal optimum. In Part 2 we will test whether those boundaries actually retrieve the evidence we need.",{"type":22,"tag":23,"props":790,"children":791},{},[792],{"type":27,"value":793},"The app rejects image-only scans with a clear OCR hint. Silently indexing an empty page would make later answers look confident while leaving the document effectively invisible.",{"type":22,"tag":30,"props":795,"children":797},{"id":796},"index-and-replace-documents-without-stale-answers",[798],{"type":27,"value":799},"Index and replace documents without stale answers",{"type":22,"tag":23,"props":801,"children":802},{},[803],{"type":27,"value":804},"The embedding model converts each chunk to a vector. Chroma stores that vector together with the chunk text and source metadata. At question time, the same embedding model converts the question, and Chroma returns the nearest stored chunks. Using a different embedding model for queries and indexed documents would compare positions in different spaces; if you change models, rebuild the index.",{"type":22,"tag":23,"props":806,"children":807},{},[808],{"type":27,"value":809},"The index also tracks a hash of each PDF. Re-uploading the same bytes does nothing. Re-uploading changed bytes under the same filename replaces its old chunks, so a later question does not cite an outdated version of that document. The app adds the new vectors before removing the old ones, which protects the existing index if the new embedding or upload fails. You can remove a document from the sidebar as well.",{"type":22,"tag":23,"props":811,"children":812},{},[813],{"type":27,"value":814},"This replacement behavior is easy to overlook in a demo and important in a useful tool. An answer with a precise page number is not trustworthy if that page belongs to a file you replaced yesterday.",{"type":22,"tag":30,"props":816,"children":818},{"id":817},"make-the-answer-inspectable",[819],{"type":27,"value":820},"Make the answer inspectable",{"type":22,"tag":23,"props":822,"children":823},{},[824,826,831],{"type":27,"value":825},"The app sends the answer model numbered excerpts, each prefixed with filename, section, and page. It asks for a JSON response with an answer and the source numbers used. The application accepts only citation numbers that correspond to retrieved excerpts, then displays those excerpts beneath the answer. This prevents a fabricated citation to a nonexistent result, although it does ",{"type":22,"tag":691,"props":827,"children":828},{},[829],{"type":27,"value":830},"not",{"type":27,"value":832}," prove that the model interpreted a real excerpt correctly.",{"type":22,"tag":23,"props":834,"children":835},{},[836],{"type":27,"value":837},"If the model reports that the excerpts do not support an answer, or provides no valid source number, the app returns: “I couldn't find a supported answer in the indexed documents.” That is a useful outcome. The system should not turn the nearest passage into a fact simply because every search returns something.",{"type":22,"tag":23,"props":839,"children":840},{},[841],{"type":27,"value":842},"The source card is the final human check: read the cited paragraph, open the original PDF if needed, and verify the claim. The model is a shortcut to evidence, not a replacement for it. For decisions with real consequences, that distinction is part of the product design.",{"type":22,"tag":30,"props":844,"children":846},{"id":845},"where-this-first-version-stops",[847],{"type":27,"value":848},"Where this first version stops",{"type":22,"tag":23,"props":850,"children":851},{},[852],{"type":27,"value":853},"This example intentionally has a small boundary. It handles selectable text, a single local user, and a straightforward nearest-neighbor search over chunks. It does not perform OCR, manage access permissions, or prove answer quality. Tables, multi-column layouts, and vague questions can still produce poor retrieval. Even a valid source reference can accompany an incorrect paraphrase.",{"type":22,"tag":23,"props":855,"children":856},{},[857,859,865],{"type":27,"value":858},"The next post will use this same project to ask a harder practical question: how do we know whether the right passage was retrieved and the answer is actually supported? For now, you have a working local application, a reproducible ",{"type":22,"tag":14,"props":860,"children":862},{"className":861},[],[863],{"type":27,"value":864},"Makefile",{"type":27,"value":866},", and source metadata that lets you check what it tells you.",{"type":22,"tag":305,"props":868,"children":869},{},[870],{"type":27,"value":309},{"title":7,"searchDepth":311,"depth":311,"links":872},[873,874,875,876,877,878],{"id":370,"depth":311,"text":373},{"id":485,"depth":311,"text":488},{"id":714,"depth":311,"text":717},{"id":796,"depth":311,"text":799},{"id":817,"depth":311,"text":820},{"id":845,"depth":311,"text":848},"content:posts:chat-with-pdfs-locally-part-1.md","posts/chat-with-pdfs-locally-part-1.md","posts/chat-with-pdfs-locally-part-1",{"_path":883,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":884,"description":885,"layout":11,"date":886,"subtitle":887,"image":888,"optimized_image":888,"category":14,"tags":889,"author":17,"paginate":6,"body":891,"_type":316,"_id":2040,"_source":318,"_file":2041,"_stem":2042,"_extension":321},"/posts/ai-development-workspace-opencode-openspec","Build Your AI Development Workspace with OpenCode and OpenSpec","A practical introduction to OpenCode, OpenSpec, AGENTS.md, custom agents, and skills, using one small API change to show what each piece does.","2025-11-15T11:00:00.000Z","Give an AI coding agent context, a clear change, and reusable ways to work","/assets/img/uploads/ai-development-workspace.jpg",[14,331,332,890],"best practices",{"type":19,"children":892,"toc":2033},[893,905,936,1060,1081,1087,1100,1149,1161,1190,1253,1266,1272,1277,1291,1341,1371,1384,1398,1403,1442,1468,1556,1568,1574,1588,1601,1704,1734,1739,1745,1765,1776,1909,1939,1951,1957,1962,2019,2024,2029],{"type":22,"tag":23,"props":894,"children":895},{},[896,898,903],{"type":27,"value":897},"An AI coding agent can edit files and run tests within minutes. That speed is useful, but it also makes a familiar mistake faster: implementing a reasonable answer to a question nobody has properly defined. The agent may know the language and framework while knowing very little about ",{"type":22,"tag":341,"props":899,"children":900},{},[901],{"type":27,"value":902},"your",{"type":27,"value":904}," repository, the behavior you promised users, or the checks your team expects before a change is done.",{"type":22,"tag":23,"props":906,"children":907},{},[908,910,917,919,925,927,934],{"type":27,"value":909},"My starting point is a small development workspace with five distinct parts: ",{"type":22,"tag":352,"props":911,"children":914},{"href":912,"rel":913},"https://opencode.ai/docs/",[356],[915],{"type":27,"value":916},"OpenCode",{"type":27,"value":918}," to work in the repository, ",{"type":22,"tag":14,"props":920,"children":922},{"className":921},[],[923],{"type":27,"value":924},"AGENTS.md",{"type":27,"value":926}," to explain the repository, ",{"type":22,"tag":352,"props":928,"children":931},{"href":929,"rel":930},"https://github.com/Fission-AI/OpenSpec/blob/main/docs/getting-started.md",[356],[932],{"type":27,"value":933},"OpenSpec",{"type":27,"value":935}," to make a change reviewable, and custom agents and skills for work that benefits from a repeatable role or procedure. You do not need to configure every part on day one. It helps to understand what question each one answers.",{"type":22,"tag":937,"props":938,"children":939},"table",{},[940,964],{"type":22,"tag":941,"props":942,"children":943},"thead",{},[944],{"type":22,"tag":945,"props":946,"children":947},"tr",{},[948,954,959],{"type":22,"tag":949,"props":950,"children":951},"th",{},[952],{"type":27,"value":953},"Piece",{"type":22,"tag":949,"props":955,"children":956},{},[957],{"type":27,"value":958},"Question it answers",{"type":22,"tag":949,"props":960,"children":961},{},[962],{"type":27,"value":963},"Example",{"type":22,"tag":965,"props":966,"children":967},"tbody",{},[968,986,1007,1024,1042],{"type":22,"tag":945,"props":969,"children":970},{},[971,976,981],{"type":22,"tag":972,"props":973,"children":974},"td",{},[975],{"type":27,"value":916},{"type":22,"tag":972,"props":977,"children":978},{},[979],{"type":27,"value":980},"Who can inspect and change this repository with me?",{"type":22,"tag":972,"props":982,"children":983},{},[984],{"type":27,"value":985},"Explore the API, edit code, run tests",{"type":22,"tag":945,"props":987,"children":988},{},[989,997,1002],{"type":22,"tag":972,"props":990,"children":991},{},[992],{"type":22,"tag":14,"props":993,"children":995},{"className":994},[],[996],{"type":27,"value":924},{"type":22,"tag":972,"props":998,"children":999},{},[1000],{"type":27,"value":1001},"What should any agent know about this project?",{"type":22,"tag":972,"props":1003,"children":1004},{},[1005],{"type":27,"value":1006},"Architecture, commands, conventions",{"type":22,"tag":945,"props":1008,"children":1009},{},[1010,1014,1019],{"type":22,"tag":972,"props":1011,"children":1012},{},[1013],{"type":27,"value":933},{"type":22,"tag":972,"props":1015,"children":1016},{},[1017],{"type":27,"value":1018},"What behavior are we agreeing to change?",{"type":22,"tag":972,"props":1020,"children":1021},{},[1022],{"type":27,"value":1023},"A named change with scenarios and tasks",{"type":22,"tag":945,"props":1025,"children":1026},{},[1027,1032,1037],{"type":22,"tag":972,"props":1028,"children":1029},{},[1030],{"type":27,"value":1031},"Custom agent",{"type":22,"tag":972,"props":1033,"children":1034},{},[1035],{"type":27,"value":1036},"Who should take a specialized role?",{"type":22,"tag":972,"props":1038,"children":1039},{},[1040],{"type":27,"value":1041},"A reviewer with no edit permission",{"type":22,"tag":945,"props":1043,"children":1044},{},[1045,1050,1055],{"type":22,"tag":972,"props":1046,"children":1047},{},[1048],{"type":27,"value":1049},"Skill",{"type":22,"tag":972,"props":1051,"children":1052},{},[1053],{"type":27,"value":1054},"How do we repeat a particular procedure?",{"type":22,"tag":972,"props":1056,"children":1057},{},[1058],{"type":27,"value":1059},"A checklist for reviewing API compatibility",{"type":22,"tag":23,"props":1061,"children":1062},{},[1063,1065,1071,1073,1079],{"type":27,"value":1064},"These are different layers, not competing ways to write a longer prompt. Let's assemble them around a small example: an existing task API needs an optional ",{"type":22,"tag":14,"props":1066,"children":1068},{"className":1067},[],[1069],{"type":27,"value":1070},"status",{"type":27,"value":1072}," filter on ",{"type":22,"tag":14,"props":1074,"children":1076},{"className":1075},[],[1077],{"type":27,"value":1078},"GET /tasks",{"type":27,"value":1080},".",{"type":22,"tag":30,"props":1082,"children":1084},{"id":1083},"start-with-repository-context",[1085],{"type":27,"value":1086},"Start with repository context",{"type":22,"tag":23,"props":1088,"children":1089},{},[1090,1092,1098],{"type":27,"value":1091},"Install OpenCode using one of the methods in its ",{"type":22,"tag":352,"props":1093,"children":1095},{"href":912,"rel":1094},[356],[1096],{"type":27,"value":1097},"official setup guide",{"type":27,"value":1099},". For a Node-based setup, the documented command is:",{"type":22,"tag":97,"props":1101,"children":1103},{"className":99,"code":1102,"language":101,"meta":7,"style":7},"npm install -g opencode-ai\ncd path/to/your-project\nopencode\n",[1104],{"type":22,"tag":14,"props":1105,"children":1106},{"__ignoreMap":7},[1107,1129,1141],{"type":22,"tag":107,"props":1108,"children":1109},{"class":109,"line":110},[1110,1115,1119,1124],{"type":22,"tag":107,"props":1111,"children":1112},{"style":114},[1113],{"type":27,"value":1114},"npm",{"type":22,"tag":107,"props":1116,"children":1117},{"style":120},[1118],{"type":27,"value":264},{"type":22,"tag":107,"props":1120,"children":1121},{"style":126},[1122],{"type":27,"value":1123}," -g",{"type":22,"tag":107,"props":1125,"children":1126},{"style":120},[1127],{"type":27,"value":1128}," opencode-ai\n",{"type":22,"tag":107,"props":1130,"children":1131},{"class":109,"line":311},[1132,1136],{"type":22,"tag":107,"props":1133,"children":1134},{"style":126},[1135],{"type":27,"value":555},{"type":22,"tag":107,"props":1137,"children":1138},{"style":120},[1139],{"type":27,"value":1140}," path/to/your-project\n",{"type":22,"tag":107,"props":1142,"children":1143},{"class":109,"line":458},[1144],{"type":22,"tag":107,"props":1145,"children":1146},{"style":114},[1147],{"type":27,"value":1148},"opencode\n",{"type":22,"tag":23,"props":1150,"children":1151},{},[1152,1154,1159],{"type":27,"value":1153},"Connect a model provider using OpenCode's setup flow. Before asking it to implement the filter, let it inspect the project: “Where is ",{"type":22,"tag":14,"props":1155,"children":1157},{"className":1156},[],[1158],{"type":27,"value":1078},{"type":27,"value":1160}," implemented, and which tests define its current behavior?” A useful answer should point to actual files and acknowledge uncertainty. If it invents an endpoint, stop there; adding more roles will not repair missing context.",{"type":22,"tag":23,"props":1162,"children":1163},{},[1164,1166,1171,1173,1179,1181,1188],{"type":27,"value":1165},"OpenCode can create a starter ",{"type":22,"tag":14,"props":1167,"children":1169},{"className":1168},[],[1170],{"type":27,"value":924},{"type":27,"value":1172}," with ",{"type":22,"tag":14,"props":1174,"children":1176},{"className":1175},[],[1177],{"type":27,"value":1178},"/init",{"type":27,"value":1180},". Review what it writes. This file is durable project context, so it should contain facts and working rules that future sessions need, rather than a transcript of today's task. OpenCode's ",{"type":22,"tag":352,"props":1182,"children":1185},{"href":1183,"rel":1184},"https://opencode.ai/docs/rules/",[356],[1186],{"type":27,"value":1187},"rules documentation",{"type":27,"value":1189}," recommends project-specific instructions such as build commands, architecture, and conventions. A compact example:",{"type":22,"tag":97,"props":1191,"children":1194},{"className":1192,"code":1193,"language":316,"meta":7,"style":7},"language-markdown shiki shiki-themes github-dark github-light","# Task API\n\n- Routes live in `src/http`; persistence lives in `src/repositories`.\n- Preserve the response shape of existing endpoints.\n- Run `npm test` for behavior changes and `npm run build` before handoff.\n- Do not modify database migrations that have already been applied.\n- Record API contract changes in `openapi.yaml`.\n",[1195],{"type":22,"tag":14,"props":1196,"children":1197},{"__ignoreMap":7},[1198,1206,1213,1221,1229,1237,1245],{"type":22,"tag":107,"props":1199,"children":1200},{"class":109,"line":110},[1201],{"type":22,"tag":107,"props":1202,"children":1203},{},[1204],{"type":27,"value":1205},"# Task API\n",{"type":22,"tag":107,"props":1207,"children":1208},{"class":109,"line":311},[1209],{"type":22,"tag":107,"props":1210,"children":1211},{"emptyLinePlaceholder":452},[1212],{"type":27,"value":455},{"type":22,"tag":107,"props":1214,"children":1215},{"class":109,"line":458},[1216],{"type":22,"tag":107,"props":1217,"children":1218},{},[1219],{"type":27,"value":1220},"- Routes live in `src/http`; persistence lives in `src/repositories`.\n",{"type":22,"tag":107,"props":1222,"children":1223},{"class":109,"line":575},[1224],{"type":22,"tag":107,"props":1225,"children":1226},{},[1227],{"type":27,"value":1228},"- Preserve the response shape of existing endpoints.\n",{"type":22,"tag":107,"props":1230,"children":1231},{"class":109,"line":588},[1232],{"type":22,"tag":107,"props":1233,"children":1234},{},[1235],{"type":27,"value":1236},"- Run `npm test` for behavior changes and `npm run build` before handoff.\n",{"type":22,"tag":107,"props":1238,"children":1239},{"class":109,"line":601},[1240],{"type":22,"tag":107,"props":1241,"children":1242},{},[1243],{"type":27,"value":1244},"- Do not modify database migrations that have already been applied.\n",{"type":22,"tag":107,"props":1246,"children":1247},{"class":109,"line":614},[1248],{"type":22,"tag":107,"props":1249,"children":1250},{},[1251],{"type":27,"value":1252},"- Record API contract changes in `openapi.yaml`.\n",{"type":22,"tag":23,"props":1254,"children":1255},{},[1256,1258,1264],{"type":27,"value":1257},"The exact paths and commands must match your repository. If your actual check is ",{"type":22,"tag":14,"props":1259,"children":1261},{"className":1260},[],[1262],{"type":27,"value":1263},"./gradlew test",{"type":27,"value":1265},", write that instead. Treat this file like code: review it, keep it short, and update it when the project changes. It should help an agent avoid avoidable mistakes; it cannot guarantee correctness.",{"type":22,"tag":30,"props":1267,"children":1269},{"id":1268},"give-the-change-a-contract-with-openspec",[1270],{"type":27,"value":1271},"Give the change a contract with OpenSpec",{"type":22,"tag":23,"props":1273,"children":1274},{},[1275],{"type":27,"value":1276},"The request “add a status filter” still leaves questions. Which statuses are valid? What happens when the parameter is omitted? Is an unknown status an empty result or a client error? Does filtering happen before pagination? Those decisions affect clients, so I want them visible before implementation.",{"type":22,"tag":23,"props":1278,"children":1279},{},[1280,1282,1289],{"type":27,"value":1281},"OpenSpec can be installed and initialized from the terminal. Its ",{"type":22,"tag":352,"props":1283,"children":1286},{"href":1284,"rel":1285},"https://github.com/Fission-AI/OpenSpec/blob/main/docs/installation.md",[356],[1287],{"type":27,"value":1288},"installation guide",{"type":27,"value":1290}," documents the Node requirement and setup commands:",{"type":22,"tag":97,"props":1292,"children":1294},{"className":99,"code":1293,"language":101,"meta":7,"style":7},"npm install -g @fission-ai/openspec@latest\nopenspec init --tools opencode\n",[1295],{"type":22,"tag":14,"props":1296,"children":1297},{"__ignoreMap":7},[1298,1318],{"type":22,"tag":107,"props":1299,"children":1300},{"class":109,"line":110},[1301,1305,1309,1313],{"type":22,"tag":107,"props":1302,"children":1303},{"style":114},[1304],{"type":27,"value":1114},{"type":22,"tag":107,"props":1306,"children":1307},{"style":120},[1308],{"type":27,"value":264},{"type":22,"tag":107,"props":1310,"children":1311},{"style":126},[1312],{"type":27,"value":1123},{"type":22,"tag":107,"props":1314,"children":1315},{"style":120},[1316],{"type":27,"value":1317}," @fission-ai/openspec@latest\n",{"type":22,"tag":107,"props":1319,"children":1320},{"class":109,"line":311},[1321,1326,1331,1336],{"type":22,"tag":107,"props":1322,"children":1323},{"style":114},[1324],{"type":27,"value":1325},"openspec",{"type":22,"tag":107,"props":1327,"children":1328},{"style":120},[1329],{"type":27,"value":1330}," init",{"type":22,"tag":107,"props":1332,"children":1333},{"style":126},[1334],{"type":27,"value":1335}," --tools",{"type":22,"tag":107,"props":1337,"children":1338},{"style":120},[1339],{"type":27,"value":1340}," opencode\n",{"type":22,"tag":23,"props":1342,"children":1343},{},[1344,1346,1352,1354,1360,1362,1369],{"type":27,"value":1345},"Run ",{"type":22,"tag":14,"props":1347,"children":1349},{"className":1348},[],[1350],{"type":27,"value":1351},"openspec init",{"type":27,"value":1353}," at the project root. The ",{"type":22,"tag":14,"props":1355,"children":1357},{"className":1356},[],[1358],{"type":27,"value":1359},"--tools opencode",{"type":27,"value":1361}," option selects the OpenCode integration without an interactive picker. OpenSpec will create its planning directory and the workflow files for that integration. Check the generated files rather than assuming that every tool exposes the same slash-command names; OpenSpec's ",{"type":22,"tag":352,"props":1363,"children":1366},{"href":1364,"rel":1365},"https://github.com/Fission-AI/OpenSpec/blob/main/docs/supported-tools.md",[356],[1367],{"type":27,"value":1368},"tool reference",{"type":27,"value":1370}," describes those differences.",{"type":22,"tag":23,"props":1372,"children":1373},{},[1374,1376,1382],{"type":27,"value":1375},"In OpenCode, the OpenSpec integration currently provides ",{"type":22,"tag":14,"props":1377,"children":1379},{"className":1378},[],[1380],{"type":27,"value":1381},"/opsx-propose",{"type":27,"value":1383}," for this step. Start a named change in the chat:",{"type":22,"tag":97,"props":1385,"children":1387},{"className":433,"code":1386,"language":27,"meta":7,"style":7},"/opsx-propose add-task-status-filter\n",[1388],{"type":22,"tag":14,"props":1389,"children":1390},{"__ignoreMap":7},[1391],{"type":22,"tag":107,"props":1392,"children":1393},{"class":109,"line":110},[1394],{"type":22,"tag":107,"props":1395,"children":1396},{},[1397],{"type":27,"value":1386},{"type":22,"tag":23,"props":1399,"children":1400},{},[1401],{"type":27,"value":1402},"Give it the context the command needs:",{"type":22,"tag":97,"props":1404,"children":1406},{"className":433,"code":1405,"language":27,"meta":7,"style":7},"Propose a change to add an optional status filter to GET /tasks.\nInspect the current endpoint and tests first. Define the behavior when the\nparameter is absent, when it is valid, and when it is invalid. Do not\nimplement the change until we have reviewed the proposal and scenarios.\n",[1407],{"type":22,"tag":14,"props":1408,"children":1409},{"__ignoreMap":7},[1410,1418,1426,1434],{"type":22,"tag":107,"props":1411,"children":1412},{"class":109,"line":110},[1413],{"type":22,"tag":107,"props":1414,"children":1415},{},[1416],{"type":27,"value":1417},"Propose a change to add an optional status filter to GET /tasks.\n",{"type":22,"tag":107,"props":1419,"children":1420},{"class":109,"line":311},[1421],{"type":22,"tag":107,"props":1422,"children":1423},{},[1424],{"type":27,"value":1425},"Inspect the current endpoint and tests first. Define the behavior when the\n",{"type":22,"tag":107,"props":1427,"children":1428},{"class":109,"line":458},[1429],{"type":22,"tag":107,"props":1430,"children":1431},{},[1432],{"type":27,"value":1433},"parameter is absent, when it is valid, and when it is invalid. Do not\n",{"type":22,"tag":107,"props":1435,"children":1436},{"class":109,"line":575},[1437],{"type":22,"tag":107,"props":1438,"children":1439},{},[1440],{"type":27,"value":1441},"implement the change until we have reviewed the proposal and scenarios.\n",{"type":22,"tag":23,"props":1443,"children":1444},{},[1445,1447,1452,1454,1459,1461,1466],{"type":27,"value":1446},"The generated command is part of the OpenSpec workflow installed for OpenCode; check the names printed by ",{"type":22,"tag":14,"props":1448,"children":1450},{"className":1449},[],[1451],{"type":27,"value":1351},{"type":27,"value":1453}," if your installation differs. In the default spec-driven structure, a proposal explains ",{"type":22,"tag":341,"props":1455,"children":1456},{},[1457],{"type":27,"value":1458},"why",{"type":27,"value":1460},", a spec describes ",{"type":22,"tag":341,"props":1462,"children":1463},{},[1464],{"type":27,"value":1465},"observable behavior",{"type":27,"value":1467},", a design captures technical decisions when needed, and tasks track implementation. A scenario might say:",{"type":22,"tag":97,"props":1469,"children":1471},{"className":1192,"code":1470,"language":316,"meta":7,"style":7},"### Requirement: Filter tasks by status\nThe API MUST allow clients to filter tasks by a supported status.\n\n#### Scenario: No filter\n- **WHEN** a client calls GET /tasks without a status parameter\n- **THEN** the existing unfiltered behavior is preserved\n\n#### Scenario: Unsupported status\n- **WHEN** a client requests a status the API does not support\n- **THEN** the API returns the documented validation error\n",[1472],{"type":22,"tag":14,"props":1473,"children":1474},{"__ignoreMap":7},[1475,1483,1491,1498,1506,1514,1522,1529,1538,1547],{"type":22,"tag":107,"props":1476,"children":1477},{"class":109,"line":110},[1478],{"type":22,"tag":107,"props":1479,"children":1480},{},[1481],{"type":27,"value":1482},"### Requirement: Filter tasks by status\n",{"type":22,"tag":107,"props":1484,"children":1485},{"class":109,"line":311},[1486],{"type":22,"tag":107,"props":1487,"children":1488},{},[1489],{"type":27,"value":1490},"The API MUST allow clients to filter tasks by a supported status.\n",{"type":22,"tag":107,"props":1492,"children":1493},{"class":109,"line":458},[1494],{"type":22,"tag":107,"props":1495,"children":1496},{"emptyLinePlaceholder":452},[1497],{"type":27,"value":455},{"type":22,"tag":107,"props":1499,"children":1500},{"class":109,"line":575},[1501],{"type":22,"tag":107,"props":1502,"children":1503},{},[1504],{"type":27,"value":1505},"#### Scenario: No filter\n",{"type":22,"tag":107,"props":1507,"children":1508},{"class":109,"line":588},[1509],{"type":22,"tag":107,"props":1510,"children":1511},{},[1512],{"type":27,"value":1513},"- **WHEN** a client calls GET /tasks without a status parameter\n",{"type":22,"tag":107,"props":1515,"children":1516},{"class":109,"line":601},[1517],{"type":22,"tag":107,"props":1518,"children":1519},{},[1520],{"type":27,"value":1521},"- **THEN** the existing unfiltered behavior is preserved\n",{"type":22,"tag":107,"props":1523,"children":1524},{"class":109,"line":614},[1525],{"type":22,"tag":107,"props":1526,"children":1527},{"emptyLinePlaceholder":452},[1528],{"type":27,"value":455},{"type":22,"tag":107,"props":1530,"children":1532},{"class":109,"line":1531},8,[1533],{"type":22,"tag":107,"props":1534,"children":1535},{},[1536],{"type":27,"value":1537},"#### Scenario: Unsupported status\n",{"type":22,"tag":107,"props":1539,"children":1541},{"class":109,"line":1540},9,[1542],{"type":22,"tag":107,"props":1543,"children":1544},{},[1545],{"type":27,"value":1546},"- **WHEN** a client requests a status the API does not support\n",{"type":22,"tag":107,"props":1548,"children":1550},{"class":109,"line":1549},10,[1551],{"type":22,"tag":107,"props":1552,"children":1553},{},[1554],{"type":27,"value":1555},"- **THEN** the API returns the documented validation error\n",{"type":22,"tag":23,"props":1557,"children":1558},{},[1559,1561,1567],{"type":27,"value":1560},"That is only an example contract. Your API may deliberately handle invalid values differently. Decide it with the people who own the API, then write the chosen behavior down. For a deeper walkthrough of proposals, scenarios, and design trade-offs, see my ",{"type":22,"tag":352,"props":1562,"children":1564},{"href":1563},"/spec-driven-development-openspec/",[1565],{"type":27,"value":1566},"OpenSpec article",{"type":27,"value":1080},{"type":22,"tag":30,"props":1569,"children":1571},{"id":1570},"add-a-specialist-only-when-it-has-a-clear-job",[1572],{"type":27,"value":1573},"Add a specialist only when it has a clear job",{"type":22,"tag":23,"props":1575,"children":1576},{},[1577,1579,1586],{"type":27,"value":1578},"Once the change is implemented, a second perspective can help. OpenCode supports ",{"type":22,"tag":352,"props":1580,"children":1583},{"href":1581,"rel":1582},"https://opencode.ai/docs/agents/",[356],[1584],{"type":27,"value":1585},"custom primary agents and subagents",{"type":27,"value":1587},". A custom agent has a role and its own permissions; it is not a second specification. For this example, a review subagent should read the diff and contract, question edge cases, and report findings without editing files.",{"type":22,"tag":23,"props":1589,"children":1590},{},[1591,1593,1599],{"type":27,"value":1592},"Create ",{"type":22,"tag":14,"props":1594,"children":1596},{"className":1595},[],[1597],{"type":27,"value":1598},".opencode/agents/api-reviewer.md",{"type":27,"value":1600},":",{"type":22,"tag":97,"props":1602,"children":1604},{"className":1192,"code":1603,"language":316,"meta":7,"style":7},"---\ndescription: Reviews API changes against their contract and existing clients\nmode: subagent\npermission:\n  edit: deny\n  bash: ask\n---\n\nReview the proposed API change and its tests. Compare the implementation\nwith the OpenSpec scenarios and the existing endpoint behavior. Look for\ncompatibility changes, pagination mistakes, invalid input handling, and\nmissing tests. Cite the files and lines behind each finding. Do not edit.\n",[1605],{"type":22,"tag":14,"props":1606,"children":1607},{"__ignoreMap":7},[1608,1616,1624,1632,1640,1648,1656,1663,1670,1678,1686,1695],{"type":22,"tag":107,"props":1609,"children":1610},{"class":109,"line":110},[1611],{"type":22,"tag":107,"props":1612,"children":1613},{},[1614],{"type":27,"value":1615},"---\n",{"type":22,"tag":107,"props":1617,"children":1618},{"class":109,"line":311},[1619],{"type":22,"tag":107,"props":1620,"children":1621},{},[1622],{"type":27,"value":1623},"description: Reviews API changes against their contract and existing clients\n",{"type":22,"tag":107,"props":1625,"children":1626},{"class":109,"line":458},[1627],{"type":22,"tag":107,"props":1628,"children":1629},{},[1630],{"type":27,"value":1631},"mode: subagent\n",{"type":22,"tag":107,"props":1633,"children":1634},{"class":109,"line":575},[1635],{"type":22,"tag":107,"props":1636,"children":1637},{},[1638],{"type":27,"value":1639},"permission:\n",{"type":22,"tag":107,"props":1641,"children":1642},{"class":109,"line":588},[1643],{"type":22,"tag":107,"props":1644,"children":1645},{},[1646],{"type":27,"value":1647},"  edit: deny\n",{"type":22,"tag":107,"props":1649,"children":1650},{"class":109,"line":601},[1651],{"type":22,"tag":107,"props":1652,"children":1653},{},[1654],{"type":27,"value":1655},"  bash: ask\n",{"type":22,"tag":107,"props":1657,"children":1658},{"class":109,"line":614},[1659],{"type":22,"tag":107,"props":1660,"children":1661},{},[1662],{"type":27,"value":1615},{"type":22,"tag":107,"props":1664,"children":1665},{"class":109,"line":1531},[1666],{"type":22,"tag":107,"props":1667,"children":1668},{"emptyLinePlaceholder":452},[1669],{"type":27,"value":455},{"type":22,"tag":107,"props":1671,"children":1672},{"class":109,"line":1540},[1673],{"type":22,"tag":107,"props":1674,"children":1675},{},[1676],{"type":27,"value":1677},"Review the proposed API change and its tests. Compare the implementation\n",{"type":22,"tag":107,"props":1679,"children":1680},{"class":109,"line":1549},[1681],{"type":22,"tag":107,"props":1682,"children":1683},{},[1684],{"type":27,"value":1685},"with the OpenSpec scenarios and the existing endpoint behavior. Look for\n",{"type":22,"tag":107,"props":1687,"children":1689},{"class":109,"line":1688},11,[1690],{"type":22,"tag":107,"props":1691,"children":1692},{},[1693],{"type":27,"value":1694},"compatibility changes, pagination mistakes, invalid input handling, and\n",{"type":22,"tag":107,"props":1696,"children":1698},{"class":109,"line":1697},12,[1699],{"type":22,"tag":107,"props":1700,"children":1701},{},[1702],{"type":27,"value":1703},"missing tests. Cite the files and lines behind each finding. Do not edit.\n",{"type":22,"tag":23,"props":1705,"children":1706},{},[1707,1709,1715,1717,1723,1725,1732],{"type":27,"value":1708},"You can invoke that subagent by mentioning ",{"type":22,"tag":14,"props":1710,"children":1712},{"className":1711},[],[1713],{"type":27,"value":1714},"@api-reviewer",{"type":27,"value":1716}," in OpenCode. It cannot edit files, and it must ask before running a shell command such as ",{"type":22,"tag":14,"props":1718,"children":1720},{"className":1719},[],[1721],{"type":27,"value":1722},"git diff",{"type":27,"value":1724},". Those permissions make its role explicit. They do not make its analysis infallible: the engineer still decides whether each finding is correct and whether the implementation satisfies the contract. OpenCode's ",{"type":22,"tag":352,"props":1726,"children":1729},{"href":1727,"rel":1728},"https://opencode.ai/docs/permissions/",[356],[1730],{"type":27,"value":1731},"permissions guide",{"type":27,"value":1733}," is worth reading before giving a specialist shell or editing access.",{"type":22,"tag":23,"props":1735,"children":1736},{},[1737],{"type":27,"value":1738},"Do you need separate “backend”, “tester”, “architect”, and “reviewer” agents for this endpoint? Probably not. Start with the general agent and add a specialist when you can name a recurring job, its inputs, its output, and the permissions it needs. More agents can also create more handoffs and inconsistent assumptions.",{"type":22,"tag":30,"props":1740,"children":1742},{"id":1741},"capture-a-procedure-as-a-skill",[1743],{"type":27,"value":1744},"Capture a procedure as a skill",{"type":22,"tag":23,"props":1746,"children":1747},{},[1748,1750,1755,1757,1763],{"type":27,"value":1749},"A skill is different from an agent. It packages instructions for ",{"type":22,"tag":341,"props":1751,"children":1752},{},[1753],{"type":27,"value":1754},"how",{"type":27,"value":1756}," to do a recurring task, and OpenCode loads it when relevant. For instance, the API reviewer's role is “review this change”; an ",{"type":22,"tag":14,"props":1758,"children":1760},{"className":1759},[],[1761],{"type":27,"value":1762},"api-compatibility",{"type":27,"value":1764}," skill could define the checks the reviewer should apply across many API changes.",{"type":22,"tag":23,"props":1766,"children":1767},{},[1768,1769,1775],{"type":27,"value":1592},{"type":22,"tag":14,"props":1770,"children":1772},{"className":1771},[],[1773],{"type":27,"value":1774},".opencode/skills/api-compatibility/SKILL.md",{"type":27,"value":1600},{"type":22,"tag":97,"props":1777,"children":1779},{"className":1192,"code":1778,"language":316,"meta":7,"style":7},"---\nname: api-compatibility\ndescription: Check an API change for client-visible compatibility risks\n---\n\n## Use this skill when\n\nAn endpoint, request, response, or documented error behavior changes.\n\n## Procedure\n\n1. Read the current API contract and at least one existing client call.\n2. Compare old and new request and response shapes.\n3. Check defaults, invalid input, pagination, and error status codes.\n4. Identify changes that require clients to deploy at the same time.\n5. Report each risk with evidence and a compatible migration option.\n",[1780],{"type":22,"tag":14,"props":1781,"children":1782},{"__ignoreMap":7},[1783,1790,1798,1806,1813,1820,1828,1835,1843,1850,1858,1865,1873,1882,1891,1900],{"type":22,"tag":107,"props":1784,"children":1785},{"class":109,"line":110},[1786],{"type":22,"tag":107,"props":1787,"children":1788},{},[1789],{"type":27,"value":1615},{"type":22,"tag":107,"props":1791,"children":1792},{"class":109,"line":311},[1793],{"type":22,"tag":107,"props":1794,"children":1795},{},[1796],{"type":27,"value":1797},"name: api-compatibility\n",{"type":22,"tag":107,"props":1799,"children":1800},{"class":109,"line":458},[1801],{"type":22,"tag":107,"props":1802,"children":1803},{},[1804],{"type":27,"value":1805},"description: Check an API change for client-visible compatibility risks\n",{"type":22,"tag":107,"props":1807,"children":1808},{"class":109,"line":575},[1809],{"type":22,"tag":107,"props":1810,"children":1811},{},[1812],{"type":27,"value":1615},{"type":22,"tag":107,"props":1814,"children":1815},{"class":109,"line":588},[1816],{"type":22,"tag":107,"props":1817,"children":1818},{"emptyLinePlaceholder":452},[1819],{"type":27,"value":455},{"type":22,"tag":107,"props":1821,"children":1822},{"class":109,"line":601},[1823],{"type":22,"tag":107,"props":1824,"children":1825},{},[1826],{"type":27,"value":1827},"## Use this skill when\n",{"type":22,"tag":107,"props":1829,"children":1830},{"class":109,"line":614},[1831],{"type":22,"tag":107,"props":1832,"children":1833},{"emptyLinePlaceholder":452},[1834],{"type":27,"value":455},{"type":22,"tag":107,"props":1836,"children":1837},{"class":109,"line":1531},[1838],{"type":22,"tag":107,"props":1839,"children":1840},{},[1841],{"type":27,"value":1842},"An endpoint, request, response, or documented error behavior changes.\n",{"type":22,"tag":107,"props":1844,"children":1845},{"class":109,"line":1540},[1846],{"type":22,"tag":107,"props":1847,"children":1848},{"emptyLinePlaceholder":452},[1849],{"type":27,"value":455},{"type":22,"tag":107,"props":1851,"children":1852},{"class":109,"line":1549},[1853],{"type":22,"tag":107,"props":1854,"children":1855},{},[1856],{"type":27,"value":1857},"## Procedure\n",{"type":22,"tag":107,"props":1859,"children":1860},{"class":109,"line":1688},[1861],{"type":22,"tag":107,"props":1862,"children":1863},{"emptyLinePlaceholder":452},[1864],{"type":27,"value":455},{"type":22,"tag":107,"props":1866,"children":1867},{"class":109,"line":1697},[1868],{"type":22,"tag":107,"props":1869,"children":1870},{},[1871],{"type":27,"value":1872},"1. Read the current API contract and at least one existing client call.\n",{"type":22,"tag":107,"props":1874,"children":1876},{"class":109,"line":1875},13,[1877],{"type":22,"tag":107,"props":1878,"children":1879},{},[1880],{"type":27,"value":1881},"2. Compare old and new request and response shapes.\n",{"type":22,"tag":107,"props":1883,"children":1885},{"class":109,"line":1884},14,[1886],{"type":22,"tag":107,"props":1887,"children":1888},{},[1889],{"type":27,"value":1890},"3. Check defaults, invalid input, pagination, and error status codes.\n",{"type":22,"tag":107,"props":1892,"children":1894},{"class":109,"line":1893},15,[1895],{"type":22,"tag":107,"props":1896,"children":1897},{},[1898],{"type":27,"value":1899},"4. Identify changes that require clients to deploy at the same time.\n",{"type":22,"tag":107,"props":1901,"children":1903},{"class":109,"line":1902},16,[1904],{"type":22,"tag":107,"props":1905,"children":1906},{},[1907],{"type":27,"value":1908},"5. Report each risk with evidence and a compatible migration option.\n",{"type":22,"tag":23,"props":1910,"children":1911},{},[1912,1914,1921,1923,1929,1931,1937],{"type":27,"value":1913},"OpenCode's ",{"type":22,"tag":352,"props":1915,"children":1918},{"href":1916,"rel":1917},"https://opencode.ai/docs/skills/",[356],[1919],{"type":27,"value":1920},"skill documentation",{"type":27,"value":1922}," describes this directory structure and the required ",{"type":22,"tag":14,"props":1924,"children":1926},{"className":1925},[],[1927],{"type":27,"value":1928},"name",{"type":27,"value":1930}," and ",{"type":22,"tag":14,"props":1932,"children":1934},{"className":1933},[],[1935],{"type":27,"value":1936},"description",{"type":27,"value":1938}," frontmatter. Keep the description specific: it helps the agent decide whether to load the skill. A skill should carry a procedure that survives one task. If it merely repeats today's prompt, it probably belongs in the conversation instead.",{"type":22,"tag":23,"props":1940,"children":1941},{},[1942,1944,1949],{"type":27,"value":1943},"You can ask the reviewer to use ",{"type":22,"tag":14,"props":1945,"children":1947},{"className":1946},[],[1948],{"type":27,"value":1762},{"type":27,"value":1950},", or let the agent discover it when the task matches its description. For a small project, you might not need this file yet. Write it when you notice yourself repeating the same review steps across changes.",{"type":22,"tag":30,"props":1952,"children":1954},{"id":1953},"run-one-complete-change",[1955],{"type":27,"value":1956},"Run one complete change",{"type":22,"tag":23,"props":1958,"children":1959},{},[1960],{"type":27,"value":1961},"With those pieces in place, the loop is straightforward:",{"type":22,"tag":78,"props":1963,"children":1964},{},[1965,1970,1975,1994,2006],{"type":22,"tag":46,"props":1966,"children":1967},{},[1968],{"type":27,"value":1969},"Ask OpenCode to inspect the current endpoint and tests.",{"type":22,"tag":46,"props":1971,"children":1972},{},[1973],{"type":27,"value":1974},"Use OpenSpec to propose the filter and review its scenarios. Resolve ambiguous client behavior before coding.",{"type":22,"tag":46,"props":1976,"children":1977},{},[1978,1980,1986,1988,1993],{"type":27,"value":1979},"Use ",{"type":22,"tag":14,"props":1981,"children":1983},{"className":1982},[],[1984],{"type":27,"value":1985},"/opsx-apply",{"type":27,"value":1987}," to implement the approved change, then run the repository checks from ",{"type":22,"tag":14,"props":1989,"children":1991},{"className":1990},[],[1992],{"type":27,"value":924},{"type":27,"value":1080},{"type":22,"tag":46,"props":1995,"children":1996},{},[1997,1999,2004],{"type":27,"value":1998},"Ask ",{"type":22,"tag":14,"props":2000,"children":2002},{"className":2001},[],[2003],{"type":27,"value":1714},{"type":27,"value":2005}," to compare the result with the spec and apply the compatibility procedure.",{"type":22,"tag":46,"props":2007,"children":2008},{},[2009,2011,2017],{"type":27,"value":2010},"Read the diff, run the checks yourself or inspect their output, and correct any mismatch. Update the spec if a decision legitimately changed during implementation. Archive the change with ",{"type":22,"tag":14,"props":2012,"children":2014},{"className":2013},[],[2015],{"type":27,"value":2016},"/opsx-archive",{"type":27,"value":2018}," once its implementation is finished and accepted.",{"type":22,"tag":23,"props":2020,"children":2021},{},[2022],{"type":27,"value":2023},"Notice what remains the developer's responsibility: choosing the behavior, assessing trade-offs, checking evidence, and deciding when the change is ready. The workspace helps the agent work with better context and leaves a clearer trail for a human reviewer. It does not turn a generated patch into a trustworthy one by itself.",{"type":22,"tag":23,"props":2025,"children":2026},{},[2027],{"type":27,"value":2028},"There is also no prize for using every component in every repository. Start with OpenCode and accurate project instructions. Add OpenSpec when the risk is implementing the wrong behavior; add a custom agent when a specialized perspective repeatedly helps; add a skill when a procedure deserves to be reused. That progression keeps the setup understandable as the project grows.",{"type":22,"tag":305,"props":2030,"children":2031},{},[2032],{"type":27,"value":309},{"title":7,"searchDepth":311,"depth":311,"links":2034},[2035,2036,2037,2038,2039],{"id":1083,"depth":311,"text":1086},{"id":1268,"depth":311,"text":1271},{"id":1570,"depth":311,"text":1573},{"id":1741,"depth":311,"text":1744},{"id":1953,"depth":311,"text":1956},"content:posts:ai-development-workspace-opencode-openspec.md","posts/ai-development-workspace-opencode-openspec.md","posts/ai-development-workspace-opencode-openspec",{"_path":2044,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":2045,"description":2046,"layout":11,"date":2047,"subtitle":2048,"image":2049,"optimized_image":2049,"category":14,"tags":2050,"author":17,"paginate":6,"body":2052,"_type":316,"_id":3442,"_source":318,"_file":3443,"_stem":3444,"_extension":321},"/posts/spec-driven-development-openspec","Spec-Driven Development with OpenSpec: From Vague Request to Verifiable Change","A practical OpenSpec walkthrough: clarify an order-cancellation request, write behavior as scenarios, make design decisions, implement with AI, and revise the spec when reality changes.","2025-08-15T11:00:00.000Z","Turn an idea into behavior you can review, implement, and test","/assets/img/uploads/sdd-openspec.jpg",[14,332,331,2051],"testing",{"type":19,"children":2053,"toc":3430},[2054,2059,2071,2076,2082,2087,2190,2209,2214,2220,2225,2294,2367,2372,2503,2508,2514,2527,2532,2775,2788,2809,2815,2820,2905,2918,2930,2936,2946,2985,2990,3004,3010,3015,3054,3081,3094,3100,3119,3124,3163,3175,3181,3186,3285,3290,3337,3342,3362,3368,3381,3386,3392,3426],{"type":22,"tag":23,"props":2055,"children":2056},{},[2057],{"type":27,"value":2058},"“Let customers cancel orders” sounds like a small feature. A developer or an AI coding agent could start writing an endpoint immediately. A few hours later, the code might compile and the happy-path test might pass. We could still disagree about what happens after shipment starts, whether a retry creates a second cancellation, or which customer is allowed to act.",{"type":22,"tag":23,"props":2060,"children":2061},{},[2062,2064,2069],{"type":27,"value":2063},"I use OpenSpec for this kind of conversation: make the intended behavior visible before implementation hides the unanswered questions. ",{"type":22,"tag":691,"props":2065,"children":2066},{},[2067],{"type":27,"value":2068},"Spec-driven development (SDD) is useful when the expensive mistake is building a plausible interpretation of an unclear request.",{"type":27,"value":2070}," It does not remove judgment from the engineer. It gives people and AI agents something concrete to review, challenge, and verify.",{"type":22,"tag":23,"props":2072,"children":2073},{},[2074],{"type":27,"value":2075},"Let's follow one illustrative change from the first request to a finished implementation. The business rules below are example decisions for this walkthrough, not universal rules for order systems.",{"type":22,"tag":30,"props":2077,"children":2079},{"id":2078},"the-request-is-not-yet-a-specification",[2080],{"type":27,"value":2081},"The request is not yet a specification",{"type":22,"tag":23,"props":2083,"children":2084},{},[2085],{"type":27,"value":2086},"Before opening an editor, ask what “cancel” means in this service:",{"type":22,"tag":937,"props":2088,"children":2089},{},[2090,2106],{"type":22,"tag":941,"props":2091,"children":2092},{},[2093],{"type":22,"tag":945,"props":2094,"children":2095},{},[2096,2101],{"type":22,"tag":949,"props":2097,"children":2098},{},[2099],{"type":27,"value":2100},"Question",{"type":22,"tag":949,"props":2102,"children":2103},{},[2104],{"type":27,"value":2105},"A decision the team needs to make",{"type":22,"tag":965,"props":2107,"children":2108},{},[2109,2122,2143,2156,2169],{"type":22,"tag":945,"props":2110,"children":2111},{},[2112,2117],{"type":22,"tag":972,"props":2113,"children":2114},{},[2115],{"type":27,"value":2116},"Who may cancel?",{"type":22,"tag":972,"props":2118,"children":2119},{},[2120],{"type":27,"value":2121},"The order owner, an administrator, or both?",{"type":22,"tag":945,"props":2123,"children":2124},{},[2125,2130],{"type":22,"tag":972,"props":2126,"children":2127},{},[2128],{"type":27,"value":2129},"Until when?",{"type":22,"tag":972,"props":2131,"children":2132},{},[2133,2135,2141],{"type":27,"value":2134},"Only while ",{"type":22,"tag":14,"props":2136,"children":2138},{"className":2137},[],[2139],{"type":27,"value":2140},"PENDING",{"type":27,"value":2142},", or also after payment and before shipment?",{"type":22,"tag":945,"props":2144,"children":2145},{},[2146,2151],{"type":22,"tag":972,"props":2147,"children":2148},{},[2149],{"type":27,"value":2150},"What does a retry do?",{"type":22,"tag":972,"props":2152,"children":2153},{},[2154],{"type":27,"value":2155},"Return the existing cancellation, or fail?",{"type":22,"tag":945,"props":2157,"children":2158},{},[2159,2164],{"type":22,"tag":972,"props":2160,"children":2161},{},[2162],{"type":27,"value":2163},"What if two requests race?",{"type":22,"tag":972,"props":2165,"children":2166},{},[2167],{"type":27,"value":2168},"Can both succeed without recording two transitions?",{"type":22,"tag":945,"props":2170,"children":2171},{},[2172,2177],{"type":22,"tag":972,"props":2173,"children":2174},{},[2175],{"type":27,"value":2176},"What happens downstream?",{"type":22,"tag":972,"props":2178,"children":2179},{},[2180,2182,2188],{"type":27,"value":2181},"Is an ",{"type":22,"tag":14,"props":2183,"children":2185},{"className":2184},[],[2186],{"type":27,"value":2187},"OrderCancelled",{"type":27,"value":2189}," event part of the contract?",{"type":22,"tag":23,"props":2191,"children":2192},{},[2193,2195,2200,2202,2207],{"type":27,"value":2194},"The list is not an excuse to delay work indefinitely. It is a way to find the decisions that would otherwise be made accidentally in a controller, an ORM mapping, or an agent's generated code. For this example, assume the team agrees that the authenticated owner may cancel a ",{"type":22,"tag":14,"props":2196,"children":2198},{"className":2197},[],[2199],{"type":27,"value":2140},{"type":27,"value":2201}," order; a shipped order cannot be cancelled; repeating a successful request returns the cancelled state; and one logical ",{"type":22,"tag":14,"props":2203,"children":2205},{"className":2204},[],[2206],{"type":27,"value":2187},{"type":27,"value":2208}," event is produced for the transition.",{"type":22,"tag":23,"props":2210,"children":2211},{},[2212],{"type":27,"value":2213},"The goal is now specific enough to propose a change.",{"type":22,"tag":30,"props":2215,"children":2217},{"id":2216},"create-a-change-that-explains-why",[2218],{"type":27,"value":2219},"Create a change that explains why",{"type":22,"tag":23,"props":2221,"children":2222},{},[2223],{"type":27,"value":2224},"With OpenSpec initialized in the project, create a named change:",{"type":22,"tag":97,"props":2226,"children":2228},{"className":99,"code":2227,"language":101,"meta":7,"style":7},"openspec new change add-order-cancellation \\\n  --goal \"Customers can cancel their pending orders safely\"\nopenspec status --change add-order-cancellation\n",[2229],{"type":22,"tag":14,"props":2230,"children":2231},{"__ignoreMap":7},[2232,2259,2272],{"type":22,"tag":107,"props":2233,"children":2234},{"class":109,"line":110},[2235,2239,2244,2249,2254],{"type":22,"tag":107,"props":2236,"children":2237},{"style":114},[2238],{"type":27,"value":1325},{"type":22,"tag":107,"props":2240,"children":2241},{"style":120},[2242],{"type":27,"value":2243}," new",{"type":22,"tag":107,"props":2245,"children":2246},{"style":120},[2247],{"type":27,"value":2248}," change",{"type":22,"tag":107,"props":2250,"children":2251},{"style":120},[2252],{"type":27,"value":2253}," add-order-cancellation",{"type":22,"tag":107,"props":2255,"children":2256},{"style":126},[2257],{"type":27,"value":2258}," \\\n",{"type":22,"tag":107,"props":2260,"children":2261},{"class":109,"line":311},[2262,2267],{"type":22,"tag":107,"props":2263,"children":2264},{"style":126},[2265],{"type":27,"value":2266},"  --goal",{"type":22,"tag":107,"props":2268,"children":2269},{"style":120},[2270],{"type":27,"value":2271}," \"Customers can cancel their pending orders safely\"\n",{"type":22,"tag":107,"props":2273,"children":2274},{"class":109,"line":458},[2275,2279,2284,2289],{"type":22,"tag":107,"props":2276,"children":2277},{"style":114},[2278],{"type":27,"value":1325},{"type":22,"tag":107,"props":2280,"children":2281},{"style":120},[2282],{"type":27,"value":2283}," status",{"type":22,"tag":107,"props":2285,"children":2286},{"style":126},[2287],{"type":27,"value":2288}," --change",{"type":22,"tag":107,"props":2290,"children":2291},{"style":120},[2292],{"type":27,"value":2293}," add-order-cancellation\n",{"type":22,"tag":23,"props":2295,"children":2296},{},[2297,2299,2305,2307,2313,2315,2321,2323,2327,2329,2334,2336,2342,2344,2348,2350,2356,2358,2365],{"type":27,"value":2298},"The CLI creates ",{"type":22,"tag":14,"props":2300,"children":2302},{"className":2301},[],[2303],{"type":27,"value":2304},"openspec/changes/add-order-cancellation/",{"type":27,"value":2306},". In OpenSpec's default ",{"type":22,"tag":14,"props":2308,"children":2310},{"className":2309},[],[2311],{"type":27,"value":2312},"spec-driven",{"type":27,"value":2314}," workflow, ",{"type":22,"tag":14,"props":2316,"children":2318},{"className":2317},[],[2319],{"type":27,"value":2320},"proposal.md",{"type":27,"value":2322}," captures ",{"type":22,"tag":691,"props":2324,"children":2325},{},[2326],{"type":27,"value":1458},{"type":27,"value":2328},", a spec delta captures ",{"type":22,"tag":691,"props":2330,"children":2331},{},[2332],{"type":27,"value":2333},"what behavior changes",{"type":27,"value":2335},", ",{"type":22,"tag":14,"props":2337,"children":2339},{"className":2338},[],[2340],{"type":27,"value":2341},"design.md",{"type":27,"value":2343}," records ",{"type":22,"tag":691,"props":2345,"children":2346},{},[2347],{"type":27,"value":1754},{"type":27,"value":2349}," where a design is needed, and ",{"type":22,"tag":14,"props":2351,"children":2353},{"className":2352},[],[2354],{"type":27,"value":2355},"tasks.md",{"type":27,"value":2357}," tracks implementation. Proposal comes first; spec and design can follow in either order; tasks depend on both. The ",{"type":22,"tag":352,"props":2359,"children":2362},{"href":2360,"rel":2361},"https://openspec.dev/docs/schemas/spec-driven",[356],[2363],{"type":27,"value":2364},"schema reference",{"type":27,"value":2366}," describes those roles.",{"type":22,"tag":23,"props":2368,"children":2369},{},[2370],{"type":27,"value":2371},"The proposal should make the problem and scope clear without pretending to be the implementation plan. A shortened example:",{"type":22,"tag":97,"props":2373,"children":2375},{"className":1192,"code":2374,"language":316,"meta":7,"style":7},"# Proposal\n\n## Why\nCustomers cannot cancel an eligible order without contacting support.\n\n## What Changes\n- Allow an authenticated owner to cancel a pending order.\n- Preserve a single logical cancellation when requests are repeated.\n- Publish the cancellation for downstream consumers.\n\n## Capabilities\n### New Capabilities\n- `order-cancellation`: Rules and outcomes of cancelling an order.\n\n## Impact\nOrder API, order persistence, cancellation event, and contract tests.\n",[2376],{"type":22,"tag":14,"props":2377,"children":2378},{"__ignoreMap":7},[2379,2387,2394,2402,2410,2417,2425,2433,2441,2449,2456,2464,2472,2480,2487,2495],{"type":22,"tag":107,"props":2380,"children":2381},{"class":109,"line":110},[2382],{"type":22,"tag":107,"props":2383,"children":2384},{},[2385],{"type":27,"value":2386},"# Proposal\n",{"type":22,"tag":107,"props":2388,"children":2389},{"class":109,"line":311},[2390],{"type":22,"tag":107,"props":2391,"children":2392},{"emptyLinePlaceholder":452},[2393],{"type":27,"value":455},{"type":22,"tag":107,"props":2395,"children":2396},{"class":109,"line":458},[2397],{"type":22,"tag":107,"props":2398,"children":2399},{},[2400],{"type":27,"value":2401},"## Why\n",{"type":22,"tag":107,"props":2403,"children":2404},{"class":109,"line":575},[2405],{"type":22,"tag":107,"props":2406,"children":2407},{},[2408],{"type":27,"value":2409},"Customers cannot cancel an eligible order without contacting support.\n",{"type":22,"tag":107,"props":2411,"children":2412},{"class":109,"line":588},[2413],{"type":22,"tag":107,"props":2414,"children":2415},{"emptyLinePlaceholder":452},[2416],{"type":27,"value":455},{"type":22,"tag":107,"props":2418,"children":2419},{"class":109,"line":601},[2420],{"type":22,"tag":107,"props":2421,"children":2422},{},[2423],{"type":27,"value":2424},"## What Changes\n",{"type":22,"tag":107,"props":2426,"children":2427},{"class":109,"line":614},[2428],{"type":22,"tag":107,"props":2429,"children":2430},{},[2431],{"type":27,"value":2432},"- Allow an authenticated owner to cancel a pending order.\n",{"type":22,"tag":107,"props":2434,"children":2435},{"class":109,"line":1531},[2436],{"type":22,"tag":107,"props":2437,"children":2438},{},[2439],{"type":27,"value":2440},"- Preserve a single logical cancellation when requests are repeated.\n",{"type":22,"tag":107,"props":2442,"children":2443},{"class":109,"line":1540},[2444],{"type":22,"tag":107,"props":2445,"children":2446},{},[2447],{"type":27,"value":2448},"- Publish the cancellation for downstream consumers.\n",{"type":22,"tag":107,"props":2450,"children":2451},{"class":109,"line":1549},[2452],{"type":22,"tag":107,"props":2453,"children":2454},{"emptyLinePlaceholder":452},[2455],{"type":27,"value":455},{"type":22,"tag":107,"props":2457,"children":2458},{"class":109,"line":1688},[2459],{"type":22,"tag":107,"props":2460,"children":2461},{},[2462],{"type":27,"value":2463},"## Capabilities\n",{"type":22,"tag":107,"props":2465,"children":2466},{"class":109,"line":1697},[2467],{"type":22,"tag":107,"props":2468,"children":2469},{},[2470],{"type":27,"value":2471},"### New Capabilities\n",{"type":22,"tag":107,"props":2473,"children":2474},{"class":109,"line":1875},[2475],{"type":22,"tag":107,"props":2476,"children":2477},{},[2478],{"type":27,"value":2479},"- `order-cancellation`: Rules and outcomes of cancelling an order.\n",{"type":22,"tag":107,"props":2481,"children":2482},{"class":109,"line":1884},[2483],{"type":22,"tag":107,"props":2484,"children":2485},{"emptyLinePlaceholder":452},[2486],{"type":27,"value":455},{"type":22,"tag":107,"props":2488,"children":2489},{"class":109,"line":1893},[2490],{"type":22,"tag":107,"props":2491,"children":2492},{},[2493],{"type":27,"value":2494},"## Impact\n",{"type":22,"tag":107,"props":2496,"children":2497},{"class":109,"line":1902},[2498],{"type":22,"tag":107,"props":2499,"children":2500},{},[2501],{"type":27,"value":2502},"Order API, order persistence, cancellation event, and contract tests.\n",{"type":22,"tag":23,"props":2504,"children":2505},{},[2506],{"type":27,"value":2507},"At review time, the most useful question is whether this is the right change. If the request also asks for refunds, inventory release, and cancelling shipments, the proposal needs to state which of those are in scope and which require another change. A small, named unit of work is easier to understand and integrate.",{"type":22,"tag":30,"props":2509,"children":2511},{"id":2510},"put-observable-behavior-in-the-spec",[2512],{"type":27,"value":2513},"Put observable behavior in the spec",{"type":22,"tag":23,"props":2515,"children":2516},{},[2517,2519,2525],{"type":27,"value":2518},"The delta spec belongs under the capability named in the proposal, for example ",{"type":22,"tag":14,"props":2520,"children":2522},{"className":2521},[],[2523],{"type":27,"value":2524},"openspec/changes/add-order-cancellation/specs/order-cancellation/spec.md",{"type":27,"value":2526},". It describes the behavior that changes, not the classes or SQL statements that happen to implement it.",{"type":22,"tag":23,"props":2528,"children":2529},{},[2530],{"type":27,"value":2531},"Here is an illustrative excerpt in OpenSpec's requirement-and-scenario format:",{"type":22,"tag":97,"props":2533,"children":2535},{"className":1192,"code":2534,"language":316,"meta":7,"style":7},"# Spec Delta\n\n## Purpose\nDefine when the owner of an order may cancel it and how the service\nresponds to rejected or repeated cancellation requests.\n\n## ADDED Requirements\n\n### Requirement: Cancel an eligible order\nThe service MUST allow the authenticated owner to cancel a PENDING order.\n\n#### Scenario: Eligible order\n- **WHEN** the owner requests cancellation of a PENDING order\n- **THEN** the order becomes CANCELLED and one logical OrderCancelled event is recorded\n\n#### Scenario: Shipment has started\n- **WHEN** the owner requests cancellation of a SHIPPED order\n- **THEN** the service rejects the request and leaves the order unchanged\n\n#### Scenario: Not the owner\n- **WHEN** a different authenticated customer requests cancellation\n- **THEN** the service rejects the request and leaves the order unchanged\n\n### Requirement: Repeat a completed cancellation\nThe service MUST handle repeated requests without creating a second cancellation.\n\n#### Scenario: Repeated request\n- **WHEN** the owner repeats a successful cancellation request\n- **THEN** the service returns the existing CANCELLED state without a new transition\n",[2536],{"type":22,"tag":14,"props":2537,"children":2538},{"__ignoreMap":7},[2539,2547,2554,2562,2570,2578,2585,2593,2600,2608,2616,2623,2631,2639,2647,2654,2662,2671,2680,2688,2697,2706,2714,2722,2731,2740,2748,2757,2766],{"type":22,"tag":107,"props":2540,"children":2541},{"class":109,"line":110},[2542],{"type":22,"tag":107,"props":2543,"children":2544},{},[2545],{"type":27,"value":2546},"# Spec Delta\n",{"type":22,"tag":107,"props":2548,"children":2549},{"class":109,"line":311},[2550],{"type":22,"tag":107,"props":2551,"children":2552},{"emptyLinePlaceholder":452},[2553],{"type":27,"value":455},{"type":22,"tag":107,"props":2555,"children":2556},{"class":109,"line":458},[2557],{"type":22,"tag":107,"props":2558,"children":2559},{},[2560],{"type":27,"value":2561},"## Purpose\n",{"type":22,"tag":107,"props":2563,"children":2564},{"class":109,"line":575},[2565],{"type":22,"tag":107,"props":2566,"children":2567},{},[2568],{"type":27,"value":2569},"Define when the owner of an order may cancel it and how the service\n",{"type":22,"tag":107,"props":2571,"children":2572},{"class":109,"line":588},[2573],{"type":22,"tag":107,"props":2574,"children":2575},{},[2576],{"type":27,"value":2577},"responds to rejected or repeated cancellation requests.\n",{"type":22,"tag":107,"props":2579,"children":2580},{"class":109,"line":601},[2581],{"type":22,"tag":107,"props":2582,"children":2583},{"emptyLinePlaceholder":452},[2584],{"type":27,"value":455},{"type":22,"tag":107,"props":2586,"children":2587},{"class":109,"line":614},[2588],{"type":22,"tag":107,"props":2589,"children":2590},{},[2591],{"type":27,"value":2592},"## ADDED Requirements\n",{"type":22,"tag":107,"props":2594,"children":2595},{"class":109,"line":1531},[2596],{"type":22,"tag":107,"props":2597,"children":2598},{"emptyLinePlaceholder":452},[2599],{"type":27,"value":455},{"type":22,"tag":107,"props":2601,"children":2602},{"class":109,"line":1540},[2603],{"type":22,"tag":107,"props":2604,"children":2605},{},[2606],{"type":27,"value":2607},"### Requirement: Cancel an eligible order\n",{"type":22,"tag":107,"props":2609,"children":2610},{"class":109,"line":1549},[2611],{"type":22,"tag":107,"props":2612,"children":2613},{},[2614],{"type":27,"value":2615},"The service MUST allow the authenticated owner to cancel a PENDING order.\n",{"type":22,"tag":107,"props":2617,"children":2618},{"class":109,"line":1688},[2619],{"type":22,"tag":107,"props":2620,"children":2621},{"emptyLinePlaceholder":452},[2622],{"type":27,"value":455},{"type":22,"tag":107,"props":2624,"children":2625},{"class":109,"line":1697},[2626],{"type":22,"tag":107,"props":2627,"children":2628},{},[2629],{"type":27,"value":2630},"#### Scenario: Eligible order\n",{"type":22,"tag":107,"props":2632,"children":2633},{"class":109,"line":1875},[2634],{"type":22,"tag":107,"props":2635,"children":2636},{},[2637],{"type":27,"value":2638},"- **WHEN** the owner requests cancellation of a PENDING order\n",{"type":22,"tag":107,"props":2640,"children":2641},{"class":109,"line":1884},[2642],{"type":22,"tag":107,"props":2643,"children":2644},{},[2645],{"type":27,"value":2646},"- **THEN** the order becomes CANCELLED and one logical OrderCancelled event is recorded\n",{"type":22,"tag":107,"props":2648,"children":2649},{"class":109,"line":1893},[2650],{"type":22,"tag":107,"props":2651,"children":2652},{"emptyLinePlaceholder":452},[2653],{"type":27,"value":455},{"type":22,"tag":107,"props":2655,"children":2656},{"class":109,"line":1902},[2657],{"type":22,"tag":107,"props":2658,"children":2659},{},[2660],{"type":27,"value":2661},"#### Scenario: Shipment has started\n",{"type":22,"tag":107,"props":2663,"children":2665},{"class":109,"line":2664},17,[2666],{"type":22,"tag":107,"props":2667,"children":2668},{},[2669],{"type":27,"value":2670},"- **WHEN** the owner requests cancellation of a SHIPPED order\n",{"type":22,"tag":107,"props":2672,"children":2674},{"class":109,"line":2673},18,[2675],{"type":22,"tag":107,"props":2676,"children":2677},{},[2678],{"type":27,"value":2679},"- **THEN** the service rejects the request and leaves the order unchanged\n",{"type":22,"tag":107,"props":2681,"children":2683},{"class":109,"line":2682},19,[2684],{"type":22,"tag":107,"props":2685,"children":2686},{"emptyLinePlaceholder":452},[2687],{"type":27,"value":455},{"type":22,"tag":107,"props":2689,"children":2691},{"class":109,"line":2690},20,[2692],{"type":22,"tag":107,"props":2693,"children":2694},{},[2695],{"type":27,"value":2696},"#### Scenario: Not the owner\n",{"type":22,"tag":107,"props":2698,"children":2700},{"class":109,"line":2699},21,[2701],{"type":22,"tag":107,"props":2702,"children":2703},{},[2704],{"type":27,"value":2705},"- **WHEN** a different authenticated customer requests cancellation\n",{"type":22,"tag":107,"props":2707,"children":2709},{"class":109,"line":2708},22,[2710],{"type":22,"tag":107,"props":2711,"children":2712},{},[2713],{"type":27,"value":2679},{"type":22,"tag":107,"props":2715,"children":2717},{"class":109,"line":2716},23,[2718],{"type":22,"tag":107,"props":2719,"children":2720},{"emptyLinePlaceholder":452},[2721],{"type":27,"value":455},{"type":22,"tag":107,"props":2723,"children":2725},{"class":109,"line":2724},24,[2726],{"type":22,"tag":107,"props":2727,"children":2728},{},[2729],{"type":27,"value":2730},"### Requirement: Repeat a completed cancellation\n",{"type":22,"tag":107,"props":2732,"children":2734},{"class":109,"line":2733},25,[2735],{"type":22,"tag":107,"props":2736,"children":2737},{},[2738],{"type":27,"value":2739},"The service MUST handle repeated requests without creating a second cancellation.\n",{"type":22,"tag":107,"props":2741,"children":2743},{"class":109,"line":2742},26,[2744],{"type":22,"tag":107,"props":2745,"children":2746},{"emptyLinePlaceholder":452},[2747],{"type":27,"value":455},{"type":22,"tag":107,"props":2749,"children":2751},{"class":109,"line":2750},27,[2752],{"type":22,"tag":107,"props":2753,"children":2754},{},[2755],{"type":27,"value":2756},"#### Scenario: Repeated request\n",{"type":22,"tag":107,"props":2758,"children":2760},{"class":109,"line":2759},28,[2761],{"type":22,"tag":107,"props":2762,"children":2763},{},[2764],{"type":27,"value":2765},"- **WHEN** the owner repeats a successful cancellation request\n",{"type":22,"tag":107,"props":2767,"children":2769},{"class":109,"line":2768},29,[2770],{"type":22,"tag":107,"props":2771,"children":2772},{},[2773],{"type":27,"value":2774},"- **THEN** the service returns the existing CANCELLED state without a new transition\n",{"type":22,"tag":23,"props":2776,"children":2777},{},[2778,2780,2786],{"type":27,"value":2779},"This still leaves questions for the API contract: should a shipped order return ",{"type":22,"tag":14,"props":2781,"children":2783},{"className":2782},[],[2784],{"type":27,"value":2785},"409 Conflict",{"type":27,"value":2787},"? What should an unauthenticated caller see? Those decisions should be made explicitly and added to the relevant spec or existing API contract before implementation. A spec is useful because its gaps are visible to a reviewer.",{"type":22,"tag":23,"props":2789,"children":2790},{},[2791,2793,2799,2801,2807],{"type":27,"value":2792},"Keep the wording at the level of behavior. “Use optimistic locking in ",{"type":22,"tag":14,"props":2794,"children":2796},{"className":2795},[],[2797],{"type":27,"value":2798},"OrderRepository",{"type":27,"value":2800},"” is a possible design decision, not a customer-facing requirement. If the implementation can switch from one concurrency mechanism to another without changing the promise, the mechanism belongs in the design. OpenSpec's ",{"type":22,"tag":352,"props":2802,"children":2804},{"href":2360,"rel":2803},[356],[2805],{"type":27,"value":2806},"spec guidance",{"type":27,"value":2808}," makes the same distinction.",{"type":22,"tag":30,"props":2810,"children":2812},{"id":2811},"use-the-design-for-technical-trade-offs",[2813],{"type":27,"value":2814},"Use the design for technical trade-offs",{"type":22,"tag":23,"props":2816,"children":2817},{},[2818],{"type":27,"value":2819},"The design connects those requirements to the actual system. For this example, it might record:",{"type":22,"tag":97,"props":2821,"children":2823},{"className":1192,"code":2822,"language":316,"meta":7,"style":7},"# Design\n\n## Decisions\n- Use an atomic conditional update to change PENDING to CANCELLED.\n- Record the OrderCancelled event in the same database transaction.\n- On a lost race, reload the order and return its current state when it is CANCELLED.\n\n## Trade-offs\n- A conditional update avoids holding a database lock across the request.\n- The event must be durable even if the process stops after commit.\n",[2824],{"type":22,"tag":14,"props":2825,"children":2826},{"__ignoreMap":7},[2827,2835,2842,2850,2858,2866,2874,2881,2889,2897],{"type":22,"tag":107,"props":2828,"children":2829},{"class":109,"line":110},[2830],{"type":22,"tag":107,"props":2831,"children":2832},{},[2833],{"type":27,"value":2834},"# Design\n",{"type":22,"tag":107,"props":2836,"children":2837},{"class":109,"line":311},[2838],{"type":22,"tag":107,"props":2839,"children":2840},{"emptyLinePlaceholder":452},[2841],{"type":27,"value":455},{"type":22,"tag":107,"props":2843,"children":2844},{"class":109,"line":458},[2845],{"type":22,"tag":107,"props":2846,"children":2847},{},[2848],{"type":27,"value":2849},"## Decisions\n",{"type":22,"tag":107,"props":2851,"children":2852},{"class":109,"line":575},[2853],{"type":22,"tag":107,"props":2854,"children":2855},{},[2856],{"type":27,"value":2857},"- Use an atomic conditional update to change PENDING to CANCELLED.\n",{"type":22,"tag":107,"props":2859,"children":2860},{"class":109,"line":588},[2861],{"type":22,"tag":107,"props":2862,"children":2863},{},[2864],{"type":27,"value":2865},"- Record the OrderCancelled event in the same database transaction.\n",{"type":22,"tag":107,"props":2867,"children":2868},{"class":109,"line":601},[2869],{"type":22,"tag":107,"props":2870,"children":2871},{},[2872],{"type":27,"value":2873},"- On a lost race, reload the order and return its current state when it is CANCELLED.\n",{"type":22,"tag":107,"props":2875,"children":2876},{"class":109,"line":614},[2877],{"type":22,"tag":107,"props":2878,"children":2879},{"emptyLinePlaceholder":452},[2880],{"type":27,"value":455},{"type":22,"tag":107,"props":2882,"children":2883},{"class":109,"line":1531},[2884],{"type":22,"tag":107,"props":2885,"children":2886},{},[2887],{"type":27,"value":2888},"## Trade-offs\n",{"type":22,"tag":107,"props":2890,"children":2891},{"class":109,"line":1540},[2892],{"type":22,"tag":107,"props":2893,"children":2894},{},[2895],{"type":27,"value":2896},"- A conditional update avoids holding a database lock across the request.\n",{"type":22,"tag":107,"props":2898,"children":2899},{"class":109,"line":1549},[2900],{"type":22,"tag":107,"props":2901,"children":2902},{},[2903],{"type":27,"value":2904},"- The event must be durable even if the process stops after commit.\n",{"type":22,"tag":23,"props":2906,"children":2907},{},[2908,2910,2916],{"type":27,"value":2909},"That is a sketch, not production-ready code. It shows where the technical reasoning lives. A database lock could be valid instead; the design should explain why the chosen approach fits this service. If the event is required for downstream consistency, the ",{"type":22,"tag":352,"props":2911,"children":2913},{"href":2912},"/transactional-outbox/",[2914],{"type":27,"value":2915},"Transactional Outbox",{"type":27,"value":2917}," pattern is one way to make state and publication intent atomic.",{"type":22,"tag":23,"props":2919,"children":2920},{},[2921,2923,2928],{"type":27,"value":2922},"Design is not compulsory paperwork for every change. OpenSpec can omit ",{"type":22,"tag":14,"props":2924,"children":2926},{"className":2925},[],[2927],{"type":27,"value":2341},{"type":27,"value":2929}," when the change does not need a separate technical decision. It becomes valuable when the choice has consequences for concurrency, migrations, security, dependencies, or recovery.",{"type":22,"tag":30,"props":2931,"children":2933},{"id":2932},"turn-the-plan-into-work-you-can-verify",[2934],{"type":27,"value":2935},"Turn the plan into work you can verify",{"type":22,"tag":23,"props":2937,"children":2938},{},[2939,2944],{"type":22,"tag":14,"props":2940,"children":2942},{"className":2941},[],[2943],{"type":27,"value":2355},{"type":27,"value":2945}," turns the spec and design into an implementation checklist. Each item should end in an observable result:",{"type":22,"tag":97,"props":2947,"children":2949},{"className":1192,"code":2948,"language":316,"meta":7,"style":7},"- [ ] Add the cancellation API and authorization checks.\n- [ ] Persist the order transition and event intent atomically.\n- [ ] Test eligible, shipped, unauthorized, and repeated requests.\n- [ ] Run the project checks and review the API response contract.\n",[2950],{"type":22,"tag":14,"props":2951,"children":2952},{"__ignoreMap":7},[2953,2961,2969,2977],{"type":22,"tag":107,"props":2954,"children":2955},{"class":109,"line":110},[2956],{"type":22,"tag":107,"props":2957,"children":2958},{},[2959],{"type":27,"value":2960},"- [ ] Add the cancellation API and authorization checks.\n",{"type":22,"tag":107,"props":2962,"children":2963},{"class":109,"line":311},[2964],{"type":22,"tag":107,"props":2965,"children":2966},{},[2967],{"type":27,"value":2968},"- [ ] Persist the order transition and event intent atomically.\n",{"type":22,"tag":107,"props":2970,"children":2971},{"class":109,"line":458},[2972],{"type":22,"tag":107,"props":2973,"children":2974},{},[2975],{"type":27,"value":2976},"- [ ] Test eligible, shipped, unauthorized, and repeated requests.\n",{"type":22,"tag":107,"props":2978,"children":2979},{"class":109,"line":575},[2980],{"type":22,"tag":107,"props":2981,"children":2982},{},[2983],{"type":27,"value":2984},"- [ ] Run the project checks and review the API response contract.\n",{"type":22,"tag":23,"props":2986,"children":2987},{},[2988],{"type":27,"value":2989},"The spec says what must be true. The tasks say what work remains. Tests provide evidence that the implementation behaves as promised. None of these files replaces the others.",{"type":22,"tag":23,"props":2991,"children":2992},{},[2993,2995,3002],{"type":27,"value":2994},"Before code starts, review the three questions OpenSpec's ",{"type":22,"tag":352,"props":2996,"children":2999},{"href":2997,"rel":2998},"https://openspec.dev/docs/quickstart",[356],[3000],{"type":27,"value":3001},"quickstart",{"type":27,"value":3003}," emphasizes: Is the proposal the right problem and size? Would you accept the spec's scenarios as done? Do the tasks cover those scenarios without adding unrelated work? Fixing a sentence now is cheaper than reviewing a large implementation of the wrong rule.",{"type":22,"tag":30,"props":3005,"children":3007},{"id":3006},"give-the-agent-a-bounded-handoff",[3008],{"type":27,"value":3009},"Give the agent a bounded handoff",{"type":22,"tag":23,"props":3011,"children":3012},{},[3013],{"type":27,"value":3014},"Once the artifacts are agreed, the implementation request can be short:",{"type":22,"tag":97,"props":3016,"children":3018},{"className":433,"code":3017,"language":27,"meta":7,"style":7},"Apply the add-order-cancellation change. Read its proposal, spec,\ndesign, and tasks before editing code. Implement one task at a time.\nRun the relevant tests and report any requirement that is ambiguous\nor cannot be met by the current design. Do not invent a new business rule.\n",[3019],{"type":22,"tag":14,"props":3020,"children":3021},{"__ignoreMap":7},[3022,3030,3038,3046],{"type":22,"tag":107,"props":3023,"children":3024},{"class":109,"line":110},[3025],{"type":22,"tag":107,"props":3026,"children":3027},{},[3028],{"type":27,"value":3029},"Apply the add-order-cancellation change. Read its proposal, spec,\n",{"type":22,"tag":107,"props":3031,"children":3032},{"class":109,"line":311},[3033],{"type":22,"tag":107,"props":3034,"children":3035},{},[3036],{"type":27,"value":3037},"design, and tasks before editing code. Implement one task at a time.\n",{"type":22,"tag":107,"props":3039,"children":3040},{"class":109,"line":458},[3041],{"type":22,"tag":107,"props":3042,"children":3043},{},[3044],{"type":27,"value":3045},"Run the relevant tests and report any requirement that is ambiguous\n",{"type":22,"tag":107,"props":3047,"children":3048},{"class":109,"line":575},[3049],{"type":22,"tag":107,"props":3050,"children":3051},{},[3052],{"type":27,"value":3053},"or cannot be met by the current design. Do not invent a new business rule.\n",{"type":22,"tag":23,"props":3055,"children":3056},{},[3057,3059,3064,3066,3071,3073,3079],{"type":27,"value":3058},"OpenSpec calls this the ",{"type":22,"tag":691,"props":3060,"children":3061},{},[3062],{"type":27,"value":3063},"apply",{"type":27,"value":3065}," phase. It tracks progress in the checkboxes in ",{"type":22,"tag":14,"props":3067,"children":3069},{"className":3068},[],[3070],{"type":27,"value":2355},{"type":27,"value":3072},"; it is not a promise that the CLI has turned the spec into correct code. The ",{"type":22,"tag":352,"props":3074,"children":3076},{"href":2997,"rel":3075},[356],[3077],{"type":27,"value":3078},"OpenSpec workflow",{"type":27,"value":3080}," explicitly allows the artifacts to be corrected while work is underway.",{"type":22,"tag":23,"props":3082,"children":3083},{},[3084,3086,3092],{"type":27,"value":3085},"This is particularly useful with AI agents. A new session can read the change and the remaining tasks instead of relying on an old chat transcript. The agent still needs access to the actual codebase and its tests, and a person still needs to review behavior at the boundary of the change. If several agents work in parallel, give each a separate ",{"type":22,"tag":352,"props":3087,"children":3089},{"href":3088},"/git-worktrees-parallel-development/",[3090],{"type":27,"value":3091},"Git worktree",{"type":27,"value":3093}," and keep ownership of integration clear.",{"type":22,"tag":30,"props":3095,"children":3097},{"id":3096},"when-implementation-reveals-a-missing-scenario",[3098],{"type":27,"value":3099},"When implementation reveals a missing scenario",{"type":22,"tag":23,"props":3101,"children":3102},{},[3103,3105,3110,3112,3117],{"type":27,"value":3104},"Suppose a test exposes a race: two cancellation requests read the same ",{"type":22,"tag":14,"props":3106,"children":3108},{"className":3107},[],[3109],{"type":27,"value":2140},{"type":27,"value":3111}," order before either writes. The original repeated-request scenario covers a request made ",{"type":22,"tag":341,"props":3113,"children":3114},{},[3115],{"type":27,"value":3116},"after",{"type":27,"value":3118}," cancellation, but does not say what simultaneous requests should do.",{"type":22,"tag":23,"props":3120,"children":3121},{},[3122],{"type":27,"value":3123},"Do not silently choose the easiest behavior in code. Add the missing scenario to the spec:",{"type":22,"tag":97,"props":3125,"children":3127},{"className":1192,"code":3126,"language":316,"meta":7,"style":7},"#### Scenario: Concurrent cancellation requests\n- **WHEN** two requests from the owner try to cancel the same PENDING order\n- **THEN** exactly one cancellation transition and one logical event are recorded\n- **AND** both requests can observe the final CANCELLED state\n",[3128],{"type":22,"tag":14,"props":3129,"children":3130},{"__ignoreMap":7},[3131,3139,3147,3155],{"type":22,"tag":107,"props":3132,"children":3133},{"class":109,"line":110},[3134],{"type":22,"tag":107,"props":3135,"children":3136},{},[3137],{"type":27,"value":3138},"#### Scenario: Concurrent cancellation requests\n",{"type":22,"tag":107,"props":3140,"children":3141},{"class":109,"line":311},[3142],{"type":22,"tag":107,"props":3143,"children":3144},{},[3145],{"type":27,"value":3146},"- **WHEN** two requests from the owner try to cancel the same PENDING order\n",{"type":22,"tag":107,"props":3148,"children":3149},{"class":109,"line":458},[3150],{"type":22,"tag":107,"props":3151,"children":3152},{},[3153],{"type":27,"value":3154},"- **THEN** exactly one cancellation transition and one logical event are recorded\n",{"type":22,"tag":107,"props":3156,"children":3157},{"class":109,"line":575},[3158],{"type":22,"tag":107,"props":3159,"children":3160},{},[3161],{"type":27,"value":3162},"- **AND** both requests can observe the final CANCELLED state\n",{"type":22,"tag":23,"props":3164,"children":3165},{},[3166,3168,3173],{"type":27,"value":3167},"Then revisit the design and tasks. The conditional update may let one request win and the other load the newly cancelled row; add a concurrency test that actually runs both requests. If the chosen API semantics require one request to report a conflict instead, record that decision in the spec and test it. The point is that ",{"type":22,"tag":691,"props":3169,"children":3170},{},[3171],{"type":27,"value":3172},"the spec changes when understanding changes",{"type":27,"value":3174},". It should describe the behavior the team intends to ship, not preserve an early guess for appearances.",{"type":22,"tag":30,"props":3176,"children":3178},{"id":3177},"verify-the-implementation-then-archive-the-change",[3179],{"type":27,"value":3180},"Verify the implementation, then archive the change",{"type":22,"tag":23,"props":3182,"children":3183},{},[3184],{"type":27,"value":3185},"The final review needs evidence for each important scenario:",{"type":22,"tag":937,"props":3187,"children":3188},{},[3189,3205],{"type":22,"tag":941,"props":3190,"children":3191},{},[3192],{"type":22,"tag":945,"props":3193,"children":3194},{},[3195,3200],{"type":22,"tag":949,"props":3196,"children":3197},{},[3198],{"type":27,"value":3199},"Scenario",{"type":22,"tag":949,"props":3201,"children":3202},{},[3203],{"type":27,"value":3204},"Useful evidence",{"type":22,"tag":965,"props":3206,"children":3207},{},[3208,3221,3234,3246,3259,3272],{"type":22,"tag":945,"props":3209,"children":3210},{},[3211,3216],{"type":22,"tag":972,"props":3212,"children":3213},{},[3214],{"type":27,"value":3215},"Eligible owner",{"type":22,"tag":972,"props":3217,"children":3218},{},[3219],{"type":27,"value":3220},"API test confirms the state transition and response",{"type":22,"tag":945,"props":3222,"children":3223},{},[3224,3229],{"type":22,"tag":972,"props":3225,"children":3226},{},[3227],{"type":27,"value":3228},"Shipped order",{"type":22,"tag":972,"props":3230,"children":3231},{},[3232],{"type":27,"value":3233},"API test confirms rejection and unchanged state",{"type":22,"tag":945,"props":3235,"children":3236},{},[3237,3242],{"type":22,"tag":972,"props":3238,"children":3239},{},[3240],{"type":27,"value":3241},"Different customer",{"type":22,"tag":972,"props":3243,"children":3244},{},[3245],{"type":27,"value":3233},{"type":22,"tag":945,"props":3247,"children":3248},{},[3249,3254],{"type":22,"tag":972,"props":3250,"children":3251},{},[3252],{"type":27,"value":3253},"Repeated request",{"type":22,"tag":972,"props":3255,"children":3256},{},[3257],{"type":27,"value":3258},"Test confirms no second logical transition or event",{"type":22,"tag":945,"props":3260,"children":3261},{},[3262,3267],{"type":22,"tag":972,"props":3263,"children":3264},{},[3265],{"type":27,"value":3266},"Concurrent requests",{"type":22,"tag":972,"props":3268,"children":3269},{},[3270],{"type":27,"value":3271},"Integration test confirms the chosen race behavior",{"type":22,"tag":945,"props":3273,"children":3274},{},[3275,3280],{"type":22,"tag":972,"props":3276,"children":3277},{},[3278],{"type":27,"value":3279},"Downstream event",{"type":22,"tag":972,"props":3281,"children":3282},{},[3283],{"type":27,"value":3284},"Test confirms the event intent commits with the order change",{"type":22,"tag":23,"props":3286,"children":3287},{},[3288],{"type":27,"value":3289},"Run OpenSpec's structural validation too:",{"type":22,"tag":97,"props":3291,"children":3293},{"className":99,"code":3292,"language":101,"meta":7,"style":7},"openspec validate add-order-cancellation --strict\nopenspec status --change add-order-cancellation\n",[3294],{"type":22,"tag":14,"props":3295,"children":3296},{"__ignoreMap":7},[3297,3318],{"type":22,"tag":107,"props":3298,"children":3299},{"class":109,"line":110},[3300,3304,3309,3313],{"type":22,"tag":107,"props":3301,"children":3302},{"style":114},[3303],{"type":27,"value":1325},{"type":22,"tag":107,"props":3305,"children":3306},{"style":120},[3307],{"type":27,"value":3308}," validate",{"type":22,"tag":107,"props":3310,"children":3311},{"style":120},[3312],{"type":27,"value":2253},{"type":22,"tag":107,"props":3314,"children":3315},{"style":126},[3316],{"type":27,"value":3317}," --strict\n",{"type":22,"tag":107,"props":3319,"children":3320},{"class":109,"line":311},[3321,3325,3329,3333],{"type":22,"tag":107,"props":3322,"children":3323},{"style":114},[3324],{"type":27,"value":1325},{"type":22,"tag":107,"props":3326,"children":3327},{"style":120},[3328],{"type":27,"value":2283},{"type":22,"tag":107,"props":3330,"children":3331},{"style":126},[3332],{"type":27,"value":2288},{"type":22,"tag":107,"props":3334,"children":3335},{"style":120},[3336],{"type":27,"value":2293},{"type":22,"tag":23,"props":3338,"children":3339},{},[3340],{"type":27,"value":3341},"Validation checks the change artifacts; it cannot prove that the service meets its requirements. Run the project's tests, inspect the diff, and compare the result with the spec. If code and spec disagree, decide which is wrong before declaring the task complete.",{"type":22,"tag":23,"props":3343,"children":3344},{},[3345,3347,3353,3355,3360],{"type":27,"value":3346},"When the implementation and checklist are complete, archive the change. For a behavior-changing capability, OpenSpec moves the change folder to ",{"type":22,"tag":14,"props":3348,"children":3350},{"className":3349},[],[3351],{"type":27,"value":3352},"openspec/changes/archive/",{"type":27,"value":3354}," and merges the delta requirements into the main specs, so they describe the system as built. The ",{"type":22,"tag":352,"props":3356,"children":3358},{"href":2997,"rel":3357},[356],[3359],{"type":27,"value":3001},{"type":27,"value":3361}," walks through this step. Use your team's normal Git review and integration process around it; OpenSpec does not replace that process.",{"type":22,"tag":30,"props":3363,"children":3365},{"id":3364},"keep-the-method-proportional-to-the-change",[3366],{"type":27,"value":3367},"Keep the method proportional to the change",{"type":22,"tag":23,"props":3369,"children":3370},{},[3371,3373,3379],{"type":27,"value":3372},"SDD earns its keep when an unclear decision would be expensive: public API behavior, cross-service workflows, state transitions, migrations, or security rules. It is lighter when you keep each change small and write only the artifacts it needs. A typo fix does not require an invented business requirement. OpenSpec supports ",{"type":22,"tag":14,"props":3374,"children":3376},{"className":3375},[],[3377],{"type":27,"value":3378},"skip_specs: true",{"type":27,"value":3380}," for changes with no spec-level behavior change, such as documentation or some tooling work, and allows a design to be omitted when no design decision warrants it.",{"type":22,"tag":23,"props":3382,"children":3383},{},[3384],{"type":27,"value":3385},"For me, the value is not producing more Markdown. It is discovering disagreement while the change is still easy to reshape, and giving implementation and review a shared definition of done. An AI agent can accelerate the code. A good spec makes it easier to tell whether that acceleration took us in the right direction.",{"type":22,"tag":30,"props":3387,"children":3389},{"id":3388},"further-reading",[3390],{"type":27,"value":3391},"Further reading",{"type":22,"tag":42,"props":3393,"children":3394},{},[3395,3405,3415],{"type":22,"tag":46,"props":3396,"children":3397},{},[3398,3404],{"type":22,"tag":352,"props":3399,"children":3401},{"href":2997,"rel":3400},[356],[3402],{"type":27,"value":3403},"OpenSpec: Quickstart",{"type":27,"value":1080},{"type":22,"tag":46,"props":3406,"children":3407},{},[3408,3414],{"type":22,"tag":352,"props":3409,"children":3411},{"href":2360,"rel":3410},[356],[3412],{"type":27,"value":3413},"OpenSpec: spec-driven schema",{"type":27,"value":1080},{"type":22,"tag":46,"props":3416,"children":3417},{},[3418,3425],{"type":22,"tag":352,"props":3419,"children":3422},{"href":3420,"rel":3421},"https://openspec.dev/docs/cli",[356],[3423],{"type":27,"value":3424},"OpenSpec: CLI reference",{"type":27,"value":1080},{"type":22,"tag":305,"props":3427,"children":3428},{},[3429],{"type":27,"value":309},{"title":7,"searchDepth":311,"depth":311,"links":3431},[3432,3433,3434,3435,3436,3437,3438,3439,3440,3441],{"id":2078,"depth":311,"text":2081},{"id":2216,"depth":311,"text":2219},{"id":2510,"depth":311,"text":2513},{"id":2811,"depth":311,"text":2814},{"id":2932,"depth":311,"text":2935},{"id":3006,"depth":311,"text":3009},{"id":3096,"depth":311,"text":3099},{"id":3177,"depth":311,"text":3180},{"id":3364,"depth":311,"text":3367},{"id":3388,"depth":311,"text":3391},"content:posts:spec-driven-development-openspec.md","posts/spec-driven-development-openspec.md","posts/spec-driven-development-openspec",[3446,3447,3448,3449,3453,3457,3461,3465,3469,3473,3477,3481,3485,3489,3493,3497,3501,3505,3509,3513,3517,3521,3525,3526,3530,3534,3538,3541,3545,3549],{"_path":324,"title":325,"date":327},{"_path":883,"title":884,"date":886},{"_path":2044,"title":2045,"date":2047},{"_path":3450,"title":3451,"date":3452},"/posts/git-worktrees-parallel-development","Git Worktrees: Parallel Work Without Clobbering Your Workspace","2025-07-15T11:00:00.000Z",{"_path":3454,"title":3455,"date":3456},"/posts/transactional-outbox","Transactional Outbox: Saving State Without Losing Events","2025-06-15T11:00:00.000Z",{"_path":3458,"title":3459,"date":3460},"/posts/api-evolution-strategy","Your API Needs an Evolution Strategy","2025-05-15T11:00:00.000Z",{"_path":3462,"title":3463,"date":3464},"/posts/n-plus-one-queries","The N+1 Query Problem: Finding and Fixing a Slow Search Endpoint","2025-04-15T11:00:00.000Z",{"_path":3466,"title":3467,"date":3468},"/posts/relational-vs-nosql-databases","Relational vs. NoSQL Databases: Why PostgreSQL Is My Default","2025-03-15T11:00:00.000Z",{"_path":3470,"title":3471,"date":3472},"/posts/hidden-cost-event-driven-architectures","The Hidden Cost of Event-Driven Architectures","2025-02-15T11:00:00.000Z",{"_path":3474,"title":3475,"date":3476},"/posts/gpt-sql-integration","Natural Language to SQL with GPT: Build a Safer Query Interface","2025-01-15T11:00:00.000Z",{"_path":3478,"title":3479,"date":3480},"/posts/java-security-best-practices","Java Security Best Practices: Preventing Vulnerabilities and Threats","2024-08-15T11:00:00.000Z",{"_path":3482,"title":3483,"date":3484},"/posts/circuit-breaking-resilience","Circuit Breaking in Distributed Systems","2024-07-15T11:00:00.000Z",{"_path":3486,"title":3487,"date":3488},"/posts/maven-and-github-actions","Automating Java Builds with Maven and GitHub Actions CI/CD","2024-06-15T11:00:00.000Z",{"_path":3490,"title":3491,"date":3492},"/posts/pattern-matching-sealed-classes-java21","A Guide to Pattern Matching and Sealed Classes in Java 21","2024-05-15T11:00:00.000Z",{"_path":3494,"title":3495,"date":3496},"/posts/async-java-completable-vs-reactive","Asynchronous Programming in Java: CompletableFuture vs Reactive Streams","2024-04-15T11:00:00.000Z",{"_path":3498,"title":3499,"date":3500},"/posts/mastering-concurrency","Mastering Concurrency in Java: Threads, Executors, and Virtual Threads","2024-03-15T11:00:00.000Z",{"_path":3502,"title":3503,"date":3504},"/posts/microservices-mistakes","Common Mistakes When Developing Microservices","2024-02-15T11:00:00.000Z",{"_path":3506,"title":3507,"date":3508},"/posts/tdd-testing","Test-Driven Development (TDD) in Java with JUnit and Mockito","2024-01-15T11:00:00.000Z",{"_path":3510,"title":3511,"date":3512},"/posts/single-table-design","Single Table Design","2023-12-27T11:00:00.000Z",{"_path":3514,"title":3515,"date":3516},"/posts/java-17","Exploring Java 17's New Features","2023-10-08T11:00:00.000Z",{"_path":3518,"title":3519,"date":3520},"/posts/hexagonal-architecture","Introduction to Hexagonal Architecture","2023-10-02 18:00:00",{"_path":3522,"title":3523,"date":3524},"/posts/terraform","Unleashing the Power of Infrastructure as Code (IaC)","2023-09-24 18:00:00",{"_path":4,"title":8,"date":10},{"_path":3527,"title":3528,"date":3529},"/posts/api-first-principles","API First principles","2023-09-01 22:15:18",{"_path":3531,"title":3532,"date":3533},"/posts/how-to-configure-ssh-key-based-authentication","How To Configure SSH Key-Based Authentication","2021-02-21 03:46:12",{"_path":3535,"title":3536,"date":3537},"/posts/algorithms-binary-search-tree","Algorithms - Binary Search Tree in kotlin","2020-12-06T11:00:00.000Z",{"_path":3539,"title":3540,"date":3537},"/posts/how-to-create-a-twitter-bot","How to create a Twitter bot",{"_path":3542,"title":3543,"date":3544},"/posts/manage-aws-infrastructure-with-terraform","Bootstrap complete Java application infrastructure in AWS with Terraform","2020-12-02T11:00:00.000Z",{"_path":3546,"title":3547,"date":3548},"/posts/algorithms-working-with-trees","Algorithms - Binary tree traversals in kotlin","2020-11-24T11:00:00.000Z",{"_path":3550,"title":3551,"date":3552},"/posts/playing-with-ocr-libraries","Playing with OCR libraries","2020-11-22T11:00:00.000Z",1791700922321]