yongol — The Keel of AI-Coded SaaS 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:

SSOTConcern
features.yamlFeature catalog — what to build
manifest.yamlProject config — auth, middleware, infra
OpenAPIAPI contract — routes, parameters, responses
SQL DDL + sqlcData model — tables, columns, constraints, queries
SSaCService flow — decision sequence inside an endpoint
RegoAuthorization — who can do what
Mermaid stateDiagramState transitions — entity lifecycles
FuncSpecCustom functions — logic that can’t be expressed as CRUD
HurlTest scenarios — smoke, scenario, invariant trichotomy
STMLFrontend — 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

AnnotationRoleFormat
@getDB readType var = Model.Method({args})
@postRow creationType var = Model.Method({args})
@putRow update (no return)Model.Method({args})
@deleteRow deletionModel.Method({args})
@emptynil guard -> 404var "message" [STATUS]
@existsnot-nil guard -> 409var "message" [STATUS]
@authAuthorization check"action" "resource" {inputs} "message" [STATUS]
@stateState machine transitiondiagram {inputs} "transition" "message" [STATUS]
@callFunction call[Type var =] pkg.Func({args})
@evalPredicate guard (true -> error)pkg.Func({args}) "message" STATUS
@publishQueue publish"topic" {payload}
@subscribeQueue trigger function"topic"
@verify-passwordLogin (timing-safe)Model.col=source Model.hash vs source -> var STATUS "msg"
@responseJSON return{ field: var, ... } or var
@no-paginationPagination rule exemption(function level)
@state-neutralState 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.

StageDescriptionTimeCumulative
Initial build10 endpoints, 6 tables, auth, state machine13 min13 min
+ Versioningworkflow clone, version list6 min19 min
+ Webhookswebhook CRUD, queue backend6 min25 min
+ Template marketplacecursor pagination, cross-org clone3 min28 min
+ File attachmentsexecution reports, file backend4 min32 min
+ Schedulingcron scheduling, session backend6 min38 min
+ Audit logsoffset pagination, cache backend3 min41 min
+ Dashboardrelation joins, func response types7 min48 min
+ Batch operationsjsonb bulk insert14 min62 min
+ External APIgeocoding func, column addition3 min65 min
+ Conditional updatesentinel pattern, auto-assign4 min69 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 status shows preserved files and contract drift (PRV-01/PRV-02 errors).
  • 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)

PackageFunctionDescription
authhashPassword, verifyPasswordbcrypt hashing/verification
authissueToken, verifyToken, refreshTokenJWT tokens
authgenerateResetTokenPassword reset
cryptoencrypt, decryptAES-256-GCM
cryptogenerateOTP, verifyOTPTOTP
storageuploadFile, deleteFile, presignURLS3
mailsendEmail, sendTemplateEmailSMTP
textgenerateSlug, sanitizeHTML, truncateTextText processing
imageogImage, thumbnailImage generation

Built-in Models (configured via manifest.yaml)

PackageInterfaceBackendSSaC Usage
sessionSessionModel (Set/Get/Delete + TTL)PostgreSQL, Memorysession.Session.Get({key: ...})
cacheCacheModel (Set/Get/Delete + TTL)PostgreSQL, Memorycache.Cache.Set({key: ..., value: ..., ttl: ...})
fileFileModel (Upload/Download/Delete)S3, LocalFilefile.File.Upload({key: ..., body: ...})
queuesingleton 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

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

  1. Google DORA Team. DORA State of AI-Assisted Software Development 2025. Google Cloud, 2025. dora.dev/dora-report-2025
  2. 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
  3. 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
  4. 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
  5. 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
  6. Anton Jansen, Jan Bosch. “Software Architecture as a Set of Architectural Design Decisions.” EWSA 2005, LNCS 3527, Springer, 2005. semanticscholar.org
  7. Marco Brambilla, Jordi Cabot, Manuel Wimmer. Model-Driven Software Engineering in Practice. 2nd ed., Springer, 2017. doi:10.1007/978-3-031-02546-4
  8. GitClear. AI Copilot Code Quality 2025. February 2025. gitclear.com

Code: github.com/park-jun-woo/yongol


Changelog

DateChanges
2026-05-18Initial publication
2026-05-19Added Opus benchmark. Updated 10 SSOTs
2026-05-21README 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-26v0.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