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:
parent
d54af628df
commit
cf682281d7
36 changed files with 5394 additions and 279 deletions
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">'
|
||||
|
|
|
|||
|
|
@ -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
134
lab/workflows.py
Normal 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,
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue