DEV Community

Thomas Landgraf
Thomas Landgraf

Posted on

Why I Shipped Two Artifact Mechanisms In My VS Code Extension — Not One

A specification is more than text. It comes with a wireframe, the regulatory PDF it answers to, the API contract it has to honour, the stakeholder slide deck someone negotiated against. For a long time, none of that lived in my spec tree. The Markdown files were git-tracked; the evidence behind them rotted in Confluence, in shared drives, in pasted-and-lost screenshots in chat.

This week I shipped a fix in v0.9.7 of the VS Code extension I maintain. The shape of the fix is the part I want to write about, because the obvious version of it would have been wrong.

Full disclosure: I'm the creator of SPECLAN, a VS Code extension that manages product specifications as Markdown files with YAML frontmatter — Git-native, one file per requirement, organized in a hierarchical tree. The pattern (Markdown + YAML + Git) works without the tool; SPECLAN is just where I observed and engineered around the design problem below.

The obvious version: one artifact mechanism, governance everywhere

Specs travel in a lifecycle: draft → review → approved → in-development → under-test → released → deprecated. Once a spec is approved, the team has agreed on its content; the implementation gets built, tested, and shipped against that agreement. The natural next thought: artifact attachments should follow the same discipline. If you can't silently rewrite a released requirement's body, you also can't silently swap out the API contract attached to it. So: one universal artifact mechanism, Change Request governance everywhere.

I sketched that. I didn't ship it. Two days into testing, I realized the obvious version forced ceremony onto material that had no business going through a review cycle.

The thing the obvious version got wrong

Consider the artifacts a project actually accumulates over a year:

  • The login-flow-mockup.png attached to a specific feature. Owned by that feature. Means nothing without it.
  • The api-contract.json attached to a specific requirement. Verified against that requirement. Drift from it is a real bug.
  • architecture/system-overview.png. Referenced by half the specs. Owned by no spec.
  • brand-guidelines.pdf. Cited by every customer-facing surface. Owned by no spec.
  • regulatory/pet-handling-compliance.md. Read whenever a new compliance-touching feature is drafted. Owned by no spec.
  • meeting-notes/2026-04-22-kickoff.md. Reference material for context. Owned by no spec.

The first two are evidence pinned to a specific entity, with a specific lifecycle, where governance is the whole point. The other four are reference material — shared across many specs, owned by none, not bound to any specification's release cycle.

If I force-march the second class through a Change Request workflow, every architecture-diagram update needs a 4-stage review against… what spec? It doesn't belong to one. The reviewer would be approving a change with no parent entity to compare against. The ceremony has no anchor.

So I shipped two mechanisms with deliberately different governance:

Axis Spec Artifacts Project Artifacts
Hierarchy Flat — top-level files only Full nested filetree
Change Request governance Mandatory on locked specs None — direct file ops always
Filename sanitation Enforced at every Add path Whatever the filesystem accepts
Scope Pinned to one spec entity Project-wide reference, owned by no one
Visual surface Section at the bottom of a spec page Third entry in the project tree

The split falls out of one observation: locking applies to entities with status; project folders don't have status. A spec has a status. A project folder doesn't. There is no "released project" to gate changes against, so a unified governance mechanism would have no anchor for one of the two cases.

How Spec Artifacts work

Every feature, requirement, or change request gets its own artifacts/ folder right next to its .md file:

speclan/features/F-2419-login-flow/
├── F-2419-login-flow.md
├── artifacts/
│   ├── login-mockup.png
│   ├── api-response-schema.json
│   └── stakeholder-approval.pdf
└── change-requests/
Enter fullscreen mode Exit fullscreen mode

In the WYSIWYG editor, an Artifacts section appears at the bottom of the spec page. Drag-drop or pick to add. Click to open in the registered default viewer. Image artifacts (PNG, JPEG, GIF, WebP, SVG) get an extra trick: they can be embedded inline in the spec body as illustrations, diagrams, or mockups — not as separate attachment rows. The on-disk artifact stays a plain ![alt](artifacts/file.png) markdown link, so the spec is portable to any markdown viewer.

The Change Request gate kicks in on locked specs. The dispatch table:

Parent spec status Add / Remove behavior
draft review
in-development under-test
deprecated Add/remove disabled

On a locked spec, dropping in an artifact doesn't overwrite the canonical file. The system creates a Change Request in draft status and stages your file under a CR-suffixed disk name. The CR flows through the standard draft → review → approved → in-development → under-test → released lifecycle. When you click Merge, the staged file becomes canonical.

The reason for this discipline isn't audit hygiene — it's artifact / implementation drift. A spec in released is one whose word the implementation team has built against. The API contract attached to it is the contract the build was verified against. If that contract silently shifts, the implementation no longer matches the evidence and nobody knows. The CR-staging mechanism prevents the silent shift; an approved CR doubles as a signal into the implementation flow — same trigger releases the new evidence and tells the implementation it needs to update.

How Project Artifacts work

A single project-wide directory at speclan/artifacts/. Folders nest to any depth. Drop files in via the picker, drag from your OS file manager, or organize subfolders directly through the filesystem — the on-disk filetree is the source of truth, the editor's tree view auto-refreshes via a filesystem watcher.

No CR governance. No filename sanitation. No status check. Direct file ops always.

This isn't laziness; it's the second half of the design choice. Reference material doesn't have a release cycle of its own. An architecture diagram updates when the architecture updates. A brand-guidelines PDF updates when marketing pushes a new revision. The project's own lifecycle drives those changes — there is no spec entity with a status that owns them, so there's nothing to gate against.

The two mechanisms compose: a spec body can link to a project artifact via plain relative markdown ([brand guidelines](../../../artifacts/brand-guidelines.pdf)). The linkage is plain markdown — SPECLAN doesn't track it as a referential relationship, doesn't auto-stage it under a CR, and doesn't validate the path. They share one thing: a consistent icon vocabulary. A .pdf artifact gets the same icon in the spec's Artifacts section as it does in the project tree. Different governance, same visual language.

What the extension does NOT do with artifact bytes

One boundary worth being explicit about, because the question gets asked: SPECLAN does not read, parse, summarise, or interpret the bytes of your artifacts. None of the AI features (clarification assistants, code-walking inference, change-request merging) consume artifact contents. The file is stored, referenced, surfaced in the UI, governed through CRs, and kept in sync on rename — that's the whole interaction the extension has with it.

What the extension does is make sure the implementation agent — Claude Code, Codex, Cursor, whatever you hand the spec to — can find the artifacts and decide for itself how to read them. Markdown, JSON, source code, and most images are read natively by modern coding agents. PDFs, DOCX, PPTX usually need a skill attached to the agent or a pre-extraction step into a sibling Markdown artifact before the implementation hand-off.

This is deliberate. Bundling PDF/DOCX parsers into the extension would (a) bloat it, (b) lock users into one extraction pipeline, and (c) silently expose binary content to AI providers users may not have authorised for that scope. Artifacts are evidence the implementation agent can find — not pre-digested input the AI has already consumed.

The one-click bridge to implementation

While the architecture work was the headline of this release, the quality-of-life win that ships with it is a button the extension has needed since v0.9.0: Quick Impl. A pill-shaped button in the editor topbar, visible on Features, Requirements, and Change Requests, that turns an approved spec into a paste-ready implementation prompt with a single click.

The prompt is structured to read the spec from its relative path, ask the user which implementation technique to use, set the spec's status to in-development for the duration of the work, and bookend the lifecycle by flipping to under-test when development is complete. It's a single-spec, fire-and-forget hand-off — the explicit alternative to the planfile-based workflow for "I have one approved feature and just want to ship it now."

What this changed about my own workflow

For a year I'd been treating the spec body as the only thing that lived in git. The wireframes lived in Figma; the contracts lived in OpenAPI files in another repo; the regulatory references lived in shared-drive PDFs that I emailed myself when I needed them. The day I started attaching them all to the spec tree under v0.9.7, I noticed how much friction the previous pattern carried that I'd just become numb to.

The deliberate split — governed Spec Artifacts, ungoverned Project Artifacts — is the kind of design choice that's obvious in hindsight but easy to get wrong upfront. A less careful version would have shipped one universal artifact mechanism with CR governance everywhere, and users (myself included) would have spent months filing bugs about why the architecture diagram needs a 4-stage review cycle to update. Two mechanisms with different governance is what falls out of taking the entity-lifecycle abstraction seriously.

If you're building tooling that touches a spec lifecycle, the practical lesson generalizes: governance is determined by what the parent entity needs, not by what's most uniform. A unified mechanism is satisfying from the architect's seat. From the user's seat, it forces ceremony on material that has none.

For the SPECLAN-specific tour, the release notes and the Artifacts help page walk through the user surfaces in detail. And if you're curious which of today's frontier models writes specs you'd actually trust to carry real artifacts, speclan.net/compare parks 13 models' output on the same brief side-by-side.

What's the cleanest split you've made between governed and ungoverned state in tooling you've shipped? Curious whether the "what does the parent entity need" lens lands the same way elsewhere.

Top comments (0)