Image: AI generated
If you are dealing with the AI overwriting your code, if vibe coding collapsed at 200 endpoints, if you want to shift the AI’s target from code to specs – yongol is that keel.
The 200th Endpoint
You build a SaaS with vibe coding. At first it’s fast. 5 tables, 12 endpoints — twenty minutes and it runs.
But past 50 endpoints, something strange happens. The AI produces a pattern today that contradicts yesterday’s. Past 100, existing features silently break. Past 200, adding a single feature costs 10x what the first ten cost.
The DORA 2025 report demonstrated this empirically — AI tools boost throughput by 2-18%, but simultaneously increase change failure rate and rework[1]. AI is a “mirror and multiplier” that amplifies existing process weaknesses.
It’s not that the model is stupid.
Decisions and Implementation
Three things are tangled in source code:
- User decisions — this column is
BIGINT, this endpoint is owner-only, pagination is cursor-based. - Business logic — pricing, workflows, lifecycle rules.
- Implementation details — variable names, library call order, error wrapping.
When AI reads this code, it cannot tell which line is a decision and which is a detail. So when it “refactors” or “cleans up,” it silently overwrites decisions it mistook for details. The user doesn’t notice until the behavior is already wrong. In 1972, Parnas said “hide design decisions likely to change behind interfaces”[2] — he was speaking to humans. But now that AI edits code, unless the distinction between decisions and details exists in the medium itself, no one — human or model — can maintain it.
This is why vibe coding collapses at 200 endpoints. A larger model doesn’t fix it. The medium — raw code — simply doesn’t preserve decisions. Every model eventually hits the same wall.
The Keel
The keel is the first bone laid when building a ship. It bears the hull’s weight, prevents side-to-side rolling, and every other structure is built on top of it. A ship built without a keel floats in calm water but warps when the waves come.
A SaaS built with vibe coding is the same. It floats when small. It warps when it grows.
yongol is the keel of AI-coded SaaS.
Harness with reins — not a bigger model, but more precise reins. A deterministic validator judges every artifact, a ratchet enforces progress, and the machine decides whether the job is done.
Move Decisions Out of Code
yongol’s core is simple. Separate decisions from code.
Ten declarative specs (SSOTs) each handle a single concern:
| SSOT | Concern |
|---|---|
| features.yaml | Feature catalog — what to build |
| manifest.yaml | Project config — auth, middleware, infra |
| OpenAPI | API contract — routes, parameters, responses |
| SQL DDL + sqlc | Data model — tables, columns, constraints, queries |
| SSaC | Service flow — decision sequence inside an endpoint |
| Rego | Authorization — who can do what |
| Mermaid stateDiagram | State transitions — entity lifecycles |
| FuncSpec | Custom functions — logic that can’t be expressed as CRUD |
| Hurl | Test scenarios — smoke, scenario, invariant trichotomy |
| STML | Frontend — Semantic Template Markup Language (data-* attribute-based HTML) |
Eight of the ten are industry standards (OpenAPI, SQL, sqlc, Rego, Mermaid, Hurl, YAML). Only SSaC and STML are DSLs created by yongol. What the AI must learn from scratch is minimized.
Each SSOT contains only decisions. No implementation details. The AI edits SSOTs; yongol generate renders code from them. Decisions live permanently in the SSOTs; the code is a disposable projection.
Enforcing Consistency
Decisions are now spread across ten files, so contradictions can appear. DDL says BIGINT but OpenAPI says string? SSaC declares @auth but Rego has no matching rule? The state diagram has a transition but SSaC has no corresponding function?
A contradicted SSOT is a corrupted decision. No matter how clean the code is, if the decisions conflict, the behavior is wrong.
yongol validate catches this.
✓ manifest ✓ openapi_ddl ✓ ssac_rego
✓ openapi ✓ openapi_ssac ✓ ssac_authz
✓ ddl ✓ hurl_openapi ✓ ssac_sqlc
✓ query ✓ hurl_statemachine ✓ ddl_statemachine
✓ ssac ✓ hurl_manifest ✓ ddl_rego
✓ statemachine ✓ openapi_manifest ✓ rego_manifest
✓ rego ✓ ssac_ddl ✓ stml_openapi
✓ hurl ✓ ssac_statemachine
✓ funcspec ✓ ssac_func
0 errors, 0 warnings
It validates each SSOT individually first, then runs cross-layer checks. ~287 rules inspect every symbolic reference across all ten SSOTs. If a single contradiction exists, compilation is refused. Torres et al.’s systematic literature review[3] noted that most model management tools handle only intra-model consistency, leaving cross-heterogeneous-model verification as an open problem — yongol validate fills precisely that gap.
The AI writes freely. Step off the rails and validate catches it instantly. Freedom on rails.
yongol next — The Ratchet Command
While yongol validate shows all errors at once, yongol next shows errors one at a time. This is the ratchet.
$ yongol next specs/
[ERROR] DDL-003: users.id must be BIGINT, got INT
file: specs/db/users.sql:2
▶ Fix this error. Then run `yongol next specs/`.
The only instruction an AI agent needs is one sentence: “Run yongol next specs/ and fix errors until there are 0.”
Fix the error, and the next one appears. Pass them all and it stops:
$ yongol next specs/
✓ All validations passed. 0 errors.
The agent doesn’t declare “I’m done.” The machine rules “still remaining” or “all passed.” The agent has no authority over the termination judgment.
Project Creation and Feature Management
yongol init
Auto-generates SSOT scaffolding from features.yaml.
yongol init Myapp features.yaml "My workflow automation SaaS"
cd Myapp && yongol validate specs # 0 errors
Manifest, OpenAPI operationId stubs, SSaC stub files, Rego authorization rules, Hurl smoke tests, and sqlc configuration are generated in one shot. The project starts in a yongol validate-passing state from the very beginning.
yongol features add / remove
Add or remove features:
yongol features add new_features.yaml # Generate SSaC stubs for new operationIds
yongol features remove ExportWorkflow --yes # Delete operationId + SSaC stubs
yongol import
Generate Go client packages from external OpenAPI specs (Stripe, GitHub, etc.):
yongol import https://api.stripe.com/openapi.yaml ./external/
Call generated functions in SSaC with @call <pkg>.<Func>({...}).
operationId Is the Keystone
How do you bind ten layers together? With a single PascalCase identifier.
Enter the operationId ExecuteWorkflow:
── Feature Chain: ExecuteWorkflow ──
OpenAPI api/openapi.yaml POST /workflows/{id}/execute
SSaC service/workflow/execute_workflow.ssac @get @empty @auth @state @call @publish @response
DDL db/workflows.sql CREATE TABLE workflows
DDL db/execution_logs.sql CREATE TABLE execution_logs
Rego policy/authz.rego resource: workflow
StateDiag states/workflow.md diagram: workflow → ExecuteWorkflow
FuncSpec func/billing/check_credits.go @func billing.CheckCredits
FuncSpec func/billing/deduct_credit.go @func billing.DeductCredit
FuncSpec func/worker/process_actions.go @func worker.ProcessActions
FuncSpec func/webhook/deliver.go @func webhook.Deliver
Hurl tests/scenario-happy-path.hurl scenario: scenario-happy-path.hurl
From API spec to DB schema, authorization policy to state transitions, function implementations to test scenarios — a single feature’s entire topology on one screen. Dozens of greps replaced by one command.
operationId is the keystone because in a full-stack application, the unit of a feature is the API endpoint. A user presses a button, an API is called, and that API cuts through every other layer. One name physically chains ten layers together.
SSaC — Why a Custom DSL
Eight of yongol’s 10 SSOTs are industry standards. Only SSaC (Service Sequences as Code) and STML were created by yongol. SSaC captures service flow decisions.
The gap SSaC fills. Look at the spectrum of declarative tools: at one end sit contract standards (OpenAPI, SQL, Rego) — they declare what but not in what order. At the other end sit workflow runtimes (Temporal, Inngest, Restate) — those are code. Decisions and implementation details recombine in the same file. SSaC sits in the gap between: “inside a single endpoint, what happens, in what order, with what guards.”
SSaC has a total of 16 annotations. Learnable from a one-page manual.
Complete SSaC Annotation List
| Annotation | Role | Format |
|---|---|---|
@get | DB read | Type var = Model.Method({args}) |
@post | Row creation | Type var = Model.Method({args}) |
@put | Row update (no return) | Model.Method({args}) |
@delete | Row deletion | Model.Method({args}) |
@empty | nil guard -> 404 | var "message" [STATUS] |
@exists | not-nil guard -> 409 | var "message" [STATUS] |
@auth | Authorization check | "action" "resource" {inputs} "message" [STATUS] |
@state | State machine transition | diagram {inputs} "transition" "message" [STATUS] |
@call | Function call | [Type var =] pkg.Func({args}) |
@eval | Predicate guard (true -> error) | pkg.Func({args}) "message" STATUS |
@publish | Queue publish | "topic" {payload} |
@subscribe | Queue trigger function | "topic" |
@verify-password | Login (timing-safe) | Model.col=source Model.hash vs source -> var STATUS "msg" |
@response | JSON return | { field: var, ... } or var |
@no-pagination | Pagination rule exemption | (function level) |
@state-neutral | State machine rule exemption | (function level) |
SSaC Example — AcceptProposal
Authorization + dual state machines + escrow + queue:
package service
import "github.com/org/project/internal/billing"
// @get Proposal p = Proposal.FindByID({ID: request.id})
// @empty p "Proposal not found" 404
// @get Gig gig = Gig.FindByID({ID: p.GigID})
// @empty gig "Gig not found" 404
// @auth "AcceptProposal" "gig" {ResourceID: request.id} "Forbidden" 403
// @state proposal {status: p.Status} "AcceptProposal" "Cannot accept" 409
// @state gig {status: gig.Status} "AcceptProposal" "Cannot accept on gig" 409
// @put Proposal.UpdateStatus({ID: p.ID, Status: "accepted"})
// @put Gig.AssignFreelancer({ID: gig.ID, FreelancerID: p.FreelancerID, Status: "in_progress"})
// @call billing.HoldEscrowResponse escrow = billing.HoldEscrow({GigID: gig.ID, Amount: gig.Budget})
// @publish "proposal.accepted" {GigID: gig.ID, FreelancerID: p.FreelancerID}
// @get Proposal updated = Proposal.FindByID({ID: p.ID})
// @response { proposal: updated }
func AcceptProposal() {}
16 lines. 10 annotations. Two state machines, authorization, escrow, queue event, response — every decision visible, every detail absent.
Benchmark: ZenFlow
ZenFlow — a multi-tenant workflow automation SaaS.
| Stage | Description | Time | Cumulative |
|---|---|---|---|
| Initial build | 10 endpoints, 6 tables, auth, state machine | 13 min | 13 min |
| + Versioning | workflow clone, version list | 6 min | 19 min |
| + Webhooks | webhook CRUD, queue backend | 6 min | 25 min |
| + Template marketplace | cursor pagination, cross-org clone | 3 min | 28 min |
| + File attachments | execution reports, file backend | 4 min | 32 min |
| + Scheduling | cron scheduling, session backend | 6 min | 38 min |
| + Audit logs | offset pagination, cache backend | 3 min | 41 min |
| + Dashboard | relation joins, func response types | 7 min | 48 min |
| + Batch operations | jsonb bulk insert | 14 min | 62 min |
| + External API | geocoding func, column addition | 3 min | 65 min |
| + Conditional update | sentinel pattern, auto-assign | 4 min | 69 min |
Final: 32 endpoints, 14 tables, 47 Hurl requests. 11/11 stages passed.
Adding features never slowed down. Existing tests never broke. The 200-endpoint wall didn’t exist.
Opus 4.7 benchmark — 32 endpoints, 14 tables, 47 Hurl requests, ~69 min. Sonnet 4.6 benchmark — 32 endpoints, 9 tables, 37 Hurl requests, ~43 min.
yongol agent
An LLM automatically fixes SSOT files through a validate-fix loop.
yongol agent specs/ --model ollama:gemma4:e4b --max-rounds 20
Validate errors are fed back to the LLM, the LLM fixes them, and validation runs again. The loop repeats until 0 errors. Works even with a local 4.5B model (Gemma4).
Supported backends: ollama (local), xai (Grok), gemini.
Can You Edit Generated Code
Yes. yongol generate preserves user edits on re-run:
- Every generated file gets a
//yg:checked llm=yongol-gen hash=<8hex>annotation. - If the hash differs, the file is marked as preserved and skipped on the next
generate. yongol statusshows preserved files and contract drift (PRV-01/PRV-02errors).- To record intent, add
//yg:preserve reason="..."(optional). To un-preserve, delete the file.
Built-in Functions and Models
Built-in Functions (called via @call in SSaC)
| Package | Function | Description |
|---|---|---|
auth | hashPassword, verifyPassword | bcrypt hashing/verification |
auth | issueToken, verifyToken, refreshToken | JWT tokens |
auth | generateResetToken | Password reset |
crypto | encrypt, decrypt | AES-256-GCM |
crypto | generateOTP, verifyOTP | TOTP |
storage | uploadFile, deleteFile, presignURL | S3 |
mail | sendEmail, sendTemplateEmail | SMTP |
text | generateSlug, sanitizeHTML, truncateText | Text processing |
image | ogImage, thumbnail | Image generation |
Built-in Models (configured via manifest.yaml)
| Package | Interface | Backend | SSaC Usage |
|---|---|---|---|
session | SessionModel (Set/Get/Delete + TTL) | PostgreSQL, Memory | session.Session.Get({key: ...}) |
cache | CacheModel (Set/Get/Delete + TTL) | PostgreSQL, Memory | cache.Cache.Set({key: ..., value: ..., ttl: ...}) |
file | FileModel (Upload/Download/Delete) | S3, LocalFile | file.File.Upload({key: ..., body: ...}) |
queue | singleton Pub/Sub (Publish/Subscribe) | PostgreSQL, Memory | @publish "topic" {payload} |
Automatic DDL Migration Generation
yongol generate detects DDL changes and automatically generates migration files.
specs/db/
└── users.sql # SSOT — edit here
artifacts/db/
├── .latest_schema.sql # Baseline snapshot
└── migrations/
├── 0001_initial.up.sql
├── 0001_initial.down.sql
├── 0002_add_users_email.up.sql
└── 0002_add_users_email.down.sql
Ambiguous changes (column renames, type casting, NOT NULL backfill) are disambiguated with DDL comment hints (-- @rename, -- @cast, -- @backfill, -- @data_migration, -- @allow_destructive). Six rules (MIG-001 through MIG-006) gate dangerous changes. Actual DB application is delegated to standard tools like golang-migrate, flyway, etc.
Why a Bigger Model Isn’t the Answer
“GPT-6 will fix this.”
It won’t. The problem isn’t model intelligence — it’s the medium.
Code as a medium doesn’t distinguish decisions from implementation. Whatever model reads the code sees text where decisions and details are interleaved. No matter how smart the model is, if the medium doesn’t provide the distinction, the model can’t make it.
yongol changes the medium. It moves what the AI edits from code to declarative specs. Since specs contain only decisions with no implementation details, the AI can never mistake a decision for a detail. Decision survival becomes independent of model size.
A small LLM editing only SSOTs, with validate providing precise feedback on every miss, can maintain the same decision integrity as a much larger model editing raw code. yongol bridges that gap.
Runtime Tests
Hurl tests are all user-authored. Write them in specs/tests/ and yongol generate mirrors them to artifacts/tests/. During validation, rules XOH-01 through XOH-09 cross-check Hurl against OpenAPI, state machines, and manifest.auth.
hurl --test --variable host=http://localhost:8080 artifacts/my-project/tests/*.hurl
Three categories:
- smoke.hurl — Endpoint smoke tests
- scenario-*.hurl — Business scenario tests
- invariant-*.hurl — Cross-endpoint invariant tests
Current Status
Go+Gin backend generation: Beta — end-to-end functional. React frontend generation: Alpha (work in progress).
Get Started
Method 1: Install the Skill (Recommended)
npx skills add park-jun-woo/yongol
Install the yongol skill into your AI agent (Claude Code, Cursor, Copilot, and more). The agent learns the workflow on install.
/yongol Build a multi-tenant todo SaaS with auth and CRUD.
Method 2: Direct Installation
Requires Go 1.25+ and gcc (cgo dependency: pg_query_go links libpg_query for DDL parsing).
git clone https://github.com/park-jun-woo/yongol && cd yongol
make install
yongol validate examples/zenflow
0 errors, 0 warnings.
Point an AI at these specs and tell it to add a feature. validate lays the rails; the AI runs on them. There is no wall.
References
- Google DORA Team. DORA State of AI-Assisted Software Development 2025. Google Cloud, 2025. dora.dev/dora-report-2025
- David L. Parnas. “On the Criteria to Be Used in Decomposing Systems into Modules.” Communications of the ACM 15(12): 1053-1058, 1972. doi:10.1145/361598.361623
- Weslley Torres, Mark G.J. van den Brand, Alexander Serebrenik. “A Systematic Literature Review of Cross-Domain Model Consistency Checking by Model Management Tools.” Software and Systems Modeling 20(3): 897-916, 2021. doi:10.1007/s10270-020-00834-1
- Deepak Babu Piskala. “Spec-Driven Development: From Code to Contract in the Age of AI Coding Assistants.” arXiv:2602.00180, January 2026. arxiv.org/abs/2602.00180
- Ehsani et al. “When AI Code Doesn’t Stick: An Empirical Study on Reverted Changes Introduced by AI Coding Agents.” MSR 2026 Mining Challenge, April 2026. 2026.msrconf.org
- Anton Jansen, Jan Bosch. “Software Architecture as a Set of Architectural Design Decisions.” EWSA 2005, LNCS 3527, Springer, 2005. semanticscholar.org
- Marco Brambilla, Jordi Cabot, Manuel Wimmer. Model-Driven Software Engineering in Practice. 2nd ed., Springer, 2017. doi:10.1007/978-3-031-02546-4
- GitClear. AI Copilot Code Quality 2025. February 2025. gitclear.com
Related
- SSaC — Service Sequences as Code — yongol’s keystone DSL. Declares decisions inside endpoints.
- Feature Chain — Trace a Full Stack with One operationId — Trace all eight layers through a single operationId.
- Ratchet Pattern — How to Make an Agent Finish the Job — The theoretical foundation of how validate feeds back to agents.
- IFEval-Exploiting Ratchet Code — A code generation loop exploiting sycophancy bias, and Reins.
Code: github.com/park-jun-woo/yongol
Changelog
| Date | Changes |
|---|---|
| 2026-05-18 | Initial publication |
| 2026-05-19 | Added Opus benchmark. Updated 10 SSOTs |
| 2026-05-21 | README sync: updated benchmarks (Opus 32ep/14tbl/47hurl/69min, Sonnet 32ep/9tbl/37hurl/43min), added “Harness with reins” statement, added SSaC example (AcceptProposal), added yongol agent command, added Preserve system, added built-in functions/models list, added automatic DDL migration generation, added STML description, added ifeval-ratchet related post link |
| 2026-05-26 | v0.6.10 sync: added yongol next ratchet command, added yongol init/features add/features remove project creation and management, added yongol import external OpenAPI import, added complete SSaC annotation list (16 annotations) (@eval, @subscribe, @verify-password, @no-pagination, @state-neutral), Hurl test trichotomy (smoke/scenario/invariant), runtime tests section, Preserve detail (PRV error codes), DDL migration hint expansion (@data_migration, @allow_destructive, MIG rules), current status (Go+Gin Beta, React Alpha), split installation into 2 methods |