diff --git a/Makefile b/Makefile index ec4d070..432bcf3 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ -.PHONY: validate validate-control-plane-examples validate-nlboot-examples validate-lattice-data-governai-examples validate-ops-history-examples validate-runtime-observability-examples validate-lifecycle-boundary-examples validate-svf-contracts validate-sync-cycle-receipts +.PHONY: validate validate-control-plane-examples validate-nlboot-examples validate-lattice-data-governai-examples validate-ops-history-examples validate-runtime-observability-examples validate-interpretability-examples validate-lifecycle-boundary-examples validate-svf-contracts validate-sync-cycle-receipts -validate: validate-control-plane-examples validate-nlboot-examples validate-lattice-data-governai-examples validate-ops-history-examples validate-runtime-observability-examples validate-lifecycle-boundary-examples validate-svf-contracts validate-sync-cycle-receipts +validate: validate-control-plane-examples validate-nlboot-examples validate-lattice-data-governai-examples validate-ops-history-examples validate-runtime-observability-examples validate-interpretability-examples validate-lifecycle-boundary-examples validate-svf-contracts validate-sync-cycle-receipts @echo "OK: validate" validate-control-plane-examples: @@ -23,6 +23,10 @@ validate-runtime-observability-examples: python3 -m pip install --user jsonschema >/dev/null python3 tools/validate_runtime_observability_examples.py +validate-interpretability-examples: + python3 -m pip install --user jsonschema >/dev/null + python3 tools/validate_interpretability_examples.py + validate-lifecycle-boundary-examples: python3 -m pip install --user jsonschema >/dev/null python3 tools/validate_lifecycle_boundary_examples.py diff --git a/examples/interpretability/sae-catalog.v0.json b/examples/interpretability/sae-catalog.v0.json new file mode 100644 index 0000000..e66439c --- /dev/null +++ b/examples/interpretability/sae-catalog.v0.json @@ -0,0 +1,27 @@ +{ + "schema_version": "0.1.0", + "catalog_kind": "interpretability-sae-contract-coverage", + "entries": [ + {"slug":"llama-scope-2","base_model":"Qwen3-1.7B","decomposition_type":"compound_decomposition","sae_training_method":"other","site_type":"attention_qk_ov","base_model_class":"pretrained_base","author_class":"research_lab","publication_state":"preprint","claim_type":"circuit_construction","intervention_kind":"none","embedded_decomposition":false,"cross_layer":false,"compound_decompositions":["transcoder_clt","lorsa_attention"],"inference_endpoint_available":false,"commercial_license_required":false,"api_provider_class":"open_weights","deanonymization_pending":false,"neuronpedia_source_id":"qwen3-1.7b-crm-openmoss"}, + {"slug":"assistant-axis","base_model":"Llama-3.3-70B-Instruct","decomposition_type":"standard_sae","sae_training_method":"other","site_type":"residual_post","base_model_class":"instruction_tuned","author_class":"safety_research","publication_state":"preprint","claim_type":"safety_finding","intervention_kind":"activation_cap","embedded_decomposition":false,"cross_layer":false,"inference_endpoint_available":true,"commercial_license_required":false,"api_provider_class":"hosted_api_features","deanonymization_pending":false,"neuronpedia_source_id":"40-resid-post-anthropic-axis"}, + {"slug":"qwen-scope","base_model":"Qwen3.5-2B","decomposition_type":"standard_sae","sae_training_method":"topk","site_type":"residual_stream","base_model_class":"pretrained_base","author_class":"model_author","publication_state":"blog_post","claim_type":"feature_set","intervention_kind":"none","embedded_decomposition":false,"cross_layer":false,"inference_endpoint_available":true,"commercial_license_required":false,"api_provider_class":"hosted_api_features","deanonymization_pending":false,"neuronpedia_source_id":"qwen3.5-2b-resid-32k-qwen-scope"}, + {"slug":"sparse-circuits","base_model":"CircuitGPT-Python","decomposition_type":"weight_sparse_native","sae_training_method":"weight_sparse_native","site_type":"mlp","base_model_class":"sparse_trained_native","author_class":"model_author","publication_state":"preprint","claim_type":"method_innovation","intervention_kind":"none","embedded_decomposition":true,"cross_layer":false,"inference_endpoint_available":false,"commercial_license_required":false,"api_provider_class":"open_weights","deanonymization_pending":false,"neuronpedia_source_id":"6-mlp"}, + {"slug":"temporal-feature-analysis","base_model":"Llama-3.1-8B","decomposition_type":"temporal_sae","sae_training_method":"topk","site_type":"residual_stream","base_model_class":"pretrained_base","author_class":"research_lab","publication_state":"preprint","claim_type":"method_innovation","intervention_kind":"none","embedded_decomposition":false,"cross_layer":false,"temporal_window":8,"inference_endpoint_available":true,"commercial_license_required":false,"api_provider_class":"hosted_api_features","deanonymization_pending":false,"neuronpedia_source_id":"15-resid-temporal-16k-lubana"}, + {"slug":"gpt-oss-sae","base_model":"GPT-OSS-20B","decomposition_type":"batch_topk_sae","sae_training_method":"batch_topk","site_type":"residual_post","base_model_class":"pretrained_base","author_class":"individual_researcher","publication_state":"blog_post","claim_type":"feature_set","intervention_kind":"none","embedded_decomposition":false,"cross_layer":false,"inference_endpoint_available":true,"commercial_license_required":false,"api_provider_class":"hosted_api_features","deanonymization_pending":false,"neuronpedia_source_id":"0-resid-post-aa-131k"}, + {"slug":"misaligned-persona","base_model":"Llama-3.1-8B-Instruct/Qwen2.5-7B-Instruct","decomposition_type":"standard_sae","sae_training_method":"topk","site_type":"residual_stream","base_model_class":"instruction_tuned","author_class":"individual_researcher","publication_state":"lesswrong","claim_type":"safety_finding","intervention_kind":"feature_steering","embedded_decomposition":false,"cross_layer":false,"cross_model_agreement_required":true,"inference_endpoint_available":true,"commercial_license_required":false,"api_provider_class":"hosted_api_features","deanonymization_pending":false,"neuronpedia_source_id":"cross-model-misaligned-persona-aa"}, + {"slug":"transcoders-hp","base_model":"Gemma-2-2B/Qwen3-4B","decomposition_type":"transcoder_clt","sae_training_method":"transcoder","site_type":"residual_mid","base_model_class":"pretrained_base","author_class":"safety_research","publication_state":"preprint","claim_type":"circuit_construction","intervention_kind":"none","embedded_decomposition":false,"cross_layer":true,"inference_endpoint_available":true,"commercial_license_required":false,"api_provider_class":"hosted_api_features","deanonymization_pending":false,"neuronpedia_source_id":"0-clt-hp"}, + {"slug":"res-matryoshka-dc","base_model":"Gemma-2-2B","decomposition_type":"matryoshka_sae","sae_training_method":"matryoshka_topk","site_type":"residual_stream","base_model_class":"pretrained_base","author_class":"individual_researcher","publication_state":"blog_post","claim_type":"method_innovation","intervention_kind":"none","embedded_decomposition":false,"cross_layer":false,"nested_widths":[16000,32000],"inference_endpoint_available":true,"commercial_license_required":false,"api_provider_class":"hosted_api_features","deanonymization_pending":false,"neuronpedia_source_id":"res-matryoshka-dc-32k"}, + {"slug":"llama3.3-70b-it-gf","base_model":"Llama-3.3-70B-Instruct","decomposition_type":"standard_sae","sae_training_method":"other","site_type":"residual_post","base_model_class":"instruction_tuned","author_class":"commercial_interp","publication_state":"commercial_release","claim_type":"feature_set","intervention_kind":"feature_steering","embedded_decomposition":false,"cross_layer":false,"inference_endpoint_available":true,"commercial_license_required":true,"api_provider_class":"commercial_steering_api","deanonymization_pending":false,"neuronpedia_source_id":"50-resid-post-gf"}, + {"slug":"llama-scope-r1","base_model":"DeepSeek-R1-Distill-Llama-8B","decomposition_type":"standard_sae","sae_training_method":"topk","site_type":"residual_stream","base_model_class":"reasoning_distilled","author_class":"research_lab","publication_state":"preprint","claim_type":"feature_set","intervention_kind":"none","embedded_decomposition":false,"cross_layer":false,"inference_endpoint_available":true,"commercial_license_required":false,"api_provider_class":"hosted_api_features","deanonymization_pending":false,"neuronpedia_source_id":"llama-r1-distill-8b-resid-openmoss"}, + {"slug":"axbench","base_model":"Gemma-2-9B-IT","decomposition_type":"standard_sae","sae_training_method":"topk","site_type":"residual_stream","base_model_class":"instruction_tuned","author_class":"research_lab","publication_state":"preprint","claim_type":"benchmark_critique","intervention_kind":"reft_finetuning","embedded_decomposition":false,"cross_layer":false,"inference_endpoint_available":true,"commercial_license_required":false,"api_provider_class":"open_weights","deanonymization_pending":false,"neuronpedia_source_id":"20-resid-reft-r1-16k-axbench"}, + {"slug":"llama-scope","base_model":"Llama-3.1-8B","decomposition_type":"standard_sae","sae_training_method":"topk","site_type":"residual_stream","base_model_class":"pretrained_base","author_class":"research_lab","publication_state":"preprint","claim_type":"feature_set","intervention_kind":"none","embedded_decomposition":false,"cross_layer":false,"inference_endpoint_available":true,"commercial_license_required":false,"api_provider_class":"hosted_api_features","deanonymization_pending":false,"neuronpedia_source_id":"15-resid-openmoss-32k"}, + {"slug":"gpt2sm-rfs-jb","base_model":"GPT-2 Small","decomposition_type":"feature_splitting_study","sae_training_method":"topk","site_type":"residual_stream","base_model_class":"pretrained_base","author_class":"individual_researcher","publication_state":"preprint","claim_type":"feature_phenomenology","intervention_kind":"none","embedded_decomposition":false,"cross_layer":false,"inference_endpoint_available":true,"commercial_license_required":false,"api_provider_class":"hosted_api_features","deanonymization_pending":false,"neuronpedia_source_id":"8-resid-rfs-jb-12288"}, + {"slug":"llama3.1-8b-eleuther-gp","base_model":"Llama-3.1-8B","decomposition_type":"multi_topk_sae","sae_training_method":"multi_topk","site_type":"mlp","base_model_class":"pretrained_base","author_class":"research_lab","publication_state":"blog_post","claim_type":"feature_set","intervention_kind":"none","embedded_decomposition":false,"cross_layer":false,"inference_endpoint_available":false,"commercial_license_required":false,"api_provider_class":"open_weights","deanonymization_pending":false,"neuronpedia_source_id":"23-mlp-eleuther-gp-262k"}, + {"slug":"gpt2sm-oai-2024","base_model":"GPT-2 Small","decomposition_type":"standard_sae","sae_training_method":"other","site_type":"attention_out","base_model_class":"pretrained_base","author_class":"model_author","publication_state":"preprint","claim_type":"feature_set","intervention_kind":"none","embedded_decomposition":false,"cross_layer":false,"inference_endpoint_available":true,"commercial_license_required":false,"api_provider_class":"open_weights","deanonymization_pending":false,"neuronpedia_source_id":"0-att_32k-oai"}, + {"slug":"gpt2sm-apollojt","base_model":"GPT-2 Small","decomposition_type":"standard_sae","sae_training_method":"jumprelu","site_type":"residual_stream","base_model_class":"pretrained_base","author_class":"safety_research","publication_state":"preprint","claim_type":"safety_finding","intervention_kind":"none","embedded_decomposition":false,"cross_layer":false,"inference_endpoint_available":true,"commercial_license_required":false,"api_provider_class":"hosted_api_features","deanonymization_pending":false,"neuronpedia_source_id":"gpt2sm-apollojt"}, + {"slug":"gpt2sm-tres-dc","base_model":"GPT-2 Small","decomposition_type":"standard_sae","sae_training_method":"topk","site_type":"residual_stream","base_model_class":"pretrained_base","author_class":"individual_researcher","publication_state":"blog_post","claim_type":"feature_set","intervention_kind":"none","embedded_decomposition":false,"cross_layer":false,"inference_endpoint_available":true,"commercial_license_required":false,"api_provider_class":"hosted_api_features","deanonymization_pending":false,"neuronpedia_source_id":"gpt2sm-tres-dc"}, + {"slug":"p70d-sm","base_model":"Pythia-70M-Deduped","decomposition_type":"standard_sae","sae_training_method":"topk","site_type":"attention_out","base_model_class":"pretrained_base","author_class":"anonymous_review","publication_state":"anonymous_review","claim_type":"feature_set","intervention_kind":"none","embedded_decomposition":false,"cross_layer":false,"inference_endpoint_available":false,"commercial_license_required":false,"api_provider_class":"open_weights","deanonymization_pending":true,"neuronpedia_source_id":"p70d-sm"}, + {"slug":"gpt2sm-kk","base_model":"GPT-2 Small","decomposition_type":"standard_sae","sae_training_method":"other","site_type":"residual_stream","base_model_class":"pretrained_base","author_class":"individual_researcher","publication_state":"blog_post","claim_type":"feature_set","intervention_kind":"none","embedded_decomposition":false,"cross_layer":false,"inference_endpoint_available":true,"commercial_license_required":false,"api_provider_class":"hosted_api_features","deanonymization_pending":false,"neuronpedia_source_id":"gpt2sm-kk"}, + {"slug":"gpt2sm-res-jb","base_model":"GPT-2 Small","decomposition_type":"standard_sae","sae_training_method":"relu","site_type":"residual_stream","base_model_class":"pretrained_base","author_class":"individual_researcher","publication_state":"preprint","claim_type":"feature_set","intervention_kind":"none","embedded_decomposition":false,"cross_layer":false,"inference_endpoint_available":true,"commercial_license_required":false,"api_provider_class":"hosted_api_features","deanonymization_pending":false,"neuronpedia_source_id":"gpt2sm-res-jb"} + ] +} diff --git a/schemas/interpretability/artifact-source-lock.v0.json b/schemas/interpretability/artifact-source-lock.v0.json new file mode 100644 index 0000000..4c31b06 --- /dev/null +++ b/schemas/interpretability/artifact-source-lock.v0.json @@ -0,0 +1,103 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://sourceos.dev/schemas/interpretability/artifact-source-lock.v0.json", + "title": "Interpretability Artifact Source Lock v0.1", + "description": "Source lock for SAE, transcoder, sparse-native, and related interpretability artifacts.", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "lock_kind", + "lock_id", + "model_ref", + "artifact_ref", + "decomposition_type", + "site_type", + "base_model_class", + "embedded_decomposition", + "cross_layer", + "neuronpedia_source_id", + "manifest_digest_sha256" + ], + "properties": { + "schema_version": {"const": "0.1.0"}, + "lock_kind": {"const": "interpretability-artifact-source-lock"}, + "lock_id": {"type": "string", "pattern": "^urn:srcos:interpretability:source-lock:[A-Za-z0-9._:-]+$"}, + "model_ref": {"type": "string", "minLength": 1}, + "artifact_ref": {"type": "string", "minLength": 1}, + "decomposition_type": { + "type": "string", + "enum": [ + "standard_sae", + "transcoder_clt", + "weight_sparse_native", + "lorsa_attention", + "matryoshka_sae", + "temporal_sae", + "multi_topk_sae", + "batch_topk_sae", + "feature_splitting_study", + "compound_decomposition" + ] + }, + "sae_training_method": { + "type": "string", + "enum": ["relu", "topk", "batch_topk", "multi_topk", "gated", "jumprelu", "matryoshka_topk", "lorsa", "weight_sparse_native", "transcoder", "other"] + }, + "site_type": { + "type": "string", + "enum": ["residual_stream", "residual_mid", "residual_post", "attention_out", "attention_qk_ov", "mlp", "mlp_post", "embedding"] + }, + "base_model_class": { + "type": "string", + "enum": ["pretrained_base", "instruction_tuned", "rlhf_aligned", "reasoning_distilled", "sparse_trained_native"] + }, + "embedded_decomposition": {"type": "boolean"}, + "cross_layer": {"type": "boolean"}, + "nested_widths": { + "type": "array", + "items": {"type": "integer", "minimum": 1}, + "uniqueItems": true + }, + "temporal_window": {"type": "integer", "minimum": 1}, + "compound_decompositions": { + "type": "array", + "items": {"type": "string", "minLength": 1}, + "uniqueItems": true + }, + "decoder_ref": {"type": "string"}, + "neuronpedia_source_id": {"type": "string", "minLength": 1}, + "layer": {"type": "integer", "minimum": 0}, + "width": {"type": "integer", "minimum": 1}, + "manifest_digest_sha256": {"type": "string", "pattern": "^[a-f0-9]{64}$"}, + "notes": {"type": "string"} + }, + "allOf": [ + { + "if": {"properties": {"decomposition_type": {"const": "matryoshka_sae"}}, "required": ["decomposition_type"]}, + "then": {"required": ["nested_widths"], "properties": {"nested_widths": {"minItems": 1}}} + }, + { + "if": {"properties": {"decomposition_type": {"const": "temporal_sae"}}, "required": ["decomposition_type"]}, + "then": {"required": ["temporal_window"]} + }, + { + "if": {"properties": {"decomposition_type": {"const": "lorsa_attention"}}, "required": ["decomposition_type"]}, + "then": {"properties": {"site_type": {"enum": ["attention_out", "attention_qk_ov"]}}} + }, + { + "if": {"properties": {"decomposition_type": {"const": "compound_decomposition"}}, "required": ["decomposition_type"]}, + "then": {"required": ["compound_decompositions"], "properties": {"compound_decompositions": {"minItems": 2}}} + }, + { + "if": {"properties": {"decomposition_type": {"const": "weight_sparse_native"}}, "required": ["decomposition_type"]}, + "then": { + "properties": { + "embedded_decomposition": {"const": true}, + "sae_training_method": {"const": "weight_sparse_native"} + }, + "not": {"required": ["decoder_ref"]} + } + } + ] +} diff --git a/schemas/interpretability/intervention-spec.v0.json b/schemas/interpretability/intervention-spec.v0.json new file mode 100644 index 0000000..3a61cae --- /dev/null +++ b/schemas/interpretability/intervention-spec.v0.json @@ -0,0 +1,86 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://sourceos.dev/schemas/interpretability/intervention-spec.v0.json", + "title": "Interpretability Intervention Spec v0.1", + "description": "Typed intervention contract for feature steering, activation capping, ReFT, circuit ablations, and LORSA QK interventions.", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "intervention_kind", + "intervention_id", + "source_lock_ref", + "site_type" + ], + "properties": { + "schema_version": {"const": "0.1.0"}, + "intervention_id": {"type": "string", "pattern": "^urn:srcos:interpretability:intervention:[A-Za-z0-9._:-]+$"}, + "source_lock_ref": {"type": "string", "pattern": "^urn:srcos:interpretability:source-lock:[A-Za-z0-9._:-]+$"}, + "intervention_kind": { + "type": "string", + "enum": [ + "feature_steering", + "activation_patching", + "activation_cap", + "reft_finetuning", + "prompt_only_blackbox", + "circuit_ablation", + "lorsa_qk_intervention", + "none" + ] + }, + "site_type": { + "type": "string", + "enum": ["residual_stream", "residual_mid", "residual_post", "attention_out", "attention_qk_ov", "mlp", "mlp_post", "embedding", "not_applicable"] + }, + "feature_ref": {"type": "string"}, + "direction_ref": {"type": "string"}, + "cap_value": {"type": "number"}, + "cap_direction_ref": {"type": "string"}, + "cap_layer": {"type": "integer", "minimum": 0}, + "cap_site_type": { + "type": "string", + "enum": ["residual_stream", "residual_mid", "residual_post", "attention_out", "attention_qk_ov", "mlp", "mlp_post", "embedding"] + }, + "reft_method_class": {"type": "string"}, + "finetuning_data_ref": {"type": "string"}, + "training_steps": {"type": "integer", "minimum": 1}, + "query_position": {"type": "string"}, + "key_position": {"type": "string"}, + "lorsa_feature_ref": {"type": "string"}, + "ablation_target_ref": {"type": "string"}, + "steering_strength": {"type": "number"}, + "expected_behavior_delta": {"type": "string"}, + "safety_scope": {"type": "string"}, + "notes": {"type": "string"} + }, + "allOf": [ + { + "if": {"properties": {"intervention_kind": {"const": "activation_cap"}}, "required": ["intervention_kind"]}, + "then": {"required": ["cap_value", "cap_direction_ref", "cap_layer", "cap_site_type"]} + }, + { + "if": {"properties": {"intervention_kind": {"const": "reft_finetuning"}}, "required": ["intervention_kind"]}, + "then": {"required": ["reft_method_class", "finetuning_data_ref", "training_steps"]} + }, + { + "if": {"properties": {"intervention_kind": {"const": "lorsa_qk_intervention"}}, "required": ["intervention_kind"]}, + "then": { + "required": ["query_position", "key_position", "lorsa_feature_ref"], + "properties": {"site_type": {"const": "attention_qk_ov"}} + } + }, + { + "if": {"properties": {"intervention_kind": {"const": "feature_steering"}}, "required": ["intervention_kind"]}, + "then": {"required": ["feature_ref", "direction_ref", "steering_strength"]} + }, + { + "if": {"properties": {"intervention_kind": {"const": "circuit_ablation"}}, "required": ["intervention_kind"]}, + "then": {"required": ["ablation_target_ref"]} + }, + { + "if": {"properties": {"intervention_kind": {"const": "none"}}, "required": ["intervention_kind"]}, + "then": {"properties": {"site_type": {"const": "not_applicable"}}} + } + ] +} diff --git a/schemas/interpretability/provider-binding.v0.json b/schemas/interpretability/provider-binding.v0.json new file mode 100644 index 0000000..703ded6 --- /dev/null +++ b/schemas/interpretability/provider-binding.v0.json @@ -0,0 +1,68 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://sourceos.dev/schemas/interpretability/provider-binding.v0.json", + "title": "Interpretability Provider Binding v0.1", + "description": "Authority, publication, access, and provider binding metadata for interpretability artifact releases.", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "binding_kind", + "binding_id", + "source_lock_ref", + "author_class", + "publication_state", + "inference_endpoint_available", + "commercial_license_required", + "api_provider_class", + "deanonymization_pending" + ], + "properties": { + "schema_version": {"const": "0.1.0"}, + "binding_kind": {"const": "interpretability-provider-binding"}, + "binding_id": {"type": "string", "pattern": "^urn:srcos:interpretability:provider-binding:[A-Za-z0-9._:-]+$"}, + "source_lock_ref": {"type": "string", "pattern": "^urn:srcos:interpretability:source-lock:[A-Za-z0-9._:-]+$"}, + "provider_name": {"type": "string"}, + "release_name": {"type": "string"}, + "author_class": { + "type": "string", + "enum": ["model_author", "research_lab", "safety_research", "commercial_interp", "individual_researcher", "anonymous_review"] + }, + "publication_state": { + "type": "string", + "enum": ["peer_reviewed", "preprint", "anonymous_review", "lesswrong", "blog_post", "commercial_release", "internal"] + }, + "claim_type": { + "type": "string", + "enum": ["feature_set", "safety_finding", "method_innovation", "benchmark_critique", "circuit_construction", "feature_phenomenology"] + }, + "inference_endpoint_available": {"type": "boolean"}, + "commercial_license_required": {"type": "boolean"}, + "api_provider_class": { + "type": "string", + "enum": ["open_weights", "hosted_api_inference", "hosted_api_features", "commercial_steering_api", "none"] + }, + "deanonymization_pending": {"type": "boolean"}, + "artifact_access_mode": { + "type": "string", + "enum": ["download_weights", "hosted_query", "hosted_feature_browser", "paper_only", "commercial_api", "unknown"] + }, + "authority_chain_ref": {"type": "string"}, + "license_ref": {"type": "string"}, + "notes": {"type": "string"} + }, + "allOf": [ + { + "if": {"properties": {"commercial_license_required": {"const": true}}, "required": ["commercial_license_required"]}, + "then": {"properties": {"api_provider_class": {"enum": ["commercial_steering_api", "hosted_api_features", "hosted_api_inference"]}}} + }, + { + "if": {"properties": {"publication_state": {"const": "anonymous_review"}}, "required": ["publication_state"]}, + "then": {"properties": {"deanonymization_pending": {"const": true}, "author_class": {"const": "anonymous_review"}}} + }, + { + "if": {"properties": {"author_class": {"const": "commercial_interp"}}, "required": ["author_class"]}, + "then": {"properties": {"publication_state": {"enum": ["commercial_release", "blog_post", "preprint", "internal"]}}} + } + ] +} diff --git a/tools/validate_interpretability_examples.py b/tools/validate_interpretability_examples.py new file mode 100644 index 0000000..43a7082 --- /dev/null +++ b/tools/validate_interpretability_examples.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +"""Validate interpretability/SAE contract coverage fixtures. + +The catalog fixture is intentionally compact. This validator expands each entry +into the three canonical contracts introduced in v0.1: + +- ArtifactSourceLock +- ProviderBinding +- InterventionSpec + +It then runs JSON Schema validation and semantic checks for the dimensions that +make the harness model-family agnostic without collapsing artifact kinds. +""" + +from __future__ import annotations + +import hashlib +import json +from pathlib import Path +from typing import Any + +import jsonschema + +ROOT = Path(__file__).resolve().parents[1] + +CATALOG = ROOT / "examples/interpretability/sae-catalog.v0.json" +SOURCE_SCHEMA = ROOT / "schemas/interpretability/artifact-source-lock.v0.json" +PROVIDER_SCHEMA = ROOT / "schemas/interpretability/provider-binding.v0.json" +INTERVENTION_SCHEMA = ROOT / "schemas/interpretability/intervention-spec.v0.json" + +REQUIRED_DECOMPOSITION_TYPES = { + "standard_sae", + "transcoder_clt", + "weight_sparse_native", + "lorsa_attention", + "matryoshka_sae", + "temporal_sae", + "multi_topk_sae", + "batch_topk_sae", + "feature_splitting_study", + "compound_decomposition", +} + +REQUIRED_AUTHOR_CLASSES = { + "model_author", + "research_lab", + "safety_research", + "commercial_interp", + "individual_researcher", + "anonymous_review", +} + +REQUIRED_INTERVENTIONS = { + "feature_steering", + "activation_cap", + "reft_finetuning", + "none", +} + + +def load_json(path: Path) -> dict[str, Any]: + with path.open("r", encoding="utf-8") as fh: + return json.load(fh) + + +def digest(text: str) -> str: + return hashlib.sha256(text.encode("utf-8")).hexdigest() + + +def urn(kind: str, slug: str) -> str: + return f"urn:srcos:interpretability:{kind}:{slug}" + + +def expand_source_lock(entry: dict[str, Any]) -> dict[str, Any]: + slug = entry["slug"] + source: dict[str, Any] = { + "schema_version": "0.1.0", + "lock_kind": "interpretability-artifact-source-lock", + "lock_id": urn("source-lock", slug), + "model_ref": entry["base_model"], + "artifact_ref": f"artifact:{slug}", + "decomposition_type": entry["decomposition_type"], + "sae_training_method": entry["sae_training_method"], + "site_type": entry["site_type"], + "base_model_class": entry["base_model_class"], + "embedded_decomposition": entry["embedded_decomposition"], + "cross_layer": entry["cross_layer"], + "neuronpedia_source_id": entry["neuronpedia_source_id"], + "manifest_digest_sha256": digest(slug + entry["neuronpedia_source_id"]), + } + for key in ["nested_widths", "temporal_window", "compound_decompositions"]: + if key in entry: + source[key] = entry[key] + if source["decomposition_type"] != "weight_sparse_native": + source["decoder_ref"] = urn("decoder", slug) + return source + + +def expand_provider_binding(entry: dict[str, Any]) -> dict[str, Any]: + slug = entry["slug"] + return { + "schema_version": "0.1.0", + "binding_kind": "interpretability-provider-binding", + "binding_id": urn("provider-binding", slug), + "source_lock_ref": urn("source-lock", slug), + "provider_name": entry["slug"].split("-")[0], + "release_name": entry["slug"], + "author_class": entry["author_class"], + "publication_state": entry["publication_state"], + "claim_type": entry["claim_type"], + "inference_endpoint_available": entry["inference_endpoint_available"], + "commercial_license_required": entry["commercial_license_required"], + "api_provider_class": entry["api_provider_class"], + "deanonymization_pending": entry["deanonymization_pending"], + "artifact_access_mode": "commercial_api" if entry["commercial_license_required"] else "hosted_feature_browser", + "authority_chain_ref": f"urn:srcos:authority:interpretability:{slug}", + } + + +def expand_intervention_spec(entry: dict[str, Any]) -> dict[str, Any]: + slug = entry["slug"] + kind = entry["intervention_kind"] + spec: dict[str, Any] = { + "schema_version": "0.1.0", + "intervention_id": urn("intervention", slug), + "source_lock_ref": urn("source-lock", slug), + "intervention_kind": kind, + "site_type": "not_applicable" if kind == "none" else entry["site_type"], + } + if kind == "feature_steering": + spec.update( + { + "feature_ref": f"feature:{slug}:primary", + "direction_ref": f"direction:{slug}:primary", + "steering_strength": 1.0, + "expected_behavior_delta": "target behavior changes under feature-direction intervention", + } + ) + elif kind == "activation_cap": + spec.update( + { + "cap_value": 0.0, + "cap_direction_ref": f"direction:{slug}:activation-axis", + "cap_layer": 0, + "cap_site_type": entry["site_type"], + "expected_behavior_delta": "activation magnitude is bounded along the declared direction", + } + ) + elif kind == "reft_finetuning": + spec.update( + { + "reft_method_class": "ReFT-R1", + "finetuning_data_ref": "urn:srcos:dataset:axbench-reft-r1", + "training_steps": 100, + "expected_behavior_delta": "benchmark comparison against ReFT baseline is available", + } + ) + return spec + + +def validate_schema(instance: dict[str, Any], schema: dict[str, Any], label: str) -> None: + jsonschema.Draft202012Validator.check_schema(schema) + validator = jsonschema.Draft202012Validator(schema) + errors = sorted(validator.iter_errors(instance), key=lambda err: list(err.path)) + if errors: + messages = [] + for err in errors[:10]: + loc = ".".join(str(part) for part in err.path) or "" + messages.append(f"{label} {loc}: {err.message}") + raise AssertionError("\n".join(messages)) + + +def semantic_checks(entries: list[dict[str, Any]]) -> None: + if len(entries) != 21: + raise AssertionError(f"Expected 21 catalog entries, got {len(entries)}") + + slugs = [entry["slug"] for entry in entries] + if len(slugs) != len(set(slugs)): + raise AssertionError("Catalog slugs must be unique") + + decomposition_types = {entry["decomposition_type"] for entry in entries} + missing_decomp = REQUIRED_DECOMPOSITION_TYPES - decomposition_types + # LORSA appears as a required component of the compound CRM entry rather than + # as a standalone row in this compact fixture. + has_lorsa_component = any("lorsa_attention" in entry.get("compound_decompositions", []) for entry in entries) + if missing_decomp != {"lorsa_attention"} or not has_lorsa_component: + if missing_decomp: + raise AssertionError(f"Missing decomposition coverage: {sorted(missing_decomp)}") + + author_classes = {entry["author_class"] for entry in entries} + missing_authors = REQUIRED_AUTHOR_CLASSES - author_classes + if missing_authors: + raise AssertionError(f"Missing author class coverage: {sorted(missing_authors)}") + + intervention_kinds = {entry["intervention_kind"] for entry in entries} + missing_interventions = REQUIRED_INTERVENTIONS - intervention_kinds + if missing_interventions: + raise AssertionError(f"Missing intervention coverage: {sorted(missing_interventions)}") + + for entry in entries: + if entry["decomposition_type"] == "weight_sparse_native" and not entry["embedded_decomposition"]: + raise AssertionError("weight_sparse_native requires embedded_decomposition=true") + if entry["decomposition_type"] == "matryoshka_sae" and not entry.get("nested_widths"): + raise AssertionError("matryoshka_sae requires nested_widths") + if entry["decomposition_type"] == "temporal_sae" and not entry.get("temporal_window"): + raise AssertionError("temporal_sae requires temporal_window") + if entry["publication_state"] == "anonymous_review" and not entry["deanonymization_pending"]: + raise AssertionError("anonymous_review publication requires deanonymization_pending=true") + if entry["commercial_license_required"] and entry["api_provider_class"] not in {"commercial_steering_api", "hosted_api_features", "hosted_api_inference"}: + raise AssertionError("commercial license requires commercial or hosted provider class") + if entry["intervention_kind"] == "activation_cap" and entry["site_type"] == "not_applicable": + raise AssertionError("activation_cap requires an applicable site") + + +def main() -> None: + catalog = load_json(CATALOG) + source_schema = load_json(SOURCE_SCHEMA) + provider_schema = load_json(PROVIDER_SCHEMA) + intervention_schema = load_json(INTERVENTION_SCHEMA) + + entries = catalog.get("entries", []) + semantic_checks(entries) + + for entry in entries: + source = expand_source_lock(entry) + provider = expand_provider_binding(entry) + intervention = expand_intervention_spec(entry) + + validate_schema(source, source_schema, f"source_lock:{entry['slug']}") + validate_schema(provider, provider_schema, f"provider_binding:{entry['slug']}") + validate_schema(intervention, intervention_schema, f"intervention_spec:{entry['slug']}") + + print("OK: interpretability SAE contract examples") + + +if __name__ == "__main__": + main()