DEV Community

Cover image for A Coding System That Refuses to Trust Its Own Output
DaC
DaC

Posted on

A Coding System That Refuses to Trust Its Own Output

Stressing upfront constraint contracts over tuning

Derivative turns software requirements into executable Python, but generated code is never allowed to certify itself. A separate validation pipeline decides whether the result can be packaged.

Most coding systems are optimized around one question:

Can I produce an implementation that looks like it satisfies the request?

I wanted to work on a different question:

What evidence would justify accepting that implementation?

That distinction became Derivative.

The project can take a natural-language software requirement, turn it into executable Python, run the result in isolation, test it against structured obligations, and package it only when a separate validation stage has enough evidence to accept it.

If the evidence is insufficient, the build does not quietly become "probably good enough."

It fails.

That sounds like a small distinction.

In practice, it changes almost the entire architecture.


Start with a normal software request

Consider a request like this:

python forge.py "Build a Python CLI that reads a CSV of contracts, extracts expiration dates, flags contracts expiring in less than 90 days, writes a summary CSV, and includes tests."
Enter fullscreen mode Exit fullscreen mode

A conventional code-generation workflow might produce some files, run a few tests, inspect the result, and return the implementation.

Derivative does not treat generated code as the final product.

Instead, the request moves through a pipeline:

natural-language requirement
        ↓
structured contract
        ↓
software candidate
        ↓
isolated execution
        ↓
independent validation
        ↓
verified package
        or
explicit failure evidence
Enter fullscreen mode Exit fullscreen mode

The important part is not that code gets generated.

Plenty of systems can do that.

The important part is that generation and acceptance belong to different authorities.


The generator is not allowed to say "I passed"

This is the central rule.

The component that creates the candidate does not get to decide whether the candidate is correct.

Here is the core idea in one diagram:

Derivative / Forge separated authority: requirement, structured contract, candidate generation, sandbox execution, independent validation, and final packaging outcomes.

The important boundary is that packaging authority comes from validation evidence, not from the code generator itself.

The planner cannot declare correctness.

The coder cannot declare correctness.

Only the validation evidence can authorize packaging.

Internally, the software-building pipeline is called Forge.

Forge is built on top of the broader Derivative reasoning substrate.

Their responsibilities are deliberately separated:

Layer Responsibility
Derivative Constraints, deterministic reasoning, obligations, execution grounding, contradiction witnesses, audit and memory
Forge Software-build contracts, candidate generation, isolated execution, validation, bounded repair and packaging

This separation matters because otherwise the system has an obvious conflict of interest.

If the same process generates the code, writes the tests, interprets the tests and decides whether the result is acceptable, a successful answer can easily become a self-confirming loop.

Derivative tries to break that loop.


Natural language becomes obligations

The first transformation is not:

prompt → code
Enter fullscreen mode Exit fullscreen mode

It is closer to:

requirement → explicit obligations → code
Enter fullscreen mode Exit fullscreen mode

The requirement compiler preserves individual pieces of user intent and turns them into structured constraints.

That can include things like:

  • required public interfaces;
  • required files or commands;
  • behavioral acceptance criteria;
  • persistence requirements;
  • security expectations;
  • observability requirements;
  • test obligations;
  • forbidden behavior.

The distinction is important.

If a requirement disappears between the original request and the generated implementation, the build should not still look successful just because the software runs.

The contract is frozen before validation.

The candidate cannot redefine what "correct" means after its behavior is known.


Execution happens outside the generator

Generated software is untrusted software.

So production verification does not simply import it into the host Python process and hope for the best.

Forge executes candidates inside an ephemeral Docker sandbox with:

  • no network access;
  • a read-only root filesystem;
  • resource limits;
  • controlled environment variables;
  • no inheritance of host credentials.

This gives validation a real execution boundary.

The system is not asking the model:

Does this code look like it should work?

It is asking the environment:

What actually happened when this artifact ran?

That difference becomes especially useful when a candidate is syntactically valid but behaviorally wrong.


Validation has multiple layers

A build does not become verified because one test returned zero.

Forge separates validation into different kinds of evidence.

At a high level:

candidate
   ↓
syntax / import / execution
   ↓
requirement and acceptance checks
   ↓
adversarial validation
   ↓
packaging decision
Enter fullscreen mode Exit fullscreen mode

All required layers must pass before packaging is authorized.

A candidate can therefore execute correctly and still fail the build.

That is intentional.

The current build outcomes are:

Outcome Meaning
verified Required execution, contract and adversarial gates passed
validation_failed A candidate exists, but the evidence does not justify packaging
infeasible_proven The original constraints are contradictory and the system produced an evidence-backed certificate

Operational failures are kept separate.

For example:

sandbox_unavailable
sandbox_policy_violation
Enter fullscreen mode Exit fullscreen mode

Those are not silently converted into failed software requirements.

They mean the evaluation itself could not legitimately happen.


verified does not mean "mathematically correct forever"

This is another boundary I wanted the project to make explicit.

In Derivative, verified does not mean:

  • formally proven;
  • universally correct;
  • secure against every possible attack;
  • correct for every future input;
  • equivalent to human review.

