Given a dependency and the subsets a product ships, it names the failing criterion 88% of the time and gets "the general depends on the specific" almost always. The one it cannot see is criterion 1, where the judgment is whether a call actually makes the caller simpler. Here, unlike the MIRO test, sending the rule along in the state helps: 92%.
The rule. Parnas, 1979: a system is a family of programs, and structure is judged by which useful subsets you can build without rewriting. That turns "should module A use module B?" into four testable questions. A uses B is acceptable only when all four hold: A is essentially simpler because it uses B; B is not substantially more complex because it may not use A; a useful subset contains B but not A; no conceivably useful subset contains A but not B. The slogan is don't let the general depend on the specific.
The corpus. 30 dependencies written for this test, six per class: six sound ones and six that break exactly one criterion. Each carries a product description, the dependency named in plain words, and a sketch of both modules. 14 are graded obvious and 16 subtle. One case, post_comments, is the Mirdin quiz question, so accuracy excluding it is reported too.
The axes. Each case is sent four times. spec lists the useful subsets the product actually ships; blind gives only the product description, so the model must imagine the subsets itself — which is the hard half of applying this rule. none defines the criteria only in the question text; article adds a 522-word condensed statement of the rule as a parnas_reference state field.
| Class | Criterion | What it means | N per arm |
|---|---|---|---|
| sound | sound | All four criteria hold. | 12 |
| no_simplification | criterion 1 | A is not essentially simpler for using B: it maps in and out of B, repeats B's work, or bypasses B where it matters. | 12 |
| b_contorted | criterion 2 | B is substantially more complex because it may not use A: duplicated tables, a mirrored copy of A's state, a parameter bag that exists to avoid the call. | 12 |
| b_not_reusable | criterion 3 | No useful subset contains B but not A: B takes A's types, is keyed by A's ids, or branches on A's phases. | 12 |
| a_useful_without_b | criterion 4 | A useful subset contains A but not B: the general module references the specific one. | 12 |
This is the quiz question. The Mirdin quiz asks which change most improves the maintainability of a Post class holding a list of Comment. The answer is that Post should not contain comments, and the reason is the subset criteria: a useful subset of a blog has posts and no comment system. Below is that case as this bench sends it — no multiple choice, just the dependency and the five criteria as a choice question.
Jev answers a_useful_without_b at 1.00. On the quiz's own four-option form, in the tab to the right, it also gets this one right — the only one of the five it does.
{
"state": {
"product": "Blog or news website",
"dependency": "Post uses Comment",
"useful_subsets_shipped": [
"A publishing-only build with no comment system",
"A read-only archive mirror that shows posts and hides comments"
],
"code": "// comment.ts (B)\nexport class Comment {\n constructor(private readonly author: string, private readonly body: string) {}\n}\n\n// post.ts (A)\nimport { Comment } from './comment';\nexport class Post {\n private comments: Comment[];\n constructor(private readonly id: number, private content: string,\n private readonly author: string, comments: Comment[]) {\n this.comments = [...comments];\n }\n getComments(): Comment[] { return [...this.comments]; }\n addComment(c: Comment): void { this.comments.push(c); }\n}",
"notes": "`code` sketches only the parts of each module that matter for the dependency. Imports name the direction of use. Assume both modules are otherwise complete and correct."
},
"model": "jev-latest",
"questions": {
"verdict": {
"type": "choice",
"instructions": "`dependency` names one dependency in `code`: module A uses module B. Judge that dependency against Parnas' subset criteria. All four must hold: (1) A is essentially simpler because it uses B; (2) B is not substantially more complex because it may not use A; (3) a useful subset of the system contains B but not A; (4) no conceivably useful subset contains A but not B. `product` says what the system is, and `useful_subsets_shipped` lists real subsets when it is present. Name the single criterion that fails most clearly, or `sound` if all four hold. Judge only the named dependency; ignore unrelated faults in the code.",
"criteria": {
"sound": {
"what": "All four criteria hold: B knows nothing of A, B is useful in subsets without A, A is genuinely simpler for using B, and A cannot work without B.",
"not_for": "Code that merely looks clean; check each of the four criteria in turn."
},
"no_simplification": {
"what": "Criterion 1 fails: using B does not make A essentially simpler. A translates in and out of B's shapes, repeats B's work, or bypasses B where it matters.",
"not_for": "A that is genuinely shorter or clearer for the call, even if the call site is verbose.",
"examples": [
"A validates every field again by hand after calling the validator",
"An interface with one implementation reached through a large options bag at the only call site"
]
},
"b_contorted": {
"what": "Criterion 2 fails: B is substantially more complex because it is not allowed to call A. B carries a duplicate of A's knowledge, a mirrored copy of A's state, or a parameter bag that exists only to avoid the call.",
"not_for": "B that is simply large on its own terms, with no duplication of A.",
"examples": [
"A parser carrying its own name resolver because layering forbids calling the resolver",
"Two files holding the same table with a 'keep in sync' comment"
]
},
"b_not_reusable": {
"what": "Criterion 3 fails: no useful subset ships B without A. B takes A's types, is keyed by A's identifiers, or branches on A's internal phases. A general name hides that B is a piece of A in another file.",
"not_for": "B that happens to have one caller today but is written in terms of its own concepts.",
"examples": [
"A module named StringUtils exporting truncateForProductCard(p: Product)",
"A message table keyed by one form's field ids"
]
},
"a_useful_without_b": {
"what": "Criterion 4 fails: a useful subset contains A but not B, so the general module must not reference the specific one. The dependency points from general to specific and should be inverted or held by id.",
"not_for": "A that genuinely cannot do its job without B.",
"examples": [
"Post holding a list of Comment when a publishing-only build has no comments",
"Logger taking an HttpRequest when the batch importer logs and serves no HTTP"
]
}
}
},
"not_simpler": {
"type": "noul",
"instructions": "For the dependency named in `dependency`: Does using B fail to make A essentially simpler?",
"criteria": {
"true": "Yes, clearly: A is no simpler for using B is the situation here.",
"false": "No: A is no simpler for using B does not apply to this dependency."
}
},
"b_contorted": {
"type": "noul",
"instructions": "For the dependency named in `dependency`: Is B substantially more complex because it is not allowed to use A?",
"criteria": {
"true": "Yes, clearly: B is bloated by the ban on calling A is the situation here.",
"false": "No: B is bloated by the ban on calling A does not apply to this dependency."
}
},
"b_not_reusable": {
"type": "noul",
"instructions": "For the dependency named in `dependency`: Is there no useful subset of the system containing B but not A?",
"criteria": {
"true": "Yes, clearly: B ships nowhere without A is the situation here.",
"false": "No: B ships nowhere without A does not apply to this dependency."
}
},
"a_alone": {
"type": "noul",
"instructions": "For the dependency named in `dependency`: Is there a conceivably useful subset of the system containing A but not B?",
"criteria": {
"true": "Yes, clearly: a useful subset has A but not B is the situation here.",
"false": "No: a useful subset has A but not B does not apply to this dependency."
}
}
}
}{
"model": "jev-1.13.0",
"answers": {
"verdict": {
"type": "choice",
"choice": "a_useful_without_b",
"confidence": 0.99,
"probabilities": {
"a_useful_without_b": 1.0,
"b_contorted": 0.0,
"b_not_reusable": 0.0,
"no_simplification": 0.0,
"sound": 0.0
}
},
"not_simpler": {
"type": "noul",
"noul": 0.58
},
"b_contorted": {
"type": "noul",
"noul": 0.13
},
"b_not_reusable": {
"type": "noul",
"noul": 0.66
},
"a_alone": {
"type": "noul",
"noul": 0.94
}
},
"usage": {
"input_tokens": 1549,
"output_tokens": 149
}
}| Metric | None | Reference | Δ |
|---|---|---|---|
| 5-way · all | 88 | 92 | +3 |
| 5-way · subsets listed | 90 | 90 | ±0 |
| 5-way · subsets withheld | 87 | 93 | +7 |
| 5-way · excluding the quiz case | 88 | 91 | +3 |
| bad dependency found · recall | 85 | 96 | +10 |
| sound kept · specificity | 100 | 100 | ±0 |
| recall sound | 100 | 100 | ±0 |
| recall no_simplification | 50 | 58 | +8 |
| recall b_contorted | 100 | 100 | ±0 |
| recall b_not_reusable | 100 | 100 | ±0 |
| recall a_useful_without_b | 92 | 100 | +8 |
| subtle cases | 78 | 84 | +6 |
| ECE | 0.134 | 0.061 | −0.073 |
| input tokens per request | 1555 | 2352 | +797 |
| latency p50 ms | 288 | 285 | −3 |
Paired by case: both right 53, right only without the reference 0, right only with it 2, both wrong 5. Unlike the MIRO test, the flips here go one way.
The gain is concentrated in the blind condition (87 → 93%), where the model has to imagine the subsets itself and the reference supplies the worked examples. With the subsets listed, the reference changes nothing. Two flips on 60 pairs is weak evidence; treat the direction, not the size.
THE PARNAS SUBSET CRITERIA (David Parnas, "Designing Software for Ease of Extension and Contraction", 1979). A software system is not one program but a family of programs. Good structure is judged by which useful subsets and extensions you can build without rewriting. That turns "should module A use module B?" into four testable questions. Say A uses B. The dependency is acceptable only when all four hold: 1. SIMPLER A is essentially simpler because it uses B. 2. FREE B is not substantially more complex because it is not allowed to use A. 3. B ALONE There is a useful subset of the system containing B but not A. 4. NOT A ALONE There is no conceivably useful subset containing A but not B. The slogan: do not let the general depend on the specific. The general module is the one that appears in more subsets. WHEN CRITERION 1 FAILS (A is not simpler for using B) A translates in and out of B's shapes, re-does B's work, or bypasses B on the paths that matter. Examples: a service that maps 100 lines to and from a client and then calls raw HTTP for two of three outcomes; a handler that calls a schema validator and then re-validates every field by hand; an interface with exactly one implementation reached through a seven-field options bag at the only call site. WHEN CRITERION 2 FAILS (B is contorted by the ban) The layering rule forbids B from calling A, so B grows a second copy of A's knowledge. Look for duplicated tables with "keep in sync" comments, a mirrored cache of another module's state, a parser carrying its own name resolver, a layout engine carrying its own font metrics. Removing the ban would delete most of B. WHEN CRITERION 3 FAILS (B is not reusable) B exists only to serve A. Its parameters are A's view model, its keys are A's field ids, it branches on A's internal phases, it imports A's types. A general-sounding name (StringUtils, Geometry, CacheKeys, RetryPolicy) hides that no subset can ship B alone. B is a piece of A that moved to another file. WHEN CRITERION 4 FAILS (A is useful without B) The general module references the specific one. A useful subset has A and no B at all. Examples: Post holding a list of Comment, when a publishing-only build has no comments; Money with a formatForInvoiceLine method, when payroll needs money and no invoices; Logger taking an HttpRequest, when the batch importer logs and serves no HTTP; Vector2 with a drawDebug(SpriteRenderer), when the headless physics build has no renderer. The fix is to invert the reference, hold an id instead of the object, or move the operation to the specific side. SOUND DEPENDENCIES LOOK LIKE THIS InvoicePdf uses Money. HttpServer uses Router. SpellChecker uses Trie. Comment holds a PostId. In each case the depended-on module knows nothing of its caller, ships in subsets that have no caller, and saves the caller real work. Judging: name the single criterion that fails most clearly. A dependency that satisfies all four is sound even if the code has other faults.
No sound dependency is ever called broken, in either arm: 0 false alarms on 12 sound cases. For a review tool that is the expensive error, and it does not happen here.
Every miss is the same one: a criterion-1 case read as something else. Whether a call actually makes the caller simpler is the only one of the four that cannot be answered by looking at what each module names.
| Truth | N | Prec % | Recall % | F1 | State |
|---|---|---|---|---|---|
| sound | 6 | 67 | 100 | 0.80 | ok |
| no_simplification | 6 | 100 | 50 | 0.67 | warn |
| b_contorted | 6 | 100 | 100 | 1.00 | ok |
| b_not_reusable | 6 | 100 | 100 | 1.00 | ok |
| a_useful_without_b | 6 | 100 | 100 | 1.00 | ok |
| Truth | N | Prec % | Recall % | F1 | State |
|---|---|---|---|---|---|
| sound | 6 | 60 | 100 | 0.75 | warn |
| no_simplification | 6 | 100 | 50 | 0.67 | warn |
| b_contorted | 6 | 100 | 100 | 1.00 | ok |
| b_not_reusable | 6 | 100 | 100 | 1.00 | ok |
| a_useful_without_b | 6 | 100 | 83 | 0.91 | ok |
| Detector | AUC | Best cut | Prec % | Recall % | State |
|---|---|---|---|---|---|
| no_simplification | 0.68 | 0.49 | 30 | 100 | warn |
| b_contorted | 1.00 | 0.80 | 100 | 100 | ok |
| b_not_reusable | 0.87 | 0.46 | 69 | 75 | ok |
| a_useful_without_b | 0.85 | 0.61 | 45 | 83 | ok |
Four independent yes/no probabilities sent alongside the choice. The best cut is picked on the same 60 samples it is scored on, so read it as an upper bound.
| Tier | N | No reference | With reference |
|---|---|---|---|
| obvious | 28 | 100% | 100% |
| subtle | 32 | 78% | 84% |
| Case | Dependency | Truth | Tier | No reference | With reference | Verdicts given |
|---|---|---|---|---|---|---|
| eventbus_selftalk | Editor uses EventBus | no_simplification | subtle | 0% | 0% | sound ×2 |
| repo_escape_hatches | ReportStore uses Repository<T> | no_simplification | subtle | 0% | 0% | sound ×2 |
| formatter_one_impl | EmailBuilder uses Formatter | no_simplification | subtle | 0% | 50% | sound ×2 |
| user_cart | User uses ShoppingCart | a_useful_without_b | subtle | 50% | 100% | a_useful_without_b ×1, sound ×1 |
| post_comments | Post uses Comment · from the quiz | a_useful_without_b | obvious | 100% | 100% | a_useful_without_b ×2 |
| money_invoice_line | Money uses InvoiceLine | a_useful_without_b | obvious | 100% | 100% | a_useful_without_b ×2 |
| logger_httprequest | Logger uses HttpRequest | a_useful_without_b | obvious | 100% | 100% | a_useful_without_b ×2 |
| vector_sprite | Vector2 uses SpriteRenderer | a_useful_without_b | obvious | 100% | 100% | a_useful_without_b ×2 |
| daterange_payroll | DateRange uses PayrollPeriod | a_useful_without_b | subtle | 100% | 100% | a_useful_without_b ×2 |
| parser_resolver | Parser uses SymbolTable (and the layering rule forbids Parser from using Resolver) | b_contorted | subtle | 100% | 100% | b_contorted ×2 |
| scheduler_jobstore | Runner uses Scheduler (and Scheduler may not use JobStore) | b_contorted | subtle | 100% | 100% | b_contorted ×2 |
| renderer_layout | Renderer uses Layout (and Layout may not use Renderer) | b_contorted | subtle | 100% | 100% | b_contorted ×2 |
| tokens_policy | TokenIssuer uses Policy (and Policy may not use TokenIssuer) | b_contorted | subtle | 100% | 100% | b_contorted ×2 |
| csv_money | CsvExporter uses NumberFormat (and the export layer may not use Money) | b_contorted | subtle | 100% | 100% | b_contorted ×2 |
| notifier_directory | Notifier uses NameFormatter (and NameFormatter may not use Directory) | b_contorted | subtle | 100% | 100% | b_contorted ×2 |
| totals_checkoutpage | CheckoutPage uses OrderTotals | b_not_reusable | obvious | 100% | 100% | b_not_reusable ×2 |
| stringutils_productcard | ProductCard uses StringUtils | b_not_reusable | obvious | 100% | 100% | b_not_reusable ×2 |
| retry_syncjob | SyncJob uses RetryPolicy | b_not_reusable | subtle | 100% | 100% | b_not_reusable ×2 |
| validation_formids | QuoteForm uses ValidationMessages | b_not_reusable | obvious | 100% | 100% | b_not_reusable ×2 |
| cachekeys_feed | FeedService uses CacheKeys | b_not_reusable | subtle | 100% | 100% | b_not_reusable ×2 |
| geometry_gallery | Gallery uses Geometry | b_not_reusable | subtle | 100% | 100% | b_not_reusable ×2 |
| wrapper_rewrites | PaymentService uses GatewayClient | no_simplification | obvious | 100% | 100% | no_simplification ×2 |
| config_one_constant | ThumbnailWorker uses ConfigLoader | no_simplification | subtle | 100% | 100% | no_simplification ×2 |
| double_validation | SignupHandler uses SchemaValidator | no_simplification | obvious | 100% | 100% | no_simplification ×2 |
| invoice_money | InvoicePdf uses Money | sound | obvious | 100% | 100% | sound ×2 |
| server_router | HttpServer uses Router | sound | obvious | 100% | 100% | sound ×2 |
| orders_clock | OrderService uses Clock | sound | obvious | 100% | 100% | sound ×2 |
| spellcheck_trie | SpellChecker uses Trie | sound | obvious | 100% | 100% | sound ×2 |
| comment_postid | Comment uses PostId | sound | subtle | 100% | 100% | sound ×2 |
| resizer_rect | ImageResizer uses Rect | sound | obvious | 100% | 100% | sound ×2 |
| ID | Truth | Verdict | Conf | Ref | Cond | Tier |
|---|