Direct Answer
An AI coding workflow becomes useful when it produces reviewable behavior and evidence, not when it merely produces code quickly. This case study uses QubitTool's SBTI quiz to show the complete loop:
explore -> propose -> review -> apply -> verify -> archive
The implementation has multiple coupled contracts: dynamic routes, 30 scored questions, conditional questions, 15 dimensions, deterministic template matching, bilingual content, result URLs, share images, and browser-specific download behavior. That makes it a better Spec Coding example than a one-screen demo.
This article does not claim the site was built in a fixed number of hours. Elapsed time depends on the starting codebase, acceptance depth, review, rework, visual QA, accessibility, and release gates.
Key Takeaways
- Use OpenSpec to make the intended behavior inspectable before code changes.
- Keep product data and scoring rules separate from React views.
- Treat every AI-generated patch as a proposal that still requires deterministic verification.
- Test the scoring engine independently from the UI and result prose.
- Define bilingual parity by IDs and behavior, not by identical sentence order.
- Test canvas output as an image artifact; a correct DOM is insufficient evidence.
- Archive only after implementation and verification agree with the reviewed change.
What the Current SBTI System Contains
The repository uses the Next.js Pages Router and a shared quiz registry. SBTI is one registered quiz implementation:
pages/quiz/[slug]/
index.tsx
test.tsx
result/[code].tsx
types.tsx
about.tsx
src/quiz/
quiz-definitions.ts
sbti/
data/questions.js
data/personalities.js
engine/scoring.js
engine/matching.js
components/
pages/
styles/
This structure separates generic routing from quiz-specific data, calculation, presentation, and style. The architecture is evidence from the current repository; it is not a universal template for every quiz.
Start with a Change, Not a Prompt
The current OpenSpec core workflow creates a reviewable change before implementation:
npm install -g @fission-ai/openspec@latest
openspec init
In an AI assistant that supports OpenSpec skills:
/opsx:explore
/opsx:propose add-sbti-quiz
/opsx:apply add-sbti-quiz
/opsx:archive add-sbti-quiz
Command spelling varies by host, and the current OpenSpec documentation requires Node.js 20.19 or newer. Verify the installed version and generated instructions instead of copying a command from an old article.
The core distinction is:
| Artifact | Decision it should preserve |
|---|---|
proposal.md |
why the change exists, scope, non-goals, impact |
specs/<capability>/spec.md |
observable behavior and scenarios |
design.md |
architecture, data ownership, trade-offs, migration |
tasks.md |
implementation and verification work |
Simple changes may omit a design document when the active schema permits it. Do not invent architecture ceremony just to fill a template.
Write the Behavioral Spec
The highest-value artifact is the spec. It should say what users and systems can observe:
## Requirement: The quiz produces a reproducible result
### Scenario: Complete regular-answer profile
- **WHEN** a user answers all 30 scored questions
- **THEN** the system computes exactly 15 dimension grades
- **AND** each grade is one of L, M, or H
- **AND** the matcher returns a valid result code
### Scenario: Missing scored answer
- **WHEN** any scored question is unanswered
- **THEN** the result calculation is not committed
- **AND** the user returns to the first incomplete question
### Scenario: Same answers in English and Chinese
- **WHEN** both locales submit the same option IDs
- **THEN** both produce the same dimension grades and result code
Notice what the scenarios do not specify: component names, CSS classes, or a preferred state library. Those are design or implementation choices unless they affect a public contract.
Make the Data Model Explicit
Questions and result definitions are executable content. Give them stable identities:
Question
id
dimension
options[] -> stable value
Dimension
id
group
localized name
grade thresholds
ResultTemplate
code
15-grade pattern
localized content
related result codes
The current SBTI implementation has 30 scored questions, two per dimension. A branch question is always shown, and one follow-up is conditional. The regular matcher contains 25 templates; a fallback and a conditional route produce the other two displayed results.
The i18n files may translate labels and explanations, but they must not change question IDs, option values, dimension order, result codes, or route identity.
Specify the Scoring Contract
For each dimension, two values in the range 1 to 3 are summed:
| Sum | Grade |
|---|---|
| 2-3 | L |
| 4 | M |
| 5-6 | H |
The 15 grades are converted to 0, 1, and 2 and compared with every regular template using Manhattan distance. The first minimum distance wins under the current implementation, so template order is part of tie behavior and must be tested.
Runnable Go Oracle
The following program checks the pair thresholds and produces the profile for the current CTRL template:
package main
import (
"errors"
"fmt"
"strings"
)
func gradePair(left, right int) (byte, error) {
if left < 1 || left > 3 || right < 1 || right > 3 {
return 0, errors.New("answers must be between 1 and 3")
}
switch sum := left + right; {
case sum <= 3:
return 'L', nil
case sum == 4:
return 'M', nil
default:
return 'H', nil
}
}
func buildProfile(answers [][2]int) (string, error) {
if len(answers) != 15 {
return "", errors.New("exactly 15 answer pairs are required")
}
grades := make([]byte, 0, 15)
for _, pair := range answers {
grade, err := gradePair(pair[0], pair[1])
if err != nil {
return "", err
}
grades = append(grades, grade)
}
groups := []string{
string(grades[0:3]),
string(grades[3:6]),
string(grades[6:9]),
string(grades[9:12]),
string(grades[12:15]),
}
return strings.Join(groups, "-"), nil
}
func main() {
high := [2]int{3, 3}
mid := [2]int{2, 2}
answers := [][2]int{
high, high, high,
high, mid, high,
mid, high, high,
high, high, high,
mid, high, mid,
}
profile, err := buildProfile(answers)
if err != nil {
panic(err)
}
fmt.Println(profile)
}
Expected output:
HHH-HMH-MHH-HHH-MHM
This oracle validates the current calculation contract. It does not validate the psychological meaning of the questions or labels.
Design the Routing Boundary
The shared router should resolve a quiz definition before rendering a page. Unknown slugs and result codes require explicit behavior.
| Route | Contract |
|---|---|
/quiz/[slug] |
registered quiz introduction |
/quiz/[slug]/test |
answer flow for the same definition |
/quiz/[slug]/result/[code] |
valid result or a controlled not-found state |
/quiz/[slug]/types |
all registered result types |
/quiz/[slug]/about |
methodology and limitations |
Do not duplicate quiz metadata in every page. The registry should own slug, namespace, page components, result codes, and navigation identity. This prevents a new quiz from becoming a copy-and-edit fork.
Separate State from Presentation
A robust split is:
The scoring engine should not read translations, browser dimensions, or canvas state. The result view should not recalculate grades. The poster should receive a committed result model rather than scraping visible DOM text.
Treat AI Coding as Controlled Execution
OpenSpec reduces ambiguity; it does not grant correctness. During apply:
- Give the agent a bounded file scope.
- Keep question and result data as a single source of truth.
- Require focused tests before broad UI polish.
- Review the diff after each ownership boundary.
- Reject unrelated refactors.
- Run the repository's real lint, build, and browser checks.
- Update the spec when the accepted behavior changes.
An AI agent that marks every task complete has only reported its own progress. Acceptance evidence must come from tests, rendered behavior, and review.
Poster Generation Is a Separate Product Surface
DOM-to-canvas libraries do not implement every CSS feature exactly like the browser renderer. Define a constrained poster contract:
- fixed capture width and aspect behavior;
- loaded local fonts and images before capture;
- supported color formats and effects;
- deterministic pixel-ratio policy;
- no unsupported filters or external resources;
- explicit timeout and failure state;
- download behavior for normal browsers;
- long-press guidance for browsers that block programmatic downloads.
Test the bitmap, not only the component. Use representative long names, narrow screens, high device-pixel ratios, and every result image family. A successful promise resolution is not proof that the pixels are correct.
Verification Matrix
| Layer | Required evidence |
|---|---|
| Data | unique IDs, valid dimensions, option ranges, valid related codes |
| Scoring | boundaries, exact matches, ties, fallback, conditional branch |
| Routing | every registered page and result URL resolves |
| i18n | same IDs and result code for equivalent answers |
| UI | keyboard path, focus, readable labels, no layout overflow |
| Poster | nonblank image, expected dimensions, assets loaded, save fallback |
| Privacy | local calculation verified; any analytics or result submission documented separately |
| Release | lint, build, focused tests, browser screenshots, rollback path |
For the current project, shared routing and SEO are registered in quiz-definitions.ts, while SBTI owns its questions, templates, scoring, matching, components, and styles under src/quiz/sbti/.
What the Original Case Study Got Wrong
Several attractive claims are not useful engineering evidence:
| Claim | Why it fails | Better record |
|---|---|---|
| “Built in half a day” | scope and verification are unknown | task timeline with review and rework |
| “AI translated everything perfectly” | no bilingual acceptance evidence | ID parity plus language review |
| “The agent solved the canvas bug” | outcome and regression surface are missing | failing fixture, patch, screenshot checks |
| “Independent routes make it viral” | route shape does not prove distribution | share preview, canonical, click and retention data |
| “Commit often” | commit count does not establish quality | coherent checkpoints with passing gates |
The reusable lesson is not speed. It is that an explicit behavior contract lets humans and agents disagree early, test the same outcome, and resume without relying on chat memory.
Frequently Asked Questions
Should every feature use OpenSpec?
No. A tiny deterministic edit may not justify a full change folder. Use it when behavior spans components, data, migrations, security, compatibility, or several sessions and reviewers.
Can the spec contain implementation details?
Only when they are externally required or necessary to preserve a decision. Put observable behavior in specs and architecture trade-offs in design; keep routine file-level steps in tasks.
How do you prevent scoring and translations from drifting?
Use stable IDs and values as the shared identity, then validate both locale files against the same schema. Translate text, not dimensions, option weights, result codes, or routes.
Should visual polish happen after the spec?
Visual behavior belongs in the spec when it affects readability, interaction, accessibility, or sharing. Fine-grained styling can iterate during apply, but its acceptance viewport and output must remain explicit.
When is the change ready to archive?
When all required behavior is implemented, every task is resolved, automated checks pass, browser evidence covers the risky paths, and the reviewed implementation matches the current artifacts.
Conclusion
The SBTI case study shows what Spec Coding should make durable: behavior, ownership, failure semantics, and evidence. OpenSpec creates the review loop, but the engineering value comes from a typed data model, pure scoring contract, locale parity, image-level poster checks, and release gates that remain valid when the model or coding tool changes.
The finished behavior is visible in the SBTI quiz. For the broader method, read the OpenSpec tutorial and Spec Coding guide.