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