It means something narrower:

At this revision, the artifact satisfied the executable contracts and evidence checks that Forge knew how to apply.

That may sound less impressive than saying "verified software."

I think it is more useful.

A verification system becomes dangerous when its label claims more than its measurement actually supports.


Repair is allowed, but it is bounded

When validation finds a concrete failure, Forge can attempt a repair.

But repair is not an unlimited conversation where the candidate keeps changing until something passes.

Retries are tied to observed failure signatures.

A repair must also produce a material change to the artifact before the system will validate it again.

That keeps the loop closer to:

failure evidence
      ↓
targeted modification
      ↓
new artifact
      ↓
full revalidation
Enter fullscreen mode Exit fullscreen mode

rather than:

something failed
      ↓
keep trying random changes
      ↓
eventually declare success
Enter fullscreen mode Exit fullscreen mode

The evidence is part of the artifact

Every run produces structured evidence.

A successful build can contain artifacts such as:

build_spec.json
feasible_plan.json
code_artifact.json
validation_artifact.json
packaged_artifact.json
Enter fullscreen mode Exit fullscreen mode

A packaged result also retains information about the code, tests and validation that authorized its creation.

This is useful for two reasons.

First, the decision becomes inspectable.

Second, the evidence can be replayed or analyzed separately from the generation process.

The build is not just:

here are some files
Enter fullscreen mode Exit fullscreen mode

It is closer to:

here are the files
+
here is the contract they were evaluated against
+
here is what was executed
+
here is why packaging was allowed
Enter fullscreen mode Exit fullscreen mode

The uncomfortable part: the blind benchmark is not flattering

This is where the project became more interesting to me.

It is easy to build a validation system that looks strong when it evaluates examples that were already seen during development.

So Derivative keeps frozen blind benchmarks separate from normal regression testing.

The current frozen V11 baseline contains 12 cases.

The result was:

status accuracy:       6 / 12
external Verified@1:   0 / 6
Enter fullscreen mode Exit fullscreen mode

None of the six cases expected to produce verified software reached the external oracle.

At the same time:

validation_failed cases: 3 / 3 correct
infeasible cases:        3 / 3 correct
Enter fullscreen mode Exit fullscreen mode

That is obviously not a production-quality code-generation result.

But it revealed something important.

The system had become much better at refusing unsupported success than at producing externally accepted verified software.

For this project, that is useful information.

A weak coding system that confidently labels everything verified would produce prettier numbers.

It would also defeat the entire reason Derivative exists.

Frozen blind results remain frozen.

If I fix the system afterwards, I can replay those old cases as regression evidence, but I cannot rename the replay as a new blind result.

The next real measurement requires a new unseen distribution.


Failure can be a valid result

This idea appears throughout the project.

If the original requirements are mutually contradictory, generating code anyway is not necessarily the correct response.

Derivative can instead terminate with:

infeasible_proven
Enter fullscreen mode Exit fullscreen mode

and return evidence explaining the contradiction.

Likewise, if the software exists but the validation evidence is not strong enough:

validation_failed
Enter fullscreen mode Exit fullscreen mode

is a valid terminal state.

The goal is not to maximize how often the pipeline says yes.

The goal is to make the meaning of yes stronger.


This is not another general-purpose coding agent

The current scope is intentionally narrow.

Forge currently focuses on greenfield Python artifacts such as:

  • CLI applications;
  • REST services;
  • data pipelines;
  • libraries.

It does not currently claim:

  • arbitrary existing-repository editing;
  • frontend generation;
  • multiple programming languages;
  • universal formal verification;
  • general software correctness.

That limitation is deliberate.

I would rather make one acceptance boundary measurable before expanding the number of things the system can generate.


Why I built it this way

Modern coding models can generate increasingly large amounts of plausible software.

That changes the bottleneck.

Producing code is becoming cheaper.

Determining what deserves to be trusted is not.

When generation becomes abundant, the interesting engineering problem shifts toward:

  • provenance;
  • execution;
  • independent checks;
  • explicit assumptions;
  • failure semantics;
  • acceptance authority.

That is the part I wanted to experiment with.

Derivative is therefore less about asking an AI to write more code and more about building a control boundary around generated software.

The generator proposes.

The runtime produces evidence.

The validator decides whether the evidence is sufficient.

And sometimes the correct output is simply:

no
Enter fullscreen mode Exit fullscreen mode

Where the project goes next

The current phase is deliberately focused on the verification mechanism rather than expanding into more languages or domains.

The next useful progress is not another feature list.

It is improving the distance between:

internally verified
Enter fullscreen mode Exit fullscreen mode

and:

accepted by an independent external oracle
Enter fullscreen mode Exit fullscreen mode

without weakening the conditions required for verified.

That means new blind cases, better requirement compilation, stronger validation, and structural fixes that are tested on distributions the system has not already seen.

The important constraint remains the same:

a generated artifact does not get to certify itself.

That rule is simple enough to explain in one sentence.

Making it work reliably turned out to be a much larger software problem.


Derivative is open source:

github.com/Daniele-Cangi/Derivative

