SOFTWARE DESIGN · JEV
EVALUATION · TYPESAFE JEV · PARNAS SUBSET CRITERIA

Jev applies the subset criteria well — it never calls a sound dependency broken, and it finds 85% of the bad ones

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%.

jev-1.13.0 · 120 REQUESTS · 30 DEPENDENCIES · 2 REFERENCE ARMS · 1 CHOICE + 4 NOUL PER REQUEST · 2026-09-17
5-WAY · NO REFERENCE
88.3%
SPEC 90 · BLIND 87
5-WAY · WITH REFERENCE
91.7%
SPEC 90 · BLIND 93
BAD DEPENDENCY FOUND
85.4%
SOUND KEPT 100 · FP 0
WEAKEST CRITERION
50.0%
CRITERION 1 · NOT SIMPLER
REQUESTS
120
30 CASES · 2 CONDITIONS · 2 ARMS
COST
0.0106USD
234K TOK · P50 285 MS
WHAT WAS TESTEDTHE RULE · THE CORPUS · THE AXES

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.

ClassCriterionWhat it meansN per arm
soundsoundAll four criteria hold.12
no_simplificationcriterion 1A 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_contortedcriterion 2B 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_reusablecriterion 3No 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_bcriterion 4A useful subset contains A but not B: the general module references the specific one.12
ONE REQUEST, VERBATIMpost_comments.spec · 291 MS · 1549 TOK IN

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.

REQUEST · POST api.typesafe.ai/v1/systemoneAUTHORIZATION: BEARER …
{
  "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."
      }
    }
  }
}
RESPONSE · HTTP 200jev-1.13.0
{
  "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
  }
}
WITH VS WITHOUT THE RULE IN THE STATE60 PAIRED REQUESTS PER ARM · Δ = REFERENCE − NONE
MetricNoneReferenceΔ
5-way · all8892+3
5-way · subsets listed9090±0
5-way · subsets withheld8793+7
5-way · excluding the quiz case8891+3
bad dependency found · recall8596+10
sound kept · specificity100100±0
recall sound100100±0
recall no_simplification5058+8
recall b_contorted100100±0
recall b_not_reusable100100±0
recall a_useful_without_b92100+8
subtle cases7884+6
ECE0.1340.061−0.073
input tokens per request15552352+797
latency p50 ms288285−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.

STATE.PARNAS_REFERENCE · SENT VERBATIM IN THE REFERENCE ARM522 WORDS
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.
WHERE VERDICTS LANDROWS = TRUTH · COLS = JEV · SHADE = SHARE OF ROW
NO REFERENCE
WITH REFERENCE

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.

PER CLASS · SUBSETS LISTEDNO REFERENCE · N = 30
TruthNPrec %Recall %F1State
sound6671000.80ok
no_simplification6100500.67warn
b_contorted61001001.00ok
b_not_reusable61001001.00ok
a_useful_without_b61001001.00ok
PER CLASS · SUBSETS WITHHELDNO REFERENCE · N = 30
TruthNPrec %Recall %F1State
sound6601000.75warn
no_simplification6100500.67warn
b_contorted61001001.00ok
b_not_reusable61001001.00ok
a_useful_without_b6100830.91ok
NOUL DETECTORSNO REFERENCE · ONE PER CRITERION
DetectorAUCBest cutPrec %Recall %State
no_simplification0.680.4930100warn
b_contorted1.000.80100100ok
b_not_reusable0.870.466975ok
a_useful_without_b0.850.614583ok

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.

