|
| 1 | +<!DOCTYPE html> |
| 2 | +<html lang="en"> |
| 3 | +<head> |
| 4 | + <meta charset="UTF-8"> |
| 5 | + <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| 6 | + <title>Capability requirements — check what your AI actually built | MetaObjects</title> |
| 7 | + <meta name="description" content="Your agent reports the feature done. The columns exist, the tests pass, and nothing ever writes the value. MetaObjects makes a capability a node in your model, so the gap between what your software claims and what it does becomes a build failure instead of a surprise."> |
| 8 | + |
| 9 | + <link rel="icon" href="/images/mo-logo-only.png" type="image/png"> |
| 10 | + |
| 11 | + <link rel="preconnect" href="https://fonts.googleapis.com"> |
| 12 | + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
| 13 | + <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> |
| 14 | + |
| 15 | + <link rel="stylesheet" href="/styles.css"> |
| 16 | + |
| 17 | + <meta property="og:title" content="Check what your AI actually built"> |
| 18 | + <meta property="og:description" content="The columns exist, the tests pass, and nothing ever writes the value. Make a capability a node in your model, and the gap becomes a build failure."> |
| 19 | + <meta property="og:type" content="website"> |
| 20 | + <meta property="og:url" content="https://metaobjects.dev/requirements.html"> |
| 21 | + <meta property="og:image" content="https://metaobjects.dev/images/mo-logo.png"> |
| 22 | + <meta name="twitter:card" content="summary_large_image"> |
| 23 | + <link rel="canonical" href="https://metaobjects.dev/requirements.html"> |
| 24 | + |
| 25 | + <style> |
| 26 | + /* Page-scoped styles — do not touch the shared landing styles. */ |
| 27 | + .page { max-width: var(--reading-width); margin: 0 auto; padding: 3.5rem 1.5rem 4rem; } |
| 28 | + .page-eyebrow { font-family: var(--font-mono); font-size: 0.8rem; letter-spacing: 0.04em; |
| 29 | + text-transform: uppercase; color: var(--medium-gray); margin: 0 0 0.75rem; } |
| 30 | + .page h1 { font-size: 2.4rem; line-height: 1.15; color: var(--primary-blue); |
| 31 | + margin: 0 0 1rem; letter-spacing: -0.02em; } |
| 32 | + .page-lede { font-size: 1.15rem; line-height: 1.65; color: var(--dark-gray); margin: 0 0 2rem; } |
| 33 | + .page h2 { font-size: 1.5rem; line-height: 1.25; color: var(--primary-blue); |
| 34 | + margin: 3rem 0 1rem; letter-spacing: -0.01em; } |
| 35 | + .page h3 { font-size: 1.05rem; color: var(--primary-blue); margin: 1.75rem 0 0.5rem; } |
| 36 | + .page p { margin: 0 0 1.25rem; } |
| 37 | + .page ul { margin: 0 0 1.4rem; padding-left: 1.4rem; } |
| 38 | + .page li { margin: 0 0 0.6rem; } |
| 39 | + .page strong { color: var(--primary-blue); } |
| 40 | + .page blockquote { margin: 1.6rem 0; padding: 0.4rem 0 0.4rem 1.4rem; |
| 41 | + border-left: 3px solid var(--accent-gold); color: var(--dark-gray); |
| 42 | + font-size: 1.1rem; font-style: italic; } |
| 43 | + .page pre { background: var(--code-bg); color: var(--code-fg); border-radius: 8px; |
| 44 | + padding: 1.25rem 1.4rem; overflow-x: auto; font-family: var(--font-mono); |
| 45 | + font-size: 0.86rem; line-height: 1.6; margin: 0 0 1.6rem; box-shadow: var(--shadow); } |
| 46 | + .page pre code { background: none; color: inherit; padding: 0; font-size: inherit; } |
| 47 | + .tok-key { color: #8FB7E8; } .tok-val { color: #A7E0C0; } |
| 48 | + .tok-cmt { color: #7A8AA8; } .tok-err { color: #F0A0A0; } |
| 49 | + .page hr { border: none; border-top: 1px solid var(--rule); margin: 3rem 0; } |
| 50 | + |
| 51 | + .shapes { display: grid; gap: 1rem; margin: 0 0 1.5rem; } |
| 52 | + @media (min-width: 720px) { .shapes { grid-template-columns: repeat(3, 1fr); } } |
| 53 | + .shape { background: var(--light-gray); border: 1px solid var(--rule); |
| 54 | + border-radius: 10px; padding: 1.1rem 1.2rem; } |
| 55 | + .shape h3 { margin: 0 0 0.4rem; font-size: 0.95rem; font-family: var(--font-mono); |
| 56 | + letter-spacing: 0.02em; } |
| 57 | + .shape p { margin: 0; font-size: 0.93rem; line-height: 1.6; color: var(--dark-gray); } |
| 58 | + |
| 59 | + .findings { border: 1px solid var(--rule); border-radius: 10px; overflow: hidden; |
| 60 | + margin: 0 0 1.5rem; } |
| 61 | + .findings div { padding: 0.95rem 1.2rem; border-bottom: 1px solid var(--rule); |
| 62 | + font-size: 0.95rem; line-height: 1.6; } |
| 63 | + .findings div:last-child { border-bottom: none; } |
| 64 | + .findings b { color: var(--primary-blue); display: block; margin-bottom: 0.15rem; } |
| 65 | + .findings span { color: var(--dark-gray); } |
| 66 | + |
| 67 | + .split { display: grid; gap: 1.25rem; margin: 0 0 1.5rem; } |
| 68 | + @media (min-width: 720px) { .split { grid-template-columns: 1fr 1fr; } } |
| 69 | + .split section { border: 1px solid var(--rule); border-radius: 10px; padding: 1.2rem 1.35rem; } |
| 70 | + .split section.you-add { background: var(--light-gray); } |
| 71 | + .split h3 { margin: 0 0 0.7rem; font-size: 0.82rem; font-family: var(--font-mono); |
| 72 | + text-transform: uppercase; letter-spacing: 0.05em; color: var(--medium-gray); } |
| 73 | + .split ul { margin: 0; padding-left: 1.1rem; } |
| 74 | + .split li { font-size: 0.93rem; line-height: 1.6; } |
| 75 | + |
| 76 | + .page-cta { background: var(--light-gray); border: 1px solid var(--rule); |
| 77 | + border-radius: 10px; padding: 1.5rem 1.75rem; margin: 2.5rem 0 0; } |
| 78 | + .page-cta p { margin: 0 0 0.9rem; font-size: 0.98rem; } |
| 79 | + .page-cta p:last-child { margin: 0; } |
| 80 | + .page-cta .cmds { font-family: var(--font-mono); font-size: 0.85rem; |
| 81 | + display: block; color: var(--royal-blue); } |
| 82 | + </style> |
| 83 | +</head> |
| 84 | +<body> |
| 85 | + <header class="site-header"> |
| 86 | + <a href="/" class="brand"> |
| 87 | + <img src="/images/mo-logo-only.png" alt="" class="brand-mark"> |
| 88 | + <span class="brand-name">MetaObjects</span> |
| 89 | + </a> |
| 90 | + <nav> |
| 91 | + <a href="/getting-started.html" class="nav-link">Get started</a> |
| 92 | + <a href="/story.html" class="nav-link">Story</a> |
| 93 | + <a href="/videos.html" class="nav-link">Videos</a> |
| 94 | + <a href="https://github.com/metaobjectsdev/metaobjects" class="nav-link">GitHub</a> |
| 95 | + <a href="https://github.com/metaobjectsdev/metaobjects/tree/main/spec" class="nav-link nav-cta">Spec</a> |
| 96 | + </nav> |
| 97 | + </header> |
| 98 | + |
| 99 | + <main> |
| 100 | + <article class="page"> |
| 101 | + <p class="page-eyebrow">Capability requirements · ships in all five language ports</p> |
| 102 | + <h1>Check what your AI actually built.</h1> |
| 103 | + <p class="page-lede"> |
| 104 | + Your agent reports the feature done. The columns are there, the types line up, the tests |
| 105 | + pass — and no code path ever writes the value. Nothing is broken, so nothing fails. |
| 106 | + MetaObjects makes a capability a node in your model, so the distance between what your |
| 107 | + software <em>claims</em> to do and what it does becomes something a build can check. |
| 108 | + </p> |
| 109 | + |
| 110 | + <h2>The failure that comes after drift</h2> |
| 111 | + <p> |
| 112 | + Drift is when your code stops agreeing with your model. <a href="/videos.html">MetaObjects |
| 113 | + already turns that into a build error</a> across your code, your prompts and your schema. |
| 114 | + </p> |
| 115 | + <p> |
| 116 | + This is the other one. Your code can agree with your model perfectly, and the model can |
| 117 | + still describe a capability your software does not have. Agent-written work fails that way |
| 118 | + in three recognisable shapes: |
| 119 | + </p> |
| 120 | + |
| 121 | + <div class="shapes"> |
| 122 | + <div class="shape"> |
| 123 | + <h3>Duplicate</h3> |
| 124 | + <p>It builds a capability that already exists, because nothing in the repo told it the |
| 125 | + capability was there.</p> |
| 126 | + </div> |
| 127 | + <div class="shape"> |
| 128 | + <h3>Missing</h3> |
| 129 | + <p>It reports the feature done. Every layer is present and nothing ever writes the |
| 130 | + value. Nothing is wrong <em>enough</em> to fail.</p> |
| 131 | + </div> |
| 132 | + <div class="shape"> |
| 133 | + <h3>Partial</h3> |
| 134 | + <p>It ships the legible 80% and reports done. The recovery path, the rollup, the retire |
| 135 | + operation — invisible to everyone.</p> |
| 136 | + </div> |
| 137 | + </div> |
| 138 | + |
| 139 | + <p> |
| 140 | + A test suite cannot see any of this. A test exercises code that exists; there is no test |
| 141 | + that fails because a column nobody writes was never wired to anything. <strong>The absence |
| 142 | + has no address.</strong> |
| 143 | + </p> |
| 144 | + |
| 145 | + <h2>So give it one</h2> |
| 146 | + <p> |
| 147 | + A requirements document does not help — prose goes stale silently and nothing in your build |
| 148 | + has an opinion about it. Neither does a requirements tool, where the link from a claim to |
| 149 | + the code is a <em>string</em>: rename the class and the link still looks fine. |
| 150 | + </p> |
| 151 | + <p> |
| 152 | + A capability declared in MetaObjects is a node in the same model as your entities, so its |
| 153 | + link is <strong>resolved, not trusted</strong>: |
| 154 | + </p> |
| 155 | + |
| 156 | +<pre><code><span class="tok-cmt">- requirement.functional:</span> |
| 157 | + <span class="tok-key">name</span>: <span class="tok-val">botCanBeRetiredWithoutErasingItsResults</span> |
| 158 | + <span class="tok-key">level</span>: <span class="tok-val">5</span> |
| 159 | + <span class="tok-key">status</span>: <span class="tok-val">live</span> |
| 160 | + <span class="tok-key">statement</span>: <span class="tok-val">"A bot's participation is switched by its own status, so it |
| 161 | + can be withdrawn while the hands it played stay on the record."</span> |
| 162 | + <span class="tok-key">counterexample</span>: <span class="tok-val">"A misbehaving bot that can only be stopped by deleting |
| 163 | + it, taking every hand it played with it."</span> |
| 164 | + <span class="tok-key">implementedBy</span>: [<span class="tok-val">arena::Bot.status</span>]</code></pre> |
| 165 | + |
| 166 | + <ul> |
| 167 | + <li><strong>The statement</strong> says what the capability is, in a sentence.</li> |
| 168 | + <li><strong>The counterexample</strong> says what a violation looks like. That single line |
| 169 | + is what an assertion gets written against.</li> |
| 170 | + <li><strong><code>implementedBy</code></strong> names the <em>member</em> — the real field |
| 171 | + in the real model — that carries it. Rename or delete that field and the build tells you |
| 172 | + which capability just lost its implementation.</li> |
| 173 | + </ul> |
| 174 | + |
| 175 | + <p> |
| 176 | + That is a real entry from a live product, and it is <span class="tok-err">wrong</span>. |
| 177 | + <code>arena::Bot.status</code> exists, so the link resolved and every structural check |
| 178 | + passed. Nothing in the codebase ever writes that column, so every bot holds the default |
| 179 | + forever. The bot could not be retired. |
| 180 | + </p> |
| 181 | + |
| 182 | + <blockquote> |
| 183 | + Consistency is not completeness. Knowing where a capability is supposed to live is what |
| 184 | + lets you ask the only question that finds these: what would I have to break to make this |
| 185 | + claim fail? |
| 186 | + </blockquote> |
| 187 | + |
| 188 | + <h2>What it found</h2> |
| 189 | + <p> |
| 190 | + Declared across three shipping products — roughly 700 capabilities — against suites that |
| 191 | + were passing at the time. A sample: |
| 192 | + </p> |
| 193 | + |
| 194 | + <div class="findings"> |
| 195 | + <div><b>A bot could not be retired.</b><span>A <code>status</code> column with no writer |
| 196 | + anywhere. Every bot held the default, permanently.</span></div> |
| 197 | + <div><b>A season could not be run privately.</b><span>Owner and password columns exist. No |
| 198 | + code path sets either one.</span></div> |
| 199 | + <div><b>Private tables ignored their own stakes.</b><span>The blinds column is never |
| 200 | + written, so every private table silently ran at the database default.</span></div> |
| 201 | + <div><b>Interrupted work was never recovered.</b><span>No lease, no timeout, and both |
| 202 | + recovery queries select only pending rows — caught while production still had zero |
| 203 | + stranded rows.</span></div> |
| 204 | + <div><b>A metrics rollup was marked done with an empty table.</b><span>Its only insert path |
| 205 | + was generated CRUD that nothing called.</span></div> |
| 206 | + </div> |
| 207 | + |
| 208 | + <p> |
| 209 | + These were found in a codebase with <strong>1,280 tests passing and zero failures</strong>, |
| 210 | + and the deepest retrofit of the three — a codebase where 93% of the history predates its |
| 211 | + requirements — produced the most of them. <strong>You do not need a greenfield project.</strong> |
| 212 | + This works on the code you already have, and best on the parts you are least sure about. |
| 213 | + </p> |
| 214 | + |
| 215 | + <h2>What you get, and what you build</h2> |
| 216 | + <p> |
| 217 | + Being exact about this matters, because the last part is the important one and it is not in |
| 218 | + the box yet. |
| 219 | + </p> |
| 220 | + |
| 221 | + <div class="split"> |
| 222 | + <section> |
| 223 | + <h3>MetaObjects ships</h3> |
| 224 | + <ul> |
| 225 | + <li>Capabilities as registered vocabulary in all five language ports — a project that |
| 226 | + declares none sees no change at all.</li> |
| 227 | + <li><code>implementedBy</code> resolved by the loader. Dangling is an error, not a note.</li> |
| 228 | + <li><code>meta verify</code> reporting claims whose implementation vanished, entities no |
| 229 | + claim covers, and gaps recorded versus gaps nobody has ruled on — with a summary on |
| 230 | + every run.</li> |
| 231 | + <li><code>requirementTests()</code> scaffolding a test stub per claim, statement and |
| 232 | + counterexample carried in, kept from rotting by <code>verify --codegen</code>.</li> |
| 233 | + <li><code>meta docs</code> rendering the ledger for humans and for agents — including a |
| 234 | + machine-readable index carrying a declared count.</li> |
| 235 | + </ul> |
| 236 | + </section> |
| 237 | + <section class="you-add"> |
| 238 | + <h3>You write</h3> |
| 239 | + <ul> |
| 240 | + <li><strong>The assertion.</strong> A generated stub is a place to put a proof and a |
| 241 | + guarantee it stays in step with the claim. It is not a proof.</li> |
| 242 | + <li><strong>The proof that the assertion bites</strong> — a declared edit to your source |
| 243 | + that must turn that test red. If it survives, the test is decoration.</li> |
| 244 | + </ul> |
| 245 | + <p style="margin:0.9rem 0 0;font-size:0.9rem;color:var(--medium-gray);"> |
| 246 | + That second one is not a MetaObjects feature today. Two projects built their own |
| 247 | + independently, which is the best argument for moving it into the library — and the |
| 248 | + reason we would rather tell you now than have you find the gap. |
| 249 | + </p> |
| 250 | + </section> |
| 251 | + </div> |
| 252 | + |
| 253 | + <h2>Where it sits</h2> |
| 254 | + <ul> |
| 255 | + <li><strong>Codegen</strong> turns a model change into a compile error on the exact line |
| 256 | + your own logic needs updating. Refactoring stops being a bug hunt.</li> |
| 257 | + <li><strong><code>meta verify</code></strong> turns divergence between your code, prompts, |
| 258 | + schema and the model into a failed build. Drift cannot merge.</li> |
| 259 | + <li><strong>Capabilities</strong> put what you said the software does on that same spine — |
| 260 | + resolved links, coverage reporting, and a generated check per claim.</li> |
| 261 | + </ul> |
| 262 | + <p> |
| 263 | + The AI writes the code. The model keeps all of it honest — and now <em>honest</em> covers |
| 264 | + whether the feature is actually there. |
| 265 | + </p> |
| 266 | + |
| 267 | + <hr> |
| 268 | + |
| 269 | + <div class="page-cta"> |
| 270 | + <p><strong>Start with one capability you are not certain is fully built.</strong> Write down |
| 271 | + what it means and what a violation would look like, link it to the field or endpoint that |
| 272 | + carries it, then try to write a test that would fail if it were false. In three codebases, |
| 273 | + that exercise is where every finding above came from.</p> |
| 274 | + <p> |
| 275 | + <a href="/getting-started.html">Get started</a> · |
| 276 | + <a href="https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/requirements.md">Full reference</a> · |
| 277 | + <a href="https://github.com/metaobjectsdev/metaobjects/tree/main/spec">Spec</a> |
| 278 | + <span class="cmds">npm i @metaobjectsdev/cli && npx meta init</span> |
| 279 | + </p> |
| 280 | + </div> |
| 281 | + </article> |
| 282 | + </main> |
| 283 | + |
| 284 | + <footer class="site-footer"> |
| 285 | + <div class="site-footer-inner"> |
| 286 | + <p> |
| 287 | + <strong>MetaObjects</strong> · <a href="https://github.com/metaobjectsdev/metaobjects">GitHub</a> · <a href="https://github.com/metaobjectsdev/metaobjects/blob/main/LICENSE">Apache 2.0</a> · <a href="https://github.com/metaobjectsdev/metaobjects/blob/main/spec/roadmap.md">Roadmap</a> · <a href="/llms.txt">llms.txt</a> |
| 288 | + </p> |
| 289 | + <p class="site-footer-meta"> |
| 290 | + Commercial inquiries: <a href="https://metaobjects.com">metaobjects.com</a> · Created by <a href="https://dougmealing.com">Doug Mealing</a> |
| 291 | + </p> |
| 292 | + </div> |
| 293 | + </footer> |
| 294 | +</body> |
| 295 | +</html> |
0 commit comments