From cc61301776d16451808de9c5c5f59ae5d89e35ea Mon Sep 17 00:00:00 2001 From: Branimir Georgiev Date: Fri, 25 Sep 2026 13:13:04 +0300 Subject: [PATCH 1/3] feat(efficacy): the control gains a second application and the security and data-protection trees The round-1 application has no sign-in or customers, so the security and data_protection rows could not move on it. base-2.zip pins the uncounted calibration trial (git archive of f74c0cc) by sha256, and the builder makes four trees from it: insecure-1 and secured-1 for security, leaky-1 and protected-1 for data protection. Twelve landing checks confirm each edit changed the tree it names and no other; the self-test in CI builds both applications. Part of #1767. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/design/efficacy-benchmark.md | 10 +- tests/efficacy/control/README.md | 38 ++- tests/efficacy/control/base-2.zip | Bin 0 -> 62722 bytes tests/efficacy/control/control.py | 374 ++++++++++++++++++++++++++++-- 4 files changed, 386 insertions(+), 36 deletions(-) create mode 100644 tests/efficacy/control/base-2.zip diff --git a/docs/design/efficacy-benchmark.md b/docs/design/efficacy-benchmark.md index dd6a1b54..90babd36 100644 --- a/docs/design/efficacy-benchmark.md +++ b/docs/design/efficacy-benchmark.md @@ -589,11 +589,11 @@ No person scores the judge. Two automated checks do: bundle it was quoted from, and the report prints the share found per trial. A judge that never opened the code still returns plausible numbers; this is what tells the two apart. -- **The control fixture:** six trees built from one pinned application - (`tests/efficacy/control/`). Every tree passes the same tests, so only - structure separates them. Some are damaged, some improved. A primary - dimension counts only if it falls on the damaged trees and rises on the - improved ones. +- **The control fixture:** eleven trees built from two pinned applications + (`tests/efficacy/control/`). Every tree passes the same tests as its + application, so only what was changed separates them. Some are damaged, + some improved. A primary dimension counts only if it falls on the damaged + trees and rises on the improved ones. **Where the control's application comes from.** A row can only fall or rise on code that has what it measures. The first pinned application is a diff --git a/tests/efficacy/control/README.md b/tests/efficacy/control/README.md index c8ae8b68..0781ecd8 100644 --- a/tests/efficacy/control/README.md +++ b/tests/efficacy/control/README.md @@ -5,10 +5,16 @@ not change when the code changes measures nothing, and nothing in a normal run would show that: an arm that moves no row looks like an arm that changed nothing. -This control settles it. Six trees are built from one pinned application and -the judge scores each. Every tree passes the application's whole suite, so what -separates them is structure alone, and each damages or improves one thing, so a -row that moves says which. +This control settles it. Trees are built from two pinned applications and the +judge scores each. Every tree passes its application's whole suite, so what +separates it from its base is the one thing it damages or improves, and a row +that moves says which. + +The first application, from round 1, carries the six trees for `design`, +`readability` and `maintainability`. It predates sign-in and customers, so the +`security` and `data_protection` rows cannot move on it; their five trees are +built from the second, one uncounted calibration trial on the extended +specification (design ยง5.7). ## Running it @@ -59,6 +65,21 @@ reading. A root left claimed by a run that crashed is taken over by the next. | `improved-2` | `improved-1`, and the store maps a rule's own terms onto its columns with a codec per column, so no module dispatches on a kind | | `improved-3` | `improved-2`, and each kind declares the fields it asks a form for, so the form-building code and the template are derived from the registry and no layer names a kind | +The second application's trees: + +| Tree | What it is | +|---|---| +| `base-2` | the calibration trial's application, unaltered: one guard before every route but sign-in, a CSRF token on every form, a salted password hash, `next` followed only to a local path, a secret key generated into a file; erasure clears the customer's fields, and nothing is logged | +| `insecure-1` | the secret key a literal, the password stored and compared as typed, the user lookup built from a string, and `next` followed wherever it points | +| `secured-1` | the secret key read from the environment first, the session cookie marked `HttpOnly` and `SameSite`, three security headers on every answer, and the check on `next` a named function that also refuses a backslash | +| `leaky-1` | the customer's name, email and address written to the log on creation and erasure, and erasure only raising the flag, leaving every field in the database | +| `protected-1` | one tuple names the personal fields, which erasure clears and the export returns; one lookup answers None for an erased customer, so no view or template reads the flag for itself; SQLite overwrites what it deletes | + +The damage in `insecure-1` and `leaky-1` sits where the hidden suite does not +look: it states that password storage, cookie flags and an off-site `next` are +not graded, and it reads erasure through the pages. Each tree's hidden-suite +result is identical, test by test, to `base-2`'s. + ## Reading a result A row is doing its job when it falls on the tree that damages what it @@ -198,14 +219,15 @@ rises on a tree built to improve it. `design` and `maintainability` fall on `improved-3`. The one-point flips of single judgings remain inside each triple, which is what the mean is there to absorb. -## Why the base is a pinned archive +## Why the bases are pinned archives -`base.zip` holds one trial's output: the application every tree is built -from. It is an input to this control, not an artifact of it: +`base.zip` and `base-2.zip` each hold one trial's output: the application its +trees are built from. `base-2.zip` is `git archive` of the calibration trial's +commit `f74c0cc`, which reproduces it byte for byte. It is an input to this control, not an artifact of it: regenerating it would compare a later run against a different application, and every number above would silently stop meaning what it says. -Two things hold it still. The builder refuses any archive whose sha256 is not +Two things hold them still. The builder refuses any archive whose sha256 is not the one recorded in `control.py`, so a changed base stops the control instead of quietly shifting what it measures. And every edit the builder makes is an exact-text swap that refuses when its target is absent, so a base that drifts diff --git a/tests/efficacy/control/base-2.zip b/tests/efficacy/control/base-2.zip new file mode 100644 index 0000000000000000000000000000000000000000..41eba7d5f098b4e0a52b148d37c43aaa5f3cbc31 GIT binary patch literal 62722 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 literal 0 HcmV?d00001 diff --git a/tests/efficacy/control/control.py b/tests/efficacy/control/control.py index 159bec3e..9258f8f1 100644 --- a/tests/efficacy/control/control.py +++ b/tests/efficacy/control/control.py @@ -1,11 +1,11 @@ """Build the rubric control's trees, so the judge can be validated. The benchmark's primary dimensions are a model's opinion, and an opinion that -never changes measures nothing. This builds three trees from one pinned -application and leaves them where `judge.py` finds them: the application -unaltered, a deliberately damaged copy, and a deliberately improved one. All -three pass the same test suite, so what the judge sees between them is -structure and nothing else. +never changes measures nothing. This builds trees from two pinned +applications and leaves them where `judge.py` finds them: each application +unaltered, and copies deliberately damaged or improved in one respect. Every +tree of one application passes that application's test suite, so what the +judge sees between them is what was changed and nothing else. Reading the result is in README.md. What matters here is that a run whose damaged tree scores no worse, or whose improved tree scores no better, has @@ -14,8 +14,8 @@ py tests/efficacy/control/control.py --root C:/efficacy/control-2026-09-20 py tests/efficacy/judge.py --root C:/efficacy/control-2026-09-20 -`base/` is an input, not an artifact: it is one trial's output, pinned, and it -is never regenerated. Regenerating it would compare a later run against a +Each base archive is an input, not an artifact: it is one trial's output, +pinned, and it is never regenerated. Regenerating it would compare a later run against a different application, and the before-and-after numbers would mean nothing. Every edit below is an exact-text swap that refuses when its target is absent, so a base that drifts stops the control instead of silently mutating less. @@ -42,6 +42,12 @@ # as source to be brought into line. BASE_SHA256 = "38b9a1f262ebae7b5c858178468ff53257c3308b6a57a7caa81db6fbe4671f0c" +# The second application, for the rows the first cannot move: it has sign-in +# and customers, which the round-1 application predates. It is one uncounted +# calibration trial on the extended specification, pinned the same way. +BASE_2 = os.path.join(HERE, "base-2.zip") +BASE_2_SHA256 = "a07d606c035078cff718de7cf9ed02131ea06b6bcf09ee8ecf0f296638425b74" + # The domain modules the damaged tree merges into one, in the order they are # concatenated: errors before the objects that raise them, rules before the # algorithm that applies them. @@ -50,6 +56,10 @@ TRIALS = ("base-1", "degraded-1", "improved-1", "improved-2", "improved-3", "obscured-1") +# The second application's trees: its security and its handling of personal +# data, each damaged in one tree and improved in another. +TRIALS_2 = ("base-2", "insecure-1", "secured-1", "leaky-1", "protected-1") + class ControlError(Exception): """A mutation could not be applied to the tree as written.""" @@ -308,6 +318,164 @@ def obscure(tree): os.path.join(tree, "tariff", "pricing.py")) +def edit(tree, rel, *swaps): + """Apply `(old, new, where)` swaps to one file of the tree, in order.""" + text = read(tree, rel) + for old, new, where in swaps: + text = swap(text, old, new, where) + write(tree, rel, text) + + +def insecure(tree): + """Damage how sign-in and its secrets are defended, and nothing else. + + The secret key becomes a literal, the password is stored and compared as + typed, the user lookup is built from a string, and `next` is followed + wherever it points. The suite grades none of those, so every tree still + answers it alike and the security row is what has something to move on. + """ + edit(tree, "tariff/web/__init__.py", + (SECRET_FROM_FILE, "", "the generated secret key"), + (" app.secret_key = _get_or_create_secret_key(db_path)\n", + ' app.secret_key = "tariff-secret-key"\n', "the key's source")) + edit(tree, "tariff/web/views_auth.py", + ("from werkzeug.security import check_password_hash\n", "", + "the hash import"), + ('not check_password_hash(user["password_hash"], password)', + 'user["password"] != password', "the password comparison"), + (' if not next_url.startswith("/") or ' + 'next_url.startswith("//"):\n', + " if not next_url:\n", "the local-path check on next")) + edit(tree, "tariff/db.py", + (" password_hash TEXT NOT NULL\n", " password TEXT NOT NULL\n", + "the password column"), + (' "SELECT * FROM users WHERE username = ?", (username,)\n', + " f\"SELECT * FROM users WHERE username = '{username}'\"\n", + "the user lookup"), + ("def set_user_password(conn, username, password_hash):", + "def set_user_password(conn, username, password):", + "the password setter"), + (' "INSERT INTO users (username, password_hash) VALUES (?, ?) "\n' + ' "ON CONFLICT(username) DO UPDATE SET password_hash = ' + 'excluded.password_hash",\n' + " (username, password_hash),\n", + ' "INSERT INTO users (username, password) VALUES (?, ?) "\n' + ' "ON CONFLICT(username) DO UPDATE SET password = ' + 'excluded.password",\n' + " (username, password),\n", "the password write"), + (" from werkzeug.security import generate_password_hash\n\n", "", + "the seed's hash import"), + (' set_user_password(conn, "admin", ' + "generate_password_hash(admin_password))", + ' set_user_password(conn, "admin", admin_password)', + "the seeded password")) + + +def secure(tree): + """Close what the base leaves open around sign-in, and nothing else. + + The secret key is read from the environment before the generated file, + the session cookie is marked, every answer carries the headers a browser + reads, and the check on `next` becomes one named function. The base + already has one guard, a CSRF token on every form and a salted hash, so + this is the step from there to the security row's top description. + """ + edit(tree, "tariff/web/__init__.py", + (" app.secret_key = _get_or_create_secret_key(db_path)\n", + SECURED_CONFIG, "the key's source and the cookie flags"), + (" @app.teardown_appcontext\n", + SECURITY_HEADERS + " @app.teardown_appcontext\n", + "the security headers")) + edit(tree, "tariff/web/views_auth.py", + ("def register(app):\n", LOCAL_PATH + "def register(app):\n", + "the named next check"), + (' if not next_url.startswith("/") or ' + 'next_url.startswith("//"):\n' + ' next_url = url_for("dashboard")\n' + " return redirect(next_url)\n", + ' return redirect(next_url if _is_local_path(next_url) ' + 'else url_for("dashboard"))\n', "the redirect after sign-in")) + + +def leak(tree): + """Let customer data escape where the suite does not look. + + Creating and erasing a customer writes their name, email and address to + the log, and erasure only raises the flag, leaving every field in the + database. The pages read the flag, so what the suite sees is unchanged. + """ + edit(tree, "tariff/web/views_customers.py", + ("import json\n", "import json\nimport logging\n", "the log import"), + ("from .helpers import require\n", + "from .helpers import require\n\nlog = logging.getLogger(__name__)\n", + "the module logger"), + (" customer_id = db_module.add_customer(conn, name, email, " + "address)\n", + " customer_id = db_module.add_customer(conn, name, email, " + "address)\n" + ' log.info("created customer %s: %s <%s>, %s", customer_id, ' + "name, email, address)\n", "the creation log line"), + (" db_module.erase_customer(conn, customer_id)\n", + ' log.info("erasing customer %s: %s <%s>", customer_id, ' + 'customer["name"], customer["email"])\n' + " db_module.erase_customer(conn, customer_id)\n", + "the erasure log line")) + edit(tree, "tariff/db.py", + (' "UPDATE customers SET erased = 1, name = NULL, email = NULL, ' + 'address = NULL "\n', + ' "UPDATE customers SET erased = 1 "\n', + "the erasure's cleared fields")) + + +def protect(tree): + """Give the customers' personal data one place, read by everything. + + One tuple names the personal fields; erasure clears exactly those and the + export returns exactly those. One lookup answers None for an erased + customer, so no view or template checks the flag for itself, and SQLite + overwrites what it deletes instead of leaving it in free pages. + """ + edit(tree, "tariff/db.py", + (' conn.execute("PRAGMA foreign_keys = ON")\n', + ' conn.execute("PRAGMA foreign_keys = ON")\n' + ' conn.execute("PRAGMA secure_delete = ON")\n', + "the overwrite on delete"), + ("def list_customers(conn):\n", PERSONAL_FIELDS + "def list_customers(conn):\n", + "the personal fields"), + ("def email_taken(conn, email):\n", + LIVE_CUSTOMER + "def email_taken(conn, email):\n", + "the one erased check"), + (' "UPDATE customers SET erased = 1, name = NULL, email = NULL, ' + 'address = NULL "\n' + ' "WHERE customer_id = ? AND erased = 0",\n', + " _ERASE,\n", "the erasure's cleared fields")) + edit(tree, "tariff/web/views_customers.py", *[ + (" customer = db_module.get_customer(conn, customer_id)\n" + ' if customer is None or customer["erased"]:\n', + " customer = db_module.get_live_customer(conn, customer_id)\n" + " if customer is None:\n", "view %d's erased check" % n) + for n in (1, 2, 3)] + [ + (' "name": customer["name"],\n' + ' "email": customer["email"],\n' + ' "address": customer["address"],\n', + " **db_module.personal_data(customer),\n", + "the export's fields")]) + edit(tree, "tariff/web/views_invoices.py", + (" customer = db_module.get_customer(conn, customer_id)\n" + ' if customer is None or customer["erased"]:\n', + " customer = db_module.get_live_customer(conn, customer_id)\n" + " if customer is None:\n", "the invoice form's erased check"), + *[(" customer = db_module.get_customer(conn, " + 'invoice_row["customer_id"])\n', + " customer = db_module.get_live_customer(conn, " + 'invoice_row["customer_id"])\n', "invoice page %d's lookup" % n) + for n in (1, 2)]) + for page in ("invoice_detail", "invoice_print"): + edit(tree, "tariff/web/templates/%s.html" % page, + (" {% if customer and not customer.erased %}\n", + " {% if customer %}\n", "%s's erased check" % page)) + + # Every entity a variant is supposed to change, with what each tree must read # after the build. A control whose mutation silently did nothing reports the # tree it never touched as clean, so the build refuses rather than hand that @@ -366,9 +534,84 @@ def obscure(tree): "improved-2": True, "improved-3": False, "obscured-1": True}), ) + +def is_in(rel, *needles): + """A probe: whether any of `needles` is in one file of the tree.""" + return lambda t: any(needle in read(t, rel) for needle in needles) + + +# The same, for the second application: each row names what one tree was +# built to change, and every other tree of that application must read as the +# base does. +LANDINGS_2 = ( + ("the secret key is a literal", + is_in("tariff/web/__init__.py", 'app.secret_key = "'), + {"base-2": False, "insecure-1": True, "secured-1": False, + "leaky-1": False, "protected-1": False}), + ("the password is stored hashed", + is_in("tariff/db.py", "generate_password_hash(admin_password)"), + {"base-2": True, "insecure-1": False, "secured-1": True, + "leaky-1": True, "protected-1": True}), + ("the user lookup is built from a string", + is_in("tariff/db.py", "username = '{username}'"), + {"base-2": False, "insecure-1": True, "secured-1": False, + "leaky-1": False, "protected-1": False}), + ("next is checked for a local path", + is_in("tariff/web/views_auth.py", 'next_url.startswith("//")', + "_is_local_path(next_url)"), + {"base-2": True, "insecure-1": False, "secured-1": True, + "leaky-1": True, "protected-1": True}), + ("the secret key is read from the environment", + is_in("tariff/web/__init__.py", "TARIFF_SECRET_KEY"), + {"base-2": False, "insecure-1": False, "secured-1": True, + "leaky-1": False, "protected-1": False}), + ("the session cookie is marked", + is_in("tariff/web/__init__.py", "SESSION_COOKIE_SAMESITE"), + {"base-2": False, "insecure-1": False, "secured-1": True, + "leaky-1": False, "protected-1": False}), + ("every answer carries security headers", + is_in("tariff/web/__init__.py", "X-Content-Type-Options"), + {"base-2": False, "insecure-1": False, "secured-1": True, + "leaky-1": False, "protected-1": False}), + ("customer data is written to the log", + is_in("tariff/web/views_customers.py", "log.info("), + {"base-2": False, "insecure-1": False, "secured-1": False, + "leaky-1": True, "protected-1": False}), + ("erasure clears the personal fields", + is_in("tariff/db.py", "name = NULL, email = NULL", + '"%s = NULL" % column'), + {"base-2": True, "insecure-1": True, "secured-1": True, + "leaky-1": False, "protected-1": True}), + ("one tuple names the personal fields", + is_in("tariff/db.py", "PERSONAL_FIELDS = ("), + {"base-2": False, "insecure-1": False, "secured-1": False, + "leaky-1": False, "protected-1": True}), + ("a view or template reads the erased flag for itself", + lambda t: ('customer["erased"]' in web(t) + or "customer.erased" in web(t)), + {"base-2": True, "insecure-1": True, "secured-1": True, + "leaky-1": True, "protected-1": False}), + ("deleted data is overwritten in the file", + is_in("tariff/db.py", "secure_delete"), + {"base-2": False, "insecure-1": False, "secured-1": False, + "leaky-1": False, "protected-1": True}), +) + _CACHE = {} +def web(tree): + """The web package's modules and templates, without the store beneath.""" + chunks = [] + for dirpath, dirs, files in os.walk(os.path.join(tree, "tariff", "web")): + dirs[:] = [d for d in dirs if d != "__pycache__"] + for name in sorted(files): + if os.path.splitext(name)[1] in (".py", ".html"): + chunks.append(read(tree, os.path.relpath( + os.path.join(dirpath, name), tree))) + return "\n".join(chunks) + + def domain(tree): """The domain package's own modules, without the web package beneath it.""" chunks = [] @@ -417,24 +660,23 @@ def whole(tree): return _CACHE[tree] -def pinned(): - """The base archive, refusing any content but the one pinned above.""" - with io.open(BASE, "rb") as handle: +def pinned(archive=BASE, expected=BASE_SHA256): + """A base archive, refusing any content but the one pinned above.""" + with io.open(archive, "rb") as handle: data = handle.read() digest = hashlib.sha256(data).hexdigest() - if digest != BASE_SHA256: - raise ControlError("base.zip is %s, not the pinned %s; the control's " + if digest != expected: + raise ControlError("%s is %s, not the pinned %s; the control's " "application has changed and its recorded readings " - "no longer describe it" % (digest, BASE_SHA256)) + "no longer describe it" + % (os.path.basename(archive), digest, expected)) return data -def build(root): - """Write the three trees where `judge.py` reads them.""" - data = pinned() - scoring = os.path.abspath(root).rstrip("\\/") + "-scoring" +def extract(data, scoring, trials): + """Write one fresh copy of an archive per trial, where the judge reads.""" made = {} - for trial in TRIALS: + for trial in trials: tree = os.path.join(scoring, "scoring", trial, "tree", trial) if os.path.isdir(tree): shutil.rmtree(tree) @@ -442,21 +684,37 @@ def build(root): with zipfile.ZipFile(io.BytesIO(data)) as archive: archive.extractall(tree) made[trial] = tree + return made + + +def build(root): + """Write every tree of both applications where `judge.py` reads them.""" + scoring = os.path.abspath(root).rstrip("\\/") + "-scoring" + made = extract(pinned(), scoring, TRIALS) degrade(made["degraded-1"]) improve(made["improved-1"]) improve_further(made["improved-2"]) improve_form(made["improved-3"]) obscure(made["obscured-1"]) + + second = extract(pinned(BASE_2, BASE_2_SHA256), scoring, TRIALS_2) + insecure(second["insecure-1"]) + secure(second["secured-1"]) + leak(second["leaky-1"]) + protect(second["protected-1"]) + made.update(second) _CACHE.clear() return made def landings(made): - """Each entity a variant claims to change, read back off the trees.""" + """Each entity a variant claims to change, read back off the trees of the + application it was built from.""" rows = [] - for label, probe, expected in LANDINGS: - actual = {trial: probe(tree) for trial, tree in sorted(made.items())} - rows.append((label, actual, actual == expected)) + for table, trials in ((LANDINGS, TRIALS), (LANDINGS_2, TRIALS_2)): + for label, probe, expected in table: + actual = {trial: probe(made[trial]) for trial in sorted(trials)} + rows.append((label, actual, actual == expected)) return rows @@ -496,7 +754,7 @@ def main(argv): "differ as this control claims" % failed) return 1 if not options.self_test: - print("built %s under %s" % (", ".join(TRIALS), + print("built %s under %s" % (", ".join(TRIALS + TRIALS_2), os.path.abspath(options.root).rstrip( "\\/") + "-scoring")) print("next: py tests/efficacy/judge.py --root %s" % options.root) @@ -834,6 +1092,76 @@ def _build_rule(kind, rule_id, form, errors): {% endfor %} """ +SECRET_FROM_FILE = '''def _get_or_create_secret_key(db_path): + key_path = f"{db_path}.secretkey" + if os.path.exists(key_path): + with open(key_path, "rb") as handle: + return handle.read() + key = secrets.token_bytes(32) + with open(key_path, "wb") as handle: + handle.write(key) + return key + + +''' + +SECURED_CONFIG = ''' # The deployment's own key wins; the generated file keeps a bare install + # working without one. + app.secret_key = (os.environ.get("TARIFF_SECRET_KEY") + or _get_or_create_secret_key(db_path)) + + # `Secure` is opt-in because the development server speaks plain HTTP, and + # a browser drops a secure cookie there, which would sign everyone out. + app.config.update( + SESSION_COOKIE_HTTPONLY=True, + SESSION_COOKIE_SAMESITE="Lax", + SESSION_COOKIE_SECURE=os.environ.get("TARIFF_SECURE_COOKIES") == "1", + ) +''' + +SECURITY_HEADERS = ''' @app.after_request + def _security_headers(response): + response.headers.setdefault("X-Content-Type-Options", "nosniff") + response.headers.setdefault("X-Frame-Options", "DENY") + response.headers.setdefault("Referrer-Policy", "same-origin") + return response + +''' + +LOCAL_PATH = '''def _is_local_path(target): + """True when `target` names a path on this site: a protocol-relative + `//host` or a backslash a browser reads as one would leave it.""" + return (target.startswith("/") and not target.startswith("//") + and "\\\\" not in target) + + +''' + +PERSONAL_FIELDS = '''# Every column that identifies a customer. Erasure clears exactly these and +# the export returns exactly these, so a field added here is covered by both. +PERSONAL_FIELDS = ("name", "email", "address") + +_ERASE = ("UPDATE customers SET erased = 1, " + + ", ".join("%s = NULL" % column for column in PERSONAL_FIELDS) + + " WHERE customer_id = ? AND erased = 0") + + +def personal_data(customer): + """The personal fields of one customer row, in declaration order.""" + return {column: customer[column] for column in PERSONAL_FIELDS} + + +''' + +LIVE_CUSTOMER = '''def get_live_customer(conn, customer_id): + """The customer, or None when there is none or they were erased; the one + place the erased flag is read for a single customer.""" + customer = get_customer(conn, customer_id) + return None if customer is None or customer["erased"] else customer + + +''' + if __name__ == "__main__": sys.exit(main(sys.argv[1:])) From ded262061c1a8f8467b3556c00afe417d80313a7 Mon Sep 17 00:00:00 2001 From: Branimir Georgiev Date: Fri, 25 Sep 2026 13:24:49 +0300 Subject: [PATCH 2/3] docs(efficacy): the second application's hidden-suite comparison and its limit Every tree of the second application fails the same 240 hidden-suite checks as its base, and passes the same 293: the calibration trial does not seed an empty database, so checks behind sign-in fail alike on all five. Also wraps a line past the declared width. Part of #1767. Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/efficacy/control/README.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/efficacy/control/README.md b/tests/efficacy/control/README.md index 0781ecd8..63ef3afc 100644 --- a/tests/efficacy/control/README.md +++ b/tests/efficacy/control/README.md @@ -78,7 +78,12 @@ The second application's trees: The damage in `insecure-1` and `leaky-1` sits where the hidden suite does not look: it states that password storage, cookie flags and an off-site `next` are not graded, and it reads erasure through the pages. Each tree's hidden-suite -result is identical, test by test, to `base-2`'s. +result is identical, test by test, to `base-2`'s: 293 of 534 pass. The +calibration trial seeds only through its command, not when the application +opens an empty database as the specification asks, so the suite's +administrator does not exist and 240 checks behind sign-in fail on every tree +alike. That comparison therefore covers the 293 that pass, the pricing and API +checks among them. ## Reading a result @@ -223,9 +228,10 @@ triple, which is what the mean is there to absorb. `base.zip` and `base-2.zip` each hold one trial's output: the application its trees are built from. `base-2.zip` is `git archive` of the calibration trial's -commit `f74c0cc`, which reproduces it byte for byte. It is an input to this control, not an artifact of it: -regenerating it would compare a later run against a different application, and -every number above would silently stop meaning what it says. +commit `f74c0cc`, which reproduces it byte for byte. Each is an input to this +control, not an artifact of it: regenerating one would compare a later run +against a different application, and every number above would silently stop +meaning what it says. Two things hold them still. The builder refuses any archive whose sha256 is not the one recorded in `control.py`, so a changed base stops the control instead From 04816430f677175b22fa10f580eff8b37e3f07d7 Mon Sep 17 00:00:00 2001 From: Branimir Georgiev Date: Fri, 25 Sep 2026 13:25:36 +0300 Subject: [PATCH 3/3] docs(efficacy): mark the hidden-suite comparison as measured on its date Part of #1767. Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/efficacy/control/README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/efficacy/control/README.md b/tests/efficacy/control/README.md index 63ef3afc..676c684d 100644 --- a/tests/efficacy/control/README.md +++ b/tests/efficacy/control/README.md @@ -77,13 +77,17 @@ The second application's trees: The damage in `insecure-1` and `leaky-1` sits where the hidden suite does not look: it states that password storage, cookie flags and an off-site `next` are -not graded, and it reads erasure through the pages. Each tree's hidden-suite -result is identical, test by test, to `base-2`'s: 293 of 534 pass. The +not graded, and it reads erasure through the pages. + + +Each tree's hidden-suite result is identical, test by test, to `base-2`'s, +at suite revision `65bfb9a`: 293 of 534 pass. The calibration trial seeds only through its command, not when the application opens an empty database as the specification asks, so the suite's administrator does not exist and 240 checks behind sign-in fail on every tree alike. That comparison therefore covers the 293 that pass, the pricing and API checks among them. + ## Reading a result