CALIBRATIONNO REFERENCE · ECE 0.134
TierNNo referenceWith reference
obvious28
100%
100%
subtle32
78%
84%
EVERY DEPENDENCY4 REQUESTS EACH · 2 CONDITIONS × 2 ARMS
CaseDependencyTruthTierNo referenceWith referenceVerdicts given
eventbus_selftalkEditor uses EventBusno_simplificationsubtle
0%
0%
sound ×2
repo_escape_hatchesReportStore uses Repository<T>no_simplificationsubtle
0%
0%
sound ×2
formatter_one_implEmailBuilder uses Formatterno_simplificationsubtle
0%
50%
sound ×2
user_cartUser uses ShoppingCarta_useful_without_bsubtle
50%
100%
a_useful_without_b ×1, sound ×1
post_commentsPost uses Comment · from the quiza_useful_without_bobvious
100%
100%
a_useful_without_b ×2
money_invoice_lineMoney uses InvoiceLinea_useful_without_bobvious
100%
100%
a_useful_without_b ×2
logger_httprequestLogger uses HttpRequesta_useful_without_bobvious
100%
100%
a_useful_without_b ×2
vector_spriteVector2 uses SpriteRenderera_useful_without_bobvious
100%
100%
a_useful_without_b ×2
daterange_payrollDateRange uses PayrollPerioda_useful_without_bsubtle
100%
100%
a_useful_without_b ×2
parser_resolverParser uses SymbolTable (and the layering rule forbids Parser from using Resolver)b_contortedsubtle
100%
100%
b_contorted ×2
scheduler_jobstoreRunner uses Scheduler (and Scheduler may not use JobStore)b_contortedsubtle
100%
100%
b_contorted ×2
renderer_layoutRenderer uses Layout (and Layout may not use Renderer)b_contortedsubtle
100%
100%
b_contorted ×2
tokens_policyTokenIssuer uses Policy (and Policy may not use TokenIssuer)b_contortedsubtle
100%
100%
b_contorted ×2
csv_moneyCsvExporter uses NumberFormat (and the export layer may not use Money)b_contortedsubtle
100%
100%
b_contorted ×2
notifier_directoryNotifier uses NameFormatter (and NameFormatter may not use Directory)b_contortedsubtle
100%
100%
b_contorted ×2
totals_checkoutpageCheckoutPage uses OrderTotalsb_not_reusableobvious
100%
100%
b_not_reusable ×2
stringutils_productcardProductCard uses StringUtilsb_not_reusableobvious
100%
100%
b_not_reusable ×2
retry_syncjobSyncJob uses RetryPolicyb_not_reusablesubtle
100%
100%
b_not_reusable ×2
validation_formidsQuoteForm uses ValidationMessagesb_not_reusableobvious
100%
100%
b_not_reusable ×2
cachekeys_feedFeedService uses CacheKeysb_not_reusablesubtle
100%
100%
b_not_reusable ×2
geometry_galleryGallery uses Geometryb_not_reusablesubtle
100%
100%
b_not_reusable ×2
wrapper_rewritesPaymentService uses GatewayClientno_simplificationobvious
100%
100%
no_simplification ×2
config_one_constantThumbnailWorker uses ConfigLoaderno_simplificationsubtle
100%
100%
no_simplification ×2
double_validationSignupHandler uses SchemaValidatorno_simplificationobvious
100%
100%
no_simplification ×2
invoice_moneyInvoicePdf uses Moneysoundobvious
100%
100%
sound ×2
server_routerHttpServer uses Routersoundobvious
100%
100%
sound ×2
orders_clockOrderService uses Clocksoundobvious
100%
100%
sound ×2
spellcheck_trieSpellChecker uses Triesoundobvious
100%
100%
sound ×2
comment_postidComment uses PostIdsoundsubtle
100%
100%
sound ×2
resizer_rectImageResizer uses Rectsoundobvious
100%
100%
sound ×2
EVERY SAMPLECLICK A ROW FOR THE CODE, THE AUTHORED REASON AND THE FULL DISTRIBUTION
IDTruthVerdictConfRefCondTier
CAVEATSREAD BEFORE QUOTING A NUMBER
  • 30 cases is a small corpus. One case is six samples, so a single case moves the headline by about three points. The 60-pair reference comparison is weaker still.
  • The cases were written by Claude for this test, with the failing criterion chosen first and the code written to fit it. Real dependencies are rarely that clean, and several of the criterion-2 cases carry a NOTE comment that states the duplication outright — which makes them easier than the code they stand for.
  • Whether a subset is "useful" is a judgment about a product, not a fact about code. The spec condition supplies that judgment; the blind condition asks the model to make it.
  • Criterion 1 is the subjective one, and its recall (50%) is the number most likely to be an artefact of how those six cases were written.
  • One prompt design, one run per arm. Repeats are not needed: on the quiz tab the same request returned the same choice in 10 of 10 runs, 20 times out of 20.