Here's a pattern every PM recognizes: you spend days writing a spec. You present it in a planning meeting. Engineers nod. Work begins. Two weeks later, what ships looks nothing like what you specified.
Nobody is at fault. The PM wrote a clear document. The engineers built a working feature. The problem is that the spec described one reality and the code demanded another. When engineers hit that friction, they adapt to the code — not the spec. Every time.
If you want specs that engineers actually follow, you need specs that align with what the code demands. Here are five principles that make that happen.
Principle 1: Reference Actual Code Paths, Not Abstract Flows
Most specs describe features as abstract flows: "User clicks button, system processes request, result displays on screen." This tells the engineer what should happen but not where it happens in the codebase.
Abstract flow:
When a user submits the form, the system validates the input, creates the record, and returns a success response.
Code path reference:
Form submission in
CreateProjectForm.tsxcalls theuseCreateProjectmutation (defined insrc/hooks/use-projects.ts), which POSTs to/api/projects. The route handler insrc/projects/projects.controller.tsdelegates toProjectService.create(), which runs a transaction across theprojects,project_members, andactivity_logtables.
The second version draws a line through the codebase. An engineer reading it knows exactly which files they're working in, which patterns to follow, and which existing abstractions to use. There's no ambiguity, no "I think the PM meant this endpoint," no discovery phase.
This doesn't mean PMs need to write code paths from memory. A codebase-aware tool surfaces these paths automatically. The PM's job is to describe the feature. The tool's job is to ground it in the code.
Why engineers follow it: When a spec references the actual code path, deviating from the spec means deviating from an explicit plan that accounts for the architecture. Engineers deviate from abstract specs because the code demands it. They follow concrete specs because the code aligns with them.
Principle 2: Include Schema Context
The data model is the most important technical context a spec can include, and the most commonly omitted. A spec that says "add a priority field to tasks" without referencing the schema is asking engineers to make architectural decisions that should be product decisions.
What type is the priority field? An enum with fixed values? An integer for custom ordering? A foreign key to a priority_levels table that supports tenant-specific configuration? Each choice has product implications — and the answer often depends on what's already in the schema.
Spec without schema context:
Tasks should have a priority: low, medium, high, urgent.
Spec with schema context:
Add a
prioritycolumn to thetaskstable (src/tasks/entities/task.entity.ts). The codebase uses string enums for similar fields — seeproject_statusin the projects entity. Define aTaskPriorityenum with valueslow,medium,high,urgent. Default tomedium. Add an index on(org_id, priority)to support the filtered dashboard query inTaskRepository.findByOrg().
The schema-aware version makes three things explicit: the column type (string enum, matching existing patterns), the default value (a product decision), and the index (a performance requirement that an engineer would need to discover independently).
Why engineers follow it: Schema decisions in specs eliminate the most common source of spec deviation — engineers making data model choices that differ from what the PM intended. When the spec says "string enum matching existing patterns," the engineer isn't guessing.
Principle 3: Name the Edge Cases the Code Already Handles
Every mature codebase handles edge cases that no spec ever mentioned. Soft delete logic. Rate limiting. Input sanitization. Retry mechanisms. These aren't features — they're infrastructure that affects every new feature.
When a spec doesn't acknowledge existing edge case handling, engineers face a recurring question: does this new feature need to respect the existing patterns, or is it an exception? Without guidance, each engineer answers differently.
Generic spec:
Users can delete their own comments.
Edge-case-aware spec:
Implement comment deletion using the existing soft delete pattern (BaseEntity's
deletedAtcolumn, automatically filtered byTypeormBaseRepository). TheCommentServiceshould callthis.commentRepository.delete(id, { cascade: true })to soft-delete replies viacascadeDelete(). Note: the existingActivityLogrecords referencing the comment should be preserved (not cascade-deleted) for audit purposes — the activity log'sentityIdforeign key is nullable to support this pattern, consistent with how project deletion is handled.
This spec doesn't just describe the feature. It describes how the feature interacts with existing infrastructure. The engineer doesn't need to research how deletes work in this codebase, whether cascade behavior is expected, or how the activity log handles deleted entities. The spec already answered those questions.
Why engineers follow it: Engineers deviate from specs most often when the spec ignores something they know about the codebase. When the spec demonstrates awareness of existing patterns, it earns technical credibility. Engineers follow specs they trust.
Principle 4: Write Acceptance Criteria That Match Test Assertions
Vague acceptance criteria create vague tests. "User can see their notifications" could mean five different things depending on who writes the test. Code-aware acceptance criteria translate directly into test assertions, removing ambiguity about what "done" means.
Vague acceptance criteria:
- User can view their notifications
- Notifications are sorted by most recent
- User can mark notifications as read
Testable acceptance criteria:
GET /api/notificationsreturnsPaginatedResponse<NotificationDto>with default sortcreated_at DESC. Response includestotal,page,pageSize, anditemsarray matching the existing pagination contract.- Each
NotificationDtoincludes:id,type(enum:comment,mention,assignment,status_change),entityType,entityId,message,read(boolean),createdAt.PATCH /api/notifications/:idaccepts{ read: true }and returns the updated notification. Batch endpoint:PATCH /api/notifications/batchaccepts{ ids: string[], read: true }.- Unread count:
GET /api/notifications/unread-countreturns{ count: number }. This endpoint is called by theNotificationBellcomponent on mount and after each mark-as-read action.
An engineer reading the second version can write tests directly from the spec. The API contract is explicit. The data shape is defined. The component behavior is specified. There's no interpretation gap.
Why engineers follow it: When acceptance criteria match test assertions, the spec becomes the test plan. Engineers don't need to invent test cases — they implement the ones the spec defines. The spec is followed because it's directly useful, not because process demands it.
Principle 5: Link to the Files That Need to Change
This is the simplest principle and the most impactful. A spec that tells engineers which files to modify gets followed more than a spec that describes abstract changes to abstract systems.
Abstract change list:
- Update the API to support the new field
- Add the field to the database
- Update the frontend form
- Add tests
Explicit file list:
Files to modify:
src/tasks/entities/task.entity.ts— addprioritycolumnsrc/tasks/domain/task.ts— addpriorityfield to domain class, updateTaskCreateFieldsandTaskUpdateFieldstypessrc/tasks/repositories/task.repository.ts— no changes needed (fromEntitymapping auto-includes new fields)src/tasks/dto/create-task.dto.ts— addpriorityfield with validationsrc/tasks/dto/task-response.dto.ts— addpriorityto responsesrc/tasks/tasks.service.ts— no changes needed (passes through DTO)src/tasks/tasks.controller.ts— no changes needed (existing endpoints)New files:
src/migrations/{timestamp}-AddTaskPriority.tsFrontend:
src/components/tasks/CreateTaskForm.tsx— add priority selectorsrc/components/tasks/TaskCard.tsx— display priority badgesrc/hooks/use-tasks.ts— updateCreateTaskInputtypeTests:
src/tasks/__tests__/tasks.service.spec.ts— add priority to creation and update tests
This list takes two minutes to compile when a tool can read the codebase. Without a tool, it takes an engineer thirty minutes of exploration — and the exploration happens after the spec is written, creating a gap where deviation is inevitable.
Why engineers follow it: An explicit file list turns a spec from a product wish into an engineering checklist. Engineers work through checklists. They deviate from wishes.
The Meta-Principle
All five principles share a common thread: specs that engineers follow are specs that demonstrate understanding of the codebase.
Engineers don't ignore specs out of arrogance. They ignore specs that ignore their reality. When a spec references actual code paths, actual schemas, actual patterns, and actual files, it signals that the plan was made with the implementation in mind. Engineers follow plans that account for the terrain.
You don't need to be technical to write these specs. You need a tool that reads the codebase and surfaces the context that makes specs concrete. That's what Stonewall does — it turns product intent into code-grounded specs that engineers follow because they reflect the system those engineers work in every day.