From cda3e5d81ca44d77f016e6a585f600769b6e6da8 Mon Sep 17 00:00:00 2001 From: Dylan Roy Date: Tue, 6 Oct 2026 14:14:51 +0000 Subject: [PATCH 1/3] Add read-only evidence-profile view over the downstream gate graph tools/evidence_profile.py renders, for a GRAPH.json node or every math.* root, the dependency tree with each node's recorded classification and providers, whether a recorded reviewer family differs from the author's, and the required dependencies that do not satisfy the hard gate. It loads and validates the graph with the gate's own strict loader and dependency rule, writes nothing, and lists the axes the graph does not record as outside it. Derived view adopted in main#275; scientific effect NONE. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017Mi3hxjaxV45x6zo6o1ee3 --- tests/test_evidence_profile.py | 134 ++++++++++++++++++++++++++ tools/evidence_profile.py | 165 +++++++++++++++++++++++++++++++++ 2 files changed, 299 insertions(+) create mode 100644 tests/test_evidence_profile.py create mode 100644 tools/evidence_profile.py diff --git a/tests/test_evidence_profile.py b/tests/test_evidence_profile.py new file mode 100644 index 000000000..c4efb2d56 --- /dev/null +++ b/tests/test_evidence_profile.py @@ -0,0 +1,134 @@ +"""Behavior of tools/evidence_profile.py (standard library only; the live graph is read, never written).""" +import contextlib +import hashlib +import importlib.util +import io +import json +import pathlib +import tempfile +import unittest + +ROOT = pathlib.Path(__file__).resolve().parent.parent +GRAPH = ROOT / 'frontiers' / 'downstream_gate_20260925' / 'GRAPH.json' +SPEC = importlib.util.spec_from_file_location('evidence_profile', ROOT / 'tools' / 'evidence_profile.py') +ep = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(ep) +GATE = ep.load_gate() + + +def node(classification, **fields): + return dict(classification=classification, controlling=False, **fields) + + +def synthetic(): + return { + 'schema_version': 1, + 'object': 'SYNTHETIC', + 'nodes': { + 'math.top': node('OPEN_ACTIVE'), + 'math.mid': node('PROVED_REVIEWED', author_provider='OpenAI', + review_basis=[{'provider': 'xAI'}, {'provider': 'Anthropic/Claude'}]), + 'math.leaf': node('AUTHOR_SIDE_CANDIDATE', author_provider='Anthropic', + review_provider='Anthropic Claude session'), + 'math.shared': node('PROVED_REVIEWED', author_provider='OpenAI', review_providers=['nonauthor']), + 'math.boundary': node('REFUTED'), + }, + 'edges': [ + {'from': 'math.top', 'to': 'math.mid', 'required': True, 'relation': 'uses'}, + {'from': 'math.top', 'to': 'math.shared', 'required': True, 'relation': 'uses'}, + {'from': 'math.top', 'to': 'math.boundary', 'required': False, 'relation': 'boundary_only'}, + {'from': 'math.mid', 'to': 'math.leaf', 'required': True, 'relation': 'imports'}, + {'from': 'math.mid', 'to': 'math.shared', 'required': True, 'relation': 'imports'}, + ], + } + + +def run(argv): + out = io.StringIO() + with contextlib.redirect_stdout(out): + code = ep.main(argv) + return code, out.getvalue() + + +class SyntheticGraph(unittest.TestCase): + def setUp(self): + self.graph = synthetic() + GATE.validate_graph_fail_closed(self.graph) + + def test_unsatisfied_required_is_transitive_and_ignores_context_edges(self): + p = ep.profile(GATE, self.graph, 'math.top') + self.assertEqual(p['unsatisfied_required'], + [{'node': 'math.leaf', 'classification': 'AUTHOR_SIDE_CANDIDATE'}]) + self.assertEqual(ep.profile(GATE, self.graph, 'math.shared')['unsatisfied_required'], []) + + def test_provider_distinct_reads_only_recorded_families(self): + self.assertEqual(ep.provider_distinct(self.graph['nodes']['math.mid']), 'yes') + self.assertEqual(ep.provider_distinct(self.graph['nodes']['math.leaf']), 'no') + self.assertEqual(ep.provider_distinct(self.graph['nodes']['math.shared']), 'not recorded') + self.assertEqual(ep.provider_distinct(self.graph['nodes']['math.top']), 'not recorded') + self.assertIsNone(ep.family('OpenAI reviewed by xAI')) + self.assertEqual(ep.family('xAI/Grok via Cursor'), 'xAI') + + def test_tree_marks_context_edges_and_expands_shared_nodes_once(self): + lines = ep.tree_lines(GATE, self.graph, 'math.top') + text = '\n'.join(lines) + self.assertIn('context (boundary_only) math.boundary REFUTED', text) + self.assertEqual(sum('math.shared' in line for line in lines[1:-1]), 2) + self.assertEqual(sum('(expanded above)' in line for line in lines), 1) + self.assertEqual(lines[-1], 'Unsatisfied required dependencies (1): math.leaf (AUTHOR_SIDE_CANDIDATE)') + + def test_roots_are_math_nodes_without_incoming_edges(self): + self.assertEqual(ep.roots(self.graph), ['math.top']) + + def test_cli_on_file_json_and_text(self): + with tempfile.TemporaryDirectory() as tmp: + path = pathlib.Path(tmp) / 'graph.json' + path.write_text(json.dumps(self.graph)) + code, text = run(['--graph', str(path)]) + self.assertEqual(code, 0) + self.assertTrue(text.startswith('math.top OPEN_ACTIVE')) + self.assertIn('no status authority; scientific effect NONE', text) + code, text = run(['--graph', str(path), '--node', 'math.mid', '--json']) + data = json.loads(text) + self.assertEqual([p['node'] for p in data['profiles']], ['math.mid']) + self.assertIs(data['status_authority'], False) + self.assertIs(data['lemma_closed'], False) + self.assertEqual(data['scientific_effect'], 'NONE') + self.assertIn('blind_reconstruction', data['axes_outside_graph']) + + def test_invalid_graphs_and_unknown_nodes_are_rejected(self): + with tempfile.TemporaryDirectory() as tmp: + path = pathlib.Path(tmp) / 'graph.json' + bad = synthetic() + bad['edges'].append({'from': 'math.leaf', 'to': 'math.top', 'required': True, 'relation': 'cycle'}) + path.write_text(json.dumps(bad)) + with self.assertRaisesRegex(ValueError, 'required dependency cycle'): + run(['--graph', str(path)]) + bad = synthetic() + bad['nodes']['math.top']['classification'] = 'CLOSED' + path.write_text(json.dumps(bad)) + with self.assertRaisesRegex(ValueError, 'unknown node classification'): + run(['--graph', str(path)]) + path.write_text(json.dumps(synthetic())) + with self.assertRaisesRegex(KeyError, 'unknown node'): + run(['--graph', str(path), '--node', 'math.absent']) + + +class LiveGraph(unittest.TestCase): + def test_live_graph_renders_without_writing(self): + before = hashlib.sha256(GRAPH.read_bytes()).hexdigest() + code, text = run([]) + self.assertEqual(code, 0) + graph = GATE.load_graph(GRAPH) + for root in ep.roots(graph): + self.assertIn('\n' + root + ' ' if not text.startswith(root) else root, text) + code, text = run(['--json']) + data = json.loads(text) + self.assertEqual(len(data['profiles']), len(ep.roots(graph))) + for p in data['profiles']: + self.assertEqual(p['classification'], graph['nodes'][p['node']]['classification']) + self.assertEqual(hashlib.sha256(GRAPH.read_bytes()).hexdigest(), before) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/evidence_profile.py b/tools/evidence_profile.py new file mode 100644 index 000000000..0a6dbcd46 --- /dev/null +++ b/tools/evidence_profile.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +"""Read-only dependency tree and evidence profile for the downstream gate graph (standard library only). + +Renders, for a node of frontiers/downstream_gate_20260925/GRAPH.json (or every math.* root), the tree of +its outgoing edges with each node's recorded classification, the providers its record names, and the +required dependencies that do not yet satisfy the hard gate. The graph is loaded and validated with the +gate's own strict loader; the dependency rule is the gate's (an edge is required unless it says +otherwise; only PROVED_REVIEWED satisfies a required premise). + +This is the derived view adopted in main#275 and governance/OP-CLOSURE-EVIDENCE-20261006.md: it writes +nothing, promotes nothing and is not a status record. Axes the graph does not record (blind +reconstruction, adversarial attack, formal evidence, numerical reproduction, novelty, human reading) +are listed as outside the graph rather than reported as passed or failed. Scientific effect: NONE. + +Usage: python3 -B -S tools/evidence_profile.py [--graph PATH] [--node ID] [--json] +""" +import argparse +import importlib.util +import json +import pathlib +import sys + +ROOT = pathlib.Path(__file__).resolve().parent.parent +GATE_DIR = ROOT / 'frontiers' / 'downstream_gate_20260925' +AXES_OUTSIDE_GRAPH = ( + 'blind_reconstruction', 'adversarial_attack', 'formal_evidence', + 'numerical_reproduction', 'novelty', 'human_reading', +) +FAMILIES = (('openai', 'OpenAI'), ('chatgpt', 'OpenAI'), ('codex', 'OpenAI'), ('anthropic', 'Anthropic'), + ('claude', 'Anthropic'), ('xai', 'xAI'), ('grok', 'xAI'), ('google', 'Google'), ('gemini', 'Google')) + + +def load_gate(): + spec = importlib.util.spec_from_file_location('downstream_hard_gate', GATE_DIR / 'hard_gate.py') + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def family(text): + """Provider family named by a free-form record string, or None when it names none or several.""" + found = {name for token, name in FAMILIES if token in text.lower()} + return found.pop() if len(found) == 1 else None + + +def reviewer_strings(node): + out = [] + for item in node.get('review_basis', []) or []: + if isinstance(item, dict) and isinstance(item.get('provider'), str): + out.append(item['provider']) + providers = node.get('review_providers') + if isinstance(providers, list): + out.extend(p for p in providers if isinstance(p, str)) + if isinstance(node.get('review_provider'), str): + out.append(node['review_provider']) + return out + + +def provider_distinct(node): + """'yes' if a recorded reviewer family differs from the recorded author family; 'no' if every + recorded reviewer shares it; 'not recorded' if either side is missing or names no single family.""" + author = node.get('author_provider') + author_family = family(author) if isinstance(author, str) else None + reviewer_families = [family(s) for s in reviewer_strings(node)] + known = [f for f in reviewer_families if f is not None] + if author_family is None or not known: + return 'not recorded' + return 'yes' if any(f != author_family for f in known) else 'no' + + +def profile(gate, graph, node_id): + node = gate.require_node(graph, node_id) + unsatisfied = [dep for dep in gate.transitive_required(graph, node_id) + if graph['nodes'][dep]['classification'] not in gate.REQUIRED_SATISFIED] + return { + 'node': node_id, + 'classification': node['classification'], + 'review_disposition': node.get('review_disposition', 'not recorded'), + 'author_provider': node.get('author_provider', 'not recorded'), + 'reviewer_providers': reviewer_strings(node) or ['not recorded'], + 'provider_distinct': provider_distinct(node), + 'unsatisfied_required': [{'node': dep, 'classification': graph['nodes'][dep]['classification']} + for dep in unsatisfied], + 'axes_outside_graph': list(AXES_OUTSIDE_GRAPH), + } + + +def roots(graph): + targets = {e['to'] for e in graph['edges']} + return sorted(n for n in graph['nodes'] if n.startswith('math.') and n not in targets) + + +def tree_lines(gate, graph, node_id): + lines = [] + seen = set() + + def label(nid): + p = profile(gate, graph, nid) + return '%s %s [author %s; reviewers %s; provider-distinct %s]' % ( + nid, p['classification'], p['author_provider'], ', '.join(p['reviewer_providers']), + p['provider_distinct']) + + def walk(nid, prefix): + edges = sorted((e for e in graph['edges'] if e['from'] == nid), key=lambda e: e['to']) + for i, e in enumerate(edges): + last = i == len(edges) - 1 + kind = 'requires' if e.get('required', True) else 'context (%s)' % e.get('relation', '?') + child = e['to'] + again = child in seen + lines.append('%s%s %s %s%s' % (prefix, '└──' if last else '├──', kind, label(child), + ' (expanded above)' if again else '')) + if not again: + seen.add(child) + walk(child, prefix + (' ' if last else '│ ')) + + seen.add(node_id) + lines.append(label(node_id)) + walk(node_id, '') + unsatisfied = profile(gate, graph, node_id)['unsatisfied_required'] + if unsatisfied: + lines.append('Unsatisfied required dependencies (%d): %s' % (len(unsatisfied), ', '.join( + '%s (%s)' % (u['node'], u['classification']) for u in unsatisfied))) + else: + lines.append('Unsatisfied required dependencies: none') + return lines + + +def report(gate, graph, node_ids): + return { + 'object': graph.get('object'), + 'profiles': [profile(gate, graph, nid) for nid in node_ids], + 'axes_outside_graph': list(AXES_OUTSIDE_GRAPH), + 'meaning': 'derived read-only view of recorded graph fields; not a status record or acceptance', + 'status_authority': False, + 'lemma_closed': False, + 'scientific_effect': 'NONE', + } + + +def main(argv=None): + ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + ap.add_argument('--graph', default=str(GATE_DIR / 'GRAPH.json')) + ap.add_argument('--node', action='append', help='node id (repeatable); default: every math.* root') + ap.add_argument('--json', action='store_true', help='print the profiles as JSON instead of trees') + args = ap.parse_args(argv) + gate = load_gate() + graph = gate.load_graph(pathlib.Path(args.graph)) + node_ids = args.node or roots(graph) + for nid in node_ids: + gate.require_node(graph, nid) + if args.json: + print(json.dumps(report(gate, graph, node_ids), indent=2, sort_keys=True)) + return 0 + for index, nid in enumerate(node_ids): + if index: + print() + print('\n'.join(tree_lines(gate, graph, nid))) + print() + print('Not in the graph (consult review records): ' + ', '.join(AXES_OUTSIDE_GRAPH)) + print('Derived view only: no status authority; scientific effect NONE.') + return 0 + + +if __name__ == '__main__': + sys.exit(main()) From 6aada4bb6483c3c2e32b300a7b31b0d8a440c5b1 Mon Sep 17 00:00:00 2001 From: Dylan Roy Date: Tue, 6 Oct 2026 14:26:21 +0000 Subject: [PATCH 2/3] Evidence profile: conservative provider attribution, cycle coverage, proposed wording Applies AMEND_ENGINEERING_SCOPED review 5430026100 (EP381-01, EP381-02), with the matching observations 6018400003, 6018473703 and 6018572392: - provider_distinct: 'yes' when some reviewer resolves to a different family, 'no' only when every recorded reviewer resolves to the author's, otherwise 'not recorded' (unresolved or ambiguous entries no longer drop out before 'no'); - family(): a documented alias set matched on whole alphanumeric tokens; a string carrying a negation word ('not OpenAI', 'provider not disclosed') is unresolved; profiles keep unresolved reviewer strings verbatim; - default selection also lists any math.* component not reached from an edge-free root (nonblocking item: a context-only cycle); - header wording: proposed in main#275 v1.1, not adopted. Five new tests, one through the actual command line in the current Python mode (mixed unknown, mixed ambiguous, distinct-plus-unknown, negated, embedded name, and the Anthropic/Claude, ChatGPT/Codex and xAI/Grok via Cursor controls). The live graph's ten roots and every provider_distinct value are unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017Mi3hxjaxV45x6zo6o1ee3 --- tests/test_evidence_profile.py | 98 ++++++++++++++++++++++++++++++++++ tools/evidence_profile.py | 54 +++++++++++++++---- 2 files changed, 143 insertions(+), 9 deletions(-) diff --git a/tests/test_evidence_profile.py b/tests/test_evidence_profile.py index c4efb2d56..6fc06726b 100644 --- a/tests/test_evidence_profile.py +++ b/tests/test_evidence_profile.py @@ -5,6 +5,8 @@ import io import json import pathlib +import subprocess +import sys import tempfile import unittest @@ -69,6 +71,63 @@ def test_provider_distinct_reads_only_recorded_families(self): self.assertIsNone(ep.family('OpenAI reviewed by xAI')) self.assertEqual(ep.family('xAI/Grok via Cursor'), 'xAI') + def test_unresolved_reviewer_never_yields_no(self): + # 6018400003 item 1: an unresolved or ambiguous reviewer must not be dropped before deciding 'no'. + for reviewers in (['OpenAI', 'unattributed'], ['OpenAI', 'OpenAI / xAI'], ['OpenAI', 'nonauthor']): + with self.subTest(reviewers=reviewers): + self.assertEqual(ep.provider_distinct(node('PROVED_REVIEWED', author_provider='OpenAI', + review_providers=reviewers)), 'not recorded') + self.assertEqual(ep.provider_distinct(node('PROVED_REVIEWED', author_provider='OpenAI', + review_providers=['unattributed', 'xAI'])), 'yes') + self.assertEqual(ep.provider_distinct(node('PROVED_REVIEWED', author_provider='OpenAI', + review_providers=['OpenAI / GPT-6', 'Codex'])), 'no') + self.assertEqual(ep.provider_distinct(node('PROVED_REVIEWED', author_provider='OpenAI', + review_providers=[])), 'not recorded') + # EP381-UNKNOWN-01 (6018473703): the same rule across fields, singular review_provider plus review_basis. + self.assertEqual(ep.provider_distinct(node('PROVED_REVIEWED', author_provider='OpenAI', review_provider='OpenAI', + review_basis=[{'provider': 'unidentified'}])), 'not recorded') + self.assertEqual(ep.provider_distinct(node('PROVED_REVIEWED', author_provider='OpenAI', review_provider='OpenAI', + review_basis=[{'provider': 'xAI'}])), 'yes') + self.assertEqual(ep.provider_distinct(node('PROVED_REVIEWED', author_provider='OpenAI', + review_providers=['OpenAI or xAI'])), 'not recorded') + + def test_negated_or_undisclosed_provider_is_unresolved(self): + # EP381-02 (6018548621): non-identities must not be read as a provider. + for text in ('not OpenAI; provider not disclosed', 'no Anthropic involvement', 'other than xAI', + 'unidentified', 'provider not disclosed'): + with self.subTest(text=text): + self.assertIsNone(ep.family(text)) + self.assertEqual(ep.provider_distinct(node('PROVED_REVIEWED', author_provider='Anthropic', + review_providers=['not OpenAI; provider not disclosed'])), + 'not recorded') + self.assertEqual(ep.provider_distinct(node('PROVED_REVIEWED', author_provider='OpenAI', + review_providers=['claudette'])), 'not recorded') + p = ep.profile(GATE, {'schema_version': 1, 'nodes': {'math.x': node( + 'PROVED_REVIEWED', author_provider='OpenAI', review_providers=['OpenAI', 'not disclosed'])}, + 'edges': []}, 'math.x') + self.assertEqual(p['provider_distinct'], 'not recorded') + self.assertEqual(p['unresolved_reviewer_providers'], ['not disclosed']) + + def test_default_selection_covers_context_only_cycles(self): + # 6018548621 nonblocking: a component without an edge-free root is still listed. + graph = synthetic() + graph['nodes']['math.a'] = node('OPEN_ACTIVE') + graph['nodes']['math.b'] = node('PROVED_REVIEWED') + graph['edges'] += [{'from': 'math.a', 'to': 'math.b', 'required': False, 'relation': 'context'}, + {'from': 'math.b', 'to': 'math.a', 'required': False, 'relation': 'context'}] + GATE.validate_graph_fail_closed(graph) + self.assertEqual(ep.roots(graph), ['math.top', 'math.a']) + + def test_family_matches_whole_tokens_only(self): + # 6018400003 item 2: substring hits such as 'notopenai' or 'claudette' name no family. + for text in ('notopenai', 'claudette', 'xairline', 'grokking-free', 'googleplex', ''): + with self.subTest(text=text): + self.assertIsNone(ep.family(text)) + for text, expected in (('OpenAI / GPT-6 Astra Pro', 'OpenAI'), ('Anthropic/Claude (A1-A7)', 'Anthropic'), + ('Claude Code session', 'Anthropic'), ('ChatGPT', 'OpenAI'), ('Gemini', 'Google')): + with self.subTest(text=text): + self.assertEqual(ep.family(text), expected) + def test_tree_marks_context_edges_and_expands_shared_nodes_once(self): lines = ep.tree_lines(GATE, self.graph, 'math.top') text = '\n'.join(lines) @@ -114,6 +173,45 @@ def test_invalid_graphs_and_unknown_nodes_are_rejected(self): run(['--graph', str(path), '--node', 'math.absent']) +class ProviderAttributionCLI(unittest.TestCase): + """EP381-01/EP381-02 (review 5430026100) through the actual command line, in the current Python mode.""" + + CASES = { + 'math.mixed-unknown': ('OpenAI', ['OpenAI', 'not disclosed'], 'not recorded'), + 'math.mixed-ambiguous': ('OpenAI', ['OpenAI', 'OpenAI or Anthropic'], 'not recorded'), + 'math.distinct-plus-unknown': ('OpenAI', ['not disclosed', 'xAI'], 'yes'), + 'math.negated': ('Anthropic', ['not OpenAI; provider not disclosed'], 'not recorded'), + 'math.embedded-name': ('OpenAI', ['claudette'], 'not recorded'), + 'math.anthropic-claude': ('OpenAI', ['Anthropic/Claude'], 'yes'), + 'math.chatgpt-codex': ('OpenAI', ['ChatGPT/Codex'], 'no'), + 'math.xai-grok-cursor': ('OpenAI', ['xAI/Grok via Cursor'], 'yes'), + 'math.all-same': ('xAI', ['xAI/Grok via Cursor', 'Grok 4.7'], 'no'), + } + + def test_cli_json_reports_conservative_provider_attribution(self): + graph = {'schema_version': 1, 'object': 'SYNTHETIC-CLI', 'edges': [], + 'nodes': {nid: node('PROVED_REVIEWED', author_provider=a, review_providers=r) + for nid, (a, r, _) in self.CASES.items()}} + with tempfile.TemporaryDirectory() as tmp: + path = pathlib.Path(tmp) / 'graph.json' + path.write_text(json.dumps(graph)) + before = path.read_bytes() + flags = ['-O'] if sys.flags.optimize else [] + run = subprocess.run([sys.executable, '-B', *flags, '-S', str(ROOT / 'tools' / 'evidence_profile.py'), + '--graph', str(path), '--json'], capture_output=True, text=True, timeout=60) + self.assertEqual((run.returncode, run.stderr), (0, '')) + self.assertEqual(path.read_bytes(), before) + profiles = {p['node']: p for p in json.loads(run.stdout)['profiles']} + self.assertEqual(set(profiles), set(self.CASES)) + for nid, (_, reviewers, expected) in self.CASES.items(): + with self.subTest(node=nid): + self.assertEqual(profiles[nid]['provider_distinct'], expected) + self.assertEqual(profiles[nid]['reviewer_providers'], reviewers) + self.assertEqual(profiles['math.mixed-unknown'].get('unresolved_reviewer_providers'), ['not disclosed']) + self.assertEqual(profiles['math.negated'].get('unresolved_reviewer_providers'), + ['not OpenAI; provider not disclosed']) + + class LiveGraph(unittest.TestCase): def test_live_graph_renders_without_writing(self): before = hashlib.sha256(GRAPH.read_bytes()).hexdigest() diff --git a/tools/evidence_profile.py b/tools/evidence_profile.py index 0a6dbcd46..9a1645b16 100644 --- a/tools/evidence_profile.py +++ b/tools/evidence_profile.py @@ -7,7 +7,8 @@ gate's own strict loader; the dependency rule is the gate's (an edge is required unless it says otherwise; only PROVED_REVIEWED satisfies a required premise). -This is the derived view adopted in main#275 and governance/OP-CLOSURE-EVIDENCE-20261006.md: it writes +This is the derived view proposed in main#275 (decision record v1.1, rows PROPOSED) and main#276 +(governance/OP-CLOSURE-EVIDENCE-20261006.md, unmerged at publication): it writes nothing, promotes nothing and is not a status record. Axes the graph does not record (blind reconstruction, adversarial attack, formal evidence, numerical reproduction, novelty, human reading) are listed as outside the graph rather than reported as passed or failed. Scientific effect: NONE. @@ -18,6 +19,7 @@ import importlib.util import json import pathlib +import re import sys ROOT = pathlib.Path(__file__).resolve().parent.parent @@ -26,6 +28,9 @@ 'blind_reconstruction', 'adversarial_attack', 'formal_evidence', 'numerical_reproduction', 'novelty', 'human_reading', ) +# A record string carrying any of these words is not read as naming a provider (EP381-02: 'not OpenAI'). +NEGATIONS = frozenset({'not', 'no', 'non', 'never', 'without', 'except', 'excluding', 'unknown', 'undisclosed', + 'unnamed', 'unidentified', 'unattributed', 'other', 'than'}) FAMILIES = (('openai', 'OpenAI'), ('chatgpt', 'OpenAI'), ('codex', 'OpenAI'), ('anthropic', 'Anthropic'), ('claude', 'Anthropic'), ('xai', 'xAI'), ('grok', 'xAI'), ('google', 'Google'), ('gemini', 'Google')) @@ -38,8 +43,13 @@ def load_gate(): def family(text): - """Provider family named by a free-form record string, or None when it names none or several.""" - found = {name for token, name in FAMILIES if token in text.lower()} + """Provider family named by a free-form record string, or None when it names none, several, or is negated. + Matching is on whole alphanumeric tokens, so 'xAI/Grok via Cursor' names xAI but 'notopenai' names nothing; + a string with a negation word ('not OpenAI', 'provider not disclosed') is unresolved rather than guessed.""" + tokens = set(re.findall(r'[a-z0-9]+', text.lower())) + if tokens & NEGATIONS: + return None + found = {name for token, name in FAMILIES if token in tokens} return found.pop() if len(found) == 1 else None @@ -57,15 +67,19 @@ def reviewer_strings(node): def provider_distinct(node): - """'yes' if a recorded reviewer family differs from the recorded author family; 'no' if every - recorded reviewer shares it; 'not recorded' if either side is missing or names no single family.""" + """'yes' if some recorded reviewer resolves to a family other than the recorded author's; 'no' only if + every recorded reviewer resolves to the author's family; otherwise 'not recorded' (missing author or + reviewers, or a reviewer string naming no single family). Never organizational independence.""" author = node.get('author_provider') author_family = family(author) if isinstance(author, str) else None reviewer_families = [family(s) for s in reviewer_strings(node)] - known = [f for f in reviewer_families if f is not None] - if author_family is None or not known: + if author_family is None or not reviewer_families: return 'not recorded' - return 'yes' if any(f != author_family for f in known) else 'no' + if any(f is not None and f != author_family for f in reviewer_families): + return 'yes' + if all(f == author_family for f in reviewer_families): + return 'no' + return 'not recorded' def profile(gate, graph, node_id): @@ -79,6 +93,7 @@ def profile(gate, graph, node_id): 'author_provider': node.get('author_provider', 'not recorded'), 'reviewer_providers': reviewer_strings(node) or ['not recorded'], 'provider_distinct': provider_distinct(node), + 'unresolved_reviewer_providers': [s for s in reviewer_strings(node) if family(s) is None], 'unsatisfied_required': [{'node': dep, 'classification': graph['nodes'][dep]['classification']} for dep in unsatisfied], 'axes_outside_graph': list(AXES_OUTSIDE_GRAPH), @@ -86,8 +101,29 @@ def profile(gate, graph, node_id): def roots(graph): + """math.* nodes without an incoming edge, then, in sorted order, any math.* node not reached from those + (a component whose math nodes all have incoming edges, e.g. a context-only cycle), so none is omitted.""" targets = {e['to'] for e in graph['edges']} - return sorted(n for n in graph['nodes'] if n.startswith('math.') and n not in targets) + math_nodes = sorted(n for n in graph['nodes'] if n.startswith('math.')) + chosen = [n for n in math_nodes if n not in targets] + reached = set() + + def reach(nid): + stack = [nid] + while stack: + cur = stack.pop() + if cur in reached: + continue + reached.add(cur) + stack.extend(e['to'] for e in graph['edges'] if e['from'] == cur) + + for n in chosen: + reach(n) + for n in math_nodes: + if n not in reached: + chosen.append(n) + reach(n) + return chosen def tree_lines(gate, graph, node_id): From 07127f27471ffcc52b1c4fad8f969f23751985ca Mon Sep 17 00:00:00 2001 From: Dylan Roy Date: Tue, 6 Oct 2026 14:45:21 +0000 Subject: [PATCH 3/3] Evidence profile: positive provider-identity grammar (EP381-02 residual) Delta read 6018757501 found that an alias-token match with a negation blacklist still read 'possibly OpenAI', 'OpenAI or unspecified' and 'review requested from OpenAI' as provider identities. family() now accepts only a documented positive form: a family alias followed by same-family aliases, listed product/agent/session decorations or version identifiers, with one optional trailing parenthetical note. Everything else is unresolved and listed verbatim. The negation list is removed. Two new tests (grammar and the three CLI cases plus the 'OpenAI / GPT-6 Astra Pro' control); the live graph's roots and every provider_distinct value are unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017Mi3hxjaxV45x6zo6o1ee3 --- tests/test_evidence_profile.py | 17 ++++++++++++++ tools/evidence_profile.py | 42 ++++++++++++++++++++++++---------- 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/tests/test_evidence_profile.py b/tests/test_evidence_profile.py index 6fc06726b..5e49e2737 100644 --- a/tests/test_evidence_profile.py +++ b/tests/test_evidence_profile.py @@ -118,6 +118,18 @@ def test_default_selection_covers_context_only_cycles(self): GATE.validate_graph_fail_closed(graph) self.assertEqual(ep.roots(graph), ['math.top', 'math.a']) + def test_positive_identity_grammar(self): + # EP381-02 residual (6018757501): only the documented positive form names a family. + for text, expected in (('OpenAI / GPT-6 Astra Pro', 'OpenAI'), ('Grok 4.7', 'xAI'), + ('xAI/Grok via Cursor (D1-A-E, section 9 v1.1, partial slices)', 'xAI'), + ('Claude Code session', 'Anthropic'), ('Google DeepMind Gemini', 'Google')): + with self.subTest(text=text): + self.assertEqual(ep.family(text), expected) + for text in ('possibly OpenAI', 'OpenAI or unspecified', 'review requested from OpenAI', + 'OpenAI reviewed by xAI', 'OpenAI (a) (b)', 'OpenAI, probably', 'Claude/Grok'): + with self.subTest(text=text): + self.assertIsNone(ep.family(text)) + def test_family_matches_whole_tokens_only(self): # 6018400003 item 2: substring hits such as 'notopenai' or 'claudette' name no family. for text in ('notopenai', 'claudette', 'xairline', 'grokking-free', 'googleplex', ''): @@ -186,6 +198,11 @@ class ProviderAttributionCLI(unittest.TestCase): 'math.chatgpt-codex': ('OpenAI', ['ChatGPT/Codex'], 'no'), 'math.xai-grok-cursor': ('OpenAI', ['xAI/Grok via Cursor'], 'yes'), 'math.all-same': ('xAI', ['xAI/Grok via Cursor', 'Grok 4.7'], 'no'), + # Delta 6018757501: prose around a provider name is not a positive identity. + 'math.possibly': ('Anthropic', ['possibly OpenAI'], 'not recorded'), + 'math.or-unspecified': ('Anthropic', ['OpenAI or unspecified'], 'not recorded'), + 'math.invitation': ('Anthropic', ['review requested from OpenAI'], 'not recorded'), + 'math.astra-control': ('Anthropic', ['OpenAI / GPT-6 Astra Pro'], 'yes'), } def test_cli_json_reports_conservative_provider_attribution(self): diff --git a/tools/evidence_profile.py b/tools/evidence_profile.py index 9a1645b16..a970f207e 100644 --- a/tools/evidence_profile.py +++ b/tools/evidence_profile.py @@ -28,11 +28,22 @@ 'blind_reconstruction', 'adversarial_attack', 'formal_evidence', 'numerical_reproduction', 'novelty', 'human_reading', ) -# A record string carrying any of these words is not read as naming a provider (EP381-02: 'not OpenAI'). -NEGATIONS = frozenset({'not', 'no', 'non', 'never', 'without', 'except', 'excluding', 'unknown', 'undisclosed', - 'unnamed', 'unidentified', 'unattributed', 'other', 'than'}) -FAMILIES = (('openai', 'OpenAI'), ('chatgpt', 'OpenAI'), ('codex', 'OpenAI'), ('anthropic', 'Anthropic'), - ('claude', 'Anthropic'), ('xai', 'xAI'), ('grok', 'xAI'), ('google', 'Google'), ('gemini', 'Google')) +# Provider attribution grammar (EP381-02, review 5430026100 and delta 6018757501). A record string names a provider +# family only in this positive form, compared case-insensitively: +# FAMILY_ALIAS ( ('/' | ' ') (FAMILY_ALIAS | DECORATION | VERSION) )* [ ' (' note ')' ] +# where every alias belongs to the same family, DECORATION is one of the listed product/agent/session words, VERSION is +# a model or version identifier containing a digit (gpt-6, 4.7, grok-4.7-high-fast), and one trailing parenthetical +# scope note is ignored. Anything else ('possibly OpenAI', 'OpenAI or Anthropic', 'review requested from OpenAI', +# 'not OpenAI', 'claudette') names no family and is reported verbatim as unresolved. +FAMILY_ALIASES = { + 'openai': 'OpenAI', 'chatgpt': 'OpenAI', 'codex': 'OpenAI', 'gpt': 'OpenAI', + 'anthropic': 'Anthropic', 'claude': 'Anthropic', + 'xai': 'xAI', 'grok': 'xAI', + 'google': 'Google', 'gemini': 'Google', +} +DECORATIONS = frozenset({'via', 'cursor', 'pro', 'code', 'session', 'agent', 'cloud', 'bot', 'model', 'astra', 'sol', + 'deepmind', 'high', 'fast'}) +VERSION = re.compile(r'[a-z]*-?\d[a-z0-9.\-]*') def load_gate(): @@ -43,14 +54,21 @@ def load_gate(): def family(text): - """Provider family named by a free-form record string, or None when it names none, several, or is negated. - Matching is on whole alphanumeric tokens, so 'xAI/Grok via Cursor' names xAI but 'notopenai' names nothing; - a string with a negation word ('not OpenAI', 'provider not disclosed') is unresolved rather than guessed.""" - tokens = set(re.findall(r'[a-z0-9]+', text.lower())) - if tokens & NEGATIONS: + """Provider family named by a record string in the positive grammar above, else None (unresolved).""" + match = re.fullmatch(r'\s*([^()]*?)\s*(?:\([^()]*\))?\s*', text) + if match is None: return None - found = {name for token, name in FAMILIES if token in tokens} - return found.pop() if len(found) == 1 else None + tokens = [t for t in re.split(r'[\s/]+', match.group(1).lower()) if t] + if not tokens or tokens[0] not in FAMILY_ALIASES: + return None + name = FAMILY_ALIASES[tokens[0]] + for token in tokens[1:]: + if token in FAMILY_ALIASES: + if FAMILY_ALIASES[token] != name: + return None + elif token not in DECORATIONS and not VERSION.fullmatch(token): + return None + return name def reviewer_strings(node):