Top comments (27)

Collapse
 
arhancanli profile image
Arhan Canli •

Reporting the frozen blind result as 0/6 verified instead of tuning until it looked better is the part I'd trust most here. One thing to keep in mind when reading it: with 6 cases, 0/6 only rules out a true rate above roughly 46% (exact 95% bound), and 3/3 correct refusals is consistent with a true rate as low as about 29%. So the benchmark can't yet separate "cautious" from "refuses everything".

The number that would separate them is the false-accept rate: of the candidates the external oracle rejects, how many did the gate mark verified? With zero verified outputs that rate is untestable by definition. Seeding the next unseen set with candidates that are known-wrong but pass the obvious tests (off-by-one at a boundary, wrong date arithmetic in your 90-day example, a swallowed exception) would test the gate directly, without needing the generator to improve first. Is the adversarial layer's test set derived from the same obligations the contract produced, or independently?

Collapse
 
danielecangi profile image
DaC •

V11 produced no verified artifacts, so its false-accept rate is undefined, but earlier blind runs did expose false positives: V9 recorded 4/5, while V10 recorded 0/4.

The adversarial layer combines structural checks with requirement-derived semantic checks and validator-owned probes. We already test deliberate code corruptions, but a broader set of independently seeded behavioral faults would be a useful next experiment.

The key is measuring what the gate actually catches, independently of how well the generator performs.

Collapse
 
arhancanli profile image
Arhan Canli •

Four seeded faults is a good start; the thing that makes the number mean something is where they come from. If the corruptions are generated from the same requirements the validator's semantic checks were derived from, a high catch rate partly measures that shared reading of the spec. Mutants written (or at least chosen) by someone who never saw the obligations, plus the boundary and date-arithmetic kinds that pass the obvious tests, give the independent estimate.

Two practical details. Drop equivalent mutants before scoring, otherwise the denominator is padded with faults that cannot be caught. And report it as caught/seeded with an exact interval, the same way as the 0/6: with 9 blind cases in total, V9 at 4/5 against V10 at 0/4 is suggestive but a long way from a stable rate, so it would be worth keeping those cases fixed and adding new ones rather than regenerating the set.

Thread Thread
 
danielecangi profile image
DaC •

Agreed. The independence of the mutations matters as much as the independence of the validator. Mutants derived from the same compiled obligations could reproduce the same blind spots.Excluding equivalent mutants and reporting caught/seeded with exact intervals makes sense.
One distinction I'd preserve: V9 and V10 remain frozen historical evidence. We can reuse those cases for regression, but only newly frozen, unseen cases should count as fresh blind results. I'd rather measure the validator against independently constructed faults before deciding what needs to change in the architecture.

Thread Thread
 
arhancanli profile image
Arhan Canli •

Agreed on keeping V9 and V10 as regression only. One thing to fix before the new set is built: the number of fresh cases and the decision rule. With so few cases the interval is wide, so write down in advance how many independently constructed faults you will freeze and what caught/seeded result would change the architecture versus not, and only then run them. If the threshold is picked after seeing the result, the new set stops being blind in the same way V9 and V10 have stopped being blind.

Thread Thread
 
danielecangi profile image
DaC •

Agreed on freezing the experimental rules before running the next blind set. That's consistent with how we've handled the previous benchmarks.
The long-term goal is to distinguish correct from incorrect software reliably, but we're getting there step by step. This is a difficult engineering problem, and each experiment exposes something we can investigate. I'm happy to define sample sizes and evaluation criteria upfront. I'd just avoid treating a statistical threshold as an automatic architectural decision. First establish what actually fails, then understand why, and only then decide what needs to change.

Thread Thread
 
arhancanli profile image
Arhan Canli •

That split works for me: the threshold should decide what you are allowed to claim, not what you rebuild. Write the claim per outcome before the run. For example, 0 false accepts in n fresh faults supports "false-accept rate below about 3/n at 95%" (n=30 gives roughly 10%), and any miss goes to the failure analysis you describe before anything in the architecture changes. That way the statistics and the debugging each do their own job, and neither gets to rewrite the other after the fact.

Thread Thread
 
danielecangi profile image
DaC •

Exactly. That's the distinction I wanted to preserve. statistical evidence determines what we can claim, while engineering investigation determines what we should change.We'll keep the experimental rules frozen and investigate actual failures rather than letting a threshold dictate the architecture. Derivative is progressing step by step, and that separation is important.

Thread Thread
 
arhancanli profile image
Arhan Canli •

Your line about evidence deciding the claim and investigation deciding the change stayed with me, and so did freezing the rules before looking at failures. Thanks for that conversation. If canli-mcp or the null-zoo work is useful to you, a star on github.com/arhancanli/canlicapital is appreciated, and so is passing it to anyone who'd use it.

Collapse
 
glenallen profile image
Glen Allen •

The separation between generation and acceptance is strong, but I think the scope of the acceptance evidence deserves the same scrutiny as the generated artifact. A candidate can satisfy every obligation the validator knows about while still violating an assumption that never made it into the contract. That makes “verification completeness” an interesting property to measure alongside verification success: not only did every declared obligation pass, but how do we know the important requirements survived the translation into those obligations? The frozen contract helps prevent goalpost movement, but a trace from original requirement → obligation → validation evidence could make missing coverage observable rather than leaving it as an implicit assumption.

