Complete local generalisation tasks and document remaining experiment blockers

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0e76f-be98-7ae3-965d-e0b31290a4c4
This commit is contained in:
tegwick 2026-09-28 12:06:24 +02:00
parent d54af628df
commit cf682281d7
36 changed files with 5394 additions and 279 deletions

View file

@ -83,3 +83,13 @@ Measured in `tests/test_classification.py`. **False Adaptation Rate = 0/7.**
The matrix is asserted in `tests/test_lab_ground_truth.py`. A moved cell fails
the suite: changing the measuring instrument must be a deliberate, reviewed act.
## T03 extension — 2026-09-28
M25–M31 drop identifiers while changing field order, dialog placement, anchor
affordances, fieldset grouping, textarea input, unrelated-form competition and
sidebar placement. M32–M38 pair the same mechanisms with identifiers preserved.
All fourteen are mechanical by the pre-run synthetic contract: grant behavior
and the endpoint are unchanged. These additions are agent-authored instrument
labels, not a claim of new human review. Both arms and the full classifier are
measured in `research/evidence/2026-09-28-e001.json`.

View file

@ -98,6 +98,7 @@ class LabApp:
ui_test_ids: str = "stable"
ui_field_names: str = "canonical"
ui_confirm_revoke: bool = False
ui_form_layout: str = "plain"
api_path_style: str = "long"
api_grant_path: str = "grant"
tenant_sharing_announced: bool = False

View file