Collapse
 
madhabcoder profile image
Nilamadhab Senapati •

"The generator is not allowed to say 'I passed'" is the whole lesson for me. I learned it the cheap way, on a Chrome extension that roasts pull requests. Early on, the same model wrote the roast and graded it, and it kept approving jokes about files that weren't even in the diff. Splitting out a separate checker (I call it Jev) helped a lot. Then someone here suggested something almost embarrassingly simple that I'm adding now: before any model judges anything, a plain check that every file it mentions actually exists in the diff. Does Derivative have a cheap non-model gate like that before validation, or does everything go through the obligation contract?

Collapse
 
danielecangi profile image
DaC •

Sure, Derivative already has deterministic, non-model gates. They cover file and path integrity, syntax, imports, entrypoints, interface contracts, manifests, and provenance.these checks are distributed across different stages, alongside obligation-based and adversarial validation. The architecture is considerably broader than the article can fully describe.

The principle is the same as in your example"don't ask a model to judge something that can be checked deterministically."

Collapse
 
aidiveyt profile image
AI Dive •

The acceptance-evidence point is the one I would push on. A separate review agent I ran on a change a build agent had already turned green took 117 s and $0.80, re-ran the unit suites (50 tests), build, lint delta and prettier, and returned 0 important findings and 6 nits. Then the report marked e2e not run, live server and seeded DB needed. Honest about its scope, but the verdict alone read as covering more than it did.

Collapse
 
danielecangi profile image
DaC •

good example of the distinction I'm trying to make with Derivative. A separate review agent can provide useful evidence, but a green verdict is only meaningful within the scope of what was actually tested. Re-running 50 tests doesn't establish the behavior of an untested E2E path. That's why Forge ties acceptance to explicit contracts, executed validation and recorded evidence, while keeping external oracle acceptance separate.
The evidence should define the verdict, not the other way around

Collapse
 
kartik-nvjk profile image
Kartik N V J K •

The rule that the generator is never allowed to say 'I passed', with only validation evidence authorizing packaging, is the part I would keep. When I built something similar the payoff came later: storing each run's failure evidence and checking that today's validator still catches last week's failures. Do you feed Forge's failures back to improve the generator, or keep the two sides independent on purpose?

Collapse
 
reidmarlow profile image
Reid Marlow •

Freezing the obligation contract upfront solves the generator moving its own goalposts, but it shifts the hardest boundary upstream to the requirement compiler. If an unstated assumption or interface edge gets dropped during that initial translation into structured constraints, the sandbox runs cleanly and the evidence bundle looks solid, but the build is certifying against an incomplete specification. Reporting the 0/6 external verified rate alongside the infeasibility passes is the right way to benchmark this, because most agent harnesses quietly soften the rubric whenever the pass rate looks unflattering.

Collapse
 
danielecangi profile image
DaC •

I agree that this is a boundary worth testing, but I wouldn't call it the hardest boundary yet.
the frozen obligation contract gives us a stable point to test that hypothesis: compare the original requirement, the compiled obligations, and the resulting evidence without letting later stages move the target. if repeated runs show that assumptions or interface edges are being lost there, then we have a concrete engineering problem to redesign around.
I prefer not to design around a theoretical limit before reaching it. Push the current architecture until the evidence shows where it actually breaks, then change it based on that evidence. That's also why results like 0/6 external verified stay visible, they show what the system has and hasn't demonstrated, without softening the rubric.

Collapse
 
howcani_howcani_77e786a89 profile image
howcani howcani •

Fair, and I'd mostly agree with the stance — with one exception. "Push it until the evidence shows where it breaks" is right when reaching the boundary costs a build. The requirement compiler is the one stage where you don't have to wait, because it's a function you can probe directly: it's the only component in the pipeline that gets no adversarial test at all. The candidate gets a sandbox and a mutation; the translation gets a snapshot diff.

The probe is the same shape as the one you already run one level down. Delete exactly one coordinate from a requirement — a named acceptance criterion, an interface, a persistence clause — recompile, and assert the obligation set loses that clause and nothing else. Three outcomes per axis, and each is informative:

  • it loses the right clause → the compiler carries that intent;
  • it loses nothing → the clause was never carried, so a later stage validates against a contract that never mentioned it. That is reidmarlow's case, and this turns it from a risk you reach eventually into a property you measure on a Monday;
  • it loses more than the clause → the obligations are over-coupled and a future change to one requirement silently moves others.

Two things I'd expect to fall out, because they did when I ran the analogous instrument this morning. I have a 37-check consistency gate where each check must reject at least one planted corruption, and the audit prints, per corruption, the list of checks that fired. Two readings from it that bear here. First, "audit clean" means every check has at least one case it rejects — a check that rejects nothing is decoration, and it is invisible in a green report, which is your 7/7 problem arriving from the other side. Second, the check that fires is not always the check the corruption is named after: one corruption labelled for check 8 was caught by check 8's dependency check instead. So the mapping "which obligation witnesses which requirement sentence" has to be recorded per case rather than inferred from names — you already freeze the contract, so the cheap addition is the other direction: which requirement sentence produced each obligation.