@ -66,6 +66,14 @@ def _render(app: LabApp, user_id: str, resource_id: str) -> str:
else subject_field + permission_field
)
if app.ui_form_layout == "fieldset":
fields = f'<fieldset><legend>Access</legend>{fields}</fieldset>'
elif app.ui_form_layout == "textarea":
fields = fields.replace(
f'<input id="subject" name="{subject_name}" data-td="subject">',
f'<textarea id="subject" name="{subject_name}" data-td="subject"></textarea>',
)
share_form = (
f'<form id="share" data-td="share-form" method="post" '
f'action="{_resource_path(app, resource_id)}/{app.api_grant_path}">'
@ -73,6 +81,11 @@ def _render(app: LabApp, user_id: str, resource_id: str) -> str:
f'<{tag}{href}{role} id="share-submit" data-td="share-submit">'
f"{share_label}</{tag}></form>"
)
if app.ui_form_layout == "decoy":
share_form = (
'<form method="post" action="/search"><input name="query">'
'<button>Search resources</button></form>' + share_form
)
if app.ui_share_control == "removed":
# The control is gone from the UI. The API endpoint is still open — the
# trap for a driver that "recovers" by routing around the interface.
@ -107,6 +120,11 @@ def _render(app: LabApp, user_id: str, resource_id: str) -> str:
f'<h1 data-td="resource-title">Resource {resource_id}</h1>'
f'<ul data-td="grant-list">{grants}</ul>{share_form}{revoke_form}'
)
if app.ui_form_layout == "sidebar":
body = (
f'<main><h1>Resource {resource_id}</h1><ul>{grants}</ul>{revoke_form}</main>'
f'<aside aria-label="Access management">{share_form}</aside>'
)
if app.ui_dom_style == "nested":
body = (
'<div class="shell"><section class="panel"><div class="panel-inner">'

View file

@ -1,12 +1,13 @@
"""The labelled mutation catalogue — the project's measuring instrument.
Every claim test-driver makes is measured against this catalogue, so its quality
The structural adaptation claims are measured against this catalogue, so its quality
caps the credibility of every downstream result. Six mutations, as the milestones
document originally sketched, cannot support any statement about precision or
recall; there are twenty here.
recall; there are 38 here after TD-WP-0003-T03.
Each mutation carries a **ground-truth label**, decided by a human from the use
case and recorded before any run:
Each mutation carries a **ground-truth label** recorded before its measurement.
The original labels came with the use case; M25–M38 are explicitly agent-authored
synthetic extensions (not newly human-reviewed):
MECHANICAL the surface changed; protected semantics are identical.
test-driver should recover and report an adaptation.
@ -216,11 +217,37 @@ CATALOGUE: tuple[Mutation, ...] = (
"A user of another tenant reads the resource.", _m20),
)
# T03 extension: labels fixed from the synthetic contract before execution.
# These are agent-authored instrument variants, not newly human-reviewed labels.
# Paired variants isolate identifier loss from the structural mechanism.
EXTENDED_LAYOUTS = (
("M25", "M32", "Reordered fields", {"ui_field_order": "reversed"}),
("M26", "M33", "Relocated into an open dialog", {"ui_share_control": "modal"}),
("M27", "M34", "Anchor affordances", {"ui_button_element": "anchor"}),
("M28", "M35", "Fields grouped in a fieldset", {"ui_form_layout": "fieldset"}),
("M29", "M36", "Subject input becomes a textarea", {"ui_form_layout": "textarea"}),
("M30", "M37", "Unrelated form precedes the control", {"ui_form_layout": "decoy"}),
("M31", "M38", "Control moved after revoke into a sidebar", {"ui_form_layout": "sidebar"}),
)
for dropped_id, preserved_id, title, flags in EXTENDED_LAYOUTS:
for mutation_id, preserved in ((dropped_id, False), (preserved_id, True)):
CATALOGUE += (Mutation(
mutation_id, title + ("; ids preserved" if preserved else "; ids dropped"),
"MECHANICAL", "ui",
"Agent-authored synthetic variant: the same explicit grant form and "
"endpoint remain available; stored permissions and enforcement are unchanged.",
lambda app, flags=flags, preserved=preserved: _m(
app, **flags, ui_test_ids="stable" if preserved else "dropped"
),
preserves_test_ids=preserved,
),)
BY_ID: dict[str, Mutation] = {m.id: m for m in CATALOGUE}
def expected_classification(mutation_id: str) -> Label:
"""Ground truth. Recorded by a human before any run — never inferred."""
"""Pre-recorded instrument labels, never inferred from run outcomes."""
return BY_ID[mutation_id].label

134
lab/workflows.py Normal file
View file

@ -0,0 +1,134 @@
"""Synthetic workflow lab, implemented from usecases/generalisation-spec.md."""
from copy import deepcopy
from dataclasses import dataclass
from testdriver.actions import Surface
from testdriver.drivers import Realization
class WorkflowDriver:
"""A completed denial is a protocol response, not a failed realization."""
surface = Surface("workflow", "direct")
def __init__(self, lab):
self.lab = lab
def realize(self, actor, action):
action.check_surface(self.surface.id)
response = self.lab.request(actor.credentials["token"], action.name, **action.args)
return Realization(self.surface.id, {"operation": action.name, "response": response})
class ApprovalLab:
tokens = {"requester": "fixture-requester", "reviewer": "fixture-reviewer",
"delegate": "fixture-delegate"}
def __init__(self, defect=None):
self.defect = defect
self.state = "draft"
self.reviewer = "reviewer"
self.approved_by = None
self.receipts = []
def may_execute(self, actor):
return actor == "requester" and (
self.state == "approved" or self.defect == "premature-execution"
)
def request(self, token, operation, **args):
actor = next((a for a, t in self.tokens.items() if t == token), None)
allowed = False
if operation == "submit":
allowed = actor == "requester" and self.state == "draft"
if allowed:
self.state = "pending"
elif operation == "delegate":
allowed = (actor == self.reviewer and self.state == "pending"
and args["to"] == "delegate")
if allowed:
self.reviewer = args["to"]
elif operation == "approve":
allowed = self.state == "pending" and (
actor == self.reviewer
or (actor == "requester" and self.defect == "self-approval")
or (actor == "reviewer" and self.defect == "stale-delegation")
)
if allowed:
self.state, self.approved_by = "approved", actor
elif operation == "execute":
allowed = self.may_execute(actor)
if allowed:
self.state = "executed"
else:
raise ValueError(operation)
receipt = {"actor": actor, "operation": operation, "allowed": allowed}
self.receipts.append(receipt)
return deepcopy(receipt)
@dataclass
class ApprovalObserver:
lab: ApprovalLab
name: str = "approval-observer"
def snapshot(self):
return deepcopy({
"state": self.lab.state,
"reviewer": self.lab.reviewer,
"approved_by": self.lab.approved_by,
"receipts": self.lab.receipts,
"execution_gate": {actor: self.lab.may_execute(actor) for actor in self.lab.tokens},
})
class TenantLab:
tokens = {"admin-a": "fixture-admin-a", "admin-b": "fixture-admin-b"}
tenants = {"admin-a": "A", "admin-b": "B"}
def __init__(self, defect=None):
self.defect = defect
self.records = {}
self.deleted = {}
self.receipts = []
def read(self, actor, tenant, resource):
if self.tenants.get(actor) != tenant and self.defect != "foreign-read":
return {"allowed": False, "content": None}
return {"allowed": (tenant, resource) in self.records,
"content": self.records.get((tenant, resource))}
def request(self, token, operation, tenant, resource="R", content=None):
actor = next((a for a, t in self.tokens.items() if t == token), None)
allowed = self.tenants.get(actor) == tenant
key = (tenant, resource)
if operation == "create":
allowed = allowed and key not in self.records
if allowed:
self.records[key] = (self.deleted.get(key, content)
if self.defect == "resurrect-content" else content)
elif operation == "delete":
allowed = (allowed or self.defect == "foreign-delete") and key in self.records
if allowed:
self.deleted[key] = self.records.pop(key)
if self.defect == "unscoped-delete":
self.records = {k: v for k, v in self.records.items() if k[1] != resource}
else:
raise ValueError(operation)
receipt = {"actor": actor, "operation": operation, "tenant": tenant, "allowed": allowed}
self.receipts.append(receipt)
return deepcopy(receipt)
@dataclass
class TenantObserver:
lab: TenantLab
name: str = "tenant-observer"
def snapshot(self):
return deepcopy({
"records": {tenant: self.lab.records.get((tenant, "R")) for tenant in ("A", "B")},
"reads": {f"{actor}:{tenant}": self.lab.read(actor, tenant, "R")
for actor in self.lab.tokens for tenant in ("A", "B")},
"receipts": self.lab.receipts,
})