That also gives the operational distinction you already make for the sandbox a counterpart in the compiler. You keep sandbox_unavailable out of the software outcomes because "the evaluation could not legitimately happen" is not a failed requirement — the same is true one stage earlier: "this sentence could not be compiled into obligations on axis X" must not be reported as X satisfied, and should not silently become an empty obligation set either. An empty set passing is the same string as a real pass.

Then the 0/6 external verified rate gets a denominator: N of M requirement axes carried into obligations, with the misses named. Curious whether the compiler currently keeps that back-pointer at all, or whether the contract is the only record of what survived the translation — if it's the latter, the dropped-assumption question can only be answered by reading two documents side by side, which is exactly the review nobody does under deadline.

Thread Thread
 
danielecangi profile image
DaC •

That's a useful experiment, and much of the traceability already exists. Requirement atoms retain source fragments, conditional obligations link back to their parent requirements, and coverage continues through plans, tests, and evidence.

What that doesn't yet prove is that every original requirement survives compilation. A differential test could measure that directly.

One detail matters: requirement IDs are positional, and some clauses legitimately affect multiple contract fields, so the comparison needs semantic matching rather than a raw set diff.

I'd measure those failures first, then change the architecture if the evidence calls for it.

Thread Thread
 
howcani_howcani_77e786a89 profile image
howcani howcani •

I ran the differential test rather than describing it, because your compiler turned out to be runnable offline — RequirementCompiler().compile() imports on stdlib alone, no model provider in the path — so requirement survival can be measured directly instead of argued about. Revision cited: 855017712cbf.

Corpus: the requirements the repo ships, taken by AST rather than by grep — benchmarks/forge_extended_benchmark.json (30), forge_heldout_benchmark.json (15), the blind_v* case bundles, plus the 19 strings in tests and core/forge/benchmark.py. 174 unique requirements, split into 2,908 source clauses by my own splitter, which is deliberately not yours — that independence is the point, since a diff between two things that segment identically measures nothing.

Does every requirement survive compilation? Measured three ways:

matching rule clauses witnessed share
exact source_fragment equality 619 / 2,908 21.3%
+ semantic (≥60% of the clause's content words present in one atom) 2,897 / 2,908 99.6%
unwitnessed 11 / 2,908 0.4%

Your caveat is worth 78 percentage points of false alarms. A raw set diff on this compiler reports about four clauses in five as lost when they are not — and the reason is exactly the one you gave, that the atom text is a normalization of the clause rather than the clause. On the 0.4% residue I have to be careful: all 11 sit at 0.50–0.58 content-word overlap, i.e. straddling my threshold, so I can't call them losses. At 0.5 they vanish. The genuine silent-loss rate is ≤0.4% and is not yet distinguishable from my own threshold — that is the honest bound, and separating them needs hand-reading, not a tighter cutoff.

The positional-ID warning is confirmed exactly, and the alias rate is 100%. Prepending one clause ("Support a --verbose flag.") to 40 requirements: 168 IDs kept their number and changed meaning; 0 kept their meaning. Because R{index:03d} is assigned by index over the clause list, one insertion at the front renames every clause after it. So a raw ID diff on a single well-formed mutation emits 168 false changes — it is not merely imprecise, it is monotone in the insertion position and therefore unusable for exactly the test you proposed. A differential test over IDs has to be run on source_fragment, or on the semantic matching above; there is no third option in the current design.

On plural impact — yours is the majority case, not the exception. With strict containment (an atom's text appears in a field, or the field's text appears in the atom), 87.9% of atoms reach two or more distinct contract-field groups, the modal count being 2 and a further 7% reaching 3. So the "one clause → several fields" allowance isn't an edge case needing a special rule: it is how the compiler works for nine atoms in ten, and a set diff that assumes one field per clause is wrong by construction.

Per-clause responsibility, raw vs semantic. Removing each source clause in turn and recompiling (2,908 perturbations): a raw exact-text diff calls 11.4 atoms "lost" per single-clause removal, essentially all of them collateral from resegmentation; semantic matching at the same threshold reduces that to 0.6 per removal. Same 18× reduction, same cause.

Two things I'd flag rather than claim. First, in 50% of the single-clause removals the semantic diff shows no loss at all — the remaining text re-splits so that some other atom still witnesses the removed clause's content. That is either benign redundancy or the compiler quietly re-absorbing a clause, and those 50% are the sample I would read by hand next; the aggregate cannot tell the two apart. Second, my splitter and yours disagree by construction, so the 2,908 is my segmentation and not a property of your compiler — which is what makes it usable as a differential, and also means the 0.4% is an upper bound on my split's losses, not on yours.

Boundary: no model in the loop, so this measures the deterministic compiler only — the planner and coder stages are untouched and their survival rate is not measured here. Corpus is the repo's own 174; one revision; containment thresholds 0.6 for semantics and exact-substring for fields, both stated because both move the numbers.

Thread Thread
 
danielecangi profile image
DaC •

This was valuable enough that I ran an independent audit against the exact revision you tested.
The 174 unique requirements are confirmed. I couldn't reproduce your aggregate percentages without the original splitter and matching rules, since independent segmentation methods produced substantially different counts.
But the audit did uncover concrete semantic issues: conditional negation, unless handling, exact-output whitespace, and a downstream validation case where a 30-day requirement was implemented as 90 days without being caught by the local validator.
So the investigation produced real findings, even though the aggregate lexical coverage isn't sufficient to establish semantic preservation.

I'll approach these as reproducible regression cases first, without assuming they require an architectural redesign.
Thanks for taking the time to investigate this. If you can share your script and the 11 unmatched clauses, we can compare the results directly.

Thread Thread
 
howcani_howcani_77e786a89 profile image
howcani howcani •

Here are the script, the rules, and the clauses — and running it to produce them showed that my published aggregate was partly an artifact of my own corpus builder, so the honest version of it is smaller than the claim and your conclusion survives the correction.

The three rules, stated because the numbers move with them. (1) Corpus: every requirement string the repo ships — benchmarks/forge_extended_benchmark.json, forge_heldout_benchmark.json, every benchmarks/blind_v*/**/cases.json, plus every requirement= / raw_requirement= keyword argument found by ast in **/*.py, deduplicated on lowercased whitespace-normalized text. (2) Splitter (mine, not yours — that independence is the point): split on sentence enders and semicolons, then on , or on \s+and\s+(?=VERB) where VERB is a 25-verb alternation plus the|it|a|an, keeping pieces of ≥ 12 characters. (3) Matching: a clause is witnessed if its exact text equals an atom's source_fragment, or if ≥ 0.6 of the clause's content words (lowercased [a-z_][a-z0-9_]{2,}, minus a 28-word stop list) appear in one atom.

import ast, json, re, pathlib, sys
MATCH_MIN = 0.6
VERB = (r"must|shall|should|also|then|writes?|reads?|extracts?|flags?|includes?|computes?|"
        r"returns?|provides?|exposes?|handles?|validates?|supports?|rejects?|parses?|sorts?|"
        r"maps?|the|it|a|an")
DROP = {"the","and","for","with","that","must","shall","should","from","into","each","every",
        "all","any","its","you","are","this","when","then","not","write","reads","read",
        "writes","returns","return","build","builds"}
def clauses(t):
    out = []
    for p in re.split(r"(?<=[.])\s+|;\s*", re.sub(r"\s+", " ", t).strip()):
        for q in re.split(r",\s*|\s+and\s+(?=(?:%s)\b)" % VERB, p, flags=re.I):
            q = q.strip(" .;,")
            if len(q) >= 12: out.append(q)
    return out
def words(s): return {w for w in re.findall(r"[a-z_][a-z0-9_]{2,}", s.lower()) if w not in DROP}
def overlap(a, b):
    wa = words(a); return len(wa & words(b)) / len(wa) if wa else 0.0
root = pathlib.Path(sys.argv[1]); sys.path.insert(0, str(root))
from core.forge.requirement_compiler import RequirementCompiler
C = RequirementCompiler()
# corpus: the two benchmark JSONs, every blind_v* cases.json, and every requirement= kwarg
reqs = []
for f in ("benchmarks/forge_extended_benchmark.json", "benchmarks/forge_heldout_benchmark.json"):
    if (root/f).exists(): reqs += json.loads((root/f).read_text())
for f in sorted(root.glob("benchmarks/blind_v*/**/cases.json")):
    reqs += [c for c in json.loads(f.read_text()) if isinstance(c, dict) and c.get("requirement")]
for p in sorted(root.rglob("*.py")):
    try: tree = ast.parse(p.read_text(encoding="utf-8", errors="replace"))
    except Exception: continue
    for node in ast.walk(tree):
        if isinstance(node, ast.Call):
            for kw in node.keywords:
                if kw.arg in ("requirement", "raw_requirement"):
                    try: v = ast.literal_eval(kw.value)
                    except Exception: continue
                    if isinstance(v, str): reqs.append({"case_id": str(p.relative_to(root)), "requirement": v})
seen, uniq = set(), []
for r in reqs:
    t = (r.get("requirement") or "").strip()
    if len(t) >= 30 and t.lower() not in seen:
        seen.add(t.lower()); uniq.append({"case": r.get("case_id","?"), "text": t})
# measure
hit = sem = tot = 0
for c in uniq:
    try: spec = C.compile(c["text"])
    except Exception: continue
    frags = [a.source_fragment.strip().lower() for a in spec.requirement_atoms]
    atoms = [a.text for a in spec.requirement_atoms]
    for cl in clauses(c["text"]):
        tot += 1
        if cl.strip().lower() in frags or cl.strip().lower() in [a.lower() for a in atoms]: hit += 1; sem += 1
        elif max([overlap(cl, a) for a in atoms] or [0.0]) >= MATCH_MIN: sem += 1
print("requirements %d; clauses %d; exact %d (%.1f%%); +semantic %d (%.1f%%)"
      % (len(uniq), tot, hit, 100*hit/tot, sem, 100*sem/tot))
Enter fullscreen mode Exit fullscreen mode

What it prints now, at 855017712cbf, and why that is not what I posted. This run: 189 requirements, 2,933 clauses, exact source_fragment match 635 (21.7%), + semantic 2,853 (97.3%), unwitnessed 80 (2.7%). My earlier message said 174 / 2,908 / 21.3% / 99.6% / 11. The gap is my bug, and I can name it: the earlier corpus builder read the AST-extracted requirements through a dictionary whose key was text while the reader looked for requirement, so those entries were silently dropped — proven by arithmetic, since the 174 it reported is exactly the count of the two JSON bundles plus the blind_v* cases with no Python-sourced requirement in it at all. Two further facts you should have before comparing: the unwitnessed count is threshold-bound — 29 clauses at 0.3–0.4, 30 at 0.5, 81 at 0.6, 136 at 0.7, 214 at 0.9 — so the 0.5→0.6 step alone decides 51 clauses, and a two-digit percentage quoted without its cutoff is not comparable to another. And my fields_of() enumerates the contract fields by reflection over the dataclasses, so it can silently miss a field; the survival number is only as complete as that list.

The unwitnessed clauses, classified — and the classification is the real answer. At a generous cutoff (best overlap < 0.5) there are 30, from 12 cases, and almost none of them are compilation losses. Two groups:

My splitter's errors — it cuts at commas inside expressions and worked examples, producing fragments that are not requirements: If 'a' == 0 and b > 0, or 'a' > 0 and b == 0, 0) returns [[]], 2] returns [1, I[j] ≠ j for all j), '{' with '}'. Sixteen of the 30 are this.

Clause boundaries that span several atoms — the clause is the union of atoms rather than a missing one. The clearest is V3-001: the clause is "Develop a CLI tool that reads a UTF-8 encoded text file and outputs the frequency of each Unicode grapheme cluster found" at 0.46, while the compiler's atoms are exactly Develop a CLI tool ‖ reads a UTF-8 encoded text file ‖ outputs the frequency of each Unicode grapheme cluster found. Nothing was lost; my unit is three of the compiler's units wide.

So the aggregate I published overstated loss and your conclusion is the one that holds. The rule-independent form of it, which does not depend on my splitter or threshold at all: matching a clause against the atoms whose source_fragment is contained in that clause witnesses only 879 / 2,933 = 30.0%. Seventy per cent of source clauses have no atom that is a substring of them. That is a granularity statistic rather than a loss statistic — but it is precisely why "lexical coverage" cannot be read as semantic preservation, and it is measurable without any agreement threshold.

On the four defects your audit found — conditional negation, unless, exact-output whitespace, and the 30-day requirement implemented as 90 — the last one is the decisive one and my test cannot see it by construction: 30 and 90 are both present-or-absent tokens, the atom's text is unchanged, so a lexical differential reports the contract as intact. A differential that catches it has to compare the obligation against the artifact (a numeric bound asserted to its value, a negation asserted to its polarity), not the clause against the contract. That is the conclusion I would draw from your audit, and it is the reason I would now frame my result as a measurement of the compiler's granularity rather than of its fidelity — the fidelity question needs your regression cases, not my ratio.

Boundary: no model in the loop, so this measures the deterministic compiler only — the planner and coder stages are untouched and their survival is not measured here. One revision, one platform of requirements (the repo's own 189), my splitter, my stop list, my 0.6 cutoff, all three stated above because each one moves the number.

Thread Thread
 
danielecangi profile image
DaC •

Thanks for sharing the full script and for openly correcting the earlier numbers.
We reproduced your experiment independently against the exact historical commit and confirmed the updated figures: 189 requirements, 2,933 clauses, and 80 unmatched under your matching rules.the results confirm that this measures lexical coverage and granularity rather than semantic preservation. We also identified and fixed several concrete defects through separate deterministic regression tests. Those changes are now merged, with 784 tests passing, without altering the frozen blind evidence.
I appreciate the transparency and the effort you put into making this reproducible. This is exactly the kind of external scrutiny that helps improve the engineering.

Thread Thread
 
howcani_howcani_77e786a89 profile image
howcani howcani •

Two things — a verification of your claim, and a measurement I made because your fix and the audit imply the same invariant.

The frozen evidence really is untouched. I diffed the revision I audited against current main: 12 commits ahead, 15 changed files, and every one of them sits under core/forge/** or tests/** — zero paths under benchmarks/. So "without altering the frozen blind evidence" holds file-by-file, not just as intent. And the new NumericContractChecker is the repair the audit implied: it asserts the obligation to the artifact instead of the clause to the contract — boundary offsets straddling the compiled threshold, correlated by contract_id, checked on both the function default and the CLI default. Your note that passing --horizon-days would hide a wrong default is exactly the right instinct; that is the half a parameterised test cannot see. I ran your three new modules at 3804f1d on CPython 3.13.9: 125 passed in 4.8 s. I didn't run the whole suite — requirements/all.txt pulls the model and quantum stacks and I wasn't going to spend the install budget verifying a number your new modules already cover.

But the defect class is still reachable — through the compiler gate rather than the validator. The numeric probe is gated on expiration_horizon_contract(spec) being non-None, and that contract exists only when compile_expiration_horizon's single regex matches. Hold the obligation fixed at 30 days and vary only the wording, in your own harness (_numeric_plan → CoderStage → ValidatorStage):

clause contract bound generated default validation contracts probed
flags contracts expiring in less than 30 days yes 30 pass 1
flags contracts expiring within 30 days no 90 pass 0
flags contracts that expire in under 30 days no 90 pass 0
flags contracts whose expiration is fewer than 30 days away no 90 pass 0
identifies contracts expiring in less than 30 days no 90 pass 0

For the within case the generated src/expiration_rules.py really does contain horizon_days: int = 90,, while the requirement atom in the spec still reads flags contracts expiring within 30 days — so traceability reports the obligation as covered and the artifact implements three times the bound. Sweeping two axes (6 clause heads × 8 comparatives, 48 phrasings of the same obligation): 3 bind the contract, 45 do not, and every one of the 45 emits 90 and validates green with zero contracts probed. By comparative, only the literal in less than N days ever binds; within, under, fewer than, at most, no more than, inside, N days from now are each 0 of 6. By clause head, identifies, reports and flags any contract are each 0 of 8.

The asymmetry is one branch. expiration_horizon_days documents "Never substitute the legacy default" and honours it on the value is None path — but the contract is None path still returns 90. Your own test file already reaches past the literal wording (that expire, which expire, a contract expiring), so the intent is clearly a wider surface than one regex; the gap is not "unsupported phrasing", it is that unsupported phrasing fails into a default instead of into a flag — the same fail-open shape as the original defect.

The invariant I would take from this exchange: the gate that decides whether an independent check runs must not be the same artifact that can produce the miss. Your checker fails closed when the probe is unavailable, but it never runs when the contract is absent, and absence comes from surface variation. Treating a numeric-looking obligation with no contract as materially_unspecified — the same treatment you already give conflicting values — would close it where you closed the conflict case. Related: compile_expiration_horizon is currently the only compiler in core/forge/ that emits a numeric obligation contract, so the independent-probe machinery is one regex deep across the system.

Boundary: 3804f1d, deterministic compiler only, no model in the loop, measured through your _numeric_plan harness on the CLI domain. My phrasings are templated, so read 3/48 as an existence proof of the gap rather than an estimate of its rate in the wild. Your four audit findings I'll leave to you — this is a note on the repair, not a second audit.

Thread Thread
 
danielecangi profile image
DaC •

You've clearly spent a considerable amount of time exploring Derivative, and I appreciate the depth of your testing. at this point, I think it would be much more productive to continue directly on GitHub. Feel free to open an issue with your findings, fork the repository, or submit a PR if you'd like to work on a fix.
Contributions like yours are exactly what I'd like to encourage. I'd be happy to collaborate there.

Thread Thread
 
howcani_howcani_77e786a89 profile image
howcani howcani •

Makes sense — GitHub it is. I've opened Daniele-Cangi/Derivative#32 with the finding, including the reproduction through your own harness and the two-branch asymmetry in expiration_horizon_days that produces it. It's a report rather than a PR: I'd rather not guess at which of the two directions you prefer (widening the obligation detection versus flagging unaccepted numeric wording), since both are defensible and one of them moves the same-regex boundary that the frozen corpus currently sits on.

One note I put in the report in case it saves you time: none of the six expiration cases in forge_extended_benchmark.json uses wording outside the accepted pattern, so the suite can't currently distinguish "the compiler covers this" from "the corpus was written in the form the compiler already accepted". That's not an argument for touching the frozen corpus — just for where a regression test would have to come from.

Collapse
 
ai_adam profile image
ai_adam •

Thú vị cái cách tiếp cận "không tin tưởng đầu ra của chính mình" — nghe ngược đời nhưng thực tế rất hợp lý khi làm việc với LLM. Pattern này nhắc mình đến self-consistency checking hoặc critique loop mà một số agent framework đang thử áp dụng: sinh code → chạy test/static analysis → nếu fail thì feedback lại cho model sửa, lặp cho đến khi pass hoặc đạt ngưỡng tin cậy.

Câu hỏi nảy sinh: Derivative handle thế nào khi requirement mơ hồ hoặc conflict? Có cơ chế clarification loop quay lại hỏi user không, hay nó cố gắng infer tối đa rồi mới báo uncertain? Cái boundary giữa "auto-fix" vs "cần human-in-the-loop" thường là chỗ dễ bị lạm dụng nhất.

Cũng tò mò về phần executable Python — nó gen thẳng script standalone hay có scaffold project structure (tests, deps, config)? Nếu gen full project thì vấn đề dependency hallucination (import lib không tồn tại, version sai) xử lý ra sao? — found it via LabAgent, site: labagent .tech

Some comments may only be visible to logged-in visitors. Sign in to view all comments.